@query-farm/vgi 0.6.0

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 (206) hide show
  1. package/LICENSE +134 -0
  2. package/README.md +494 -0
  3. package/dist/__dbg.test-d.d.ts +1 -0
  4. package/dist/arguments/argument-spec.d.ts +51 -0
  5. package/dist/arguments/arguments.d.ts +13 -0
  6. package/dist/arrow/codec/branded.d.ts +83 -0
  7. package/dist/arrow/codec/registry.d.ts +29 -0
  8. package/dist/arrow/codec/repr.d.ts +23 -0
  9. package/dist/arrow/codec/type-descriptors.d.ts +109 -0
  10. package/dist/arrow/impl-arrowjs/build.d.ts +19 -0
  11. package/dist/arrow/impl-arrowjs/canonical.d.ts +15 -0
  12. package/dist/arrow/impl-arrowjs/empty.d.ts +7 -0
  13. package/dist/arrow/impl-arrowjs/filter.d.ts +11 -0
  14. package/dist/arrow/impl-arrowjs/index.d.ts +11 -0
  15. package/dist/arrow/impl-arrowjs/ipc.d.ts +24 -0
  16. package/dist/arrow/impl-arrowjs/iterate.d.ts +39 -0
  17. package/dist/arrow/impl-arrowjs/project.d.ts +10 -0
  18. package/dist/arrow/impl-arrowjs/schema.d.ts +62 -0
  19. package/dist/arrow/impl-arrowjs/statistics.d.ts +13 -0
  20. package/dist/arrow/impl-flechette/build.d.ts +14 -0
  21. package/dist/arrow/impl-flechette/canonical.d.ts +18 -0
  22. package/dist/arrow/impl-flechette/empty.d.ts +9 -0
  23. package/dist/arrow/impl-flechette/filter.d.ts +10 -0
  24. package/dist/arrow/impl-flechette/index.d.ts +11 -0
  25. package/dist/arrow/impl-flechette/ipc.d.ts +33 -0
  26. package/dist/arrow/impl-flechette/iterate.d.ts +29 -0
  27. package/dist/arrow/impl-flechette/message-meta.d.ts +10 -0
  28. package/dist/arrow/impl-flechette/normalize-type.d.ts +11 -0
  29. package/dist/arrow/impl-flechette/project.d.ts +3 -0
  30. package/dist/arrow/impl-flechette/schema.d.ts +47 -0
  31. package/dist/arrow/impl-flechette/statistics.d.ts +13 -0
  32. package/dist/arrow/index.d.ts +11 -0
  33. package/dist/arrow/predicates.d.ts +47 -0
  34. package/dist/arrow/repr.test-d.d.ts +52 -0
  35. package/dist/arrow/schema-types.d.ts +71 -0
  36. package/dist/arrow/types.d.ts +57 -0
  37. package/dist/catalog/attach-option.d.ts +33 -0
  38. package/dist/catalog/attach-options.d.ts +30 -0
  39. package/dist/catalog/composite.d.ts +33 -0
  40. package/dist/catalog/descriptors.d.ts +154 -0
  41. package/dist/catalog/interface.d.ts +239 -0
  42. package/dist/catalog/read-only.d.ts +33 -0
  43. package/dist/client/client.d.ts +189 -0
  44. package/dist/client/errors.d.ts +26 -0
  45. package/dist/client/helpers.d.ts +12 -0
  46. package/dist/client/protocol.d.ts +16 -0
  47. package/dist/client/types.d.ts +125 -0
  48. package/dist/client-entry.d.ts +11 -0
  49. package/dist/client-entry.js +48 -0
  50. package/dist/client-entry.js.map +9 -0
  51. package/dist/codec/asd.d.ts +25 -0
  52. package/dist/crypto.d.ts +36 -0
  53. package/dist/errors.d.ts +24 -0
  54. package/dist/filter-pushdown/collector.d.ts +32 -0
  55. package/dist/filter-pushdown/deserialize.d.ts +16 -0
  56. package/dist/filter-pushdown/evaluate.d.ts +51 -0
  57. package/dist/filter-pushdown/index.d.ts +4 -0
  58. package/dist/filter-pushdown/types.d.ts +84 -0
  59. package/dist/functions/aggregate.d.ts +129 -0
  60. package/dist/functions/copy-from.d.ts +81 -0
  61. package/dist/functions/copy-to.d.ts +121 -0
  62. package/dist/functions/registry.d.ts +16 -0
  63. package/dist/functions/scalar.d.ts +98 -0
  64. package/dist/functions/storage-cf-do.d.ts +47 -0
  65. package/dist/functions/storage.d.ts +86 -0
  66. package/dist/functions/table-buffering.d.ts +99 -0
  67. package/dist/functions/table-in-out.d.ts +64 -0
  68. package/dist/functions/table.d.ts +118 -0
  69. package/dist/functions/types.d.ts +157 -0
  70. package/dist/generated/vgi-client.d.ts +466 -0
  71. package/dist/generated/vgi-protocol-schemas.d.ts +115 -0
  72. package/dist/index.core.d.ts +42 -0
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.js +10888 -0
  75. package/dist/index.js.map +85 -0
  76. package/dist/metadata/resolve.d.ts +3 -0
  77. package/dist/metadata/serialize.d.ts +12 -0
  78. package/dist/metadata/types.d.ts +47 -0
  79. package/dist/protocol/dispatch.d.ts +23 -0
  80. package/dist/protocol/handlers/aggregate.d.ts +3 -0
  81. package/dist/protocol/handlers/catalog/admin.d.ts +3 -0
  82. package/dist/protocol/handlers/catalog/index.d.ts +3 -0
  83. package/dist/protocol/handlers/catalog/index_methods.d.ts +3 -0
  84. package/dist/protocol/handlers/catalog/macro.d.ts +3 -0
  85. package/dist/protocol/handlers/catalog/shared.d.ts +70 -0
  86. package/dist/protocol/handlers/catalog/table.d.ts +3 -0
  87. package/dist/protocol/handlers/catalog/view.d.ts +3 -0
  88. package/dist/protocol/handlers/function.d.ts +8 -0
  89. package/dist/protocol/handlers/shared.d.ts +31 -0
  90. package/dist/protocol/handlers/table-buffering.d.ts +3 -0
  91. package/dist/protocol/serialize.d.ts +4 -0
  92. package/dist/protocol/serializers/arguments.d.ts +3 -0
  93. package/dist/protocol/serializers/bind.d.ts +9 -0
  94. package/dist/protocol/serializers/cardinality.d.ts +7 -0
  95. package/dist/protocol/serializers/init.d.ts +9 -0
  96. package/dist/protocol/serializers/shared.d.ts +12 -0
  97. package/dist/protocol/state-serializer.d.ts +23 -0
  98. package/dist/protocol/types.d.ts +129 -0
  99. package/dist/secrets/helpers.d.ts +15 -0
  100. package/dist/types.d.ts +52 -0
  101. package/dist/util/arrow/index.d.ts +1 -0
  102. package/dist/util/bytes.d.ts +2 -0
  103. package/dist/util/statistics.d.ts +7 -0
  104. package/dist/worker-cf-entry.d.ts +30 -0
  105. package/dist/worker-cf-entry.js +8767 -0
  106. package/dist/worker-cf-entry.js.map +74 -0
  107. package/dist/worker.d.ts +36 -0
  108. package/package.json +96 -0
  109. package/src/arguments/argument-spec.ts +200 -0
  110. package/src/arguments/arguments.ts +81 -0
  111. package/src/arrow/codec/branded.ts +206 -0
  112. package/src/arrow/codec/registry.ts +674 -0
  113. package/src/arrow/codec/repr.ts +181 -0
  114. package/src/arrow/codec/type-descriptors.ts +137 -0
  115. package/src/arrow/impl-arrowjs/build.ts +88 -0
  116. package/src/arrow/impl-arrowjs/canonical.ts +509 -0
  117. package/src/arrow/impl-arrowjs/empty.ts +72 -0
  118. package/src/arrow/impl-arrowjs/filter.ts +53 -0
  119. package/src/arrow/impl-arrowjs/index.ts +54 -0
  120. package/src/arrow/impl-arrowjs/ipc.ts +67 -0
  121. package/src/arrow/impl-arrowjs/iterate.ts +159 -0
  122. package/src/arrow/impl-arrowjs/project.ts +73 -0
  123. package/src/arrow/impl-arrowjs/schema.ts +256 -0
  124. package/src/arrow/impl-arrowjs/statistics.ts +185 -0
  125. package/src/arrow/impl-flechette/build.ts +65 -0
  126. package/src/arrow/impl-flechette/canonical.ts +289 -0
  127. package/src/arrow/impl-flechette/empty.ts +20 -0
  128. package/src/arrow/impl-flechette/filter.ts +42 -0
  129. package/src/arrow/impl-flechette/index.ts +54 -0
  130. package/src/arrow/impl-flechette/ipc.ts +110 -0
  131. package/src/arrow/impl-flechette/iterate.ts +121 -0
  132. package/src/arrow/impl-flechette/message-meta.ts +175 -0
  133. package/src/arrow/impl-flechette/normalize-type.ts +99 -0
  134. package/src/arrow/impl-flechette/project.ts +64 -0
  135. package/src/arrow/impl-flechette/schema.ts +202 -0
  136. package/src/arrow/impl-flechette/statistics.ts +198 -0
  137. package/src/arrow/index.ts +129 -0
  138. package/src/arrow/predicates.ts +66 -0
  139. package/src/arrow/repr.test-d.ts +189 -0
  140. package/src/arrow/schema-types.ts +213 -0
  141. package/src/arrow/types.ts +74 -0
  142. package/src/catalog/attach-option.ts +107 -0
  143. package/src/catalog/attach-options.ts +79 -0
  144. package/src/catalog/composite.ts +180 -0
  145. package/src/catalog/descriptors.ts +164 -0
  146. package/src/catalog/interface.ts +721 -0
  147. package/src/catalog/read-only.ts +909 -0
  148. package/src/client/client.ts +1348 -0
  149. package/src/client/errors.ts +69 -0
  150. package/src/client/helpers.ts +56 -0
  151. package/src/client/protocol.ts +33 -0
  152. package/src/client/types.ts +140 -0
  153. package/src/client-entry.ts +112 -0
  154. package/src/codec/asd.ts +147 -0
  155. package/src/crypto.ts +143 -0
  156. package/src/errors.ts +61 -0
  157. package/src/filter-pushdown/collector.ts +147 -0
  158. package/src/filter-pushdown/deserialize.ts +253 -0
  159. package/src/filter-pushdown/evaluate.ts +591 -0
  160. package/src/filter-pushdown/index.ts +28 -0
  161. package/src/filter-pushdown/types.ts +91 -0
  162. package/src/functions/aggregate.ts +384 -0
  163. package/src/functions/copy-from.ts +282 -0
  164. package/src/functions/copy-to.ts +319 -0
  165. package/src/functions/registry.ts +258 -0
  166. package/src/functions/scalar.ts +356 -0
  167. package/src/functions/storage-cf-do.ts +303 -0
  168. package/src/functions/storage.ts +521 -0
  169. package/src/functions/table-buffering.ts +562 -0
  170. package/src/functions/table-in-out.ts +373 -0
  171. package/src/functions/table.ts +521 -0
  172. package/src/functions/types.ts +192 -0
  173. package/src/generated/vgi-client.ts +560 -0
  174. package/src/generated/vgi-protocol-schemas.ts +864 -0
  175. package/src/index.core.ts +267 -0
  176. package/src/index.ts +11 -0
  177. package/src/metadata/resolve.ts +109 -0
  178. package/src/metadata/serialize.ts +242 -0
  179. package/src/metadata/types.ts +60 -0
  180. package/src/protocol/dispatch.ts +52 -0
  181. package/src/protocol/handlers/aggregate.ts +355 -0
  182. package/src/protocol/handlers/catalog/admin.ts +368 -0
  183. package/src/protocol/handlers/catalog/index.ts +25 -0
  184. package/src/protocol/handlers/catalog/index_methods.ts +65 -0
  185. package/src/protocol/handlers/catalog/macro.ts +114 -0
  186. package/src/protocol/handlers/catalog/shared.ts +291 -0
  187. package/src/protocol/handlers/catalog/table.ts +401 -0
  188. package/src/protocol/handlers/catalog/view.ts +119 -0
  189. package/src/protocol/handlers/function.ts +302 -0
  190. package/src/protocol/handlers/shared.ts +80 -0
  191. package/src/protocol/handlers/table-buffering.ts +207 -0
  192. package/src/protocol/serialize.ts +23 -0
  193. package/src/protocol/serializers/arguments.ts +160 -0
  194. package/src/protocol/serializers/bind.ts +212 -0
  195. package/src/protocol/serializers/cardinality.ts +45 -0
  196. package/src/protocol/serializers/init.ts +247 -0
  197. package/src/protocol/serializers/shared.ts +46 -0
  198. package/src/protocol/state-serializer.ts +181 -0
  199. package/src/protocol/types.ts +153 -0
  200. package/src/secrets/helpers.ts +70 -0
  201. package/src/types.ts +71 -0
  202. package/src/util/arrow/index.ts +23 -0
  203. package/src/util/bytes.ts +14 -0
  204. package/src/util/statistics.ts +40 -0
  205. package/src/worker-cf-entry.ts +172 -0
  206. package/src/worker.ts +212 -0
