@luckystack/server 0.8.6 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.7] - 2026-08-18
11
+
12
+ ### Added
13
+
14
+ - `RunningLuckyStackServer.afterListen(task, options?)` — runs post-listen work (durable queue workers, warm-up passes, reconciliation jobs) isolated from the caller's fatal boot chain. A rejection is logged loudly and swallowed by default, because the server is already accepting traffic: killing a healthy process over one unreachable dependency turns an outage into a supervisor crash-loop, while the log claims "failed to start" about a server that started fine. Pass `{ fatal: true }` when the process genuinely has no purpose without the task, and `{ label }` to name it in the failure log.
15
+
10
16
  ## [0.8.5] - 2026-08-17
11
17
 
12
18
  ### Added
package/CLAUDE.md CHANGED
@@ -25,7 +25,8 @@ One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js
25
25
 
26
26
  | Function / Export | One-liner | Deep doc |
27
27
  |---|---|---|
28
- | `createLuckyStackServer(options)` | Lower-level factory that returns `{ httpServer, ioServer, listen, stop, close }`. Wires HTTP + Socket.io + framework routes; after a successful listen starts the boot-UUID TTL heartbeat, and stops it during shutdown. Installs a persistent `httpServer.on('error')` listener and (in prod) SIGTERM/SIGINT → graceful `stop()`. | -> docs/create-server.md |
28
+ | `createLuckyStackServer(options)` | Lower-level factory that returns `{ httpServer, ioServer, listen, afterListen, stop, close }`. Wires HTTP + Socket.io + framework routes; after a successful listen starts the boot-UUID TTL heartbeat, and stops it during shutdown. Installs a persistent `httpServer.on('error')` listener and (in prod) SIGTERM/SIGINT → graceful `stop()`. | -> docs/create-server.md |
29
+ | `RunningLuckyStackServer.afterListen(task, options?)` | Runs post-listen work (queue workers, warm-up, reconciliation) isolated from the caller's fatal boot chain. A rejection is logged loudly and swallowed by default — the server is already serving, and killing it over one broken dependency turns an outage into a supervisor crash-loop. `{ fatal: true }` re-throws for a task the process cannot run without; `{ label }` names it in the log. | -> docs/create-server.md |
29
30
  | `RunningLuckyStackServer.stop(options?)` / `.close(options?)` | Graceful shutdown (MIS-016). Stops accepting new connections, dispatches the core `preServerStop` hook, flushes error-trackers (bounded by a per-step timeout race), then closes io + http + the Redis-adapter pub/sub clients. Each step isolated + time-bounded so one hanging step can't stall shutdown. Idempotent. `options`: `{ reason?, timeoutMs? }` (default `timeoutMs` 10000). | -> docs/create-server.md |
30
31
  | `bootstrapLuckyStack(options)` | High-level entry: auto-imports `luckystack/<pkg>/*.ts` overlay in topological order, then delegates to `createLuckyStackServer`. An overlay file that fails to import (classic: it imports a package removed via `luckystack remove`) aborts boot with an ACTIONABLE error naming the file, instead of a raw ERR_MODULE_NOT_FOUND. | -> docs/create-server.md |
31
32
  | `OVERLAY_ORDER` (exported const) | The canonical overlay walk order (`core, deploy, login, email, sentry, presence, cron, docs-ui, server`). Exported so the consumer's `scripts/bundleServer.mjs` imports it at build time — a hardcoded bundler copy once silently dropped the cron slot from prod; `overlayOrderParity.test.ts` pins the bundlers' fallback lists to this. | -> docs/create-server.md |
@@ -60,7 +61,7 @@ One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js
60
61
  | Hook payload: `PreRoomLeavePayload` | Payload type for `preRoomLeave` hook handlers. | -> docs/create-server.md |
61
62
  | Hook payload: `PostRoomLeavePayload` | Payload type for `postRoomLeave` hook handlers. | -> docs/create-server.md |
62
63
  | Hook payload: `OnLocationUpdatePayload` | Payload type for `onLocationUpdate` hook handlers. | -> docs/create-server.md |
