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,392 @@
1
+ // Capture deeper stacks in dev mode (default is 10, async boundaries still truncate)
2
+ Error.stackTraceLimit = 50;
3
+
4
+ import "../plugin";
5
+ import type { CliContext } from "./context";
6
+ import { parseFlag } from "./context";
7
+ import { autoLoadConfig, loadConfig } from "../config";
8
+ import { GenerationManager } from "../generation-manager";
9
+ import { FileWatcher } from "../file-watcher";
10
+ import { WorkerRegistry } from "../worker-registry";
11
+ import { loadBunflareConfig } from "../bunflare-config";
12
+ import { QueuePullConsumer } from "../bindings/queue";
13
+ import type { PullRequest, AckRequest } from "../bindings/queue";
14
+ import { getDatabase } from "../db";
15
+ import { addCfProperty } from "../request-cf";
16
+ import { handleDashboardRequest, setDashboardConfig, setGenerationManager, setWorkerRegistry } from "../dashboard/api";
17
+ import { CFWebSocket } from "../bindings/websocket-pair";
18
+ import { getTraceStore } from "../tracing/store";
19
+ import type { TraceEvent } from "../tracing/types";
20
+ import path from "node:path";
21
+
22
+ export async function run(ctx: CliContext) {
23
+ const envFlag = parseFlag(ctx.args, "--env") ?? parseFlag(ctx.args, "-e");
24
+ const listenFlag = parseFlag(ctx.args, "--listen");
25
+ const portFlag = parseFlag(ctx.args, "--port");
26
+
27
+ const baseDir = process.cwd();
28
+ const watchers: FileWatcher[] = [];
29
+
30
+ // Try to load bunflare.config.ts for multi-worker mode
31
+ const bunflareConfig = await loadBunflareConfig(baseDir);
32
+
33
+ let manager: GenerationManager;
34
+
35
+ if (bunflareConfig) {
36
+ // ─── Multi-worker mode ─────────────────────────────────────────
37
+ console.log("[bunflare] Multi-worker mode (bunflare.config.ts found)");
38
+
39
+ // Create executor factory based on isolation mode
40
+ let executorFactory: import("../bindings/do-executor").DOExecutorFactory | undefined;
41
+ if (bunflareConfig.isolation === "isolated") {
42
+ const { WorkerExecutorFactory } = await import("../bindings/do-executor-worker");
43
+ executorFactory = new WorkerExecutorFactory();
44
+ console.log("[bunflare] DO isolation: isolated (Worker threads)");
45
+ } else if (bunflareConfig.isolation && bunflareConfig.isolation !== "dev") {
46
+ console.warn(`[bunflare] Unknown isolation mode "${bunflareConfig.isolation}", using "dev"`);
47
+ }
48
+
49
+ const registry = new WorkerRegistry();
50
+
51
+ // Load main worker config
52
+ const mainConfig = await loadConfig(bunflareConfig.main, envFlag);
53
+ const mainBaseDir = path.dirname(bunflareConfig.main);
54
+ console.log(`[bunflare] Main worker: ${mainConfig.name}${envFlag ? ` (env: ${envFlag})` : ""}`);
55
+ setDashboardConfig(mainConfig);
56
+
57
+ const mainManager = new GenerationManager(mainConfig, mainBaseDir, {
58
+ workerName: mainConfig.name,
59
+ workerRegistry: registry,
60
+ isMain: true,
61
+ cron: bunflareConfig.cron,
62
+ executorFactory,
63
+ configPath: bunflareConfig.main,
64
+ browserConfig: bunflareConfig.browser,
65
+ });
66
+ registry.register(mainConfig.name, mainManager, true);
67
+
68
+ // Load auxiliary workers
69
+ for (const workerDef of bunflareConfig.workers ?? []) {
70
+ const auxConfig = await loadConfig(workerDef.config, envFlag);
71
+ const auxBaseDir = path.dirname(workerDef.config);
72
+ console.log(`[bunflare] Auxiliary worker: ${workerDef.name} (${auxConfig.name})`);
73
+
74
+ const auxManager = new GenerationManager(auxConfig, auxBaseDir, {
75
+ workerName: workerDef.name,
76
+ workerRegistry: registry,
77
+ isMain: false,
78
+ cron: bunflareConfig.cron,
79
+ executorFactory,
80
+ configPath: workerDef.config,
81
+ });
82
+ registry.register(workerDef.name, auxManager);
83
+
84
+ // Load aux worker first so main's service bindings can resolve
85
+ try {
86
+ const gen = await auxManager.reload();
87
+ console.log(`[bunflare] Auxiliary worker "${workerDef.name}" → generation ${gen.id}`);
88
+ } catch (err) {
89
+ console.error(`[bunflare] Failed to load auxiliary worker "${workerDef.name}":`, err);
90
+ }
91
+
92
+ // File watcher for aux worker
93
+ const auxSrcDir = path.dirname(path.resolve(auxBaseDir, auxConfig.main));
94
+ const auxWatcher = new FileWatcher(auxSrcDir, () => {
95
+ auxManager.reload().then(gen => {
96
+ console.log(`[bunflare] Auxiliary worker "${workerDef.name}" reloaded → generation ${gen.id}`);
97
+ }).catch(err => {
98
+ console.error(`[bunflare] Reload failed for "${workerDef.name}":`, err);
99
+ });
100
+ });
101
+ auxWatcher.start();
102
+ watchers.push(auxWatcher);
103
+ console.log(`[bunflare] Watching ${auxSrcDir} for changes (${workerDef.name})`);
104
+ }
105
+
106
+ // Load main worker after aux workers
107
+ const firstGen = await mainManager.reload();
108
+ console.log(`[bunflare] Main worker → generation ${firstGen.id}`);
109
+
110
+ manager = mainManager;
111
+ setGenerationManager(manager);
112
+ setWorkerRegistry(registry);
113
+
114
+ // File watcher for main worker
115
+ const mainSrcDir = path.dirname(path.resolve(mainBaseDir, mainConfig.main));
116
+ const mainWatcher = new FileWatcher(mainSrcDir, () => {
117
+ mainManager.reload().then(gen => {
118
+ console.log(`[bunflare] Main worker reloaded → generation ${gen.id}`);
119
+ }).catch(err => {
120
+ console.error("[bunflare] Reload failed:", err);
121
+ });
122
+ });
123
+ mainWatcher.start();
124
+ watchers.push(mainWatcher);
125
+ console.log(`[bunflare] Watching ${mainSrcDir} for changes (main)`);
126
+ } else {
127
+ // ─── Single-worker mode (current behavior) ────────────────────
128
+ const config = await autoLoadConfig(baseDir, envFlag);
129
+ console.log(`[bunflare] Loaded config: ${config.name}${envFlag ? ` (env: ${envFlag})` : ""}`);
130
+ setDashboardConfig(config);
131
+
132
+ manager = new GenerationManager(config, baseDir);
133
+ const firstGen = await manager.reload();
134
+ console.log(`[bunflare] Generation ${firstGen.id} loaded`);
135
+ setGenerationManager(manager);
136
+
137
+ // File watcher — watch the source directory
138
+ const srcDir = path.dirname(path.resolve(baseDir, config.main));
139
+ const watcher = new FileWatcher(srcDir, () => {
140
+ manager.reload().then(gen => {
141
+ console.log(`[bunflare] Reloaded → generation ${gen.id}`);
142
+ }).catch(err => {
143
+ console.error("[bunflare] Reload failed:", err);
144
+ });
145
+ });
146
+ watcher.start();
147
+ watchers.push(watcher);
148
+ console.log(`[bunflare] Watching ${srcDir} for changes`);
149
+ }
150
+
151
+ // Start server — one Bun.serve(), delegates to active generation
152
+ const port = parseInt(portFlag ?? process.env.PORT ?? "8787", 10);
153
+ const hostname = listenFlag ?? process.env.HOST ?? "localhost";
154
+
155
+ const server = Bun.serve({
156
+ port,
157
+ hostname,
158
+ async fetch(request, server) {
159
+ addCfProperty(request);
160
+
161
+ const url = new URL(request.url);
162
+
163
+ // Dashboard trace WebSocket stream (must be before dashboard catch-all for server.upgrade)
164
+ if (url.pathname === "/__dashboard/api/traces/ws") {
165
+ const upgraded = server.upgrade(request, { data: { type: "trace-stream", _url: request.url } as any });
166
+ if (!upgraded) return new Response("WebSocket upgrade failed", { status: 500 });
167
+ return undefined as unknown as Response;
168
+ }
169
+
170
+ // Dashboard routes (HTML, assets, API)
171
+ if (url.pathname.startsWith("/__dashboard")) {
172
+ return handleDashboardRequest(request);
173
+ }
174
+
175
+ // Queue pull consumer endpoints: POST /cdn-cgi/handler/queues/<name>/messages/pull and /ack
176
+ const queuePullMatch = url.pathname.match(/^\/cdn-cgi\/handler\/queues\/([^/]+)\/messages\/(pull|ack)$/);
177
+ if (queuePullMatch && request.method === "POST") {
178
+ const queueName = decodeURIComponent(queuePullMatch[1]!);
179
+ const action = queuePullMatch[2]!;
180
+ const queueDb = getDatabase();
181
+ const pullConsumer = new QueuePullConsumer(queueDb, queueName);
182
+
183
+ try {
184
+ const body = await request.json() as PullRequest | AckRequest;
185
+ if (action === "pull") {
186
+ const result = pullConsumer.pull(body as PullRequest);
187
+ return Response.json(result);
188
+ } else {
189
+ const result = pullConsumer.ack(body as AckRequest);
190
+ return Response.json(result);
191
+ }
192
+ } catch (err) {
193
+ return Response.json({ error: String(err) }, { status: 400 });
194
+ }
195
+ }
196
+
197
+ // Email handler: POST /cdn-cgi/handler/email?from=...&to=...
198
+ if (url.pathname === "/cdn-cgi/handler/email" && request.method === "POST") {
199
+ const gen = manager.active;
200
+ if (!gen) return new Response("No active generation", { status: 503 });
201
+ const from = url.searchParams.get("from") ?? "";
202
+ const to = url.searchParams.get("to") ?? "";
203
+ const raw = await request.arrayBuffer();
204
+ return gen.callEmail(new Uint8Array(raw), from, to);
205
+ }
206
+
207
+ // Manual trigger: GET /cdn-cgi/handler/scheduled?cron=<expression>
208
+ if (url.pathname === "/cdn-cgi/handler/scheduled") {
209
+ const gen = manager.active;
210
+ if (!gen) return new Response("No active generation", { status: 503 });
211
+ const cronExpr = url.searchParams.get("cron") ?? "* * * * *";
212
+ return gen.callScheduled(cronExpr);
213
+ }
214
+
215
+ // Delegate to active generation
216
+ const gen = manager.active;
217
+ if (!gen) {
218
+ return new Response("No active generation", { status: 503 });
219
+ }
220
+
221
+ return (await gen.callFetch(request, server)) as Response;
222
+ },
223
+ websocket: {
224
+ open(ws) {
225
+ const data = ws.data as unknown as Record<string, unknown>;
226
+ if (data.type === "trace-stream") {
227
+ // Trace streaming WebSocket
228
+ const store = getTraceStore();
229
+
230
+ let filter: { path?: string; status?: string; attributeFilters?: Array<{ key: string; value: string; type: "include" | "exclude" }> } = {};
231
+ let buffer: TraceEvent[] = [];
232
+ const MAX_BUFFER = 1000;
233
+
234
+ // Track which traceIds pass/fail the filter so child spans don't leak
235
+ const allowedTraces = new Set<string>();
236
+ const excludedTraces = new Set<string>();
237
+
238
+ function isRootSpanFiltered(span: { name: string; status: string; parentSpanId: string | null; attributes: Record<string, unknown> }): boolean {
239
+ if (filter.status && filter.status !== "all") {
240
+ if (span.status !== "unset" && span.status !== filter.status) return true;
241
+ }
242
+ if (filter.path) {
243
+ if (!matchGlob(span.name, filter.path)) return true;
244
+ }
245
+ if (filter.attributeFilters && filter.attributeFilters.length > 0) {
246
+ const attrs = span.attributes;
247
+ for (const af of filter.attributeFilters) {
248
+ const val = attrs[af.key];
249
+ const matches = val !== undefined && String(val).toLowerCase().includes(af.value.toLowerCase());
250
+ if (af.type === "include" && !matches) return true;
251
+ if (af.type === "exclude" && matches) return true;
252
+ }
253
+ }
254
+ return false;
255
+ }
256
+
257
+ const unsubscribe = store.subscribe((event) => {
258
+ // Determine traceId for this event
259
+ const traceId = event.type === "span.event" ? event.event.traceId
260
+ : event.span.traceId;
261
+
262
+ // For root spans, evaluate filter and track decision
263
+ if ((event.type === "span.start" || event.type === "span.end") && event.span.parentSpanId === null) {
264
+ if (isRootSpanFiltered(event.span)) {
265
+ excludedTraces.add(traceId);
266
+ allowedTraces.delete(traceId);
267
+ return;
268
+ }
269
+ excludedTraces.delete(traceId);
270
+ allowedTraces.add(traceId);
271
+ } else {
272
+ // Child span or event: check if its trace was already filtered
273
+ if (excludedTraces.has(traceId)) return;
274
+ // If we haven't seen the root span yet, allow it through
275
+ }
276
+
277
+ if (buffer.length < MAX_BUFFER) {
278
+ buffer.push(event);
279
+ }
280
+ });
281
+
282
+ const interval = setInterval(() => {
283
+ if (buffer.length > 0) {
284
+ ws.send(JSON.stringify({ type: "batch", events: buffer }));
285
+ buffer = [];
286
+ }
287
+ }, 500);
288
+
289
+ // Send initial traces (after filter is available from query params)
290
+ try {
291
+ // Parse filter from initial connection URL if provided
292
+ const reqUrl = new URL((data as any)._url ?? "ws://localhost");
293
+ const statusParam = reqUrl.searchParams.get("status");
294
+ const pathParam = reqUrl.searchParams.get("path");
295
+ if (statusParam) filter.status = statusParam;
296
+ if (pathParam) filter.path = pathParam;
297
+ } catch {}
298
+
299
+ let sinceMs = 15 * 60 * 1000; // default 15 minutes
300
+ const since = Date.now() - sinceMs;
301
+ const recent = store.getRecentTraces(since, 200);
302
+ ws.send(JSON.stringify({ type: "initial", traces: recent }));
303
+
304
+ // Store cleanup handles on ws.data
305
+ (data as any)._traceCleanup = { unsubscribe, interval };
306
+ (data as any)._setFilter = (f: typeof filter & { sinceMs?: number }) => {
307
+ filter = f;
308
+ if (f.sinceMs !== undefined) sinceMs = f.sinceMs;
309
+ // Reset trace tracking when filter changes
310
+ allowedTraces.clear();
311
+ excludedTraces.clear();
312
+ // Re-send filtered initial traces so the client replaces stale data
313
+ const freshSince = sinceMs > 0 ? Date.now() - sinceMs : 0;
314
+ const freshTraces = store.getRecentTraces(freshSince, 200);
315
+ ws.send(JSON.stringify({ type: "initial", traces: freshTraces }));
316
+ };
317
+ return;
318
+ }
319
+
320
+ // CF WebSocket bridge
321
+ const cfSocket = (data as { cfSocket: CFWebSocket }).cfSocket;
322
+ cfSocket.addEventListener("message", (ev: Event) => {
323
+ const msgData = (ev as MessageEvent).data;
324
+ ws.send(msgData);
325
+ });
326
+ cfSocket.addEventListener("close", (ev: Event) => {
327
+ const ce = ev as CloseEvent;
328
+ ws.close(ce.code, ce.reason);
329
+ });
330
+ },
331
+ message(ws, message) {
332
+ const data = ws.data as unknown as Record<string, unknown>;
333
+ if (data.type === "trace-stream") {
334
+ try {
335
+ const msg = JSON.parse(typeof message === "string" ? message : new TextDecoder().decode(message));
336
+ if (msg.type === "filter") {
337
+ const setFilter = (data as any)._setFilter;
338
+ if (setFilter) setFilter({ path: msg.path, status: msg.status, attributeFilters: msg.attributeFilters, sinceMs: msg.sinceMs });
339
+ }
340
+ } catch {}
341
+ return;
342
+ }
343
+
344
+ const cfSocket = (data as { cfSocket: CFWebSocket }).cfSocket;
345
+ if (cfSocket._peer && cfSocket._peer._accepted) {
346
+ cfSocket._peer._dispatchWSEvent({ type: "message", data: typeof message === "string" ? message : message.buffer as ArrayBuffer });
347
+ } else if (cfSocket._peer) {
348
+ cfSocket._peer._eventQueue.push({ type: "message", data: typeof message === "string" ? message : message.buffer as ArrayBuffer });
349
+ }
350
+ },
351
+ close(ws, code, reason) {
352
+ const data = ws.data as unknown as Record<string, unknown>;
353
+ if (data.type === "trace-stream") {
354
+ const cleanup = (data as any)._traceCleanup;
355
+ if (cleanup) {
356
+ cleanup.unsubscribe();
357
+ clearInterval(cleanup.interval);
358
+ }
359
+ return;
360
+ }
361
+
362
+ const cfSocket = (data as { cfSocket: CFWebSocket }).cfSocket;
363
+ if (cfSocket._peer && cfSocket._peer.readyState !== 3 /* CLOSED */) {
364
+ const evt = { type: "close" as const, code: code ?? 1000, reason: reason ?? "", wasClean: true };
365
+ if (cfSocket._peer._accepted) {
366
+ cfSocket._peer._dispatchWSEvent(evt);
367
+ } else {
368
+ cfSocket._peer._eventQueue.push(evt);
369
+ }
370
+ cfSocket._peer.readyState = 3;
371
+ }
372
+ cfSocket.readyState = 3;
373
+ },
374
+ },
375
+ });
376
+
377
+ console.log(`[bunflare] Server running at http://${hostname}:${port}`);
378
+ console.log(`[bunflare] Dashboard: http://${hostname}:${port}/__dashboard`);
379
+
380
+ // Keep the process alive until terminated
381
+ await new Promise(() => {});
382
+ }
383
+
384
+ function matchGlob(text: string, pattern: string): boolean {
385
+ // Placeholder approach: protect ** before escaping special chars
386
+ const regex = pattern
387
+ .replace(/\*\*/g, "\0")
388
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
389
+ .replace(/\0/g, ".*")
390
+ .replace(/\*/g, "[^/]*");
391
+ return new RegExp(`^${regex}`).test(text);
392
+ }
@@ -0,0 +1,84 @@
1
+ import type { CliContext } from "./context";
2
+ import { parseFlag, resolveBinding } from "./context";
3
+ import { SqliteKVNamespace } from "../bindings/kv";
4
+
5
+ export async function run(ctx: CliContext, args: string[]) {
6
+ const sub = args[0];
7
+ if (sub !== "key") {
8
+ console.error(`Usage: bunflare kv key <list|get|put|delete> [options]`);
9
+ process.exit(1);
10
+ }
11
+
12
+ const action = args[1];
13
+ const config = await ctx.config();
14
+ const bindingFlag = parseFlag(ctx.args, "--binding");
15
+ const binding = resolveBinding(config.kv_namespaces, bindingFlag, "KV namespace");
16
+ const kv = new SqliteKVNamespace(ctx.db(), binding.binding);
17
+
18
+ switch (action) {
19
+ case "list": {
20
+ const prefix = parseFlag(ctx.args, "--prefix") ?? "";
21
+ let cursor = "";
22
+ let total = 0;
23
+ do {
24
+ const result = await kv.list({ prefix, cursor: cursor || undefined });
25
+ for (const key of result.keys) {
26
+ let line = key.name;
27
+ if (key.expiration) {
28
+ const exp = new Date(key.expiration * 1000).toISOString().slice(0, 19).replace("T", " ");
29
+ line += ` (expires: ${exp})`;
30
+ }
31
+ console.log(line);
32
+ }
33
+ total += result.keys.length;
34
+ cursor = result.cursor;
35
+ } while (cursor);
36
+ if (total === 0) console.log("(no keys)");
37
+ break;
38
+ }
39
+ case "get": {
40
+ const key = args[2];
41
+ if (!key) {
42
+ console.error("Usage: bunflare kv key get <key> [--binding NAME]");
43
+ process.exit(1);
44
+ }
45
+ const value = await kv.get(key);
46
+ if (value === null) {
47
+ console.error(`Key not found: ${key}`);
48
+ process.exit(1);
49
+ }
50
+ if (typeof value === "string") {
51
+ process.stdout.write(value);
52
+ // Add newline if stdout is a terminal
53
+ if (process.stdout.isTTY) process.stdout.write("\n");
54
+ } else {
55
+ process.stdout.write(String(value));
56
+ }
57
+ break;
58
+ }
59
+ case "put": {
60
+ const key = args[2];
61
+ const value = args[3];
62
+ if (!key || value === undefined) {
63
+ console.error("Usage: bunflare kv key put <key> <value> [--binding NAME]");
64
+ process.exit(1);
65
+ }
66
+ await kv.put(key, value);
67
+ console.log(`Put ${key}`);
68
+ break;
69
+ }
70
+ case "delete": {
71
+ const key = args[2];
72
+ if (!key) {
73
+ console.error("Usage: bunflare kv key delete <key> [--binding NAME]");
74
+ process.exit(1);
75
+ }
76
+ await kv.delete(key);
77
+ console.log(`Deleted ${key}`);
78
+ break;
79
+ }
80
+ default:
81
+ console.error(`Usage: bunflare kv key <list|get|put|delete> [options]`);
82
+ process.exit(1);
83
+ }
84
+ }
@@ -0,0 +1,109 @@
1
+ import type { CliContext } from "./context";
2
+ import { parseFlag } from "./context";
3
+
4
+ export async function run(ctx: CliContext, args: string[]) {
5
+ const action = args[0];
6
+
7
+ switch (action) {
8
+ case "list": {
9
+ const config = await ctx.config();
10
+ const producers = config.queues?.producers ?? [];
11
+ const consumers = config.queues?.consumers ?? [];
12
+ if (producers.length === 0 && consumers.length === 0) {
13
+ console.log("No queues configured.");
14
+ return;
15
+ }
16
+ const queues = new Map<string, { producer?: string; consumer: boolean }>();
17
+ for (const p of producers) {
18
+ queues.set(p.queue, { producer: p.binding, consumer: false });
19
+ }
20
+ for (const c of consumers) {
21
+ const existing = queues.get(c.queue);
22
+ if (existing) {
23
+ existing.consumer = true;
24
+ } else {
25
+ queues.set(c.queue, { consumer: true });
26
+ }
27
+ }
28
+ for (const [name, info] of queues) {
29
+ const parts = [name];
30
+ if (info.producer) parts.push(`binding=${info.producer}`);
31
+ if (info.consumer) parts.push("(consumer)");
32
+ console.log(parts.join(" "));
33
+ }
34
+ break;
35
+ }
36
+ case "message": {
37
+ const sub = args[1];
38
+ const queueName = args[2];
39
+
40
+ switch (sub) {
41
+ case "list": {
42
+ if (!queueName) {
43
+ console.error("Usage: bunflare queues message list <queue>");
44
+ process.exit(1);
45
+ }
46
+ const db = ctx.db();
47
+ const rows = db.query<
48
+ { id: string; status: string; attempts: number; created_at: number; content_type: string; body: Buffer },
49
+ [string]
50
+ >(
51
+ "SELECT id, status, attempts, created_at, content_type, body FROM queue_messages WHERE queue = ? ORDER BY created_at DESC LIMIT 100",
52
+ ).all(queueName);
53
+ if (rows.length === 0) {
54
+ console.log("(no messages)");
55
+ return;
56
+ }
57
+ for (const row of rows) {
58
+ const date = new Date(row.created_at).toISOString().slice(0, 19).replace("T", " ");
59
+ let preview = "";
60
+ if (row.content_type === "json" || row.content_type === "text") {
61
+ preview = Buffer.from(row.body).toString().slice(0, 80);
62
+ } else {
63
+ preview = `(${row.content_type}, ${row.body.length} bytes)`;
64
+ }
65
+ console.log(`${row.id} ${row.status.padEnd(8)} attempts=${row.attempts} ${date} ${preview}`);
66
+ }
67
+ break;
68
+ }
69
+ case "send": {
70
+ const body = args[3];
71
+ if (!queueName || body === undefined) {
72
+ console.error("Usage: bunflare queues message send <queue> <body>");
73
+ process.exit(1);
74
+ }
75
+ const { SqliteQueueProducer } = await import("../bindings/queue");
76
+ const producer = new SqliteQueueProducer(ctx.db(), queueName);
77
+ // Try parsing as JSON, fall back to text
78
+ let parsed: unknown;
79
+ try {
80
+ parsed = JSON.parse(body);
81
+ } catch {
82
+ parsed = body;
83
+ }
84
+ await producer.send(parsed, { contentType: typeof parsed === "string" ? "text" : "json" });
85
+ console.log(`Sent message to queue "${queueName}"`);
86
+ break;
87
+ }
88
+ case "purge": {
89
+ if (!queueName) {
90
+ console.error("Usage: bunflare queues message purge <queue>");
91
+ process.exit(1);
92
+ }
93
+ const db = ctx.db();
94
+ const result = db.run("DELETE FROM queue_messages WHERE queue = ?", [queueName]);
95
+ db.run("DELETE FROM queue_leases WHERE queue = ?", [queueName]);
96
+ console.log(`Purged ${result.changes} message(s) from queue "${queueName}"`);
97
+ break;
98
+ }
99
+ default:
100
+ console.error("Usage: bunflare queues message <list|send|purge> <queue>");
101
+ process.exit(1);
102
+ }
103
+ break;
104
+ }
105
+ default:
106
+ console.error("Usage: bunflare queues <list|message> [options]");
107
+ process.exit(1);
108
+ }
109
+ }