@geekmidas/telescope 0.0.1

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 (103) hide show
  1. package/README.md +521 -0
  2. package/dist/Telescope-B3Wd82yk.cjs +602 -0
  3. package/dist/Telescope-B3Wd82yk.cjs.map +1 -0
  4. package/dist/Telescope-C5dyDYYB.d.cts +133 -0
  5. package/dist/Telescope-D-uoZB6b.mjs +596 -0
  6. package/dist/Telescope-D-uoZB6b.mjs.map +1 -0
  7. package/dist/Telescope-DyIWgh9-.d.mts +133 -0
  8. package/dist/Telescope.cjs +3 -0
  9. package/dist/Telescope.d.cts +3 -0
  10. package/dist/Telescope.d.mts +3 -0
  11. package/dist/Telescope.mjs +3 -0
  12. package/dist/chunk-CUT6urMc.cjs +30 -0
  13. package/dist/index.cjs +5 -0
  14. package/dist/index.d.cts +4 -0
  15. package/dist/index.d.mts +4 -0
  16. package/dist/index.mjs +4 -0
  17. package/dist/logger/console.cjs +161 -0
  18. package/dist/logger/console.cjs.map +1 -0
  19. package/dist/logger/console.d.cts +109 -0
  20. package/dist/logger/console.d.mts +109 -0
  21. package/dist/logger/console.mjs +159 -0
  22. package/dist/logger/console.mjs.map +1 -0
  23. package/dist/logger/pino.cjs +118 -0
  24. package/dist/logger/pino.cjs.map +1 -0
  25. package/dist/logger/pino.d.cts +89 -0
  26. package/dist/logger/pino.d.mts +89 -0
  27. package/dist/logger/pino.mjs +116 -0
  28. package/dist/logger/pino.mjs.map +1 -0
  29. package/dist/memory-9-B9WACq.cjs +110 -0
  30. package/dist/memory-9-B9WACq.cjs.map +1 -0
  31. package/dist/memory-Cm0eevCS.d.mts +38 -0
  32. package/dist/memory-DiP1a-pp.d.cts +38 -0
  33. package/dist/memory-SdN5vtG9.mjs +104 -0
  34. package/dist/memory-SdN5vtG9.mjs.map +1 -0
  35. package/dist/server/hono.cjs +180 -0
  36. package/dist/server/hono.cjs.map +1 -0
  37. package/dist/server/hono.d.cts +26 -0
  38. package/dist/server/hono.d.mts +26 -0
  39. package/dist/server/hono.mjs +176 -0
  40. package/dist/server/hono.mjs.map +1 -0
  41. package/dist/storage/kysely.cjs +336 -0
  42. package/dist/storage/kysely.cjs.map +1 -0
  43. package/dist/storage/kysely.d.cts +161 -0
  44. package/dist/storage/kysely.d.mts +161 -0
  45. package/dist/storage/kysely.mjs +334 -0
  46. package/dist/storage/kysely.mjs.map +1 -0
  47. package/dist/storage/memory.cjs +3 -0
  48. package/dist/storage/memory.d.cts +3 -0
  49. package/dist/storage/memory.d.mts +3 -0
  50. package/dist/storage/memory.mjs +3 -0
  51. package/dist/types-BGDhFv4R.d.cts +170 -0
  52. package/dist/types-CZbzz8kx.d.mts +170 -0
  53. package/dist/types.cjs +0 -0
  54. package/dist/types.d.cts +2 -0
  55. package/dist/types.d.mts +2 -0
  56. package/dist/types.mjs +0 -0
  57. package/dist/ui-assets-D6-8TAr_.mjs +30 -0
  58. package/dist/ui-assets-D6-8TAr_.mjs.map +1 -0
  59. package/dist/ui-assets-ulevVble.cjs +48 -0
  60. package/dist/ui-assets-ulevVble.cjs.map +1 -0
  61. package/dist/ui-assets.cjs +5 -0
  62. package/dist/ui-assets.d.cts +12 -0
  63. package/dist/ui-assets.d.mts +12 -0
  64. package/dist/ui-assets.mjs +3 -0
  65. package/package.json +83 -0
  66. package/scripts/embed-ui.ts +90 -0
  67. package/src/Telescope.ts +714 -0
  68. package/src/__tests__/Telescope.spec.ts +356 -0
  69. package/src/index.ts +23 -0
  70. package/src/logger/__tests__/console.spec.ts +266 -0
  71. package/src/logger/__tests__/pino.spec.ts +217 -0
  72. package/src/logger/console.ts +230 -0
  73. package/src/logger/pino.ts +191 -0
  74. package/src/server/__tests__/hono.spec.ts +340 -0
  75. package/src/server/hono.ts +247 -0
  76. package/src/storage/__tests__/kysely.spec.ts +715 -0
  77. package/src/storage/__tests__/memory.spec.ts +411 -0
  78. package/src/storage/kysely.ts +572 -0
  79. package/src/storage/memory.ts +168 -0
  80. package/src/types.ts +188 -0
  81. package/src/ui-assets.ts +40 -0
  82. package/ui/index.html +12 -0
  83. package/ui/node_modules/.bin/browserslist +21 -0
  84. package/ui/node_modules/.bin/jiti +21 -0
  85. package/ui/node_modules/.bin/terser +21 -0
  86. package/ui/node_modules/.bin/tsc +21 -0
  87. package/ui/node_modules/.bin/tsserver +21 -0
  88. package/ui/node_modules/.bin/tsx +21 -0
  89. package/ui/node_modules/.bin/vite +21 -0
  90. package/ui/package.json +24 -0
  91. package/ui/src/App.tsx +342 -0
  92. package/ui/src/api.ts +75 -0
  93. package/ui/src/components/ExceptionDetail.tsx +100 -0
  94. package/ui/src/components/LogDetail.tsx +91 -0
  95. package/ui/src/components/RequestDetail.tsx +143 -0
  96. package/ui/src/main.tsx +10 -0
  97. package/ui/src/styles.css +10 -0
  98. package/ui/src/types.ts +63 -0
  99. package/ui/src/vite-env.d.ts +1 -0
  100. package/ui/src/vite-plugin-gkm-config.ts +54 -0
  101. package/ui/tsconfig.json +20 -0
  102. package/ui/tsconfig.tsbuildinfo +14 -0
  103. package/ui/vite.config.ts +13 -0
