Files
nodewarden/src/index.ts
T

130 lines
4.1 KiB
TypeScript

import { Env } from './types';
import { NotificationsHub } from './durable/notifications-hub';
import { handleRequest } from './router';
import { StorageService } from './services/storage';
import { applyCors, jsonResponse } from './utils/response';
import { runScheduledBackupIfDue } from './handlers/backup';
let dbInitialized = false;
let dbInitError: string | null = null;
let dbInitPromise: Promise<void> | null = null;
function normalizeRequestUrl(request: Request): Request {
const url = new URL(request.url);
const normalizedPathname = url.pathname.length <= 1 ? url.pathname : url.pathname.replace(/\/+$/, '');
if (normalizedPathname === url.pathname) return request;
url.pathname = normalizedPathname;
return new Request(url.toString(), request);
}
function isWorkerHandledPath(path: string): boolean {
return (
path.startsWith('/api/') ||
path.startsWith('/identity/') ||
path.startsWith('/icons/') ||
path.startsWith('/notifications/') ||
path.startsWith('/.well-known/') ||
path === '/config' ||
path === '/api/config' ||
path === '/api/version'
);
}
function addSearchIndexHeaders(request: Request, response: Response): Response {
const url = new URL(request.url);
const contentType = String(response.headers.get('Content-Type') || '').toLowerCase();
const shouldNoIndex =
url.pathname === '/robots.txt' ||
contentType.includes('text/html');
if (!shouldNoIndex) return response;
const headers = new Headers(response.headers);
headers.set('X-Robots-Tag', 'noindex, nofollow, noarchive, nosnippet');
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
async function maybeServeAsset(request: Request, env: Env): Promise<Response | null> {
if (!env.ASSETS) return null;
if (request.method !== 'GET' && request.method !== 'HEAD') return null;
const url = new URL(request.url);
if (isWorkerHandledPath(url.pathname)) return null;
const response = await env.ASSETS.fetch(request);
return addSearchIndexHeaders(request, response);
}
async function ensureDatabaseInitialized(env: Env): Promise<void> {
if (dbInitialized) return;
if (!dbInitPromise) {
dbInitPromise = (async () => {
const storage = new StorageService(env.DB);
await storage.initializeDatabase();
dbInitialized = true;
dbInitError = null;
})()
.catch((error: unknown) => {
console.error('Failed to initialize database:', error);
dbInitError = error instanceof Error ? error.message : 'Unknown database initialization error';
})
.finally(() => {
dbInitPromise = null;
});
}
await dbInitPromise;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
void ctx;
const normalizedRequest = normalizeRequestUrl(request);
const assetResponse = await maybeServeAsset(normalizedRequest, env);
if (assetResponse) {
return applyCors(normalizedRequest, assetResponse);
}
await ensureDatabaseInitialized(env);
if (dbInitError) {
// Log full error server-side, return generic message to client.
console.error('DB init error (not forwarded to client):', dbInitError);
const resp = jsonResponse(
{
error: 'Database not initialized',
error_description: 'Database initialization failed. Check server logs for details.',
ErrorModel: {
Message: 'Service temporarily unavailable',
Object: 'error',
},
},
500
);
return applyCors(normalizedRequest, resp);
}
const resp = await handleRequest(normalizedRequest, env);
return applyCors(normalizedRequest, resp);
},
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
void controller;
await ensureDatabaseInitialized(env);
if (dbInitError) {
console.error('Skipping scheduled backup because DB init failed:', dbInitError);
return;
}
ctx.waitUntil(runScheduledBackupIfDue(env).catch((error) => {
console.error('Scheduled backup failed:', error);
}));
},
};
export { NotificationsHub };