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.
- package/README.md +15 -0
- package/package.json +51 -0
- package/runtime/bindings/ai.ts +132 -0
- package/runtime/bindings/analytics-engine.ts +96 -0
- package/runtime/bindings/browser.ts +64 -0
- package/runtime/bindings/cache.ts +179 -0
- package/runtime/bindings/cf-streams.ts +56 -0
- package/runtime/bindings/container-docker.ts +225 -0
- package/runtime/bindings/container.ts +662 -0
- package/runtime/bindings/crypto-extras.ts +89 -0
- package/runtime/bindings/d1.ts +315 -0
- package/runtime/bindings/do-executor-inprocess.ts +140 -0
- package/runtime/bindings/do-executor-worker.ts +368 -0
- package/runtime/bindings/do-executor.ts +45 -0
- package/runtime/bindings/do-websocket-bridge.ts +70 -0
- package/runtime/bindings/do-worker-entry.ts +220 -0
- package/runtime/bindings/do-worker-env.ts +74 -0
- package/runtime/bindings/durable-object.ts +992 -0
- package/runtime/bindings/email.ts +180 -0
- package/runtime/bindings/html-rewriter.ts +84 -0
- package/runtime/bindings/hyperdrive.ts +130 -0
- package/runtime/bindings/images.ts +381 -0
- package/runtime/bindings/kv.ts +359 -0
- package/runtime/bindings/queue.ts +507 -0
- package/runtime/bindings/r2.ts +759 -0
- package/runtime/bindings/rpc-stub.ts +267 -0
- package/runtime/bindings/scheduled.ts +172 -0
- package/runtime/bindings/service-binding.ts +217 -0
- package/runtime/bindings/static-assets.ts +481 -0
- package/runtime/bindings/websocket-pair.ts +182 -0
- package/runtime/bindings/workflow.ts +858 -0
- package/runtime/bunflare-config.ts +56 -0
- package/runtime/cli/cache.ts +39 -0
- package/runtime/cli/context.ts +105 -0
- package/runtime/cli/d1.ts +163 -0
- package/runtime/cli/dev.ts +392 -0
- package/runtime/cli/kv.ts +84 -0
- package/runtime/cli/queues.ts +109 -0
- package/runtime/cli/r2.ts +140 -0
- package/runtime/cli/traces.ts +251 -0
- package/runtime/cli.ts +102 -0
- package/runtime/config.ts +148 -0
- package/runtime/d1-migrate.ts +37 -0
- package/runtime/dashboard/api.ts +174 -0
- package/runtime/dashboard/app.tsx +220 -0
- package/runtime/dashboard/components/breadcrumb.tsx +16 -0
- package/runtime/dashboard/components/buttons.tsx +13 -0
- package/runtime/dashboard/components/code-block.tsx +5 -0
- package/runtime/dashboard/components/detail-field.tsx +8 -0
- package/runtime/dashboard/components/empty-state.tsx +8 -0
- package/runtime/dashboard/components/filter-input.tsx +11 -0
- package/runtime/dashboard/components/index.ts +16 -0
- package/runtime/dashboard/components/key-value-table.tsx +23 -0
- package/runtime/dashboard/components/modal.tsx +23 -0
- package/runtime/dashboard/components/page-header.tsx +11 -0
- package/runtime/dashboard/components/pill-button.tsx +14 -0
- package/runtime/dashboard/components/refresh-button.tsx +7 -0
- package/runtime/dashboard/components/service-info.tsx +45 -0
- package/runtime/dashboard/components/status-badge.tsx +7 -0
- package/runtime/dashboard/components/table-link.tsx +5 -0
- package/runtime/dashboard/components/table.tsx +26 -0
- package/runtime/dashboard/components.tsx +19 -0
- package/runtime/dashboard/index.html +23 -0
- package/runtime/dashboard/lib.ts +45 -0
- package/runtime/dashboard/rpc/client.ts +20 -0
- package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
- package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
- package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
- package/runtime/dashboard/rpc/handlers/config.ts +137 -0
- package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
- package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
- package/runtime/dashboard/rpc/handlers/do.ts +117 -0
- package/runtime/dashboard/rpc/handlers/email.ts +82 -0
- package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
- package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
- package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
- package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
- package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
- package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
- package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
- package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
- package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
- package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
- package/runtime/dashboard/rpc/hooks.ts +132 -0
- package/runtime/dashboard/rpc/server.ts +70 -0
- package/runtime/dashboard/rpc/types.ts +396 -0
- package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
- package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
- package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
- package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
- package/runtime/dashboard/sql-browser/hooks.ts +137 -0
- package/runtime/dashboard/sql-browser/index.ts +4 -0
- package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
- package/runtime/dashboard/sql-browser/modals.tsx +116 -0
- package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
- package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
- package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
- package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
- package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
- package/runtime/dashboard/sql-browser/types.ts +61 -0
- package/runtime/dashboard/sql-browser/utils.ts +167 -0
- package/runtime/dashboard/style.css +177 -0
- package/runtime/dashboard/views/ai.tsx +152 -0
- package/runtime/dashboard/views/analytics-engine.tsx +169 -0
- package/runtime/dashboard/views/cache.tsx +93 -0
- package/runtime/dashboard/views/containers.tsx +197 -0
- package/runtime/dashboard/views/d1.tsx +81 -0
- package/runtime/dashboard/views/do.tsx +168 -0
- package/runtime/dashboard/views/email.tsx +235 -0
- package/runtime/dashboard/views/errors.tsx +558 -0
- package/runtime/dashboard/views/home.tsx +287 -0
- package/runtime/dashboard/views/kv.tsx +273 -0
- package/runtime/dashboard/views/queue.tsx +193 -0
- package/runtime/dashboard/views/r2.tsx +202 -0
- package/runtime/dashboard/views/scheduled.tsx +89 -0
- package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
- package/runtime/dashboard/views/traces.tsx +768 -0
- package/runtime/dashboard/views/workers.tsx +55 -0
- package/runtime/dashboard/views/workflows.tsx +473 -0
- package/runtime/db.ts +258 -0
- package/runtime/env.ts +362 -0
- package/runtime/error-page/app.tsx +394 -0
- package/runtime/error-page/build.ts +269 -0
- package/runtime/error-page/index.html +16 -0
- package/runtime/error-page/style.css +31 -0
- package/runtime/execution-context.ts +18 -0
- package/runtime/file-watcher.ts +57 -0
- package/runtime/generation-manager.ts +230 -0
- package/runtime/generation.ts +411 -0
- package/runtime/plugin.ts +292 -0
- package/runtime/request-cf.ts +28 -0
- package/runtime/rpc-validate.ts +154 -0
- package/runtime/tracing/context.ts +40 -0
- package/runtime/tracing/db.ts +73 -0
- package/runtime/tracing/frames.ts +75 -0
- package/runtime/tracing/instrument.ts +186 -0
- package/runtime/tracing/span.ts +138 -0
- package/runtime/tracing/store.ts +499 -0
- package/runtime/tracing/types.ts +47 -0
- package/runtime/vite-plugin/config-plugin.ts +68 -0
- package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
- package/runtime/vite-plugin/dist/index.mjs +52333 -0
- package/runtime/vite-plugin/globals-plugin.ts +94 -0
- package/runtime/vite-plugin/index.ts +43 -0
- package/runtime/vite-plugin/modules-plugin.ts +88 -0
- package/runtime/vite-plugin/react-router-plugin.ts +95 -0
- package/runtime/worker-registry.ts +52 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkerExecutor — runs each DO instance in a separate Bun Worker thread.
|
|
3
|
+
*
|
|
4
|
+
* Main thread side: spawns a Worker on first command, maintains a serial
|
|
5
|
+
* command queue, and bridges WebSocket events between main thread and worker.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolve, dirname } from "node:path";
|
|
9
|
+
import type { DOExecutor, DOExecutorFactory, ExecutorConfig } from "./do-executor";
|
|
10
|
+
import type { WsBridgeOutbound } from "./do-websocket-bridge";
|
|
11
|
+
|
|
12
|
+
// --- Message protocol ---
|
|
13
|
+
|
|
14
|
+
/** Commands sent from main thread to worker */
|
|
15
|
+
export type DOCommand =
|
|
16
|
+
| { type: "fetch"; url: string; method: string; headers: [string, string][]; body: ArrayBuffer | null }
|
|
17
|
+
| { type: "rpc-call"; method: string; args: unknown[] }
|
|
18
|
+
| { type: "rpc-get"; prop: string }
|
|
19
|
+
| { type: "alarm"; retryCount: number }
|
|
20
|
+
| { type: "ws-create"; wsId: string };
|
|
21
|
+
|
|
22
|
+
/** Results returned from worker to main thread */
|
|
23
|
+
export type DOResult =
|
|
24
|
+
| { type: "fetch"; status: number; statusText: string; headers: [string, string][]; body: ArrayBuffer | null }
|
|
25
|
+
| { type: "rpc-call"; value: unknown }
|
|
26
|
+
| { type: "rpc-get"; value: unknown }
|
|
27
|
+
| { type: "alarm" }
|
|
28
|
+
| { type: "ws-created"; wsId: string }
|
|
29
|
+
| { type: "error"; message: string; stack?: string; name?: string };
|
|
30
|
+
|
|
31
|
+
/** Messages from main thread → worker */
|
|
32
|
+
export type DOWorkerMessage =
|
|
33
|
+
| { type: "command"; id: number; command: DOCommand }
|
|
34
|
+
| { type: "ws-message"; wsId: string; data: string | ArrayBuffer }
|
|
35
|
+
| { type: "ws-close"; wsId: string; code: number; reason: string; wasClean: boolean }
|
|
36
|
+
| { type: "ws-error"; wsId: string };
|
|
37
|
+
|
|
38
|
+
/** Messages from worker → main thread */
|
|
39
|
+
export type DOMainMessage =
|
|
40
|
+
| { type: "need-init" }
|
|
41
|
+
| { type: "ready" }
|
|
42
|
+
| { type: "result"; id: number; result: DOResult }
|
|
43
|
+
| { type: "alarm-set"; time: number | null }
|
|
44
|
+
| { type: "ws-bridge"; payload: WsBridgeOutbound };
|
|
45
|
+
|
|
46
|
+
// --- Pending command tracking ---
|
|
47
|
+
|
|
48
|
+
interface PendingCommand {
|
|
49
|
+
resolve: (result: DOResult) => void;
|
|
50
|
+
reject: (error: Error) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- WorkerExecutor ---
|
|
54
|
+
|
|
55
|
+
const WORKER_ENTRY_PATH = resolve(dirname(new URL(import.meta.url).pathname), "do-worker-entry.ts");
|
|
56
|
+
|
|
57
|
+
export class WorkerExecutor implements DOExecutor {
|
|
58
|
+
private _config: ExecutorConfig;
|
|
59
|
+
private _worker: Worker | null = null;
|
|
60
|
+
private _ready: Promise<void> | null = null;
|
|
61
|
+
private _readyResolve: (() => void) | null = null;
|
|
62
|
+
private _pending = new Map<number, PendingCommand>();
|
|
63
|
+
private _nextId = 1;
|
|
64
|
+
private _disposed = false;
|
|
65
|
+
private _inFlightCount = 0;
|
|
66
|
+
private _blocked = false;
|
|
67
|
+
private _wsCount = 0;
|
|
68
|
+
private _bridgedWebSockets = new Map<string, WebSocket>();
|
|
69
|
+
|
|
70
|
+
constructor(config: ExecutorConfig) {
|
|
71
|
+
this._config = config;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private _ensureWorker(): Worker {
|
|
75
|
+
if (this._disposed) throw new Error("WorkerExecutor has been disposed");
|
|
76
|
+
if (this._worker) return this._worker;
|
|
77
|
+
|
|
78
|
+
const config = this._config;
|
|
79
|
+
const worker = new Worker(WORKER_ENTRY_PATH);
|
|
80
|
+
|
|
81
|
+
this._ready = new Promise<void>((resolve) => {
|
|
82
|
+
this._readyResolve = resolve;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
worker.onmessage = (event: MessageEvent<DOMainMessage>) => {
|
|
86
|
+
const msg = event.data;
|
|
87
|
+
|
|
88
|
+
switch (msg.type) {
|
|
89
|
+
case "need-init":
|
|
90
|
+
// Worker is alive, send configuration
|
|
91
|
+
worker.postMessage({
|
|
92
|
+
type: "init",
|
|
93
|
+
config: {
|
|
94
|
+
modulePath: this._resolveModulePath(),
|
|
95
|
+
configPath: this._resolveConfigPath(),
|
|
96
|
+
dataDir: this._resolveDataDir(),
|
|
97
|
+
namespaceName: config.namespaceName,
|
|
98
|
+
idStr: config.id.toString(),
|
|
99
|
+
idName: config.id.name,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case "ready":
|
|
105
|
+
this._readyResolve?.();
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case "result": {
|
|
109
|
+
if (msg.id === -1 && msg.result.type === "error") {
|
|
110
|
+
// Worker init error — reject all pending and the ready promise
|
|
111
|
+
const error = new Error(msg.result.message);
|
|
112
|
+
if (msg.result.stack) error.stack = msg.result.stack;
|
|
113
|
+
for (const [, pending] of this._pending) {
|
|
114
|
+
pending.reject(error);
|
|
115
|
+
}
|
|
116
|
+
this._pending.clear();
|
|
117
|
+
// Resolve ready so _sendCommand doesn't hang forever
|
|
118
|
+
this._readyResolve?.();
|
|
119
|
+
this._disposed = true;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
const pending = this._pending.get(msg.id);
|
|
123
|
+
if (pending) {
|
|
124
|
+
this._pending.delete(msg.id);
|
|
125
|
+
pending.resolve(msg.result);
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case "alarm-set":
|
|
131
|
+
// Forward alarm set/delete to namespace via callback
|
|
132
|
+
config.onAlarmSet?.(msg.time);
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
case "ws-bridge":
|
|
136
|
+
this._handleWsBridge(msg.payload);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
worker.onerror = (event) => {
|
|
142
|
+
// Reject all pending commands
|
|
143
|
+
const error = new Error(`Worker error: ${event.message}`);
|
|
144
|
+
for (const [id, pending] of this._pending) {
|
|
145
|
+
pending.reject(error);
|
|
146
|
+
}
|
|
147
|
+
this._pending.clear();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
this._worker = worker;
|
|
151
|
+
return worker;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private _resolveModulePath(): string {
|
|
155
|
+
return (this._config as any)._modulePath ?? "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private _resolveConfigPath(): string {
|
|
159
|
+
return (this._config as any)._configPath ?? "";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private _resolveDataDir(): string {
|
|
163
|
+
return this._config.dataDir ?? "";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async _sendCommand(command: DOCommand): Promise<DOResult> {
|
|
167
|
+
const worker = this._ensureWorker();
|
|
168
|
+
await this._ready;
|
|
169
|
+
|
|
170
|
+
if (this._disposed) throw new Error("Worker terminated");
|
|
171
|
+
|
|
172
|
+
const id = this._nextId++;
|
|
173
|
+
this._inFlightCount++;
|
|
174
|
+
|
|
175
|
+
return new Promise<DOResult>((resolve, reject) => {
|
|
176
|
+
this._pending.set(id, {
|
|
177
|
+
resolve: (result) => {
|
|
178
|
+
this._inFlightCount--;
|
|
179
|
+
if (result.type === "error") {
|
|
180
|
+
const err = new Error(result.message);
|
|
181
|
+
err.name = result.name ?? "Error";
|
|
182
|
+
if (result.stack) err.stack = result.stack;
|
|
183
|
+
reject(err);
|
|
184
|
+
} else {
|
|
185
|
+
resolve(result);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
reject: (err) => {
|
|
189
|
+
this._inFlightCount--;
|
|
190
|
+
reject(err);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
worker.postMessage({ type: "command", id, command } satisfies DOWorkerMessage);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private _handleWsBridge(payload: WsBridgeOutbound): void {
|
|
198
|
+
switch (payload.type) {
|
|
199
|
+
case "ws-send": {
|
|
200
|
+
const ws = this._bridgedWebSockets.get(payload.wsId);
|
|
201
|
+
if (ws && ws.readyState === 1) {
|
|
202
|
+
ws.send(payload.data);
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case "ws-close": {
|
|
207
|
+
const ws = this._bridgedWebSockets.get(payload.wsId);
|
|
208
|
+
if (ws) {
|
|
209
|
+
ws.close(payload.code, payload.reason);
|
|
210
|
+
this._bridgedWebSockets.delete(payload.wsId);
|
|
211
|
+
this._wsCount--;
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case "ws-accept": {
|
|
216
|
+
// WebSocket was accepted by the DO — increment count
|
|
217
|
+
this._wsCount++;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Register a real WebSocket for bridging to the worker */
|
|
224
|
+
_bridgeWebSocket(wsId: string, ws: WebSocket): void {
|
|
225
|
+
this._bridgedWebSockets.set(wsId, ws);
|
|
226
|
+
|
|
227
|
+
// Forward events from real WS to worker
|
|
228
|
+
ws.addEventListener("message", (event: MessageEvent) => {
|
|
229
|
+
this._worker?.postMessage({
|
|
230
|
+
type: "ws-message",
|
|
231
|
+
wsId,
|
|
232
|
+
data: event.data,
|
|
233
|
+
} satisfies DOWorkerMessage);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
ws.addEventListener("close", (event: CloseEvent) => {
|
|
237
|
+
this._worker?.postMessage({
|
|
238
|
+
type: "ws-close",
|
|
239
|
+
wsId,
|
|
240
|
+
code: event.code,
|
|
241
|
+
reason: event.reason,
|
|
242
|
+
wasClean: event.wasClean,
|
|
243
|
+
} satisfies DOWorkerMessage);
|
|
244
|
+
this._bridgedWebSockets.delete(wsId);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
ws.addEventListener("error", () => {
|
|
248
|
+
this._worker?.postMessage({
|
|
249
|
+
type: "ws-error",
|
|
250
|
+
wsId,
|
|
251
|
+
} satisfies DOWorkerMessage);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- DOExecutor interface ---
|
|
256
|
+
|
|
257
|
+
async executeFetch(request: Request): Promise<Response> {
|
|
258
|
+
// Serialize Request
|
|
259
|
+
const headers: [string, string][] = [];
|
|
260
|
+
request.headers.forEach((v, k) => headers.push([k, v]));
|
|
261
|
+
const body = request.body ? await request.arrayBuffer() : null;
|
|
262
|
+
|
|
263
|
+
const result = await this._sendCommand({
|
|
264
|
+
type: "fetch",
|
|
265
|
+
url: request.url,
|
|
266
|
+
method: request.method,
|
|
267
|
+
headers,
|
|
268
|
+
body,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (result.type !== "fetch") throw new Error("Unexpected result type");
|
|
272
|
+
|
|
273
|
+
// Reconstruct Response
|
|
274
|
+
return new Response(result.body, {
|
|
275
|
+
status: result.status,
|
|
276
|
+
statusText: result.statusText,
|
|
277
|
+
headers: result.headers,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async executeRpc(method: string, args: unknown[]): Promise<unknown> {
|
|
282
|
+
const result = await this._sendCommand({
|
|
283
|
+
type: "rpc-call",
|
|
284
|
+
method,
|
|
285
|
+
args,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (result.type !== "rpc-call") throw new Error("Unexpected result type");
|
|
289
|
+
return result.value;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async executeRpcGet(prop: string): Promise<unknown> {
|
|
293
|
+
const result = await this._sendCommand({
|
|
294
|
+
type: "rpc-get",
|
|
295
|
+
prop,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (result.type !== "rpc-get") throw new Error("Unexpected result type");
|
|
299
|
+
// Functions can't cross the boundary — return a callable stub
|
|
300
|
+
if (result.value === "__function__") {
|
|
301
|
+
return (...args: unknown[]) => this.executeRpc(prop, args);
|
|
302
|
+
}
|
|
303
|
+
return result.value;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async executeAlarm(retryCount: number): Promise<void> {
|
|
307
|
+
const result = await this._sendCommand({
|
|
308
|
+
type: "alarm",
|
|
309
|
+
retryCount,
|
|
310
|
+
});
|
|
311
|
+
if (result.type === "error") {
|
|
312
|
+
const err = new Error(result.message);
|
|
313
|
+
if (result.stack) err.stack = result.stack;
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
isActive(): boolean {
|
|
319
|
+
return this._inFlightCount > 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
isBlocked(): boolean {
|
|
323
|
+
return this._blocked;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
activeWebSocketCount(): number {
|
|
327
|
+
return this._wsCount;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async dispose(): Promise<void> {
|
|
331
|
+
this._disposed = true;
|
|
332
|
+
if (this._worker) {
|
|
333
|
+
this._worker.terminate();
|
|
334
|
+
this._worker = null;
|
|
335
|
+
}
|
|
336
|
+
// Reject all pending commands
|
|
337
|
+
const error = new Error("Worker terminated");
|
|
338
|
+
for (const [, pending] of this._pending) {
|
|
339
|
+
pending.reject(error);
|
|
340
|
+
}
|
|
341
|
+
this._pending.clear();
|
|
342
|
+
this._bridgedWebSockets.clear();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- Factory ---
|
|
347
|
+
|
|
348
|
+
export class WorkerExecutorFactory implements DOExecutorFactory {
|
|
349
|
+
private _modulePath?: string;
|
|
350
|
+
private _configPath?: string;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Set the module and config paths for all executors created by this factory.
|
|
354
|
+
* Called by the generation manager after loading config.
|
|
355
|
+
*/
|
|
356
|
+
configure(modulePath: string, configPath: string): void {
|
|
357
|
+
this._modulePath = modulePath;
|
|
358
|
+
this._configPath = configPath;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
create(config: ExecutorConfig): DOExecutor {
|
|
362
|
+
// Attach paths to the config for the executor to use
|
|
363
|
+
const extendedConfig = config as any;
|
|
364
|
+
extendedConfig._modulePath = this._modulePath ?? "";
|
|
365
|
+
extendedConfig._configPath = this._configPath ?? "";
|
|
366
|
+
return new WorkerExecutor(extendedConfig);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { DurableObjectIdImpl, DurableObjectBase, DurableObjectLimits } from "./durable-object";
|
|
3
|
+
import type { ContainerConfig } from "./container";
|
|
4
|
+
|
|
5
|
+
export interface ExecutorConfig {
|
|
6
|
+
id: DurableObjectIdImpl;
|
|
7
|
+
db: Database;
|
|
8
|
+
namespaceName: string;
|
|
9
|
+
cls: new (ctx: any, env: unknown) => DurableObjectBase;
|
|
10
|
+
env: unknown;
|
|
11
|
+
dataDir?: string;
|
|
12
|
+
limits?: DurableObjectLimits;
|
|
13
|
+
containerConfig?: ContainerConfig;
|
|
14
|
+
onAlarmSet?: (time: number | null) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DOExecutor {
|
|
18
|
+
/** Execute a fetch call on the DO instance */
|
|
19
|
+
executeFetch(request: Request): Promise<Response>;
|
|
20
|
+
|
|
21
|
+
/** Execute an RPC method call */
|
|
22
|
+
executeRpc(method: string, args: unknown[]): Promise<unknown>;
|
|
23
|
+
|
|
24
|
+
/** Execute an RPC property get */
|
|
25
|
+
executeRpcGet(prop: string): Promise<unknown>;
|
|
26
|
+
|
|
27
|
+
/** Execute the alarm handler */
|
|
28
|
+
executeAlarm(retryCount: number): Promise<void>;
|
|
29
|
+
|
|
30
|
+
/** Whether the instance has in-flight requests */
|
|
31
|
+
isActive(): boolean;
|
|
32
|
+
|
|
33
|
+
/** Whether blockConcurrencyWhile is running */
|
|
34
|
+
isBlocked(): boolean;
|
|
35
|
+
|
|
36
|
+
/** Count of accepted WebSockets */
|
|
37
|
+
activeWebSocketCount(): number;
|
|
38
|
+
|
|
39
|
+
/** Kill the instance */
|
|
40
|
+
dispose(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DOExecutorFactory {
|
|
44
|
+
create(config: ExecutorConfig): DOExecutor;
|
|
45
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket bridge for isolated DO mode.
|
|
3
|
+
*
|
|
4
|
+
* Real WebSocket lives in the main thread (Bun.serve). The worker thread
|
|
5
|
+
* gets a BridgeWebSocket proxy that forwards send/close via postMessage.
|
|
6
|
+
* The main thread forwards incoming message/close/error events to the worker.
|
|
7
|
+
*
|
|
8
|
+
* Each bridged WebSocket is identified by a unique wsId.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// --- Messages from worker → main ---
|
|
12
|
+
export type WsBridgeOutbound =
|
|
13
|
+
| { type: "ws-send"; wsId: string; data: string | ArrayBuffer }
|
|
14
|
+
| { type: "ws-close"; wsId: string; code?: number; reason?: string }
|
|
15
|
+
| { type: "ws-accept"; wsId: string; tags: string[] };
|
|
16
|
+
|
|
17
|
+
// --- Messages from main → worker ---
|
|
18
|
+
export type WsBridgeInbound =
|
|
19
|
+
| { type: "ws-message"; wsId: string; data: string | ArrayBuffer }
|
|
20
|
+
| { type: "ws-close"; wsId: string; code: number; reason: string; wasClean: boolean }
|
|
21
|
+
| { type: "ws-error"; wsId: string };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A WebSocket proxy that lives in the worker thread.
|
|
25
|
+
* Implements enough of the WebSocket interface for DO's state.acceptWebSocket().
|
|
26
|
+
*/
|
|
27
|
+
export class BridgeWebSocket extends EventTarget {
|
|
28
|
+
readonly wsId: string;
|
|
29
|
+
readyState = 1; // OPEN
|
|
30
|
+
private _postMessage: (msg: WsBridgeOutbound) => void;
|
|
31
|
+
|
|
32
|
+
constructor(wsId: string, postMessage: (msg: WsBridgeOutbound) => void) {
|
|
33
|
+
super();
|
|
34
|
+
this.wsId = wsId;
|
|
35
|
+
this._postMessage = postMessage;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
send(data: string | ArrayBuffer): void {
|
|
39
|
+
if (this.readyState !== 1) return;
|
|
40
|
+
this._postMessage({ type: "ws-send", wsId: this.wsId, data });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
close(code?: number, reason?: string): void {
|
|
44
|
+
if (this.readyState >= 2) return;
|
|
45
|
+
this.readyState = 2; // CLOSING
|
|
46
|
+
this._postMessage({ type: "ws-close", wsId: this.wsId, code, reason });
|
|
47
|
+
this.readyState = 3; // CLOSED
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @internal Called by the worker entry when the main thread forwards a message */
|
|
51
|
+
_onMessage(data: string | ArrayBuffer): void {
|
|
52
|
+
this.dispatchEvent(new MessageEvent("message", { data }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @internal Called by the worker entry when the main thread forwards a close */
|
|
56
|
+
_onClose(code: number, reason: string, wasClean: boolean): void {
|
|
57
|
+
this.readyState = 3;
|
|
58
|
+
this.dispatchEvent(new CloseEvent("close", { code, reason, wasClean }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @internal Called by the worker entry when the main thread forwards an error */
|
|
62
|
+
_onError(): void {
|
|
63
|
+
this.dispatchEvent(new Event("error"));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Signal that this WS has been accepted by the DO (via state.acceptWebSocket) */
|
|
67
|
+
_signalAccepted(tags: string[]): void {
|
|
68
|
+
this._postMessage({ type: "ws-accept", wsId: this.wsId, tags });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker thread entry point for isolated DO mode.
|
|
3
|
+
*
|
|
4
|
+
* Spawned by WorkerExecutor. Receives configuration via the first
|
|
5
|
+
* postMessage from the main thread (handshake), then initializes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DOCommand, DOResult, DOWorkerMessage, DOMainMessage } from "./do-executor-worker";
|
|
9
|
+
|
|
10
|
+
declare var self: Worker;
|
|
11
|
+
|
|
12
|
+
interface WorkerConfig {
|
|
13
|
+
modulePath: string;
|
|
14
|
+
configPath: string;
|
|
15
|
+
dataDir: string;
|
|
16
|
+
namespaceName: string;
|
|
17
|
+
idStr: string;
|
|
18
|
+
idName?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Wait for init config from main thread, then set up the worker
|
|
22
|
+
self.onmessage = async (event: MessageEvent) => {
|
|
23
|
+
const msg = event.data;
|
|
24
|
+
if (msg.type !== "init") return;
|
|
25
|
+
|
|
26
|
+
const workerConfig: WorkerConfig = msg.config;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await initWorker(workerConfig);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
32
|
+
postMessage({
|
|
33
|
+
type: "result",
|
|
34
|
+
id: -1,
|
|
35
|
+
result: {
|
|
36
|
+
type: "error",
|
|
37
|
+
message: `Worker init failed: ${error.message}`,
|
|
38
|
+
stack: error.stack,
|
|
39
|
+
name: error.name,
|
|
40
|
+
},
|
|
41
|
+
} satisfies DOMainMessage);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Signal that we're alive and waiting for config
|
|
46
|
+
postMessage({ type: "need-init" });
|
|
47
|
+
|
|
48
|
+
async function initWorker(workerConfig: WorkerConfig) {
|
|
49
|
+
// Register Bun plugins for cloudflare:workers etc.
|
|
50
|
+
await import("../plugin");
|
|
51
|
+
|
|
52
|
+
const { loadConfig } = await import("../config");
|
|
53
|
+
const { buildWorkerEnv } = await import("./do-worker-env");
|
|
54
|
+
const { DurableObjectStateImpl, DurableObjectIdImpl } = await import("./durable-object");
|
|
55
|
+
const { BridgeWebSocket } = await import("./do-websocket-bridge");
|
|
56
|
+
|
|
57
|
+
const config = await loadConfig(workerConfig.configPath);
|
|
58
|
+
const { db, env, doNamespaces } = buildWorkerEnv(config, workerConfig.dataDir);
|
|
59
|
+
|
|
60
|
+
// Import user's worker module
|
|
61
|
+
const workerModule = await import(workerConfig.modulePath);
|
|
62
|
+
|
|
63
|
+
// Wire DO classes for nested DOs
|
|
64
|
+
for (const entry of doNamespaces) {
|
|
65
|
+
const cls = workerModule[entry.className];
|
|
66
|
+
if (cls) {
|
|
67
|
+
entry.namespace._setClass(cls as any, env);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create this DO's instance
|
|
72
|
+
const id = new DurableObjectIdImpl(workerConfig.idStr, workerConfig.idName);
|
|
73
|
+
const cls = workerModule[workerConfig.namespaceName];
|
|
74
|
+
if (!cls) {
|
|
75
|
+
throw new Error(`DO class "${workerConfig.namespaceName}" not exported from worker module`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const state = new DurableObjectStateImpl(id, db, workerConfig.namespaceName, workerConfig.dataDir);
|
|
79
|
+
const instance = new (cls as any)(state, env);
|
|
80
|
+
|
|
81
|
+
state._setInstanceResolver(() => instance);
|
|
82
|
+
|
|
83
|
+
const bridgedWebSockets = new Map<string, InstanceType<typeof BridgeWebSocket>>();
|
|
84
|
+
|
|
85
|
+
// Wire alarm callback
|
|
86
|
+
state.storage._setAlarmCallback((time: number | null) => {
|
|
87
|
+
postMessage({ type: "alarm-set", time } satisfies DOMainMessage);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// --- Command handler ---
|
|
91
|
+
|
|
92
|
+
async function handleCommand(cmd: DOCommand): Promise<DOResult> {
|
|
93
|
+
switch (cmd.type) {
|
|
94
|
+
case "fetch": {
|
|
95
|
+
const unlock = await state._lock();
|
|
96
|
+
try {
|
|
97
|
+
await state._waitForReady();
|
|
98
|
+
const fetchFn = (instance as any).fetch;
|
|
99
|
+
if (typeof fetchFn !== "function") {
|
|
100
|
+
throw new Error("Durable Object does not implement fetch()");
|
|
101
|
+
}
|
|
102
|
+
const request = new Request(cmd.url, {
|
|
103
|
+
method: cmd.method,
|
|
104
|
+
headers: cmd.headers,
|
|
105
|
+
body: cmd.body,
|
|
106
|
+
});
|
|
107
|
+
const response = await fetchFn.call(instance, request);
|
|
108
|
+
const resBody = response.body ? await response.arrayBuffer() : null;
|
|
109
|
+
const resHeaders: [string, string][] = [];
|
|
110
|
+
response.headers.forEach((v: string, k: string) => resHeaders.push([k, v]));
|
|
111
|
+
return {
|
|
112
|
+
type: "fetch",
|
|
113
|
+
status: response.status,
|
|
114
|
+
statusText: response.statusText,
|
|
115
|
+
headers: resHeaders,
|
|
116
|
+
body: resBody,
|
|
117
|
+
};
|
|
118
|
+
} finally {
|
|
119
|
+
unlock();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case "rpc-call": {
|
|
124
|
+
const unlock = await state._lock();
|
|
125
|
+
try {
|
|
126
|
+
await state._waitForReady();
|
|
127
|
+
const val = (instance as any)[cmd.method];
|
|
128
|
+
if (typeof val !== "function") {
|
|
129
|
+
throw new Error(`"${cmd.method}" is not a method on the Durable Object`);
|
|
130
|
+
}
|
|
131
|
+
const result = await val.call(instance, ...cmd.args);
|
|
132
|
+
return { type: "rpc-call", value: result };
|
|
133
|
+
} finally {
|
|
134
|
+
unlock();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case "rpc-get": {
|
|
139
|
+
const unlock = await state._lock();
|
|
140
|
+
try {
|
|
141
|
+
await state._waitForReady();
|
|
142
|
+
const val = (instance as any)[cmd.prop];
|
|
143
|
+
if (typeof val === "function") {
|
|
144
|
+
return { type: "rpc-get", value: "__function__" };
|
|
145
|
+
}
|
|
146
|
+
return { type: "rpc-get", value: val };
|
|
147
|
+
} finally {
|
|
148
|
+
unlock();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "alarm": {
|
|
153
|
+
const unlock = await state._lock();
|
|
154
|
+
try {
|
|
155
|
+
await state._waitForReady();
|
|
156
|
+
const alarmFn = (instance as any).alarm;
|
|
157
|
+
if (typeof alarmFn === "function") {
|
|
158
|
+
await alarmFn.call(instance, {
|
|
159
|
+
retryCount: cmd.retryCount,
|
|
160
|
+
isRetry: cmd.retryCount > 0,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return { type: "alarm" };
|
|
164
|
+
} finally {
|
|
165
|
+
unlock();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case "ws-create": {
|
|
170
|
+
const bridgeWs = new BridgeWebSocket(cmd.wsId, (msg: any) => {
|
|
171
|
+
postMessage({ type: "ws-bridge", payload: msg } satisfies DOMainMessage);
|
|
172
|
+
});
|
|
173
|
+
bridgedWebSockets.set(cmd.wsId, bridgeWs);
|
|
174
|
+
return { type: "ws-created", wsId: cmd.wsId };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
default:
|
|
178
|
+
throw new Error(`Unknown command type: ${(cmd as any).type}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Replace the init handler with the command handler
|
|
183
|
+
self.onmessage = async (event: MessageEvent<DOWorkerMessage>) => {
|
|
184
|
+
const msg = event.data;
|
|
185
|
+
|
|
186
|
+
if (msg.type === "command") {
|
|
187
|
+
try {
|
|
188
|
+
const result = await handleCommand(msg.command);
|
|
189
|
+
postMessage({ type: "result", id: msg.id, result } satisfies DOMainMessage);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
192
|
+
postMessage({
|
|
193
|
+
type: "result",
|
|
194
|
+
id: msg.id,
|
|
195
|
+
result: {
|
|
196
|
+
type: "error",
|
|
197
|
+
message: error.message,
|
|
198
|
+
stack: error.stack,
|
|
199
|
+
name: error.name,
|
|
200
|
+
},
|
|
201
|
+
} satisfies DOMainMessage);
|
|
202
|
+
}
|
|
203
|
+
} else if (msg.type === "ws-message") {
|
|
204
|
+
const ws = bridgedWebSockets.get(msg.wsId);
|
|
205
|
+
if (ws) ws._onMessage(msg.data);
|
|
206
|
+
} else if (msg.type === "ws-close") {
|
|
207
|
+
const ws = bridgedWebSockets.get(msg.wsId);
|
|
208
|
+
if (ws) {
|
|
209
|
+
ws._onClose(msg.code, msg.reason, msg.wasClean);
|
|
210
|
+
bridgedWebSockets.delete(msg.wsId);
|
|
211
|
+
}
|
|
212
|
+
} else if (msg.type === "ws-error") {
|
|
213
|
+
const ws = bridgedWebSockets.get(msg.wsId);
|
|
214
|
+
if (ws) ws._onError();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Signal ready
|
|
219
|
+
postMessage({ type: "ready" } satisfies DOMainMessage);
|
|
220
|
+
}
|