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,12 @@
1
+ export declare class MetricsCollector {
2
+ private currentIntervalStart;
3
+ private pendingDetails;
4
+ private eventLoopHistogram;
5
+ private timer;
6
+ constructor();
7
+ recordRequest(duration: number, isError: boolean): void;
8
+ private alignTimestamp;
9
+ private collect;
10
+ private flushInterval;
11
+ stop(): void;
12
+ }
@@ -1,6 +1,5 @@
1
- import { ShokupanContext } from '../../../context';
2
- import { ShokupanRouter } from '../../../router';
3
- import { ShokupanHooks } from '../../../util/types';
1
+ import { HeadersInit } from 'bun';
2
+ import { ShokupanHooks, ShokupanPlugin } from '../../../util/types';
4
3
  export interface RequestLog {
5
4
  method: string;
6
5
  url: string;
@@ -10,22 +9,28 @@ export interface RequestLog {
10
9
  handlerStack?: any[];
11
10
  }
12
11
  export interface DashboardConfig {
13
- /**
14
- * Function to get request headers to include in the debug dashboard
15
- */
16
- getHeaders?: (ctx: ShokupanContext) => Record<string, string>;
12
+ getRequestHeaders?: () => HeadersInit;
13
+ path?: string;
17
14
  /**
18
15
  * Retention time in milliseconds
19
16
  */
20
17
  retentionMs?: number;
21
18
  }
22
- export declare class Dashboard extends ShokupanRouter {
19
+ export declare class Dashboard implements ShokupanPlugin {
23
20
  private readonly dashboardConfig;
21
+ private static __dirname;
22
+ private static getBasePath;
23
+ private router;
24
24
  private metrics;
25
25
  private eta;
26
26
  private startTime;
27
27
  private instrumented;
28
+ private metricsCollector;
28
29
  constructor(dashboardConfig?: DashboardConfig);
30
+ onInit(app: any, options?: {
31
+ path?: string;
32
+ }): void;
33
+ private setupRoutes;
29
34
  private instrumentApp;
30
35
  private assignIdsToRegistry;
31
36
  recordNodeMetric(id: string, type: string, duration: number, isError: boolean): void;
@@ -34,3 +39,4 @@ export declare class Dashboard extends ShokupanRouter {
34
39
  getHooks(): ShokupanHooks;
35
40
  private updateTiming;
36
41
  }
42
+ export default function DebugDashboard(config?: DashboardConfig): Dashboard;
@@ -0,0 +1,328 @@
1
+
2
+ // Common chart config
3
+ const commonOptions = {
4
+ responsive: true,
5
+ maintainAspectRatio: false,
6
+ plugins: {
7
+ legend: { labels: { color: '#94a3b8' } }
8
+ },
9
+ scales: {
10
+ x: {
11
+ ticks: { color: '#94a3b8' },
12
+ grid: { color: '#334155' }
13
+ },
14
+ y: {
15
+ ticks: { color: '#94a3b8' },
16
+ grid: { color: '#334155' },
17
+ beginAtZero: true
18
+ }
19
+ },
20
+ animation: { duration: 0 },
21
+ interaction: {
22
+ mode: 'index',
23
+ intersect: false,
24
+ },
25
+ };
26
+
27
+ // Get request headers from global function if available
28
+ const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
29
+
30
+ // Determine base path for API requests
31
+ const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
32
+ const url = basePath + '/';
33
+
34
+ // Chart instances
35
+
36
+ // --- Latency Chart ---
37
+ const latencyCtx = document.getElementById('latencyChart').getContext('2d');
38
+ const latencyChart = new Chart(latencyCtx, {
39
+ type: 'line',
40
+ data: {
41
+ labels: [],
42
+ datasets: [
43
+ {
44
+ label: 'Msg (Avg)',
45
+ data: [],
46
+ borderColor: '#3b82f6',
47
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
48
+ borderWidth: 2,
49
+ tension: 0.4,
50
+ fill: false
51
+ },
52
+ {
53
+ label: 'p95',
54
+ data: [],
55
+ borderColor: '#eab308',
56
+ backgroundColor: 'rgba(234, 179, 8, 0.1)',
57
+ borderWidth: 2,
58
+ tension: 0.4,
59
+ fill: false
60
+ },
61
+ {
62
+ label: 'p99',
63
+ data: [],
64
+ borderColor: '#ef4444',
65
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
66
+ borderWidth: 2,
67
+ tension: 0.4,
68
+ fill: false
69
+ }
70
+ ]
71
+ },
72
+ options: commonOptions
73
+ });
74
+
75
+ // --- RPS Chart ---
76
+ const rpsCtx = document.getElementById('rpsChart').getContext('2d');
77
+ const rpsChart = new Chart(rpsCtx, {
78
+ type: 'line',
79
+ data: {
80
+ labels: [],
81
+ datasets: [
82
+ {
83
+ label: 'Total Requests',
84
+ data: [],
85
+ borderColor: '#10b981',
86
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
87
+ borderWidth: 2,
88
+ tension: 0.4,
89
+ fill: true
90
+ },
91
+ {
92
+ label: 'Errors',
93
+ data: [],
94
+ borderColor: '#ef4444',
95
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
96
+ borderWidth: 2,
97
+ tension: 0.4,
98
+ fill: true
99
+ }
100
+ ]
101
+ },
102
+ options: commonOptions
103
+ });
104
+
105
+ // --- CPU Chart ---
106
+ const cpuCtx = document.getElementById('cpuChart').getContext('2d');
107
+ const cpuChart = new Chart(cpuCtx, {
108
+ type: 'line',
109
+ data: {
110
+ labels: [],
111
+ datasets: [
112
+ {
113
+ label: 'CPU Load (1m)',
114
+ data: [],
115
+ borderColor: '#8b5cf6',
116
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
117
+ borderWidth: 2,
118
+ tension: 0.4,
119
+ fill: true
120
+ }
121
+ ]
122
+ },
123
+ options: commonOptions
124
+ });
125
+
126
+ // --- Memory Chart ---
127
+ const memoryCtx = document.getElementById('memoryChart').getContext('2d');
128
+ const memoryChart = new Chart(memoryCtx, {
129
+ type: 'line',
130
+ data: {
131
+ labels: [],
132
+ datasets: [
133
+ {
134
+ label: 'Heap Used (MB)',
135
+ data: [],
136
+ borderColor: '#f97316',
137
+ backgroundColor: 'rgba(249, 115, 22, 0.1)',
138
+ borderWidth: 2,
139
+ tension: 0.4,
140
+ fill: true
141
+ },
142
+ {
143
+ label: 'RSS (MB)',
144
+ data: [],
145
+ borderColor: '#06b6d4',
146
+ backgroundColor: 'rgba(6, 182, 212, 0.1)',
147
+ borderWidth: 2,
148
+ tension: 0.4,
149
+ fill: true
150
+ }
151
+ ]
152
+ },
153
+ options: commonOptions
154
+ });
155
+
156
+ // --- Heap Chart ---
157
+ const heapCtx = document.getElementById('heapChart').getContext('2d');
158
+ const heapChart = new Chart(heapCtx, {
159
+ type: 'line',
160
+ data: {
161
+ labels: [],
162
+ datasets: [
163
+ {
164
+ label: 'Heap Used (MB)',
165
+ data: [],
166
+ borderColor: '#f97316',
167
+ backgroundColor: 'rgba(249, 115, 22, 0.1)',
168
+ borderWidth: 2,
169
+ tension: 0.4,
170
+ fill: true
171
+ },
172
+ {
173
+ label: 'Heap Total (MB)',
174
+ data: [],
175
+ borderColor: '#fb923c',
176
+ backgroundColor: 'rgba(251, 146, 60, 0.1)',
177
+ borderWidth: 2,
178
+ tension: 0.4,
179
+ fill: false
180
+ }
181
+ ]
182
+ },
183
+ options: commonOptions
184
+ });
185
+
186
+ // --- Event Loop Latency Chart ---
187
+ const eventLoopCtx = document.getElementById('eventLoopChart').getContext('2d');
188
+ const eventLoopChart = new Chart(eventLoopCtx, {
189
+ type: 'line',
190
+ data: {
191
+ labels: [],
192
+ datasets: [
193
+ {
194
+ label: 'Mean (ms)',
195
+ data: [],
196
+ borderColor: '#3b82f6',
197
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
198
+ borderWidth: 2,
199
+ tension: 0.4,
200
+ fill: false
201
+ },
202
+ {
203
+ label: 'p95 (ms)',
204
+ data: [],
205
+ borderColor: '#eab308',
206
+ backgroundColor: 'rgba(234, 179, 8, 0.1)',
207
+ borderWidth: 2,
208
+ tension: 0.4,
209
+ fill: false
210
+ },
211
+ {
212
+ label: 'p99 (ms)',
213
+ data: [],
214
+ borderColor: '#ef4444',
215
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
216
+ borderWidth: 2,
217
+ tension: 0.4,
218
+ fill: false
219
+ }
220
+ ]
221
+ },
222
+ options: commonOptions
223
+ });
224
+
225
+ // --- Error Rate Chart ---
226
+ const errorRateCtx = document.getElementById('errorRateChart').getContext('2d');
227
+ const errorRateChart = new Chart(errorRateCtx, {
228
+ type: 'line',
229
+ data: {
230
+ labels: [],
231
+ datasets: [
232
+ {
233
+ label: 'Success Rate (%)',
234
+ data: [],
235
+ borderColor: '#10b981',
236
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
237
+ borderWidth: 2,
238
+ tension: 0.4,
239
+ fill: true
240
+ },
241
+ {
242
+ label: 'Error Rate (%)',
243
+ data: [],
244
+ borderColor: '#ef4444',
245
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
246
+ borderWidth: 2,
247
+ tension: 0.4,
248
+ fill: true
249
+ }
250
+ ]
251
+ },
252
+ options: commonOptions
253
+ });
254
+
255
+ async function updateCharts() {
256
+ const period = document.getElementById('time-range-selector').value || '1m';
257
+ try {
258
+ const res = await fetch(`${url}metrics/history?interval=${period}`, { headers });
259
+ const data = await res.json();
260
+ const metrics = data.metrics || [];
261
+
262
+ const labels = metrics.map(m => new Date(m.timestamp).toLocaleTimeString());
263
+
264
+ // Latency
265
+ latencyChart.data.labels = labels;
266
+ latencyChart.data.datasets[0].data = metrics.map(m => m.responseTime.avg);
267
+ latencyChart.data.datasets[1].data = metrics.map(m => m.responseTime.p95);
268
+ latencyChart.data.datasets[2].data = metrics.map(m => m.responseTime.p99);
269
+ latencyChart.update();
270
+
271
+ // RPS
272
+ rpsChart.data.labels = labels;
273
+ rpsChart.data.datasets[0].data = metrics.map(m => m.requests.total);
274
+ rpsChart.data.datasets[1].data = metrics.map(m => m.requests.error);
275
+ rpsChart.update();
276
+
277
+ // CPU
278
+ cpuChart.data.labels = labels;
279
+ cpuChart.data.datasets[0].data = metrics.map(m => m.cpu);
280
+ cpuChart.update();
281
+
282
+ // Memory
283
+ memoryChart.data.labels = labels;
284
+ memoryChart.data.datasets[0].data = metrics.map(m => (m.memory.heapUsed / 1024 / 1024).toFixed(2));
285
+ memoryChart.data.datasets[1].data = metrics.map(m => (m.memory.used / 1024 / 1024).toFixed(2));
286
+ memoryChart.update();
287
+
288
+ // Heap
289
+ heapChart.data.labels = labels;
290
+ heapChart.data.datasets[0].data = metrics.map(m => (m.memory.heapUsed / 1024 / 1024).toFixed(2));
291
+ heapChart.data.datasets[1].data = metrics.map(m => (m.memory.heapTotal / 1024 / 1024).toFixed(2));
292
+ heapChart.update();
293
+
294
+ // Event Loop Latency
295
+ eventLoopChart.data.labels = labels;
296
+ eventLoopChart.data.datasets[0].data = metrics.map(m => m.eventLoopLatency.mean);
297
+ eventLoopChart.data.datasets[1].data = metrics.map(m => m.eventLoopLatency.p95);
298
+ eventLoopChart.data.datasets[2].data = metrics.map(m => m.eventLoopLatency.p99);
299
+ eventLoopChart.update();
300
+
301
+ // Error Rate
302
+ errorRateChart.data.labels = labels;
303
+ errorRateChart.data.datasets[0].data = metrics.map(m => {
304
+ const total = m.requests.success + m.requests.error;
305
+ return total > 0 ? ((m.requests.success / total) * 100).toFixed(2) : 100;
306
+ });
307
+ errorRateChart.data.datasets[1].data = metrics.map(m => {
308
+ const total = m.requests.success + m.requests.error;
309
+ return total > 0 ? ((m.requests.error / total) * 100).toFixed(2) : 0;
310
+ });
311
+ errorRateChart.update();
312
+
313
+ } catch (e) {
314
+ console.error("Failed to fetch metrics", e);
315
+ }
316
+ }
317
+
318
+ // Initial load
319
+ document.addEventListener("DOMContentLoaded", () => {
320
+ updateCharts();
321
+ // Poll every 10s for short intervals
322
+ setInterval(() => {
323
+ const period = document.getElementById('time-range-selector').value;
324
+ if (['1m', '5m', '30m', '1h', '2h'].includes(period)) {
325
+ updateCharts();
326
+ }
327
+ }, 10000);
328
+ });
@@ -0,0 +1,85 @@
1
+
2
+ // --- Failures Tab Logic ---
3
+ const failuresTable = new Tabulator("#failures-table-container", {
4
+ layout: "fitColumns",
5
+ height: "500px",
6
+ placeholder: "No failed requests found",
7
+ data: [],
8
+ columns: [
9
+ { title: "Time", field: "timestamp", width: 180, formatter: (cell) => new Date(cell.getValue()).toLocaleString() },
10
+ { title: "Method", field: "method", width: 100 },
11
+ { title: "URL", field: "url" },
12
+ { title: "Status", field: "status", width: 90, formatter: (cell) => `<span style="color: #ef4444; font-weight: bold;">${cell.getValue()}</span>` },
13
+ {
14
+ title: "Actions", formatter: (cell) => {
15
+ return `
16
+ <button class="replay-btn" style="background:#3b82f6; color:white; border:none; padding:4px 8px; border-radius:4px; cursor:pointer; margin-right:4px;">Replay</button>
17
+ <button class="export-btn" style="background:#64748b; color:white; border:none; padding:4px 8px; border-radius:4px; cursor:pointer;">Export</button>
18
+ `;
19
+ }, cellClick: (e, cell) => {
20
+ if (e.target.classList.contains('replay-btn')) {
21
+ replayRequest(cell.getRow().getData());
22
+ } else if (e.target.classList.contains('export-btn')) {
23
+ exportFailure(cell.getRow().getData());
24
+ }
25
+ }, width: 140, headerSort: false
26
+ }
27
+ ]
28
+ });
29
+
30
+
31
+ function exportFailure(data) {
32
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
33
+ const url = URL.createObjectURL(blob);
34
+ const a = document.createElement('a');
35
+ a.href = url;
36
+ a.download = `failure-${data.timestamp}.json`;
37
+ a.click();
38
+ URL.revokeObjectURL(url);
39
+ }
40
+
41
+ function importFailure() {
42
+ const input = document.createElement('input');
43
+ input.type = 'file';
44
+ input.accept = '.json';
45
+ input.onchange = e => {
46
+ const file = e.target.files[0];
47
+ if (!file) return;
48
+ const reader = new FileReader();
49
+ reader.onload = ev => {
50
+ try {
51
+ const data = JSON.parse(ev.target.result);
52
+ replayRequest(data);
53
+ } catch (err) { alert("Invalid JSON: " + err); }
54
+ };
55
+ reader.readAsText(file);
56
+ };
57
+ input.click();
58
+ }
59
+
60
+ async function replayRequest(req) {
61
+ if (!confirm(`Replay ${req.method} ${req.url}?`)) return;
62
+
63
+ try {
64
+ const headers = getRequestHeaders ? getRequestHeaders() : {};
65
+ const basePath = window.location.pathname.endsWith('/') ? '' : window.location.pathname;
66
+ const url = basePath + (basePath.endsWith('/') ? 'replay' : '/replay');
67
+
68
+ const res = await fetch(url, {
69
+ method: 'POST',
70
+ headers: { ...headers, 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({
72
+ method: req.method,
73
+ url: req.url,
74
+ headers: req.headers,
75
+ body: req.body
76
+ })
77
+ });
78
+
79
+ const result = await res.json();
80
+ alert(`Replay Status: ${res.status}\n\nResponse:\n${JSON.stringify(result, null, 2)}`);
81
+ } catch (e) {
82
+ alert("Replay Failed: " + e);
83
+ }
84
+ }
85
+