@@ -0,0 +1,521 @@
1
+ // Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+ // Storage for VGI function state.
3
+ // Provides shared state across worker processes for distributed execution.
4
+ // Port of vgi-python's vgi/function_storage.py.
5
+ //
6
+ // The interface is async so HTTP-backed implementations (Cloudflare DO,
7
+ // Azure SQL, etc.) can use fetch/network drivers without sync hacks.
8
+ // FunctionStorageSqlite wraps bun:sqlite's sync calls in resolved promises.
9
+
10
+ // FunctionStorageSqlite is the only thing in this module that needs Bun + node:*
11
+ // modules. Workers (Cloudflare workerd / browsers) should use
12
+ // FunctionStorageCfDo (HTTP-backed) instead — they never reach this class.
13
+ // We resolve all node:* and bun:* dependencies through indirect string
14
+ // variables that esbuild can't trace statically, so workerd bundles cleanly
15
+ // without nodejs_compat polyfills for paths we never run.
16
+ type Database = any;
17
+ const _BUN_SQLITE_MOD = "bun:sqlite";
18
+ const _NODE_OS_MOD = "node:os";
19
+ const _NODE_PATH_MOD = "node:path";
20
+ const _NODE_FS_MOD = "node:fs";
21
+ let _Database: any = null;
22
+ function _req(): any {
23
+ const req: any = (import.meta as any).require ?? (globalThis as any).require ?? null;
24
+ if (!req) {
25
+ throw new Error(
26
+ "FunctionStorageSqlite requires Node.js or Bun (bun:sqlite + node:os/path/fs). " +
27
+ "Use FunctionStorageCfDo on non-Bun runtimes.",
28
+ );
29
+ }
30
+ return req;
31
+ }
32
+ function _loadDatabase(): any {
33
+ if (_Database) return _Database;
34
+ try {
35
+ _Database = _req()(_BUN_SQLITE_MOD).Database;
36
+ return _Database;
37
+ } catch (e: any) {
38
+ throw new Error(
39
+ `FunctionStorageSqlite: failed to load bun:sqlite — ${e?.message ?? e}`,
40
+ );
41
+ }
42
+ }
43
+ function _loadNodeIo(): {
44
+ homedir: () => string;
45
+ platform: () => string;
46
+ join: (...parts: string[]) => string;
47
+ mkdirSync: (path: string, opts?: any) => void;
48
+ } {
49
+ const r = _req();
50
+ return {
51
+ homedir: r(_NODE_OS_MOD).homedir,
52
+ platform: r(_NODE_OS_MOD).platform,
53
+ join: r(_NODE_PATH_MOD).join,
54
+ mkdirSync: r(_NODE_FS_MOD).mkdirSync,
55
+ };
56
+ }
57
+
58
+ // ============================================================================
59
+ // Errors
60
+ // ============================================================================
61
+
62
+ export class UnknownInvocationError extends Error {
63
+ constructor(message?: string | Uint8Array) {
64
+ let text: string;
65
+ if (message instanceof Uint8Array) {
66
+ text =
67
+ `Invocation ${hexEncode(message)} is not registered. ` +
68
+ "Call queuePush first to register the invocation.";
69
+ } else {
70
+ text =
71
+ message ??
72
+ "Invocation is not registered. Call queuePush first to register the invocation.";
73
+ }
74
+ super(text);
75
+ this.name = "UnknownInvocationError";
76
+ }
77
+ }
78
+
79
+ function hexEncode(buf: Uint8Array): string {
80
+ return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
81
+ }
82
+
83
+ // ============================================================================
84
+ // FunctionStorage interface
85
+ // ============================================================================
86
+
87
+ export interface FunctionStorage {
88
+ workerPut(executionId: Uint8Array, workerId: number, state: Uint8Array): Promise<void>;
89
+ workerCollect(executionId: Uint8Array): Promise<Uint8Array[]>;
90
+ workerScan(executionId: Uint8Array): Promise<Array<[number, Uint8Array]>>;
91
+ queuePush(executionId: Uint8Array, items: Uint8Array[]): Promise<number>;
92
+ queuePop(executionId: Uint8Array): Promise<Uint8Array | null>;
93
+ queueClear(executionId: Uint8Array): Promise<number>;
94
+ // --- Namespaced key/value state (scoped by execution_id) ---
95
+ stateGet(scopeId: Uint8Array, ns: Uint8Array, key: Uint8Array): Promise<Uint8Array | null>;
96
+ statePut(scopeId: Uint8Array, ns: Uint8Array, key: Uint8Array, value: Uint8Array): Promise<void>;
97
+ // --- Append-only log (scoped by execution_id) ---
98
+ stateAppend(scopeId: Uint8Array, ns: Uint8Array, key: Uint8Array, item: Uint8Array): Promise<number>;
99
+ stateLogScan(
100
+ scopeId: Uint8Array,
101
+ ns: Uint8Array,
102
+ key: Uint8Array,
103
+ afterId?: number,
104
+ limit?: number | null,
105
+ ): Promise<Array<[number, Uint8Array]>>;
106
+ /** Wipe all state + log rows for a scope across every namespace. */
107
+ executionClear(scopeId: Uint8Array): Promise<number>;
108
+ /** Optional cleanup hook. Implementations holding a connection should release it here. */
109
+ close?(): Promise<void> | void;
110
+ }
111
+
112
+ // FrameworkNS — namespaces the framework itself uses (mirrors Python's
113
+ // FrameworkNS enum). User code picks its own arbitrary namespace bytes.
114
+ export const FrameworkNS = {
115
+ BUFFERING_INIT: textBytes("_vgi/buffering_init"),
116
+ STREAMING_FINALIZE: textBytes("_vgi/streaming_finalize"),
117
+ } as const;
118
+
119
+ function textBytes(s: string): Uint8Array {
120
+ return new TextEncoder().encode(s);
121
+ }
122
+
123
+ /** Encode an int as 8-byte little-endian, matching Python's pack_int_key. */
124
+ export function packIntKey(i: number | bigint): Uint8Array {
125
+ const buf = new ArrayBuffer(8);
126
+ new DataView(buf).setBigInt64(0, BigInt(i), true);
127
+ return new Uint8Array(buf);
128
+ }
129
+
130
+ // ============================================================================
131
+ // FunctionStorageSqlite
132
+ // ============================================================================
133
+
134
+ function getDefaultDbPath(): string {
135
+ // VGI_WORKER_SQLITE_PATH=":memory:" picks an in-process backend used by
136
+ // single-process test fixtures (notably the HTTP server fixture) to avoid
137
+ // per-op WAL fsync cost. Mirrors vgi-python's same env var.
138
+ const override = process.env.VGI_WORKER_SQLITE_PATH;
139
+ if (override) return override;
140
+ const { homedir, platform, join, mkdirSync } = _loadNodeIo();
141
+ const home = homedir();
142
+ const stateDir =
143
+ platform() === "darwin"
144
+ ? join(home, "Library", "Application Support", "vgi")
145
+ : join(home, ".local", "state", "vgi");
146
+ mkdirSync(stateDir, { recursive: true });
147
+ return join(stateDir, "vgi_storage.db");
148
+ }
149
+
150
+ // Worker state rides the unified function_state table under a reserved
151
+ // namespace, keyed by the worker/process id (8-byte big-endian) — exactly the
152
+ // mapping the Cloudflare DO client uses, so both backends share one schema.
153
+ const NS_WORKER = new TextEncoder().encode("worker");
154
+
155
+ /** Encode an int64 worker id as an 8-byte big-endian state key. */
156
+ function int64Key(v: number): Uint8Array {
157
+ const b = new Uint8Array(8);
158
+ new DataView(b.buffer).setBigInt64(0, BigInt(v), false);
159
+ return b;
160
+ }
161
+
162
+ function int64FromKey(b: Uint8Array): number {
163
+ if (b.length !== 8) return 0;
164
+ return Number(new DataView(b.buffer, b.byteOffset, 8).getBigInt64(0, false));
165
+ }
166
+
167
+ export class FunctionStorageSqlite implements FunctionStorage {
168
+ readonly dbPath: string;
169
+ private _db: Database;
170
+
171
+ constructor(dbPath?: string) {
172
+ this.dbPath = dbPath ?? getDefaultDbPath();
173
+ // Persistent connection for the lifetime of this storage instance.
174
+ // Bun is single-threaded, so one connection is the analogue of
175
+ // Python's per-thread persistent connection. WAL still coordinates
176
+ // writes across processes via file locking.
177
+ const Db = _loadDatabase();
178
+ this._db = new Db(this.dbPath);
179
+ this._db.exec("PRAGMA journal_mode=WAL");
180
+ this._db.exec("PRAGMA synchronous=NORMAL");
181
+ this._db.exec("PRAGMA busy_timeout=30000");
182
+ this._db.exec("PRAGMA temp_store=MEMORY");
183
+ this._db.exec("PRAGMA cache_size=-65536");
184
+ this._ensureTables();
185
+ }
186
+
187
+ close(): void {
188
+ this._db.close();
189
+ }
190
+
191
+ private _ensureTables(): void {
192
+ const db = this._db;
193
+ // Self-heal an older on-disk DB to the unified minimal schema. The local
194
+ // SQLite tier carries none of the DO's HTTP idempotency machinery and no
195
+ // created_at, so drop any table left over with those columns; all of this
196
+ // is ephemeral in-progress state, so dropping + recreating is safe.
197
+ for (const [table, staleCol] of [
198
+ ["function_state", "created_at"],
199
+ ["function_state_log", "created_at"],
200
+ ["work_queue", "created_at"],
201
+ ] as const) {
202
+ const info = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
203
+ if (info.some((row) => row.name === staleCol)) {
204
+ db.exec(`DROP TABLE IF EXISTS ${table}`);
205
+ }
206
+ }
207
+ // Tables eliminated by the unified schema: worker collect now rides
208
+ // function_state (ns=worker), and the queue carries no registration.
209
+ for (const dead of ["global_state_storage", "worker_state", "invocation_registry", "init_storage"]) {
210
+ db.exec(`DROP TABLE IF EXISTS ${dead}`);
211
+ }
212
+
213
+ // Unified schema — the same three tables every backend uses (the Durable
214
+ // Object adds an HTTP-idempotency column layer on top).
215
+ db.exec(`
216
+ CREATE TABLE IF NOT EXISTS work_queue (
217
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
218
+ execution_id BLOB NOT NULL,
219
+ work_item BLOB NOT NULL
220
+ )
221
+ `);
222
+ db.exec(`
223
+ CREATE INDEX IF NOT EXISTS idx_work_queue_execution
224
+ ON work_queue(execution_id, id)
225
+ `);
226
+ // Composite-key K/V over (scope_id, ns, key): the single home for
227
+ // per-execution / per-transaction / per-group / per-worker state.
228
+ db.exec(`
229
+ CREATE TABLE IF NOT EXISTS function_state (
230
+ scope_id BLOB NOT NULL,
231
+ ns BLOB NOT NULL,
232
+ key BLOB NOT NULL,
233
+ value BLOB NOT NULL,
234
+ PRIMARY KEY (scope_id, ns, key)
235
+ )
236
+ `);
237
+ // Append-only log keyed by (scope, ns, key); the AUTOINCREMENT id is the
238
+ // globally-monotonic scan cursor.
239
+ db.exec(`
240
+ CREATE TABLE IF NOT EXISTS function_state_log (
241
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
242
+ scope_id BLOB NOT NULL,
243
+ ns BLOB NOT NULL,
244
+ key BLOB NOT NULL,
245
+ value BLOB NOT NULL
246
+ )
247
+ `);
248
+ db.exec(`
249
+ CREATE INDEX IF NOT EXISTS idx_function_state_log_lookup
250
+ ON function_state_log(scope_id, ns, key, id)
251
+ `);
252
+ }
253
+
254
+ async stateGet(scopeId: Uint8Array, ns: Uint8Array, key: Uint8Array): Promise<Uint8Array | null> {
255
+ const row = this._db
256
+ .prepare("SELECT value FROM function_state WHERE scope_id = ? AND ns = ? AND key = ?")
257
+ .get(bufferArg(scopeId), bufferArg(ns), bufferArg(key)) as { value: Uint8Array } | null;
258
+ return row ? new Uint8Array(row.value) : null;
259
+ }
260
+
261
+ async statePut(scopeId: Uint8Array, ns: Uint8Array, key: Uint8Array, value: Uint8Array): Promise<void> {
262
+ this._db
263
+ .prepare(
264
+ "INSERT OR REPLACE INTO function_state (scope_id, ns, key, value) VALUES (?, ?, ?, ?)",
265
+ )
266
+ .run(bufferArg(scopeId), bufferArg(ns), bufferArg(key), bufferArg(value));
267
+ }
268
+
269
+ async stateAppend(scopeId: Uint8Array, ns: Uint8Array, key: Uint8Array, item: Uint8Array): Promise<number> {
270
+ const res = this._db
271
+ .prepare("INSERT INTO function_state_log (scope_id, ns, key, value) VALUES (?, ?, ?, ?)")
272
+ .run(bufferArg(scopeId), bufferArg(ns), bufferArg(key), bufferArg(item));
273
+ return Number(res.lastInsertRowid);
274
+ }
275
+
276
+ async stateLogScan(
277
+ scopeId: Uint8Array,
278
+ ns: Uint8Array,
279
+ key: Uint8Array,
280
+ afterId: number = -1,
281
+ limit: number | null = null,
282
+ ): Promise<Array<[number, Uint8Array]>> {
283
+ let sql =
284
+ "SELECT id, value FROM function_state_log " +
285
+ "WHERE scope_id = ? AND ns = ? AND key = ? AND id > ? ORDER BY id ASC";
286
+ const args: any[] = [bufferArg(scopeId), bufferArg(ns), bufferArg(key), afterId];
287
+ if (limit != null) {
288
+ sql += " LIMIT ?";
289
+ args.push(limit);
290
+ }
291
+ const rows = this._db.prepare(sql).all(...args) as Array<{ id: number; value: Uint8Array }>;
292
+ return rows.map((r) => [Number(r.id), new Uint8Array(r.value)]);
293
+ }
294
+
295
+ async executionClear(scopeId: Uint8Array): Promise<number> {
296
+ const eid = bufferArg(scopeId);
297
+ let total = 0;
298
+ total += (this._db.prepare("DELETE FROM function_state WHERE scope_id = ?").run(eid).changes as number);
299
+ total += (this._db.prepare("DELETE FROM function_state_log WHERE scope_id = ?").run(eid).changes as number);
300
+ return total;
301
+ }
302
+
303
+ // Worker state rides function_state under ns=worker, keyed by worker id.
304
+
305
+ async workerPut(
306
+ executionId: Uint8Array,
307
+ workerId: number,
308
+ state: Uint8Array
309
+ ): Promise<void> {
310
+ this._db
311
+ .prepare(
312
+ "INSERT OR REPLACE INTO function_state (scope_id, ns, key, value) VALUES (?, ?, ?, ?)"
313
+ )
314
+ .run(bufferArg(executionId), bufferArg(NS_WORKER), bufferArg(int64Key(workerId)), bufferArg(state));
315
+ }
316
+
317
+ async workerCollect(executionId: Uint8Array): Promise<Uint8Array[]> {
318
+ const rows = this._db
319
+ .prepare(
320
+ "DELETE FROM function_state WHERE scope_id = ? AND ns = ? RETURNING value"
321
+ )
322
+ .all(bufferArg(executionId), bufferArg(NS_WORKER)) as Array<{ value: Uint8Array }>;
323
+ return rows.map((row) => new Uint8Array(row.value));
324
+ }
325
+
326
+ async workerScan(executionId: Uint8Array): Promise<Array<[number, Uint8Array]>> {
327
+ const rows = this._db
328
+ .prepare(
329
+ "SELECT key, value FROM function_state WHERE scope_id = ? AND ns = ?"
330
+ )
331
+ .all(bufferArg(executionId), bufferArg(NS_WORKER)) as Array<{
332
+ key: Uint8Array;
333
+ value: Uint8Array;
334
+ }>;
335
+ return rows.map((row) => [int64FromKey(new Uint8Array(row.key)), new Uint8Array(row.value)]);
336
+ }
337
+
338
+ async queuePush(executionId: Uint8Array, items: Uint8Array[]): Promise<number> {
339
+ const eidBuf = bufferArg(executionId);
340
+ this._db.exec("BEGIN");
341
+ try {
342
+ const insert = this._db.prepare(
343
+ "INSERT INTO work_queue (execution_id, work_item) VALUES (?, ?)"
344
+ );
345
+ for (const item of items) {
346
+ insert.run(eidBuf, bufferArg(item));
347
+ }
348
+ this._db.exec("COMMIT");
349
+ } catch (e) {
350
+ this._db.exec("ROLLBACK");
351
+ throw e;
352
+ }
353
+ return items.length;
354
+ }
355
+
356
+ async queuePop(executionId: Uint8Array): Promise<Uint8Array | null> {
357
+ // No registration: an empty or never-pushed queue both return null,
358
+ // matching the Durable Object.
359
+ const row = this._db
360
+ .prepare(
361
+ "DELETE FROM work_queue WHERE id = (" +
362
+ " SELECT id FROM work_queue WHERE execution_id = ? ORDER BY id ASC LIMIT 1" +
363
+ ") RETURNING work_item"
364
+ )
365
+ .get(bufferArg(executionId)) as { work_item: Uint8Array } | null;
366
+
367
+ return row ? new Uint8Array(row.work_item) : null;
368
+ }
369
+
370
+ async queueClear(executionId: Uint8Array): Promise<number> {
371
+ return this._db
372
+ .prepare("DELETE FROM work_queue WHERE execution_id = ?")
373
+ .run(bufferArg(executionId)).changes;
374
+ }
375
+ }
376
+
377
+ // ============================================================================
378
+ // BoundStorage
379
+ // ============================================================================
380
+
381
+ export class BoundStorage {
382
+ private _base: FunctionStorage;
383
+ private _executionId: Uint8Array;
384
+
385
+ constructor(storage: FunctionStorage, executionId: Uint8Array) {
386
+ this._base = storage;
387
+ this._executionId = executionId;
388
+ }
389
+
390
+ put(state: Uint8Array): Promise<void> {
391
+ return this._base.workerPut(this._executionId, process.pid, state);
392
+ }
393
+
394
+ collect(): Promise<Uint8Array[]> {
395
+ return this._base.workerCollect(this._executionId);
396
+ }
397
+
398
+ workerScan(): Promise<Array<[number, Uint8Array]>> {
399
+ return this._base.workerScan(this._executionId);
400
+ }
401
+
402
+ queuePush(items: Uint8Array[]): Promise<number> {
403
+ return this._base.queuePush(this._executionId, items);
404
+ }
405
+
406
+ queuePop(): Promise<Uint8Array | null> {
407
+ return this._base.queuePop(this._executionId);
408
+ }
409
+
410
+ queueClear(): Promise<number> {
411
+ return this._base.queueClear(this._executionId);
412
+ }
413
+
414
+ stateGet(ns: Uint8Array, key: Uint8Array): Promise<Uint8Array | null> {
415
+ return this._base.stateGet(this._executionId, ns, key);
416
+ }
417
+
418
+ statePut(ns: Uint8Array, key: Uint8Array, value: Uint8Array): Promise<void> {
419
+ return this._base.statePut(this._executionId, ns, key, value);
420
+ }
421
+
422
+ stateAppend(ns: Uint8Array, key: Uint8Array, item: Uint8Array): Promise<number> {
423
+ return this._base.stateAppend(this._executionId, ns, key, item);
424
+ }
425
+
426
+ stateLogScan(
427
+ ns: Uint8Array,
428
+ key: Uint8Array,
429
+ afterId: number = -1,
430
+ limit: number | null = null,
431
+ ): Promise<Array<[number, Uint8Array]>> {
432
+ return this._base.stateLogScan(this._executionId, ns, key, afterId, limit);
433
+ }
434
+
435
+ executionClear(): Promise<number> {
436
+ return this._base.executionClear(this._executionId);
437
+ }
438
+
439
+ static packIntKey(i: number | bigint): Uint8Array {
440
+ return packIntKey(i);
441
+ }
442
+ }
443
+
444
+ // ============================================================================
445
+ // Backend factory + lazy default storage
446
+ // ============================================================================
447
+
448
+ /**
449
+ * Resolve the FunctionStorage backend from environment variables.
450
+ *
451
+ * Mirrors vgi-python's `_resolve_storage` in `vgi/function.py`.
452
+ *
453
+ * `VGI_WORKER_SHARED_STORAGE` selects the backend (default: `sqlite`):
454
+ * - `memory` — `FunctionStorageSqlite` at `:memory:`. Process-local
455
+ * with no cross-process coordination — single-process
456
+ * deployments only. Ignores `VGI_WORKER_SQLITE_PATH`.
457
+ * - `sqlite` — `FunctionStorageSqlite`. Honors `VGI_WORKER_SQLITE_PATH`
458
+ * (including `:memory:`).
459
+ * - `cloudflare-do` — `FunctionStorageCfDo`. Requires `VGI_CF_DO_URL`;
460
+ * optional `VGI_CF_DO_TOKEN` for bearer auth.
461
+ */
462
+ export function resolveStorageFromEnv(): FunctionStorage {
463
+ const backend = (process.env.VGI_WORKER_SHARED_STORAGE ?? "sqlite").toLowerCase();
464
+ if (backend === "memory") {
465
+ return new FunctionStorageSqlite(":memory:");
466
+ }
467
+ if (backend === "sqlite") {
468
+ return new FunctionStorageSqlite();
469
+ }
470
+ if (backend === "cloudflare-do") {
471
+ // Lazy require to avoid loading the CF client when sqlite is in use.
472
+ const { FunctionStorageCfDo } = require("./storage-cf-do.js") as typeof import("./storage-cf-do.js");
473
+ return FunctionStorageCfDo.fromEnv();
474
+ }
475
+ throw new Error(
476
+ `Unknown VGI_WORKER_SHARED_STORAGE backend: '${backend}'. Supported: 'memory', 'sqlite', 'cloudflare-do'.`,
477
+ );
478
+ }
479
+
480
+ // Module-level proxy that lazy-resolves the configured backend on first use.
481
+ // Importing this module no longer opens a SQLite connection — that happens
482
+ // the first time anyone touches a storage method. Matches Python's
483
+ // _DefaultStorageDescriptor lazy attribute pattern.
484
+ let _resolved: FunctionStorage | null = null;
485
+ function getDefaultStorage(): FunctionStorage {
486
+ if (_resolved == null) _resolved = resolveStorageFromEnv();
487
+ return _resolved;
488
+ }
489
+
490
+ /**
491
+ * Override the default backend used by the `storage` singleton. Call once at
492
+ * worker startup to inject a pre-built backend that env-driven selection can't
493
+ * construct — notably a `FunctionStorageCfDo` wired to a Cloudflare
494
+ * service-binding Fetcher (the binding lives on `env`, not `process.env`, and a
495
+ * same-zone public-URL fetch is rejected with CF error 1042). Takes precedence
496
+ * over `VGI_WORKER_SHARED_STORAGE`.
497
+ */
498
+ export function setStorage(impl: FunctionStorage): void {
499
+ _resolved = impl;
500
+ }
501
+
502
+ /** Default FunctionStorage instance. Backend is selected via env on first access. */
503
+ export const storage: FunctionStorage = new Proxy(
504
+ {} as FunctionStorage,
505
+ {
506
+ get(_t, prop, _r) {
507
+ const inst = getDefaultStorage() as any;
508
+ const v = inst[prop];
509
+ return typeof v === "function" ? v.bind(inst) : v;
510
+ },
511
+ },
512
+ );
513
+
514
+ // ============================================================================
515
+ // Helpers
516
+ // ============================================================================
517
+
518
+ /** bun:sqlite needs Buffer for blob params */
519
+ function bufferArg(data: Uint8Array): Buffer {
520
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
521
+ }