@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 +6 -0
- package/CLAUDE.md +3 -2
- package/dist/index.d.ts +25 -1
- package/dist/index.js +27 -15
- package/dist/index.js.map +1 -1
- package/docs/create-server.md +33 -0
- package/package.json +11 -11
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2421
|
+
getLogger14().error(rangeError.message);
|
|
2410
2422
|
reject(rangeError);
|
|
2411
2423
|
return;
|
|
2412
2424
|
}
|
|
2413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|