@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.
- package/README.md +26 -43
- package/dist/_index.d.ts +245 -0
- package/dist/_index.mjs +2106 -0
- package/dist/_index.mjs.map +1 -0
- package/dist/index.d.ts +824 -0
- package/dist/index.mjs +856 -0
- package/dist/index.mjs.map +1 -0
- package/dist/protocol-DBwVPJYN.d.ts +332 -0
- package/dist/tasks-X-3clDS8.d.ts +31 -0
- package/dist/workers/app-worker.d.ts +2 -0
- package/dist/workers/app-worker.mjs +1907 -0
- package/dist/workers/app-worker.mjs.map +1 -0
- package/dist/workers/task-worker.d.ts +45 -0
- package/dist/workers/task-worker.mjs +146 -0
- package/dist/workers/task-worker.mjs.map +1 -0
- package/package.json +47 -23
- package/CHANGELOG.md +0 -31
- package/dist/config.d.ts +0 -101
- package/dist/config.js +0 -113
- package/dist/config.js.map +0 -1
- package/dist/context/event-context.d.ts +0 -53
- package/dist/context/event-context.js +0 -42
- package/dist/context/event-context.js.map +0 -1
- package/dist/context/index.d.ts +0 -16
- package/dist/context/index.js +0 -29
- package/dist/context/index.js.map +0 -1
- package/dist/context/lithia-context.d.ts +0 -47
- package/dist/context/lithia-context.js +0 -43
- package/dist/context/lithia-context.js.map +0 -1
- package/dist/context/route-context.d.ts +0 -74
- package/dist/context/route-context.js +0 -42
- package/dist/context/route-context.js.map +0 -1
- package/dist/env.d.ts +0 -1
- package/dist/env.js +0 -32
- package/dist/env.js.map +0 -1
- package/dist/errors.d.ts +0 -51
- package/dist/errors.js +0 -80
- package/dist/errors.js.map +0 -1
- package/dist/hooks/dependency-hooks.d.ts +0 -105
- package/dist/hooks/dependency-hooks.js +0 -96
- package/dist/hooks/dependency-hooks.js.map +0 -1
- package/dist/hooks/event-hooks.d.ts +0 -61
- package/dist/hooks/event-hooks.js +0 -70
- package/dist/hooks/event-hooks.js.map +0 -1
- package/dist/hooks/index.d.ts +0 -41
- package/dist/hooks/index.js +0 -59
- package/dist/hooks/index.js.map +0 -1
- package/dist/hooks/route-hooks.d.ts +0 -154
- package/dist/hooks/route-hooks.js +0 -174
- package/dist/hooks/route-hooks.js.map +0 -1
- package/dist/lib.d.ts +0 -10
- package/dist/lib.js +0 -30
- package/dist/lib.js.map +0 -1
- package/dist/lithia.d.ts +0 -447
- package/dist/lithia.js +0 -649
- package/dist/lithia.js.map +0 -1
- package/dist/logger.d.ts +0 -11
- package/dist/logger.js +0 -55
- package/dist/logger.js.map +0 -1
- package/dist/module-loader.d.ts +0 -12
- package/dist/module-loader.js +0 -78
- package/dist/module-loader.js.map +0 -1
- package/dist/server/event-processor.d.ts +0 -195
- package/dist/server/event-processor.js +0 -253
- package/dist/server/event-processor.js.map +0 -1
- package/dist/server/http-server.d.ts +0 -196
- package/dist/server/http-server.js +0 -295
- package/dist/server/http-server.js.map +0 -1
- package/dist/server/middlewares/validation.d.ts +0 -12
- package/dist/server/middlewares/validation.js +0 -34
- package/dist/server/middlewares/validation.js.map +0 -1
- package/dist/server/request-processor.d.ts +0 -400
- package/dist/server/request-processor.js +0 -652
- package/dist/server/request-processor.js.map +0 -1
- package/dist/server/request.d.ts +0 -73
- package/dist/server/request.js +0 -207
- package/dist/server/request.js.map +0 -1
- package/dist/server/response.d.ts +0 -69
- package/dist/server/response.js +0 -173
- package/dist/server/response.js.map +0 -1
- package/src/config.ts +0 -212
- package/src/context/event-context.ts +0 -66
- package/src/context/index.ts +0 -32
- package/src/context/lithia-context.ts +0 -59
- package/src/context/route-context.ts +0 -89
- package/src/env.ts +0 -31
- package/src/errors.ts +0 -96
- package/src/hooks/dependency-hooks.ts +0 -122
- package/src/hooks/event-hooks.ts +0 -69
- package/src/hooks/index.ts +0 -58
- package/src/hooks/route-hooks.ts +0 -177
- package/src/lib.ts +0 -27
- package/src/lithia.ts +0 -777
- package/src/logger.ts +0 -66
- package/src/module-loader.ts +0 -45
- package/src/server/event-processor.ts +0 -344
- package/src/server/http-server.ts +0 -371
- package/src/server/middlewares/validation.ts +0 -46
- package/src/server/request-processor.ts +0 -860
- package/src/server/request.ts +0 -247
- package/src/server/response.ts +0 -204
- 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
|