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,411 @@
1
+ import type { WranglerConfig } from "./config";
2
+ import type { DurableObjectNamespaceImpl } from "./bindings/durable-object";
3
+ import type { SqliteWorkflowBinding } from "./bindings/workflow";
4
+ import { QueueConsumer } from "./bindings/queue";
5
+ import { startCronScheduler, createScheduledController } from "./bindings/scheduled";
6
+ import { ForwardableEmailMessage } from "./bindings/email";
7
+ import { randomUUIDv7 } from "bun";
8
+ import { ExecutionContext } from "./execution-context";
9
+ import { CFWebSocket } from "./bindings/websocket-pair";
10
+ import { getDatabase } from "./db";
11
+ import { startSpan, setSpanAttribute, persistError } from "./tracing/span";
12
+ import { getActiveContext } from "./tracing/context";
13
+ import { renderErrorPage } from "./error-page/build";
14
+
15
+ interface ClassRegistry {
16
+ durableObjects: { bindingName: string; className: string; namespace: DurableObjectNamespaceImpl }[];
17
+ workflows: { bindingName: string; className: string; binding: SqliteWorkflowBinding }[];
18
+ containers: { className: string; image: string; maxInstances?: number; namespace: DurableObjectNamespaceImpl }[];
19
+ queueConsumers: { queue: string; maxBatchSize: number; maxBatchTimeout: number; maxRetries: number; deadLetterQueue: string | null }[];
20
+ serviceBindings: { bindingName: string; serviceName: string; entrypoint?: string; proxy: Record<string, unknown> }[];
21
+ staticAssets: { fetch(req: Request): Promise<Response> } | null;
22
+ }
23
+
24
+ export type GenerationState = "active" | "draining" | "stopped";
25
+
26
+ export interface GenerationInfo {
27
+ id: number;
28
+ state: GenerationState;
29
+ createdAt: number;
30
+ activeRequests: number;
31
+ }
32
+
33
+ export class Generation {
34
+ readonly id: number;
35
+ state: GenerationState = "active";
36
+ readonly createdAt: number;
37
+ readonly workerModule: Record<string, unknown>;
38
+ readonly defaultExport: unknown;
39
+ readonly classBasedExport: boolean;
40
+ readonly env: Record<string, unknown>;
41
+ readonly registry: ClassRegistry;
42
+ readonly config: WranglerConfig;
43
+ readonly workerName: string | undefined;
44
+ readonly cronEnabled: boolean;
45
+ activeRequests = 0;
46
+
47
+ private queueConsumers: QueueConsumer[] = [];
48
+ private cronTimer: NodeJS.Timer | ReturnType<typeof setInterval> | null = null;
49
+ drainTimer: ReturnType<typeof setTimeout> | null = null;
50
+
51
+ constructor(
52
+ id: number,
53
+ workerModule: Record<string, unknown>,
54
+ defaultExport: unknown,
55
+ classBasedExport: boolean,
56
+ env: Record<string, unknown>,
57
+ registry: ClassRegistry,
58
+ config: WranglerConfig,
59
+ workerName?: string,
60
+ cronEnabled?: boolean,
61
+ ) {
62
+ this.id = id;
63
+ this.createdAt = Date.now();
64
+ this.workerModule = workerModule;
65
+ this.defaultExport = defaultExport;
66
+ this.classBasedExport = classBasedExport;
67
+ this.env = env;
68
+ this.registry = registry;
69
+ this.config = config;
70
+ this.workerName = workerName;
71
+ this.cronEnabled = cronEnabled ?? false;
72
+ }
73
+
74
+ /** Get a handler method from the worker module (class-based or object-based) */
75
+ private getHandler(name: string): ((...args: unknown[]) => Promise<void>) | undefined {
76
+ if (this.classBasedExport) {
77
+ if (typeof (this.defaultExport as any).prototype[name] === "function") {
78
+ return (...args: unknown[]) => {
79
+ const ctx = new ExecutionContext();
80
+ const instance = new (this.defaultExport as new (ctx: ExecutionContext, env: unknown) => Record<string, unknown>)(ctx, this.env);
81
+ return (instance[name] as (...a: unknown[]) => Promise<void>)(...args);
82
+ };
83
+ }
84
+ return undefined;
85
+ }
86
+ const method = (this.defaultExport as Record<string, unknown>)?.[name];
87
+ return typeof method === "function" ? method.bind(this.defaultExport) : undefined;
88
+ }
89
+
90
+ /** Dispatch a fetch request through this generation's handler */
91
+ async callFetch(request: Request, server: any): Promise<Response | undefined> {
92
+ this.activeRequests++;
93
+ const ctx = new ExecutionContext();
94
+ try {
95
+ const url = new URL(request.url);
96
+
97
+ // Skip tracing for internal/infrastructure paths (Bun HMR, browser probes, etc.)
98
+ const skipTracing = url.pathname.startsWith("/_bun/") || url.pathname.startsWith("/.well-known/");
99
+
100
+ // Capture caller stack before entering the worker — frameworks like Hono
101
+ // use .then()/.catch() internally which destroys async stack traces in Bun.
102
+ // We stitch this context onto caught errors so the error page shows the
103
+ // full call chain even when the engine loses it.
104
+ const callerStack = skipTracing ? null : new Error();
105
+
106
+ const handler = async () => {
107
+ const callWorkerFetch = async (req: Request) => {
108
+ if (this.classBasedExport) {
109
+ const instance = new (this.defaultExport as new (ctx: ExecutionContext, env: unknown) => Record<string, unknown>)(ctx, this.env);
110
+ return await (instance.fetch as (r: Request) => Promise<Response>)(req);
111
+ }
112
+ return await (this.defaultExport as { fetch: Function }).fetch(req, this.env, ctx) as Response;
113
+ };
114
+
115
+ const handleResponse = (response: Response): Response | undefined => {
116
+ setSpanAttribute("http.status_code", response.status);
117
+ const ws = (response as Response & { webSocket?: CFWebSocket }).webSocket;
118
+ if (response.status === 101 && ws instanceof CFWebSocket) {
119
+ const upgraded = (server as { upgrade(req: Request, opts: { data: unknown }): boolean }).upgrade(request, { data: { cfSocket: ws } });
120
+ if (!upgraded) {
121
+ return new Response("WebSocket upgrade failed", { status: 500 });
122
+ }
123
+ return undefined;
124
+ }
125
+ const ctx = getActiveContext();
126
+ if (ctx) {
127
+ const res = new Response(response.body, response);
128
+ res.headers.set("X-Trace-Id", ctx.traceId);
129
+ return res;
130
+ }
131
+ return response;
132
+ };
133
+
134
+ const handleError = async (err: unknown): Promise<Response> => {
135
+ if (err instanceof Error) {
136
+ // Prefer fetch call-site stack — it shows the user's code that
137
+ // triggered the outbound call (e.g. graphql client → user handler).
138
+ // Fall back to callerStack which only shows bunflare entry frames.
139
+ const ctx = getActiveContext();
140
+ const fetchCallStack = ctx?.fetchStack.current;
141
+ stitchAsyncStack(err, fetchCallStack ?? callerStack);
142
+ }
143
+ console.error("[bunflare] Request error:\n" + (err instanceof Error ? err.stack : String(err)));
144
+ return renderErrorPage(err, request, this.env, this.config, this.workerName);
145
+ };
146
+
147
+ const runWorkerFirst = this.config.assets?.run_worker_first;
148
+ const hasAssets = this.registry.staticAssets && !this.config.assets?.binding;
149
+ const workerFirst = hasAssets && shouldRunWorkerFirst(runWorkerFirst, url.pathname);
150
+
151
+ if (workerFirst) {
152
+ try {
153
+ const workerResponse = await callWorkerFetch(request);
154
+ const result = handleResponse(workerResponse);
155
+ if (result === undefined) return undefined;
156
+ if (result.status !== 404) {
157
+ ctx._awaitAll().catch(() => {});
158
+ return result;
159
+ }
160
+ } catch (err) {
161
+ return handleError(err);
162
+ }
163
+ return await this.registry.staticAssets!.fetch(request);
164
+ }
165
+
166
+ if (hasAssets) {
167
+ const assetResponse = await this.registry.staticAssets!.fetch(request);
168
+ if (assetResponse.status !== 404) {
169
+ return assetResponse;
170
+ }
171
+ }
172
+
173
+ try {
174
+ const response = await callWorkerFetch(request);
175
+ const result = handleResponse(response);
176
+ if (result === undefined) return undefined;
177
+ ctx._awaitAll().catch(() => {});
178
+ return result;
179
+ } catch (err) {
180
+ return handleError(err);
181
+ }
182
+ };
183
+
184
+ if (skipTracing) {
185
+ return await handler();
186
+ }
187
+
188
+ return await startSpan({
189
+ name: `${request.method} ${url.pathname}`,
190
+ kind: "server",
191
+ attributes: { "http.method": request.method, "http.url": request.url },
192
+ workerName: this.workerName,
193
+ }, handler);
194
+ } finally {
195
+ this.activeRequests--;
196
+ }
197
+ }
198
+
199
+ /** Handle manual /cdn-cgi/handler/scheduled trigger */
200
+ async callScheduled(cronExpr: string): Promise<Response> {
201
+ return startSpan({
202
+ name: "scheduled",
203
+ kind: "server",
204
+ attributes: { cron: cronExpr },
205
+ workerName: this.workerName,
206
+ }, async () => {
207
+ const ctx = new ExecutionContext();
208
+ let handler: Function | undefined;
209
+ if (this.classBasedExport) {
210
+ const proto = (this.defaultExport as { prototype: Record<string, unknown> }).prototype;
211
+ if (typeof proto.scheduled === "function") {
212
+ const instance = new (this.defaultExport as new (ctx: ExecutionContext, env: unknown) => Record<string, Function>)(ctx, this.env);
213
+ handler = instance.scheduled!.bind(instance);
214
+ }
215
+ } else {
216
+ const obj = this.defaultExport as Record<string, unknown>;
217
+ if (typeof obj.scheduled === "function") {
218
+ handler = (obj.scheduled as Function).bind(obj);
219
+ }
220
+ }
221
+
222
+ if (!handler) {
223
+ return new Response("No scheduled handler defined", { status: 404 });
224
+ }
225
+ const controller = createScheduledController(cronExpr, Date.now());
226
+ try {
227
+ await handler(controller, this.env, ctx);
228
+ await ctx._awaitAll();
229
+ return new Response(`Scheduled handler executed (cron: ${cronExpr})`, { status: 200 });
230
+ } catch (err) {
231
+ console.error("[bunflare] Scheduled handler error:", err);
232
+ persistError(err, "scheduled", this.workerName);
233
+ throw err;
234
+ }
235
+ });
236
+ }
237
+
238
+ /** Handle incoming email — dispatches to the worker's email() handler */
239
+ async callEmail(rawBytes: Uint8Array, from: string, to: string): Promise<Response> {
240
+ return startSpan({
241
+ name: "email",
242
+ kind: "server",
243
+ attributes: { "email.from": from, "email.to": to },
244
+ workerName: this.workerName,
245
+ }, async () => {
246
+ const ctx = new ExecutionContext();
247
+ let handler: Function | undefined;
248
+ if (this.classBasedExport) {
249
+ const proto = (this.defaultExport as { prototype: Record<string, unknown> }).prototype;
250
+ if (typeof proto.email === "function") {
251
+ const instance = new (this.defaultExport as new (ctx: ExecutionContext, env: unknown) => Record<string, Function>)(ctx, this.env);
252
+ handler = instance.email!.bind(instance);
253
+ }
254
+ } else {
255
+ const obj = this.defaultExport as Record<string, unknown>;
256
+ if (typeof obj.email === "function") {
257
+ handler = (obj.email as Function).bind(obj);
258
+ }
259
+ }
260
+
261
+ if (!handler) {
262
+ return new Response("No email handler defined", { status: 404 });
263
+ }
264
+
265
+ // Persist incoming email to DB
266
+ const db = getDatabase();
267
+ const messageId = randomUUIDv7();
268
+ db.run(
269
+ "INSERT INTO email_messages (id, binding, from_addr, to_addr, raw, raw_size, status, created_at) VALUES (?, ?, ?, ?, ?, ?, 'received', ?)",
270
+ [messageId, "_incoming", from, to, rawBytes, rawBytes.byteLength, Date.now()],
271
+ );
272
+
273
+ const message = new ForwardableEmailMessage(db, messageId, from, to, rawBytes);
274
+
275
+ try {
276
+ await handler(message, this.env, ctx);
277
+ await ctx._awaitAll();
278
+ return new Response(`Email handled (from: ${from}, to: ${to})`, { status: 200 });
279
+ } catch (err) {
280
+ console.error("[bunflare] Email handler error:", err);
281
+ persistError(err, "email", this.workerName);
282
+ throw err;
283
+ }
284
+ });
285
+ }
286
+
287
+ /** Start queue consumers and cron scheduler */
288
+ startConsumers(): void {
289
+ const queueHandler = this.getHandler("queue");
290
+ if (this.registry.queueConsumers.length > 0 && queueHandler) {
291
+ const db = getDatabase();
292
+ for (const config of this.registry.queueConsumers) {
293
+ const consumer = new QueueConsumer(db, config, queueHandler as any, this.env, this.workerName);
294
+ consumer.start();
295
+ this.queueConsumers.push(consumer);
296
+ }
297
+ }
298
+
299
+ if (this.cronEnabled) {
300
+ const crons = this.config.triggers?.crons ?? [];
301
+ const scheduledHandler = this.getHandler("scheduled");
302
+ if (crons.length > 0 && scheduledHandler) {
303
+ this.cronTimer = startCronScheduler(crons, scheduledHandler as any, this.env, this.workerName);
304
+ }
305
+ }
306
+ }
307
+
308
+ /** Stop queue consumers and cron scheduler */
309
+ stopConsumers(): void {
310
+ for (const consumer of this.queueConsumers) {
311
+ consumer.stop();
312
+ }
313
+ this.queueConsumers = [];
314
+ if (this.cronTimer) {
315
+ clearInterval(this.cronTimer);
316
+ this.cronTimer = null;
317
+ }
318
+ }
319
+
320
+ /** Transition to draining — stops consumers, keeps in-flight requests alive */
321
+ drain(): void {
322
+ if (this.state === "stopped") return;
323
+ this.state = "draining";
324
+ this.stopConsumers();
325
+ }
326
+
327
+ /** Force-stop: drain + destroy all DO namespaces + abort workflows */
328
+ stop(): void {
329
+ if (this.state === "stopped") return;
330
+ this.drain();
331
+ this.state = "stopped";
332
+ if (this.drainTimer) {
333
+ clearTimeout(this.drainTimer);
334
+ this.drainTimer = null;
335
+ }
336
+ for (const entry of this.registry.durableObjects) {
337
+ entry.namespace.destroy();
338
+ }
339
+ for (const entry of this.registry.workflows) {
340
+ entry.binding.abortRunning();
341
+ }
342
+ }
343
+
344
+ /** Check if this generation has no more work */
345
+ isIdle(): boolean {
346
+ if (this.activeRequests > 0) return false;
347
+ for (const entry of this.registry.durableObjects) {
348
+ // Check if any DO instances still have active WebSockets
349
+ const ns = entry.namespace as any;
350
+ if (ns.instances) {
351
+ for (const [, instance] of ns.instances as Map<string, any>) {
352
+ const state = instance.ctx;
353
+ if (state.getWebSockets().length > 0) return false;
354
+ }
355
+ }
356
+ }
357
+ return true;
358
+ }
359
+
360
+ /** Get info for dashboard */
361
+ getInfo(): GenerationInfo {
362
+ return {
363
+ id: this.id,
364
+ state: this.state,
365
+ createdAt: this.createdAt,
366
+ activeRequests: this.activeRequests,
367
+ };
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Stitch a pre-captured caller stack onto an error whose async stack was
373
+ * destroyed by .then()/.catch() boundaries (e.g. Hono's dispatch) or by
374
+ * ALS.run() in Bun/JSC.
375
+ *
376
+ * Only appends if the error's stack looks truncated (few frames or contains
377
+ * processTicksAndRejections). Strips bunflare runtime frames from the
378
+ * captured stack so only user/library code is shown.
379
+ */
380
+ function stitchAsyncStack(err: Error, callerError: Error | null): void {
381
+ if (!callerError) return;
382
+ if (!err.stack || !callerError.stack) return;
383
+ // Already stitched
384
+ if (err.stack.includes("--- async ---")) return;
385
+
386
+ const errFrames = err.stack.split("\n").filter(l => l.trim().startsWith("at "));
387
+ // Only stitch when the stack looks truncated (≤5 real frames or has processTicksAndRejections)
388
+ const looksShort = errFrames.length <= 5 || err.stack.includes("processTicksAndRejections");
389
+ if (!looksShort) return;
390
+
391
+ const callerLines = callerError.stack.split("\n").slice(1);
392
+
393
+ // Strip bunflare runtime frames — keep only user/library code
394
+ const filtered = callerLines.filter(l => !l.includes("/bunflare/runtime/"));
395
+ if (filtered.length === 0) return;
396
+
397
+ err.stack += "\n --- async ---\n" + filtered.join("\n");
398
+ }
399
+
400
+ function shouldRunWorkerFirst(config: boolean | string[] | undefined, pathname: string): boolean {
401
+ if (config === true) return true;
402
+ if (!config) return false;
403
+ return config.some(pattern => {
404
+ if (pattern === pathname) return true;
405
+ if (pattern.endsWith("/*")) {
406
+ const prefix = pattern.slice(0, -1);
407
+ return pathname.startsWith(prefix) || pathname === pattern.slice(0, -2);
408
+ }
409
+ return false;
410
+ });
411
+ }
@@ -0,0 +1,292 @@
1
+ import { plugin } from "bun";
2
+ import { DurableObjectBase, WebSocketRequestResponsePair } from "./bindings/durable-object";
3
+ import { WorkflowEntrypointBase, NonRetryableError } from "./bindings/workflow";
4
+ import { ContainerBase, getContainer, getRandom } from "./bindings/container";
5
+ import { EmailMessage } from "./bindings/email";
6
+ import type { BrowserBinding } from "./bindings/browser";
7
+ import { SqliteCacheStorage } from "./bindings/cache";
8
+ import { HTMLRewriter } from "./bindings/html-rewriter";
9
+ import { WebSocketPair } from "./bindings/websocket-pair";
10
+ import { IdentityTransformStream, FixedLengthStream } from "./bindings/cf-streams";
11
+ import { patchGlobalCrypto } from "./bindings/crypto-extras";
12
+ import { getDatabase } from "./db";
13
+ import { globalEnv } from "./env";
14
+ import { instrumentBinding } from "./tracing/instrument";
15
+ import { getActiveContext } from "./tracing/context";
16
+ import { startSpan, setSpanAttribute, addSpanEvent } from "./tracing/span";
17
+
18
+ // Register global `caches` object (CacheStorage) with tracing
19
+ const rawCacheStorage = new SqliteCacheStorage(getDatabase());
20
+ const cacheMethods = ["match", "put", "delete"];
21
+
22
+ // Instrument the default cache
23
+ rawCacheStorage.default = instrumentBinding(rawCacheStorage.default, {
24
+ type: "cache", name: "default", methods: cacheMethods,
25
+ }) as typeof rawCacheStorage.default;
26
+
27
+ // Wrap open() to return instrumented caches
28
+ const originalOpen = rawCacheStorage.open.bind(rawCacheStorage);
29
+ rawCacheStorage.open = async (cacheName: string) => {
30
+ const cache = await originalOpen(cacheName);
31
+ return instrumentBinding(cache, {
32
+ type: "cache", name: cacheName, methods: cacheMethods,
33
+ });
34
+ };
35
+
36
+ Object.defineProperty(globalThis, "caches", {
37
+ value: rawCacheStorage,
38
+ writable: false,
39
+ configurable: true,
40
+ });
41
+
42
+ // Register global `HTMLRewriter` class
43
+ Object.defineProperty(globalThis, "HTMLRewriter", {
44
+ value: HTMLRewriter,
45
+ writable: false,
46
+ configurable: true,
47
+ });
48
+
49
+ // Register global `WebSocketPair` class
50
+ Object.defineProperty(globalThis, "WebSocketPair", {
51
+ value: WebSocketPair,
52
+ writable: false,
53
+ configurable: true,
54
+ });
55
+
56
+ // Register global CF stream classes
57
+ Object.defineProperty(globalThis, "IdentityTransformStream", {
58
+ value: IdentityTransformStream,
59
+ writable: false,
60
+ configurable: true,
61
+ });
62
+
63
+ Object.defineProperty(globalThis, "FixedLengthStream", {
64
+ value: FixedLengthStream,
65
+ writable: false,
66
+ configurable: true,
67
+ });
68
+
69
+ // Patch crypto with CF-specific extensions (timingSafeEqual, DigestStream)
70
+ patchGlobalCrypto();
71
+
72
+ // Set navigator.userAgent to match Cloudflare Workers
73
+ Object.defineProperty(globalThis.navigator, "userAgent", {
74
+ value: "Cloudflare-Workers",
75
+ writable: false,
76
+ configurable: true,
77
+ });
78
+
79
+ // Set navigator.language (behind enable_navigator_language compat flag in CF)
80
+ if (!globalThis.navigator.language) {
81
+ Object.defineProperty(globalThis.navigator, "language", {
82
+ value: "en",
83
+ writable: false,
84
+ configurable: true,
85
+ });
86
+ }
87
+
88
+ // Set performance.timeOrigin to 0 (CF semantics)
89
+ Object.defineProperty(globalThis.performance, "timeOrigin", {
90
+ value: 0,
91
+ writable: false,
92
+ configurable: true,
93
+ });
94
+
95
+ // Register scheduler.wait(ms) — await-able setTimeout alternative
96
+ Object.defineProperty(globalThis, "scheduler", {
97
+ value: {
98
+ wait(ms: number): Promise<void> {
99
+ return new Promise((resolve) => setTimeout(resolve, ms));
100
+ },
101
+ },
102
+ writable: false,
103
+ configurable: true,
104
+ });
105
+
106
+ // ─── Console instrumentation ─────────────────────────────────────────
107
+ // Captures console.log/info/warn/error/debug as span events when inside a trace context.
108
+
109
+ function formatConsoleArg(arg: unknown): string {
110
+ if (typeof arg === "string") return arg;
111
+ if (arg instanceof Error) return arg.stack ?? arg.message;
112
+ try { return JSON.stringify(arg); } catch { return String(arg); }
113
+ }
114
+
115
+ const consoleMethods = ["log", "info", "warn", "error", "debug"] as const;
116
+ type ConsoleMethod = (typeof consoleMethods)[number];
117
+
118
+ const _originalConsole: Record<ConsoleMethod, (...args: unknown[]) => void> = {} as any;
119
+
120
+ for (const method of consoleMethods) {
121
+ _originalConsole[method] = console[method].bind(console);
122
+ (console as any)[method] = (...args: unknown[]) => {
123
+ _originalConsole[method](...args);
124
+ const ctx = getActiveContext();
125
+ if (!ctx) return;
126
+ const message = args.map(formatConsoleArg).join(" ");
127
+ addSpanEvent(`console.${method}`, method, message);
128
+ };
129
+ }
130
+
131
+ // ─── Fetch instrumentation ───────────────────────────────────────────
132
+ // Creates a tracing span for every outgoing fetch and captures request/response bodies.
133
+ // Also captures call-site stacks for async stack reconstruction (see stitchAsyncStack).
134
+
135
+ const MAX_BODY_CAPTURE = 128 * 1024; // 128 KB
136
+ const TEXT_TYPES = ["application/json", "text/", "application/xml", "application/javascript", "application/x-www-form-urlencoded", "application/graphql"];
137
+
138
+ function isTextContent(ct: string | null): boolean {
139
+ if (!ct) return true; // no content-type → assume text
140
+ return TEXT_TYPES.some(t => ct.includes(t));
141
+ }
142
+
143
+ function headersToRecord(h: Headers): Record<string, string> {
144
+ const obj: Record<string, string> = {};
145
+ h.forEach((v, k) => { obj[k] = v; });
146
+ return obj;
147
+ }
148
+
149
+ async function readBodyLimited(r: Request | Response): Promise<string | null> {
150
+ if (!r.body) return null;
151
+ const ct = r.headers.get("content-type");
152
+ const cl = r.headers.get("content-length");
153
+ const size = cl ? parseInt(cl, 10) : null;
154
+ if (!isTextContent(ct)) {
155
+ return size != null ? `[binary ${ct}: ${size} bytes]` : `[binary: ${ct ?? "unknown"}]`;
156
+ }
157
+ if (size != null && size > MAX_BODY_CAPTURE) {
158
+ return `[body too large: ${size} bytes]`;
159
+ }
160
+ try {
161
+ const text = await r.text();
162
+ return text.length > MAX_BODY_CAPTURE
163
+ ? text.slice(0, MAX_BODY_CAPTURE) + "… [truncated]"
164
+ : text || null;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ const _originalFetch = globalThis.fetch;
171
+ globalThis.fetch = ((input: any, init?: any): Promise<Response> => {
172
+ const ctx = getActiveContext();
173
+ if (ctx) {
174
+ ctx.fetchStack.current = new Error();
175
+ }
176
+ // Outside a trace context, just pass through
177
+ if (!ctx) return _originalFetch(input, init);
178
+
179
+ const request = new Request(input, init);
180
+ const fetchRequest = request.clone();
181
+ const url = request.url;
182
+ const method = request.method;
183
+ let pathname: string;
184
+ try { pathname = new URL(url).pathname; } catch { pathname = url; }
185
+
186
+ return startSpan({
187
+ name: `fetch ${method} ${pathname}`,
188
+ kind: "client",
189
+ attributes: {
190
+ "http.method": method,
191
+ "http.url": url,
192
+ "http.request.headers": headersToRecord(request.headers),
193
+ },
194
+ }, async () => {
195
+ // Capture request body (from the original — fetchRequest is sent to the network)
196
+ const reqBody = await readBodyLimited(request);
197
+ if (reqBody) setSpanAttribute("http.request.body", reqBody);
198
+
199
+ const response = await _originalFetch(fetchRequest as globalThis.Request);
200
+
201
+ setSpanAttribute("http.status_code", response.status);
202
+ setSpanAttribute("http.response.headers", headersToRecord(response.headers));
203
+
204
+ // Capture response body from a clone (caller keeps the original stream)
205
+ const resBody = await readBodyLimited(response.clone());
206
+ if (resBody) setSpanAttribute("http.response.body", resBody);
207
+
208
+ return response;
209
+ });
210
+ }) as typeof globalThis.fetch;
211
+
212
+ plugin({
213
+ name: "cloudflare-workers-shim",
214
+ setup(build) {
215
+ build.module("cloudflare:workers", () => {
216
+ // Use a getter so `env` always returns the latest built env object
217
+ return {
218
+ exports: {
219
+ DurableObject: DurableObjectBase,
220
+ WorkflowEntrypoint: WorkflowEntrypointBase,
221
+ WorkerEntrypoint: class WorkerEntrypoint {
222
+ protected ctx: unknown;
223
+ protected env: unknown;
224
+ constructor(ctx: unknown, env: unknown) {
225
+ this.ctx = ctx;
226
+ this.env = env;
227
+ (this as any)[Symbol.for("bunflare.RpcTarget")] = true;
228
+ }
229
+ },
230
+ WebSocketRequestResponsePair,
231
+ WebSocketPair,
232
+ RpcTarget: class RpcTarget {
233
+ constructor() {
234
+ (this as any)[Symbol.for("bunflare.RpcTarget")] = true;
235
+ }
236
+ },
237
+ env: globalEnv,
238
+ },
239
+ loader: "object",
240
+ };
241
+ });
242
+
243
+ build.module("@cloudflare/containers", () => {
244
+ return {
245
+ exports: {
246
+ Container: ContainerBase,
247
+ getContainer,
248
+ getRandom,
249
+ switchPort(request: Request, port: number): Request {
250
+ const headers = new Headers(request.headers);
251
+ headers.set("cf-container-target-port", port.toString());
252
+ return new Request(request, { headers });
253
+ },
254
+ loadBalance: getRandom,
255
+ },
256
+ loader: "object",
257
+ };
258
+ });
259
+
260
+ build.module("cloudflare:email", () => {
261
+ return {
262
+ exports: {
263
+ EmailMessage,
264
+ },
265
+ loader: "object",
266
+ };
267
+ });
268
+
269
+ build.module("cloudflare:workflows", () => {
270
+ return {
271
+ exports: {
272
+ NonRetryableError,
273
+ },
274
+ loader: "object",
275
+ };
276
+ });
277
+
278
+ build.module("@cloudflare/puppeteer", () => {
279
+ return {
280
+ exports: {
281
+ default: {
282
+ launch: (endpoint: BrowserBinding, opts?: { keep_alive?: number }) => endpoint.launch(opts),
283
+ connect: (endpoint: BrowserBinding, sessionId: string) => endpoint.connect(sessionId),
284
+ sessions: (endpoint: BrowserBinding) => endpoint.sessions(),
285
+ },
286
+ ActiveSession: {} as any, // type-only re-export placeholder
287
+ },
288
+ loader: "object",
289
+ };
290
+ });
291
+ },
292
+ });