63
- | Types: `CreateLuckyStackServerOptions`, `BootstrapLuckyStackOptions`, `BootstrapRequirements`, `RunningLuckyStackServer`, `StopLuckyStackServerOptions`, `RouteContext`, `StaticFileHandler`, `FaviconHandler`, `CustomRouteHandler`, `ProdRuntimeMapsLoaderOptions`, `ParsedServerArgv`, `SecurityHeadersBuilder`, `ErrorFormatter`, `ErrorFormatterContext` | Handler + option typing. | -> docs/create-server.md |
64
+ | Types: `CreateLuckyStackServerOptions`, `BootstrapLuckyStackOptions`, `BootstrapRequirements`, `RunningLuckyStackServer`, `StopLuckyStackServerOptions`, `AfterListenOptions`, `RouteContext`, `StaticFileHandler`, `FaviconHandler`, `CustomRouteHandler`, `ProdRuntimeMapsLoaderOptions`, `ParsedServerArgv`, `SecurityHeadersBuilder`, `ErrorFormatter`, `ErrorFormatterContext` | Handler + option typing. | -> docs/create-server.md |
64
65
 
65
66
  ## Config keys (env vars + registerProjectConfig slots)
66
67
 
package/dist/index.d.ts CHANGED
@@ -157,6 +157,16 @@ interface StopLuckyStackServerOptions {
157
157
  */
158
158
  timeoutMs?: number;
159
159
  }
