shokupan 0.6.1 → 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.
- package/README.md +55 -2
- package/dist/{openapi-analyzer-Bei1sVWp.cjs → analyzer-Bei1sVWp.cjs} +1 -1
- package/dist/analyzer-Bei1sVWp.cjs.map +1 -0
- package/dist/{openapi-analyzer-Ce_7JxZh.js → analyzer-Ce_7JxZh.js} +1 -1
- package/dist/analyzer-Ce_7JxZh.js.map +1 -0
- package/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts +58 -23
- package/dist/{server-adapter-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
- package/dist/http-server-BEMPIs33.cjs.map +1 -0
- package/dist/{server-adapter-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
- package/dist/http-server-CCeagTyU.js.map +1 -0
- package/dist/index.cjs +1940 -917
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +18 -17
- package/dist/index.js +1948 -925
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/plugins/{auth.d.ts → application/auth.d.ts} +72 -3
- package/dist/plugins/application/cluster.d.ts +33 -0
- package/dist/plugins/{failed-request-recorder.d.ts → application/dashboard/failed-request-recorder.d.ts} +1 -1
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
- package/dist/plugins/application/dashboard/plugin.d.ts +42 -0
- package/dist/plugins/application/dashboard/static/charts.js +328 -0
- package/dist/plugins/application/dashboard/static/failures.js +85 -0
- package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
- package/dist/plugins/application/dashboard/static/poll.js +146 -0
- package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
- package/dist/plugins/application/dashboard/static/registry.css +131 -0
- package/dist/plugins/application/dashboard/static/registry.js +269 -0
- package/dist/plugins/application/dashboard/static/requests.js +118 -0
- package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
- package/dist/plugins/application/dashboard/static/styles.css +175 -0
- package/dist/plugins/application/dashboard/static/tables.js +92 -0
- package/dist/plugins/application/dashboard/static/tabs.js +113 -0
- package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
- package/dist/plugins/application/dashboard/template.eta +246 -0
- package/dist/plugins/{server-adapter.d.ts → application/http-server.d.ts} +1 -1
- package/dist/plugins/{idempotency → application/idempotency}/plugin.d.ts +7 -1
- package/dist/plugins/{openapi.d.ts → application/openapi/openapi.d.ts} +2 -2
- package/dist/plugins/application/scalar.d.ts +36 -0
- package/dist/plugins/application/socket-io.d.ts +14 -0
- package/dist/plugins/middleware/compression.d.ts +17 -0
- package/dist/plugins/middleware/cors.d.ts +34 -0
- package/dist/plugins/{express.d.ts → middleware/express.d.ts} +1 -1
- package/dist/plugins/{openapi-validator.d.ts → middleware/openapi-validator.d.ts} +2 -2
- package/dist/plugins/middleware/proxy.d.ts +37 -0
- package/dist/plugins/middleware/rate-limit.d.ts +58 -0
- package/dist/plugins/{security-headers.d.ts → middleware/security-headers.d.ts} +51 -1
- package/dist/plugins/{serve-static.d.ts → middleware/serve-static.d.ts} +1 -1
- package/dist/plugins/{session.d.ts → middleware/session.d.ts} +89 -3
- package/dist/plugins/{validation.d.ts → middleware/validation.d.ts} +6 -1
- package/dist/router.d.ts +17 -5
- package/dist/shokupan.d.ts +31 -5
- package/dist/util/async-hooks.d.ts +8 -2
- package/dist/util/datastore.d.ts +4 -3
- package/dist/{decorators.d.ts → util/decorators.d.ts} +6 -1
- package/dist/util/http-error.d.ts +38 -0
- package/dist/util/http-status.d.ts +32 -0
- package/dist/util/instrumentation.d.ts +1 -1
- package/dist/{request.d.ts → util/request.d.ts} +1 -1
- package/dist/util/symbol.d.ts +34 -0
- package/dist/{router → util}/trie.d.ts +1 -1
- package/dist/{types.d.ts → util/types.d.ts} +38 -2
- package/package.json +9 -6
- package/dist/openapi-analyzer-Bei1sVWp.cjs.map +0 -1
- package/dist/openapi-analyzer-Ce_7JxZh.js.map +0 -1
- package/dist/plugins/compression.d.ts +0 -5
- package/dist/plugins/cors.d.ts +0 -11
- package/dist/plugins/debugview/plugin.d.ts +0 -29
- package/dist/plugins/proxy.d.ts +0 -11
- package/dist/plugins/rate-limit.d.ts +0 -15
- package/dist/plugins/scalar.d.ts +0 -15
- package/dist/server-adapter-0xH174zz.js.map +0 -1
- package/dist/server-adapter-DFhwlK8e.cjs.map +0 -1
- package/dist/symbol.d.ts +0 -15
- /package/dist/{analysis/openapi-analyzer.d.ts → plugins/application/openapi/analyzer.d.ts} +0 -0
- /package/dist/{di.d.ts → util/di.d.ts} +0 -0
- /package/dist/{response.d.ts → util/response.d.ts} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -22,21 +22,29 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
mod
|
|
23
23
|
));
|
|
24
24
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
25
|
+
const nanoid = require("nanoid");
|
|
25
26
|
const promises = require("node:fs/promises");
|
|
27
|
+
const api = require("@opentelemetry/api");
|
|
28
|
+
const node_async_hooks = require("node:async_hooks");
|
|
29
|
+
const surrealdb = require("surrealdb");
|
|
26
30
|
const eta$2 = require("eta");
|
|
27
31
|
const promises$1 = require("fs/promises");
|
|
28
32
|
const path = require("path");
|
|
29
|
-
const node_async_hooks = require("node:async_hooks");
|
|
30
|
-
const api = require("@opentelemetry/api");
|
|
31
33
|
const os = require("node:os");
|
|
32
34
|
const arctic = require("arctic");
|
|
33
35
|
const jose = require("jose");
|
|
36
|
+
const cluster = require("node:cluster");
|
|
37
|
+
const net = require("node:net");
|
|
38
|
+
const path$1 = require("node:path");
|
|
39
|
+
const node_url = require("node:url");
|
|
40
|
+
const node_perf_hooks = require("node:perf_hooks");
|
|
41
|
+
const analyzer = require("./analyzer-Bei1sVWp.cjs");
|
|
34
42
|
const zlib = require("node:zlib");
|
|
35
43
|
const Ajv = require("ajv");
|
|
36
44
|
const addFormats = require("ajv-formats");
|
|
37
|
-
const openapiAnalyzer = require("./openapi-analyzer-Bei1sVWp.cjs");
|
|
38
45
|
const crypto = require("crypto");
|
|
39
46
|
const events = require("events");
|
|
47
|
+
var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
|
|
40
48
|
function _interopNamespaceDefault(e) {
|
|
41
49
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
42
50
|
if (e) {
|
|
@@ -56,6 +64,102 @@ function _interopNamespaceDefault(e) {
|
|
|
56
64
|
const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
|
|
57
65
|
const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
|
|
58
66
|
const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
|
|
67
|
+
const HTTP_STATUS = {
|
|
68
|
+
// 2xx Success
|
|
69
|
+
OK: 200,
|
|
70
|
+
CREATED: 201,
|
|
71
|
+
ACCEPTED: 202,
|
|
72
|
+
NO_CONTENT: 204,
|
|
73
|
+
// 3xx Redirection
|
|
74
|
+
MOVED_PERMANENTLY: 301,
|
|
75
|
+
FOUND: 302,
|
|
76
|
+
SEE_OTHER: 303,
|
|
77
|
+
NOT_MODIFIED: 304,
|
|
78
|
+
TEMPORARY_REDIRECT: 307,
|
|
79
|
+
PERMANENT_REDIRECT: 308,
|
|
80
|
+
// 4xx Client Errors
|
|
81
|
+
BAD_REQUEST: 400,
|
|
82
|
+
UNAUTHORIZED: 401,
|
|
83
|
+
FORBIDDEN: 403,
|
|
84
|
+
NOT_FOUND: 404,
|
|
85
|
+
METHOD_NOT_ALLOWED: 405,
|
|
86
|
+
REQUEST_TIMEOUT: 408,
|
|
87
|
+
CONFLICT: 409,
|
|
88
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
89
|
+
TOO_MANY_REQUESTS: 429,
|
|
90
|
+
// 5xx Server Errors
|
|
91
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
92
|
+
NOT_IMPLEMENTED: 501,
|
|
93
|
+
BAD_GATEWAY: 502,
|
|
94
|
+
SERVICE_UNAVAILABLE: 503,
|
|
95
|
+
GATEWAY_TIMEOUT: 504
|
|
96
|
+
};
|
|
97
|
+
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
98
|
+
100,
|
|
99
|
+
101,
|
|
100
|
+
102,
|
|
101
|
+
103,
|
|
102
|
+
200,
|
|
103
|
+
201,
|
|
104
|
+
202,
|
|
105
|
+
203,
|
|
106
|
+
204,
|
|
107
|
+
205,
|
|
108
|
+
206,
|
|
109
|
+
207,
|
|
110
|
+
208,
|
|
111
|
+
226,
|
|
112
|
+
300,
|
|
113
|
+
301,
|
|
114
|
+
302,
|
|
115
|
+
303,
|
|
116
|
+
304,
|
|
117
|
+
305,
|
|
118
|
+
306,
|
|
119
|
+
307,
|
|
120
|
+
308,
|
|
121
|
+
400,
|
|
122
|
+
401,
|
|
123
|
+
402,
|
|
124
|
+
403,
|
|
125
|
+
404,
|
|
126
|
+
405,
|
|
127
|
+
406,
|
|
128
|
+
407,
|
|
129
|
+
408,
|
|
130
|
+
409,
|
|
131
|
+
410,
|
|
132
|
+
411,
|
|
133
|
+
412,
|
|
134
|
+
413,
|
|
135
|
+
414,
|
|
136
|
+
415,
|
|
137
|
+
416,
|
|
138
|
+
417,
|
|
139
|
+
418,
|
|
140
|
+
421,
|
|
141
|
+
422,
|
|
142
|
+
423,
|
|
143
|
+
424,
|
|
144
|
+
425,
|
|
145
|
+
426,
|
|
146
|
+
428,
|
|
147
|
+
429,
|
|
148
|
+
431,
|
|
149
|
+
451,
|
|
150
|
+
500,
|
|
151
|
+
501,
|
|
152
|
+
502,
|
|
153
|
+
503,
|
|
154
|
+
504,
|
|
155
|
+
505,
|
|
156
|
+
506,
|
|
157
|
+
507,
|
|
158
|
+
508,
|
|
159
|
+
510,
|
|
160
|
+
511
|
|
161
|
+
]);
|
|
162
|
+
const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
59
163
|
class ShokupanResponse {
|
|
60
164
|
_headers = null;
|
|
61
165
|
_status = 200;
|
|
@@ -119,6 +223,40 @@ class ShokupanResponse {
|
|
|
119
223
|
return this._headers !== null;
|
|
120
224
|
}
|
|
121
225
|
}
|
|
226
|
+
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
227
|
+
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
228
|
+
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
229
|
+
const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
|
|
230
|
+
const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
|
|
231
|
+
const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
|
|
232
|
+
const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
|
|
233
|
+
const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
|
|
234
|
+
const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
|
|
235
|
+
const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
|
|
236
|
+
const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
|
|
237
|
+
const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
|
|
238
|
+
const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
|
|
239
|
+
const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
|
|
240
|
+
const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
|
|
241
|
+
const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
|
|
242
|
+
const $url = /* @__PURE__ */ Symbol.for("Shokupan.ctx.url");
|
|
243
|
+
const $requestId = /* @__PURE__ */ Symbol.for("Shokupan.ctx.requestId");
|
|
244
|
+
const $debug = /* @__PURE__ */ Symbol.for("Shokupan.ctx.debug");
|
|
245
|
+
const $finalResponse = /* @__PURE__ */ Symbol.for("Shokupan.ctx.finalResponse");
|
|
246
|
+
const $rawBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.rawBody");
|
|
247
|
+
const $cachedBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedBody");
|
|
248
|
+
const $bodyType = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyType");
|
|
249
|
+
const $bodyParsed = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParsed");
|
|
250
|
+
const $bodyParseError = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParseError");
|
|
251
|
+
const $routeMatched = /* @__PURE__ */ Symbol.for("Shokupan.ctx.routeMatched");
|
|
252
|
+
const $cachedHostname = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHostname");
|
|
253
|
+
const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol");
|
|
254
|
+
const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
|
|
255
|
+
const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
|
|
256
|
+
const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
|
|
257
|
+
const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
|
|
258
|
+
const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
|
|
259
|
+
const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
|
|
122
260
|
function isValidCookieDomain(domain, currentHost) {
|
|
123
261
|
const hostWithoutPort = currentHost.split(":")[0];
|
|
124
262
|
if (domain === hostWithoutPort) return true;
|
|
@@ -128,72 +266,6 @@ function isValidCookieDomain(domain, currentHost) {
|
|
|
128
266
|
}
|
|
129
267
|
return false;
|
|
130
268
|
}
|
|
131
|
-
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
132
|
-
100,
|
|
133
|
-
101,
|
|
134
|
-
102,
|
|
135
|
-
103,
|
|
136
|
-
200,
|
|
137
|
-
201,
|
|
138
|
-
202,
|
|
139
|
-
203,
|
|
140
|
-
204,
|
|
141
|
-
205,
|
|
142
|
-
206,
|
|
143
|
-
207,
|
|
144
|
-
208,
|
|
145
|
-
226,
|
|
146
|
-
300,
|
|
147
|
-
301,
|
|
148
|
-
302,
|
|
149
|
-
303,
|
|
150
|
-
304,
|
|
151
|
-
305,
|
|
152
|
-
306,
|
|
153
|
-
307,
|
|
154
|
-
308,
|
|
155
|
-
400,
|
|
156
|
-
401,
|
|
157
|
-
402,
|
|
158
|
-
403,
|
|
159
|
-
404,
|
|
160
|
-
405,
|
|
161
|
-
406,
|
|
162
|
-
407,
|
|
163
|
-
408,
|
|
164
|
-
409,
|
|
165
|
-
410,
|
|
166
|
-
411,
|
|
167
|
-
412,
|
|
168
|
-
413,
|
|
169
|
-
414,
|
|
170
|
-
415,
|
|
171
|
-
416,
|
|
172
|
-
417,
|
|
173
|
-
418,
|
|
174
|
-
421,
|
|
175
|
-
422,
|
|
176
|
-
423,
|
|
177
|
-
424,
|
|
178
|
-
425,
|
|
179
|
-
426,
|
|
180
|
-
428,
|
|
181
|
-
429,
|
|
182
|
-
431,
|
|
183
|
-
451,
|
|
184
|
-
500,
|
|
185
|
-
501,
|
|
186
|
-
502,
|
|
187
|
-
503,
|
|
188
|
-
504,
|
|
189
|
-
505,
|
|
190
|
-
506,
|
|
191
|
-
507,
|
|
192
|
-
508,
|
|
193
|
-
510,
|
|
194
|
-
511
|
|
195
|
-
]);
|
|
196
|
-
const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
197
269
|
class ShokupanContext {
|
|
198
270
|
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
199
271
|
this.request = request;
|
|
@@ -222,28 +294,43 @@ class ShokupanContext {
|
|
|
222
294
|
state;
|
|
223
295
|
handlerStack = [];
|
|
224
296
|
response;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
297
|
+
[$debug];
|
|
298
|
+
[$finalResponse];
|
|
299
|
+
[$rawBody];
|
|
228
300
|
// Raw body for compression optimization
|
|
229
301
|
// Body caching to avoid double parsing
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
302
|
+
[$url];
|
|
303
|
+
[$cachedBody];
|
|
304
|
+
[$bodyType];
|
|
305
|
+
[$bodyParsed] = false;
|
|
306
|
+
[$bodyParseError];
|
|
307
|
+
[$routeMatched] = false;
|
|
235
308
|
// Cached URL properties to avoid repeated parsing
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
309
|
+
[$cachedHostname];
|
|
310
|
+
[$cachedProtocol];
|
|
311
|
+
[$cachedHost];
|
|
312
|
+
[$cachedOrigin];
|
|
313
|
+
[$cachedQuery];
|
|
314
|
+
[$ws];
|
|
315
|
+
[$socket];
|
|
316
|
+
[$io];
|
|
317
|
+
/**
|
|
318
|
+
* JSX Rendering Function
|
|
319
|
+
*/
|
|
320
|
+
renderer;
|
|
321
|
+
setRenderer(renderer) {
|
|
322
|
+
this.renderer = renderer;
|
|
323
|
+
}
|
|
324
|
+
[$requestId];
|
|
325
|
+
get requestId() {
|
|
326
|
+
return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid.nanoid();
|
|
327
|
+
}
|
|
241
328
|
get url() {
|
|
242
|
-
if (!this
|
|
329
|
+
if (!this[$url]) {
|
|
243
330
|
const urlString = this.request.url || "http://localhost/";
|
|
244
|
-
this
|
|
331
|
+
this[$url] = new URL(urlString);
|
|
245
332
|
}
|
|
246
|
-
return this
|
|
333
|
+
return this[$url];
|
|
247
334
|
}
|
|
248
335
|
/**
|
|
249
336
|
* Base request
|
|
@@ -261,7 +348,7 @@ class ShokupanContext {
|
|
|
261
348
|
* Request path
|
|
262
349
|
*/
|
|
263
350
|
get path() {
|
|
264
|
-
if (this
|
|
351
|
+
if (this[$url]) return this[$url].pathname;
|
|
265
352
|
const url = this.request.url;
|
|
266
353
|
let queryIndex = url.indexOf("?");
|
|
267
354
|
const end = queryIndex === -1 ? url.length : queryIndex;
|
|
@@ -286,7 +373,7 @@ class ShokupanContext {
|
|
|
286
373
|
* Request query params
|
|
287
374
|
*/
|
|
288
375
|
get query() {
|
|
289
|
-
if (this
|
|
376
|
+
if (this[$cachedQuery]) return this[$cachedQuery];
|
|
290
377
|
const q = /* @__PURE__ */ Object.create(null);
|
|
291
378
|
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
292
379
|
const entries = Object.entries(this.url.searchParams);
|
|
@@ -303,7 +390,7 @@ class ShokupanContext {
|
|
|
303
390
|
q[key] = value;
|
|
304
391
|
}
|
|
305
392
|
}
|
|
306
|
-
this
|
|
393
|
+
this[$cachedQuery] = q;
|
|
307
394
|
return q;
|
|
308
395
|
}
|
|
309
396
|
/**
|
|
@@ -316,19 +403,19 @@ class ShokupanContext {
|
|
|
316
403
|
* Request hostname (e.g. "localhost")
|
|
317
404
|
*/
|
|
318
405
|
get hostname() {
|
|
319
|
-
return this
|
|
406
|
+
return this[$cachedHostname] ??= this.url.hostname;
|
|
320
407
|
}
|
|
321
408
|
/**
|
|
322
409
|
* Request host (e.g. "localhost:3000")
|
|
323
410
|
*/
|
|
324
411
|
get host() {
|
|
325
|
-
return this
|
|
412
|
+
return this[$cachedHost] ??= this.url.host;
|
|
326
413
|
}
|
|
327
414
|
/**
|
|
328
415
|
* Request protocol (e.g. "http:", "https:")
|
|
329
416
|
*/
|
|
330
417
|
get protocol() {
|
|
331
|
-
return this
|
|
418
|
+
return this[$cachedProtocol] ??= this.url.protocol;
|
|
332
419
|
}
|
|
333
420
|
/**
|
|
334
421
|
* Whether request is secure (https)
|
|
@@ -340,7 +427,7 @@ class ShokupanContext {
|
|
|
340
427
|
* Request origin (e.g. "http://localhost:3000")
|
|
341
428
|
*/
|
|
342
429
|
get origin() {
|
|
343
|
-
return this
|
|
430
|
+
return this[$cachedOrigin] ??= this.url.origin;
|
|
344
431
|
}
|
|
345
432
|
/**
|
|
346
433
|
* Request headers
|
|
@@ -361,6 +448,24 @@ class ShokupanContext {
|
|
|
361
448
|
get res() {
|
|
362
449
|
return this.response;
|
|
363
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Raw WebSocket connection
|
|
453
|
+
*/
|
|
454
|
+
get ws() {
|
|
455
|
+
return this[$ws];
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Socket.io socket
|
|
459
|
+
*/
|
|
460
|
+
get socket() {
|
|
461
|
+
return this[$socket];
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Socket.io server
|
|
465
|
+
*/
|
|
466
|
+
get io() {
|
|
467
|
+
return this[$io];
|
|
468
|
+
}
|
|
364
469
|
/**
|
|
365
470
|
* Helper to set a header on the response
|
|
366
471
|
* @param key Header key
|
|
@@ -370,6 +475,20 @@ class ShokupanContext {
|
|
|
370
475
|
this.response.set(key, value);
|
|
371
476
|
return this;
|
|
372
477
|
}
|
|
478
|
+
isUpgraded = false;
|
|
479
|
+
/**
|
|
480
|
+
* Upgrades the request to a WebSocket connection.
|
|
481
|
+
* @param options Upgrade options
|
|
482
|
+
* @returns true if upgraded, false otherwise
|
|
483
|
+
*/
|
|
484
|
+
upgrade(options) {
|
|
485
|
+
if (!this.server) return false;
|
|
486
|
+
const success = this.server.upgrade(this.req, options);
|
|
487
|
+
if (success) {
|
|
488
|
+
this.isUpgraded = true;
|
|
489
|
+
}
|
|
490
|
+
return success;
|
|
491
|
+
}
|
|
373
492
|
/**
|
|
374
493
|
* Set a cookie
|
|
375
494
|
* @param name Cookie name
|
|
@@ -447,33 +566,37 @@ class ShokupanContext {
|
|
|
447
566
|
* The body is only parsed once and cached for subsequent reads.
|
|
448
567
|
*/
|
|
449
568
|
async body() {
|
|
450
|
-
if (this
|
|
451
|
-
throw this
|
|
569
|
+
if (this[$bodyParseError]) {
|
|
570
|
+
throw this[$bodyParseError];
|
|
452
571
|
}
|
|
453
|
-
if (this
|
|
454
|
-
return this
|
|
572
|
+
if (this[$bodyParsed]) {
|
|
573
|
+
return this[$cachedBody];
|
|
455
574
|
}
|
|
456
575
|
const contentType = this.request.headers.get("content-type") || "";
|
|
457
576
|
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
458
|
-
const rawText = await this.readRawBody();
|
|
459
577
|
const parserType = this.app?.applicationConfig?.jsonParser || "native";
|
|
460
578
|
if (parserType === "native") {
|
|
461
|
-
|
|
579
|
+
try {
|
|
580
|
+
this[$cachedBody] = await this.request.json();
|
|
581
|
+
} catch (e) {
|
|
582
|
+
throw e;
|
|
583
|
+
}
|
|
462
584
|
} else {
|
|
585
|
+
const rawText = await this.request.text();
|
|
463
586
|
const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
|
|
464
587
|
const parser = getJSONParser(parserType);
|
|
465
|
-
this
|
|
588
|
+
this[$cachedBody] = parser(rawText);
|
|
466
589
|
}
|
|
467
|
-
this
|
|
590
|
+
this[$bodyType] = "json";
|
|
468
591
|
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
469
|
-
this
|
|
470
|
-
this
|
|
592
|
+
this[$cachedBody] = await this.request.formData();
|
|
593
|
+
this[$bodyType] = "formData";
|
|
471
594
|
} else {
|
|
472
|
-
this
|
|
473
|
-
this
|
|
595
|
+
this[$cachedBody] = await this.request.text();
|
|
596
|
+
this[$bodyType] = "text";
|
|
474
597
|
}
|
|
475
|
-
this
|
|
476
|
-
return this
|
|
598
|
+
this[$bodyParsed] = true;
|
|
599
|
+
return this[$cachedBody];
|
|
477
600
|
}
|
|
478
601
|
/**
|
|
479
602
|
* Pre-parse the request body before handler execution.
|
|
@@ -481,7 +604,7 @@ class ShokupanContext {
|
|
|
481
604
|
* Errors are deferred until the body is actually accessed in the handler.
|
|
482
605
|
*/
|
|
483
606
|
async parseBody() {
|
|
484
|
-
if (this
|
|
607
|
+
if (this[$bodyParsed]) {
|
|
485
608
|
return;
|
|
486
609
|
}
|
|
487
610
|
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
@@ -490,7 +613,7 @@ class ShokupanContext {
|
|
|
490
613
|
try {
|
|
491
614
|
await this.body();
|
|
492
615
|
} catch (error) {
|
|
493
|
-
this
|
|
616
|
+
this[$bodyParseError] = error;
|
|
494
617
|
}
|
|
495
618
|
}
|
|
496
619
|
/**
|
|
@@ -540,10 +663,21 @@ class ShokupanContext {
|
|
|
540
663
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
541
664
|
}
|
|
542
665
|
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
543
|
-
this
|
|
666
|
+
this[$rawBody] = body;
|
|
667
|
+
}
|
|
668
|
+
return this[$finalResponse] ??= new Response(body, { status, headers });
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Emit an event to the client (WebSocket only)
|
|
672
|
+
* @param event Event name
|
|
673
|
+
* @param data Event data (Must be JSON serializable)
|
|
674
|
+
*/
|
|
675
|
+
emit(event, data) {
|
|
676
|
+
if (this[$ws]) {
|
|
677
|
+
this[$ws].send(JSON.stringify({ event, data }));
|
|
678
|
+
} else if (this[$socket]) {
|
|
679
|
+
this[$socket].emit(event, data);
|
|
544
680
|
}
|
|
545
|
-
this._finalResponse = new Response(body, { status, headers });
|
|
546
|
-
return this._finalResponse;
|
|
547
681
|
}
|
|
548
682
|
/**
|
|
549
683
|
* Respond with a JSON object
|
|
@@ -554,18 +688,18 @@ class ShokupanContext {
|
|
|
554
688
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
555
689
|
}
|
|
556
690
|
const jsonString = JSON.stringify(data);
|
|
557
|
-
this
|
|
691
|
+
this[$rawBody] = jsonString;
|
|
558
692
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
559
|
-
this
|
|
693
|
+
this[$finalResponse] = new Response(jsonString, {
|
|
560
694
|
status: finalStatus,
|
|
561
695
|
headers: { "content-type": "application/json" }
|
|
562
696
|
});
|
|
563
|
-
return this
|
|
697
|
+
return this[$finalResponse];
|
|
564
698
|
}
|
|
565
699
|
const finalHeaders = this.mergeHeaders(headers);
|
|
566
700
|
finalHeaders.set("content-type", "application/json");
|
|
567
|
-
this
|
|
568
|
-
return this
|
|
701
|
+
this[$finalResponse] = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
|
|
702
|
+
return this[$finalResponse];
|
|
569
703
|
}
|
|
570
704
|
/**
|
|
571
705
|
* Respond with a text string
|
|
@@ -575,18 +709,18 @@ class ShokupanContext {
|
|
|
575
709
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
576
710
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
577
711
|
}
|
|
578
|
-
this
|
|
712
|
+
this[$rawBody] = data;
|
|
579
713
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
580
|
-
this
|
|
714
|
+
this[$finalResponse] = new Response(data, {
|
|
581
715
|
status: finalStatus,
|
|
582
716
|
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
583
717
|
});
|
|
584
|
-
return this
|
|
718
|
+
return this[$finalResponse];
|
|
585
719
|
}
|
|
586
720
|
const finalHeaders = this.mergeHeaders(headers);
|
|
587
721
|
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
588
|
-
this
|
|
589
|
-
return this
|
|
722
|
+
this[$finalResponse] = new Response(data, { status: finalStatus, headers: finalHeaders });
|
|
723
|
+
return this[$finalResponse];
|
|
590
724
|
}
|
|
591
725
|
/**
|
|
592
726
|
* Respond with HTML content
|
|
@@ -598,9 +732,9 @@ class ShokupanContext {
|
|
|
598
732
|
}
|
|
599
733
|
const finalHeaders = this.mergeHeaders(headers);
|
|
600
734
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
601
|
-
this
|
|
602
|
-
this
|
|
603
|
-
return this
|
|
735
|
+
this[$rawBody] = html;
|
|
736
|
+
this[$finalResponse] = new Response(html, { status: finalStatus, headers: finalHeaders });
|
|
737
|
+
return this[$finalResponse];
|
|
604
738
|
}
|
|
605
739
|
/**
|
|
606
740
|
* Respond with a redirect
|
|
@@ -611,8 +745,8 @@ class ShokupanContext {
|
|
|
611
745
|
}
|
|
612
746
|
const headers = this.mergeHeaders();
|
|
613
747
|
headers.set("Location", url);
|
|
614
|
-
this
|
|
615
|
-
return this
|
|
748
|
+
this[$finalResponse] = new Response(null, { status, headers });
|
|
749
|
+
return this[$finalResponse];
|
|
616
750
|
}
|
|
617
751
|
/**
|
|
618
752
|
* Respond with a status code
|
|
@@ -623,8 +757,8 @@ class ShokupanContext {
|
|
|
623
757
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
624
758
|
}
|
|
625
759
|
const headers = this.mergeHeaders();
|
|
626
|
-
this
|
|
627
|
-
return this
|
|
760
|
+
this[$finalResponse] = new Response(null, { status, headers });
|
|
761
|
+
return this[$finalResponse];
|
|
628
762
|
}
|
|
629
763
|
/**
|
|
630
764
|
* Respond with a file
|
|
@@ -636,21 +770,17 @@ class ShokupanContext {
|
|
|
636
770
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
637
771
|
}
|
|
638
772
|
if (typeof Bun !== "undefined") {
|
|
639
|
-
this
|
|
640
|
-
return this
|
|
773
|
+
this[$finalResponse] = new Response(Bun.file(path2, fileOptions), { status, headers });
|
|
774
|
+
return this[$finalResponse];
|
|
641
775
|
} else {
|
|
642
776
|
const fileBuffer = await promises.readFile(path2);
|
|
643
777
|
if (fileOptions?.type) {
|
|
644
778
|
headers.set("content-type", fileOptions.type);
|
|
645
779
|
}
|
|
646
|
-
this
|
|
647
|
-
return this
|
|
780
|
+
this[$finalResponse] = new Response(fileBuffer, { status, headers });
|
|
781
|
+
return this[$finalResponse];
|
|
648
782
|
}
|
|
649
783
|
}
|
|
650
|
-
/**
|
|
651
|
-
* JSX Rendering Function
|
|
652
|
-
*/
|
|
653
|
-
renderer;
|
|
654
784
|
/**
|
|
655
785
|
* Render a JSX element
|
|
656
786
|
* @param element JSX Element
|
|
@@ -669,284 +799,67 @@ class ShokupanContext {
|
|
|
669
799
|
return this.html(html, status, headers);
|
|
670
800
|
}
|
|
671
801
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (xForwardedFor && trustedProxies.length > 0) {
|
|
686
|
-
const ips = xForwardedFor.split(",").map((ip) => ip.trim());
|
|
687
|
-
for (let i = ips.length - 1; i >= 0; i--) {
|
|
688
|
-
const ip = ips[i];
|
|
689
|
-
if (!trustedProxies.includes(ip)) {
|
|
690
|
-
if (/^[\d.:a-fA-F]+$/.test(ip)) {
|
|
691
|
-
return ip;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
802
|
+
const compose = (middleware) => {
|
|
803
|
+
if (!middleware.length) {
|
|
804
|
+
return (context, next) => {
|
|
805
|
+
return next ? next() : Promise.resolve();
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
return function dispatch(context, next) {
|
|
809
|
+
let index = -1;
|
|
810
|
+
async function runner(i) {
|
|
811
|
+
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
812
|
+
index = i;
|
|
813
|
+
if (i >= middleware.length) {
|
|
814
|
+
return next ? next() : Promise.resolve();
|
|
694
815
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
816
|
+
const fn = middleware[i];
|
|
817
|
+
if (!context[$debug]) {
|
|
818
|
+
return fn(context, () => runner(i + 1));
|
|
819
|
+
}
|
|
820
|
+
const debug = context[$debug];
|
|
821
|
+
const debugId = fn._debugId || fn.name || "anonymous";
|
|
822
|
+
const previousNode = debug.getCurrentNode();
|
|
823
|
+
debug.trackEdge(previousNode, debugId);
|
|
824
|
+
debug.setNode(debugId);
|
|
825
|
+
const start = performance.now();
|
|
826
|
+
try {
|
|
827
|
+
const res = await Promise.resolve(fn(context, () => runner(i + 1)));
|
|
828
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "success");
|
|
829
|
+
return res;
|
|
830
|
+
} catch (err) {
|
|
831
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
|
|
832
|
+
return Promise.reject(err);
|
|
833
|
+
} finally {
|
|
834
|
+
if (previousNode) debug.setNode(previousNode);
|
|
707
835
|
}
|
|
708
836
|
}
|
|
709
|
-
|
|
710
|
-
if (interval.unref) interval.unref();
|
|
711
|
-
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
712
|
-
if (skip(ctx)) return next();
|
|
713
|
-
const key = keyGenerator(ctx);
|
|
714
|
-
const now = Date.now();
|
|
715
|
-
let record = hits.get(key);
|
|
716
|
-
if (!record || record.resetTime <= now) {
|
|
717
|
-
record = {
|
|
718
|
-
hits: 0,
|
|
719
|
-
resetTime: now + windowMs
|
|
720
|
-
};
|
|
721
|
-
hits.set(key, record);
|
|
722
|
-
}
|
|
723
|
-
record.hits++;
|
|
724
|
-
const remaining = Math.max(0, max - record.hits);
|
|
725
|
-
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
726
|
-
const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
|
|
727
|
-
const setHeaders = (res) => {
|
|
728
|
-
if (!headers || !res || !res.headers) return;
|
|
729
|
-
try {
|
|
730
|
-
res.headers.set("X-RateLimit-Limit", String(max));
|
|
731
|
-
res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
732
|
-
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
733
|
-
} catch (e) {
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
if (record.hits > max) {
|
|
737
|
-
typeof message === "object" ? JSON.stringify(message) : String(message);
|
|
738
|
-
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
739
|
-
if (headers) {
|
|
740
|
-
setHeaders(res);
|
|
741
|
-
res.headers.set("Retry-After", String(retryAfter));
|
|
742
|
-
}
|
|
743
|
-
return res;
|
|
744
|
-
}
|
|
745
|
-
const response = await next();
|
|
746
|
-
if (response instanceof Response && headers) {
|
|
747
|
-
setHeaders(response);
|
|
748
|
-
}
|
|
749
|
-
return response;
|
|
750
|
-
};
|
|
751
|
-
rateLimitMiddleware.isBuiltin = true;
|
|
752
|
-
rateLimitMiddleware.pluginName = "RateLimit";
|
|
753
|
-
return rateLimitMiddleware;
|
|
754
|
-
}
|
|
755
|
-
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
756
|
-
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
757
|
-
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
758
|
-
const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
|
|
759
|
-
const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
|
|
760
|
-
const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
|
|
761
|
-
const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
|
|
762
|
-
const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
|
|
763
|
-
const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
|
|
764
|
-
const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
|
|
765
|
-
const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
|
|
766
|
-
const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
|
|
767
|
-
const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
|
|
768
|
-
const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
|
|
769
|
-
const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
|
|
770
|
-
const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
|
|
771
|
-
var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
772
|
-
RouteParamType2["BODY"] = "BODY";
|
|
773
|
-
RouteParamType2["PARAM"] = "PARAM";
|
|
774
|
-
RouteParamType2["QUERY"] = "QUERY";
|
|
775
|
-
RouteParamType2["HEADER"] = "HEADER";
|
|
776
|
-
RouteParamType2["REQUEST"] = "REQUEST";
|
|
777
|
-
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
778
|
-
return RouteParamType2;
|
|
779
|
-
})(RouteParamType || {});
|
|
780
|
-
function Controller(path2 = "/") {
|
|
781
|
-
return (target) => {
|
|
782
|
-
target[$controllerPath] = path2;
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
function Use(...middleware) {
|
|
786
|
-
return (target, propertyKey, descriptor) => {
|
|
787
|
-
if (!propertyKey) {
|
|
788
|
-
const existing = target[$middleware] || [];
|
|
789
|
-
target[$middleware] = [...existing, ...middleware];
|
|
790
|
-
} else {
|
|
791
|
-
if (!target[$middleware]) {
|
|
792
|
-
target[$middleware] = /* @__PURE__ */ new Map();
|
|
793
|
-
}
|
|
794
|
-
const existing = target[$middleware].get(propertyKey) || [];
|
|
795
|
-
target[$middleware].set(propertyKey, [...existing, ...middleware]);
|
|
796
|
-
}
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
function createParamDecorator(type) {
|
|
800
|
-
return (name) => {
|
|
801
|
-
return (target, propertyKey, parameterIndex) => {
|
|
802
|
-
if (!target[$routeArgs]) {
|
|
803
|
-
target[$routeArgs] = /* @__PURE__ */ new Map();
|
|
804
|
-
}
|
|
805
|
-
if (!target[$routeArgs].has(propertyKey)) {
|
|
806
|
-
target[$routeArgs].set(propertyKey, []);
|
|
807
|
-
}
|
|
808
|
-
target[$routeArgs].get(propertyKey).push({
|
|
809
|
-
index: parameterIndex,
|
|
810
|
-
type,
|
|
811
|
-
name
|
|
812
|
-
});
|
|
813
|
-
};
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
const Body = createParamDecorator(RouteParamType.BODY);
|
|
817
|
-
const Param = createParamDecorator(RouteParamType.PARAM);
|
|
818
|
-
const Query = createParamDecorator(RouteParamType.QUERY);
|
|
819
|
-
const Headers$1 = createParamDecorator(RouteParamType.HEADER);
|
|
820
|
-
const Req = createParamDecorator(RouteParamType.REQUEST);
|
|
821
|
-
const Ctx = createParamDecorator(RouteParamType.CONTEXT);
|
|
822
|
-
function Spec(spec) {
|
|
823
|
-
return (target, propertyKey, descriptor) => {
|
|
824
|
-
if (!target[$routeSpec]) {
|
|
825
|
-
target[$routeSpec] = /* @__PURE__ */ new Map();
|
|
826
|
-
}
|
|
827
|
-
target[$routeSpec].set(propertyKey, spec);
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
function createMethodDecorator(method) {
|
|
831
|
-
return (path2 = "/") => {
|
|
832
|
-
return (target, propertyKey, descriptor) => {
|
|
833
|
-
if (!target[$routeMethods]) {
|
|
834
|
-
target[$routeMethods] = /* @__PURE__ */ new Map();
|
|
835
|
-
}
|
|
836
|
-
target[$routeMethods].set(propertyKey, {
|
|
837
|
-
method,
|
|
838
|
-
path: path2
|
|
839
|
-
});
|
|
840
|
-
};
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
const Get = createMethodDecorator("GET");
|
|
844
|
-
const Post = createMethodDecorator("POST");
|
|
845
|
-
const Put = createMethodDecorator("PUT");
|
|
846
|
-
const Delete = createMethodDecorator("DELETE");
|
|
847
|
-
const Patch = createMethodDecorator("PATCH");
|
|
848
|
-
const Options = createMethodDecorator("OPTIONS");
|
|
849
|
-
const Head = createMethodDecorator("HEAD");
|
|
850
|
-
const All = createMethodDecorator("ALL");
|
|
851
|
-
function RateLimit(options) {
|
|
852
|
-
return Use(RateLimitMiddleware(options));
|
|
853
|
-
}
|
|
854
|
-
class Container {
|
|
855
|
-
static services = /* @__PURE__ */ new Map();
|
|
856
|
-
static register(target, instance) {
|
|
857
|
-
this.services.set(target, instance);
|
|
858
|
-
}
|
|
859
|
-
static get(target) {
|
|
860
|
-
return this.services.get(target);
|
|
861
|
-
}
|
|
862
|
-
static has(target) {
|
|
863
|
-
return this.services.has(target);
|
|
864
|
-
}
|
|
865
|
-
static resolve(target) {
|
|
866
|
-
if (this.services.has(target)) {
|
|
867
|
-
return this.services.get(target);
|
|
868
|
-
}
|
|
869
|
-
const instance = new target();
|
|
870
|
-
this.services.set(target, instance);
|
|
871
|
-
return instance;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
function Injectable() {
|
|
875
|
-
return (target) => {
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
function Inject(token) {
|
|
879
|
-
return (target, key) => {
|
|
880
|
-
Object.defineProperty(target, key, {
|
|
881
|
-
get: () => Container.resolve(token),
|
|
882
|
-
enumerable: true,
|
|
883
|
-
configurable: true
|
|
884
|
-
});
|
|
837
|
+
return runner(0);
|
|
885
838
|
};
|
|
886
|
-
}
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
async function runner(i) {
|
|
896
|
-
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
897
|
-
index = i;
|
|
898
|
-
if (i >= middleware.length) {
|
|
899
|
-
return next ? next() : Promise.resolve();
|
|
900
|
-
}
|
|
901
|
-
const fn = middleware[i];
|
|
902
|
-
if (!context._debug) {
|
|
903
|
-
return fn(context, () => runner(i + 1));
|
|
839
|
+
};
|
|
840
|
+
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
841
|
+
function traceHandler(fn, name) {
|
|
842
|
+
return async function(...args) {
|
|
843
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
844
|
+
kind: api.SpanKind.INTERNAL,
|
|
845
|
+
attributes: {
|
|
846
|
+
"http.route": name,
|
|
847
|
+
"component": "shokupan.route"
|
|
904
848
|
}
|
|
905
|
-
|
|
906
|
-
const debugId = fn._debugId || fn.name || "anonymous";
|
|
907
|
-
const previousNode = debug.getCurrentNode();
|
|
908
|
-
debug.trackEdge(previousNode, debugId);
|
|
909
|
-
debug.setNode(debugId);
|
|
910
|
-
const start = performance.now();
|
|
849
|
+
}, async (span) => {
|
|
911
850
|
try {
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
return res;
|
|
851
|
+
const result = await fn.apply(this, args);
|
|
852
|
+
return result;
|
|
915
853
|
} catch (err) {
|
|
916
|
-
|
|
917
|
-
|
|
854
|
+
span.recordException(err);
|
|
855
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
856
|
+
throw err;
|
|
918
857
|
} finally {
|
|
919
|
-
|
|
858
|
+
span.end();
|
|
920
859
|
}
|
|
921
|
-
}
|
|
922
|
-
return runner(0);
|
|
860
|
+
});
|
|
923
861
|
};
|
|
924
|
-
};
|
|
925
|
-
class ShokupanRequestBase {
|
|
926
|
-
method;
|
|
927
|
-
url;
|
|
928
|
-
headers;
|
|
929
|
-
body;
|
|
930
|
-
async json() {
|
|
931
|
-
return JSON.parse(this.body);
|
|
932
|
-
}
|
|
933
|
-
async text() {
|
|
934
|
-
return this.body;
|
|
935
|
-
}
|
|
936
|
-
async formData() {
|
|
937
|
-
if (this.body instanceof FormData) {
|
|
938
|
-
return this.body;
|
|
939
|
-
}
|
|
940
|
-
return new Response(this.body, { headers: this.headers }).formData();
|
|
941
|
-
}
|
|
942
|
-
constructor(props) {
|
|
943
|
-
Object.assign(this, props);
|
|
944
|
-
if (!(this.headers instanceof Headers)) {
|
|
945
|
-
this.headers = new Headers(this.headers);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
862
|
}
|
|
949
|
-
const ShokupanRequest = ShokupanRequestBase;
|
|
950
863
|
function isObject(item) {
|
|
951
864
|
return item && typeof item === "object" && !Array.isArray(item);
|
|
952
865
|
}
|
|
@@ -1172,9 +1085,9 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1172
1085
|
const defaultTagName = options.defaultTag || "Application";
|
|
1173
1086
|
let astRoutes = [];
|
|
1174
1087
|
try {
|
|
1175
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./
|
|
1176
|
-
const
|
|
1177
|
-
const { applications } = await
|
|
1088
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-Bei1sVWp.cjs"));
|
|
1089
|
+
const analyzer2 = new OpenAPIAnalyzer(process.cwd());
|
|
1090
|
+
const { applications } = await analyzer2.analyze();
|
|
1178
1091
|
astRoutes = await getAstRoutes(applications);
|
|
1179
1092
|
} catch (e) {
|
|
1180
1093
|
}
|
|
@@ -1361,6 +1274,40 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1361
1274
|
"x-tagGroups": xTagGroups
|
|
1362
1275
|
};
|
|
1363
1276
|
}
|
|
1277
|
+
class RequestContextStore {
|
|
1278
|
+
request;
|
|
1279
|
+
span;
|
|
1280
|
+
}
|
|
1281
|
+
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
1282
|
+
class HttpError extends Error {
|
|
1283
|
+
status;
|
|
1284
|
+
constructor(message, status) {
|
|
1285
|
+
super(message);
|
|
1286
|
+
this.name = "HttpError";
|
|
1287
|
+
this.status = status;
|
|
1288
|
+
if (Error.captureStackTrace) {
|
|
1289
|
+
Error.captureStackTrace(this, HttpError);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function getErrorStatus(err) {
|
|
1294
|
+
if (!err || typeof err !== "object") {
|
|
1295
|
+
return 500;
|
|
1296
|
+
}
|
|
1297
|
+
if (typeof err.status === "number") {
|
|
1298
|
+
return err.status;
|
|
1299
|
+
}
|
|
1300
|
+
if (typeof err.statusCode === "number") {
|
|
1301
|
+
return err.statusCode;
|
|
1302
|
+
}
|
|
1303
|
+
return 500;
|
|
1304
|
+
}
|
|
1305
|
+
class EventError extends HttpError {
|
|
1306
|
+
constructor(message = "Event Error") {
|
|
1307
|
+
super(message, 500);
|
|
1308
|
+
this.name = "EventError";
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1364
1311
|
const eta$1 = new eta$2.Eta();
|
|
1365
1312
|
function serveStatic(config, prefix) {
|
|
1366
1313
|
const rootPath = path.resolve(config.root || ".");
|
|
@@ -1513,14 +1460,162 @@ function serveStatic(config, prefix) {
|
|
|
1513
1460
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1514
1461
|
return serveStaticMiddleware;
|
|
1515
1462
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1463
|
+
const G = globalThis;
|
|
1464
|
+
G.__shokupan_db = G.__shokupan_db || null;
|
|
1465
|
+
G.__shokupan_db_promise = G.__shokupan_db_promise || null;
|
|
1466
|
+
async function ensureDb() {
|
|
1467
|
+
if (G.__shokupan_db) return G.__shokupan_db;
|
|
1468
|
+
if (G.__shokupan_db_promise) return G.__shokupan_db_promise;
|
|
1469
|
+
G.__shokupan_db_promise = (async () => {
|
|
1470
|
+
try {
|
|
1471
|
+
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1472
|
+
const surreal = await import("surrealdb");
|
|
1473
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1474
|
+
const _db = new surrealdb.Surreal({
|
|
1475
|
+
engines: createNodeEngines()
|
|
1476
|
+
});
|
|
1477
|
+
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1478
|
+
await _db.query(`
|
|
1479
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1480
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1481
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1482
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1483
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1484
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1485
|
+
DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
|
|
1486
|
+
`);
|
|
1487
|
+
G.__shokupan_db = _db;
|
|
1488
|
+
return _db;
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
G.__shokupan_db_promise = null;
|
|
1491
|
+
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1492
|
+
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1493
|
+
}
|
|
1494
|
+
throw e;
|
|
1495
|
+
}
|
|
1496
|
+
})();
|
|
1497
|
+
return G.__shokupan_db_promise;
|
|
1498
|
+
}
|
|
1499
|
+
const datastore = {
|
|
1500
|
+
async get(recordId) {
|
|
1501
|
+
await ensureDb();
|
|
1502
|
+
return G.__shokupan_db.select(recordId);
|
|
1503
|
+
},
|
|
1504
|
+
async set(recordId, value) {
|
|
1505
|
+
await ensureDb();
|
|
1506
|
+
return G.__shokupan_db.upsert(recordId).content(value);
|
|
1507
|
+
},
|
|
1508
|
+
async query(query, vars) {
|
|
1509
|
+
await ensureDb();
|
|
1510
|
+
try {
|
|
1511
|
+
return G.__shokupan_db.query(query, vars).collect();
|
|
1512
|
+
} catch (e) {
|
|
1513
|
+
console.error("DS ERROR:", e);
|
|
1514
|
+
throw e;
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
get ready() {
|
|
1518
|
+
return ensureDb().then(() => void 0);
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
process.on("exit", async () => {
|
|
1522
|
+
if (G.__shokupan_db) await G.__shokupan_db.close();
|
|
1523
|
+
});
|
|
1524
|
+
class Container {
|
|
1525
|
+
static services = /* @__PURE__ */ new Map();
|
|
1526
|
+
static register(target, instance) {
|
|
1527
|
+
this.services.set(target, instance);
|
|
1528
|
+
}
|
|
1529
|
+
static get(target) {
|
|
1530
|
+
return this.services.get(target);
|
|
1531
|
+
}
|
|
1532
|
+
static has(target) {
|
|
1533
|
+
return this.services.has(target);
|
|
1534
|
+
}
|
|
1535
|
+
static resolve(target) {
|
|
1536
|
+
if (this.services.has(target)) {
|
|
1537
|
+
return this.services.get(target);
|
|
1538
|
+
}
|
|
1539
|
+
const instance = new target();
|
|
1540
|
+
this.services.set(target, instance);
|
|
1541
|
+
return instance;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
function Injectable() {
|
|
1545
|
+
return (target) => {
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function Inject(token) {
|
|
1549
|
+
return (target, key) => {
|
|
1550
|
+
Object.defineProperty(target, key, {
|
|
1551
|
+
get: () => Container.resolve(token),
|
|
1552
|
+
enumerable: true,
|
|
1553
|
+
configurable: true
|
|
1554
|
+
});
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
class ShokupanRequestBase {
|
|
1558
|
+
method;
|
|
1559
|
+
url;
|
|
1560
|
+
headers;
|
|
1561
|
+
body;
|
|
1562
|
+
async json() {
|
|
1563
|
+
return JSON.parse(this.body);
|
|
1564
|
+
}
|
|
1565
|
+
async text() {
|
|
1566
|
+
return this.body;
|
|
1567
|
+
}
|
|
1568
|
+
async formData() {
|
|
1569
|
+
if (this.body instanceof FormData) {
|
|
1570
|
+
return this.body;
|
|
1571
|
+
}
|
|
1572
|
+
return new Response(this.body, { headers: this.headers }).formData();
|
|
1573
|
+
}
|
|
1574
|
+
constructor(props) {
|
|
1575
|
+
Object.assign(this, props);
|
|
1576
|
+
if (!(this.headers instanceof Headers)) {
|
|
1577
|
+
this.headers = new Headers(this.headers);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
const ShokupanRequest = ShokupanRequestBase;
|
|
1582
|
+
function getCallerInfo(skipFrames = 1) {
|
|
1583
|
+
let file = "unknown";
|
|
1584
|
+
let line = 0;
|
|
1585
|
+
try {
|
|
1586
|
+
const err = new Error();
|
|
1587
|
+
const stack = err.stack?.split("\n") || [];
|
|
1588
|
+
let found = 0;
|
|
1589
|
+
for (let i = 1; i < stack.length; i++) {
|
|
1590
|
+
const l = stack[i];
|
|
1591
|
+
if (!l.includes(":")) continue;
|
|
1592
|
+
if (l.includes("node_modules")) continue;
|
|
1593
|
+
if (l.includes("bun:main")) continue;
|
|
1594
|
+
if (l.includes("src/util/stack.ts")) continue;
|
|
1595
|
+
if (l.includes("src/router.ts")) continue;
|
|
1596
|
+
if (l.includes("src/shokupan.ts")) continue;
|
|
1597
|
+
found++;
|
|
1598
|
+
if (found >= skipFrames) {
|
|
1599
|
+
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
1600
|
+
if (match) {
|
|
1601
|
+
file = match[1];
|
|
1602
|
+
line = parseInt(match[2], 10);
|
|
1603
|
+
return { file, line };
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
} catch (e) {
|
|
1608
|
+
}
|
|
1609
|
+
return { file, line };
|
|
1610
|
+
}
|
|
1611
|
+
class RouterTrie {
|
|
1612
|
+
root;
|
|
1613
|
+
constructor() {
|
|
1614
|
+
this.root = this.createNode();
|
|
1615
|
+
}
|
|
1616
|
+
createNode() {
|
|
1617
|
+
return {
|
|
1618
|
+
children: {}
|
|
1524
1619
|
};
|
|
1525
1620
|
}
|
|
1526
1621
|
insert(method, path2, handler) {
|
|
@@ -1613,124 +1708,16 @@ class RouterTrie {
|
|
|
1613
1708
|
return s.split("/");
|
|
1614
1709
|
}
|
|
1615
1710
|
}
|
|
1616
|
-
const
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
const surreal = await import("surrealdb");
|
|
1627
|
-
const Surreal = surreal.Surreal;
|
|
1628
|
-
RecordId = surreal.RecordId;
|
|
1629
|
-
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1630
|
-
const _db = new Surreal({
|
|
1631
|
-
engines: createNodeEngines()
|
|
1632
|
-
});
|
|
1633
|
-
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1634
|
-
await _db.query(`
|
|
1635
|
-
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1636
|
-
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1637
|
-
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1638
|
-
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1639
|
-
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1640
|
-
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1641
|
-
`);
|
|
1642
|
-
db = _db;
|
|
1643
|
-
return db;
|
|
1644
|
-
} catch (e) {
|
|
1645
|
-
dbPromise = null;
|
|
1646
|
-
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1647
|
-
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1648
|
-
}
|
|
1649
|
-
throw e;
|
|
1650
|
-
}
|
|
1651
|
-
})();
|
|
1652
|
-
return dbPromise;
|
|
1653
|
-
}
|
|
1654
|
-
const datastore = {
|
|
1655
|
-
async get(store, key) {
|
|
1656
|
-
await ensureDb();
|
|
1657
|
-
return db.select(new RecordId(store, key));
|
|
1658
|
-
},
|
|
1659
|
-
async set(store, key, value) {
|
|
1660
|
-
await ensureDb();
|
|
1661
|
-
return db.create(new RecordId(store, key)).content(value);
|
|
1662
|
-
},
|
|
1663
|
-
async query(query, vars) {
|
|
1664
|
-
await ensureDb();
|
|
1665
|
-
try {
|
|
1666
|
-
const r = await db.query(query, vars);
|
|
1667
|
-
return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
|
|
1668
|
-
} catch (e) {
|
|
1669
|
-
console.error("DS ERROR:", e);
|
|
1670
|
-
throw e;
|
|
1671
|
-
}
|
|
1672
|
-
},
|
|
1673
|
-
get ready() {
|
|
1674
|
-
return ensureDb().then(() => void 0);
|
|
1675
|
-
}
|
|
1676
|
-
};
|
|
1677
|
-
process.on("exit", async () => {
|
|
1678
|
-
if (db) await db.close();
|
|
1679
|
-
});
|
|
1680
|
-
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1681
|
-
function traceHandler(fn, name) {
|
|
1682
|
-
return async function(...args) {
|
|
1683
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1684
|
-
kind: api.SpanKind.INTERNAL,
|
|
1685
|
-
attributes: {
|
|
1686
|
-
"http.route": name,
|
|
1687
|
-
"component": "shokupan.route"
|
|
1688
|
-
}
|
|
1689
|
-
}, async (span) => {
|
|
1690
|
-
try {
|
|
1691
|
-
const result = await fn.apply(this, args);
|
|
1692
|
-
return result;
|
|
1693
|
-
} catch (err) {
|
|
1694
|
-
span.recordException(err);
|
|
1695
|
-
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
1696
|
-
throw err;
|
|
1697
|
-
} finally {
|
|
1698
|
-
span.end();
|
|
1699
|
-
}
|
|
1700
|
-
});
|
|
1701
|
-
};
|
|
1702
|
-
}
|
|
1703
|
-
function getCallerInfo(skipFrames = 1) {
|
|
1704
|
-
let file = "unknown";
|
|
1705
|
-
let line = 0;
|
|
1706
|
-
try {
|
|
1707
|
-
const err = new Error();
|
|
1708
|
-
const stack = err.stack?.split("\n") || [];
|
|
1709
|
-
let found = 0;
|
|
1710
|
-
for (let i = 1; i < stack.length; i++) {
|
|
1711
|
-
const l = stack[i];
|
|
1712
|
-
if (!l.includes(":")) continue;
|
|
1713
|
-
if (l.includes("node_modules")) continue;
|
|
1714
|
-
if (l.includes("bun:main")) continue;
|
|
1715
|
-
if (l.includes("src/util/stack.ts")) continue;
|
|
1716
|
-
if (l.includes("src/router.ts")) continue;
|
|
1717
|
-
if (l.includes("src/shokupan.ts")) continue;
|
|
1718
|
-
found++;
|
|
1719
|
-
if (found >= skipFrames) {
|
|
1720
|
-
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
1721
|
-
if (match) {
|
|
1722
|
-
file = match[1];
|
|
1723
|
-
line = parseInt(match[2], 10);
|
|
1724
|
-
return { file, line };
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
} catch (e) {
|
|
1729
|
-
}
|
|
1730
|
-
return { file, line };
|
|
1731
|
-
}
|
|
1732
|
-
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1733
|
-
const ShokupanApplicationTree = {};
|
|
1711
|
+
const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
|
|
1712
|
+
var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
1713
|
+
RouteParamType2["BODY"] = "BODY";
|
|
1714
|
+
RouteParamType2["PARAM"] = "PARAM";
|
|
1715
|
+
RouteParamType2["QUERY"] = "QUERY";
|
|
1716
|
+
RouteParamType2["HEADER"] = "HEADER";
|
|
1717
|
+
RouteParamType2["REQUEST"] = "REQUEST";
|
|
1718
|
+
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
1719
|
+
return RouteParamType2;
|
|
1720
|
+
})(RouteParamType || {});
|
|
1734
1721
|
class ShokupanRouter {
|
|
1735
1722
|
constructor(config) {
|
|
1736
1723
|
this.config = config;
|
|
@@ -1763,6 +1750,7 @@ class ShokupanRouter {
|
|
|
1763
1750
|
metadata;
|
|
1764
1751
|
// Metadata for the router itself
|
|
1765
1752
|
currentGuards = [];
|
|
1753
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
1766
1754
|
// Registry Accessor
|
|
1767
1755
|
getComponentRegistry() {
|
|
1768
1756
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
@@ -1823,6 +1811,34 @@ class ShokupanRouter {
|
|
|
1823
1811
|
isRouterInstance(target) {
|
|
1824
1812
|
return typeof target === "object" && target !== null && $isRouter in target;
|
|
1825
1813
|
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Registers an event handler for WebSocket.
|
|
1816
|
+
*/
|
|
1817
|
+
event(name, handler) {
|
|
1818
|
+
if (this.eventHandlers.has(name)) {
|
|
1819
|
+
const err = new EventError(`Event handler \`${name}\` already exists.`);
|
|
1820
|
+
console.warn(err);
|
|
1821
|
+
const handlers = this.eventHandlers.get(name);
|
|
1822
|
+
handlers.push(handler);
|
|
1823
|
+
this.eventHandlers.set(name, handlers);
|
|
1824
|
+
} else {
|
|
1825
|
+
this.eventHandlers.set(name, [handler]);
|
|
1826
|
+
}
|
|
1827
|
+
return this;
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Finds an event handler(s) by name.
|
|
1831
|
+
*/
|
|
1832
|
+
findEvent(name) {
|
|
1833
|
+
if (this.eventHandlers.has(name)) {
|
|
1834
|
+
return this.eventHandlers.get(name);
|
|
1835
|
+
}
|
|
1836
|
+
for (const child of this[$childRouters]) {
|
|
1837
|
+
const handler = child.findEvent(name);
|
|
1838
|
+
if (handler) return handler;
|
|
1839
|
+
}
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1826
1842
|
/**
|
|
1827
1843
|
* Mounts a controller instance to a path prefix.
|
|
1828
1844
|
*
|
|
@@ -1841,216 +1857,9 @@ class ShokupanRouter {
|
|
|
1841
1857
|
throw new Error(`[Shokupan] strict controller check failed: ${controller.constructor.name || typeof controller} is not a class constructor.`);
|
|
1842
1858
|
}
|
|
1843
1859
|
if (this.isRouterInstance(controller)) {
|
|
1844
|
-
|
|
1845
|
-
throw new Error("Router is already mounted");
|
|
1846
|
-
}
|
|
1847
|
-
controller[$mountPath] = prefix;
|
|
1848
|
-
if (!controller.metadata) {
|
|
1849
|
-
const info = getCallerInfo();
|
|
1850
|
-
controller.metadata = {
|
|
1851
|
-
file: info.file,
|
|
1852
|
-
line: info.line,
|
|
1853
|
-
name: "MountedRouter"
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
this[$childRouters].push(controller);
|
|
1857
|
-
controller[$parent] = this;
|
|
1858
|
-
const setRouterContext = (router) => {
|
|
1859
|
-
router[$appRoot] = this.root;
|
|
1860
|
-
router[$childRouters].forEach((child) => setRouterContext(child));
|
|
1861
|
-
};
|
|
1862
|
-
setRouterContext(controller);
|
|
1863
|
-
if (this[$appRoot]) ;
|
|
1864
|
-
controller[$appRoot] = this.root;
|
|
1865
|
-
controller[$isMounted] = true;
|
|
1860
|
+
this.mountRouter(prefix, controller);
|
|
1866
1861
|
} else {
|
|
1867
|
-
|
|
1868
|
-
if (typeof controller === "function") {
|
|
1869
|
-
instance = Container.resolve(controller);
|
|
1870
|
-
const controllerPath = controller[$controllerPath];
|
|
1871
|
-
if (controllerPath) {
|
|
1872
|
-
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1873
|
-
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
1874
|
-
prefix = p1 + p2;
|
|
1875
|
-
if (!prefix) prefix = "/";
|
|
1876
|
-
}
|
|
1877
|
-
} else {
|
|
1878
|
-
const ctor = instance.constructor;
|
|
1879
|
-
const controllerPath = ctor[$controllerPath];
|
|
1880
|
-
if (controllerPath) {
|
|
1881
|
-
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1882
|
-
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
1883
|
-
prefix = p1 + p2;
|
|
1884
|
-
if (!prefix) prefix = "/";
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
instance[$mountPath] = prefix;
|
|
1888
|
-
const info = getCallerInfo();
|
|
1889
|
-
instance.metadata = {
|
|
1890
|
-
file: info.file,
|
|
1891
|
-
line: info.line,
|
|
1892
|
-
name: instance.constructor.name
|
|
1893
|
-
};
|
|
1894
|
-
this[$childControllers].push(instance);
|
|
1895
|
-
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
1896
|
-
const proto = Object.getPrototypeOf(instance);
|
|
1897
|
-
const methods = /* @__PURE__ */ new Set();
|
|
1898
|
-
let current = proto;
|
|
1899
|
-
while (current && current !== Object.prototype) {
|
|
1900
|
-
Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
|
|
1901
|
-
current = Object.getPrototypeOf(current);
|
|
1902
|
-
}
|
|
1903
|
-
Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
|
|
1904
|
-
const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
|
|
1905
|
-
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1906
|
-
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
1907
|
-
let routesAttached = 0;
|
|
1908
|
-
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
1909
|
-
const name = Array.from(methods)[i];
|
|
1910
|
-
if (name === "constructor") continue;
|
|
1911
|
-
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
1912
|
-
const originalHandler = instance[name];
|
|
1913
|
-
if (typeof originalHandler !== "function") continue;
|
|
1914
|
-
let method;
|
|
1915
|
-
let subPath = "";
|
|
1916
|
-
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
1917
|
-
const config = decoratedRoutes.get(name);
|
|
1918
|
-
method = config.method;
|
|
1919
|
-
subPath = config.path;
|
|
1920
|
-
} else {
|
|
1921
|
-
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
1922
|
-
const m = HTTPMethods[j];
|
|
1923
|
-
if (name.toUpperCase().startsWith(m)) {
|
|
1924
|
-
method = m;
|
|
1925
|
-
const rest = name.slice(m.length);
|
|
1926
|
-
if (rest.length === 0) {
|
|
1927
|
-
subPath = "/";
|
|
1928
|
-
} else {
|
|
1929
|
-
subPath = "";
|
|
1930
|
-
let buffer = "";
|
|
1931
|
-
const flush = () => {
|
|
1932
|
-
if (buffer.length > 0) {
|
|
1933
|
-
subPath += "/" + buffer.toLowerCase();
|
|
1934
|
-
buffer = "";
|
|
1935
|
-
}
|
|
1936
|
-
};
|
|
1937
|
-
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
1938
|
-
const char = rest[i2];
|
|
1939
|
-
if (char === "$") {
|
|
1940
|
-
flush();
|
|
1941
|
-
subPath += "/:";
|
|
1942
|
-
continue;
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
|
|
1946
|
-
if (!subPath.startsWith("/")) {
|
|
1947
|
-
subPath = "/" + subPath;
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
break;
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
if (method) {
|
|
1955
|
-
routesAttached++;
|
|
1956
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1957
|
-
const cleanSubPath = subPath === "/" ? "" : subPath;
|
|
1958
|
-
let joined;
|
|
1959
|
-
if (cleanSubPath.length === 0) {
|
|
1960
|
-
joined = cleanPrefix;
|
|
1961
|
-
} else if (cleanSubPath.startsWith("/")) {
|
|
1962
|
-
joined = cleanPrefix + cleanSubPath;
|
|
1963
|
-
} else {
|
|
1964
|
-
joined = cleanPrefix + "/" + cleanSubPath;
|
|
1965
|
-
}
|
|
1966
|
-
const fullPath = joined || "/";
|
|
1967
|
-
const normalizedPath = fullPath.replace(/\/+/g, "/");
|
|
1968
|
-
const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
|
|
1969
|
-
const allMiddleware = [...controllerMiddleware, ...methodMw];
|
|
1970
|
-
const routeArgs = decoratedArgs && decoratedArgs.get(name);
|
|
1971
|
-
const wrappedHandler = async (ctx) => {
|
|
1972
|
-
let args = [ctx];
|
|
1973
|
-
if (routeArgs?.length > 0) {
|
|
1974
|
-
args = [];
|
|
1975
|
-
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
1976
|
-
for (let k = 0; k < sortedArgs.length; k++) {
|
|
1977
|
-
const arg = sortedArgs[k];
|
|
1978
|
-
switch (arg.type) {
|
|
1979
|
-
case RouteParamType.BODY:
|
|
1980
|
-
try {
|
|
1981
|
-
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
1982
|
-
args[arg.index] = await ctx.req.json();
|
|
1983
|
-
} else {
|
|
1984
|
-
const text = await ctx.req.text();
|
|
1985
|
-
if (!text) {
|
|
1986
|
-
args[arg.index] = {};
|
|
1987
|
-
} else {
|
|
1988
|
-
args[arg.index] = JSON.parse(text);
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
} catch (e) {
|
|
1992
|
-
const err = new Error("Invalid JSON body");
|
|
1993
|
-
err.status = 400;
|
|
1994
|
-
throw err;
|
|
1995
|
-
}
|
|
1996
|
-
break;
|
|
1997
|
-
case RouteParamType.PARAM:
|
|
1998
|
-
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
1999
|
-
break;
|
|
2000
|
-
case RouteParamType.QUERY: {
|
|
2001
|
-
const url = new URL(ctx.req.url);
|
|
2002
|
-
if (arg.name) {
|
|
2003
|
-
const vals = url.searchParams.getAll(arg.name);
|
|
2004
|
-
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
2005
|
-
} else {
|
|
2006
|
-
const query = {};
|
|
2007
|
-
const keys = Object.keys(url.searchParams);
|
|
2008
|
-
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
2009
|
-
const key = keys[k2];
|
|
2010
|
-
const vals = url.searchParams.getAll(key);
|
|
2011
|
-
query[key] = vals.length > 1 ? vals : vals[0];
|
|
2012
|
-
}
|
|
2013
|
-
args[arg.index] = query;
|
|
2014
|
-
}
|
|
2015
|
-
break;
|
|
2016
|
-
}
|
|
2017
|
-
case RouteParamType.HEADER:
|
|
2018
|
-
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2019
|
-
break;
|
|
2020
|
-
case RouteParamType.REQUEST:
|
|
2021
|
-
args[arg.index] = ctx.req;
|
|
2022
|
-
break;
|
|
2023
|
-
case RouteParamType.CONTEXT:
|
|
2024
|
-
args[arg.index] = ctx;
|
|
2025
|
-
break;
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
|
|
2030
|
-
return tracedOriginalHandler.apply(instance, args);
|
|
2031
|
-
};
|
|
2032
|
-
let finalHandler = wrappedHandler;
|
|
2033
|
-
if (allMiddleware.length > 0) {
|
|
2034
|
-
const composed = compose(allMiddleware);
|
|
2035
|
-
finalHandler = async (ctx) => {
|
|
2036
|
-
return composed(ctx, () => wrappedHandler(ctx));
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
finalHandler.originalHandler = originalHandler;
|
|
2040
|
-
if (finalHandler !== wrappedHandler) {
|
|
2041
|
-
wrappedHandler.originalHandler = originalHandler;
|
|
2042
|
-
}
|
|
2043
|
-
const tagName = instance.constructor.name;
|
|
2044
|
-
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2045
|
-
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2046
|
-
const spec = { tags: [tagName], ...userSpec };
|
|
2047
|
-
this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
if (routesAttached === 0) {
|
|
2051
|
-
console.warn(`No routes attached to controller ${instance.constructor.name}`);
|
|
2052
|
-
}
|
|
2053
|
-
instance[$isMounted] = true;
|
|
1862
|
+
this.scanControllerRoutes(prefix, controller);
|
|
2054
1863
|
}
|
|
2055
1864
|
return this;
|
|
2056
1865
|
}
|
|
@@ -2088,8 +1897,6 @@ class ShokupanRouter {
|
|
|
2088
1897
|
*/
|
|
2089
1898
|
async internalRequest(arg) {
|
|
2090
1899
|
const options = typeof arg === "string" ? { path: arg } : arg;
|
|
2091
|
-
const store = asyncContext.getStore();
|
|
2092
|
-
store?.get("req");
|
|
2093
1900
|
let url = options.path;
|
|
2094
1901
|
if (!url.startsWith("http")) {
|
|
2095
1902
|
const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig.port || 3e3}`;
|
|
@@ -2132,7 +1939,7 @@ class ShokupanRouter {
|
|
|
2132
1939
|
});
|
|
2133
1940
|
const ctx = new ShokupanContext(req);
|
|
2134
1941
|
let result = null;
|
|
2135
|
-
let status =
|
|
1942
|
+
let status = HTTP_STATUS.OK;
|
|
2136
1943
|
const headers = {};
|
|
2137
1944
|
const match = this.find(req.method, ctx.path);
|
|
2138
1945
|
if (match) {
|
|
@@ -2141,12 +1948,12 @@ class ShokupanRouter {
|
|
|
2141
1948
|
result = await match.handler(ctx);
|
|
2142
1949
|
} catch (err) {
|
|
2143
1950
|
console.error(err);
|
|
2144
|
-
status = err
|
|
1951
|
+
status = getErrorStatus(err);
|
|
2145
1952
|
result = { error: err.message || "Internal Server Error" };
|
|
2146
1953
|
if (err.errors) result.errors = err.errors;
|
|
2147
1954
|
}
|
|
2148
1955
|
} else {
|
|
2149
|
-
status =
|
|
1956
|
+
status = HTTP_STATUS.NOT_FOUND;
|
|
2150
1957
|
result = "Not Found";
|
|
2151
1958
|
}
|
|
2152
1959
|
if (result instanceof Response) {
|
|
@@ -2175,7 +1982,7 @@ class ShokupanRouter {
|
|
|
2175
1982
|
const originalHandler = handler;
|
|
2176
1983
|
const wrapped = async (ctx) => {
|
|
2177
1984
|
await this.runHooks("onRequestStart", ctx);
|
|
2178
|
-
const debug = ctx
|
|
1985
|
+
const debug = ctx[$debug];
|
|
2179
1986
|
let debugId;
|
|
2180
1987
|
let previousNode;
|
|
2181
1988
|
if (debug) {
|
|
@@ -2201,6 +2008,254 @@ class ShokupanRouter {
|
|
|
2201
2008
|
wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
|
|
2202
2009
|
return wrapped;
|
|
2203
2010
|
}
|
|
2011
|
+
mountRouter(prefix, router) {
|
|
2012
|
+
if (router[$isMounted]) {
|
|
2013
|
+
throw new Error("Router is already mounted");
|
|
2014
|
+
}
|
|
2015
|
+
router[$mountPath] = prefix;
|
|
2016
|
+
if (!router.metadata) {
|
|
2017
|
+
const info = getCallerInfo();
|
|
2018
|
+
router.metadata = {
|
|
2019
|
+
file: info.file,
|
|
2020
|
+
line: info.line,
|
|
2021
|
+
name: "MountedRouter"
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
this[$childRouters].push(router);
|
|
2025
|
+
router[$parent] = this;
|
|
2026
|
+
const setRouterContext = (router2) => {
|
|
2027
|
+
router2[$appRoot] = this.root;
|
|
2028
|
+
router2[$childRouters].forEach((child) => setRouterContext(child));
|
|
2029
|
+
};
|
|
2030
|
+
setRouterContext(router);
|
|
2031
|
+
router[$appRoot] = this.root;
|
|
2032
|
+
router[$isMounted] = true;
|
|
2033
|
+
}
|
|
2034
|
+
scanControllerRoutes(prefix, controller) {
|
|
2035
|
+
let instance = controller;
|
|
2036
|
+
if (typeof controller === "function") {
|
|
2037
|
+
instance = Container.resolve(controller);
|
|
2038
|
+
const controllerPath = controller[$controllerPath];
|
|
2039
|
+
if (controllerPath) {
|
|
2040
|
+
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2041
|
+
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
2042
|
+
prefix = p1 + p2;
|
|
2043
|
+
if (!prefix) prefix = "/";
|
|
2044
|
+
}
|
|
2045
|
+
} else {
|
|
2046
|
+
const ctor = instance.constructor;
|
|
2047
|
+
const controllerPath = ctor[$controllerPath];
|
|
2048
|
+
if (controllerPath) {
|
|
2049
|
+
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2050
|
+
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
2051
|
+
prefix = p1 + p2;
|
|
2052
|
+
if (!prefix) prefix = "/";
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
instance[$mountPath] = prefix;
|
|
2056
|
+
const info = getCallerInfo();
|
|
2057
|
+
instance.metadata = {
|
|
2058
|
+
file: info.file,
|
|
2059
|
+
line: info.line,
|
|
2060
|
+
name: instance.constructor.name
|
|
2061
|
+
};
|
|
2062
|
+
this[$childControllers].push(instance);
|
|
2063
|
+
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
2064
|
+
const proto = Object.getPrototypeOf(instance);
|
|
2065
|
+
const methods = /* @__PURE__ */ new Set();
|
|
2066
|
+
let current = proto;
|
|
2067
|
+
while (current && current !== Object.prototype) {
|
|
2068
|
+
Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
|
|
2069
|
+
current = Object.getPrototypeOf(current);
|
|
2070
|
+
}
|
|
2071
|
+
Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
|
|
2072
|
+
const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
|
|
2073
|
+
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
2074
|
+
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
2075
|
+
const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
|
|
2076
|
+
let routesAttached = 0;
|
|
2077
|
+
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
2078
|
+
const name = Array.from(methods)[i];
|
|
2079
|
+
if (name === "constructor") continue;
|
|
2080
|
+
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
2081
|
+
const originalHandler = instance[name];
|
|
2082
|
+
if (typeof originalHandler !== "function") continue;
|
|
2083
|
+
let method;
|
|
2084
|
+
let subPath = "";
|
|
2085
|
+
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
2086
|
+
const config = decoratedRoutes.get(name);
|
|
2087
|
+
method = config.method;
|
|
2088
|
+
subPath = config.path;
|
|
2089
|
+
} else {
|
|
2090
|
+
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
2091
|
+
const m = HTTPMethods[j];
|
|
2092
|
+
if (name.toUpperCase().startsWith(m)) {
|
|
2093
|
+
method = m;
|
|
2094
|
+
const rest = name.slice(m.length);
|
|
2095
|
+
if (rest.length === 0) {
|
|
2096
|
+
subPath = "/";
|
|
2097
|
+
} else {
|
|
2098
|
+
subPath = "";
|
|
2099
|
+
let buffer = "";
|
|
2100
|
+
const flush = () => {
|
|
2101
|
+
if (buffer.length > 0) {
|
|
2102
|
+
subPath += "/" + buffer.toLowerCase();
|
|
2103
|
+
buffer = "";
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
2107
|
+
const char = rest[i2];
|
|
2108
|
+
if (char === "$") {
|
|
2109
|
+
flush();
|
|
2110
|
+
subPath += "/:";
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
2113
|
+
buffer += char;
|
|
2114
|
+
}
|
|
2115
|
+
if (buffer.length > 0) flush();
|
|
2116
|
+
subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
|
|
2117
|
+
if (!subPath.startsWith("/")) {
|
|
2118
|
+
subPath = "/" + subPath;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
break;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (method) {
|
|
2126
|
+
routesAttached++;
|
|
2127
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2128
|
+
const cleanSubPath = subPath === "/" ? "" : subPath;
|
|
2129
|
+
let joined;
|
|
2130
|
+
if (cleanSubPath.length === 0) {
|
|
2131
|
+
joined = cleanPrefix;
|
|
2132
|
+
} else if (cleanSubPath.startsWith("/")) {
|
|
2133
|
+
joined = cleanPrefix + cleanSubPath;
|
|
2134
|
+
} else {
|
|
2135
|
+
joined = cleanPrefix + "/" + cleanSubPath;
|
|
2136
|
+
}
|
|
2137
|
+
const fullPath = joined || "/";
|
|
2138
|
+
const normalizedPath = fullPath.replace(/\/+/g, "/");
|
|
2139
|
+
const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
|
|
2140
|
+
const allMiddleware = [...controllerMiddleware, ...methodMw];
|
|
2141
|
+
const routeArgs = decoratedArgs && decoratedArgs.get(name);
|
|
2142
|
+
const wrappedHandler = async (ctx) => {
|
|
2143
|
+
let args = [ctx];
|
|
2144
|
+
if (routeArgs?.length > 0) {
|
|
2145
|
+
args = [];
|
|
2146
|
+
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
2147
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
2148
|
+
const arg = sortedArgs[k];
|
|
2149
|
+
switch (arg.type) {
|
|
2150
|
+
case RouteParamType.BODY:
|
|
2151
|
+
try {
|
|
2152
|
+
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
2153
|
+
args[arg.index] = await ctx.req.json();
|
|
2154
|
+
} else {
|
|
2155
|
+
const text = await ctx.req.text();
|
|
2156
|
+
if (!text) {
|
|
2157
|
+
args[arg.index] = {};
|
|
2158
|
+
} else {
|
|
2159
|
+
args[arg.index] = JSON.parse(text);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
} catch (e) {
|
|
2163
|
+
const err = new Error("Invalid JSON body");
|
|
2164
|
+
err.status = 400;
|
|
2165
|
+
throw err;
|
|
2166
|
+
}
|
|
2167
|
+
break;
|
|
2168
|
+
case RouteParamType.PARAM:
|
|
2169
|
+
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
2170
|
+
break;
|
|
2171
|
+
case RouteParamType.QUERY: {
|
|
2172
|
+
const url = new URL(ctx.req.url);
|
|
2173
|
+
if (arg.name) {
|
|
2174
|
+
const vals = url.searchParams.getAll(arg.name);
|
|
2175
|
+
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
2176
|
+
} else {
|
|
2177
|
+
const query = {};
|
|
2178
|
+
const keys = Object.keys(url.searchParams);
|
|
2179
|
+
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
2180
|
+
const key = keys[k2];
|
|
2181
|
+
const vals = url.searchParams.getAll(key);
|
|
2182
|
+
query[key] = vals.length > 1 ? vals : vals[0];
|
|
2183
|
+
}
|
|
2184
|
+
args[arg.index] = query;
|
|
2185
|
+
}
|
|
2186
|
+
break;
|
|
2187
|
+
}
|
|
2188
|
+
case RouteParamType.HEADER:
|
|
2189
|
+
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2190
|
+
break;
|
|
2191
|
+
case RouteParamType.REQUEST:
|
|
2192
|
+
args[arg.index] = ctx.req;
|
|
2193
|
+
break;
|
|
2194
|
+
case RouteParamType.CONTEXT:
|
|
2195
|
+
args[arg.index] = ctx;
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
|
|
2201
|
+
return tracedOriginalHandler.apply(instance, args);
|
|
2202
|
+
};
|
|
2203
|
+
let finalHandler = wrappedHandler;
|
|
2204
|
+
if (allMiddleware.length > 0) {
|
|
2205
|
+
const composed = compose(allMiddleware);
|
|
2206
|
+
finalHandler = async (ctx) => {
|
|
2207
|
+
return composed(ctx, () => wrappedHandler(ctx));
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
finalHandler.originalHandler = originalHandler;
|
|
2211
|
+
if (finalHandler !== wrappedHandler) {
|
|
2212
|
+
wrappedHandler.originalHandler = originalHandler;
|
|
2213
|
+
}
|
|
2214
|
+
const tagName = instance.constructor.name;
|
|
2215
|
+
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2216
|
+
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2217
|
+
const spec = { tags: [tagName], ...userSpec };
|
|
2218
|
+
this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
|
|
2219
|
+
}
|
|
2220
|
+
if (decoratedEvents?.has(name)) {
|
|
2221
|
+
routesAttached++;
|
|
2222
|
+
const config = decoratedEvents.get(name);
|
|
2223
|
+
const routeArgs = decoratedArgs?.get(name);
|
|
2224
|
+
const wrappedHandler = async (ctx) => {
|
|
2225
|
+
let args = [ctx];
|
|
2226
|
+
if (routeArgs?.length > 0) {
|
|
2227
|
+
args = [];
|
|
2228
|
+
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
2229
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
2230
|
+
const arg = sortedArgs[k];
|
|
2231
|
+
switch (arg.type) {
|
|
2232
|
+
case RouteParamType.BODY:
|
|
2233
|
+
args[arg.index] = await ctx.body();
|
|
2234
|
+
break;
|
|
2235
|
+
case RouteParamType.CONTEXT:
|
|
2236
|
+
args[arg.index] = ctx;
|
|
2237
|
+
break;
|
|
2238
|
+
case RouteParamType.REQUEST:
|
|
2239
|
+
args[arg.index] = ctx.req;
|
|
2240
|
+
break;
|
|
2241
|
+
case RouteParamType.HEADER:
|
|
2242
|
+
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2243
|
+
break;
|
|
2244
|
+
default:
|
|
2245
|
+
args[arg.index] = void 0;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return originalHandler.apply(instance, args);
|
|
2250
|
+
};
|
|
2251
|
+
this.event(config.eventName, wrappedHandler);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
if (routesAttached === 0) {
|
|
2255
|
+
console.warn(`No routes attached to controller ${instance.constructor.name}`);
|
|
2256
|
+
}
|
|
2257
|
+
instance[$isMounted] = true;
|
|
2258
|
+
}
|
|
2204
2259
|
/**
|
|
2205
2260
|
* Find a route matching the given method and path.
|
|
2206
2261
|
* @param method HTTP method
|
|
@@ -2327,7 +2382,7 @@ class ShokupanRouter {
|
|
|
2327
2382
|
if (effectiveRenderer) {
|
|
2328
2383
|
const innerHandler = wrappedHandler;
|
|
2329
2384
|
wrappedHandler = async (ctx) => {
|
|
2330
|
-
ctx.
|
|
2385
|
+
ctx.setRenderer(effectiveRenderer);
|
|
2331
2386
|
return innerHandler(ctx);
|
|
2332
2387
|
};
|
|
2333
2388
|
}
|
|
@@ -2358,8 +2413,10 @@ class ShokupanRouter {
|
|
|
2358
2413
|
Promise.resolve().then(async () => {
|
|
2359
2414
|
try {
|
|
2360
2415
|
const timestamp = Date.now();
|
|
2361
|
-
|
|
2362
|
-
|
|
2416
|
+
await datastore.set(new surrealdb.RecordId("middleware_tracking", {
|
|
2417
|
+
timestamp,
|
|
2418
|
+
name: handler.name || "anonymous"
|
|
2419
|
+
}), {
|
|
2363
2420
|
name: handler.name || "anonymous",
|
|
2364
2421
|
path: ctx.path,
|
|
2365
2422
|
timestamp,
|
|
@@ -2377,7 +2434,7 @@ class ShokupanRouter {
|
|
|
2377
2434
|
const cutoff = Date.now() - ttl;
|
|
2378
2435
|
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2379
2436
|
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2380
|
-
if (results
|
|
2437
|
+
if (results?.[0]?.count > maxCapacity) {
|
|
2381
2438
|
const toDelete = results[0].count - maxCapacity;
|
|
2382
2439
|
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2383
2440
|
}
|
|
@@ -2454,7 +2511,7 @@ class ShokupanRouter {
|
|
|
2454
2511
|
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
2455
2512
|
);
|
|
2456
2513
|
if (callerLine) {
|
|
2457
|
-
const match = callerLine.match(/\((
|
|
2514
|
+
const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
|
|
2458
2515
|
if (match) {
|
|
2459
2516
|
file = match[1];
|
|
2460
2517
|
line = parseInt(match[2], 10);
|
|
@@ -2472,7 +2529,7 @@ class ShokupanRouter {
|
|
|
2472
2529
|
}
|
|
2473
2530
|
return guardHandler(ctx, next);
|
|
2474
2531
|
};
|
|
2475
|
-
trackedGuard.originalHandler = guardHandler.originalHandler
|
|
2532
|
+
trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
|
|
2476
2533
|
this.currentGuards.push({ handler: trackedGuard, spec });
|
|
2477
2534
|
return this;
|
|
2478
2535
|
}
|
|
@@ -2585,7 +2642,7 @@ class ShokupanRouter {
|
|
|
2585
2642
|
const fns = this.hookCache.get(name);
|
|
2586
2643
|
if (!fns) return;
|
|
2587
2644
|
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2588
|
-
const debug = ctx?.
|
|
2645
|
+
const debug = ctx?.[$debug];
|
|
2589
2646
|
if (debug) {
|
|
2590
2647
|
await Promise.all(fns.map(async (fn, index) => {
|
|
2591
2648
|
const hookId = `hook_${name}_${fn.name || index}`;
|
|
@@ -2658,6 +2715,7 @@ const defaults = {
|
|
|
2658
2715
|
hostname: "localhost",
|
|
2659
2716
|
development: process.env.NODE_ENV !== "production",
|
|
2660
2717
|
enableAsyncLocalStorage: false,
|
|
2718
|
+
enableHttpBridge: false,
|
|
2661
2719
|
reusePort: false
|
|
2662
2720
|
};
|
|
2663
2721
|
api.trace.getTracer("shokupan.application");
|
|
@@ -2666,6 +2724,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2666
2724
|
openApiSpec;
|
|
2667
2725
|
composedMiddleware;
|
|
2668
2726
|
cpuMonitor;
|
|
2727
|
+
server;
|
|
2669
2728
|
get logger() {
|
|
2670
2729
|
return this.applicationConfig.logger;
|
|
2671
2730
|
}
|
|
@@ -2729,6 +2788,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2729
2788
|
}
|
|
2730
2789
|
return this;
|
|
2731
2790
|
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Registers a plugin.
|
|
2793
|
+
*/
|
|
2794
|
+
register(plugin, options) {
|
|
2795
|
+
plugin.onInit(this, options);
|
|
2796
|
+
return this;
|
|
2797
|
+
}
|
|
2732
2798
|
startupHooks = [];
|
|
2733
2799
|
/**
|
|
2734
2800
|
* Registers a callback to be executed before the server starts listening.
|
|
@@ -2767,6 +2833,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2767
2833
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
2768
2834
|
this.cpuMonitor.start();
|
|
2769
2835
|
}
|
|
2836
|
+
const self = this;
|
|
2770
2837
|
const serveOptions = {
|
|
2771
2838
|
port: finalPort,
|
|
2772
2839
|
hostname: this.applicationConfig.hostname,
|
|
@@ -2778,8 +2845,61 @@ class Shokupan extends ShokupanRouter {
|
|
|
2778
2845
|
open(ws) {
|
|
2779
2846
|
ws.data?.handler?.open?.(ws);
|
|
2780
2847
|
},
|
|
2781
|
-
message(ws, message) {
|
|
2782
|
-
ws.data?.handler?.message
|
|
2848
|
+
async message(ws, message) {
|
|
2849
|
+
if (ws.data?.handler?.message) {
|
|
2850
|
+
return ws.data.handler.message(ws, message);
|
|
2851
|
+
}
|
|
2852
|
+
if (typeof message !== "string") return;
|
|
2853
|
+
try {
|
|
2854
|
+
const payload = JSON.parse(message);
|
|
2855
|
+
if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
|
|
2856
|
+
const { id, method, path: path2, headers, body } = payload;
|
|
2857
|
+
const url = new URL(path2, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
|
|
2858
|
+
const req = new Request(url.toString(), {
|
|
2859
|
+
method,
|
|
2860
|
+
headers,
|
|
2861
|
+
body: typeof body === "object" ? JSON.stringify(body) : body
|
|
2862
|
+
});
|
|
2863
|
+
const res = await self.fetch(req);
|
|
2864
|
+
const resBody = await res.json().catch((err) => res.text());
|
|
2865
|
+
const resHeaders = {};
|
|
2866
|
+
res.headers.forEach((v, k) => resHeaders[k] = v);
|
|
2867
|
+
ws.send(JSON.stringify({
|
|
2868
|
+
type: "RESPONSE",
|
|
2869
|
+
id,
|
|
2870
|
+
status: res.status,
|
|
2871
|
+
headers: resHeaders,
|
|
2872
|
+
body: resBody
|
|
2873
|
+
}));
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
|
|
2877
|
+
if (eventName) {
|
|
2878
|
+
const handlers = self.findEvent(eventName);
|
|
2879
|
+
const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
|
|
2880
|
+
if (handler) {
|
|
2881
|
+
const data = payload.data || payload.payload;
|
|
2882
|
+
const req = new ShokupanRequest({
|
|
2883
|
+
url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
|
|
2884
|
+
method: "POST",
|
|
2885
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
2886
|
+
body: JSON.stringify(data)
|
|
2887
|
+
});
|
|
2888
|
+
const ctx = new ShokupanContext(req, self.server);
|
|
2889
|
+
ctx[$ws] = ws;
|
|
2890
|
+
try {
|
|
2891
|
+
await handler(ctx);
|
|
2892
|
+
} catch (err) {
|
|
2893
|
+
if (self.applicationConfig["websocketErrorHandler"]) {
|
|
2894
|
+
await self.applicationConfig["websocketErrorHandler"](err, ctx);
|
|
2895
|
+
} else {
|
|
2896
|
+
console.error(`Error in event ${eventName}:`, err);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
} catch (e) {
|
|
2902
|
+
}
|
|
2783
2903
|
},
|
|
2784
2904
|
drain(ws) {
|
|
2785
2905
|
ws.data?.handler?.drain?.(ws);
|
|
@@ -2791,12 +2911,40 @@ class Shokupan extends ShokupanRouter {
|
|
|
2791
2911
|
};
|
|
2792
2912
|
let factory = this.applicationConfig.serverFactory;
|
|
2793
2913
|
if (!factory && typeof Bun === "undefined") {
|
|
2794
|
-
const { createHttpServer } = await Promise.resolve().then(() => require("./server-
|
|
2914
|
+
const { createHttpServer } = await Promise.resolve().then(() => require("./http-server-BEMPIs33.cjs"));
|
|
2795
2915
|
factory = createHttpServer();
|
|
2796
2916
|
}
|
|
2797
|
-
|
|
2917
|
+
this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
2798
2918
|
console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
|
|
2799
|
-
return server;
|
|
2919
|
+
return this.server;
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* Stops the application server.
|
|
2923
|
+
*
|
|
2924
|
+
* This method gracefully shuts down the server and stops any running monitors.
|
|
2925
|
+
* Works transparently in both Bun and Node.js runtimes.
|
|
2926
|
+
*
|
|
2927
|
+
* @returns A promise that resolves when the server has been stopped.
|
|
2928
|
+
*
|
|
2929
|
+
* @example
|
|
2930
|
+
* ```typescript
|
|
2931
|
+
* const app = new Shokupan();
|
|
2932
|
+
* const server = await app.listen(3000);
|
|
2933
|
+
*
|
|
2934
|
+
* // Later, when you want to stop the server
|
|
2935
|
+
* await app.stop();
|
|
2936
|
+
* ```
|
|
2937
|
+
* @param closeActiveConnections — Immediately terminate in-flight requests, websockets, and stop accepting new connections.
|
|
2938
|
+
*/
|
|
2939
|
+
async stop(closeActiveConnections) {
|
|
2940
|
+
if (this.cpuMonitor) {
|
|
2941
|
+
this.cpuMonitor.stop();
|
|
2942
|
+
this.cpuMonitor = void 0;
|
|
2943
|
+
}
|
|
2944
|
+
if (this.server) {
|
|
2945
|
+
await this.server.stop(closeActiveConnections);
|
|
2946
|
+
this.server = void 0;
|
|
2947
|
+
}
|
|
2800
2948
|
}
|
|
2801
2949
|
[$dispatch](req) {
|
|
2802
2950
|
return this.fetch(req);
|
|
@@ -2860,19 +3008,19 @@ class Shokupan extends ShokupanRouter {
|
|
|
2860
3008
|
"http.method": req.method
|
|
2861
3009
|
}
|
|
2862
3010
|
};
|
|
2863
|
-
const parent = store?.
|
|
3011
|
+
const parent = store?.span;
|
|
2864
3012
|
const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
|
|
2865
3013
|
return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
|
|
2866
|
-
const
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
return asyncContext.run(
|
|
3014
|
+
const ctxStore = new RequestContextStore();
|
|
3015
|
+
ctxStore.span = span;
|
|
3016
|
+
ctxStore.request = req;
|
|
3017
|
+
return asyncContext.run(ctxStore, () => this.handleRequest(req, server).finally(() => span.end()));
|
|
2870
3018
|
});
|
|
2871
3019
|
}
|
|
2872
3020
|
if (this.applicationConfig.enableAsyncLocalStorage) {
|
|
2873
|
-
const
|
|
2874
|
-
|
|
2875
|
-
return asyncContext.run(
|
|
3021
|
+
const ctxStore = new RequestContextStore();
|
|
3022
|
+
ctxStore.request = req;
|
|
3023
|
+
return asyncContext.run(ctxStore, () => this.handleRequest(req, server));
|
|
2876
3024
|
}
|
|
2877
3025
|
return this.handleRequest(req, server);
|
|
2878
3026
|
}
|
|
@@ -2894,24 +3042,34 @@ class Shokupan extends ShokupanRouter {
|
|
|
2894
3042
|
const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
|
|
2895
3043
|
const match = this.find(req.method, ctx.path);
|
|
2896
3044
|
if (match) {
|
|
3045
|
+
ctx[$routeMatched] = true;
|
|
2897
3046
|
ctx.params = match.params;
|
|
2898
3047
|
await bodyParsing;
|
|
2899
3048
|
return match.handler(ctx);
|
|
2900
3049
|
}
|
|
3050
|
+
if (ctx.upgrade()) {
|
|
3051
|
+
return void 0;
|
|
3052
|
+
}
|
|
2901
3053
|
return null;
|
|
2902
3054
|
});
|
|
2903
3055
|
let response;
|
|
2904
3056
|
if (result instanceof Response) {
|
|
2905
3057
|
response = result;
|
|
2906
|
-
} else if ((result === null || result === void 0) && ctx
|
|
2907
|
-
response = ctx
|
|
3058
|
+
} else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
|
|
3059
|
+
response = ctx[$finalResponse];
|
|
2908
3060
|
} else if (result === null || result === void 0) {
|
|
2909
|
-
if (ctx
|
|
2910
|
-
response = ctx
|
|
2911
|
-
} else if (ctx.
|
|
3061
|
+
if (ctx[$finalResponse] instanceof Response) {
|
|
3062
|
+
response = ctx[$finalResponse];
|
|
3063
|
+
} else if (ctx.isUpgraded) {
|
|
3064
|
+
return void 0;
|
|
3065
|
+
} else if (ctx[$routeMatched]) {
|
|
2912
3066
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2913
3067
|
} else {
|
|
2914
|
-
|
|
3068
|
+
if (ctx.response.status !== HTTP_STATUS.OK) {
|
|
3069
|
+
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
3070
|
+
} else {
|
|
3071
|
+
response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
|
|
3072
|
+
}
|
|
2915
3073
|
}
|
|
2916
3074
|
} else if (typeof result === "object") {
|
|
2917
3075
|
response = ctx.json(result);
|
|
@@ -2922,10 +3080,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
2922
3080
|
await this.runHooks("onResponseStart", ctx, response);
|
|
2923
3081
|
return response;
|
|
2924
3082
|
} catch (err) {
|
|
2925
|
-
|
|
2926
|
-
const span = asyncContext.getStore()?.get("span");
|
|
3083
|
+
const span = asyncContext.getStore()?.span;
|
|
2927
3084
|
if (span) span.setStatus({ code: 2 });
|
|
2928
|
-
const status = err
|
|
3085
|
+
const status = getErrorStatus(err);
|
|
2929
3086
|
const body = { error: err.message || "Internal Server Error" };
|
|
2930
3087
|
if (err.errors) body.errors = err.errors;
|
|
2931
3088
|
await this.runHooks("onError", ctx, err);
|
|
@@ -2947,16 +3104,188 @@ class Shokupan extends ShokupanRouter {
|
|
|
2947
3104
|
}
|
|
2948
3105
|
return executionPromise.catch((err) => {
|
|
2949
3106
|
if (err.message === "Request Timeout") {
|
|
2950
|
-
return ctx.text("Request Timeout",
|
|
3107
|
+
return ctx.text("Request Timeout", HTTP_STATUS.REQUEST_TIMEOUT);
|
|
2951
3108
|
}
|
|
2952
3109
|
console.error("Unexpected error in request execution:", err);
|
|
2953
|
-
return ctx.text("Internal Server Error",
|
|
3110
|
+
return ctx.text("Internal Server Error", HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
2954
3111
|
}).then(async (res) => {
|
|
2955
3112
|
await this.runHooks("onResponseEnd", ctx, res);
|
|
2956
3113
|
return res;
|
|
2957
3114
|
});
|
|
2958
3115
|
}
|
|
2959
3116
|
}
|
|
3117
|
+
function RateLimitMiddleware(options = {}) {
|
|
3118
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
3119
|
+
const max = options.limit || options.max || 5;
|
|
3120
|
+
const message = options.message || "Too many requests, please try again later.";
|
|
3121
|
+
const statusCode = options.statusCode || 429;
|
|
3122
|
+
const headers = options.headers !== false;
|
|
3123
|
+
const mode = options.mode || "user";
|
|
3124
|
+
const trustedProxies = options.trustedProxies || [];
|
|
3125
|
+
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
3126
|
+
if (mode === "absolute") {
|
|
3127
|
+
return "global";
|
|
3128
|
+
}
|
|
3129
|
+
const xForwardedFor = ctx.headers.get("x-forwarded-for");
|
|
3130
|
+
if (xForwardedFor && trustedProxies.length > 0) {
|
|
3131
|
+
const ips = xForwardedFor.split(",").map((ip) => ip.trim());
|
|
3132
|
+
for (let i = ips.length - 1; i >= 0; i--) {
|
|
3133
|
+
const ip = ips[i];
|
|
3134
|
+
if (!trustedProxies.includes(ip)) {
|
|
3135
|
+
if (/^[\d.:a-fA-F]+$/.test(ip)) {
|
|
3136
|
+
return ip;
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
|
|
3142
|
+
});
|
|
3143
|
+
const skip = options.skip || (() => false);
|
|
3144
|
+
const hits = /* @__PURE__ */ new Map();
|
|
3145
|
+
const interval = setInterval(() => {
|
|
3146
|
+
const now = Date.now();
|
|
3147
|
+
const entries = Array.from(hits.entries());
|
|
3148
|
+
for (let i = 0; i < entries.length; i++) {
|
|
3149
|
+
const [key, record] = entries[i];
|
|
3150
|
+
if (record.resetTime <= now) {
|
|
3151
|
+
hits.delete(key);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
}, windowMs);
|
|
3155
|
+
if (interval.unref) interval.unref();
|
|
3156
|
+
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
3157
|
+
if (skip(ctx)) return next();
|
|
3158
|
+
const key = keyGenerator(ctx);
|
|
3159
|
+
const now = Date.now();
|
|
3160
|
+
let record = hits.get(key);
|
|
3161
|
+
if (!record || record.resetTime <= now) {
|
|
3162
|
+
record = {
|
|
3163
|
+
hits: 0,
|
|
3164
|
+
resetTime: now + windowMs
|
|
3165
|
+
};
|
|
3166
|
+
hits.set(key, record);
|
|
3167
|
+
}
|
|
3168
|
+
record.hits++;
|
|
3169
|
+
const remaining = Math.max(0, max - record.hits);
|
|
3170
|
+
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
3171
|
+
const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
|
|
3172
|
+
const setHeaders = (res) => {
|
|
3173
|
+
if (!headers || !res || !res.headers) return;
|
|
3174
|
+
try {
|
|
3175
|
+
res.headers.set("X-RateLimit-Limit", String(max));
|
|
3176
|
+
res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
3177
|
+
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
3178
|
+
} catch (e) {
|
|
3179
|
+
}
|
|
3180
|
+
};
|
|
3181
|
+
if (record.hits > max) {
|
|
3182
|
+
if (options.onRateLimited) {
|
|
3183
|
+
const result = await options.onRateLimited(ctx, key);
|
|
3184
|
+
if (result instanceof Response) {
|
|
3185
|
+
return result;
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
const msg = typeof message === "function" ? message(ctx, key) : message;
|
|
3189
|
+
typeof msg === "object" ? JSON.stringify(msg) : String(msg);
|
|
3190
|
+
const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
|
|
3191
|
+
if (headers) {
|
|
3192
|
+
setHeaders(res);
|
|
3193
|
+
res.headers.set("Retry-After", String(retryAfter));
|
|
3194
|
+
}
|
|
3195
|
+
return res;
|
|
3196
|
+
}
|
|
3197
|
+
const response = await next();
|
|
3198
|
+
if (response instanceof Response && headers) {
|
|
3199
|
+
setHeaders(response);
|
|
3200
|
+
}
|
|
3201
|
+
return response;
|
|
3202
|
+
};
|
|
3203
|
+
rateLimitMiddleware.isBuiltin = true;
|
|
3204
|
+
rateLimitMiddleware.pluginName = "RateLimit";
|
|
3205
|
+
return rateLimitMiddleware;
|
|
3206
|
+
}
|
|
3207
|
+
function Controller(path2 = "/") {
|
|
3208
|
+
return (target) => {
|
|
3209
|
+
target[$controllerPath] = path2;
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
function Use(...middleware) {
|
|
3213
|
+
return (target, propertyKey, descriptor) => {
|
|
3214
|
+
if (!propertyKey) {
|
|
3215
|
+
const existing = target[$middleware] || [];
|
|
3216
|
+
target[$middleware] = [...existing, ...middleware];
|
|
3217
|
+
} else {
|
|
3218
|
+
if (!target[$middleware]) {
|
|
3219
|
+
target[$middleware] = /* @__PURE__ */ new Map();
|
|
3220
|
+
}
|
|
3221
|
+
const existing = target[$middleware].get(propertyKey) || [];
|
|
3222
|
+
target[$middleware].set(propertyKey, [...existing, ...middleware]);
|
|
3223
|
+
}
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
function createParamDecorator(type) {
|
|
3227
|
+
return (name) => {
|
|
3228
|
+
return (target, propertyKey, parameterIndex) => {
|
|
3229
|
+
if (!target[$routeArgs]) {
|
|
3230
|
+
target[$routeArgs] = /* @__PURE__ */ new Map();
|
|
3231
|
+
}
|
|
3232
|
+
if (!target[$routeArgs].has(propertyKey)) {
|
|
3233
|
+
target[$routeArgs].set(propertyKey, []);
|
|
3234
|
+
}
|
|
3235
|
+
target[$routeArgs].get(propertyKey).push({
|
|
3236
|
+
index: parameterIndex,
|
|
3237
|
+
type,
|
|
3238
|
+
name
|
|
3239
|
+
});
|
|
3240
|
+
};
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
const Body = createParamDecorator(RouteParamType.BODY);
|
|
3244
|
+
const Param = createParamDecorator(RouteParamType.PARAM);
|
|
3245
|
+
const Query = createParamDecorator(RouteParamType.QUERY);
|
|
3246
|
+
const Headers$1 = createParamDecorator(RouteParamType.HEADER);
|
|
3247
|
+
const Req = createParamDecorator(RouteParamType.REQUEST);
|
|
3248
|
+
const Ctx = createParamDecorator(RouteParamType.CONTEXT);
|
|
3249
|
+
function Spec(spec) {
|
|
3250
|
+
return (target, propertyKey, descriptor) => {
|
|
3251
|
+
if (!target[$routeSpec]) {
|
|
3252
|
+
target[$routeSpec] = /* @__PURE__ */ new Map();
|
|
3253
|
+
}
|
|
3254
|
+
target[$routeSpec].set(propertyKey, spec);
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
function createMethodDecorator(method) {
|
|
3258
|
+
return (path2 = "/") => {
|
|
3259
|
+
return (target, propertyKey, descriptor) => {
|
|
3260
|
+
if (!target[$routeMethods]) {
|
|
3261
|
+
target[$routeMethods] = /* @__PURE__ */ new Map();
|
|
3262
|
+
}
|
|
3263
|
+
target[$routeMethods].set(propertyKey, {
|
|
3264
|
+
method,
|
|
3265
|
+
path: path2
|
|
3266
|
+
});
|
|
3267
|
+
};
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
const Get = createMethodDecorator("GET");
|
|
3271
|
+
const Post = createMethodDecorator("POST");
|
|
3272
|
+
const Put = createMethodDecorator("PUT");
|
|
3273
|
+
const Delete = createMethodDecorator("DELETE");
|
|
3274
|
+
const Patch = createMethodDecorator("PATCH");
|
|
3275
|
+
const Options = createMethodDecorator("OPTIONS");
|
|
3276
|
+
const Head = createMethodDecorator("HEAD");
|
|
3277
|
+
const All = createMethodDecorator("ALL");
|
|
3278
|
+
function Event(eventName) {
|
|
3279
|
+
return (target, propertyKey, descriptor) => {
|
|
3280
|
+
target[$eventMethods] ??= /* @__PURE__ */ new Map();
|
|
3281
|
+
target[$eventMethods].set(propertyKey, {
|
|
3282
|
+
eventName
|
|
3283
|
+
});
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
function RateLimit(options) {
|
|
3287
|
+
return Use(RateLimitMiddleware(options));
|
|
3288
|
+
}
|
|
2960
3289
|
class AuthPlugin extends ShokupanRouter {
|
|
2961
3290
|
constructor(authConfig) {
|
|
2962
3291
|
super();
|
|
@@ -2965,6 +3294,13 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2965
3294
|
this.init();
|
|
2966
3295
|
}
|
|
2967
3296
|
secret;
|
|
3297
|
+
onInit(app, options) {
|
|
3298
|
+
if (options?.path) {
|
|
3299
|
+
app.mount(options.path, this);
|
|
3300
|
+
} else {
|
|
3301
|
+
app.mount(options.path ?? "/", this);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
2968
3304
|
getProviderInstance(name, p) {
|
|
2969
3305
|
switch (name) {
|
|
2970
3306
|
case "github":
|
|
@@ -3117,73 +3453,809 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3117
3453
|
provider,
|
|
3118
3454
|
raw: data
|
|
3119
3455
|
};
|
|
3120
|
-
} else if (provider === "auth0" || provider === "okta") {
|
|
3121
|
-
const domain = config.domain.startsWith("http") ? config.domain : `https://${config.domain}`;
|
|
3122
|
-
const endpoint = provider === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
|
|
3123
|
-
const res = await fetch(endpoint, {
|
|
3124
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
3456
|
+
} else if (provider === "auth0" || provider === "okta") {
|
|
3457
|
+
const domain = config.domain.startsWith("http") ? config.domain : `https://${config.domain}`;
|
|
3458
|
+
const endpoint = provider === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
|
|
3459
|
+
const res = await fetch(endpoint, {
|
|
3460
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3461
|
+
});
|
|
3462
|
+
const data = await res.json();
|
|
3463
|
+
user = {
|
|
3464
|
+
id: data.sub,
|
|
3465
|
+
name: data.name,
|
|
3466
|
+
email: data.email,
|
|
3467
|
+
picture: data.picture,
|
|
3468
|
+
provider,
|
|
3469
|
+
raw: data
|
|
3470
|
+
};
|
|
3471
|
+
} else if (provider === "apple") {
|
|
3472
|
+
if (idToken) {
|
|
3473
|
+
const payload = jose__namespace.decodeJwt(idToken);
|
|
3474
|
+
user = {
|
|
3475
|
+
id: payload.sub,
|
|
3476
|
+
email: payload["email"],
|
|
3477
|
+
provider,
|
|
3478
|
+
raw: payload
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
} else if (provider === "oauth2") {
|
|
3482
|
+
if (config.userInfoUrl) {
|
|
3483
|
+
const res = await fetch(config.userInfoUrl, {
|
|
3484
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3485
|
+
});
|
|
3486
|
+
const data = await res.json();
|
|
3487
|
+
user = {
|
|
3488
|
+
id: data.id || data.sub || "unknown",
|
|
3489
|
+
name: data.name,
|
|
3490
|
+
email: data.email,
|
|
3491
|
+
picture: data.picture,
|
|
3492
|
+
provider,
|
|
3493
|
+
raw: data
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
return user;
|
|
3498
|
+
}
|
|
3499
|
+
/**
|
|
3500
|
+
* Middleware to verify JWT
|
|
3501
|
+
*/
|
|
3502
|
+
getMiddleware() {
|
|
3503
|
+
return async (ctx, next) => {
|
|
3504
|
+
const authHeader = ctx.req.headers.get("Authorization");
|
|
3505
|
+
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
3506
|
+
if (!token) {
|
|
3507
|
+
const cookieHeader = ctx.req.headers.get("Cookie");
|
|
3508
|
+
token = cookieHeader?.match(/auth_token=([^;]+)/)?.[1] || null;
|
|
3509
|
+
}
|
|
3510
|
+
if (token) {
|
|
3511
|
+
try {
|
|
3512
|
+
const { payload } = await jose__namespace.jwtVerify(token, this.secret);
|
|
3513
|
+
ctx.user = payload;
|
|
3514
|
+
} catch {
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
return next();
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
class ClusterPlugin {
|
|
3522
|
+
constructor(options = {}) {
|
|
3523
|
+
this.options = options;
|
|
3524
|
+
}
|
|
3525
|
+
onInit(app) {
|
|
3526
|
+
const originalListen = app.listen.bind(app);
|
|
3527
|
+
const { workers = "auto", silent = false, sticky = false } = this.options;
|
|
3528
|
+
const isBun = typeof Bun !== "undefined";
|
|
3529
|
+
const numCPUs = os.cpus().length;
|
|
3530
|
+
const numWorkers = workers === "auto" || workers === -1 ? numCPUs : workers;
|
|
3531
|
+
if (numWorkers <= 1) {
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
app.listen = async (port) => {
|
|
3535
|
+
const finalPort = port ?? app.applicationConfig.port ?? 3e3;
|
|
3536
|
+
if (isBun) {
|
|
3537
|
+
return this.handleBun(app, finalPort, numWorkers, originalListen);
|
|
3538
|
+
} else {
|
|
3539
|
+
return this.handleNode(app, finalPort, numWorkers, originalListen, silent, sticky);
|
|
3540
|
+
}
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
async handleBun(app, port, workers, originalListen) {
|
|
3544
|
+
const workerId = process.env["SHOKUPAN_WORKER_ID"];
|
|
3545
|
+
if (workerId) {
|
|
3546
|
+
app.applicationConfig.reusePort = true;
|
|
3547
|
+
return originalListen(port);
|
|
3548
|
+
}
|
|
3549
|
+
console.log(`[Cluster] Starting ${workers} Bun workers on port ${port}...`);
|
|
3550
|
+
const spawnWorker = (id) => {
|
|
3551
|
+
Bun.spawn([process.argv0, ...process.argv.slice(1)], {
|
|
3552
|
+
env: { ...process.env, SHOKUPAN_WORKER_ID: id },
|
|
3553
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
3554
|
+
onExit(proc, exitCode, signalCode, error) {
|
|
3555
|
+
console.log(`[Cluster] Worker ${id} died (code: ${exitCode}). Restarting...`);
|
|
3556
|
+
spawnWorker(id);
|
|
3557
|
+
}
|
|
3558
|
+
});
|
|
3559
|
+
};
|
|
3560
|
+
for (let i = 0; i < workers; i++) {
|
|
3561
|
+
spawnWorker(process.pid + "_" + i + 1);
|
|
3562
|
+
}
|
|
3563
|
+
setInterval(() => {
|
|
3564
|
+
}, 1e3 * 60 * 60);
|
|
3565
|
+
return {
|
|
3566
|
+
stop: () => {
|
|
3567
|
+
},
|
|
3568
|
+
port
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
async handleNode(app, port, workers, originalListen, silent, sticky) {
|
|
3572
|
+
if (cluster.isPrimary) {
|
|
3573
|
+
console.log(`[Cluster] Master ${process.pid} is running`);
|
|
3574
|
+
const fork = () => cluster.fork(process.env);
|
|
3575
|
+
for (let i = 0; i < workers; i++) {
|
|
3576
|
+
fork();
|
|
3577
|
+
}
|
|
3578
|
+
cluster.on("exit", (worker, code, signal) => {
|
|
3579
|
+
console.log(`[Cluster] Worker ${worker.process.pid} died. Restarting...`);
|
|
3580
|
+
fork();
|
|
3581
|
+
});
|
|
3582
|
+
if (sticky) {
|
|
3583
|
+
const server = net.createServer({ pauseOnConnect: true }, (connection) => {
|
|
3584
|
+
const remote = connection.remoteAddress || "";
|
|
3585
|
+
let hash = 0;
|
|
3586
|
+
for (let i = 0; i < remote.length; i++) {
|
|
3587
|
+
hash = (hash << 5) - hash + remote.charCodeAt(i);
|
|
3588
|
+
hash |= 0;
|
|
3589
|
+
}
|
|
3590
|
+
const index = Math.abs(hash) % workers;
|
|
3591
|
+
const worker = Object.values(cluster.workers)[index];
|
|
3592
|
+
if (worker) {
|
|
3593
|
+
worker.send("sticky-session:connection", connection);
|
|
3594
|
+
} else {
|
|
3595
|
+
connection.end();
|
|
3596
|
+
}
|
|
3597
|
+
});
|
|
3598
|
+
server.listen(port, () => {
|
|
3599
|
+
console.log(`[Cluster] Sticky Load Balancer listening on port ${port}`);
|
|
3600
|
+
});
|
|
3601
|
+
return {
|
|
3602
|
+
close: () => server.close(),
|
|
3603
|
+
port
|
|
3604
|
+
};
|
|
3605
|
+
} else {
|
|
3606
|
+
return {
|
|
3607
|
+
close: () => {
|
|
3608
|
+
},
|
|
3609
|
+
// Master controls
|
|
3610
|
+
port
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
} else {
|
|
3614
|
+
if (sticky) {
|
|
3615
|
+
const server = await originalListen(0);
|
|
3616
|
+
process.on("message", (message, handle) => {
|
|
3617
|
+
if (message !== "sticky-session:connection") return;
|
|
3618
|
+
if (!handle) return;
|
|
3619
|
+
server.emit("connection", handle);
|
|
3620
|
+
handle.resume();
|
|
3621
|
+
});
|
|
3622
|
+
return server;
|
|
3623
|
+
} else {
|
|
3624
|
+
return originalListen(port);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
const INTERVALS = [
|
|
3630
|
+
{ label: "10s", ms: 10 * 1e3 },
|
|
3631
|
+
{ label: "1m", ms: 60 * 1e3 },
|
|
3632
|
+
{ label: "5m", ms: 5 * 60 * 1e3 },
|
|
3633
|
+
{ label: "1h", ms: 60 * 60 * 1e3 },
|
|
3634
|
+
{ label: "2h", ms: 2 * 60 * 60 * 1e3 },
|
|
3635
|
+
{ label: "6h", ms: 6 * 60 * 60 * 1e3 },
|
|
3636
|
+
{ label: "12h", ms: 12 * 60 * 60 * 1e3 },
|
|
3637
|
+
{ label: "1d", ms: 24 * 60 * 60 * 1e3 },
|
|
3638
|
+
{ label: "3d", ms: 3 * 24 * 60 * 60 * 1e3 },
|
|
3639
|
+
{ label: "7d", ms: 7 * 24 * 60 * 60 * 1e3 },
|
|
3640
|
+
{ label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
|
|
3641
|
+
];
|
|
3642
|
+
class MetricsCollector {
|
|
3643
|
+
currentIntervalStart = {};
|
|
3644
|
+
pendingDetails = {};
|
|
3645
|
+
eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
|
|
3646
|
+
timer = null;
|
|
3647
|
+
constructor() {
|
|
3648
|
+
this.eventLoopHistogram.enable();
|
|
3649
|
+
const now = Date.now();
|
|
3650
|
+
INTERVALS.forEach((int) => {
|
|
3651
|
+
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
3652
|
+
this.pendingDetails[int.label] = [];
|
|
3653
|
+
});
|
|
3654
|
+
this.timer = setInterval(() => this.collect(), 1e4);
|
|
3655
|
+
}
|
|
3656
|
+
recordRequest(duration, isError) {
|
|
3657
|
+
INTERVALS.forEach((int) => {
|
|
3658
|
+
this.pendingDetails[int.label].push({ duration, isError });
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3661
|
+
alignTimestamp(ts, intervalMs) {
|
|
3662
|
+
return Math.floor(ts / intervalMs) * intervalMs;
|
|
3663
|
+
}
|
|
3664
|
+
async collect() {
|
|
3665
|
+
try {
|
|
3666
|
+
const now = Date.now();
|
|
3667
|
+
console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
|
|
3668
|
+
for (const int of INTERVALS) {
|
|
3669
|
+
const start = this.currentIntervalStart[int.label];
|
|
3670
|
+
if (now >= start + int.ms) {
|
|
3671
|
+
console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
|
|
3672
|
+
await this.flushInterval(int.label, start, int.ms);
|
|
3673
|
+
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
} catch (error) {
|
|
3677
|
+
console.error("[MetricsCollector] Error in collect():", error);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
async flushInterval(label, timestamp, durationMs) {
|
|
3681
|
+
const reqs = this.pendingDetails[label];
|
|
3682
|
+
console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
|
|
3683
|
+
this.pendingDetails[label] = [];
|
|
3684
|
+
if (reqs.length === 0) {
|
|
3685
|
+
console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
const totalReqs = reqs.length;
|
|
3689
|
+
const errorReqs = reqs.filter((r) => r.isError).length;
|
|
3690
|
+
const successReqs = totalReqs - errorReqs;
|
|
3691
|
+
const duratons = reqs.map((r) => r.duration).sort((a, b) => a - b);
|
|
3692
|
+
const rps = totalReqs / (durationMs / 1e3);
|
|
3693
|
+
const sum = duratons.reduce((a, b) => a + b, 0);
|
|
3694
|
+
const avg = totalReqs > 0 ? sum / totalReqs : 0;
|
|
3695
|
+
const getP = (p) => {
|
|
3696
|
+
if (duratons.length === 0) return 0;
|
|
3697
|
+
const idx = Math.floor(duratons.length * p);
|
|
3698
|
+
return duratons[idx];
|
|
3699
|
+
};
|
|
3700
|
+
const metric = {
|
|
3701
|
+
timestamp,
|
|
3702
|
+
interval: label,
|
|
3703
|
+
cpu: os__namespace.loadavg()[0],
|
|
3704
|
+
// Using load avg for simplicity as per requirements (Load)
|
|
3705
|
+
load: os__namespace.loadavg(),
|
|
3706
|
+
memory: {
|
|
3707
|
+
used: process.memoryUsage().rss,
|
|
3708
|
+
total: os__namespace.totalmem(),
|
|
3709
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
3710
|
+
heapTotal: process.memoryUsage().heapTotal
|
|
3711
|
+
},
|
|
3712
|
+
eventLoopLatency: {
|
|
3713
|
+
min: this.eventLoopHistogram.min / 1e6,
|
|
3714
|
+
max: this.eventLoopHistogram.max / 1e6,
|
|
3715
|
+
mean: this.eventLoopHistogram.mean / 1e6,
|
|
3716
|
+
p50: this.eventLoopHistogram.percentile(50) / 1e6,
|
|
3717
|
+
p95: this.eventLoopHistogram.percentile(95) / 1e6,
|
|
3718
|
+
p99: this.eventLoopHistogram.percentile(99) / 1e6
|
|
3719
|
+
},
|
|
3720
|
+
requests: {
|
|
3721
|
+
total: totalReqs,
|
|
3722
|
+
rps,
|
|
3723
|
+
success: successReqs,
|
|
3724
|
+
error: errorReqs
|
|
3725
|
+
},
|
|
3726
|
+
responseTime: {
|
|
3727
|
+
min: duratons[0] || 0,
|
|
3728
|
+
max: duratons[duratons.length - 1] || 0,
|
|
3729
|
+
avg,
|
|
3730
|
+
p50: getP(0.5),
|
|
3731
|
+
p95: getP(0.95),
|
|
3732
|
+
p99: getP(0.99)
|
|
3733
|
+
}
|
|
3734
|
+
};
|
|
3735
|
+
console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
|
|
3736
|
+
try {
|
|
3737
|
+
const recordId = new surrealdb.RecordId("metrics", timestamp);
|
|
3738
|
+
await datastore.set(recordId, metric);
|
|
3739
|
+
console.log(`[MetricsCollector] ✓ Successfully saved ${label} metric to datastore`);
|
|
3740
|
+
const test = await datastore.get(recordId);
|
|
3741
|
+
console.log(`[MetricsCollector] DEBUG: Immediate .get() returned:`, test ? "DATA" : "NULL");
|
|
3742
|
+
const queryTest = await datastore.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
3743
|
+
console.log(`[MetricsCollector] DEBUG: Query by id returned ${queryTest[0]?.length || 0} records`);
|
|
3744
|
+
} catch (e) {
|
|
3745
|
+
console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
// Cleanup if needed
|
|
3749
|
+
stop() {
|
|
3750
|
+
if (this.timer) clearInterval(this.timer);
|
|
3751
|
+
this.eventLoopHistogram.disable();
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
class Collector {
|
|
3755
|
+
constructor(dashboard) {
|
|
3756
|
+
this.dashboard = dashboard;
|
|
3757
|
+
}
|
|
3758
|
+
currentNode;
|
|
3759
|
+
trackStep(id, type, duration, status, error) {
|
|
3760
|
+
if (!id) return;
|
|
3761
|
+
this.dashboard.recordNodeMetric(id, type, duration, status === "error");
|
|
3762
|
+
}
|
|
3763
|
+
trackEdge(fromId, toId) {
|
|
3764
|
+
if (!fromId || !toId) return;
|
|
3765
|
+
this.dashboard.recordEdgeMetric(fromId, toId);
|
|
3766
|
+
}
|
|
3767
|
+
setNode(id) {
|
|
3768
|
+
this.currentNode = id;
|
|
3769
|
+
}
|
|
3770
|
+
getCurrentNode() {
|
|
3771
|
+
return this.currentNode;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
class Dashboard {
|
|
3775
|
+
constructor(dashboardConfig = {}) {
|
|
3776
|
+
this.dashboardConfig = dashboardConfig;
|
|
3777
|
+
}
|
|
3778
|
+
static __dirname = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
|
|
3779
|
+
// Get base path for dashboard files - works in both dev (src/) and production (dist/)
|
|
3780
|
+
static getBasePath() {
|
|
3781
|
+
const dir = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
|
|
3782
|
+
if (dir.endsWith("dist")) {
|
|
3783
|
+
return dir + "/plugins/application/dashboard";
|
|
3784
|
+
}
|
|
3785
|
+
return dir;
|
|
3786
|
+
}
|
|
3787
|
+
router = new ShokupanRouter();
|
|
3788
|
+
metrics = {
|
|
3789
|
+
totalRequests: 0,
|
|
3790
|
+
successfulRequests: 0,
|
|
3791
|
+
failedRequests: 0,
|
|
3792
|
+
activeRequests: 0,
|
|
3793
|
+
averageTotalTime_ms: 0,
|
|
3794
|
+
recentTimings: [],
|
|
3795
|
+
logs: [],
|
|
3796
|
+
rateLimitedCounts: {},
|
|
3797
|
+
nodeMetrics: {},
|
|
3798
|
+
edgeMetrics: {}
|
|
3799
|
+
};
|
|
3800
|
+
eta = new eta$2.Eta({
|
|
3801
|
+
views: Dashboard.getBasePath() + "/static",
|
|
3802
|
+
cache: false
|
|
3803
|
+
});
|
|
3804
|
+
startTime = Date.now();
|
|
3805
|
+
instrumented = false;
|
|
3806
|
+
metricsCollector = new MetricsCollector();
|
|
3807
|
+
// ShokupanPlugin interface implementation
|
|
3808
|
+
onInit(app, options) {
|
|
3809
|
+
this[$appRoot] = app;
|
|
3810
|
+
const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
3811
|
+
const hooks = this.getHooks();
|
|
3812
|
+
if (!app.middleware) {
|
|
3813
|
+
app.middleware = [];
|
|
3814
|
+
}
|
|
3815
|
+
const hooksMiddleware = async (ctx, next) => {
|
|
3816
|
+
if (hooks.onRequestStart) {
|
|
3817
|
+
await hooks.onRequestStart(ctx);
|
|
3818
|
+
}
|
|
3819
|
+
await next();
|
|
3820
|
+
if (hooks.onResponseEnd) {
|
|
3821
|
+
const effectiveResponse = ctx._finalResponse || ctx.response || {};
|
|
3822
|
+
await hooks.onResponseEnd(ctx, effectiveResponse);
|
|
3823
|
+
}
|
|
3824
|
+
};
|
|
3825
|
+
app.use(hooksMiddleware);
|
|
3826
|
+
app.mount(mountPath, this.router);
|
|
3827
|
+
this.setupRoutes();
|
|
3828
|
+
}
|
|
3829
|
+
setupRoutes() {
|
|
3830
|
+
this.router.get("/metrics", async (ctx) => {
|
|
3831
|
+
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
3832
|
+
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
3833
|
+
const interval = ctx.query["interval"];
|
|
3834
|
+
if (interval) {
|
|
3835
|
+
const intervalMap = {
|
|
3836
|
+
"10s": 10 * 1e3,
|
|
3837
|
+
"1m": 60 * 1e3,
|
|
3838
|
+
"5m": 5 * 60 * 1e3,
|
|
3839
|
+
"30m": 30 * 60 * 1e3,
|
|
3840
|
+
"1h": 60 * 60 * 1e3,
|
|
3841
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
3842
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
3843
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
3844
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
3845
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
3846
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
3847
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
3848
|
+
};
|
|
3849
|
+
const ms = intervalMap[interval] || 60 * 1e3;
|
|
3850
|
+
const startTime = Date.now() - ms;
|
|
3851
|
+
let stats;
|
|
3852
|
+
try {
|
|
3853
|
+
stats = await datastore.query(`
|
|
3854
|
+
SELECT
|
|
3855
|
+
count() as total,
|
|
3856
|
+
count(IF status < 400 THEN 1 END) as success,
|
|
3857
|
+
count(IF status >= 400 THEN 1 END) as failed,
|
|
3858
|
+
math::mean(duration) as avg_latency
|
|
3859
|
+
FROM requests
|
|
3860
|
+
WHERE timestamp >= $start
|
|
3861
|
+
GROUP ALL
|
|
3862
|
+
`, { start: startTime });
|
|
3863
|
+
} catch (error) {
|
|
3864
|
+
console.error("[Dashboard] Query failed at plugin.ts:180-191", {
|
|
3865
|
+
error,
|
|
3866
|
+
interval,
|
|
3867
|
+
startTime,
|
|
3868
|
+
query: "metrics interval stats",
|
|
3869
|
+
stack: new Error().stack
|
|
3870
|
+
});
|
|
3871
|
+
throw error;
|
|
3872
|
+
}
|
|
3873
|
+
const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
|
|
3874
|
+
return ctx.json({
|
|
3875
|
+
metrics: {
|
|
3876
|
+
totalRequests: s.total || 0,
|
|
3877
|
+
successfulRequests: s.success || 0,
|
|
3878
|
+
failedRequests: s.failed || 0,
|
|
3879
|
+
activeRequests: this.metrics.activeRequests,
|
|
3880
|
+
averageTotalTime_ms: s.avg_latency || 0,
|
|
3881
|
+
recentTimings: this.metrics.recentTimings,
|
|
3882
|
+
logs: [],
|
|
3883
|
+
rateLimitedCounts: this.metrics.rateLimitedCounts,
|
|
3884
|
+
nodeMetrics: this.metrics.nodeMetrics,
|
|
3885
|
+
edgeMetrics: this.metrics.edgeMetrics
|
|
3886
|
+
},
|
|
3887
|
+
uptime
|
|
3888
|
+
});
|
|
3889
|
+
}
|
|
3890
|
+
return ctx.json({
|
|
3891
|
+
metrics: this.metrics,
|
|
3892
|
+
uptime
|
|
3893
|
+
});
|
|
3894
|
+
});
|
|
3895
|
+
this.router.get("/metrics/history", async (ctx) => {
|
|
3896
|
+
const interval = ctx.query["interval"] || "1m";
|
|
3897
|
+
const intervalMap = {
|
|
3898
|
+
"10s": 10 * 1e3,
|
|
3899
|
+
"1m": 60 * 1e3,
|
|
3900
|
+
"5m": 5 * 60 * 1e3,
|
|
3901
|
+
"30m": 30 * 60 * 1e3,
|
|
3902
|
+
"1h": 60 * 60 * 1e3,
|
|
3903
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
3904
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
3905
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
3906
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
3907
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
3908
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
3909
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
3910
|
+
};
|
|
3911
|
+
const periodMs = intervalMap[interval] || 60 * 1e3;
|
|
3912
|
+
const startTime = Date.now() - periodMs * 3;
|
|
3913
|
+
const endTime = Date.now();
|
|
3914
|
+
const result = await datastore.query(
|
|
3915
|
+
"SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
|
|
3916
|
+
{ start: startTime, end: endTime, interval }
|
|
3917
|
+
);
|
|
3918
|
+
return ctx.json({
|
|
3919
|
+
metrics: result[0] || []
|
|
3125
3920
|
});
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3921
|
+
});
|
|
3922
|
+
const getIntervalStartTime = (interval) => {
|
|
3923
|
+
if (!interval) return 0;
|
|
3924
|
+
const intervalMap = {
|
|
3925
|
+
"10s": 10 * 1e3,
|
|
3926
|
+
"1m": 60 * 1e3,
|
|
3927
|
+
"5m": 5 * 60 * 1e3,
|
|
3928
|
+
"30m": 30 * 60 * 1e3,
|
|
3929
|
+
"1h": 60 * 60 * 1e3,
|
|
3930
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
3931
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
3932
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
3933
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
3934
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
3935
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
3936
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
3134
3937
|
};
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3938
|
+
const ms = intervalMap[interval] || 0;
|
|
3939
|
+
return ms ? Date.now() - ms : 0;
|
|
3940
|
+
};
|
|
3941
|
+
this.router.get("/requests/top", async (ctx) => {
|
|
3942
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3943
|
+
const result = await datastore.query(
|
|
3944
|
+
"SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
3945
|
+
{ start: startTime }
|
|
3946
|
+
);
|
|
3947
|
+
return ctx.json({ top: result[0] || [] });
|
|
3948
|
+
});
|
|
3949
|
+
this.router.get("/errors/top", async (ctx) => {
|
|
3950
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3951
|
+
const result = await datastore.query(
|
|
3952
|
+
"SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
|
|
3953
|
+
{ start: startTime }
|
|
3954
|
+
);
|
|
3955
|
+
return ctx.json({ top: result[0] || [] });
|
|
3956
|
+
});
|
|
3957
|
+
this.router.get("/requests/failing", async (ctx) => {
|
|
3958
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3959
|
+
const result = await datastore.query(
|
|
3960
|
+
"SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
3961
|
+
{ start: startTime }
|
|
3962
|
+
);
|
|
3963
|
+
return ctx.json({ top: result[0] || [] });
|
|
3964
|
+
});
|
|
3965
|
+
this.router.get("/requests/slowest", async (ctx) => {
|
|
3966
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3967
|
+
const result = await datastore.query(
|
|
3968
|
+
"SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
|
|
3969
|
+
{ start: startTime }
|
|
3970
|
+
);
|
|
3971
|
+
return ctx.json({ slowest: result[0] || [] });
|
|
3972
|
+
});
|
|
3973
|
+
this.router.get("/registry", (ctx) => {
|
|
3974
|
+
const app = this[$appRoot];
|
|
3975
|
+
if (!this.instrumented && app) {
|
|
3976
|
+
this.instrumentApp(app);
|
|
3144
3977
|
}
|
|
3145
|
-
|
|
3146
|
-
if (
|
|
3147
|
-
|
|
3148
|
-
|
|
3978
|
+
const registry = app?.getComponentRegistry?.();
|
|
3979
|
+
if (registry) {
|
|
3980
|
+
this.assignIdsToRegistry(registry, "root");
|
|
3981
|
+
}
|
|
3982
|
+
return ctx.json({ registry: registry || {} });
|
|
3983
|
+
});
|
|
3984
|
+
this.router.get("/requests", async (ctx) => {
|
|
3985
|
+
const result = await datastore.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
|
|
3986
|
+
return ctx.json({ requests: result[0] || [] });
|
|
3987
|
+
});
|
|
3988
|
+
this.router.get("/requests/:id", async (ctx) => {
|
|
3989
|
+
const result = await datastore.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
|
|
3990
|
+
return ctx.json({ request: result[0]?.[0] });
|
|
3991
|
+
});
|
|
3992
|
+
this.router.get("/failures", async (ctx) => {
|
|
3993
|
+
const result = await datastore.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
|
|
3994
|
+
return ctx.json({ failures: result[0] });
|
|
3995
|
+
});
|
|
3996
|
+
this.router.post("/replay", async (ctx) => {
|
|
3997
|
+
const body = await ctx.body();
|
|
3998
|
+
const app = this[$appRoot];
|
|
3999
|
+
if (!app) return unknownError(ctx);
|
|
4000
|
+
try {
|
|
4001
|
+
const result = await app.processRequest({
|
|
4002
|
+
method: body.method,
|
|
4003
|
+
path: body.url,
|
|
4004
|
+
// or path
|
|
4005
|
+
headers: body.headers,
|
|
4006
|
+
body: body.body
|
|
3149
4007
|
});
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
raw: data
|
|
3158
|
-
};
|
|
4008
|
+
return ctx.json({
|
|
4009
|
+
status: result.status,
|
|
4010
|
+
headers: result.headers,
|
|
4011
|
+
data: result.data
|
|
4012
|
+
});
|
|
4013
|
+
} catch (e) {
|
|
4014
|
+
return ctx.json({ error: String(e) }, 500);
|
|
3159
4015
|
}
|
|
4016
|
+
});
|
|
4017
|
+
this.router.get("/", async (ctx) => {
|
|
4018
|
+
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
4019
|
+
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
4020
|
+
const linkPattern = this.getLinkPattern();
|
|
4021
|
+
const template = await promises.readFile(Dashboard.getBasePath() + "/template.eta", "utf8");
|
|
4022
|
+
return ctx.html(this.eta.renderString(template, {
|
|
4023
|
+
metrics: this.metrics,
|
|
4024
|
+
uptime,
|
|
4025
|
+
rootPath: process.cwd(),
|
|
4026
|
+
linkPattern,
|
|
4027
|
+
headers: this.dashboardConfig.getRequestHeaders?.()
|
|
4028
|
+
}));
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
instrumentApp(app) {
|
|
4032
|
+
if (!app.getComponentRegistry) return;
|
|
4033
|
+
const registry = app.getComponentRegistry();
|
|
4034
|
+
this.assignIdsToRegistry(registry, "root");
|
|
4035
|
+
this.instrumented = true;
|
|
4036
|
+
}
|
|
4037
|
+
// Traverses registry, generates IDs, and attaches them to the actual function objects
|
|
4038
|
+
assignIdsToRegistry(node, parentId) {
|
|
4039
|
+
if (!node) return;
|
|
4040
|
+
const makeId = (type, parent, idx, name) => `${type}_${parent}_${idx}_${name.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
4041
|
+
node.middleware?.forEach((mw, idx) => {
|
|
4042
|
+
const id = makeId("mw", parentId, idx, mw.name);
|
|
4043
|
+
mw.id = id;
|
|
4044
|
+
if (mw._fn) mw._fn._debugId = id;
|
|
4045
|
+
});
|
|
4046
|
+
node.controllers?.forEach((ctrl, idx) => {
|
|
4047
|
+
const id = makeId("ctrl", parentId, idx, ctrl.name);
|
|
4048
|
+
ctrl.id = id;
|
|
4049
|
+
});
|
|
4050
|
+
node.routes?.forEach((r, idx) => {
|
|
4051
|
+
const id = makeId("route", parentId, idx, r.handlerName || "handler");
|
|
4052
|
+
r.id = id;
|
|
4053
|
+
if (r._fn) r._fn._debugId = id;
|
|
4054
|
+
});
|
|
4055
|
+
node.routers?.forEach((r, idx) => {
|
|
4056
|
+
const id = makeId("router", parentId, idx, r.path);
|
|
4057
|
+
r.id = id;
|
|
4058
|
+
this.assignIdsToRegistry(r.children, id);
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
recordNodeMetric(id, type, duration, isError) {
|
|
4062
|
+
if (!this.metrics.nodeMetrics[id]) {
|
|
4063
|
+
this.metrics.nodeMetrics[id] = {
|
|
4064
|
+
id,
|
|
4065
|
+
type,
|
|
4066
|
+
requests: 0,
|
|
4067
|
+
totalTime: 0,
|
|
4068
|
+
failures: 0,
|
|
4069
|
+
name: id
|
|
4070
|
+
// simplify
|
|
4071
|
+
};
|
|
3160
4072
|
}
|
|
3161
|
-
|
|
4073
|
+
const m = this.metrics.nodeMetrics[id];
|
|
4074
|
+
m.requests++;
|
|
4075
|
+
m.totalTime += duration;
|
|
4076
|
+
if (isError) m.failures++;
|
|
3162
4077
|
}
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
4078
|
+
recordEdgeMetric(from, to) {
|
|
4079
|
+
const key = `${from}|${to}`;
|
|
4080
|
+
this.metrics.edgeMetrics[key] = (this.metrics.edgeMetrics[key] || 0) + 1;
|
|
4081
|
+
}
|
|
4082
|
+
getLinkPattern() {
|
|
4083
|
+
const term = process.env["TERM_PROGRAM"] || "";
|
|
4084
|
+
if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
|
|
4085
|
+
return "vscode://file/{{absolute}}:{{line}}";
|
|
4086
|
+
}
|
|
4087
|
+
return "file:///{{absolute}}:{{line}}";
|
|
4088
|
+
}
|
|
4089
|
+
getHooks() {
|
|
4090
|
+
return {
|
|
4091
|
+
onRequestStart: (ctx) => {
|
|
4092
|
+
const app = this[$appRoot];
|
|
4093
|
+
if (!this.instrumented && app) {
|
|
4094
|
+
this.instrumentApp(app);
|
|
4095
|
+
}
|
|
4096
|
+
this.metrics.totalRequests++;
|
|
4097
|
+
this.metrics.activeRequests++;
|
|
4098
|
+
ctx._debugStartTime = performance.now();
|
|
4099
|
+
ctx[$debug] = new Collector(this);
|
|
4100
|
+
},
|
|
4101
|
+
onResponseEnd: async (ctx, response) => {
|
|
4102
|
+
this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
|
|
4103
|
+
const start = ctx._debugStartTime;
|
|
4104
|
+
let duration = 0;
|
|
4105
|
+
if (start) {
|
|
4106
|
+
duration = performance.now() - start;
|
|
4107
|
+
this.updateTiming(duration);
|
|
4108
|
+
}
|
|
4109
|
+
const isError = response.status >= 400;
|
|
4110
|
+
this.metricsCollector.recordRequest(duration, isError);
|
|
4111
|
+
if (response.status >= 400) {
|
|
4112
|
+
this.metrics.failedRequests++;
|
|
4113
|
+
if (response.status === 429) {
|
|
4114
|
+
const path2 = ctx.path;
|
|
4115
|
+
this.metrics.rateLimitedCounts[path2] = (this.metrics.rateLimitedCounts[path2] || 0) + 1;
|
|
4116
|
+
}
|
|
4117
|
+
try {
|
|
4118
|
+
const headers = {};
|
|
4119
|
+
if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
|
|
4120
|
+
ctx.request.headers.forEach((v, k) => {
|
|
4121
|
+
headers[k] = v;
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
await datastore.set(new surrealdb.RecordId("failed_requests", ctx.requestId), {
|
|
4125
|
+
method: ctx.method,
|
|
4126
|
+
url: ctx.url.toString(),
|
|
4127
|
+
headers,
|
|
4128
|
+
status: response.status,
|
|
4129
|
+
timestamp: Date.now(),
|
|
4130
|
+
state: ctx.state
|
|
4131
|
+
// body?
|
|
4132
|
+
});
|
|
4133
|
+
} catch (e) {
|
|
4134
|
+
console.error("Failed to record failed request", e);
|
|
4135
|
+
}
|
|
4136
|
+
} else {
|
|
4137
|
+
this.metrics.successfulRequests++;
|
|
4138
|
+
}
|
|
4139
|
+
const logEntry = {
|
|
4140
|
+
method: ctx.method,
|
|
4141
|
+
url: ctx.url.toString(),
|
|
4142
|
+
status: response.status,
|
|
4143
|
+
duration,
|
|
4144
|
+
timestamp: Date.now(),
|
|
4145
|
+
handlerStack: ctx.handlerStack
|
|
4146
|
+
};
|
|
4147
|
+
this.metrics.logs.push(logEntry);
|
|
3175
4148
|
try {
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
4149
|
+
await datastore.set(new surrealdb.RecordId("requests", ctx.requestId), logEntry);
|
|
4150
|
+
} catch (e) {
|
|
4151
|
+
console.error("Failed to record request log", e);
|
|
4152
|
+
}
|
|
4153
|
+
const retention = this.dashboardConfig.retentionMs ?? 72e5;
|
|
4154
|
+
const cutoff = Date.now() - retention;
|
|
4155
|
+
if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
|
|
4156
|
+
this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
|
|
3179
4157
|
}
|
|
3180
4158
|
}
|
|
3181
|
-
return next();
|
|
3182
4159
|
};
|
|
3183
4160
|
}
|
|
4161
|
+
updateTiming(duration) {
|
|
4162
|
+
const alpha = 0.1;
|
|
4163
|
+
if (this.metrics.averageTotalTime_ms === 0) {
|
|
4164
|
+
this.metrics.averageTotalTime_ms = duration;
|
|
4165
|
+
} else {
|
|
4166
|
+
this.metrics.averageTotalTime_ms = alpha * duration + (1 - alpha) * this.metrics.averageTotalTime_ms;
|
|
4167
|
+
}
|
|
4168
|
+
this.metrics.recentTimings.push(duration);
|
|
4169
|
+
if (this.metrics.recentTimings.length > 50) {
|
|
4170
|
+
this.metrics.recentTimings.shift();
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
function unknownError(ctx) {
|
|
4175
|
+
return ctx.json({ error: "Unknown Error" }, 500);
|
|
4176
|
+
}
|
|
4177
|
+
const eta = new eta$2.Eta();
|
|
4178
|
+
class ScalarPlugin extends ShokupanRouter {
|
|
4179
|
+
constructor(pluginOptions = {}) {
|
|
4180
|
+
pluginOptions.config ??= {};
|
|
4181
|
+
super();
|
|
4182
|
+
this.pluginOptions = pluginOptions;
|
|
4183
|
+
this.init();
|
|
4184
|
+
}
|
|
4185
|
+
onInit(app, options) {
|
|
4186
|
+
if (options?.path) {
|
|
4187
|
+
app.mount(options.path, this);
|
|
4188
|
+
} else {
|
|
4189
|
+
app.mount(options.path ?? "/", this);
|
|
4190
|
+
}
|
|
4191
|
+
this.onMount(app);
|
|
4192
|
+
}
|
|
4193
|
+
init() {
|
|
4194
|
+
this.get("/", (ctx) => {
|
|
4195
|
+
let path2 = ctx.url.toString();
|
|
4196
|
+
if (!path2.endsWith("/")) path2 += "/";
|
|
4197
|
+
return ctx.html(eta.renderString(`<!doctype html>
|
|
4198
|
+
<html>
|
|
4199
|
+
<head>
|
|
4200
|
+
<title>API Reference</title>
|
|
4201
|
+
<meta charset = "utf-8" />
|
|
4202
|
+
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
4203
|
+
</head>
|
|
4204
|
+
|
|
4205
|
+
<body>
|
|
4206
|
+
<div id="app"></div>
|
|
4207
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
|
|
4208
|
+
<script>
|
|
4209
|
+
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
4210
|
+
url: "<%= it.path %>openapi.json",
|
|
4211
|
+
}
|
|
4212
|
+
])
|
|
4213
|
+
<\/script>
|
|
4214
|
+
</body>
|
|
4215
|
+
|
|
4216
|
+
</html>`, { path: path2, config: this.pluginOptions }));
|
|
4217
|
+
});
|
|
4218
|
+
this.get("/openapi.json", async (ctx) => {
|
|
4219
|
+
let spec;
|
|
4220
|
+
if (this.root.openApiSpec) {
|
|
4221
|
+
try {
|
|
4222
|
+
spec = structuredClone(this.root.openApiSpec);
|
|
4223
|
+
} catch (e) {
|
|
4224
|
+
spec = Object.assign({}, this.root.openApiSpec);
|
|
4225
|
+
}
|
|
4226
|
+
} else {
|
|
4227
|
+
spec = await (this.root || this).generateApiSpec();
|
|
4228
|
+
}
|
|
4229
|
+
if (this.pluginOptions.baseDocument) {
|
|
4230
|
+
deepMerge(spec, this.pluginOptions.baseDocument);
|
|
4231
|
+
}
|
|
4232
|
+
return ctx.json(spec);
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
// New lifecycle method to be called by router.mount
|
|
4236
|
+
onMount(parent) {
|
|
4237
|
+
if (parent.onStart) {
|
|
4238
|
+
parent.onStart(async () => {
|
|
4239
|
+
if (this.pluginOptions.enableStaticAnalysis) {
|
|
4240
|
+
try {
|
|
4241
|
+
const entrypoint = process.argv[1];
|
|
4242
|
+
console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
|
|
4243
|
+
const analyzer$1 = new analyzer.OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
4244
|
+
let staticSpec = await analyzer$1.analyze();
|
|
4245
|
+
if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
|
|
4246
|
+
deepMerge(this.pluginOptions.baseDocument, staticSpec);
|
|
4247
|
+
console.log("[ScalarPlugin] Static analysis completed successfully.");
|
|
4248
|
+
} catch (err) {
|
|
4249
|
+
console.error("[ScalarPlugin] Failed to run static analysis:", err);
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
});
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
3184
4255
|
}
|
|
3185
4256
|
function Compression(options = {}) {
|
|
3186
4257
|
const threshold = options.threshold ?? 512;
|
|
4258
|
+
const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
|
|
3187
4259
|
const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
|
|
3188
4260
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
3189
4261
|
let method = null;
|
|
@@ -3196,24 +4268,27 @@ function Compression(options = {}) {
|
|
|
3196
4268
|
} else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
3197
4269
|
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
3198
4270
|
if (!method) return next();
|
|
4271
|
+
if (!allowedAlgorithms.has(method)) {
|
|
4272
|
+
return next();
|
|
4273
|
+
}
|
|
3199
4274
|
let response = await next();
|
|
3200
|
-
if (!(response instanceof Response) && ctx
|
|
3201
|
-
response = ctx
|
|
4275
|
+
if (!(response instanceof Response) && ctx[$finalResponse] instanceof Response) {
|
|
4276
|
+
response = ctx[$finalResponse];
|
|
3202
4277
|
}
|
|
3203
4278
|
if (response instanceof Response) {
|
|
3204
4279
|
if (response.headers.has("Content-Encoding")) return response;
|
|
3205
4280
|
let body;
|
|
3206
4281
|
let bodySize;
|
|
3207
|
-
if (ctx
|
|
3208
|
-
if (typeof ctx
|
|
3209
|
-
const encoded = new TextEncoder().encode(ctx
|
|
4282
|
+
if (ctx[$rawBody] !== void 0) {
|
|
4283
|
+
if (typeof ctx[$rawBody] === "string") {
|
|
4284
|
+
const encoded = new TextEncoder().encode(ctx[$rawBody]);
|
|
3210
4285
|
body = encoded;
|
|
3211
4286
|
bodySize = encoded.byteLength;
|
|
3212
|
-
} else if (ctx
|
|
3213
|
-
body = ctx
|
|
3214
|
-
bodySize = ctx.
|
|
4287
|
+
} else if (ctx[$rawBody] instanceof Uint8Array) {
|
|
4288
|
+
body = ctx[$rawBody];
|
|
4289
|
+
bodySize = ctx[$rawBody].byteLength;
|
|
3215
4290
|
} else {
|
|
3216
|
-
body = ctx
|
|
4291
|
+
body = ctx[$rawBody];
|
|
3217
4292
|
bodySize = body.byteLength;
|
|
3218
4293
|
}
|
|
3219
4294
|
} else {
|
|
@@ -3750,77 +4825,6 @@ function enableOpenApiValidation(app) {
|
|
|
3750
4825
|
precompileValidators(app, spec);
|
|
3751
4826
|
});
|
|
3752
4827
|
}
|
|
3753
|
-
const eta = new eta$2.Eta();
|
|
3754
|
-
class ScalarPlugin extends ShokupanRouter {
|
|
3755
|
-
constructor(pluginOptions = {}) {
|
|
3756
|
-
pluginOptions.config ??= {};
|
|
3757
|
-
super();
|
|
3758
|
-
this.pluginOptions = pluginOptions;
|
|
3759
|
-
this.init();
|
|
3760
|
-
}
|
|
3761
|
-
init() {
|
|
3762
|
-
this.get("/", (ctx) => {
|
|
3763
|
-
let path2 = ctx.url.toString();
|
|
3764
|
-
if (!path2.endsWith("/")) path2 += "/";
|
|
3765
|
-
return ctx.html(eta.renderString(`<!doctype html>
|
|
3766
|
-
<html>
|
|
3767
|
-
<head>
|
|
3768
|
-
<title>API Reference</title>
|
|
3769
|
-
<meta charset = "utf-8" />
|
|
3770
|
-
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
3771
|
-
</head>
|
|
3772
|
-
|
|
3773
|
-
<body>
|
|
3774
|
-
<div id="app"></div>
|
|
3775
|
-
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
|
|
3776
|
-
<script>
|
|
3777
|
-
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
3778
|
-
url: "<%= it.path %>openapi.json",
|
|
3779
|
-
}
|
|
3780
|
-
])
|
|
3781
|
-
<\/script>
|
|
3782
|
-
</body>
|
|
3783
|
-
|
|
3784
|
-
</html>`, { path: path2, config: this.pluginOptions }));
|
|
3785
|
-
});
|
|
3786
|
-
this.get("/openapi.json", async (ctx) => {
|
|
3787
|
-
let spec;
|
|
3788
|
-
if (this.root.openApiSpec) {
|
|
3789
|
-
try {
|
|
3790
|
-
spec = structuredClone(this.root.openApiSpec);
|
|
3791
|
-
} catch (e) {
|
|
3792
|
-
spec = Object.assign({}, this.root.openApiSpec);
|
|
3793
|
-
}
|
|
3794
|
-
} else {
|
|
3795
|
-
spec = await (this.root || this).generateApiSpec();
|
|
3796
|
-
}
|
|
3797
|
-
if (this.pluginOptions.baseDocument) {
|
|
3798
|
-
deepMerge(spec, this.pluginOptions.baseDocument);
|
|
3799
|
-
}
|
|
3800
|
-
return ctx.json(spec);
|
|
3801
|
-
});
|
|
3802
|
-
}
|
|
3803
|
-
// New lifecycle method to be called by router.mount
|
|
3804
|
-
onMount(parent) {
|
|
3805
|
-
if (parent.onStart) {
|
|
3806
|
-
parent.onStart(async () => {
|
|
3807
|
-
if (this.pluginOptions.enableStaticAnalysis) {
|
|
3808
|
-
try {
|
|
3809
|
-
const entrypoint = process.argv[1];
|
|
3810
|
-
console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
|
|
3811
|
-
const analyzer = new openapiAnalyzer.OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
3812
|
-
let staticSpec = await analyzer.analyze();
|
|
3813
|
-
if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
|
|
3814
|
-
deepMerge(this.pluginOptions.baseDocument, staticSpec);
|
|
3815
|
-
console.log("[ScalarPlugin] Static analysis completed successfully.");
|
|
3816
|
-
} catch (err) {
|
|
3817
|
-
console.error("[ScalarPlugin] Failed to run static analysis:", err);
|
|
3818
|
-
}
|
|
3819
|
-
}
|
|
3820
|
-
});
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
}
|
|
3824
4828
|
function SecurityHeaders(options = {}) {
|
|
3825
4829
|
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
3826
4830
|
const headers = {};
|
|
@@ -3944,18 +4948,18 @@ class MemoryStore extends events.EventEmitter {
|
|
|
3944
4948
|
}
|
|
3945
4949
|
set(sid, sess, cb) {
|
|
3946
4950
|
this.sessions[sid] = JSON.stringify(sess);
|
|
3947
|
-
cb
|
|
4951
|
+
cb?.();
|
|
3948
4952
|
}
|
|
3949
4953
|
destroy(sid, cb) {
|
|
3950
4954
|
delete this.sessions[sid];
|
|
3951
|
-
cb
|
|
4955
|
+
cb?.();
|
|
3952
4956
|
}
|
|
3953
4957
|
touch(sid, sess, cb) {
|
|
3954
4958
|
const current = this.sessions[sid];
|
|
3955
4959
|
if (current) {
|
|
3956
4960
|
this.sessions[sid] = JSON.stringify(sess);
|
|
3957
4961
|
}
|
|
3958
|
-
cb
|
|
4962
|
+
cb?.();
|
|
3959
4963
|
}
|
|
3960
4964
|
all(cb) {
|
|
3961
4965
|
const result = {};
|
|
@@ -3971,7 +4975,7 @@ class MemoryStore extends events.EventEmitter {
|
|
|
3971
4975
|
}
|
|
3972
4976
|
clear(cb) {
|
|
3973
4977
|
this.sessions = {};
|
|
3974
|
-
cb
|
|
4978
|
+
cb?.();
|
|
3975
4979
|
}
|
|
3976
4980
|
}
|
|
3977
4981
|
function sign(val, secret) {
|
|
@@ -4149,29 +5153,51 @@ function Session(options) {
|
|
|
4149
5153
|
return sessionMiddleware;
|
|
4150
5154
|
}
|
|
4151
5155
|
exports.$appRoot = $appRoot;
|
|
5156
|
+
exports.$bodyParseError = $bodyParseError;
|
|
5157
|
+
exports.$bodyParsed = $bodyParsed;
|
|
5158
|
+
exports.$bodyType = $bodyType;
|
|
5159
|
+
exports.$cachedBody = $cachedBody;
|
|
5160
|
+
exports.$cachedHost = $cachedHost;
|
|
5161
|
+
exports.$cachedHostname = $cachedHostname;
|
|
5162
|
+
exports.$cachedOrigin = $cachedOrigin;
|
|
5163
|
+
exports.$cachedProtocol = $cachedProtocol;
|
|
5164
|
+
exports.$cachedQuery = $cachedQuery;
|
|
4152
5165
|
exports.$childControllers = $childControllers;
|
|
4153
5166
|
exports.$childRouters = $childRouters;
|
|
4154
5167
|
exports.$controllerPath = $controllerPath;
|
|
5168
|
+
exports.$debug = $debug;
|
|
4155
5169
|
exports.$dispatch = $dispatch;
|
|
5170
|
+
exports.$eventMethods = $eventMethods;
|
|
5171
|
+
exports.$finalResponse = $finalResponse;
|
|
5172
|
+
exports.$io = $io;
|
|
4156
5173
|
exports.$isApplication = $isApplication;
|
|
4157
5174
|
exports.$isMounted = $isMounted;
|
|
4158
5175
|
exports.$isRouter = $isRouter;
|
|
4159
5176
|
exports.$middleware = $middleware;
|
|
4160
5177
|
exports.$mountPath = $mountPath;
|
|
4161
5178
|
exports.$parent = $parent;
|
|
5179
|
+
exports.$rawBody = $rawBody;
|
|
5180
|
+
exports.$requestId = $requestId;
|
|
4162
5181
|
exports.$routeArgs = $routeArgs;
|
|
5182
|
+
exports.$routeMatched = $routeMatched;
|
|
4163
5183
|
exports.$routeMethods = $routeMethods;
|
|
4164
5184
|
exports.$routeSpec = $routeSpec;
|
|
4165
5185
|
exports.$routes = $routes;
|
|
5186
|
+
exports.$socket = $socket;
|
|
5187
|
+
exports.$url = $url;
|
|
5188
|
+
exports.$ws = $ws;
|
|
4166
5189
|
exports.All = All;
|
|
4167
5190
|
exports.AuthPlugin = AuthPlugin;
|
|
4168
5191
|
exports.Body = Body;
|
|
5192
|
+
exports.ClusterPlugin = ClusterPlugin;
|
|
4169
5193
|
exports.Compression = Compression;
|
|
4170
5194
|
exports.Container = Container;
|
|
4171
5195
|
exports.Controller = Controller;
|
|
4172
5196
|
exports.Cors = Cors;
|
|
4173
5197
|
exports.Ctx = Ctx;
|
|
5198
|
+
exports.Dashboard = Dashboard;
|
|
4174
5199
|
exports.Delete = Delete;
|
|
5200
|
+
exports.Event = Event;
|
|
4175
5201
|
exports.Get = Get;
|
|
4176
5202
|
exports.HTTPMethods = HTTPMethods;
|
|
4177
5203
|
exports.Head = Head;
|
|
@@ -4189,16 +5215,13 @@ exports.RateLimit = RateLimit;
|
|
|
4189
5215
|
exports.RateLimitMiddleware = RateLimitMiddleware;
|
|
4190
5216
|
exports.Req = Req;
|
|
4191
5217
|
exports.RouteParamType = RouteParamType;
|
|
4192
|
-
exports.RouterRegistry = RouterRegistry;
|
|
4193
5218
|
exports.ScalarPlugin = ScalarPlugin;
|
|
4194
5219
|
exports.SecurityHeaders = SecurityHeaders;
|
|
4195
5220
|
exports.Session = Session;
|
|
4196
5221
|
exports.Shokupan = Shokupan;
|
|
4197
|
-
exports.ShokupanApplicationTree = ShokupanApplicationTree;
|
|
4198
5222
|
exports.ShokupanContext = ShokupanContext;
|
|
4199
5223
|
exports.ShokupanRequest = ShokupanRequest;
|
|
4200
5224
|
exports.ShokupanResponse = ShokupanResponse;
|
|
4201
|
-
exports.ShokupanRouter = ShokupanRouter;
|
|
4202
5225
|
exports.Spec = Spec;
|
|
4203
5226
|
exports.Use = Use;
|
|
4204
5227
|
exports.ValidationError = ValidationError;
|