@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.
- package/LICENSE +134 -0
- package/README.md +494 -0
- package/dist/__dbg.test-d.d.ts +1 -0
- package/dist/arguments/argument-spec.d.ts +51 -0
- package/dist/arguments/arguments.d.ts +13 -0
- package/dist/arrow/codec/branded.d.ts +83 -0
- package/dist/arrow/codec/registry.d.ts +29 -0
- package/dist/arrow/codec/repr.d.ts +23 -0
- package/dist/arrow/codec/type-descriptors.d.ts +109 -0
- package/dist/arrow/impl-arrowjs/build.d.ts +19 -0
- package/dist/arrow/impl-arrowjs/canonical.d.ts +15 -0
- package/dist/arrow/impl-arrowjs/empty.d.ts +7 -0
- package/dist/arrow/impl-arrowjs/filter.d.ts +11 -0
- package/dist/arrow/impl-arrowjs/index.d.ts +11 -0
- package/dist/arrow/impl-arrowjs/ipc.d.ts +24 -0
- package/dist/arrow/impl-arrowjs/iterate.d.ts +39 -0
- package/dist/arrow/impl-arrowjs/project.d.ts +10 -0
- package/dist/arrow/impl-arrowjs/schema.d.ts +62 -0
- package/dist/arrow/impl-arrowjs/statistics.d.ts +13 -0
- package/dist/arrow/impl-flechette/build.d.ts +14 -0
- package/dist/arrow/impl-flechette/canonical.d.ts +18 -0
- package/dist/arrow/impl-flechette/empty.d.ts +9 -0
- package/dist/arrow/impl-flechette/filter.d.ts +10 -0
- package/dist/arrow/impl-flechette/index.d.ts +11 -0
- package/dist/arrow/impl-flechette/ipc.d.ts +33 -0
- package/dist/arrow/impl-flechette/iterate.d.ts +29 -0
- package/dist/arrow/impl-flechette/message-meta.d.ts +10 -0
- package/dist/arrow/impl-flechette/normalize-type.d.ts +11 -0
- package/dist/arrow/impl-flechette/project.d.ts +3 -0
- package/dist/arrow/impl-flechette/schema.d.ts +47 -0
- package/dist/arrow/impl-flechette/statistics.d.ts +13 -0
- package/dist/arrow/index.d.ts +11 -0
- package/dist/arrow/predicates.d.ts +47 -0
- package/dist/arrow/repr.test-d.d.ts +52 -0
- package/dist/arrow/schema-types.d.ts +71 -0
- package/dist/arrow/types.d.ts +57 -0
- package/dist/catalog/attach-option.d.ts +33 -0
- package/dist/catalog/attach-options.d.ts +30 -0
- package/dist/catalog/composite.d.ts +33 -0
- package/dist/catalog/descriptors.d.ts +154 -0
- package/dist/catalog/interface.d.ts +239 -0
- package/dist/catalog/read-only.d.ts +33 -0
- package/dist/client/client.d.ts +189 -0
- package/dist/client/errors.d.ts +26 -0
- package/dist/client/helpers.d.ts +12 -0
- package/dist/client/protocol.d.ts +16 -0
- package/dist/client/types.d.ts +125 -0
- package/dist/client-entry.d.ts +11 -0
- package/dist/client-entry.js +48 -0
- package/dist/client-entry.js.map +9 -0
- package/dist/codec/asd.d.ts +25 -0
- package/dist/crypto.d.ts +36 -0
- package/dist/errors.d.ts +24 -0
- package/dist/filter-pushdown/collector.d.ts +32 -0
- package/dist/filter-pushdown/deserialize.d.ts +16 -0
- package/dist/filter-pushdown/evaluate.d.ts +51 -0
- package/dist/filter-pushdown/index.d.ts +4 -0
- package/dist/filter-pushdown/types.d.ts +84 -0
- package/dist/functions/aggregate.d.ts +129 -0
- package/dist/functions/copy-from.d.ts +81 -0
- package/dist/functions/copy-to.d.ts +121 -0
- package/dist/functions/registry.d.ts +16 -0
- package/dist/functions/scalar.d.ts +98 -0
- package/dist/functions/storage-cf-do.d.ts +47 -0
- package/dist/functions/storage.d.ts +86 -0
- package/dist/functions/table-buffering.d.ts +99 -0
- package/dist/functions/table-in-out.d.ts +64 -0
- package/dist/functions/table.d.ts +118 -0
- package/dist/functions/types.d.ts +157 -0
- package/dist/generated/vgi-client.d.ts +466 -0
- package/dist/generated/vgi-protocol-schemas.d.ts +115 -0
- package/dist/index.core.d.ts +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10888 -0
- package/dist/index.js.map +85 -0
- package/dist/metadata/resolve.d.ts +3 -0
- package/dist/metadata/serialize.d.ts +12 -0
- package/dist/metadata/types.d.ts +47 -0
- package/dist/protocol/dispatch.d.ts +23 -0
- package/dist/protocol/handlers/aggregate.d.ts +3 -0
- package/dist/protocol/handlers/catalog/admin.d.ts +3 -0
- package/dist/protocol/handlers/catalog/index.d.ts +3 -0
- package/dist/protocol/handlers/catalog/index_methods.d.ts +3 -0
- package/dist/protocol/handlers/catalog/macro.d.ts +3 -0
- package/dist/protocol/handlers/catalog/shared.d.ts +70 -0
- package/dist/protocol/handlers/catalog/table.d.ts +3 -0
- package/dist/protocol/handlers/catalog/view.d.ts +3 -0
- package/dist/protocol/handlers/function.d.ts +8 -0
- package/dist/protocol/handlers/shared.d.ts +31 -0
- package/dist/protocol/handlers/table-buffering.d.ts +3 -0
- package/dist/protocol/serialize.d.ts +4 -0
- package/dist/protocol/serializers/arguments.d.ts +3 -0
- package/dist/protocol/serializers/bind.d.ts +9 -0
- package/dist/protocol/serializers/cardinality.d.ts +7 -0
- package/dist/protocol/serializers/init.d.ts +9 -0
- package/dist/protocol/serializers/shared.d.ts +12 -0
- package/dist/protocol/state-serializer.d.ts +23 -0
- package/dist/protocol/types.d.ts +129 -0
- package/dist/secrets/helpers.d.ts +15 -0
- package/dist/types.d.ts +52 -0
- package/dist/util/arrow/index.d.ts +1 -0
- package/dist/util/bytes.d.ts +2 -0
- package/dist/util/statistics.d.ts +7 -0
- package/dist/worker-cf-entry.d.ts +30 -0
- package/dist/worker-cf-entry.js +8767 -0
- package/dist/worker-cf-entry.js.map +74 -0
- package/dist/worker.d.ts +36 -0
- package/package.json +96 -0
- package/src/arguments/argument-spec.ts +200 -0
- package/src/arguments/arguments.ts +81 -0
- package/src/arrow/codec/branded.ts +206 -0
- package/src/arrow/codec/registry.ts +674 -0
- package/src/arrow/codec/repr.ts +181 -0
- package/src/arrow/codec/type-descriptors.ts +137 -0
- package/src/arrow/impl-arrowjs/build.ts +88 -0
- package/src/arrow/impl-arrowjs/canonical.ts +509 -0
- package/src/arrow/impl-arrowjs/empty.ts +72 -0
- package/src/arrow/impl-arrowjs/filter.ts +53 -0
- package/src/arrow/impl-arrowjs/index.ts +54 -0
- package/src/arrow/impl-arrowjs/ipc.ts +67 -0
- package/src/arrow/impl-arrowjs/iterate.ts +159 -0
- package/src/arrow/impl-arrowjs/project.ts +73 -0
- package/src/arrow/impl-arrowjs/schema.ts +256 -0
- package/src/arrow/impl-arrowjs/statistics.ts +185 -0
- package/src/arrow/impl-flechette/build.ts +65 -0
- package/src/arrow/impl-flechette/canonical.ts +289 -0
- package/src/arrow/impl-flechette/empty.ts +20 -0
- package/src/arrow/impl-flechette/filter.ts +42 -0
- package/src/arrow/impl-flechette/index.ts +54 -0
- package/src/arrow/impl-flechette/ipc.ts +110 -0
- package/src/arrow/impl-flechette/iterate.ts +121 -0
- package/src/arrow/impl-flechette/message-meta.ts +175 -0
- package/src/arrow/impl-flechette/normalize-type.ts +99 -0
- package/src/arrow/impl-flechette/project.ts +64 -0
- package/src/arrow/impl-flechette/schema.ts +202 -0
- package/src/arrow/impl-flechette/statistics.ts +198 -0
- package/src/arrow/index.ts +129 -0
- package/src/arrow/predicates.ts +66 -0
- package/src/arrow/repr.test-d.ts +189 -0
- package/src/arrow/schema-types.ts +213 -0
- package/src/arrow/types.ts +74 -0
- package/src/catalog/attach-option.ts +107 -0
- package/src/catalog/attach-options.ts +79 -0
- package/src/catalog/composite.ts +180 -0
- package/src/catalog/descriptors.ts +164 -0
- package/src/catalog/interface.ts +721 -0
- package/src/catalog/read-only.ts +909 -0
- package/src/client/client.ts +1348 -0
- package/src/client/errors.ts +69 -0
- package/src/client/helpers.ts +56 -0
- package/src/client/protocol.ts +33 -0
- package/src/client/types.ts +140 -0
- package/src/client-entry.ts +112 -0
- package/src/codec/asd.ts +147 -0
- package/src/crypto.ts +143 -0
- package/src/errors.ts +61 -0
- package/src/filter-pushdown/collector.ts +147 -0
- package/src/filter-pushdown/deserialize.ts +253 -0
- package/src/filter-pushdown/evaluate.ts +591 -0
- package/src/filter-pushdown/index.ts +28 -0
- package/src/filter-pushdown/types.ts +91 -0
- package/src/functions/aggregate.ts +384 -0
- package/src/functions/copy-from.ts +282 -0
- package/src/functions/copy-to.ts +319 -0
- package/src/functions/registry.ts +258 -0
- package/src/functions/scalar.ts +356 -0
- package/src/functions/storage-cf-do.ts +303 -0
- package/src/functions/storage.ts +521 -0
- package/src/functions/table-buffering.ts +562 -0
- package/src/functions/table-in-out.ts +373 -0
- package/src/functions/table.ts +521 -0
- package/src/functions/types.ts +192 -0
- package/src/generated/vgi-client.ts +560 -0
- package/src/generated/vgi-protocol-schemas.ts +864 -0
- package/src/index.core.ts +267 -0
- package/src/index.ts +11 -0
- package/src/metadata/resolve.ts +109 -0
- package/src/metadata/serialize.ts +242 -0
- package/src/metadata/types.ts +60 -0
- package/src/protocol/dispatch.ts +52 -0
- package/src/protocol/handlers/aggregate.ts +355 -0
- package/src/protocol/handlers/catalog/admin.ts +368 -0
- package/src/protocol/handlers/catalog/index.ts +25 -0
- package/src/protocol/handlers/catalog/index_methods.ts +65 -0
- package/src/protocol/handlers/catalog/macro.ts +114 -0
- package/src/protocol/handlers/catalog/shared.ts +291 -0
- package/src/protocol/handlers/catalog/table.ts +401 -0
- package/src/protocol/handlers/catalog/view.ts +119 -0
- package/src/protocol/handlers/function.ts +302 -0
- package/src/protocol/handlers/shared.ts +80 -0
- package/src/protocol/handlers/table-buffering.ts +207 -0
- package/src/protocol/serialize.ts +23 -0
- package/src/protocol/serializers/arguments.ts +160 -0
- package/src/protocol/serializers/bind.ts +212 -0
- package/src/protocol/serializers/cardinality.ts +45 -0
- package/src/protocol/serializers/init.ts +247 -0
- package/src/protocol/serializers/shared.ts +46 -0
- package/src/protocol/state-serializer.ts +181 -0
- package/src/protocol/types.ts +153 -0
- package/src/secrets/helpers.ts +70 -0
- package/src/types.ts +71 -0
- package/src/util/arrow/index.ts +23 -0
- package/src/util/bytes.ts +14 -0
- package/src/util/statistics.ts +40 -0
- package/src/worker-cf-entry.ts +172 -0
- 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
|
+
}
|