160
+ interface AfterListenOptions {
161
+ /**
162
+ * Re-throw instead of swallowing. Use only when the process is genuinely
163
+ * useless without this task. Default `false` — a listening server survives a
164
+ * failing post-listen task.
165
+ */
166
+ fatal?: boolean;
167
+ /** Label used in the failure log. Defaults to `post-listen task`. */
168
+ label?: string;
169
+ }
160
170
  interface RunningLuckyStackServer {
161
171
  httpServer: Server;
162
172
  ioServer: Server$1;
@@ -165,6 +175,20 @@ interface RunningLuckyStackServer {
165
175
  * URL on success.
166
176
  */
167
177
  listen: (callback?: () => void) => Promise<Server>;
178
+ /**
179
+ * Run work that belongs AFTER the server is accepting traffic — durable queue
180
+ * workers, warm-up passes, reconciliation jobs.
181
+ *
182
+ * A rejection here is logged loudly and swallowed by default, because the
183
+ * server is already up: killing a healthy process over one broken dependency
184
+ * turns an outage into a crash-loop (supervisor restarts, dependency is still
185
+ * down, repeat). Pass `{ fatal: true }` when the process genuinely has no
186
+ * purpose without the task — the rejection then propagates to the caller.
187
+ *
188
+ * Never resolves to the task's value; it is fire-and-report, not a pipeline
189
+ * stage. Read what you need inside the callback.
190
+ */
191
+ afterListen: (task: () => void | Promise<void>, options?: AfterListenOptions) => Promise<void>;
168
192
  /**
169
193
  * Graceful shutdown (MIS-016). Stops accepting new connections, dispatches the
170
194
  * core `preServerStop` hook, flushes error-trackers (bounded by a timeout
@@ -325,4 +349,4 @@ declare const registerSecurityHeaders: (builder: SecurityHeadersBuilder | null)
325
349
  /** Read the active builder (or null). */
326
350
  declare const getSecurityHeadersBuilder: () => SecurityHeadersBuilder | null;
327
351
 
328
- export { type BootstrapLuckyStackOptions, type BootstrapRequirements, type CreateLuckyStackServerOptions, type CustomRouteHandler, type CustomRoutePhase, type FaviconHandler, type GeneratedRuntimeMapsModule, OVERLAY_ORDER, type OnLocationUpdatePayload, type OnSocketConnectPayload, type OnSocketDisconnectPayload, type OriginExemptMatcher, type OverlayEntries, type ParsedServerArgv, type PostRoomJoinPayload, type PostRoomLeavePayload, type PreRoomJoinPayload, type PreRoomLeavePayload, type ProdRuntimeMapsLoaderOptions, type RegisterCustomRouteOptions, type RouteContext, type RunningLuckyStackServer, type SecurityHeadersBuilder, type StaticFileHandler, type StopLuckyStackServerOptions, applyServerArgv, bootstrapLuckyStack, clearCustomRoutes, clearOriginExemptPaths, collectOverlayEntries, createLuckyStackServer, createProdRuntimeMapsProvider, getCustomRoutes, getOriginExemptPaths, getParsedBundles, getParsedPort, getPreParamsCustomRoutes, getSecurityHeadersBuilder, isOriginExemptPath, parseServerArgv, registerCustomRoute, registerOriginExemptPath, registerOverlayLoader, registerProdRuntimeMapsProvider, registerSecurityHeaders, verifyBootstrap };
352
+ export { type AfterListenOptions, type BootstrapLuckyStackOptions, type BootstrapRequirements, type CreateLuckyStackServerOptions, type CustomRouteHandler, type CustomRoutePhase, type FaviconHandler, type GeneratedRuntimeMapsModule, OVERLAY_ORDER, type OnLocationUpdatePayload, type OnSocketConnectPayload, type OnSocketDisconnectPayload, type OriginExemptMatcher, type OverlayEntries, type ParsedServerArgv, type PostRoomJoinPayload, type PostRoomLeavePayload, type PreRoomJoinPayload, type PreRoomLeavePayload, type ProdRuntimeMapsLoaderOptions, type RegisterCustomRouteOptions, type RouteContext, type RunningLuckyStackServer, type SecurityHeadersBuilder, type StaticFileHandler, type StopLuckyStackServerOptions, applyServerArgv, bootstrapLuckyStack, clearCustomRoutes, clearOriginExemptPaths, collectOverlayEntries, createLuckyStackServer, createProdRuntimeMapsProvider, getCustomRoutes, getOriginExemptPaths, getParsedBundles, getParsedPort, getPreParamsCustomRoutes, getSecurityHeadersBuilder, isOriginExemptPath, parseServerArgv, registerCustomRoute, registerOriginExemptPath, registerOverlayLoader, registerProdRuntimeMapsProvider, registerSecurityHeaders, verifyBootstrap };
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  // src/createServer.ts
11
11
  import http from "http";
12
- import { registerBindAddress, registerBoundAddress, writeBootUuid, startBootUuidHeartbeat, getLogger as getLogger13, getProjectConfig as getProjectConfig15, tryCatch as tryCatch10, tryCatchSync as tryCatchSync3, resolveRuntimeMode, dispatchHook as dispatchHook8 } from "@luckystack/core";
12
+ import { registerBindAddress, registerBoundAddress, writeBootUuid, startBootUuidHeartbeat, getLogger as getLogger14, getProjectConfig as getProjectConfig15, tryCatch as tryCatch11, tryCatchSync as tryCatchSync3, resolveRuntimeMode, dispatchHook as dispatchHook8 } from "@luckystack/core";
13
13
 
14
14
  // src/httpHandler.ts
15
15
  import { randomUUID } from "crypto";
@@ -2340,6 +2340,18 @@ var clearDevServerInfo = () => {
2340
2340
  }
2341
2341
  };
2342
2342
 
2343
+ // src/afterListen.ts
2344
+ import { getLogger as getLogger13, tryCatch as tryCatch10 } from "@luckystack/core";
2345
+ var runAfterListenTask = async (task, { fatal = false, label = "post-listen task" } = {}) => {
2346
+ const [error] = await tryCatch10(async () => task());
2347
+ if (!error) return;
2348
+ if (fatal) throw error;
2349
+ getLogger13().error(
2350
+ `[server] ${label} failed \u2014 the server is listening and stays up. Fix the underlying cause; pass { fatal: true } to afterListen if this process should not run without it.`,
2351
+ error
2352
+ );
2353
+ };
2354
+
2343
2355
  // src/createServer.ts
2344
2356
  var initDevTools = async () => {
2345
2357
  const { initConsolelog } = await import("@luckystack/core");
@@ -2361,19 +2373,19 @@ var initDevTools = async () => {
2361
2373
  });
2362
2374
  const devkitModuleId = "@luckystack/devkit";
2363
2375
  if (!canResolve(devkitModuleId)) {
2364
- getLogger13().warn(
2376
+ getLogger14().warn(
2365
2377
  "dev tooling unavailable \u2014 @luckystack/devkit is not installed, so hot reload + type-map generation are off. Install it as a devDependency for dev, or set NODE_ENV=production to run in production mode."
2366
2378
  );
2367
2379
  return;
2368
2380
  }
2369
- const [devkitError] = await tryCatch10(async () => {
2381
+ const [devkitError] = await tryCatch11(async () => {
2370
2382
  const devkit = await import(devkitModuleId);
2371
2383
  await devkit.initializeAll();
2372
2384
  devkit.setupWatchers();
2373
2385
  });
2374
2386
  if (devkitError) {
2375
2387
  markDevToolsInitFailed(devkitError);
2376
- getLogger13().error(
2388
+ getLogger14().error(
2377
2389
  `dev tooling FAILED to initialize \u2014 the server is running but EVERY /api and /sync route will fail until this is fixed. Hot reload is OFF (the file watchers never started), so fix the cause below and RESTART the server. Cause: ${devkitError.message}`,
2378
2390
  devkitError
2379
2391
  );
@@ -2396,7 +2408,7 @@ var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((re
2396
2408
  return;
2397
2409
  }
2398
2410
  if (autoIncrement && attemptPort < 65535) {
2399
- getLogger13().warn(
2411
+ getLogger14().warn(
2400
2412
  `Port ${String(attemptPort)} is in use \u2014 trying ${String(attemptPort + 1)} (auto-increment; set SERVER_PORT_AUTO_INCREMENT=0 to disable). A previous/zombie dev server is still holding :${String(attemptPort)}: a manual client pinned there will keep talking to the OLD process, while the Vite proxy follows this restart's advertised port. If this restart was meant to replace it, stop the process on :${String(attemptPort)} first.`
2401
2413
  );
2402
2414
  tryListen(attemptPort + 1);
@@ -2406,11 +2418,11 @@ var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((re
2406
2418
  const rangeError = new RangeError(
2407
2419
  "Port 65535 is already in use and auto-increment cannot select a valid higher TCP port."
2408
2420
  );
2409
- getLogger13().error(rangeError.message);
2421
+ getLogger14().error(rangeError.message);
2410
2422
  reject(rangeError);
2411
2423
  return;
2412
2424
  }
2413
- getLogger13().error(
2425
+ getLogger14().error(
2414
2426
  `Port ${String(attemptPort)} is already in use \u2014 the server did NOT start. Another \`npm run server\` is probably still running (stop it), or choose a free port in config.ports.ts / positional argv, or set SERVER_PORT_AUTO_INCREMENT=1 to auto-pick the next free port.`
2415
2427
  );
2416
2428
  reject(err);
@@ -2432,14 +2444,14 @@ var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((re
2432
2444
  return callbackUrl.port || (callbackUrl.protocol === "https:" ? "443" : "80");
2433
2445
  })[1] ?? "";
2434
2446
  if (callbackPort === String(startPort) && callbackPort !== String(boundPort)) {
2435
- getLogger13().warn(
2447
+ getLogger14().warn(
2436
2448
  `OAuth port drift: the server bound :${String(boundPort)} but your OAuth callback base is configured for :${callbackPort} (${configuredCallbackBase}). The framework now auto-targets :${String(boundPort)} for OAuth so the callback reaches THIS server \u2014 but your provider (Google/GitHub/\u2026) still exact-matches its registered redirect URI, so add :${String(boundPort)} to the authorized redirect URIs, OR set SERVER_PORT_AUTO_INCREMENT=0 to pin :${callbackPort} (stop whatever holds it) instead of hopping.`
2437
2449
  );
2438
2450
  }
2439
2451
  }
2440
2452
  const config = getProjectConfig15();
2441
2453
  if (config.logging.socketStartup || config.logging.devLogs) {
2442
- getLogger13().info(`Server is running on http://${ip}:${String(boundPort)}/`);
2454
+ getLogger14().info(`Server is running on http://${ip}:${String(boundPort)}/`);
2443
2455
  }
2444
2456
  callback?.();
2445
2457
  resolve(httpServer);
@@ -2470,7 +2482,7 @@ var createLuckyStackServer = async (options = {}) => {
2470
2482
  if (enableDevTools) {
2471
2483
  await initDevTools();
2472
2484
  }
2473
- const [bootUuidError] = await tryCatch10(() => writeBootUuid());
2485
+ const [bootUuidError] = await tryCatch11(() => writeBootUuid());
2474
2486
  if (bootUuidError) {
2475
2487
  throw new Error(
2476
2488
  "Failed to write the boot UUID to Redis. Check REDIS_HOST / REDIS_PORT / REDIS_USER / REDIS_PASSWORD and that Redis is reachable.",
@@ -2482,7 +2494,7 @@ var createLuckyStackServer = async (options = {}) => {
2482
2494
  });
2483
2495
  httpServer.on("error", (err) => {
2484
2496
  if (err.code === "EADDRINUSE") return;
2485
- getLogger13().error("[http-server] runtime error", err);
2497
+ getLogger14().error("[http-server] runtime error", err);
2486
2498
  });
2487
2499
  const { io: ioServer, adapterClients } = loadSocket(httpServer, {
2488
2500
  maxHttpBufferSize: options.maxHttpBufferSize
@@ -2513,14 +2525,14 @@ var createLuckyStackServer = async (options = {}) => {
2513
2525
  handleSignal("SIGINT");
2514
2526
  });
2515
2527
  }
2516
- return { httpServer, ioServer, listen, stop, close: stop };
2528
+ return { httpServer, ioServer, listen, afterListen: runAfterListenTask, stop, close: stop };
2517
2529
  };
2518
2530
 
2519
2531
  // src/bootstrap.ts
2520
2532
  import path3 from "path";
2521
2533
  import fs2 from "fs";
2522
2534
  import { pathToFileURL } from "url";
2523
- import { ROOT_DIR, tryCatch as tryCatch11, getLogger as getLogger14, isTestFile, isTestDirectory } from "@luckystack/core";
2535
+ import { ROOT_DIR, tryCatch as tryCatch12, getLogger as getLogger15, isTestFile, isTestDirectory } from "@luckystack/core";
2524
2536
  var OVERLAY_ORDER = [
2525
2537
  // Core registries first — clients, paths, routing rules. Anything below
2526
2538
  // depends on these being in place.
@@ -2545,7 +2557,7 @@ var OVERLAY_ORDER = [
2545
2557
  ];
2546
2558
  var importIfExists = async (filePath) => {
2547
2559
  if (!fs2.existsSync(filePath)) return;
2548
- const [error] = await tryCatch11(async () => {
2560
+ const [error] = await tryCatch12(async () => {
2549
2561
  await import(pathToFileURL(filePath).href);
2550
2562
  });
2551
2563
  if (error) {
@@ -2586,7 +2598,7 @@ var loadOverlayFolder = async (overlayRoot) => {
2586
2598
  if (!fs2.existsSync(packageDir)) continue;
2587
2599
  const { fileNames, ignoredDirectoryNames } = collectOverlayEntries(packageDir);
2588
2600
  for (const directoryName of ignoredDirectoryNames) {
2589
- getLogger14().warn(
2601
+ getLogger15().warn(
2590
2602
  `[luckystack:overlay] ignoring \`${overlayRoot}/${packageName}/${directoryName}/\` \u2014 the overlay walk does not recurse. Move the file(s) up into \`${overlayRoot}/${packageName}/\`, or import them from a file that is there.`
2591
2603
  );
2592
2604
  }