lopata 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 (147) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/runtime/bindings/ai.ts +132 -0
  4. package/runtime/bindings/analytics-engine.ts +96 -0
  5. package/runtime/bindings/browser.ts +64 -0
  6. package/runtime/bindings/cache.ts +179 -0
  7. package/runtime/bindings/cf-streams.ts +56 -0
  8. package/runtime/bindings/container-docker.ts +225 -0
  9. package/runtime/bindings/container.ts +662 -0
  10. package/runtime/bindings/crypto-extras.ts +89 -0
  11. package/runtime/bindings/d1.ts +315 -0
  12. package/runtime/bindings/do-executor-inprocess.ts +140 -0
  13. package/runtime/bindings/do-executor-worker.ts +368 -0
  14. package/runtime/bindings/do-executor.ts +45 -0
  15. package/runtime/bindings/do-websocket-bridge.ts +70 -0
  16. package/runtime/bindings/do-worker-entry.ts +220 -0
  17. package/runtime/bindings/do-worker-env.ts +74 -0
  18. package/runtime/bindings/durable-object.ts +992 -0
  19. package/runtime/bindings/email.ts +180 -0
  20. package/runtime/bindings/html-rewriter.ts +84 -0
  21. package/runtime/bindings/hyperdrive.ts +130 -0
  22. package/runtime/bindings/images.ts +381 -0
  23. package/runtime/bindings/kv.ts +359 -0
  24. package/runtime/bindings/queue.ts +507 -0
  25. package/runtime/bindings/r2.ts +759 -0
  26. package/runtime/bindings/rpc-stub.ts +267 -0
  27. package/runtime/bindings/scheduled.ts +172 -0
  28. package/runtime/bindings/service-binding.ts +217 -0
  29. package/runtime/bindings/static-assets.ts +481 -0
  30. package/runtime/bindings/websocket-pair.ts +182 -0
  31. package/runtime/bindings/workflow.ts +858 -0
  32. package/runtime/bunflare-config.ts +56 -0
  33. package/runtime/cli/cache.ts +39 -0
  34. package/runtime/cli/context.ts +105 -0
  35. package/runtime/cli/d1.ts +163 -0
  36. package/runtime/cli/dev.ts +392 -0
  37. package/runtime/cli/kv.ts +84 -0
  38. package/runtime/cli/queues.ts +109 -0
  39. package/runtime/cli/r2.ts +140 -0
  40. package/runtime/cli/traces.ts +251 -0
  41. package/runtime/cli.ts +102 -0
  42. package/runtime/config.ts +148 -0
  43. package/runtime/d1-migrate.ts +37 -0
  44. package/runtime/dashboard/api.ts +174 -0
  45. package/runtime/dashboard/app.tsx +220 -0
  46. package/runtime/dashboard/components/breadcrumb.tsx +16 -0
  47. package/runtime/dashboard/components/buttons.tsx +13 -0
  48. package/runtime/dashboard/components/code-block.tsx +5 -0
  49. package/runtime/dashboard/components/detail-field.tsx +8 -0
  50. package/runtime/dashboard/components/empty-state.tsx +8 -0
  51. package/runtime/dashboard/components/filter-input.tsx +11 -0
  52. package/runtime/dashboard/components/index.ts +16 -0
  53. package/runtime/dashboard/components/key-value-table.tsx +23 -0
  54. package/runtime/dashboard/components/modal.tsx +23 -0
  55. package/runtime/dashboard/components/page-header.tsx +11 -0
  56. package/runtime/dashboard/components/pill-button.tsx +14 -0
  57. package/runtime/dashboard/components/refresh-button.tsx +7 -0
  58. package/runtime/dashboard/components/service-info.tsx +45 -0
  59. package/runtime/dashboard/components/status-badge.tsx +7 -0
  60. package/runtime/dashboard/components/table-link.tsx +5 -0
  61. package/runtime/dashboard/components/table.tsx +26 -0
  62. package/runtime/dashboard/components.tsx +19 -0
  63. package/runtime/dashboard/index.html +23 -0
  64. package/runtime/dashboard/lib.ts +45 -0
  65. package/runtime/dashboard/rpc/client.ts +20 -0
  66. package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
  67. package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
  68. package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
  69. package/runtime/dashboard/rpc/handlers/config.ts +137 -0
  70. package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
  71. package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
  72. package/runtime/dashboard/rpc/handlers/do.ts +117 -0
  73. package/runtime/dashboard/rpc/handlers/email.ts +82 -0
  74. package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
  75. package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
  76. package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
  77. package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
  78. package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
  79. package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
  80. package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
  81. package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
  82. package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
  83. package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
  84. package/runtime/dashboard/rpc/hooks.ts +132 -0
  85. package/runtime/dashboard/rpc/server.ts +70 -0
  86. package/runtime/dashboard/rpc/types.ts +396 -0
  87. package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
  88. package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
  89. package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
  90. package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
  91. package/runtime/dashboard/sql-browser/hooks.ts +137 -0
  92. package/runtime/dashboard/sql-browser/index.ts +4 -0
  93. package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
  94. package/runtime/dashboard/sql-browser/modals.tsx +116 -0
  95. package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
  96. package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
  97. package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
  98. package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
  99. package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
  100. package/runtime/dashboard/sql-browser/types.ts +61 -0
  101. package/runtime/dashboard/sql-browser/utils.ts +167 -0
  102. package/runtime/dashboard/style.css +177 -0
  103. package/runtime/dashboard/views/ai.tsx +152 -0
  104. package/runtime/dashboard/views/analytics-engine.tsx +169 -0
  105. package/runtime/dashboard/views/cache.tsx +93 -0
  106. package/runtime/dashboard/views/containers.tsx +197 -0
  107. package/runtime/dashboard/views/d1.tsx +81 -0
  108. package/runtime/dashboard/views/do.tsx +168 -0
  109. package/runtime/dashboard/views/email.tsx +235 -0
  110. package/runtime/dashboard/views/errors.tsx +558 -0
  111. package/runtime/dashboard/views/home.tsx +287 -0
  112. package/runtime/dashboard/views/kv.tsx +273 -0
  113. package/runtime/dashboard/views/queue.tsx +193 -0
  114. package/runtime/dashboard/views/r2.tsx +202 -0
  115. package/runtime/dashboard/views/scheduled.tsx +89 -0
  116. package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
  117. package/runtime/dashboard/views/traces.tsx +768 -0
  118. package/runtime/dashboard/views/workers.tsx +55 -0
  119. package/runtime/dashboard/views/workflows.tsx +473 -0
  120. package/runtime/db.ts +258 -0
  121. package/runtime/env.ts +362 -0
  122. package/runtime/error-page/app.tsx +394 -0
  123. package/runtime/error-page/build.ts +269 -0
  124. package/runtime/error-page/index.html +16 -0
  125. package/runtime/error-page/style.css +31 -0
  126. package/runtime/execution-context.ts +18 -0
  127. package/runtime/file-watcher.ts +57 -0
  128. package/runtime/generation-manager.ts +230 -0
  129. package/runtime/generation.ts +411 -0
  130. package/runtime/plugin.ts +292 -0
  131. package/runtime/request-cf.ts +28 -0
  132. package/runtime/rpc-validate.ts +154 -0
  133. package/runtime/tracing/context.ts +40 -0
  134. package/runtime/tracing/db.ts +73 -0
  135. package/runtime/tracing/frames.ts +75 -0
  136. package/runtime/tracing/instrument.ts +186 -0
  137. package/runtime/tracing/span.ts +138 -0
  138. package/runtime/tracing/store.ts +499 -0
  139. package/runtime/tracing/types.ts +47 -0
  140. package/runtime/vite-plugin/config-plugin.ts +68 -0
  141. package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
  142. package/runtime/vite-plugin/dist/index.mjs +52333 -0
  143. package/runtime/vite-plugin/globals-plugin.ts +94 -0
  144. package/runtime/vite-plugin/index.ts +43 -0
  145. package/runtime/vite-plugin/modules-plugin.ts +88 -0
  146. package/runtime/vite-plugin/react-router-plugin.ts +95 -0
  147. package/runtime/worker-registry.ts +52 -0
