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.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { nanoid } from "nanoid";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
3
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { Surreal, RecordId } from "surrealdb";
4
6
  import { Eta } from "eta";
5
7
  import { stat, readdir, readFile as readFile$1 } from "fs/promises";
6
8
  import { resolve, join, sep, basename } from "path";
@@ -10,12 +12,45 @@ import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, gen
10
12
  import * as jose from "jose";
11
13
  import cluster from "node:cluster";
12
14
  import net from "node:net";
15
+ import { dirname } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { monitorEventLoopDelay } from "node:perf_hooks";
13
18
  import { OpenAPIAnalyzer } from "./analyzer-Ce_7JxZh.js";
14
19
  import * as zlib from "node:zlib";
15
20
  import Ajv from "ajv";
16
21
  import addFormats from "ajv-formats";
17
22
  import { randomUUID, createHmac } from "crypto";
18
23
  import { EventEmitter } from "events";
24
+ const HTTP_STATUS = {
25
+ // 2xx Success
26
+ OK: 200,
27
+ CREATED: 201,
28
+ ACCEPTED: 202,
29
+ NO_CONTENT: 204,
30
+ // 3xx Redirection
31
+ MOVED_PERMANENTLY: 301,
32
+ FOUND: 302,
33
+ SEE_OTHER: 303,
34
+ NOT_MODIFIED: 304,
35
+ TEMPORARY_REDIRECT: 307,
36
+ PERMANENT_REDIRECT: 308,
37
+ // 4xx Client Errors
38
+ BAD_REQUEST: 400,
39
+ UNAUTHORIZED: 401,
40
+ FORBIDDEN: 403,
41
+ NOT_FOUND: 404,
42
+ METHOD_NOT_ALLOWED: 405,
43
+ REQUEST_TIMEOUT: 408,
44
+ CONFLICT: 409,
45
+ UNPROCESSABLE_ENTITY: 422,
46
+ TOO_MANY_REQUESTS: 429,
47
+ // 5xx Server Errors
48
+ INTERNAL_SERVER_ERROR: 500,
49
+ NOT_IMPLEMENTED: 501,
50
+ BAD_GATEWAY: 502,
51
+ SERVICE_UNAVAILABLE: 503,
52
+ GATEWAY_TIMEOUT: 504
53
+ };
19
54
  const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
20
55
  100,
21
56
  101,
@@ -145,6 +180,40 @@ class ShokupanResponse {
145
180
  return this._headers !== null;
146
181
  }
147
182
  }
