shokupan 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +53 -0
  2. package/dist/context.d.ts +50 -15
  3. package/dist/{http-server-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
  4. package/dist/http-server-BEMPIs33.cjs.map +1 -0
  5. package/dist/{http-server-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
  6. package/dist/http-server-CCeagTyU.js.map +1 -0
  7. package/dist/index.cjs +998 -136
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +996 -135
  11. package/dist/index.js.map +1 -1
  12. package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
  13. package/dist/plugins/application/dashboard/plugin.d.ts +14 -8
  14. package/dist/plugins/application/dashboard/static/charts.js +328 -0
  15. package/dist/plugins/application/dashboard/static/failures.js +85 -0
  16. package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
  17. package/dist/plugins/application/dashboard/static/poll.js +146 -0
  18. package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
  19. package/dist/plugins/application/dashboard/static/registry.css +131 -0
  20. package/dist/plugins/application/dashboard/static/registry.js +269 -0
  21. package/dist/plugins/application/dashboard/static/requests.js +118 -0
  22. package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
  23. package/dist/plugins/application/dashboard/static/styles.css +175 -0
  24. package/dist/plugins/application/dashboard/static/tables.js +92 -0
  25. package/dist/plugins/application/dashboard/static/tabs.js +113 -0
  26. package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
  27. package/dist/plugins/application/dashboard/template.eta +246 -0
  28. package/dist/plugins/application/socket-io.d.ts +14 -0
  29. package/dist/router.d.ts +12 -0
  30. package/dist/shokupan.d.ts +21 -1
  31. package/dist/util/datastore.d.ts +4 -3
  32. package/dist/util/decorators.d.ts +5 -0
  33. package/dist/util/http-error.d.ts +38 -0
  34. package/dist/util/http-status.d.ts +30 -0
  35. package/dist/util/request.d.ts +1 -1
  36. package/dist/util/symbol.d.ts +19 -0
  37. package/dist/util/types.d.ts +30 -1
  38. package/package.json +6 -3
  39. package/dist/http-server-0xH174zz.js.map +0 -1
  40. package/dist/http-server-DFhwlK8e.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -22,9 +22,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  mod
23
23
  ));
24
24
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
25
+ const nanoid = require("nanoid");
25
26
  const promises = require("node:fs/promises");
26
27
  const api = require("@opentelemetry/api");
27
28
  const node_async_hooks = require("node:async_hooks");
29
+ const surrealdb = require("surrealdb");
28
30
  const eta$2 = require("eta");
29
31
  const promises$1 = require("fs/promises");
30
32
  const path = require("path");
@@ -33,12 +35,16 @@ const arctic = require("arctic");
33
35
  const jose = require("jose");
34
36
  const cluster = require("node:cluster");
35
37
  const net = require("node:net");
38
+ const path$1 = require("node:path");
39
+ const node_url = require("node:url");
40
+ const node_perf_hooks = require("node:perf_hooks");
36
41
  const analyzer = require("./analyzer-Bei1sVWp.cjs");
37
42
  const zlib = require("node:zlib");
38
43
  const Ajv = require("ajv");
39
44
  const addFormats = require("ajv-formats");
40
45
  const crypto = require("crypto");
41
46
  const events = require("events");
