sevok 0.0.2
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/LICENSE +21 -0
- package/README.md +342 -0
- package/README.zh-CN.md +342 -0
- package/bin/sevok.mjs +13 -0
- package/dist/_color-B42q-MpL.mjs +34 -0
- package/dist/_node_like-BRaAGeNM.mjs +112 -0
- package/dist/_shared-38k_JIsU.mjs +92 -0
- package/dist/bun.d.mts +31 -0
- package/dist/bun.mjs +58 -0
- package/dist/cli.d.mts +142 -0
- package/dist/cli.mjs +461 -0
- package/dist/core-BGp2ZR_k.d.mts +665 -0
- package/dist/core-CmUugTW7.mjs +732 -0
- package/dist/deno.d.mts +32 -0
- package/dist/deno.mjs +65 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/log.d.mts +27 -0
- package/dist/log.mjs +26 -0
- package/dist/node.d.mts +32 -0
- package/dist/node.mjs +139 -0
- package/dist/static.d.mts +35 -0
- package/dist/static.mjs +100 -0
- package/dist/stream.d.mts +56 -0
- package/dist/stream.mjs +68 -0
- package/package.json +68 -0
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import { i as extractParams, n as compilePath, o as normalizePathname, r as extractKeys, t as canonicalizePath } from "./_shared-38k_JIsU.mjs";
|
|
2
|
+
import { t as _color_default } from "./_color-B42q-MpL.mjs";
|
|
3
|
+
import { EventDispatcher } from "@hornjs/evt";
|
|
4
|
+
//#region \0rolldown/runtime.js
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __exportAll = (all, no_symbols) => {
|
|
7
|
+
let target = {};
|
|
8
|
+
for (var name in all) __defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
return target;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/_plugins.ts
|
|
17
|
+
/**
|
|
18
|
+
* Wrap the request pipeline with the user-provided error handler.
|
|
19
|
+
*
|
|
20
|
+
* The plugin inserts a middleware at the front of the stack so both thrown
|
|
21
|
+
* exceptions and rejected downstream promises are normalized through
|
|
22
|
+
* `server.options.error`.
|
|
23
|
+
*/
|
|
24
|
+
const errorPlugin = (server) => {
|
|
25
|
+
const errorHandler = server.options.error;
|
|
26
|
+
if (!errorHandler) return;
|
|
27
|
+
server.options.middleware.unshift((ctx, next) => {
|
|
28
|
+
try {
|
|
29
|
+
const res = next(ctx);
|
|
30
|
+
return res instanceof Promise ? res.catch((error) => errorHandler(error)) : res;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return errorHandler(error);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Register process signal handlers that close the server gracefully.
|
|
38
|
+
*
|
|
39
|
+
* The first `SIGINT` / `SIGTERM` starts a graceful shutdown countdown. A second
|
|
40
|
+
* `SIGINT` forces active connections to close immediately.
|
|
41
|
+
*/
|
|
42
|
+
const gracefulShutdownPlugin = (server) => {
|
|
43
|
+
const config = server.options?.gracefulShutdown;
|
|
44
|
+
if (!globalThis.process?.on || config === false || config === void 0 && (process.env.CI || process.env.TEST)) return;
|
|
45
|
+
const gracefulTimeout = config === true || !config?.gracefulTimeout ? Number.parseInt(process.env.SERVER_SHUTDOWN_TIMEOUT || "") || 5 : config.gracefulTimeout;
|
|
46
|
+
let isClosing = false;
|
|
47
|
+
let isClosed = false;
|
|
48
|
+
const write = server.options.silent ? () => {} : process.stderr.write.bind(process.stderr);
|
|
49
|
+
/**
|
|
50
|
+
* Forcefully close active connections once graceful shutdown times out or the
|
|
51
|
+
* user presses Ctrl+C again.
|
|
52
|
+
*/
|
|
53
|
+
const forceClose = async () => {
|
|
54
|
+
if (isClosed) return;
|
|
55
|
+
write(_color_default.red("\x1B[2K\rForcibly closing connections...\n"));
|
|
56
|
+
isClosed = true;
|
|
57
|
+
await server.close(true);
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Attempt to close the server cleanly while printing countdown updates.
|
|
61
|
+
*/
|
|
62
|
+
const shutdown = async () => {
|
|
63
|
+
if (isClosing || isClosed) return;
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
globalThis.process.once("SIGINT", forceClose);
|
|
66
|
+
}, 100);
|
|
67
|
+
isClosing = true;
|
|
68
|
+
const closePromise = server.close();
|
|
69
|
+
for (let remaining = gracefulTimeout; remaining > 0; remaining--) {
|
|
70
|
+
write(_color_default.gray(`\rStopping server gracefully (${remaining}s)... Press ${_color_default.bold("Ctrl+C")} again to force close.`));
|
|
71
|
+
if (await Promise.race([closePromise.then(() => true), new Promise((r) => setTimeout(() => r(false), 1e3))])) {
|
|
72
|
+
write("\x1B[2K\r" + _color_default.green("Server closed successfully.\n"));
|
|
73
|
+
isClosed = true;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
write("\x1B[2K\rGraceful shutdown timed out.\n");
|
|
78
|
+
await forceClose();
|
|
79
|
+
};
|
|
80
|
+
for (const sig of ["SIGINT", "SIGTERM"]) globalThis.process.on(sig, shutdown);
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/core.ts
|
|
84
|
+
var core_exports = /* @__PURE__ */ __exportAll({
|
|
85
|
+
InvocationContext: () => InvocationContext,
|
|
86
|
+
Server: () => Server,
|
|
87
|
+
ServerCloseEvent: () => ServerCloseEvent,
|
|
88
|
+
ServerErrorEvent: () => ServerErrorEvent,
|
|
89
|
+
ServerServeEvent: () => ServerServeEvent,
|
|
90
|
+
createContextKey: () => createContextKey,
|
|
91
|
+
createWaitUntil: () => createWaitUntil,
|
|
92
|
+
isServerHandlerObject: () => isServerHandlerObject,
|
|
93
|
+
loadServerAdapter: () => loadServerAdapter,
|
|
94
|
+
raceRequestAbort: () => raceRequestAbort,
|
|
95
|
+
runMiddleware: () => runMiddleware,
|
|
96
|
+
serve: () => serve,
|
|
97
|
+
toServerHandlerObject: () => toServerHandlerObject,
|
|
98
|
+
unstable_buildRouteTree: () => unstable_buildRouteTree,
|
|
99
|
+
unstable_convertRoutesToHandler: () => unstable_convertRoutesToHandler,
|
|
100
|
+
unstable_match: () => unstable_match,
|
|
101
|
+
wrapFetch: () => wrapFetch
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Create an invocation context key with an optional default value.
|
|
105
|
+
*
|
|
106
|
+
* When a default value is provided, `InvocationContext#get()` can always
|
|
107
|
+
* return a value for that key even if nothing has been explicitly written yet.
|
|
108
|
+
*
|
|
109
|
+
* @param defaultValue The default value for the context key
|
|
110
|
+
* @returns The new context key
|
|
111
|
+
*/
|
|
112
|
+
function createContextKey(defaultValue) {
|
|
113
|
+
return { defaultValue };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A context object that contains information about the current invocation.
|
|
117
|
+
* Each handler and middleware receives its own context instance, which may be
|
|
118
|
+
* derived from a parent context via `with()`.
|
|
119
|
+
*
|
|
120
|
+
* Context values are immutable - use `with()` to create a modified copy.
|
|
121
|
+
*/
|
|
122
|
+
var InvocationContext = class InvocationContext {
|
|
123
|
+
/**
|
|
124
|
+
* The original request that was dispatched to the router.
|
|
125
|
+
*
|
|
126
|
+
* Note: The request body may already have been consumed by middleware
|
|
127
|
+
* (available as `context.get(FormData)`). Use `context.with({ request })`
|
|
128
|
+
* if you need to pass a modified request downstream.
|
|
129
|
+
*/
|
|
130
|
+
request;
|
|
131
|
+
/**
|
|
132
|
+
* Cached parsed URL for middleware and handlers that need repeated URL access.
|
|
133
|
+
*/
|
|
134
|
+
url;
|
|
135
|
+
/**
|
|
136
|
+
* Runtime specific invocation context.
|
|
137
|
+
*/
|
|
138
|
+
capabilities;
|
|
139
|
+
/**
|
|
140
|
+
* The current route parameters for this request.
|
|
141
|
+
*
|
|
142
|
+
* This reflects the active matched route and returns an empty object before
|
|
143
|
+
* routing has resolved a match.
|
|
144
|
+
*/
|
|
145
|
+
params;
|
|
146
|
+
/**
|
|
147
|
+
* Tell the runtime about an ongoing operation that shouldn't close until the
|
|
148
|
+
* promise resolves.
|
|
149
|
+
*
|
|
150
|
+
* This mirrors service-worker-style `waitUntil()` semantics and is wired into
|
|
151
|
+
* `Server.close()`.
|
|
152
|
+
*/
|
|
153
|
+
waitUntil;
|
|
154
|
+
#contextMap;
|
|
155
|
+
constructor(init) {
|
|
156
|
+
this.request = init.request;
|
|
157
|
+
this.capabilities = init.capabilities;
|
|
158
|
+
this.params = init.params ?? emptyRouteParams;
|
|
159
|
+
this.waitUntil = init.waitUntil;
|
|
160
|
+
this.#contextMap = /* @__PURE__ */ new Map();
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Create a derived context with optional overrides.
|
|
164
|
+
* Copies all context values to the new instance.
|
|
165
|
+
*/
|
|
166
|
+
with(overrides) {
|
|
167
|
+
const child = new InvocationContext({
|
|
168
|
+
request: overrides.request ?? this.request,
|
|
169
|
+
capabilities: overrides.capabilities ?? this.capabilities,
|
|
170
|
+
params: overrides.params ?? this.params,
|
|
171
|
+
waitUntil: overrides.waitUntil ?? this.waitUntil
|
|
172
|
+
});
|
|
173
|
+
for (const [key, value] of this.#contextMap) child.#contextMap.set(key, value);
|
|
174
|
+
return child;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get a value from invocation context.
|
|
178
|
+
*
|
|
179
|
+
* @param TKey The key to read
|
|
180
|
+
* @returns The value for the given key
|
|
181
|
+
*/
|
|
182
|
+
get(key) {
|
|
183
|
+
if (this.#contextMap.has(key)) return this.#contextMap.get(key);
|
|
184
|
+
const contextKey = key;
|
|
185
|
+
if (contextKey.defaultValue === void 0) throw new Error(`Missing default value in context for key ${key}`);
|
|
186
|
+
return contextKey.defaultValue;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Check whether a value exists in invocation context.
|
|
190
|
+
*
|
|
191
|
+
* @param TKey The key to check
|
|
192
|
+
* @returns `true` if a value has been set for the key
|
|
193
|
+
*/
|
|
194
|
+
has(key) {
|
|
195
|
+
return this.#contextMap.has(key);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Set a value in invocation context.
|
|
199
|
+
*
|
|
200
|
+
* @param key The key to write
|
|
201
|
+
* @param value The value to write
|
|
202
|
+
*/
|
|
203
|
+
set(key, value) {
|
|
204
|
+
this.#contextMap.set(key, value);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Reject when the request abort signal fires before the promise settles.
|
|
209
|
+
*
|
|
210
|
+
* This keeps long-running async work aligned with fetch semantics: once the
|
|
211
|
+
* request has been aborted, downstream work should stop surfacing successful
|
|
212
|
+
* results for it.
|
|
213
|
+
*/
|
|
214
|
+
function raceRequestAbort(promise, request) {
|
|
215
|
+
let signal = request.signal;
|
|
216
|
+
if (signal.aborted) throw signal.reason;
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
let onAbort = () => reject(signal.reason);
|
|
219
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
220
|
+
promise.then((value) => {
|
|
221
|
+
signal.removeEventListener("abort", onAbort);
|
|
222
|
+
resolve(value);
|
|
223
|
+
}, (error) => {
|
|
224
|
+
signal.removeEventListener("abort", onAbort);
|
|
225
|
+
reject(error);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Create a `waitUntil()` registry that keeps track of pending background work.
|
|
231
|
+
*
|
|
232
|
+
* Rejected tasks are logged and removed from the registry so shutdown can
|
|
233
|
+
* continue cleanly.
|
|
234
|
+
*/
|
|
235
|
+
function createWaitUntil() {
|
|
236
|
+
const promises = /* @__PURE__ */ new Set();
|
|
237
|
+
return {
|
|
238
|
+
waitUntil: (promise) => {
|
|
239
|
+
if (typeof promise?.then !== "function") return;
|
|
240
|
+
let tracked;
|
|
241
|
+
tracked = Promise.resolve(promise).catch(console.error).finally(() => promises.delete(tracked));
|
|
242
|
+
promises.add(tracked);
|
|
243
|
+
},
|
|
244
|
+
wait: () => {
|
|
245
|
+
return Promise.all(promises);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Normalize handlers so middleware runners can treat function and object forms
|
|
251
|
+
* uniformly.
|
|
252
|
+
*
|
|
253
|
+
* A bare handler becomes `{ handleRequest }`, while object handlers are returned
|
|
254
|
+
* unchanged.
|
|
255
|
+
*/
|
|
256
|
+
function toServerHandlerObject(handler) {
|
|
257
|
+
if (typeof handler === "function") return { handleRequest: handler };
|
|
258
|
+
return handler;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Type guard to check if a value conforms to the `ServerHandlerObject` shape.
|
|
262
|
+
*
|
|
263
|
+
* This is used to distinguish between a bare `ServerHandler` function and an
|
|
264
|
+
* object wrapper that may also carry per-route middleware.
|
|
265
|
+
*/
|
|
266
|
+
function isServerHandlerObject(value) {
|
|
267
|
+
return value != null && !Array.isArray(value) && typeof value === "object" && "handleRequest" in value && typeof value.handleRequest === "function";
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Execute middleware in sequence and then hand off to the terminal handler.
|
|
271
|
+
*
|
|
272
|
+
* Middleware may:
|
|
273
|
+
* - return a `Response` to short-circuit downstream execution
|
|
274
|
+
* - call `next()` and return its result
|
|
275
|
+
* - call `next()` without returning it, in which case the downstream response
|
|
276
|
+
* is still used
|
|
277
|
+
*
|
|
278
|
+
* If the terminal handler is a `ServerHandlerObject`, its own `middleware`
|
|
279
|
+
* array is executed after the outer middleware chain completes.
|
|
280
|
+
*/
|
|
281
|
+
function runMiddleware(context, middleware, terminal) {
|
|
282
|
+
let index = -1;
|
|
283
|
+
const dispatch = async (context, i) => {
|
|
284
|
+
if (i <= index) throw new Error("next() called multiple times");
|
|
285
|
+
index = i;
|
|
286
|
+
if (context.request.signal.aborted) throw context.request.signal.reason;
|
|
287
|
+
const fn = middleware[i];
|
|
288
|
+
if (!fn) {
|
|
289
|
+
const { middleware, handleRequest } = toServerHandlerObject(terminal);
|
|
290
|
+
if (middleware?.length) return runMiddleware(context, middleware, { handleRequest });
|
|
291
|
+
return await raceRequestAbort(Promise.resolve(handleRequest(context)), context.request);
|
|
292
|
+
}
|
|
293
|
+
let nextPromise;
|
|
294
|
+
let next = (nextContext) => {
|
|
295
|
+
nextPromise = dispatch(nextContext, i + 1);
|
|
296
|
+
return nextPromise;
|
|
297
|
+
};
|
|
298
|
+
let response = await raceRequestAbort(Promise.resolve(fn(context, next)), context.request);
|
|
299
|
+
if (response instanceof Response) return response;
|
|
300
|
+
if (nextPromise != null) return nextPromise;
|
|
301
|
+
return next(context);
|
|
302
|
+
};
|
|
303
|
+
return dispatch(context, 0);
|
|
304
|
+
}
|
|
305
|
+
function resolveRouteHandler(route, method) {
|
|
306
|
+
if (typeof route === "function" || isServerHandlerObject(route)) return route;
|
|
307
|
+
if (method && route[method]) return route[method];
|
|
308
|
+
if (method === "HEAD" && route.GET) return route.GET;
|
|
309
|
+
}
|
|
310
|
+
function resolveRouteMethods(route) {
|
|
311
|
+
if (typeof route === "function" || isServerHandlerObject(route)) return [];
|
|
312
|
+
const methods = Object.keys(route);
|
|
313
|
+
if (methods.includes("GET") && !methods.includes("HEAD")) methods.push("HEAD");
|
|
314
|
+
return methods;
|
|
315
|
+
}
|
|
316
|
+
function resolveRouteInput(input) {
|
|
317
|
+
if (typeof input === "string") try {
|
|
318
|
+
return { pathname: new URL(input).pathname };
|
|
319
|
+
} catch {
|
|
320
|
+
return { pathname: input };
|
|
321
|
+
}
|
|
322
|
+
if (input instanceof URL) return { pathname: input.pathname };
|
|
323
|
+
if (input instanceof Request) return {
|
|
324
|
+
pathname: new URL(input.url).pathname,
|
|
325
|
+
method: input.method
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
pathname: typeof input.url === "string" ? new URL(input.url).pathname : input.url.pathname,
|
|
329
|
+
method: input.method
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Compile a declarative `ServerRoutes` table into precedence buckets for fast
|
|
334
|
+
* request matching.
|
|
335
|
+
*
|
|
336
|
+
* The build step also rejects conflicting route shapes such as duplicate exact
|
|
337
|
+
* routes or parameter routes that collapse to the same canonical pattern.
|
|
338
|
+
*/
|
|
339
|
+
function unstable_buildRouteTree(routes) {
|
|
340
|
+
const tree = {
|
|
341
|
+
exact: /* @__PURE__ */ new Map(),
|
|
342
|
+
param: [],
|
|
343
|
+
wildcard: []
|
|
344
|
+
};
|
|
345
|
+
const seen = /* @__PURE__ */ new Map();
|
|
346
|
+
for (const [path, route] of Object.entries(routes)) {
|
|
347
|
+
const normalized = normalizePathname(path);
|
|
348
|
+
const canonical = canonicalizePath(normalized);
|
|
349
|
+
const previous = seen.get(canonical);
|
|
350
|
+
if (previous) throw new Error(`Conflicting routes: "${previous}" and "${path}" both resolve to "${canonical}".`);
|
|
351
|
+
seen.set(canonical, path);
|
|
352
|
+
const compiled = {
|
|
353
|
+
path: normalized,
|
|
354
|
+
route,
|
|
355
|
+
regexp: compilePath(normalized),
|
|
356
|
+
keys: extractKeys(normalized)
|
|
357
|
+
};
|
|
358
|
+
if (normalized === "/*") {
|
|
359
|
+
tree.catchAll = compiled;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (normalized.includes("*")) {
|
|
363
|
+
tree.wildcard.push(compiled);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (normalized.includes(":")) {
|
|
367
|
+
tree.param.push(compiled);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
tree.exact.set(normalized, route);
|
|
371
|
+
}
|
|
372
|
+
return tree;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Match a request-like input against a compiled route tree.
|
|
376
|
+
*
|
|
377
|
+
* `all` contains every pathname candidate in precedence order, while
|
|
378
|
+
* `matched` further filters that list by HTTP method and resolves the concrete
|
|
379
|
+
* handler that would run.
|
|
380
|
+
*/
|
|
381
|
+
function unstable_match(tree, input) {
|
|
382
|
+
const all = [];
|
|
383
|
+
const { pathname, method } = resolveRouteInput(input);
|
|
384
|
+
const normalized = normalizePathname(pathname);
|
|
385
|
+
const exact = tree.exact.get(normalized);
|
|
386
|
+
if (exact) all.push({
|
|
387
|
+
path: normalized,
|
|
388
|
+
route: exact,
|
|
389
|
+
params: {}
|
|
390
|
+
});
|
|
391
|
+
for (const compiled of tree.param) {
|
|
392
|
+
const result = compiled.regexp.exec(normalized);
|
|
393
|
+
if (!result) continue;
|
|
394
|
+
all.push({
|
|
395
|
+
path: compiled.path,
|
|
396
|
+
route: compiled.route,
|
|
397
|
+
params: extractParams(compiled.keys, result)
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
for (const compiled of tree.wildcard) {
|
|
401
|
+
if (!compiled.regexp.test(normalized)) continue;
|
|
402
|
+
all.push({
|
|
403
|
+
path: compiled.path,
|
|
404
|
+
route: compiled.route,
|
|
405
|
+
params: {}
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if (tree.catchAll && tree.catchAll.regexp.test(normalized)) all.push({
|
|
409
|
+
path: tree.catchAll.path,
|
|
410
|
+
route: tree.catchAll.route,
|
|
411
|
+
params: {}
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
all,
|
|
415
|
+
matched: all.flatMap((candidate) => {
|
|
416
|
+
const handler = resolveRouteHandler(candidate.route, method);
|
|
417
|
+
if (!handler) return [];
|
|
418
|
+
return [{
|
|
419
|
+
...candidate,
|
|
420
|
+
handler,
|
|
421
|
+
method
|
|
422
|
+
}];
|
|
423
|
+
})
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Turn a precompiled route tree into a `ServerHandler`.
|
|
428
|
+
*
|
|
429
|
+
* Pathname matches are resolved using Bun-style precedence. When a pathname
|
|
430
|
+
* matches but the method does not, the returned handler responds with `405`
|
|
431
|
+
* and an `Allow` header. If no route matches, `fallback` is used when
|
|
432
|
+
* provided; otherwise a `404` response is returned.
|
|
433
|
+
*/
|
|
434
|
+
function unstable_convertRoutesToHandler({ input, fallback, runRouteMiddleware }) {
|
|
435
|
+
return async (context) => {
|
|
436
|
+
const result = unstable_match(input, context.request);
|
|
437
|
+
const route = result.matched[0];
|
|
438
|
+
if (route) {
|
|
439
|
+
context = context.with({ params: route.params });
|
|
440
|
+
if (typeof route.handler === "function") return route.handler(context);
|
|
441
|
+
if (!route.handler.middleware?.length) return route.handler.handleRequest(context);
|
|
442
|
+
if (!runRouteMiddleware) throw new Error("Route handler middleware requires `runRouteMiddleware`.");
|
|
443
|
+
return runRouteMiddleware(context, route.handler.middleware, route.handler);
|
|
444
|
+
}
|
|
445
|
+
if (result.all.length > 0) {
|
|
446
|
+
const allow = /* @__PURE__ */ new Set();
|
|
447
|
+
for (const candidate of result.all) for (const method of resolveRouteMethods(candidate.route)) allow.add(method);
|
|
448
|
+
const headers = allow.size > 0 ? { Allow: [...allow].join(", ") } : void 0;
|
|
449
|
+
return new Response("Method Not Allowed", {
|
|
450
|
+
status: 405,
|
|
451
|
+
headers
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (fallback) return fallback(context);
|
|
455
|
+
return new Response("Not Found", { status: 404 });
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const emptyRouteParams = Object.freeze({});
|
|
459
|
+
/**
|
|
460
|
+
* Resolve the runtime adapter for the current process.
|
|
461
|
+
*
|
|
462
|
+
* The adapter is chosen lazily so the package can stay portable across Bun,
|
|
463
|
+
* Deno, and Node without importing runtime-specific code up front.
|
|
464
|
+
*/
|
|
465
|
+
async function loadServerAdapter() {
|
|
466
|
+
if (process.versions.bun) {
|
|
467
|
+
const { BunRuntimeAdapter } = await import("./bun.mjs");
|
|
468
|
+
return new BunRuntimeAdapter();
|
|
469
|
+
} else if (process.versions.deno) {
|
|
470
|
+
const { DenoRuntimeAdapter } = await import("./deno.mjs");
|
|
471
|
+
return new DenoRuntimeAdapter();
|
|
472
|
+
} else {
|
|
473
|
+
const { NodeRuntimeAdapter } = await import("./node.mjs");
|
|
474
|
+
return new NodeRuntimeAdapter();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Temporary adapter that rejects server operations until the real runtime
|
|
479
|
+
* adapter has been resolved asynchronously.
|
|
480
|
+
*/
|
|
481
|
+
var PlaceholderRuntimeAdapter = class {
|
|
482
|
+
#callback;
|
|
483
|
+
constructor(callback) {
|
|
484
|
+
this.#callback = callback;
|
|
485
|
+
}
|
|
486
|
+
get capabilities() {
|
|
487
|
+
throw new Error("Server runtime adapter is still initializing.");
|
|
488
|
+
}
|
|
489
|
+
setup() {
|
|
490
|
+
loadServerAdapter().then((adapter) => this.#callback(null, adapter), (error) => this.#callback(error, null));
|
|
491
|
+
}
|
|
492
|
+
async serve() {
|
|
493
|
+
throw new Error("Server runtime adapter is still initializing.");
|
|
494
|
+
}
|
|
495
|
+
async close() {}
|
|
496
|
+
};
|
|
497
|
+
/**
|
|
498
|
+
* Emitted after the server starts accepting requests.
|
|
499
|
+
*/
|
|
500
|
+
var ServerServeEvent = class extends Event {
|
|
501
|
+
constructor() {
|
|
502
|
+
super("serve");
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
/**
|
|
506
|
+
* Emitted after the server has fully closed.
|
|
507
|
+
*/
|
|
508
|
+
var ServerCloseEvent = class extends Event {
|
|
509
|
+
constructor() {
|
|
510
|
+
super("close");
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
/**
|
|
514
|
+
* Emitted when server startup or runtime handling raises an error.
|
|
515
|
+
*/
|
|
516
|
+
var ServerErrorEvent = class extends Event {
|
|
517
|
+
error;
|
|
518
|
+
constructor(error) {
|
|
519
|
+
super("error");
|
|
520
|
+
this.error = error;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
/**
|
|
524
|
+
* Convenience factory for creating a `Server` without `new`.
|
|
525
|
+
*/
|
|
526
|
+
function serve(init) {
|
|
527
|
+
return new Server(init);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Runtime-agnostic fetch server.
|
|
531
|
+
*
|
|
532
|
+
* `Server` owns middleware composition, per-invocation context setup, background
|
|
533
|
+
* task tracking, and runtime lifecycle coordination. Concrete adapters handle
|
|
534
|
+
* the host-specific details of listening for requests.
|
|
535
|
+
*/
|
|
536
|
+
var Server = class extends EventDispatcher {
|
|
537
|
+
/**
|
|
538
|
+
* Server options.
|
|
539
|
+
*/
|
|
540
|
+
options;
|
|
541
|
+
/**
|
|
542
|
+
* Register a background task that the server should await before closing.
|
|
543
|
+
*
|
|
544
|
+
* Same as `request.waitUntil` but available at the server level for use
|
|
545
|
+
* outside of request handlers.
|
|
546
|
+
*/
|
|
547
|
+
waitUntil;
|
|
548
|
+
#wait;
|
|
549
|
+
#kernel;
|
|
550
|
+
#adapter;
|
|
551
|
+
#url;
|
|
552
|
+
#adapterPromise;
|
|
553
|
+
#rejectAdapter;
|
|
554
|
+
#readyPromise;
|
|
555
|
+
#rejectReady;
|
|
556
|
+
#closePromise;
|
|
557
|
+
/**
|
|
558
|
+
* Create a server, apply plugins, prepare the runtime adapter, and optionally
|
|
559
|
+
* start listening immediately.
|
|
560
|
+
*/
|
|
561
|
+
constructor({ middleware = [], plugins = [], adapter, ...options }) {
|
|
562
|
+
super();
|
|
563
|
+
this.options = {
|
|
564
|
+
...options,
|
|
565
|
+
middleware: [...middleware]
|
|
566
|
+
};
|
|
567
|
+
for (const plugin of plugins) plugin(this);
|
|
568
|
+
errorPlugin(this);
|
|
569
|
+
if (!this.options.fetch) {
|
|
570
|
+
if (!this.options.routes || !this.options.routes["/*"]) throw new Error("Server requires either `fetch` or a `routes` table with `/*`.");
|
|
571
|
+
}
|
|
572
|
+
this.#wait = createWaitUntil();
|
|
573
|
+
this.waitUntil = this.#wait.waitUntil;
|
|
574
|
+
this.#kernel = () => {
|
|
575
|
+
throw new Error("Server request handler is not ready until the runtime adapter finishes initializing.");
|
|
576
|
+
};
|
|
577
|
+
const initializeAdapter = (adapter, resolve) => {
|
|
578
|
+
if (adapter.graceful) gracefulShutdownPlugin(this);
|
|
579
|
+
const kernel = wrapFetch(this);
|
|
580
|
+
adapter.setup(this);
|
|
581
|
+
this.#adapter = adapter;
|
|
582
|
+
this.#kernel = kernel;
|
|
583
|
+
if (!options.manual) this.#startServing();
|
|
584
|
+
resolve?.();
|
|
585
|
+
};
|
|
586
|
+
if (adapter) {
|
|
587
|
+
this.#adapter = adapter;
|
|
588
|
+
initializeAdapter(adapter);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
592
|
+
this.#adapterPromise = promise;
|
|
593
|
+
this.#rejectAdapter = reject;
|
|
594
|
+
promise.catch(() => {}).finally(() => {
|
|
595
|
+
this.#adapterPromise = void 0;
|
|
596
|
+
this.#rejectAdapter = void 0;
|
|
597
|
+
});
|
|
598
|
+
this.#adapter = new PlaceholderRuntimeAdapter((error, adapter) => {
|
|
599
|
+
if (adapter != null) initializeAdapter(adapter, resolve);
|
|
600
|
+
else {
|
|
601
|
+
reject(error);
|
|
602
|
+
if (!(error instanceof Error) || error.name !== "AbortError") this.dispatchEvent(new ServerErrorEvent(error));
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
this.#adapter.setup(this);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Listener URL reported by the runtime adapter after `serve()` succeeds.
|
|
609
|
+
*/
|
|
610
|
+
get url() {
|
|
611
|
+
return this.#url;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Create a `InvocationContext` view over an arbitrary `Request`.
|
|
615
|
+
*
|
|
616
|
+
* This preserves the upstream request object and pairs it with sevok
|
|
617
|
+
* invocation state such as runtime capabilities, `waitUntil`, and `params`.
|
|
618
|
+
*/
|
|
619
|
+
createContext(request, params) {
|
|
620
|
+
return new InvocationContext({
|
|
621
|
+
request,
|
|
622
|
+
capabilities: this.#adapter.capabilities,
|
|
623
|
+
waitUntil: this.#wait?.waitUntil,
|
|
624
|
+
params
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Invoke the composed request pipeline directly.
|
|
629
|
+
*/
|
|
630
|
+
handle(context) {
|
|
631
|
+
if (this.#adapterPromise) return this.#adapterPromise.then(() => this.#kernel(context));
|
|
632
|
+
return this.#kernel(context);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Adapt an arbitrary `Request` into an `InvocationContext` and invoke the handler pipeline.
|
|
636
|
+
*/
|
|
637
|
+
async fetch(request) {
|
|
638
|
+
if (this.#adapterPromise) await this.#adapterPromise;
|
|
639
|
+
return await this.handle(this.createContext(request));
|
|
640
|
+
}
|
|
641
|
+
#startServing() {
|
|
642
|
+
if (this.#readyPromise) return;
|
|
643
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
644
|
+
this.#readyPromise = promise;
|
|
645
|
+
this.#rejectReady = reject;
|
|
646
|
+
this.#url = void 0;
|
|
647
|
+
this.#adapter.serve(this).then((info) => {
|
|
648
|
+
this.#url = info?.url;
|
|
649
|
+
this.dispatchEvent(new ServerServeEvent());
|
|
650
|
+
resolve();
|
|
651
|
+
}, reject);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Start the runtime adapter if it has not been started already.
|
|
655
|
+
*
|
|
656
|
+
* The returned promise resolves once the adapter reports the final listener
|
|
657
|
+
* URL. Repeated calls reuse the same in-flight startup.
|
|
658
|
+
*/
|
|
659
|
+
async serve() {
|
|
660
|
+
if (this.#readyPromise) return;
|
|
661
|
+
if (this.#adapterPromise) await this.#adapterPromise;
|
|
662
|
+
this.#startServing();
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Wait until the server has completed startup and then return the instance.
|
|
666
|
+
*
|
|
667
|
+
* This is mainly a convenience for fluent startup code.
|
|
668
|
+
*/
|
|
669
|
+
async ready() {
|
|
670
|
+
if (this.#adapterPromise) await this.#adapterPromise;
|
|
671
|
+
if (!this.#readyPromise) throw new Error("Server has not been started. Call serve() first.");
|
|
672
|
+
return Promise.resolve(this.#readyPromise).then(() => this);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Close the runtime adapter and wait for outstanding `waitUntil()` tasks.
|
|
676
|
+
*
|
|
677
|
+
* If startup was still in progress, `ready()` is rejected with an
|
|
678
|
+
* `AbortError`.
|
|
679
|
+
*/
|
|
680
|
+
async close(closeActiveConnections = false) {
|
|
681
|
+
if (this.#closePromise) return this.#closePromise;
|
|
682
|
+
const finalizeClose = async () => {
|
|
683
|
+
try {
|
|
684
|
+
if (this.#rejectAdapter) {
|
|
685
|
+
const error = /* @__PURE__ */ new Error("Server closed before adapter initialization completed.");
|
|
686
|
+
error.name = "AbortError";
|
|
687
|
+
this.#rejectAdapter(error);
|
|
688
|
+
}
|
|
689
|
+
await Promise.all([this.#adapter.close(closeActiveConnections), this.#wait?.wait()]);
|
|
690
|
+
} finally {
|
|
691
|
+
if (this.#rejectReady) {
|
|
692
|
+
const error = /* @__PURE__ */ new Error("Server closed before becoming ready.");
|
|
693
|
+
error.name = "AbortError";
|
|
694
|
+
this.#rejectReady(error);
|
|
695
|
+
this.#rejectReady = void 0;
|
|
696
|
+
}
|
|
697
|
+
this.dispatchEvent(new ServerCloseEvent());
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
this.#closePromise = finalizeClose().finally(() => {
|
|
701
|
+
this.#readyPromise = void 0;
|
|
702
|
+
this.#closePromise = void 0;
|
|
703
|
+
});
|
|
704
|
+
return this.#closePromise;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
/**
|
|
708
|
+
* Compose fetch handler, middleware, and optional error handling into the
|
|
709
|
+
* single request kernel used by `Server`.
|
|
710
|
+
*/
|
|
711
|
+
function wrapFetch(server) {
|
|
712
|
+
const fetchHandler = server.options.fetch;
|
|
713
|
+
const routes = server.options.routes;
|
|
714
|
+
const middleware = server.options.middleware || [];
|
|
715
|
+
let handler;
|
|
716
|
+
if (!routes) {
|
|
717
|
+
if (!fetchHandler) throw new Error("Server requires either `routes` or `fetch`.");
|
|
718
|
+
handler = fetchHandler;
|
|
719
|
+
} else {
|
|
720
|
+
if (!routes["/*"]) {
|
|
721
|
+
if (!fetchHandler) throw new Error("Route tables without `/*` require a fallback `fetch` handler.");
|
|
722
|
+
}
|
|
723
|
+
handler = unstable_convertRoutesToHandler({
|
|
724
|
+
input: unstable_buildRouteTree(routes),
|
|
725
|
+
fallback: fetchHandler,
|
|
726
|
+
runRouteMiddleware: runMiddleware
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
return middleware.length === 0 ? handler : (context) => runMiddleware(context, middleware, handler);
|
|
730
|
+
}
|
|
731
|
+
//#endregion
|
|
732
|
+
export { unstable_match as _, ServerServeEvent as a, createWaitUntil as c, raceRequestAbort as d, runMiddleware as f, unstable_convertRoutesToHandler as g, unstable_buildRouteTree as h, ServerErrorEvent as i, isServerHandlerObject as l, toServerHandlerObject as m, Server as n, core_exports as o, serve as p, ServerCloseEvent as r, createContextKey as s, InvocationContext as t, loadServerAdapter as u, wrapFetch as v };
|