shokupan 0.1.0 → 0.3.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 (61) hide show
  1. package/README.md +1 -0
  2. package/dist/benchmarking/advanced-cases/elysia.d.ts +1 -0
  3. package/dist/benchmarking/advanced-cases/express.d.ts +1 -0
  4. package/dist/benchmarking/advanced-cases/fastify.d.ts +1 -0
  5. package/dist/benchmarking/advanced-cases/hapi.d.ts +1 -0
  6. package/dist/benchmarking/advanced-cases/hono.d.ts +1 -0
  7. package/dist/benchmarking/advanced-cases/koa.d.ts +1 -0
  8. package/dist/benchmarking/advanced-cases/nest.d.ts +1 -0
  9. package/dist/benchmarking/advanced-cases/shokupan.d.ts +1 -0
  10. package/dist/benchmarking/advanced-data.d.ts +33 -0
  11. package/dist/benchmarking/advanced-runner.d.ts +1 -0
  12. package/dist/benchmarking/advanced-worker.d.ts +0 -0
  13. package/dist/benchmarking/cases/elysia.d.ts +1 -0
  14. package/dist/benchmarking/cases/express.d.ts +1 -0
  15. package/dist/benchmarking/cases/fastify.d.ts +1 -0
  16. package/dist/benchmarking/cases/hapi.d.ts +1 -0
  17. package/dist/benchmarking/cases/hono.d.ts +1 -0
  18. package/dist/benchmarking/cases/koa.d.ts +1 -0
  19. package/dist/benchmarking/cases/nest.d.ts +1 -0
  20. package/dist/benchmarking/cases/shokupan.d.ts +1 -0
  21. package/dist/benchmarking/data.d.ts +15 -0
  22. package/dist/benchmarking/quick_bench.d.ts +1 -0
  23. package/dist/benchmarking/runner.d.ts +1 -0
  24. package/dist/benchmarking/worker.d.ts +0 -0
  25. package/dist/buntest.d.ts +1 -0
  26. package/dist/cli.cjs +1 -1
  27. package/dist/cli.js +1 -1
  28. package/dist/context.d.ts +25 -8
  29. package/dist/decorators.d.ts +47 -0
  30. package/dist/index.cjs +1538 -655
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1532 -651
  34. package/dist/index.js.map +1 -1
  35. package/dist/middleware.d.ts +2 -0
  36. package/dist/{openapi-analyzer-cjdGeQ5a.js → openapi-analyzer-BtIaHIfe.js} +14 -6
  37. package/dist/openapi-analyzer-BtIaHIfe.js.map +1 -0
  38. package/dist/{openapi-analyzer-CFqgSLNK.cjs → openapi-analyzer-D9YB3IkV.cjs} +14 -6
  39. package/dist/openapi-analyzer-D9YB3IkV.cjs.map +1 -0
  40. package/dist/plugins/auth.d.ts +1 -1
  41. package/dist/plugins/debugview/plugin.d.ts +28 -0
  42. package/dist/plugins/failed-request-recorder.d.ts +14 -0
  43. package/dist/plugins/idempotency/plugin.d.ts +14 -0
  44. package/dist/plugins/openapi-validator.d.ts +30 -0
  45. package/dist/plugins/proxy.d.ts +9 -0
  46. package/dist/plugins/rate-limit.d.ts +3 -1
  47. package/dist/plugins/serve-static.d.ts +2 -3
  48. package/dist/response.d.ts +4 -0
  49. package/dist/router/trie.d.ts +14 -0
  50. package/dist/router.d.ts +50 -3
  51. package/dist/server-adapter-BWrEJbKL.js +64 -0
  52. package/dist/server-adapter-BWrEJbKL.js.map +1 -0
  53. package/dist/server-adapter-fVKP60e0.cjs +81 -0
  54. package/dist/server-adapter-fVKP60e0.cjs.map +1 -0
  55. package/dist/shokupan.d.ts +16 -3
  56. package/dist/types.d.ts +108 -4
  57. package/dist/util/cpu-monitor.d.ts +11 -0
  58. package/dist/util/stack.d.ts +8 -0
  59. package/package.json +8 -3
  60. package/dist/openapi-analyzer-CFqgSLNK.cjs.map +0 -1
  61. package/dist/openapi-analyzer-cjdGeQ5a.js.map +0 -1
