shokupan 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +53 -0
  2. package/dist/context.d.ts +50 -15
  3. package/dist/{http-server-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
  4. package/dist/http-server-BEMPIs33.cjs.map +1 -0
  5. package/dist/{http-server-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
  6. package/dist/http-server-CCeagTyU.js.map +1 -0
  7. package/dist/index.cjs +998 -136
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +996 -135
  11. package/dist/index.js.map +1 -1
  12. package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
  13. package/dist/plugins/application/dashboard/plugin.d.ts +14 -8
  14. package/dist/plugins/application/dashboard/static/charts.js +328 -0
  15. package/dist/plugins/application/dashboard/static/failures.js +85 -0
  16. package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
  17. package/dist/plugins/application/dashboard/static/poll.js +146 -0
  18. package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
  19. package/dist/plugins/application/dashboard/static/registry.css +131 -0
  20. package/dist/plugins/application/dashboard/static/registry.js +269 -0
  21. package/dist/plugins/application/dashboard/static/requests.js +118 -0
  22. package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
  23. package/dist/plugins/application/dashboard/static/styles.css +175 -0
  24. package/dist/plugins/application/dashboard/static/tables.js +92 -0
  25. package/dist/plugins/application/dashboard/static/tabs.js +113 -0
  26. package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
  27. package/dist/plugins/application/dashboard/template.eta +246 -0
  28. package/dist/plugins/application/socket-io.d.ts +14 -0
  29. package/dist/router.d.ts +12 -0
  30. package/dist/shokupan.d.ts +21 -1
  31. package/dist/util/datastore.d.ts +4 -3
  32. package/dist/util/decorators.d.ts +5 -0
  33. package/dist/util/http-error.d.ts +38 -0
  34. package/dist/util/http-status.d.ts +30 -0
  35. package/dist/util/request.d.ts +1 -1
  36. package/dist/util/symbol.d.ts +19 -0
  37. package/dist/util/types.d.ts +30 -1
  38. package/package.json +6 -3
  39. package/dist/http-server-0xH174zz.js.map +0 -1
  40. package/dist/http-server-DFhwlK8e.cjs.map +0 -1
@@ -0,0 +1,246 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Shokupan Debug Dashboard</title>
8
+ <link href="https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://esm.sh/@xyflow/react@12.3.6/dist/style.css" />
10
+ <style id="styles"><%~ include("styles.css") %></style>
11
+ <style id="reactflow"><%~ include("reactflow.css") %></style>
12
+ <style id="registry"><%~ include("registry.css") %></style>
13
+ <style id="scrollbar"><%~ include("scrollbar.css") %></style>
14
+ <style id="tabulator"><%~ include("tabulator.css") %></style>
15
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
16
+ <script type="text/javascript" src="https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js"></script>
17
+ </head>
18
+ <body>
19
+ <div class="container">
20
+ <header>
21
+ <div>
22
+ <h1>Debug Dashboard</h1>
23
+ <div style="color: var(--text-secondary)">Uptime: <span id="uptime"><%= it.uptime %></span></div>
24
+ </div>
25
+ <div class="tabs">
26
+ <button class="tab-btn active" onclick="switchTab('overview')">Overview</button>
27
+ <button class="tab-btn" onclick="switchTab('registry')">Registry</button>
28
+ <button class="tab-btn" onclick="switchTab('graph')">Graph</button>
29
+ <button class="tab-btn" onclick="switchTab('requests')">Requests</button>
30
+ <button class="tab-btn" onclick="switchTab('failures')">Failures</button>
31
+ </div>
32
+ </header>
33
+
34
+ <!-- Overview Tab -->
35
+ <div id="tab-overview" class="tab-content active">
36
+
37
+ <div class="metrics-grid">
38
+ <div class="card">
39
+ <div class="card-title">Total Requests</div>
40
+ <div class="card-value" id="total-requests">
41
+ <%= it.metrics.totalRequests %>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="card">
46
+ <div class="card-title">Active Requests</div>
47
+ <div class="card-value" style="color: var(--accent)" id="active-requests">
48
+ <%= it.metrics.activeRequests %>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="card">
53
+ <div class="card-title">Success Rate</div>
54
+ <div class="card-value text-success">
55
+ <span id="success-rate">
56
+ <%= (it.metrics.totalRequests - it.metrics.activeRequests) ?
57
+ Math.round((it.metrics.successfulRequests / (it.metrics.totalRequests -
58
+ it.metrics.activeRequests)) * 100) : 100 %>%
59
+ </span>
60
+ </div>
61
+ <div style="color: var(--text-secondary); margin-top: 0.5rem">
62
+ <span id="successful-requests">
63
+ <%= it.metrics.successfulRequests %>
64
+ </span> successful
65
+ </div>
66
+ </div>
67
+
68
+ <div class="card">
69
+ <div class="card-title">Fail Rate</div>
70
+ <div class="card-value text-error">
71
+ <span id="fail-rate">
72
+ <%= (it.metrics.totalRequests - it.metrics.activeRequests) ?
73
+ Math.round((it.metrics.failedRequests / (it.metrics.totalRequests -
74
+ it.metrics.activeRequests)) * 100) : 0 %>%
75
+ </span>
76
+ </div>
77
+ <div style="color: var(--text-secondary); margin-top: 0.5rem">
78
+ <span id="failed-requests">
79
+ <%= it.metrics.failedRequests %>
80
+ </span> failed
81
+ </div>
82
+ </div>
83
+
84
+ <div class="card">
85
+ <div class="card-title">Avg Latency</div>
86
+ <div class="card-value">
87
+ <span id="avg-latency">
88
+ <%= it.metrics.averageTotalTime_ms.toFixed(2) %>
89
+ </span> <span style="font-size: 1rem; color: var(--text-secondary)">ms</span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <div id="chart-container" style="display: flex; flex-direction: column; gap: 1rem;">
95
+ <div style="display: flex; justify-content: flex-end;">
96
+ <select id="time-range-selector" onchange="updateCharts(); updateDashboard(); fetchTopStats();" style="background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px; border-radius: 4px;">
97
+ <option value="1m">1 Minute</option>
98
+ <option value="5m">5 Minutes</option>
99
+ <option value="30m">30 Minutes</option>
100
+ <option value="1h">1 Hour</option>
101
+ <option value="2h">2 Hours</option>
102
+ <option value="6h">6 Hours</option>
103
+ <option value="12h">12 Hours</option>
104
+ <option value="1d">1 Day</option>
105
+ <option value="3d">3 Days</option>
106
+ <option value="7d">7 Days</option>
107
+ <option value="30d">30 Days</option>
108
+ </select>
109
+ </div>
110
+
111
+
112
+ <div class="card-container" >
113
+ <div class="card" style="height: 300px;">
114
+ <div class="card-title">Response Time</div>
115
+ <div class="card-chart">
116
+ <canvas id="latencyChart"></canvas>
117
+ </div>
118
+ </div>
119
+ <div class="card" style="height: 300px;">
120
+ <div class="card-title">Requests / Second</div>
121
+ <div class="card-chart">
122
+ <canvas id="rpsChart"></canvas>
123
+ </div>
124
+ </div>
125
+ <div class="card" style="height: 300px;">
126
+ <div class="card-title">CPU & Load</div>
127
+ <div class="card-chart">
128
+ <canvas id="cpuChart"></canvas>
129
+ </div>
130
+ </div>
131
+ <div class="card" style="height: 300px;">
132
+ <div class="card-title">Memory</div>
133
+ <div class="card-chart">
134
+ <canvas id="memoryChart"></canvas>
135
+ </div>
136
+ </div>
137
+ <div class="card" style="height: 300px;">
138
+ <div class="card-title">Heap Usage</div>
139
+ <div class="card-chart">
140
+ <canvas id="heapChart"></canvas>
141
+ </div>
142
+ </div>
143
+ <div class="card" style="height: 300px;">
144
+ <div class="card-title">Event Loop Latency</div>
145
+ <div class="card-chart">
146
+ <canvas id="eventLoopChart"></canvas>
147
+ </div>
148
+ </div>
149
+ <div class="card" style="height: 300px;">
150
+ <div class="card-title">Error Rate</div>
151
+ <div class="card-chart">
152
+ <canvas id="errorRateChart"></canvas>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <div class="card-title" style="margin-top: 1rem;">Top Statistics</div>
158
+ <div class="card-container">
159
+ <div class="card">
160
+ <div class="card-title">Top Requests</div>
161
+ <div id="top-requests-table"></div>
162
+ </div>
163
+ <div class="card">
164
+ <div class="card-title">Top Errors</div>
165
+ <div id="top-errors-table"></div>
166
+ </div>
167
+ <div class="card">
168
+ <div class="card-title">Most Frequent Failures</div>
169
+ <div id="failing-requests-table"></div>
170
+ </div>
171
+ <div class="card">
172
+ <div class="card-title">Slowest Requests</div>
173
+ <div id="slowest-requests-table"></div>
174
+ </div>
175
+ </div>
176
+
177
+ <div id="table-container" style="padding: 0; margin-top: 1rem;">
178
+ <div id="requests-table" class="table-dark"></div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+
183
+ <!-- Registry Tab -->
184
+ <div id="tab-registry" class="tab-content">
185
+ <div id="registry-container" class="card" style="margin-top: 2rem;">
186
+ <div class="card-title">Component Registry</div>
187
+ <div id="registry-tree" style="padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;"></div>
188
+ </div>
189
+ </div>
190
+
191
+ <!-- Graph Tab -->
192
+ <div id="tab-graph" class="tab-content">
193
+ <div class="card" style="margin-bottom: 1rem;">
194
+ <div style="display: flex; gap: 1rem;">
195
+ <input type="text" id="graph-search" placeholder="Search routes or middleware..." style="flex:1; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg-primary); border: 1px solid var(--card-border); color: var(--text-primary);">
196
+ </div>
197
+ </div>
198
+ <div id="cy"></div>
199
+ </div>
200
+
201
+ <!-- Requests Tab -->
202
+ <div id="tab-requests" class="tab-content">
203
+ <div class="card" style="margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;">
204
+ <div class="card-title">Recent Requests (Last 100)</div>
205
+ <div>
206
+ <button onclick="fetchRequests()" style="background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;">Refresh</button>
207
+ </div>
208
+ </div>
209
+ <div id="requests-list-container" style="height: calc(100vh - 300px); margin-bottom: 2rem;"></div>
210
+
211
+ <div id="request-details-container" class="card" style="display: none;">
212
+ <div class="card-title">Request Details</div>
213
+ <div id="request-details-content"></div>
214
+ <div class="card-title" style="margin-top: 1rem;">Middleware Trace</div>
215
+ <div id="middleware-trace-container"></div>
216
+ </div>
217
+ </div>
218
+
219
+ <!-- Failures Tab -->
220
+ <div id="tab-failures" class="tab-content">
221
+ <div class="card" style="margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;">
222
+ <div class="card-title">Failed Requests (Last 50)</div>
223
+ <div>
224
+ <button onclick="importFailure()" style="background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-right: 8px;">Import</button>
225
+ <button onclick="fetchFailures()" style="background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;">Refresh</button>
226
+ </div>
227
+ </div>
228
+ <div id="failures-table-container"></div>
229
+ </div>
230
+ </div>
231
+
232
+ <script>
233
+ // Injected function from server config
234
+ const getRequestHeaders = <%~ it.getRequestHeaders || "undefined" %>;
235
+ </script>
236
+ <script id="poll.js"><%~ include("poll.js") %></script>
237
+ <script id="graph.mjs" type="module"><%~ include("graph.mjs") %></script>
238
+ <script id="charts.js"><%~ include("charts.js") %></script>
239
+ <script id="tables.js"><%~ include("tables.js") %></script>
240
+ <script id="registry.js"><%~ include("registry.js") %></script>
241
+ <script id="failures.js"><%~ include("failures.js") %></script>
242
+ <script id="requests.js"><%~ include("requests.js") %></script>
243
+ <script id="tabs.js"><%~ include("tabs.js") %></script>
244
+ </body>
245
+
246
+ </html>
@@ -0,0 +1,14 @@
1
+ import { Server } from 'socket.io';
2
+ import { Shokupan } from '../../shokupan';
3
+ /**
4
+ * Attaches the Shokupan HTTP Bridge and Event System to a Socket.IO server.
5
+ * This makes the Shokupan HTTP APIs accessible via Socket.IO events.
6
+ *
7
+ * Send events as `shokupan:request` events with the payload { type: "http", id: "123", body: {} }.
8
+ *
9
+ * Responses are emitted as `shokupan:response` events with the payload { id, status, body }
10
+ *
11
+ * @param io The Socket.IO server instance
12
+ * @param app The Shokupan application instance
13
+ */
14
+ export declare function attachSocketIOBridge(io: Server, app: Shokupan): void;
package/dist/router.d.ts CHANGED
@@ -91,6 +91,9 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
91
91
  autoBackpressureLevel?: number;
92
92
  enableMiddlewareTracking?: boolean;
93
93
  middlewareTrackingMaxCapacity?: number;
94
+ enableHTTPBridge?: boolean;
95
+ websocketErrorHandler?: (err: any, ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void | Promise<void>;
96
+ idGenerator?: () => string;
94
97
  middlewareTrackingTTL?: number;
95
98
  httpLogger?: (ctx: ShokupanContext<Record<string, any>, Record<string, string>>) => void;
96
99
  logger?: {
@@ -136,6 +139,7 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
136
139
  private trie;
137
140
  metadata?: RouteMetadata;
138
141
  private currentGuards;
142
+ private eventHandlers;
139
143
  getComponentRegistry(): {
140
144
  metadata: RouteMetadata;
141
145
  middleware: {
@@ -174,6 +178,14 @@ export declare class ShokupanRouter<T extends Record<string, any> = Record<strin
174
178
  };
175
179
  constructor(config?: ShokupanRouteConfig);
176
180
  private isRouterInstance;
181
+ /**
182
+ * Registers an event handler for WebSocket.
183
+ */
184
+ event(name: string, handler: ShokupanHandler<T>): this;
185
+ /**
186
+ * Finds an event handler(s) by name.
187
+ */
188
+ findEvent(name: string): ShokupanHandler<T>[] | null;
177
189
  /**
178
190
  * Mounts a controller instance to a path prefix.
179
191
  *
@@ -72,6 +72,7 @@ export declare class Shokupan<T = any> extends ShokupanRouter<T> {
72
72
  openApiSpec?: any;
73
73
  private composedMiddleware?;
74
74
  private cpuMonitor?;
75
+ private server?;
75
76
  get logger(): {
76
77
  verbose?: boolean;
77
78
  info?: (msg: string, props: Record<string, any>) => void;
@@ -108,7 +109,26 @@ export declare class Shokupan<T = any> extends ShokupanRouter<T> {
108
109
  * @param port - The port to listen on. If not specified, the port from the configuration is used. If that is not specified, port 3000 is used.
109
110
  * @returns The server instance.
110
111
  */
111
- listen(port?: number): Promise<Server | import('http').Server<typeof import('http').IncomingMessage, typeof import('http').ServerResponse>>;
112
+ listen(port?: number): Promise<Server>;
113
+ /**
114
+ * Stops the application server.
115
+ *
116
+ * This method gracefully shuts down the server and stops any running monitors.
117
+ * Works transparently in both Bun and Node.js runtimes.
118
+ *
119
+ * @returns A promise that resolves when the server has been stopped.
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const app = new Shokupan();
124
+ * const server = await app.listen(3000);
125
+ *
126
+ * // Later, when you want to stop the server
127
+ * await app.stop();
128
+ * ```
129
+ * @param closeActiveConnections — Immediately terminate in-flight requests, websockets, and stop accepting new connections.
130
+ */
131
+ stop(closeActiveConnections?: boolean): Promise<void>;
112
132
  [$dispatch](req: ShokupanRequest<T>): Promise<Response>;
113
133
  /**
114
134
  * Processes a request by wrapping the standard fetch method.
@@ -1,6 +1,7 @@
1
+ import { RecordId, Range, Table } from 'surrealdb';
1
2
  export declare const datastore: {
2
- get<T extends Record<string, any>>(store: string, key: string): Promise<T>;
3
- set(store: string, key: string, value: any): Promise<any>;
4
- query(query: string, vars?: Record<string, unknown>): Promise<any>;
3
+ get<T extends Record<string, any>>(recordId: RecordId | Table | Range<any, any>): Promise<T>;
4
+ set(recordId: RecordId, value: Record<string, any>): Promise<any>;
5
+ query<T extends Record<string, any>>(query: string, vars?: Record<string, unknown>): Promise<T>;
5
6
  readonly ready: Promise<any>;
6
7
  };
@@ -68,6 +68,11 @@ export declare const Head: (path?: string) => (target: any, propertyKey: string,
68
68
  * Decorator: Binds a method to ANY HTTP verb.
69
69
  */
70
70
  export declare const All: (path?: string) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
71
+ /**
72
+ * Decorator: Binds a method to the WebSocket event.
73
+ * @param eventName The name of the event to listen for.
74
+ */
75
+ export declare function Event(eventName: string): (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void;
71
76
  /**
72
77
  * Decorator: Applies a rate limit to a class or method.
73
78
  */
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Standard HTTP Error class with status code.
3
+ * This standardizes on the `status` property instead of dual `status`/`statusCode`.
4
+ */
5
+ export declare class HttpError extends Error {
6
+ readonly status: number;
7
+ constructor(message: string, status: number);
8
+ }
9
+ /**
10
+ * Extracts HTTP status code from an error object.
11
+ * Supports both `status` and `statusCode` properties for backward compatibility.
12
+ * Defaults to 500 (Internal Server Error) if no status is found.
13
+ *
14
+ * @param err - Error object (may have `status` or `statusCode` property)
15
+ * @returns HTTP status code
16
+ */
17
+ export declare function getErrorStatus(err: any): number;
18
+ /**
19
+ * Common HTTP Errors
20
+ */
21
+ export declare class BadRequestError extends HttpError {
22
+ constructor(message?: string);
23
+ }
24
+ export declare class UnauthorizedError extends HttpError {
25
+ constructor(message?: string);
26
+ }
27
+ export declare class ForbiddenError extends HttpError {
28
+ constructor(message?: string);
29
+ }
30
+ export declare class NotFoundError extends HttpError {
31
+ constructor(message?: string);
32
+ }
33
+ export declare class InternalServerError extends HttpError {
34
+ constructor(message?: string);
35
+ }
36
+ export declare class EventError extends HttpError {
37
+ constructor(message?: string);
38
+ }
@@ -1,2 +1,32 @@
1
+ /**
2
+ * Common HTTP Status Codes
3
+ * Use these constants instead of magic numbers for better readability
4
+ */
5
+ export declare const HTTP_STATUS: {
6
+ readonly OK: 200;
7
+ readonly CREATED: 201;
8
+ readonly ACCEPTED: 202;
9
+ readonly NO_CONTENT: 204;
10
+ readonly MOVED_PERMANENTLY: 301;
11
+ readonly FOUND: 302;
12
+ readonly SEE_OTHER: 303;
13
+ readonly NOT_MODIFIED: 304;
14
+ readonly TEMPORARY_REDIRECT: 307;
15
+ readonly PERMANENT_REDIRECT: 308;
16
+ readonly BAD_REQUEST: 400;
17
+ readonly UNAUTHORIZED: 401;
18
+ readonly FORBIDDEN: 403;
19
+ readonly NOT_FOUND: 404;
20
+ readonly METHOD_NOT_ALLOWED: 405;
21
+ readonly REQUEST_TIMEOUT: 408;
22
+ readonly CONFLICT: 409;
23
+ readonly UNPROCESSABLE_ENTITY: 422;
24
+ readonly TOO_MANY_REQUESTS: 429;
25
+ readonly INTERNAL_SERVER_ERROR: 500;
26
+ readonly NOT_IMPLEMENTED: 501;
27
+ readonly BAD_GATEWAY: 502;
28
+ readonly SERVICE_UNAVAILABLE: 503;
29
+ readonly GATEWAY_TIMEOUT: 504;
30
+ };
1
31
  export declare const VALID_HTTP_STATUSES: Set<number>;
2
32
  export declare const VALID_REDIRECT_STATUSES: Set<number>;
@@ -2,7 +2,7 @@ import { Method } from './types';
2
2
  export type ShokupanRequestProps = {
3
3
  method: Method;
4
4
  url: string;
5
- headers: Headers;
5
+ headers: Headers | Record<string, string>;
6
6
  body: any;
7
7
  };
8
8
  /**
@@ -2,6 +2,7 @@ export declare const $isApplication: unique symbol;
2
2
  export declare const $appRoot: unique symbol;
3
3
  export declare const $isMounted: unique symbol;
4
4
  export declare const $routeMethods: unique symbol;
5
+ export declare const $eventMethods: unique symbol;
5
6
  export declare const $routeArgs: unique symbol;
6
7
  export declare const $controllerPath: unique symbol;
7
8
  export declare const $middleware: unique symbol;
@@ -13,3 +14,21 @@ export declare const $mountPath: unique symbol;
13
14
  export declare const $dispatch: unique symbol;
14
15
  export declare const $routes: unique symbol;
15
16
  export declare const $routeSpec: unique symbol;
17
+ export declare const $url: unique symbol;
18
+ export declare const $requestId: unique symbol;
19
+ export declare const $debug: unique symbol;
20
+ export declare const $finalResponse: unique symbol;
21
+ export declare const $rawBody: unique symbol;
22
+ export declare const $cachedBody: unique symbol;
23
+ export declare const $bodyType: unique symbol;
24
+ export declare const $bodyParsed: unique symbol;
25
+ export declare const $bodyParseError: unique symbol;
26
+ export declare const $routeMatched: unique symbol;
27
+ export declare const $cachedHostname: unique symbol;
28
+ export declare const $cachedProtocol: unique symbol;
29
+ export declare const $cachedHost: unique symbol;
30
+ export declare const $cachedOrigin: unique symbol;
31
+ export declare const $cachedQuery: unique symbol;
32
+ export declare const $ws: unique symbol;
33
+ export declare const $socket: unique symbol;
34
+ export declare const $io: unique symbol;
@@ -83,7 +83,9 @@ export interface CookieOptions {
83
83
  sameSite?: boolean | 'lax' | 'strict' | 'none' | 'Lax' | 'Strict' | 'None';
84
84
  priority?: 'low' | 'medium' | 'high' | 'Low' | 'Medium' | 'High';
85
85
  }
86
- export type ShokupanHandler<State extends Record<string, any> = Record<string, any>, Params extends Record<string, string> = Record<string, string>> = (ctx: ShokupanContext<State, Params>, next?: NextFn) => Promise<any> | any;
86
+ export type ShokupanHandler<State extends Record<string, any> = Record<string, any>, Params extends Record<string, string> = Record<string, string>> = ((ctx: ShokupanContext<State, Params>, next?: NextFn) => Promise<any> | any) & {
87
+ originalHandler?: ShokupanHandler<State, Params>;
88
+ };
87
89
  export declare const HTTPMethods: string[];
88
90
  export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD" | "ALL";
89
91
  export declare enum RouteParamType {
@@ -289,6 +291,33 @@ export type ShokupanConfig<T extends Record<string, any> = Record<string, any>>
289
291
  * @default 10000
290
292
  */
291
293
  middlewareTrackingMaxCapacity?: number;
294
+ /**
295
+ * Whether to enable the HTTP bridge for WebSocket.
296
+ * This enables websocket messages to run through the HTTP server.
297
+ * e.g.
298
+ * ```json
299
+ * {
300
+ * "method": "POST",
301
+ * "path": "/api/v1/myHttpEndpoint",
302
+ * "headers": {},
303
+ * "body": {
304
+ * "type": "text",
305
+ * "data": "Hello, world!"
306
+ * }
307
+ * }
308
+ * ```
309
+ * @default false
310
+ */
311
+ enableHTTPBridge?: boolean;
312
+ /**
313
+ * Handler for WebSocket events that throw an exception.
314
+ */
315
+ websocketErrorHandler?: (err: any, ctx: ShokupanContext<T>) => void | Promise<void>;
316
+ /**
317
+ * Unique ID generator function for requests.
318
+ * @default nanoid
319
+ */
320
+ idGenerator?: () => string;
292
321
  /**
293
322
  * Time-to-live for middleware tracking entries in milliseconds.
294
323
  * Entries older than this will be cleaned up.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shokupan",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Shokupan is a low-lift modern web framework for Bun.",
5
5
  "author": "Andrew G. Knackstedt",
6
6
  "publishConfig": {
@@ -65,9 +65,13 @@
65
65
  "@opentelemetry/sdk-trace-node": "^2.2.0",
66
66
  "@opentelemetry/semantic-conventions": "^1.38.0",
67
67
  "@scalar/openapi-types": "^0.5.3",
68
+ "nanoid": "^5.1.6",
69
+ "socket.io": "^4.8.3",
68
70
  "tslib": "^2.8.1"
69
71
  },
70
72
  "peerDependencies": {
73
+ "@scalar/api-reference": "^1.0.0",
74
+ "@surrealdb/node": "^2.4.0",
71
75
  "ajv": "^8.0.0",
72
76
  "ajv-formats": "^3.0.0",
73
77
  "arctic": "^3",
@@ -78,8 +82,6 @@
78
82
  "parse-json": "^8.0.0",
79
83
  "reflect-metadata": "^0.2.0",
80
84
  "secure-json-parse": "^4.0.0",
81
- "@scalar/api-reference": "^1.0.0",
82
- "@surrealdb/node": "^2.4.0",
83
85
  "surrealdb": "^2.0.0-alpha.14"
84
86
  },
85
87
  "peerDependenciesMeta": {
@@ -150,6 +152,7 @@
150
152
  "valibot": "^1.2.0",
151
153
  "vite": "^7.3.0",
152
154
  "vite-plugin-dts": "^4.5.4",
155
+ "vite-plugin-static-copy": "^3.1.4",
153
156
  "zod": "^4.2.1"
154
157
  }
155
158
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"http-server-0xH174zz.js","sources":["../src/plugins/application/http-server.ts"],"sourcesContent":["import type { Server } from \"bun\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport type { ServerFactory } from \"../../util/types\";\n\n/**\n * Creates a server factory that uses the standard Node.js `http` module.\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpServer(): ServerFactory {\n return async (options: any): Promise<Server> => {\n const server = http.createServer(async (req, res) => {\n const url = new URL(req.url!, `http://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any,\n // Required for Node.js undici when sending a body\n duplex: 'half'\n } as any);\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server = {\n stop: () => {\n server.close();\n return Promise.resolve(); // Bun.Server stop usually returns void but in type definition it might vary.\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`http://${options.hostname}:${options.port}`)\n } as unknown as Server;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n\n/**\n * Creates a server factory that uses the standard Node.js `https` module.\n * @param sslOptions - Node.js HTTPS options (key, cert, etc.)\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpsServer(sslOptions: https.ServerOptions): ServerFactory {\n return async (options: any): Promise<Server> => {\n const server = https.createServer(sslOptions, async (req, res) => {\n const url = new URL(req.url!, `https://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any,\n // Required for Node.js undici when sending a body\n duplex: 'half'\n } as any);\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server = {\n stop: () => {\n server.close();\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`https://${options.hostname}:${options.port}`)\n } as unknown as Server;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n"],"names":["options"],"mappings":";;AASO,SAAS,mBAAkC;AAC9C,SAAO,OAAO,YAAkC;AAC5C,UAAM,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AACjD,YAAM,MAAM,IAAI,IAAI,IAAI,KAAM,UAAU,IAAI,QAAQ,IAAI,EAAE;AAC1D,YAAM,UAAU,IAAI,QAAQ,IAAI,YAAY;AAAA,QACxC,QAAQ,IAAI;AAAA,QACZ,SAAS,IAAI;AAAA,QACb,MAAM,CAAC,OAAO,MAAM,EAAE,SAAS,IAAI,MAAO,IAAI,SAAY,IAAI,eAAe;AAAA,UACzE,MAAM,YAAY;AACd,gBAAI,GAAG,QAAQ,CAAA,UAAS,WAAW,QAAQ,KAAK,CAAC;AACjD,gBAAI,GAAG,OAAO,MAAM,WAAW,OAAO;AACtC,gBAAI,GAAG,SAAS,CAAA,QAAO,WAAW,MAAM,GAAG,CAAC;AAAA,UAChD;AAAA,QAAA,CACH;AAAA;AAAA,QAED,QAAQ;AAAA,MAAA,CACJ;AAER,YAAM,WAAW,MAAM,QAAQ,MAAM,SAAS,UAAU;AAExD,UAAI,aAAa,SAAS;AAC1B,eAAS,QAAQ,QAAQ,CAAC,GAAG,MAAM,IAAI,UAAU,GAAG,CAAC,CAAC;AAEtD,UAAI,SAAS,MAAM;AAEf,cAAM,SAAS,MAAM,SAAS,YAAA;AAC9B,YAAI,IAAI,OAAO,KAAK,MAAM,CAAC;AAAA,MAC/B,OAAO;AACH,YAAI,IAAA;AAAA,MACR;AAAA,IACJ,CAAC;AAED,UAAM,aAAqB;AAAA,MACvB,MAAM,MAAM;AACR,eAAO,MAAA;AACP,eAAO,QAAQ,QAAA;AAAA,MACnB;AAAA,MACA,QAAQ,KAAKA,UAAS;AAClB,eAAO;AAAA,MACX;AAAA,MACA,OAAOA,UAAS;AACZ,eAAO;AAAA,MACX;AAAA,MACA,IAAI,OAAO;AACP,cAAM,OAAO,OAAO,QAAA;AACpB,YAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC3C,iBAAO,KAAK;AAAA,QAChB;AACA,eAAO,QAAQ;AAAA,MACnB;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB,aAAa,QAAQ;AAAA,MACrB,iBAAiB;AAAA,MACjB,WAAW,CAAC,QAAQ;AAAA,MACpB,SAAS,MAAM;AAAA,MACf,iBAAiB,MAAM;AAAA,MACvB,KAAK,IAAI,IAAI,UAAU,QAAQ,QAAQ,IAAI,QAAQ,IAAI,EAAE;AAAA,IAAA;AAG7D,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC5B,aAAO,OAAO,QAAQ,MAAM,QAAQ,UAAU,MAAM;AAChD,gBAAQ,UAAU;AAAA,MACtB,CAAC;AAAA,IACL,CAAC;AAAA,EACL;AACJ;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"http-server-DFhwlK8e.cjs","sources":["../src/plugins/application/http-server.ts"],"sourcesContent":["import type { Server } from \"bun\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport type { ServerFactory } from \"../../util/types\";\n\n/**\n * Creates a server factory that uses the standard Node.js `http` module.\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpServer(): ServerFactory {\n return async (options: any): Promise<Server> => {\n const server = http.createServer(async (req, res) => {\n const url = new URL(req.url!, `http://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any,\n // Required for Node.js undici when sending a body\n duplex: 'half'\n } as any);\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server = {\n stop: () => {\n server.close();\n return Promise.resolve(); // Bun.Server stop usually returns void but in type definition it might vary.\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`http://${options.hostname}:${options.port}`)\n } as unknown as Server;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n\n/**\n * Creates a server factory that uses the standard Node.js `https` module.\n * @param sslOptions - Node.js HTTPS options (key, cert, etc.)\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpsServer(sslOptions: https.ServerOptions): ServerFactory {\n return async (options: any): Promise<Server> => {\n const server = https.createServer(sslOptions, async (req, res) => {\n const url = new URL(req.url!, `https://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any,\n // Required for Node.js undici when sending a body\n duplex: 'half'\n } as any);\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server = {\n stop: () => {\n server.close();\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`https://${options.hostname}:${options.port}`)\n } as unknown as Server;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n"],"names":["http","options"],"mappings":";;;;;;;;;;;;;;;;;;;;;AASO,SAAS,mBAAkC;AAC9C,SAAO,OAAO,YAAkC;AAC5C,UAAM,SAASA,gBAAK,aAAa,OAAO,KAAK,QAAQ;AACjD,YAAM,MAAM,IAAI,IAAI,IAAI,KAAM,UAAU,IAAI,QAAQ,IAAI,EAAE;AAC1D,YAAM,UAAU,IAAI,QAAQ,IAAI,YAAY;AAAA,QACxC,QAAQ,IAAI;AAAA,QACZ,SAAS,IAAI;AAAA,QACb,MAAM,CAAC,OAAO,MAAM,EAAE,SAAS,IAAI,MAAO,IAAI,SAAY,IAAI,eAAe;AAAA,UACzE,MAAM,YAAY;AACd,gBAAI,GAAG,QAAQ,CAAA,UAAS,WAAW,QAAQ,KAAK,CAAC;AACjD,gBAAI,GAAG,OAAO,MAAM,WAAW,OAAO;AACtC,gBAAI,GAAG,SAAS,CAAA,QAAO,WAAW,MAAM,GAAG,CAAC;AAAA,UAChD;AAAA,QAAA,CACH;AAAA;AAAA,QAED,QAAQ;AAAA,MAAA,CACJ;AAER,YAAM,WAAW,MAAM,QAAQ,MAAM,SAAS,UAAU;AAExD,UAAI,aAAa,SAAS;AAC1B,eAAS,QAAQ,QAAQ,CAAC,GAAG,MAAM,IAAI,UAAU,GAAG,CAAC,CAAC;AAEtD,UAAI,SAAS,MAAM;AAEf,cAAM,SAAS,MAAM,SAAS,YAAA;AAC9B,YAAI,IAAI,OAAO,KAAK,MAAM,CAAC;AAAA,MAC/B,OAAO;AACH,YAAI,IAAA;AAAA,MACR;AAAA,IACJ,CAAC;AAED,UAAM,aAAqB;AAAA,MACvB,MAAM,MAAM;AACR,eAAO,MAAA;AACP,eAAO,QAAQ,QAAA;AAAA,MACnB;AAAA,MACA,QAAQ,KAAKC,UAAS;AAClB,eAAO;AAAA,MACX;AAAA,MACA,OAAOA,UAAS;AACZ,eAAO;AAAA,MACX;AAAA,MACA,IAAI,OAAO;AACP,cAAM,OAAO,OAAO,QAAA;AACpB,YAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC3C,iBAAO,KAAK;AAAA,QAChB;AACA,eAAO,QAAQ;AAAA,MACnB;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB,aAAa,QAAQ;AAAA,MACrB,iBAAiB;AAAA,MACjB,WAAW,CAAC,QAAQ;AAAA,MACpB,SAAS,MAAM;AAAA,MACf,iBAAiB,MAAM;AAAA,MACvB,KAAK,IAAI,IAAI,UAAU,QAAQ,QAAQ,IAAI,QAAQ,IAAI,EAAE;AAAA,IAAA;AAG7D,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC5B,aAAO,OAAO,QAAQ,MAAM,QAAQ,UAAU,MAAM;AAChD,gBAAQ,UAAU;AAAA,MACtB,CAAC;AAAA,IACL,CAAC;AAAA,EACL;AACJ;;"}