@@ -0,0 +1,499 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { getTracingDatabase } from "./db";
3
+ import type { SpanData, SpanEventData, TraceEvent, TraceSummary, TraceDetail } from "./types";
4
+
5
+ type Listener = (event: TraceEvent) => void;
6
+
7
+ const TRACE_CAP = 10_000;
8
+ const PRUNE_BATCH = 100;
9
+ const STALE_SPAN_TTL_MS = 10 * 60 * 1000; // 10 minutes
10
+ const STALE_CLEANUP_INTERVAL_MS = 60 * 1000; // run every minute
11
+
12
+ export class TraceStore {
13
+ private db: Database;
14
+ private listeners = new Set<Listener>();
15
+ private startTimeCache = new Map<string, number>();
16
+ private rootSpanCount: number;
17
+
18
+ private staleCleanupTimer: ReturnType<typeof setInterval>;
19
+ private insertSpanStmt;
20
+ private endSpanStmt;
21
+ private getSpanStmt;
22
+ private insertEventStmt;
23
+ private updateAttributesStmt;
24
+
25
+ constructor(db?: Database) {
26
+ this.db = db ?? getTracingDatabase();
27
+
28
+ this.insertSpanStmt = this.db.prepare<void, [string, string, string | null, string, string, string, string | null, number, number | null, number | null, string, string | null]>(
29
+ `INSERT INTO spans (span_id, trace_id, parent_span_id, name, kind, status, status_message, start_time, end_time, duration_ms, attributes, worker_name)
30
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
31
+ );
32
+
33
+ this.endSpanStmt = this.db.prepare<void, [number, number, string, string | null, string]>(
34
+ `UPDATE spans SET end_time = ?, duration_ms = ?, status = ?, status_message = ? WHERE span_id = ?`
35
+ );
36
+
37
+ this.getSpanStmt = this.db.prepare<Record<string, unknown>, [string]>(
38
+ `SELECT * FROM spans WHERE span_id = ?`
39
+ );
40
+
41
+ this.insertEventStmt = this.db.prepare<void, [string, string, number, string, string | null, string | null, string]>(
42
+ `INSERT INTO span_events (span_id, trace_id, timestamp, name, level, message, attributes)
43
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
44
+ );
45
+
46
+ this.updateAttributesStmt = this.db.prepare<void, [string, string]>(
47
+ `UPDATE spans SET attributes = json_patch(COALESCE(attributes, '{}'), ?) WHERE span_id = ?`
48
+ );
49
+
50
+ // Init root span counter from DB
51
+ this.rootSpanCount = this.db.prepare<{ cnt: number }, []>(
52
+ "SELECT COUNT(*) as cnt FROM spans WHERE parent_span_id IS NULL"
53
+ ).get()?.cnt ?? 0;
54
+
55
+ // Periodically evict stale entries from startTimeCache (spans that never ended)
56
+ this.staleCleanupTimer = setInterval(() => this.evictStaleSpans(), STALE_CLEANUP_INTERVAL_MS);
57
+ this.staleCleanupTimer.unref();
58
+ }
59
+
60
+ insertSpan(span: SpanData): void {
61
+ this.startTimeCache.set(span.spanId, span.startTime);
62
+ this.insertSpanStmt.run(
63
+ span.spanId, span.traceId, span.parentSpanId, span.name, span.kind,
64
+ span.status, span.statusMessage, span.startTime, span.endTime,
65
+ span.durationMs, JSON.stringify(span.attributes), span.workerName,
66
+ );
67
+ this.broadcast({ type: "span.start", span });
68
+ if (!span.parentSpanId) {
69
+ this.rootSpanCount++;
70
+ this.enforceTraceCap();
71
+ }
72
+ }
73
+
74
+ endSpan(spanId: string, endTime: number, status: "ok" | "error", statusMessage?: string): void {
75
+ const startTime = this.startTimeCache.get(spanId);
76
+ if (startTime === undefined) return;
77
+
78
+ const durationMs = endTime - startTime;
79
+ this.endSpanStmt.run(endTime, durationMs, status, statusMessage ?? null, spanId);
80
+ this.startTimeCache.delete(spanId);
81
+
82
+ const span = this.rowToSpan(this.getSpanStmt.get(spanId));
83
+ if (span) {
84
+ this.broadcast({ type: "span.end", span });
85
+ }
86
+ }
87
+
88
+ getSpanStatus(spanId: string): string | null {
89
+ return this.db.prepare<{ status: string }, [string]>("SELECT status FROM spans WHERE span_id = ?").get(spanId)?.status ?? null;
90
+ }
91
+
92
+ setSpanStatus(spanId: string, status: "ok" | "error", statusMessage: string | null): void {
93
+ this.db.prepare("UPDATE spans SET status = ?, status_message = ? WHERE span_id = ?").run(status, statusMessage, spanId);
94
+ }
95
+
96
+ updateAttributes(spanId: string, attrs: Record<string, unknown>): void {
97
+ this.updateAttributesStmt.run(JSON.stringify(attrs), spanId);
98
+ }
99
+
100
+ addEvent(event: Omit<SpanEventData, "id">): void {
101
+ this.insertEventStmt.run(
102
+ event.spanId, event.traceId, event.timestamp,
103
+ event.name, event.level, event.message, JSON.stringify(event.attributes),
104
+ );
105
+ const id = this.db.prepare<{ id: number }, []>("SELECT last_insert_rowid() as id").get()?.id;
106
+ this.broadcast({ type: "span.event", event: { ...event, id } });
107
+ }
108
+
109
+ listTraces(opts: { limit?: number; cursor?: string }): { items: TraceSummary[]; cursor: string | null } {
110
+ const limit = opts.limit ?? 50;
111
+ const { time: cursorTime, id: cursorId } = parseCursor(opts.cursor);
112
+
113
+ const rows = this.db.prepare<Record<string, unknown>, [number, number, string, number]>(`
114
+ SELECT
115
+ s.trace_id,
116
+ s.name as root_span_name,
117
+ s.worker_name,
118
+ s.status,
119
+ s.status_message,
120
+ s.start_time,
121
+ s.duration_ms,
122
+ COUNT(c.span_id) as span_count,
123
+ SUM(CASE WHEN c.status = 'error' THEN 1 ELSE 0 END) as error_count
124
+ FROM spans s
125
+ LEFT JOIN spans c ON c.trace_id = s.trace_id
126
+ WHERE s.parent_span_id IS NULL AND (s.start_time < ? OR (s.start_time = ? AND s.trace_id < ?))
127
+ GROUP BY s.trace_id
128
+ ORDER BY s.start_time DESC, s.trace_id DESC
129
+ LIMIT ?
130
+ `).all(cursorTime, cursorTime, cursorId, limit + 1);
131
+
132
+ const hasMore = rows.length > limit;
133
+ const items = (hasMore ? rows.slice(0, limit) : rows).map(r => ({
134
+ traceId: r.trace_id as string,
135
+ rootSpanName: r.root_span_name as string,
136
+ workerName: r.worker_name as string | null,
137
+ status: r.status as "ok" | "error" | "unset",
138
+ statusMessage: r.status_message ? String(r.status_message).slice(0, 80) : null,
139
+ startTime: r.start_time as number,
140
+ durationMs: r.duration_ms as number | null,
141
+ spanCount: r.span_count as number,
142
+ errorCount: r.error_count as number,
143
+ }));
144
+
145
+ const last = items[items.length - 1];
146
+ const cursor = hasMore && last ? buildCursor(last.startTime, last.traceId) : null;
147
+ return { items, cursor };
148
+ }
149
+
150
+ getTrace(traceId: string): TraceDetail {
151
+ const spanRows = this.db.prepare<Record<string, unknown>, [string]>(
152
+ "SELECT * FROM spans WHERE trace_id = ? ORDER BY start_time ASC"
153
+ ).all(traceId);
154
+
155
+ const eventRows = this.db.prepare<Record<string, unknown>, [string]>(
156
+ "SELECT * FROM span_events WHERE trace_id = ? ORDER BY timestamp ASC"
157
+ ).all(traceId);
158
+
159
+ return {
160
+ spans: spanRows.map(r => this.rowToSpan(r)!),
161
+ events: eventRows.map(r => ({
162
+ id: r.id as number,
163
+ spanId: r.span_id as string,
164
+ traceId: r.trace_id as string,
165
+ timestamp: r.timestamp as number,
166
+ name: r.name as string,
167
+ level: r.level as string | null,
168
+ message: r.message as string | null,
169
+ attributes: r.attributes ? JSON.parse(r.attributes as string) : {},
170
+ })),
171
+ };
172
+ }
173
+
174
+ getRecentTraces(since: number, limit: number): TraceSummary[] {
175
+ const rows = this.db.prepare<Record<string, unknown>, [number, number]>(`
176
+ SELECT
177
+ s.trace_id,
178
+ s.name as root_span_name,
179
+ s.worker_name,
180
+ s.status,
181
+ s.status_message,
182
+ s.start_time,
183
+ s.duration_ms,
184
+ COUNT(c.span_id) as span_count,
185
+ SUM(CASE WHEN c.status = 'error' THEN 1 ELSE 0 END) as error_count
186
+ FROM spans s
187
+ LEFT JOIN spans c ON c.trace_id = s.trace_id
188
+ WHERE s.parent_span_id IS NULL AND s.start_time >= ?
189
+ GROUP BY s.trace_id
190
+ ORDER BY s.start_time DESC
191
+ LIMIT ?
192
+ `).all(since, limit);
193
+
194
+ return rows.map(r => ({
195
+ traceId: r.trace_id as string,
196
+ rootSpanName: r.root_span_name as string,
197
+ workerName: r.worker_name as string | null,
198
+ status: r.status as "ok" | "error" | "unset",
199
+ statusMessage: r.status_message ? String(r.status_message).slice(0, 80) : null,
200
+ startTime: r.start_time as number,
201
+ durationMs: r.duration_ms as number | null,
202
+ spanCount: r.span_count as number,
203
+ errorCount: r.error_count as number,
204
+ }));
205
+ }
206
+
207
+ searchTraces(query: string, limit: number = 50): { items: TraceSummary[]; cursor: string | null } {
208
+ const like = `%${query}%`;
209
+ const rows = this.db.prepare<Record<string, unknown>, [string, string, string, string, number]>(`
210
+ SELECT DISTINCT
211
+ s.trace_id,
212
+ s.name as root_span_name,
213
+ s.worker_name,
214
+ s.status,
215
+ s.status_message,
216
+ s.start_time,
217
+ s.duration_ms,
218
+ (SELECT COUNT(*) FROM spans WHERE trace_id = s.trace_id) as span_count,
219
+ (SELECT COUNT(*) FROM spans WHERE trace_id = s.trace_id AND status = 'error') as error_count
220
+ FROM spans s
221
+ LEFT JOIN span_events ev ON ev.trace_id = s.trace_id
222
+ WHERE s.parent_span_id IS NULL
223
+ AND (s.name LIKE ? OR s.attributes LIKE ? OR s.status_message LIKE ? OR ev.message LIKE ?)
224
+ ORDER BY s.start_time DESC
225
+ LIMIT ?
226
+ `).all(like, like, like, like, limit);
227
+
228
+ return {
229
+ items: rows.map(r => ({
230
+ traceId: r.trace_id as string,
231
+ rootSpanName: r.root_span_name as string,
232
+ workerName: r.worker_name as string | null,
233
+ status: r.status as "ok" | "error" | "unset",
234
+ statusMessage: r.status_message ? String(r.status_message).slice(0, 80) : null,
235
+ startTime: r.start_time as number,
236
+ durationMs: r.duration_ms as number | null,
237
+ spanCount: r.span_count as number,
238
+ errorCount: r.error_count as number,
239
+ })),
240
+ cursor: null,
241
+ };
242
+ }
243
+
244
+ listAllSpans(opts: { limit?: number; cursor?: string }): { items: Array<{ spanId: string; traceId: string; name: string; status: string; durationMs: number | null; startTime: number; workerName: string | null }>; cursor: string | null } {
245
+ const limit = opts.limit ?? 50;
246
+ const { time: cursorTime, id: cursorId } = parseCursor(opts.cursor);
247
+
248
+ const rows = this.db.prepare<Record<string, unknown>, [number, number, string, number]>(`
249
+ SELECT span_id, trace_id, name, status, duration_ms, start_time, worker_name
250
+ FROM spans
251
+ WHERE start_time < ? OR (start_time = ? AND span_id < ?)
252
+ ORDER BY start_time DESC, span_id DESC
253
+ LIMIT ?
254
+ `).all(cursorTime, cursorTime, cursorId, limit + 1);
255
+
256
+ const hasMore = rows.length > limit;
257
+ const items = (hasMore ? rows.slice(0, limit) : rows).map(r => ({
258
+ spanId: r.span_id as string,
259
+ traceId: r.trace_id as string,
260
+ name: r.name as string,
261
+ status: r.status as string,
262
+ durationMs: r.duration_ms as number | null,
263
+ startTime: r.start_time as number,
264
+ workerName: r.worker_name as string | null,
265
+ }));
266
+
267
+ const last = items[items.length - 1];
268
+ const cursor = hasMore && last ? buildCursor(last.startTime, last.spanId) : null;
269
+ return { items, cursor };
270
+ }
271
+
272
+ listAllLogs(opts: { limit?: number; cursor?: string }): { items: Array<{ id: number; spanId: string; traceId: string; timestamp: number; name: string; level: string | null; message: string | null }>; cursor: string | null } {
273
+ const limit = opts.limit ?? 50;
274
+ const cursorId = opts.cursor ? parseInt(opts.cursor, 10) : Number.MAX_SAFE_INTEGER;
275
+
276
+ const rows = this.db.prepare<Record<string, unknown>, [number, number]>(`
277
+ SELECT id, span_id, trace_id, timestamp, name, level, message
278
+ FROM span_events
279
+ WHERE id < ?
280
+ ORDER BY id DESC
281
+ LIMIT ?
282
+ `).all(cursorId, limit + 1);
283
+
284
+ const hasMore = rows.length > limit;
285
+ const items = (hasMore ? rows.slice(0, limit) : rows).map(r => ({
286
+ id: r.id as number,
287
+ spanId: r.span_id as string,
288
+ traceId: r.trace_id as string,
289
+ timestamp: r.timestamp as number,
290
+ name: r.name as string,
291
+ level: r.level as string | null,
292
+ message: r.message as string | null,
293
+ }));
294
+
295
+ const cursor = hasMore && items.length > 0 ? String(items[items.length - 1]!.id) : null;
296
+ return { items, cursor };
297
+ }
298
+
299
+ clearTraces(): void {
300
+ this.db.run("DELETE FROM span_events");
301
+ this.db.run("DELETE FROM spans");
302
+ this.rootSpanCount = 0;
303
+ this.startTimeCache.clear();
304
+ }
305
+
306
+ // ─── Error persistence ──────────────────────────────────────────────
307
+
308
+ insertError(opts: {
309
+ id: string;
310
+ timestamp: number;
311
+ errorName: string;
312
+ errorMessage: string;
313
+ requestMethod?: string | null;
314
+ requestUrl?: string | null;
315
+ workerName?: string | null;
316
+ traceId?: string | null;
317
+ spanId?: string | null;
318
+ source?: string | null;
319
+ data: string;
320
+ }): void {
321
+ this.db.prepare(
322
+ `INSERT INTO errors (id, timestamp, error_name, error_message, request_method, request_url, worker_name, trace_id, span_id, source, data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
323
+ ).run(
324
+ opts.id, opts.timestamp, opts.errorName, opts.errorMessage,
325
+ opts.requestMethod ?? null, opts.requestUrl ?? null, opts.workerName ?? null,
326
+ opts.traceId ?? null, opts.spanId ?? null, opts.source ?? null, opts.data,
327
+ );
328
+ this.enforceErrorCap();
329
+ }
330
+
331
+ listErrors(opts: { limit?: number; cursor?: string }): { items: Array<{ id: string; timestamp: number; errorName: string; errorMessage: string; requestMethod: string | null; requestUrl: string | null; workerName: string | null; traceId: string | null; spanId: string | null; source: string | null }>; cursor: string | null } {
332
+ const limit = opts.limit ?? 50;
333
+ const { time: cursorTime, id: cursorId } = parseCursor(opts.cursor);
334
+
335
+ const rows = this.db.prepare<Record<string, unknown>, [number, number, string, number]>(`
336
+ SELECT id, timestamp, error_name, error_message, request_method, request_url, worker_name, trace_id, span_id, source
337
+ FROM errors
338
+ WHERE timestamp < ? OR (timestamp = ? AND id < ?)
339
+ ORDER BY timestamp DESC, id DESC
340
+ LIMIT ?
341
+ `).all(cursorTime, cursorTime, cursorId, limit + 1);
342
+
343
+ const hasMore = rows.length > limit;
344
+ const items = (hasMore ? rows.slice(0, limit) : rows).map(r => ({
345
+ id: r.id as string,
346
+ timestamp: r.timestamp as number,
347
+ errorName: r.error_name as string,
348
+ errorMessage: r.error_message as string,
349
+ requestMethod: r.request_method as string | null,
350
+ requestUrl: r.request_url as string | null,
351
+ workerName: r.worker_name as string | null,
352
+ traceId: r.trace_id as string | null,
353
+ spanId: r.span_id as string | null,
354
+ source: r.source as string | null,
355
+ }));
356
+
357
+ const last = items[items.length - 1];
358
+ const cursor = hasMore && last ? buildCursor(last.timestamp, last.id) : null;
359
+ return { items, cursor };
360
+ }
361
+
362
+ getError(id: string): { id: string; timestamp: number; traceId: string | null; spanId: string | null; source: string | null; data: unknown } | null {
363
+ const row = this.db.prepare<Record<string, unknown>, [string]>(
364
+ "SELECT id, timestamp, trace_id, span_id, source, data FROM errors WHERE id = ?"
365
+ ).get(id);
366
+ if (!row) return null;
367
+ return {
368
+ id: row.id as string,
369
+ timestamp: row.timestamp as number,
370
+ traceId: row.trace_id as string | null,
371
+ spanId: row.span_id as string | null,
372
+ source: row.source as string | null,
373
+ data: JSON.parse(row.data as string),
374
+ };
375
+ }
376
+
377
+ getErrorsForTrace(traceId: string): Array<{ id: string; timestamp: number; errorName: string; errorMessage: string; source: string | null; data: unknown }> {
378
+ return this.db.prepare<Record<string, unknown>, [string]>(
379
+ "SELECT id, timestamp, error_name, error_message, source, data FROM errors WHERE trace_id = ? ORDER BY timestamp ASC"
380
+ ).all(traceId).map(r => ({
381
+ id: r.id as string,
382
+ timestamp: r.timestamp as number,
383
+ errorName: r.error_name as string,
384
+ errorMessage: r.error_message as string,
385
+ source: r.source as string | null,
386
+ data: r.data ? JSON.parse(r.data as string) : null,
387
+ }));
388
+ }
389
+
390
+ deleteError(id: string): void {
391
+ this.db.prepare("DELETE FROM errors WHERE id = ?").run(id);
392
+ }
393
+
394
+ clearErrors(): void {
395
+ this.db.run("DELETE FROM errors");
396
+ }
397
+
398
+ getErrorCount(): number {
399
+ return this.db.prepare<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM errors").get()?.cnt ?? 0;
400
+ }
401
+
402
+ getErrorCountsByWorker(): Record<string, number> {
403
+ const rows = this.db.prepare<{ worker_name: string; cnt: number }, []>(
404
+ "SELECT COALESCE(worker_name, '') as worker_name, COUNT(*) as cnt FROM errors GROUP BY worker_name"
405
+ ).all();
406
+ const map: Record<string, number> = {};
407
+ for (const r of rows) map[r.worker_name] = r.cnt;
408
+ return map;
409
+ }
410
+
411
+ private enforceErrorCap(): void {
412
+ const count = this.db.prepare<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM errors").get()?.cnt ?? 0;
413
+ if (count <= 1000) return;
414
+ this.db.prepare(
415
+ "DELETE FROM errors WHERE id IN (SELECT id FROM errors ORDER BY timestamp ASC LIMIT 50)"
416
+ ).run();
417
+ }
418
+
419
+ subscribe(listener: Listener): () => void {
420
+ this.listeners.add(listener);
421
+ return () => { this.listeners.delete(listener); };
422
+ }
423
+
424
+ private evictStaleSpans(): void {
425
+ const cutoff = Date.now() - STALE_SPAN_TTL_MS;
426
+ for (const [spanId, startTime] of this.startTimeCache) {
427
+ if (startTime < cutoff) {
428
+ this.startTimeCache.delete(spanId);
429
+ }
430
+ }
431
+ }
432
+
433
+ private broadcast(event: TraceEvent): void {
434
+ for (const listener of this.listeners) {
435
+ try { listener(event); } catch {}
436
+ }
437
+ }
438
+
439
+ private enforceTraceCap(): void {
440
+ if (this.rootSpanCount <= TRACE_CAP) return;
441
+
442
+ const oldest = this.db.prepare<{ trace_id: string }, [number]>(
443
+ "SELECT trace_id FROM spans WHERE parent_span_id IS NULL ORDER BY start_time ASC LIMIT ?"
444
+ ).all(PRUNE_BATCH);
445
+
446
+ if (oldest.length === 0) return;
447
+
448
+ const ids = oldest.map(r => r.trace_id);
449
+ const placeholders = ids.map(() => "?").join(",");
450
+
451
+ this.db.transaction(() => {
452
+ this.db.prepare(`DELETE FROM span_events WHERE trace_id IN (${placeholders})`).run(...ids);
453
+ this.db.prepare(`DELETE FROM spans WHERE trace_id IN (${placeholders})`).run(...ids);
454
+ })();
455
+
456
+ this.rootSpanCount -= oldest.length;
457
+ }
458
+
459
+ private rowToSpan(row: Record<string, unknown> | null): SpanData | null {
460
+ if (!row) return null;
461
+ return {
462
+ spanId: row.span_id as string,
463
+ traceId: row.trace_id as string,
464
+ parentSpanId: row.parent_span_id as string | null,
465
+ name: row.name as string,
466
+ kind: row.kind as SpanData["kind"],
467
+ status: row.status as SpanData["status"],
468
+ statusMessage: row.status_message as string | null,
469
+ startTime: row.start_time as number,
470
+ endTime: row.end_time as number | null,
471
+ durationMs: row.duration_ms as number | null,
472
+ attributes: row.attributes ? JSON.parse(row.attributes as string) : {},
473
+ workerName: row.worker_name as string | null,
474
+ };
475
+ }
476
+ }
477
+
478
+ function buildCursor(time: number, id: string): string {
479
+ return `${time}:${id}`;
480
+ }
481
+
482
+ function parseCursor(cursor?: string): { time: number; id: string } {
483
+ if (!cursor) return { time: Number.MAX_SAFE_INTEGER, id: "\uffff" };
484
+ const sep = cursor.indexOf(":");
485
+ if (sep === -1) {
486
+ // Backwards-compatible: old numeric-only cursors
487
+ return { time: parseInt(cursor, 10), id: "\uffff" };
488
+ }
489
+ return { time: parseInt(cursor.substring(0, sep), 10), id: cursor.substring(sep + 1) };
490
+ }
491
+
492
+ let defaultStore: TraceStore | null = null;
493
+
494
+ export function getTraceStore(): TraceStore {
495
+ if (!defaultStore) {
496
+ defaultStore = new TraceStore();
497
+ }
498
+ return defaultStore;
499
+ }
@@ -0,0 +1,47 @@
1
+ export interface SpanData {
2
+ spanId: string;
3
+ traceId: string;
4
+ parentSpanId: string | null;
5
+ name: string;
6
+ kind: "server" | "internal" | "client";
7
+ status: "ok" | "error" | "unset";
8
+ statusMessage: string | null;
9
+ startTime: number;
10
+ endTime: number | null;
11
+ durationMs: number | null;
12
+ attributes: Record<string, unknown>;
13
+ workerName: string | null;
14
+ }
15
+
16
+ export interface SpanEventData {
17
+ id?: number;
18
+ spanId: string;
19
+ traceId: string;
20
+ timestamp: number;
21
+ name: string;
22
+ level: string | null;
23
+ message: string | null;
24
+ attributes: Record<string, unknown>;
25
+ }
26
+
27
+ export type TraceEvent =
28
+ | { type: "span.start"; span: SpanData }
29
+ | { type: "span.end"; span: SpanData }
30
+ | { type: "span.event"; event: SpanEventData };
31
+
32
+ export interface TraceSummary {
33
+ traceId: string;
34
+ rootSpanName: string;
35
+ workerName: string | null;
36
+ status: "ok" | "error" | "unset";
37
+ statusMessage: string | null;
38
+ startTime: number;
39
+ durationMs: number | null;
40
+ spanCount: number;
41
+ errorCount: number;
42
+ }
43
+
44
+ export interface TraceDetail {
45
+ spans: SpanData[];
46
+ events: SpanEventData[];
47
+ }
@@ -0,0 +1,68 @@
1
+ import { DevEnvironment, createServerModuleRunner, createServerHotChannel, type Plugin } from "vite";
2
+ import type { ModuleRunner } from "vite/module-runner";
3
+
4
+ /**
5
+ * Custom DevEnvironment that is NOT a RunnableDevEnvironment.
6
+ *
7
+ * This is critical for React Router integration: React Router's dev middleware
8
+ * checks `isRunnableDevEnvironment(ssrEnvironment)` — if true, it tries to
9
+ * handle SSR requests itself (loading virtual:react-router/server-build).
10
+ * By extending DevEnvironment directly (not RunnableDevEnvironment), the
11
+ * instanceof check returns false, React Router calls next(), and Bunflare's
12
+ * middleware handles the request through the worker's fetch() handler.
13
+ *
14
+ * We still provide a `runner` getter for Bunflare's own middleware to import
15
+ * modules through Vite's transform pipeline (JSX, HMR, etc.).
16
+ */
17
+ class BunflareDevEnvironment extends DevEnvironment {
18
+ private _runner: ModuleRunner | undefined;
19
+
20
+ get runner(): ModuleRunner {
21
+ if (!this._runner) {
22
+ this._runner = createServerModuleRunner(this);
23
+ }
24
+ return this._runner;
25
+ }
26
+
27
+ override async close() {
28
+ if (this._runner) {
29
+ await this._runner.close();
30
+ }
31
+ await super.close();
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Sets SSR environment resolve conditions for Cloudflare Workers compatibility.
37
+ * Creates a BunflareDevEnvironment (non-runnable) so framework plugins (React Router)
38
+ * delegate SSR handling to Bunflare's middleware.
39
+ */
40
+ export function configPlugin(envName: string): Plugin {
41
+ return {
42
+ name: "bunflare:config",
43
+ config() {
44
+ return {
45
+ server: {
46
+ watch: {
47
+ ignored: ["**/.bunflare/**"],
48
+ },
49
+ },
50
+ environments: {
51
+ [envName]: {
52
+ resolve: {
53
+ externalConditions: ["workerd", "worker"],
54
+ },
55
+ dev: {
56
+ createEnvironment(name, config) {
57
+ return new BunflareDevEnvironment(name, config, {
58
+ hot: true,
59
+ transport: createServerHotChannel(),
60
+ });
61
+ },
62
+ },
63
+ },
64
+ },
65
+ };
66
+ },
67
+ };
68
+ }