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.js
CHANGED
|
@@ -1,18 +1,122 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { Surreal, RecordId } from "surrealdb";
|
|
2
6
|
import { Eta } from "eta";
|
|
3
7
|
import { stat, readdir, readFile as readFile$1 } from "fs/promises";
|
|
4
8
|
import { resolve, join, sep, basename } from "path";
|
|
5
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
-
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
7
9
|
import * as os from "node:os";
|
|
10
|
+
import os__default from "node:os";
|
|
8
11
|
import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
|
|
9
12
|
import * as jose from "jose";
|
|
13
|
+
import cluster from "node:cluster";
|
|
14
|
+
import net from "node:net";
|
|
15
|
+
import { dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
18
|
+
import { OpenAPIAnalyzer } from "./analyzer-Ce_7JxZh.js";
|
|
10
19
|
import * as zlib from "node:zlib";
|
|
11
20
|
import Ajv from "ajv";
|
|
12
21
|
import addFormats from "ajv-formats";
|
|
13
|
-
import { OpenAPIAnalyzer } from "./openapi-analyzer-Ce_7JxZh.js";
|
|
14
22
|
import { randomUUID, createHmac } from "crypto";
|
|
15
23
|
import { EventEmitter } from "events";
|
|
24
|
+
const HTTP_STATUS = {
|
|
25
|
+
// 2xx Success
|
|
26
|
+
OK: 200,
|
|
27
|
+
CREATED: 201,
|
|
28
|
+
ACCEPTED: 202,
|
|
29
|
+
NO_CONTENT: 204,
|
|
30
|
+
// 3xx Redirection
|
|
31
|
+
MOVED_PERMANENTLY: 301,
|
|
32
|
+
FOUND: 302,
|
|
33
|
+
SEE_OTHER: 303,
|
|
34
|
+
NOT_MODIFIED: 304,
|
|
35
|
+
TEMPORARY_REDIRECT: 307,
|
|
36
|
+
PERMANENT_REDIRECT: 308,
|
|
37
|
+
// 4xx Client Errors
|
|
38
|
+
BAD_REQUEST: 400,
|
|
39
|
+
UNAUTHORIZED: 401,
|
|
40
|
+
FORBIDDEN: 403,
|
|
41
|
+
NOT_FOUND: 404,
|
|
42
|
+
METHOD_NOT_ALLOWED: 405,
|
|
43
|
+
REQUEST_TIMEOUT: 408,
|
|
44
|
+
CONFLICT: 409,
|
|
45
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
46
|
+
TOO_MANY_REQUESTS: 429,
|
|
47
|
+
// 5xx Server Errors
|
|
48
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
49
|
+
NOT_IMPLEMENTED: 501,
|
|
50
|
+
BAD_GATEWAY: 502,
|
|
51
|
+
SERVICE_UNAVAILABLE: 503,
|
|
52
|
+
GATEWAY_TIMEOUT: 504
|
|
53
|
+
};
|
|
54
|
+
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
55
|
+
100,
|
|
56
|
+
101,
|
|
57
|
+
102,
|
|
58
|
+
103,
|
|
59
|
+
200,
|
|
60
|
+
201,
|
|
61
|
+
202,
|
|
62
|
+
203,
|
|
63
|
+
204,
|
|
64
|
+
205,
|
|
65
|
+
206,
|
|
66
|
+
207,
|
|
67
|
+
208,
|
|
68
|
+
226,
|
|
69
|
+
300,
|
|
70
|
+
301,
|
|
71
|
+
302,
|
|
72
|
+
303,
|
|
73
|
+
304,
|
|
74
|
+
305,
|
|
75
|
+
306,
|
|
76
|
+
307,
|
|
77
|
+
308,
|
|
78
|
+
400,
|
|
79
|
+
401,
|
|
80
|
+
402,
|
|
81
|
+
403,
|
|
82
|
+
404,
|
|
83
|
+
405,
|
|
84
|
+
406,
|
|
85
|
+
407,
|
|
86
|
+
408,
|
|
87
|
+
409,
|
|
88
|
+
410,
|
|
89
|
+
411,
|
|
90
|
+
412,
|
|
91
|
+
413,
|
|
92
|
+
414,
|
|
93
|
+
415,
|
|
94
|
+
416,
|
|
95
|
+
417,
|
|
96
|
+
418,
|
|
97
|
+
421,
|
|
98
|
+
422,
|
|
99
|
+
423,
|
|
100
|
+
424,
|
|
101
|
+
425,
|
|
102
|
+
426,
|
|
103
|
+
428,
|
|
104
|
+
429,
|
|
105
|
+
431,
|
|
106
|
+
451,
|
|
107
|
+
500,
|
|
108
|
+
501,
|
|
109
|
+
502,
|
|
110
|
+
503,
|
|
111
|
+
504,
|
|
112
|
+
505,
|
|
113
|
+
506,
|
|
114
|
+
507,
|
|
115
|
+
508,
|
|
116
|
+
510,
|
|
117
|
+
511
|
|
118
|
+
]);
|
|
119
|
+
const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
16
120
|
class ShokupanResponse {
|
|
17
121
|
_headers = null;
|
|
18
122
|
_status = 200;
|
|
@@ -76,6 +180,40 @@ class ShokupanResponse {
|
|
|
76
180
|
return this._headers !== null;
|
|
77
181
|
}
|
|
78
182
|
}
|
|
183
|
+
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
184
|
+
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
185
|
+
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
186
|
+
const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
|
|
187
|
+
const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
|
|
188
|
+
const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
|
|
189
|
+
const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
|
|
190
|
+
const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
|
|
191
|
+
const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
|
|
192
|
+
const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
|
|
193
|
+
const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
|
|
194
|
+
const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
|
|
195
|
+
const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
|
|
196
|
+
const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
|
|
197
|
+
const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
|
|
198
|
+
const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
|
|
199
|
+
const $url = /* @__PURE__ */ Symbol.for("Shokupan.ctx.url");
|
|
200
|
+
const $requestId = /* @__PURE__ */ Symbol.for("Shokupan.ctx.requestId");
|
|
201
|
+
const $debug = /* @__PURE__ */ Symbol.for("Shokupan.ctx.debug");
|
|
202
|
+
const $finalResponse = /* @__PURE__ */ Symbol.for("Shokupan.ctx.finalResponse");
|
|
203
|
+
const $rawBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.rawBody");
|
|
204
|
+
const $cachedBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedBody");
|
|
205
|
+
const $bodyType = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyType");
|
|
206
|
+
const $bodyParsed = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParsed");
|
|
207
|
+
const $bodyParseError = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParseError");
|
|
208
|
+
const $routeMatched = /* @__PURE__ */ Symbol.for("Shokupan.ctx.routeMatched");
|
|
209
|
+
const $cachedHostname = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHostname");
|
|
210
|
+
const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol");
|
|
211
|
+
const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
|
|
212
|
+
const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
|
|
213
|
+
const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
|
|
214
|
+
const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
|
|
215
|
+
const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
|
|
216
|
+
const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
|
|
79
217
|
function isValidCookieDomain(domain, currentHost) {
|
|
80
218
|
const hostWithoutPort = currentHost.split(":")[0];
|
|
81
219
|
if (domain === hostWithoutPort) return true;
|
|
@@ -85,72 +223,6 @@ function isValidCookieDomain(domain, currentHost) {
|
|
|
85
223
|
}
|
|
86
224
|
return false;
|
|
87
225
|
}
|
|
88
|
-
const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
|
|
89
|
-
100,
|
|
90
|
-
101,
|
|
91
|
-
102,
|
|
92
|
-
103,
|
|
93
|
-
200,
|
|
94
|
-
201,
|
|
95
|
-
202,
|
|
96
|
-
203,
|
|
97
|
-
204,
|
|
98
|
-
205,
|
|
99
|
-
206,
|
|
100
|
-
207,
|
|
101
|
-
208,
|
|
102
|
-
226,
|
|
103
|
-
300,
|
|
104
|
-
301,
|
|
105
|
-
302,
|
|
106
|
-
303,
|
|
107
|
-
304,
|
|
108
|
-
305,
|
|
109
|
-
306,
|
|
110
|
-
307,
|
|
111
|
-
308,
|
|
112
|
-
400,
|
|
113
|
-
401,
|
|
114
|
-
402,
|
|
115
|
-
403,
|
|
116
|
-
404,
|
|
117
|
-
405,
|
|
118
|
-
406,
|
|
119
|
-
407,
|
|
120
|
-
408,
|
|
121
|
-
409,
|
|
122
|
-
410,
|
|
123
|
-
411,
|
|
124
|
-
412,
|
|
125
|
-
413,
|
|
126
|
-
414,
|
|
127
|
-
415,
|
|
128
|
-
416,
|
|
129
|
-
417,
|
|
130
|
-
418,
|
|
131
|
-
421,
|
|
132
|
-
422,
|
|
133
|
-
423,
|
|
134
|
-
424,
|
|
135
|
-
425,
|
|
136
|
-
426,
|
|
137
|
-
428,
|
|
138
|
-
429,
|
|
139
|
-
431,
|
|
140
|
-
451,
|
|
141
|
-
500,
|
|
142
|
-
501,
|
|
143
|
-
502,
|
|
144
|
-
503,
|
|
145
|
-
504,
|
|
146
|
-
505,
|
|
147
|
-
506,
|
|
148
|
-
507,
|
|
149
|
-
508,
|
|
150
|
-
510,
|
|
151
|
-
511
|
|
152
|
-
]);
|
|
153
|
-
const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
154
226
|
class ShokupanContext {
|
|
155
227
|
constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
|
|
156
228
|
this.request = request;
|
|
@@ -179,28 +251,43 @@ class ShokupanContext {
|
|
|
179
251
|
state;
|
|
180
252
|
handlerStack = [];
|
|
181
253
|
response;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
254
|
+
[$debug];
|
|
255
|
+
[$finalResponse];
|
|
256
|
+
[$rawBody];
|
|
185
257
|
// Raw body for compression optimization
|
|
186
258
|
// Body caching to avoid double parsing
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
259
|
+
[$url];
|
|
260
|
+
[$cachedBody];
|
|
261
|
+
[$bodyType];
|
|
262
|
+
[$bodyParsed] = false;
|
|
263
|
+
[$bodyParseError];
|
|
264
|
+
[$routeMatched] = false;
|
|
192
265
|
// Cached URL properties to avoid repeated parsing
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
266
|
+
[$cachedHostname];
|
|
267
|
+
[$cachedProtocol];
|
|
268
|
+
[$cachedHost];
|
|
269
|
+
[$cachedOrigin];
|
|
270
|
+
[$cachedQuery];
|
|
271
|
+
[$ws];
|
|
272
|
+
[$socket];
|
|
273
|
+
[$io];
|
|
274
|
+
/**
|
|
275
|
+
* JSX Rendering Function
|
|
276
|
+
*/
|
|
277
|
+
renderer;
|
|
278
|
+
setRenderer(renderer) {
|
|
279
|
+
this.renderer = renderer;
|
|
280
|
+
}
|
|
281
|
+
[$requestId];
|
|
282
|
+
get requestId() {
|
|
283
|
+
return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid();
|
|
284
|
+
}
|
|
198
285
|
get url() {
|
|
199
|
-
if (!this
|
|
286
|
+
if (!this[$url]) {
|
|
200
287
|
const urlString = this.request.url || "http://localhost/";
|
|
201
|
-
this
|
|
288
|
+
this[$url] = new URL(urlString);
|
|
202
289
|
}
|
|
203
|
-
return this
|
|
290
|
+
return this[$url];
|
|
204
291
|
}
|
|
205
292
|
/**
|
|
206
293
|
* Base request
|
|
@@ -218,7 +305,7 @@ class ShokupanContext {
|
|
|
218
305
|
* Request path
|
|
219
306
|
*/
|
|
220
307
|
get path() {
|
|
221
|
-
if (this
|
|
308
|
+
if (this[$url]) return this[$url].pathname;
|
|
222
309
|
const url = this.request.url;
|
|
223
310
|
let queryIndex = url.indexOf("?");
|
|
224
311
|
const end = queryIndex === -1 ? url.length : queryIndex;
|
|
@@ -243,7 +330,7 @@ class ShokupanContext {
|
|
|
243
330
|
* Request query params
|
|
244
331
|
*/
|
|
245
332
|
get query() {
|
|
246
|
-
if (this
|
|
333
|
+
if (this[$cachedQuery]) return this[$cachedQuery];
|
|
247
334
|
const q = /* @__PURE__ */ Object.create(null);
|
|
248
335
|
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
249
336
|
const entries = Object.entries(this.url.searchParams);
|
|
@@ -260,7 +347,7 @@ class ShokupanContext {
|
|
|
260
347
|
q[key] = value;
|
|
261
348
|
}
|
|
262
349
|
}
|
|
263
|
-
this
|
|
350
|
+
this[$cachedQuery] = q;
|
|
264
351
|
return q;
|
|
265
352
|
}
|
|
266
353
|
/**
|
|
@@ -273,19 +360,19 @@ class ShokupanContext {
|
|
|
273
360
|
* Request hostname (e.g. "localhost")
|
|
274
361
|
*/
|
|
275
362
|
get hostname() {
|
|
276
|
-
return this
|
|
363
|
+
return this[$cachedHostname] ??= this.url.hostname;
|
|
277
364
|
}
|
|
278
365
|
/**
|
|
279
366
|
* Request host (e.g. "localhost:3000")
|
|
280
367
|
*/
|
|
281
368
|
get host() {
|
|
282
|
-
return this
|
|
369
|
+
return this[$cachedHost] ??= this.url.host;
|
|
283
370
|
}
|
|
284
371
|
/**
|
|
285
372
|
* Request protocol (e.g. "http:", "https:")
|
|
286
373
|
*/
|
|
287
374
|
get protocol() {
|
|
288
|
-
return this
|
|
375
|
+
return this[$cachedProtocol] ??= this.url.protocol;
|
|
289
376
|
}
|
|
290
377
|
/**
|
|
291
378
|
* Whether request is secure (https)
|
|
@@ -297,7 +384,7 @@ class ShokupanContext {
|
|
|
297
384
|
* Request origin (e.g. "http://localhost:3000")
|
|
298
385
|
*/
|
|
299
386
|
get origin() {
|
|
300
|
-
return this
|
|
387
|
+
return this[$cachedOrigin] ??= this.url.origin;
|
|
301
388
|
}
|
|
302
389
|
/**
|
|
303
390
|
* Request headers
|
|
@@ -318,6 +405,24 @@ class ShokupanContext {
|
|
|
318
405
|
get res() {
|
|
319
406
|
return this.response;
|
|
320
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Raw WebSocket connection
|
|
410
|
+
*/
|
|
411
|
+
get ws() {
|
|
412
|
+
return this[$ws];
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Socket.io socket
|
|
416
|
+
*/
|
|
417
|
+
get socket() {
|
|
418
|
+
return this[$socket];
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Socket.io server
|
|
422
|
+
*/
|
|
423
|
+
get io() {
|
|
424
|
+
return this[$io];
|
|
425
|
+
}
|
|
321
426
|
/**
|
|
322
427
|
* Helper to set a header on the response
|
|
323
428
|
* @param key Header key
|
|
@@ -327,6 +432,20 @@ class ShokupanContext {
|
|
|
327
432
|
this.response.set(key, value);
|
|
328
433
|
return this;
|
|
329
434
|
}
|
|
435
|
+
isUpgraded = false;
|
|
436
|
+
/**
|
|
437
|
+
* Upgrades the request to a WebSocket connection.
|
|
438
|
+
* @param options Upgrade options
|
|
439
|
+
* @returns true if upgraded, false otherwise
|
|
440
|
+
*/
|
|
441
|
+
upgrade(options) {
|
|
442
|
+
if (!this.server) return false;
|
|
443
|
+
const success = this.server.upgrade(this.req, options);
|
|
444
|
+
if (success) {
|
|
445
|
+
this.isUpgraded = true;
|
|
446
|
+
}
|
|
447
|
+
return success;
|
|
448
|
+
}
|
|
330
449
|
/**
|
|
331
450
|
* Set a cookie
|
|
332
451
|
* @param name Cookie name
|
|
@@ -404,33 +523,37 @@ class ShokupanContext {
|
|
|
404
523
|
* The body is only parsed once and cached for subsequent reads.
|
|
405
524
|
*/
|
|
406
525
|
async body() {
|
|
407
|
-
if (this
|
|
408
|
-
throw this
|
|
526
|
+
if (this[$bodyParseError]) {
|
|
527
|
+
throw this[$bodyParseError];
|
|
409
528
|
}
|
|
410
|
-
if (this
|
|
411
|
-
return this
|
|
529
|
+
if (this[$bodyParsed]) {
|
|
530
|
+
return this[$cachedBody];
|
|
412
531
|
}
|
|
413
532
|
const contentType = this.request.headers.get("content-type") || "";
|
|
414
533
|
if (contentType.includes("application/json") || contentType.includes("+json")) {
|
|
415
|
-
const rawText = await this.readRawBody();
|
|
416
534
|
const parserType = this.app?.applicationConfig?.jsonParser || "native";
|
|
417
535
|
if (parserType === "native") {
|
|
418
|
-
|
|
536
|
+
try {
|
|
537
|
+
this[$cachedBody] = await this.request.json();
|
|
538
|
+
} catch (e) {
|
|
539
|
+
throw e;
|
|
540
|
+
}
|
|
419
541
|
} else {
|
|
542
|
+
const rawText = await this.request.text();
|
|
420
543
|
const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
|
|
421
544
|
const parser = getJSONParser(parserType);
|
|
422
|
-
this
|
|
545
|
+
this[$cachedBody] = parser(rawText);
|
|
423
546
|
}
|
|
424
|
-
this
|
|
547
|
+
this[$bodyType] = "json";
|
|
425
548
|
} else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
|
|
426
|
-
this
|
|
427
|
-
this
|
|
549
|
+
this[$cachedBody] = await this.request.formData();
|
|
550
|
+
this[$bodyType] = "formData";
|
|
428
551
|
} else {
|
|
429
|
-
this
|
|
430
|
-
this
|
|
552
|
+
this[$cachedBody] = await this.request.text();
|
|
553
|
+
this[$bodyType] = "text";
|
|
431
554
|
}
|
|
432
|
-
this
|
|
433
|
-
return this
|
|
555
|
+
this[$bodyParsed] = true;
|
|
556
|
+
return this[$cachedBody];
|
|
434
557
|
}
|
|
435
558
|
/**
|
|
436
559
|
* Pre-parse the request body before handler execution.
|
|
@@ -438,7 +561,7 @@ class ShokupanContext {
|
|
|
438
561
|
* Errors are deferred until the body is actually accessed in the handler.
|
|
439
562
|
*/
|
|
440
563
|
async parseBody() {
|
|
441
|
-
if (this
|
|
564
|
+
if (this[$bodyParsed]) {
|
|
442
565
|
return;
|
|
443
566
|
}
|
|
444
567
|
if (this.request.method === "GET" || this.request.method === "HEAD") {
|
|
@@ -447,7 +570,7 @@ class ShokupanContext {
|
|
|
447
570
|
try {
|
|
448
571
|
await this.body();
|
|
449
572
|
} catch (error) {
|
|
450
|
-
this
|
|
573
|
+
this[$bodyParseError] = error;
|
|
451
574
|
}
|
|
452
575
|
}
|
|
453
576
|
/**
|
|
@@ -497,10 +620,21 @@ class ShokupanContext {
|
|
|
497
620
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
498
621
|
}
|
|
499
622
|
if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
|
|
500
|
-
this
|
|
623
|
+
this[$rawBody] = body;
|
|
624
|
+
}
|
|
625
|
+
return this[$finalResponse] ??= new Response(body, { status, headers });
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Emit an event to the client (WebSocket only)
|
|
629
|
+
* @param event Event name
|
|
630
|
+
* @param data Event data (Must be JSON serializable)
|
|
631
|
+
*/
|
|
632
|
+
emit(event, data) {
|
|
633
|
+
if (this[$ws]) {
|
|
634
|
+
this[$ws].send(JSON.stringify({ event, data }));
|
|
635
|
+
} else if (this[$socket]) {
|
|
636
|
+
this[$socket].emit(event, data);
|
|
501
637
|
}
|
|
502
|
-
this._finalResponse = new Response(body, { status, headers });
|
|
503
|
-
return this._finalResponse;
|
|
504
638
|
}
|
|
505
639
|
/**
|
|
506
640
|
* Respond with a JSON object
|
|
@@ -511,18 +645,18 @@ class ShokupanContext {
|
|
|
511
645
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
512
646
|
}
|
|
513
647
|
const jsonString = JSON.stringify(data);
|
|
514
|
-
this
|
|
648
|
+
this[$rawBody] = jsonString;
|
|
515
649
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
516
|
-
this
|
|
650
|
+
this[$finalResponse] = new Response(jsonString, {
|
|
517
651
|
status: finalStatus,
|
|
518
652
|
headers: { "content-type": "application/json" }
|
|
519
653
|
});
|
|
520
|
-
return this
|
|
654
|
+
return this[$finalResponse];
|
|
521
655
|
}
|
|
522
656
|
const finalHeaders = this.mergeHeaders(headers);
|
|
523
657
|
finalHeaders.set("content-type", "application/json");
|
|
524
|
-
this
|
|
525
|
-
return this
|
|
658
|
+
this[$finalResponse] = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
|
|
659
|
+
return this[$finalResponse];
|
|
526
660
|
}
|
|
527
661
|
/**
|
|
528
662
|
* Respond with a text string
|
|
@@ -532,18 +666,18 @@ class ShokupanContext {
|
|
|
532
666
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
533
667
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
534
668
|
}
|
|
535
|
-
this
|
|
669
|
+
this[$rawBody] = data;
|
|
536
670
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
537
|
-
this
|
|
671
|
+
this[$finalResponse] = new Response(data, {
|
|
538
672
|
status: finalStatus,
|
|
539
673
|
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
540
674
|
});
|
|
541
|
-
return this
|
|
675
|
+
return this[$finalResponse];
|
|
542
676
|
}
|
|
543
677
|
const finalHeaders = this.mergeHeaders(headers);
|
|
544
678
|
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
545
|
-
this
|
|
546
|
-
return this
|
|
679
|
+
this[$finalResponse] = new Response(data, { status: finalStatus, headers: finalHeaders });
|
|
680
|
+
return this[$finalResponse];
|
|
547
681
|
}
|
|
548
682
|
/**
|
|
549
683
|
* Respond with HTML content
|
|
@@ -555,9 +689,9 @@ class ShokupanContext {
|
|
|
555
689
|
}
|
|
556
690
|
const finalHeaders = this.mergeHeaders(headers);
|
|
557
691
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
558
|
-
this
|
|
559
|
-
this
|
|
560
|
-
return this
|
|
692
|
+
this[$rawBody] = html;
|
|
693
|
+
this[$finalResponse] = new Response(html, { status: finalStatus, headers: finalHeaders });
|
|
694
|
+
return this[$finalResponse];
|
|
561
695
|
}
|
|
562
696
|
/**
|
|
563
697
|
* Respond with a redirect
|
|
@@ -568,8 +702,8 @@ class ShokupanContext {
|
|
|
568
702
|
}
|
|
569
703
|
const headers = this.mergeHeaders();
|
|
570
704
|
headers.set("Location", url);
|
|
571
|
-
this
|
|
572
|
-
return this
|
|
705
|
+
this[$finalResponse] = new Response(null, { status, headers });
|
|
706
|
+
return this[$finalResponse];
|
|
573
707
|
}
|
|
574
708
|
/**
|
|
575
709
|
* Respond with a status code
|
|
@@ -580,8 +714,8 @@ class ShokupanContext {
|
|
|
580
714
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
581
715
|
}
|
|
582
716
|
const headers = this.mergeHeaders();
|
|
583
|
-
this
|
|
584
|
-
return this
|
|
717
|
+
this[$finalResponse] = new Response(null, { status, headers });
|
|
718
|
+
return this[$finalResponse];
|
|
585
719
|
}
|
|
586
720
|
/**
|
|
587
721
|
* Respond with a file
|
|
@@ -593,21 +727,17 @@ class ShokupanContext {
|
|
|
593
727
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
594
728
|
}
|
|
595
729
|
if (typeof Bun !== "undefined") {
|
|
596
|
-
this
|
|
597
|
-
return this
|
|
730
|
+
this[$finalResponse] = new Response(Bun.file(path, fileOptions), { status, headers });
|
|
731
|
+
return this[$finalResponse];
|
|
598
732
|
} else {
|
|
599
733
|
const fileBuffer = await readFile(path);
|
|
600
734
|
if (fileOptions?.type) {
|
|
601
735
|
headers.set("content-type", fileOptions.type);
|
|
602
736
|
}
|
|
603
|
-
this
|
|
604
|
-
return this
|
|
737
|
+
this[$finalResponse] = new Response(fileBuffer, { status, headers });
|
|
738
|
+
return this[$finalResponse];
|
|
605
739
|
}
|
|
606
740
|
}
|
|
607
|
-
/**
|
|
608
|
-
* JSX Rendering Function
|
|
609
|
-
*/
|
|
610
|
-
renderer;
|
|
611
741
|
/**
|
|
612
742
|
* Render a JSX element
|
|
613
743
|
* @param element JSX Element
|
|
@@ -626,284 +756,67 @@ class ShokupanContext {
|
|
|
626
756
|
return this.html(html, status, headers);
|
|
627
757
|
}
|
|
628
758
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (xForwardedFor && trustedProxies.length > 0) {
|
|
643
|
-
const ips = xForwardedFor.split(",").map((ip) => ip.trim());
|
|
644
|
-
for (let i = ips.length - 1; i >= 0; i--) {
|
|
645
|
-
const ip = ips[i];
|
|
646
|
-
if (!trustedProxies.includes(ip)) {
|
|
647
|
-
if (/^[\d.:a-fA-F]+$/.test(ip)) {
|
|
648
|
-
return ip;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
759
|
+
const compose = (middleware) => {
|
|
760
|
+
if (!middleware.length) {
|
|
761
|
+
return (context2, next) => {
|
|
762
|
+
return next ? next() : Promise.resolve();
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
return function dispatch(context2, next) {
|
|
766
|
+
let index = -1;
|
|
767
|
+
async function runner(i) {
|
|
768
|
+
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
769
|
+
index = i;
|
|
770
|
+
if (i >= middleware.length) {
|
|
771
|
+
return next ? next() : Promise.resolve();
|
|
651
772
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
773
|
+
const fn = middleware[i];
|
|
774
|
+
if (!context2[$debug]) {
|
|
775
|
+
return fn(context2, () => runner(i + 1));
|
|
776
|
+
}
|
|
777
|
+
const debug = context2[$debug];
|
|
778
|
+
const debugId = fn._debugId || fn.name || "anonymous";
|
|
779
|
+
const previousNode = debug.getCurrentNode();
|
|
780
|
+
debug.trackEdge(previousNode, debugId);
|
|
781
|
+
debug.setNode(debugId);
|
|
782
|
+
const start = performance.now();
|
|
783
|
+
try {
|
|
784
|
+
const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
|
|
785
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "success");
|
|
786
|
+
return res;
|
|
787
|
+
} catch (err) {
|
|
788
|
+
debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
|
|
789
|
+
return Promise.reject(err);
|
|
790
|
+
} finally {
|
|
791
|
+
if (previousNode) debug.setNode(previousNode);
|
|
664
792
|
}
|
|
665
793
|
}
|
|
666
|
-
|
|
667
|
-
if (interval.unref) interval.unref();
|
|
668
|
-
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
669
|
-
if (skip(ctx)) return next();
|
|
670
|
-
const key = keyGenerator(ctx);
|
|
671
|
-
const now = Date.now();
|
|
672
|
-
let record = hits.get(key);
|
|
673
|
-
if (!record || record.resetTime <= now) {
|
|
674
|
-
record = {
|
|
675
|
-
hits: 0,
|
|
676
|
-
resetTime: now + windowMs
|
|
677
|
-
};
|
|
678
|
-
hits.set(key, record);
|
|
679
|
-
}
|
|
680
|
-
record.hits++;
|
|
681
|
-
const remaining = Math.max(0, max - record.hits);
|
|
682
|
-
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
683
|
-
const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
|
|
684
|
-
const setHeaders = (res) => {
|
|
685
|
-
if (!headers || !res || !res.headers) return;
|
|
686
|
-
try {
|
|
687
|
-
res.headers.set("X-RateLimit-Limit", String(max));
|
|
688
|
-
res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
689
|
-
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
690
|
-
} catch (e) {
|
|
691
|
-
}
|
|
692
|
-
};
|
|
693
|
-
if (record.hits > max) {
|
|
694
|
-
typeof message === "object" ? JSON.stringify(message) : String(message);
|
|
695
|
-
const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
|
|
696
|
-
if (headers) {
|
|
697
|
-
setHeaders(res);
|
|
698
|
-
res.headers.set("Retry-After", String(retryAfter));
|
|
699
|
-
}
|
|
700
|
-
return res;
|
|
701
|
-
}
|
|
702
|
-
const response = await next();
|
|
703
|
-
if (response instanceof Response && headers) {
|
|
704
|
-
setHeaders(response);
|
|
705
|
-
}
|
|
706
|
-
return response;
|
|
707
|
-
};
|
|
708
|
-
rateLimitMiddleware.isBuiltin = true;
|
|
709
|
-
rateLimitMiddleware.pluginName = "RateLimit";
|
|
710
|
-
return rateLimitMiddleware;
|
|
711
|
-
}
|
|
712
|
-
const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
|
|
713
|
-
const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
|
|
714
|
-
const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
|
|
715
|
-
const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
|
|
716
|
-
const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
|
|
717
|
-
const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
|
|
718
|
-
const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
|
|
719
|
-
const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
|
|
720
|
-
const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
|
|
721
|
-
const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
|
|
722
|
-
const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
|
|
723
|
-
const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
|
|
724
|
-
const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
|
|
725
|
-
const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
|
|
726
|
-
const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
|
|
727
|
-
const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
|
|
728
|
-
var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
729
|
-
RouteParamType2["BODY"] = "BODY";
|
|
730
|
-
RouteParamType2["PARAM"] = "PARAM";
|
|
731
|
-
RouteParamType2["QUERY"] = "QUERY";
|
|
732
|
-
RouteParamType2["HEADER"] = "HEADER";
|
|
733
|
-
RouteParamType2["REQUEST"] = "REQUEST";
|
|
734
|
-
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
735
|
-
return RouteParamType2;
|
|
736
|
-
})(RouteParamType || {});
|
|
737
|
-
function Controller(path = "/") {
|
|
738
|
-
return (target) => {
|
|
739
|
-
target[$controllerPath] = path;
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
function Use(...middleware) {
|
|
743
|
-
return (target, propertyKey, descriptor) => {
|
|
744
|
-
if (!propertyKey) {
|
|
745
|
-
const existing = target[$middleware] || [];
|
|
746
|
-
target[$middleware] = [...existing, ...middleware];
|
|
747
|
-
} else {
|
|
748
|
-
if (!target[$middleware]) {
|
|
749
|
-
target[$middleware] = /* @__PURE__ */ new Map();
|
|
750
|
-
}
|
|
751
|
-
const existing = target[$middleware].get(propertyKey) || [];
|
|
752
|
-
target[$middleware].set(propertyKey, [...existing, ...middleware]);
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
function createParamDecorator(type) {
|
|
757
|
-
return (name) => {
|
|
758
|
-
return (target, propertyKey, parameterIndex) => {
|
|
759
|
-
if (!target[$routeArgs]) {
|
|
760
|
-
target[$routeArgs] = /* @__PURE__ */ new Map();
|
|
761
|
-
}
|
|
762
|
-
if (!target[$routeArgs].has(propertyKey)) {
|
|
763
|
-
target[$routeArgs].set(propertyKey, []);
|
|
764
|
-
}
|
|
765
|
-
target[$routeArgs].get(propertyKey).push({
|
|
766
|
-
index: parameterIndex,
|
|
767
|
-
type,
|
|
768
|
-
name
|
|
769
|
-
});
|
|
770
|
-
};
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
const Body = createParamDecorator(RouteParamType.BODY);
|
|
774
|
-
const Param = createParamDecorator(RouteParamType.PARAM);
|
|
775
|
-
const Query = createParamDecorator(RouteParamType.QUERY);
|
|
776
|
-
const Headers$1 = createParamDecorator(RouteParamType.HEADER);
|
|
777
|
-
const Req = createParamDecorator(RouteParamType.REQUEST);
|
|
778
|
-
const Ctx = createParamDecorator(RouteParamType.CONTEXT);
|
|
779
|
-
function Spec(spec) {
|
|
780
|
-
return (target, propertyKey, descriptor) => {
|
|
781
|
-
if (!target[$routeSpec]) {
|
|
782
|
-
target[$routeSpec] = /* @__PURE__ */ new Map();
|
|
783
|
-
}
|
|
784
|
-
target[$routeSpec].set(propertyKey, spec);
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
function createMethodDecorator(method) {
|
|
788
|
-
return (path = "/") => {
|
|
789
|
-
return (target, propertyKey, descriptor) => {
|
|
790
|
-
if (!target[$routeMethods]) {
|
|
791
|
-
target[$routeMethods] = /* @__PURE__ */ new Map();
|
|
792
|
-
}
|
|
793
|
-
target[$routeMethods].set(propertyKey, {
|
|
794
|
-
method,
|
|
795
|
-
path
|
|
796
|
-
});
|
|
797
|
-
};
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
const Get = createMethodDecorator("GET");
|
|
801
|
-
const Post = createMethodDecorator("POST");
|
|
802
|
-
const Put = createMethodDecorator("PUT");
|
|
803
|
-
const Delete = createMethodDecorator("DELETE");
|
|
804
|
-
const Patch = createMethodDecorator("PATCH");
|
|
805
|
-
const Options = createMethodDecorator("OPTIONS");
|
|
806
|
-
const Head = createMethodDecorator("HEAD");
|
|
807
|
-
const All = createMethodDecorator("ALL");
|
|
808
|
-
function RateLimit(options) {
|
|
809
|
-
return Use(RateLimitMiddleware(options));
|
|
810
|
-
}
|
|
811
|
-
class Container {
|
|
812
|
-
static services = /* @__PURE__ */ new Map();
|
|
813
|
-
static register(target, instance) {
|
|
814
|
-
this.services.set(target, instance);
|
|
815
|
-
}
|
|
816
|
-
static get(target) {
|
|
817
|
-
return this.services.get(target);
|
|
818
|
-
}
|
|
819
|
-
static has(target) {
|
|
820
|
-
return this.services.has(target);
|
|
821
|
-
}
|
|
822
|
-
static resolve(target) {
|
|
823
|
-
if (this.services.has(target)) {
|
|
824
|
-
return this.services.get(target);
|
|
825
|
-
}
|
|
826
|
-
const instance = new target();
|
|
827
|
-
this.services.set(target, instance);
|
|
828
|
-
return instance;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
function Injectable() {
|
|
832
|
-
return (target) => {
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
function Inject(token) {
|
|
836
|
-
return (target, key) => {
|
|
837
|
-
Object.defineProperty(target, key, {
|
|
838
|
-
get: () => Container.resolve(token),
|
|
839
|
-
enumerable: true,
|
|
840
|
-
configurable: true
|
|
841
|
-
});
|
|
794
|
+
return runner(0);
|
|
842
795
|
};
|
|
843
|
-
}
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
async function runner(i) {
|
|
853
|
-
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
854
|
-
index = i;
|
|
855
|
-
if (i >= middleware.length) {
|
|
856
|
-
return next ? next() : Promise.resolve();
|
|
857
|
-
}
|
|
858
|
-
const fn = middleware[i];
|
|
859
|
-
if (!context2._debug) {
|
|
860
|
-
return fn(context2, () => runner(i + 1));
|
|
796
|
+
};
|
|
797
|
+
const tracer = trace.getTracer("shokupan.middleware");
|
|
798
|
+
function traceHandler(fn, name) {
|
|
799
|
+
return async function(...args) {
|
|
800
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
801
|
+
kind: SpanKind.INTERNAL,
|
|
802
|
+
attributes: {
|
|
803
|
+
"http.route": name,
|
|
804
|
+
"component": "shokupan.route"
|
|
861
805
|
}
|
|
862
|
-
|
|
863
|
-
const debugId = fn._debugId || fn.name || "anonymous";
|
|
864
|
-
const previousNode = debug.getCurrentNode();
|
|
865
|
-
debug.trackEdge(previousNode, debugId);
|
|
866
|
-
debug.setNode(debugId);
|
|
867
|
-
const start = performance.now();
|
|
806
|
+
}, async (span) => {
|
|
868
807
|
try {
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
return res;
|
|
808
|
+
const result = await fn.apply(this, args);
|
|
809
|
+
return result;
|
|
872
810
|
} catch (err) {
|
|
873
|
-
|
|
874
|
-
|
|
811
|
+
span.recordException(err);
|
|
812
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
813
|
+
throw err;
|
|
875
814
|
} finally {
|
|
876
|
-
|
|
815
|
+
span.end();
|
|
877
816
|
}
|
|
878
|
-
}
|
|
879
|
-
return runner(0);
|
|
817
|
+
});
|
|
880
818
|
};
|
|
881
|
-
};
|
|
882
|
-
class ShokupanRequestBase {
|
|
883
|
-
method;
|
|
884
|
-
url;
|
|
885
|
-
headers;
|
|
886
|
-
body;
|
|
887
|
-
async json() {
|
|
888
|
-
return JSON.parse(this.body);
|
|
889
|
-
}
|
|
890
|
-
async text() {
|
|
891
|
-
return this.body;
|
|
892
|
-
}
|
|
893
|
-
async formData() {
|
|
894
|
-
if (this.body instanceof FormData) {
|
|
895
|
-
return this.body;
|
|
896
|
-
}
|
|
897
|
-
return new Response(this.body, { headers: this.headers }).formData();
|
|
898
|
-
}
|
|
899
|
-
constructor(props) {
|
|
900
|
-
Object.assign(this, props);
|
|
901
|
-
if (!(this.headers instanceof Headers)) {
|
|
902
|
-
this.headers = new Headers(this.headers);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
819
|
}
|
|
906
|
-
const ShokupanRequest = ShokupanRequestBase;
|
|
907
820
|
function isObject(item) {
|
|
908
821
|
return item && typeof item === "object" && !Array.isArray(item);
|
|
909
822
|
}
|
|
@@ -1129,7 +1042,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1129
1042
|
const defaultTagName = options.defaultTag || "Application";
|
|
1130
1043
|
let astRoutes = [];
|
|
1131
1044
|
try {
|
|
1132
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./
|
|
1045
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-Ce_7JxZh.js");
|
|
1133
1046
|
const analyzer = new OpenAPIAnalyzer2(process.cwd());
|
|
1134
1047
|
const { applications } = await analyzer.analyze();
|
|
1135
1048
|
astRoutes = await getAstRoutes(applications);
|
|
@@ -1318,6 +1231,40 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1318
1231
|
"x-tagGroups": xTagGroups
|
|
1319
1232
|
};
|
|
1320
1233
|
}
|
|
1234
|
+
class RequestContextStore {
|
|
1235
|
+
request;
|
|
1236
|
+
span;
|
|
1237
|
+
}
|
|
1238
|
+
const asyncContext = new AsyncLocalStorage();
|
|
1239
|
+
class HttpError extends Error {
|
|
1240
|
+
status;
|
|
1241
|
+
constructor(message, status) {
|
|
1242
|
+
super(message);
|
|
1243
|
+
this.name = "HttpError";
|
|
1244
|
+
this.status = status;
|
|
1245
|
+
if (Error.captureStackTrace) {
|
|
1246
|
+
Error.captureStackTrace(this, HttpError);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function getErrorStatus(err) {
|
|
1251
|
+
if (!err || typeof err !== "object") {
|
|
1252
|
+
return 500;
|
|
1253
|
+
}
|
|
1254
|
+
if (typeof err.status === "number") {
|
|
1255
|
+
return err.status;
|
|
1256
|
+
}
|
|
1257
|
+
if (typeof err.statusCode === "number") {
|
|
1258
|
+
return err.statusCode;
|
|
1259
|
+
}
|
|
1260
|
+
return 500;
|
|
1261
|
+
}
|
|
1262
|
+
class EventError extends HttpError {
|
|
1263
|
+
constructor(message = "Event Error") {
|
|
1264
|
+
super(message, 500);
|
|
1265
|
+
this.name = "EventError";
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1321
1268
|
const eta$1 = new Eta();
|
|
1322
1269
|
function serveStatic(config, prefix) {
|
|
1323
1270
|
const rootPath = resolve(config.root || ".");
|
|
@@ -1470,14 +1417,162 @@ function serveStatic(config, prefix) {
|
|
|
1470
1417
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1471
1418
|
return serveStaticMiddleware;
|
|
1472
1419
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1420
|
+
const G = globalThis;
|
|
1421
|
+
G.__shokupan_db = G.__shokupan_db || null;
|
|
1422
|
+
G.__shokupan_db_promise = G.__shokupan_db_promise || null;
|
|
1423
|
+
async function ensureDb() {
|
|
1424
|
+
if (G.__shokupan_db) return G.__shokupan_db;
|
|
1425
|
+
if (G.__shokupan_db_promise) return G.__shokupan_db_promise;
|
|
1426
|
+
G.__shokupan_db_promise = (async () => {
|
|
1427
|
+
try {
|
|
1428
|
+
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1429
|
+
const surreal = await import("surrealdb");
|
|
1430
|
+
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1431
|
+
const _db = new Surreal({
|
|
1432
|
+
engines: createNodeEngines()
|
|
1433
|
+
});
|
|
1434
|
+
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1435
|
+
await _db.query(`
|
|
1436
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1437
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1438
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1439
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1440
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1441
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1442
|
+
DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
|
|
1443
|
+
`);
|
|
1444
|
+
G.__shokupan_db = _db;
|
|
1445
|
+
return _db;
|
|
1446
|
+
} catch (e) {
|
|
1447
|
+
G.__shokupan_db_promise = null;
|
|
1448
|
+
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1449
|
+
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1450
|
+
}
|
|
1451
|
+
throw e;
|
|
1452
|
+
}
|
|
1453
|
+
})();
|
|
1454
|
+
return G.__shokupan_db_promise;
|
|
1455
|
+
}
|
|
1456
|
+
const datastore = {
|
|
1457
|
+
async get(recordId) {
|
|
1458
|
+
await ensureDb();
|
|
1459
|
+
return G.__shokupan_db.select(recordId);
|
|
1460
|
+
},
|
|
1461
|
+
async set(recordId, value) {
|
|
1462
|
+
await ensureDb();
|
|
1463
|
+
return G.__shokupan_db.upsert(recordId).content(value);
|
|
1464
|
+
},
|
|
1465
|
+
async query(query, vars) {
|
|
1466
|
+
await ensureDb();
|
|
1467
|
+
try {
|
|
1468
|
+
return G.__shokupan_db.query(query, vars).collect();
|
|
1469
|
+
} catch (e) {
|
|
1470
|
+
console.error("DS ERROR:", e);
|
|
1471
|
+
throw e;
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
get ready() {
|
|
1475
|
+
return ensureDb().then(() => void 0);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
process.on("exit", async () => {
|
|
1479
|
+
if (G.__shokupan_db) await G.__shokupan_db.close();
|
|
1480
|
+
});
|
|
1481
|
+
class Container {
|
|
1482
|
+
static services = /* @__PURE__ */ new Map();
|
|
1483
|
+
static register(target, instance) {
|
|
1484
|
+
this.services.set(target, instance);
|
|
1485
|
+
}
|
|
1486
|
+
static get(target) {
|
|
1487
|
+
return this.services.get(target);
|
|
1488
|
+
}
|
|
1489
|
+
static has(target) {
|
|
1490
|
+
return this.services.has(target);
|
|
1491
|
+
}
|
|
1492
|
+
static resolve(target) {
|
|
1493
|
+
if (this.services.has(target)) {
|
|
1494
|
+
return this.services.get(target);
|
|
1495
|
+
}
|
|
1496
|
+
const instance = new target();
|
|
1497
|
+
this.services.set(target, instance);
|
|
1498
|
+
return instance;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function Injectable() {
|
|
1502
|
+
return (target) => {
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
function Inject(token) {
|
|
1506
|
+
return (target, key) => {
|
|
1507
|
+
Object.defineProperty(target, key, {
|
|
1508
|
+
get: () => Container.resolve(token),
|
|
1509
|
+
enumerable: true,
|
|
1510
|
+
configurable: true
|
|
1511
|
+
});
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
class ShokupanRequestBase {
|
|
1515
|
+
method;
|
|
1516
|
+
url;
|
|
1517
|
+
headers;
|
|
1518
|
+
body;
|
|
1519
|
+
async json() {
|
|
1520
|
+
return JSON.parse(this.body);
|
|
1521
|
+
}
|
|
1522
|
+
async text() {
|
|
1523
|
+
return this.body;
|
|
1524
|
+
}
|
|
1525
|
+
async formData() {
|
|
1526
|
+
if (this.body instanceof FormData) {
|
|
1527
|
+
return this.body;
|
|
1528
|
+
}
|
|
1529
|
+
return new Response(this.body, { headers: this.headers }).formData();
|
|
1530
|
+
}
|
|
1531
|
+
constructor(props) {
|
|
1532
|
+
Object.assign(this, props);
|
|
1533
|
+
if (!(this.headers instanceof Headers)) {
|
|
1534
|
+
this.headers = new Headers(this.headers);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const ShokupanRequest = ShokupanRequestBase;
|
|
1539
|
+
function getCallerInfo(skipFrames = 1) {
|
|
1540
|
+
let file = "unknown";
|
|
1541
|
+
let line = 0;
|
|
1542
|
+
try {
|
|
1543
|
+
const err = new Error();
|
|
1544
|
+
const stack = err.stack?.split("\n") || [];
|
|
1545
|
+
let found = 0;
|
|
1546
|
+
for (let i = 1; i < stack.length; i++) {
|
|
1547
|
+
const l = stack[i];
|
|
1548
|
+
if (!l.includes(":")) continue;
|
|
1549
|
+
if (l.includes("node_modules")) continue;
|
|
1550
|
+
if (l.includes("bun:main")) continue;
|
|
1551
|
+
if (l.includes("src/util/stack.ts")) continue;
|
|
1552
|
+
if (l.includes("src/router.ts")) continue;
|
|
1553
|
+
if (l.includes("src/shokupan.ts")) continue;
|
|
1554
|
+
found++;
|
|
1555
|
+
if (found >= skipFrames) {
|
|
1556
|
+
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
1557
|
+
if (match) {
|
|
1558
|
+
file = match[1];
|
|
1559
|
+
line = parseInt(match[2], 10);
|
|
1560
|
+
return { file, line };
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
} catch (e) {
|
|
1565
|
+
}
|
|
1566
|
+
return { file, line };
|
|
1567
|
+
}
|
|
1568
|
+
class RouterTrie {
|
|
1569
|
+
root;
|
|
1570
|
+
constructor() {
|
|
1571
|
+
this.root = this.createNode();
|
|
1572
|
+
}
|
|
1573
|
+
createNode() {
|
|
1574
|
+
return {
|
|
1575
|
+
children: {}
|
|
1481
1576
|
};
|
|
1482
1577
|
}
|
|
1483
1578
|
insert(method, path, handler) {
|
|
@@ -1570,124 +1665,16 @@ class RouterTrie {
|
|
|
1570
1665
|
return s.split("/");
|
|
1571
1666
|
}
|
|
1572
1667
|
}
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
const surreal = await import("surrealdb");
|
|
1584
|
-
const Surreal = surreal.Surreal;
|
|
1585
|
-
RecordId = surreal.RecordId;
|
|
1586
|
-
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1587
|
-
const _db = new Surreal({
|
|
1588
|
-
engines: createNodeEngines()
|
|
1589
|
-
});
|
|
1590
|
-
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1591
|
-
await _db.query(`
|
|
1592
|
-
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1593
|
-
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1594
|
-
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1595
|
-
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1596
|
-
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1597
|
-
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1598
|
-
`);
|
|
1599
|
-
db = _db;
|
|
1600
|
-
return db;
|
|
1601
|
-
} catch (e) {
|
|
1602
|
-
dbPromise = null;
|
|
1603
|
-
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1604
|
-
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1605
|
-
}
|
|
1606
|
-
throw e;
|
|
1607
|
-
}
|
|
1608
|
-
})();
|
|
1609
|
-
return dbPromise;
|
|
1610
|
-
}
|
|
1611
|
-
const datastore = {
|
|
1612
|
-
async get(store, key) {
|
|
1613
|
-
await ensureDb();
|
|
1614
|
-
return db.select(new RecordId(store, key));
|
|
1615
|
-
},
|
|
1616
|
-
async set(store, key, value) {
|
|
1617
|
-
await ensureDb();
|
|
1618
|
-
return db.create(new RecordId(store, key)).content(value);
|
|
1619
|
-
},
|
|
1620
|
-
async query(query, vars) {
|
|
1621
|
-
await ensureDb();
|
|
1622
|
-
try {
|
|
1623
|
-
const r = await db.query(query, vars);
|
|
1624
|
-
return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
|
|
1625
|
-
} catch (e) {
|
|
1626
|
-
console.error("DS ERROR:", e);
|
|
1627
|
-
throw e;
|
|
1628
|
-
}
|
|
1629
|
-
},
|
|
1630
|
-
get ready() {
|
|
1631
|
-
return ensureDb().then(() => void 0);
|
|
1632
|
-
}
|
|
1633
|
-
};
|
|
1634
|
-
process.on("exit", async () => {
|
|
1635
|
-
if (db) await db.close();
|
|
1636
|
-
});
|
|
1637
|
-
const tracer = trace.getTracer("shokupan.middleware");
|
|
1638
|
-
function traceHandler(fn, name) {
|
|
1639
|
-
return async function(...args) {
|
|
1640
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1641
|
-
kind: SpanKind.INTERNAL,
|
|
1642
|
-
attributes: {
|
|
1643
|
-
"http.route": name,
|
|
1644
|
-
"component": "shokupan.route"
|
|
1645
|
-
}
|
|
1646
|
-
}, async (span) => {
|
|
1647
|
-
try {
|
|
1648
|
-
const result = await fn.apply(this, args);
|
|
1649
|
-
return result;
|
|
1650
|
-
} catch (err) {
|
|
1651
|
-
span.recordException(err);
|
|
1652
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
1653
|
-
throw err;
|
|
1654
|
-
} finally {
|
|
1655
|
-
span.end();
|
|
1656
|
-
}
|
|
1657
|
-
});
|
|
1658
|
-
};
|
|
1659
|
-
}
|
|
1660
|
-
function getCallerInfo(skipFrames = 1) {
|
|
1661
|
-
let file = "unknown";
|
|
1662
|
-
let line = 0;
|
|
1663
|
-
try {
|
|
1664
|
-
const err = new Error();
|
|
1665
|
-
const stack = err.stack?.split("\n") || [];
|
|
1666
|
-
let found = 0;
|
|
1667
|
-
for (let i = 1; i < stack.length; i++) {
|
|
1668
|
-
const l = stack[i];
|
|
1669
|
-
if (!l.includes(":")) continue;
|
|
1670
|
-
if (l.includes("node_modules")) continue;
|
|
1671
|
-
if (l.includes("bun:main")) continue;
|
|
1672
|
-
if (l.includes("src/util/stack.ts")) continue;
|
|
1673
|
-
if (l.includes("src/router.ts")) continue;
|
|
1674
|
-
if (l.includes("src/shokupan.ts")) continue;
|
|
1675
|
-
found++;
|
|
1676
|
-
if (found >= skipFrames) {
|
|
1677
|
-
const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
|
|
1678
|
-
if (match) {
|
|
1679
|
-
file = match[1];
|
|
1680
|
-
line = parseInt(match[2], 10);
|
|
1681
|
-
return { file, line };
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
} catch (e) {
|
|
1686
|
-
}
|
|
1687
|
-
return { file, line };
|
|
1688
|
-
}
|
|
1689
|
-
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1690
|
-
const ShokupanApplicationTree = {};
|
|
1668
|
+
const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
|
|
1669
|
+
var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
1670
|
+
RouteParamType2["BODY"] = "BODY";
|
|
1671
|
+
RouteParamType2["PARAM"] = "PARAM";
|
|
1672
|
+
RouteParamType2["QUERY"] = "QUERY";
|
|
1673
|
+
RouteParamType2["HEADER"] = "HEADER";
|
|
1674
|
+
RouteParamType2["REQUEST"] = "REQUEST";
|
|
1675
|
+
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
1676
|
+
return RouteParamType2;
|
|
1677
|
+
})(RouteParamType || {});
|
|
1691
1678
|
class ShokupanRouter {
|
|
1692
1679
|
constructor(config) {
|
|
1693
1680
|
this.config = config;
|
|
@@ -1720,6 +1707,7 @@ class ShokupanRouter {
|
|
|
1720
1707
|
metadata;
|
|
1721
1708
|
// Metadata for the router itself
|
|
1722
1709
|
currentGuards = [];
|
|
1710
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
1723
1711
|
// Registry Accessor
|
|
1724
1712
|
getComponentRegistry() {
|
|
1725
1713
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
@@ -1781,233 +1769,54 @@ class ShokupanRouter {
|
|
|
1781
1769
|
return typeof target === "object" && target !== null && $isRouter in target;
|
|
1782
1770
|
}
|
|
1783
1771
|
/**
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
* Controller can be a convection router or an arbitrary class.
|
|
1787
|
-
*
|
|
1788
|
-
* Routes are derived from method names:
|
|
1789
|
-
* - get(ctx) -> GET /prefix/
|
|
1790
|
-
* - getUsers(ctx) -> GET /prefix/users
|
|
1791
|
-
* - postCreate(ctx) -> POST /prefix/create
|
|
1772
|
+
* Registers an event handler for WebSocket.
|
|
1792
1773
|
*/
|
|
1793
|
-
|
|
1794
|
-
|
|
1774
|
+
event(name, handler) {
|
|
1775
|
+
if (this.eventHandlers.has(name)) {
|
|
1776
|
+
const err = new EventError(`Event handler \`${name}\` already exists.`);
|
|
1777
|
+
console.warn(err);
|
|
1778
|
+
const handlers = this.eventHandlers.get(name);
|
|
1779
|
+
handlers.push(handler);
|
|
1780
|
+
this.eventHandlers.set(name, handlers);
|
|
1781
|
+
} else {
|
|
1782
|
+
this.eventHandlers.set(name, [handler]);
|
|
1783
|
+
}
|
|
1784
|
+
return this;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Finds an event handler(s) by name.
|
|
1788
|
+
*/
|
|
1789
|
+
findEvent(name) {
|
|
1790
|
+
if (this.eventHandlers.has(name)) {
|
|
1791
|
+
return this.eventHandlers.get(name);
|
|
1792
|
+
}
|
|
1793
|
+
for (const child of this[$childRouters]) {
|
|
1794
|
+
const handler = child.findEvent(name);
|
|
1795
|
+
if (handler) return handler;
|
|
1796
|
+
}
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Mounts a controller instance to a path prefix.
|
|
1801
|
+
*
|
|
1802
|
+
* Controller can be a convection router or an arbitrary class.
|
|
1803
|
+
*
|
|
1804
|
+
* Routes are derived from method names:
|
|
1805
|
+
* - get(ctx) -> GET /prefix/
|
|
1806
|
+
* - getUsers(ctx) -> GET /prefix/users
|
|
1807
|
+
* - postCreate(ctx) -> POST /prefix/create
|
|
1808
|
+
*/
|
|
1809
|
+
mount(prefix, controller) {
|
|
1810
|
+
const isRouter = this.isRouterInstance(controller);
|
|
1795
1811
|
const isFunction = typeof controller === "function";
|
|
1796
1812
|
const controllersOnly = this.config?.controllersOnly ?? this.rootConfig?.controllersOnly ?? false;
|
|
1797
1813
|
if (controllersOnly && !isFunction && !isRouter) {
|
|
1798
1814
|
throw new Error(`[Shokupan] strict controller check failed: ${controller.constructor.name || typeof controller} is not a class constructor.`);
|
|
1799
1815
|
}
|
|
1800
1816
|
if (this.isRouterInstance(controller)) {
|
|
1801
|
-
|
|
1802
|
-
throw new Error("Router is already mounted");
|
|
1803
|
-
}
|
|
1804
|
-
controller[$mountPath] = prefix;
|
|
1805
|
-
if (!controller.metadata) {
|
|
1806
|
-
const info = getCallerInfo();
|
|
1807
|
-
controller.metadata = {
|
|
1808
|
-
file: info.file,
|
|
1809
|
-
line: info.line,
|
|
1810
|
-
name: "MountedRouter"
|
|
1811
|
-
};
|
|
1812
|
-
}
|
|
1813
|
-
this[$childRouters].push(controller);
|
|
1814
|
-
controller[$parent] = this;
|
|
1815
|
-
const setRouterContext = (router) => {
|
|
1816
|
-
router[$appRoot] = this.root;
|
|
1817
|
-
router[$childRouters].forEach((child) => setRouterContext(child));
|
|
1818
|
-
};
|
|
1819
|
-
setRouterContext(controller);
|
|
1820
|
-
if (this[$appRoot]) ;
|
|
1821
|
-
controller[$appRoot] = this.root;
|
|
1822
|
-
controller[$isMounted] = true;
|
|
1817
|
+
this.mountRouter(prefix, controller);
|
|
1823
1818
|
} else {
|
|
1824
|
-
|
|
1825
|
-
if (typeof controller === "function") {
|
|
1826
|
-
instance = Container.resolve(controller);
|
|
1827
|
-
const controllerPath = controller[$controllerPath];
|
|
1828
|
-
if (controllerPath) {
|
|
1829
|
-
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1830
|
-
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
1831
|
-
prefix = p1 + p2;
|
|
1832
|
-
if (!prefix) prefix = "/";
|
|
1833
|
-
}
|
|
1834
|
-
} else {
|
|
1835
|
-
const ctor = instance.constructor;
|
|
1836
|
-
const controllerPath = ctor[$controllerPath];
|
|
1837
|
-
if (controllerPath) {
|
|
1838
|
-
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1839
|
-
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
1840
|
-
prefix = p1 + p2;
|
|
1841
|
-
if (!prefix) prefix = "/";
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
instance[$mountPath] = prefix;
|
|
1845
|
-
const info = getCallerInfo();
|
|
1846
|
-
instance.metadata = {
|
|
1847
|
-
file: info.file,
|
|
1848
|
-
line: info.line,
|
|
1849
|
-
name: instance.constructor.name
|
|
1850
|
-
};
|
|
1851
|
-
this[$childControllers].push(instance);
|
|
1852
|
-
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
1853
|
-
const proto = Object.getPrototypeOf(instance);
|
|
1854
|
-
const methods = /* @__PURE__ */ new Set();
|
|
1855
|
-
let current = proto;
|
|
1856
|
-
while (current && current !== Object.prototype) {
|
|
1857
|
-
Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
|
|
1858
|
-
current = Object.getPrototypeOf(current);
|
|
1859
|
-
}
|
|
1860
|
-
Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
|
|
1861
|
-
const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
|
|
1862
|
-
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
1863
|
-
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
1864
|
-
let routesAttached = 0;
|
|
1865
|
-
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
1866
|
-
const name = Array.from(methods)[i];
|
|
1867
|
-
if (name === "constructor") continue;
|
|
1868
|
-
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
1869
|
-
const originalHandler = instance[name];
|
|
1870
|
-
if (typeof originalHandler !== "function") continue;
|
|
1871
|
-
let method;
|
|
1872
|
-
let subPath = "";
|
|
1873
|
-
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
1874
|
-
const config = decoratedRoutes.get(name);
|
|
1875
|
-
method = config.method;
|
|
1876
|
-
subPath = config.path;
|
|
1877
|
-
} else {
|
|
1878
|
-
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
1879
|
-
const m = HTTPMethods[j];
|
|
1880
|
-
if (name.toUpperCase().startsWith(m)) {
|
|
1881
|
-
method = m;
|
|
1882
|
-
const rest = name.slice(m.length);
|
|
1883
|
-
if (rest.length === 0) {
|
|
1884
|
-
subPath = "/";
|
|
1885
|
-
} else {
|
|
1886
|
-
subPath = "";
|
|
1887
|
-
let buffer = "";
|
|
1888
|
-
const flush = () => {
|
|
1889
|
-
if (buffer.length > 0) {
|
|
1890
|
-
subPath += "/" + buffer.toLowerCase();
|
|
1891
|
-
buffer = "";
|
|
1892
|
-
}
|
|
1893
|
-
};
|
|
1894
|
-
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
1895
|
-
const char = rest[i2];
|
|
1896
|
-
if (char === "$") {
|
|
1897
|
-
flush();
|
|
1898
|
-
subPath += "/:";
|
|
1899
|
-
continue;
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
|
|
1903
|
-
if (!subPath.startsWith("/")) {
|
|
1904
|
-
subPath = "/" + subPath;
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
break;
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
if (method) {
|
|
1912
|
-
routesAttached++;
|
|
1913
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1914
|
-
const cleanSubPath = subPath === "/" ? "" : subPath;
|
|
1915
|
-
let joined;
|
|
1916
|
-
if (cleanSubPath.length === 0) {
|
|
1917
|
-
joined = cleanPrefix;
|
|
1918
|
-
} else if (cleanSubPath.startsWith("/")) {
|
|
1919
|
-
joined = cleanPrefix + cleanSubPath;
|
|
1920
|
-
} else {
|
|
1921
|
-
joined = cleanPrefix + "/" + cleanSubPath;
|
|
1922
|
-
}
|
|
1923
|
-
const fullPath = joined || "/";
|
|
1924
|
-
const normalizedPath = fullPath.replace(/\/+/g, "/");
|
|
1925
|
-
const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
|
|
1926
|
-
const allMiddleware = [...controllerMiddleware, ...methodMw];
|
|
1927
|
-
const routeArgs = decoratedArgs && decoratedArgs.get(name);
|
|
1928
|
-
const wrappedHandler = async (ctx) => {
|
|
1929
|
-
let args = [ctx];
|
|
1930
|
-
if (routeArgs?.length > 0) {
|
|
1931
|
-
args = [];
|
|
1932
|
-
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
1933
|
-
for (let k = 0; k < sortedArgs.length; k++) {
|
|
1934
|
-
const arg = sortedArgs[k];
|
|
1935
|
-
switch (arg.type) {
|
|
1936
|
-
case RouteParamType.BODY:
|
|
1937
|
-
try {
|
|
1938
|
-
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
1939
|
-
args[arg.index] = await ctx.req.json();
|
|
1940
|
-
} else {
|
|
1941
|
-
const text = await ctx.req.text();
|
|
1942
|
-
if (!text) {
|
|
1943
|
-
args[arg.index] = {};
|
|
1944
|
-
} else {
|
|
1945
|
-
args[arg.index] = JSON.parse(text);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
} catch (e) {
|
|
1949
|
-
const err = new Error("Invalid JSON body");
|
|
1950
|
-
err.status = 400;
|
|
1951
|
-
throw err;
|
|
1952
|
-
}
|
|
1953
|
-
break;
|
|
1954
|
-
case RouteParamType.PARAM:
|
|
1955
|
-
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
1956
|
-
break;
|
|
1957
|
-
case RouteParamType.QUERY: {
|
|
1958
|
-
const url = new URL(ctx.req.url);
|
|
1959
|
-
if (arg.name) {
|
|
1960
|
-
const vals = url.searchParams.getAll(arg.name);
|
|
1961
|
-
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
1962
|
-
} else {
|
|
1963
|
-
const query = {};
|
|
1964
|
-
const keys = Object.keys(url.searchParams);
|
|
1965
|
-
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
1966
|
-
const key = keys[k2];
|
|
1967
|
-
const vals = url.searchParams.getAll(key);
|
|
1968
|
-
query[key] = vals.length > 1 ? vals : vals[0];
|
|
1969
|
-
}
|
|
1970
|
-
args[arg.index] = query;
|
|
1971
|
-
}
|
|
1972
|
-
break;
|
|
1973
|
-
}
|
|
1974
|
-
case RouteParamType.HEADER:
|
|
1975
|
-
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
1976
|
-
break;
|
|
1977
|
-
case RouteParamType.REQUEST:
|
|
1978
|
-
args[arg.index] = ctx.req;
|
|
1979
|
-
break;
|
|
1980
|
-
case RouteParamType.CONTEXT:
|
|
1981
|
-
args[arg.index] = ctx;
|
|
1982
|
-
break;
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
|
|
1987
|
-
return tracedOriginalHandler.apply(instance, args);
|
|
1988
|
-
};
|
|
1989
|
-
let finalHandler = wrappedHandler;
|
|
1990
|
-
if (allMiddleware.length > 0) {
|
|
1991
|
-
const composed = compose(allMiddleware);
|
|
1992
|
-
finalHandler = async (ctx) => {
|
|
1993
|
-
return composed(ctx, () => wrappedHandler(ctx));
|
|
1994
|
-
};
|
|
1995
|
-
}
|
|
1996
|
-
finalHandler.originalHandler = originalHandler;
|
|
1997
|
-
if (finalHandler !== wrappedHandler) {
|
|
1998
|
-
wrappedHandler.originalHandler = originalHandler;
|
|
1999
|
-
}
|
|
2000
|
-
const tagName = instance.constructor.name;
|
|
2001
|
-
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2002
|
-
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2003
|
-
const spec = { tags: [tagName], ...userSpec };
|
|
2004
|
-
this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
if (routesAttached === 0) {
|
|
2008
|
-
console.warn(`No routes attached to controller ${instance.constructor.name}`);
|
|
2009
|
-
}
|
|
2010
|
-
instance[$isMounted] = true;
|
|
1819
|
+
this.scanControllerRoutes(prefix, controller);
|
|
2011
1820
|
}
|
|
2012
1821
|
return this;
|
|
2013
1822
|
}
|
|
@@ -2045,8 +1854,6 @@ class ShokupanRouter {
|
|
|
2045
1854
|
*/
|
|
2046
1855
|
async internalRequest(arg) {
|
|
2047
1856
|
const options = typeof arg === "string" ? { path: arg } : arg;
|
|
2048
|
-
const store = asyncContext.getStore();
|
|
2049
|
-
store?.get("req");
|
|
2050
1857
|
let url = options.path;
|
|
2051
1858
|
if (!url.startsWith("http")) {
|
|
2052
1859
|
const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig.port || 3e3}`;
|
|
@@ -2089,7 +1896,7 @@ class ShokupanRouter {
|
|
|
2089
1896
|
});
|
|
2090
1897
|
const ctx = new ShokupanContext(req);
|
|
2091
1898
|
let result = null;
|
|
2092
|
-
let status =
|
|
1899
|
+
let status = HTTP_STATUS.OK;
|
|
2093
1900
|
const headers = {};
|
|
2094
1901
|
const match = this.find(req.method, ctx.path);
|
|
2095
1902
|
if (match) {
|
|
@@ -2098,12 +1905,12 @@ class ShokupanRouter {
|
|
|
2098
1905
|
result = await match.handler(ctx);
|
|
2099
1906
|
} catch (err) {
|
|
2100
1907
|
console.error(err);
|
|
2101
|
-
status = err
|
|
1908
|
+
status = getErrorStatus(err);
|
|
2102
1909
|
result = { error: err.message || "Internal Server Error" };
|
|
2103
1910
|
if (err.errors) result.errors = err.errors;
|
|
2104
1911
|
}
|
|
2105
1912
|
} else {
|
|
2106
|
-
status =
|
|
1913
|
+
status = HTTP_STATUS.NOT_FOUND;
|
|
2107
1914
|
result = "Not Found";
|
|
2108
1915
|
}
|
|
2109
1916
|
if (result instanceof Response) {
|
|
@@ -2132,7 +1939,7 @@ class ShokupanRouter {
|
|
|
2132
1939
|
const originalHandler = handler;
|
|
2133
1940
|
const wrapped = async (ctx) => {
|
|
2134
1941
|
await this.runHooks("onRequestStart", ctx);
|
|
2135
|
-
const debug = ctx
|
|
1942
|
+
const debug = ctx[$debug];
|
|
2136
1943
|
let debugId;
|
|
2137
1944
|
let previousNode;
|
|
2138
1945
|
if (debug) {
|
|
@@ -2158,6 +1965,254 @@ class ShokupanRouter {
|
|
|
2158
1965
|
wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
|
|
2159
1966
|
return wrapped;
|
|
2160
1967
|
}
|
|
1968
|
+
mountRouter(prefix, router) {
|
|
1969
|
+
if (router[$isMounted]) {
|
|
1970
|
+
throw new Error("Router is already mounted");
|
|
1971
|
+
}
|
|
1972
|
+
router[$mountPath] = prefix;
|
|
1973
|
+
if (!router.metadata) {
|
|
1974
|
+
const info = getCallerInfo();
|
|
1975
|
+
router.metadata = {
|
|
1976
|
+
file: info.file,
|
|
1977
|
+
line: info.line,
|
|
1978
|
+
name: "MountedRouter"
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
this[$childRouters].push(router);
|
|
1982
|
+
router[$parent] = this;
|
|
1983
|
+
const setRouterContext = (router2) => {
|
|
1984
|
+
router2[$appRoot] = this.root;
|
|
1985
|
+
router2[$childRouters].forEach((child) => setRouterContext(child));
|
|
1986
|
+
};
|
|
1987
|
+
setRouterContext(router);
|
|
1988
|
+
router[$appRoot] = this.root;
|
|
1989
|
+
router[$isMounted] = true;
|
|
1990
|
+
}
|
|
1991
|
+
scanControllerRoutes(prefix, controller) {
|
|
1992
|
+
let instance = controller;
|
|
1993
|
+
if (typeof controller === "function") {
|
|
1994
|
+
instance = Container.resolve(controller);
|
|
1995
|
+
const controllerPath = controller[$controllerPath];
|
|
1996
|
+
if (controllerPath) {
|
|
1997
|
+
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1998
|
+
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
1999
|
+
prefix = p1 + p2;
|
|
2000
|
+
if (!prefix) prefix = "/";
|
|
2001
|
+
}
|
|
2002
|
+
} else {
|
|
2003
|
+
const ctor = instance.constructor;
|
|
2004
|
+
const controllerPath = ctor[$controllerPath];
|
|
2005
|
+
if (controllerPath) {
|
|
2006
|
+
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2007
|
+
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
2008
|
+
prefix = p1 + p2;
|
|
2009
|
+
if (!prefix) prefix = "/";
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
instance[$mountPath] = prefix;
|
|
2013
|
+
const info = getCallerInfo();
|
|
2014
|
+
instance.metadata = {
|
|
2015
|
+
file: info.file,
|
|
2016
|
+
line: info.line,
|
|
2017
|
+
name: instance.constructor.name
|
|
2018
|
+
};
|
|
2019
|
+
this[$childControllers].push(instance);
|
|
2020
|
+
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
2021
|
+
const proto = Object.getPrototypeOf(instance);
|
|
2022
|
+
const methods = /* @__PURE__ */ new Set();
|
|
2023
|
+
let current = proto;
|
|
2024
|
+
while (current && current !== Object.prototype) {
|
|
2025
|
+
Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
|
|
2026
|
+
current = Object.getPrototypeOf(current);
|
|
2027
|
+
}
|
|
2028
|
+
Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
|
|
2029
|
+
const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
|
|
2030
|
+
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
2031
|
+
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
2032
|
+
const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
|
|
2033
|
+
let routesAttached = 0;
|
|
2034
|
+
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
2035
|
+
const name = Array.from(methods)[i];
|
|
2036
|
+
if (name === "constructor") continue;
|
|
2037
|
+
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
2038
|
+
const originalHandler = instance[name];
|
|
2039
|
+
if (typeof originalHandler !== "function") continue;
|
|
2040
|
+
let method;
|
|
2041
|
+
let subPath = "";
|
|
2042
|
+
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
2043
|
+
const config = decoratedRoutes.get(name);
|
|
2044
|
+
method = config.method;
|
|
2045
|
+
subPath = config.path;
|
|
2046
|
+
} else {
|
|
2047
|
+
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
2048
|
+
const m = HTTPMethods[j];
|
|
2049
|
+
if (name.toUpperCase().startsWith(m)) {
|
|
2050
|
+
method = m;
|
|
2051
|
+
const rest = name.slice(m.length);
|
|
2052
|
+
if (rest.length === 0) {
|
|
2053
|
+
subPath = "/";
|
|
2054
|
+
} else {
|
|
2055
|
+
subPath = "";
|
|
2056
|
+
let buffer = "";
|
|
2057
|
+
const flush = () => {
|
|
2058
|
+
if (buffer.length > 0) {
|
|
2059
|
+
subPath += "/" + buffer.toLowerCase();
|
|
2060
|
+
buffer = "";
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
2064
|
+
const char = rest[i2];
|
|
2065
|
+
if (char === "$") {
|
|
2066
|
+
flush();
|
|
2067
|
+
subPath += "/:";
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
buffer += char;
|
|
2071
|
+
}
|
|
2072
|
+
if (buffer.length > 0) flush();
|
|
2073
|
+
subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
|
|
2074
|
+
if (!subPath.startsWith("/")) {
|
|
2075
|
+
subPath = "/" + subPath;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
break;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
if (method) {
|
|
2083
|
+
routesAttached++;
|
|
2084
|
+
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2085
|
+
const cleanSubPath = subPath === "/" ? "" : subPath;
|
|
2086
|
+
let joined;
|
|
2087
|
+
if (cleanSubPath.length === 0) {
|
|
2088
|
+
joined = cleanPrefix;
|
|
2089
|
+
} else if (cleanSubPath.startsWith("/")) {
|
|
2090
|
+
joined = cleanPrefix + cleanSubPath;
|
|
2091
|
+
} else {
|
|
2092
|
+
joined = cleanPrefix + "/" + cleanSubPath;
|
|
2093
|
+
}
|
|
2094
|
+
const fullPath = joined || "/";
|
|
2095
|
+
const normalizedPath = fullPath.replace(/\/+/g, "/");
|
|
2096
|
+
const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
|
|
2097
|
+
const allMiddleware = [...controllerMiddleware, ...methodMw];
|
|
2098
|
+
const routeArgs = decoratedArgs && decoratedArgs.get(name);
|
|
2099
|
+
const wrappedHandler = async (ctx) => {
|
|
2100
|
+
let args = [ctx];
|
|
2101
|
+
if (routeArgs?.length > 0) {
|
|
2102
|
+
args = [];
|
|
2103
|
+
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
2104
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
2105
|
+
const arg = sortedArgs[k];
|
|
2106
|
+
switch (arg.type) {
|
|
2107
|
+
case RouteParamType.BODY:
|
|
2108
|
+
try {
|
|
2109
|
+
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
2110
|
+
args[arg.index] = await ctx.req.json();
|
|
2111
|
+
} else {
|
|
2112
|
+
const text = await ctx.req.text();
|
|
2113
|
+
if (!text) {
|
|
2114
|
+
args[arg.index] = {};
|
|
2115
|
+
} else {
|
|
2116
|
+
args[arg.index] = JSON.parse(text);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
} catch (e) {
|
|
2120
|
+
const err = new Error("Invalid JSON body");
|
|
2121
|
+
err.status = 400;
|
|
2122
|
+
throw err;
|
|
2123
|
+
}
|
|
2124
|
+
break;
|
|
2125
|
+
case RouteParamType.PARAM:
|
|
2126
|
+
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
2127
|
+
break;
|
|
2128
|
+
case RouteParamType.QUERY: {
|
|
2129
|
+
const url = new URL(ctx.req.url);
|
|
2130
|
+
if (arg.name) {
|
|
2131
|
+
const vals = url.searchParams.getAll(arg.name);
|
|
2132
|
+
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
2133
|
+
} else {
|
|
2134
|
+
const query = {};
|
|
2135
|
+
const keys = Object.keys(url.searchParams);
|
|
2136
|
+
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
2137
|
+
const key = keys[k2];
|
|
2138
|
+
const vals = url.searchParams.getAll(key);
|
|
2139
|
+
query[key] = vals.length > 1 ? vals : vals[0];
|
|
2140
|
+
}
|
|
2141
|
+
args[arg.index] = query;
|
|
2142
|
+
}
|
|
2143
|
+
break;
|
|
2144
|
+
}
|
|
2145
|
+
case RouteParamType.HEADER:
|
|
2146
|
+
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2147
|
+
break;
|
|
2148
|
+
case RouteParamType.REQUEST:
|
|
2149
|
+
args[arg.index] = ctx.req;
|
|
2150
|
+
break;
|
|
2151
|
+
case RouteParamType.CONTEXT:
|
|
2152
|
+
args[arg.index] = ctx;
|
|
2153
|
+
break;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
|
|
2158
|
+
return tracedOriginalHandler.apply(instance, args);
|
|
2159
|
+
};
|
|
2160
|
+
let finalHandler = wrappedHandler;
|
|
2161
|
+
if (allMiddleware.length > 0) {
|
|
2162
|
+
const composed = compose(allMiddleware);
|
|
2163
|
+
finalHandler = async (ctx) => {
|
|
2164
|
+
return composed(ctx, () => wrappedHandler(ctx));
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
finalHandler.originalHandler = originalHandler;
|
|
2168
|
+
if (finalHandler !== wrappedHandler) {
|
|
2169
|
+
wrappedHandler.originalHandler = originalHandler;
|
|
2170
|
+
}
|
|
2171
|
+
const tagName = instance.constructor.name;
|
|
2172
|
+
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2173
|
+
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2174
|
+
const spec = { tags: [tagName], ...userSpec };
|
|
2175
|
+
this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
|
|
2176
|
+
}
|
|
2177
|
+
if (decoratedEvents?.has(name)) {
|
|
2178
|
+
routesAttached++;
|
|
2179
|
+
const config = decoratedEvents.get(name);
|
|
2180
|
+
const routeArgs = decoratedArgs?.get(name);
|
|
2181
|
+
const wrappedHandler = async (ctx) => {
|
|
2182
|
+
let args = [ctx];
|
|
2183
|
+
if (routeArgs?.length > 0) {
|
|
2184
|
+
args = [];
|
|
2185
|
+
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
2186
|
+
for (let k = 0; k < sortedArgs.length; k++) {
|
|
2187
|
+
const arg = sortedArgs[k];
|
|
2188
|
+
switch (arg.type) {
|
|
2189
|
+
case RouteParamType.BODY:
|
|
2190
|
+
args[arg.index] = await ctx.body();
|
|
2191
|
+
break;
|
|
2192
|
+
case RouteParamType.CONTEXT:
|
|
2193
|
+
args[arg.index] = ctx;
|
|
2194
|
+
break;
|
|
2195
|
+
case RouteParamType.REQUEST:
|
|
2196
|
+
args[arg.index] = ctx.req;
|
|
2197
|
+
break;
|
|
2198
|
+
case RouteParamType.HEADER:
|
|
2199
|
+
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2200
|
+
break;
|
|
2201
|
+
default:
|
|
2202
|
+
args[arg.index] = void 0;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return originalHandler.apply(instance, args);
|
|
2207
|
+
};
|
|
2208
|
+
this.event(config.eventName, wrappedHandler);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (routesAttached === 0) {
|
|
2212
|
+
console.warn(`No routes attached to controller ${instance.constructor.name}`);
|
|
2213
|
+
}
|
|
2214
|
+
instance[$isMounted] = true;
|
|
2215
|
+
}
|
|
2161
2216
|
/**
|
|
2162
2217
|
* Find a route matching the given method and path.
|
|
2163
2218
|
* @param method HTTP method
|
|
@@ -2284,7 +2339,7 @@ class ShokupanRouter {
|
|
|
2284
2339
|
if (effectiveRenderer) {
|
|
2285
2340
|
const innerHandler = wrappedHandler;
|
|
2286
2341
|
wrappedHandler = async (ctx) => {
|
|
2287
|
-
ctx.
|
|
2342
|
+
ctx.setRenderer(effectiveRenderer);
|
|
2288
2343
|
return innerHandler(ctx);
|
|
2289
2344
|
};
|
|
2290
2345
|
}
|
|
@@ -2315,8 +2370,10 @@ class ShokupanRouter {
|
|
|
2315
2370
|
Promise.resolve().then(async () => {
|
|
2316
2371
|
try {
|
|
2317
2372
|
const timestamp = Date.now();
|
|
2318
|
-
|
|
2319
|
-
|
|
2373
|
+
await datastore.set(new RecordId("middleware_tracking", {
|
|
2374
|
+
timestamp,
|
|
2375
|
+
name: handler.name || "anonymous"
|
|
2376
|
+
}), {
|
|
2320
2377
|
name: handler.name || "anonymous",
|
|
2321
2378
|
path: ctx.path,
|
|
2322
2379
|
timestamp,
|
|
@@ -2334,7 +2391,7 @@ class ShokupanRouter {
|
|
|
2334
2391
|
const cutoff = Date.now() - ttl;
|
|
2335
2392
|
await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2336
2393
|
const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2337
|
-
if (results
|
|
2394
|
+
if (results?.[0]?.count > maxCapacity) {
|
|
2338
2395
|
const toDelete = results[0].count - maxCapacity;
|
|
2339
2396
|
await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2340
2397
|
}
|
|
@@ -2411,7 +2468,7 @@ class ShokupanRouter {
|
|
|
2411
2468
|
(l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
|
|
2412
2469
|
);
|
|
2413
2470
|
if (callerLine) {
|
|
2414
|
-
const match = callerLine.match(/\((
|
|
2471
|
+
const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
|
|
2415
2472
|
if (match) {
|
|
2416
2473
|
file = match[1];
|
|
2417
2474
|
line = parseInt(match[2], 10);
|
|
@@ -2429,7 +2486,7 @@ class ShokupanRouter {
|
|
|
2429
2486
|
}
|
|
2430
2487
|
return guardHandler(ctx, next);
|
|
2431
2488
|
};
|
|
2432
|
-
trackedGuard.originalHandler = guardHandler.originalHandler
|
|
2489
|
+
trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
|
|
2433
2490
|
this.currentGuards.push({ handler: trackedGuard, spec });
|
|
2434
2491
|
return this;
|
|
2435
2492
|
}
|
|
@@ -2542,7 +2599,7 @@ class ShokupanRouter {
|
|
|
2542
2599
|
const fns = this.hookCache.get(name);
|
|
2543
2600
|
if (!fns) return;
|
|
2544
2601
|
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2545
|
-
const debug = ctx?.
|
|
2602
|
+
const debug = ctx?.[$debug];
|
|
2546
2603
|
if (debug) {
|
|
2547
2604
|
await Promise.all(fns.map(async (fn, index) => {
|
|
2548
2605
|
const hookId = `hook_${name}_${fn.name || index}`;
|
|
@@ -2615,6 +2672,7 @@ const defaults = {
|
|
|
2615
2672
|
hostname: "localhost",
|
|
2616
2673
|
development: process.env.NODE_ENV !== "production",
|
|
2617
2674
|
enableAsyncLocalStorage: false,
|
|
2675
|
+
enableHttpBridge: false,
|
|
2618
2676
|
reusePort: false
|
|
2619
2677
|
};
|
|
2620
2678
|
trace.getTracer("shokupan.application");
|
|
@@ -2623,6 +2681,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2623
2681
|
openApiSpec;
|
|
2624
2682
|
composedMiddleware;
|
|
2625
2683
|
cpuMonitor;
|
|
2684
|
+
server;
|
|
2626
2685
|
get logger() {
|
|
2627
2686
|
return this.applicationConfig.logger;
|
|
2628
2687
|
}
|
|
@@ -2686,6 +2745,13 @@ class Shokupan extends ShokupanRouter {
|
|
|
2686
2745
|
}
|
|
2687
2746
|
return this;
|
|
2688
2747
|
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Registers a plugin.
|
|
2750
|
+
*/
|
|
2751
|
+
register(plugin, options) {
|
|
2752
|
+
plugin.onInit(this, options);
|
|
2753
|
+
return this;
|
|
2754
|
+
}
|
|
2689
2755
|
startupHooks = [];
|
|
2690
2756
|
/**
|
|
2691
2757
|
* Registers a callback to be executed before the server starts listening.
|
|
@@ -2724,6 +2790,7 @@ class Shokupan extends ShokupanRouter {
|
|
|
2724
2790
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
2725
2791
|
this.cpuMonitor.start();
|
|
2726
2792
|
}
|
|
2793
|
+
const self = this;
|
|
2727
2794
|
const serveOptions = {
|
|
2728
2795
|
port: finalPort,
|
|
2729
2796
|
hostname: this.applicationConfig.hostname,
|
|
@@ -2735,8 +2802,61 @@ class Shokupan extends ShokupanRouter {
|
|
|
2735
2802
|
open(ws) {
|
|
2736
2803
|
ws.data?.handler?.open?.(ws);
|
|
2737
2804
|
},
|
|
2738
|
-
message(ws, message) {
|
|
2739
|
-
ws.data?.handler?.message
|
|
2805
|
+
async message(ws, message) {
|
|
2806
|
+
if (ws.data?.handler?.message) {
|
|
2807
|
+
return ws.data.handler.message(ws, message);
|
|
2808
|
+
}
|
|
2809
|
+
if (typeof message !== "string") return;
|
|
2810
|
+
try {
|
|
2811
|
+
const payload = JSON.parse(message);
|
|
2812
|
+
if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
|
|
2813
|
+
const { id, method, path, headers, body } = payload;
|
|
2814
|
+
const url = new URL(path, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
|
|
2815
|
+
const req = new Request(url.toString(), {
|
|
2816
|
+
method,
|
|
2817
|
+
headers,
|
|
2818
|
+
body: typeof body === "object" ? JSON.stringify(body) : body
|
|
2819
|
+
});
|
|
2820
|
+
const res = await self.fetch(req);
|
|
2821
|
+
const resBody = await res.json().catch((err) => res.text());
|
|
2822
|
+
const resHeaders = {};
|
|
2823
|
+
res.headers.forEach((v, k) => resHeaders[k] = v);
|
|
2824
|
+
ws.send(JSON.stringify({
|
|
2825
|
+
type: "RESPONSE",
|
|
2826
|
+
id,
|
|
2827
|
+
status: res.status,
|
|
2828
|
+
headers: resHeaders,
|
|
2829
|
+
body: resBody
|
|
2830
|
+
}));
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
|
|
2834
|
+
if (eventName) {
|
|
2835
|
+
const handlers = self.findEvent(eventName);
|
|
2836
|
+
const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
|
|
2837
|
+
if (handler) {
|
|
2838
|
+
const data = payload.data || payload.payload;
|
|
2839
|
+
const req = new ShokupanRequest({
|
|
2840
|
+
url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
|
|
2841
|
+
method: "POST",
|
|
2842
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
2843
|
+
body: JSON.stringify(data)
|
|
2844
|
+
});
|
|
2845
|
+
const ctx = new ShokupanContext(req, self.server);
|
|
2846
|
+
ctx[$ws] = ws;
|
|
2847
|
+
try {
|
|
2848
|
+
await handler(ctx);
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
if (self.applicationConfig["websocketErrorHandler"]) {
|
|
2851
|
+
await self.applicationConfig["websocketErrorHandler"](err, ctx);
|
|
2852
|
+
} else {
|
|
2853
|
+
console.error(`Error in event ${eventName}:`, err);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
} catch (e) {
|
|
2859
|
+
}
|
|
2740
2860
|
},
|
|
2741
2861
|
drain(ws) {
|
|
2742
2862
|
ws.data?.handler?.drain?.(ws);
|
|
@@ -2748,12 +2868,40 @@ class Shokupan extends ShokupanRouter {
|
|
|
2748
2868
|
};
|
|
2749
2869
|
let factory = this.applicationConfig.serverFactory;
|
|
2750
2870
|
if (!factory && typeof Bun === "undefined") {
|
|
2751
|
-
const { createHttpServer } = await import("./server-
|
|
2871
|
+
const { createHttpServer } = await import("./http-server-CCeagTyU.js");
|
|
2752
2872
|
factory = createHttpServer();
|
|
2753
2873
|
}
|
|
2754
|
-
|
|
2874
|
+
this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
2755
2875
|
console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
|
|
2756
|
-
return server;
|
|
2876
|
+
return this.server;
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Stops the application server.
|
|
2880
|
+
*
|
|
2881
|
+
* This method gracefully shuts down the server and stops any running monitors.
|
|
2882
|
+
* Works transparently in both Bun and Node.js runtimes.
|
|
2883
|
+
*
|
|
2884
|
+
* @returns A promise that resolves when the server has been stopped.
|
|
2885
|
+
*
|
|
2886
|
+
* @example
|
|
2887
|
+
* ```typescript
|
|
2888
|
+
* const app = new Shokupan();
|
|
2889
|
+
* const server = await app.listen(3000);
|
|
2890
|
+
*
|
|
2891
|
+
* // Later, when you want to stop the server
|
|
2892
|
+
* await app.stop();
|
|
2893
|
+
* ```
|
|
2894
|
+
* @param closeActiveConnections — Immediately terminate in-flight requests, websockets, and stop accepting new connections.
|
|
2895
|
+
*/
|
|
2896
|
+
async stop(closeActiveConnections) {
|
|
2897
|
+
if (this.cpuMonitor) {
|
|
2898
|
+
this.cpuMonitor.stop();
|
|
2899
|
+
this.cpuMonitor = void 0;
|
|
2900
|
+
}
|
|
2901
|
+
if (this.server) {
|
|
2902
|
+
await this.server.stop(closeActiveConnections);
|
|
2903
|
+
this.server = void 0;
|
|
2904
|
+
}
|
|
2757
2905
|
}
|
|
2758
2906
|
[$dispatch](req) {
|
|
2759
2907
|
return this.fetch(req);
|
|
@@ -2817,19 +2965,19 @@ class Shokupan extends ShokupanRouter {
|
|
|
2817
2965
|
"http.method": req.method
|
|
2818
2966
|
}
|
|
2819
2967
|
};
|
|
2820
|
-
const parent = store?.
|
|
2968
|
+
const parent = store?.span;
|
|
2821
2969
|
const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
|
|
2822
2970
|
return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
|
|
2823
|
-
const
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
return asyncContext.run(
|
|
2971
|
+
const ctxStore = new RequestContextStore();
|
|
2972
|
+
ctxStore.span = span;
|
|
2973
|
+
ctxStore.request = req;
|
|
2974
|
+
return asyncContext.run(ctxStore, () => this.handleRequest(req, server).finally(() => span.end()));
|
|
2827
2975
|
});
|
|
2828
2976
|
}
|
|
2829
2977
|
if (this.applicationConfig.enableAsyncLocalStorage) {
|
|
2830
|
-
const
|
|
2831
|
-
|
|
2832
|
-
return asyncContext.run(
|
|
2978
|
+
const ctxStore = new RequestContextStore();
|
|
2979
|
+
ctxStore.request = req;
|
|
2980
|
+
return asyncContext.run(ctxStore, () => this.handleRequest(req, server));
|
|
2833
2981
|
}
|
|
2834
2982
|
return this.handleRequest(req, server);
|
|
2835
2983
|
}
|
|
@@ -2851,24 +2999,34 @@ class Shokupan extends ShokupanRouter {
|
|
|
2851
2999
|
const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
|
|
2852
3000
|
const match = this.find(req.method, ctx.path);
|
|
2853
3001
|
if (match) {
|
|
3002
|
+
ctx[$routeMatched] = true;
|
|
2854
3003
|
ctx.params = match.params;
|
|
2855
3004
|
await bodyParsing;
|
|
2856
3005
|
return match.handler(ctx);
|
|
2857
3006
|
}
|
|
3007
|
+
if (ctx.upgrade()) {
|
|
3008
|
+
return void 0;
|
|
3009
|
+
}
|
|
2858
3010
|
return null;
|
|
2859
3011
|
});
|
|
2860
3012
|
let response;
|
|
2861
3013
|
if (result instanceof Response) {
|
|
2862
3014
|
response = result;
|
|
2863
|
-
} else if ((result === null || result === void 0) && ctx
|
|
2864
|
-
response = ctx
|
|
3015
|
+
} else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
|
|
3016
|
+
response = ctx[$finalResponse];
|
|
2865
3017
|
} else if (result === null || result === void 0) {
|
|
2866
|
-
if (ctx
|
|
2867
|
-
response = ctx
|
|
2868
|
-
} else if (ctx.
|
|
3018
|
+
if (ctx[$finalResponse] instanceof Response) {
|
|
3019
|
+
response = ctx[$finalResponse];
|
|
3020
|
+
} else if (ctx.isUpgraded) {
|
|
3021
|
+
return void 0;
|
|
3022
|
+
} else if (ctx[$routeMatched]) {
|
|
2869
3023
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
2870
3024
|
} else {
|
|
2871
|
-
|
|
3025
|
+
if (ctx.response.status !== HTTP_STATUS.OK) {
|
|
3026
|
+
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
3027
|
+
} else {
|
|
3028
|
+
response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
|
|
3029
|
+
}
|
|
2872
3030
|
}
|
|
2873
3031
|
} else if (typeof result === "object") {
|
|
2874
3032
|
response = ctx.json(result);
|
|
@@ -2879,10 +3037,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
2879
3037
|
await this.runHooks("onResponseStart", ctx, response);
|
|
2880
3038
|
return response;
|
|
2881
3039
|
} catch (err) {
|
|
2882
|
-
|
|
2883
|
-
const span = asyncContext.getStore()?.get("span");
|
|
3040
|
+
const span = asyncContext.getStore()?.span;
|
|
2884
3041
|
if (span) span.setStatus({ code: 2 });
|
|
2885
|
-
const status = err
|
|
3042
|
+
const status = getErrorStatus(err);
|
|
2886
3043
|
const body = { error: err.message || "Internal Server Error" };
|
|
2887
3044
|
if (err.errors) body.errors = err.errors;
|
|
2888
3045
|
await this.runHooks("onError", ctx, err);
|
|
@@ -2904,16 +3061,188 @@ class Shokupan extends ShokupanRouter {
|
|
|
2904
3061
|
}
|
|
2905
3062
|
return executionPromise.catch((err) => {
|
|
2906
3063
|
if (err.message === "Request Timeout") {
|
|
2907
|
-
return ctx.text("Request Timeout",
|
|
3064
|
+
return ctx.text("Request Timeout", HTTP_STATUS.REQUEST_TIMEOUT);
|
|
2908
3065
|
}
|
|
2909
3066
|
console.error("Unexpected error in request execution:", err);
|
|
2910
|
-
return ctx.text("Internal Server Error",
|
|
3067
|
+
return ctx.text("Internal Server Error", HTTP_STATUS.INTERNAL_SERVER_ERROR);
|
|
2911
3068
|
}).then(async (res) => {
|
|
2912
3069
|
await this.runHooks("onResponseEnd", ctx, res);
|
|
2913
3070
|
return res;
|
|
2914
3071
|
});
|
|
2915
3072
|
}
|
|
2916
3073
|
}
|
|
3074
|
+
function RateLimitMiddleware(options = {}) {
|
|
3075
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
3076
|
+
const max = options.limit || options.max || 5;
|
|
3077
|
+
const message = options.message || "Too many requests, please try again later.";
|
|
3078
|
+
const statusCode = options.statusCode || 429;
|
|
3079
|
+
const headers = options.headers !== false;
|
|
3080
|
+
const mode = options.mode || "user";
|
|
3081
|
+
const trustedProxies = options.trustedProxies || [];
|
|
3082
|
+
const keyGenerator = options.keyGenerator || ((ctx) => {
|
|
3083
|
+
if (mode === "absolute") {
|
|
3084
|
+
return "global";
|
|
3085
|
+
}
|
|
3086
|
+
const xForwardedFor = ctx.headers.get("x-forwarded-for");
|
|
3087
|
+
if (xForwardedFor && trustedProxies.length > 0) {
|
|
3088
|
+
const ips = xForwardedFor.split(",").map((ip) => ip.trim());
|
|
3089
|
+
for (let i = ips.length - 1; i >= 0; i--) {
|
|
3090
|
+
const ip = ips[i];
|
|
3091
|
+
if (!trustedProxies.includes(ip)) {
|
|
3092
|
+
if (/^[\d.:a-fA-F]+$/.test(ip)) {
|
|
3093
|
+
return ip;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
|
|
3099
|
+
});
|
|
3100
|
+
const skip = options.skip || (() => false);
|
|
3101
|
+
const hits = /* @__PURE__ */ new Map();
|
|
3102
|
+
const interval = setInterval(() => {
|
|
3103
|
+
const now = Date.now();
|
|
3104
|
+
const entries = Array.from(hits.entries());
|
|
3105
|
+
for (let i = 0; i < entries.length; i++) {
|
|
3106
|
+
const [key, record] = entries[i];
|
|
3107
|
+
if (record.resetTime <= now) {
|
|
3108
|
+
hits.delete(key);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
}, windowMs);
|
|
3112
|
+
if (interval.unref) interval.unref();
|
|
3113
|
+
const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
|
|
3114
|
+
if (skip(ctx)) return next();
|
|
3115
|
+
const key = keyGenerator(ctx);
|
|
3116
|
+
const now = Date.now();
|
|
3117
|
+
let record = hits.get(key);
|
|
3118
|
+
if (!record || record.resetTime <= now) {
|
|
3119
|
+
record = {
|
|
3120
|
+
hits: 0,
|
|
3121
|
+
resetTime: now + windowMs
|
|
3122
|
+
};
|
|
3123
|
+
hits.set(key, record);
|
|
3124
|
+
}
|
|
3125
|
+
record.hits++;
|
|
3126
|
+
const remaining = Math.max(0, max - record.hits);
|
|
3127
|
+
const resetTime = Math.ceil(record.resetTime / 1e3);
|
|
3128
|
+
const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
|
|
3129
|
+
const setHeaders = (res) => {
|
|
3130
|
+
if (!headers || !res || !res.headers) return;
|
|
3131
|
+
try {
|
|
3132
|
+
res.headers.set("X-RateLimit-Limit", String(max));
|
|
3133
|
+
res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
3134
|
+
res.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
3135
|
+
} catch (e) {
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
if (record.hits > max) {
|
|
3139
|
+
if (options.onRateLimited) {
|
|
3140
|
+
const result = await options.onRateLimited(ctx, key);
|
|
3141
|
+
if (result instanceof Response) {
|
|
3142
|
+
return result;
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
const msg = typeof message === "function" ? message(ctx, key) : message;
|
|
3146
|
+
typeof msg === "object" ? JSON.stringify(msg) : String(msg);
|
|
3147
|
+
const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
|
|
3148
|
+
if (headers) {
|
|
3149
|
+
setHeaders(res);
|
|
3150
|
+
res.headers.set("Retry-After", String(retryAfter));
|
|
3151
|
+
}
|
|
3152
|
+
return res;
|
|
3153
|
+
}
|
|
3154
|
+
const response = await next();
|
|
3155
|
+
if (response instanceof Response && headers) {
|
|
3156
|
+
setHeaders(response);
|
|
3157
|
+
}
|
|
3158
|
+
return response;
|
|
3159
|
+
};
|
|
3160
|
+
rateLimitMiddleware.isBuiltin = true;
|
|
3161
|
+
rateLimitMiddleware.pluginName = "RateLimit";
|
|
3162
|
+
return rateLimitMiddleware;
|
|
3163
|
+
}
|
|
3164
|
+
function Controller(path = "/") {
|
|
3165
|
+
return (target) => {
|
|
3166
|
+
target[$controllerPath] = path;
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
function Use(...middleware) {
|
|
3170
|
+
return (target, propertyKey, descriptor) => {
|
|
3171
|
+
if (!propertyKey) {
|
|
3172
|
+
const existing = target[$middleware] || [];
|
|
3173
|
+
target[$middleware] = [...existing, ...middleware];
|
|
3174
|
+
} else {
|
|
3175
|
+
if (!target[$middleware]) {
|
|
3176
|
+
target[$middleware] = /* @__PURE__ */ new Map();
|
|
3177
|
+
}
|
|
3178
|
+
const existing = target[$middleware].get(propertyKey) || [];
|
|
3179
|
+
target[$middleware].set(propertyKey, [...existing, ...middleware]);
|
|
3180
|
+
}
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
function createParamDecorator(type) {
|
|
3184
|
+
return (name) => {
|
|
3185
|
+
return (target, propertyKey, parameterIndex) => {
|
|
3186
|
+
if (!target[$routeArgs]) {
|
|
3187
|
+
target[$routeArgs] = /* @__PURE__ */ new Map();
|
|
3188
|
+
}
|
|
3189
|
+
if (!target[$routeArgs].has(propertyKey)) {
|
|
3190
|
+
target[$routeArgs].set(propertyKey, []);
|
|
3191
|
+
}
|
|
3192
|
+
target[$routeArgs].get(propertyKey).push({
|
|
3193
|
+
index: parameterIndex,
|
|
3194
|
+
type,
|
|
3195
|
+
name
|
|
3196
|
+
});
|
|
3197
|
+
};
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
const Body = createParamDecorator(RouteParamType.BODY);
|
|
3201
|
+
const Param = createParamDecorator(RouteParamType.PARAM);
|
|
3202
|
+
const Query = createParamDecorator(RouteParamType.QUERY);
|
|
3203
|
+
const Headers$1 = createParamDecorator(RouteParamType.HEADER);
|
|
3204
|
+
const Req = createParamDecorator(RouteParamType.REQUEST);
|
|
3205
|
+
const Ctx = createParamDecorator(RouteParamType.CONTEXT);
|
|
3206
|
+
function Spec(spec) {
|
|
3207
|
+
return (target, propertyKey, descriptor) => {
|
|
3208
|
+
if (!target[$routeSpec]) {
|
|
3209
|
+
target[$routeSpec] = /* @__PURE__ */ new Map();
|
|
3210
|
+
}
|
|
3211
|
+
target[$routeSpec].set(propertyKey, spec);
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
function createMethodDecorator(method) {
|
|
3215
|
+
return (path = "/") => {
|
|
3216
|
+
return (target, propertyKey, descriptor) => {
|
|
3217
|
+
if (!target[$routeMethods]) {
|
|
3218
|
+
target[$routeMethods] = /* @__PURE__ */ new Map();
|
|
3219
|
+
}
|
|
3220
|
+
target[$routeMethods].set(propertyKey, {
|
|
3221
|
+
method,
|
|
3222
|
+
path
|
|
3223
|
+
});
|
|
3224
|
+
};
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
const Get = createMethodDecorator("GET");
|
|
3228
|
+
const Post = createMethodDecorator("POST");
|
|
3229
|
+
const Put = createMethodDecorator("PUT");
|
|
3230
|
+
const Delete = createMethodDecorator("DELETE");
|
|
3231
|
+
const Patch = createMethodDecorator("PATCH");
|
|
3232
|
+
const Options = createMethodDecorator("OPTIONS");
|
|
3233
|
+
const Head = createMethodDecorator("HEAD");
|
|
3234
|
+
const All = createMethodDecorator("ALL");
|
|
3235
|
+
function Event(eventName) {
|
|
3236
|
+
return (target, propertyKey, descriptor) => {
|
|
3237
|
+
target[$eventMethods] ??= /* @__PURE__ */ new Map();
|
|
3238
|
+
target[$eventMethods].set(propertyKey, {
|
|
3239
|
+
eventName
|
|
3240
|
+
});
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
function RateLimit(options) {
|
|
3244
|
+
return Use(RateLimitMiddleware(options));
|
|
3245
|
+
}
|
|
2917
3246
|
class AuthPlugin extends ShokupanRouter {
|
|
2918
3247
|
constructor(authConfig) {
|
|
2919
3248
|
super();
|
|
@@ -2922,6 +3251,13 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
2922
3251
|
this.init();
|
|
2923
3252
|
}
|
|
2924
3253
|
secret;
|
|
3254
|
+
onInit(app, options) {
|
|
3255
|
+
if (options?.path) {
|
|
3256
|
+
app.mount(options.path, this);
|
|
3257
|
+
} else {
|
|
3258
|
+
app.mount(options.path ?? "/", this);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
2925
3261
|
getProviderInstance(name, p) {
|
|
2926
3262
|
switch (name) {
|
|
2927
3263
|
case "github":
|
|
@@ -3074,73 +3410,809 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3074
3410
|
provider,
|
|
3075
3411
|
raw: data
|
|
3076
3412
|
};
|
|
3077
|
-
} else if (provider === "auth0" || provider === "okta") {
|
|
3078
|
-
const domain = config.domain.startsWith("http") ? config.domain : `https://${config.domain}`;
|
|
3079
|
-
const endpoint = provider === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
|
|
3080
|
-
const res = await fetch(endpoint, {
|
|
3081
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
3413
|
+
} else if (provider === "auth0" || provider === "okta") {
|
|
3414
|
+
const domain = config.domain.startsWith("http") ? config.domain : `https://${config.domain}`;
|
|
3415
|
+
const endpoint = provider === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
|
|
3416
|
+
const res = await fetch(endpoint, {
|
|
3417
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3418
|
+
});
|
|
3419
|
+
const data = await res.json();
|
|
3420
|
+
user = {
|
|
3421
|
+
id: data.sub,
|
|
3422
|
+
name: data.name,
|
|
3423
|
+
email: data.email,
|
|
3424
|
+
picture: data.picture,
|
|
3425
|
+
provider,
|
|
3426
|
+
raw: data
|
|
3427
|
+
};
|
|
3428
|
+
} else if (provider === "apple") {
|
|
3429
|
+
if (idToken) {
|
|
3430
|
+
const payload = jose.decodeJwt(idToken);
|
|
3431
|
+
user = {
|
|
3432
|
+
id: payload.sub,
|
|
3433
|
+
email: payload["email"],
|
|
3434
|
+
provider,
|
|
3435
|
+
raw: payload
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
} else if (provider === "oauth2") {
|
|
3439
|
+
if (config.userInfoUrl) {
|
|
3440
|
+
const res = await fetch(config.userInfoUrl, {
|
|
3441
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
3442
|
+
});
|
|
3443
|
+
const data = await res.json();
|
|
3444
|
+
user = {
|
|
3445
|
+
id: data.id || data.sub || "unknown",
|
|
3446
|
+
name: data.name,
|
|
3447
|
+
email: data.email,
|
|
3448
|
+
picture: data.picture,
|
|
3449
|
+
provider,
|
|
3450
|
+
raw: data
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
return user;
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Middleware to verify JWT
|
|
3458
|
+
*/
|
|
3459
|
+
getMiddleware() {
|
|
3460
|
+
return async (ctx, next) => {
|
|
3461
|
+
const authHeader = ctx.req.headers.get("Authorization");
|
|
3462
|
+
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
3463
|
+
if (!token) {
|
|
3464
|
+
const cookieHeader = ctx.req.headers.get("Cookie");
|
|
3465
|
+
token = cookieHeader?.match(/auth_token=([^;]+)/)?.[1] || null;
|
|
3466
|
+
}
|
|
3467
|
+
if (token) {
|
|
3468
|
+
try {
|
|
3469
|
+
const { payload } = await jose.jwtVerify(token, this.secret);
|
|
3470
|
+
ctx.user = payload;
|
|
3471
|
+
} catch {
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
return next();
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
class ClusterPlugin {
|
|
3479
|
+
constructor(options = {}) {
|
|
3480
|
+
this.options = options;
|
|
3481
|
+
}
|
|
3482
|
+
onInit(app) {
|
|
3483
|
+
const originalListen = app.listen.bind(app);
|
|
3484
|
+
const { workers = "auto", silent = false, sticky = false } = this.options;
|
|
3485
|
+
const isBun = typeof Bun !== "undefined";
|
|
3486
|
+
const numCPUs = os__default.cpus().length;
|
|
3487
|
+
const numWorkers = workers === "auto" || workers === -1 ? numCPUs : workers;
|
|
3488
|
+
if (numWorkers <= 1) {
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
app.listen = async (port) => {
|
|
3492
|
+
const finalPort = port ?? app.applicationConfig.port ?? 3e3;
|
|
3493
|
+
if (isBun) {
|
|
3494
|
+
return this.handleBun(app, finalPort, numWorkers, originalListen);
|
|
3495
|
+
} else {
|
|
3496
|
+
return this.handleNode(app, finalPort, numWorkers, originalListen, silent, sticky);
|
|
3497
|
+
}
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
async handleBun(app, port, workers, originalListen) {
|
|
3501
|
+
const workerId = process.env["SHOKUPAN_WORKER_ID"];
|
|
3502
|
+
if (workerId) {
|
|
3503
|
+
app.applicationConfig.reusePort = true;
|
|
3504
|
+
return originalListen(port);
|
|
3505
|
+
}
|
|
3506
|
+
console.log(`[Cluster] Starting ${workers} Bun workers on port ${port}...`);
|
|
3507
|
+
const spawnWorker = (id) => {
|
|
3508
|
+
Bun.spawn([process.argv0, ...process.argv.slice(1)], {
|
|
3509
|
+
env: { ...process.env, SHOKUPAN_WORKER_ID: id },
|
|
3510
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
3511
|
+
onExit(proc, exitCode, signalCode, error) {
|
|
3512
|
+
console.log(`[Cluster] Worker ${id} died (code: ${exitCode}). Restarting...`);
|
|
3513
|
+
spawnWorker(id);
|
|
3514
|
+
}
|
|
3515
|
+
});
|
|
3516
|
+
};
|
|
3517
|
+
for (let i = 0; i < workers; i++) {
|
|
3518
|
+
spawnWorker(process.pid + "_" + i + 1);
|
|
3519
|
+
}
|
|
3520
|
+
setInterval(() => {
|
|
3521
|
+
}, 1e3 * 60 * 60);
|
|
3522
|
+
return {
|
|
3523
|
+
stop: () => {
|
|
3524
|
+
},
|
|
3525
|
+
port
|
|
3526
|
+
};
|
|
3527
|
+
}
|
|
3528
|
+
async handleNode(app, port, workers, originalListen, silent, sticky) {
|
|
3529
|
+
if (cluster.isPrimary) {
|
|
3530
|
+
console.log(`[Cluster] Master ${process.pid} is running`);
|
|
3531
|
+
const fork = () => cluster.fork(process.env);
|
|
3532
|
+
for (let i = 0; i < workers; i++) {
|
|
3533
|
+
fork();
|
|
3534
|
+
}
|
|
3535
|
+
cluster.on("exit", (worker, code, signal) => {
|
|
3536
|
+
console.log(`[Cluster] Worker ${worker.process.pid} died. Restarting...`);
|
|
3537
|
+
fork();
|
|
3538
|
+
});
|
|
3539
|
+
if (sticky) {
|
|
3540
|
+
const server = net.createServer({ pauseOnConnect: true }, (connection) => {
|
|
3541
|
+
const remote = connection.remoteAddress || "";
|
|
3542
|
+
let hash = 0;
|
|
3543
|
+
for (let i = 0; i < remote.length; i++) {
|
|
3544
|
+
hash = (hash << 5) - hash + remote.charCodeAt(i);
|
|
3545
|
+
hash |= 0;
|
|
3546
|
+
}
|
|
3547
|
+
const index = Math.abs(hash) % workers;
|
|
3548
|
+
const worker = Object.values(cluster.workers)[index];
|
|
3549
|
+
if (worker) {
|
|
3550
|
+
worker.send("sticky-session:connection", connection);
|
|
3551
|
+
} else {
|
|
3552
|
+
connection.end();
|
|
3553
|
+
}
|
|
3554
|
+
});
|
|
3555
|
+
server.listen(port, () => {
|
|
3556
|
+
console.log(`[Cluster] Sticky Load Balancer listening on port ${port}`);
|
|
3557
|
+
});
|
|
3558
|
+
return {
|
|
3559
|
+
close: () => server.close(),
|
|
3560
|
+
port
|
|
3561
|
+
};
|
|
3562
|
+
} else {
|
|
3563
|
+
return {
|
|
3564
|
+
close: () => {
|
|
3565
|
+
},
|
|
3566
|
+
// Master controls
|
|
3567
|
+
port
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
} else {
|
|
3571
|
+
if (sticky) {
|
|
3572
|
+
const server = await originalListen(0);
|
|
3573
|
+
process.on("message", (message, handle) => {
|
|
3574
|
+
if (message !== "sticky-session:connection") return;
|
|
3575
|
+
if (!handle) return;
|
|
3576
|
+
server.emit("connection", handle);
|
|
3577
|
+
handle.resume();
|
|
3578
|
+
});
|
|
3579
|
+
return server;
|
|
3580
|
+
} else {
|
|
3581
|
+
return originalListen(port);
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
const INTERVALS = [
|
|
3587
|
+
{ label: "10s", ms: 10 * 1e3 },
|
|
3588
|
+
{ label: "1m", ms: 60 * 1e3 },
|
|
3589
|
+
{ label: "5m", ms: 5 * 60 * 1e3 },
|
|
3590
|
+
{ label: "1h", ms: 60 * 60 * 1e3 },
|
|
3591
|
+
{ label: "2h", ms: 2 * 60 * 60 * 1e3 },
|
|
3592
|
+
{ label: "6h", ms: 6 * 60 * 60 * 1e3 },
|
|
3593
|
+
{ label: "12h", ms: 12 * 60 * 60 * 1e3 },
|
|
3594
|
+
{ label: "1d", ms: 24 * 60 * 60 * 1e3 },
|
|
3595
|
+
{ label: "3d", ms: 3 * 24 * 60 * 60 * 1e3 },
|
|
3596
|
+
{ label: "7d", ms: 7 * 24 * 60 * 60 * 1e3 },
|
|
3597
|
+
{ label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
|
|
3598
|
+
];
|
|
3599
|
+
class MetricsCollector {
|
|
3600
|
+
currentIntervalStart = {};
|
|
3601
|
+
pendingDetails = {};
|
|
3602
|
+
eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
|
|
3603
|
+
timer = null;
|
|
3604
|
+
constructor() {
|
|
3605
|
+
this.eventLoopHistogram.enable();
|
|
3606
|
+
const now = Date.now();
|
|
3607
|
+
INTERVALS.forEach((int) => {
|
|
3608
|
+
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
3609
|
+
this.pendingDetails[int.label] = [];
|
|
3610
|
+
});
|
|
3611
|
+
this.timer = setInterval(() => this.collect(), 1e4);
|
|
3612
|
+
}
|
|
3613
|
+
recordRequest(duration, isError) {
|
|
3614
|
+
INTERVALS.forEach((int) => {
|
|
3615
|
+
this.pendingDetails[int.label].push({ duration, isError });
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
alignTimestamp(ts, intervalMs) {
|
|
3619
|
+
return Math.floor(ts / intervalMs) * intervalMs;
|
|
3620
|
+
}
|
|
3621
|
+
async collect() {
|
|
3622
|
+
try {
|
|
3623
|
+
const now = Date.now();
|
|
3624
|
+
console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
|
|
3625
|
+
for (const int of INTERVALS) {
|
|
3626
|
+
const start = this.currentIntervalStart[int.label];
|
|
3627
|
+
if (now >= start + int.ms) {
|
|
3628
|
+
console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
|
|
3629
|
+
await this.flushInterval(int.label, start, int.ms);
|
|
3630
|
+
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
} catch (error) {
|
|
3634
|
+
console.error("[MetricsCollector] Error in collect():", error);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
async flushInterval(label, timestamp, durationMs) {
|
|
3638
|
+
const reqs = this.pendingDetails[label];
|
|
3639
|
+
console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
|
|
3640
|
+
this.pendingDetails[label] = [];
|
|
3641
|
+
if (reqs.length === 0) {
|
|
3642
|
+
console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
const totalReqs = reqs.length;
|
|
3646
|
+
const errorReqs = reqs.filter((r) => r.isError).length;
|
|
3647
|
+
const successReqs = totalReqs - errorReqs;
|
|
3648
|
+
const duratons = reqs.map((r) => r.duration).sort((a, b) => a - b);
|
|
3649
|
+
const rps = totalReqs / (durationMs / 1e3);
|
|
3650
|
+
const sum = duratons.reduce((a, b) => a + b, 0);
|
|
3651
|
+
const avg = totalReqs > 0 ? sum / totalReqs : 0;
|
|
3652
|
+
const getP = (p) => {
|
|
3653
|
+
if (duratons.length === 0) return 0;
|
|
3654
|
+
const idx = Math.floor(duratons.length * p);
|
|
3655
|
+
return duratons[idx];
|
|
3656
|
+
};
|
|
3657
|
+
const metric = {
|
|
3658
|
+
timestamp,
|
|
3659
|
+
interval: label,
|
|
3660
|
+
cpu: os.loadavg()[0],
|
|
3661
|
+
// Using load avg for simplicity as per requirements (Load)
|
|
3662
|
+
load: os.loadavg(),
|
|
3663
|
+
memory: {
|
|
3664
|
+
used: process.memoryUsage().rss,
|
|
3665
|
+
total: os.totalmem(),
|
|
3666
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
3667
|
+
heapTotal: process.memoryUsage().heapTotal
|
|
3668
|
+
},
|
|
3669
|
+
eventLoopLatency: {
|
|
3670
|
+
min: this.eventLoopHistogram.min / 1e6,
|
|
3671
|
+
max: this.eventLoopHistogram.max / 1e6,
|
|
3672
|
+
mean: this.eventLoopHistogram.mean / 1e6,
|
|
3673
|
+
p50: this.eventLoopHistogram.percentile(50) / 1e6,
|
|
3674
|
+
p95: this.eventLoopHistogram.percentile(95) / 1e6,
|
|
3675
|
+
p99: this.eventLoopHistogram.percentile(99) / 1e6
|
|
3676
|
+
},
|
|
3677
|
+
requests: {
|
|
3678
|
+
total: totalReqs,
|
|
3679
|
+
rps,
|
|
3680
|
+
success: successReqs,
|
|
3681
|
+
error: errorReqs
|
|
3682
|
+
},
|
|
3683
|
+
responseTime: {
|
|
3684
|
+
min: duratons[0] || 0,
|
|
3685
|
+
max: duratons[duratons.length - 1] || 0,
|
|
3686
|
+
avg,
|
|
3687
|
+
p50: getP(0.5),
|
|
3688
|
+
p95: getP(0.95),
|
|
3689
|
+
p99: getP(0.99)
|
|
3690
|
+
}
|
|
3691
|
+
};
|
|
3692
|
+
console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
|
|
3693
|
+
try {
|
|
3694
|
+
const recordId = new RecordId("metrics", timestamp);
|
|
3695
|
+
await datastore.set(recordId, metric);
|
|
3696
|
+
console.log(`[MetricsCollector] ✓ Successfully saved ${label} metric to datastore`);
|
|
3697
|
+
const test = await datastore.get(recordId);
|
|
3698
|
+
console.log(`[MetricsCollector] DEBUG: Immediate .get() returned:`, test ? "DATA" : "NULL");
|
|
3699
|
+
const queryTest = await datastore.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
3700
|
+
console.log(`[MetricsCollector] DEBUG: Query by id returned ${queryTest[0]?.length || 0} records`);
|
|
3701
|
+
} catch (e) {
|
|
3702
|
+
console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
// Cleanup if needed
|
|
3706
|
+
stop() {
|
|
3707
|
+
if (this.timer) clearInterval(this.timer);
|
|
3708
|
+
this.eventLoopHistogram.disable();
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
class Collector {
|
|
3712
|
+
constructor(dashboard) {
|
|
3713
|
+
this.dashboard = dashboard;
|
|
3714
|
+
}
|
|
3715
|
+
currentNode;
|
|
3716
|
+
trackStep(id, type, duration, status, error) {
|
|
3717
|
+
if (!id) return;
|
|
3718
|
+
this.dashboard.recordNodeMetric(id, type, duration, status === "error");
|
|
3719
|
+
}
|
|
3720
|
+
trackEdge(fromId, toId) {
|
|
3721
|
+
if (!fromId || !toId) return;
|
|
3722
|
+
this.dashboard.recordEdgeMetric(fromId, toId);
|
|
3723
|
+
}
|
|
3724
|
+
setNode(id) {
|
|
3725
|
+
this.currentNode = id;
|
|
3726
|
+
}
|
|
3727
|
+
getCurrentNode() {
|
|
3728
|
+
return this.currentNode;
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
class Dashboard {
|
|
3732
|
+
constructor(dashboardConfig = {}) {
|
|
3733
|
+
this.dashboardConfig = dashboardConfig;
|
|
3734
|
+
}
|
|
3735
|
+
static __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3736
|
+
// Get base path for dashboard files - works in both dev (src/) and production (dist/)
|
|
3737
|
+
static getBasePath() {
|
|
3738
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
3739
|
+
if (dir.endsWith("dist")) {
|
|
3740
|
+
return dir + "/plugins/application/dashboard";
|
|
3741
|
+
}
|
|
3742
|
+
return dir;
|
|
3743
|
+
}
|
|
3744
|
+
router = new ShokupanRouter();
|
|
3745
|
+
metrics = {
|
|
3746
|
+
totalRequests: 0,
|
|
3747
|
+
successfulRequests: 0,
|
|
3748
|
+
failedRequests: 0,
|
|
3749
|
+
activeRequests: 0,
|
|
3750
|
+
averageTotalTime_ms: 0,
|
|
3751
|
+
recentTimings: [],
|
|
3752
|
+
logs: [],
|
|
3753
|
+
rateLimitedCounts: {},
|
|
3754
|
+
nodeMetrics: {},
|
|
3755
|
+
edgeMetrics: {}
|
|
3756
|
+
};
|
|
3757
|
+
eta = new Eta({
|
|
3758
|
+
views: Dashboard.getBasePath() + "/static",
|
|
3759
|
+
cache: false
|
|
3760
|
+
});
|
|
3761
|
+
startTime = Date.now();
|
|
3762
|
+
instrumented = false;
|
|
3763
|
+
metricsCollector = new MetricsCollector();
|
|
3764
|
+
// ShokupanPlugin interface implementation
|
|
3765
|
+
onInit(app, options) {
|
|
3766
|
+
this[$appRoot] = app;
|
|
3767
|
+
const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
3768
|
+
const hooks = this.getHooks();
|
|
3769
|
+
if (!app.middleware) {
|
|
3770
|
+
app.middleware = [];
|
|
3771
|
+
}
|
|
3772
|
+
const hooksMiddleware = async (ctx, next) => {
|
|
3773
|
+
if (hooks.onRequestStart) {
|
|
3774
|
+
await hooks.onRequestStart(ctx);
|
|
3775
|
+
}
|
|
3776
|
+
await next();
|
|
3777
|
+
if (hooks.onResponseEnd) {
|
|
3778
|
+
const effectiveResponse = ctx._finalResponse || ctx.response || {};
|
|
3779
|
+
await hooks.onResponseEnd(ctx, effectiveResponse);
|
|
3780
|
+
}
|
|
3781
|
+
};
|
|
3782
|
+
app.use(hooksMiddleware);
|
|
3783
|
+
app.mount(mountPath, this.router);
|
|
3784
|
+
this.setupRoutes();
|
|
3785
|
+
}
|
|
3786
|
+
setupRoutes() {
|
|
3787
|
+
this.router.get("/metrics", async (ctx) => {
|
|
3788
|
+
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
3789
|
+
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
3790
|
+
const interval = ctx.query["interval"];
|
|
3791
|
+
if (interval) {
|
|
3792
|
+
const intervalMap = {
|
|
3793
|
+
"10s": 10 * 1e3,
|
|
3794
|
+
"1m": 60 * 1e3,
|
|
3795
|
+
"5m": 5 * 60 * 1e3,
|
|
3796
|
+
"30m": 30 * 60 * 1e3,
|
|
3797
|
+
"1h": 60 * 60 * 1e3,
|
|
3798
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
3799
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
3800
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
3801
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
3802
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
3803
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
3804
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
3805
|
+
};
|
|
3806
|
+
const ms = intervalMap[interval] || 60 * 1e3;
|
|
3807
|
+
const startTime = Date.now() - ms;
|
|
3808
|
+
let stats;
|
|
3809
|
+
try {
|
|
3810
|
+
stats = await datastore.query(`
|
|
3811
|
+
SELECT
|
|
3812
|
+
count() as total,
|
|
3813
|
+
count(IF status < 400 THEN 1 END) as success,
|
|
3814
|
+
count(IF status >= 400 THEN 1 END) as failed,
|
|
3815
|
+
math::mean(duration) as avg_latency
|
|
3816
|
+
FROM requests
|
|
3817
|
+
WHERE timestamp >= $start
|
|
3818
|
+
GROUP ALL
|
|
3819
|
+
`, { start: startTime });
|
|
3820
|
+
} catch (error) {
|
|
3821
|
+
console.error("[Dashboard] Query failed at plugin.ts:180-191", {
|
|
3822
|
+
error,
|
|
3823
|
+
interval,
|
|
3824
|
+
startTime,
|
|
3825
|
+
query: "metrics interval stats",
|
|
3826
|
+
stack: new Error().stack
|
|
3827
|
+
});
|
|
3828
|
+
throw error;
|
|
3829
|
+
}
|
|
3830
|
+
const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
|
|
3831
|
+
return ctx.json({
|
|
3832
|
+
metrics: {
|
|
3833
|
+
totalRequests: s.total || 0,
|
|
3834
|
+
successfulRequests: s.success || 0,
|
|
3835
|
+
failedRequests: s.failed || 0,
|
|
3836
|
+
activeRequests: this.metrics.activeRequests,
|
|
3837
|
+
averageTotalTime_ms: s.avg_latency || 0,
|
|
3838
|
+
recentTimings: this.metrics.recentTimings,
|
|
3839
|
+
logs: [],
|
|
3840
|
+
rateLimitedCounts: this.metrics.rateLimitedCounts,
|
|
3841
|
+
nodeMetrics: this.metrics.nodeMetrics,
|
|
3842
|
+
edgeMetrics: this.metrics.edgeMetrics
|
|
3843
|
+
},
|
|
3844
|
+
uptime
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
return ctx.json({
|
|
3848
|
+
metrics: this.metrics,
|
|
3849
|
+
uptime
|
|
3850
|
+
});
|
|
3851
|
+
});
|
|
3852
|
+
this.router.get("/metrics/history", async (ctx) => {
|
|
3853
|
+
const interval = ctx.query["interval"] || "1m";
|
|
3854
|
+
const intervalMap = {
|
|
3855
|
+
"10s": 10 * 1e3,
|
|
3856
|
+
"1m": 60 * 1e3,
|
|
3857
|
+
"5m": 5 * 60 * 1e3,
|
|
3858
|
+
"30m": 30 * 60 * 1e3,
|
|
3859
|
+
"1h": 60 * 60 * 1e3,
|
|
3860
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
3861
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
3862
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
3863
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
3864
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
3865
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
3866
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
3867
|
+
};
|
|
3868
|
+
const periodMs = intervalMap[interval] || 60 * 1e3;
|
|
3869
|
+
const startTime = Date.now() - periodMs * 3;
|
|
3870
|
+
const endTime = Date.now();
|
|
3871
|
+
const result = await datastore.query(
|
|
3872
|
+
"SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
|
|
3873
|
+
{ start: startTime, end: endTime, interval }
|
|
3874
|
+
);
|
|
3875
|
+
return ctx.json({
|
|
3876
|
+
metrics: result[0] || []
|
|
3082
3877
|
});
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3878
|
+
});
|
|
3879
|
+
const getIntervalStartTime = (interval) => {
|
|
3880
|
+
if (!interval) return 0;
|
|
3881
|
+
const intervalMap = {
|
|
3882
|
+
"10s": 10 * 1e3,
|
|
3883
|
+
"1m": 60 * 1e3,
|
|
3884
|
+
"5m": 5 * 60 * 1e3,
|
|
3885
|
+
"30m": 30 * 60 * 1e3,
|
|
3886
|
+
"1h": 60 * 60 * 1e3,
|
|
3887
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
3888
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
3889
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
3890
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
3891
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
3892
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
3893
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
3091
3894
|
};
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3895
|
+
const ms = intervalMap[interval] || 0;
|
|
3896
|
+
return ms ? Date.now() - ms : 0;
|
|
3897
|
+
};
|
|
3898
|
+
this.router.get("/requests/top", async (ctx) => {
|
|
3899
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3900
|
+
const result = await datastore.query(
|
|
3901
|
+
"SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
3902
|
+
{ start: startTime }
|
|
3903
|
+
);
|
|
3904
|
+
return ctx.json({ top: result[0] || [] });
|
|
3905
|
+
});
|
|
3906
|
+
this.router.get("/errors/top", async (ctx) => {
|
|
3907
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3908
|
+
const result = await datastore.query(
|
|
3909
|
+
"SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
|
|
3910
|
+
{ start: startTime }
|
|
3911
|
+
);
|
|
3912
|
+
return ctx.json({ top: result[0] || [] });
|
|
3913
|
+
});
|
|
3914
|
+
this.router.get("/requests/failing", async (ctx) => {
|
|
3915
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3916
|
+
const result = await datastore.query(
|
|
3917
|
+
"SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
3918
|
+
{ start: startTime }
|
|
3919
|
+
);
|
|
3920
|
+
return ctx.json({ top: result[0] || [] });
|
|
3921
|
+
});
|
|
3922
|
+
this.router.get("/requests/slowest", async (ctx) => {
|
|
3923
|
+
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3924
|
+
const result = await datastore.query(
|
|
3925
|
+
"SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
|
|
3926
|
+
{ start: startTime }
|
|
3927
|
+
);
|
|
3928
|
+
return ctx.json({ slowest: result[0] || [] });
|
|
3929
|
+
});
|
|
3930
|
+
this.router.get("/registry", (ctx) => {
|
|
3931
|
+
const app = this[$appRoot];
|
|
3932
|
+
if (!this.instrumented && app) {
|
|
3933
|
+
this.instrumentApp(app);
|
|
3101
3934
|
}
|
|
3102
|
-
|
|
3103
|
-
if (
|
|
3104
|
-
|
|
3105
|
-
|
|
3935
|
+
const registry = app?.getComponentRegistry?.();
|
|
3936
|
+
if (registry) {
|
|
3937
|
+
this.assignIdsToRegistry(registry, "root");
|
|
3938
|
+
}
|
|
3939
|
+
return ctx.json({ registry: registry || {} });
|
|
3940
|
+
});
|
|
3941
|
+
this.router.get("/requests", async (ctx) => {
|
|
3942
|
+
const result = await datastore.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
|
|
3943
|
+
return ctx.json({ requests: result[0] || [] });
|
|
3944
|
+
});
|
|
3945
|
+
this.router.get("/requests/:id", async (ctx) => {
|
|
3946
|
+
const result = await datastore.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
|
|
3947
|
+
return ctx.json({ request: result[0]?.[0] });
|
|
3948
|
+
});
|
|
3949
|
+
this.router.get("/failures", async (ctx) => {
|
|
3950
|
+
const result = await datastore.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
|
|
3951
|
+
return ctx.json({ failures: result[0] });
|
|
3952
|
+
});
|
|
3953
|
+
this.router.post("/replay", async (ctx) => {
|
|
3954
|
+
const body = await ctx.body();
|
|
3955
|
+
const app = this[$appRoot];
|
|
3956
|
+
if (!app) return unknownError(ctx);
|
|
3957
|
+
try {
|
|
3958
|
+
const result = await app.processRequest({
|
|
3959
|
+
method: body.method,
|
|
3960
|
+
path: body.url,
|
|
3961
|
+
// or path
|
|
3962
|
+
headers: body.headers,
|
|
3963
|
+
body: body.body
|
|
3106
3964
|
});
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
raw: data
|
|
3115
|
-
};
|
|
3965
|
+
return ctx.json({
|
|
3966
|
+
status: result.status,
|
|
3967
|
+
headers: result.headers,
|
|
3968
|
+
data: result.data
|
|
3969
|
+
});
|
|
3970
|
+
} catch (e) {
|
|
3971
|
+
return ctx.json({ error: String(e) }, 500);
|
|
3116
3972
|
}
|
|
3973
|
+
});
|
|
3974
|
+
this.router.get("/", async (ctx) => {
|
|
3975
|
+
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
3976
|
+
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
3977
|
+
const linkPattern = this.getLinkPattern();
|
|
3978
|
+
const template = await readFile(Dashboard.getBasePath() + "/template.eta", "utf8");
|
|
3979
|
+
return ctx.html(this.eta.renderString(template, {
|
|
3980
|
+
metrics: this.metrics,
|
|
3981
|
+
uptime,
|
|
3982
|
+
rootPath: process.cwd(),
|
|
3983
|
+
linkPattern,
|
|
3984
|
+
headers: this.dashboardConfig.getRequestHeaders?.()
|
|
3985
|
+
}));
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3988
|
+
instrumentApp(app) {
|
|
3989
|
+
if (!app.getComponentRegistry) return;
|
|
3990
|
+
const registry = app.getComponentRegistry();
|
|
3991
|
+
this.assignIdsToRegistry(registry, "root");
|
|
3992
|
+
this.instrumented = true;
|
|
3993
|
+
}
|
|
3994
|
+
// Traverses registry, generates IDs, and attaches them to the actual function objects
|
|
3995
|
+
assignIdsToRegistry(node, parentId) {
|
|
3996
|
+
if (!node) return;
|
|
3997
|
+
const makeId = (type, parent, idx, name) => `${type}_${parent}_${idx}_${name.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
3998
|
+
node.middleware?.forEach((mw, idx) => {
|
|
3999
|
+
const id = makeId("mw", parentId, idx, mw.name);
|
|
4000
|
+
mw.id = id;
|
|
4001
|
+
if (mw._fn) mw._fn._debugId = id;
|
|
4002
|
+
});
|
|
4003
|
+
node.controllers?.forEach((ctrl, idx) => {
|
|
4004
|
+
const id = makeId("ctrl", parentId, idx, ctrl.name);
|
|
4005
|
+
ctrl.id = id;
|
|
4006
|
+
});
|
|
4007
|
+
node.routes?.forEach((r, idx) => {
|
|
4008
|
+
const id = makeId("route", parentId, idx, r.handlerName || "handler");
|
|
4009
|
+
r.id = id;
|
|
4010
|
+
if (r._fn) r._fn._debugId = id;
|
|
4011
|
+
});
|
|
4012
|
+
node.routers?.forEach((r, idx) => {
|
|
4013
|
+
const id = makeId("router", parentId, idx, r.path);
|
|
4014
|
+
r.id = id;
|
|
4015
|
+
this.assignIdsToRegistry(r.children, id);
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
recordNodeMetric(id, type, duration, isError) {
|
|
4019
|
+
if (!this.metrics.nodeMetrics[id]) {
|
|
4020
|
+
this.metrics.nodeMetrics[id] = {
|
|
4021
|
+
id,
|
|
4022
|
+
type,
|
|
4023
|
+
requests: 0,
|
|
4024
|
+
totalTime: 0,
|
|
4025
|
+
failures: 0,
|
|
4026
|
+
name: id
|
|
4027
|
+
// simplify
|
|
4028
|
+
};
|
|
3117
4029
|
}
|
|
3118
|
-
|
|
4030
|
+
const m = this.metrics.nodeMetrics[id];
|
|
4031
|
+
m.requests++;
|
|
4032
|
+
m.totalTime += duration;
|
|
4033
|
+
if (isError) m.failures++;
|
|
3119
4034
|
}
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
4035
|
+
recordEdgeMetric(from, to) {
|
|
4036
|
+
const key = `${from}|${to}`;
|
|
4037
|
+
this.metrics.edgeMetrics[key] = (this.metrics.edgeMetrics[key] || 0) + 1;
|
|
4038
|
+
}
|
|
4039
|
+
getLinkPattern() {
|
|
4040
|
+
const term = process.env["TERM_PROGRAM"] || "";
|
|
4041
|
+
if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
|
|
4042
|
+
return "vscode://file/{{absolute}}:{{line}}";
|
|
4043
|
+
}
|
|
4044
|
+
return "file:///{{absolute}}:{{line}}";
|
|
4045
|
+
}
|
|
4046
|
+
getHooks() {
|
|
4047
|
+
return {
|
|
4048
|
+
onRequestStart: (ctx) => {
|
|
4049
|
+
const app = this[$appRoot];
|
|
4050
|
+
if (!this.instrumented && app) {
|
|
4051
|
+
this.instrumentApp(app);
|
|
4052
|
+
}
|
|
4053
|
+
this.metrics.totalRequests++;
|
|
4054
|
+
this.metrics.activeRequests++;
|
|
4055
|
+
ctx._debugStartTime = performance.now();
|
|
4056
|
+
ctx[$debug] = new Collector(this);
|
|
4057
|
+
},
|
|
4058
|
+
onResponseEnd: async (ctx, response) => {
|
|
4059
|
+
this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
|
|
4060
|
+
const start = ctx._debugStartTime;
|
|
4061
|
+
let duration = 0;
|
|
4062
|
+
if (start) {
|
|
4063
|
+
duration = performance.now() - start;
|
|
4064
|
+
this.updateTiming(duration);
|
|
4065
|
+
}
|
|
4066
|
+
const isError = response.status >= 400;
|
|
4067
|
+
this.metricsCollector.recordRequest(duration, isError);
|
|
4068
|
+
if (response.status >= 400) {
|
|
4069
|
+
this.metrics.failedRequests++;
|
|
4070
|
+
if (response.status === 429) {
|
|
4071
|
+
const path = ctx.path;
|
|
4072
|
+
this.metrics.rateLimitedCounts[path] = (this.metrics.rateLimitedCounts[path] || 0) + 1;
|
|
4073
|
+
}
|
|
4074
|
+
try {
|
|
4075
|
+
const headers = {};
|
|
4076
|
+
if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
|
|
4077
|
+
ctx.request.headers.forEach((v, k) => {
|
|
4078
|
+
headers[k] = v;
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
4081
|
+
await datastore.set(new RecordId("failed_requests", ctx.requestId), {
|
|
4082
|
+
method: ctx.method,
|
|
4083
|
+
url: ctx.url.toString(),
|
|
4084
|
+
headers,
|
|
4085
|
+
status: response.status,
|
|
4086
|
+
timestamp: Date.now(),
|
|
4087
|
+
state: ctx.state
|
|
4088
|
+
// body?
|
|
4089
|
+
});
|
|
4090
|
+
} catch (e) {
|
|
4091
|
+
console.error("Failed to record failed request", e);
|
|
4092
|
+
}
|
|
4093
|
+
} else {
|
|
4094
|
+
this.metrics.successfulRequests++;
|
|
4095
|
+
}
|
|
4096
|
+
const logEntry = {
|
|
4097
|
+
method: ctx.method,
|
|
4098
|
+
url: ctx.url.toString(),
|
|
4099
|
+
status: response.status,
|
|
4100
|
+
duration,
|
|
4101
|
+
timestamp: Date.now(),
|
|
4102
|
+
handlerStack: ctx.handlerStack
|
|
4103
|
+
};
|
|
4104
|
+
this.metrics.logs.push(logEntry);
|
|
3132
4105
|
try {
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
4106
|
+
await datastore.set(new RecordId("requests", ctx.requestId), logEntry);
|
|
4107
|
+
} catch (e) {
|
|
4108
|
+
console.error("Failed to record request log", e);
|
|
4109
|
+
}
|
|
4110
|
+
const retention = this.dashboardConfig.retentionMs ?? 72e5;
|
|
4111
|
+
const cutoff = Date.now() - retention;
|
|
4112
|
+
if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
|
|
4113
|
+
this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
|
|
3136
4114
|
}
|
|
3137
4115
|
}
|
|
3138
|
-
return next();
|
|
3139
4116
|
};
|
|
3140
4117
|
}
|
|
4118
|
+
updateTiming(duration) {
|
|
4119
|
+
const alpha = 0.1;
|
|
4120
|
+
if (this.metrics.averageTotalTime_ms === 0) {
|
|
4121
|
+
this.metrics.averageTotalTime_ms = duration;
|
|
4122
|
+
} else {
|
|
4123
|
+
this.metrics.averageTotalTime_ms = alpha * duration + (1 - alpha) * this.metrics.averageTotalTime_ms;
|
|
4124
|
+
}
|
|
4125
|
+
this.metrics.recentTimings.push(duration);
|
|
4126
|
+
if (this.metrics.recentTimings.length > 50) {
|
|
4127
|
+
this.metrics.recentTimings.shift();
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
function unknownError(ctx) {
|
|
4132
|
+
return ctx.json({ error: "Unknown Error" }, 500);
|
|
4133
|
+
}
|
|
4134
|
+
const eta = new Eta();
|
|
4135
|
+
class ScalarPlugin extends ShokupanRouter {
|
|
4136
|
+
constructor(pluginOptions = {}) {
|
|
4137
|
+
pluginOptions.config ??= {};
|
|
4138
|
+
super();
|
|
4139
|
+
this.pluginOptions = pluginOptions;
|
|
4140
|
+
this.init();
|
|
4141
|
+
}
|
|
4142
|
+
onInit(app, options) {
|
|
4143
|
+
if (options?.path) {
|
|
4144
|
+
app.mount(options.path, this);
|
|
4145
|
+
} else {
|
|
4146
|
+
app.mount(options.path ?? "/", this);
|
|
4147
|
+
}
|
|
4148
|
+
this.onMount(app);
|
|
4149
|
+
}
|
|
4150
|
+
init() {
|
|
4151
|
+
this.get("/", (ctx) => {
|
|
4152
|
+
let path = ctx.url.toString();
|
|
4153
|
+
if (!path.endsWith("/")) path += "/";
|
|
4154
|
+
return ctx.html(eta.renderString(`<!doctype html>
|
|
4155
|
+
<html>
|
|
4156
|
+
<head>
|
|
4157
|
+
<title>API Reference</title>
|
|
4158
|
+
<meta charset = "utf-8" />
|
|
4159
|
+
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
4160
|
+
</head>
|
|
4161
|
+
|
|
4162
|
+
<body>
|
|
4163
|
+
<div id="app"></div>
|
|
4164
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
|
|
4165
|
+
<script>
|
|
4166
|
+
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
4167
|
+
url: "<%= it.path %>openapi.json",
|
|
4168
|
+
}
|
|
4169
|
+
])
|
|
4170
|
+
<\/script>
|
|
4171
|
+
</body>
|
|
4172
|
+
|
|
4173
|
+
</html>`, { path, config: this.pluginOptions }));
|
|
4174
|
+
});
|
|
4175
|
+
this.get("/openapi.json", async (ctx) => {
|
|
4176
|
+
let spec;
|
|
4177
|
+
if (this.root.openApiSpec) {
|
|
4178
|
+
try {
|
|
4179
|
+
spec = structuredClone(this.root.openApiSpec);
|
|
4180
|
+
} catch (e) {
|
|
4181
|
+
spec = Object.assign({}, this.root.openApiSpec);
|
|
4182
|
+
}
|
|
4183
|
+
} else {
|
|
4184
|
+
spec = await (this.root || this).generateApiSpec();
|
|
4185
|
+
}
|
|
4186
|
+
if (this.pluginOptions.baseDocument) {
|
|
4187
|
+
deepMerge(spec, this.pluginOptions.baseDocument);
|
|
4188
|
+
}
|
|
4189
|
+
return ctx.json(spec);
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
4192
|
+
// New lifecycle method to be called by router.mount
|
|
4193
|
+
onMount(parent) {
|
|
4194
|
+
if (parent.onStart) {
|
|
4195
|
+
parent.onStart(async () => {
|
|
4196
|
+
if (this.pluginOptions.enableStaticAnalysis) {
|
|
4197
|
+
try {
|
|
4198
|
+
const entrypoint = process.argv[1];
|
|
4199
|
+
console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
|
|
4200
|
+
const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
4201
|
+
let staticSpec = await analyzer.analyze();
|
|
4202
|
+
if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
|
|
4203
|
+
deepMerge(this.pluginOptions.baseDocument, staticSpec);
|
|
4204
|
+
console.log("[ScalarPlugin] Static analysis completed successfully.");
|
|
4205
|
+
} catch (err) {
|
|
4206
|
+
console.error("[ScalarPlugin] Failed to run static analysis:", err);
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
});
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
3141
4212
|
}
|
|
3142
4213
|
function Compression(options = {}) {
|
|
3143
4214
|
const threshold = options.threshold ?? 512;
|
|
4215
|
+
const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
|
|
3144
4216
|
const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
|
|
3145
4217
|
const acceptEncoding = ctx.headers.get("accept-encoding") || "";
|
|
3146
4218
|
let method = null;
|
|
@@ -3153,24 +4225,27 @@ function Compression(options = {}) {
|
|
|
3153
4225
|
} else if (acceptEncoding.includes("gzip")) method = "gzip";
|
|
3154
4226
|
else if (acceptEncoding.includes("deflate")) method = "deflate";
|
|
3155
4227
|
if (!method) return next();
|
|
4228
|
+
if (!allowedAlgorithms.has(method)) {
|
|
4229
|
+
return next();
|
|
4230
|
+
}
|
|
3156
4231
|
let response = await next();
|
|
3157
|
-
if (!(response instanceof Response) && ctx
|
|
3158
|
-
response = ctx
|
|
4232
|
+
if (!(response instanceof Response) && ctx[$finalResponse] instanceof Response) {
|
|
4233
|
+
response = ctx[$finalResponse];
|
|
3159
4234
|
}
|
|
3160
4235
|
if (response instanceof Response) {
|
|
3161
4236
|
if (response.headers.has("Content-Encoding")) return response;
|
|
3162
4237
|
let body;
|
|
3163
4238
|
let bodySize;
|
|
3164
|
-
if (ctx
|
|
3165
|
-
if (typeof ctx
|
|
3166
|
-
const encoded = new TextEncoder().encode(ctx
|
|
4239
|
+
if (ctx[$rawBody] !== void 0) {
|
|
4240
|
+
if (typeof ctx[$rawBody] === "string") {
|
|
4241
|
+
const encoded = new TextEncoder().encode(ctx[$rawBody]);
|
|
3167
4242
|
body = encoded;
|
|
3168
4243
|
bodySize = encoded.byteLength;
|
|
3169
|
-
} else if (ctx
|
|
3170
|
-
body = ctx
|
|
3171
|
-
bodySize = ctx.
|
|
4244
|
+
} else if (ctx[$rawBody] instanceof Uint8Array) {
|
|
4245
|
+
body = ctx[$rawBody];
|
|
4246
|
+
bodySize = ctx[$rawBody].byteLength;
|
|
3172
4247
|
} else {
|
|
3173
|
-
body = ctx
|
|
4248
|
+
body = ctx[$rawBody];
|
|
3174
4249
|
bodySize = body.byteLength;
|
|
3175
4250
|
}
|
|
3176
4251
|
} else {
|
|
@@ -3707,77 +4782,6 @@ function enableOpenApiValidation(app) {
|
|
|
3707
4782
|
precompileValidators(app, spec);
|
|
3708
4783
|
});
|
|
3709
4784
|
}
|
|
3710
|
-
const eta = new Eta();
|
|
3711
|
-
class ScalarPlugin extends ShokupanRouter {
|
|
3712
|
-
constructor(pluginOptions = {}) {
|
|
3713
|
-
pluginOptions.config ??= {};
|
|
3714
|
-
super();
|
|
3715
|
-
this.pluginOptions = pluginOptions;
|
|
3716
|
-
this.init();
|
|
3717
|
-
}
|
|
3718
|
-
init() {
|
|
3719
|
-
this.get("/", (ctx) => {
|
|
3720
|
-
let path = ctx.url.toString();
|
|
3721
|
-
if (!path.endsWith("/")) path += "/";
|
|
3722
|
-
return ctx.html(eta.renderString(`<!doctype html>
|
|
3723
|
-
<html>
|
|
3724
|
-
<head>
|
|
3725
|
-
<title>API Reference</title>
|
|
3726
|
-
<meta charset = "utf-8" />
|
|
3727
|
-
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
3728
|
-
</head>
|
|
3729
|
-
|
|
3730
|
-
<body>
|
|
3731
|
-
<div id="app"></div>
|
|
3732
|
-
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
|
|
3733
|
-
<script>
|
|
3734
|
-
Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
|
|
3735
|
-
url: "<%= it.path %>openapi.json",
|
|
3736
|
-
}
|
|
3737
|
-
])
|
|
3738
|
-
<\/script>
|
|
3739
|
-
</body>
|
|
3740
|
-
|
|
3741
|
-
</html>`, { path, config: this.pluginOptions }));
|
|
3742
|
-
});
|
|
3743
|
-
this.get("/openapi.json", async (ctx) => {
|
|
3744
|
-
let spec;
|
|
3745
|
-
if (this.root.openApiSpec) {
|
|
3746
|
-
try {
|
|
3747
|
-
spec = structuredClone(this.root.openApiSpec);
|
|
3748
|
-
} catch (e) {
|
|
3749
|
-
spec = Object.assign({}, this.root.openApiSpec);
|
|
3750
|
-
}
|
|
3751
|
-
} else {
|
|
3752
|
-
spec = await (this.root || this).generateApiSpec();
|
|
3753
|
-
}
|
|
3754
|
-
if (this.pluginOptions.baseDocument) {
|
|
3755
|
-
deepMerge(spec, this.pluginOptions.baseDocument);
|
|
3756
|
-
}
|
|
3757
|
-
return ctx.json(spec);
|
|
3758
|
-
});
|
|
3759
|
-
}
|
|
3760
|
-
// New lifecycle method to be called by router.mount
|
|
3761
|
-
onMount(parent) {
|
|
3762
|
-
if (parent.onStart) {
|
|
3763
|
-
parent.onStart(async () => {
|
|
3764
|
-
if (this.pluginOptions.enableStaticAnalysis) {
|
|
3765
|
-
try {
|
|
3766
|
-
const entrypoint = process.argv[1];
|
|
3767
|
-
console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
|
|
3768
|
-
const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
3769
|
-
let staticSpec = await analyzer.analyze();
|
|
3770
|
-
if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
|
|
3771
|
-
deepMerge(this.pluginOptions.baseDocument, staticSpec);
|
|
3772
|
-
console.log("[ScalarPlugin] Static analysis completed successfully.");
|
|
3773
|
-
} catch (err) {
|
|
3774
|
-
console.error("[ScalarPlugin] Failed to run static analysis:", err);
|
|
3775
|
-
}
|
|
3776
|
-
}
|
|
3777
|
-
});
|
|
3778
|
-
}
|
|
3779
|
-
}
|
|
3780
|
-
}
|
|
3781
4785
|
function SecurityHeaders(options = {}) {
|
|
3782
4786
|
const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
|
|
3783
4787
|
const headers = {};
|
|
@@ -3901,18 +4905,18 @@ class MemoryStore extends EventEmitter {
|
|
|
3901
4905
|
}
|
|
3902
4906
|
set(sid, sess, cb) {
|
|
3903
4907
|
this.sessions[sid] = JSON.stringify(sess);
|
|
3904
|
-
cb
|
|
4908
|
+
cb?.();
|
|
3905
4909
|
}
|
|
3906
4910
|
destroy(sid, cb) {
|
|
3907
4911
|
delete this.sessions[sid];
|
|
3908
|
-
cb
|
|
4912
|
+
cb?.();
|
|
3909
4913
|
}
|
|
3910
4914
|
touch(sid, sess, cb) {
|
|
3911
4915
|
const current = this.sessions[sid];
|
|
3912
4916
|
if (current) {
|
|
3913
4917
|
this.sessions[sid] = JSON.stringify(sess);
|
|
3914
4918
|
}
|
|
3915
|
-
cb
|
|
4919
|
+
cb?.();
|
|
3916
4920
|
}
|
|
3917
4921
|
all(cb) {
|
|
3918
4922
|
const result = {};
|
|
@@ -3928,7 +4932,7 @@ class MemoryStore extends EventEmitter {
|
|
|
3928
4932
|
}
|
|
3929
4933
|
clear(cb) {
|
|
3930
4934
|
this.sessions = {};
|
|
3931
|
-
cb
|
|
4935
|
+
cb?.();
|
|
3932
4936
|
}
|
|
3933
4937
|
}
|
|
3934
4938
|
function sign(val, secret) {
|
|
@@ -4107,29 +5111,51 @@ function Session(options) {
|
|
|
4107
5111
|
}
|
|
4108
5112
|
export {
|
|
4109
5113
|
$appRoot,
|
|
5114
|
+
$bodyParseError,
|
|
5115
|
+
$bodyParsed,
|
|
5116
|
+
$bodyType,
|
|
5117
|
+
$cachedBody,
|
|
5118
|
+
$cachedHost,
|
|
5119
|
+
$cachedHostname,
|
|
5120
|
+
$cachedOrigin,
|
|
5121
|
+
$cachedProtocol,
|
|
5122
|
+
$cachedQuery,
|
|
4110
5123
|
$childControllers,
|
|
4111
5124
|
$childRouters,
|
|
4112
5125
|
$controllerPath,
|
|
5126
|
+
$debug,
|
|
4113
5127
|
$dispatch,
|
|
5128
|
+
$eventMethods,
|
|
5129
|
+
$finalResponse,
|
|
5130
|
+
$io,
|
|
4114
5131
|
$isApplication,
|
|
4115
5132
|
$isMounted,
|
|
4116
5133
|
$isRouter,
|
|
4117
5134
|
$middleware,
|
|
4118
5135
|
$mountPath,
|
|
4119
5136
|
$parent,
|
|
5137
|
+
$rawBody,
|
|
5138
|
+
$requestId,
|
|
4120
5139
|
$routeArgs,
|
|
5140
|
+
$routeMatched,
|
|
4121
5141
|
$routeMethods,
|
|
4122
5142
|
$routeSpec,
|
|
4123
5143
|
$routes,
|
|
5144
|
+
$socket,
|
|
5145
|
+
$url,
|
|
5146
|
+
$ws,
|
|
4124
5147
|
All,
|
|
4125
5148
|
AuthPlugin,
|
|
4126
5149
|
Body,
|
|
5150
|
+
ClusterPlugin,
|
|
4127
5151
|
Compression,
|
|
4128
5152
|
Container,
|
|
4129
5153
|
Controller,
|
|
4130
5154
|
Cors,
|
|
4131
5155
|
Ctx,
|
|
5156
|
+
Dashboard,
|
|
4132
5157
|
Delete,
|
|
5158
|
+
Event,
|
|
4133
5159
|
Get,
|
|
4134
5160
|
HTTPMethods,
|
|
4135
5161
|
Head,
|
|
@@ -4147,16 +5173,13 @@ export {
|
|
|
4147
5173
|
RateLimitMiddleware,
|
|
4148
5174
|
Req,
|
|
4149
5175
|
RouteParamType,
|
|
4150
|
-
RouterRegistry,
|
|
4151
5176
|
ScalarPlugin,
|
|
4152
5177
|
SecurityHeaders,
|
|
4153
5178
|
Session,
|
|
4154
5179
|
Shokupan,
|
|
4155
|
-
ShokupanApplicationTree,
|
|
4156
5180
|
ShokupanContext,
|
|
4157
5181
|
ShokupanRequest,
|
|
4158
5182
|
ShokupanResponse,
|
|
4159
|
-
ShokupanRouter,
|
|
4160
5183
|
Spec,
|
|
4161
5184
|
Use,
|
|
4162
5185
|
ValidationError,
|