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,992 @@
|
|
|
1
|
+
import { Database, type SQLQueryBindings } from "bun:sqlite";
|
|
2
|
+
import { mkdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ContainerContext } from "./container";
|
|
5
|
+
import type { ContainerConfig } from "./container";
|
|
6
|
+
import type { DOExecutor, DOExecutorFactory } from "./do-executor";
|
|
7
|
+
import { persistError, startSpan } from "../tracing/span";
|
|
8
|
+
import { NON_RPC_PROPS, createRpcPromise } from "./rpc-stub";
|
|
9
|
+
|
|
10
|
+
// --- SQL Storage Cursor ---
|
|
11
|
+
|
|
12
|
+
export class SqlStorageCursor implements Iterable<Record<string, unknown>> {
|
|
13
|
+
private _rows: Record<string, unknown>[];
|
|
14
|
+
private _rawRows: unknown[][];
|
|
15
|
+
private _columnNames: string[];
|
|
16
|
+
private _rowsRead: number;
|
|
17
|
+
private _rowsWritten: number;
|
|
18
|
+
private _index = 0;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
rows: Record<string, unknown>[],
|
|
22
|
+
rawRows: unknown[][],
|
|
23
|
+
columnNames: string[],
|
|
24
|
+
rowsRead: number,
|
|
25
|
+
rowsWritten: number,
|
|
26
|
+
) {
|
|
27
|
+
this._rows = rows;
|
|
28
|
+
this._rawRows = rawRows;
|
|
29
|
+
this._columnNames = columnNames;
|
|
30
|
+
this._rowsRead = rowsRead;
|
|
31
|
+
this._rowsWritten = rowsWritten;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get columnNames(): string[] {
|
|
35
|
+
return this._columnNames;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get rowsRead(): number {
|
|
39
|
+
return this._rowsRead;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get rowsWritten(): number {
|
|
43
|
+
return this._rowsWritten;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[Symbol.iterator](): Iterator<Record<string, unknown>> {
|
|
47
|
+
let i = 0;
|
|
48
|
+
const rows = this._rows;
|
|
49
|
+
return {
|
|
50
|
+
next(): IteratorResult<Record<string, unknown>> {
|
|
51
|
+
if (i < rows.length) {
|
|
52
|
+
return { done: false, value: rows[i++]! };
|
|
53
|
+
}
|
|
54
|
+
return { done: true, value: undefined };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
next(): IteratorResult<Record<string, unknown>> {
|
|
60
|
+
if (this._index < this._rows.length) {
|
|
61
|
+
return { done: false, value: this._rows[this._index++]! };
|
|
62
|
+
}
|
|
63
|
+
return { done: true, value: undefined };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toArray(): Record<string, unknown>[] {
|
|
67
|
+
return [...this._rows];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
one(): Record<string, unknown> {
|
|
71
|
+
if (this._rows.length !== 1) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Expected exactly one row, got ${this._rows.length}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return this._rows[0]!;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
raw(): unknown[][] {
|
|
80
|
+
return [...this._rawRows];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- SQL Storage API ---
|
|
85
|
+
|
|
86
|
+
export class SqlStorage {
|
|
87
|
+
private _dbPath: string;
|
|
88
|
+
private _db: Database | null = null;
|
|
89
|
+
|
|
90
|
+
constructor(dbPath: string) {
|
|
91
|
+
this._dbPath = dbPath;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private _getDb(): Database {
|
|
95
|
+
if (!this._db) {
|
|
96
|
+
// Ensure parent directory exists
|
|
97
|
+
const dir = this._dbPath.substring(0, this._dbPath.lastIndexOf("/"));
|
|
98
|
+
mkdirSync(dir, { recursive: true });
|
|
99
|
+
this._db = new Database(this._dbPath, { create: true });
|
|
100
|
+
this._db.run("PRAGMA journal_mode=WAL");
|
|
101
|
+
}
|
|
102
|
+
return this._db;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
exec(query: string, ...bindings: SQLQueryBindings[]): SqlStorageCursor {
|
|
106
|
+
const db = this._getDb();
|
|
107
|
+
const stmt = db.prepare(query);
|
|
108
|
+
|
|
109
|
+
// Determine if this is a query that returns rows
|
|
110
|
+
const trimmed = query.trim().toUpperCase();
|
|
111
|
+
const isSelect = trimmed.startsWith("SELECT") || trimmed.startsWith("WITH") || trimmed.startsWith("PRAGMA");
|
|
112
|
+
|
|
113
|
+
if (isSelect) {
|
|
114
|
+
const rows = stmt.all(...bindings) as Record<string, unknown>[];
|
|
115
|
+
const columnNames = stmt.columnNames as string[] ?? [];
|
|
116
|
+
const rawRows = rows.map((row) =>
|
|
117
|
+
columnNames.map((col) => row[col]),
|
|
118
|
+
);
|
|
119
|
+
return new SqlStorageCursor(rows, rawRows, columnNames, rows.length, 0);
|
|
120
|
+
} else {
|
|
121
|
+
stmt.run(...bindings);
|
|
122
|
+
const changes = db.query("SELECT changes() as c").get() as { c: number };
|
|
123
|
+
return new SqlStorageCursor([], [], [], 0, changes.c);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get databaseSize(): number {
|
|
128
|
+
try {
|
|
129
|
+
return statSync(this._dbPath).size;
|
|
130
|
+
} catch {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- WebSocket support ---
|
|
137
|
+
|
|
138
|
+
export class WebSocketRequestResponsePair {
|
|
139
|
+
readonly request: string;
|
|
140
|
+
readonly response: string;
|
|
141
|
+
|
|
142
|
+
constructor(request: string, response: string) {
|
|
143
|
+
this.request = request;
|
|
144
|
+
this.response = response;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Options accepted by DO storage methods — all are no-ops in dev */
|
|
149
|
+
interface StorageOptions {
|
|
150
|
+
allowConcurrency?: boolean;
|
|
151
|
+
allowUnconfirmed?: boolean;
|
|
152
|
+
noCache?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Configurable limits for Durable Objects */
|
|
156
|
+
export interface DurableObjectLimits {
|
|
157
|
+
maxTagsPerWebSocket?: number;
|
|
158
|
+
maxTagLength?: number;
|
|
159
|
+
maxConcurrentWebSockets?: number;
|
|
160
|
+
maxAutoResponseLength?: number;
|
|
161
|
+
/** Eviction timeout in ms. Idle instances are evicted after this time. 0 = disabled. Default: 120000 */
|
|
162
|
+
evictionTimeoutMs?: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const DO_DEFAULTS: Required<DurableObjectLimits> = {
|
|
166
|
+
maxTagsPerWebSocket: 10,
|
|
167
|
+
maxTagLength: 256,
|
|
168
|
+
maxConcurrentWebSockets: 32_768,
|
|
169
|
+
maxAutoResponseLength: 2048,
|
|
170
|
+
evictionTimeoutMs: 120_000,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// --- Storage ---
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Synchronous KV API for Durable Object storage.
|
|
177
|
+
* Uses the same `do_storage` table as the async API.
|
|
178
|
+
*/
|
|
179
|
+
export class SyncKV {
|
|
180
|
+
private db: Database;
|
|
181
|
+
private namespace: string;
|
|
182
|
+
private id: string;
|
|
183
|
+
|
|
184
|
+
constructor(db: Database, namespace: string, id: string) {
|
|
185
|
+
this.db = db;
|
|
186
|
+
this.namespace = namespace;
|
|
187
|
+
this.id = id;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get(key: string): unknown {
|
|
191
|
+
const row = this.db
|
|
192
|
+
.query("SELECT value FROM do_storage WHERE namespace = ? AND id = ? AND key = ?")
|
|
193
|
+
.get(this.namespace, this.id, key) as { value: string } | null;
|
|
194
|
+
if (!row) return undefined;
|
|
195
|
+
return JSON.parse(row.value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
put(key: string, value: unknown): void {
|
|
199
|
+
this.db
|
|
200
|
+
.query("INSERT OR REPLACE INTO do_storage (namespace, id, key, value) VALUES (?, ?, ?, ?)")
|
|
201
|
+
.run(this.namespace, this.id, key, JSON.stringify(value));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
delete(key: string): boolean {
|
|
205
|
+
const existing = this.db
|
|
206
|
+
.query("SELECT 1 FROM do_storage WHERE namespace = ? AND id = ? AND key = ?")
|
|
207
|
+
.get(this.namespace, this.id, key);
|
|
208
|
+
this.db
|
|
209
|
+
.query("DELETE FROM do_storage WHERE namespace = ? AND id = ? AND key = ?")
|
|
210
|
+
.run(this.namespace, this.id, key);
|
|
211
|
+
return existing !== null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
*list(options?: { prefix?: string; start?: string; startAfter?: string; end?: string; limit?: number; reverse?: boolean }): Iterable<[string, unknown]> {
|
|
215
|
+
const prefix = options?.prefix ?? "";
|
|
216
|
+
const limit = options?.limit ?? 1000;
|
|
217
|
+
const reverse = options?.reverse ?? false;
|
|
218
|
+
|
|
219
|
+
let sql = "SELECT key, value FROM do_storage WHERE namespace = ? AND id = ?";
|
|
220
|
+
const params: (string | number)[] = [this.namespace, this.id];
|
|
221
|
+
|
|
222
|
+
if (prefix) {
|
|
223
|
+
sql += " AND key LIKE ?";
|
|
224
|
+
const escaped = prefix.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
225
|
+
params.push(escaped + "%");
|
|
226
|
+
sql += " ESCAPE '\\'";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (options?.startAfter) {
|
|
230
|
+
sql += " AND key > ?";
|
|
231
|
+
params.push(options.startAfter);
|
|
232
|
+
} else if (options?.start) {
|
|
233
|
+
sql += " AND key >= ?";
|
|
234
|
+
params.push(options.start);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (options?.end) {
|
|
238
|
+
sql += " AND key < ?";
|
|
239
|
+
params.push(options.end);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
sql += ` ORDER BY key ${reverse ? "DESC" : "ASC"} LIMIT ?`;
|
|
243
|
+
params.push(limit);
|
|
244
|
+
|
|
245
|
+
const rows = this.db.query(sql).all(...params) as { key: string; value: string }[];
|
|
246
|
+
for (const row of rows) {
|
|
247
|
+
yield [row.key, JSON.parse(row.value)];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export class SqliteDurableObjectStorage {
|
|
253
|
+
private db: Database;
|
|
254
|
+
private namespace: string;
|
|
255
|
+
private id: string;
|
|
256
|
+
private _sql: SqlStorage | null = null;
|
|
257
|
+
private _dataDir: string | null = null;
|
|
258
|
+
private _kv: SyncKV | null = null;
|
|
259
|
+
|
|
260
|
+
constructor(db: Database, namespace: string, id: string, dataDir?: string) {
|
|
261
|
+
this.db = db;
|
|
262
|
+
this.namespace = namespace;
|
|
263
|
+
this.id = id;
|
|
264
|
+
this._dataDir = dataDir ?? null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get kv(): SyncKV {
|
|
268
|
+
if (!this._kv) {
|
|
269
|
+
this._kv = new SyncKV(this.db, this.namespace, this.id);
|
|
270
|
+
}
|
|
271
|
+
return this._kv;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
get sql(): SqlStorage {
|
|
275
|
+
if (!this._sql) {
|
|
276
|
+
if (!this._dataDir) {
|
|
277
|
+
throw new Error("SQL storage not available: dataDir not configured");
|
|
278
|
+
}
|
|
279
|
+
const dbPath = join(this._dataDir, "do-sql", this.namespace, `${this.id}.sqlite`);
|
|
280
|
+
this._sql = new SqlStorage(dbPath);
|
|
281
|
+
}
|
|
282
|
+
return this._sql;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async get<T = unknown>(key: string, options?: StorageOptions): Promise<T | undefined>;
|
|
286
|
+
async get<T = unknown>(keys: string[], options?: StorageOptions): Promise<Map<string, T>>;
|
|
287
|
+
async get<T = unknown>(keyOrKeys: string | string[], _options?: StorageOptions): Promise<T | undefined | Map<string, T>> {
|
|
288
|
+
if (Array.isArray(keyOrKeys)) {
|
|
289
|
+
if (keyOrKeys.length === 0) return new Map<string, T>();
|
|
290
|
+
const placeholders = keyOrKeys.map(() => "?").join(", ");
|
|
291
|
+
const rows = this.db
|
|
292
|
+
.query(`SELECT key, value FROM do_storage WHERE namespace = ? AND id = ? AND key IN (${placeholders})`)
|
|
293
|
+
.all(this.namespace, this.id, ...keyOrKeys) as { key: string; value: string }[];
|
|
294
|
+
const result = new Map<string, T>();
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
result.set(row.key, JSON.parse(row.value) as T);
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
const row = this.db
|
|
301
|
+
.query("SELECT value FROM do_storage WHERE namespace = ? AND id = ? AND key = ?")
|
|
302
|
+
.get(this.namespace, this.id, keyOrKeys) as { value: string } | null;
|
|
303
|
+
if (!row) return undefined;
|
|
304
|
+
return JSON.parse(row.value) as T;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async put(key: string, value: unknown, options?: StorageOptions): Promise<void>;
|
|
308
|
+
async put(entries: Record<string, unknown>, options?: StorageOptions): Promise<void>;
|
|
309
|
+
async put(keyOrEntries: string | Record<string, unknown>, valueOrOptions?: unknown, _options?: StorageOptions): Promise<void> {
|
|
310
|
+
if (typeof keyOrEntries === "string") {
|
|
311
|
+
this.db
|
|
312
|
+
.query("INSERT OR REPLACE INTO do_storage (namespace, id, key, value) VALUES (?, ?, ?, ?)")
|
|
313
|
+
.run(this.namespace, this.id, keyOrEntries, JSON.stringify(valueOrOptions));
|
|
314
|
+
} else {
|
|
315
|
+
const stmt = this.db.query(
|
|
316
|
+
"INSERT OR REPLACE INTO do_storage (namespace, id, key, value) VALUES (?, ?, ?, ?)",
|
|
317
|
+
);
|
|
318
|
+
this.db.run("BEGIN");
|
|
319
|
+
try {
|
|
320
|
+
for (const [k, v] of Object.entries(keyOrEntries)) {
|
|
321
|
+
stmt.run(this.namespace, this.id, k, JSON.stringify(v));
|
|
322
|
+
}
|
|
323
|
+
this.db.run("COMMIT");
|
|
324
|
+
} catch (e) {
|
|
325
|
+
this.db.run("ROLLBACK");
|
|
326
|
+
throw e;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async delete(key: string, options?: StorageOptions): Promise<boolean>;
|
|
332
|
+
async delete(keys: string[], options?: StorageOptions): Promise<number>;
|
|
333
|
+
async delete(keyOrKeys: string | string[], _options?: StorageOptions): Promise<boolean | number> {
|
|
334
|
+
if (Array.isArray(keyOrKeys)) {
|
|
335
|
+
if (keyOrKeys.length === 0) return 0;
|
|
336
|
+
// Count existing keys first
|
|
337
|
+
const placeholders = keyOrKeys.map(() => "?").join(", ");
|
|
338
|
+
const countRow = this.db
|
|
339
|
+
.query(`SELECT COUNT(*) as c FROM do_storage WHERE namespace = ? AND id = ? AND key IN (${placeholders})`)
|
|
340
|
+
.get(this.namespace, this.id, ...keyOrKeys) as { c: number };
|
|
341
|
+
const count = countRow.c;
|
|
342
|
+
this.db
|
|
343
|
+
.query(`DELETE FROM do_storage WHERE namespace = ? AND id = ? AND key IN (${placeholders})`)
|
|
344
|
+
.run(this.namespace, this.id, ...keyOrKeys);
|
|
345
|
+
return count;
|
|
346
|
+
}
|
|
347
|
+
const existing = this.db
|
|
348
|
+
.query("SELECT 1 FROM do_storage WHERE namespace = ? AND id = ? AND key = ?")
|
|
349
|
+
.get(this.namespace, this.id, keyOrKeys);
|
|
350
|
+
this.db
|
|
351
|
+
.query("DELETE FROM do_storage WHERE namespace = ? AND id = ? AND key = ?")
|
|
352
|
+
.run(this.namespace, this.id, keyOrKeys);
|
|
353
|
+
return existing !== null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async deleteAll(_options?: StorageOptions): Promise<void> {
|
|
357
|
+
this.db
|
|
358
|
+
.query("DELETE FROM do_storage WHERE namespace = ? AND id = ?")
|
|
359
|
+
.run(this.namespace, this.id);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async list(options?: { prefix?: string; start?: string; startAfter?: string; end?: string; limit?: number; reverse?: boolean }): Promise<Map<string, unknown>> {
|
|
363
|
+
const prefix = options?.prefix ?? "";
|
|
364
|
+
const limit = options?.limit ?? 1000;
|
|
365
|
+
const reverse = options?.reverse ?? false;
|
|
366
|
+
|
|
367
|
+
let sql = "SELECT key, value FROM do_storage WHERE namespace = ? AND id = ?";
|
|
368
|
+
const params: (string | number)[] = [this.namespace, this.id];
|
|
369
|
+
|
|
370
|
+
if (prefix) {
|
|
371
|
+
sql += " AND key LIKE ?";
|
|
372
|
+
// Escape % and _ in prefix for LIKE, then append %
|
|
373
|
+
const escaped = prefix.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
374
|
+
params.push(escaped + "%");
|
|
375
|
+
sql += " ESCAPE '\\'";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (options?.startAfter) {
|
|
379
|
+
sql += " AND key > ?";
|
|
380
|
+
params.push(options.startAfter);
|
|
381
|
+
} else if (options?.start) {
|
|
382
|
+
sql += " AND key >= ?";
|
|
383
|
+
params.push(options.start);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (options?.end) {
|
|
387
|
+
sql += " AND key < ?";
|
|
388
|
+
params.push(options.end);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
sql += ` ORDER BY key ${reverse ? "DESC" : "ASC"} LIMIT ?`;
|
|
392
|
+
params.push(limit);
|
|
393
|
+
|
|
394
|
+
const rows = this.db.query(sql).all(...params) as { key: string; value: string }[];
|
|
395
|
+
const result = new Map<string, unknown>();
|
|
396
|
+
for (const row of rows) {
|
|
397
|
+
result.set(row.key, JSON.parse(row.value));
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async sync(): Promise<void> {
|
|
403
|
+
// No-op in dev — in production this flushes the write buffer
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async transaction<T>(closure: (txn: SqliteDurableObjectStorage) => Promise<T>): Promise<T> {
|
|
407
|
+
this.db.run("BEGIN");
|
|
408
|
+
try {
|
|
409
|
+
const result = await closure(this);
|
|
410
|
+
this.db.run("COMMIT");
|
|
411
|
+
return result;
|
|
412
|
+
} catch (e) {
|
|
413
|
+
this.db.run("ROLLBACK");
|
|
414
|
+
throw e;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
transactionSync<T>(callback: () => T): T {
|
|
419
|
+
this.db.run("BEGIN IMMEDIATE");
|
|
420
|
+
try {
|
|
421
|
+
const result = callback();
|
|
422
|
+
this.db.run("COMMIT");
|
|
423
|
+
return result;
|
|
424
|
+
} catch (e) {
|
|
425
|
+
this.db.run("ROLLBACK");
|
|
426
|
+
throw e;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// --- Alarm methods ---
|
|
431
|
+
|
|
432
|
+
private _onAlarmSet?: (scheduledTime: number | null) => void;
|
|
433
|
+
|
|
434
|
+
/** @internal Register callback for when alarm is set/deleted */
|
|
435
|
+
_setAlarmCallback(cb: (scheduledTime: number | null) => void) {
|
|
436
|
+
this._onAlarmSet = cb;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async getAlarm(_options?: StorageOptions): Promise<number | null> {
|
|
440
|
+
const row = this.db
|
|
441
|
+
.query("SELECT alarm_time FROM do_alarms WHERE namespace = ? AND id = ?")
|
|
442
|
+
.get(this.namespace, this.id) as { alarm_time: number } | null;
|
|
443
|
+
return row ? row.alarm_time : null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async setAlarm(scheduledTime: number | Date, _options?: StorageOptions): Promise<void> {
|
|
447
|
+
const time = scheduledTime instanceof Date ? scheduledTime.getTime() : scheduledTime;
|
|
448
|
+
this.db
|
|
449
|
+
.query("INSERT OR REPLACE INTO do_alarms (namespace, id, alarm_time) VALUES (?, ?, ?)")
|
|
450
|
+
.run(this.namespace, this.id, time);
|
|
451
|
+
this._onAlarmSet?.(time);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async deleteAlarm(_options?: StorageOptions): Promise<void> {
|
|
455
|
+
this.db
|
|
456
|
+
.query("DELETE FROM do_alarms WHERE namespace = ? AND id = ?")
|
|
457
|
+
.run(this.namespace, this.id);
|
|
458
|
+
this._onAlarmSet?.(null);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// --- ID ---
|
|
463
|
+
|
|
464
|
+
export class DurableObjectIdImpl {
|
|
465
|
+
readonly name?: string;
|
|
466
|
+
|
|
467
|
+
constructor(readonly id: string, name?: string) {
|
|
468
|
+
this.name = name;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
toString() {
|
|
472
|
+
return this.id;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
equals(other: DurableObjectIdImpl): boolean {
|
|
476
|
+
return this.id === other.id;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// --- State ---
|
|
481
|
+
|
|
482
|
+
interface AcceptedWebSocket {
|
|
483
|
+
ws: WebSocket;
|
|
484
|
+
tags: string[];
|
|
485
|
+
autoResponseTimestamp: Date | null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export class DurableObjectStateImpl {
|
|
489
|
+
readonly id: DurableObjectIdImpl;
|
|
490
|
+
readonly storage: SqliteDurableObjectStorage;
|
|
491
|
+
container?: ContainerContext;
|
|
492
|
+
private _concurrencyGate: Promise<void> | null = null;
|
|
493
|
+
private _acceptedWebSockets: Set<AcceptedWebSocket> = new Set();
|
|
494
|
+
private _autoResponsePair: WebSocketRequestResponsePair | null = null;
|
|
495
|
+
private _hibernatableTimeout: number | null = null;
|
|
496
|
+
private _limits: Required<DurableObjectLimits>;
|
|
497
|
+
private _instanceResolver: (() => DurableObjectBase | null) | null = null;
|
|
498
|
+
private _lockTail: Promise<void> = Promise.resolve();
|
|
499
|
+
private _activeRequests = 0;
|
|
500
|
+
|
|
501
|
+
constructor(id: DurableObjectIdImpl, db: Database, namespace: string, dataDir?: string, limits?: DurableObjectLimits) {
|
|
502
|
+
this.id = id;
|
|
503
|
+
this.storage = new SqliteDurableObjectStorage(db, namespace, id.toString(), dataDir);
|
|
504
|
+
this._limits = { ...DO_DEFAULTS, ...limits };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T> {
|
|
508
|
+
let resolve: () => void;
|
|
509
|
+
this._concurrencyGate = new Promise<void>(r => { resolve = r; });
|
|
510
|
+
return callback().finally(() => {
|
|
511
|
+
this._concurrencyGate = null;
|
|
512
|
+
resolve!();
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** @internal Wait until blockConcurrencyWhile completes */
|
|
517
|
+
async _waitForReady(): Promise<void> {
|
|
518
|
+
while (this._concurrencyGate) {
|
|
519
|
+
await this._concurrencyGate;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** @internal Whether blockConcurrencyWhile is active */
|
|
524
|
+
_isBlocked(): boolean {
|
|
525
|
+
return this._concurrencyGate !== null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** @internal Whether there are active requests in the queue */
|
|
529
|
+
_hasActiveRequests(): boolean {
|
|
530
|
+
return this._activeRequests > 0;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** @internal Acquire the serial-execution lock. Caller awaits this, then runs work in its own async stack. */
|
|
534
|
+
async _lock(): Promise<() => void> {
|
|
535
|
+
let unlockNext: () => void;
|
|
536
|
+
const nextTail = new Promise<void>(r => { unlockNext = r; });
|
|
537
|
+
const ready = this._lockTail;
|
|
538
|
+
this._lockTail = nextTail;
|
|
539
|
+
await ready;
|
|
540
|
+
this._activeRequests++;
|
|
541
|
+
return () => {
|
|
542
|
+
this._activeRequests--;
|
|
543
|
+
unlockNext!();
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** @internal Set the instance resolver for WebSocket handler delegation */
|
|
548
|
+
_setInstanceResolver(resolver: () => DurableObjectBase | null) {
|
|
549
|
+
this._instanceResolver = resolver;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** @internal Get the DO instance via resolver */
|
|
553
|
+
_resolveInstance(): DurableObjectBase | null {
|
|
554
|
+
return this._instanceResolver?.() ?? null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
waitUntil(_promise: Promise<unknown>) {
|
|
558
|
+
// no-op in dev
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// --- WebSocket Hibernation API ---
|
|
562
|
+
|
|
563
|
+
acceptWebSocket(ws: WebSocket, tags?: string[]): void {
|
|
564
|
+
const tagList = tags ?? [];
|
|
565
|
+
if (tagList.length > this._limits.maxTagsPerWebSocket) {
|
|
566
|
+
throw new Error(`Exceeded max tags per WebSocket (${this._limits.maxTagsPerWebSocket})`);
|
|
567
|
+
}
|
|
568
|
+
for (const tag of tagList) {
|
|
569
|
+
if (tag.length > this._limits.maxTagLength) {
|
|
570
|
+
throw new Error(`Tag exceeds max length of ${this._limits.maxTagLength} characters`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (this._acceptedWebSockets.size >= this._limits.maxConcurrentWebSockets) {
|
|
574
|
+
throw new Error(`Exceeded max concurrent WebSocket connections (${this._limits.maxConcurrentWebSockets})`);
|
|
575
|
+
}
|
|
576
|
+
const entry: AcceptedWebSocket = { ws, tags: tagList, autoResponseTimestamp: null };
|
|
577
|
+
this._acceptedWebSockets.add(entry);
|
|
578
|
+
|
|
579
|
+
ws.addEventListener("message", (event: MessageEvent) => {
|
|
580
|
+
const message = event.data;
|
|
581
|
+
// Check auto-response before delegating to handler
|
|
582
|
+
if (this._autoResponsePair !== null) {
|
|
583
|
+
const msgStr = typeof message === "string" ? message : null;
|
|
584
|
+
if (msgStr !== null && msgStr === this._autoResponsePair.request) {
|
|
585
|
+
ws.send(this._autoResponsePair.response);
|
|
586
|
+
entry.autoResponseTimestamp = new Date();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const instance = this._resolveInstance();
|
|
591
|
+
const obj = instance as unknown as Record<string, unknown>;
|
|
592
|
+
if (instance && typeof obj.webSocketMessage === "function") {
|
|
593
|
+
(obj.webSocketMessage as (ws: WebSocket, message: string | ArrayBuffer) => Promise<void>).call(instance, ws, message);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
ws.addEventListener("close", (event: CloseEvent) => {
|
|
598
|
+
this._acceptedWebSockets.delete(entry);
|
|
599
|
+
const instance = this._resolveInstance();
|
|
600
|
+
const obj = instance as unknown as Record<string, unknown>;
|
|
601
|
+
if (instance && typeof obj.webSocketClose === "function") {
|
|
602
|
+
(obj.webSocketClose as (ws: WebSocket, code: number, reason: string, wasClean: boolean) => Promise<void>).call(instance, ws, event.code, event.reason, event.wasClean);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
ws.addEventListener("error", (event: Event) => {
|
|
607
|
+
const instance = this._resolveInstance();
|
|
608
|
+
const obj = instance as unknown as Record<string, unknown>;
|
|
609
|
+
if (instance && typeof obj.webSocketError === "function") {
|
|
610
|
+
(obj.webSocketError as (ws: WebSocket, error: unknown) => Promise<void>).call(instance, ws, event);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
getWebSockets(tag?: string): WebSocket[] {
|
|
616
|
+
const results: WebSocket[] = [];
|
|
617
|
+
for (const entry of this._acceptedWebSockets) {
|
|
618
|
+
if (tag === undefined || entry.tags.includes(tag)) {
|
|
619
|
+
results.push(entry.ws);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return results;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
getTags(ws: WebSocket): string[] {
|
|
626
|
+
for (const entry of this._acceptedWebSockets) {
|
|
627
|
+
if (entry.ws === ws) return entry.tags;
|
|
628
|
+
}
|
|
629
|
+
return [];
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
setWebSocketAutoResponse(pair?: WebSocketRequestResponsePair): void {
|
|
633
|
+
if (pair) {
|
|
634
|
+
if (pair.request.length > this._limits.maxAutoResponseLength) {
|
|
635
|
+
throw new Error(`Auto-response request exceeds max length of ${this._limits.maxAutoResponseLength} characters`);
|
|
636
|
+
}
|
|
637
|
+
if (pair.response.length > this._limits.maxAutoResponseLength) {
|
|
638
|
+
throw new Error(`Auto-response response exceeds max length of ${this._limits.maxAutoResponseLength} characters`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
this._autoResponsePair = pair ?? null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
getWebSocketAutoResponse(): WebSocketRequestResponsePair | null {
|
|
645
|
+
return this._autoResponsePair;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null {
|
|
649
|
+
for (const entry of this._acceptedWebSockets) {
|
|
650
|
+
if (entry.ws === ws) return entry.autoResponseTimestamp;
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
setHibernatableWebSocketEventTimeout(ms?: number): void {
|
|
656
|
+
this._hibernatableTimeout = ms ?? null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
getHibernatableWebSocketEventTimeout(): number | null {
|
|
660
|
+
return this._hibernatableTimeout;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// --- Base class ---
|
|
665
|
+
|
|
666
|
+
export class DurableObjectBase {
|
|
667
|
+
ctx: DurableObjectStateImpl;
|
|
668
|
+
env: unknown;
|
|
669
|
+
|
|
670
|
+
constructor(ctx: DurableObjectStateImpl, env: unknown) {
|
|
671
|
+
this.ctx = ctx;
|
|
672
|
+
this.env = env;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// --- Alarm scheduling ---
|
|
677
|
+
|
|
678
|
+
const MAX_ALARM_RETRIES = 6;
|
|
679
|
+
|
|
680
|
+
// Properties handled specially on the stub (not forwarded as RPC)
|
|
681
|
+
const STUB_PROPS = new Set(["id", "name", "fetch"]);
|
|
682
|
+
|
|
683
|
+
// --- Namespace ---
|
|
684
|
+
|
|
685
|
+
export class DurableObjectNamespaceImpl {
|
|
686
|
+
private _executors = new Map<string, DOExecutor>();
|
|
687
|
+
private _stubs = new Map<string, unknown>();
|
|
688
|
+
private _knownIds = new Map<string, DurableObjectIdImpl>();
|
|
689
|
+
private _class?: new (ctx: DurableObjectStateImpl, env: unknown) => DurableObjectBase;
|
|
690
|
+
private _env?: unknown;
|
|
691
|
+
private db: Database;
|
|
692
|
+
private namespaceName: string;
|
|
693
|
+
private alarmTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
694
|
+
private dataDir: string | undefined;
|
|
695
|
+
private limits: DurableObjectLimits | undefined;
|
|
696
|
+
private _lastActivity = new Map<string, number>();
|
|
697
|
+
private _evictionTimer: ReturnType<typeof setInterval> | null = null;
|
|
698
|
+
private _evictionTimeoutMs: number;
|
|
699
|
+
private _containerConfig?: ContainerConfig;
|
|
700
|
+
private _factoryOverride?: DOExecutorFactory;
|
|
701
|
+
private _defaultFactory?: DOExecutorFactory;
|
|
702
|
+
|
|
703
|
+
constructor(db: Database, namespaceName: string, dataDir?: string, limits?: DurableObjectLimits, factory?: DOExecutorFactory) {
|
|
704
|
+
this.db = db;
|
|
705
|
+
this.namespaceName = namespaceName;
|
|
706
|
+
this.dataDir = dataDir;
|
|
707
|
+
this.limits = limits;
|
|
708
|
+
this._factoryOverride = factory;
|
|
709
|
+
this._evictionTimeoutMs = limits?.evictionTimeoutMs ?? 120_000;
|
|
710
|
+
if (this._evictionTimeoutMs > 0) {
|
|
711
|
+
this._evictionTimer = setInterval(() => this._evictIdle(), 30_000);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private _getFactory(): DOExecutorFactory {
|
|
716
|
+
if (this._factoryOverride) return this._factoryOverride;
|
|
717
|
+
if (!this._defaultFactory) {
|
|
718
|
+
const { InProcessExecutorFactory } = require("./do-executor-inprocess") as typeof import("./do-executor-inprocess");
|
|
719
|
+
this._defaultFactory = new InProcessExecutorFactory();
|
|
720
|
+
}
|
|
721
|
+
return this._defaultFactory;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** Called after worker module is loaded to wire the actual class */
|
|
725
|
+
_setClass(cls: new (ctx: DurableObjectStateImpl, env: unknown) => DurableObjectBase, env: unknown) {
|
|
726
|
+
this._class = cls;
|
|
727
|
+
this._env = env;
|
|
728
|
+
// Restore persisted alarms on startup
|
|
729
|
+
this._restoreAlarms();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** Set container config for this namespace (makes it a container namespace) */
|
|
733
|
+
_setContainerConfig(config: ContainerConfig) {
|
|
734
|
+
this._containerConfig = config;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** @internal Restore all persisted alarms for this namespace */
|
|
738
|
+
private _restoreAlarms() {
|
|
739
|
+
const rows = this.db
|
|
740
|
+
.query("SELECT id, alarm_time FROM do_alarms WHERE namespace = ?")
|
|
741
|
+
.all(this.namespaceName) as { id: string; alarm_time: number }[];
|
|
742
|
+
for (const row of rows) {
|
|
743
|
+
this._scheduleAlarmTimer(row.id, row.alarm_time);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** @internal Schedule a timer for an alarm */
|
|
748
|
+
private _scheduleAlarmTimer(idStr: string, scheduledTime: number) {
|
|
749
|
+
// Clear any existing timer for this instance
|
|
750
|
+
const existing = this.alarmTimers.get(idStr);
|
|
751
|
+
if (existing) clearTimeout(existing);
|
|
752
|
+
|
|
753
|
+
const delay = Math.max(0, scheduledTime - Date.now());
|
|
754
|
+
const timer = setTimeout(() => {
|
|
755
|
+
this.alarmTimers.delete(idStr);
|
|
756
|
+
this._fireAlarm(idStr, 0);
|
|
757
|
+
}, delay);
|
|
758
|
+
this.alarmTimers.set(idStr, timer);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** @internal Fire the alarm handler on a DO instance */
|
|
762
|
+
private async _fireAlarm(idStr: string, retryCount: number): Promise<void> {
|
|
763
|
+
const executor = this._getOrCreateExecutor(idStr);
|
|
764
|
+
if (!executor) return;
|
|
765
|
+
|
|
766
|
+
this._lastActivity.set(idStr, Date.now());
|
|
767
|
+
|
|
768
|
+
// Delete alarm from DB before calling handler (matching CF behavior)
|
|
769
|
+
this.db
|
|
770
|
+
.query("DELETE FROM do_alarms WHERE namespace = ? AND id = ?")
|
|
771
|
+
.run(this.namespaceName, idStr);
|
|
772
|
+
|
|
773
|
+
try {
|
|
774
|
+
await startSpan({
|
|
775
|
+
name: `do.alarm ${this.namespaceName}`,
|
|
776
|
+
kind: "server",
|
|
777
|
+
attributes: {
|
|
778
|
+
"do.namespace": this.namespaceName,
|
|
779
|
+
"do.id": idStr,
|
|
780
|
+
"do.alarm.retryCount": retryCount,
|
|
781
|
+
},
|
|
782
|
+
}, () => executor.executeAlarm(retryCount));
|
|
783
|
+
} catch (e) {
|
|
784
|
+
persistError(e, "alarm");
|
|
785
|
+
if (retryCount < MAX_ALARM_RETRIES) {
|
|
786
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s
|
|
787
|
+
const backoffMs = Math.pow(2, retryCount) * 1000;
|
|
788
|
+
const retryTime = Date.now() + backoffMs;
|
|
789
|
+
// Re-persist alarm for retry
|
|
790
|
+
this.db
|
|
791
|
+
.query("INSERT OR REPLACE INTO do_alarms (namespace, id, alarm_time) VALUES (?, ?, ?)")
|
|
792
|
+
.run(this.namespaceName, idStr, retryTime);
|
|
793
|
+
const timer = setTimeout(() => {
|
|
794
|
+
this.alarmTimers.delete(idStr);
|
|
795
|
+
this._fireAlarm(idStr, retryCount + 1);
|
|
796
|
+
}, backoffMs);
|
|
797
|
+
this.alarmTimers.set(idStr, timer);
|
|
798
|
+
}
|
|
799
|
+
// After max retries, alarm is discarded
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** @internal Get or create a DO executor by id string */
|
|
804
|
+
private _getOrCreateExecutor(idStr: string, doId?: DurableObjectIdImpl): DOExecutor | null {
|
|
805
|
+
if (this._executors.has(idStr)) return this._executors.get(idStr)!;
|
|
806
|
+
if (!this._class) return null;
|
|
807
|
+
|
|
808
|
+
// Use provided doId, or look up known id (preserves name after eviction), or create new
|
|
809
|
+
const id = doId ?? this._knownIds.get(idStr) ?? new DurableObjectIdImpl(idStr);
|
|
810
|
+
if (doId) this._knownIds.set(idStr, doId);
|
|
811
|
+
|
|
812
|
+
// Register instance in do_instances table
|
|
813
|
+
this.db
|
|
814
|
+
.query("INSERT OR IGNORE INTO do_instances (namespace, id, name) VALUES (?, ?, ?)")
|
|
815
|
+
.run(this.namespaceName, idStr, id.name ?? null);
|
|
816
|
+
|
|
817
|
+
const executor = this._getFactory().create({
|
|
818
|
+
id,
|
|
819
|
+
db: this.db,
|
|
820
|
+
namespaceName: this.namespaceName,
|
|
821
|
+
cls: this._class,
|
|
822
|
+
env: this._env,
|
|
823
|
+
dataDir: this.dataDir,
|
|
824
|
+
limits: this.limits,
|
|
825
|
+
containerConfig: this._containerConfig,
|
|
826
|
+
onAlarmSet: (time) => {
|
|
827
|
+
if (time === null) {
|
|
828
|
+
const t = this.alarmTimers.get(idStr);
|
|
829
|
+
if (t) clearTimeout(t);
|
|
830
|
+
this.alarmTimers.delete(idStr);
|
|
831
|
+
} else {
|
|
832
|
+
this._scheduleAlarmTimer(idStr, time);
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
this._executors.set(idStr, executor);
|
|
838
|
+
this._lastActivity.set(idStr, Date.now());
|
|
839
|
+
return executor;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** @internal Evict idle executors */
|
|
843
|
+
private _evictIdle() {
|
|
844
|
+
const now = Date.now();
|
|
845
|
+
for (const [idStr, lastActivity] of this._lastActivity) {
|
|
846
|
+
if (now - lastActivity < this._evictionTimeoutMs) continue;
|
|
847
|
+
const executor = this._executors.get(idStr);
|
|
848
|
+
if (!executor) continue;
|
|
849
|
+
if (executor.isBlocked()) continue;
|
|
850
|
+
if (executor.isActive()) continue;
|
|
851
|
+
if (executor.activeWebSocketCount() > 0) continue;
|
|
852
|
+
// Evict
|
|
853
|
+
executor.dispose().catch(() => {});
|
|
854
|
+
this._executors.delete(idStr);
|
|
855
|
+
this._lastActivity.delete(idStr);
|
|
856
|
+
// _knownIds and alarmTimers survive eviction
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** @internal Destroy this namespace: clear timers, evict executors without active WebSockets */
|
|
861
|
+
destroy(): void {
|
|
862
|
+
if (this._evictionTimer) {
|
|
863
|
+
clearInterval(this._evictionTimer);
|
|
864
|
+
this._evictionTimer = null;
|
|
865
|
+
}
|
|
866
|
+
for (const timer of this.alarmTimers.values()) clearTimeout(timer);
|
|
867
|
+
this.alarmTimers.clear();
|
|
868
|
+
// Dispose executors without active WebSockets; keep the rest alive
|
|
869
|
+
for (const [idStr, executor] of this._executors) {
|
|
870
|
+
if (executor.activeWebSocketCount() === 0) {
|
|
871
|
+
executor.dispose().catch(() => {});
|
|
872
|
+
this._executors.delete(idStr);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
this._lastActivity.clear();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** @internal Get a raw instance for testing (no proxy) */
|
|
879
|
+
_getInstance(idStr: string): DurableObjectBase | null {
|
|
880
|
+
const executor = this._executors.get(idStr);
|
|
881
|
+
if (!executor) return null;
|
|
882
|
+
// InProcessExecutor exposes _rawInstance
|
|
883
|
+
if ("_rawInstance" in executor) {
|
|
884
|
+
return (executor as any)._rawInstance;
|
|
885
|
+
}
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** @internal Get the executor for a given id */
|
|
890
|
+
_getExecutor(idStr: string): DOExecutor | null {
|
|
891
|
+
return this._executors.get(idStr) ?? null;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** Whether the DO class defines an alarm() handler */
|
|
895
|
+
hasAlarmHandler(): boolean {
|
|
896
|
+
return typeof this._class?.prototype?.alarm === "function";
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** @internal Trigger the alarm handler immediately (used by dashboard) */
|
|
900
|
+
triggerAlarm(idStr: string): Promise<void> {
|
|
901
|
+
// Cancel any existing scheduled timer
|
|
902
|
+
const existing = this.alarmTimers.get(idStr);
|
|
903
|
+
if (existing) clearTimeout(existing);
|
|
904
|
+
this.alarmTimers.delete(idStr);
|
|
905
|
+
return this._fireAlarm(idStr, 0);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
newUniqueId(_options?: { jurisdiction?: string }): DurableObjectIdImpl {
|
|
909
|
+
return new DurableObjectIdImpl(crypto.randomUUID().replace(/-/g, ""));
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
idFromName(name: string): DurableObjectIdImpl {
|
|
913
|
+
// Deterministic ID from name using simple hash
|
|
914
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
915
|
+
hasher.update(name);
|
|
916
|
+
const hex = hasher.digest("hex");
|
|
917
|
+
return new DurableObjectIdImpl(hex, name);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
idFromString(id: string): DurableObjectIdImpl {
|
|
921
|
+
return new DurableObjectIdImpl(id);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
getByName(name: string): unknown {
|
|
925
|
+
return this.get(this.idFromName(name));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
get(id: DurableObjectIdImpl): unknown {
|
|
929
|
+
const idStr = id.toString();
|
|
930
|
+
|
|
931
|
+
// Return cached stub if available — stub survives eviction
|
|
932
|
+
if (this._stubs.has(idStr)) return this._stubs.get(idStr)!;
|
|
933
|
+
|
|
934
|
+
if (!this._class) throw new Error("DurableObject class not wired yet. Call _setClass() first.");
|
|
935
|
+
|
|
936
|
+
// Store the known id (preserves name)
|
|
937
|
+
this._knownIds.set(idStr, id);
|
|
938
|
+
|
|
939
|
+
// Ensure executor exists
|
|
940
|
+
this._getOrCreateExecutor(idStr, id);
|
|
941
|
+
|
|
942
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
943
|
+
const self = this;
|
|
944
|
+
|
|
945
|
+
// Return a Proxy stub that lazily resolves executors (survives eviction)
|
|
946
|
+
const stub = new Proxy({} as Record<string, unknown>, {
|
|
947
|
+
get(_target, prop: string | symbol) {
|
|
948
|
+
// Non-RPC props (Promise protocol, symbols, conversion)
|
|
949
|
+
if (NON_RPC_PROPS.has(prop)) return undefined;
|
|
950
|
+
|
|
951
|
+
// stub.id — returns the DurableObjectId
|
|
952
|
+
if (prop === "id") return id;
|
|
953
|
+
// stub.name — returns the name if available
|
|
954
|
+
if (prop === "name") return id.name;
|
|
955
|
+
|
|
956
|
+
// stub.fetch() — calls the DO's fetch() handler
|
|
957
|
+
if (prop === "fetch") {
|
|
958
|
+
return async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
959
|
+
const executor = self._getOrCreateExecutor(idStr, id)!;
|
|
960
|
+
self._lastActivity.set(idStr, Date.now());
|
|
961
|
+
const request = input instanceof Request ? input : new Request(input instanceof URL ? input.href : input, init);
|
|
962
|
+
return await executor.executeFetch(request);
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// RPC: return a callable that also acts as a thenable for property access
|
|
967
|
+
const rpcCallable = (...args: unknown[]) => {
|
|
968
|
+
const executor = self._getOrCreateExecutor(idStr, id)!;
|
|
969
|
+
self._lastActivity.set(idStr, Date.now());
|
|
970
|
+
const promise = executor.executeRpc(String(prop), args);
|
|
971
|
+
return createRpcPromise(promise);
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
// Make it thenable for property access: `await stub.myProp`
|
|
975
|
+
rpcCallable.then = (
|
|
976
|
+
onFulfilled?: ((value: unknown) => unknown) | null,
|
|
977
|
+
onRejected?: ((reason: unknown) => unknown) | null,
|
|
978
|
+
) => {
|
|
979
|
+
const executor = self._getOrCreateExecutor(idStr, id)!;
|
|
980
|
+
self._lastActivity.set(idStr, Date.now());
|
|
981
|
+
const promise = executor.executeRpcGet(String(prop));
|
|
982
|
+
return promise.then(onFulfilled, onRejected);
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
return rpcCallable;
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
this._stubs.set(idStr, stub);
|
|
990
|
+
return stub;
|
|
991
|
+
}
|
|
992
|
+
}
|