weifuwu 0.30.0 → 0.30.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/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var HttpError = class extends Error {
9
9
  };
10
10
 
11
11
  // src/core/trace.ts
12
- import crypto2 from "node:crypto";
12
+ import crypto from "node:crypto";
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
14
  var als = new AsyncLocalStorage();
15
15
  function currentTraceId() {
@@ -19,7 +19,7 @@ function currentTrace() {
19
19
  return als.getStore();
20
20
  }
21
21
  function runWithTrace(incomingTraceId, fn) {
22
- const traceId = incomingTraceId || crypto2.randomUUID();
22
+ const traceId = incomingTraceId || crypto.randomUUID();
23
23
  const startTime = Date.now();
24
24
  return als.run({ traceId, startTime }, fn);
25
25
  }
@@ -30,14 +30,14 @@ function traceElapsed() {
30
30
  }
31
31
  function trace(options) {
32
32
  const header = options?.header ?? "X-Request-ID";
33
- const gen = options?.generator ?? (() => crypto2.randomUUID());
33
+ const gen = options?.generator ?? (() => crypto.randomUUID());
34
34
  return async (req, ctx, next) => {
35
35
  const existing = req.headers.get(header);
36
- const requestId2 = existing ?? gen();
36
+ const requestId = existing ?? gen();
37
37
  const tc = als.getStore();
38
38
  ctx.trace = {
39
- requestId: requestId2,
40
- traceId: tc?.traceId ?? requestId2,
39
+ requestId,
40
+ traceId: tc?.traceId ?? requestId,
41
41
  startTime: tc?.startTime ?? Date.now(),
42
42
  elapsed: () => {
43
43
  const t = als.getStore();
@@ -47,71 +47,11 @@ function trace(options) {
47
47
  const res = await next(req, ctx);
48
48
  if (res.headers.has(header)) return res;
49
49
  const h = new Headers(res.headers);
50
- h.set(header, requestId2);
50
+ h.set(header, requestId);
51
51
  return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h });
52
52
  };
53
53
  }
54
54
 
55
- // src/core/env.ts
56
- import { readFileSync } from "node:fs";
57
- import { resolve } from "node:path";
58
- var PUBLIC_PREFIX = "WEIFUWU_PUBLIC_";
59
- function getPublicEnv() {
60
- const result = {};
61
- for (const [key, value] of Object.entries(process.env)) {
62
- if (key.startsWith(PUBLIC_PREFIX) && value !== void 0) {
63
- result[key.slice(PUBLIC_PREFIX.length)] = value;
64
- }
65
- }
66
- return result;
67
- }
68
- function isBundled() {
69
- return typeof __WFW_BUNDLED__ !== "undefined" ? __WFW_BUNDLED__ : false;
70
- }
71
- function isDev() {
72
- const env2 = process.env.NODE_ENV;
73
- return env2 !== "production" && env2 !== "test";
74
- }
75
- function isProd() {
76
- return process.env.NODE_ENV === "production";
77
- }
78
- function loadEnv(path) {
79
- const filePath = resolve(process.cwd(), path ?? ".env");
80
- let content;
81
- try {
82
- content = readFileSync(filePath, "utf-8");
83
- } catch {
84
- return;
85
- }
86
- for (const line of content.split("\n")) {
87
- const trimmed = line.trim();
88
- if (!trimmed || trimmed.startsWith("#")) continue;
89
- const eqIdx = trimmed.indexOf("=");
90
- if (eqIdx === -1) continue;
91
- const key = trimmed.slice(0, eqIdx).trim();
92
- if (!key) continue;
93
- if (process.env[key] !== void 0) continue;
94
- let value = trimmed.slice(eqIdx + 1).trim();
95
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
96
- value = value.slice(1, -1);
97
- } else {
98
- const commentIdx = value.search(/\s#/);
99
- if (commentIdx !== -1) {
100
- value = value.slice(0, commentIdx).trimEnd();
101
- }
102
- }
103
- process.env[key] = value;
104
- }
105
- }
106
- function env() {
107
- const entries = getPublicEnv();
108
- return async (req, ctx, next) => {
109
- ;
110
- ctx.env = entries;
111
- return next(req, ctx);
112
- };
113
- }
114
-
115
55
  // src/core/serve.ts
116
56
  import http from "node:http";
117
57
  var DEFAULT_MAX_BODY = 10 * 1024 * 1024;
@@ -180,11 +120,6 @@ async function sendResponse(res, response, opts) {
180
120
  }
181
121
  res.end();
182
122
  }
183
- async function createTestServer(router, options) {
184
- const server = serve(router, { ...options, port: options?.port ?? 0, shutdown: false });
185
- await server.ready;
186
- return { server, url: `http://localhost:${server.port}` };
187
- }
188
123
  function serve(router, options) {
189
124
  const ws = router.websocketHandler();
190
125
  const handler = router.handler();
@@ -204,9 +139,6 @@ function serve(router, options) {
204
139
  res.end("Request Body Too Large");
205
140
  return;
206
141
  }
207
- const msg = err instanceof Error ? err.message : String(err);
208
- console.error(`[${currentTraceId()}] unhandled error: ${msg}`);
209
- if (err instanceof Error && err.stack) console.error(err.stack);
210
142
  res.writeHead(500, { "Content-Type": "text/plain" });
211
143
  res.end("Internal Server Error");
212
144
  }
@@ -269,7 +201,7 @@ function serve(router, options) {
269
201
  );
270
202
  }