183
+ const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
184
+ const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
185
+ const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
186
+ const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
187
+ const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
188
+ const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
189
+ const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
190
+ const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
191
+ const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
192
+ const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
193
+ const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
194
+ const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
195
+ const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
196
+ const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
197
+ const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
198
+ const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
199
+ const $url = /* @__PURE__ */ Symbol.for("Shokupan.ctx.url");
200
+ const $requestId = /* @__PURE__ */ Symbol.for("Shokupan.ctx.requestId");
201
+ const $debug = /* @__PURE__ */ Symbol.for("Shokupan.ctx.debug");
202
+ const $finalResponse = /* @__PURE__ */ Symbol.for("Shokupan.ctx.finalResponse");
203
+ const $rawBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.rawBody");
204
+ const $cachedBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedBody");
205
+ const $bodyType = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyType");
206
+ const $bodyParsed = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParsed");
207
+ const $bodyParseError = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParseError");
208
+ const $routeMatched = /* @__PURE__ */ Symbol.for("Shokupan.ctx.routeMatched");
209
+ const $cachedHostname = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHostname");
210
+ const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol");
211
+ const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
212
+ const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
213
+ const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
214
+ const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
215
+ const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
216
+ const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
148
217
  function isValidCookieDomain(domain, currentHost) {
149
218
  const hostWithoutPort = currentHost.split(":")[0];
150
219
  if (domain === hostWithoutPort) return true;
@@ -182,23 +251,26 @@ class ShokupanContext {
182
251
  state;
183
252
  handlerStack = [];
184
253
  response;
185
- _debug;
186
- _finalResponse;
187
- _rawBody;
254
+ [$debug];
255
+ [$finalResponse];
256
+ [$rawBody];
188
257
  // Raw body for compression optimization
189
258
  // Body caching to avoid double parsing
190
- _url;
191
- _cachedBody;
192
- _bodyType;
193
- _bodyParsed = false;
194
- _bodyParseError;
195
- _routeMatched = false;
259
+ [$url];
260
+ [$cachedBody];
261
+ [$bodyType];
262
+ [$bodyParsed] = false;
263
+ [$bodyParseError];
264
+ [$routeMatched] = false;
196
265
  // Cached URL properties to avoid repeated parsing
197
- _cachedHostname;
198
- _cachedProtocol;
199
- _cachedHost;
200
- _cachedOrigin;
201
- _cachedQuery;
266
+ [$cachedHostname];
267
+ [$cachedProtocol];
268
+ [$cachedHost];
269
+ [$cachedOrigin];
270
+ [$cachedQuery];
271
+ [$ws];
272
+ [$socket];
273
+ [$io];
202
274
  /**
203
275
  * JSX Rendering Function
204
276
  */
@@ -206,12 +278,16 @@ class ShokupanContext {
206
278
  setRenderer(renderer) {
207
279
  this.renderer = renderer;
208
280
  }
281
+ [$requestId];
282
+ get requestId() {
283
+ return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid();
284
+ }
209
285
  get url() {
210
- if (!this._url) {
286
+ if (!this[$url]) {
211
287
  const urlString = this.request.url || "http://localhost/";
212
- this._url = new URL(urlString);
288
+ this[$url] = new URL(urlString);
213
289
  }
214
- return this._url;
290
+ return this[$url];
215
291
  }
216
292
  /**
217
293
  * Base request
@@ -229,7 +305,7 @@ class ShokupanContext {
229
305
  * Request path
230
306
  */
231
307
  get path() {
232
- if (this._url) return this._url.pathname;
308
+ if (this[$url]) return this[$url].pathname;
233
309
  const url = this.request.url;
234
310
  let queryIndex = url.indexOf("?");
235
311
  const end = queryIndex === -1 ? url.length : queryIndex;
@@ -254,7 +330,7 @@ class ShokupanContext {
254
330
  * Request query params
255
331
  */
256
332
  get query() {
257
- if (this._cachedQuery) return this._cachedQuery;
333
+ if (this[$cachedQuery]) return this[$cachedQuery];
258
334
  const q = /* @__PURE__ */ Object.create(null);
259
335
  const blocklist = ["__proto__", "constructor", "prototype"];
260
336
  const entries = Object.entries(this.url.searchParams);
@@ -271,7 +347,7 @@ class ShokupanContext {
271
347
  q[key] = value;
272
348
  }
273
349
  }
274
- this._cachedQuery = q;
350
+ this[$cachedQuery] = q;
275
351
  return q;
276
352
  }
277
353
  /**
@@ -284,19 +360,19 @@ class ShokupanContext {
284
360
  * Request hostname (e.g. "localhost")
285
361
  */
286
362
  get hostname() {
287
- return this._cachedHostname ??= this.url.hostname;
363
+ return this[$cachedHostname] ??= this.url.hostname;
288
364
  }
289
365
  /**
290
366
  * Request host (e.g. "localhost:3000")
291
367
  */
292
368
  get host() {
293
- return this._cachedHost ??= this.url.host;
369
+ return this[$cachedHost] ??= this.url.host;
294
370
  }
295
371
  /**
296
372
  * Request protocol (e.g. "http:", "https:")
297
373
  */
298
374
  get protocol() {
299
- return this._cachedProtocol ??= this.url.protocol;
375
+ return this[$cachedProtocol] ??= this.url.protocol;
300
376
  }
301
377
  /**
302
378
  * Whether request is secure (https)
@@ -308,7 +384,7 @@ class ShokupanContext {
308
384
  * Request origin (e.g. "http://localhost:3000")
309
385
  */
310
386
  get origin() {
311
- return this._cachedOrigin ??= this.url.origin;
387
+ return this[$cachedOrigin] ??= this.url.origin;
312
388
  }
313
389
  /**
314
390
  * Request headers
@@ -329,6 +405,24 @@ class ShokupanContext {
329
405
  get res() {
330
406
  return this.response;
331
407
  }
408
+ /**
409
+ * Raw WebSocket connection
410
+ */
411
+ get ws() {
412
+ return this[$ws];
413
+ }
414
+ /**
415
+ * Socket.io socket
416
+ */
417
+ get socket() {
418
+ return this[$socket];
419
+ }
420
+ /**
421
+ * Socket.io server
422
+ */
423
+ get io() {
424
+ return this[$io];
425
+ }
332
426
  /**
333
427
  * Helper to set a header on the response
334
428
  * @param key Header key
@@ -338,6 +432,20 @@ class ShokupanContext {
338
432
  this.response.set(key, value);
339
433
  return this;
340
434
  }
435
+ isUpgraded = false;
436
+ /**
437
+ * Upgrades the request to a WebSocket connection.
438
+ * @param options Upgrade options
439
+ * @returns true if upgraded, false otherwise
440
+ */
441
+ upgrade(options) {
442
+ if (!this.server) return false;
443
+ const success = this.server.upgrade(this.req, options);
444
+ if (success) {
445
+ this.isUpgraded = true;
446
+ }
447
+ return success;
448
+ }
341
449
  /**
342
450
  * Set a cookie
343
451
  * @param name Cookie name
@@ -415,18 +523,18 @@ class ShokupanContext {
415
523
  * The body is only parsed once and cached for subsequent reads.
416
524
  */
417
525
  async body() {
418
- if (this._bodyParseError) {
419
- throw this._bodyParseError;
526
+ if (this[$bodyParseError]) {
527
+ throw this[$bodyParseError];
420
528
  }
421
- if (this._bodyParsed) {
422
- return this._cachedBody;
529
+ if (this[$bodyParsed]) {
530
+ return this[$cachedBody];
423
531
  }
424
532
  const contentType = this.request.headers.get("content-type") || "";
425
533
  if (contentType.includes("application/json") || contentType.includes("+json")) {
426
534
  const parserType = this.app?.applicationConfig?.jsonParser || "native";
427
535
  if (parserType === "native") {
428
536
  try {
429
- this._cachedBody = await this.request.json();
537
+ this[$cachedBody] = await this.request.json();
430
538
  } catch (e) {
431
539
  throw e;
432
540
  }
@@ -434,18 +542,18 @@ class ShokupanContext {
434
542
  const rawText = await this.request.text();
435
543
  const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
436
544
  const parser = getJSONParser(parserType);
437
- this._cachedBody = parser(rawText);
545
+ this[$cachedBody] = parser(rawText);
438
546
  }
439
- this._bodyType = "json";
547
+ this[$bodyType] = "json";
440
548
  } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
441
- this._cachedBody = await this.request.formData();
442
- this._bodyType = "formData";
549
+ this[$cachedBody] = await this.request.formData();
550
+ this[$bodyType] = "formData";
443
551
  } else {
444
- this._cachedBody = await this.request.text();
445
- this._bodyType = "text";
552
+ this[$cachedBody] = await this.request.text();
553
+ this[$bodyType] = "text";
446
554
  }
447
- this._bodyParsed = true;
448
- return this._cachedBody;
555
+ this[$bodyParsed] = true;
556
+ return this[$cachedBody];
449
557
  }
450
558
  /**
451
559
  * Pre-parse the request body before handler execution.
@@ -453,7 +561,7 @@ class ShokupanContext {
453
561
  * Errors are deferred until the body is actually accessed in the handler.
454
562
  */
455
563
  async parseBody() {
456
- if (this._bodyParsed) {
564
+ if (this[$bodyParsed]) {
457
565
  return;
458
566
  }
459
567
  if (this.request.method === "GET" || this.request.method === "HEAD") {
@@ -462,7 +570,7 @@ class ShokupanContext {
462
570
  try {
463
571
  await this.body();
464
572
  } catch (error) {
465
- this._bodyParseError = error;
573
+ this[$bodyParseError] = error;
466
574
  }
467
575
  }
468
576
  /**
@@ -512,10 +620,21 @@ class ShokupanContext {
512
620
  throw new Error(`Invalid HTTP status code: ${status}`);
513
621
  }
514
622
  if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
515
- this._rawBody = body;
623
+ this[$rawBody] = body;
624
+ }
625
+ return this[$finalResponse] ??= new Response(body, { status, headers });
626
+ }
627
+ /**
628
+ * Emit an event to the client (WebSocket only)
629
+ * @param event Event name
630
+ * @param data Event data (Must be JSON serializable)
631
+ */
632
+ emit(event, data) {
633
+ if (this[$ws]) {
634
+ this[$ws].send(JSON.stringify({ event, data }));
635
+ } else if (this[$socket]) {
636
+ this[$socket].emit(event, data);
516
637
  }
517
- this._finalResponse = new Response(body, { status, headers });
518
- return this._finalResponse;
519
638
  }
520
639
  /**
521
640
  * Respond with a JSON object
@@ -526,18 +645,18 @@ class ShokupanContext {
526
645
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
527
646
  }
528
647
  const jsonString = JSON.stringify(data);
529
- this._rawBody = jsonString;
648
+ this[$rawBody] = jsonString;
530
649
  if (!headers && !this.response.hasPopulatedHeaders) {
531
- this._finalResponse = new Response(jsonString, {
650
+ this[$finalResponse] = new Response(jsonString, {
532
651
  status: finalStatus,
533
652
  headers: { "content-type": "application/json" }
534
653
  });
535
- return this._finalResponse;
654
+ return this[$finalResponse];
536
655
  }
537
656
  const finalHeaders = this.mergeHeaders(headers);
538
657
  finalHeaders.set("content-type", "application/json");
539
- this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
540
- return this._finalResponse;
658
+ this[$finalResponse] = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
659
+ return this[$finalResponse];
541
660
  }
542
661
  /**
543
662
  * Respond with a text string
@@ -547,18 +666,18 @@ class ShokupanContext {
547
666
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
548
667
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
549
668
  }
550
- this._rawBody = data;
669
+ this[$rawBody] = data;
551
670
  if (!headers && !this.response.hasPopulatedHeaders) {
552
- this._finalResponse = new Response(data, {
671
+ this[$finalResponse] = new Response(data, {
553
672
  status: finalStatus,
554
673
  headers: { "content-type": "text/plain; charset=utf-8" }
555
674
  });
556
- return this._finalResponse;
675
+ return this[$finalResponse];
557
676
  }
558
677
  const finalHeaders = this.mergeHeaders(headers);
559
678
  finalHeaders.set("content-type", "text/plain; charset=utf-8");
560
- this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
561
- return this._finalResponse;
679
+ this[$finalResponse] = new Response(data, { status: finalStatus, headers: finalHeaders });
680
+ return this[$finalResponse];
562
681
  }
563
682
  /**
564
683
  * Respond with HTML content
@@ -570,9 +689,9 @@ class ShokupanContext {
570
689
  }
571
690
  const finalHeaders = this.mergeHeaders(headers);
572
691
  finalHeaders.set("content-type", "text/html; charset=utf-8");
573
- this._rawBody = html;
574
- this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
575
- return this._finalResponse;
692
+ this[$rawBody] = html;
693
+ this[$finalResponse] = new Response(html, { status: finalStatus, headers: finalHeaders });
694
+ return this[$finalResponse];
576
695
  }
577
696
  /**
578
697
  * Respond with a redirect
@@ -583,8 +702,8 @@ class ShokupanContext {
583
702
  }
584
703
  const headers = this.mergeHeaders();
585
704
  headers.set("Location", url);
586
- this._finalResponse = new Response(null, { status, headers });
587
- return this._finalResponse;
705
+ this[$finalResponse] = new Response(null, { status, headers });
706
+ return this[$finalResponse];
588
707
  }
589
708
  /**
590
709
  * Respond with a status code
@@ -595,8 +714,8 @@ class ShokupanContext {
595
714
  throw new Error(`Invalid HTTP status code: ${status}`);
596
715
  }
597
716
  const headers = this.mergeHeaders();
598
- this._finalResponse = new Response(null, { status, headers });
599
- return this._finalResponse;
717
+ this[$finalResponse] = new Response(null, { status, headers });
718
+ return this[$finalResponse];
600
719
  }
601
720
  /**
602
721
  * Respond with a file
@@ -608,15 +727,15 @@ class ShokupanContext {
608
727
  throw new Error(`Invalid HTTP status code: ${status}`);
609
728
  }
610
729
  if (typeof Bun !== "undefined") {
611
- this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
612
- return this._finalResponse;
730
+ this[$finalResponse] = new Response(Bun.file(path, fileOptions), { status, headers });
731
+ return this[$finalResponse];
613
732
  } else {
614
733
  const fileBuffer = await readFile(path);
615
734
  if (fileOptions?.type) {
616
735
  headers.set("content-type", fileOptions.type);
617
736
  }
618
- this._finalResponse = new Response(fileBuffer, { status, headers });
619
- return this._finalResponse;
737
+ this[$finalResponse] = new Response(fileBuffer, { status, headers });
738
+ return this[$finalResponse];
620
739
  }
621
740
  }
622
741
  /**
@@ -652,10 +771,10 @@ const compose = (middleware) => {
652
771
  return next ? next() : Promise.resolve();
653
772
  }
654
773
  const fn = middleware[i];
655
- if (!context2._debug) {
774
+ if (!context2[$debug]) {
656
775
  return fn(context2, () => runner(i + 1));
657
776
  }
658
- const debug = context2._debug;
777
+ const debug = context2[$debug];
659
778
  const debugId = fn._debugId || fn.name || "anonymous";
660
779
  const previousNode = debug.getCurrentNode();
661
780
  debug.trackEdge(previousNode, debugId);
@@ -731,21 +850,6 @@ function deepMerge(target, ...sources) {
731
850
  }
732
851
  return deepMerge(target, ...sources);
733
852
  }
734
- const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
735
- const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
736
- const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
737
- const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
738
- const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
739
- const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
740
- const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
741
- const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
742
- const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
743
- const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
744
- const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
745
- const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
746
- const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
747
- const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
748
- const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
749
853
  const REGEX_PATTERNS = {
750
854
  QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
751
855
  QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
@@ -1132,6 +1236,35 @@ class RequestContextStore {
1132
1236
  span;
1133
1237
  }
1134
1238
  const asyncContext = new AsyncLocalStorage();
1239
+ class HttpError extends Error {
1240
+ status;
1241
+ constructor(message, status) {
1242
+ super(message);
1243
+ this.name = "HttpError";
1244
+ this.status = status;
1245
+ if (Error.captureStackTrace) {
1246
+ Error.captureStackTrace(this, HttpError);
1247
+ }
1248
+ }
1249
+ }
1250
+ function getErrorStatus(err) {
1251
+ if (!err || typeof err !== "object") {
1252
+ return 500;
1253
+ }
1254
+ if (typeof err.status === "number") {
1255
+ return err.status;
1256
+ }
1257
+ if (typeof err.statusCode === "number") {
1258
+ return err.statusCode;
1259
+ }
1260
+ return 500;
1261
+ }
1262
+ class EventError extends HttpError {
1263
+ constructor(message = "Event Error") {
1264
+ super(message, 500);
1265
+ this.name = "EventError";
1266
+ }
1267
+ }
1135
1268
  const eta$1 = new Eta();
1136
1269
  function serveStatic(config, prefix) {
1137
1270
  const rootPath = resolve(config.root || ".");
@@ -1284,18 +1417,16 @@ function serveStatic(config, prefix) {
1284
1417
  serveStaticMiddleware.pluginName = "ServeStatic";
1285
1418
  return serveStaticMiddleware;
1286
1419
  }
1287
- let db;
1288
- let dbPromise = null;
1289
- let RecordId;
1420
+ const G = globalThis;
1421
+ G.__shokupan_db = G.__shokupan_db || null;
1422
+ G.__shokupan_db_promise = G.__shokupan_db_promise || null;
1290
1423
  async function ensureDb() {
1291
- if (db) return db;
1292
- if (dbPromise) return dbPromise;
1293
- dbPromise = (async () => {
1424
+ if (G.__shokupan_db) return G.__shokupan_db;
1425
+ if (G.__shokupan_db_promise) return G.__shokupan_db_promise;
1426
+ G.__shokupan_db_promise = (async () => {
1294
1427
  try {
1295
1428
  const { createNodeEngines } = await import("@surrealdb/node");
1296
1429
  const surreal = await import("surrealdb");
1297
- const Surreal = surreal.Surreal;
1298
- RecordId = surreal.RecordId;
1299
1430
  const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1300
1431
  const _db = new Surreal({
1301
1432
  engines: createNodeEngines()
@@ -1308,33 +1439,33 @@ async function ensureDb() {
1308
1439
  DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1309
1440
  DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1310
1441
  DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1442
+ DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
1311
1443
  `);
1312
- db = _db;
1313
- return db;
1444
+ G.__shokupan_db = _db;
1445
+ return _db;
1314
1446
  } catch (e) {
1315
- dbPromise = null;
1447
+ G.__shokupan_db_promise = null;
1316
1448
  if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1317
1449
  throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1318
1450
  }
1319
1451
  throw e;
1320
1452
  }
1321
1453
  })();
1322
- return dbPromise;
1454
+ return G.__shokupan_db_promise;
1323
1455
  }
1324
1456
  const datastore = {
1325
- async get(store, key) {
1457
+ async get(recordId) {
1326
1458
  await ensureDb();
1327
- return db.select(new RecordId(store, key));
1459
+ return G.__shokupan_db.select(recordId);
1328
1460
  },
1329
- async set(store, key, value) {
1461
+ async set(recordId, value) {
1330
1462
  await ensureDb();
1331
- return db.create(new RecordId(store, key)).content(value);
1463
+ return G.__shokupan_db.upsert(recordId).content(value);
1332
1464
  },
1333
1465
  async query(query, vars) {
1334
1466
  await ensureDb();
1335
1467
  try {
1336
- const r = await db.query(query, vars);
1337
- return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
1468
+ return G.__shokupan_db.query(query, vars).collect();
1338
1469
  } catch (e) {
1339
1470
  console.error("DS ERROR:", e);
1340
1471
  throw e;
@@ -1345,7 +1476,7 @@ const datastore = {
1345
1476
  }
1346
1477
  };
1347
1478
  process.on("exit", async () => {
1348
- if (db) await db.close();
1479
+ if (G.__shokupan_db) await G.__shokupan_db.close();
1349
1480
  });
1350
1481
  class Container {
1351
1482
  static services = /* @__PURE__ */ new Map();
@@ -1576,6 +1707,7 @@ class ShokupanRouter {
1576
1707
  metadata;
1577
1708
  // Metadata for the router itself
1578
1709
  currentGuards = [];
1710
+ eventHandlers = /* @__PURE__ */ new Map();
1579
1711
  // Registry Accessor
1580
1712
  getComponentRegistry() {
1581
1713
  const controllerRoutesMap = /* @__PURE__ */ new Map();
@@ -1636,6 +1768,34 @@ class ShokupanRouter {
1636
1768
  isRouterInstance(target) {
1637
1769
  return typeof target === "object" && target !== null && $isRouter in target;
1638
1770
  }
1771
+ /**
1772
+ * Registers an event handler for WebSocket.
1773
+ */
1774
+ event(name, handler) {
1775
+ if (this.eventHandlers.has(name)) {
1776
+ const err = new EventError(`Event handler \`${name}\` already exists.`);
1777
+ console.warn(err);
1778
+ const handlers = this.eventHandlers.get(name);
1779
+ handlers.push(handler);
1780
+ this.eventHandlers.set(name, handlers);
1781
+ } else {
1782
+ this.eventHandlers.set(name, [handler]);
1783
+ }
1784
+ return this;
1785
+ }
1786
+ /**
1787
+ * Finds an event handler(s) by name.
1788
+ */
1789
+ findEvent(name) {
1790
+ if (this.eventHandlers.has(name)) {
1791
+ return this.eventHandlers.get(name);
1792
+ }
1793
+ for (const child of this[$childRouters]) {
1794
+ const handler = child.findEvent(name);
1795
+ if (handler) return handler;
1796
+ }
1797
+ return null;
1798
+ }
1639
1799
  /**
1640
1800
  * Mounts a controller instance to a path prefix.
1641
1801
  *
@@ -1736,7 +1896,7 @@ class ShokupanRouter {
1736
1896
  });
1737
1897
  const ctx = new ShokupanContext(req);
1738
1898
  let result = null;
1739
- let status = 200;
1899
+ let status = HTTP_STATUS.OK;
1740
1900
  const headers = {};
1741
1901
  const match = this.find(req.method, ctx.path);
1742
1902
  if (match) {
@@ -1745,12 +1905,12 @@ class ShokupanRouter {
1745
1905
  result = await match.handler(ctx);
1746
1906
  } catch (err) {
1747
1907
  console.error(err);
1748
- status = err.status || err.statusCode || 500;
1908
+ status = getErrorStatus(err);
1749
1909
  result = { error: err.message || "Internal Server Error" };
1750
1910
  if (err.errors) result.errors = err.errors;
1751
1911
  }
1752
1912
  } else {
1753
- status = 404;
1913
+ status = HTTP_STATUS.NOT_FOUND;
1754
1914
  result = "Not Found";
1755
1915
  }
1756
1916
  if (result instanceof Response) {
@@ -1779,7 +1939,7 @@ class ShokupanRouter {
1779
1939
  const originalHandler = handler;
1780
1940
  const wrapped = async (ctx) => {
1781
1941
  await this.runHooks("onRequestStart", ctx);
1782
- const debug = ctx._debug;
1942
+ const debug = ctx[$debug];
1783
1943
  let debugId;
1784
1944
  let previousNode;
1785
1945
  if (debug) {
@@ -1869,6 +2029,7 @@ class ShokupanRouter {
1869
2029
  const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1870
2030
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1871
2031
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
2032
+ const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
1872
2033
  let routesAttached = 0;
1873
2034
  for (let i = 0; i < Array.from(methods).length; i++) {
1874
2035
  const name = Array.from(methods)[i];
@@ -2013,6 +2174,39 @@ class ShokupanRouter {
2013
2174
  const spec = { tags: [tagName], ...userSpec };
2014
2175
  this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2015
2176
  }
2177
+ if (decoratedEvents?.has(name)) {
2178
+ routesAttached++;
2179
+ const config = decoratedEvents.get(name);
2180
+ const routeArgs = decoratedArgs?.get(name);
2181
+ const wrappedHandler = async (ctx) => {
2182
+ let args = [ctx];
2183
+ if (routeArgs?.length > 0) {
2184
+ args = [];
2185
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2186
+ for (let k = 0; k < sortedArgs.length; k++) {
2187
+ const arg = sortedArgs[k];
2188
+ switch (arg.type) {
2189
+ case RouteParamType.BODY:
2190
+ args[arg.index] = await ctx.body();
2191
+ break;
2192
+ case RouteParamType.CONTEXT:
2193
+ args[arg.index] = ctx;
2194
+ break;
2195
+ case RouteParamType.REQUEST:
2196
+ args[arg.index] = ctx.req;
2197
+ break;
2198
+ case RouteParamType.HEADER:
2199
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2200
+ break;
2201
+ default:
2202
+ args[arg.index] = void 0;
2203
+ }
2204
+ }
2205
+ }
2206
+ return originalHandler.apply(instance, args);
2207
+ };
2208
+ this.event(config.eventName, wrappedHandler);
2209
+ }
2016
2210
  }
2017
2211
  if (routesAttached === 0) {
2018
2212
  console.warn(`No routes attached to controller ${instance.constructor.name}`);
@@ -2176,8 +2370,10 @@ class ShokupanRouter {
2176
2370
  Promise.resolve().then(async () => {
2177
2371
  try {
2178
2372
  const timestamp = Date.now();
2179
- const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2180
- await datastore.set("middleware_tracking", key, {
2373
+ await datastore.set(new RecordId("middleware_tracking", {
2374
+ timestamp,
2375
+ name: handler.name || "anonymous"
2376
+ }), {
2181
2377
  name: handler.name || "anonymous",
2182
2378
  path: ctx.path,
2183
2379
  timestamp,
@@ -2195,7 +2391,7 @@ class ShokupanRouter {
2195
2391
  const cutoff = Date.now() - ttl;
2196
2392
  await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2197
2393
  const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2198
- if (results && results[0] && results[0].count > maxCapacity) {
2394
+ if (results?.[0]?.count > maxCapacity) {
2199
2395
  const toDelete = results[0].count - maxCapacity;
2200
2396
  await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2201
2397
  }
@@ -2272,7 +2468,7 @@ class ShokupanRouter {
2272
2468
  (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
2273
2469
  );
2274
2470
  if (callerLine) {
2275
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
2471
+ const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
2276
2472
  if (match) {
2277
2473
  file = match[1];
2278
2474
  line = parseInt(match[2], 10);
@@ -2290,7 +2486,7 @@ class ShokupanRouter {
2290
2486
  }
2291
2487
  return guardHandler(ctx, next);
2292
2488
  };
2293
- trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
2489
+ trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
2294
2490
  this.currentGuards.push({ handler: trackedGuard, spec });
2295
2491
  return this;
2296
2492
  }
@@ -2403,7 +2599,7 @@ class ShokupanRouter {
2403
2599
  const fns = this.hookCache.get(name);
2404
2600
  if (!fns) return;
2405
2601
  const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2406
- const debug = ctx?._debug;
2602
+ const debug = ctx?.[$debug];
2407
2603
  if (debug) {
2408
2604
  await Promise.all(fns.map(async (fn, index) => {
2409
2605
  const hookId = `hook_${name}_${fn.name || index}`;
@@ -2476,6 +2672,7 @@ const defaults = {
2476
2672
  hostname: "localhost",
2477
2673
  development: process.env.NODE_ENV !== "production",
2478
2674
  enableAsyncLocalStorage: false,
2675
+ enableHttpBridge: false,
2479
2676
  reusePort: false
2480
2677
  };
2481
2678
  trace.getTracer("shokupan.application");
@@ -2484,6 +2681,7 @@ class Shokupan extends ShokupanRouter {
2484
2681
  openApiSpec;
2485
2682
  composedMiddleware;
2486
2683
  cpuMonitor;
2684
+ server;
2487
2685
  get logger() {
2488
2686
  return this.applicationConfig.logger;
2489
2687
  }
@@ -2592,6 +2790,7 @@ class Shokupan extends ShokupanRouter {
2592
2790
  this.cpuMonitor = new SystemCpuMonitor();
2593
2791
  this.cpuMonitor.start();
2594
2792
  }
2793
+ const self = this;
2595
2794
  const serveOptions = {
2596
2795
  port: finalPort,
2597
2796
  hostname: this.applicationConfig.hostname,
@@ -2603,8 +2802,61 @@ class Shokupan extends ShokupanRouter {
2603
2802
  open(ws) {
2604
2803
  ws.data?.handler?.open?.(ws);
2605
2804
  },
2606
- message(ws, message) {
2607
- ws.data?.handler?.message?.(ws, message);
2805
+ async message(ws, message) {
2806
+ if (ws.data?.handler?.message) {
2807
+ return ws.data.handler.message(ws, message);
2808
+ }
2809
+ if (typeof message !== "string") return;
2810
+ try {
2811
+ const payload = JSON.parse(message);
2812
+ if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
2813
+ const { id, method, path, headers, body } = payload;
2814
+ const url = new URL(path, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
2815
+ const req = new Request(url.toString(), {
2816
+ method,
2817
+ headers,
2818
+ body: typeof body === "object" ? JSON.stringify(body) : body
2819
+ });
2820
+ const res = await self.fetch(req);
2821
+ const resBody = await res.json().catch((err) => res.text());
2822
+ const resHeaders = {};
2823
+ res.headers.forEach((v, k) => resHeaders[k] = v);
2824
+ ws.send(JSON.stringify({
2825
+ type: "RESPONSE",
2826
+ id,
2827
+ status: res.status,
2828
+ headers: resHeaders,
2829
+ body: resBody
2830
+ }));
2831
+ return;
2832
+ }
2833
+ const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
2834
+ if (eventName) {
2835
+ const handlers = self.findEvent(eventName);
2836
+ const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
2837
+ if (handler) {
2838
+ const data = payload.data || payload.payload;
2839
+ const req = new ShokupanRequest({
2840
+ url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
2841
+ method: "POST",
2842
+ headers: new Headers({ "content-type": "application/json" }),
2843
+ body: JSON.stringify(data)
2844
+ });
2845
+ const ctx = new ShokupanContext(req, self.server);
2846
+ ctx[$ws] = ws;
2847
+ try {
2848
+ await handler(ctx);
2849
+ } catch (err) {
2850
+ if (self.applicationConfig["websocketErrorHandler"]) {
2851
+ await self.applicationConfig["websocketErrorHandler"](err, ctx);
2852
+ } else {
2853
+ console.error(`Error in event ${eventName}:`, err);
2854
+ }
2855
+ }
2856
+ }
2857
+ }
2858
+ } catch (e) {
2859
+ }
2608
2860
  },
2609
2861
  drain(ws) {
2610
2862
  ws.data?.handler?.drain?.(ws);
@@ -2616,12 +2868,40 @@ class Shokupan extends ShokupanRouter {
2616
2868
  };
2617
2869
  let factory = this.applicationConfig.serverFactory;
2618
2870
  if (!factory && typeof Bun === "undefined") {
2619
- const { createHttpServer } = await import("./http-server-0xH174zz.js");
2871
+ const { createHttpServer } = await import("./http-server-CCeagTyU.js");
2620
2872
  factory = createHttpServer();
2621
2873
  }
2622
- const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2874
+ this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2623
2875
  console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
2624
- return server;
2876
+ return this.server;
2877
+ }
2878
+ /**
2879
+ * Stops the application server.
2880
+ *
2881
+ * This method gracefully shuts down the server and stops any running monitors.
2882
+ * Works transparently in both Bun and Node.js runtimes.
2883
+ *
2884
+ * @returns A promise that resolves when the server has been stopped.
2885
+ *
2886
+ * @example
2887
+ * ```typescript
2888
+ * const app = new Shokupan();
2889
+ * const server = await app.listen(3000);
2890
+ *
2891
+ * // Later, when you want to stop the server
2892
+ * await app.stop();
2893
+ * ```
2894
+ * @param closeActiveConnections — Immediately terminate in-flight requests, websockets, and stop accepting new connections.
2895
+ */
2896
+ async stop(closeActiveConnections) {
2897
+ if (this.cpuMonitor) {
2898
+ this.cpuMonitor.stop();
2899
+ this.cpuMonitor = void 0;
2900
+ }
2901
+ if (this.server) {
2902
+ await this.server.stop(closeActiveConnections);
2903
+ this.server = void 0;
2904
+ }
2625
2905
  }
2626
2906
  [$dispatch](req) {
2627
2907
  return this.fetch(req);
@@ -2719,28 +2999,33 @@ class Shokupan extends ShokupanRouter {
2719
2999
  const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2720
3000
  const match = this.find(req.method, ctx.path);
2721
3001
  if (match) {
2722
- ctx._routeMatched = true;
3002
+ ctx[$routeMatched] = true;
2723
3003
  ctx.params = match.params;
2724
3004
  await bodyParsing;
2725
3005
  return match.handler(ctx);
2726
3006
  }
3007
+ if (ctx.upgrade()) {
3008
+ return void 0;
3009
+ }
2727
3010
  return null;
2728
3011
  });
2729
3012
  let response;
2730
3013
  if (result instanceof Response) {
2731
3014
  response = result;
2732
- } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2733
- response = ctx._finalResponse;
3015
+ } else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
3016
+ response = ctx[$finalResponse];
2734
3017
  } else if (result === null || result === void 0) {
2735
- if (ctx._finalResponse instanceof Response) {
2736
- response = ctx._finalResponse;
2737
- } else if (ctx._routeMatched) {
3018
+ if (ctx[$finalResponse] instanceof Response) {
3019
+ response = ctx[$finalResponse];
3020
+ } else if (ctx.isUpgraded) {
3021
+ return void 0;
3022
+ } else if (ctx[$routeMatched]) {
2738
3023
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2739
3024
  } else {
2740
- if (ctx.response.status !== 200) {
3025
+ if (ctx.response.status !== HTTP_STATUS.OK) {
2741
3026
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2742
3027
  } else {
2743
- response = ctx.text("Not Found", 404);
3028
+ response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
2744
3029
  }
2745
3030
  }
2746
3031
  } else if (typeof result === "object") {
@@ -2752,10 +3037,9 @@ class Shokupan extends ShokupanRouter {
2752
3037
  await this.runHooks("onResponseStart", ctx, response);
2753
3038
  return response;
2754
3039
  } catch (err) {
2755
- console.error(err);
2756
3040
  const span = asyncContext.getStore()?.span;
2757
3041
  if (span) span.setStatus({ code: 2 });
2758
- const status = err.status || err.statusCode || 500;
3042
+ const status = getErrorStatus(err);
2759
3043
  const body = { error: err.message || "Internal Server Error" };
2760
3044
  if (err.errors) body.errors = err.errors;
2761
3045
  await this.runHooks("onError", ctx, err);
@@ -2777,10 +3061,10 @@ class Shokupan extends ShokupanRouter {
2777
3061
  }
2778
3062
  return executionPromise.catch((err) => {
2779
3063
  if (err.message === "Request Timeout") {
2780
- return ctx.text("Request Timeout", 408);
3064
+ return ctx.text("Request Timeout", HTTP_STATUS.REQUEST_TIMEOUT);
2781
3065
  }
2782
3066
  console.error("Unexpected error in request execution:", err);
2783
- return ctx.text("Internal Server Error", 500);
3067
+ return ctx.text("Internal Server Error", HTTP_STATUS.INTERNAL_SERVER_ERROR);
2784
3068
  }).then(async (res) => {
2785
3069
  await this.runHooks("onResponseEnd", ctx, res);
2786
3070
  return res;
@@ -2948,6 +3232,14 @@ const Patch = createMethodDecorator("PATCH");
2948
3232
  const Options = createMethodDecorator("OPTIONS");
2949
3233
  const Head = createMethodDecorator("HEAD");
2950
3234
  const All = createMethodDecorator("ALL");
3235
+ function Event(eventName) {
3236
+ return (target, propertyKey, descriptor) => {
3237
+ target[$eventMethods] ??= /* @__PURE__ */ new Map();
3238
+ target[$eventMethods].set(propertyKey, {
3239
+ eventName
3240
+ });
3241
+ };
3242
+ }
2951
3243
  function RateLimit(options) {
2952
3244
  return Use(RateLimitMiddleware(options));
2953
3245
  }
@@ -3291,6 +3583,554 @@ class ClusterPlugin {
3291
3583
  }
3292
3584
  }
3293
3585
  }
3586
+ const INTERVALS = [
3587
+ { label: "10s", ms: 10 * 1e3 },
3588
+ { label: "1m", ms: 60 * 1e3 },
3589
+ { label: "5m", ms: 5 * 60 * 1e3 },
3590
+ { label: "1h", ms: 60 * 60 * 1e3 },
3591
+ { label: "2h", ms: 2 * 60 * 60 * 1e3 },
3592
+ { label: "6h", ms: 6 * 60 * 60 * 1e3 },
3593
+ { label: "12h", ms: 12 * 60 * 60 * 1e3 },
3594
+ { label: "1d", ms: 24 * 60 * 60 * 1e3 },
3595
+ { label: "3d", ms: 3 * 24 * 60 * 60 * 1e3 },
3596
+ { label: "7d", ms: 7 * 24 * 60 * 60 * 1e3 },
3597
+ { label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
3598
+ ];
3599
+ class MetricsCollector {
3600
+ currentIntervalStart = {};
3601
+ pendingDetails = {};
3602
+ eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
3603
+ timer = null;
3604
+ constructor() {
3605
+ this.eventLoopHistogram.enable();
3606
+ const now = Date.now();
3607
+ INTERVALS.forEach((int) => {
3608
+ this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3609
+ this.pendingDetails[int.label] = [];
3610
+ });
3611
+ this.timer = setInterval(() => this.collect(), 1e4);
3612
+ }
3613
+ recordRequest(duration, isError) {
3614
+ INTERVALS.forEach((int) => {
3615
+ this.pendingDetails[int.label].push({ duration, isError });
3616
+ });
3617
+ }
3618
+ alignTimestamp(ts, intervalMs) {
3619
+ return Math.floor(ts / intervalMs) * intervalMs;
3620
+ }
3621
+ async collect() {
3622
+ try {
3623
+ const now = Date.now();
3624
+ console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
3625
+ for (const int of INTERVALS) {
3626
+ const start = this.currentIntervalStart[int.label];
3627
+ if (now >= start + int.ms) {
3628
+ console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
3629
+ await this.flushInterval(int.label, start, int.ms);
3630
+ this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
3631
+ }
3632
+ }
3633
+ } catch (error) {
3634
+ console.error("[MetricsCollector] Error in collect():", error);
3635
+ }
3636
+ }
3637
+ async flushInterval(label, timestamp, durationMs) {
3638
+ const reqs = this.pendingDetails[label];
3639
+ console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
3640
+ this.pendingDetails[label] = [];
3641
+ if (reqs.length === 0) {
3642
+ console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
3643
+ return;
3644
+ }
3645
+ const totalReqs = reqs.length;
3646
+ const errorReqs = reqs.filter((r) => r.isError).length;
3647
+ const successReqs = totalReqs - errorReqs;
3648
+ const duratons = reqs.map((r) => r.duration).sort((a, b) => a - b);
3649
+ const rps = totalReqs / (durationMs / 1e3);
3650
+ const sum = duratons.reduce((a, b) => a + b, 0);
3651
+ const avg = totalReqs > 0 ? sum / totalReqs : 0;
3652
+ const getP = (p) => {
3653
+ if (duratons.length === 0) return 0;
3654
+ const idx = Math.floor(duratons.length * p);
3655
+ return duratons[idx];
3656
+ };
3657
+ const metric = {
3658
+ timestamp,
3659
+ interval: label,
3660
+ cpu: os.loadavg()[0],
3661
+ // Using load avg for simplicity as per requirements (Load)
3662
+ load: os.loadavg(),
3663
+ memory: {
3664
+ used: process.memoryUsage().rss,
3665
+ total: os.totalmem(),
3666
+ heapUsed: process.memoryUsage().heapUsed,
3667
+ heapTotal: process.memoryUsage().heapTotal
3668
+ },
3669
+ eventLoopLatency: {
3670
+ min: this.eventLoopHistogram.min / 1e6,
3671
+ max: this.eventLoopHistogram.max / 1e6,
3672
+ mean: this.eventLoopHistogram.mean / 1e6,
3673
+ p50: this.eventLoopHistogram.percentile(50) / 1e6,
3674
+ p95: this.eventLoopHistogram.percentile(95) / 1e6,
3675
+ p99: this.eventLoopHistogram.percentile(99) / 1e6
3676
+ },
3677
+ requests: {
3678
+ total: totalReqs,
3679
+ rps,
3680
+ success: successReqs,
3681
+ error: errorReqs
3682
+ },
3683
+ responseTime: {
3684
+ min: duratons[0] || 0,
3685
+ max: duratons[duratons.length - 1] || 0,
3686
+ avg,
3687
+ p50: getP(0.5),
3688
+ p95: getP(0.95),
3689
+ p99: getP(0.99)
3690
+ }
3691
+ };
3692
+ console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
3693
+ try {
3694
+ const recordId = new RecordId("metrics", timestamp);
3695
+ await datastore.set(recordId, metric);
3696
+ console.log(`[MetricsCollector] ✓ Successfully saved ${label} metric to datastore`);
3697
+ const test = await datastore.get(recordId);
3698
+ console.log(`[MetricsCollector] DEBUG: Immediate .get() returned:`, test ? "DATA" : "NULL");
3699
+ const queryTest = await datastore.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
3700
+ console.log(`[MetricsCollector] DEBUG: Query by id returned ${queryTest[0]?.length || 0} records`);
3701
+ } catch (e) {
3702
+ console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
3703
+ }
3704
+ }
3705
+ // Cleanup if needed
3706
+ stop() {
3707
+ if (this.timer) clearInterval(this.timer);
3708
+ this.eventLoopHistogram.disable();
3709
+ }
3710
+ }
3711
+ class Collector {
3712
+ constructor(dashboard) {
3713
+ this.dashboard = dashboard;
3714
+ }
3715
+ currentNode;
3716
+ trackStep(id, type, duration, status, error) {
3717
+ if (!id) return;
3718
+ this.dashboard.recordNodeMetric(id, type, duration, status === "error");
3719
+ }
3720
+ trackEdge(fromId, toId) {
3721
+ if (!fromId || !toId) return;
3722
+ this.dashboard.recordEdgeMetric(fromId, toId);
3723
+ }
3724
+ setNode(id) {
3725
+ this.currentNode = id;
3726
+ }
3727
+ getCurrentNode() {
3728
+ return this.currentNode;
3729
+ }
3730
+ }
3731
+ class Dashboard {
3732
+ constructor(dashboardConfig = {}) {
3733
+ this.dashboardConfig = dashboardConfig;
3734
+ }
3735
+ static __dirname = dirname(fileURLToPath(import.meta.url));
3736
+ // Get base path for dashboard files - works in both dev (src/) and production (dist/)
3737
+ static getBasePath() {
3738
+ const dir = dirname(fileURLToPath(import.meta.url));
3739
+ if (dir.endsWith("dist")) {
3740
+ return dir + "/plugins/application/dashboard";
3741
+ }
3742
+ return dir;
3743
+ }
3744
+ router = new ShokupanRouter();
3745
+ metrics = {
3746
+ totalRequests: 0,
3747
+ successfulRequests: 0,
3748
+ failedRequests: 0,
3749
+ activeRequests: 0,
3750
+ averageTotalTime_ms: 0,
3751
+ recentTimings: [],
3752
+ logs: [],
3753
+ rateLimitedCounts: {},
3754
+ nodeMetrics: {},
3755
+ edgeMetrics: {}
3756
+ };
3757
+ eta = new Eta({
3758
+ views: Dashboard.getBasePath() + "/static",
3759
+ cache: false
3760
+ });
3761
+ startTime = Date.now();
3762
+ instrumented = false;
3763
+ metricsCollector = new MetricsCollector();
3764
+ // ShokupanPlugin interface implementation
3765
+ onInit(app, options) {
3766
+ this[$appRoot] = app;
3767
+ const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
3768
+ const hooks = this.getHooks();
3769
+ if (!app.middleware) {
3770
+ app.middleware = [];
3771
+ }
3772
+ const hooksMiddleware = async (ctx, next) => {
3773
+ if (hooks.onRequestStart) {
3774
+ await hooks.onRequestStart(ctx);
3775
+ }
3776
+ await next();
3777
+ if (hooks.onResponseEnd) {
3778
+ const effectiveResponse = ctx._finalResponse || ctx.response || {};
3779
+ await hooks.onResponseEnd(ctx, effectiveResponse);
3780
+ }
3781
+ };
3782
+ app.use(hooksMiddleware);
3783
+ app.mount(mountPath, this.router);
3784
+ this.setupRoutes();
3785
+ }
3786
+ setupRoutes() {
3787
+ this.router.get("/metrics", async (ctx) => {
3788
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
3789
+ const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
3790
+ const interval = ctx.query["interval"];
3791
+ if (interval) {
3792
+ const intervalMap = {
3793
+ "10s": 10 * 1e3,
3794
+ "1m": 60 * 1e3,
3795
+ "5m": 5 * 60 * 1e3,
3796
+ "30m": 30 * 60 * 1e3,
3797
+ "1h": 60 * 60 * 1e3,
3798
+ "2h": 2 * 60 * 60 * 1e3,
3799
+ "6h": 6 * 60 * 60 * 1e3,
3800
+ "12h": 12 * 60 * 60 * 1e3,
3801
+ "1d": 24 * 60 * 60 * 1e3,
3802
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3803
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3804
+ "30d": 30 * 24 * 60 * 60 * 1e3
3805
+ };
3806
+ const ms = intervalMap[interval] || 60 * 1e3;
3807
+ const startTime = Date.now() - ms;
3808
+ let stats;
3809
+ try {
3810
+ stats = await datastore.query(`
3811
+ SELECT
3812
+ count() as total,
3813
+ count(IF status < 400 THEN 1 END) as success,
3814
+ count(IF status >= 400 THEN 1 END) as failed,
3815
+ math::mean(duration) as avg_latency
3816
+ FROM requests
3817
+ WHERE timestamp >= $start
3818
+ GROUP ALL
3819
+ `, { start: startTime });
3820
+ } catch (error) {
3821
+ console.error("[Dashboard] Query failed at plugin.ts:180-191", {
3822
+ error,
3823
+ interval,
3824
+ startTime,
3825
+ query: "metrics interval stats",
3826
+ stack: new Error().stack
3827
+ });
3828
+ throw error;
3829
+ }
3830
+ const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
3831
+ return ctx.json({
3832
+ metrics: {
3833
+ totalRequests: s.total || 0,
3834
+ successfulRequests: s.success || 0,
3835
+ failedRequests: s.failed || 0,
3836
+ activeRequests: this.metrics.activeRequests,
3837
+ averageTotalTime_ms: s.avg_latency || 0,
3838
+ recentTimings: this.metrics.recentTimings,
3839
+ logs: [],
3840
+ rateLimitedCounts: this.metrics.rateLimitedCounts,
3841
+ nodeMetrics: this.metrics.nodeMetrics,
3842
+ edgeMetrics: this.metrics.edgeMetrics
3843
+ },
3844
+ uptime
3845
+ });
3846
+ }
3847
+ return ctx.json({
3848
+ metrics: this.metrics,
3849
+ uptime
3850
+ });
3851
+ });
3852
+ this.router.get("/metrics/history", async (ctx) => {
3853
+ const interval = ctx.query["interval"] || "1m";
3854
+ const intervalMap = {
3855
+ "10s": 10 * 1e3,
3856
+ "1m": 60 * 1e3,
3857
+ "5m": 5 * 60 * 1e3,
3858
+ "30m": 30 * 60 * 1e3,
3859
+ "1h": 60 * 60 * 1e3,
3860
+ "2h": 2 * 60 * 60 * 1e3,
3861
+ "6h": 6 * 60 * 60 * 1e3,
3862
+ "12h": 12 * 60 * 60 * 1e3,
3863
+ "1d": 24 * 60 * 60 * 1e3,
3864
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3865
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3866
+ "30d": 30 * 24 * 60 * 60 * 1e3
3867
+ };
3868
+ const periodMs = intervalMap[interval] || 60 * 1e3;
3869
+ const startTime = Date.now() - periodMs * 3;
3870
+ const endTime = Date.now();
3871
+ const result = await datastore.query(
3872
+ "SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
3873
+ { start: startTime, end: endTime, interval }
3874
+ );
3875
+ return ctx.json({
3876
+ metrics: result[0] || []
3877
+ });
3878
+ });
3879
+ const getIntervalStartTime = (interval) => {
3880
+ if (!interval) return 0;
3881
+ const intervalMap = {
3882
+ "10s": 10 * 1e3,
3883
+ "1m": 60 * 1e3,
3884
+ "5m": 5 * 60 * 1e3,
3885
+ "30m": 30 * 60 * 1e3,
3886
+ "1h": 60 * 60 * 1e3,
3887
+ "2h": 2 * 60 * 60 * 1e3,
3888
+ "6h": 6 * 60 * 60 * 1e3,
3889
+ "12h": 12 * 60 * 60 * 1e3,
3890
+ "1d": 24 * 60 * 60 * 1e3,
3891
+ "3d": 3 * 24 * 60 * 60 * 1e3,
3892
+ "7d": 7 * 24 * 60 * 60 * 1e3,
3893
+ "30d": 30 * 24 * 60 * 60 * 1e3
3894
+ };
3895
+ const ms = intervalMap[interval] || 0;
3896
+ return ms ? Date.now() - ms : 0;
3897
+ };
3898
+ this.router.get("/requests/top", async (ctx) => {
3899
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3900
+ const result = await datastore.query(
3901
+ "SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3902
+ { start: startTime }
3903
+ );
3904
+ return ctx.json({ top: result[0] || [] });
3905
+ });
3906
+ this.router.get("/errors/top", async (ctx) => {
3907
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3908
+ const result = await datastore.query(
3909
+ "SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
3910
+ { start: startTime }
3911
+ );
3912
+ return ctx.json({ top: result[0] || [] });
3913
+ });
3914
+ this.router.get("/requests/failing", async (ctx) => {
3915
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3916
+ const result = await datastore.query(
3917
+ "SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
3918
+ { start: startTime }
3919
+ );
3920
+ return ctx.json({ top: result[0] || [] });
3921
+ });
3922
+ this.router.get("/requests/slowest", async (ctx) => {
3923
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
3924
+ const result = await datastore.query(
3925
+ "SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
3926
+ { start: startTime }
3927
+ );
3928
+ return ctx.json({ slowest: result[0] || [] });
3929
+ });
3930
+ this.router.get("/registry", (ctx) => {
3931
+ const app = this[$appRoot];
3932
+ if (!this.instrumented && app) {
3933
+ this.instrumentApp(app);
3934
+ }
3935
+ const registry = app?.getComponentRegistry?.();
3936
+ if (registry) {
3937
+ this.assignIdsToRegistry(registry, "root");
3938
+ }
3939
+ return ctx.json({ registry: registry || {} });
3940
+ });
3941
+ this.router.get("/requests", async (ctx) => {
3942
+ const result = await datastore.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
3943
+ return ctx.json({ requests: result[0] || [] });
3944
+ });
3945
+ this.router.get("/requests/:id", async (ctx) => {
3946
+ const result = await datastore.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
3947
+ return ctx.json({ request: result[0]?.[0] });
3948
+ });
3949
+ this.router.get("/failures", async (ctx) => {
3950
+ const result = await datastore.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
3951
+ return ctx.json({ failures: result[0] });
3952
+ });
3953
+ this.router.post("/replay", async (ctx) => {
3954
+ const body = await ctx.body();
3955
+ const app = this[$appRoot];
3956
+ if (!app) return unknownError(ctx);
3957
+ try {
3958
+ const result = await app.processRequest({
3959
+ method: body.method,
3960
+ path: body.url,
3961
+ // or path
3962
+ headers: body.headers,
3963
+ body: body.body
3964
+ });
3965
+ return ctx.json({
3966
+ status: result.status,
3967
+ headers: result.headers,
3968
+ data: result.data
3969
+ });
3970
+ } catch (e) {
3971
+ return ctx.json({ error: String(e) }, 500);
3972
+ }
3973
+ });
3974
+ this.router.get("/", async (ctx) => {
3975
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
3976
+ const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
3977
+ const linkPattern = this.getLinkPattern();
3978
+ const template = await readFile(Dashboard.getBasePath() + "/template.eta", "utf8");
3979
+ return ctx.html(this.eta.renderString(template, {
3980
+ metrics: this.metrics,
3981
+ uptime,
3982
+ rootPath: process.cwd(),
3983
+ linkPattern,
3984
+ headers: this.dashboardConfig.getRequestHeaders?.()
3985
+ }));
3986
+ });
3987
+ }
3988
+ instrumentApp(app) {
3989
+ if (!app.getComponentRegistry) return;
3990
+ const registry = app.getComponentRegistry();
3991
+ this.assignIdsToRegistry(registry, "root");
3992
+ this.instrumented = true;
3993
+ }
3994
+ // Traverses registry, generates IDs, and attaches them to the actual function objects
3995
+ assignIdsToRegistry(node, parentId) {
3996
+ if (!node) return;
3997
+ const makeId = (type, parent, idx, name) => `${type}_${parent}_${idx}_${name.replace(/[^a-zA-Z0-9]/g, "")}`;
3998
+ node.middleware?.forEach((mw, idx) => {
3999
+ const id = makeId("mw", parentId, idx, mw.name);
4000
+ mw.id = id;
4001
+ if (mw._fn) mw._fn._debugId = id;
4002
+ });
4003
+ node.controllers?.forEach((ctrl, idx) => {
4004
+ const id = makeId("ctrl", parentId, idx, ctrl.name);
4005
+ ctrl.id = id;
4006
+ });
4007
+ node.routes?.forEach((r, idx) => {
4008
+ const id = makeId("route", parentId, idx, r.handlerName || "handler");
4009
+ r.id = id;
4010
+ if (r._fn) r._fn._debugId = id;
4011
+ });
4012
+ node.routers?.forEach((r, idx) => {
4013
+ const id = makeId("router", parentId, idx, r.path);
4014
+ r.id = id;
4015
+ this.assignIdsToRegistry(r.children, id);
4016
+ });
4017
+ }
4018
+ recordNodeMetric(id, type, duration, isError) {
4019
+ if (!this.metrics.nodeMetrics[id]) {
4020
+ this.metrics.nodeMetrics[id] = {
4021
+ id,
4022
+ type,
4023
+ requests: 0,
4024
+ totalTime: 0,
4025
+ failures: 0,
4026
+ name: id
4027
+ // simplify
4028
+ };
4029
+ }
4030
+ const m = this.metrics.nodeMetrics[id];
4031
+ m.requests++;
4032
+ m.totalTime += duration;
4033
+ if (isError) m.failures++;
4034
+ }
4035
+ recordEdgeMetric(from, to) {
4036
+ const key = `${from}|${to}`;
4037
+ this.metrics.edgeMetrics[key] = (this.metrics.edgeMetrics[key] || 0) + 1;
4038
+ }
4039
+ getLinkPattern() {
4040
+ const term = process.env["TERM_PROGRAM"] || "";
4041
+ if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
4042
+ return "vscode://file/{{absolute}}:{{line}}";
4043
+ }
4044
+ return "file:///{{absolute}}:{{line}}";
4045
+ }
4046
+ getHooks() {
4047
+ return {
4048
+ onRequestStart: (ctx) => {
4049
+ const app = this[$appRoot];
4050
+ if (!this.instrumented && app) {
4051
+ this.instrumentApp(app);
4052
+ }
4053
+ this.metrics.totalRequests++;
4054
+ this.metrics.activeRequests++;
4055
+ ctx._debugStartTime = performance.now();
4056
+ ctx[$debug] = new Collector(this);
4057
+ },
4058
+ onResponseEnd: async (ctx, response) => {
4059
+ this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
4060
+ const start = ctx._debugStartTime;
4061
+ let duration = 0;
4062
+ if (start) {
4063
+ duration = performance.now() - start;
4064
+ this.updateTiming(duration);
4065
+ }
4066
+ const isError = response.status >= 400;
4067
+ this.metricsCollector.recordRequest(duration, isError);
4068
+ if (response.status >= 400) {
4069
+ this.metrics.failedRequests++;
4070
+ if (response.status === 429) {
4071
+ const path = ctx.path;
4072
+ this.metrics.rateLimitedCounts[path] = (this.metrics.rateLimitedCounts[path] || 0) + 1;
4073
+ }
4074
+ try {
4075
+ const headers = {};
4076
+ if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
4077
+ ctx.request.headers.forEach((v, k) => {
4078
+ headers[k] = v;
4079
+ });
4080
+ }
4081
+ await datastore.set(new RecordId("failed_requests", ctx.requestId), {
4082
+ method: ctx.method,
4083
+ url: ctx.url.toString(),
4084
+ headers,
4085
+ status: response.status,
4086
+ timestamp: Date.now(),
4087
+ state: ctx.state
4088
+ // body?
4089
+ });
4090
+ } catch (e) {
4091
+ console.error("Failed to record failed request", e);
4092
+ }
4093
+ } else {
4094
+ this.metrics.successfulRequests++;
4095
+ }
4096
+ const logEntry = {
4097
+ method: ctx.method,
4098
+ url: ctx.url.toString(),
4099
+ status: response.status,
4100
+ duration,
4101
+ timestamp: Date.now(),
4102
+ handlerStack: ctx.handlerStack
4103
+ };
4104
+ this.metrics.logs.push(logEntry);
4105
+ try {
4106
+ await datastore.set(new RecordId("requests", ctx.requestId), logEntry);
4107
+ } catch (e) {
4108
+ console.error("Failed to record request log", e);
4109
+ }
4110
+ const retention = this.dashboardConfig.retentionMs ?? 72e5;
4111
+ const cutoff = Date.now() - retention;
4112
+ if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
4113
+ this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
4114
+ }
4115
+ }
4116
+ };
4117
+ }
4118
+ updateTiming(duration) {
4119
+ const alpha = 0.1;
4120
+ if (this.metrics.averageTotalTime_ms === 0) {
4121
+ this.metrics.averageTotalTime_ms = duration;
4122
+ } else {
4123
+ this.metrics.averageTotalTime_ms = alpha * duration + (1 - alpha) * this.metrics.averageTotalTime_ms;
4124
+ }
4125
+ this.metrics.recentTimings.push(duration);
4126
+ if (this.metrics.recentTimings.length > 50) {
4127
+ this.metrics.recentTimings.shift();
4128
+ }
4129
+ }
4130
+ }
4131
+ function unknownError(ctx) {
4132
+ return ctx.json({ error: "Unknown Error" }, 500);
4133
+ }
3294
4134
  const eta = new Eta();
3295
4135
  class ScalarPlugin extends ShokupanRouter {
3296
4136
  constructor(pluginOptions = {}) {
@@ -3389,23 +4229,23 @@ function Compression(options = {}) {
3389
4229
  return next();
3390
4230
  }
3391
4231
  let response = await next();
3392
- if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
3393
- response = ctx._finalResponse;
4232
+ if (!(response instanceof Response) && ctx[$finalResponse] instanceof Response) {
4233
+ response = ctx[$finalResponse];
3394
4234
  }
3395
4235
  if (response instanceof Response) {
3396
4236
  if (response.headers.has("Content-Encoding")) return response;
3397
4237
  let body;
3398
4238
  let bodySize;
3399
- if (ctx._rawBody !== void 0) {
3400
- if (typeof ctx._rawBody === "string") {
3401
- const encoded = new TextEncoder().encode(ctx._rawBody);
4239
+ if (ctx[$rawBody] !== void 0) {
4240
+ if (typeof ctx[$rawBody] === "string") {
4241
+ const encoded = new TextEncoder().encode(ctx[$rawBody]);
3402
4242
  body = encoded;
3403
4243
  bodySize = encoded.byteLength;
3404
- } else if (ctx._rawBody instanceof Uint8Array) {
3405
- body = ctx._rawBody;
3406
- bodySize = ctx._rawBody.byteLength;
4244
+ } else if (ctx[$rawBody] instanceof Uint8Array) {
4245
+ body = ctx[$rawBody];
4246
+ bodySize = ctx[$rawBody].byteLength;
3407
4247
  } else {
3408
- body = ctx._rawBody;
4248
+ body = ctx[$rawBody];
3409
4249
  bodySize = body.byteLength;
3410
4250
  }
3411
4251
  } else {
@@ -4271,20 +5111,39 @@ function Session(options) {
4271
5111
  }
4272
5112
  export {
4273
5113
  $appRoot,
5114
+ $bodyParseError,
5115
+ $bodyParsed,
5116
+ $bodyType,
5117
+ $cachedBody,
5118
+ $cachedHost,
5119
+ $cachedHostname,
5120
+ $cachedOrigin,
5121
+ $cachedProtocol,
5122
+ $cachedQuery,
4274
5123
  $childControllers,
4275
5124
  $childRouters,
4276
5125
  $controllerPath,
5126
+ $debug,
4277
5127
  $dispatch,
5128
+ $eventMethods,
5129
+ $finalResponse,
5130
+ $io,
4278
5131
  $isApplication,
4279
5132
  $isMounted,
4280
5133
  $isRouter,
4281
5134
  $middleware,
4282
5135
  $mountPath,
4283
5136
  $parent,
5137
+ $rawBody,
5138
+ $requestId,
4284
5139
  $routeArgs,
5140
+ $routeMatched,
4285
5141
  $routeMethods,
4286
5142
  $routeSpec,
4287
5143
  $routes,
5144
+ $socket,
5145
+ $url,
5146
+ $ws,
4288
5147
  All,
4289
5148
  AuthPlugin,
4290
5149
  Body,
@@ -4294,7 +5153,9 @@ export {
4294
5153
  Controller,
4295
5154
  Cors,
4296
5155
  Ctx,
5156
+ Dashboard,
4297
5157
  Delete,
5158
+ Event,
4298
5159
  Get,
4299
5160
  HTTPMethods,
4300
5161
  Head,