@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
package/dist/index.mjs ADDED
@@ -0,0 +1,856 @@
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+ import { randomUUID } from 'crypto';
3
+ import { parentPort } from 'worker_threads';
4
+ import busboy from 'busboy';
5
+ import { parse, serialize } from 'cookie';
6
+ import { statSync, createReadStream } from 'fs';
7
+ import { join } from 'path';
8
+ import { logger } from '@lithia-js/utils';
9
+
10
+ // src/config.ts
11
+ function defineConfig(config) {
12
+ return config;
13
+ }
14
+
15
+ // src/errors/base.ts
16
+ var LithiaError = class extends Error {
17
+ isLithiaError = true;
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = this.constructor.name;
21
+ Object.setPrototypeOf(this, new.target.prototype);
22
+ }
23
+ };
24
+ var LithiaClientError = class extends LithiaError {
25
+ constructor(message, statusCode, details) {
26
+ super(message);
27
+ this.statusCode = statusCode;
28
+ this.details = details;
29
+ }
30
+ timestamp = /* @__PURE__ */ new Date();
31
+ };
32
+ var LithiaEventError = class extends LithiaError {
33
+ constructor(message, eventName, details) {
34
+ super(message);
35
+ this.eventName = eventName;
36
+ this.details = details;
37
+ }
38
+ timestamp = /* @__PURE__ */ new Date();
39
+ };
40
+
41
+ // src/errors/app/client.ts
42
+ var BadRequestError = class extends LithiaClientError {
43
+ constructor(m, d) {
44
+ super(m, 400, d);
45
+ }
46
+ };
47
+ var UnauthorizedError = class extends LithiaClientError {
48
+ constructor(m, d) {
49
+ super(m, 401, d);
50
+ }
51
+ };
52
+ var ForbiddenError = class extends LithiaClientError {
53
+ constructor(m, d) {
54
+ super(m, 403, d);
55
+ }
56
+ };
57
+ var RouteNotFoundError = class extends LithiaClientError {
58
+ constructor(m, d) {
59
+ super(m, 404, d);
60
+ }
61
+ };
62
+ var NotFoundError = class extends LithiaClientError {
63
+ constructor(m, d) {
64
+ super(m, 404, d);
65
+ }
66
+ };
67
+ var ConflictError = class extends LithiaClientError {
68
+ constructor(m, d) {
69
+ super(m, 409, d);
70
+ }
71
+ };
72
+
73
+ // src/errors/app/server.ts
74
+ var InternalServerError = class extends LithiaClientError {
75
+ constructor(m, d) {
76
+ super(m, 500, d);
77
+ }
78
+ };
79
+ var ServiceUnavailableError = class extends LithiaClientError {
80
+ constructor(m, d) {
81
+ super(m, 503, d);
82
+ }
83
+ };
84
+ var GatewayTimeoutError = class extends LithiaClientError {
85
+ constructor(m, d) {
86
+ super(m, 504, d);
87
+ }
88
+ };
89
+
90
+ // src/errors/internal/context.ts
91
+ var NotInLithiaContextError = class extends LithiaError {
92
+ constructor() {
93
+ super("Lithia hooks must be used within a managed invocation.");
94
+ }
95
+ };
96
+ var DependencyNotInitializedError = class extends LithiaError {
97
+ constructor(dependencyName) {
98
+ super(`Dependency '${dependencyName}' not initialized. Check _app.ts.`);
99
+ }
100
+ };
101
+ var NotInEventContextError = class extends NotInLithiaContextError {
102
+ };
103
+ var NotInRequestContextError = class extends NotInLithiaContextError {
104
+ };
105
+
106
+ // src/context/event-context.ts
107
+ var CONTEXT_GLOBAL_KEY = /* @__PURE__ */ Symbol.for("lithia.event_context.v1");
108
+ function getGlobalStore() {
109
+ const globalAny = globalThis;
110
+ if (!globalAny[CONTEXT_GLOBAL_KEY]) {
111
+ globalAny[CONTEXT_GLOBAL_KEY] = new AsyncLocalStorage();
112
+ }
113
+ return globalAny[CONTEXT_GLOBAL_KEY];
114
+ }
115
+ var eventContextStore = getGlobalStore();
116
+ function getEventContext() {
117
+ const ctx = eventContextStore.getStore();
118
+ if (!ctx) {
119
+ throw new NotInEventContextError();
120
+ }
121
+ return ctx;
122
+ }
123
+
124
+ // src/hooks/event-hooks.ts
125
+ function useData() {
126
+ return getEventContext().data;
127
+ }
128
+ function useSocket() {
129
+ return getEventContext().socket;
130
+ }
131
+ function useEvent() {
132
+ return getEventContext().event;
133
+ }
134
+ var LITHIA_CONTEXT_KEY = /* @__PURE__ */ Symbol.for("lithia.base_context.v1");
135
+ function getGlobalLithiaStore() {
136
+ const globalAny = globalThis;
137
+ if (!globalAny[LITHIA_CONTEXT_KEY]) {
138
+ globalAny[LITHIA_CONTEXT_KEY] = new AsyncLocalStorage();
139
+ }
140
+ return globalAny[LITHIA_CONTEXT_KEY];
141
+ }
142
+ var lithiaContextStore = getGlobalLithiaStore();
143
+ function getLithiaContext() {
144
+ const ctx = lithiaContextStore.getStore();
145
+ if (!ctx) {
146
+ throw new NotInLithiaContextError();
147
+ }
148
+ return ctx;
149
+ }
150
+
151
+ // src/hooks/lithia-hooks.ts
152
+ function provide(key, value) {
153
+ const { container } = getLithiaContext();
154
+ container.set(key, value);
155
+ }
156
+ function useDependency(key) {
157
+ const { container } = getLithiaContext();
158
+ if (!container.has(key)) {
159
+ const name = typeof key === "function" ? key.name : String(key);
160
+ throw new DependencyNotInitializedError(name);
161
+ }
162
+ return container.get(key);
163
+ }
164
+ function useOptionalDependency(key) {
165
+ const { container } = getLithiaContext();
166
+ return container.get(key);
167
+ }
168
+ function ensureCloneableTaskArgs(taskId, args) {
169
+ try {
170
+ structuredClone(args);
171
+ } catch (error) {
172
+ throw new Error(
173
+ `[task:${taskId}] Task arguments could not be cloned for worker dispatch.`,
174
+ { cause: error }
175
+ );
176
+ }
177
+ }
178
+ function createTaskError(payload) {
179
+ const error = new Error(payload.message, { cause: payload.cause });
180
+ error.name = payload.name;
181
+ if (payload.stack) {
182
+ error.stack = payload.stack;
183
+ }
184
+ return error;
185
+ }
186
+ function postTaskInvocation(taskId, args, options) {
187
+ ensureCloneableTaskArgs(String(taskId), args);
188
+ parentPort?.postMessage({
189
+ type: "invoke",
190
+ taskId,
191
+ async: options.async,
192
+ requestId: options.requestId,
193
+ executionId: options.executionId,
194
+ args,
195
+ source: options.source,
196
+ attempt: 0
197
+ });
198
+ return {
199
+ taskId: String(taskId),
200
+ executionId: options.executionId,
201
+ source: options.source
202
+ };
203
+ }
204
+ async function executeTask(taskId, ...args) {
205
+ if (!parentPort) {
206
+ throw new Error(
207
+ "Async task invocations can only be used within a Lithia managed instance."
208
+ );
209
+ }
210
+ const requestId = randomUUID();
211
+ const executionId = randomUUID();
212
+ return new Promise((resolve, reject) => {
213
+ const handler = (msg) => {
214
+ if (msg.requestId === requestId) {
215
+ cleanup();
216
+ if (msg.type === "invoke_success") {
217
+ resolve(msg.result);
218
+ } else {
219
+ reject(createTaskError(msg.error));
220
+ }
221
+ }
222
+ };
223
+ const closeHandler = () => {
224
+ cleanup();
225
+ reject(
226
+ new Error(
227
+ `[task:${String(taskId)}] Worker thread closed before task execution could complete.`
228
+ )
229
+ );
230
+ };
231
+ const cleanup = () => {
232
+ parentPort?.off("message", handler);
233
+ parentPort?.off("close", closeHandler);
234
+ };
235
+ parentPort?.on("message", handler);
236
+ parentPort?.on("close", closeHandler);
237
+ postTaskInvocation(taskId, args, {
238
+ async: false,
239
+ requestId,
240
+ executionId,
241
+ source: "ON_DEMAND"
242
+ });
243
+ });
244
+ }
245
+ function dispatchTask(taskId, ...args) {
246
+ if (!parentPort) {
247
+ throw new Error(
248
+ "Async task invocations can only be used within a Lithia managed instance."
249
+ );
250
+ }
251
+ return postTaskInvocation(taskId, args, {
252
+ async: true,
253
+ executionId: randomUUID(),
254
+ source: "ON_DEMAND"
255
+ });
256
+ }
257
+ async function runTask(taskId, ...args) {
258
+ return executeTask(taskId, ...args);
259
+ }
260
+ function runTaskAsync(taskId, ...args) {
261
+ return dispatchTask(taskId, ...args);
262
+ }
263
+ var ROUTE_CONTEXT_KEY = /* @__PURE__ */ Symbol.for("lithia.route_context.v1");
264
+ function getGlobalRouteStore() {
265
+ const globalAny = globalThis;
266
+ if (!globalAny[ROUTE_CONTEXT_KEY]) {
267
+ globalAny[ROUTE_CONTEXT_KEY] = new AsyncLocalStorage();
268
+ }
269
+ return globalAny[ROUTE_CONTEXT_KEY];
270
+ }
271
+ var routeContextStore = getGlobalRouteStore();
272
+ function getRouteContext() {
273
+ const ctx = routeContextStore.getStore();
274
+ if (!ctx) {
275
+ throw new NotInRequestContextError();
276
+ }
277
+ return ctx;
278
+ }
279
+
280
+ // src/hooks/route-hooks.ts
281
+ function useRequest() {
282
+ return getRouteContext().req;
283
+ }
284
+ function useResponse() {
285
+ return getRouteContext().res;
286
+ }
287
+ function useRoute() {
288
+ return getRouteContext().route;
289
+ }
290
+ function usePathname() {
291
+ return getRouteContext().req.pathname;
292
+ }
293
+ function useParams() {
294
+ return getRouteContext().req.params;
295
+ }
296
+ function useQuery() {
297
+ return getRouteContext().req.query;
298
+ }
299
+ function useHeaders() {
300
+ return getRouteContext().req.headers;
301
+ }
302
+ function useSocketServer() {
303
+ return getRouteContext().socketServer;
304
+ }
305
+ var LithiaRequest = class {
306
+ /**
307
+ * Creates a request wrapper for the current HTTP transaction.
308
+ *
309
+ * The constructor captures headers, reconstructs a best-effort absolute URL,
310
+ * normalizes the HTTP method to uppercase, and initializes parsed query and
311
+ * route-param containers for later middleware and handler use.
312
+ *
313
+ * @param {IncomingMessage} req - Raw Node.js request object received by the
314
+ * HTTP server.
315
+ * @param {{ maxBodySize?: number }} opts - Per-request parsing options used
316
+ * when consuming the request body stream.
317
+ */
318
+ constructor(req, opts) {
319
+ this.req = req;
320
+ this.opts = opts;
321
+ this.headers = req.headers;
322
+ const isSecure = this.isSecure();
323
+ const host = this.headers.host || req.headers.host || "unknown";
324
+ const fullUrl = `${isSecure ? "https" : "http"}://${host}${req.url || "/"}`;
325
+ const url = new URL(fullUrl);
326
+ this.pathname = url.pathname;
327
+ this.method = (req.method || "GET").toUpperCase();
328
+ this.query = parseQueryToObject(url.searchParams);
329
+ this.params = {};
330
+ }
331
+ headers;
332
+ method;
333
+ pathname;
334
+ query;
335
+ params;
336
+ storage = /* @__PURE__ */ new Map();
337
+ _bodyCache = null;
338
+ _filesCache = null;
339
+ _cookies = null;
340
+ /**
341
+ * Returns the best-effort client IP address for the current request.
342
+ *
343
+ * The lookup prefers proxy-forwarded headers before falling back to the raw
344
+ * socket address, which makes the result suitable for deployments behind
345
+ * reverse proxies that preserve `x-forwarded-for` or `x-real-ip`.
346
+ *
347
+ * @returns {string} The resolved client IP address, or `"unknown"` when no
348
+ * address can be derived.
349
+ */
350
+ ip() {
351
+ return this.headers["x-forwarded-for"]?.split(",")[0]?.trim() || this.headers["x-real-ip"] || this.req.socket?.remoteAddress || "unknown";
352
+ }
353
+ /**
354
+ * Returns the current request user-agent string.
355
+ *
356
+ * @returns {string} The raw `user-agent` header value, or an empty string
357
+ * when the header is missing.
358
+ */
359
+ userAgent() {
360
+ return this.headers["user-agent"] || "";
361
+ }
362
+ /**
363
+ * Returns whether the current request is using HTTPS.
364
+ *
365
+ * The check prefers `x-forwarded-proto` for proxy-aware deployments and then
366
+ * falls back to the encrypted state of the underlying socket.
367
+ *
368
+ * @returns {boolean} `true` when the request should be treated as HTTPS.
369
+ */
370
+ isSecure() {
371
+ return this.headers["x-forwarded-proto"] === "https" || this.req.socket?.encrypted === true;
372
+ }
373
+ /**
374
+ * Returns the request host header.
375
+ *
376
+ * @returns {string} The current host header value, or `"unknown"` when it is
377
+ * not available.
378
+ */
379
+ host() {
380
+ return this.headers.host || "unknown";
381
+ }
382
+ /**
383
+ * Returns the absolute request URL reconstructed from the current request.
384
+ *
385
+ * This helper rebuilds the URL from the current security state, host header,
386
+ * and parsed pathname. It does not append the original query string.
387
+ *
388
+ * @returns {string} Absolute URL for the current request pathname.
389
+ */
390
+ url() {
391
+ return `${this.isSecure() ? "https" : "http"}://${this.host()}${this.pathname}`;
392
+ }
393
+ /**
394
+ * Parses and returns the request body.
395
+ *
396
+ * JSON and plain text bodies are supported automatically. Multipart requests
397
+ * populate both `body()` and `files()` through a shared parsing pass. The
398
+ * parsed value is cached after the first read so later consumers do not touch
399
+ * the underlying stream again.
400
+ *
401
+ * Requests whose method is not one of `POST`, `PUT`, `PATCH`, or `DELETE`
402
+ * resolve to an empty object without reading the stream.
403
+ *
404
+ * @returns {Promise<T>} Parsed request body, multipart field map, raw text, or
405
+ * an empty object for methods that do not consume a body by default.
406
+ * @throws {BadRequestError} Thrown when the declared or streamed body size
407
+ * exceeds `maxBodySize`, or when JSON parsing fails.
408
+ */
409
+ async body() {
410
+ const methodsWithBody = ["POST", "PUT", "PATCH", "DELETE"];
411
+ if (!methodsWithBody.includes(this.method)) {
412
+ return {};
413
+ }
414
+ if (this._bodyCache !== null) return this._bodyCache;
415
+ const contentType = this.headers["content-type"] || "";
416
+ if (contentType.includes("multipart/form-data")) {
417
+ await this.parseMultipart();
418
+ return this._bodyCache;
419
+ }
420
+ const contentLength = parseInt(
421
+ this.headers["content-length"] || "0",
422
+ 10
423
+ );
424
+ const maxBodySize = this.opts.maxBodySize || 1024 * 1024;
425
+ if (contentLength > maxBodySize) {
426
+ throw new BadRequestError("Request body too large.");
427
+ }
428
+ const body = await new Promise((resolve, reject) => {
429
+ const chunks = [];
430
+ let currentSize = 0;
431
+ this.req.on("data", (chunk) => {
432
+ currentSize += chunk.length;
433
+ if (currentSize > maxBodySize) {
434
+ reject(new BadRequestError("Request body too large."));
435
+ }
436
+ chunks.push(chunk);
437
+ });
438
+ this.req.on("end", () => {
439
+ if (chunks.length === 0) return resolve({});
440
+ const rawBody = Buffer.concat(chunks).toString("utf-8");
441
+ try {
442
+ if (contentType.includes("application/json")) {
443
+ resolve(JSON.parse(rawBody));
444
+ } else {
445
+ resolve(rawBody);
446
+ }
447
+ } catch {
448
+ reject(new BadRequestError("Invalid request body format."));
449
+ }
450
+ });
451
+ this.req.on("error", (err) => reject(err));
452
+ });
453
+ this._bodyCache = body;
454
+ this.storage.set("body", body);
455
+ return body;
456
+ }
457
+ /**
458
+ * Returns uploaded files for multipart/form-data requests.
459
+ *
460
+ * `files()` shares the same multipart parsing pass used by `body()`. The
461
+ * first call buffers every uploaded file into memory and caches both the
462
+ * parsed field object and file array for later access.
463
+ *
464
+ * @returns {Promise<UploadedFile[]>} Buffered multipart files, or an empty
465
+ * array when the request is not multipart.
466
+ */
467
+ async files() {
468
+ const contentType = this.headers["content-type"] || "";
469
+ if (!contentType.includes("multipart/form-data")) return [];
470
+ if (this._filesCache !== null) return this._filesCache;
471
+ await this.parseMultipart();
472
+ return this._filesCache || [];
473
+ }
474
+ /**
475
+ * Overrides the cached body value for the current request context.
476
+ *
477
+ * This mutates only the wrapper cache and the internal storage map. It does
478
+ * not modify the underlying Node.js request stream.
479
+ *
480
+ * @param {unknown} value - Replacement body value to expose through `body()`
481
+ * and internal request storage.
482
+ */
483
+ setBody(value) {
484
+ this._bodyCache = value;
485
+ this.storage.set("body", value);
486
+ }
487
+ /**
488
+ * Returns all parsed cookies from the request.
489
+ *
490
+ * Cookies are parsed lazily on first access and cached for the remainder of
491
+ * the request lifecycle.
492
+ *
493
+ * @returns {Cookies} Parsed cookie map for the current request.
494
+ */
495
+ cookies() {
496
+ if (this._cookies === null) {
497
+ const cookieHeader = this.headers.cookie;
498
+ this._cookies = cookieHeader ? parse(cookieHeader) : {};
499
+ }
500
+ return this._cookies;
501
+ }
502
+ /**
503
+ * Returns a single cookie value by name.
504
+ *
505
+ * @param {string} name - Cookie name to read from the parsed cookie map.
506
+ * @returns {string | undefined} The cookie value when present.
507
+ */
508
+ cookie(name) {
509
+ return this.cookies()[name];
510
+ }
511
+ /**
512
+ * Returns a value stored in the per-request internal storage map.
513
+ *
514
+ * This storage is local to the current request wrapper and can be used by
515
+ * middleware and handlers to exchange derived values without mutating the
516
+ * typed request surface.
517
+ *
518
+ * @param {string} key - Storage key associated with the requested value.
519
+ * @returns {T | undefined} Stored value for the key, if one exists.
520
+ */
521
+ get(key) {
522
+ return this.storage.get(key);
523
+ }
524
+ /**
525
+ * Stores a value in the per-request internal storage map.
526
+ *
527
+ * @param {string} key - Storage key to create or overwrite.
528
+ * @param {unknown} value - Arbitrary value to retain for the lifetime of the
529
+ * current request wrapper.
530
+ */
531
+ set(key, value) {
532
+ this.storage.set(key, value);
533
+ }
534
+ /**
535
+ * Parses a multipart/form-data request into cached fields and file buffers.
536
+ *
537
+ * The request stream is piped into Busboy exactly once. Field values are
538
+ * collected into a plain object, file contents are buffered fully in memory,
539
+ * and both results are stored in the request cache and internal storage map.
540
+ *
541
+ * @returns {Promise<void>} Resolves after Busboy finishes consuming the
542
+ * multipart stream and caches the parsed payload.
543
+ */
544
+ async parseMultipart() {
545
+ if (this._bodyCache !== null && this._filesCache !== null) return;
546
+ return new Promise((resolve, reject) => {
547
+ const bb = busboy({ headers: this.headers });
548
+ const fields = {};
549
+ const files = [];
550
+ bb.on("file", (name, file, info) => {
551
+ const chunks = [];
552
+ file.on("data", (data) => chunks.push(data));
553
+ file.on("end", () => {
554
+ files.push({
555
+ fieldname: name,
556
+ buffer: Buffer.concat(chunks),
557
+ ...info
558
+ });
559
+ });
560
+ });
561
+ bb.on("field", (name, value) => {
562
+ fields[name] = value;
563
+ });
564
+ bb.on("finish", () => {
565
+ this._bodyCache = fields;
566
+ this._filesCache = files;
567
+ this.storage.set("body", fields);
568
+ this.storage.set("files", files);
569
+ resolve();
570
+ });
571
+ bb.on("error", reject);
572
+ this.req.pipe(bb);
573
+ });
574
+ }
575
+ };
576
+ function parseQueryToObject(searchParams) {
577
+ const query = {};
578
+ for (const [key, value] of searchParams.entries()) {
579
+ query[key] = value;
580
+ }
581
+ return query;
582
+ }
583
+ var LithiaResponse = class {
584
+ /**
585
+ * Creates a response wrapper for the current HTTP transaction.
586
+ *
587
+ * The wrapper binds a pass-through `on()` helper to the underlying Node.js
588
+ * response object so route-adjacent code can subscribe to response events
589
+ * without holding the raw `ServerResponse`.
590
+ *
591
+ * @param {ServerResponse} res - Raw Node.js response object associated with
592
+ * the current request.
593
+ */
594
+ constructor(res) {
595
+ this.res = res;
596
+ this.on = this.res.on.bind(this.res);
597
+ }
598
+ _ended = false;
599
+ _cookies = [];
600
+ on;
601
+ /**
602
+ * Returns the current HTTP status code.
603
+ *
604
+ * @returns {number} Status code currently assigned to the underlying
605
+ * response.
606
+ */
607
+ get statusCode() {
608
+ return this.res.statusCode;
609
+ }
610
+ /**
611
+ * Sets the HTTP status code for the response.
612
+ *
613
+ * This mutates the underlying response only while it is still active.
614
+ *
615
+ * @param {number} status - HTTP status code to assign before the response is
616
+ * sent.
617
+ * @returns {this} The current response wrapper for fluent chaining.
618
+ * @throws {Error} Thrown when the response has already ended or when the
619
+ * supplied status code falls outside the valid HTTP range.
620
+ */
621
+ status(status) {
622
+ this.ensureActive();
623
+ if (status < 100 || status > 599) {
624
+ throw new Error(`Invalid HTTP status code: ${status}`);
625
+ }
626
+ this.res.statusCode = status;
627
+ return this;
628
+ }
629
+ /**
630
+ * Returns the currently assigned response headers.
631
+ *
632
+ * @returns {Readonly<OutgoingHttpHeaders>} Snapshot of the headers currently
633
+ * stored on the underlying response.
634
+ */
635
+ headers() {
636
+ return this.res.getHeaders();
637
+ }
638
+ /**
639
+ * Sets multiple response headers at once.
640
+ *
641
+ * @param {OutgoingHttpHeaders} headers - Header entries to assign to the
642
+ * response before it is sent.
643
+ * @returns {this} The current response wrapper for fluent chaining.
644
+ * @throws {Error} Thrown when the response has already ended.
645
+ */
646
+ setHeaders(headers) {
647
+ this.ensureActive();
648
+ Object.entries(headers).forEach(([key, value]) => {
649
+ this.res.setHeader(key, value);
650
+ });
651
+ return this;
652
+ }
653
+ /**
654
+ * Sets a single response header.
655
+ *
656
+ * @param {string} name - Header name to create or overwrite.
657
+ * @param {string | number | string[]} value - Header value written to the
658
+ * underlying response.
659
+ * @returns {this} The current response wrapper for fluent chaining.
660
+ * @throws {Error} Thrown when the response has already ended.
661
+ */
662
+ setHeader(name, value) {
663
+ this.ensureActive();
664
+ this.res.setHeader(name, value);
665
+ return this;
666
+ }
667
+ /**
668
+ * Removes a response header.
669
+ *
670
+ * @param {string} name - Header name to remove.
671
+ * @returns {this} The current response wrapper for fluent chaining.
672
+ * @throws {Error} Thrown when the response has already ended.
673
+ */
674
+ removeHeader(name) {
675
+ this.ensureActive();
676
+ this.res.removeHeader(name);
677
+ return this;
678
+ }
679
+ /**
680
+ * Queues a cookie to be written when the response is sent.
681
+ *
682
+ * Cookies are accumulated in memory and serialized only when a terminal
683
+ * response method flushes headers.
684
+ *
685
+ * @param {string} name - Cookie name.
686
+ * @param {string} value - Cookie value.
687
+ * @param {CookieOptions} [options={}] - Cookie serialization options.
688
+ * @returns {this} The current response wrapper for fluent chaining.
689
+ * @throws {Error} Thrown when the response has already ended.
690
+ */
691
+ cookie(name, value, options = {}) {
692
+ this.ensureActive();
693
+ this._cookies.push({ name, value, options });
694
+ return this;
695
+ }
696
+ /**
697
+ * Clears a cookie by expiring it immediately.
698
+ *
699
+ * @param {string} name - Cookie name to expire.
700
+ * @param {CookieOptions} [options={}] - Additional cookie attributes that
701
+ * must match the original cookie scope.
702
+ * @returns {this} The current response wrapper for fluent chaining.
703
+ */
704
+ clearCookie(name, options = {}) {
705
+ return this.cookie(name, "", { ...options, expires: /* @__PURE__ */ new Date(0) });
706
+ }
707
+ /**
708
+ * Sends a response body using a best-effort content type.
709
+ *
710
+ * The method flushes pending cookies before writing, chooses a default
711
+ * content type when none is set, and treats plain objects as JSON by
712
+ * delegating to `json()`. Calling `send()` is a terminal operation for the
713
+ * response lifecycle.
714
+ *
715
+ * @param {unknown} [data] - Response payload to send.
716
+ * @throws {Error} Thrown when the response has already ended.
717
+ */
718
+ send(data) {
719
+ this.applyPendingCookies();
720
+ this.ensureActive();
721
+ try {
722
+ if (data === void 0 || data === null) {
723
+ this.end();
724
+ return;
725
+ }
726
+ if (Buffer.isBuffer(data)) {
727
+ if (!this.res.getHeader("Content-Type")) {
728
+ this.setHeader("Content-Type", "application/octet-stream");
729
+ }
730
+ this.res.end(data);
731
+ } else if (typeof data === "object") {
732
+ this.json(data);
733
+ return;
734
+ } else {
735
+ if (!this.res.getHeader("Content-Type")) {
736
+ this.setHeader("Content-Type", "text/plain; charset=utf-8");
737
+ }
738
+ this.res.end(String(data));
739
+ }
740
+ } finally {
741
+ this._ended = true;
742
+ }
743
+ }
744
+ /**
745
+ * Sends a JSON response.
746
+ *
747
+ * Pending cookies are flushed before serialization. If JSON serialization
748
+ * throws, the method logs the failure and falls back to a `500 Internal
749
+ * Server Error` response body.
750
+ *
751
+ * @param {object} obj - Plain object to serialize as JSON.
752
+ */
753
+ json(obj) {
754
+ this.applyPendingCookies();
755
+ this.ensureActive();
756
+ try {
757
+ const body = JSON.stringify(obj);
758
+ this.res.setHeader("Content-Type", "application/json; charset=utf-8");
759
+ this.res.end(body);
760
+ } catch (err) {
761
+ logger.error("Failed to serialize JSON response:", err);
762
+ this.res.statusCode = 500;
763
+ this.res.end("Internal Server Error");
764
+ } finally {
765
+ this._ended = true;
766
+ }
767
+ }
768
+ /**
769
+ * Sends a redirect response.
770
+ *
771
+ * This sets the status code, writes the `Location` header, and then ends the
772
+ * response.
773
+ *
774
+ * @param {string} url - Redirect target written to the `Location` header.
775
+ * @param {number} [status=302] - Redirect status code.
776
+ */
777
+ redirect(url, status = 302) {
778
+ this.status(status).setHeader("Location", url).end();
779
+ }
780
+ /**
781
+ * Ends the response without sending additional data.
782
+ *
783
+ * Pending cookies are flushed before the underlying response is closed.
784
+ *
785
+ * @throws {Error} Thrown when the response has already ended.
786
+ */
787
+ end() {
788
+ this.applyPendingCookies();
789
+ this.ensureActive();
790
+ this.res.end();
791
+ this._ended = true;
792
+ }
793
+ /**
794
+ * Streams a file to the client.
795
+ *
796
+ * The method resolves the final path, verifies that it points to a regular
797
+ * file, sets `Content-Length`, flushes pending cookies, and pipes the file
798
+ * stream into the underlying response. Missing files and stream failures fall
799
+ * back to a `404` JSON error payload.
800
+ *
801
+ * @param {string} filePath - File path to stream. When `opts.root` is set, it
802
+ * is resolved relative to that root.
803
+ * @param {{ root?: string }} [opts={}] - Optional root directory used to
804
+ * resolve relative file paths.
805
+ */
806
+ sendFile(filePath, opts = {}) {
807
+ this.ensureActive();
808
+ try {
809
+ const fullPath = opts.root ? join(opts.root, filePath) : filePath;
810
+ const stats = statSync(fullPath);
811
+ if (!stats.isFile()) throw new Error("Target is not a file");
812
+ this.setHeader("Content-Length", String(stats.size));
813
+ const stream = createReadStream(fullPath);
814
+ this.applyPendingCookies();
815
+ stream.pipe(this.res);
816
+ stream.on("error", () => {
817
+ this.status(404).send({ error: "File not found" });
818
+ });
819
+ } catch {
820
+ this.status(404).send({ error: "File not found" });
821
+ } finally {
822
+ this._ended = true;
823
+ }
824
+ }
825
+ /**
826
+ * Serializes queued cookies into the response headers and clears the queue.
827
+ *
828
+ * Existing `Set-Cookie` headers are preserved and extended so multiple
829
+ * middleware and handler calls can contribute cookies before the response is
830
+ * finalized.
831
+ */
832
+ applyPendingCookies() {
833
+ if (this._cookies.length === 0) return;
834
+ const existing = this.res.getHeader("Set-Cookie") || [];
835
+ const serialized = this._cookies.map(
836
+ (cookie) => serialize(cookie.name, cookie.value, cookie.options)
837
+ );
838
+ this.res.setHeader("Set-Cookie", [...existing, ...serialized]);
839
+ this._cookies = [];
840
+ }
841
+ /**
842
+ * Ensures the response has not already been finalized.
843
+ *
844
+ * @throws {Error} Thrown when a terminal response method has already sent or
845
+ * ended the response.
846
+ */
847
+ ensureActive() {
848
+ if (this._ended) {
849
+ throw new Error("Response has already been sent.");
850
+ }
851
+ }
852
+ };
853
+
854
+ export { BadRequestError, ConflictError, ForbiddenError, GatewayTimeoutError, InternalServerError, LithiaClientError, LithiaError, LithiaEventError, LithiaRequest, LithiaResponse, NotFoundError, RouteNotFoundError, ServiceUnavailableError, UnauthorizedError, defineConfig, dispatchTask, executeTask, provide, runTask, runTaskAsync, useData, useDependency, useEvent, useHeaders, useOptionalDependency, useParams, usePathname, useQuery, useRequest, useResponse, useRoute, useSocket, useSocketServer };
855
+ //# sourceMappingURL=index.mjs.map
856
+ //# sourceMappingURL=index.mjs.map