feat(queue): add typed application queue lifecycle
This commit is contained in:
@@ -813,6 +813,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
watcher?.close();
|
||||
unsubscribeDevToolbar?.();
|
||||
server.stop();
|
||||
void handlers.shutdown();
|
||||
void pluginRunner.hook("shutdown");
|
||||
rmSync(cacheDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
||||
},
|
||||
|
||||
@@ -79,6 +79,8 @@ export interface ProdManifest {
|
||||
layouts: { name: string; mod: RouteModule }[];
|
||||
/** RPC service implementations (from app/services/*.ts). */
|
||||
services?: { name: string; mod: RouteModule }[];
|
||||
/** Background queue modules (from app/queues/*.ts). */
|
||||
queues?: { name: string; mod: RouteModule }[];
|
||||
}
|
||||
|
||||
export interface ProductionPluginAsset {
|
||||
@@ -292,6 +294,7 @@ function buildProdRouter(manifest: ProdManifest): {
|
||||
for (const l of manifest.layouts) modules.set(`layout:${l.name}`, l.mod);
|
||||
for (const service of manifest.services ?? [])
|
||||
modules.set(`service:${service.name}`, service.mod);
|
||||
for (const queue of manifest.queues ?? []) modules.set(`queue:${queue.name}`, queue.mod);
|
||||
|
||||
const router: Router = {
|
||||
pages,
|
||||
@@ -307,6 +310,10 @@ function buildProdRouter(manifest: ProdManifest): {
|
||||
name: service.name,
|
||||
file: `service:${service.name}`,
|
||||
})),
|
||||
queues: (manifest.queues ?? []).map((queue) => ({
|
||||
name: queue.name,
|
||||
file: `queue:${queue.name}`,
|
||||
})),
|
||||
matchPage: optimizedMatcher(pages),
|
||||
matchApi: optimizedMatcher(api),
|
||||
matchRealtime: optimizedMatcher(realtime),
|
||||
@@ -652,11 +659,12 @@ export async function createProductionServer(manifest: ProdManifest, opts: ProdO
|
||||
|
||||
// Graceful shutdown: stop accepting connections, then exit.
|
||||
let shuttingDown = false;
|
||||
const shutdown = () => {
|
||||
const shutdown = async () => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
console.log("WrNexus: shutting down…");
|
||||
server.stop();
|
||||
await handlers.shutdown();
|
||||
process.exit(0);
|
||||
};
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
@@ -909,6 +909,7 @@ export interface Ws {
|
||||
|
||||
export interface Handlers {
|
||||
fetch(req: Request, server: UpgradeServer): Promise<Response | undefined>;
|
||||
shutdown(): Promise<void>;
|
||||
websocket: {
|
||||
open(ws: Ws): void;
|
||||
message(ws: Ws, message: string | Uint8Array): void;
|
||||
@@ -985,6 +986,27 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
}
|
||||
return servicesPromise;
|
||||
};
|
||||
type QueueModule = { start(): void; shutdown(options?: { force?: boolean }): Promise<void> };
|
||||
let queuesPromise: Promise<QueueModule[]> | undefined;
|
||||
const loadQueues = (): Promise<QueueModule[]> => {
|
||||
queuesPromise ??= (async () => {
|
||||
const queues: QueueModule[] = [];
|
||||
for (const entry of router.queues ?? []) {
|
||||
const imported = await loadModule(entry.file);
|
||||
const queue = imported.default as QueueModule | undefined;
|
||||
if (!queue || typeof queue.start !== "function" || typeof queue.shutdown !== "function") {
|
||||
throw new Error(`Queue ${entry.file} must default-export defineQueue(...)`);
|
||||
}
|
||||
queue.start();
|
||||
queues.push(queue);
|
||||
}
|
||||
return queues;
|
||||
})().catch((error) => {
|
||||
queuesPromise = undefined;
|
||||
throw error;
|
||||
});
|
||||
return queuesPromise;
|
||||
};
|
||||
|
||||
// Server-side realtime room manager (shared by every `defineRoom` connection).
|
||||
const realtime = createRealtimeRegistry();
|
||||
@@ -1041,6 +1063,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
};
|
||||
|
||||
async function fetchHandler(req: Request, server: UpgradeServer): Promise<Response | undefined> {
|
||||
await loadQueues();
|
||||
// Behind a trusted proxy (nginx / gateway), honor X-Forwarded-Proto/Host so
|
||||
// ctx.url reflects the external HTTPS scheme — makes CSRF/session cookies Secure.
|
||||
const url = resolveRequestUrl(req, deps.security?.trustProxy);
|
||||
@@ -2321,6 +2344,10 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
|
||||
return {
|
||||
fetch: fetchHandler,
|
||||
async shutdown() {
|
||||
const queues = await queuesPromise?.catch(() => []);
|
||||
await Promise.allSettled((queues ?? []).map((queue) => queue.shutdown()));
|
||||
},
|
||||
websocket: {
|
||||
open(ws) {
|
||||
if (ws.data?.kind === "hmr") hub?.add(ws);
|
||||
|
||||
Reference in New Issue
Block a user