package/dist/router.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ShokupanContext } from './context';
2
2
  import { Shokupan } from './shokupan';
3
3
  import { $appRoot, $childControllers, $childRouters, $isApplication, $isMounted, $isRouter, $mountPath, $parent, $routes } from './symbol';
4
- import { GuardAPISpec, MethodAPISpec, OpenAPIOptions, ProcessResult, RequestOptions, ShokupanRouteConfig, StaticServeOptions, JSXRenderer, Method, ShokupanController, ShokupanHandler, ShokupanRoute } from './types';
4
+ import { GuardAPISpec, JSXRenderer, Method, MethodAPISpec, Middleware, OpenAPIOptions, ProcessResult, RequestOptions, RouteMetadata, ShokupanController, ShokupanHandler, ShokupanRoute, ShokupanRouteConfig, StaticServeOptions } from './types';
5
5
  type HeadersInit = Headers | Record<string, string> | [string, string][];
6
6
  export declare const RouterRegistry: Map<string, ShokupanRouter<any>>;
7
7
  export declare const ShokupanApplicationTree: {};
@@ -15,6 +15,7 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
15
15
  private [$parent];
16
16
  [$childRouters]: ShokupanRouter<T>[];
17
17
  [$childControllers]: ShokupanController[];
18
+ middleware: Middleware[];
18
19
  get rootConfig(): {
19
20
  [x: string]: any;
20
21
  port?: number;
@@ -24,6 +25,12 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
24
25
  enableOpenApiGen?: boolean;
25
26
  reusePort?: boolean;
26
27
  controllersOnly?: boolean;
28
+ enableTracing?: boolean;
29
+ autoBackpressureFeedback?: boolean;
30
+ autoBackpressureLevel?: number;
31
+ enableMiddlewareTracking?: boolean;
32
+ middlewareTrackingMaxCapacity?: number;
33
+ middlewareTrackingTTL?: number;
27
34
  httpLogger?: (ctx: ShokupanContext<Record<string, any>>) => void;
28
35
  logger?: {
29
36
  verbose?: boolean;
@@ -49,11 +56,50 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
49
56
  onReadTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
50
57
  onWriteTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
51
58
  onRequestTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
52
- };
59
+ } | {
60
+ onError?: (error: unknown, ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
61
+ onRequestStart?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
62
+ onRequestEnd?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
63
+ onResponseStart?: (ctx: ShokupanContext<Record<string, any>>, response: Response) => void | Promise<void>;
64
+ onResponseEnd?: (ctx: ShokupanContext<Record<string, any>>, response: Response) => void | Promise<void>;
65
+ beforeValidate?: (ctx: ShokupanContext<Record<string, any>>, data: any) => void | Promise<void>;
66
+ afterValidate?: (ctx: ShokupanContext<Record<string, any>>, data: any) => void | Promise<void>;
67
+ onReadTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
68
+ onWriteTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
69
+ onRequestTimeout?: (ctx: ShokupanContext<Record<string, any>>) => void | Promise<void>;
70
+ }[];
53
71
  };
54
72
  get root(): Shokupan<any>;
55
73
  [$routes]: ShokupanRoute[];
74
+ private trie;
75
+ metadata?: RouteMetadata;
56
76
  private currentGuards;
77
+ getComponentRegistry(): {
78
+ metadata: RouteMetadata;
79
+ middleware: {
80
+ name: string;
81
+ metadata: RouteMetadata;
82
+ order: number;
83
+ _fn: Middleware;
84
+ }[];
85
+ routes: {
86
+ type: string;
87
+ path: string;
88
+ method: Method;
89
+ metadata: RouteMetadata;
90
+ handlerName: string;
91
+ tags: any;
92
+ order: number;
93
+ _fn: ShokupanHandler<Record<string, any>>;
94
+ }[];
95
+ routers: any;
96
+ controllers: {
97
+ type: string;
98
+ path: any;
99
+ name: string;
100
+ metadata: any;
101
+ }[];
102
+ };
57
103
  constructor(config?: ShokupanRouteConfig);
58
104
  private isRouterInstance;
59
105
  /**
@@ -91,7 +137,8 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
91
137
  * Processes a request directly.
92
138
  */
93
139
  processRequest(options: RequestOptions): Promise<ProcessResult>;
