@lithia-js/core 1.0.0-canary.2 → 1.0.0-canary.21

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.
Files changed (102) hide show
  1. package/README.md +26 -43
  2. package/dist/_index.d.ts +245 -0
  3. package/dist/_index.mjs +2106 -0
  4. package/dist/_index.mjs.map +1 -0
  5. package/dist/index.d.ts +824 -0
  6. package/dist/index.mjs +856 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/dist/protocol-DBwVPJYN.d.ts +332 -0
  9. package/dist/tasks-X-3clDS8.d.ts +31 -0
  10. package/dist/workers/app-worker.d.ts +2 -0
  11. package/dist/workers/app-worker.mjs +1907 -0
  12. package/dist/workers/app-worker.mjs.map +1 -0
  13. package/dist/workers/task-worker.d.ts +45 -0
  14. package/dist/workers/task-worker.mjs +146 -0
  15. package/dist/workers/task-worker.mjs.map +1 -0
  16. package/package.json +47 -23
  17. package/CHANGELOG.md +0 -31
  18. package/dist/config.d.ts +0 -101
  19. package/dist/config.js +0 -113
  20. package/dist/config.js.map +0 -1
  21. package/dist/context/event-context.d.ts +0 -53
  22. package/dist/context/event-context.js +0 -42
  23. package/dist/context/event-context.js.map +0 -1
  24. package/dist/context/index.d.ts +0 -16
  25. package/dist/context/index.js +0 -29
  26. package/dist/context/index.js.map +0 -1
  27. package/dist/context/lithia-context.d.ts +0 -47
  28. package/dist/context/lithia-context.js +0 -43
  29. package/dist/context/lithia-context.js.map +0 -1
  30. package/dist/context/route-context.d.ts +0 -74
  31. package/dist/context/route-context.js +0 -42
  32. package/dist/context/route-context.js.map +0 -1
  33. package/dist/env.d.ts +0 -1
  34. package/dist/env.js +0 -32
  35. package/dist/env.js.map +0 -1
  36. package/dist/errors.d.ts +0 -51
  37. package/dist/errors.js +0 -80
  38. package/dist/errors.js.map +0 -1
  39. package/dist/hooks/dependency-hooks.d.ts +0 -105
  40. package/dist/hooks/dependency-hooks.js +0 -96
  41. package/dist/hooks/dependency-hooks.js.map +0 -1
  42. package/dist/hooks/event-hooks.d.ts +0 -61
  43. package/dist/hooks/event-hooks.js +0 -70
  44. package/dist/hooks/event-hooks.js.map +0 -1
  45. package/dist/hooks/index.d.ts +0 -41
  46. package/dist/hooks/index.js +0 -59
  47. package/dist/hooks/index.js.map +0 -1
  48. package/dist/hooks/route-hooks.d.ts +0 -154
  49. package/dist/hooks/route-hooks.js +0 -174
  50. package/dist/hooks/route-hooks.js.map +0 -1
  51. package/dist/lib.d.ts +0 -10
  52. package/dist/lib.js +0 -30
  53. package/dist/lib.js.map +0 -1
  54. package/dist/lithia.d.ts +0 -447
  55. package/dist/lithia.js +0 -649
  56. package/dist/lithia.js.map +0 -1
  57. package/dist/logger.d.ts +0 -11
  58. package/dist/logger.js +0 -55
  59. package/dist/logger.js.map +0 -1
  60. package/dist/module-loader.d.ts +0 -12
  61. package/dist/module-loader.js +0 -78
  62. package/dist/module-loader.js.map +0 -1
  63. package/dist/server/event-processor.d.ts +0 -195
  64. package/dist/server/event-processor.js +0 -253
  65. package/dist/server/event-processor.js.map +0 -1
  66. package/dist/server/http-server.d.ts +0 -196
  67. package/dist/server/http-server.js +0 -295
  68. package/dist/server/http-server.js.map +0 -1
  69. package/dist/server/middlewares/validation.d.ts +0 -12
  70. package/dist/server/middlewares/validation.js +0 -34
  71. package/dist/server/middlewares/validation.js.map +0 -1
  72. package/dist/server/request-processor.d.ts +0 -400
  73. package/dist/server/request-processor.js +0 -652
  74. package/dist/server/request-processor.js.map +0 -1
  75. package/dist/server/request.d.ts +0 -73
  76. package/dist/server/request.js +0 -207
  77. package/dist/server/request.js.map +0 -1
  78. package/dist/server/response.d.ts +0 -69
  79. package/dist/server/response.js +0 -173
  80. package/dist/server/response.js.map +0 -1
  81. package/src/config.ts +0 -212
  82. package/src/context/event-context.ts +0 -66
  83. package/src/context/index.ts +0 -32
  84. package/src/context/lithia-context.ts +0 -59
  85. package/src/context/route-context.ts +0 -89
  86. package/src/env.ts +0 -31
  87. package/src/errors.ts +0 -96
  88. package/src/hooks/dependency-hooks.ts +0 -122
  89. package/src/hooks/event-hooks.ts +0 -69
  90. package/src/hooks/index.ts +0 -58
  91. package/src/hooks/route-hooks.ts +0 -177
  92. package/src/lib.ts +0 -27
  93. package/src/lithia.ts +0 -777
  94. package/src/logger.ts +0 -66
  95. package/src/module-loader.ts +0 -45
  96. package/src/server/event-processor.ts +0 -344
  97. package/src/server/http-server.ts +0 -371
  98. package/src/server/middlewares/validation.ts +0 -46
  99. package/src/server/request-processor.ts +0 -860
  100. package/src/server/request.ts +0 -247
  101. package/src/server/response.ts +0 -204
  102. package/tsconfig.json +0 -8
