feat(queue): add typed application queue lifecycle
Quality / quality (ubuntu-latest) (push) Failing after 10m55s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-23 09:22:05 +05:30
parent 9332522614
commit 9377a69d88
17 changed files with 667 additions and 13 deletions
+1
View File
@@ -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 });
},
+9 -1
View File
@@ -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);
+27
View File
@@ -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);