271
203
  server.on("error", (err) => {
272
- console.error("Failed to start server:", err.message);
204
+ throw err;
273
205
  server.close();
274
206
  _cachedPort = 0;
275
207
  resolveReady();
@@ -293,14 +225,14 @@ function serve(router, options) {
293
225
  if (!server.listening) return;
294
226
  server.close();
295
227
  server.closeIdleConnections();
296
- return new Promise((resolve4) => {
228
+ return new Promise((resolve2) => {
297
229
  const timer = setTimeout(() => {
298
230
  server.closeAllConnections();
299
- resolve4();
231
+ resolve2();
300
232
  }, timeoutMs);
301
233
  server.on("close", () => {
302
234
  clearTimeout(timer);
303
- resolve4();
235
+ resolve2();
304
236
  });
305
237
  });
306
238
  }
@@ -345,7 +277,7 @@ function createHub(opts) {
345
277
  }
346
278
  });
347
279
  }
348
- function join3(key, ws) {
280
+ function join2(key, ws) {
349
281
  if (!channels.has(key)) {
350
282
  channels.set(key, /* @__PURE__ */ new Set());
351
283
  redisSub?.subscribe(`${prefix}${key}`);
@@ -407,7 +339,97 @@ function createHub(opts) {
407
339
  redisPub = void 0;
408
340
  redisSub = null;
409
341
  }
410
- return { join: join3, leave, broadcast, close };
342
+ return { join: join2, leave, broadcast, close };
343
+ }
344
+
345
+ // src/core/ws.ts
346
+ function nodeReqHeadersToRecord(headers) {
347
+ const result = {};
348
+ for (const [k, v] of Object.entries(headers)) {
349
+ if (v !== void 0) result[k] = Array.isArray(v) ? v.join(", ") : v;
350
+ }
351
+ return result;
352
+ }
353
+ function sendHttpResponseOnSocket(socket, response) {
354
+ const statusLine = `HTTP/1.1 ${response.status} ${response.statusText}`;
355
+ const lines = [statusLine];
356
+ response.headers.forEach((v, k) => lines.push(`${k}: ${v}`));
357
+ lines.push("Connection: close", "");
358
+ const headerStr = lines.join("\r\n");
359
+ response.arrayBuffer().then((buf) => {
360
+ socket.write(headerStr + "\r\n");
361
+ if (buf.byteLength > 0) socket.write(Buffer.from(buf));
362
+ socket.end();
363
+ }).catch(() => {
364
+ socket.write(headerStr + "\r\n");
365
+ socket.end();
366
+ });
367
+ }
368
+ function upgradeSocket(wss, req, socket, head, handler, ctx, hub) {
369
+ wss.handleUpgrade(req, socket, head, (ws) => {
370
+ const connCtx = { ...ctx, params: { ...ctx.params }, query: { ...ctx.query } };
371
+ const wsState = {};
372
+ connCtx.ws = {
373
+ get state() {
374
+ return wsState;
375
+ },
376
+ json(data) {
377
+ ws.send(JSON.stringify(data));
378
+ },
379
+ join(room) {
380
+ hub.join(room, ws);
381
+ },
382
+ leave() {
383
+ hub.leave(ws);
384
+ },
385
+ sendRoom(room, data) {
386
+ hub.broadcast(room, data);
387
+ }
388
+ };
389
+ handler.open?.(ws, connCtx);
390
+ ws.on("message", (data) => handler.message?.(ws, connCtx, data));
391
+ ws.on("close", () => {
392
+ hub.leave(ws);
393
+ handler.close?.(ws, connCtx);
394
+ });
395
+ ws.on("error", (err) => handler.error?.(ws, connCtx, err));
396
+ });
397
+ }
398
+ function createWsUpgradeHandler(wss, hub, matchWsFn, globalMws, runChainFn) {
399
+ return (req, socket, head) => {
400
+ const url = new URL(req.url ?? "/", "http://localhost");
401
+ const segments = url.pathname.split("/").filter(Boolean);
402
+ const match = matchWsFn(segments);
403
+ if (!match) {
404
+ socket.destroy();
405
+ return;
406
+ }
407
+ const query = Object.fromEntries(url.searchParams);
408
+ const ctx = { params: match.params, query };
409
+ const mws = globalMws.length === 0 && match.middlewares.length === 0 ? [] : [...globalMws, ...match.middlewares];
410
+ if (mws.length === 0) {
411
+ upgradeSocket(wss, req, socket, head, match.handler, ctx, hub);
412
+ return;
413
+ }
414
+ const finalHandler = () => {
415
+ try {
416
+ upgradeSocket(wss, req, socket, head, match.handler, ctx, hub);
417
+ } catch {
418
+ socket.destroy();
419
+ return new Response("WebSocket upgrade failed", { status: 500 });
420
+ }
421
+ return new Response(null, { status: 200 });
422
+ };
423
+ const webReq = new Request(url.href, {
424
+ method: req.method ?? "GET",
425
+ headers: nodeReqHeadersToRecord(req.headers)
426
+ });
427
+ void runChainFn(mws, finalHandler, webReq, ctx).then((result) => {
428
+ if (result.status >= 400) sendHttpResponseOnSocket(socket, result);
429
+ }).catch(() => {
430
+ socket.destroy();
431
+ });
432
+ };
411
433
  }
412
434
 
413
435
  // src/core/router.ts
@@ -462,7 +484,6 @@ var Router = class _Router {
462
484
  _hasWildcard = false;
463
485
  _hub;
464
486
  _wss;
465
- /** Track which ctx fields have been injected so far (for dependency checking). */
466
487
  _ctxFields = /* @__PURE__ */ new Set();
467
488
  get wss() {
468
489
  if (!this._wss) this._wss = new WebSocketServer({ noServer: true });
@@ -472,90 +493,88 @@ var Router = class _Router {
472
493
  if (!this._hub) this._hub = createHub();
473
494
  return this._hub;
474
495
  }
475
- /** Inject a custom hub (e.g. with Redis for cross-process broadcast). */
476
496
  wsHub(hub) {
477
497
  this._hub = hub;
478
498
  return this;
479
499
  }
480
- use(arg1) {
481
- this.globalMws.push(arg1);
482
- this._checkMiddlewareMeta(arg1, "global");
500
+ // ── Middleware & mounting ─────────────────────────────────
501
+ use(mw) {
502
+ this.globalMws.push(mw);
503
+ this._checkMiddlewareMeta(mw, "global");
483
504
  return this;
484
505
  }
485
- /**
486
- * Mount a sub-router at the given path prefix.
487
- * All routes from the sub-router are registered with the prefix.
488
- *
489
- * ```ts
490
- * const admin = new Router()
491
- * admin.get('/dashboard', handler)
492
- * app.mount('/admin', admin) // → GET /admin/dashboard
493
- * ```
494
- */
495
506
  mount(path, router) {
496
507
  this._mountRouter(path, router);
497
508
  return this;
498
509
  }
499
- /**
500
- * Check a middleware's dependency metadata and emit warnings if
501
- * required fields haven't been injected yet.
502
- * Attach __meta to a middleware function:
503
- *
504
- * ```ts
505
- * mw.__meta = { injects: ['sql'], depends: ['session'] }
506
- * ```
507
- */
508
- _checkMiddlewareMeta(mw, location) {
509
- const meta = mw.__meta ?? (typeof mw === "object" && mw && "middleware" in mw ? mw.middleware().__meta : void 0);
510
- if (!meta) return;
511
- for (const dep of meta.depends) {
512
- if (!this._ctxFields.has(dep)) {
513
- console.warn(
514
- `[weifuwu] Middleware at "${location}" depends on ctx.${dep} but it hasn't been registered yet.
515
- Register the provider before this middleware:
516
- app.use(${dep}()) // add before this middleware
517
- Current ctx fields: [${[...this._ctxFields].join(", ")}]`
518
- );
519
- }
520
- }
521
- for (const field of meta.injects) {
522
- this._ctxFields.add(field);
523
- }
510
+ onError(handler) {
511
+ this.errorHandler = handler;
512
+ return this;
524
513
  }
525
- // Route registration — returns Router<T> unchanged.
526
- // Route-level middleware and handlers get Context<T>.
527
- get(path, ...args) {
528
- return this._route("GET", path, ...args);
514
+ // ── Route registration ────────────────────────────────────
515
+ get(path, ...rest) {
516
+ return this._route("GET", path, ...rest);
529
517
  }
530
- post(path, ...args) {
531
- return this._route("POST", path, ...args);
518
+ post(path, ...rest) {
519
+ return this._route("POST", path, ...rest);
532
520
  }
533
- put(path, ...args) {
534
- return this._route("PUT", path, ...args);
521
+ put(path, ...rest) {
522
+ return this._route("PUT", path, ...rest);
535
523
  }
536
- delete(path, ...args) {
537
- return this._route("DELETE", path, ...args);
524
+ delete(path, ...rest) {
525
+ return this._route("DELETE", path, ...rest);
538
526
  }
539
- patch(path, ...args) {
540
- return this._route("PATCH", path, ...args);
527
+ patch(path, ...rest) {
528
+ return this._route("PATCH", path, ...rest);
541
529
  }
542
- head(path, ...args) {
543
- return this._route("HEAD", path, ...args);
530
+ head(path, ...rest) {
531
+ return this._route("HEAD", path, ...rest);
544
532
  }
545
- options(path, ...args) {
546
- return this._route("OPTIONS", path, ...args);
533
+ options(path, ...rest) {
534
+ return this._route("OPTIONS", path, ...rest);
547
535
  }
548
- all(path, ...args) {
549
- return this._route("*", path, ...args);
536
+ all(path, ...rest) {
537
+ return this._route("*", path, ...rest);
550
538
  }
551
- onError(handler) {
552
- this.errorHandler = handler;
539
+ ws(path, ...args) {
540
+ const handler = args.pop();
541
+ const mws = args;
542
+ let node = this.wsRoot;
543
+ for (const segment of this.splitPath(path)) {
544
+ node = getOrCreateChild(node, segment, createWsNode, true);
545
+ }
546
+ node.handler = handler;
547
+ node.middlewares = mws;
553
548
  return this;
554
549
  }
550
+ // ── Handler compilation ────────────────────────────────────
551
+ handler() {
552
+ return (req, ctx) => {
553
+ const url = new URL(req.url);
554
+ return this.handle(req, ctx, this.splitPath(url.pathname));
555
+ };
556
+ }
557
+ websocketHandler() {
558
+ return createWsUpgradeHandler(
559
+ this.wss,
560
+ this.hub,
561
+ (segments) => this.matchWsTrie(this.wsRoot, segments),
562
+ this.globalMws,
563
+ (mws, h, req, ctx) => this.runChain(mws, h, req, ctx)
564
+ );
565
+ }
566
+ // ── Debug ──────────────────────────────────────────────────
567
+ routes() {
568
+ const result = [];
569
+ if (this.globalMws.length > 0) result.push(`MIDDLEWARE [${this.globalMws.length} global]`);
570
+ this._collectRoutes(this.root, "", result);
571
+ this._collectWsRoutes(this.wsRoot, "", result);
572
+ return result;
573
+ }
574
+ // ── Private: Route impl ────────────────────────────────────
555
575
  _route(method, path, ...args) {
556
576
  return this._routeImpl(method, path, args);
557
577
  }
558
- /** Internal route registration — no type constraints (used by _mountRouter). */
559
578
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
560
579
  _routeImpl(method, path, args) {
561
580
  const last = args[args.length - 1];
@@ -564,123 +583,26 @@ var Router = class _Router {
564
583
  return this;
565
584
  }
566
585
  const handler = args.pop();
567
- const middlewares = args;
568
- const segments = this.splitPath(path);
586
+ const mws = args;
569
587
  let node = this.root;
570
- for (const segment of segments) {
588
+ for (const segment of this.splitPath(path)) {
571
589
  if (segment === "*") {
572
590
  this._hasWildcard = true;
573
- const remaining = segments.indexOf("*") < segments.length - 1;
574
- if (remaining) {
575
- console.warn(`Route "${path}": segments after "*" are ignored`);
576
- }
577
591
  node.wildcard = true;
578
592
  node.handlers.set(method, handler);
579
- if (middlewares.length > 0) node.middlewares.set(method, middlewares);
593
+ if (mws.length > 0) node.middlewares.set(method, mws);
580
594
  return this;
581
595
  }
582
596
  node = getOrCreateChild(node, segment, createTrieNode, false);
583
597
  }
584
- if (!isProd() && node.handlers.has(method)) {
585
- console.warn(`[router] route conflict: ${method} ${path} overwrites existing handler`);
598
+ if (node.handlers.has(method)) {
599
+ throw new Error(`[router] route conflict: ${method} ${path} already registered`);
586
600
  }
587
601
  node.handlers.set(method, handler);
588
- if (middlewares.length > 0) node.middlewares.set(method, middlewares);
589
- return this;
590
- }
591
- ws(path, ...args) {
592
- const handler = args.pop();
593
- const middlewares = args;
594
- const segments = this.splitPath(path);
595
- let node = this.wsRoot;
596
- for (const segment of segments) {
597
- node = getOrCreateChild(node, segment, createWsNode, true);
598
- }
599
- node.handler = handler;
600
- node.middlewares = middlewares;
602
+ if (mws.length > 0) node.middlewares.set(method, mws);
601
603
  return this;
602
604
  }
603
- handler() {
604
- return (req, ctx) => {
605
- const url = new URL(req.url);
606
- return this.handle(req, ctx, this.splitPath(url.pathname));
607
- };
608
- }
609
- /** Returns a human-readable list of all registered routes. Useful for debugging. */
610
- routes() {
611
- const result = [];
612
- if (this.globalMws.length > 0) {
613
- result.push(`MIDDLEWARE [${this.globalMws.length} global]`);
614
- }
615
- this._collectRoutes(this.root, "", result);
616
- this._collectWsRoutes(this.wsRoot, "", result);
617
- return result;
618
- }
619
- _collectRoutes(node, prefix, result) {
620
- for (const [method] of node.handlers) {
621
- const m = method === "*" ? "ANY" : method;
622
- const path = (prefix || "/") + (node.wildcard ? "/*" : "");
623
- const middlewares = node.middlewares.get(method);
624
- const mwCount = middlewares ? ` (+${middlewares.length} mw)` : "";
625
- result.push(`${m.padEnd(7)} ${path}${mwCount}`);
626
- }
627
- for (const [seg, child] of node.children) {
628
- const segment = seg === ":" ? `:${child.param}` : seg;
629
- this._collectRoutes(child, prefix + "/" + segment, result);
630
- }
631
- }
632
- _collectWsRoutes(node, prefix, result) {
633
- if (node.handler) {
634
- const path = prefix || "/";
635
- const mwCount = node.middlewares.length ? ` (+${node.middlewares.length} mw)` : "";
636
- result.push(`WS ${path}${mwCount}`);
637
- }
638
- for (const [seg, child] of node.children) {
639
- const segment = seg === ":" ? `:${child.param}` : seg;
640
- this._collectWsRoutes(child, prefix + "/" + segment, result);
641
- }
642
- }
643
- websocketHandler() {
644
- const wsRoot = this.wsRoot;
645
- const router = this;
646
- return (req, socket, head) => {
647
- const url = new URL(req.url ?? "/", "http://localhost");
648
- const segments = url.pathname.split("/").filter(Boolean);
649
- const match = router.matchWsTrie(wsRoot, segments);
650
- if (!match) {
651
- socket.destroy();
652
- return;
653
- }
654
- const query = Object.fromEntries(url.searchParams);
655
- const ctx = { params: match.params, query };
656
- const allMws = router.globalMws.length === 0 && match.middlewares.length === 0 ? [] : [...router.globalMws, ...match.middlewares];
657
- if (allMws.length === 0) {
658
- upgradeSocket(router.wss, req, socket, head, match.handler, ctx, router.hub);
659
- return;
660
- }
661
- const finalHandler = () => {
662
- try {
663
- upgradeSocket(router.wss, req, socket, head, match.handler, ctx, router.hub);
664
- } catch {
665
- socket.destroy();
666
- return new Response("WebSocket upgrade failed", { status: 500 });
667
- }
668
- return new Response(null, { status: 200 });
669
- };
670
- const webReq = new Request(url.href, {
671
- method: req.method ?? "GET",
672
- headers: nodeReqHeadersToRecord(req.headers)
673
- });
674
- void router.runChain(allMws, finalHandler, webReq, ctx).then((result) => {
675
- if (result.status >= 400) {
676
- sendHttpResponseOnSocket(socket, result);
677
- }
678
- }).catch((err) => {
679
- console.error("[router] WS middleware chain error:", err);
680
- socket.destroy();
681
- });
682
- };
683
- }
605
+ // ── Private: Mount ─────────────────────────────────────────
684
606
  _mountRouter(prefix, sub, extraMws = []) {
685
607
  const base = prefix === "/" ? "" : prefix.replace(/\/$/, "");
686
608
  const mountMw = (req, ctx, next) => {
@@ -696,92 +618,96 @@ var Router = class _Router {
696
618
  const wsRoutes = [];
697
619
  this._collectWs(sub.wsRoot, "", wsRoutes);
698
620
  for (const { path, handler, middlewares } of wsRoutes) {
699
- this.ws(
700
- base + path,
701
- ...allExtra,
702
- ...middlewares,
703
- handler
704
- );
621
+ this.ws(base + path, ...allExtra, ...middlewares, handler);
705
622
  }
706
623
  }
707
624
  _collect(node, prefix, result) {
708
625
  for (const [method, handler] of node.handlers) {
709
626
  const rmws = node.middlewares.get(method) || [];
710
627
  const suffix = node.wildcard ? "/*" : "";
711
- result.push({
712
- method,
713
- path: (prefix || "/") + suffix,
714
- handler,
715
- middlewares: [...rmws]
716
- });
628
+ result.push({ method, path: (prefix || "/") + suffix, handler, middlewares: [...rmws] });
717
629
  }
718
630
  for (const [seg, child] of node.children) {
719
- const next = seg === ":" ? `/:${child.param}` : `/${seg}`;
720
- this._collect(child, prefix + next, result);
631
+ this._collect(child, prefix + "/" + (seg === ":" ? `:${child.param}` : seg), result);
721
632
  }
722
633
  }
723
634
  _collectWs(node, prefix, result, mwsAcc = []) {
724
635
  const mws = [...mwsAcc, ...node.middlewares];
725
636
  if (node.handler) result.push({ path: prefix || "/", handler: node.handler, middlewares: mws });
726
637
  for (const [seg, child] of node.children) {
727
- const next = seg === ":" ? `/:${child.param}` : `/${seg}`;
728
- this._collectWs(child, prefix + next, result, mws);
638
+ this._collectWs(child, prefix + "/" + (seg === ":" ? `:${child.param}` : seg), result, mws);
729
639
  }
730
640
  }
641
+ // ── Private: Matching ──────────────────────────────────────
731
642
  splitPath(path) {
732
643
  return path.split("/").filter(Boolean);
733
644
  }
645
+ _collectRoutes(node, prefix, result) {
646
+ for (const [method] of node.handlers) {
647
+ const m = method === "*" ? "ANY" : method;
648
+ const path = (prefix || "/") + (node.wildcard ? "/*" : "");
649
+ const middlewares = node.middlewares.get(method);
650
+ const mwCount = middlewares ? ` (+${middlewares.length} mw)` : "";
651
+ result.push(`${m.padEnd(7)} ${path}${mwCount}`);
652
+ }
653
+ for (const [seg, child] of node.children) {
654
+ const segment = seg === ":" ? `:${child.param}` : seg;
655
+ this._collectRoutes(child, prefix + "/" + segment, result);
656
+ }
657
+ }
658
+ _collectWsRoutes(node, prefix, result) {
659
+ if (node.handler) {
660
+ const path = prefix || "/";
661
+ const mwCount = node.middlewares.length ? ` (+${node.middlewares.length} mw)` : "";
662
+ result.push(`WS ${path}${mwCount}`);
663
+ }
664
+ for (const [seg, child] of node.children) {
665
+ const segment = seg === ":" ? `:${child.param}` : seg;
666
+ this._collectWsRoutes(child, prefix + "/" + segment, result);
667
+ }
668
+ }
734
669
  matchTrie(method, segments) {
735
670
  let node = this.root;
736
671
  const params = {};
737
- let wildcardHandler = null;
738
- let wildcardMws = [];
739
- let wildcardIdx = -1;
672
+ for (const seg of segments) {
673
+ const next = matchChild(node, seg, params, false);
674
+ if (!next) return this._wildcardMatch(method, segments);
675
+ node = next;
676
+ }
677
+ return this._resolveMatch(node, method, params, segments.length);
678
+ }
679
+ _wildcardMatch(method, segments) {
680
+ if (!this._hasWildcard) return null;
681
+ let node = this.root;
682
+ const params = {};
740
683
  for (let i = 0; i < segments.length; i++) {
741
- if (this._hasWildcard && node.wildcard) {
742
- let h = node.handlers.get("*") || node.handlers.get(method);
743
- if (!h && method === "HEAD") {
744
- h = node.handlers.get("GET");
745
- }
684
+ if (node.wildcard) {
685
+ const h = node.handlers.get("*") || node.handlers.get(method) || (method === "HEAD" ? node.handlers.get("GET") : void 0);
746
686
  if (h) {
747
- wildcardHandler = h;
748
- wildcardMws = node.middlewares.get(method) || node.middlewares.get("*") || [];
749
- wildcardIdx = i;
687
+ params["*"] = segments.slice(i).join("/");
688
+ return { kind: "route", handler: h, mws: node.middlewares.get(method) || node.middlewares.get("*") || [], params };
750
689
  }
751
690
  }
752
- const segment = segments[i];
753
- const next = matchChild(node, segment, params, false);
754
- if (!next) {
755
- if (wildcardHandler) {
756
- params["*"] = segments.slice(wildcardIdx).join("/");
757
- return { kind: "route", handler: wildcardHandler, mws: wildcardMws, params };
758
- }
759
- return null;
760
- }
691
+ const next = matchChild(node, segments[i], params, false);
692
+ if (!next) return null;
761
693
  node = next;
762
694
  }
695
+ return null;
696
+ }
697
+ _resolveMatch(node, method, params, _segLen) {
763
698
  let handler = node.handlers.get(method) || node.handlers.get("*");
764
- if (!handler && method === "HEAD") {
765
- handler = node.handlers.get("GET");
766
- }
699
+ if (!handler && method === "HEAD") handler = node.handlers.get("GET");
700
+ if (node.wildcard) params["*"] = "";
767
701
  if (handler) {
768
- if (node.wildcard) params["*"] = segments.slice(segments.length).join("/");
769
- return {
770
- kind: "route",
771
- handler,
772
- mws: node.middlewares.get(method) || node.middlewares.get("*") || [],
773
- params
774
- };
775
- }
776
- if (wildcardHandler) {
777
- params["*"] = segments.slice(wildcardIdx).join("/");
778
- return { kind: "route", handler: wildcardHandler, mws: wildcardMws, params };
702
+ return { kind: "route", handler, mws: node.middlewares.get(method) || node.middlewares.get("*") || [], params };
779
703
  }
780
704
  if (node.handlers.size > 0) {
781
705
  return {
782
706
  kind: "not-allowed",
783
- methods: [...node.handlers.keys()].filter((k) => k !== "*"),
784
- params
707
+ handler: () => new Response("", { status: 405 }),
708
+ mws: [],
709
+ params,
710
+ methods: [...node.handlers.keys()].filter((k) => k !== "*")
785
711
  };
786
712
  }
787
713
  return null;
@@ -789,167 +715,89 @@ var Router = class _Router {
789
715
  matchWsTrie(root, segments) {
790
716
  let node = root;
791
717
  const params = {};
792
- for (const segment of segments) {
793
- const next = matchChild(node, segment, params, true);
718
+ for (const seg of segments) {
719
+ const next = matchChild(node, seg, params, true);
794
720
  if (!next) return null;
795
721
  node = next;
796
722
  }
797
723
  return node.handler ? { handler: node.handler, middlewares: node.middlewares, params } : null;
798
724
  }
799
- async handleError(e, req, ctx) {
800
- const err = e instanceof Error ? e : new Error(String(e));
801
- console.error(err);
802
- return this.errorHandler ? await this.errorHandler(err, req, ctx) : new Response("Internal Server Error", { status: 500 });
803
- }
804
- _notFoundResponse(method, segments) {
805
- if (!isProd()) {
806
- return Response.json(
807
- { error: "Not Found", path: "/" + segments.join("/"), method },
808
- { status: 404 }
809
- );
810
- }
811
- return new Response("Not Found", { status: 404 });
812
- }
725
+ // ── Private: Request handling ──────────────────────────────
813
726
  async handle(req, ctx, segments) {
814
727
  const match = this.matchTrie(req.method, segments);
815
728
  if (match) {
816
729
  Object.assign(ctx.params, match.params);
817
- switch (match.kind) {
818
- case "route": {
819
- const mws = [...this.globalMws, ...match.mws];
820
- try {
821
- return await this.runChain(mws, match.handler, req, ctx);
822
- } catch (e) {
823
- return this.handleError(e, req, ctx);
824
- }
730
+ if (match.kind === "route") {
731
+ try {
732
+ return await this.runChain([...this.globalMws, ...match.mws], match.handler, req, ctx);
733
+ } catch (e) {
734
+ return this.handleError(e, req, ctx);
825
735
  }
826
- case "not-allowed": {
827
- if (this.globalMws.length > 0) {
828
- try {
829
- return await this.runChain(
830
- this.globalMws,
831
- () => new Response("Method Not Allowed", {
832
- status: 405,
833
- headers: { Allow: match.methods.join(", ") }
834
- }),
835
- req,
836
- ctx
837
- );
838
- } catch (e) {
839
- return this.handleError(e, req, ctx);
840
- }
841
- }
842
- return new Response("Method Not Allowed", {
736
+ }
737
+ if (this.globalMws.length > 0) {
738
+ try {
739
+ return await this.runChain(this.globalMws, () => new Response("Method Not Allowed", {
843
740
  status: 405,
844
- headers: { Allow: match.methods.join(", ") }
845
- });
741
+ headers: { Allow: (match.methods || []).join(", ") }
742
+ }), req, ctx);
743
+ } catch (e) {
744
+ return this.handleError(e, req, ctx);
846
745
  }
847
746
  }
747
+ return new Response("Method Not Allowed", { status: 405, headers: { Allow: (match.methods || []).join(", ") } });
848
748
  }
749
+ const nf = () => Response.json({ error: "Not Found", path: "/" + segments.join("/"), method: req.method }, { status: 404 });
849
750
  if (this.globalMws.length > 0) {
850
751
  try {
851
- return await this.runChain(
852
- this.globalMws,
853
- () => this._notFoundResponse(req.method, segments),
854
- req,
855
- ctx
856
- );
752
+ return await this.runChain(this.globalMws, nf, req, ctx);
857
753
  } catch (e) {
858
754
  return this.handleError(e, req, ctx);
859
755
  }
860
756
  }
861
- return this._notFoundResponse(req.method, segments);
757
+ return nf();
862
758
  }
863
- async runChain(middlewares, finalHandler, req, ctx) {
864
- if (middlewares.length === 0) return await finalHandler(req, ctx);
865
- return await runChainLoop(middlewares, 0, finalHandler, req, ctx);
759
+ async handleError(e, req, ctx) {
760
+ const err = e instanceof Error ? e : new Error(String(e));
761
+ return this.errorHandler ? this.errorHandler(err, req, ctx) : new Response("Internal Server Error", { status: 500 });
866
762
  }
867
- };
868
- function runChainLoop(middlewares, index, finalHandler, req, ctx) {
869
- if (index < middlewares.length) {
870
- const mw = middlewares[index];
871
- let called = false;
763
+ // ── Private: Middleware chain ───────────────────────────────
764
+ async runChain(mws, finalHandler, req, ctx) {
765
+ if (mws.length === 0) return finalHandler(req, ctx);
766
+ let i = 0;
872
767
  const dispatch = (r, c) => {
873
- if (called) {
874
- console.warn(
875
- "[router] next() called more than once in middleware \u2014 ignoring duplicate call"
768
+ if (i >= mws.length) return Promise.resolve(finalHandler(r, c));
769
+ const mw = mws[i++];
770
+ let called = false;
771
+ const next = (r2, c2) => {
772
+ if (called) throw new Error("[router] next() called more than once in middleware");
773
+ called = true;
774
+ return dispatch(r2, c2);
775
+ };
776
+ return Promise.resolve(mw(r, c, next));
777
+ };
778
+ return dispatch(req, ctx);
779
+ }
780
+ // ── Private: Meta checking ──────────────────────────────────
781
+ _checkMiddlewareMeta(mw, location) {
782
+ const meta = mw.__meta ?? (typeof mw === "object" && mw && "middleware" in mw ? mw.middleware().__meta : void 0);
783
+ if (!meta) return;
784
+ for (const dep of meta.depends) {
785
+ if (!this._ctxFields.has(dep)) {
786
+ throw new Error(
787
+ `[weifuwu] Middleware at "${location}" depends on ctx.${dep} but it hasn't been registered.
788
+ Register the provider before this middleware: app.use(${dep}())`
876
789
  );
877
- return Promise.resolve(new Response("", { status: 499 }));
878
790
  }
879
- called = true;
880
- return runChainLoop(middlewares, index + 1, finalHandler, r, c);
881
- };
882
- return Promise.resolve(mw(req, ctx, dispatch));
791
+ }
792
+ for (const field of meta.injects) this._ctxFields.add(field);
883
793
  }
884
- return Promise.resolve(finalHandler(req, ctx));
885
- }
886
- function upgradeSocket(wss, req, socket, head, handler, ctx, hub) {
887
- wss.handleUpgrade(req, socket, head, (ws) => {
888
- const connCtx = { ...ctx, params: { ...ctx.params }, query: { ...ctx.query } };
889
- const wsState = {};
890
- connCtx.ws = {
891
- get state() {
892
- return wsState;
893
- },
894
- json(data) {
895
- ws.send(JSON.stringify(data));
896
- },
897
- join(room) {
898
- hub.join(room, ws);
899
- },
900
- leave(_room) {
901
- hub.leave(ws);
902
- },
903
- sendRoom(room, data) {
904
- hub.broadcast(room, data);
905
- }
906
- };
907
- if (handler.open) {
908
- handler.open(ws, connCtx);
909
- }
910
- ws.on("message", (data) => {
911
- handler.message?.(ws, connCtx, data);
912
- });
913
- ws.on("close", () => {
914
- hub.leave(ws);
915
- handler.close?.(ws, connCtx);
916
- });
917
- ws.on("error", (err) => {
918
- handler.error?.(ws, connCtx, err);
919
- });
920
- });
921
- }
922
- function nodeReqHeadersToRecord(headers) {
923
- const result = {};
924
- for (const [k, v] of Object.entries(headers)) {
925
- if (v !== void 0) result[k] = Array.isArray(v) ? v.join(", ") : v;
926
- }
927
- return result;
928
- }
929
- function sendHttpResponseOnSocket(socket, response) {
930
- const statusLine = `HTTP/1.1 ${response.status} ${response.statusText}`;
931
- const headerLines = [statusLine];
932
- response.headers.forEach((value, key) => {
933
- headerLines.push(`${key}: ${value}`);
934
- });
935
- headerLines.push("Connection: close");
936
- headerLines.push("");
937
- const headerStr = headerLines.join("\r\n");
938
- response.arrayBuffer().then((buf) => {
939
- socket.write(headerStr + "\r\n");
940
- if (buf.byteLength > 0) socket.write(Buffer.from(buf));
941
- socket.end();
942
- }).catch(() => {
943
- socket.write(headerStr + "\r\n");
944
- socket.end();
945
- });
946
- }
947
-
948
- // src/core/logger.ts
949
- function emit(event) {
950
- event.traceId = event.traceId ?? currentTraceId();
951
- event.timestamp = (/* @__PURE__ */ new Date()).toISOString();
952
- process.stderr.write(JSON.stringify(event) + "\n");
794
+ };
795
+
796
+ // src/core/logger.ts
797
+ function emit(event) {
798
+ event.traceId = event.traceId ?? currentTraceId();
799
+ event.timestamp = (/* @__PURE__ */ new Date()).toISOString();
800
+ process.stderr.write(JSON.stringify(event) + "\n");
953
801
  }
954
802
  function logger(options) {
955
803
  const format = options?.format ?? "short";
@@ -1046,10 +894,10 @@ function cors(options) {
1046
894
 
1047
895
  // src/middleware/static.ts
1048
896
  import { open, realpath } from "node:fs/promises";
1049
- import { extname, resolve as resolve2, normalize, sep } from "node:path";
897
+ import { extname, resolve, normalize, sep } from "node:path";
1050
898
  import { Readable } from "node:stream";
1051
899
  function serveStatic(root, options) {
1052
- const rootDir = resolve2(root);
900
+ const rootDir = resolve(root);
1053
901
  const opts = options ?? {};
1054
902
  return async (req, ctx) => {
1055
903
  const relativePath = ctx.params["*"] ?? new URL(req.url).pathname.slice(1);
@@ -1057,50 +905,50 @@ function serveStatic(root, options) {
1057
905
  if (decoded.includes("..") || decoded.includes("\0")) {
1058
906
  return new Response("Forbidden", { status: 403 });
1059
907
  }
1060
- let filePath = normalize(resolve2(rootDir, decoded));
908
+ let filePath = normalize(resolve(rootDir, decoded));
1061
909
  if (!filePath.startsWith(rootDir + sep) && filePath !== rootDir) {
1062
910
  return new Response("Forbidden", { status: 403 });
1063
911
  }
1064
912
  let fileHandle;
1065
913
  try {
1066
914
  fileHandle = await open(filePath, "r");
1067
- let stat2 = await fileHandle.stat();
915
+ let stat = await fileHandle.stat();
1068
916
  const realPath = await realpath(filePath);
1069
917
  if (!realPath.startsWith(rootDir + sep) && realPath !== rootDir) {
1070
918
  await fileHandle.close();
1071
919
  return new Response("Forbidden", { status: 403 });
1072
920
  }
1073
- if (stat2.isDirectory()) {
921
+ if (stat.isDirectory()) {
1074
922
  await fileHandle.close();
1075
923
  const indexFile = opts.index ?? "index.html";
1076
- filePath = resolve2(filePath, indexFile);
924
+ filePath = resolve(filePath, indexFile);
1077
925
  if (!filePath.startsWith(rootDir + sep)) {
1078
926
  return new Response("Forbidden", { status: 403 });
1079
927
  }
1080
928
  fileHandle = await open(filePath, "r");
1081
- stat2 = await fileHandle.stat();
1082
- if (!stat2.isFile()) {
929
+ stat = await fileHandle.stat();
930
+ if (!stat.isFile()) {
1083
931
  await fileHandle.close();
1084
932
  return new Response("Not Found", { status: 404 });
1085
933
  }
1086
934
  }
1087
935
  const mimeType = MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
1088
- const etag = `"${stat2.ino}-${stat2.size}-${stat2.mtimeMs}"`;
936
+ const etag = `"${stat.ino}-${stat.size}-${stat.mtimeMs}"`;
1089
937
  const ifNoneMatch = req.headers.get("if-none-match");
1090
938
  if (ifNoneMatch === etag) {
1091
939
  await fileHandle.close();
1092
940
  return new Response(null, { status: 304 });
1093
941
  }
1094
942
  const ifModifiedSince = req.headers.get("if-modified-since");
1095
- if (ifModifiedSince && stat2.mtimeMs <= new Date(ifModifiedSince).getTime()) {
943
+ if (ifModifiedSince && stat.mtimeMs <= new Date(ifModifiedSince).getTime()) {
1096
944
  await fileHandle.close();
1097
945
  return new Response(null, { status: 304 });
1098
946
  }
1099
947
  const headers = {
1100
948
  "Content-Type": mimeType,
1101
- "Content-Length": String(stat2.size),
949
+ "Content-Length": String(stat.size),
1102
950
  ETag: etag,
1103
- "Last-Modified": stat2.mtime.toUTCString(),
951
+ "Last-Modified": stat.mtime.toUTCString(),
1104
952
  "Cache-Control": opts.immutable ? `public, max-age=${opts.maxAge ?? 31536e3}, immutable` : `public, max-age=${opts.maxAge ?? 0}`
1105
953
  };
1106
954
  const readStream = fileHandle.createReadStream();
@@ -1157,187 +1005,6 @@ var MIME_TYPES = {
1157
1005
  ".wav": "audio/wav"
1158
1006
  };
1159
1007
 
1160
- // src/middleware/validate.ts
1161
- function parseFormBody(text) {
1162
- const params = new URLSearchParams(text);
1163
- const result = {};
1164
- for (const [key, value] of params) {
1165
- result[key] = value;
1166
- }
1167
- return result;
1168
- }
1169
- function parseBody(text, ct) {
1170
- if (ct.includes("application/x-www-form-urlencoded")) {
1171
- return parseFormBody(text);
1172
- }
1173
- const isExplicitJson = ct.includes("application/json") || ct.includes("+json") || ct.includes("text/") || ct.includes("*/json");
1174
- const isNotSpecialMultipart = !ct.includes("multipart/form-data") && !ct.includes("application/x-www-form-urlencoded");
1175
- if (isExplicitJson || isNotSpecialMultipart) {
1176
- try {
1177
- return JSON.parse(text);
1178
- } catch {
1179
- }
1180
- }
1181
- return text;
1182
- }
1183
- function validate(schemas) {
1184
- const mw = async (req, ctx, next) => {
1185
- const parsed = {};
1186
- const issues = [];
1187
- if (schemas?.params) {
1188
- const result = schemas.params.safeParse(ctx.params);
1189
- if (result.success) {
1190
- parsed.params = result.data;
1191
- } else {
1192
- issues.push(
1193
- ...result.error.issues.map((i) => ({
1194
- path: ["params", ...i.path.map(String)],
1195
- message: i.message
1196
- }))
1197
- );
1198
- }
1199
- }
1200
- if (schemas?.query) {
1201
- const result = schemas.query.safeParse(ctx.query);
1202
- if (result.success) {
1203
- parsed.query = result.data;
1204
- } else {
1205
- issues.push(
1206
- ...result.error.issues.map((i) => ({
1207
- path: ["query", ...i.path.map(String)],
1208
- message: i.message
1209
- }))
1210
- );
1211
- }
1212
- }
1213
- if (schemas?.headers) {
1214
- const rawHeaders = {};
1215
- req.headers.forEach((v, k) => {
1216
- rawHeaders[k] = v;
1217
- });
1218
- const result = schemas.headers.safeParse(rawHeaders);
1219
- if (result.success) {
1220
- parsed.headers = result.data;
1221
- } else {
1222
- issues.push(
1223
- ...result.error.issues.map((i) => ({
1224
- path: ["headers", ...i.path.map(String)],
1225
- message: i.message
1226
- }))
1227
- );
1228
- }
1229
- }
1230
- if (req.method !== "GET" && req.method !== "HEAD") {
1231
- const ct = req.headers.get("content-type") ?? "";
1232
- const isForm = ct.includes("application/x-www-form-urlencoded");
1233
- if (schemas?.body || isForm) {
1234
- if (req.body === null) {
1235
- if (schemas?.body) {
1236
- issues.push({ path: ["body"], message: "Request body is required" });
1237
- }
1238
- } else {
1239
- const bodyText = await req.text();
1240
- if (!bodyText) {
1241
- if (schemas?.body) {
1242
- issues.push({ path: ["body"], message: "Request body is required" });
1243
- }
1244
- } else {
1245
- const bodyValue = parseBody(bodyText, ct);
1246
- if (schemas?.body) {
1247
- const result = schemas.body.safeParse(bodyValue);
1248
- if (result.success) {
1249
- parsed.body = result.data;
1250
- } else {
1251
- issues.push(
1252
- ...result.error.issues.map((i) => ({
1253
- path: ["body", ...i.path.map(String)],
1254
- message: i.message
1255
- }))
1256
- );
1257
- }
1258
- } else {
1259
- parsed.body = bodyValue;
1260
- }
1261
- }
1262
- }
1263
- }
1264
- }
1265
- if (issues.length > 0) {
1266
- return Response.json({ error: "Validation failed", issues }, { status: 400 });
1267
- }
1268
- ctx.parsed = { ...ctx.parsed, ...parsed };
1269
- return next(req, ctx);
1270
- };
1271
- mw.__meta = { injects: ["parsed"], depends: [] };
1272
- return mw;
1273
- }
1274
-
1275
- // src/core/cookie.ts
1276
- function getCookies(req) {
1277
- const header = req.headers.get("cookie");
1278
- if (!header) return {};
1279
- const cookies = {};
1280
- for (const pair of header.split(";")) {
1281
- const idx = pair.indexOf("=");
1282
- if (idx === -1) continue;
1283
- let name = pair.slice(0, idx).trim();
1284
- let value = pair.slice(idx + 1).trim();
1285
- if (!name) continue;
1286
- try {
1287
- name = decodeURIComponent(name);
1288
- } catch {
1289
- }
1290
- if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
1291
- value = value.slice(1, -1);
1292
- }
1293
- try {
1294
- cookies[name] = decodeURIComponent(value);
1295
- } catch {
1296
- cookies[name] = value;
1297
- }
1298
- }
1299
- return cookies;
1300
- }
1301
- function serializeCookie(name, value, options) {
1302
- if (/[\x00-\x1F\x7F-\x9F;,]/.test(name) || /[\x00-\x1F\x7F-\x9F;,]/.test(value)) {
1303
- throw new Error(`Invalid cookie name or value: contains control characters or special chars`);
1304
- }
1305
- const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
1306
- if (options?.maxAge != null) parts.push(`Max-Age=${options.maxAge}`);
1307
- if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
1308
- if (options?.domain) parts.push(`Domain=${options.domain}`);
1309
- if (options?.path) parts.push(`Path=${options.path}`);
1310
- if (options?.httpOnly) parts.push("HttpOnly");
1311
- if (options?.secure) parts.push("Secure");
1312
- if (options?.sameSite) parts.push(`SameSite=${options.sameSite}`);
1313
- return parts.join("; ");
1314
- }
1315
- function setCookie(res, name, value, options) {
1316
- const headers = new Headers(res.headers);
1317
- headers.append("Set-Cookie", serializeCookie(name, value, options));
1318
- return new Response(res.body, {
1319
- status: res.status,
1320
- statusText: res.statusText,
1321
- headers
1322
- });
1323
- }
1324
- function deleteCookie(res, name, options) {
1325
- const headers = new Headers(res.headers);
1326
- headers.append(
1327
- "Set-Cookie",
1328
- serializeCookie(name, "", {
1329
- ...options,
1330
- maxAge: 0,
1331
- expires: /* @__PURE__ */ new Date(0)
1332
- })
1333
- );
1334
- return new Response(res.body, {
1335
- status: res.status,
1336
- statusText: res.statusText,
1337
- headers
1338
- });
1339
- }
1340
-
1341
1008
  // src/middleware/upload.ts
1342
1009
  import { writeFile, mkdir } from "node:fs/promises";
1343
1010
  import { randomUUID } from "node:crypto";
@@ -1378,8 +1045,7 @@ function upload(options) {
1378
1045
  if (!ct.includes("multipart/form-data")) return next(req, ctx);
1379
1046
  try {
1380
1047
  if (saveDir) await mkdir(saveDir, { recursive: true });
1381
- } catch (e) {
1382
- console.error("upload: failed to create directory", saveDir, e);
1048
+ } catch {
1383
1049
  return Response.json({ error: "Server configuration error" }, { status: 500 });
1384
1050
  }
1385
1051
  let formData;
@@ -1637,482 +1303,6 @@ var DEFAULTS = {
1637
1303
  permissionsPolicy: "camera=(),display-capture=(),fullscreen=(),geolocation=(),microphone=()"
1638
1304
  };
1639
1305
 
1640
- // src/middleware/request-id.ts
1641
- import crypto3 from "node:crypto";
1642
- function requestId(options) {
1643
- const header = options?.header ?? "X-Request-ID";
1644
- const gen = options?.generator ?? (() => crypto3.randomUUID());
1645
- const mw = async (req, ctx, next) => {
1646
- const existing = req.headers.get(header);
1647
- const id = existing ?? gen();
1648
- ctx.requestId = id;
1649
- const res = await next(req, ctx);
1650
- if (res.headers.has(header)) return res;
1651
- const h = new Headers(res.headers);
1652
- h.set(header, id);
1653
- return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h });
1654
- };
1655
- mw.__meta = { injects: ["requestId"], depends: [] };
1656
- return mw;
1657
- }
1658
-
1659
- // src/core/sse.ts
1660
- var encoder = new TextEncoder();
1661
- function formatSSE(event, data) {
1662
- return `event: ${event}
1663
- data: ${JSON.stringify(data)}
1664
-
1665
- `;
1666
- }
1667
- function formatSSEData(data) {
1668
- return `data: ${JSON.stringify(data)}
1669
-
1670
- `;
1671
- }
1672
- function createSSEStream(iterable, opts) {
1673
- return new Response(
1674
- new ReadableStream({
1675
- async start(controller) {
1676
- try {
1677
- for await (const event of iterable) {
1678
- const text = event.type ? formatSSE(event.type, event) : formatSSEData(event);
1679
- controller.enqueue(encoder.encode(text));
1680
- }
1681
- } catch (e) {
1682
- if (e instanceof Error && e.name !== "AbortError") {
1683
- controller.enqueue(encoder.encode(formatSSE("error", { error: e.message })));
1684
- }
1685
- } finally {
1686
- controller.close();
1687
- }
1688
- }
1689
- }),
1690
- {
1691
- status: opts?.status ?? 200,
1692
- headers: {
1693
- "Content-Type": "text/event-stream",
1694
- "Cache-Control": "no-cache",
1695
- Connection: "keep-alive",
1696
- ...opts?.headers
1697
- }
1698
- }
1699
- );
1700
- }
1701
-
1702
- // src/test/test-utils.ts
1703
- import { WebSocket as WSWebSocket } from "ws";
1704
- var TestResponseImpl = class {
1705
- response;
1706
- constructor(response) {
1707
- this.response = response;
1708
- }
1709
- get status() {
1710
- return this.response.status;
1711
- }
1712
- get headers() {
1713
- return this.response.headers;
1714
- }
1715
- async json() {
1716
- return this.response.json();
1717
- }
1718
- async text() {
1719
- return this.response.text();
1720
- }
1721
- async bytes() {
1722
- return this.response.bytes();
1723
- }
1724
- async arrayBuffer() {
1725
- return this.response.arrayBuffer();
1726
- }
1727
- };
1728
- var TestRequest = class {
1729
- headers = {};
1730
- ctxMixin = {};
1731
- bodyData = null;
1732
- app;
1733
- method;
1734
- path;
1735
- constructor(app, method, path) {
1736
- this.app = app;
1737
- this.method = method;
1738
- this.path = path;
1739
- }
1740
- /** Set a request header */
1741
- header(name, value) {
1742
- this.headers[name.toLowerCase()] = value;
1743
- return this;
1744
- }
1745
- /** Mix properties into ctx (simulating middleware injection) */
1746
- with(mixin) {
1747
- Object.assign(this.ctxMixin, mixin);
1748
- return this;
1749
- }
1750
- /** Shortcut: set ctx.user */
1751
- withUser(user) {
1752
- ;
1753
- this.ctxMixin.user = user;
1754
- return this;
1755
- }
1756
- /** Shortcut: set ctx.tenant */
1757
- withTenant(tenant) {
1758
- this.ctxMixin.tenant = tenant;
1759
- return this;
1760
- }
1761
- /** Set JSON request body */
1762
- body(data) {
1763
- this.bodyData = JSON.stringify(data);
1764
- this.headers["content-type"] = "application/json";
1765
- return this;
1766
- }
1767
- /** Set raw text body */
1768
- rawBody(data) {
1769
- this.bodyData = data;
1770
- return this;
1771
- }
1772
- /** Send the request and return the response */
1773
- async send() {
1774
- const url = `http://localhost${this.path}`;
1775
- const query = {};
1776
- const qIdx = this.path.indexOf("?");
1777
- if (qIdx !== -1) {
1778
- const searchParams = new URLSearchParams(this.path.slice(qIdx));
1779
- for (const [k, v] of searchParams) {
1780
- query[k] = v;
1781
- }
1782
- }
1783
- const request = new Request(url, {
1784
- method: this.method,
1785
- headers: this.headers,
1786
- body: this.bodyData
1787
- });
1788
- const ctx = {
1789
- params: {},
1790
- query,
1791
- ...this.ctxMixin
1792
- };
1793
- const handler = this.app.handler();
1794
- const response = await handler(request, ctx);
1795
- return new TestResponseImpl(response);
1796
- }
1797
- };
1798
- var TestApp = class {
1799
- router;
1800
- wsServer = null;
1801
- wsConnections = [];
1802
- constructor() {
1803
- this.router = new Router();
1804
- }
1805
- /**
1806
- * Register a WebSocket handler.
1807
- */
1808
- ws(path, handler) {
1809
- this.router.ws(path, handler);
1810
- return this;
1811
- }
1812
- /** Get the raw Router (for advanced use). */
1813
- get _router() {
1814
- return this.router;
1815
- }
1816
- /** Add global middleware */
1817
- use(mw) {
1818
- this.router.use(mw);
1819
- return this;
1820
- }
1821
- /** Register a GET route — supports route-level middleware via spread args. */
1822
- get(path, ...args) {
1823
- ;
1824
- this.router.get(path, ...args);
1825
- return this;
1826
- }
1827
- /** Register a POST route. */
1828
- post(path, ...args) {
1829
- ;
1830
- this.router.post(path, ...args);
1831
- return this;
1832
- }
1833
- /** Register a PUT route. */
1834
- put(path, ...args) {
1835
- ;
1836
- this.router.put(path, ...args);
1837
- return this;
1838
- }
1839
- /** Register a PATCH route. */
1840
- patch(path, ...args) {
1841
- ;
1842
- this.router.patch(path, ...args);
1843
- return this;
1844
- }
1845
- /** Register a DELETE route. */
1846
- delete(path, ...args) {
1847
- ;
1848
- this.router.delete(path, ...args);
1849
- return this;
1850
- }
1851
- /** Start building a GET request */
1852
- getReq(path) {
1853
- return new TestRequest(this, "GET", path);
1854
- }
1855
- /** Start building a POST request */
1856
- postReq(path) {
1857
- return new TestRequest(this, "POST", path);
1858
- }
1859
- /** Start building a PUT request */
1860
- putReq(path) {
1861
- return new TestRequest(this, "PUT", path);
1862
- }
1863
- /** Start building a PATCH request */
1864
- patchReq(path) {
1865
- return new TestRequest(this, "PATCH", path);
1866
- }
1867
- /** Start building a DELETE request */
1868
- deleteReq(path) {
1869
- return new TestRequest(this, "DELETE", path);
1870
- }
1871
- /** Get the underlying handler (for advanced usage) */
1872
- handler() {
1873
- return this.router.handler();
1874
- }
1875
- /** Start building a WebSocket connection to the given path. */
1876
- wsReq(path) {
1877
- return new TestWSRequest(this, path);
1878
- }
1879
- /**
1880
- * Internal: ensure HTTP server is running for WebSocket connections.
1881
- * Starts on a random port.
1882
- */
1883
- /* @internal */
1884
- async _ensureServer() {
1885
- if (this.wsServer) {
1886
- return `http://localhost:${this.wsServer.port}`;
1887
- }
1888
- const wsHandler = this.router.websocketHandler();
1889
- if (!wsHandler) {
1890
- throw new Error(
1891
- "No WebSocket routes registered. Use app.ws(path, handler) before calling wsReq()."
1892
- );
1893
- }
1894
- this.wsServer = serve(this.router);
1895
- await this.wsServer.ready;
1896
- return `http://localhost:${this.wsServer.port}`;
1897
- }
1898
- /**
1899
- * Internal: register a WS connection for cleanup.
1900
- */
1901
- /* @internal */
1902
- _trackConnection(conn) {
1903
- this.wsConnections.push(conn);
1904
- }
1905
- /**
1906
- * Cleanup all WebSocket connections and stop the server.
1907
- */
1908
- async close() {
1909
- for (const conn of this.wsConnections) {
1910
- try {
1911
- conn.close();
1912
- } catch {
1913
- }
1914
- }
1915
- this.wsConnections = [];
1916
- if (this.wsServer) {
1917
- this.wsServer.close();
1918
- this.wsServer = null;
1919
- }
1920
- }
1921
- };
1922
- var TestWSRequest = class {
1923
- app;
1924
- path;
1925
- _timeout = 5e3;
1926
- constructor(app, path) {
1927
- this.app = app;
1928
- this.path = path;
1929
- }
1930
- /** Set the timeout for operations (default: 5000ms). */
1931
- timeout(ms) {
1932
- this._timeout = ms;
1933
- return this;
1934
- }
1935
- /**
1936
- * Connect to the WebSocket endpoint.
1937
- * Starts a real HTTP server (random port) if not already running.
1938
- */
1939
- async connect() {
1940
- const baseUrl = await this.app._ensureServer();
1941
- const wsUrl = baseUrl.replace(/^http/, "ws") + this.path;
1942
- const ws = new WSWebSocket(wsUrl, { handshakeTimeout: this._timeout });
1943
- return new Promise((resolve4, reject) => {
1944
- const timer = setTimeout(() => {
1945
- reject(new Error(`WebSocket connection timed out after ${this._timeout}ms`));
1946
- ws.close();
1947
- }, this._timeout);
1948
- ws.on("open", () => {
1949
- clearTimeout(timer);
1950
- const conn = new TestWSConnection(ws, this._timeout);
1951
- this.app._trackConnection(conn);
1952
- resolve4(conn);
1953
- });
1954
- ws.on("error", (err) => {
1955
- clearTimeout(timer);
1956
- reject(new Error(`WebSocket connection error: ${err.message}`));
1957
- });
1958
- ws.on("unexpected-response", (_req, res) => {
1959
- clearTimeout(timer);
1960
- let body = "";
1961
- res.on("data", (chunk) => {
1962
- body += chunk.toString();
1963
- });
1964
- res.on("end", () => {
1965
- reject(new Error(`WebSocket upgrade rejected (${res.statusCode}): ${body.slice(0, 200)}`));
1966
- });
1967
- });
1968
- });
1969
- }
1970
- };
1971
- var TestWSConnection = class {
1972
- ws;
1973
- _timeout;
1974
- messageQueue = [];
1975
- resolveQueue = [];
1976
- _closed = false;
1977
- constructor(ws, timeout = 5e3) {
1978
- this.ws = ws;
1979
- this._timeout = timeout;
1980
- ws.on("message", (data) => {
1981
- const str = data.toString();
1982
- if (this.resolveQueue.length > 0) {
1983
- const resolve4 = this.resolveQueue.shift();
1984
- resolve4(str);
1985
- } else {
1986
- this.messageQueue.push(str);
1987
- }
1988
- });
1989
- ws.on("close", () => {
1990
- this._closed = true;
1991
- for (const _r of this.resolveQueue) {
1992
- }
1993
- });
1994
- }
1995
- /** Send a text message. */
1996
- send(data) {
1997
- this.ws.send(data);
1998
- }
1999
- /** Send a JSON message. */
2000
- json(data) {
2001
- this.ws.send(JSON.stringify(data));
2002
- }
2003
- /**
2004
- * Wait for the next message. Returns the raw text.
2005
- * Throws on timeout or if the connection is closed.
2006
- */
2007
- async receive(timeout) {
2008
- if (this.messageQueue.length > 0) {
2009
- return this.messageQueue.shift();
2010
- }
2011
- if (this._closed) {
2012
- throw new Error("WebSocket connection closed");
2013
- }
2014
- return new Promise((resolve4, reject) => {
2015
- const timer = setTimeout(() => {
2016
- const idx = this.resolveQueue.indexOf(resolve4);
2017
- if (idx !== -1) this.resolveQueue.splice(idx, 1);
2018
- reject(new Error(`WebSocket receive timed out after ${timeout ?? this._timeout}ms`));
2019
- }, timeout ?? this._timeout);
2020
- this.resolveQueue.push((msg) => {
2021
- clearTimeout(timer);
2022
- resolve4(msg);
2023
- });
2024
- });
2025
- }
2026
- /** Wait for the next message and parse as JSON. */
2027
- async receiveJson() {
2028
- const msg = await this.receive();
2029
- return JSON.parse(msg);
2030
- }
2031
- /**
2032
- * Assert that no message is received within the given silence period.
2033
- * Useful for verifying that something did NOT happen.
2034
- */
2035
- async expectSilent(ms) {
2036
- return new Promise((resolve4, reject) => {
2037
- if (this.messageQueue.length > 0) {
2038
- reject(new Error(`Expected silence but got message: ${this.messageQueue[0].slice(0, 100)}`));
2039
- return;
2040
- }
2041
- const timer = setTimeout(() => resolve4(), ms);
2042
- const origPush = this.resolveQueue.push.bind(this.resolveQueue);
2043
- this.resolveQueue.push = (_fn) => {
2044
- clearTimeout(timer);
2045
- reject(new Error("Expected silence but received a message"));
2046
- return 0;
2047
- };
2048
- setTimeout(() => {
2049
- this.resolveQueue.push = origPush;
2050
- }, ms + 10).unref();
2051
- });
2052
- }
2053
- /** Close the connection. */
2054
- close() {
2055
- this._closed = true;
2056
- this.ws.close();
2057
- }
2058
- /** Whether the connection is closed. */
2059
- get closed() {
2060
- return this._closed;
2061
- }
2062
- };
2063
- function testApp() {
2064
- return new TestApp();
2065
- }
2066
- async function createTestDb(options) {
2067
- const dbUrl = options?.url || process.env.TEST_DATABASE_URL || process.env.DATABASE_URL;
2068
- if (!dbUrl) throw new Error("createTestDb: DATABASE_URL or TEST_DATABASE_URL required");
2069
- const schema = options?.schema || `test_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2070
- const { default: postgres2 } = await import("postgres");
2071
- const adminSql = postgres2(dbUrl);
2072
- await adminSql.unsafe('CREATE SCHEMA IF NOT EXISTS "' + schema.replace(/"/g, '""') + '"');
2073
- const schemaUrl = new URL(dbUrl);
2074
- schemaUrl.searchParams.set("search_path", schema);
2075
- const sql = postgres2(schemaUrl.toString());
2076
- await adminSql.end();
2077
- return {
2078
- sql,
2079
- url: schemaUrl.toString(),
2080
- schema,
2081
- destroy: async () => {
2082
- const destroySql = postgres2(dbUrl);
2083
- await destroySql.unsafe('DROP SCHEMA IF EXISTS "' + schema.replace(/"/g, '""') + '" CASCADE');
2084
- await destroySql.end();
2085
- await sql.end();
2086
- }
2087
- };
2088
- }
2089
- async function withTestDb(optionsOrFn, fn) {
2090
- let dbUrl;
2091
- let callback;
2092
- if (typeof optionsOrFn === "function") {
2093
- callback = optionsOrFn;
2094
- } else if (typeof optionsOrFn === "string") {
2095
- dbUrl = optionsOrFn;
2096
- callback = fn;
2097
- } else {
2098
- dbUrl = optionsOrFn?.url;
2099
- callback = fn;
2100
- }
2101
- const resolvedUrl = dbUrl || process.env.TEST_DATABASE_URL || process.env.DATABASE_URL;
2102
- if (!resolvedUrl) throw new Error("withTestDb: DATABASE_URL or TEST_DATABASE_URL required");
2103
- const { default: postgres2 } = await import("postgres");
2104
- const sql = postgres2(resolvedUrl);
2105
- try {
2106
- await sql.begin(async (txSql) => {
2107
- await callback(txSql);
2108
- throw void 0;
2109
- });
2110
- } catch {
2111
- } finally {
2112
- await sql.end();
2113
- }
2114
- }
2115
-
2116
1306
  // src/graphql.ts
2117
1307
  import {
2118
1308
  buildSchema,
@@ -2145,16 +1335,10 @@ async function parseParamsFromPost(req) {
2145
1335
  }
2146
1336
  }
2147
1337
  function buildSchemaFromOptions(options) {
2148
- try {
2149
- if (typeof options.schema === "string") {
2150
- return options.resolvers ? makeExecutableSchema({ typeDefs: options.schema, resolvers: options.resolvers }) : buildSchema(options.schema);
2151
- }
2152
- return options.schema;
2153
- } catch (err) {
2154
- const msg = err instanceof Error ? err.message : String(err);
2155
- console.error(`[graphql] schema build failed: ${msg}`);
2156
- throw err;
1338
+ if (typeof options.schema === "string") {
1339
+ return options.resolvers ? makeExecutableSchema({ typeDefs: options.schema, resolvers: options.resolvers }) : buildSchema(options.schema);
2157
1340
  }
1341
+ return options.schema;
2158
1342
  }
2159
1343
  function queryDepth(doc) {
2160
1344
  let max = 0;
@@ -2222,7 +1406,6 @@ async function executeQuery(schema, params, options, req, ctx) {
2222
1406
  return Response.json(result, { status: result.errors ? 400 : 200 });
2223
1407
  } catch (err) {
2224
1408
  const msg = err instanceof Error ? err.message : String(err);
2225
- console.error(`[${currentTraceId()}] graphql execution failed: ${msg}`);
2226
1409
  return Response.json({ errors: [{ message: msg }] }, { status: 500 });
2227
1410
  }
2228
1411
  }
@@ -2414,7 +1597,8 @@ function redis(opts) {
2414
1597
  const options = typeof opts === "string" ? { url: opts } : opts ?? {};
2415
1598
  const url = options.url ?? process.env.REDIS_URL ?? "redis://localhost:6379";
2416
1599
  const client = new IORedis(url, options);
2417
- client.on("error", (err) => console.error("[redis]", err.message));
1600
+ client.on("error", () => {
1601
+ });
2418
1602
  const mw = ((req, ctx, next) => {
2419
1603
  ctx.redis = client;
2420
1604
  return next(req, ctx);
@@ -2426,8 +1610,8 @@ function redis(opts) {
2426
1610
  }
2427
1611
 
2428
1612
  // src/queue/index.ts
1613
+ import crypto2 from "node:crypto";
2429
1614
  import { Redis as IORedis2 } from "ioredis";
2430
- import crypto4 from "node:crypto";
2431
1615
 
2432
1616
  // src/queue/cron.ts
2433
1617
  function parseField(field, min, max) {
@@ -2495,427 +1679,85 @@ function cronNext(expr, from = /* @__PURE__ */ new Date()) {
2495
1679
 
2496
1680
  // src/queue/index.ts
2497
1681
  function queue(opts) {
2498
- const store = opts?.store ?? "memory";
2499
- if (store === "redis") return createRedisQueue(opts);
2500
- if (store === "pg") return createPgQueue(opts);
2501
- return createMemoryQueue(opts);
2502
- }
2503
- function escapeIdent(s) {
2504
- return '"' + s.replace(/"/g, '""') + '"';
2505
- }
2506
- function attachCron(q, handlers) {
2507
- ;
2508
- q.cron = function(pattern, handler) {
2509
- const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto4.randomUUID().slice(0, 8);
2510
- q.process(id, async () => {
2511
- await handler();
2512
- });
2513
- q.add(id, {}, { schedule: pattern });
2514
- return { stop: () => handlers.delete(id) };
2515
- };
2516
- }
2517
- function createMemoryQueue(opts) {
1682
+ const redis2 = opts?.redis ?? new IORedis2(opts?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379");
1683
+ const prefix = opts?.prefix ?? "queue";
1684
+ const jobKey = prefix + ":jobs";
1685
+ const failedKey = prefix + ":failed";
2518
1686
  const pollInterval = opts?.pollInterval ?? 200;
2519
- const handlers = /* @__PURE__ */ new Map();
2520
- const pending = [];
2521
- const failed = [];
2522
- const MAX_FAILED = 1e3;
2523
- let running = false;
2524
- let pollTimer = null;
2525
- let _processed = 0;
2526
- let _failed = 0;
2527
- let inflight = 0;
2528
1687
  const MAX_CONCURRENT = 16;
2529
- function insertJob(job) {
2530
- let i = 0;
2531
- while (i < pending.length && pending[i].runAt <= job.runAt) i++;
2532
- pending.splice(i, 0, job);
2533
- }
2534
- async function execute(job, handler) {
2535
- inflight++;
2536
- try {
2537
- await handler(job);
2538
- _processed++;
2539
- } catch (e) {
2540
- _failed++;
2541
- failed.unshift({ ...job, error: e.message, failedAt: Date.now() });
2542
- if (failed.length > MAX_FAILED) failed.length = MAX_FAILED;
2543
- } finally {
2544
- inflight--;
2545
- }
2546
- if (job.schedule) {
2547
- try {
2548
- insertJob({
2549
- ...job,
2550
- id: crypto4.randomUUID(),
2551
- runAt: cronNext(job.schedule),
2552
- createdAt: Date.now()
2553
- });
2554
- } catch (e) {
2555
- console.error("[queue] cron re-queue failed:", e.message);
2556
- }
2557
- }
2558
- }
2559
- async function poll() {
2560
- if (!running) return;
2561
- const now = Date.now();
2562
- while (running && inflight < MAX_CONCURRENT && pending.length > 0 && pending[0].runAt <= now) {
2563
- const job = pending.shift();
2564
- const handler = handlers.get(job.type);
2565
- if (handler) execute(job, handler);
2566
- }
2567
- if (running) pollTimer = setTimeout(poll, pollInterval);
2568
- }
2569
- const mw = ((req, ctx, next) => {
2570
- ctx.queue = q;
2571
- return next(req, ctx);
2572
- });
2573
- const q = mw;
2574
- mw.add = function add(type, payload, opts2) {
2575
- const id = crypto4.randomUUID();
2576
- let runAt;
2577
- if (opts2?.schedule) {
2578
- try {
2579
- const f = parsePattern(opts2.schedule);
2580
- runAt = matches(f, /* @__PURE__ */ new Date()) ? Date.now() : cronNext(opts2.schedule);
2581
- } catch {
2582
- runAt = cronNext(opts2.schedule);
2583
- }
2584
- } else if (opts2?.delay) {
2585
- runAt = Date.now() + opts2.delay;
2586
- } else {
2587
- runAt = Date.now();
2588
- }
2589
- const job = { id, type, payload, createdAt: Date.now(), runAt };
2590
- if (opts2?.schedule) job.schedule = opts2.schedule;
2591
- insertJob(job);
2592
- return Promise.resolve(id);
2593
- };
2594
- mw.process = function process2(type, handler) {
2595
- handlers.set(type, handler);
2596
- };
2597
- mw.run = async function run() {
2598
- if (running) return;
2599
- running = true;
2600
- poll();
2601
- };
2602
- mw.close = async function close() {
2603
- running = false;
2604
- if (pollTimer) {
2605
- clearTimeout(pollTimer);
2606
- pollTimer = null;
2607
- }
2608
- while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
2609
- };
2610
- mw.jobs = async function(limit) {
2611
- return pending.slice(0, limit ?? 50);
2612
- };
2613
- mw.failedJobs = async function failedJobs(limit) {
2614
- return failed.slice(0, limit ?? 50);
2615
- };
2616
- mw.retryFailed = async function retry(jobId) {
2617
- const idx = failed.findIndex((j) => j.id === jobId);
2618
- if (idx < 0) return false;
2619
- const [entry] = failed.splice(idx, 1);
2620
- _failed--;
2621
- insertJob({ ...entry, runAt: Date.now() });
2622
- return true;
2623
- };
2624
- mw.retryAllFailed = async function retryAll(type) {
2625
- let count = 0;
2626
- for (let i = failed.length - 1; i >= 0; i--) {
2627
- if (type && failed[i].type !== type) continue;
2628
- const [entry] = failed.splice(i, 1);
2629
- _failed--;
2630
- insertJob({ ...entry, runAt: Date.now() });
2631
- count++;
2632
- }
2633
- return count;
2634
- };
2635
- mw.dashboard = function dashboard() {
2636
- return buildDashboard(q);
2637
- };
2638
- mw.stats = () => ({
2639
- running,
2640
- inflight,
2641
- processed: _processed,
2642
- failed: _failed,
2643
- handlers: handlers.size,
2644
- maxConcurrent: MAX_CONCURRENT
2645
- });
2646
- attachCron(q, handlers);
2647
- return q;
2648
- }
2649
- function createPgQueue(opts) {
2650
- const sql = opts.pg.sql;
2651
- const pollInterval = opts?.pollInterval ?? 200;
2652
- const table = (opts?.prefix ?? "queue") + "_jobs";
1688
+ const MAX_FAILED = 1e3;
2653
1689
  const handlers = /* @__PURE__ */ new Map();
2654
1690
  let running = false, pollTimer = null;
2655
- let _processed = 0, _failed = 0, inflight = 0, ready = false;
2656
- const MAX_CONCURRENT = 16;
2657
- const MAX_FAILED = 1e3;
2658
- async function ensureTable() {
2659
- if (ready) return;
2660
- await sql.unsafe(
2661
- `CREATE TABLE IF NOT EXISTS ${escapeIdent(table)} (id UUID PRIMARY KEY, type TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}', run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), schedule TEXT, status TEXT NOT NULL DEFAULT 'pending', error TEXT, failed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`
2662
- );
2663
- await sql.unsafe(
2664
- `CREATE INDEX IF NOT EXISTS ${escapeIdent(table + "_run_at_idx")} ON ${escapeIdent(table)} (run_at, status)`
2665
- );
2666
- ready = true;
2667
- }
2668
- async function processJob(job, handler) {
2669
- inflight++;
2670
- try {
2671
- await handler(job);
2672
- _processed++;
2673
- await sql.unsafe(`DELETE FROM ${escapeIdent(table)} WHERE id = $1`, [job.id]);
2674
- } catch (e) {
2675
- _failed++;
2676
- const msg = e.message;
2677
- console.error("[queue] handler error:", msg);
2678
- await sql.unsafe(
2679
- `UPDATE ${escapeIdent(table)} SET status = 'failed', error = $2, failed_at = NOW() WHERE id = $1`,
2680
- [job.id, msg]
2681
- );
2682
- } finally {
2683
- inflight--;
2684
- }
2685
- if (job.schedule) {
2686
- try {
2687
- const nextRun = cronNext(job.schedule);
2688
- await sql.unsafe(
2689
- `INSERT INTO ${escapeIdent(table)} (id, type, payload, run_at, schedule) VALUES ($1, $2, $3::jsonb, $4, $5)`,
2690
- [
2691
- crypto4.randomUUID(),
2692
- job.type,
2693
- JSON.stringify(job.payload),
2694
- new Date(nextRun).toISOString(),
2695
- job.schedule
2696
- ]
2697
- );
2698
- } catch (e) {
2699
- console.error("[queue] cron re-queue failed:", e.message);
2700
- }
1691
+ let _processed = 0, _failed = 0, inflight = 0;
1692
+ async function insert(job, error) {
1693
+ if (error) {
1694
+ await redis2.lpush(failedKey, JSON.stringify({ ...job, error, failedAt: Date.now() }));
1695
+ await redis2.ltrim(failedKey, 0, MAX_FAILED - 1);
1696
+ } else {
1697
+ await redis2.zadd(jobKey, job.runAt, JSON.stringify(job));
2701
1698
  }
2702
1699
  }
2703
1700
  async function poll() {
2704
- if (!running) return;
1701
+ const result = await redis2.zpopmin(jobKey);
1702
+ if (result.length < 2) return null;
1703
+ const score = parseInt(result[1], 10);
1704
+ if (score > Date.now()) {
1705
+ await redis2.zadd(jobKey, score, result[0]);
1706
+ return null;
1707
+ }
2705
1708
  try {
2706
- while (running && inflight < MAX_CONCURRENT) {
2707
- const rows = await sql.unsafe(
2708
- `UPDATE ${escapeIdent(table)} SET status = 'running' WHERE id = (SELECT id FROM ${escapeIdent(table)} WHERE run_at <= NOW() AND status = 'pending' ORDER BY run_at LIMIT 1 FOR UPDATE SKIP LOCKED) RETURNING *`
2709
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2710
- );
2711
- if (rows.length === 0) break;
2712
- const row = rows[0];
2713
- const job = {
2714
- id: row.id,
2715
- type: row.type,
2716
- payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
2717
- createdAt: new Date(row.created_at).getTime(),
2718
- runAt: new Date(row.run_at).getTime(),
2719
- schedule: row.schedule || void 0
2720
- };
2721
- const handler = handlers.get(job.type);
2722
- if (handler) processJob(job, handler);
2723
- }
2724
- } catch (e) {
2725
- const msg = e.message;
2726
- if (msg.includes("CONNECTION_ENDED") || msg.includes("Connection terminated")) {
2727
- running = false;
2728
- return;
2729
- }
2730
- console.error("[queue] poll error:", msg);
1709
+ return JSON.parse(result[0]);
1710
+ } catch {
1711
+ return null;
2731
1712
  }
2732
- if (running) pollTimer = setTimeout(poll, pollInterval);
2733
1713
  }
2734
- const mw = ((req, ctx, next) => {
2735
- ctx.queue = q;
2736
- return next(req, ctx);
2737
- });
2738
- const q = mw;
2739
- mw.add = function add(type, payload, opts2) {
2740
- return (async () => {
2741
- const id = crypto4.randomUUID();
2742
- let runAt;
2743
- if (opts2?.schedule) {
2744
- try {
2745
- const f = parsePattern(opts2.schedule);
2746
- runAt = matches(f, /* @__PURE__ */ new Date()) ? /* @__PURE__ */ new Date() : new Date(cronNext(opts2.schedule));
2747
- } catch {
2748
- runAt = new Date(cronNext(opts2.schedule));
2749
- }
2750
- } else if (opts2?.delay) {
2751
- runAt = new Date(Date.now() + opts2.delay);
2752
- } else {
2753
- runAt = /* @__PURE__ */ new Date();
2754
- }
2755
- await sql.unsafe(
2756
- `INSERT INTO ${escapeIdent(table)} (id, type, payload, run_at, schedule) VALUES ($1, $2, $3::jsonb, $4, $5)`,
2757
- [id, type, JSON.stringify(payload), runAt.toISOString(), opts2?.schedule || null]
2758
- );
2759
- return id;
2760
- })();
2761
- };
2762
- mw.process = function process2(type, handler) {
2763
- handlers.set(type, handler);
2764
- };
2765
- mw.migrate = ensureTable;
2766
- mw.run = async function run() {
2767
- if (running) return;
2768
- await ensureTable();
2769
- running = true;
2770
- poll();
2771
- };
2772
- mw.close = async function close() {
2773
- running = false;
2774
- if (pollTimer) {
2775
- clearTimeout(pollTimer);
2776
- pollTimer = null;
2777
- }
2778
- while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
2779
- };
2780
- mw.jobs = async function jobs(limit) {
2781
- const rows = await sql.unsafe(
2782
- `SELECT * FROM ${escapeIdent(table)} WHERE status = 'pending' ORDER BY run_at LIMIT $1`,
2783
- [limit ?? 50]
2784
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2785
- );
2786
- return rows.map((r) => ({
2787
- id: r.id,
2788
- type: r.type,
2789
- payload: typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload,
2790
- createdAt: new Date(r.created_at).getTime(),
2791
- runAt: new Date(r.run_at).getTime(),
2792
- schedule: r.schedule || void 0
2793
- }));
2794
- };
2795
- mw.failedJobs = async function failedJobs(limit) {
2796
- const rows = await sql.unsafe(
2797
- `SELECT * FROM ${escapeIdent(table)} WHERE status = 'failed' ORDER BY failed_at DESC LIMIT $1`,
2798
- [limit ?? 50]
2799
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2800
- );
2801
- return rows.map((r) => ({
2802
- id: r.id,
2803
- type: r.type,
2804
- payload: typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload,
2805
- createdAt: new Date(r.created_at).getTime(),
2806
- runAt: new Date(r.run_at).getTime(),
2807
- schedule: r.schedule || void 0,
2808
- error: r.error || "",
2809
- failedAt: new Date(r.failed_at).getTime()
2810
- }));
2811
- };
2812
- mw.retryFailed = async function retryFailed(jobId) {
2813
- const result = await sql.unsafe(
2814
- `UPDATE ${escapeIdent(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE id = $1 AND status = 'failed' RETURNING id`,
2815
- [jobId]
2816
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2817
- );
2818
- return result.length > 0;
2819
- };
2820
- mw.retryAllFailed = async function retryAllFailed(type) {
2821
- const result = await sql.unsafe(
2822
- type ? `UPDATE ${escapeIdent(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE status = 'failed' AND type = $1 RETURNING id` : `UPDATE ${escapeIdent(table)} SET status = 'pending', error = NULL, failed_at = NULL, run_at = NOW() WHERE status = 'failed' RETURNING id`,
2823
- type ? [type] : []
2824
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2825
- );
2826
- return result.length;
2827
- };
2828
- mw.dashboard = function dashboard() {
2829
- return buildDashboard(q);
2830
- };
2831
- mw.stats = () => ({
2832
- running,
2833
- inflight,
2834
- processed: _processed,
2835
- failed: _failed,
2836
- handlers: handlers.size,
2837
- maxConcurrent: MAX_CONCURRENT
2838
- });
2839
- attachCron(q, handlers);
2840
- return q;
2841
- }
2842
- function createRedisQueue(opts) {
2843
- const redis2 = opts?.redis ?? new IORedis2(opts?.url ?? process.env.REDIS_URL ?? "redis://localhost:6379");
2844
- const prefix = opts?.prefix ?? "queue";
2845
- const pollInterval = opts?.pollInterval ?? 200;
2846
- const handlers = /* @__PURE__ */ new Map();
2847
- let running = false, pollTimer = null, epoch = 0;
2848
- let _processed = 0, _failed = 0, inflight = 0;
2849
- const jobKey = prefix + ":jobs", failedKey = prefix + ":failed", MAX_FAILED = 1e3, MAX_CONCURRENT = 16;
2850
- async function processJob(job, handler) {
1714
+ async function execute(job, handler) {
2851
1715
  inflight++;
2852
1716
  try {
2853
1717
  await handler(job);
2854
1718
  _processed++;
2855
1719
  } catch (e) {
2856
1720
  _failed++;
2857
- const msg = e.message;
2858
- console.error("[queue] handler error:", msg);
2859
- await redis2.lpush(failedKey, JSON.stringify({ ...job, error: msg, failedAt: Date.now() }));
2860
- await redis2.ltrim(failedKey, 0, MAX_FAILED - 1);
1721
+ await insert(job, e.message);
2861
1722
  } finally {
2862
1723
  inflight--;
2863
1724
  }
2864
1725
  if (job.schedule) {
2865
1726
  try {
2866
- const nextRun = cronNext(job.schedule);
2867
- await redis2.zadd(
2868
- jobKey,
2869
- nextRun,
2870
- JSON.stringify({
2871
- ...job,
2872
- id: crypto4.randomUUID(),
2873
- runAt: nextRun,
2874
- createdAt: Date.now()
2875
- })
2876
- );
2877
- } catch (e) {
2878
- console.error("[queue] cron re-queue failed:", e.message);
1727
+ await insert({ ...job, id: crypto2.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
1728
+ } catch {
2879
1729
  }
2880
1730
  }
2881
1731
  }
2882
- async function poll() {
2883
- const currentEpoch = epoch;
1732
+ async function pollLoop() {
2884
1733
  if (!running) return;
2885
1734
  try {
2886
- const now = Date.now();
2887
1735
  while (running && inflight < MAX_CONCURRENT) {
2888
- const result = await redis2.zpopmin(jobKey);
2889
- if (result.length < 2) break;
2890
- const raw2 = result[0], score = parseInt(result[1], 10);
2891
- if (score > now) {
2892
- await redis2.zadd(jobKey, score, raw2);
2893
- break;
2894
- }
2895
- let job;
2896
- try {
2897
- job = JSON.parse(raw2);
2898
- } catch {
2899
- continue;
2900
- }
1736
+ const job = await poll();
1737
+ if (!job) break;
2901
1738
  const handler = handlers.get(job.type);
2902
- if (handler) processJob(job, handler);
1739
+ if (handler) execute(job, handler);
2903
1740
  }
2904
- } catch (e) {
2905
- console.error("[queue] poll error:", e.message);
1741
+ } catch {
2906
1742
  }
2907
- if (running && currentEpoch === epoch) pollTimer = setTimeout(poll, pollInterval);
1743
+ if (running) pollTimer = setTimeout(pollLoop, pollInterval);
2908
1744
  }
2909
- const mw = ((req, ctx, next) => {
1745
+ const stats = () => ({ running, inflight, processed: _processed, failed: _failed, handlers: handlers.size, maxConcurrent: MAX_CONCURRENT });
1746
+ const mw = ((_req, ctx, next) => {
1747
+ ;
2910
1748
  ctx.queue = q;
2911
- return next(req, ctx);
1749
+ return next(_req, ctx);
2912
1750
  });
2913
1751
  const q = mw;
2914
1752
  mw.add = function add(type, payload, opts2) {
2915
- const id = crypto4.randomUUID();
1753
+ const id = crypto2.randomUUID();
2916
1754
  let runAt;
2917
1755
  if (opts2?.schedule) {
2918
- runAt = cronNext(opts2.schedule);
1756
+ try {
1757
+ runAt = matches(parsePattern(opts2.schedule), /* @__PURE__ */ new Date()) ? Date.now() : cronNext(opts2.schedule);
1758
+ } catch {
1759
+ runAt = cronNext(opts2.schedule);
1760
+ }
2919
1761
  } else if (opts2?.delay) {
2920
1762
  runAt = Date.now() + opts2.delay;
2921
1763
  } else {
@@ -2931,11 +1773,10 @@ function createRedisQueue(opts) {
2931
1773
  mw.run = async function run() {
2932
1774
  if (running) return;
2933
1775
  running = true;
2934
- poll();
1776
+ pollLoop();
2935
1777
  };
2936
1778
  mw.close = async function close() {
2937
1779
  running = false;
2938
- epoch++;
2939
1780
  if (pollTimer) {
2940
1781
  clearTimeout(pollTimer);
2941
1782
  pollTimer = null;
@@ -2943,9 +1784,9 @@ function createRedisQueue(opts) {
2943
1784
  while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
2944
1785
  redis2.disconnect();
2945
1786
  };
2946
- mw.jobs = async function jobs(limit) {
2947
- const raw2 = await redis2.zrevrange(jobKey, 0, (limit ?? 50) - 1);
2948
- return raw2.map((r) => {
1787
+ mw.jobs = async (limit = 50) => {
1788
+ const raw = await redis2.zrevrange(jobKey, 0, limit - 1);
1789
+ return raw.map((r) => {
2949
1790
  try {
2950
1791
  return JSON.parse(r);
2951
1792
  } catch {
@@ -2953,9 +1794,9 @@ function createRedisQueue(opts) {
2953
1794
  }
2954
1795
  }).filter(Boolean);
2955
1796
  };
2956
- mw.failedJobs = async function failedJobs(limit) {
2957
- const raw2 = await redis2.lrange(failedKey, 0, (limit ?? 50) - 1);
2958
- return raw2.map((r) => {
1797
+ mw.failedJobs = async (limit = 50) => {
1798
+ const raw = await redis2.lrange(failedKey, 0, limit - 1);
1799
+ return raw.map((r) => {
2959
1800
  try {
2960
1801
  return JSON.parse(r);
2961
1802
  } catch {
@@ -2963,29 +1804,28 @@ function createRedisQueue(opts) {
2963
1804
  }
2964
1805
  }).filter(Boolean);
2965
1806
  };
2966
- mw.retryFailed = async function retryFailed(jobId) {
2967
- const raw2 = await redis2.lrange(failedKey, 0, -1);
2968
- for (const entry of raw2) {
1807
+ mw.retryFailed = async (jobId) => {
1808
+ const raw = await redis2.lrange(failedKey, 0, -1);
1809
+ for (const entry of raw) {
2969
1810
  try {
2970
1811
  const job = JSON.parse(entry);
2971
- if (job.id === jobId) {
2972
- await redis2.lrem(failedKey, 1, entry);
2973
- const reJob = { ...job, runAt: Date.now() };
2974
- delete reJob.error;
2975
- delete reJob.failedAt;
2976
- await redis2.zadd(jobKey, reJob.runAt, JSON.stringify(reJob));
2977
- _failed--;
2978
- return true;
2979
- }
1812
+ if (job.id !== jobId) continue;
1813
+ await redis2.lrem(failedKey, 1, entry);
1814
+ const reJob = { ...job, runAt: Date.now() };
1815
+ delete reJob.error;
1816
+ delete reJob.failedAt;
1817
+ await redis2.zadd(jobKey, reJob.runAt, JSON.stringify(reJob));
1818
+ _failed--;
1819
+ return true;
2980
1820
  } catch {
2981
1821
  }
2982
1822
  }
2983
1823
  return false;
2984
1824
  };
2985
- mw.retryAllFailed = async function retryAllFailed(type) {
1825
+ mw.retryAllFailed = async (type) => {
2986
1826
  let count = 0;
2987
- const raw2 = await redis2.lrange(failedKey, 0, -1);
2988
- for (const entry of raw2) {
1827
+ const raw = await redis2.lrange(failedKey, 0, -1);
1828
+ for (const entry of raw) {
2989
1829
  try {
2990
1830
  const job = JSON.parse(entry);
2991
1831
  if (type && job.type !== type) continue;
@@ -3001,387 +1841,63 @@ function createRedisQueue(opts) {
3001
1841
  }
3002
1842
  return count;
3003
1843
  };
3004
- mw.dashboard = function dashboard() {
3005
- return buildDashboard(q);
3006
- };
3007
- mw.stats = () => ({
3008
- running,
3009
- inflight,
3010
- processed: _processed,
3011
- failed: _failed,
3012
- handlers: handlers.size,
3013
- maxConcurrent: MAX_CONCURRENT
3014
- });
3015
- attachCron(q, handlers);
3016
- return q;
3017
- }
3018
- function buildDashboard(q) {
3019
- const r = new Router();
3020
- r.get("/", async () => {
3021
- const s = q.stats();
3022
- const pending = await q.jobs(100);
3023
- const byType = {};
3024
- for (const j of pending) {
3025
- if (!byType[j.type]) byType[j.type] = { pending: 0, failed: 0 };
3026
- byType[j.type].pending++;
3027
- }
3028
- const failed = await q.failedJobs(1e3);
3029
- for (const j of failed) {
3030
- if (!byType[j.type]) byType[j.type] = { pending: 0, failed: 0 };
3031
- byType[j.type].failed++;
3032
- }
3033
- return Response.json({ stats: s, types: byType, failedCount: failed.length });
3034
- });
3035
- r.get("/:type/failed", async (req, ctx) => {
3036
- const failed = await q.failedJobs(100);
3037
- return Response.json({ jobs: failed.filter((j) => j.type === ctx.params.type) });
3038
- });
3039
- r.post("/:type/retry", async (req, ctx) => {
3040
- return Response.json({ retried: await q.retryAllFailed(ctx.params.type) });
3041
- });
3042
- r.post("/retry/:id", async (req, ctx) => {
3043
- const ok = await q.retryFailed(ctx.params.id);
3044
- if (!ok) return new Response("Not found", { status: 404 });
3045
- return Response.json({ ok: true });
3046
- });
3047
- return r;
3048
- }
3049
-
3050
- // src/middleware/health.ts
3051
- function health(options) {
3052
- const path = options?.path ?? "/__health";
3053
- const r = new Router();
3054
- const handler = async () => {
3055
- try {
3056
- await options?.check?.();
3057
- return new Response("OK", { status: 200 });
3058
- } catch {
3059
- return new Response("Service Unavailable", { status: 503 });
3060
- }
3061
- };
3062
- r.get(path, handler);
3063
- r.head(path, handler);
3064
- return r;
3065
- }
3066
-
3067
- // src/core/html.ts
3068
- var ESCAPE = {
3069
- "&": "&amp;",
3070
- "<": "&lt;",
3071
- ">": "&gt;",
3072
- '"': "&quot;",
3073
- "'": "&#39;"
3074
- };
3075
- function esc(s) {
3076
- return s.replace(/[&<>"']/g, (c) => ESCAPE[c] ?? c);
3077
- }
3078
- function html(strings, ...values) {
3079
- let result = "";
3080
- for (let i = 0; i < strings.length; i++) {
3081
- result += strings[i];
3082
- if (i < values.length) {
3083
- const v = values[i];
3084
- if (v == null || v === false) continue;
3085
- if (Array.isArray(v)) {
3086
- result += v.join("");
3087
- } else if (typeof v === "object" && v !== null && "_raw" in v) {
3088
- result += v._raw;
3089
- } else {
3090
- result += esc(String(v));
3091
- }
3092
- }
3093
- }
3094
- return result;
3095
- }
3096
- function raw(content) {
3097
- return { _raw: content };
3098
- }
3099
-
3100
- // src/middleware/theme.ts
3101
- function makeSetTheme(cookie, location) {
3102
- return (value, loc) => {
3103
- const finalLoc = loc ?? location;
3104
- const c = `${cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
3105
- return new Response(null, { status: 302, headers: { Location: finalLoc, "Set-Cookie": c } });
3106
- };
3107
- }
3108
- function theme(options) {
3109
- const opts = { default: "system", cookie: "theme", ...options };
3110
- const mw = async (req, ctx, next) => {
3111
- let themeValue = opts.default;
3112
- if (opts.cookie) {
3113
- const fromCookie = getCookies(req)[opts.cookie];
3114
- if (fromCookie) themeValue = fromCookie;
3115
- }
3116
- ;
3117
- ctx.theme = {
3118
- value: themeValue,
3119
- set: makeSetTheme(opts.cookie, req.headers.get("referer") || "/")
3120
- };
3121
- return next(req, ctx);
3122
- };
3123
- mw.__meta = { injects: ["theme"], depends: [] };
3124
- class ThemeRouter extends Router {
3125
- middleware() {
3126
- return mw;
3127
- }
3128
- }
3129
- const router = new ThemeRouter();
3130
- router.get("/__theme/:value", (req) => {
3131
- const url = new URL(req.url);
3132
- const value = url.pathname.split("/__theme/")[1] ?? "";
3133
- const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
3134
- const accept = req.headers.get("accept") ?? "";
3135
- if (accept.includes("application/json")) {
3136
- return Response.json({ ok: true, theme: value }, { headers: { "Set-Cookie": cookie } });
3137
- }
3138
- const referer = req.headers.get("referer") || "/";
3139
- return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
3140
- });
3141
- return router;
3142
- }
3143
-
3144
- // src/middleware/i18n.ts
3145
- import { readFile, stat } from "node:fs/promises";
3146
- import { join as join2, resolve as resolve3 } from "node:path";
3147
- var DEFAULTS2 = {
3148
- default: "en",
3149
- cookie: "locale",
3150
- fromAcceptLanguage: true
3151
- };
3152
- function translate(msgs, key, params, fallback) {
3153
- const msg = key.split(".").reduce((o, k) => o?.[k], msgs);
3154
- if (msg === void 0 || msg === null) return fallback ?? key;
3155
- if (!params) return String(msg);
3156
- let result = String(msg);
3157
- for (const [k, v] of Object.entries(params)) {
3158
- result = result.replace(`{${k}}`, v);
3159
- }
3160
- return result;
3161
- }
3162
- function i18n(options) {
3163
- const opts = { ...DEFAULTS2, ...options };
3164
- const dir = opts.dir ? resolve3(opts.dir) : void 0;
3165
- const cache = /* @__PURE__ */ new Map();
3166
- function validLocale(locale) {
3167
- return /^[\w-]+$/.test(locale) && !locale.includes("..");
3168
- }
3169
- async function loadMessages(locale) {
3170
- if (opts.messages?.[locale] && Object.keys(opts.messages[locale]).length > 0) {
3171
- cache.set(locale, opts.messages[locale]);
3172
- return opts.messages[locale];
3173
- }
3174
- if (!dir || !validLocale(locale)) return {};
3175
- const cached = cache.get(locale);
3176
- if (cached) return cached;
3177
- const filePath = join2(dir, `${locale}.json`);
3178
- try {
3179
- await stat(filePath);
3180
- const content = await readFile(filePath, "utf-8");
3181
- const data = JSON.parse(content);
3182
- cache.set(locale, data);
3183
- return data;
3184
- } catch {
3185
- }
3186
- const short = locale.split("-")[0];
3187
- if (short !== locale) {
3188
- const fallback = cache.get(short) || await loadMessages(short);
3189
- if (fallback && Object.keys(fallback).length > 0) {
3190
- cache.set(locale, fallback);
3191
- return fallback;
3192
- }
3193
- }
3194
- return {};
3195
- }
3196
- function detectLocale(req) {
3197
- if (opts.cookie) {
3198
- const fromCookie = getCookies(req)[opts.cookie];
3199
- if (fromCookie && validLocale(fromCookie)) return fromCookie;
3200
- }
3201
- if (opts.fromAcceptLanguage) {
3202
- const fromHeader = req.headers.get("Accept-Language")?.split(",")[0]?.trim();
3203
- if (fromHeader && validLocale(fromHeader)) return fromHeader;
3204
- }
3205
- return opts.default;
3206
- }
3207
- const mw = async (req, ctx, next) => {
3208
- const locale = detectLocale(req);
3209
- const msgs = await loadMessages(locale);
3210
- ctx.i18n = {
3211
- locale,
3212
- messages: msgs,
3213
- t: (key, params, fallback) => translate(msgs, key, params, fallback),
3214
- set: (value, loc) => {
3215
- const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
3216
- const location = loc ?? (req.headers.get("referer") || "/");
3217
- return new Response(null, {
3218
- status: 302,
3219
- headers: { Location: location, "Set-Cookie": cookie }
3220
- });
3221
- }
3222
- };
3223
- return next(req, ctx);
3224
- };
3225
- mw.__meta = { injects: ["i18n"], depends: [] };
3226
- class I18nRouter extends Router {
3227
- middleware() {
3228
- return mw;
3229
- }
3230
- }
3231
- const router = new I18nRouter();
3232
- router.get("/__lang/:locale", async (req) => {
3233
- const url = new URL(req.url);
3234
- const value = url.pathname.split("/__lang/")[1] ?? "";
3235
- const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
3236
- const messages = await loadMessages(value);
3237
- const accept = req.headers.get("accept") ?? "";
3238
- if (accept.includes("application/json")) {
3239
- return Response.json(
3240
- {
3241
- ok: true,
3242
- locale: value,
3243
- messages: Object.keys(messages).length > 0 ? messages : void 0
3244
- },
3245
- { headers: { "Set-Cookie": cookie } }
3246
- );
3247
- }
3248
- const referer = req.headers.get("referer") || "/";
3249
- return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
3250
- });
3251
- return router;
3252
- }
3253
-
3254
- // src/middleware/flash.ts
3255
- function makeSetFlash(name, location) {
3256
- return (data, loc) => {
3257
- const finalLoc = loc ?? location;
3258
- const value = encodeURIComponent(JSON.stringify(data));
3259
- return new Response(null, {
3260
- status: 302,
3261
- headers: {
3262
- Location: finalLoc,
3263
- "Set-Cookie": `${name}=${value}; Path=/; SameSite=Lax`
3264
- }
1844
+ mw.stats = stats;
1845
+ mw.dashboard = () => {
1846
+ const r = new Router();
1847
+ r.get("/", async () => {
1848
+ const s = stats();
1849
+ const pending = await mw.jobs(100);
1850
+ const byType = {};
1851
+ for (const j of pending) {
1852
+ byType[j.type] = byType[j.type] || { pending: 0, failed: 0 };
1853
+ byType[j.type].pending++;
1854
+ }
1855
+ const failed = await mw.failedJobs(1e3);
1856
+ for (const j of failed) {
1857
+ byType[j.type] = byType[j.type] || { pending: 0, failed: 0 };
1858
+ byType[j.type].failed++;
1859
+ }
1860
+ return Response.json({ stats: s, types: byType, failedCount: failed.length });
3265
1861
  });
1862
+ r.get("/:type/failed", async (req, ctx) => Response.json({ jobs: (await mw.failedJobs(100)).filter((j) => j.type === ctx.params.type) }));
1863
+ r.post("/:type/retry", async (req, ctx) => Response.json({ retried: await mw.retryAllFailed(ctx.params.type) }));
1864
+ r.post("/retry/:id", async (req, ctx) => {
1865
+ const ok = await mw.retryFailed(ctx.params.id);
1866
+ return ok ? Response.json({ ok: true }) : new Response("Not found", { status: 404 });
1867
+ });
1868
+ return r;
3266
1869
  };
3267
- }
3268
- function flash(options) {
3269
- const name = options?.name ?? "flash";
3270
- const mw = async (req, ctx, next) => {
3271
- const raw2 = getCookies(req)[name] ?? null;
3272
- const referer = req.headers.get("referer") || "/";
3273
- let value = void 0;
3274
- if (raw2) {
3275
- try {
3276
- value = JSON.parse(decodeURIComponent(raw2));
3277
- } catch {
3278
- value = raw2;
3279
- }
3280
- }
3281
- ctx.flash = {
3282
- value,
3283
- set: makeSetFlash(name, referer)
3284
- };
3285
- const res = await next(req, ctx);
3286
- if (raw2) {
3287
- const headers = new Headers(res.headers);
3288
- headers.append("Set-Cookie", `${name}=; Path=/; Max-Age=0`);
3289
- return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
3290
- }
3291
- return res;
3292
- };
3293
- mw.__meta = { injects: ["flash"], depends: [] };
3294
- return mw;
3295
- }
3296
-
3297
- // src/middleware/csrf.ts
3298
- function csrf(options) {
3299
- const cookieName = options?.cookie ?? "_csrf";
3300
- const headerName = options?.header ?? "x-csrf-token";
3301
- const bodyKey = options?.key ?? "_csrf";
3302
- const excluded = new Set(options?.excludeMethods ?? ["GET", "HEAD", "OPTIONS"]);
3303
- const mw = async (req, ctx, next) => {
3304
- const method = req.method.toUpperCase();
3305
- if (excluded.has(method)) {
3306
- const token = getCookies(req)[cookieName] || crypto.randomUUID();
3307
- ctx.csrf = { token };
3308
- const res = await next(req, ctx);
3309
- const tokenToSet = ctx.csrf?.token;
3310
- if (tokenToSet && !getCookies(req)[cookieName]) {
3311
- return setCookie(res, cookieName, tokenToSet, {
3312
- httpOnly: true,
3313
- sameSite: "strict",
3314
- path: "/"
3315
- });
3316
- }
3317
- return res;
3318
- }
3319
- const cookieToken = getCookies(req)[cookieName];
3320
- let headerToken = req.headers.get(headerName) ?? "";
3321
- if (!headerToken && (req.method === "POST" || req.method === "PUT" || req.method === "PATCH" || req.method === "DELETE")) {
3322
- try {
3323
- const body = await req.clone().json();
3324
- headerToken = body[bodyKey] ?? "";
3325
- } catch {
3326
- return new Response("Invalid request body", { status: 400 });
3327
- }
3328
- }
3329
- if (!cookieToken || !headerToken || cookieToken !== headerToken) {
3330
- return new Response("CSRF token mismatch", { status: 403 });
3331
- }
3332
- return next(req, ctx);
1870
+ q.cron = function(pattern, handler) {
1871
+ const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto2.randomUUID().slice(0, 8);
1872
+ q.process(id, async () => {
1873
+ await handler();
1874
+ });
1875
+ q.add(id, {}, { schedule: pattern });
1876
+ return { stop: () => handlers.delete(id) };
3333
1877
  };
3334
- mw.__meta = { injects: ["csrf"], depends: [] };
3335
- return mw;
1878
+ return q;
3336
1879
  }
3337
1880
  export {
3338
1881
  DEFAULT_MAX_BODY,
3339
1882
  HttpError,
3340
1883
  MIGRATIONS_TABLE,
3341
1884
  Router,
3342
- TestApp,
3343
- TestRequest,
3344
1885
  compress,
3345
1886
  cors,
3346
1887
  createHub,
3347
- createSSEStream,
3348
- createTestDb,
3349
- createTestServer,
3350
- csrf,
3351
1888
  currentTrace,
3352
1889
  currentTraceId,
3353
- deleteCookie,
3354
- env,
3355
- flash,
3356
- formatSSE,
3357
- formatSSEData,
3358
- getCookies,
3359
- getPublicEnv,
3360
1890
  graphql,
3361
- health,
3362
1891
  helmet,
3363
- html,
3364
- i18n,
3365
- isBundled,
3366
- isDev,
3367
- isProd,
3368
- loadEnv,
3369
1892
  logger,
3370
1893
  postgres,
3371
1894
  queue,
3372
1895
  rateLimit,
3373
- raw,
3374
1896
  redis,
3375
- requestId,
3376
1897
  runWithTrace,
3377
1898
  serve,
3378
1899
  serveStatic,
3379
- setCookie,
3380
- testApp,
3381
- theme,
3382
1900
  trace,
3383
1901
  traceElapsed,
3384
- upload,
3385
- validate,
3386
- withTestDb
1902
+ upload
3387
1903
  };