@@ -0,0 +1,1907 @@
1
+ import { parentPort, workerData, isMainThread } from 'worker_threads';
2
+ import { logger, red, yellow, green } from '@lithia-js/utils';
3
+ import { randomUUID, createHash } from 'crypto';
4
+ import { AsyncLocalStorage } from 'async_hooks';
5
+ import { createServer as createServer$1 } from 'http';
6
+ import { createServer } from 'https';
7
+ import busboy from 'busboy';
8
+ import { parse, serialize } from 'cookie';
9
+ import { access, readFile, constants, stat } from 'fs/promises';
10
+ import { pathToFileURL } from 'url';
11
+ import { isAsyncFunction } from 'util/types';
12
+ import path, { join, normalize, extname } from 'path';
13
+ import { statSync, createReadStream } from 'fs';
14
+ import { Server } from 'socket.io';
15
+ import cron from 'node-cron';
16
+
17
+ // src/runtime/workers/app-worker.ts
18
+
19
+ // src/errors/base.ts
20
+ var LithiaError = class extends Error {
21
+ isLithiaError = true;
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = this.constructor.name;
25
+ Object.setPrototypeOf(this, new.target.prototype);
26
+ }
27
+ };
28
+ var LithiaClientError = class extends LithiaError {
29
+ constructor(message, statusCode, details) {
30
+ super(message);
31
+ this.statusCode = statusCode;
32
+ this.details = details;
33
+ }
34
+ timestamp = /* @__PURE__ */ new Date();
35
+ };
36
+
37
+ // src/errors/internal/loader.ts
38
+ var NoDefaultExportError = class extends LithiaError {
39
+ constructor(filePath) {
40
+ super(`Module at '${filePath}' is missing a default export.`);
41
+ }
42
+ };
43
+ var NoAsyncDefaultExportError = class extends LithiaError {
44
+ constructor(filePath) {
45
+ super(`Default export at '${filePath}' must be an async function.`);
46
+ }
47
+ };
48
+
49
+ // src/context/lithia-context.ts
50
+ var LITHIA_CONTEXT_KEY = /* @__PURE__ */ Symbol.for("lithia.base_context.v1");
51
+ function getGlobalLithiaStore() {
52
+ const globalAny = globalThis;
53
+ if (!globalAny[LITHIA_CONTEXT_KEY]) {
54
+ globalAny[LITHIA_CONTEXT_KEY] = new AsyncLocalStorage();
55
+ }
56
+ return globalAny[LITHIA_CONTEXT_KEY];
57
+ }
58
+ var lithiaContextStore = getGlobalLithiaStore();
59
+ function runInLithiaContext(context, fn) {
60
+ return lithiaContextStore.run(context, fn);
61
+ }
62
+ var ROUTE_CONTEXT_KEY = /* @__PURE__ */ Symbol.for("lithia.route_context.v1");
63
+ function getGlobalRouteStore() {
64
+ const globalAny = globalThis;
65
+ if (!globalAny[ROUTE_CONTEXT_KEY]) {
66
+ globalAny[ROUTE_CONTEXT_KEY] = new AsyncLocalStorage();
67
+ }
68
+ return globalAny[ROUTE_CONTEXT_KEY];
69
+ }
70
+ var routeContextStore = getGlobalRouteStore();
71
+
72
+ // src/errors/app/client.ts
73
+ var BadRequestError = class extends LithiaClientError {
74
+ constructor(m, d) {
75
+ super(m, 400, d);
76
+ }
77
+ };
78
+ var RouteNotFoundError = class extends LithiaClientError {
79
+ constructor(m, d) {
80
+ super(m, 404, d);
81
+ }
82
+ };
83
+
84
+ // src/errors/app/server.ts
85
+ var InternalServerError = class extends LithiaClientError {
86
+ constructor(m, d) {
87
+ super(m, 500, d);
88
+ }
89
+ };
90
+
91
+ // src/transport/http/request.ts
92
+ var LithiaRequest = class {
93
+ /**
94
+ * Creates a request wrapper for the current HTTP transaction.
95
+ *
96
+ * The constructor captures headers, reconstructs a best-effort absolute URL,
97
+ * normalizes the HTTP method to uppercase, and initializes parsed query and
98
+ * route-param containers for later middleware and handler use.
99
+ *
100
+ * @param {IncomingMessage} req - Raw Node.js request object received by the
101
+ * HTTP server.
102
+ * @param {{ maxBodySize?: number }} opts - Per-request parsing options used
103
+ * when consuming the request body stream.
104
+ */
105
+ constructor(req, opts) {
106
+ this.req = req;
107
+ this.opts = opts;
108
+ this.headers = req.headers;
109
+ const isSecure = this.isSecure();
110
+ const host = this.headers.host || req.headers.host || "unknown";
111
+ const fullUrl = `${isSecure ? "https" : "http"}://${host}${req.url || "/"}`;
112
+ const url = new URL(fullUrl);
113
+ this.pathname = url.pathname;
114
+ this.method = (req.method || "GET").toUpperCase();
115
+ this.query = parseQueryToObject(url.searchParams);
116
+ this.params = {};
117
+ }
118
+ headers;
119
+ method;
120
+ pathname;
121
+ query;
122
+ params;
123
+ storage = /* @__PURE__ */ new Map();
124
+ _bodyCache = null;
125
+ _filesCache = null;
126
+ _cookies = null;
127
+ /**
128
+ * Returns the best-effort client IP address for the current request.
129
+ *
130
+ * The lookup prefers proxy-forwarded headers before falling back to the raw
131
+ * socket address, which makes the result suitable for deployments behind
132
+ * reverse proxies that preserve `x-forwarded-for` or `x-real-ip`.
133
+ *
134
+ * @returns {string} The resolved client IP address, or `"unknown"` when no
135
+ * address can be derived.
136
+ */
137
+ ip() {
138
+ return this.headers["x-forwarded-for"]?.split(",")[0]?.trim() || this.headers["x-real-ip"] || this.req.socket?.remoteAddress || "unknown";
139
+ }
140
+ /**
141
+ * Returns the current request user-agent string.
142
+ *
143
+ * @returns {string} The raw `user-agent` header value, or an empty string
144
+ * when the header is missing.
145
+ */
146
+ userAgent() {
147
+ return this.headers["user-agent"] || "";
148
+ }
149
+ /**
150
+ * Returns whether the current request is using HTTPS.
151
+ *
152
+ * The check prefers `x-forwarded-proto` for proxy-aware deployments and then
153
+ * falls back to the encrypted state of the underlying socket.
154
+ *
155
+ * @returns {boolean} `true` when the request should be treated as HTTPS.
156
+ */
157
+ isSecure() {
158
+ return this.headers["x-forwarded-proto"] === "https" || this.req.socket?.encrypted === true;
159
+ }
160
+ /**
161
+ * Returns the request host header.
162
+ *
163
+ * @returns {string} The current host header value, or `"unknown"` when it is
164
+ * not available.
165
+ */
166
+ host() {
167
+ return this.headers.host || "unknown";
168
+ }
169
+ /**
170
+ * Returns the absolute request URL reconstructed from the current request.
171
+ *
172
+ * This helper rebuilds the URL from the current security state, host header,
173
+ * and parsed pathname. It does not append the original query string.
174
+ *
175
+ * @returns {string} Absolute URL for the current request pathname.
176
+ */
177
+ url() {
178
+ return `${this.isSecure() ? "https" : "http"}://${this.host()}${this.pathname}`;
179
+ }
180
+ /**
181
+ * Parses and returns the request body.
182
+ *
183
+ * JSON and plain text bodies are supported automatically. Multipart requests
184
+ * populate both `body()` and `files()` through a shared parsing pass. The
185
+ * parsed value is cached after the first read so later consumers do not touch
186
+ * the underlying stream again.
187
+ *
188
+ * Requests whose method is not one of `POST`, `PUT`, `PATCH`, or `DELETE`
189
+ * resolve to an empty object without reading the stream.
190
+ *
191
+ * @returns {Promise<T>} Parsed request body, multipart field map, raw text, or
192
+ * an empty object for methods that do not consume a body by default.
193
+ * @throws {BadRequestError} Thrown when the declared or streamed body size
194
+ * exceeds `maxBodySize`, or when JSON parsing fails.
195
+ */
196
+ async body() {
197
+ const methodsWithBody = ["POST", "PUT", "PATCH", "DELETE"];
198
+ if (!methodsWithBody.includes(this.method)) {
199
+ return {};
200
+ }
201
+ if (this._bodyCache !== null) return this._bodyCache;
202
+ const contentType = this.headers["content-type"] || "";
203
+ if (contentType.includes("multipart/form-data")) {
204
+ await this.parseMultipart();
205
+ return this._bodyCache;
206
+ }
207
+ const contentLength = parseInt(
208
+ this.headers["content-length"] || "0",
209
+ 10
210
+ );
211
+ const maxBodySize = this.opts.maxBodySize || 1024 * 1024;
212
+ if (contentLength > maxBodySize) {
213
+ throw new BadRequestError("Request body too large.");
214
+ }
215
+ const body = await new Promise((resolve, reject) => {
216
+ const chunks = [];
217
+ let currentSize = 0;
218
+ this.req.on("data", (chunk) => {
219
+ currentSize += chunk.length;
220
+ if (currentSize > maxBodySize) {
221
+ reject(new BadRequestError("Request body too large."));
222
+ }
223
+ chunks.push(chunk);
224
+ });
225
+ this.req.on("end", () => {
226
+ if (chunks.length === 0) return resolve({});
227
+ const rawBody = Buffer.concat(chunks).toString("utf-8");
228
+ try {
229
+ if (contentType.includes("application/json")) {
230
+ resolve(JSON.parse(rawBody));
231
+ } else {
232
+ resolve(rawBody);
233
+ }
234
+ } catch {
235
+ reject(new BadRequestError("Invalid request body format."));
236
+ }
237
+ });
238
+ this.req.on("error", (err) => reject(err));
239
+ });
240
+ this._bodyCache = body;
241
+ this.storage.set("body", body);
242
+ return body;
243
+ }
244
+ /**
245
+ * Returns uploaded files for multipart/form-data requests.
246
+ *
247
+ * `files()` shares the same multipart parsing pass used by `body()`. The
248
+ * first call buffers every uploaded file into memory and caches both the
249
+ * parsed field object and file array for later access.
250
+ *
251
+ * @returns {Promise<UploadedFile[]>} Buffered multipart files, or an empty
252
+ * array when the request is not multipart.
253
+ */
254
+ async files() {
255
+ const contentType = this.headers["content-type"] || "";
256
+ if (!contentType.includes("multipart/form-data")) return [];
257
+ if (this._filesCache !== null) return this._filesCache;
258
+ await this.parseMultipart();
259
+ return this._filesCache || [];
260
+ }
261
+ /**
262
+ * Overrides the cached body value for the current request context.
263
+ *
264
+ * This mutates only the wrapper cache and the internal storage map. It does
265
+ * not modify the underlying Node.js request stream.
266
+ *
267
+ * @param {unknown} value - Replacement body value to expose through `body()`
268
+ * and internal request storage.
269
+ */
270
+ setBody(value) {
271
+ this._bodyCache = value;
272
+ this.storage.set("body", value);
273
+ }
274
+ /**
275
+ * Returns all parsed cookies from the request.
276
+ *
277
+ * Cookies are parsed lazily on first access and cached for the remainder of
278
+ * the request lifecycle.
279
+ *
280
+ * @returns {Cookies} Parsed cookie map for the current request.
281
+ */
282
+ cookies() {
283
+ if (this._cookies === null) {
284
+ const cookieHeader = this.headers.cookie;
285
+ this._cookies = cookieHeader ? parse(cookieHeader) : {};
286
+ }
287
+ return this._cookies;
288
+ }
289
+ /**
290
+ * Returns a single cookie value by name.
291
+ *
292
+ * @param {string} name - Cookie name to read from the parsed cookie map.
293
+ * @returns {string | undefined} The cookie value when present.
294
+ */
295
+ cookie(name) {
296
+ return this.cookies()[name];
297
+ }
298
+ /**
299
+ * Returns a value stored in the per-request internal storage map.
300
+ *
301
+ * This storage is local to the current request wrapper and can be used by
302
+ * middleware and handlers to exchange derived values without mutating the
303
+ * typed request surface.
304
+ *
305
+ * @param {string} key - Storage key associated with the requested value.
306
+ * @returns {T | undefined} Stored value for the key, if one exists.
307
+ */
308
+ get(key) {
309
+ return this.storage.get(key);
310
+ }
311
+ /**
312
+ * Stores a value in the per-request internal storage map.
313
+ *
314
+ * @param {string} key - Storage key to create or overwrite.
315
+ * @param {unknown} value - Arbitrary value to retain for the lifetime of the
316
+ * current request wrapper.
317
+ */
318
+ set(key, value) {
319
+ this.storage.set(key, value);
320
+ }
321
+ /**
322
+ * Parses a multipart/form-data request into cached fields and file buffers.
323
+ *
324
+ * The request stream is piped into Busboy exactly once. Field values are
325
+ * collected into a plain object, file contents are buffered fully in memory,
326
+ * and both results are stored in the request cache and internal storage map.
327
+ *
328
+ * @returns {Promise<void>} Resolves after Busboy finishes consuming the
329
+ * multipart stream and caches the parsed payload.
330
+ */
331
+ async parseMultipart() {
332
+ if (this._bodyCache !== null && this._filesCache !== null) return;
333
+ return new Promise((resolve, reject) => {
334
+ const bb = busboy({ headers: this.headers });
335
+ const fields = {};
336
+ const files = [];
337
+ bb.on("file", (name, file, info) => {
338
+ const chunks = [];
339
+ file.on("data", (data) => chunks.push(data));
340
+ file.on("end", () => {
341
+ files.push({
342
+ fieldname: name,
343
+ buffer: Buffer.concat(chunks),
344
+ ...info
345
+ });
346
+ });
347
+ });
348
+ bb.on("field", (name, value) => {
349
+ fields[name] = value;
350
+ });
351
+ bb.on("finish", () => {
352
+ this._bodyCache = fields;
353
+ this._filesCache = files;
354
+ this.storage.set("body", fields);
355
+ this.storage.set("files", files);
356
+ resolve();
357
+ });
358
+ bb.on("error", reject);
359
+ this.req.pipe(bb);
360
+ });
361
+ }
362
+ };
363
+ function parseQueryToObject(searchParams) {
364
+ const query = {};
365
+ for (const [key, value] of searchParams.entries()) {
366
+ query[key] = value;
367
+ }
368
+ return query;
369
+ }
370
+ async function loadModule(filePath) {
371
+ const exists = await access(filePath, constants.F_OK).then(() => true).catch(() => false);
372
+ if (!exists) {
373
+ throw new LithiaError(
374
+ `The module at '${filePath}' could not be found or is not accessible.`
375
+ );
376
+ }
377
+ let mod;
378
+ try {
379
+ mod = await import(pathToFileURL(filePath).href);
380
+ } catch (err) {
381
+ throw new LithiaError(
382
+ `Failed to import module at '${filePath}': ${err.message}`
383
+ );
384
+ }
385
+ if (!mod || !mod.default) {
386
+ throw new NoDefaultExportError(filePath);
387
+ }
388
+ const isFunction = typeof mod.default === "function";
389
+ const isAsync = isAsyncFunction(mod.default);
390
+ if (!isFunction || !isAsync) {
391
+ throw new NoAsyncDefaultExportError(filePath);
392
+ }
393
+ return mod;
394
+ }
395
+
396
+ // src/shared/pipeline.ts
397
+ async function executePipeline(steps, handler) {
398
+ let index = -1;
399
+ const dispatch = async (stepIndex) => {
400
+ if (stepIndex <= index) return;
401
+ index = stepIndex;
402
+ if (stepIndex === steps.length) {
403
+ return handler();
404
+ }
405
+ const step = steps[stepIndex];
406
+ if (step) {
407
+ await step(() => dispatch(stepIndex + 1));
408
+ }
409
+ };
410
+ await dispatch(0);
411
+ }
412
+
413
+ // src/transport/http/cors-policy.ts
414
+ function applyCorsPolicy(config, req, res) {
415
+ const { cors } = config.http;
416
+ if (!cors?.origin?.length) return false;
417
+ const requestOrigin = req.headers.origin;
418
+ if (!requestOrigin) return false;
419
+ const isOriginAllowed = cors.origin.includes("*") || cors.origin.some((allowed) => allowed === requestOrigin);
420
+ if (!isOriginAllowed) return false;
421
+ const allowedOrigin = cors.origin.includes("*") ? cors.credentials ? requestOrigin : "*" : requestOrigin;
422
+ res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
423
+ res.setHeader("Vary", "Origin");
424
+ if (cors.credentials) {
425
+ res.setHeader("Access-Control-Allow-Credentials", "true");
426
+ }
427
+ if (cors.exposedHeaders?.length) {
428
+ res.setHeader(
429
+ "Access-Control-Expose-Headers",
430
+ cors.exposedHeaders.join(", ")
431
+ );
432
+ }
433
+ if (req.method !== "OPTIONS") return false;
434
+ if (cors.methods?.length) {
435
+ res.setHeader("Access-Control-Allow-Methods", cors.methods.join(", "));
436
+ }
437
+ if (cors.allowedHeaders?.length) {
438
+ res.setHeader(
439
+ "Access-Control-Allow-Headers",
440
+ cors.allowedHeaders.join(", ")
441
+ );
442
+ }
443
+ if (cors.maxAge != null) {
444
+ res.setHeader("Access-Control-Max-Age", String(cors.maxAge));
445
+ }
446
+ res.status(204).end();
447
+ return true;
448
+ }
449
+ var DOCS_FILE = path.join("_lithia", "scalar.html");
450
+ var SPEC_FILE = path.join("_lithia", "openapi.json");
451
+ async function serveOpenAPIAsset(config, req, res) {
452
+ if (!config.openapi?.enabled) return false;
453
+ if (req.method !== "GET" && req.method !== "HEAD") return false;
454
+ const docsPath = normalizeOpenAPIPath(config.openapi.docsPath || "/docs");
455
+ const specPath = normalizeOpenAPIPath(
456
+ config.openapi.specPath || "/openapi.json"
457
+ );
458
+ const pathname = normalizeOpenAPIPath(req.pathname);
459
+ if (pathname === docsPath) {
460
+ const html = await readOpenAPIArtifact(config.outDir, DOCS_FILE);
461
+ if (!html) return false;
462
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
463
+ res.send(req.method === "HEAD" ? void 0 : html);
464
+ return true;
465
+ }
466
+ if (pathname === specPath) {
467
+ const spec = await readOpenAPIArtifact(config.outDir, SPEC_FILE);
468
+ if (!spec) return false;
469
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
470
+ res.send(req.method === "HEAD" ? void 0 : spec);
471
+ return true;
472
+ }
473
+ return false;
474
+ }
475
+ function normalizeOpenAPIPath(input) {
476
+ if (!input) return "/";
477
+ const normalized = input.startsWith("/") ? input : `/${input}`;
478
+ if (normalized.length > 1 && normalized.endsWith("/")) {
479
+ return normalized.slice(0, -1);
480
+ }
481
+ return normalized;
482
+ }
483
+ async function readOpenAPIArtifact(outDir, relativePath) {
484
+ try {
485
+ return await readFile(
486
+ path.join(process.cwd(), outDir, relativePath),
487
+ "utf-8"
488
+ );
489
+ } catch {
490
+ return null;
491
+ }
492
+ }
493
+ function produceDigest(err) {
494
+ const errString = err instanceof Error ? err.stack || err.message : String(err);
495
+ const hash = createHash("sha256").update(`${errString}${Date.now()}${Math.random()}`).digest("hex");
496
+ return hash.slice(0, 12);
497
+ }
498
+
499
+ // src/transport/http/request-error-handler.ts
500
+ function handleRequestError(environment, req, res, err) {
501
+ if (res._ended) return;
502
+ const error = err instanceof LithiaClientError ? err : new InternalServerError(err);
503
+ const isProd = environment === "production";
504
+ const statusCode = error.statusCode || 500;
505
+ const digest = produceDigest(err);
506
+ const message = isProd && statusCode >= 500 ? "Internal Server Error" : error.message;
507
+ res.status(statusCode).json({
508
+ error: {
509
+ statusCode,
510
+ message,
511
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
512
+ digest,
513
+ path: req.pathname,
514
+ method: req.method,
515
+ details: error.details
516
+ }
517
+ });
518
+ if (statusCode >= 500) {
519
+ logger.error(`Digest: ${red(digest)}`);
520
+ logger.info(`Path: ${req.method} ${req.pathname}`);
521
+ logger.info(err.stack || err);
522
+ }
523
+ }
524
+
525
+ // src/transport/http/route-matcher.ts
526
+ var RouteMatcher = class {
527
+ regexCache = /* @__PURE__ */ new Map();
528
+ /**
529
+ * Finds the first discovered route that matches the current request.
530
+ *
531
+ * Method-specific routes only match the corresponding request method, while
532
+ * method-agnostic routes can match any method. Path matching is delegated to
533
+ * the route manifest regex generated during discovery.
534
+ *
535
+ * @param {LithiaRequest} req - Current request wrapper.
536
+ * @param {Route[]} routes - Discovered route manifests to scan in order.
537
+ * @returns {Route | undefined} The first matching route manifest, if one
538
+ * exists.
539
+ */
540
+ findRoute(req, routes) {
541
+ const method = req.method.toLowerCase();
542
+ return routes.find((route) => {
543
+ const methodMatches = !route.method || route.method.toLowerCase() === method;
544
+ if (!methodMatches) return false;
545
+ const regex = this.getOrCreateRegex(route.regex);
546
+ return regex.test(req.pathname);
547
+ });
548
+ }
549
+ /**
550
+ * Synchronizes route-specific context after a route match is selected.
551
+ *
552
+ * When a request context store is active, the matched route manifest is
553
+ * attached to it. Dynamic routes also populate `req.params` by extracting
554
+ * capture groups from the request pathname.
555
+ *
556
+ * @param {Route} route - Matched route manifest.
557
+ * @param {LithiaRequest} req - Current request wrapper whose params may be
558
+ * updated.
559
+ */
560
+ setupContext(route, req) {
561
+ const store = routeContextStore.getStore();
562
+ if (store) {
563
+ store.route = route;
564
+ }
565
+ if (route.dynamic) {
566
+ req.params = this.extractParams(req.pathname, route);
567
+ }
568
+ }
569
+ /**
570
+ * Extracts decoded dynamic params from a matched pathname.
571
+ *
572
+ * Param names are derived from `route.path` segments such as `:id`, while
573
+ * values are read from the corresponding regex capture groups in the actual
574
+ * request pathname.
575
+ *
576
+ * @param {string} pathname - Incoming request pathname.
577
+ * @param {Route} route - Matched route manifest that supplies the paramized
578
+ * path and regex pattern.
579
+ * @returns {Params} Decoded param object for the current request.
580
+ */
581
+ extractParams(pathname, route) {
582
+ const regex = this.getOrCreateRegex(route.regex);
583
+ const match = pathname.match(regex);
584
+ if (!match) return {};
585
+ const paramNames = (route.path.match(/:([^/]+)/g) || []).map(
586
+ (segment) => segment.slice(1)
587
+ );
588
+ return paramNames.reduce((params, name, index) => {
589
+ const value = match[index + 1];
590
+ params[name] = value ? decodeURIComponent(value) : value;
591
+ return params;
592
+ }, {});
593
+ }
594
+ /**
595
+ * Returns a cached regular expression for a route manifest pattern.
596
+ *
597
+ * @param {string} pattern - Serialized regex pattern generated during route
598
+ * discovery.
599
+ * @returns {RegExp} Cached or newly compiled regular expression.
600
+ */
601
+ getOrCreateRegex(pattern) {
602
+ let regex = this.regexCache.get(pattern);
603
+ if (!regex) {
604
+ regex = new RegExp(pattern);
605
+ this.regexCache.set(pattern, regex);
606
+ }
607
+ return regex;
608
+ }
609
+ };
610
+ async function serveStaticAsset(config, req, res) {
611
+ const { static: staticConfig, http } = config;
612
+ if (!staticConfig?.root || req.method !== "GET" && req.method !== "HEAD") {
613
+ return false;
614
+ }
615
+ let relativePath = req.pathname;
616
+ if (staticConfig.prefix) {
617
+ if (!relativePath.startsWith(staticConfig.prefix)) return false;
618
+ relativePath = relativePath.slice(staticConfig.prefix.length);
619
+ }
620
+ const safePath = normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, "");
621
+ const fullPath = join(staticConfig.root, safePath);
622
+ try {
623
+ const stats = await stat(fullPath);
624
+ if (!stats.isFile()) return false;
625
+ const ext = extname(fullPath).toLowerCase();
626
+ const mime = http.mimeTypes?.[ext];
627
+ if (!mime) return false;
628
+ res.setHeader("Content-Type", mime);
629
+ res.send(fullPath);
630
+ return true;
631
+ } catch {
632
+ return false;
633
+ }
634
+ }
635
+
636
+ // src/transport/http/request-pipeline.ts
637
+ var LithiaRequestProcessor = class {
638
+ /**
639
+ * Creates an HTTP request processor bound to a running Lithia app instance.
640
+ *
641
+ * @param {LithiaApp} app - Runtime app state that provides config, manifests,
642
+ * middleware registries, and environment information for request execution.
643
+ */
644
+ constructor(app) {
645
+ this.app = app;
646
+ }
647
+ matcher = new RouteMatcher();
648
+ /**
649
+ * Processes a single HTTP request through the Lithia route pipeline.
650
+ *
651
+ * The method may terminate early when CORS preflight handling, OpenAPI asset
652
+ * serving, or static asset serving fully handles the request. Otherwise it
653
+ * resolves the matching route, prepares request params through the route
654
+ * matcher, loads the route module, executes global and route-local middleware,
655
+ * and then runs the route handler.
656
+ *
657
+ * If the handler and middleware chain complete without ending the response,
658
+ * the processor closes the response automatically with `res.end()`.
659
+ *
660
+ * @param {LithiaRequest} req - Current request wrapper.
661
+ * @param {LithiaResponse} res - Current response wrapper.
662
+ * @returns {Promise<void>} Resolves after the request reaches a terminal
663
+ * response or the error handler completes.
664
+ */
665
+ async process(req, res) {
666
+ const startTime = performance.now();
667
+ try {
668
+ this.setInitialHeaders(res);
669
+ if (applyCorsPolicy(this.app.config, req, res)) return;
670
+ if (await serveOpenAPIAsset(this.app.config, req, res)) return;
671
+ if (await serveStaticAsset(this.app.config, req, res)) return;
672
+ const route = this.matcher.findRoute(req, this.app.routes);
673
+ if (!route) {
674
+ throw new RouteNotFoundError(
675
+ `The requested resource '${req.pathname}' does not exist on this server.`
676
+ );
677
+ }
678
+ this.matcher.setupContext(route, req);
679
+ const mod = await loadModule(route.filePath);
680
+ const pipeline = [
681
+ ...this.app.globalRouteMiddlewares,
682
+ ...mod.middlewares || []
683
+ ];
684
+ await this.runRoutePipeline(pipeline, req, res, async () => {
685
+ await mod.default(req, res);
686
+ });
687
+ if (!res._ended) res.end();
688
+ } catch (error) {
689
+ handleRequestError(this.app.environment, req, res, error);
690
+ } finally {
691
+ this.logRequest(req, res, performance.now() - startTime);
692
+ }
693
+ }
694
+ /**
695
+ * Executes the composed route middleware chain and then the final handler.
696
+ *
697
+ * Middleware functions are adapted to the generic shared pipeline runner,
698
+ * which guarantees in-order execution as long as each middleware calls its
699
+ * `next()` continuation.
700
+ *
701
+ * @param {RouteMiddleware[]} middlewares - Global and route-scoped middleware
702
+ * stack to execute before the handler.
703
+ * @param {LithiaRequest} req - Current request wrapper shared across the
704
+ * entire pipeline.
705
+ * @param {LithiaResponse} res - Current response wrapper shared across the
706
+ * entire pipeline.
707
+ * @param {RouteHandler} handler - Final route handler invoked after all
708
+ * middleware completes.
709
+ * @returns {Promise<void>} Resolves after the middleware chain and handler
710
+ * finish.
711
+ */
712
+ async runRoutePipeline(middlewares, req, res, handler) {
713
+ await executePipeline(
714
+ middlewares.map(
715
+ (middleware) => (next) => middleware(req, res, () => next())
716
+ ),
717
+ () => handler(req, res)
718
+ );
719
+ }
720
+ /**
721
+ * Applies framework-default headers before any route logic runs.
722
+ *
723
+ * @param {LithiaResponse} res - Response wrapper mutated with initial
724
+ * framework headers.
725
+ */
726
+ setInitialHeaders(res) {
727
+ res.setHeader("X-Powered-By", "Lithia");
728
+ }
729
+ /**
730
+ * Logs the completed HTTP request when request logging is enabled.
731
+ *
732
+ * The status code is color-coded according to its category and the log is
733
+ * emitted only after the request has reached a terminal state.
734
+ *
735
+ * @param {LithiaRequest} req - Request wrapper used for method and pathname
736
+ * fields.
737
+ * @param {LithiaResponse} res - Response wrapper used for the final status
738
+ * code.
739
+ * @param {number} elapsed - Total request processing time in milliseconds.
740
+ */
741
+ logRequest(req, res, elapsed) {
742
+ if (!this.app.config.logging.requests) return;
743
+ const status = res.statusCode;
744
+ const colorFunc = status >= 500 ? red : status >= 400 ? yellow : green;
745
+ logger.info(
746
+ `${req.method} ${req.pathname} ${colorFunc(status.toString())} - ${elapsed.toFixed(2)}ms`
747
+ );
748
+ }
749
+ };
750
+ var LithiaResponse = class {
751
+ /**
752
+ * Creates a response wrapper for the current HTTP transaction.
753
+ *
754
+ * The wrapper binds a pass-through `on()` helper to the underlying Node.js
755
+ * response object so route-adjacent code can subscribe to response events
756
+ * without holding the raw `ServerResponse`.
757
+ *
758
+ * @param {ServerResponse} res - Raw Node.js response object associated with
759
+ * the current request.
760
+ */
761
+ constructor(res) {
762
+ this.res = res;
763
+ this.on = this.res.on.bind(this.res);
764
+ }
765
+ _ended = false;
766
+ _cookies = [];
767
+ on;
768
+ /**
769
+ * Returns the current HTTP status code.
770
+ *
771
+ * @returns {number} Status code currently assigned to the underlying
772
+ * response.
773
+ */
774
+ get statusCode() {
775
+ return this.res.statusCode;
776
+ }
777
+ /**
778
+ * Sets the HTTP status code for the response.
779
+ *
780
+ * This mutates the underlying response only while it is still active.
781
+ *
782
+ * @param {number} status - HTTP status code to assign before the response is
783
+ * sent.
784
+ * @returns {this} The current response wrapper for fluent chaining.
785
+ * @throws {Error} Thrown when the response has already ended or when the
786
+ * supplied status code falls outside the valid HTTP range.
787
+ */
788
+ status(status) {
789
+ this.ensureActive();
790
+ if (status < 100 || status > 599) {
791
+ throw new Error(`Invalid HTTP status code: ${status}`);
792
+ }
793
+ this.res.statusCode = status;
794
+ return this;
795
+ }
796
+ /**
797
+ * Returns the currently assigned response headers.
798
+ *
799
+ * @returns {Readonly<OutgoingHttpHeaders>} Snapshot of the headers currently
800
+ * stored on the underlying response.
801
+ */
802
+ headers() {
803
+ return this.res.getHeaders();
804
+ }
805
+ /**
806
+ * Sets multiple response headers at once.
807
+ *
808
+ * @param {OutgoingHttpHeaders} headers - Header entries to assign to the
809
+ * response before it is sent.
810
+ * @returns {this} The current response wrapper for fluent chaining.
811
+ * @throws {Error} Thrown when the response has already ended.
812
+ */
813
+ setHeaders(headers) {
814
+ this.ensureActive();
815
+ Object.entries(headers).forEach(([key, value]) => {
816
+ this.res.setHeader(key, value);
817
+ });
818
+ return this;
819
+ }
820
+ /**
821
+ * Sets a single response header.
822
+ *
823
+ * @param {string} name - Header name to create or overwrite.
824
+ * @param {string | number | string[]} value - Header value written to the
825
+ * underlying response.
826
+ * @returns {this} The current response wrapper for fluent chaining.
827
+ * @throws {Error} Thrown when the response has already ended.
828
+ */
829
+ setHeader(name, value) {
830
+ this.ensureActive();
831
+ this.res.setHeader(name, value);
832
+ return this;
833
+ }
834
+ /**
835
+ * Removes a response header.
836
+ *
837
+ * @param {string} name - Header name to remove.
838
+ * @returns {this} The current response wrapper for fluent chaining.
839
+ * @throws {Error} Thrown when the response has already ended.
840
+ */
841
+ removeHeader(name) {
842
+ this.ensureActive();
843
+ this.res.removeHeader(name);
844
+ return this;
845
+ }
846
+ /**
847
+ * Queues a cookie to be written when the response is sent.
848
+ *
849
+ * Cookies are accumulated in memory and serialized only when a terminal
850
+ * response method flushes headers.
851
+ *
852
+ * @param {string} name - Cookie name.
853
+ * @param {string} value - Cookie value.
854
+ * @param {CookieOptions} [options={}] - Cookie serialization options.
855
+ * @returns {this} The current response wrapper for fluent chaining.
856
+ * @throws {Error} Thrown when the response has already ended.
857
+ */
858
+ cookie(name, value, options = {}) {
859
+ this.ensureActive();
860
+ this._cookies.push({ name, value, options });
861
+ return this;
862
+ }
863
+ /**
864
+ * Clears a cookie by expiring it immediately.
865
+ *
866
+ * @param {string} name - Cookie name to expire.
867
+ * @param {CookieOptions} [options={}] - Additional cookie attributes that
868
+ * must match the original cookie scope.
869
+ * @returns {this} The current response wrapper for fluent chaining.
870
+ */
871
+ clearCookie(name, options = {}) {
872
+ return this.cookie(name, "", { ...options, expires: /* @__PURE__ */ new Date(0) });
873
+ }
874
+ /**
875
+ * Sends a response body using a best-effort content type.
876
+ *
877
+ * The method flushes pending cookies before writing, chooses a default
878
+ * content type when none is set, and treats plain objects as JSON by
879
+ * delegating to `json()`. Calling `send()` is a terminal operation for the
880
+ * response lifecycle.
881
+ *
882
+ * @param {unknown} [data] - Response payload to send.
883
+ * @throws {Error} Thrown when the response has already ended.
884
+ */
885
+ send(data) {
886
+ this.applyPendingCookies();
887
+ this.ensureActive();
888
+ try {
889
+ if (data === void 0 || data === null) {
890
+ this.end();
891
+ return;
892
+ }
893
+ if (Buffer.isBuffer(data)) {
894
+ if (!this.res.getHeader("Content-Type")) {
895
+ this.setHeader("Content-Type", "application/octet-stream");
896
+ }
897
+ this.res.end(data);
898
+ } else if (typeof data === "object") {
899
+ this.json(data);
900
+ return;
901
+ } else {
902
+ if (!this.res.getHeader("Content-Type")) {
903
+ this.setHeader("Content-Type", "text/plain; charset=utf-8");
904
+ }
905
+ this.res.end(String(data));
906
+ }
907
+ } finally {
908
+ this._ended = true;
909
+ }
910
+ }
911
+ /**
912
+ * Sends a JSON response.
913
+ *
914
+ * Pending cookies are flushed before serialization. If JSON serialization
915
+ * throws, the method logs the failure and falls back to a `500 Internal
916
+ * Server Error` response body.
917
+ *
918
+ * @param {object} obj - Plain object to serialize as JSON.
919
+ */
920
+ json(obj) {
921
+ this.applyPendingCookies();
922
+ this.ensureActive();
923
+ try {
924
+ const body = JSON.stringify(obj);
925
+ this.res.setHeader("Content-Type", "application/json; charset=utf-8");
926
+ this.res.end(body);
927
+ } catch (err) {
928
+ logger.error("Failed to serialize JSON response:", err);
929
+ this.res.statusCode = 500;
930
+ this.res.end("Internal Server Error");
931
+ } finally {
932
+ this._ended = true;
933
+ }
934
+ }
935
+ /**
936
+ * Sends a redirect response.
937
+ *
938
+ * This sets the status code, writes the `Location` header, and then ends the
939
+ * response.
940
+ *
941
+ * @param {string} url - Redirect target written to the `Location` header.
942
+ * @param {number} [status=302] - Redirect status code.
943
+ */
944
+ redirect(url, status = 302) {
945
+ this.status(status).setHeader("Location", url).end();
946
+ }
947
+ /**
948
+ * Ends the response without sending additional data.
949
+ *
950
+ * Pending cookies are flushed before the underlying response is closed.
951
+ *
952
+ * @throws {Error} Thrown when the response has already ended.
953
+ */
954
+ end() {
955
+ this.applyPendingCookies();
956
+ this.ensureActive();
957
+ this.res.end();
958
+ this._ended = true;
959
+ }
960
+ /**
961
+ * Streams a file to the client.
962
+ *
963
+ * The method resolves the final path, verifies that it points to a regular
964
+ * file, sets `Content-Length`, flushes pending cookies, and pipes the file
965
+ * stream into the underlying response. Missing files and stream failures fall
966
+ * back to a `404` JSON error payload.
967
+ *
968
+ * @param {string} filePath - File path to stream. When `opts.root` is set, it
969
+ * is resolved relative to that root.
970
+ * @param {{ root?: string }} [opts={}] - Optional root directory used to
971
+ * resolve relative file paths.
972
+ */
973
+ sendFile(filePath, opts = {}) {
974
+ this.ensureActive();
975
+ try {
976
+ const fullPath = opts.root ? join(opts.root, filePath) : filePath;
977
+ const stats = statSync(fullPath);
978
+ if (!stats.isFile()) throw new Error("Target is not a file");
979
+ this.setHeader("Content-Length", String(stats.size));
980
+ const stream = createReadStream(fullPath);
981
+ this.applyPendingCookies();
982
+ stream.pipe(this.res);
983
+ stream.on("error", () => {
984
+ this.status(404).send({ error: "File not found" });
985
+ });
986
+ } catch {
987
+ this.status(404).send({ error: "File not found" });
988
+ } finally {
989
+ this._ended = true;
990
+ }
991
+ }
992
+ /**
993
+ * Serializes queued cookies into the response headers and clears the queue.
994
+ *
995
+ * Existing `Set-Cookie` headers are preserved and extended so multiple
996
+ * middleware and handler calls can contribute cookies before the response is
997
+ * finalized.
998
+ */
999
+ applyPendingCookies() {
1000
+ if (this._cookies.length === 0) return;
1001
+ const existing = this.res.getHeader("Set-Cookie") || [];
1002
+ const serialized = this._cookies.map(
1003
+ (cookie) => serialize(cookie.name, cookie.value, cookie.options)
1004
+ );
1005
+ this.res.setHeader("Set-Cookie", [...existing, ...serialized]);
1006
+ this._cookies = [];
1007
+ }
1008
+ /**
1009
+ * Ensures the response has not already been finalized.
1010
+ *
1011
+ * @throws {Error} Thrown when a terminal response method has already sent or
1012
+ * ended the response.
1013
+ */
1014
+ ensureActive() {
1015
+ if (this._ended) {
1016
+ throw new Error("Response has already been sent.");
1017
+ }
1018
+ }
1019
+ };
1020
+ function handleEventError(environment, socket, eventName, err) {
1021
+ const error = err instanceof LithiaClientError ? err : new InternalServerError(
1022
+ "An internal server error occurred during event processing.",
1023
+ err
1024
+ );
1025
+ const isProd = environment === "production";
1026
+ const statusCode = error.statusCode || 500;
1027
+ const digest = produceDigest(err);
1028
+ const message = isProd && statusCode >= 500 ? "Internal Server Error" : error.message;
1029
+ socket.emit("error", {
1030
+ error: {
1031
+ statusCode,
1032
+ message,
1033
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1034
+ digest,
1035
+ event: eventName,
1036
+ details: error.details
1037
+ }
1038
+ });
1039
+ if (statusCode >= 500) {
1040
+ logger.error(`Digest: ${red(digest)}`);
1041
+ logger.info(`Event: ${eventName}`);
1042
+ logger.info(`Socket ID: ${socket.id}`);
1043
+ logger.info(err.stack || err);
1044
+ }
1045
+ }
1046
+
1047
+ // src/transport/socket/event-pipeline.ts
1048
+ var LithiaEventProcessor = class {
1049
+ /**
1050
+ * Creates an event processor bound to a running Lithia app instance.
1051
+ *
1052
+ * @param {LithiaApp} app - Runtime app state that provides environment and
1053
+ * global event middleware.
1054
+ */
1055
+ constructor(app) {
1056
+ this.app = app;
1057
+ }
1058
+ /**
1059
+ * Processes a single discovered realtime event.
1060
+ *
1061
+ * @param {Socket} socket - Active socket that triggered the event.
1062
+ * @param {Event} event - Discovered event manifest selected by the socket
1063
+ * transport.
1064
+ * @param {any} [data] - Event payload forwarded from Socket.IO.
1065
+ * @returns {Promise<void>} Resolves after middleware and the event handler
1066
+ * complete, or after the error handler emits a failure payload.
1067
+ */
1068
+ async process(socket, event, data) {
1069
+ try {
1070
+ const module = await loadModule(event.filePath);
1071
+ const pipeline = [
1072
+ ...this.app.globalEventMiddlewares,
1073
+ ...module.middlewares || []
1074
+ ];
1075
+ await executePipeline(
1076
+ pipeline.map(
1077
+ (middleware) => (next) => middleware(socket, () => next())
1078
+ ),
1079
+ () => module.default(socket, data)
1080
+ );
1081
+ } catch (error) {
1082
+ handleEventError(this.app.environment, socket, event.name, error);
1083
+ }
1084
+ }
1085
+ };
1086
+ var CONTEXT_GLOBAL_KEY = /* @__PURE__ */ Symbol.for("lithia.event_context.v1");
1087
+ function getGlobalStore() {
1088
+ const globalAny = globalThis;
1089
+ if (!globalAny[CONTEXT_GLOBAL_KEY]) {
1090
+ globalAny[CONTEXT_GLOBAL_KEY] = new AsyncLocalStorage();
1091
+ }
1092
+ return globalAny[CONTEXT_GLOBAL_KEY];
1093
+ }
1094
+ var eventContextStore = getGlobalStore();
1095
+
1096
+ // src/transport/socket/socket-server.ts
1097
+ var LithiaSocketTransport = class {
1098
+ /**
1099
+ * Creates the Socket.IO transport and registers connection listeners.
1100
+ *
1101
+ * The constructor configures Socket.IO CORS from the app's HTTP settings and
1102
+ * immediately wires connection, disconnect, and custom event dispatch into
1103
+ * the Lithia event processor.
1104
+ *
1105
+ * @param {LithiaApp} app - Running app instance that provides config and the
1106
+ * discovered event manifest list.
1107
+ * @param {HttpServer | HttpsServer} httpServer - Underlying HTTP server used
1108
+ * as the Socket.IO transport base.
1109
+ * @param {LithiaEventProcessor} processor - Event processor responsible for
1110
+ * executing event middleware and handlers.
1111
+ */
1112
+ constructor(app, httpServer, processor) {
1113
+ this.app = app;
1114
+ this.processor = processor;
1115
+ const { cors } = this.app.config.http;
1116
+ this.io = new Server(httpServer, {
1117
+ cors: {
1118
+ origin: cors.origin,
1119
+ methods: cors.methods,
1120
+ credentials: cors.credentials
1121
+ }
1122
+ });
1123
+ this.io.on("connection", (socket) => {
1124
+ const eventMap = new Map(
1125
+ this.app.events.map((event) => [event.name, event])
1126
+ );
1127
+ const connectionEvent = eventMap.get("connection");
1128
+ if (connectionEvent) this.dispatch(socket, connectionEvent);
1129
+ socket.on("disconnect", (...args) => {
1130
+ const disconnectEvent = eventMap.get("disconnect");
1131
+ if (disconnectEvent) this.dispatch(socket, disconnectEvent, ...args);
1132
+ });
1133
+ socket.onAny((eventName, ...args) => {
1134
+ const customEvent = this.app.events.find(
1135
+ (event) => event.name === eventName
1136
+ );
1137
+ if (customEvent) this.dispatch(socket, customEvent, ...args);
1138
+ });
1139
+ });
1140
+ }
1141
+ io;
1142
+ /**
1143
+ * Returns the underlying Socket.IO server instance.
1144
+ *
1145
+ * @returns {SocketServer} Active Socket.IO server.
1146
+ */
1147
+ get server() {
1148
+ return this.io;
1149
+ }
1150
+ /**
1151
+ * Closes the Socket.IO transport and disconnects active sockets.
1152
+ *
1153
+ * @returns {Promise<void>} Resolves after Socket.IO finishes shutting down.
1154
+ */
1155
+ async close() {
1156
+ await this.io.close();
1157
+ }
1158
+ /**
1159
+ * Dispatches a discovered Lithia event inside the correct runtime contexts.
1160
+ *
1161
+ * The call first enters the app-level async context and then the event
1162
+ * context store so event hooks can access the active socket, payload, and
1163
+ * event metadata during middleware and handler execution.
1164
+ *
1165
+ * @param {Socket} socket - Active socket associated with the dispatch.
1166
+ * @param {Event} event - Discovered event manifest to execute.
1167
+ * @param {any[]} args - Raw Socket.IO event arguments forwarded to the event
1168
+ * processor.
1169
+ */
1170
+ dispatch(socket, event, ...args) {
1171
+ void this.app.runWithContext(async () => {
1172
+ const context = {
1173
+ data: args[0],
1174
+ socket,
1175
+ event
1176
+ };
1177
+ eventContextStore.run(context, async () => {
1178
+ await this.processor.process(socket, event, ...args);
1179
+ });
1180
+ });
1181
+ }
1182
+ };
1183
+
1184
+ // src/transport/server.ts
1185
+ var LithiaServer = class {
1186
+ /**
1187
+ * Creates the transport server for a running app instance.
1188
+ *
1189
+ * @param {LithiaApp} app - Runtime app that provides config, route/event
1190
+ * manifests, and async context helpers.
1191
+ */
1192
+ constructor(app) {
1193
+ this.app = app;
1194
+ this.requestProcessor = new LithiaRequestProcessor(this.app);
1195
+ this.eventProcessor = new LithiaEventProcessor(this.app);
1196
+ this._httpServer = this.createServer();
1197
+ this.socketTransport = new LithiaSocketTransport(
1198
+ this.app,
1199
+ this._httpServer,
1200
+ this.eventProcessor
1201
+ );
1202
+ }
1203
+ _httpServer;
1204
+ requestProcessor;
1205
+ eventProcessor;
1206
+ socketTransport;
1207
+ _activeRequests = /* @__PURE__ */ new Set();
1208
+ /**
1209
+ * Returns the set of currently open TCP connections tracked by the server.
1210
+ *
1211
+ * The set is updated from the Node.js `"connection"` event and is used during
1212
+ * shutdown to forcefully destroy lingering sockets after the server stops
1213
+ * accepting new traffic.
1214
+ *
1215
+ * @returns {Set<ActiveRequest>} Tracked open TCP sockets.
1216
+ */
1217
+ get activeRequests() {
1218
+ return this._activeRequests;
1219
+ }
1220
+ /**
1221
+ * Returns the underlying Node.js HTTP or HTTPS server instance.
1222
+ *
1223
+ * @returns {HttpServer | HttpsServer} Active low-level server instance.
1224
+ */
1225
+ get httpServer() {
1226
+ return this._httpServer;
1227
+ }
1228
+ /**
1229
+ * Returns the Socket.IO server attached to the transport.
1230
+ *
1231
+ * @returns {ReturnType<LithiaSocketTransport["server"]>} Active Socket.IO
1232
+ * server instance.
1233
+ */
1234
+ get socketServer() {
1235
+ return this.socketTransport.server;
1236
+ }
1237
+ /**
1238
+ * Starts listening on the configured host and port.
1239
+ *
1240
+ * Repeated calls are idempotent while the underlying server is already
1241
+ * listening. The returned promise resolves on the low-level `"listening"`
1242
+ * event and rejects on the first startup error emitted by Node.js.
1243
+ *
1244
+ * @returns {Promise<void>} Resolves after the transport begins accepting
1245
+ * connections.
1246
+ */
1247
+ async listen() {
1248
+ const { port, host } = this.app.config.http;
1249
+ return new Promise((resolve, reject) => {
1250
+ if (this._httpServer.listening) {
1251
+ return resolve();
1252
+ }
1253
+ const handleError = (error) => {
1254
+ this._httpServer.off("listening", handleListening);
1255
+ reject(error);
1256
+ };
1257
+ const handleListening = () => {
1258
+ this._httpServer.off("error", handleError);
1259
+ resolve();
1260
+ };
1261
+ this._httpServer.once("error", handleError);
1262
+ this._httpServer.once("listening", handleListening);
1263
+ this._httpServer.listen(port, host);
1264
+ });
1265
+ }
1266
+ /**
1267
+ * Closes the Socket.IO transport, stops accepting HTTP traffic, and destroys
1268
+ * any remaining active connections.
1269
+ *
1270
+ * Socket.IO shutdown is attempted first so realtime traffic stops before the
1271
+ * underlying HTTP server is closed. Remaining sockets are then destroyed to
1272
+ * avoid hanging shutdown on keep-alive connections.
1273
+ *
1274
+ * @returns {Promise<void>} Resolves after the transport has been shut down and
1275
+ * tracked connections have been cleared.
1276
+ */
1277
+ async close() {
1278
+ await this.socketTransport.close().catch((error) => {
1279
+ logger.error("Failed to close Socket.IO transport cleanly:", error);
1280
+ });
1281
+ if (!this._httpServer.listening) {
1282
+ for (const socket of this._activeRequests) {
1283
+ socket.destroy();
1284
+ }
1285
+ this._activeRequests.clear();
1286
+ return;
1287
+ }
1288
+ await new Promise((resolve, reject) => {
1289
+ this._httpServer.close((error) => {
1290
+ if (error) return reject(error);
1291
+ resolve();
1292
+ });
1293
+ });
1294
+ for (const socket of this._activeRequests) {
1295
+ socket.destroy();
1296
+ }
1297
+ this._activeRequests.clear();
1298
+ }
1299
+ /**
1300
+ * Creates the underlying HTTP or HTTPS server instance.
1301
+ *
1302
+ * The returned server also tracks active sockets so shutdown can destroy
1303
+ * lingering keep-alive connections after `close()`.
1304
+ *
1305
+ * @returns {HttpServer | HttpsServer} Low-level server configured for the
1306
+ * current app transport.
1307
+ */
1308
+ createServer() {
1309
+ const handler = this.handleRequest();
1310
+ const sslConfig = this.app.config.http.ssl;
1311
+ const server = sslConfig ? createServer(sslConfig, handler) : createServer$1(handler);
1312
+ server.on("connection", (socket) => {
1313
+ this._activeRequests.add(socket);
1314
+ socket.on("close", () => this._activeRequests.delete(socket));
1315
+ });
1316
+ return server;
1317
+ }
1318
+ /**
1319
+ * Creates the low-level Node request handler and bridges it into Lithia's
1320
+ * request context and request pipeline.
1321
+ *
1322
+ * Each incoming Node.js request is wrapped into `LithiaRequest` and
1323
+ * `LithiaResponse`, associated with a route context store, and then processed
1324
+ * inside the app-level async context so route hooks can access the active
1325
+ * request state.
1326
+ *
1327
+ * If request initialization itself fails before the normal request pipeline
1328
+ * takes over, the handler falls back to a minimal JSON `500` response.
1329
+ *
1330
+ * @returns {(req: IncomingMessage, res: ServerResponse) => void} Node.js
1331
+ * request listener bound to the current app instance.
1332
+ */
1333
+ handleRequest() {
1334
+ return (req, res) => {
1335
+ try {
1336
+ const lithiaReq = new LithiaRequest(req, {
1337
+ maxBodySize: this.app.config.http.maxBodySize
1338
+ });
1339
+ const lithiaRes = new LithiaResponse(res);
1340
+ const routeContext = {
1341
+ req: lithiaReq,
1342
+ res: lithiaRes,
1343
+ socketServer: this.socketTransport.server
1344
+ };
1345
+ void this.app.runWithContext(async () => {
1346
+ routeContextStore.run(routeContext, async () => {
1347
+ await this.requestProcessor.process(lithiaReq, lithiaRes);
1348
+ });
1349
+ });
1350
+ } catch (error) {
1351
+ logger.error("Failed to initialize request context:", error);
1352
+ if (!res.headersSent) {
1353
+ res.statusCode = 500;
1354
+ res.setHeader("Content-Type", "application/json");
1355
+ }
1356
+ if (!res.writableEnded) {
1357
+ res.end(
1358
+ JSON.stringify({
1359
+ error: {
1360
+ statusCode: 500,
1361
+ message: "Failed to initialize request handling.",
1362
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1363
+ }
1364
+ })
1365
+ );
1366
+ }
1367
+ }
1368
+ };
1369
+ }
1370
+ };
1371
+
1372
+ // src/runtime/app/dependency-container.ts
1373
+ var DependencyContainer = class {
1374
+ dependencies = /* @__PURE__ */ new Map();
1375
+ /**
1376
+ * Stores a dependency by its injection key.
1377
+ *
1378
+ * @param {any} key - Injection token used to identify the dependency.
1379
+ * @param {T} value - Dependency instance stored under `key`.
1380
+ */
1381
+ set(key, value) {
1382
+ this.dependencies.set(key, value);
1383
+ }
1384
+ /**
1385
+ * Returns whether a dependency has been registered.
1386
+ *
1387
+ * @param {any} key - Injection token to test.
1388
+ * @returns {boolean} `true` when the container includes `key`.
1389
+ */
1390
+ has(key) {
1391
+ return this.dependencies.has(key);
1392
+ }
1393
+ /**
1394
+ * Resolves a dependency by key.
1395
+ *
1396
+ * @param {any} key - Injection token to resolve.
1397
+ * @returns {T | undefined} Registered dependency instance, or `undefined`
1398
+ * when the key is absent.
1399
+ */
1400
+ get(key) {
1401
+ return this.dependencies.get(key);
1402
+ }
1403
+ /**
1404
+ * Returns an immutable snapshot of the container contents.
1405
+ *
1406
+ * The returned `Map` is detached from future container mutations, which lets
1407
+ * the runtime execute work against a stable dependency view.
1408
+ *
1409
+ * @returns {Map<any, any>} Shallow copy of the current container contents.
1410
+ */
1411
+ snapshot() {
1412
+ return new Map(this.dependencies);
1413
+ }
1414
+ /**
1415
+ * Returns the underlying mutable container.
1416
+ *
1417
+ * Mutating the returned `Map` mutates the container itself.
1418
+ *
1419
+ * @returns {Map<any, any>} Backing dependency map used by the runtime.
1420
+ */
1421
+ mutable() {
1422
+ return this.dependencies;
1423
+ }
1424
+ };
1425
+
1426
+ // src/runtime/app/middleware-registry.ts
1427
+ var MiddlewareRegistry = class {
1428
+ routeMiddlewares = [];
1429
+ eventMiddlewares = [];
1430
+ /**
1431
+ * Registers a middleware for either the route or event pipeline.
1432
+ *
1433
+ * Route middleware is appended to the global HTTP pipeline. Event
1434
+ * middleware is appended to the global socket event pipeline.
1435
+ *
1436
+ * @param {"route" | "event"} context - Pipeline that should receive the
1437
+ * middleware.
1438
+ * @param {TRouteMiddleware | TEventMiddleware} middleware - Middleware
1439
+ * instance appended to the selected pipeline.
1440
+ */
1441
+ use(context, middleware) {
1442
+ if (context === "route") {
1443
+ this.routeMiddlewares.push(middleware);
1444
+ return;
1445
+ }
1446
+ this.eventMiddlewares.push(middleware);
1447
+ }
1448
+ /**
1449
+ * Returns the registered global route middlewares.
1450
+ *
1451
+ * The returned array is the live registry array and preserves registration
1452
+ * order.
1453
+ *
1454
+ * @returns {TRouteMiddleware[]} Registered route middlewares.
1455
+ */
1456
+ getRoutes() {
1457
+ return this.routeMiddlewares;
1458
+ }
1459
+ /**
1460
+ * Returns the registered global event middlewares.
1461
+ *
1462
+ * The returned array is the live registry array and preserves registration
1463
+ * order.
1464
+ *
1465
+ * @returns {TEventMiddleware[]} Registered event middlewares.
1466
+ */
1467
+ getEvents() {
1468
+ return this.eventMiddlewares;
1469
+ }
1470
+ };
1471
+ async function fileExists(filePath) {
1472
+ return await access(filePath).then(() => true).catch(() => false);
1473
+ }
1474
+ async function fileHasMeaningfulModuleContent(filePath) {
1475
+ const source = await readFile(filePath, "utf8");
1476
+ const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
1477
+ return withoutComments.trim().length > 0;
1478
+ }
1479
+
1480
+ // src/runtime/app/server-bootstrap.ts
1481
+ async function resolveServerBootstrapPath(outDir, cwd = process.cwd()) {
1482
+ const candidates = [
1483
+ path.join(cwd, outDir, "app", "server.js"),
1484
+ path.join(cwd, outDir, "app", "server.mjs")
1485
+ ];
1486
+ for (const candidate of candidates) {
1487
+ if (await fileExists(candidate) && await fileHasMeaningfulModuleContent(candidate)) {
1488
+ return candidate;
1489
+ }
1490
+ }
1491
+ return null;
1492
+ }
1493
+ async function loadServerBootstrap(filePath) {
1494
+ const mod = await loadModule(filePath);
1495
+ return mod.default;
1496
+ }
1497
+ function normalizeServerBootstrapCleanup(value) {
1498
+ if (value === void 0) return null;
1499
+ if (typeof value !== "function") {
1500
+ throw new Error(
1501
+ "`app/server.ts` must return either nothing or a cleanup function."
1502
+ );
1503
+ }
1504
+ return async () => {
1505
+ await value();
1506
+ };
1507
+ }
1508
+ var TaskScheduler = class {
1509
+ /**
1510
+ * Creates a scheduler for the task manifest loaded into the current app
1511
+ * worker.
1512
+ *
1513
+ * @param {TaskCore[]} tasks - Task manifest entries available to the app
1514
+ * runtime.
1515
+ */
1516
+ constructor(tasks) {
1517
+ this.tasks = tasks;
1518
+ }
1519
+ cronJobs = [];
1520
+ /**
1521
+ * Starts all CRON tasks and invokes the callback when a schedule fires.
1522
+ *
1523
+ * Each CRON task must already include a validated `schedule` string. When a
1524
+ * schedule triggers, the scheduler invokes `onTrigger` with the original
1525
+ * task metadata.
1526
+ *
1527
+ * @param {(task: TaskCore) => void} onTrigger - Callback invoked when a CRON
1528
+ * task schedule fires.
1529
+ * @throws {Error} Throws when a CRON task is missing its resolved
1530
+ * `schedule`.
1531
+ */
1532
+ start(onTrigger) {
1533
+ const cronTasks = this.tasks.filter((task) => task.trigger === "CRON");
1534
+ for (const task of cronTasks) {
1535
+ if (!task.schedule) {
1536
+ throw new Error(
1537
+ `CRON task '${task.id}' is missing a resolved schedule.`
1538
+ );
1539
+ }
1540
+ const job = cron.schedule(task.schedule, () => {
1541
+ onTrigger(task);
1542
+ });
1543
+ this.cronJobs.push(job);
1544
+ }
1545
+ }
1546
+ /**
1547
+ * Stops and destroys all registered CRON jobs.
1548
+ *
1549
+ * This is used during app shutdown to ensure scheduled tasks no longer fire
1550
+ * after the runtime begins closing.
1551
+ */
1552
+ stop() {
1553
+ for (const job of this.cronJobs.splice(0)) {
1554
+ job.stop();
1555
+ if ("destroy" in job && typeof job.destroy === "function") {
1556
+ job.destroy();
1557
+ }
1558
+ }
1559
+ }
1560
+ };
1561
+
1562
+ // src/runtime/app/app-runtime.ts
1563
+ var LithiaApp = class {
1564
+ _environment;
1565
+ _config;
1566
+ _routes;
1567
+ _events;
1568
+ _tasks;
1569
+ _isFirstApp;
1570
+ dependencies = new DependencyContainer();
1571
+ middlewares = new MiddlewareRegistry();
1572
+ serverBootstrapCleanup = null;
1573
+ _server;
1574
+ taskScheduler;
1575
+ /**
1576
+ * Creates the app runtime from the worker payload prepared by the Lithia CLI.
1577
+ *
1578
+ * The constructor reads routes, events, tasks, config, and environment from
1579
+ * `workerData`, then initializes the server and task scheduler that run
1580
+ * inside the worker thread.
1581
+ *
1582
+ * @throws {Error} Throws when the runtime is instantiated outside a
1583
+ * Lithia-managed worker thread.
1584
+ */
1585
+ constructor() {
1586
+ this.validateExecutionContext();
1587
+ this._config = workerData.config;
1588
+ this._routes = workerData.routes;
1589
+ this._events = workerData.events;
1590
+ this._tasks = workerData.tasks;
1591
+ this._environment = workerData.environment;
1592
+ this._isFirstApp = workerData.isFirstApp;
1593
+ this._server = new LithiaServer(this);
1594
+ this.taskScheduler = new TaskScheduler(this._tasks);
1595
+ }
1596
+ /**
1597
+ * Returns the resolved application configuration for this runtime instance.
1598
+ */
1599
+ get config() {
1600
+ return this._config;
1601
+ }
1602
+ /**
1603
+ * Returns the environment mode assigned to this app worker.
1604
+ */
1605
+ get environment() {
1606
+ return this._environment;
1607
+ }
1608
+ /**
1609
+ * Returns the discovered route manifest loaded into this app worker.
1610
+ */
1611
+ get routes() {
1612
+ return this._routes;
1613
+ }
1614
+ /**
1615
+ * Returns the discovered socket event manifest loaded into this app worker.
1616
+ */
1617
+ get events() {
1618
+ return this._events;
1619
+ }
1620
+ /**
1621
+ * Returns the discovered task manifest loaded into this app worker.
1622
+ *
1623
+ * Cron-backed tasks follow the conventions described in
1624
+ * [Async Tasks](https://lithiajs.org/docs/latest/async-tasks).
1625
+ */
1626
+ get tasks() {
1627
+ return this._tasks;
1628
+ }
1629
+ /**
1630
+ * Returns the global HTTP middlewares registered for every route pipeline.
1631
+ */
1632
+ get globalRouteMiddlewares() {
1633
+ return this.middlewares.getRoutes();
1634
+ }
1635
+ /**
1636
+ * Returns the global socket middlewares registered for every event pipeline.
1637
+ */
1638
+ get globalEventMiddlewares() {
1639
+ return this.middlewares.getEvents();
1640
+ }
1641
+ /**
1642
+ * Returns whether this worker is the first app instance in the current
1643
+ * process lifecycle.
1644
+ *
1645
+ * Lithia uses this flag to limit one-time logs and similar side effects to a
1646
+ * single runtime instance.
1647
+ */
1648
+ get isFirstApp() {
1649
+ return this._isFirstApp;
1650
+ }
1651
+ /**
1652
+ * Runs work inside an immutable snapshot of the app dependency container.
1653
+ *
1654
+ * Use this for request handling or background work that should only resolve
1655
+ * dependencies that were already registered during bootstrap.
1656
+ *
1657
+ * @param {() => Promise<T>} fn - Async work to execute inside the Lithia
1658
+ * context.
1659
+ * @returns {Promise<T>} The value resolved by `fn`.
1660
+ */
1661
+ runWithContext(fn) {
1662
+ return this.runWithContainer(this.dependencies.snapshot(), fn);
1663
+ }
1664
+ /**
1665
+ * Runs work inside the mutable app dependency container.
1666
+ *
1667
+ * This is primarily used during app bootstrap, when new dependencies may be
1668
+ * registered with `provide()`.
1669
+ *
1670
+ * This method is typically used while running `src/app/server.ts`. See
1671
+ * [Project Structure](https://lithiajs.org/docs/latest/project-structure)
1672
+ * and [Deploying](https://lithiajs.org/docs/latest/deploying).
1673
+ *
1674
+ * @param {() => Promise<T>} fn - Async work to execute inside the mutable
1675
+ * Lithia context.
1676
+ * @returns {Promise<T>} The value resolved by `fn`.
1677
+ */
1678
+ runWithMutableContext(fn) {
1679
+ return this.runWithContainer(this.dependencies.mutable(), fn);
1680
+ }
1681
+ /**
1682
+ * Runs work inside a Lithia context backed by the provided dependency
1683
+ * container.
1684
+ *
1685
+ * @param {Map<any, any>} container - Dependency container exposed to the
1686
+ * current context.
1687
+ * @param {() => Promise<T>} fn - Async work to execute inside the context.
1688
+ * @returns {Promise<T>} The value resolved by `fn`.
1689
+ */
1690
+ runWithContainer(container, fn) {
1691
+ const context = {
1692
+ container,
1693
+ config: this.config
1694
+ };
1695
+ return runInLithiaContext(context, fn);
1696
+ }
1697
+ /**
1698
+ * Registers a dependency in the app container.
1699
+ *
1700
+ * Dependencies registered here become available through the Lithia context
1701
+ * for routes, events, tasks, and startup hooks.
1702
+ *
1703
+ * @param {InjectionKey<T>} key - Token used to store and resolve the
1704
+ * dependency.
1705
+ * @param {T} value - Dependency instance associated with `key`.
1706
+ */
1707
+ provide(key, value) {
1708
+ this.dependencies.set(key, value);
1709
+ }
1710
+ /**
1711
+ * Registers a global middleware for routes or events.
1712
+ *
1713
+ * Route middlewares run for every HTTP route. Event middlewares run for
1714
+ * every socket event.
1715
+ *
1716
+ * @param {"route" | "event"} context - Middleware pipeline that should
1717
+ * receive the registration.
1718
+ * @param {K extends "route" ? RouteMiddleware : EventMiddleware} middleware
1719
+ * - Middleware implementation to append to the selected global pipeline.
1720
+ */
1721
+ use(context, middleware) {
1722
+ this.middlewares.use(context, middleware);
1723
+ }
1724
+ /**
1725
+ * Starts the app runtime.
1726
+ *
1727
+ * The startup sequence is:
1728
+ * 1. run optional `app/server.ts`
1729
+ * 2. start the HTTP/socket server
1730
+ * 3. register CRON-backed tasks
1731
+ * 4. announce readiness
1732
+ *
1733
+ * `app/server.ts` participates in the startup lifecycle described in
1734
+ * [Deploying](https://lithiajs.org/docs/latest/deploying) and
1735
+ * [Project Structure](https://lithiajs.org/docs/latest/project-structure).
1736
+ *
1737
+ * @returns {Promise<void>} Resolves after the server is listening and cron
1738
+ * tasks have been registered.
1739
+ * @throws {unknown} Rethrows any startup failure from bootstrap loading,
1740
+ * server startup, or task registration.
1741
+ */
1742
+ async start() {
1743
+ this.executeOnce(() => logger.info("Starting Lithia server..."));
1744
+ try {
1745
+ await this.runServerBootstrapIfPresent();
1746
+ await this._server.listen();
1747
+ this.taskScheduler.start((task) => {
1748
+ parentPort?.postMessage({
1749
+ type: "invoke",
1750
+ taskId: task.id,
1751
+ async: true,
1752
+ executionId: randomUUID(),
1753
+ args: [],
1754
+ source: "CRON",
1755
+ attempt: 0
1756
+ });
1757
+ });
1758
+ this.executeOnce(
1759
+ () => logger.ready(`Lithia is ready on port ${this.config.http.port}`)
1760
+ );
1761
+ } catch (error) {
1762
+ this.executeOnce(() => logger.error("Failed to start Lithia server."));
1763
+ throw error;
1764
+ }
1765
+ }
1766
+ /**
1767
+ * Stops the app runtime and runs all registered cleanup hooks.
1768
+ *
1769
+ * The shutdown sequence runs the optional cleanup returned by
1770
+ * `src/app/server.ts`, stops cron scheduling, and then closes the server.
1771
+ *
1772
+ * @returns {Promise<void>} Resolves after cleanup hooks and server shutdown
1773
+ * finish.
1774
+ */
1775
+ async stop() {
1776
+ await this.runServerBootstrapCleanup();
1777
+ this.taskScheduler.stop();
1778
+ await this._server.close();
1779
+ }
1780
+ /**
1781
+ * Loads and runs `app/server.ts` when the file exists in the build output.
1782
+ *
1783
+ * If the bootstrap exports a cleanup callback, the callback is normalized
1784
+ * and stored for `stop()`.
1785
+ *
1786
+ * @returns {Promise<void>} Resolves after the bootstrap has finished.
1787
+ */
1788
+ async runServerBootstrapIfPresent() {
1789
+ const filePath = await resolveServerBootstrapPath(this.config.outDir);
1790
+ if (!filePath) return;
1791
+ const bootstrap2 = await loadServerBootstrap(filePath);
1792
+ const cleanup = await this.runWithMutableContext(() => bootstrap2());
1793
+ this.serverBootstrapCleanup = normalizeServerBootstrapCleanup(
1794
+ cleanup
1795
+ );
1796
+ }
1797
+ /**
1798
+ * Runs the cleanup returned by `app/server.ts`, if one was registered.
1799
+ *
1800
+ * Cleanup errors are logged and do not prevent the runtime from continuing
1801
+ * its shutdown flow.
1802
+ *
1803
+ * @returns {Promise<void>} Resolves after the cleanup callback completes or
1804
+ * is skipped.
1805
+ */
1806
+ async runServerBootstrapCleanup() {
1807
+ if (!this.serverBootstrapCleanup) return;
1808
+ try {
1809
+ await this.serverBootstrapCleanup();
1810
+ } catch (error) {
1811
+ logger.error("Failed to clean up app/server.ts bootstrap.", error);
1812
+ } finally {
1813
+ this.serverBootstrapCleanup = null;
1814
+ }
1815
+ }
1816
+ /**
1817
+ * Restricts one-time logs to the first app instance for a given lifecycle.
1818
+ *
1819
+ * This prevents duplicated startup and failure logs when multiple app
1820
+ * workers share the same lifecycle but only one instance should announce
1821
+ * framework-level state changes.
1822
+ *
1823
+ * @param {() => void} fn - Side effect to run only for the first app
1824
+ * instance.
1825
+ */
1826
+ executeOnce(fn) {
1827
+ if (this.isFirstApp) {
1828
+ fn();
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Ensures the app runtime only executes inside a Lithia-managed worker.
1833
+ *
1834
+ * The runtime depends on `workerData` prepared by the Lithia CLI and on the
1835
+ * worker messaging model used by the app worker entrypoint. Direct
1836
+ * instantiation on the main thread or inside an unrelated worker is not
1837
+ * supported.
1838
+ *
1839
+ * @throws {Error} Throws when the runtime runs on the main thread or inside
1840
+ * a worker not created by the Lithia CLI.
1841
+ */
1842
+ validateExecutionContext() {
1843
+ if (isMainThread) {
1844
+ throw new Error(
1845
+ "Execution Error: LithiaApp cannot be instantiated on the main thread. It must run within a Worker Thread."
1846
+ );
1847
+ }
1848
+ if (workerData?.managedBy !== "lithia") {
1849
+ throw new Error(
1850
+ "Compatibility Error: LithiaApp must be managed by the Lithia CLI. Independent execution is not supported."
1851
+ );
1852
+ }
1853
+ }
1854
+ };
1855
+
1856
+ // src/runtime/workers/app-worker.ts
1857
+ var isInitialized = false;
1858
+ async function bootstrap() {
1859
+ if (isInitialized) return;
1860
+ isInitialized = true;
1861
+ logger.debug("Initializing Lithia background worker...");
1862
+ const app = new LithiaApp();
1863
+ try {
1864
+ await app.start();
1865
+ parentPort?.postMessage({ type: "ready" });
1866
+ } catch (error) {
1867
+ try {
1868
+ await app.stop();
1869
+ } catch (stopError) {
1870
+ logger.error(
1871
+ "Failed to stop Lithia app cleanly after startup failure:",
1872
+ stopError
1873
+ );
1874
+ }
1875
+ const errorPayload = error instanceof LithiaError ? {
1876
+ name: error.name,
1877
+ message: error.message,
1878
+ context: error.context,
1879
+ stack: error.stack
1880
+ } : {
1881
+ name: error?.name ?? "UnknownWorkerError",
1882
+ message: String(error?.message ?? error),
1883
+ stack: error?.stack
1884
+ };
1885
+ parentPort?.postMessage({ type: "error", error: errorPayload });
1886
+ if (!(error instanceof LithiaError)) {
1887
+ throw error;
1888
+ }
1889
+ return;
1890
+ }
1891
+ const shutdown = async () => {
1892
+ try {
1893
+ await app.stop();
1894
+ } catch {
1895
+ } finally {
1896
+ process.exit(0);
1897
+ }
1898
+ };
1899
+ process.once("SIGINT", shutdown);
1900
+ process.once("SIGTERM", shutdown);
1901
+ }
1902
+ bootstrap().catch((error) => {
1903
+ logger.error("Fatal exception in Lithia worker:", error);
1904
+ process.exit(1);
1905
+ });
1906
+ //# sourceMappingURL=app-worker.mjs.map
1907
+ //# sourceMappingURL=app-worker.mjs.map