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,662 @@
1
+ import { DockerManager, type DockerRunOptions } from "./container-docker";
2
+ import { DurableObjectBase, type DurableObjectStateImpl } from "./durable-object";
3
+
4
+ // ─── Types ──────────────────────────────────────────────────────────────────
5
+
6
+ export type ContainerStatus =
7
+ | "stopped"
8
+ | "running"
9
+ | "healthy"
10
+ | "stopping"
11
+ | "stopped_with_code";
12
+
13
+ export interface ContainerState {
14
+ status: ContainerStatus;
15
+ lastChange: number;
16
+ exitCode?: number;
17
+ }
18
+
19
+ export interface ContainerConfig {
20
+ image: string;
21
+ className: string;
22
+ maxInstances?: number;
23
+ dockerManager: DockerManager;
24
+ }
25
+
26
+ interface TcpPort {
27
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
28
+ connect(): never;
29
+ }
30
+
31
+ // ─── ContainerRuntime ───────────────────────────────────────────────────────
32
+ // Per-DO-instance lifecycle manager that runs one Docker container.
33
+
34
+ export class ContainerRuntime {
35
+ private _status: ContainerStatus = "stopped";
36
+ private _lastChange = Date.now();
37
+ private _exitCode?: number;
38
+ private _hostPorts = new Map<number, number>(); // containerPort -> hostPort
39
+ private _containerName: string;
40
+ private _docker: DockerManager;
41
+ private _image: string;
42
+ private _healthCheckTimer?: ReturnType<typeof setInterval>;
43
+ private _activityTimer?: ReturnType<typeof setTimeout>;
44
+ private _monitorTimer?: ReturnType<typeof setInterval>;
45
+ private _monitorResolve?: () => void;
46
+ private _monitorReject?: (err: Error) => void;
47
+ private _monitorPromise?: Promise<void>;
48
+
49
+ // Config from ContainerBase subclass
50
+ defaultPort = 8080;
51
+ requiredPorts: number[] = [];
52
+ sleepAfter?: string | number;
53
+ envVars: Record<string, string> = {};
54
+ entrypoint?: string[];
55
+ enableInternet = true;
56
+ pingEndpoint = "/";
57
+
58
+ // Lifecycle callbacks
59
+ onStart?: () => void | Promise<void>;
60
+ onStop?: () => void | Promise<void>;
61
+ onError?: (error: Error) => void | Promise<void>;
62
+ onActivityExpired?: () => void | Promise<void>;
63
+
64
+ constructor(className: string, idHex: string, image: string, docker: DockerManager) {
65
+ this._containerName = `bunflare-${className}-${idHex.slice(0, 12)}`;
66
+ this._image = image;
67
+ this._docker = docker;
68
+ }
69
+
70
+ get status(): ContainerStatus {
71
+ return this._status;
72
+ }
73
+
74
+ get containerName(): string {
75
+ return this._containerName;
76
+ }
77
+
78
+ getState(): ContainerState {
79
+ return {
80
+ status: this._status,
81
+ lastChange: this._lastChange,
82
+ ...(this._exitCode !== undefined ? { exitCode: this._exitCode } : {}),
83
+ };
84
+ }
85
+
86
+ private _transition(status: ContainerStatus, exitCode?: number) {
87
+ this._status = status;
88
+ this._lastChange = Date.now();
89
+ if (exitCode !== undefined) this._exitCode = exitCode;
90
+ }
91
+
92
+ /**
93
+ * Start the Docker container (non-blocking kickoff).
94
+ */
95
+ async start(options?: { envVars?: Record<string, string> }): Promise<void> {
96
+ if (this._status === "running" || this._status === "healthy") return;
97
+
98
+ this._transition("running");
99
+
100
+ // Check if a container with this name already exists (e.g. after process crash)
101
+ const existing = await this._docker.inspect(this._containerName);
102
+ if (existing) {
103
+ if (existing.state === "running") {
104
+ // Recover port mappings from the running container
105
+ this._hostPorts.clear();
106
+ this._recoverPortMappings(existing.ports);
107
+ await this.onStart?.();
108
+ this._startHealthCheck();
109
+ this._startMonitor();
110
+ this.renewActivityTimeout();
111
+ return;
112
+ }
113
+ // Container exists but isn't running — remove it before creating a new one
114
+ await this._docker.remove(this._containerName);
115
+ }
116
+
117
+ // Determine ports to expose
118
+ const ports = new Set<number>([this.defaultPort, ...this.requiredPorts]);
119
+ this._hostPorts.clear();
120
+
121
+ for (const port of ports) {
122
+ const hostPort = await DockerManager.allocatePort();
123
+ this._hostPorts.set(port, hostPort);
124
+ }
125
+
126
+ // Merge env vars
127
+ const mergedEnv = { ...this.envVars, ...options?.envVars };
128
+
129
+ // Build image if needed (lazy, mtime-cached)
130
+ const sanitized = this._image.toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^[^a-z0-9]+/, "");
131
+ const tag = `bunflare-${sanitized || "image"}`;
132
+ // If image looks like a Dockerfile path, build it
133
+ const isDockerfile = /Dockerfile/i.test(this._image) || this._image.startsWith("./") || this._image.startsWith("/");
134
+ if (isDockerfile) {
135
+ await this._docker.buildImage(this._image, tag);
136
+ }
137
+
138
+ const runOpts: DockerRunOptions = {
139
+ image: isDockerfile ? tag : this._image,
140
+ name: this._containerName,
141
+ ports: this._hostPorts,
142
+ envVars: mergedEnv,
143
+ entrypoint: this.entrypoint,
144
+ enableInternet: this.enableInternet,
145
+ };
146
+
147
+ try {
148
+ await this._docker.run(runOpts);
149
+ } catch (err) {
150
+ this._transition("stopped");
151
+ await this.onError?.(err instanceof Error ? err : new Error(String(err)));
152
+ throw err;
153
+ }
154
+
155
+ await this.onStart?.();
156
+
157
+ // Start health check polling
158
+ this._startHealthCheck();
159
+
160
+ // Start docker monitor
161
+ this._startMonitor();
162
+
163
+ // Start activity timeout
164
+ this.renewActivityTimeout();
165
+ }
166
+
167
+ /**
168
+ * Stop the container gracefully.
169
+ */
170
+ async stop(signal?: number): Promise<void> {
171
+ if (this._status === "stopped" || this._status === "stopped_with_code" || this._status === "stopping") return;
172
+
173
+ this._transition("stopping");
174
+ this._stopTimers();
175
+
176
+ try {
177
+ if (signal !== undefined) {
178
+ await this._docker.signal(this._containerName, signal);
179
+ // Give it a few seconds to exit after signal
180
+ await new Promise(r => setTimeout(r, 3000));
181
+ }
182
+ await this._docker.stop(this._containerName, 10);
183
+ await this._docker.remove(this._containerName);
184
+ } catch {
185
+ // Force remove on error
186
+ await this._docker.remove(this._containerName).catch(() => {});
187
+ }
188
+
189
+ this._transition("stopped");
190
+ await this.onStop?.();
191
+ this._monitorResolve?.();
192
+ }
193
+
194
+ /**
195
+ * Destroy the container immediately.
196
+ */
197
+ async destroy(error?: Error): Promise<void> {
198
+ this._stopTimers();
199
+ await this._docker.remove(this._containerName).catch(() => {});
200
+ this._transition("stopped");
201
+ if (error) {
202
+ await this.onError?.(error);
203
+ }
204
+ this._monitorResolve?.();
205
+ }
206
+
207
+ /**
208
+ * Send a signal to the container.
209
+ */
210
+ async signal(sig: number): Promise<void> {
211
+ if (this._status !== "running" && this._status !== "healthy") return;
212
+ await this._docker.signal(this._containerName, sig);
213
+ }
214
+
215
+ /**
216
+ * Forward an HTTP request to the container.
217
+ */
218
+ async fetch(input: RequestInfo | URL, init?: RequestInit, port?: number): Promise<Response> {
219
+ const targetPort = port ?? this.defaultPort;
220
+ const hostPort = this._hostPorts.get(targetPort);
221
+ if (!hostPort) {
222
+ throw new Error(`No port mapping for container port ${targetPort}`);
223
+ }
224
+
225
+ const request = input instanceof Request ? input : new Request(input instanceof URL ? input.href : input, init);
226
+ const url = new URL(request.url);
227
+ url.hostname = "localhost";
228
+ url.port = String(hostPort);
229
+ url.protocol = "http:";
230
+
231
+ const proxiedRequest = new Request(url.toString(), {
232
+ method: request.method,
233
+ headers: request.headers,
234
+ body: request.body,
235
+ redirect: "manual",
236
+ });
237
+
238
+ this.renewActivityTimeout();
239
+ return globalThis.fetch(proxiedRequest);
240
+ }
241
+
242
+ /**
243
+ * Get the host port mapped to a container port.
244
+ */
245
+ getHostPort(containerPort: number): number | undefined {
246
+ return this._hostPorts.get(containerPort);
247
+ }
248
+
249
+ /**
250
+ * Renew the activity timeout.
251
+ */
252
+ renewActivityTimeout(): void {
253
+ if (this._activityTimer) clearTimeout(this._activityTimer);
254
+ const timeoutMs = this._parseSleepAfter();
255
+ if (timeoutMs === null) return;
256
+
257
+ this._activityTimer = setTimeout(async () => {
258
+ if (this._status !== "running" && this._status !== "healthy") return;
259
+ if (this.onActivityExpired) {
260
+ await this.onActivityExpired();
261
+ } else {
262
+ // Default: SIGTERM
263
+ await this.stop(15);
264
+ }
265
+ }, timeoutMs);
266
+ }
267
+
268
+ /**
269
+ * Returns a promise that resolves when the container exits normally,
270
+ * or rejects on error.
271
+ */
272
+ monitor(): Promise<void> {
273
+ if (!this._monitorPromise) {
274
+ this._monitorPromise = new Promise<void>((resolve, reject) => {
275
+ this._monitorResolve = resolve;
276
+ this._monitorReject = reject;
277
+ });
278
+ }
279
+ return this._monitorPromise;
280
+ }
281
+
282
+ /**
283
+ * Cleanup: stop timers, remove container.
284
+ */
285
+ async cleanup(): Promise<void> {
286
+ this._stopTimers();
287
+ await this._docker.remove(this._containerName).catch(() => {});
288
+ this._transition("stopped");
289
+ this._monitorResolve?.();
290
+ }
291
+
292
+ // ─── Private ────────────────────────────────────────────────────────────
293
+
294
+ /**
295
+ * Recover host port mappings from docker inspect Ports object.
296
+ * Format: { "8080/tcp": [{ "HostIp": "...", "HostPort": "49152" }], ... }
297
+ */
298
+ private _recoverPortMappings(ports: Record<string, unknown>) {
299
+ for (const [key, bindings] of Object.entries(ports)) {
300
+ if (!Array.isArray(bindings) || bindings.length === 0) continue;
301
+ const containerPort = parseInt(key, 10);
302
+ if (isNaN(containerPort)) continue;
303
+ const hostPort = parseInt(bindings[0].HostPort, 10);
304
+ if (isNaN(hostPort)) continue;
305
+ this._hostPorts.set(containerPort, hostPort);
306
+ }
307
+ }
308
+
309
+ private _startHealthCheck() {
310
+ if (this._healthCheckTimer) clearInterval(this._healthCheckTimer);
311
+ let consecutiveOk = 0;
312
+
313
+ this._healthCheckTimer = setInterval(async () => {
314
+ if (this._status !== "running") {
315
+ if (this._healthCheckTimer) clearInterval(this._healthCheckTimer);
316
+ return;
317
+ }
318
+
319
+ const hostPort = this._hostPorts.get(this.defaultPort);
320
+ if (!hostPort) return;
321
+
322
+ try {
323
+ const resp = await fetch(`http://localhost:${hostPort}${this.pingEndpoint}`, {
324
+ signal: AbortSignal.timeout(2000),
325
+ });
326
+ if (resp.status >= 200 && resp.status < 300) {
327
+ consecutiveOk++;
328
+ if (consecutiveOk >= 1) {
329
+ this._transition("healthy");
330
+ if (this._healthCheckTimer) clearInterval(this._healthCheckTimer);
331
+ }
332
+ } else {
333
+ consecutiveOk = 0;
334
+ }
335
+ } catch {
336
+ consecutiveOk = 0;
337
+ }
338
+ }, 500);
339
+ }
340
+
341
+ private _startMonitor() {
342
+ if (this._monitorTimer) clearInterval(this._monitorTimer);
343
+
344
+ this._monitorTimer = setInterval(async () => {
345
+ if (this._status === "stopped" || this._status === "stopped_with_code" || this._status === "stopping") {
346
+ if (this._monitorTimer) clearInterval(this._monitorTimer);
347
+ return;
348
+ }
349
+
350
+ const info = await this._docker.inspect(this._containerName);
351
+ if (!info) return;
352
+
353
+ if (info.state === "exited" || info.state === "dead") {
354
+ const exitCode = info.exitCode ?? 1;
355
+ this._stopTimers();
356
+ this._transition("stopped_with_code", exitCode);
357
+ await this._docker.remove(this._containerName).catch(() => {});
358
+ await this.onStop?.();
359
+ this._monitorResolve?.();
360
+ }
361
+ }, 2000);
362
+ }
363
+
364
+ private _stopTimers() {
365
+ if (this._healthCheckTimer) {
366
+ clearInterval(this._healthCheckTimer);
367
+ this._healthCheckTimer = undefined;
368
+ }
369
+ if (this._activityTimer) {
370
+ clearTimeout(this._activityTimer);
371
+ this._activityTimer = undefined;
372
+ }
373
+ if (this._monitorTimer) {
374
+ clearInterval(this._monitorTimer);
375
+ this._monitorTimer = undefined;
376
+ }
377
+ }
378
+
379
+ private _parseSleepAfter(): number | null {
380
+ if (this.sleepAfter === undefined || this.sleepAfter === null) return null;
381
+ if (typeof this.sleepAfter === "number") return this.sleepAfter * 1000;
382
+ const str = this.sleepAfter;
383
+ const match = str.match(/^(\d+)(s|m|h)$/);
384
+ if (!match) return null;
385
+ const value = parseInt(match[1]!, 10);
386
+ switch (match[2]) {
387
+ case "s": return value * 1000;
388
+ case "m": return value * 60_000;
389
+ case "h": return value * 3_600_000;
390
+ default: return null;
391
+ }
392
+ }
393
+ }
394
+
395
+ // ─── ContainerContext ───────────────────────────────────────────────────────
396
+ // Implements the `ctx.container` low-level API.
397
+
398
+ export class ContainerContext {
399
+ private _runtime: ContainerRuntime;
400
+
401
+ constructor(runtime: ContainerRuntime) {
402
+ this._runtime = runtime;
403
+ }
404
+
405
+ get running(): boolean {
406
+ const s = this._runtime.status;
407
+ return s === "running" || s === "healthy";
408
+ }
409
+
410
+ start(options?: { envVars?: Record<string, string> }): void {
411
+ // Non-blocking kickoff
412
+ this._runtime.start(options).catch(err => {
413
+ console.error(`[bunflare] Container start error:`, err);
414
+ });
415
+ }
416
+
417
+ async destroy(error?: Error): Promise<void> {
418
+ await this._runtime.destroy(error);
419
+ }
420
+
421
+ async signal(sig: number): Promise<void> {
422
+ await this._runtime.signal(sig);
423
+ }
424
+
425
+ getTcpPort(port: number): TcpPort {
426
+ const runtime = this._runtime;
427
+ return {
428
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
429
+ return runtime.fetch(input, init, port);
430
+ },
431
+ connect(): never {
432
+ throw new Error("TCP connect() is not supported in bunflare dev mode. Use fetch() for HTTP forwarding.");
433
+ },
434
+ };
435
+ }
436
+
437
+ monitor(): Promise<void> {
438
+ return this._runtime.monitor();
439
+ }
440
+ }
441
+
442
+ // ─── ContainerBase ──────────────────────────────────────────────────────────
443
+ // User subclasses this. Extends DurableObjectBase with container lifecycle.
444
+
445
+ export class ContainerBase extends DurableObjectBase {
446
+ // Configuration properties (override in subclass)
447
+ defaultPort: number | undefined = 8080;
448
+ requiredPorts: number[] = [];
449
+ sleepAfter: string | number = "10m";
450
+ envVars: Record<string, string> = {};
451
+ entrypoint?: string[];
452
+ enableInternet = true;
453
+ pingEndpoint = "/";
454
+
455
+ /** @internal Container runtime, set during instance creation */
456
+ _containerRuntime?: ContainerRuntime;
457
+
458
+ // ─── Lifecycle hooks (override in subclass) ─────────────────────────
459
+
460
+ onStart(): void | Promise<void> {}
461
+ onStop(_params?: unknown): void | Promise<void> {}
462
+ onError(_error: unknown): void | Promise<void> {}
463
+
464
+ onActivityExpired(): void | Promise<void> {
465
+ // Default: send SIGTERM to stop container
466
+ return this._containerRuntime?.stop(15);
467
+ }
468
+
469
+ // ─── Methods ────────────────────────────────────────────────────────
470
+
471
+ /**
472
+ * Fetch handler: auto-start if stopped, renew timeout, forward to container.
473
+ * Reads `cf-container-target-port` header (set by switchPort()) to determine port.
474
+ */
475
+ async fetch(request: Request): Promise<Response> {
476
+ if (!this._containerRuntime) {
477
+ return new Response("Container runtime not initialized", { status: 500 });
478
+ }
479
+
480
+ const status = this._containerRuntime.status;
481
+ if (status === "stopped" || status === "stopped_with_code") {
482
+ await this.startAndWaitForPorts();
483
+ }
484
+
485
+ // Check for port override via switchPort() header
486
+ let port: number | undefined;
487
+ const portHeader = request.headers.get("cf-container-target-port");
488
+ if (portHeader) {
489
+ const parsed = parseInt(portHeader, 10);
490
+ if (!isNaN(parsed)) port = parsed;
491
+ }
492
+
493
+ return this._containerRuntime.fetch(request, undefined, port);
494
+ }
495
+
496
+ /**
497
+ * Forward an HTTP request to the container.
498
+ * Supports multiple call signatures matching the real Container API:
499
+ * - containerFetch(request: Request, port?: number)
500
+ * - containerFetch(url: string | URL, init?: RequestInit, port?: number)
501
+ */
502
+ async containerFetch(
503
+ requestOrUrl: Request | string | URL,
504
+ portOrInit?: number | RequestInit,
505
+ portParam?: number,
506
+ ): Promise<Response> {
507
+ if (!this._containerRuntime) {
508
+ return new Response("Container runtime not initialized", { status: 500 });
509
+ }
510
+
511
+ const { input, init, port } = this._parseContainerFetchArgs(requestOrUrl, portOrInit, portParam);
512
+ return this._containerRuntime.fetch(input, init, port);
513
+ }
514
+
515
+ /** Parse flexible containerFetch arguments */
516
+ private _parseContainerFetchArgs(
517
+ requestOrUrl: Request | string | URL,
518
+ portOrInit?: number | RequestInit,
519
+ portParam?: number,
520
+ ): { input: RequestInfo | URL; init?: RequestInit; port?: number } {
521
+ if (requestOrUrl instanceof Request) {
522
+ // containerFetch(request, port?)
523
+ const port = typeof portOrInit === "number" ? portOrInit : portParam;
524
+ return { input: requestOrUrl, port };
525
+ }
526
+ // containerFetch(url, init?, port?)
527
+ const init = typeof portOrInit === "object" ? portOrInit : undefined;
528
+ const port = typeof portOrInit === "number" ? portOrInit : portParam;
529
+ return { input: requestOrUrl, init, port };
530
+ }
531
+
532
+ /**
533
+ * Start the container and wait until it's healthy or running.
534
+ * Supports flexible call signatures matching the real Container API:
535
+ * - startAndWaitForPorts()
536
+ * - startAndWaitForPorts(ports?, cancellationOptions?, startOptions?)
537
+ * - startAndWaitForPorts({ports?, abort?, ...})
538
+ */
539
+ async startAndWaitForPorts(
540
+ portsOrArgs?: number | number[] | { ports?: number | number[]; abort?: AbortSignal; envVars?: Record<string, string> },
541
+ cancellationOptions?: { abort?: AbortSignal },
542
+ startOptions?: { envVars?: Record<string, string> },
543
+ ): Promise<void> {
544
+ if (!this._containerRuntime) throw new Error("Container runtime not initialized");
545
+
546
+ // Parse args — extract abort signal and start options
547
+ let abort: AbortSignal | undefined;
548
+ let envVars: Record<string, string> | undefined;
549
+
550
+ if (typeof portsOrArgs === "object" && portsOrArgs !== null && !Array.isArray(portsOrArgs) && !(typeof portsOrArgs === "number")) {
551
+ // Object form: startAndWaitForPorts({ports, abort, envVars})
552
+ abort = portsOrArgs.abort;
553
+ envVars = portsOrArgs.envVars;
554
+ } else {
555
+ abort = cancellationOptions?.abort;
556
+ envVars = startOptions?.envVars;
557
+ }
558
+
559
+ await this._containerRuntime.start(envVars ? { envVars } : undefined);
560
+
561
+ // Wait for healthy (with timeout)
562
+ const timeout = 60_000;
563
+ const started = Date.now();
564
+ while (this._containerRuntime.status === "running" && Date.now() - started < timeout) {
565
+ if (abort?.aborted) throw new Error("Container startup aborted");
566
+ await new Promise(r => setTimeout(r, 200));
567
+ }
568
+
569
+ if (this._containerRuntime.status !== "healthy" && this._containerRuntime.status !== "running") {
570
+ throw new Error(`Container failed to start (status: ${this._containerRuntime.status})`);
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Start the container.
576
+ */
577
+ async start(_startOptions?: unknown, _waitOptions?: unknown): Promise<void> {
578
+ await this._containerRuntime?.start().catch(err => {
579
+ console.error("[bunflare] Container start error:", err);
580
+ });
581
+ }
582
+
583
+ /**
584
+ * Stop the container.
585
+ */
586
+ async stop(signal?: number | string): Promise<void> {
587
+ const sig = typeof signal === "string" ? undefined : signal;
588
+ await this._containerRuntime?.stop(sig);
589
+ }
590
+
591
+ /**
592
+ * Destroy the container.
593
+ */
594
+ async destroy(): Promise<void> {
595
+ await this._containerRuntime?.destroy();
596
+ }
597
+
598
+ /**
599
+ * Get current container state.
600
+ */
601
+ async getState(): Promise<ContainerState> {
602
+ if (!this._containerRuntime) {
603
+ return { status: "stopped", lastChange: Date.now() };
604
+ }
605
+ return this._containerRuntime.getState();
606
+ }
607
+
608
+ /**
609
+ * Renew the activity timeout.
610
+ */
611
+ renewActivityTimeout(): void {
612
+ this._containerRuntime?.renewActivityTimeout();
613
+ }
614
+
615
+ /**
616
+ * Schedule a deferred callback via DO alarm.
617
+ */
618
+ async schedule(when: number | Date, _callback: string, _payload?: unknown): Promise<void> {
619
+ const time = when instanceof Date ? when.getTime() : when;
620
+ await this.ctx.storage.setAlarm(time);
621
+ }
622
+
623
+ /** @internal Wire the container runtime with config from this instance */
624
+ _wireRuntime(runtime: ContainerRuntime): void {
625
+ this._containerRuntime = runtime;
626
+
627
+ // Copy config from instance to runtime
628
+ runtime.defaultPort = this.defaultPort ?? 8080;
629
+ runtime.requiredPorts = this.requiredPorts;
630
+ runtime.sleepAfter = this.sleepAfter;
631
+ runtime.envVars = this.envVars;
632
+ runtime.entrypoint = this.entrypoint;
633
+ runtime.enableInternet = this.enableInternet;
634
+ runtime.pingEndpoint = this.pingEndpoint;
635
+
636
+ // Wire lifecycle callbacks
637
+ runtime.onStart = () => this.onStart();
638
+ runtime.onStop = () => this.onStop();
639
+ runtime.onError = (err) => this.onError(err);
640
+ runtime.onActivityExpired = () => this.onActivityExpired();
641
+ }
642
+ }
643
+
644
+ // ─── Utility Functions ──────────────────────────────────────────────────────
645
+
646
+ /**
647
+ * Get a Container DO stub by name (defaults to "singleton").
648
+ */
649
+ export function getContainer(binding: any, name?: string): unknown {
650
+ const id = binding.idFromName(name ?? "singleton");
651
+ return binding.get(id);
652
+ }
653
+
654
+ /**
655
+ * Get a random Container DO stub from a pool of instances.
656
+ */
657
+ export function getRandom(binding: any, instances?: number): unknown {
658
+ const count = instances ?? 1;
659
+ const index = Math.floor(Math.random() * count);
660
+ const id = binding.idFromName(`random-${index}`);
661
+ return binding.get(id);
662
+ }