@@ -0,0 +1,340 @@
1
+ import { Hono } from 'hono';
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
+ import { Telescope } from '../../Telescope';
4
+ import { InMemoryStorage } from '../../storage/memory';
5
+ import { createMiddleware, createUI, getRequestId } from '../hono';
6
+
7
+ describe('Hono Adapter', () => {
8
+ let telescope: Telescope;
9
+ let storage: InMemoryStorage;
10
+
11
+ beforeEach(() => {
12
+ storage = new InMemoryStorage();
13
+ telescope = new Telescope({ storage });
14
+ });
15
+
16
+ afterEach(() => {
17
+ telescope.destroy();
18
+ });
19
+
20
+ describe('createMiddleware', () => {
21
+ it('should capture GET requests', async () => {
22
+ const app = new Hono();
23
+ app.use('*', createMiddleware(telescope));
24
+ app.get('/api/users', (c) => c.json({ users: [] }));
25
+
26
+ const res = await app.request('/api/users');
27
+
28
+ expect(res.status).toBe(200);
29
+
30
+ const requests = await telescope.getRequests();
31
+ expect(requests).toHaveLength(1);
32
+ expect(requests[0].method).toBe('GET');
33
+ expect(requests[0].path).toBe('/api/users');
34
+ expect(requests[0].status).toBe(200);
35
+ });
36
+
37
+ it('should capture POST requests with body', async () => {
38
+ const app = new Hono();
39
+ app.use('*', createMiddleware(telescope));
40
+ app.post('/api/users', async (c) => {
41
+ const body = await c.req.json();
42
+ return c.json({ id: '123', ...body }, 201);
43
+ });
44
+
45
+ const res = await app.request('/api/users', {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ name: 'John' }),
49
+ });
50
+
51
+ expect(res.status).toBe(201);
52
+
53
+ const requests = await telescope.getRequests();
54
+ expect(requests).toHaveLength(1);
55
+ expect(requests[0].method).toBe('POST');
56
+ expect(requests[0].body).toEqual({ name: 'John' });
57
+ expect(requests[0].status).toBe(201);
58
+ });
59
+
60
+ it('should capture query parameters', async () => {
61
+ const app = new Hono();
62
+ app.use('*', createMiddleware(telescope));
63
+ app.get('/api/search', (c) => c.json({ query: c.req.query('q') }));
64
+
65
+ await app.request('/api/search?q=test&limit=10');
66
+
67
+ const requests = await telescope.getRequests();
68
+ expect(requests[0].query).toEqual({ q: 'test', limit: '10' });
69
+ });
70
+
71
+ it('should capture request headers', async () => {
72
+ const app = new Hono();
73
+ app.use('*', createMiddleware(telescope));
74
+ app.get('/api/test', (c) => c.json({ ok: true }));
75
+
76
+ await app.request('/api/test', {
77
+ headers: {
78
+ Authorization: 'Bearer token123',
79
+ 'X-Custom-Header': 'custom-value',
80
+ },
81
+ });
82
+
83
+ const requests = await telescope.getRequests();
84
+ expect(requests[0].headers['authorization']).toBe('Bearer token123');
85
+ expect(requests[0].headers['x-custom-header']).toBe('custom-value');
86
+ });
87
+
88
+ it('should capture response duration', async () => {
89
+ const app = new Hono();
90
+ app.use('*', createMiddleware(telescope));
91
+ app.get('/api/slow', async (c) => {
92
+ await new Promise((resolve) => setTimeout(resolve, 50));
93
+ return c.json({ ok: true });
94
+ });
95
+
96
+ await app.request('/api/slow');
97
+
98
+ const requests = await telescope.getRequests();
99
+ expect(requests[0].duration).toBeGreaterThan(40);
100
+ });
101
+
102
+ it('should handle errors thrown in handlers', async () => {
103
+ const app = new Hono();
104
+ app.use('*', createMiddleware(telescope));
105
+ app.get('/api/error', () => {
106
+ throw new Error('Test error');
107
+ });
108
+
109
+ // Hono catches errors and returns 500
110
+ const res = await app.request('/api/error');
111
+ expect(res.status).toBe(500);
112
+
113
+ // The error is logged to stderr but the request still completes
114
+ // Exception recording depends on Hono's error handling behavior
115
+ });
116
+
117
+ it('should skip ignored paths', async () => {
118
+ const ignoredTelescope = new Telescope({
119
+ storage,
120
+ ignorePatterns: ['/health', '/__telescope/*'],
121
+ });
122
+
123
+ const app = new Hono();
124
+ app.use('*', createMiddleware(ignoredTelescope));
125
+ app.get('/health', (c) => c.json({ status: 'ok' }));
126
+ app.get('/api/users', (c) => c.json({ users: [] }));
127
+
128
+ await app.request('/health');
129
+ await app.request('/api/users');
130
+
131
+ const requests = await ignoredTelescope.getRequests();
132
+ expect(requests).toHaveLength(1);
133
+ expect(requests[0].path).toBe('/api/users');
134
+
135
+ ignoredTelescope.destroy();
136
+ });
137
+
138
+ it('should skip when telescope is disabled', async () => {
139
+ const disabled = new Telescope({ storage, enabled: false });
140
+
141
+ const app = new Hono();
142
+ app.use('*', createMiddleware(disabled));
143
+ app.get('/api/test', (c) => c.json({ ok: true }));
144
+
145
+ await app.request('/api/test');
146
+
147
+ const requests = await storage.getRequests();
148
+ expect(requests).toHaveLength(0);
149
+
150
+ disabled.destroy();
151
+ });
152
+
153
+ it('should not record body when recordBody is false', async () => {
154
+ const noBody = new Telescope({ storage, recordBody: false });
155
+
156
+ const app = new Hono();
157
+ app.use('*', createMiddleware(noBody));
158
+ app.post('/api/users', async (c) => {
159
+ await c.req.json();
160
+ return c.json({ id: '123' });
161
+ });
162
+
163
+ await app.request('/api/users', {
164
+ method: 'POST',
165
+ headers: { 'Content-Type': 'application/json' },
166
+ body: JSON.stringify({ name: 'John' }),
167
+ });
168
+
169
+ const requests = await noBody.getRequests();
170
+ expect(requests[0].body).toBeUndefined();
171
+
172
+ noBody.destroy();
173
+ });
174
+ });
175
+
176
+ describe('createUI', () => {
177
+ it('should return requests list', async () => {
178
+ // Add a request first
179
+ await telescope.recordRequest({
180
+ method: 'GET',
181
+ path: '/api/test',
182
+ url: 'http://localhost/api/test',
183
+ headers: {},
184
+ query: {},
185
+ status: 200,
186
+ responseHeaders: {},
187
+ duration: 10,
188
+ });
189
+
190
+ const ui = createUI(telescope);
191
+ const res = await ui.request('/api/requests');
192
+ const data = await res.json();
193
+
194
+ expect(Array.isArray(data)).toBe(true);
195
+ expect(data).toHaveLength(1);
196
+ });
197
+
198
+ it('should return single request by ID', async () => {
199
+ const requestId = await telescope.recordRequest({
200
+ method: 'GET',
201
+ path: '/api/test',
202
+ url: 'http://localhost/api/test',
203
+ headers: {},
204
+ query: {},
205
+ status: 200,
206
+ responseHeaders: {},
207
+ duration: 10,
208
+ });
209
+
210
+ const ui = createUI(telescope);
211
+ const res = await ui.request(`/api/requests/${requestId}`);
212
+ const data = await res.json();
213
+
214
+ expect(data.id).toBe(requestId);
215
+ expect(data.path).toBe('/api/test');
216
+ });
217
+
218
+ it('should return 404 for non-existent request', async () => {
219
+ const ui = createUI(telescope);
220
+ const res = await ui.request('/api/requests/non-existent');
221
+
222
+ expect(res.status).toBe(404);
223
+ });
224
+
225
+ it('should return exceptions list', async () => {
226
+ await telescope.exception(new Error('Test error'));
227
+
228
+ const ui = createUI(telescope);
229
+ const res = await ui.request('/api/exceptions');
230
+ const data = await res.json();
231
+
232
+ expect(Array.isArray(data)).toBe(true);
233
+ expect(data).toHaveLength(1);
234
+ expect(data[0].message).toBe('Test error');
235
+ });
236
+
237
+ it('should return logs list', async () => {
238
+ await telescope.info('Test log', { key: 'value' });
239
+
240
+ const ui = createUI(telescope);
241
+ const res = await ui.request('/api/logs');
242
+ const data = await res.json();
243
+
244
+ expect(Array.isArray(data)).toBe(true);
245
+ expect(data).toHaveLength(1);
246
+ expect(data[0].message).toBe('Test log');
247
+ });
248
+
249
+ it('should return stats', async () => {
250
+ await telescope.recordRequest({
251
+ method: 'GET',
252
+ path: '/test',
253
+ url: 'http://localhost/test',
254
+ headers: {},
255
+ query: {},
256
+ status: 200,
257
+ responseHeaders: {},
258
+ duration: 10,
259
+ });
260
+ await telescope.info('Test');
261
+ await telescope.exception(new Error('Test'));
262
+
263
+ const ui = createUI(telescope);
264
+ const res = await ui.request('/api/stats');
265
+ const data = await res.json();
266
+
267
+ expect(data.requests).toBe(1);
268
+ expect(data.logs).toBe(1);
269
+ expect(data.exceptions).toBe(1);
270
+ });
271
+
272
+ it('should return dashboard HTML on root', async () => {
273
+ const ui = createUI(telescope);
274
+ const res = await ui.request('/');
275
+
276
+ expect(res.headers.get('content-type')).toContain('text/html');
277
+ const html = await res.text();
278
+ expect(html).toContain('<!DOCTYPE html>');
279
+ });
280
+
281
+ it('should support pagination query params', async () => {
282
+ for (let i = 0; i < 10; i++) {
283
+ await telescope.recordRequest({
284
+ method: 'GET',
285
+ path: `/api/item/${i}`,
286
+ url: `http://localhost/api/item/${i}`,
287
+ headers: {},
288
+ query: {},
289
+ status: 200,
290
+ responseHeaders: {},
291
+ duration: 10,
292
+ });
293
+ }
294
+
295
+ const ui = createUI(telescope);
296
+ const res = await ui.request('/api/requests?limit=3&offset=0');
297
+ const data = await res.json();
298
+
299
+ expect(data).toHaveLength(3);
300
+ });
301
+ });
302
+
303
+ describe('getRequestId', () => {
304
+ it('should return undefined during handler (ID is set after response)', async () => {
305
+ let capturedRequestId: string | undefined;
306
+
307
+ const app = new Hono();
308
+ app.use('*', createMiddleware(telescope));
309
+ app.get('/api/test', (c) => {
310
+ // Note: The request ID is set AFTER next() completes,
311
+ // so it's not available during the handler execution
312
+ capturedRequestId = getRequestId(c);
313
+ return c.json({ ok: true });
314
+ });
315
+
316
+ await app.request('/api/test');
317
+
318
+ // The ID is not available during handler execution
319
+ expect(capturedRequestId).toBeUndefined();
320
+
321
+ // But the request was still recorded
322
+ const requests = await telescope.getRequests();
323
+ expect(requests).toHaveLength(1);
324
+ });
325
+
326
+ it('should return undefined when middleware not used', async () => {
327
+ let capturedRequestId: string | undefined;
328
+
329
+ const app = new Hono();
330
+ app.get('/api/test', (c) => {
331
+ capturedRequestId = getRequestId(c);
332
+ return c.json({ ok: true });
333
+ });
334
+
335
+ await app.request('/api/test');
336
+
337
+ expect(capturedRequestId).toBeUndefined();
338
+ });
339
+ });
340
+ });
@@ -0,0 +1,247 @@
1
+ import { Hono } from 'hono';
2
+ import type { Context, MiddlewareHandler, Next } from 'hono';
3
+ import type { Telescope } from '../Telescope';
4
+ import type { QueryOptions } from '../types';
5
+ import { getAsset, getIndexHtml } from '../ui-assets';
6
+
7
+ const CONTEXT_KEY = 'telescope-request-id';
8
+
9
+ /**
10
+ * Create Hono middleware that captures requests and responses
11
+ */
12
+ export function createMiddleware(telescope: Telescope): MiddlewareHandler {
13
+ return async (c: Context, next: Next) => {
14
+ if (!telescope.enabled) {
15
+ return next();
16
+ }
17
+
18
+ if (telescope.shouldIgnore(c.req.path)) {
19
+ return next();
20
+ }
21
+
22
+ const startTime = performance.now();
23
+
24
+ // Capture request data
25
+ const headers: Record<string, string> = {};
26
+ c.req.raw.headers.forEach((value, key) => {
27
+ headers[key] = value;
28
+ });
29
+
30
+ const url = new URL(c.req.url);
31
+ const query: Record<string, string> = {};
32
+ url.searchParams.forEach((value, key) => {
33
+ query[key] = value;
34
+ });
35
+
36
+ let body: unknown;
37
+ if (
38
+ telescope.recordBody &&
39
+ ['POST', 'PUT', 'PATCH'].includes(c.req.method)
40
+ ) {
41
+ try {
42
+ const contentType = c.req.header('content-type') || '';
43
+ if (contentType.includes('application/json')) {
44
+ body = await c.req.json();
45
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
46
+ const formData = await c.req.formData();
47
+ body = Object.fromEntries(formData.entries());
48
+ } else if (contentType.includes('text/')) {
49
+ body = await c.req.text();
50
+ }
51
+ } catch {
52
+ // Ignore body parsing errors
53
+ }
54
+ }
55
+
56
+ const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip');
57
+
58
+ try {
59
+ await next();
60
+
61
+ // Capture response data
62
+ const duration = performance.now() - startTime;
63
+
64
+ const responseHeaders: Record<string, string> = {};
65
+ c.res.headers.forEach((value, key) => {
66
+ responseHeaders[key] = value;
67
+ });
68
+
69
+ let responseBody: unknown;
70
+ if (telescope.recordBody) {
71
+ try {
72
+ const contentType = c.res.headers.get('content-type') || '';
73
+ if (contentType.includes('application/json')) {
74
+ const cloned = c.res.clone();
75
+ responseBody = await cloned.json();
76
+ }
77
+ } catch {
78
+ // Ignore body parsing errors
79
+ }
80
+ }
81
+
82
+ const requestId = await telescope.recordRequest({
83
+ method: c.req.method,
84
+ path: c.req.path,
85
+ url: c.req.url,
86
+ headers,
87
+ body,
88
+ query,
89
+ status: c.res.status,
90
+ responseHeaders,
91
+ responseBody,
92
+ duration,
93
+ ip,
94
+ });
95
+
96
+ c.set(CONTEXT_KEY, requestId);
97
+ } catch (error) {
98
+ await telescope.exception(error as Error);
99
+ throw error;
100
+ }
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Parse query options from Hono context
106
+ */
107
+ function parseQueryOptions(c: Context): QueryOptions {
108
+ const limit = parseInt(c.req.query('limit') || '50', 10);
109
+ const offset = parseInt(c.req.query('offset') || '0', 10);
110
+ const search = c.req.query('search');
111
+ const before = c.req.query('before');
112
+ const after = c.req.query('after');
113
+ const tags = c.req.query('tags')?.split(',').filter(Boolean);
114
+
115
+ return {
116
+ limit: Math.min(limit, 100),
117
+ offset,
118
+ search,
119
+ before: before ? new Date(before) : undefined,
120
+ after: after ? new Date(after) : undefined,
121
+ tags,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Create Hono app with dashboard UI and API routes
127
+ */
128
+ export function createUI(telescope: Telescope): Hono {
129
+ const app = new Hono();
130
+
131
+ // API routes
132
+ app.get('/api/requests', async (c) => {
133
+ const options = parseQueryOptions(c);
134
+ const requests = await telescope.getRequests(options);
135
+ return c.json(requests);
136
+ });
137
+
138
+ app.get('/api/requests/:id', async (c) => {
139
+ const request = await telescope.getRequest(c.req.param('id'));
140
+ if (!request) {
141
+ return c.json({ error: 'Request not found' }, 404);
142
+ }
143
+ return c.json(request);
144
+ });
145
+
146
+ app.get('/api/exceptions', async (c) => {
147
+ const options = parseQueryOptions(c);
148
+ const exceptions = await telescope.getExceptions(options);
149
+ return c.json(exceptions);
150
+ });
151
+
152
+ app.get('/api/exceptions/:id', async (c) => {
153
+ const exception = await telescope.getException(c.req.param('id'));
154
+ if (!exception) {
155
+ return c.json({ error: 'Exception not found' }, 404);
156
+ }
157
+ return c.json(exception);
158
+ });
159
+
160
+ app.get('/api/logs', async (c) => {
161
+ const options = parseQueryOptions(c);
162
+ const logs = await telescope.getLogs(options);
163
+ return c.json(logs);
164
+ });
165
+
166
+ app.get('/api/stats', async (c) => {
167
+ const stats = await telescope.getStats();
168
+ return c.json(stats);
169
+ });
170
+
171
+ // Static assets
172
+ app.get('/assets/:filename', (c) => {
173
+ const filename = c.req.param('filename');
174
+ const assetPath = `assets/${filename}`;
175
+ const asset = getAsset(assetPath);
176
+ if (asset) {
177
+ return c.body(asset.content, 200, {
178
+ 'Content-Type': asset.contentType,
179
+ 'Cache-Control': 'public, max-age=31536000, immutable',
180
+ });
181
+ }
182
+ return c.notFound();
183
+ });
184
+
185
+ // Dashboard UI - serve React app
186
+ app.get('/', (c) => {
187
+ const html = getIndexHtml();
188
+ if (html) {
189
+ return c.html(html);
190
+ }
191
+ // Fallback to inline HTML if UI assets not available
192
+ return c.html(telescope.getDashboardHtml());
193
+ });
194
+
195
+ app.get('/*', (c) => {
196
+ // SPA fallback - serve index.html for client-side routing
197
+ const html = getIndexHtml();
198
+ if (html) {
199
+ return c.html(html);
200
+ }
201
+ return c.html(telescope.getDashboardHtml());
202
+ });
203
+
204
+ return app;
205
+ }
206
+
207
+ /**
208
+ * Set up WebSocket routes for real-time updates.
209
+ * Requires @hono/node-ws for Node.js or Bun's built-in WebSocket.
210
+ */
211
+ export function setupWebSocket(
212
+ app: Hono,
213
+ telescope: Telescope,
214
+ upgradeWebSocket: (handler: any) => any,
215
+ ): void {
216
+ app.get(
217
+ '/ws',
218
+ upgradeWebSocket(() => ({
219
+ onOpen: (_event: Event, ws: WebSocket) => {
220
+ telescope.addWsClient(ws);
221
+ },
222
+ onClose: (_event: Event, ws: WebSocket) => {
223
+ telescope.removeWsClient(ws);
224
+ },
225
+ onMessage: (event: MessageEvent, ws: WebSocket) => {
226
+ try {
227
+ const data = JSON.parse(event.data);
228
+ if (data.type === 'ping') {
229
+ ws.send(JSON.stringify({ type: 'pong' }));
230
+ }
231
+ } catch {
232
+ // Ignore invalid messages
233
+ }
234
+ },
235
+ })),
236
+ );
237
+ }
238
+
239
+ /**
240
+ * Get the request ID from Hono context (set by middleware)
241
+ */
242
+ export function getRequestId(c: Context): string | undefined {
243
+ return c.get(CONTEXT_KEY);
244
+ }
245
+
246
+ // Re-export types
247
+ export type { Telescope };