94
- private applyHooks;
140
+ private applyRouterHooks;
141
+ private wrapWithHooks;
95
142
  /**
96
143
  * Find a route matching the given method and path.
97
144
  * @param method HTTP method
@@ -0,0 +1,64 @@
1
+ import * as http from "node:http";
2
+ import "node:https";
3
+ function createHttpServer() {
4
+ return async (options) => {
5
+ const server = http.createServer(async (req, res) => {
6
+ const url = new URL(req.url, `http://${req.headers.host}`);
7
+ const request = new Request(url.toString(), {
8
+ method: req.method,
9
+ headers: req.headers,
10
+ body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
11
+ start(controller) {
12
+ req.on("data", (chunk) => controller.enqueue(chunk));
13
+ req.on("end", () => controller.close());
14
+ req.on("error", (err) => controller.error(err));
15
+ }
16
+ })
17
+ });
18
+ const response = await options.fetch(request, fauxServer);
19
+ res.statusCode = response.status;
20
+ response.headers.forEach((v, k) => res.setHeader(k, v));
21
+ if (response.body) {
22
+ const buffer = await response.arrayBuffer();
23
+ res.end(Buffer.from(buffer));
24
+ } else {
25
+ res.end();
26
+ }
27
+ });
28
+ const fauxServer = {
29
+ stop: () => {
30
+ server.close();
31
+ return Promise.resolve();
32
+ },
33
+ upgrade(req, options2) {
34
+ return false;
35
+ },
36
+ reload(options2) {
37
+ return fauxServer;
38
+ },
39
+ get port() {
40
+ const addr = server.address();
41
+ if (typeof addr === "object" && addr !== null) {
42
+ return addr.port;
43
+ }
44
+ return options.port;
45
+ },
46
+ hostname: options.hostname,
47
+ development: options.development,
48
+ pendingRequests: 0,
49
+ requestIP: (req) => null,
50
+ publish: () => 0,
51
+ subscriberCount: () => 0,
52
+ url: new URL(`http://${options.hostname}:${options.port}`)
53
+ };
54
+ return new Promise((resolve) => {
55
+ server.listen(options.port, options.hostname, () => {
56
+ resolve(fauxServer);
57
+ });
58
+ });
59
+ };
60
+ }
61
+ export {
62
+ createHttpServer
63
+ };
64
+ //# sourceMappingURL=server-adapter-BWrEJbKL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-adapter-BWrEJbKL.js","sources":["../src/plugins/server-adapter.ts"],"sourcesContent":["import type { Server } from \"bun\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport type { ServerFactory } from \"../types\";\n\n/**\n * Creates a server factory that uses the standard Node.js `http` module.\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpServer(): ServerFactory {\n return async (options: any): Promise<Server<any>> => {\n const server = http.createServer(async (req, res) => {\n const url = new URL(req.url!, `http://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any\n });\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server<any> = {\n stop: () => {\n server.close();\n return Promise.resolve(); // Bun.Server stop usually returns void but in type definition it might vary.\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`http://${options.hostname}:${options.port}`)\n } as unknown as Server<any>;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n\n/**\n * Creates a server factory that uses the standard Node.js `https` module.\n * @param sslOptions - Node.js HTTPS options (key, cert, etc.)\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpsServer(sslOptions: https.ServerOptions): ServerFactory {\n return async (options: any): Promise<Server<any>> => {\n const server = https.createServer(sslOptions, async (req, res) => {\n const url = new URL(req.url!, `https://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any\n });\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server<any> = {\n stop: () => {\n server.close();\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`https://${options.hostname}:${options.port}`)\n } as unknown as Server<any>;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n"],"names":["options"],"mappings":";;AASO,SAAS,mBAAkC;AAC9C,SAAO,OAAO,YAAuC;AACjD,UAAM,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AACjD,YAAM,MAAM,IAAI,IAAI,IAAI,KAAM,UAAU,IAAI,QAAQ,IAAI,EAAE;AAC1D,YAAM,UAAU,IAAI,QAAQ,IAAI,YAAY;AAAA,QACxC,QAAQ,IAAI;AAAA,QACZ,SAAS,IAAI;AAAA,QACb,MAAM,CAAC,OAAO,MAAM,EAAE,SAAS,IAAI,MAAO,IAAI,SAAY,IAAI,eAAe;AAAA,UACzE,MAAM,YAAY;AACd,gBAAI,GAAG,QAAQ,CAAA,UAAS,WAAW,QAAQ,KAAK,CAAC;AACjD,gBAAI,GAAG,OAAO,MAAM,WAAW,OAAO;AACtC,gBAAI,GAAG,SAAS,CAAA,QAAO,WAAW,MAAM,GAAG,CAAC;AAAA,UAChD;AAAA,QAAA,CACH;AAAA,MAAA,CACJ;AAED,YAAM,WAAW,MAAM,QAAQ,MAAM,SAAS,UAAU;AAExD,UAAI,aAAa,SAAS;AAC1B,eAAS,QAAQ,QAAQ,CAAC,GAAG,MAAM,IAAI,UAAU,GAAG,CAAC,CAAC;AAEtD,UAAI,SAAS,MAAM;AAEf,cAAM,SAAS,MAAM,SAAS,YAAA;AAC9B,YAAI,IAAI,OAAO,KAAK,MAAM,CAAC;AAAA,MAC/B,OAAO;AACH,YAAI,IAAA;AAAA,MACR;AAAA,IACJ,CAAC;AAED,UAAM,aAA0B;AAAA,MAC5B,MAAM,MAAM;AACR,eAAO,MAAA;AACP,eAAO,QAAQ,QAAA;AAAA,MACnB;AAAA,MACA,QAAQ,KAAKA,UAAS;AAClB,eAAO;AAAA,MACX;AAAA,MACA,OAAOA,UAAS;AACZ,eAAO;AAAA,MACX;AAAA,MACA,IAAI,OAAO;AACP,cAAM,OAAO,OAAO,QAAA;AACpB,YAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC3C,iBAAO,KAAK;AAAA,QAChB;AACA,eAAO,QAAQ;AAAA,MACnB;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB,aAAa,QAAQ;AAAA,MACrB,iBAAiB;AAAA,MACjB,WAAW,CAAC,QAAQ;AAAA,MACpB,SAAS,MAAM;AAAA,MACf,iBAAiB,MAAM;AAAA,MACvB,KAAK,IAAI,IAAI,UAAU,QAAQ,QAAQ,IAAI,QAAQ,IAAI,EAAE;AAAA,IAAA;AAG7D,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC5B,aAAO,OAAO,QAAQ,MAAM,QAAQ,UAAU,MAAM;AAChD,gBAAQ,UAAU;AAAA,MACtB,CAAC;AAAA,IACL,CAAC;AAAA,EACL;AACJ;"}
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const http = require("node:http");
4
+ require("node:https");
5
+ function _interopNamespaceDefault(e) {
6
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
7
+ if (e) {
8
+ for (const k in e) {
9
+ if (k !== "default") {
10
+ const d = Object.getOwnPropertyDescriptor(e, k);
11
+ Object.defineProperty(n, k, d.get ? d : {
12
+ enumerable: true,
13
+ get: () => e[k]
14
+ });
15
+ }
16
+ }
17
+ }
18
+ n.default = e;
19
+ return Object.freeze(n);
20
+ }
21
+ const http__namespace = /* @__PURE__ */ _interopNamespaceDefault(http);
22
+ function createHttpServer() {
23
+ return async (options) => {
24
+ const server = http__namespace.createServer(async (req, res) => {
25
+ const url = new URL(req.url, `http://${req.headers.host}`);
26
+ const request = new Request(url.toString(), {
27
+ method: req.method,
28
+ headers: req.headers,
29
+ body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
30
+ start(controller) {
31
+ req.on("data", (chunk) => controller.enqueue(chunk));
32
+ req.on("end", () => controller.close());
33
+ req.on("error", (err) => controller.error(err));
34
+ }
35
+ })
36
+ });
37
+ const response = await options.fetch(request, fauxServer);
38
+ res.statusCode = response.status;
39
+ response.headers.forEach((v, k) => res.setHeader(k, v));
40
+ if (response.body) {
41
+ const buffer = await response.arrayBuffer();
42
+ res.end(Buffer.from(buffer));
43
+ } else {
44
+ res.end();
45
+ }
46
+ });
47
+ const fauxServer = {
48
+ stop: () => {
49
+ server.close();
50
+ return Promise.resolve();
51
+ },
52
+ upgrade(req, options2) {
53
+ return false;
54
+ },
55
+ reload(options2) {
56
+ return fauxServer;
57
+ },
58
+ get port() {
59
+ const addr = server.address();
60
+ if (typeof addr === "object" && addr !== null) {
61
+ return addr.port;
62
+ }
63
+ return options.port;
64
+ },
65
+ hostname: options.hostname,
66
+ development: options.development,
67
+ pendingRequests: 0,
68
+ requestIP: (req) => null,
69
+ publish: () => 0,
70
+ subscriberCount: () => 0,
71
+ url: new URL(`http://${options.hostname}:${options.port}`)
72
+ };
73
+ return new Promise((resolve) => {
74
+ server.listen(options.port, options.hostname, () => {
75
+ resolve(fauxServer);
76
+ });
77
+ });
78
+ };
79
+ }
80
+ exports.createHttpServer = createHttpServer;
81
+ //# sourceMappingURL=server-adapter-fVKP60e0.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-adapter-fVKP60e0.cjs","sources":["../src/plugins/server-adapter.ts"],"sourcesContent":["import type { Server } from \"bun\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport type { ServerFactory } from \"../types\";\n\n/**\n * Creates a server factory that uses the standard Node.js `http` module.\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpServer(): ServerFactory {\n return async (options: any): Promise<Server<any>> => {\n const server = http.createServer(async (req, res) => {\n const url = new URL(req.url!, `http://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any\n });\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server<any> = {\n stop: () => {\n server.close();\n return Promise.resolve(); // Bun.Server stop usually returns void but in type definition it might vary.\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`http://${options.hostname}:${options.port}`)\n } as unknown as Server<any>;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n\n/**\n * Creates a server factory that uses the standard Node.js `https` module.\n * @param sslOptions - Node.js HTTPS options (key, cert, etc.)\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpsServer(sslOptions: https.ServerOptions): ServerFactory {\n return async (options: any): Promise<Server<any>> => {\n const server = https.createServer(sslOptions, async (req, res) => {\n const url = new URL(req.url!, `https://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any\n });\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server<any> = {\n stop: () => {\n server.close();\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`https://${options.hostname}:${options.port}`)\n } as unknown as Server<any>;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n"],"names":["http","options"],"mappings":";;;;;;;;;;;;;;;;;;;;;AASO,SAAS,mBAAkC;AAC9C,SAAO,OAAO,YAAuC;AACjD,UAAM,SAASA,gBAAK,aAAa,OAAO,KAAK,QAAQ;AACjD,YAAM,MAAM,IAAI,IAAI,IAAI,KAAM,UAAU,IAAI,QAAQ,IAAI,EAAE;AAC1D,YAAM,UAAU,IAAI,QAAQ,IAAI,YAAY;AAAA,QACxC,QAAQ,IAAI;AAAA,QACZ,SAAS,IAAI;AAAA,QACb,MAAM,CAAC,OAAO,MAAM,EAAE,SAAS,IAAI,MAAO,IAAI,SAAY,IAAI,eAAe;AAAA,UACzE,MAAM,YAAY;AACd,gBAAI,GAAG,QAAQ,CAAA,UAAS,WAAW,QAAQ,KAAK,CAAC;AACjD,gBAAI,GAAG,OAAO,MAAM,WAAW,OAAO;AACtC,gBAAI,GAAG,SAAS,CAAA,QAAO,WAAW,MAAM,GAAG,CAAC;AAAA,UAChD;AAAA,QAAA,CACH;AAAA,MAAA,CACJ;AAED,YAAM,WAAW,MAAM,QAAQ,MAAM,SAAS,UAAU;AAExD,UAAI,aAAa,SAAS;AAC1B,eAAS,QAAQ,QAAQ,CAAC,GAAG,MAAM,IAAI,UAAU,GAAG,CAAC,CAAC;AAEtD,UAAI,SAAS,MAAM;AAEf,cAAM,SAAS,MAAM,SAAS,YAAA;AAC9B,YAAI,IAAI,OAAO,KAAK,MAAM,CAAC;AAAA,MAC/B,OAAO;AACH,YAAI,IAAA;AAAA,MACR;AAAA,IACJ,CAAC;AAED,UAAM,aAA0B;AAAA,MAC5B,MAAM,MAAM;AACR,eAAO,MAAA;AACP,eAAO,QAAQ,QAAA;AAAA,MACnB;AAAA,MACA,QAAQ,KAAKC,UAAS;AAClB,eAAO;AAAA,MACX;AAAA,MACA,OAAOA,UAAS;AACZ,eAAO;AAAA,MACX;AAAA,MACA,IAAI,OAAO;AACP,cAAM,OAAO,OAAO,QAAA;AACpB,YAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC3C,iBAAO,KAAK;AAAA,QAChB;AACA,eAAO,QAAQ;AAAA,MACnB;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB,aAAa,QAAQ;AAAA,MACrB,iBAAiB;AAAA,MACjB,WAAW,CAAC,QAAQ;AAAA,MACpB,SAAS,MAAM;AAAA,MACf,iBAAiB,MAAM;AAAA,MACvB,KAAK,IAAI,IAAI,UAAU,QAAQ,QAAQ,IAAI,QAAQ,IAAI,EAAE;AAAA,IAAA;AAG7D,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC5B,aAAO,OAAO,QAAQ,MAAM,QAAQ,UAAU,MAAM;AAChD,gBAAQ,UAAU;AAAA,MACtB,CAAC;AAAA,IACL,CAAC;AAAA,EACL;AACJ;;"}
@@ -5,7 +5,10 @@ import { Middleware, ProcessResult, RequestOptions, ShokupanConfig } from './typ
5
5
  export declare class Shokupan<T = any> extends ShokupanRouter<T> {
6
6
  readonly applicationConfig: ShokupanConfig;
7
7
  openApiSpec?: any;
8
- private middleware;
8
+ private composedMiddleware?;
9
+ private cpuMonitor?;
10
+ private hookCache;
11
+ private hooksInitialized;
9
12
  get logger(): {
10
13
  verbose?: boolean;
11
14
  info?: (msg: string, props: Record<string, any>) => void;
@@ -24,13 +27,19 @@ export declare class Shokupan<T = any> extends ShokupanRouter<T> {
24
27
  * Registers a callback to be executed before the server starts listening.
25
28
  */
26
29
  onStart(callback: () => Promise<void> | void): this;
30
+ private specAvailableHooks;
31
+ /**
32
+ * Registers a callback to be executed when the OpenAPI spec is available.
33
+ * This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
34
+ */
35
+ onSpecAvailable(callback: (spec: any) => void | Promise<void>): this;
27
36
  /**
28
37
  * Starts the application server.
29
38
  *
30
39
  * @param port - The port to listen on. If not specified, the port from the configuration is used. If that is not specified, port 3000 is used.
31
40
  * @returns The server instance.
32
41
  */
33
- listen(port?: number): Promise<Bun.Server>;
42
+ listen(port?: number): Promise<Bun.Server<any>>;
34
43
  [$dispatch](req: ShokupanRequest<T>): Promise<Response>;
35
44
  /**
36
45
  * Processes a request by wrapping the standard fetch method.
@@ -44,5 +53,9 @@ export declare class Shokupan<T = any> extends ShokupanRouter<T> {
44
53
  * @param server - The server instance.
45
54
  * @returns The response to send.
46
55
  */
47
- fetch(req: Request, server?: import('bun').Server): Promise<Response>;
56
+ fetch(req: Request, server?: import('bun').Server<any>): Promise<Response>;
57
+ private handleRequest;
58
+ private ensureHooksInitialized;
59
+ private executeHook;
60
+ private hasHook;
48
61
  }
package/dist/types.d.ts CHANGED
@@ -5,6 +5,13 @@ import { $isRouter } from './symbol';
5
5
  export type DeepPartial<T> = T extends Function ? T : T extends object ? {
6
6
  [P in keyof T]?: DeepPartial<T[P]>;
7
7
  } : T;
8
+ export interface RouteMetadata {
9
+ file: string;
10
+ line: number;
11
+ name?: string;
12
+ isBuiltin?: boolean;
13
+ pluginName?: string;
14
+ }
8
15
  export type MethodAPISpec = OpenAPI.Operation;
9
16
  export type GuardAPISpec = DeepPartial<OpenAPI.Operation>;
10
17
  export type RouterAPISpec = OpenAPI.Operation & Pick<Required<OpenAPI.Operation>, 'tags'> & {
@@ -53,10 +60,15 @@ export declare enum RouteParamType {
53
60
  CONTEXT = "CONTEXT"
54
61
  }
55
62
  export interface ServerFactory {
56
- (options: any): Server | Promise<Server>;
63
+ (options: any): Server<any> | Promise<Server<any>>;
57
64
  }
58
65
  export type NextFn = () => Promise<any>;
59
- export type Middleware = (ctx: ShokupanContext<unknown>, next: NextFn) => Promise<any> | any;
66
+ export type Middleware = ((ctx: ShokupanContext<unknown>, next: NextFn) => Promise<any> | any) & {
67
+ isBuiltin?: boolean;
68
+ pluginName?: string;
69
+ metadata?: RouteMetadata;
70
+ order?: number;
71
+ };
60
72
  export type JSXRenderer = (element: any, args?: unknown) => string | Promise<string>;
61
73
  export type ShokupanRouteConfig = DeepPartial<{
62
74
  name: string;
@@ -73,30 +85,87 @@ export type ShokupanRouteConfig = DeepPartial<{
73
85
  /**
74
86
  * Hooks for this route/router.
75
87
  */
76
- hooks: ShokupanHooks;
88
+ hooks: ShokupanHooks | ShokupanHooks[];
77
89
  /**
78
90
  * Whether to enforce that only controller classes (constructors) are accepted by the router.
79
91
  */
80
92
  controllersOnly: boolean;
93
+ /**
94
+ * Whether to enable automatic backpressure based on system CPU load.
95
+ */
96
+ autoBackpressureFeedback: boolean;
97
+ /**
98
+ * The CPU usage percentage threshold (0-100) at which to start rejecting requests.
99
+ */
100
+ autoBackpressureLevel: number;
81
101
  }>;
82
102
  export type ShokupanRoute = {
103
+ /**
104
+ * HTTP method
105
+ */
83
106
  method: Method;
107
+ /**
108
+ * Route path
109
+ */
84
110
  path: string;
111
+ /**
112
+ * Compiled regex for the route
113
+ */
85
114
  regex: RegExp;
115
+ /**
116
+ * Route parameters
117
+ */
86
118
  keys: string[];
119
+ /**
120
+ * Route handler
121
+ */
87
122
  handler: ShokupanHandler;
123
+ /**
124
+ * Optimization: Handler with hooks baked in.
125
+ * Used by runtime router, while `handler` is used by OpenAPI generator.
126
+ */
127
+ bakedHandler?: ShokupanHandler;
128
+ /**
129
+ * OpenAPI spec for the route
130
+ */
88
131
  handlerSpec?: MethodAPISpec;
132
+ /**
133
+ * Group for the route
134
+ */
89
135
  group?: string;
136
+ /**
137
+ * Guards for the route
138
+ */
90
139
  guards?: {
140
+ /**
141
+ * Guard handler
142
+ */
91
143
  handler: ShokupanHandler;
144
+ /**
145
+ * Guard OpenAPI spec
146
+ */
92
147
  spec?: GuardAPISpec;
93
148
  }[];
149
+ /**
150
+ * Timeout for this specific route (milliseconds).
151
+ */
94
152
  requestTimeout?: number;
153
+ /**
154
+ * Custom JSX renderer for this route.
155
+ */
95
156
  renderer?: JSXRenderer;
96
157
  /**
97
158
  * Hooks from the router/route definition
98
159
  */
99
160
  hooks?: ShokupanHooks;
161
+ /**
162
+ * Source metadata
163
+ */
164
+ metadata?: RouteMetadata;
165
+ /**
166
+ * Order of the middleware
167
+ */
168
+ order?: number;
100
169
  };
101
170
  export type ShokupanConfig<T extends Record<string, any> = Record<string, any>> = DeepPartial<{
102
171
  /**
@@ -135,6 +204,41 @@ export type ShokupanConfig<T extends Record<string, any> = Record<string, any>>
135
204
  * @default false
136
205
  */
137
206
  controllersOnly: boolean;
207
+ /**
208
+ * Whether to enable OpenTelemetry tracing.
209
+ * @default false
210
+ */
211
+ enableTracing?: boolean;
212
+ /**
213
+ * Whether to enable automatic backpressure based on system CPU load.
214
+ * @default false
215
+ */
216
+ autoBackpressureFeedback?: boolean;
217
+ /**
218
+ * The CPU usage percentage threshold (0-100) at which to start rejecting requests.
219
+ * @default 60
220
+ */
221
+ autoBackpressureLevel?: number;
222
+ /**
223
+ * Whether to enable middleware and handler tracking.
224
+ * When enabled, `ctx.handlerStack` will be populated with the handlers the request has passed through.
225
+ * Also, `ctx.state` will be a Proxy that tracks changes made by each handler.
226
+ * @default false
227
+ */
228
+ enableMiddlewareTracking: boolean;
229
+ /**
230
+ * Maximum number of middleware executions to store in the datastore.
231
+ * Only applies when enableMiddlewareTracking is true.
232
+ * @default 10000
233
+ */
234
+ middlewareTrackingMaxCapacity?: number;
235
+ /**
236
+ * Time-to-live for middleware tracking entries in milliseconds.
237
+ * Entries older than this will be cleaned up.
238
+ * Only applies when enableMiddlewareTracking is true.
239
+ * @default 86400000 (1 day)
240
+ */
241
+ middlewareTrackingTTL?: number;
138
242
  /**
139
243
  * HTTP logger function.
140
244
  */
@@ -182,7 +286,7 @@ export type ShokupanConfig<T extends Record<string, any> = Record<string, any>>
182
286
  /**
183
287
  * Lifecycle hooks.
184
288
  */
185
- hooks: ShokupanHooks<T>;
289
+ hooks: ShokupanHooks<T> | ShokupanHooks<T>[];
186
290
  [key: string]: any;
187
291
  }>;
188
292
  export interface RequestOptions {
@@ -0,0 +1,11 @@
1
+ export declare class SystemCpuMonitor {
2
+ private readonly intervalMs;
3
+ private interval;
4
+ private lastCpus;
5
+ private currentUsage;
6
+ constructor(intervalMs?: number);
7
+ start(): void;
8
+ stop(): void;
9
+ getUsage(): number;
10
+ private update;
11
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Captures the file and line number of the caller.
3
+ * Use skipFrames to skip helper functions in the stack trace.
4
+ */
5
+ export declare function getCallerInfo(skipFrames?: number): {
6
+ file: string;
7
+ line: number;
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shokupan",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shokupan is a low-lift modern web framework for Bun.",
5
5
  "author": "Andrew G. Knackstedt",
6
6
  "repository": {
@@ -20,7 +20,9 @@
20
20
  "scripts": {
21
21
  "dev": "bun --watch --inspect src/example/main.ts",
22
22
  "debug:otel": "sh scripts/debug-otel.sh",
23
- "build": "vite build"
23
+ "build": "vite build",
24
+ "bench": "cd src/benchmarking && bun runner.ts",
25
+ "bench:advanced": "cd src/benchmarking && bun advanced-runner.ts"
24
26
  },
25
27
  "bin": {
26
28
  "shokupan": "./dist/cli.js",
@@ -58,12 +60,16 @@
58
60
  "@opentelemetry/semantic-conventions": "^1.38.0",
59
61
  "@scalar/api-reference": "^1.40.9",
60
62
  "@scalar/openapi-types": "^0.5.3",
63
+ "@surrealdb/node": "^2.4.0",
64
+ "ajv": "^8.17.1",
65
+ "ajv-formats": "^3.0.1",
61
66
  "arctic": "^3.7.0",
62
67
  "class-transformer": "^0.5.1",
63
68
  "class-validator": "^0.14.3",
64
69
  "eta": "^4.5.0",
65
70
  "jose": "^6.1.3",
66
71
  "reflect-metadata": "^0.2.2",
72
+ "surrealdb": "^2.0.0-alpha.14",
67
73
  "tslib": "^2.8.1"
68
74
  },
69
75
  "devDependencies": {
@@ -72,7 +78,6 @@
72
78
  "@types/axios": "^0.9.36",
73
79
  "@types/bun": "^1.2.23",
74
80
  "@types/supertest": "^6.0.3",
75
- "ajv": "^8.17.1",
76
81
  "axios": "^1.13.2",
77
82
  "get-port": "^7.1.0",
78
83
  "supertest": "^7.1.4",