@ixo/oracle-runtime-workers 0.2.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/README.md +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/* eslint-disable no-console -- console is the logger on workerd (no @ixo/logger); only used for stale-connection warnings */
|
|
2
|
+
/**
|
|
3
|
+
* `DoSqliteDatabase` — a small async wrapper around the wa-sqlite API for one
|
|
4
|
+
* SQLite database file stored through `DoVfs` in a Durable Object's storage.
|
|
5
|
+
*
|
|
6
|
+
* - Rows are plain objects keyed by column name. INTEGER columns come back as
|
|
7
|
+
* `number` (or `bigint` outside the safe-integer range), REAL as `number`,
|
|
8
|
+
* TEXT as `string`, BLOB as `Uint8Array` (copied out of the wasm heap), NULL
|
|
9
|
+
* as `null`.
|
|
10
|
+
* - Parameters are bound with `sqlite3.bind_collection` (positional array or
|
|
11
|
+
* `{ ':name': value }` object). `boolean` binds as 0/1, `undefined` as NULL,
|
|
12
|
+
* and integral numbers outside int32 are bound as INTEGER (via bigint) so
|
|
13
|
+
* e.g. millisecond timestamps keep the INTEGER affinity `better-sqlite3`
|
|
14
|
+
* gives them rather than becoming REAL.
|
|
15
|
+
* - The wa-sqlite API is promise-shaped even for the synchronous build, so
|
|
16
|
+
* every call here is `async`; nothing actually yields to the event loop
|
|
17
|
+
* other than microtasks.
|
|
18
|
+
*/
|
|
19
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
20
|
+
import {
|
|
21
|
+
SQLITE_DONE,
|
|
22
|
+
SQLITE_OPEN_CREATE,
|
|
23
|
+
SQLITE_OPEN_READWRITE,
|
|
24
|
+
SQLITE_ROW,
|
|
25
|
+
} from 'wa-sqlite';
|
|
26
|
+
import {
|
|
27
|
+
DoVfs,
|
|
28
|
+
DoVfsError,
|
|
29
|
+
isDoVfs,
|
|
30
|
+
VACUUM_FILE_SUFFIX,
|
|
31
|
+
VFS_PAGE_SIZE,
|
|
32
|
+
type DoVfsOptions,
|
|
33
|
+
type DoVfsStats,
|
|
34
|
+
type VfsSnapshot,
|
|
35
|
+
} from './do-vfs';
|
|
36
|
+
import {
|
|
37
|
+
getOrRegisterVfs,
|
|
38
|
+
loadSqlite,
|
|
39
|
+
type SqliteRuntime,
|
|
40
|
+
} from './wa-sqlite-loader';
|
|
41
|
+
|
|
42
|
+
export type SqlValue = null | number | bigint | string | Uint8Array;
|
|
43
|
+
export type SqlParam = SqlValue | boolean | undefined;
|
|
44
|
+
export type SqlParams =
|
|
45
|
+
| ReadonlyArray<SqlParam>
|
|
46
|
+
| Readonly<Record<string, SqlParam>>;
|
|
47
|
+
export type SqlRow = Record<string, SqlValue>;
|
|
48
|
+
|
|
49
|
+
export interface RunResult {
|
|
50
|
+
/** Rows changed by the most recent INSERT/UPDATE/DELETE. */
|
|
51
|
+
changes: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DoSqliteOpenOptions extends DoVfsOptions {
|
|
55
|
+
/**
|
|
56
|
+
* PRAGMAs applied after opening. Defaults keep only the main file in
|
|
57
|
+
* storage (`journal_mode=MEMORY`, `temp_store=MEMORY`) and let the VFS own
|
|
58
|
+
* durability (`synchronous=OFF`; see `do-vfs.ts` for why that is safe).
|
|
59
|
+
*/
|
|
60
|
+
pragmas?: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DEFAULT_PRAGMAS = [
|
|
64
|
+
`PRAGMA page_size=${VFS_PAGE_SIZE}`,
|
|
65
|
+
'PRAGMA journal_mode=MEMORY',
|
|
66
|
+
'PRAGMA synchronous=OFF',
|
|
67
|
+
'PRAGMA temp_store=MEMORY',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
export interface DoSqliteContext {
|
|
71
|
+
id: { toString(): string };
|
|
72
|
+
storage: DurableObjectStorage;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** VFS name for a Durable Object — one VFS per object, shared by all files it opens. */
|
|
76
|
+
export function vfsNameForObject(ctx: DoSqliteContext): string {
|
|
77
|
+
return `do-${ctx.id.toString()}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class DoSqliteDatabase {
|
|
81
|
+
private db: number | null;
|
|
82
|
+
private txDepth = 0;
|
|
83
|
+
private savepointSeq = 0;
|
|
84
|
+
/** Serialises top-level transactions (see `transaction()`). */
|
|
85
|
+
private txQueue: Promise<void> = Promise.resolve();
|
|
86
|
+
/** Serialises individual statements on this connection (see `serialized()`). */
|
|
87
|
+
private opQueue: Promise<void> = Promise.resolve();
|
|
88
|
+
/** Set while inside a transaction's async chain — nested calls become savepoints. */
|
|
89
|
+
private readonly txContext = new AsyncLocalStorage<{ depth: number }>();
|
|
90
|
+
|
|
91
|
+
private constructor(
|
|
92
|
+
private readonly runtime: SqliteRuntime,
|
|
93
|
+
db: number,
|
|
94
|
+
readonly vfs: DoVfs,
|
|
95
|
+
readonly fileName: string,
|
|
96
|
+
private readonly pragmas: readonly string[],
|
|
97
|
+
) {
|
|
98
|
+
this.db = db;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Open (creating if needed) `fileName` in the object's storage. The VFS for
|
|
103
|
+
* this object is registered on first use and re-bound to the new storage if
|
|
104
|
+
* the object was re-instantiated inside the same isolate.
|
|
105
|
+
*/
|
|
106
|
+
static async open(
|
|
107
|
+
ctx: DoSqliteContext,
|
|
108
|
+
fileName = 'oracle.db',
|
|
109
|
+
options: DoSqliteOpenOptions = {},
|
|
110
|
+
): Promise<DoSqliteDatabase> {
|
|
111
|
+
const runtime = await loadSqlite();
|
|
112
|
+
const vfsName = vfsNameForObject(ctx);
|
|
113
|
+
let created = false;
|
|
114
|
+
const vfs = getOrRegisterVfs(
|
|
115
|
+
runtime,
|
|
116
|
+
vfsName,
|
|
117
|
+
() => {
|
|
118
|
+
created = true;
|
|
119
|
+
return new DoVfs(vfsName, ctx.storage, options);
|
|
120
|
+
},
|
|
121
|
+
isDoVfs,
|
|
122
|
+
);
|
|
123
|
+
if (!created && !vfs.isAttachedTo(ctx.storage)) {
|
|
124
|
+
// Same object id, new instance: the previous instance was evicted with
|
|
125
|
+
// its connections open. Close them (no storage is touched — pending
|
|
126
|
+
// page state was discarded) so the wasm side doesn't leak, then re-bind.
|
|
127
|
+
for (const stale of vfs.takeConnections()) {
|
|
128
|
+
try {
|
|
129
|
+
await runtime.sqlite3.close(stale);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`[sqlite] failed to close stale connection ${stale} on VFS ${vfsName}`,
|
|
133
|
+
error,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
vfs.attach(ctx.storage);
|
|
138
|
+
}
|
|
139
|
+
if (!created) vfs.configureCache(options.cachePages);
|
|
140
|
+
const pragmas = options.pragmas ?? DEFAULT_PRAGMAS;
|
|
141
|
+
const db = await DoSqliteDatabase.openConnection(
|
|
142
|
+
runtime,
|
|
143
|
+
vfs,
|
|
144
|
+
fileName,
|
|
145
|
+
pragmas,
|
|
146
|
+
);
|
|
147
|
+
return new DoSqliteDatabase(runtime, db, vfs, fileName, pragmas);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static async openConnection(
|
|
151
|
+
runtime: SqliteRuntime,
|
|
152
|
+
vfs: DoVfs,
|
|
153
|
+
fileName: string,
|
|
154
|
+
pragmas: readonly string[],
|
|
155
|
+
): Promise<number> {
|
|
156
|
+
const db = await runtime.sqlite3.open_v2(
|
|
157
|
+
fileName,
|
|
158
|
+
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
|
|
159
|
+
vfs.name,
|
|
160
|
+
);
|
|
161
|
+
vfs.trackConnection(db);
|
|
162
|
+
try {
|
|
163
|
+
for (const pragma of pragmas) {
|
|
164
|
+
for await (const stmt of runtime.sqlite3.statements(db, pragma)) {
|
|
165
|
+
while ((await runtime.sqlite3.step(stmt)) === SQLITE_ROW) {
|
|
166
|
+
// PRAGMA statements may return a row; drain it.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
vfs.untrackConnection(db);
|
|
172
|
+
await runtime.sqlite3.close(db);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
return db;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get isOpen(): boolean {
|
|
179
|
+
return this.db !== null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
get inTransaction(): boolean {
|
|
183
|
+
return this.txDepth > 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** SQLite library version of the loaded build. */
|
|
187
|
+
get sqliteVersion(): string {
|
|
188
|
+
return this.runtime.sqlite3.libversion();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private handle(): number {
|
|
192
|
+
if (this.db === null)
|
|
193
|
+
throw new Error(`database '${this.fileName}' is closed`);
|
|
194
|
+
return this.db;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Run `sql` (one or more statements) and return every row produced. When
|
|
199
|
+
* `params` is given it is bound to each statement that declares parameters.
|
|
200
|
+
* `T` narrows the row shape for the caller; it is not validated at runtime.
|
|
201
|
+
*/
|
|
202
|
+
async exec<T extends SqlRow = SqlRow>(
|
|
203
|
+
sql: string,
|
|
204
|
+
params?: SqlParams,
|
|
205
|
+
): Promise<T[]> {
|
|
206
|
+
return this.serialized(() => this.execUnlocked<T>(sql, params));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Run a statement that produces no rows of interest; returns the change count. */
|
|
210
|
+
async run(sql: string, params?: SqlParams): Promise<RunResult> {
|
|
211
|
+
return this.serialized(() => this.runUnlocked(sql, params));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* One statement at a time per connection. wa-sqlite's prepare/step/finalize
|
|
216
|
+
* cycle is not re-entrant across interleaved async callers (LangGraph reads
|
|
217
|
+
* a checkpoint while another write is mid-flight) — without this the
|
|
218
|
+
* interleaving surfaces as SQLITE_MISUSE ("bad parameter or other API misuse").
|
|
219
|
+
*/
|
|
220
|
+
private async serialized<T>(op: () => Promise<T>): Promise<T> {
|
|
221
|
+
const previous = this.opQueue;
|
|
222
|
+
let release: () => void = () => undefined;
|
|
223
|
+
this.opQueue = new Promise<void>((resolve) => {
|
|
224
|
+
release = resolve;
|
|
225
|
+
});
|
|
226
|
+
await previous;
|
|
227
|
+
try {
|
|
228
|
+
return await op();
|
|
229
|
+
} finally {
|
|
230
|
+
release();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async execUnlocked<T extends SqlRow = SqlRow>(
|
|
235
|
+
sql: string,
|
|
236
|
+
params?: SqlParams,
|
|
237
|
+
): Promise<T[]> {
|
|
238
|
+
const { sqlite3 } = this.runtime;
|
|
239
|
+
const db = this.handle();
|
|
240
|
+
const rows: T[] = [];
|
|
241
|
+
const bindings = params === undefined ? undefined : toBindings(params);
|
|
242
|
+
try {
|
|
243
|
+
for await (const stmt of sqlite3.statements(db, sql)) {
|
|
244
|
+
if (bindings !== undefined && sqlite3.bind_parameter_count(stmt) > 0) {
|
|
245
|
+
sqlite3.bind_collection(stmt, bindings);
|
|
246
|
+
}
|
|
247
|
+
let columns: string[] | undefined;
|
|
248
|
+
while ((await sqlite3.step(stmt)) === SQLITE_ROW) {
|
|
249
|
+
columns ??= sqlite3.column_names(stmt);
|
|
250
|
+
rows.push(toRow<T>(columns, sqlite3.row(stmt)));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
throw this.withVfsCause(error);
|
|
255
|
+
}
|
|
256
|
+
this.throwIfFlushFailed();
|
|
257
|
+
return rows;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private async runUnlocked(
|
|
261
|
+
sql: string,
|
|
262
|
+
params?: SqlParams,
|
|
263
|
+
): Promise<RunResult> {
|
|
264
|
+
const { sqlite3 } = this.runtime;
|
|
265
|
+
const db = this.handle();
|
|
266
|
+
const bindings = params === undefined ? undefined : toBindings(params);
|
|
267
|
+
try {
|
|
268
|
+
for await (const stmt of sqlite3.statements(db, sql)) {
|
|
269
|
+
if (bindings !== undefined && sqlite3.bind_parameter_count(stmt) > 0) {
|
|
270
|
+
sqlite3.bind_collection(stmt, bindings);
|
|
271
|
+
}
|
|
272
|
+
let rc: number;
|
|
273
|
+
do {
|
|
274
|
+
rc = await sqlite3.step(stmt);
|
|
275
|
+
} while (rc !== SQLITE_DONE);
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
throw this.withVfsCause(error);
|
|
279
|
+
}
|
|
280
|
+
this.throwIfFlushFailed();
|
|
281
|
+
return { changes: sqlite3.changes(db) };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** First row of `sql`, or undefined. */
|
|
285
|
+
async get<T extends SqlRow = SqlRow>(
|
|
286
|
+
sql: string,
|
|
287
|
+
params?: SqlParams,
|
|
288
|
+
): Promise<T | undefined> {
|
|
289
|
+
const rows = await this.exec<T>(sql, params);
|
|
290
|
+
return rows[0];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Run `fn` inside a transaction. Top-level transactions are **serialised**
|
|
295
|
+
* through a mutex — a Durable Object is single-threaded but async, and
|
|
296
|
+
* LangGraph issues `put`/`putWrites` concurrently, so two callers awaiting
|
|
297
|
+
* `BEGIN` at the same time would otherwise collide ("cannot start a
|
|
298
|
+
* transaction within a transaction"). True nesting (a `transaction()` call
|
|
299
|
+
* made *inside* `fn`'s async chain) is detected with AsyncLocalStorage and
|
|
300
|
+
* becomes a SAVEPOINT. Commits when `fn` resolves, rolls back and rethrows
|
|
301
|
+
* when it rejects. The VFS flushes committed pages to storage atomically
|
|
302
|
+
* when SQLite releases the write lock at COMMIT.
|
|
303
|
+
*/
|
|
304
|
+
async transaction<T>(fn: () => Promise<T>): Promise<T> {
|
|
305
|
+
if (this.txContext.getStore() !== undefined) {
|
|
306
|
+
const savepoint = `sp_${++this.savepointSeq}`;
|
|
307
|
+
await this.run(`SAVEPOINT ${savepoint}`);
|
|
308
|
+
this.txDepth++;
|
|
309
|
+
try {
|
|
310
|
+
const result = await fn();
|
|
311
|
+
this.txDepth--;
|
|
312
|
+
await this.run(`RELEASE ${savepoint}`);
|
|
313
|
+
return result;
|
|
314
|
+
} catch (error) {
|
|
315
|
+
this.txDepth--;
|
|
316
|
+
try {
|
|
317
|
+
await this.run(`ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`);
|
|
318
|
+
} catch {
|
|
319
|
+
// The original error is more useful than a rollback failure.
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const previous = this.txQueue;
|
|
326
|
+
let release: () => void = () => undefined;
|
|
327
|
+
this.txQueue = new Promise<void>((resolve) => {
|
|
328
|
+
release = resolve;
|
|
329
|
+
});
|
|
330
|
+
await previous;
|
|
331
|
+
try {
|
|
332
|
+
return await this.txContext.run({ depth: 1 }, async () => {
|
|
333
|
+
await this.run('BEGIN IMMEDIATE');
|
|
334
|
+
this.txDepth++;
|
|
335
|
+
try {
|
|
336
|
+
const result = await fn();
|
|
337
|
+
this.txDepth--;
|
|
338
|
+
await this.run('COMMIT');
|
|
339
|
+
return result;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
this.txDepth--;
|
|
342
|
+
try {
|
|
343
|
+
await this.run('ROLLBACK');
|
|
344
|
+
} catch {
|
|
345
|
+
// The original error is more useful than a rollback failure.
|
|
346
|
+
}
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
} finally {
|
|
351
|
+
release();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Run `fn` while HOLDING the transaction mutex without opening a
|
|
357
|
+
* transaction — for statements SQLite refuses to run inside one (`VACUUM`).
|
|
358
|
+
* Transactions started elsewhere queue behind it; statements inside `fn`
|
|
359
|
+
* still serialise through the per-statement queue as usual.
|
|
360
|
+
*/
|
|
361
|
+
async withoutTransactions<T>(fn: () => Promise<T>): Promise<T> {
|
|
362
|
+
if (this.txContext.getStore() !== undefined) {
|
|
363
|
+
throw new DoVfsError(
|
|
364
|
+
'withoutTransactions() cannot run inside a transaction',
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const previous = this.txQueue;
|
|
368
|
+
let release: () => void = () => undefined;
|
|
369
|
+
this.txQueue = new Promise<void>((resolve) => {
|
|
370
|
+
release = resolve;
|
|
371
|
+
});
|
|
372
|
+
await previous;
|
|
373
|
+
try {
|
|
374
|
+
return await fn();
|
|
375
|
+
} finally {
|
|
376
|
+
release();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Destroy `fileName` in this object's storage regardless of connection
|
|
382
|
+
* state: force-closes every SQLite connection tracked on the object's VFS,
|
|
383
|
+
* clears its open-file registry, and deletes the pages. For "drop the
|
|
384
|
+
* working copy" flows (owner file deleted upstream, tests) where a leaked
|
|
385
|
+
* handle must not wedge the object forever.
|
|
386
|
+
*/
|
|
387
|
+
static async wipe(
|
|
388
|
+
ctx: DoSqliteContext,
|
|
389
|
+
fileName = 'oracle.db',
|
|
390
|
+
): Promise<void> {
|
|
391
|
+
const runtime = await loadSqlite();
|
|
392
|
+
const vfsName = vfsNameForObject(ctx);
|
|
393
|
+
const vfs = getOrRegisterVfs(
|
|
394
|
+
runtime,
|
|
395
|
+
vfsName,
|
|
396
|
+
() => new DoVfs(vfsName, ctx.storage),
|
|
397
|
+
isDoVfs,
|
|
398
|
+
);
|
|
399
|
+
for (const handle of vfs.takeConnections()) {
|
|
400
|
+
try {
|
|
401
|
+
await runtime.sqlite3.close(handle);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.warn(
|
|
404
|
+
`[sqlite] wipe: failed to close connection ${handle}`,
|
|
405
|
+
error,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Re-attach to the same storage: clears the open-file registry so a
|
|
410
|
+
// leaked xOpen handle cannot block the delete.
|
|
411
|
+
vfs.attach(ctx.storage);
|
|
412
|
+
vfs.deleteFile(fileName);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Close the connection. Buffered pages are flushed by the VFS on close. */
|
|
416
|
+
async close(): Promise<void> {
|
|
417
|
+
if (this.db === null) return;
|
|
418
|
+
const db = this.db;
|
|
419
|
+
this.db = null;
|
|
420
|
+
this.txDepth = 0;
|
|
421
|
+
this.vfs.untrackConnection(db);
|
|
422
|
+
await this.runtime.sqlite3.close(db);
|
|
423
|
+
this.throwIfFlushFailed();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** The database file bytes (`SQLite format 3\0`...). Not allowed inside a transaction. */
|
|
427
|
+
async export(): Promise<Uint8Array> {
|
|
428
|
+
if (this.txDepth > 0)
|
|
429
|
+
throw new DoVfsError('export() is not allowed inside a transaction');
|
|
430
|
+
return this.vfs.exportFile(this.fileName);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Replace the database with `bytes` (a SQLite file, e.g. one written by the
|
|
435
|
+
* Node runtime). Closes the connection, swaps the pages in one storage
|
|
436
|
+
* transaction and reopens with the same pragmas.
|
|
437
|
+
*/
|
|
438
|
+
async import(bytes: Uint8Array): Promise<void> {
|
|
439
|
+
if (this.txDepth > 0)
|
|
440
|
+
throw new DoVfsError('import() is not allowed inside a transaction');
|
|
441
|
+
const wasOpen = this.db !== null;
|
|
442
|
+
await this.close();
|
|
443
|
+
this.vfs.importFile(this.fileName, bytes);
|
|
444
|
+
if (wasOpen) {
|
|
445
|
+
this.db = await DoSqliteDatabase.openConnection(
|
|
446
|
+
this.runtime,
|
|
447
|
+
this.vfs,
|
|
448
|
+
this.fileName,
|
|
449
|
+
this.pragmas,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Hex SHA-256 of `export()`. */
|
|
455
|
+
async checksum(): Promise<string> {
|
|
456
|
+
if (this.txDepth > 0)
|
|
457
|
+
throw new DoVfsError('checksum() is not allowed inside a transaction');
|
|
458
|
+
return this.vfs.fileChecksum(this.fileName);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Replace the file with a streamed SQLite image (see
|
|
463
|
+
* `DoVfs.importFromStream`). Reopens the connection afterwards.
|
|
464
|
+
*/
|
|
465
|
+
async importFromStream(stream: ReadableStream<Uint8Array>): Promise<number> {
|
|
466
|
+
if (this.txDepth > 0)
|
|
467
|
+
throw new DoVfsError(
|
|
468
|
+
'importFromStream() is not allowed inside a transaction',
|
|
469
|
+
);
|
|
470
|
+
const wasOpen = this.db !== null;
|
|
471
|
+
await this.close();
|
|
472
|
+
try {
|
|
473
|
+
return await this.vfs.importFromStream(this.fileName, stream);
|
|
474
|
+
} finally {
|
|
475
|
+
// Reopen whether the import landed or was rejected: the caller keeps
|
|
476
|
+
// using this handle either way (a rejected stream leaves the file as it was).
|
|
477
|
+
if (wasOpen) {
|
|
478
|
+
this.db = await DoSqliteDatabase.openConnection(
|
|
479
|
+
this.runtime,
|
|
480
|
+
this.vfs,
|
|
481
|
+
this.fileName,
|
|
482
|
+
this.pragmas,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* A consistent, re-readable view of the file as of now (see
|
|
490
|
+
* `VfsSnapshot`). Waits for an in-flight transaction to finish, pins the
|
|
491
|
+
* content, then releases the transaction lock — later commits proceed
|
|
492
|
+
* while the snapshot is read. The caller MUST `close()` it.
|
|
493
|
+
*/
|
|
494
|
+
async snapshot(): Promise<VfsSnapshot> {
|
|
495
|
+
return this.withoutTransactions(async () =>
|
|
496
|
+
this.vfs.openSnapshot(this.fileName),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Flush count that changed the file — see `DoVfs.writeGeneration`. */
|
|
501
|
+
get writeGeneration(): number {
|
|
502
|
+
return this.vfs.writeGeneration(this.fileName);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** `PRAGMA freelist_count` / `page_count`: how much of the file is free. */
|
|
506
|
+
async pageUsage(): Promise<{ pageCount: number; freelistCount: number }> {
|
|
507
|
+
const pages = await this.get<{ page_count: number }>('PRAGMA page_count');
|
|
508
|
+
const free = await this.get<{ freelist_count: number }>(
|
|
509
|
+
'PRAGMA freelist_count',
|
|
510
|
+
);
|
|
511
|
+
return {
|
|
512
|
+
pageCount: Number(pages?.page_count ?? 0),
|
|
513
|
+
freelistCount: Number(free?.freelist_count ?? 0),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** The connection's `PRAGMA cache_size` (negative = KiB, positive = pages). */
|
|
518
|
+
async cacheSizePragma(): Promise<number> {
|
|
519
|
+
const row = await this.get<{ cache_size: number }>('PRAGMA cache_size');
|
|
520
|
+
return Number(row?.cache_size ?? 0);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Rebuild the file without its free pages at constant memory: `VACUUM INTO`
|
|
525
|
+
* a spill file (its pages go to storage every couple of MiB instead of
|
|
526
|
+
* being held until commit — see the VFS header), then swap it into place
|
|
527
|
+
* in one storage transaction and reopen. Holds the transaction lock for
|
|
528
|
+
* the duration; must not run inside a transaction or while a snapshot of
|
|
529
|
+
* the file is open. Transiently needs 2× the file in DO storage.
|
|
530
|
+
*/
|
|
531
|
+
async compact(): Promise<{ beforeBytes: number; afterBytes: number }> {
|
|
532
|
+
if (this.txDepth > 0)
|
|
533
|
+
throw new DoVfsError('compact() is not allowed inside a transaction');
|
|
534
|
+
const target = `${this.fileName}${VACUUM_FILE_SUFFIX}`;
|
|
535
|
+
return this.withoutTransactions(async () => {
|
|
536
|
+
const beforeBytes = this.fileSize;
|
|
537
|
+
if (this.vfs.fileExists(target)) this.vfs.deleteFile(target);
|
|
538
|
+
// `fileName` is a constant of the runtime, never user input.
|
|
539
|
+
await this.run(`VACUUM INTO '${target.replace(/'/g, "''")}'`);
|
|
540
|
+
await this.close();
|
|
541
|
+
try {
|
|
542
|
+
this.vfs.renameFile(target, this.fileName);
|
|
543
|
+
} finally {
|
|
544
|
+
this.db = await DoSqliteDatabase.openConnection(
|
|
545
|
+
this.runtime,
|
|
546
|
+
this.vfs,
|
|
547
|
+
this.fileName,
|
|
548
|
+
this.pragmas,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
return { beforeBytes, afterBytes: this.fileSize };
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Logical file size in bytes. */
|
|
556
|
+
get fileSize(): number {
|
|
557
|
+
return this.vfs.fileSize(this.fileName) ?? 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
vfsStats(): DoVfsStats {
|
|
561
|
+
return this.vfs.getStats();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Bytes of wasm linear memory currently allocated (grows, never shrinks). */
|
|
565
|
+
get wasmHeapBytes(): number {
|
|
566
|
+
return this.runtime.module.HEAPU8.byteLength;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* SQLite reports VFS failures as bare "disk I/O error"s; when the VFS
|
|
571
|
+
* recorded the underlying exception, attach it as the error's `cause` and
|
|
572
|
+
* mention it in the message so the real reason is visible.
|
|
573
|
+
*/
|
|
574
|
+
private withVfsCause(error: unknown): unknown {
|
|
575
|
+
const cause = this.vfs.lastFlushError;
|
|
576
|
+
if (cause === undefined || !(error instanceof Error)) return error;
|
|
577
|
+
this.vfs.lastFlushError = undefined;
|
|
578
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
579
|
+
const wrapped = new Error(`${error.message} (VFS: ${causeMessage})`, {
|
|
580
|
+
cause,
|
|
581
|
+
});
|
|
582
|
+
wrapped.name = error.name;
|
|
583
|
+
return wrapped;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private throwIfFlushFailed(): void {
|
|
587
|
+
const error = this.vfs.lastFlushError;
|
|
588
|
+
if (error === undefined) return;
|
|
589
|
+
this.vfs.lastFlushError = undefined;
|
|
590
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// -----------------------------------------------------------------------------
|
|
595
|
+
// value conversion
|
|
596
|
+
// -----------------------------------------------------------------------------
|
|
597
|
+
|
|
598
|
+
const INT32_MAX = 0x7fffffff;
|
|
599
|
+
const INT32_MIN = -0x80000000;
|
|
600
|
+
|
|
601
|
+
function toBinding(value: SqlParam): SQLiteCompatibleType | null {
|
|
602
|
+
if (value === undefined) return null;
|
|
603
|
+
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
604
|
+
if (
|
|
605
|
+
typeof value === 'number' &&
|
|
606
|
+
Number.isInteger(value) &&
|
|
607
|
+
(value > INT32_MAX || value < INT32_MIN)
|
|
608
|
+
) {
|
|
609
|
+
return BigInt(value);
|
|
610
|
+
}
|
|
611
|
+
return value;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function toBindings(
|
|
615
|
+
params: SqlParams,
|
|
616
|
+
):
|
|
617
|
+
| Array<SQLiteCompatibleType | null>
|
|
618
|
+
| Record<string, SQLiteCompatibleType | null> {
|
|
619
|
+
if (Array.isArray(params)) {
|
|
620
|
+
return (params as ReadonlyArray<SqlParam>).map(toBinding);
|
|
621
|
+
}
|
|
622
|
+
const out: Record<string, SQLiteCompatibleType | null> = {};
|
|
623
|
+
for (const [key, value] of Object.entries(
|
|
624
|
+
params as Readonly<Record<string, SqlParam>>,
|
|
625
|
+
)) {
|
|
626
|
+
out[key] = toBinding(value);
|
|
627
|
+
}
|
|
628
|
+
return out;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function toRow<T extends SqlRow>(
|
|
632
|
+
columns: string[],
|
|
633
|
+
values: Array<SQLiteCompatibleType | null>,
|
|
634
|
+
): T {
|
|
635
|
+
const row: SqlRow = {};
|
|
636
|
+
for (let i = 0; i < columns.length; i++) {
|
|
637
|
+
const name = columns[i];
|
|
638
|
+
if (name === undefined) continue;
|
|
639
|
+
const value = values[i];
|
|
640
|
+
row[name] = Array.isArray(value) ? Uint8Array.from(value) : (value ?? null);
|
|
641
|
+
}
|
|
642
|
+
return row as T;
|
|
643
|
+
}
|