47
+ var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
42
48
  function _interopNamespaceDefault(e) {
43
49
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
44
50
  if (e) {
@@ -58,6 +64,36 @@ function _interopNamespaceDefault(e) {
58
64
  const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
59
65
  const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
60
66
  const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
67
+ const HTTP_STATUS = {
68
+ // 2xx Success
69
+ OK: 200,
70
+ CREATED: 201,
71
+ ACCEPTED: 202,
72
+ NO_CONTENT: 204,
73
+ // 3xx Redirection
74
+ MOVED_PERMANENTLY: 301,
75
+ FOUND: 302,
76
+ SEE_OTHER: 303,
77
+ NOT_MODIFIED: 304,
78
+ TEMPORARY_REDIRECT: 307,
79
+ PERMANENT_REDIRECT: 308,
80
+ // 4xx Client Errors
81
+ BAD_REQUEST: 400,
82
+ UNAUTHORIZED: 401,
83
+ FORBIDDEN: 403,
84
+ NOT_FOUND: 404,
85
+ METHOD_NOT_ALLOWED: 405,
86
+ REQUEST_TIMEOUT: 408,
87
+ CONFLICT: 409,
88
+ UNPROCESSABLE_ENTITY: 422,
89
+ TOO_MANY_REQUESTS: 429,
90
+ // 5xx Server Errors
91
+ INTERNAL_SERVER_ERROR: 500,
92
+ NOT_IMPLEMENTED: 501,
93
+ BAD_GATEWAY: 502,
94
+ SERVICE_UNAVAILABLE: 503,
95
+ GATEWAY_TIMEOUT: 504
96
+ };
61
97
  const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
62
98
  100,
63
99
  101,
@@ -187,6 +223,40 @@ class ShokupanResponse {
187
223
  return this._headers !== null;
188
224
  }
189
225
  }
226
+ const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
227
+ const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
228
+ const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
229
+ const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
230
+ const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
231
+ const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
232
+ const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
233
+ const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
234
+ const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
235
+ const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
236
+ const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
237
+ const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
238
+ const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
239
+ const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
240
+ const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
241
+ const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
242
+ const $url = /* @__PURE__ */ Symbol.for("Shokupan.ctx.url");
243
+ const $requestId = /* @__PURE__ */ Symbol.for("Shokupan.ctx.requestId");
244
+ const $debug = /* @__PURE__ */ Symbol.for("Shokupan.ctx.debug");
245
+ const $finalResponse = /* @__PURE__ */ Symbol.for("Shokupan.ctx.finalResponse");
246
+ const $rawBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.rawBody");
247
+ const $cachedBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedBody");
248
+ const $bodyType = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyType");
249
+ const $bodyParsed = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParsed");
250
+ const $bodyParseError = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParseError");
251
+ const $routeMatched = /* @__PURE__ */ Symbol.for("Shokupan.ctx.routeMatched");
252
+ const $cachedHostname = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHostname");
253
+ const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol");
254
+ const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
255
+ const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
256
+ const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
257
+ const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
258
+ const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
259
+ const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
190
260
  function isValidCookieDomain(domain, currentHost) {
191
261
  const hostWithoutPort = currentHost.split(":")[0];
192
262
  if (domain === hostWithoutPort) return true;
@@ -224,23 +294,26 @@ class ShokupanContext {
224
294
  state;
225
295
  handlerStack = [];
226
296
  response;
227
- _debug;
228
- _finalResponse;
229
- _rawBody;
297
+ [$debug];
298
+ [$finalResponse];
299
+ [$rawBody];
230
300
  // Raw body for compression optimization
231
301
  // Body caching to avoid double parsing
232
- _url;
233
- _cachedBody;
234
- _bodyType;
235
- _bodyParsed = false;
236
- _bodyParseError;
237
- _routeMatched = false;
302
+ [$url];
303
+ [$cachedBody];
304
+ [$bodyType];
305
+ [$bodyParsed] = false;
306
+ [$bodyParseError];
307
+ [$routeMatched] = false;
238
308
  // Cached URL properties to avoid repeated parsing
239
- _cachedHostname;
240
- _cachedProtocol;
241
- _cachedHost;
242
- _cachedOrigin;
243
- _cachedQuery;
309
+ [$cachedHostname];
310
+ [$cachedProtocol];
311
+ [$cachedHost];
312
+ [$cachedOrigin];
313
+ [$cachedQuery];
314
+ [$ws];
315
+ [$socket];
316
+ [$io];
244
317
  /**
245
318
  * JSX Rendering Function
246
319
  */
@@ -248,12 +321,16 @@ class ShokupanContext {
248
321
  setRenderer(renderer) {
249
322
  this.renderer = renderer;
250
323
  }
324
+ [$requestId];
325
+ get requestId() {
326
+ return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid.nanoid();
327
+ }
251
328
  get url() {
252
- if (!this._url) {
329
+ if (!this[$url]) {
253
330
  const urlString = this.request.url || "http://localhost/";
254
- this._url = new URL(urlString);
331
+ this[$url] = new URL(urlString);
255
332
  }
256
- return this._url;
333
+ return this[$url];
257
334
  }
258
335
  /**
259
336
  * Base request
@@ -271,7 +348,7 @@ class ShokupanContext {
271
348
  * Request path
272
349
  */
273
350
  get path() {
274
- if (this._url) return this._url.pathname;
351
+ if (this[$url]) return this[$url].pathname;
275
352
  const url = this.request.url;
276
353
  let queryIndex = url.indexOf("?");
277
354
  const end = queryIndex === -1 ? url.length : queryIndex;
@@ -296,7 +373,7 @@ class ShokupanContext {
296
373
  * Request query params
297
374
  */
298
375
  get query() {
299
- if (this._cachedQuery) return this._cachedQuery;
376
+ if (this[$cachedQuery]) return this[$cachedQuery];
300
377
  const q = /* @__PURE__ */ Object.create(null);
301
378
  const blocklist = ["__proto__", "constructor", "prototype"];
302
379
  const entries = Object.entries(this.url.searchParams);
@@ -313,7 +390,7 @@ class ShokupanContext {
313
390
  q[key] = value;
314
391
  }
315
392
  }
316
- this._cachedQuery = q;
393
+ this[$cachedQuery] = q;
317
394
  return q;
318
395
  }
319
396
  /**
@@ -326,19 +403,19 @@ class ShokupanContext {
326
403
  * Request hostname (e.g. "localhost")
327
404
  */
328
405
  get hostname() {
329
- return this._cachedHostname ??= this.url.hostname;
406
+ return this[$cachedHostname] ??= this.url.hostname;
330
407
  }
331
408
  /**
332
409
  * Request host (e.g. "localhost:3000")
333
410
  */
334
411
  get host() {
335
- return this._cachedHost ??= this.url.host;
412
+ return this[$cachedHost] ??= this.url.host;
336
413
  }
337
414
  /**
338
415
  * Request protocol (e.g. "http:", "https:")
339
416
  */
340
417
  get protocol() {
341
- return this._cachedProtocol ??= this.url.protocol;
418
+ return this[$cachedProtocol] ??= this.url.protocol;
342
419
  }
343
420
  /**
344
421
  * Whether request is secure (https)
@@ -350,7 +427,7 @@ class ShokupanContext {
350
427
  * Request origin (e.g. "http://localhost:3000")
351
428
  */
352
429
  get origin() {
353
- return this._cachedOrigin ??= this.url.origin;
430
+ return this[$cachedOrigin] ??= this.url.origin;
354
431
  }
355
432
  /**
356
433
  * Request headers
@@ -371,6 +448,24 @@ class ShokupanContext {
371
448
  get res() {
372
449
  return this.response;
373
450
  }
451
+ /**
452
+ * Raw WebSocket connection
453
+ */
454
+ get ws() {
455
+ return this[$ws];
456
+ }
457
+ /**
458
+ * Socket.io socket
459
+ */
460
+ get socket() {
461
+ return this[$socket];
462
+ }
463
+ /**
464
+ * Socket.io server
465
+ */
466
+ get io() {
467
+ return this[$io];
468
+ }
374
469
  /**
375
470
  * Helper to set a header on the response
376
471
  * @param key Header key
@@ -380,6 +475,20 @@ class ShokupanContext {
380
475
  this.response.set(key, value);
381
476
  return this;
382
477
  }
478
+ isUpgraded = false;
479
+ /**
480
+ * Upgrades the request to a WebSocket connection.
481
+ * @param options Upgrade options
482
+ * @returns true if upgraded, false otherwise
483
+ */
484
+ upgrade(options) {
485
+ if (!this.server) return false;
486
+ const success = this.server.upgrade(this.req, options);
487
+ if (success) {
488
+ this.isUpgraded = true;
489
+ }
490
+ return success;
491
+ }
383
492
  /**
384
493
  * Set a cookie
385
494
  * @param name Cookie name
@@ -457,18 +566,18 @@ class ShokupanContext {
457
566
  * The body is only parsed once and cached for subsequent reads.
458
567
  */
459
568
  async body() {
460
- if (this._bodyParseError) {
461
- throw this._bodyParseError;
569
+ if (this[$bodyParseError]) {
570
+ throw this[$bodyParseError];
462
571
  }
463
- if (this._bodyParsed) {
464
- return this._cachedBody;
572
+ if (this[$bodyParsed]) {
573
+ return this[$cachedBody];
465
574
  }
466
575
  const contentType = this.request.headers.get("content-type") || "";
467
576
  if (contentType.includes("application/json") || contentType.includes("+json")) {
468
577
  const parserType = this.app?.applicationConfig?.jsonParser || "native";
469
578
  if (parserType === "native") {
470
579
  try {
471
- this._cachedBody = await this.request.json();
580
+ this[$cachedBody] = await this.request.json();
472
581
  } catch (e) {
473
582
  throw e;
474
583
  }
@@ -476,18 +585,18 @@ class ShokupanContext {
476
585
  const rawText = await this.request.text();
477
586
  const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
478
587
  const parser = getJSONParser(parserType);
479
- this._cachedBody = parser(rawText);
588
+ this[$cachedBody] = parser(rawText);
480
589
  }
481
- this._bodyType = "json";
590
+ this[$bodyType] = "json";
482
591
  } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
483
- this._cachedBody = await this.request.formData();
484
- this._bodyType = "formData";
592
+ this[$cachedBody] = await this.request.formData();
593
+ this[$bodyType] = "formData";
485
594
  } else {
486
- this._cachedBody = await this.request.text();
487
- this._bodyType = "text";
595
+ this[$cachedBody] = await this.request.text();
596
+ this[$bodyType] = "text";
488
597
  }
489
- this._bodyParsed = true;
490
- return this._cachedBody;
598
+ this[$bodyParsed] = true;
599
+ return this[$cachedBody];
491
600
  }
492
601
  /**
493
602
  * Pre-parse the request body before handler execution.
@@ -495,7 +604,7 @@ class ShokupanContext {
495
604
  * Errors are deferred until the body is actually accessed in the handler.
496
605
  */
497
606
  async parseBody() {
498
- if (this._bodyParsed) {
607
+ if (this[$bodyParsed]) {
499
608
  return;
500
609
  }
501
610
  if (this.request.method === "GET" || this.request.method === "HEAD") {
@@ -504,7 +613,7 @@ class ShokupanContext {
504
613
  try {
505
614
  await this.body();
506
615
  } catch (error) {
507
- this._bodyParseError = error;
616
+ this[$bodyParseError] = error;
508
617
  }
509
618
  }
510
619
  /**
@@ -554,10 +663,21 @@ class ShokupanContext {
554
663
  throw new Error(`Invalid HTTP status code: ${status}`);
555
664
  }
556
665
  if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
557
- this._rawBody = body;
666
+ this[$rawBody] = body;
667
+ }
668
+ return this[$finalResponse] ??= new Response(body, { status, headers });
669
+ }
670
+ /**
671
+ * Emit an event to the client (WebSocket only)
672
+ * @param event Event name
673
+ * @param data Event data (Must be JSON serializable)
674
+ */
675
+ emit(event, data) {
676
+ if (this[$ws]) {
677
+ this[$ws].send(JSON.stringify({ event, data }));
678
+ } else if (this[$socket]) {
679
+ this[$socket].emit(event, data);
558
680
  }
559
- this._finalResponse = new Response(body, { status, headers });
560
- return this._finalResponse;
561
681
  }
562
682
  /**
563
683
  * Respond with a JSON object
@@ -568,18 +688,18 @@ class ShokupanContext {
568
688
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
569
689
  }
570
690
  const jsonString = JSON.stringify(data);
571
- this._rawBody = jsonString;
691
+ this[$rawBody] = jsonString;
572
692
  if (!headers && !this.response.hasPopulatedHeaders) {
573
- this._finalResponse = new Response(jsonString, {
693
+ this[$finalResponse] = new Response(jsonString, {
574
694
  status: finalStatus,
575
695
  headers: { "content-type": "application/json" }
576
696
  });
577
- return this._finalResponse;
697
+ return this[$finalResponse];
578
698
  }
579
699
  const finalHeaders = this.mergeHeaders(headers);
580
700
  finalHeaders.set("content-type", "application/json");
581
- this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
582
- return this._finalResponse;
701
+ this[$finalResponse] = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
702
+ return this[$finalResponse];
583
703
  }
584
704
  /**
585
705
  * Respond with a text string
@@ -589,18 +709,18 @@ class ShokupanContext {
589
709
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
590
710
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
591
711
  }
592
- this._rawBody = data;
712
+ this[$rawBody] = data;
593
713
  if (!headers && !this.response.hasPopulatedHeaders) {
594
- this._finalResponse = new Response(data, {
714
+ this[$finalResponse] = new Response(data, {
595
715
  status: finalStatus,
596
716
  headers: { "content-type": "text/plain; charset=utf-8" }
597
717
  });
598
- return this._finalResponse;
718
+ return this[$finalResponse];
599
719
  }
600
720
  const finalHeaders = this.mergeHeaders(headers);
601
721
  finalHeaders.set("content-type", "text/plain; charset=utf-8");
602
- this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
603
- return this._finalResponse;
722
+ this[$finalResponse] = new Response(data, { status: finalStatus, headers: finalHeaders });
723
+ return this[$finalResponse];
604
724
  }
605
725
  /**
606
726
  * Respond with HTML content
@@ -612,9 +732,9 @@ class ShokupanContext {
612
732
  }
613
733
  const finalHeaders = this.mergeHeaders(headers);
614
734
  finalHeaders.set("content-type", "text/html; charset=utf-8");
615
- this._rawBody = html;
616
- this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
617
- return this._finalResponse;
735
+ this[$rawBody] = html;
736
+ this[$finalResponse] = new Response(html, { status: finalStatus, headers: finalHeaders });
737
+ return this[$finalResponse];
618
738
  }
619
739
  /**
620
740
  * Respond with a redirect
@@ -625,8 +745,8 @@ class ShokupanContext {
625
745
  }
626
746
  const headers = this.mergeHeaders();
627
747
  headers.set("Location", url);
628
- this._finalResponse = new Response(null, { status, headers });
629
- return this._finalResponse;
748
+ this[$finalResponse] = new Response(null, { status, headers });
749
+ return this[$finalResponse];
630
750
  }
631
751
  /**
632
752
  * Respond with a status code
@@ -637,8 +757,8 @@ class ShokupanContext {
637
757
  throw new Error(`Invalid HTTP status code: ${status}`);
638
758
  }
639
759
  const headers = this.mergeHeaders();
640
- this._finalResponse = new Response(null, { status, headers });
641
- return this._finalResponse;
760
+ this[$finalResponse] = new Response(null, { status, headers });
761
+ return this[$finalResponse];
642
762
  }
643
763
  /**
644
764
  * Respond with a file
@@ -650,15 +770,15 @@ class ShokupanContext {
650
770
  throw new Error(`Invalid HTTP status code: ${status}`);
651
771
  }
652
772
  if (typeof Bun !== "undefined") {
653
- this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
654
- return this._finalResponse;
773
+ this[$finalResponse] = new Response(Bun.file(path2, fileOptions), { status, headers });
774
+ return this[$finalResponse];
655
775
  } else {
656
776
  const fileBuffer = await promises.readFile(path2);
657
777
  if (fileOptions?.type) {
658
778
  headers.set("content-type", fileOptions.type);
659
779
  }
660
- this._finalResponse = new Response(fileBuffer, { status, headers });
661
- return this._finalResponse;
780
+ this[$finalResponse] = new Response(fileBuffer, { status, headers });
781
+ return this[$finalResponse];
662
782
  }
663
783
  }
664
784
  /**
@@ -694,10 +814,10 @@ const compose = (middleware) => {
694
814
  return next ? next() : Promise.resolve();
695
815
  }
696
816
  const fn = middleware[i];
697
- if (!context._debug) {
817
+ if (!context[$debug]) {
698
818
  return fn(context, () => runner(i + 1));
699
819
  }
700
- const debug = context._debug;
820
+ const debug = context[$debug];
701
821
  const debugId = fn._debugId || fn.name || "anonymous";
702
822
  const previousNode = debug.getCurrentNode();
703
823
  debug.trackEdge(previousNode, debugId);
@@ -773,21 +893,6 @@ function deepMerge(target, ...sources) {
773
893
  }
774
894
  return deepMerge(target, ...sources);
775
895
  }
776
- const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
777
- const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
778
- const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
779
- const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
780
- const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
781
- const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
782
- const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
783
- const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
784
- const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
785
- const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
786
- const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
787
- const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
788
- const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
789
- const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
790
- const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
791
896
  const REGEX_PATTERNS = {
792
897
  QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
793
898
  QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
@@ -1174,6 +1279,35 @@ class RequestContextStore {
1174
1279
  span;
1175
1280
  }
1176
1281
  const asyncContext = new node_async_hooks.AsyncLocalStorage();
1282
+ class HttpError extends Error {
1283
+ status;
1284
+ constructor(message, status) {
1285
+ super(message);
1286
+ this.name = "HttpError";
1287
+ this.status = status;
1288
+ if (Error.captureStackTrace) {
1289
+ Error.captureStackTrace(this, HttpError);
1290
+ }
1291
+ }
1292
+ }
1293
+ function getErrorStatus(err) {
1294
+ if (!err || typeof err !== "object") {
1295
+ return 500;
1296
+ }
1297
+ if (typeof err.status === "number") {
1298
+ return err.status;
1299
+ }
1300
+ if (typeof err.statusCode === "number") {
1301
+ return err.statusCode;
1302
+ }
1303
+ return 500;
1304
+ }
1305
+ class EventError extends HttpError {
1306
+ constructor(message = "Event Error") {
1307
+ super(message, 500);
1308
+ this.name = "EventError";
1309
+ }
1310
+ }
1177
1311
  const eta$1 = new eta$2.Eta();
1178
1312
  function serveStatic(config, prefix) {
1179
1313
  const rootPath = path.resolve(config.root || ".");
@@ -1326,20 +1460,18 @@ function serveStatic(config, prefix) {
1326
1460
  serveStaticMiddleware.pluginName = "ServeStatic";
1327
1461
  return serveStaticMiddleware;
1328
1462
  }
1329
- let db;
1330
- let dbPromise = null;
1331
- let RecordId;
1463
+ const G = globalThis;
1464
+ G.__shokupan_db = G.__shokupan_db || null;
1465
+ G.__shokupan_db_promise = G.__shokupan_db_promise || null;
1332
1466
  async function ensureDb() {
1333
- if (db) return db;
1334
- if (dbPromise) return dbPromise;
1335
- dbPromise = (async () => {
1467
+ if (G.__shokupan_db) return G.__shokupan_db;
1468
+ if (G.__shokupan_db_promise) return G.__shokupan_db_promise;
1469
+ G.__shokupan_db_promise = (async () => {
1336
1470
  try {
1337
1471
  const { createNodeEngines } = await import("@surrealdb/node");
1338
1472
  const surreal = await import("surrealdb");
1339
- const Surreal = surreal.Surreal;
1340
- RecordId = surreal.RecordId;
1341
1473
  const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1342
- const _db = new Surreal({
1474
+ const _db = new surrealdb.Surreal({
1343
1475
  engines: createNodeEngines()
1344
1476
  });
1345
1477
  await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
@@ -1350,33 +1482,33 @@ async function ensureDb() {
1350
1482
  DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1351
1483
  DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1352
1484
  DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1485
+ DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
1353
1486
  `);
1354
- db = _db;
1355
- return db;
1487
+ G.__shokupan_db = _db;
1488
+ return _db;
1356
1489
  } catch (e) {
1357
- dbPromise = null;
1490
+ G.__shokupan_db_promise = null;
1358
1491
  if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1359
1492
  throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1360
1493
  }
1361
1494
  throw e;
1362
1495
  }
1363
1496
  })();
1364
- return dbPromise;
1497
+ return G.__shokupan_db_promise;
1365
1498
  }
1366
1499
  const datastore = {
1367
- async get(store, key) {
1500
+ async get(recordId) {
1368
1501
  await ensureDb();
1369
- return db.select(new RecordId(store, key));
1502
+ return G.__shokupan_db.select(recordId);
1370
1503
  },
1371
- async set(store, key, value) {
1504
+ async set(recordId, value) {
1372
1505
  await ensureDb();
1373
- return db.create(new RecordId(store, key)).content(value);
1506
+ return G.__shokupan_db.upsert(recordId).content(value);
1374
1507
  },
1375
1508
  async query(query, vars) {
1376
1509
  await ensureDb();
1377
1510
  try {
1378
- const r = await db.query(query, vars);
1379
- return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
1511
+ return G.__shokupan_db.query(query, vars).collect();
1380
1512
  } catch (e) {
1381
1513
  console.error("DS ERROR:", e);
1382
1514
  throw e;
@@ -1387,7 +1519,7 @@ const datastore = {
1387
1519
  }
1388
1520
  };
1389
1521
  process.on("exit", async () => {
1390
- if (db) await db.close();
1522
+ if (G.__shokupan_db) await G.__shokupan_db.close();
1391
1523
  });
1392
1524
  class Container {
1393
1525
  static services = /* @__PURE__ */ new Map();
@@ -1618,6 +1750,7 @@ class ShokupanRouter {
1618
1750
  metadata;
1619
1751
  // Metadata for the router itself
1620
1752
  currentGuards = [];
1753
+ eventHandlers = /* @__PURE__ */ new Map();
1621
1754
  // Registry Accessor
1622
1755
  getComponentRegistry() {
1623
1756
  const controllerRoutesMap = /* @__PURE__ */ new Map();
@@ -1678,6 +1811,34 @@ class ShokupanRouter {
1678
1811
  isRouterInstance(target) {
1679
1812
  return typeof target === "object" && target !== null && $isRouter in target;
1680
1813
  }
1814
+ /**
1815
+ * Registers an event handler for WebSocket.
1816
+ */
1817
+ event(name, handler) {
1818
+ if (this.eventHandlers.has(name)) {
1819
+ const err = new EventError(`Event handler \`${name}\` already exists.`);
1820
+ console.warn(err);
1821
+ const handlers = this.eventHandlers.get(name);
1822
+ handlers.push(handler);
1823
+ this.eventHandlers.set(name, handlers);
1824
+ } else {
1825
+ this.eventHandlers.set(name, [handler]);
1826
+ }
1827
+ return this;
1828
+ }
1829
+ /**
1830
+ * Finds an event handler(s) by name.
1831
+ */
1832
+ findEvent(name) {
1833
+ if (this.eventHandlers.has(name)) {
1834
+ return this.eventHandlers.get(name);
1835
+ }
1836
+ for (const child of this[$childRouters]) {
1837
+ const handler = child.findEvent(name);
1838
+ if (handler) return handler;
1839
+ }
1840
+ return null;
1841
+ }
1681
1842
  /**
1682
1843
  * Mounts a controller instance to a path prefix.
1683
1844
  *
@@ -1778,7 +1939,7 @@ class ShokupanRouter {
1778
1939
  });
1779
1940
  const ctx = new ShokupanContext(req);
1780
1941
  let result = null;
1781
- let status = 200;
1942
+ let status = HTTP_STATUS.OK;
1782
1943
  const headers = {};
1783
1944
  const match = this.find(req.method, ctx.path);
1784
1945
  if (match) {
@@ -1787,12 +1948,12 @@ class ShokupanRouter {
1787
1948
  result = await match.handler(ctx);
1788
1949
  } catch (err) {
1789
1950
  console.error(err);
1790
- status = err.status || err.statusCode || 500;
1951
+ status = getErrorStatus(err);
1791
1952
  result = { error: err.message || "Internal Server Error" };
1792
1953
  if (err.errors) result.errors = err.errors;
1793
1954
  }
1794
1955
  } else {
1795
- status = 404;
1956
+ status = HTTP_STATUS.NOT_FOUND;
1796
1957
  result = "Not Found";
1797
1958
  }
1798
1959
  if (result instanceof Response) {
@@ -1821,7 +1982,7 @@ class ShokupanRouter {
1821
1982
  const originalHandler = handler;
1822
1983
  const wrapped = async (ctx) => {
1823
1984
  await this.runHooks("onRequestStart", ctx);
1824
- const debug = ctx._debug;
1985
+ const debug = ctx[$debug];
1825
1986
  let debugId;
1826
1987
  let previousNode;
1827
1988
  if (debug) {
@@ -1911,6 +2072,7 @@ class ShokupanRouter {
1911
2072
  const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1912
2073
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1913
2074
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
2075
+ const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
1914
2076
  let routesAttached = 0;
1915
2077
  for (let i = 0; i < Array.from(methods).length; i++) {
1916
2078
  const name = Array.from(methods)[i];
@@ -2055,6 +2217,39 @@ class ShokupanRouter {
2055
2217
  const spec = { tags: [tagName], ...userSpec };
2056
2218
  this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2057
2219
  }
2220
+ if (decoratedEvents?.has(name)) {
2221
+ routesAttached++;
2222
+ const config = decoratedEvents.get(name);
2223
+ const routeArgs = decoratedArgs?.get(name);
2224
+ const wrappedHandler = async (ctx) => {
2225
+ let args = [ctx];
2226
+ if (routeArgs?.length > 0) {
2227
+ args = [];
2228
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2229
+ for (let k = 0; k < sortedArgs.length; k++) {
2230
+ const arg = sortedArgs[k];
2231
+ switch (arg.type) {
2232
+ case RouteParamType.BODY:
2233
+ args[arg.index] = await ctx.body();
2234
+ break;
2235
+ case RouteParamType.CONTEXT:
2236
+ args[arg.index] = ctx;
2237
+ break;
2238
+ case RouteParamType.REQUEST:
2239
+ args[arg.index] = ctx.req;
2240
+ break;
2241
+ case RouteParamType.HEADER:
2242
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2243
+ break;
2244
+ default:
2245
+ args[arg.index] = void 0;
2246
+ }
2247
+ }
2248
+ }
2249
+ return originalHandler.apply(instance, args);
2250
+ };
2251
+ this.event(config.eventName, wrappedHandler);
2252
+ }
2058
2253
  }
2059
2254
  if (routesAttached === 0) {
2060
2255
  console.warn(`No routes attached to controller ${instance.constructor.name}`);
@@ -2218,8 +2413,10 @@ class ShokupanRouter {
2218
2413
  Promise.resolve().then(async () => {
2219
2414
  try {
2220
2415
  const timestamp = Date.now();
2221
- const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2222
- await datastore.set("middleware_tracking", key, {
2416
+ await datastore.set(new surrealdb.RecordId("middleware_tracking", {
2417
+ timestamp,
2418
+ name: handler.name || "anonymous"
2419
+ }), {
2223
2420
  name: handler.name || "anonymous",
2224
2421
  path: ctx.path,
2225
2422
  timestamp,
@@ -2237,7 +2434,7 @@ class ShokupanRouter {
2237
2434
  const cutoff = Date.now() - ttl;
2238
2435
  await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2239
2436
  const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2240
- if (results && results[0] && results[0].count > maxCapacity) {
2437
+ if (results?.[0]?.count > maxCapacity) {
2241
2438
  const toDelete = results[0].count - maxCapacity;
2242
2439
  await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2243
2440
  }
@@ -2314,7 +2511,7 @@ class ShokupanRouter {
2314
2511
  (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
2315
2512
  );
2316
2513
  if (callerLine) {
2317
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
2514
+ const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
2318
2515
  if (match) {
2319
2516
  file = match[1];
2320
2517
  line = parseInt(match[2], 10);
@@ -2332,7 +2529,7 @@ class ShokupanRouter {
2332
2529
  }
2333
2530
  return guardHandler(ctx, next);
2334
2531
  };
2335
- trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
2532
+ trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
2336
2533
  this.currentGuards.push({ handler: trackedGuard, spec });
2337
2534
  return this;
2338
2535
  }
@@ -2445,7 +2642,7 @@ class ShokupanRouter {
2445
2642
  const fns = this.hookCache.get(name);
2446
2643
  if (!fns) return;
2447
2644
  const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2448
- const debug = ctx?._debug;
2645
+ const debug = ctx?.[$debug];
2449
2646
  if (debug) {
2450
2647
  await Promise.all(fns.map(async (fn, index) => {
2451
2648
  const hookId = `hook_${name}_${fn.name || index}`;
@@ -2518,6 +2715,7 @@ const defaults = {
2518
2715
  hostname: "localhost",
2519
2716
  development: process.env.NODE_ENV !== "production",
2520
2717
  enableAsyncLocalStorage: false,
2718
+ enableHttpBridge: false,
2521
2719
  reusePort: false
2522
2720
  };
2523
2721
  api.trace.getTracer("shokupan.application");
@@ -2526,6 +2724,7 @@ class Shokupan extends ShokupanRouter {
2526
2724
  openApiSpec;
2527
2725
  composedMiddleware;
2528
2726
  cpuMonitor;
2727
+ server;
2529
2728
  get logger() {
2530
2729
  return this.applicationConfig.logger;
2531
2730
  }
@@ -2634,6 +2833,7 @@ class Shokupan extends ShokupanRouter {
2634
2833
  this.cpuMonitor = new SystemCpuMonitor();
2635
2834
  this.cpuMonitor.start();
2636
2835
  }
2836
+ const self = this;
2637
2837
  const serveOptions = {
2638
2838
  port: finalPort,
2639
2839
  hostname: this.applicationConfig.hostname,
@@ -2645,8 +2845,61 @@ class Shokupan extends ShokupanRouter {
2645
2845
  open(ws) {
2646
2846
  ws.data?.handler?.open?.(ws);
2647
2847
  },
2648
- message(ws, message) {
2649
- ws.data?.handler?.message?.(ws, message);
2848
+ async message(ws, message) {
2849
+ if (ws.data?.handler?.message) {
2850
+ return ws.data.handler.message(ws, message);
2851
+ }
2852
+ if (typeof message !== "string") return;
2853
+ try {
2854
+ const payload = JSON.parse(message);
2855
+ if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
2856
+ const { id, method, path: path2, headers, body } = payload;
2857
+ const url = new URL(path2, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
2858
+ const req = new Request(url.toString(), {
2859
+ method,
2860
+ headers,
2861
+ body: typeof body === "object" ? JSON.stringify(body) : body
2862
+ });
2863
+ const res = await self.fetch(req);
2864
+ const resBody = await res.json().catch((err) => res.text());
2865
+ const resHeaders = {};
2866
+ res.headers.forEach((v, k) => resHeaders[k] = v);
2867
+ ws.send(JSON.stringify({
2868
+ type: "RESPONSE",
2869
+ id,
2870
+ status: res.status,
2871
+ headers: resHeaders,
2872
+ body: resBody
2873
+ }));
2874
+ return;
2875
+ }
2876
+ const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
2877
+ if (eventName) {
2878
+ const handlers = self.findEvent(eventName);
2879
+ const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
2880
+ if (handler) {
2881
+ const data = payload.data || payload.payload;
2882
+ const req = new ShokupanRequest({
2883
+ url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
2884
+ method: "POST",
2885
+ headers: new Headers({ "content-type": "application/json" }),
2886
+ body: JSON.stringify(data)
2887
+ });
2888
+ const ctx = new ShokupanContext(req, self.server);
2889
+ ctx[$ws] = ws;
2890
+ try {
2891
+ await handler(ctx);
2892
+ } catch (err) {
2893
+ if (self.applicationConfig["websocketErrorHandler"]) {
2894
+ await self.applicationConfig["websocketErrorHandler"](err, ctx);
2895
+ } else {
2896
+ console.error(`Error in event ${eventName}:`, err);
2897
+ }
2898
+ }
2899
+ }
2900
+ }
2901
+ } catch (e) {
2902
+ }
2650
2903
  },
2651
2904
  drain(ws) {
2652
2905
  ws.data?.handler?.drain?.(ws);
@@ -2658,12 +2911,40 @@ class Shokupan extends ShokupanRouter {
2658
2911
  };
2659
2912
  let factory = this.applicationConfig.serverFactory;
2660
2913
  if (!factory && typeof Bun === "undefined") {
2661
- const { createHttpServer } = await Promise.resolve().then(() => require("./http-server-DFhwlK8e.cjs"));
2914
+ const { createHttpServer } = await Promise.resolve().then(() => require("./http-server-BEMPIs33.cjs"));
2662
2915
  factory = createHttpServer();
2663
2916
  }
2664
- const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2917
+ this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2665
2918
  console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
2666
- return server;
2919
+ return this.server;
2920
+ }
2921
+ /**
2922
+ * Stops the application server.
2923
+ *
2924
+ * This method gracefully shuts down the server and stops any running monitors.
2925
+ * Works transparently in both Bun and Node.js runtimes.
2926
+ *
2927
+ * @returns A promise that resolves when the server has been stopped.
2928
+ *
2929
+ * @example
2930
+ * ```typescript
2931
+ * const app = new Shokupan();
2932
+ * const server = await app.listen(3000);
2933
+ *
2934
+ * // Later, when you want to stop the server
2935
+ * await app.stop();
2936
+ * ```
2937
+ * @param closeActiveConnections — Immediately terminate in-flight requests, websockets, and stop accepting new connections.
2938
+ */
2939
+ async stop(closeActiveConnections) {
2940
+ if (this.cpuMonitor) {
2941
+ this.cpuMonitor.stop();
2942
+ this.cpuMonitor = void 0;
2943
+ }
2944
+ if (this.server) {
2945
+ await this.server.stop(closeActiveConnections);
2946
+ this.server = void 0;
2947
+ }
2667
2948
  }
2668
2949
  [$dispatch](req) {
2669
2950
  return this.fetch(req);
@@ -2761,28 +3042,33 @@ class Shokupan extends ShokupanRouter {
2761
3042
  const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2762
3043
  const match = this.find(req.method, ctx.path);
2763
3044
  if (match) {
2764
- ctx._routeMatched = true;
3045
+ ctx[$routeMatched] = true;
2765
3046
  ctx.params = match.params;
2766
3047
  await bodyParsing;
2767
3048
  return match.handler(ctx);
2768
3049
  }
3050
+ if (ctx.upgrade()) {
3051
+ return void 0;
3052
+ }
2769
3053
  return null;
2770
3054
  });
2771
3055
  let response;
2772
3056
  if (result instanceof Response) {
2773
3057
  response = result;
2774
- } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2775
- response = ctx._finalResponse;
3058
+ } else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
3059
+ response = ctx[$finalResponse];
2776
3060
  } else if (result === null || result === void 0) {
2777
- if (ctx._finalResponse instanceof Response) {
2778
- response = ctx._finalResponse;
2779
- } else if (ctx._routeMatched) {
3061
+ if (ctx[$finalResponse] instanceof Response) {
3062
+ response = ctx[$finalResponse];
3063
+ } else if (ctx.isUpgraded) {
3064
+ return void 0;
3065
+ } else if (ctx[$routeMatched]) {
2780
3066
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2781
3067
  } else {
2782
- if (ctx.response.status !== 200) {
3068
+ if (ctx.response.status !== HTTP_STATUS.OK) {
2783
3069
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2784
3070
  } else {
2785
- response = ctx.text("Not Found", 404);
3071
+ response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
2786
3072
  }
2787
3073
  }
2788
3074
  } else if (typeof result === "object") {
@@ -2794,10 +3080,9 @@ class Shokupan extends ShokupanRouter {
2794
3080
  await this.runHooks("onResponseStart", ctx, response);
2795
3081
  return response;
2796
3082
  } catch (err) {
2797
- console.error(err);
2798
3083
  const span = asyncContext.getStore()?.span;
2799
3084
  if (span) span.setStatus({ code: 2 });
2800
- const status = err.status || err.statusCode || 500;
3085
+ const status = getErrorStatus(err);
2801
3086
  const body = { error: err.message || "Internal Server Error" };
2802
3087
  if (err.errors) body.errors = err.errors;
2803
3088
  await this.runHooks("onError", ctx, err);
@@ -2819,10 +3104,10 @@ class Shokupan extends ShokupanRouter {
2819
3104
  }
2820
3105
  return executionPromise.catch((err) => {
2821
3106
  if (err.message === "Request Timeout") {
2822
- return ctx.text("Request Timeout", 408);
3107
+ return ctx.text("Request Timeout", HTTP_STATUS.REQUEST_TIMEOUT);
2823
3108
  }
2824
3109
  console.error("Unexpected error in request execution:", err);
2825
- return ctx.text("Internal Server Error", 500);
3110
+ return ctx.text("Internal Server Error", HTTP_STATUS.INTERNAL_SERVER_ERROR);
2826
3111
  }).then(async (res) => {
2827
3112
  await this.runHooks("onResponseEnd", ctx, res);
2828
3113
  return res;
@@ -2990,6 +3275,14 @@ const Patch = createMethodDecorator("PATCH");
2990
3275
  const Options = createMethodDecorator("OPTIONS");
2991
3276
  const Head = createMethodDecorator("HEAD");
2992
3277
  const All = createMethodDecorator("ALL");
3278
+ function Event(eventName) {
3279
+ return (target, propertyKey, descriptor) => {
3280
+ target[$eventMethods] ??= /* @__PURE__ */ new Map();
3281
+ target[$eventMethods].set(propertyKey, {
3282
+ eventName
3283
+ });
3284
+ };
3285
+ }
2993
3286
  function RateLimit(options) {
2994
3287
  return Use(RateLimitMiddleware(options));
2995
3288
  }
@@ -3333,6 +3626,554 @@ class ClusterPlugin {
3333
3626
  }
3334
3627
  }
3335
3628
  }
3629
+ const INTERVALS = [
3630
+ { label: "10s", ms: 10 * 1e3 },
3631
+ { label: "1m", ms: 60 * 1e3 },
3632
+ { label: "5m", ms: 5 * 60 * 1e3 },
3633
+ { label: "1h", ms: 60 * 60 * 1e3 },
3634
+ { label: "2h", ms: 2 * 60 * 60 * 1e3 },
3635
+ { label: "6h", ms: 6 * 60 * 60 * 1e3 },
3636
+ { label: "12h", ms: 12 * 60 * 60 * 1e3 },
3637
+ { label: "1d", ms: 24 * 60 * 60 * 1e3 },
3638
+ { label: "3d", ms: 3 * 24 * 60 * 60 * 1e3 },
3639
+ { label: "7d", ms: 7 * 24 * 60 * 60 * 1e3 },
3640
+ { label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
3641
+ ];
3642
+ class MetricsCollector {
3643
+ currentIntervalStart = {};
3644
+ pendingDetails = {};
3645
+ eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
3646
+ timer = null;
3647
+ constructor() {
3648
+ this.eventLoopHistogram.enable();
3649
+ const now = Date.now();
3650
+ INTERVALS.forEach((int) => {
3651
+ this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3652
+ this.pendingDetails[int.label] = [];
3653
+ });
3654
+ this.timer = setInterval(() => this.collect(), 1e4);
3655
+ }
3656
+ recordRequest(duration, isError) {
3657
+ INTERVALS.forEach((int) => {
3658
+ this.pendingDetails[int.label].push({ duration, isError });
3659
+ });
3660
+ }
3661
+ alignTimestamp(ts, intervalMs) {
3662
+ return Math.floor(ts / intervalMs) * intervalMs;
3663
+ }
3664
+ async collect() {
3665
+ try {
3666
+ const now = Date.now();
3667
+ console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
3668
+ for (const int of INTERVALS) {
3669
+ const start = this.currentIntervalStart[int.label];
3670
+ if (now >= start + int.ms) {
3671
+ console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
3672
+ await this.flushInterval(int.label, start, int.ms);
3673
+ this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3674
+ }
3675
+ }
3676
+ } catch (error) {
3677
+ console.error("[MetricsCollector] Error in collect():", error);
3678
+ }
3679
+ }
3680
+ async flushInterval(label, timestamp, durationMs) {
3681
+ const reqs = this.pendingDetails[label];
3682
+ console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
3683
+ this.pendingDetails[label] = [];
3684
+ if (reqs.length === 0) {
3685
+ console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
3686
+ return;
3687
+ }
3688
+ const totalReqs = reqs.length;
3689
+ const errorReqs = reqs.filter((r) => r.isError).length;
3690
+ const successReqs = totalReqs - errorReqs;
3691
+ const duratons = reqs.map((r) => r.duration).sort((a, b) => a - b);
3692
+ const rps = totalReqs / (durationMs / 1e3);
3693
+ const sum = duratons.reduce((a, b) => a + b, 0);
3694
+ const avg = totalReqs > 0 ? sum / totalReqs : 0;
3695
+ const getP = (p) => {
3696
+ if (duratons.length === 0) return 0;
3697
+ const idx = Math.floor(duratons.length * p);
3698
+ return duratons[idx];
3699
+ };
3700
+ const metric = {
3701
+ timestamp,
3702
+ interval: label,
3703
+ cpu: os__namespace.loadavg()[0],
3704
+ // Using load avg for simplicity as per requirements (Load)
3705
+ load: os__namespace.loadavg(),
3706
+ memory: {
3707
+ used: process.memoryUsage().rss,
3708
+ total: os__namespace.totalmem(),
3709
+ heapUsed: process.memoryUsage().heapUsed,
3710
+ heapTotal: process.memoryUsage().heapTotal
3711
+ },
3712
+ eventLoopLatency: {
3713
+ min: this.eventLoopHistogram.min / 1e6,
3714
+ max: this.eventLoopHistogram.max / 1e6,
3715
+ mean: this.eventLoopHistogram.mean / 1e6,
3716
+ p50: this.eventLoopHistogram.percentile(50) / 1e6,
3717
+ p95: this.eventLoopHistogram.percentile(95) / 1e6,
3718
+ p99: this.eventLoopHistogram.percentile(99) / 1e6
3719
+ },
3720
+ requests: {
3721
+ total: totalReqs,
3722
+ rps,
3723
+ success: successReqs,
3724
+ error: errorReqs
3725
+ },
3726
+ responseTime: {
3727
+ min: duratons[0] || 0,
3728
+ max: duratons[duratons.length - 1] || 0,
3729
+ avg,
3730
+ p50: getP(0.5),
3731
+ p95: getP(0.95),
3732
+ p99: getP(0.99)
3733
+ }
3734
+ };
3735
+ console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
3736
+ try {
3737
+ const recordId = new surrealdb.RecordId("metrics", timestamp);
3738
+ await datastore.set(recordId, metric);
3739
+ console.log(`[MetricsCollector] ✓ Successfully saved ${label} metric to datastore`);
3740
+ const test = await datastore.get(recordId);
3741
+ console.log(`[MetricsCollector] DEBUG: Immediate .get() returned:`, test ? "DATA" : "NULL");
3742
+ const queryTest = await datastore.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
3743
+ console.log(`[MetricsCollector] DEBUG: Query by id returned ${queryTest[0]?.length || 0} records`);
3744
+ } catch (e) {
3745
+ console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
3746
+ }
3747
+ }
3748
+ // Cleanup if needed
3749
+ stop() {
3750
+ if (this.timer) clearInterval(this.timer);
3751
+ this.eventLoopHistogram.disable();
3752
+ }
3753
+ }
3754
+ class Collector {
3755
+ constructor(dashboard) {
3756
+ this.dashboard = dashboard;
3757
+ }
3758
+ currentNode;
3759
+ trackStep(id, type, duration, status, error) {
3760
+ if (!id) return;
3761
+ this.dashboard.recordNodeMetric(id, type, duration, status === "error");
3762
+ }
3763
+ trackEdge(fromId, toId) {
3764
+ if (!fromId || !toId) return;
3765
+ this.dashboard.recordEdgeMetric(fromId, toId);
3766
+ }
3767
+ setNode(id) {
3768
+ this.currentNode = id;
3769
+ }
3770
+ getCurrentNode() {
3771
+ return this.currentNode;
3772
+ }
3773
+ }
3774
+ class Dashboard {
3775
+ constructor(dashboardConfig = {}) {
3776
+ this.dashboardConfig = dashboardConfig;
3777
+ }
3778
+ static __dirname = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
3779
+ // Get base path for dashboard files - works in both dev (src/) and production (dist/)
3780
+ static getBasePath() {
3781
+ const dir = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
3782
+ if (dir.endsWith("dist")) {
3783
+ return dir + "/plugins/application/dashboard";
3784
+ }
3785
+ return dir;
3786
+ }
3787
+ router = new ShokupanRouter();
3788
+ metrics = {
3789
+ totalRequests: 0,
3790
+ successfulRequests: 0,
3791
+ failedRequests: 0,
3792
+ activeRequests: 0,
3793
+ averageTotalTime_ms: 0,
3794
+ recentTimings: [],
3795
+ logs: [],
3796
+ rateLimitedCounts: {},
3797
+ nodeMetrics: {},
3798
+ edgeMetrics: {}
3799
+ };
3800
+ eta = new eta$2.Eta({
3801
+ views: Dashboard.getBasePath() + "/static",
3802
+ cache: false
3803
+ });
3804
+ startTime = Date.now();
3805
+ instrumented = false;
3806
+ metricsCollector = new MetricsCollector();
3807
+ // ShokupanPlugin interface implementation
3808
+ onInit(app, options) {
3809
+ this[$appRoot] = app;
3810
+ const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
3811
+ const hooks = this.getHooks();
3812
+ if (!app.middleware) {
3813
+ app.middleware = [];
3814
+ }
3815
+ const hooksMiddleware = async (ctx, next) => {
3816
+ if (hooks.onRequestStart) {
3817
+ await hooks.onRequestStart(ctx);
3818
+ }
3819
+ await next();
3820
+ if (hooks.onResponseEnd) {
3821
+ const effectiveResponse = ctx._finalResponse || ctx.response || {};
3822
+ await hooks.onResponseEnd(ctx, effectiveResponse);
3823
+ }
3824
+ };
3825
+ app.use(hooksMiddleware);
3826
+ app.mount(mountPath, this.router);
3827
+ this.setupRoutes();
3828
+ }
3829
+ setupRoutes() {
3830
+ this.router.get("/metrics", async (ctx) => {
3831
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
3832
+ const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
3833
+ const interval = ctx.query["interval"];
3834
+ if (interval) {
3835
+ const intervalMap = {
3836
+ "10s": 10 * 1e3,
3837
+ "1m": 60 * 1e3,
3838
+ "5m": 5 * 60 * 1e3,
3839
+ "30m": 30 * 60 * 1e3,
3840
+ "1h": 60 * 60 * 1e3,
3841
+ "2h": 2 * 60 * 60 * 1e3,
3842
+ "6h": 6 * 60 * 60 * 1e3,
3843
+ "12h": 12 * 60 * 60 * 1e3,
3844
+ "1d": 24 * 60 * 60 * 1e3,
3845
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3846
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3847
+ "30d": 30 * 24 * 60 * 60 * 1e3
3848
+ };
3849
+ const ms = intervalMap[interval] || 60 * 1e3;
3850
+ const startTime = Date.now() - ms;
3851
+ let stats;
3852
+ try {
3853
+ stats = await datastore.query(`
3854
+ SELECT
3855
+ count() as total,
3856
+ count(IF status < 400 THEN 1 END) as success,
3857
+ count(IF status >= 400 THEN 1 END) as failed,
3858
+ math::mean(duration) as avg_latency
3859
+ FROM requests
3860
+ WHERE timestamp >= $start
3861
+ GROUP ALL
3862
+ `, { start: startTime });
3863
+ } catch (error) {
3864
+ console.error("[Dashboard] Query failed at plugin.ts:180-191", {
3865
+ error,
3866
+ interval,
3867
+ startTime,
3868
+ query: "metrics interval stats",
3869
+ stack: new Error().stack
3870
+ });
3871
+ throw error;
3872
+ }
3873
+ const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
3874
+ return ctx.json({
3875
+ metrics: {
3876
+ totalRequests: s.total || 0,
3877
+ successfulRequests: s.success || 0,
3878
+ failedRequests: s.failed || 0,
3879
+ activeRequests: this.metrics.activeRequests,
3880
+ averageTotalTime_ms: s.avg_latency || 0,
3881
+ recentTimings: this.metrics.recentTimings,
3882
+ logs: [],
3883
+ rateLimitedCounts: this.metrics.rateLimitedCounts,
3884
+ nodeMetrics: this.metrics.nodeMetrics,
3885
+ edgeMetrics: this.metrics.edgeMetrics
3886
+ },
3887
+ uptime
3888
+ });
3889
+ }
3890
+ return ctx.json({
3891
+ metrics: this.metrics,
3892
+ uptime
3893
+ });
3894
+ });
3895
+ this.router.get("/metrics/history", async (ctx) => {
3896
+ const interval = ctx.query["interval"] || "1m";
3897
+ const intervalMap = {
3898
+ "10s": 10 * 1e3,
3899
+ "1m": 60 * 1e3,
3900
+ "5m": 5 * 60 * 1e3,
3901
+ "30m": 30 * 60 * 1e3,
3902
+ "1h": 60 * 60 * 1e3,
3903
+ "2h": 2 * 60 * 60 * 1e3,
3904
+ "6h": 6 * 60 * 60 * 1e3,
3905
+ "12h": 12 * 60 * 60 * 1e3,
3906
+ "1d": 24 * 60 * 60 * 1e3,
3907
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3908
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3909
+ "30d": 30 * 24 * 60 * 60 * 1e3
3910
+ };
3911
+ const periodMs = intervalMap[interval] || 60 * 1e3;
3912
+ const startTime = Date.now() - periodMs * 3;
3913
+ const endTime = Date.now();
3914
+ const result = await datastore.query(
3915
+ "SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
3916
+ { start: startTime, end: endTime, interval }
3917
+ );
3918
+ return ctx.json({
3919
+ metrics: result[0] || []
3920
+ });
3921
+ });
3922
+ const getIntervalStartTime = (interval) => {
3923
+ if (!interval) return 0;
3924
+ const intervalMap = {
3925
+ "10s": 10 * 1e3,
3926
+ "1m": 60 * 1e3,
3927
+ "5m": 5 * 60 * 1e3,
3928
+ "30m": 30 * 60 * 1e3,
3929
+ "1h": 60 * 60 * 1e3,
3930
+ "2h": 2 * 60 * 60 * 1e3,
3931
+ "6h": 6 * 60 * 60 * 1e3,
3932
+ "12h": 12 * 60 * 60 * 1e3,
3933
+ "1d": 24 * 60 * 60 * 1e3,
3934
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3935
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3936
+ "30d": 30 * 24 * 60 * 60 * 1e3
3937
+ };
3938
+ const ms = intervalMap[interval] || 0;
3939
+ return ms ? Date.now() - ms : 0;
3940
+ };
3941
+ this.router.get("/requests/top", async (ctx) => {
3942
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3943
+ const result = await datastore.query(
3944
+ "SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3945
+ { start: startTime }
3946
+ );
3947
+ return ctx.json({ top: result[0] || [] });
3948
+ });
3949
+ this.router.get("/errors/top", async (ctx) => {
3950
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3951
+ const result = await datastore.query(
3952
+ "SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
3953
+ { start: startTime }
3954
+ );
3955
+ return ctx.json({ top: result[0] || [] });
3956
+ });
3957
+ this.router.get("/requests/failing", async (ctx) => {
3958
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3959
+ const result = await datastore.query(
3960
+ "SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3961
+ { start: startTime }
3962
+ );
3963
+ return ctx.json({ top: result[0] || [] });
3964
+ });
3965
+ this.router.get("/requests/slowest", async (ctx) => {
3966
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3967
+ const result = await datastore.query(
3968
+ "SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
3969
+ { start: startTime }
3970
+ );
3971
+ return ctx.json({ slowest: result[0] || [] });
3972
+ });
3973
+ this.router.get("/registry", (ctx) => {
3974
+ const app = this[$appRoot];
3975
+ if (!this.instrumented && app) {
3976
+ this.instrumentApp(app);
3977
+ }
3978
+ const registry = app?.getComponentRegistry?.();
3979
+ if (registry) {
3980
+ this.assignIdsToRegistry(registry, "root");
3981
+ }
3982
+ return ctx.json({ registry: registry || {} });
3983
+ });
3984
+ this.router.get("/requests", async (ctx) => {
3985
+ const result = await datastore.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
3986
+ return ctx.json({ requests: result[0] || [] });
3987
+ });
3988
+ this.router.get("/requests/:id", async (ctx) => {
3989
+ const result = await datastore.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
3990
+ return ctx.json({ request: result[0]?.[0] });
3991
+ });
3992
+ this.router.get("/failures", async (ctx) => {
3993
+ const result = await datastore.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
3994
+ return ctx.json({ failures: result[0] });
3995
+ });
3996
+ this.router.post("/replay", async (ctx) => {
3997
+ const body = await ctx.body();
3998
+ const app = this[$appRoot];
3999
+ if (!app) return unknownError(ctx);
4000
+ try {
4001
+ const result = await app.processRequest({
4002
+ method: body.method,
4003
+ path: body.url,
4004
+ // or path
4005
+ headers: body.headers,
4006
+ body: body.body
4007
+ });
4008
+ return ctx.json({
4009
+ status: result.status,
4010
+ headers: result.headers,
4011
+ data: result.data
4012
+ });
4013
+ } catch (e) {
4014
+ return ctx.json({ error: String(e) }, 500);
4015
+ }
4016
+ });
4017
+ this.router.get("/", async (ctx) => {
4018
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
4019
+ const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
4020
+ const linkPattern = this.getLinkPattern();
4021
+ const template = await promises.readFile(Dashboard.getBasePath() + "/template.eta", "utf8");
4022
+ return ctx.html(this.eta.renderString(template, {
4023
+ metrics: this.metrics,
4024
+ uptime,
4025
+ rootPath: process.cwd(),
4026
+ linkPattern,
4027
+ headers: this.dashboardConfig.getRequestHeaders?.()
4028
+ }));
4029
+ });
4030
+ }
4031
+ instrumentApp(app) {
4032
+ if (!app.getComponentRegistry) return;
4033
+ const registry = app.getComponentRegistry();
4034
+ this.assignIdsToRegistry(registry, "root");
4035
+ this.instrumented = true;
4036
+ }
4037
+ // Traverses registry, generates IDs, and attaches them to the actual function objects
4038
+ assignIdsToRegistry(node, parentId) {
4039
+ if (!node) return;
4040
+ const makeId = (type, parent, idx, name) => `${type}_${parent}_${idx}_${name.replace(/[^a-zA-Z0-9]/g, "")}`;
4041
+ node.middleware?.forEach((mw, idx) => {
4042
+ const id = makeId("mw", parentId, idx, mw.name);
4043
+ mw.id = id;
4044
+ if (mw._fn) mw._fn._debugId = id;
4045
+ });
4046
+ node.controllers?.forEach((ctrl, idx) => {
4047
+ const id = makeId("ctrl", parentId, idx, ctrl.name);
4048
+ ctrl.id = id;
4049
+ });
4050
+ node.routes?.forEach((r, idx) => {
4051
+ const id = makeId("route", parentId, idx, r.handlerName || "handler");
4052
+ r.id = id;
4053
+ if (r._fn) r._fn._debugId = id;
4054
+ });
4055
+ node.routers?.forEach((r, idx) => {
4056
+ const id = makeId("router", parentId, idx, r.path);
4057
+ r.id = id;
4058
+ this.assignIdsToRegistry(r.children, id);
4059
+ });
4060
+ }
4061
+ recordNodeMetric(id, type, duration, isError) {
4062
+ if (!this.metrics.nodeMetrics[id]) {
4063
+ this.metrics.nodeMetrics[id] = {
4064
+ id,
4065
+ type,
4066
+ requests: 0,
4067
+ totalTime: 0,
4068
+ failures: 0,
4069
+ name: id
4070
+ // simplify
4071
+ };
4072
+ }
4073
+ const m = this.metrics.nodeMetrics[id];
4074
+ m.requests++;
4075
+ m.totalTime += duration;
4076
+ if (isError) m.failures++;
4077
+ }
4078
+ recordEdgeMetric(from, to) {
4079
+ const key = `${from}|${to}`;
4080
+ this.metrics.edgeMetrics[key] = (this.metrics.edgeMetrics[key] || 0) + 1;
4081
+ }
4082
+ getLinkPattern() {
4083
+ const term = process.env["TERM_PROGRAM"] || "";
4084
+ if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
4085
+ return "vscode://file/{{absolute}}:{{line}}";
4086
+ }
4087
+ return "file:///{{absolute}}:{{line}}";
4088
+ }
4089
+ getHooks() {
4090
+ return {
4091
+ onRequestStart: (ctx) => {
4092
+ const app = this[$appRoot];
4093
+ if (!this.instrumented && app) {
4094
+ this.instrumentApp(app);
4095
+ }
4096
+ this.metrics.totalRequests++;
4097
+ this.metrics.activeRequests++;
4098
+ ctx._debugStartTime = performance.now();
4099
+ ctx[$debug] = new Collector(this);
4100
+ },
4101
+ onResponseEnd: async (ctx, response) => {
4102
+ this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
4103
+ const start = ctx._debugStartTime;
4104
+ let duration = 0;
4105
+ if (start) {
4106
+ duration = performance.now() - start;
4107
+ this.updateTiming(duration);
4108
+ }
4109
+ const isError = response.status >= 400;
4110
+ this.metricsCollector.recordRequest(duration, isError);
4111
+ if (response.status >= 400) {
4112
+ this.metrics.failedRequests++;
4113
+ if (response.status === 429) {
4114
+ const path2 = ctx.path;
4115
+ this.metrics.rateLimitedCounts[path2] = (this.metrics.rateLimitedCounts[path2] || 0) + 1;
4116
+ }
4117
+ try {
4118
+ const headers = {};
4119
+ if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
4120
+ ctx.request.headers.forEach((v, k) => {
4121
+ headers[k] = v;
4122
+ });
4123
+ }
4124
+ await datastore.set(new surrealdb.RecordId("failed_requests", ctx.requestId), {
4125
+ method: ctx.method,
4126
+ url: ctx.url.toString(),
4127
+ headers,
4128
+ status: response.status,
4129
+ timestamp: Date.now(),
4130
+ state: ctx.state
4131
+ // body?
4132
+ });
4133
+ } catch (e) {
4134
+ console.error("Failed to record failed request", e);
4135
+ }
4136
+ } else {
4137
+ this.metrics.successfulRequests++;
4138
+ }
4139
+ const logEntry = {
4140
+ method: ctx.method,
4141
+ url: ctx.url.toString(),
4142
+ status: response.status,
4143
+ duration,
4144
+ timestamp: Date.now(),
4145
+ handlerStack: ctx.handlerStack
4146
+ };
4147
+ this.metrics.logs.push(logEntry);
4148
+ try {
4149
+ await datastore.set(new surrealdb.RecordId("requests", ctx.requestId), logEntry);
4150
+ } catch (e) {
4151
+ console.error("Failed to record request log", e);
4152
+ }
4153
+ const retention = this.dashboardConfig.retentionMs ?? 72e5;
4154
+ const cutoff = Date.now() - retention;
4155
+ if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
4156
+ this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
4157
+ }
4158
+ }
4159
+ };
4160
+ }
4161
+ updateTiming(duration) {
4162
+ const alpha = 0.1;
4163
+ if (this.metrics.averageTotalTime_ms === 0) {
4164
+ this.metrics.averageTotalTime_ms = duration;
4165
+ } else {
4166
+ this.metrics.averageTotalTime_ms = alpha * duration + (1 - alpha) * this.metrics.averageTotalTime_ms;
4167
+ }
4168
+ this.metrics.recentTimings.push(duration);
4169
+ if (this.metrics.recentTimings.length > 50) {
4170
+ this.metrics.recentTimings.shift();
4171
+ }
4172
+ }
4173
+ }
4174
+ function unknownError(ctx) {
4175
+ return ctx.json({ error: "Unknown Error" }, 500);
4176
+ }
3336
4177
  const eta = new eta$2.Eta();
3337
4178
  class ScalarPlugin extends ShokupanRouter {
3338
4179
  constructor(pluginOptions = {}) {
@@ -3431,23 +4272,23 @@ function Compression(options = {}) {
3431
4272
  return next();
3432
4273
  }
3433
4274
  let response = await next();
3434
- if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
3435
- response = ctx._finalResponse;
4275
+ if (!(response instanceof Response) && ctx[$finalResponse] instanceof Response) {
4276
+ response = ctx[$finalResponse];
3436
4277
  }
3437
4278
  if (response instanceof Response) {
3438
4279
  if (response.headers.has("Content-Encoding")) return response;
3439
4280
  let body;
3440
4281
  let bodySize;
3441
- if (ctx._rawBody !== void 0) {
3442
- if (typeof ctx._rawBody === "string") {
3443
- const encoded = new TextEncoder().encode(ctx._rawBody);
4282
+ if (ctx[$rawBody] !== void 0) {
4283
+ if (typeof ctx[$rawBody] === "string") {
4284
+ const encoded = new TextEncoder().encode(ctx[$rawBody]);
3444
4285
  body = encoded;
3445
4286
  bodySize = encoded.byteLength;
3446
- } else if (ctx._rawBody instanceof Uint8Array) {
3447
- body = ctx._rawBody;
3448
- bodySize = ctx._rawBody.byteLength;
4287
+ } else if (ctx[$rawBody] instanceof Uint8Array) {
4288
+ body = ctx[$rawBody];
4289
+ bodySize = ctx[$rawBody].byteLength;
3449
4290
  } else {
3450
- body = ctx._rawBody;
4291
+ body = ctx[$rawBody];
3451
4292
  bodySize = body.byteLength;
3452
4293
  }
3453
4294
  } else {
@@ -4312,20 +5153,39 @@ function Session(options) {
4312
5153
  return sessionMiddleware;
4313
5154
  }
4314
5155
  exports.$appRoot = $appRoot;
5156
+ exports.$bodyParseError = $bodyParseError;
5157
+ exports.$bodyParsed = $bodyParsed;
5158
+ exports.$bodyType = $bodyType;
5159
+ exports.$cachedBody = $cachedBody;
5160
+ exports.$cachedHost = $cachedHost;
5161
+ exports.$cachedHostname = $cachedHostname;
5162
+ exports.$cachedOrigin = $cachedOrigin;
5163
+ exports.$cachedProtocol = $cachedProtocol;
5164
+ exports.$cachedQuery = $cachedQuery;
4315
5165
  exports.$childControllers = $childControllers;
4316
5166
  exports.$childRouters = $childRouters;
4317
5167
  exports.$controllerPath = $controllerPath;
5168
+ exports.$debug = $debug;
4318
5169
  exports.$dispatch = $dispatch;
5170
+ exports.$eventMethods = $eventMethods;
5171
+ exports.$finalResponse = $finalResponse;
5172
+ exports.$io = $io;
4319
5173
  exports.$isApplication = $isApplication;
4320
5174
  exports.$isMounted = $isMounted;
4321
5175
  exports.$isRouter = $isRouter;
4322
5176
  exports.$middleware = $middleware;
4323
5177
  exports.$mountPath = $mountPath;
4324
5178
  exports.$parent = $parent;
5179
+ exports.$rawBody = $rawBody;
5180
+ exports.$requestId = $requestId;
4325
5181
  exports.$routeArgs = $routeArgs;
5182
+ exports.$routeMatched = $routeMatched;
4326
5183
  exports.$routeMethods = $routeMethods;
4327
5184
  exports.$routeSpec = $routeSpec;
4328
5185
  exports.$routes = $routes;
5186
+ exports.$socket = $socket;
5187
+ exports.$url = $url;
5188
+ exports.$ws = $ws;
4329
5189
  exports.All = All;
4330
5190
  exports.AuthPlugin = AuthPlugin;
4331
5191
  exports.Body = Body;
@@ -4335,7 +5195,9 @@ exports.Container = Container;
4335
5195
  exports.Controller = Controller;
4336
5196
  exports.Cors = Cors;
4337
5197
  exports.Ctx = Ctx;
5198
+ exports.Dashboard = Dashboard;
4338
5199
  exports.Delete = Delete;
5200
+ exports.Event = Event;
4339
5201
  exports.Get = Get;
4340
5202
  exports.HTTPMethods = HTTPMethods;
4341
5203
  exports.Head = Head;