@rivetkit/sqlite-wasm 2.2.1-pr.4600.b74ff3b
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 +203 -0
- package/dist/schemas/file-meta/v1.ts +43 -0
- package/dist/tsup/index.cjs +1557 -0
- package/dist/tsup/index.cjs.map +1 -0
- package/dist/tsup/index.d.cts +253 -0
- package/dist/tsup/index.d.ts +253 -0
- package/dist/tsup/index.js +1557 -0
- package/dist/tsup/index.js.map +1 -0
- package/package.json +49 -0
- package/schemas/file-meta/mod.ts +2 -0
- package/schemas/file-meta/v1.bare +7 -0
- package/schemas/file-meta/versioned.ts +25 -0
- package/src/generated/empty-db-page.ts +23 -0
- package/src/index.ts +5 -0
- package/src/kv.ts +116 -0
- package/src/pool.ts +502 -0
- package/src/types.ts +20 -0
- package/src/vfs.ts +1646 -0
- package/src/wasm.d.ts +60 -0
|
@@ -0,0 +1,1557 @@
|
|
|
1
|
+
// src/vfs.ts
|
|
2
|
+
import * as VFS from "@rivetkit/sqlite/src/VFS.js";
|
|
3
|
+
import {
|
|
4
|
+
Factory,
|
|
5
|
+
SQLITE_OPEN_CREATE as SQLITE_OPEN_CREATE2,
|
|
6
|
+
SQLITE_OPEN_READWRITE,
|
|
7
|
+
SQLITE_ROW
|
|
8
|
+
} from "@rivetkit/sqlite";
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { pathToFileURL } from "url";
|
|
13
|
+
|
|
14
|
+
// src/kv.ts
|
|
15
|
+
var CHUNK_SIZE = 4096;
|
|
16
|
+
var SQLITE_PREFIX = 8;
|
|
17
|
+
var SQLITE_SCHEMA_VERSION = 1;
|
|
18
|
+
var META_PREFIX = 0;
|
|
19
|
+
var CHUNK_PREFIX = 1;
|
|
20
|
+
var FILE_TAG_MAIN = 0;
|
|
21
|
+
var FILE_TAG_JOURNAL = 1;
|
|
22
|
+
var FILE_TAG_WAL = 2;
|
|
23
|
+
var FILE_TAG_SHM = 3;
|
|
24
|
+
function getMetaKey(fileTag) {
|
|
25
|
+
const key = new Uint8Array(4);
|
|
26
|
+
key[0] = SQLITE_PREFIX;
|
|
27
|
+
key[1] = SQLITE_SCHEMA_VERSION;
|
|
28
|
+
key[2] = META_PREFIX;
|
|
29
|
+
key[3] = fileTag;
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
function getChunkKey(fileTag, chunkIndex) {
|
|
33
|
+
const key = new Uint8Array(8);
|
|
34
|
+
key[0] = SQLITE_PREFIX;
|
|
35
|
+
key[1] = SQLITE_SCHEMA_VERSION;
|
|
36
|
+
key[2] = CHUNK_PREFIX;
|
|
37
|
+
key[3] = fileTag;
|
|
38
|
+
key[4] = chunkIndex >>> 24 & 255;
|
|
39
|
+
key[5] = chunkIndex >>> 16 & 255;
|
|
40
|
+
key[6] = chunkIndex >>> 8 & 255;
|
|
41
|
+
key[7] = chunkIndex & 255;
|
|
42
|
+
return key;
|
|
43
|
+
}
|
|
44
|
+
function getChunkKeyRangeEnd(fileTag) {
|
|
45
|
+
const key = new Uint8Array(4);
|
|
46
|
+
key[0] = SQLITE_PREFIX;
|
|
47
|
+
key[1] = SQLITE_SCHEMA_VERSION;
|
|
48
|
+
key[2] = CHUNK_PREFIX;
|
|
49
|
+
key[3] = fileTag + 1;
|
|
50
|
+
return key;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/generated/empty-db-page.ts
|
|
54
|
+
var HEADER_PREFIX = new Uint8Array([83, 81, 76, 105, 116, 101, 32, 102, 111, 114, 109, 97, 116, 32, 51, 0, 16, 0, 1, 1, 0, 64, 32, 32, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 46, 138, 17, 13, 0, 0, 0, 0, 16, 0, 0]);
|
|
55
|
+
var EMPTY_DB_PAGE = (() => {
|
|
56
|
+
const page = new Uint8Array(4096);
|
|
57
|
+
page.set(HEADER_PREFIX);
|
|
58
|
+
return page;
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
// schemas/file-meta/versioned.ts
|
|
62
|
+
import { createVersionedDataHandler } from "vbare";
|
|
63
|
+
|
|
64
|
+
// dist/schemas/file-meta/v1.ts
|
|
65
|
+
import * as bare from "@rivetkit/bare-ts";
|
|
66
|
+
var config = /* @__PURE__ */ bare.Config({});
|
|
67
|
+
function readFileMeta(bc) {
|
|
68
|
+
return {
|
|
69
|
+
size: bare.readU64(bc)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function writeFileMeta(bc, x) {
|
|
73
|
+
bare.writeU64(bc, x.size);
|
|
74
|
+
}
|
|
75
|
+
function encodeFileMeta(x) {
|
|
76
|
+
const bc = new bare.ByteCursor(
|
|
77
|
+
new Uint8Array(config.initialBufferLength),
|
|
78
|
+
config
|
|
79
|
+
);
|
|
80
|
+
writeFileMeta(bc, x);
|
|
81
|
+
return new Uint8Array(bc.view.buffer, bc.view.byteOffset, bc.offset);
|
|
82
|
+
}
|
|
83
|
+
function decodeFileMeta(bytes) {
|
|
84
|
+
const bc = new bare.ByteCursor(bytes, config);
|
|
85
|
+
const result = readFileMeta(bc);
|
|
86
|
+
if (bc.offset < bc.view.byteLength) {
|
|
87
|
+
throw new bare.BareError(bc.offset, "remaining bytes");
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// schemas/file-meta/versioned.ts
|
|
93
|
+
var CURRENT_VERSION = 1;
|
|
94
|
+
var FILE_META_VERSIONED = createVersionedDataHandler({
|
|
95
|
+
deserializeVersion: (bytes, version) => {
|
|
96
|
+
switch (version) {
|
|
97
|
+
case 1:
|
|
98
|
+
return decodeFileMeta(bytes);
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(`Unknown version ${version}`);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
serializeVersion: (data, version) => {
|
|
104
|
+
switch (version) {
|
|
105
|
+
case 1:
|
|
106
|
+
return encodeFileMeta(data);
|
|
107
|
+
default:
|
|
108
|
+
throw new Error(`Unknown version ${version}`);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
deserializeConverters: () => [],
|
|
112
|
+
serializeConverters: () => []
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// src/vfs.ts
|
|
116
|
+
function createNodeRequire() {
|
|
117
|
+
return createRequire(
|
|
118
|
+
path.join(process.cwd(), "__rivetkit_sqlite_require__.cjs")
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
var TEXT_ENCODER = new TextEncoder();
|
|
122
|
+
var TEXT_DECODER = new TextDecoder();
|
|
123
|
+
var SQLITE_MAX_PATHNAME_BYTES = 64;
|
|
124
|
+
var UINT32_SIZE = 4294967296;
|
|
125
|
+
var MAX_CHUNK_INDEX = 4294967295;
|
|
126
|
+
var MAX_FILE_SIZE_BYTES = (MAX_CHUNK_INDEX + 1) * CHUNK_SIZE;
|
|
127
|
+
var MAX_FILE_SIZE_HI32 = Math.floor(MAX_FILE_SIZE_BYTES / UINT32_SIZE);
|
|
128
|
+
var MAX_FILE_SIZE_LO32 = MAX_FILE_SIZE_BYTES % UINT32_SIZE;
|
|
129
|
+
var KV_MAX_BATCH_KEYS = 128;
|
|
130
|
+
var SQLITE_IOCAP_BATCH_ATOMIC = 16384;
|
|
131
|
+
var SQLITE_FCNTL_BEGIN_ATOMIC_WRITE = 31;
|
|
132
|
+
var SQLITE_FCNTL_COMMIT_ATOMIC_WRITE = 32;
|
|
133
|
+
var SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE = 33;
|
|
134
|
+
var SQLITE_ASYNC_METHODS = /* @__PURE__ */ new Set([
|
|
135
|
+
"xOpen",
|
|
136
|
+
"xClose",
|
|
137
|
+
"xRead",
|
|
138
|
+
"xWrite",
|
|
139
|
+
"xTruncate",
|
|
140
|
+
"xSync",
|
|
141
|
+
"xFileSize",
|
|
142
|
+
"xDelete",
|
|
143
|
+
"xAccess",
|
|
144
|
+
"xFileControl"
|
|
145
|
+
]);
|
|
146
|
+
function isSqliteEsmFactory(value) {
|
|
147
|
+
return typeof value === "function";
|
|
148
|
+
}
|
|
149
|
+
function isSQLiteModule(value) {
|
|
150
|
+
if (!value || typeof value !== "object") {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const candidate = value;
|
|
154
|
+
return typeof candidate.UTF8ToString === "function" && candidate.HEAPU8 instanceof Uint8Array;
|
|
155
|
+
}
|
|
156
|
+
async function loadSqliteRuntime(wasmModule) {
|
|
157
|
+
const require2 = createNodeRequire();
|
|
158
|
+
const sqliteModulePath = require2.resolve(
|
|
159
|
+
["@rivetkit/sqlite", "dist", "wa-sqlite-async.mjs"].join("/")
|
|
160
|
+
);
|
|
161
|
+
const sqliteModule = await nativeDynamicImport(
|
|
162
|
+
pathToFileURL(sqliteModulePath).href
|
|
163
|
+
);
|
|
164
|
+
if (!isSqliteEsmFactory(sqliteModule.default)) {
|
|
165
|
+
throw new Error("Invalid SQLite ESM factory export");
|
|
166
|
+
}
|
|
167
|
+
const sqliteEsmFactory = sqliteModule.default;
|
|
168
|
+
let module;
|
|
169
|
+
if (wasmModule) {
|
|
170
|
+
module = await sqliteEsmFactory({
|
|
171
|
+
instantiateWasm(imports, receiveInstance) {
|
|
172
|
+
WebAssembly.instantiate(wasmModule, imports).then(
|
|
173
|
+
(instance) => {
|
|
174
|
+
receiveInstance(instance);
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
const sqliteDistPath = "@rivetkit/sqlite/dist/";
|
|
182
|
+
const wasmPath = require2.resolve(
|
|
183
|
+
sqliteDistPath + "wa-sqlite-async.wasm"
|
|
184
|
+
);
|
|
185
|
+
const wasmBinary = readFileSync(wasmPath);
|
|
186
|
+
module = await sqliteEsmFactory({ wasmBinary });
|
|
187
|
+
}
|
|
188
|
+
if (!isSQLiteModule(module)) {
|
|
189
|
+
throw new Error("Invalid SQLite runtime module");
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
sqlite3: Factory(module),
|
|
193
|
+
module
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function nativeDynamicImport(specifier) {
|
|
197
|
+
try {
|
|
198
|
+
return await import(specifier);
|
|
199
|
+
} catch (directError) {
|
|
200
|
+
const importer = new Function(
|
|
201
|
+
"moduleSpecifier",
|
|
202
|
+
"return import(moduleSpecifier);"
|
|
203
|
+
);
|
|
204
|
+
try {
|
|
205
|
+
return await importer(specifier);
|
|
206
|
+
} catch {
|
|
207
|
+
throw directError;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function encodeFileMeta2(size) {
|
|
212
|
+
const meta = { size: BigInt(size) };
|
|
213
|
+
return FILE_META_VERSIONED.serializeWithEmbeddedVersion(
|
|
214
|
+
meta,
|
|
215
|
+
CURRENT_VERSION
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
function decodeFileMeta2(data) {
|
|
219
|
+
const meta = FILE_META_VERSIONED.deserializeWithEmbeddedVersion(data);
|
|
220
|
+
return Number(meta.size);
|
|
221
|
+
}
|
|
222
|
+
function isValidFileSize(size) {
|
|
223
|
+
return Number.isSafeInteger(size) && size >= 0 && size <= MAX_FILE_SIZE_BYTES;
|
|
224
|
+
}
|
|
225
|
+
var AsyncMutex = class {
|
|
226
|
+
#locked = false;
|
|
227
|
+
#waiting = [];
|
|
228
|
+
async acquire() {
|
|
229
|
+
while (this.#locked) {
|
|
230
|
+
await new Promise((resolve) => this.#waiting.push(resolve));
|
|
231
|
+
}
|
|
232
|
+
this.#locked = true;
|
|
233
|
+
}
|
|
234
|
+
release() {
|
|
235
|
+
this.#locked = false;
|
|
236
|
+
const next = this.#waiting.shift();
|
|
237
|
+
if (next) {
|
|
238
|
+
next();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async run(fn) {
|
|
242
|
+
await this.acquire();
|
|
243
|
+
try {
|
|
244
|
+
return await fn();
|
|
245
|
+
} finally {
|
|
246
|
+
this.release();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
var Database = class {
|
|
251
|
+
#sqlite3;
|
|
252
|
+
#handle;
|
|
253
|
+
#fileName;
|
|
254
|
+
#onClose;
|
|
255
|
+
#sqliteMutex;
|
|
256
|
+
#closed = false;
|
|
257
|
+
constructor(sqlite3, handle, fileName, onClose, sqliteMutex) {
|
|
258
|
+
this.#sqlite3 = sqlite3;
|
|
259
|
+
this.#handle = handle;
|
|
260
|
+
this.#fileName = fileName;
|
|
261
|
+
this.#onClose = onClose;
|
|
262
|
+
this.#sqliteMutex = sqliteMutex;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Execute SQL with optional row callback
|
|
266
|
+
* @param sql - SQL statement to execute
|
|
267
|
+
* @param callback - Called for each result row with (row, columns)
|
|
268
|
+
*/
|
|
269
|
+
async exec(sql, callback) {
|
|
270
|
+
await this.#sqliteMutex.run(async () => {
|
|
271
|
+
await this.#sqlite3.exec(this.#handle, sql, callback);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Execute a parameterized SQL statement (no result rows)
|
|
276
|
+
* @param sql - SQL statement with ? placeholders
|
|
277
|
+
* @param params - Parameter values to bind
|
|
278
|
+
*/
|
|
279
|
+
async run(sql, params) {
|
|
280
|
+
await this.#sqliteMutex.run(async () => {
|
|
281
|
+
for await (const stmt of this.#sqlite3.statements(
|
|
282
|
+
this.#handle,
|
|
283
|
+
sql
|
|
284
|
+
)) {
|
|
285
|
+
if (params) {
|
|
286
|
+
this.#sqlite3.bind_collection(stmt, params);
|
|
287
|
+
}
|
|
288
|
+
while (await this.#sqlite3.step(stmt) === SQLITE_ROW) {
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Execute a parameterized SQL query and return results
|
|
295
|
+
* @param sql - SQL query with ? placeholders
|
|
296
|
+
* @param params - Parameter values to bind
|
|
297
|
+
* @returns Object with rows (array of arrays) and columns (column names)
|
|
298
|
+
*/
|
|
299
|
+
async query(sql, params) {
|
|
300
|
+
return this.#sqliteMutex.run(async () => {
|
|
301
|
+
const rows = [];
|
|
302
|
+
let columns = [];
|
|
303
|
+
for await (const stmt of this.#sqlite3.statements(
|
|
304
|
+
this.#handle,
|
|
305
|
+
sql
|
|
306
|
+
)) {
|
|
307
|
+
if (params) {
|
|
308
|
+
this.#sqlite3.bind_collection(stmt, params);
|
|
309
|
+
}
|
|
310
|
+
while (await this.#sqlite3.step(stmt) === SQLITE_ROW) {
|
|
311
|
+
if (columns.length === 0) {
|
|
312
|
+
columns = this.#sqlite3.column_names(stmt);
|
|
313
|
+
}
|
|
314
|
+
rows.push(this.#sqlite3.row(stmt));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { rows, columns };
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Close the database
|
|
322
|
+
*/
|
|
323
|
+
async close() {
|
|
324
|
+
if (this.#closed) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.#closed = true;
|
|
328
|
+
await this.#sqliteMutex.run(async () => {
|
|
329
|
+
await this.#sqlite3.close(this.#handle);
|
|
330
|
+
});
|
|
331
|
+
await this.#onClose();
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Get the database file name
|
|
335
|
+
*/
|
|
336
|
+
get fileName() {
|
|
337
|
+
return this.#fileName;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Get the raw @rivetkit/sqlite API (for advanced usage)
|
|
341
|
+
*/
|
|
342
|
+
get sqlite3() {
|
|
343
|
+
return this.#sqlite3;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Get the raw database handle (for advanced usage)
|
|
347
|
+
*/
|
|
348
|
+
get handle() {
|
|
349
|
+
return this.#handle;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
var SqliteVfs = class {
|
|
353
|
+
#sqlite3 = null;
|
|
354
|
+
#sqliteSystem = null;
|
|
355
|
+
#initPromise = null;
|
|
356
|
+
#openMutex = new AsyncMutex();
|
|
357
|
+
#sqliteMutex = new AsyncMutex();
|
|
358
|
+
#instanceId;
|
|
359
|
+
#destroyed = false;
|
|
360
|
+
#openDatabases = /* @__PURE__ */ new Set();
|
|
361
|
+
#wasmModule;
|
|
362
|
+
constructor(wasmModule) {
|
|
363
|
+
this.#instanceId = crypto.randomUUID().replace(/-/g, "").slice(0, 8);
|
|
364
|
+
this.#wasmModule = wasmModule;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Initialize @rivetkit/sqlite and VFS (called once per instance)
|
|
368
|
+
*/
|
|
369
|
+
async #ensureInitialized() {
|
|
370
|
+
if (this.#destroyed) {
|
|
371
|
+
throw new Error("SqliteVfs is closed");
|
|
372
|
+
}
|
|
373
|
+
if (this.#sqlite3 && this.#sqliteSystem) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (!this.#initPromise) {
|
|
377
|
+
this.#initPromise = (async () => {
|
|
378
|
+
const { sqlite3, module } = await loadSqliteRuntime(
|
|
379
|
+
this.#wasmModule
|
|
380
|
+
);
|
|
381
|
+
if (this.#destroyed) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
this.#sqlite3 = sqlite3;
|
|
385
|
+
this.#sqliteSystem = new SqliteSystem(
|
|
386
|
+
sqlite3,
|
|
387
|
+
module,
|
|
388
|
+
`kv-vfs-${this.#instanceId}`
|
|
389
|
+
);
|
|
390
|
+
this.#sqliteSystem.register();
|
|
391
|
+
})();
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
await this.#initPromise;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
this.#initPromise = null;
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Open a SQLite database using KV storage backend
|
|
402
|
+
*
|
|
403
|
+
* @param fileName - The database file name (typically the actor ID)
|
|
404
|
+
* @param options - KV storage operations for this database
|
|
405
|
+
* @returns A Database instance
|
|
406
|
+
*/
|
|
407
|
+
async open(fileName, options) {
|
|
408
|
+
if (this.#destroyed) {
|
|
409
|
+
throw new Error("SqliteVfs is closed");
|
|
410
|
+
}
|
|
411
|
+
await this.#openMutex.acquire();
|
|
412
|
+
try {
|
|
413
|
+
for (const db2 of this.#openDatabases) {
|
|
414
|
+
if (db2.fileName === fileName) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`SqliteVfs: fileName "${fileName}" is already open on this instance`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
await this.#ensureInitialized();
|
|
421
|
+
if (!this.#sqlite3 || !this.#sqliteSystem) {
|
|
422
|
+
throw new Error("Failed to initialize SQLite");
|
|
423
|
+
}
|
|
424
|
+
const sqlite3 = this.#sqlite3;
|
|
425
|
+
const sqliteSystem = this.#sqliteSystem;
|
|
426
|
+
sqliteSystem.registerFile(fileName, options);
|
|
427
|
+
const db = await this.#sqliteMutex.run(
|
|
428
|
+
async () => sqlite3.open_v2(
|
|
429
|
+
fileName,
|
|
430
|
+
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE2,
|
|
431
|
+
sqliteSystem.name
|
|
432
|
+
)
|
|
433
|
+
);
|
|
434
|
+
await this.#sqliteMutex.run(async () => {
|
|
435
|
+
await sqlite3.exec(db, "PRAGMA page_size = 4096");
|
|
436
|
+
await sqlite3.exec(db, "PRAGMA journal_mode = DELETE");
|
|
437
|
+
await sqlite3.exec(db, "PRAGMA synchronous = NORMAL");
|
|
438
|
+
await sqlite3.exec(db, "PRAGMA temp_store = MEMORY");
|
|
439
|
+
await sqlite3.exec(db, "PRAGMA auto_vacuum = NONE");
|
|
440
|
+
await sqlite3.exec(db, "PRAGMA locking_mode = EXCLUSIVE");
|
|
441
|
+
});
|
|
442
|
+
const onClose = async () => {
|
|
443
|
+
this.#openDatabases.delete(database);
|
|
444
|
+
await this.#openMutex.run(async () => {
|
|
445
|
+
sqliteSystem.unregisterFile(fileName);
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
const database = new Database(
|
|
449
|
+
sqlite3,
|
|
450
|
+
db,
|
|
451
|
+
fileName,
|
|
452
|
+
onClose,
|
|
453
|
+
this.#sqliteMutex
|
|
454
|
+
);
|
|
455
|
+
this.#openDatabases.add(database);
|
|
456
|
+
return database;
|
|
457
|
+
} finally {
|
|
458
|
+
this.#openMutex.release();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Force-close all Database handles whose fileName exactly matches the
|
|
463
|
+
* given name. Snapshots the set to an array before iterating to avoid
|
|
464
|
+
* mutation during async iteration.
|
|
465
|
+
*
|
|
466
|
+
* Uses exact file name match because short names are numeric strings
|
|
467
|
+
* ('0', '1', ..., '10', '11', ...) and a prefix match like
|
|
468
|
+
* startsWith('1') would incorrectly match '10', '11', etc., causing
|
|
469
|
+
* cross-actor corruption. Sidecar files (-journal, -wal, -shm) are not
|
|
470
|
+
* tracked as separate Database handles, so prefix matching for sidecars
|
|
471
|
+
* is not needed.
|
|
472
|
+
*/
|
|
473
|
+
async forceCloseByFileName(fileName) {
|
|
474
|
+
const snapshot = [...this.#openDatabases];
|
|
475
|
+
let allSucceeded = true;
|
|
476
|
+
for (const db of snapshot) {
|
|
477
|
+
if (db.fileName === fileName) {
|
|
478
|
+
try {
|
|
479
|
+
await db.close();
|
|
480
|
+
} catch {
|
|
481
|
+
allSucceeded = false;
|
|
482
|
+
this.#openDatabases.delete(db);
|
|
483
|
+
const sqliteSystem = this.#sqliteSystem;
|
|
484
|
+
if (sqliteSystem) {
|
|
485
|
+
await this.#openMutex.run(async () => {
|
|
486
|
+
sqliteSystem.unregisterFile(db.fileName);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return { allSucceeded };
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Force-close all open Database handles. Best-effort: errors are
|
|
496
|
+
* swallowed so this is safe to call during instance teardown.
|
|
497
|
+
*/
|
|
498
|
+
async forceCloseAll() {
|
|
499
|
+
const snapshot = [...this.#openDatabases];
|
|
500
|
+
for (const db of snapshot) {
|
|
501
|
+
try {
|
|
502
|
+
await db.close();
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Tears down this VFS instance and releases internal references.
|
|
509
|
+
*/
|
|
510
|
+
async destroy() {
|
|
511
|
+
if (this.#destroyed) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
this.#destroyed = true;
|
|
515
|
+
const initPromise = this.#initPromise;
|
|
516
|
+
if (initPromise) {
|
|
517
|
+
try {
|
|
518
|
+
await initPromise;
|
|
519
|
+
} catch {
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (this.#sqliteSystem) {
|
|
523
|
+
await this.#sqliteSystem.close();
|
|
524
|
+
}
|
|
525
|
+
this.#sqliteSystem = null;
|
|
526
|
+
this.#sqlite3 = null;
|
|
527
|
+
this.#initPromise = null;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Alias for destroy to align with DB-style lifecycle naming.
|
|
531
|
+
*/
|
|
532
|
+
async close() {
|
|
533
|
+
await this.destroy();
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
var SqliteSystem = class {
|
|
537
|
+
name;
|
|
538
|
+
mxPathName = SQLITE_MAX_PATHNAME_BYTES;
|
|
539
|
+
mxPathname = SQLITE_MAX_PATHNAME_BYTES;
|
|
540
|
+
#registeredFiles = /* @__PURE__ */ new Map();
|
|
541
|
+
#openFiles = /* @__PURE__ */ new Map();
|
|
542
|
+
#sqlite3;
|
|
543
|
+
#module;
|
|
544
|
+
#heapDataView;
|
|
545
|
+
#heapDataViewBuffer;
|
|
546
|
+
constructor(sqlite3, module, name) {
|
|
547
|
+
this.name = name;
|
|
548
|
+
this.#sqlite3 = sqlite3;
|
|
549
|
+
this.#module = module;
|
|
550
|
+
this.#heapDataViewBuffer = module.HEAPU8.buffer;
|
|
551
|
+
this.#heapDataView = new DataView(this.#heapDataViewBuffer);
|
|
552
|
+
}
|
|
553
|
+
async close() {
|
|
554
|
+
this.#openFiles.clear();
|
|
555
|
+
this.#registeredFiles.clear();
|
|
556
|
+
}
|
|
557
|
+
isReady() {
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
hasAsyncMethod(methodName) {
|
|
561
|
+
return SQLITE_ASYNC_METHODS.has(methodName);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Registers the VFS with SQLite
|
|
565
|
+
*/
|
|
566
|
+
register() {
|
|
567
|
+
this.#sqlite3.vfs_register(this, false);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Registers a file with its KV options (before opening).
|
|
571
|
+
*/
|
|
572
|
+
registerFile(fileName, options) {
|
|
573
|
+
this.#registeredFiles.set(fileName, options);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Unregisters a file's KV options (after closing).
|
|
577
|
+
*/
|
|
578
|
+
unregisterFile(fileName) {
|
|
579
|
+
this.#registeredFiles.delete(fileName);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Resolve file path to a registered database file or one of its SQLite
|
|
583
|
+
* sidecars (-journal, -wal, -shm). File tags are reused across files
|
|
584
|
+
* because each file's KvVfsOptions routes to a separate KV namespace.
|
|
585
|
+
*/
|
|
586
|
+
#resolveFile(path3) {
|
|
587
|
+
const directOptions = this.#registeredFiles.get(path3);
|
|
588
|
+
if (directOptions) {
|
|
589
|
+
return { options: directOptions, fileTag: FILE_TAG_MAIN };
|
|
590
|
+
}
|
|
591
|
+
if (path3.endsWith("-journal")) {
|
|
592
|
+
const baseName = path3.slice(0, -8);
|
|
593
|
+
const options = this.#registeredFiles.get(baseName);
|
|
594
|
+
if (options) {
|
|
595
|
+
return { options, fileTag: FILE_TAG_JOURNAL };
|
|
596
|
+
}
|
|
597
|
+
} else if (path3.endsWith("-wal")) {
|
|
598
|
+
const baseName = path3.slice(0, -4);
|
|
599
|
+
const options = this.#registeredFiles.get(baseName);
|
|
600
|
+
if (options) {
|
|
601
|
+
return { options, fileTag: FILE_TAG_WAL };
|
|
602
|
+
}
|
|
603
|
+
} else if (path3.endsWith("-shm")) {
|
|
604
|
+
const baseName = path3.slice(0, -4);
|
|
605
|
+
const options = this.#registeredFiles.get(baseName);
|
|
606
|
+
if (options) {
|
|
607
|
+
return { options, fileTag: FILE_TAG_SHM };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
#resolveFileOrThrow(path3) {
|
|
613
|
+
const resolved = this.#resolveFile(path3);
|
|
614
|
+
if (resolved) {
|
|
615
|
+
return resolved;
|
|
616
|
+
}
|
|
617
|
+
if (this.#registeredFiles.size === 0) {
|
|
618
|
+
throw new Error(`No KV options registered for file: ${path3}`);
|
|
619
|
+
}
|
|
620
|
+
const registered = Array.from(this.#registeredFiles.keys()).join(", ");
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Unsupported SQLite file path ${path3}. Registered base names: ${registered}.`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
#chunkKey(file, chunkIndex) {
|
|
626
|
+
return getChunkKey(file.fileTag, chunkIndex);
|
|
627
|
+
}
|
|
628
|
+
async xOpen(_pVfs, zName, fileId, flags, pOutFlags) {
|
|
629
|
+
var _a, _b, _c;
|
|
630
|
+
const path3 = this.#decodeFilename(zName, flags);
|
|
631
|
+
if (!path3) {
|
|
632
|
+
return VFS.SQLITE_CANTOPEN;
|
|
633
|
+
}
|
|
634
|
+
const { options, fileTag } = this.#resolveFileOrThrow(path3);
|
|
635
|
+
const metaKey = getMetaKey(fileTag);
|
|
636
|
+
let sizeData;
|
|
637
|
+
try {
|
|
638
|
+
sizeData = await options.get(metaKey);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
(_a = options.onError) == null ? void 0 : _a.call(options, error);
|
|
641
|
+
return VFS.SQLITE_CANTOPEN;
|
|
642
|
+
}
|
|
643
|
+
let size;
|
|
644
|
+
if (sizeData) {
|
|
645
|
+
size = decodeFileMeta2(sizeData);
|
|
646
|
+
if (!isValidFileSize(size)) {
|
|
647
|
+
return VFS.SQLITE_IOERR;
|
|
648
|
+
}
|
|
649
|
+
} else if (flags & VFS.SQLITE_OPEN_CREATE) {
|
|
650
|
+
if (fileTag === FILE_TAG_MAIN) {
|
|
651
|
+
const chunkKey = getChunkKey(fileTag, 0);
|
|
652
|
+
size = EMPTY_DB_PAGE.length;
|
|
653
|
+
try {
|
|
654
|
+
await options.putBatch([
|
|
655
|
+
[chunkKey, EMPTY_DB_PAGE],
|
|
656
|
+
[metaKey, encodeFileMeta2(size)]
|
|
657
|
+
]);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
(_b = options.onError) == null ? void 0 : _b.call(options, error);
|
|
660
|
+
return VFS.SQLITE_CANTOPEN;
|
|
661
|
+
}
|
|
662
|
+
} else {
|
|
663
|
+
size = 0;
|
|
664
|
+
try {
|
|
665
|
+
await options.put(metaKey, encodeFileMeta2(size));
|
|
666
|
+
} catch (error) {
|
|
667
|
+
(_c = options.onError) == null ? void 0 : _c.call(options, error);
|
|
668
|
+
return VFS.SQLITE_CANTOPEN;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
return VFS.SQLITE_CANTOPEN;
|
|
673
|
+
}
|
|
674
|
+
this.#openFiles.set(fileId, {
|
|
675
|
+
path: path3,
|
|
676
|
+
fileTag,
|
|
677
|
+
metaKey,
|
|
678
|
+
size,
|
|
679
|
+
metaDirty: false,
|
|
680
|
+
flags,
|
|
681
|
+
options,
|
|
682
|
+
batchMode: false,
|
|
683
|
+
dirtyBuffer: null,
|
|
684
|
+
savedFileSize: 0
|
|
685
|
+
});
|
|
686
|
+
this.#writeInt32(pOutFlags, flags);
|
|
687
|
+
return VFS.SQLITE_OK;
|
|
688
|
+
}
|
|
689
|
+
async xClose(fileId) {
|
|
690
|
+
var _a, _b;
|
|
691
|
+
const file = this.#openFiles.get(fileId);
|
|
692
|
+
if (!file) {
|
|
693
|
+
return VFS.SQLITE_OK;
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
|
697
|
+
await this.#delete(file.path);
|
|
698
|
+
} else if (file.metaDirty) {
|
|
699
|
+
await file.options.put(file.metaKey, encodeFileMeta2(file.size));
|
|
700
|
+
file.metaDirty = false;
|
|
701
|
+
}
|
|
702
|
+
} catch (error) {
|
|
703
|
+
(_b = (_a = file.options).onError) == null ? void 0 : _b.call(_a, error);
|
|
704
|
+
this.#openFiles.delete(fileId);
|
|
705
|
+
return VFS.SQLITE_IOERR;
|
|
706
|
+
}
|
|
707
|
+
this.#openFiles.delete(fileId);
|
|
708
|
+
return VFS.SQLITE_OK;
|
|
709
|
+
}
|
|
710
|
+
async xRead(fileId, pData, iAmt, iOffsetLo, iOffsetHi) {
|
|
711
|
+
var _a;
|
|
712
|
+
if (iAmt === 0) {
|
|
713
|
+
return VFS.SQLITE_OK;
|
|
714
|
+
}
|
|
715
|
+
const file = this.#openFiles.get(fileId);
|
|
716
|
+
if (!file) {
|
|
717
|
+
return VFS.SQLITE_IOERR_READ;
|
|
718
|
+
}
|
|
719
|
+
let data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
|
|
720
|
+
const options = file.options;
|
|
721
|
+
const requestedLength = iAmt;
|
|
722
|
+
const iOffset = delegalize(iOffsetLo, iOffsetHi);
|
|
723
|
+
if (iOffset < 0) {
|
|
724
|
+
return VFS.SQLITE_IOERR_READ;
|
|
725
|
+
}
|
|
726
|
+
const fileSize = file.size;
|
|
727
|
+
if (iOffset >= fileSize) {
|
|
728
|
+
data.fill(0);
|
|
729
|
+
return VFS.SQLITE_IOERR_SHORT_READ;
|
|
730
|
+
}
|
|
731
|
+
const startChunk = Math.floor(iOffset / CHUNK_SIZE);
|
|
732
|
+
const endChunk = Math.floor(
|
|
733
|
+
(iOffset + requestedLength - 1) / CHUNK_SIZE
|
|
734
|
+
);
|
|
735
|
+
const chunkKeys = [];
|
|
736
|
+
const chunkIndexToBuffered = /* @__PURE__ */ new Map();
|
|
737
|
+
for (let i = startChunk; i <= endChunk; i++) {
|
|
738
|
+
if (file.batchMode && file.dirtyBuffer) {
|
|
739
|
+
const buffered = file.dirtyBuffer.get(i);
|
|
740
|
+
if (buffered) {
|
|
741
|
+
chunkIndexToBuffered.set(i, buffered);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
chunkKeys.push(this.#chunkKey(file, i));
|
|
746
|
+
}
|
|
747
|
+
let kvChunks;
|
|
748
|
+
try {
|
|
749
|
+
kvChunks = chunkKeys.length > 0 ? await options.getBatch(chunkKeys) : [];
|
|
750
|
+
} catch (error) {
|
|
751
|
+
(_a = options.onError) == null ? void 0 : _a.call(options, error);
|
|
752
|
+
return VFS.SQLITE_IOERR_READ;
|
|
753
|
+
}
|
|
754
|
+
data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
|
|
755
|
+
let kvIdx = 0;
|
|
756
|
+
for (let i = startChunk; i <= endChunk; i++) {
|
|
757
|
+
const chunkData = chunkIndexToBuffered.get(i) ?? kvChunks[kvIdx++];
|
|
758
|
+
const chunkOffset = i * CHUNK_SIZE;
|
|
759
|
+
const readStart = Math.max(0, iOffset - chunkOffset);
|
|
760
|
+
const readEnd = Math.min(
|
|
761
|
+
CHUNK_SIZE,
|
|
762
|
+
iOffset + requestedLength - chunkOffset
|
|
763
|
+
);
|
|
764
|
+
if (chunkData) {
|
|
765
|
+
const sourceStart = readStart;
|
|
766
|
+
const sourceEnd = Math.min(readEnd, chunkData.length);
|
|
767
|
+
const destStart = chunkOffset + readStart - iOffset;
|
|
768
|
+
if (sourceEnd > sourceStart) {
|
|
769
|
+
data.set(
|
|
770
|
+
chunkData.subarray(sourceStart, sourceEnd),
|
|
771
|
+
destStart
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (sourceEnd < readEnd) {
|
|
775
|
+
const zeroStart = destStart + (sourceEnd - sourceStart);
|
|
776
|
+
const zeroEnd = destStart + (readEnd - readStart);
|
|
777
|
+
data.fill(0, zeroStart, zeroEnd);
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
const destStart = chunkOffset + readStart - iOffset;
|
|
781
|
+
const destEnd = destStart + (readEnd - readStart);
|
|
782
|
+
data.fill(0, destStart, destEnd);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const actualBytes = Math.min(requestedLength, fileSize - iOffset);
|
|
786
|
+
if (actualBytes < requestedLength) {
|
|
787
|
+
data.fill(0, actualBytes);
|
|
788
|
+
return VFS.SQLITE_IOERR_SHORT_READ;
|
|
789
|
+
}
|
|
790
|
+
return VFS.SQLITE_OK;
|
|
791
|
+
}
|
|
792
|
+
async xWrite(fileId, pData, iAmt, iOffsetLo, iOffsetHi) {
|
|
793
|
+
var _a, _b;
|
|
794
|
+
if (iAmt === 0) {
|
|
795
|
+
return VFS.SQLITE_OK;
|
|
796
|
+
}
|
|
797
|
+
const file = this.#openFiles.get(fileId);
|
|
798
|
+
if (!file) {
|
|
799
|
+
return VFS.SQLITE_IOERR_WRITE;
|
|
800
|
+
}
|
|
801
|
+
let data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
|
|
802
|
+
const iOffset = delegalize(iOffsetLo, iOffsetHi);
|
|
803
|
+
if (iOffset < 0) {
|
|
804
|
+
return VFS.SQLITE_IOERR_WRITE;
|
|
805
|
+
}
|
|
806
|
+
const options = file.options;
|
|
807
|
+
const writeLength = iAmt;
|
|
808
|
+
const writeEndOffset = iOffset + writeLength;
|
|
809
|
+
if (writeEndOffset > MAX_FILE_SIZE_BYTES) {
|
|
810
|
+
return VFS.SQLITE_IOERR_WRITE;
|
|
811
|
+
}
|
|
812
|
+
const startChunk = Math.floor(iOffset / CHUNK_SIZE);
|
|
813
|
+
const endChunk = Math.floor((iOffset + writeLength - 1) / CHUNK_SIZE);
|
|
814
|
+
if (file.batchMode && file.dirtyBuffer) {
|
|
815
|
+
for (let i = startChunk; i <= endChunk; i++) {
|
|
816
|
+
const chunkOffset = i * CHUNK_SIZE;
|
|
817
|
+
const sourceStart = Math.max(0, chunkOffset - iOffset);
|
|
818
|
+
const sourceEnd = Math.min(
|
|
819
|
+
writeLength,
|
|
820
|
+
chunkOffset + CHUNK_SIZE - iOffset
|
|
821
|
+
);
|
|
822
|
+
file.dirtyBuffer.set(
|
|
823
|
+
i,
|
|
824
|
+
data.subarray(sourceStart, sourceEnd).slice()
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
const newSize2 = Math.max(file.size, writeEndOffset);
|
|
828
|
+
if (newSize2 !== file.size) {
|
|
829
|
+
file.size = newSize2;
|
|
830
|
+
file.metaDirty = true;
|
|
831
|
+
}
|
|
832
|
+
return VFS.SQLITE_OK;
|
|
833
|
+
}
|
|
834
|
+
const plans = [];
|
|
835
|
+
const chunkKeysToFetch = [];
|
|
836
|
+
for (let i = startChunk; i <= endChunk; i++) {
|
|
837
|
+
const chunkOffset = i * CHUNK_SIZE;
|
|
838
|
+
const writeStart = Math.max(0, iOffset - chunkOffset);
|
|
839
|
+
const writeEnd = Math.min(
|
|
840
|
+
CHUNK_SIZE,
|
|
841
|
+
iOffset + writeLength - chunkOffset
|
|
842
|
+
);
|
|
843
|
+
const existingBytesInChunk = Math.max(
|
|
844
|
+
0,
|
|
845
|
+
Math.min(CHUNK_SIZE, file.size - chunkOffset)
|
|
846
|
+
);
|
|
847
|
+
const needsExisting = writeStart > 0 || existingBytesInChunk > writeEnd;
|
|
848
|
+
const chunkKey = this.#chunkKey(file, i);
|
|
849
|
+
let existingChunkIndex = -1;
|
|
850
|
+
if (needsExisting) {
|
|
851
|
+
existingChunkIndex = chunkKeysToFetch.length;
|
|
852
|
+
chunkKeysToFetch.push(chunkKey);
|
|
853
|
+
}
|
|
854
|
+
plans.push({
|
|
855
|
+
chunkKey,
|
|
856
|
+
chunkOffset,
|
|
857
|
+
writeStart,
|
|
858
|
+
writeEnd,
|
|
859
|
+
existingChunkIndex
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
let existingChunks;
|
|
863
|
+
try {
|
|
864
|
+
existingChunks = chunkKeysToFetch.length > 0 ? await options.getBatch(chunkKeysToFetch) : [];
|
|
865
|
+
} catch (error) {
|
|
866
|
+
(_a = options.onError) == null ? void 0 : _a.call(options, error);
|
|
867
|
+
return VFS.SQLITE_IOERR_WRITE;
|
|
868
|
+
}
|
|
869
|
+
data = this.#module.HEAPU8.subarray(pData, pData + iAmt);
|
|
870
|
+
const entriesToWrite = [];
|
|
871
|
+
for (const plan of plans) {
|
|
872
|
+
const existingChunk = plan.existingChunkIndex >= 0 ? existingChunks[plan.existingChunkIndex] : null;
|
|
873
|
+
let newChunk;
|
|
874
|
+
if (existingChunk) {
|
|
875
|
+
newChunk = new Uint8Array(
|
|
876
|
+
Math.max(existingChunk.length, plan.writeEnd)
|
|
877
|
+
);
|
|
878
|
+
newChunk.set(existingChunk);
|
|
879
|
+
} else {
|
|
880
|
+
newChunk = new Uint8Array(plan.writeEnd);
|
|
881
|
+
}
|
|
882
|
+
const sourceStart = plan.chunkOffset + plan.writeStart - iOffset;
|
|
883
|
+
const sourceEnd = sourceStart + (plan.writeEnd - plan.writeStart);
|
|
884
|
+
newChunk.set(
|
|
885
|
+
data.subarray(sourceStart, sourceEnd),
|
|
886
|
+
plan.writeStart
|
|
887
|
+
);
|
|
888
|
+
entriesToWrite.push([plan.chunkKey, newChunk]);
|
|
889
|
+
}
|
|
890
|
+
const previousSize = file.size;
|
|
891
|
+
const previousMetaDirty = file.metaDirty;
|
|
892
|
+
const newSize = Math.max(file.size, writeEndOffset);
|
|
893
|
+
if (newSize !== previousSize) {
|
|
894
|
+
file.size = newSize;
|
|
895
|
+
file.metaDirty = true;
|
|
896
|
+
}
|
|
897
|
+
if (file.metaDirty) {
|
|
898
|
+
entriesToWrite.push([file.metaKey, encodeFileMeta2(file.size)]);
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
await options.putBatch(entriesToWrite);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
(_b = options.onError) == null ? void 0 : _b.call(options, error);
|
|
904
|
+
file.size = previousSize;
|
|
905
|
+
file.metaDirty = previousMetaDirty;
|
|
906
|
+
return VFS.SQLITE_IOERR_WRITE;
|
|
907
|
+
}
|
|
908
|
+
if (file.metaDirty) {
|
|
909
|
+
file.metaDirty = false;
|
|
910
|
+
}
|
|
911
|
+
file.metaDirty = false;
|
|
912
|
+
return VFS.SQLITE_OK;
|
|
913
|
+
}
|
|
914
|
+
async xTruncate(fileId, sizeLo, sizeHi) {
|
|
915
|
+
var _a, _b, _c;
|
|
916
|
+
const file = this.#openFiles.get(fileId);
|
|
917
|
+
if (!file) {
|
|
918
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
919
|
+
}
|
|
920
|
+
const size = delegalize(sizeLo, sizeHi);
|
|
921
|
+
if (size < 0 || size > MAX_FILE_SIZE_BYTES) {
|
|
922
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
923
|
+
}
|
|
924
|
+
const options = file.options;
|
|
925
|
+
if (size >= file.size) {
|
|
926
|
+
if (size > file.size) {
|
|
927
|
+
const previousSize2 = file.size;
|
|
928
|
+
const previousMetaDirty2 = file.metaDirty;
|
|
929
|
+
file.size = size;
|
|
930
|
+
file.metaDirty = true;
|
|
931
|
+
try {
|
|
932
|
+
await options.put(file.metaKey, encodeFileMeta2(file.size));
|
|
933
|
+
} catch (error) {
|
|
934
|
+
(_a = options.onError) == null ? void 0 : _a.call(options, error);
|
|
935
|
+
file.size = previousSize2;
|
|
936
|
+
file.metaDirty = previousMetaDirty2;
|
|
937
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
938
|
+
}
|
|
939
|
+
file.metaDirty = false;
|
|
940
|
+
}
|
|
941
|
+
return VFS.SQLITE_OK;
|
|
942
|
+
}
|
|
943
|
+
const lastChunkToKeep = Math.floor((size - 1) / CHUNK_SIZE);
|
|
944
|
+
const lastExistingChunk = Math.floor((file.size - 1) / CHUNK_SIZE);
|
|
945
|
+
const previousSize = file.size;
|
|
946
|
+
const previousMetaDirty = file.metaDirty;
|
|
947
|
+
file.size = size;
|
|
948
|
+
file.metaDirty = true;
|
|
949
|
+
try {
|
|
950
|
+
await options.put(file.metaKey, encodeFileMeta2(file.size));
|
|
951
|
+
} catch (error) {
|
|
952
|
+
(_b = options.onError) == null ? void 0 : _b.call(options, error);
|
|
953
|
+
file.size = previousSize;
|
|
954
|
+
file.metaDirty = previousMetaDirty;
|
|
955
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
956
|
+
}
|
|
957
|
+
file.metaDirty = false;
|
|
958
|
+
try {
|
|
959
|
+
if (size > 0 && size % CHUNK_SIZE !== 0) {
|
|
960
|
+
const lastChunkKey = this.#chunkKey(file, lastChunkToKeep);
|
|
961
|
+
const lastChunkData = await options.get(lastChunkKey);
|
|
962
|
+
if (lastChunkData && lastChunkData.length > size % CHUNK_SIZE) {
|
|
963
|
+
const truncatedChunk = lastChunkData.subarray(
|
|
964
|
+
0,
|
|
965
|
+
size % CHUNK_SIZE
|
|
966
|
+
);
|
|
967
|
+
await options.put(lastChunkKey, truncatedChunk);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const keysToDelete = [];
|
|
971
|
+
for (let i = lastChunkToKeep + 1; i <= lastExistingChunk; i++) {
|
|
972
|
+
keysToDelete.push(this.#chunkKey(file, i));
|
|
973
|
+
}
|
|
974
|
+
for (let b = 0; b < keysToDelete.length; b += KV_MAX_BATCH_KEYS) {
|
|
975
|
+
await options.deleteBatch(
|
|
976
|
+
keysToDelete.slice(b, b + KV_MAX_BATCH_KEYS)
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
} catch (error) {
|
|
980
|
+
(_c = options.onError) == null ? void 0 : _c.call(options, error);
|
|
981
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
982
|
+
}
|
|
983
|
+
return VFS.SQLITE_OK;
|
|
984
|
+
}
|
|
985
|
+
async xSync(fileId, _flags) {
|
|
986
|
+
var _a, _b;
|
|
987
|
+
const file = this.#openFiles.get(fileId);
|
|
988
|
+
if (!file || !file.metaDirty) {
|
|
989
|
+
return VFS.SQLITE_OK;
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
await file.options.put(file.metaKey, encodeFileMeta2(file.size));
|
|
993
|
+
} catch (error) {
|
|
994
|
+
(_b = (_a = file.options).onError) == null ? void 0 : _b.call(_a, error);
|
|
995
|
+
return VFS.SQLITE_IOERR_FSYNC;
|
|
996
|
+
}
|
|
997
|
+
file.metaDirty = false;
|
|
998
|
+
return VFS.SQLITE_OK;
|
|
999
|
+
}
|
|
1000
|
+
async xFileSize(fileId, pSize) {
|
|
1001
|
+
const file = this.#openFiles.get(fileId);
|
|
1002
|
+
if (!file) {
|
|
1003
|
+
return VFS.SQLITE_IOERR_FSTAT;
|
|
1004
|
+
}
|
|
1005
|
+
this.#writeBigInt64(pSize, BigInt(file.size));
|
|
1006
|
+
return VFS.SQLITE_OK;
|
|
1007
|
+
}
|
|
1008
|
+
async xDelete(_pVfs, zName, _syncDir) {
|
|
1009
|
+
try {
|
|
1010
|
+
await this.#delete(this.#module.UTF8ToString(zName));
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
return VFS.SQLITE_IOERR_DELETE;
|
|
1013
|
+
}
|
|
1014
|
+
return VFS.SQLITE_OK;
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Internal delete implementation.
|
|
1018
|
+
* Uses deleteRange for O(1) chunk deletion instead of enumerating
|
|
1019
|
+
* individual chunk keys. The chunk keys for a file tag are
|
|
1020
|
+
* lexicographically contiguous, so range deletion is always safe.
|
|
1021
|
+
*/
|
|
1022
|
+
async #delete(path3) {
|
|
1023
|
+
const { options, fileTag } = this.#resolveFileOrThrow(path3);
|
|
1024
|
+
const metaKey = getMetaKey(fileTag);
|
|
1025
|
+
const sizeData = await options.get(metaKey);
|
|
1026
|
+
if (!sizeData) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
await options.deleteRange(
|
|
1030
|
+
getChunkKey(fileTag, 0),
|
|
1031
|
+
getChunkKeyRangeEnd(fileTag)
|
|
1032
|
+
);
|
|
1033
|
+
await options.deleteBatch([metaKey]);
|
|
1034
|
+
}
|
|
1035
|
+
async xAccess(_pVfs, zName, _flags, pResOut) {
|
|
1036
|
+
var _a, _b;
|
|
1037
|
+
const path3 = this.#module.UTF8ToString(zName);
|
|
1038
|
+
const resolved = this.#resolveFile(path3);
|
|
1039
|
+
if (!resolved) {
|
|
1040
|
+
this.#writeInt32(pResOut, 0);
|
|
1041
|
+
return VFS.SQLITE_OK;
|
|
1042
|
+
}
|
|
1043
|
+
const compactMetaKey = getMetaKey(resolved.fileTag);
|
|
1044
|
+
let metaData;
|
|
1045
|
+
try {
|
|
1046
|
+
metaData = await resolved.options.get(compactMetaKey);
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
(_b = (_a = resolved.options).onError) == null ? void 0 : _b.call(_a, error);
|
|
1049
|
+
return VFS.SQLITE_IOERR_ACCESS;
|
|
1050
|
+
}
|
|
1051
|
+
this.#writeInt32(pResOut, metaData ? 1 : 0);
|
|
1052
|
+
return VFS.SQLITE_OK;
|
|
1053
|
+
}
|
|
1054
|
+
xCheckReservedLock(_fileId, pResOut) {
|
|
1055
|
+
this.#writeInt32(pResOut, 0);
|
|
1056
|
+
return VFS.SQLITE_OK;
|
|
1057
|
+
}
|
|
1058
|
+
xLock(_fileId, _flags) {
|
|
1059
|
+
return VFS.SQLITE_OK;
|
|
1060
|
+
}
|
|
1061
|
+
xUnlock(_fileId, _flags) {
|
|
1062
|
+
return VFS.SQLITE_OK;
|
|
1063
|
+
}
|
|
1064
|
+
async xFileControl(fileId, flags, _pArg) {
|
|
1065
|
+
var _a;
|
|
1066
|
+
switch (flags) {
|
|
1067
|
+
case SQLITE_FCNTL_BEGIN_ATOMIC_WRITE: {
|
|
1068
|
+
const file = this.#openFiles.get(fileId);
|
|
1069
|
+
if (!file) return VFS.SQLITE_NOTFOUND;
|
|
1070
|
+
file.savedFileSize = file.size;
|
|
1071
|
+
file.batchMode = true;
|
|
1072
|
+
file.metaDirty = false;
|
|
1073
|
+
file.dirtyBuffer = /* @__PURE__ */ new Map();
|
|
1074
|
+
return VFS.SQLITE_OK;
|
|
1075
|
+
}
|
|
1076
|
+
case SQLITE_FCNTL_COMMIT_ATOMIC_WRITE: {
|
|
1077
|
+
const file = this.#openFiles.get(fileId);
|
|
1078
|
+
if (!file) return VFS.SQLITE_NOTFOUND;
|
|
1079
|
+
const { dirtyBuffer, options } = file;
|
|
1080
|
+
const maxDirtyPages = file.metaDirty ? KV_MAX_BATCH_KEYS - 1 : KV_MAX_BATCH_KEYS;
|
|
1081
|
+
if (dirtyBuffer && dirtyBuffer.size > maxDirtyPages) {
|
|
1082
|
+
dirtyBuffer.clear();
|
|
1083
|
+
file.dirtyBuffer = null;
|
|
1084
|
+
file.size = file.savedFileSize;
|
|
1085
|
+
file.metaDirty = false;
|
|
1086
|
+
file.batchMode = false;
|
|
1087
|
+
return VFS.SQLITE_IOERR;
|
|
1088
|
+
}
|
|
1089
|
+
const entries = [];
|
|
1090
|
+
if (dirtyBuffer) {
|
|
1091
|
+
for (const [chunkIndex, data] of dirtyBuffer) {
|
|
1092
|
+
entries.push([this.#chunkKey(file, chunkIndex), data]);
|
|
1093
|
+
}
|
|
1094
|
+
dirtyBuffer.clear();
|
|
1095
|
+
}
|
|
1096
|
+
if (file.metaDirty) {
|
|
1097
|
+
entries.push([file.metaKey, encodeFileMeta2(file.size)]);
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
await options.putBatch(entries);
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
(_a = options.onError) == null ? void 0 : _a.call(options, error);
|
|
1103
|
+
file.dirtyBuffer = null;
|
|
1104
|
+
file.size = file.savedFileSize;
|
|
1105
|
+
file.metaDirty = false;
|
|
1106
|
+
file.batchMode = false;
|
|
1107
|
+
return VFS.SQLITE_IOERR;
|
|
1108
|
+
}
|
|
1109
|
+
file.dirtyBuffer = null;
|
|
1110
|
+
file.metaDirty = false;
|
|
1111
|
+
file.batchMode = false;
|
|
1112
|
+
return VFS.SQLITE_OK;
|
|
1113
|
+
}
|
|
1114
|
+
case SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE: {
|
|
1115
|
+
const file = this.#openFiles.get(fileId);
|
|
1116
|
+
if (!file || !file.batchMode) return VFS.SQLITE_OK;
|
|
1117
|
+
if (file.dirtyBuffer) {
|
|
1118
|
+
file.dirtyBuffer.clear();
|
|
1119
|
+
file.dirtyBuffer = null;
|
|
1120
|
+
}
|
|
1121
|
+
file.size = file.savedFileSize;
|
|
1122
|
+
file.metaDirty = false;
|
|
1123
|
+
file.batchMode = false;
|
|
1124
|
+
return VFS.SQLITE_OK;
|
|
1125
|
+
}
|
|
1126
|
+
default:
|
|
1127
|
+
return VFS.SQLITE_NOTFOUND;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// Return CHUNK_SIZE so SQLite aligns journal I/O to chunk boundaries.
|
|
1131
|
+
// Must match the native VFS (kv_io_sector_size in sqlite-native/src/vfs.rs).
|
|
1132
|
+
xSectorSize(_fileId) {
|
|
1133
|
+
return CHUNK_SIZE;
|
|
1134
|
+
}
|
|
1135
|
+
xDeviceCharacteristics(_fileId) {
|
|
1136
|
+
return SQLITE_IOCAP_BATCH_ATOMIC;
|
|
1137
|
+
}
|
|
1138
|
+
xFullPathname(_pVfs, zName, nOut, zOut) {
|
|
1139
|
+
const path3 = this.#module.UTF8ToString(zName);
|
|
1140
|
+
const bytes = TEXT_ENCODER.encode(path3);
|
|
1141
|
+
const out = this.#module.HEAPU8.subarray(zOut, zOut + nOut);
|
|
1142
|
+
if (bytes.length >= out.length) {
|
|
1143
|
+
return VFS.SQLITE_IOERR;
|
|
1144
|
+
}
|
|
1145
|
+
out.set(bytes, 0);
|
|
1146
|
+
out[bytes.length] = 0;
|
|
1147
|
+
return VFS.SQLITE_OK;
|
|
1148
|
+
}
|
|
1149
|
+
#decodeFilename(zName, flags) {
|
|
1150
|
+
if (!zName) {
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
if (flags & VFS.SQLITE_OPEN_URI) {
|
|
1154
|
+
let pName = zName;
|
|
1155
|
+
let state = 1;
|
|
1156
|
+
const charCodes = [];
|
|
1157
|
+
while (state) {
|
|
1158
|
+
const charCode = this.#module.HEAPU8[pName++];
|
|
1159
|
+
if (charCode) {
|
|
1160
|
+
charCodes.push(charCode);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
if (!this.#module.HEAPU8[pName]) {
|
|
1164
|
+
state = null;
|
|
1165
|
+
}
|
|
1166
|
+
switch (state) {
|
|
1167
|
+
case 1:
|
|
1168
|
+
charCodes.push("?".charCodeAt(0));
|
|
1169
|
+
state = 2;
|
|
1170
|
+
break;
|
|
1171
|
+
case 2:
|
|
1172
|
+
charCodes.push("=".charCodeAt(0));
|
|
1173
|
+
state = 3;
|
|
1174
|
+
break;
|
|
1175
|
+
case 3:
|
|
1176
|
+
charCodes.push("&".charCodeAt(0));
|
|
1177
|
+
state = 2;
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return TEXT_DECODER.decode(new Uint8Array(charCodes));
|
|
1182
|
+
}
|
|
1183
|
+
return this.#module.UTF8ToString(zName);
|
|
1184
|
+
}
|
|
1185
|
+
#heapView() {
|
|
1186
|
+
const heapBuffer = this.#module.HEAPU8.buffer;
|
|
1187
|
+
if (heapBuffer !== this.#heapDataViewBuffer) {
|
|
1188
|
+
this.#heapDataViewBuffer = heapBuffer;
|
|
1189
|
+
this.#heapDataView = new DataView(heapBuffer);
|
|
1190
|
+
}
|
|
1191
|
+
return this.#heapDataView;
|
|
1192
|
+
}
|
|
1193
|
+
#writeInt32(pointer, value) {
|
|
1194
|
+
const heapByteOffset = this.#module.HEAPU8.byteOffset + pointer;
|
|
1195
|
+
this.#heapView().setInt32(heapByteOffset, value, true);
|
|
1196
|
+
}
|
|
1197
|
+
#writeBigInt64(pointer, value) {
|
|
1198
|
+
const heapByteOffset = this.#module.HEAPU8.byteOffset + pointer;
|
|
1199
|
+
this.#heapView().setBigInt64(heapByteOffset, value, true);
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
function delegalize(lo32, hi32) {
|
|
1203
|
+
const hi = hi32 >>> 0;
|
|
1204
|
+
const lo = lo32 >>> 0;
|
|
1205
|
+
if (hi > MAX_FILE_SIZE_HI32) {
|
|
1206
|
+
return -1;
|
|
1207
|
+
}
|
|
1208
|
+
if (hi === MAX_FILE_SIZE_HI32 && lo > MAX_FILE_SIZE_LO32) {
|
|
1209
|
+
return -1;
|
|
1210
|
+
}
|
|
1211
|
+
return hi * UINT32_SIZE + lo;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// src/pool.ts
|
|
1215
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1216
|
+
import { createRequire as createRequire2 } from "module";
|
|
1217
|
+
import path2 from "path";
|
|
1218
|
+
function createNodeRequire2() {
|
|
1219
|
+
return createRequire2(
|
|
1220
|
+
path2.join(process.cwd(), "__rivetkit_sqlite_require__.cjs")
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
var SqliteVfsPool = class {
|
|
1224
|
+
#config;
|
|
1225
|
+
#modulePromise = null;
|
|
1226
|
+
#instances = /* @__PURE__ */ new Set();
|
|
1227
|
+
#actorToInstance = /* @__PURE__ */ new Map();
|
|
1228
|
+
#actorToHandle = /* @__PURE__ */ new Map();
|
|
1229
|
+
#shuttingDown = false;
|
|
1230
|
+
constructor(config2) {
|
|
1231
|
+
if (!Number.isInteger(config2.actorsPerInstance) || config2.actorsPerInstance < 1) {
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
`actorsPerInstance must be a positive integer, got ${config2.actorsPerInstance}`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
this.#config = config2;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Compile the WASM module once and cache the promise. Subsequent calls
|
|
1240
|
+
* return the same promise, avoiding redundant compilation.
|
|
1241
|
+
*/
|
|
1242
|
+
#getModule() {
|
|
1243
|
+
if (!this.#modulePromise) {
|
|
1244
|
+
this.#modulePromise = (async () => {
|
|
1245
|
+
const require2 = createNodeRequire2();
|
|
1246
|
+
const wasmPath = require2.resolve(
|
|
1247
|
+
"@rivetkit/sqlite/dist/wa-sqlite-async.wasm"
|
|
1248
|
+
);
|
|
1249
|
+
const wasmBinary = readFileSync2(wasmPath);
|
|
1250
|
+
return WebAssembly.compile(wasmBinary);
|
|
1251
|
+
})();
|
|
1252
|
+
this.#modulePromise.catch(() => {
|
|
1253
|
+
this.#modulePromise = null;
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
return this.#modulePromise;
|
|
1257
|
+
}
|
|
1258
|
+
/** Number of live WASM instances in the pool. */
|
|
1259
|
+
get instanceCount() {
|
|
1260
|
+
return this.#instances.size;
|
|
1261
|
+
}
|
|
1262
|
+
/** Number of actors currently assigned to pool instances. */
|
|
1263
|
+
get actorCount() {
|
|
1264
|
+
return this.#actorToInstance.size;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Acquire a pooled VFS handle for the given actor. Returns a
|
|
1268
|
+
* PooledSqliteHandle with sticky assignment. If the actor is already
|
|
1269
|
+
* assigned, the existing handle is returned.
|
|
1270
|
+
*
|
|
1271
|
+
* Bin-packing: picks the instance with the most actors that still has
|
|
1272
|
+
* capacity. If all instances are full, creates a new one using the
|
|
1273
|
+
* cached WASM module.
|
|
1274
|
+
*/
|
|
1275
|
+
async acquire(actorId) {
|
|
1276
|
+
if (this.#shuttingDown) {
|
|
1277
|
+
throw new Error("SqliteVfsPool is shutting down");
|
|
1278
|
+
}
|
|
1279
|
+
const existingHandle = this.#actorToHandle.get(actorId);
|
|
1280
|
+
if (existingHandle) {
|
|
1281
|
+
return existingHandle;
|
|
1282
|
+
}
|
|
1283
|
+
let bestInstance = null;
|
|
1284
|
+
let bestCount = -1;
|
|
1285
|
+
for (const instance of this.#instances) {
|
|
1286
|
+
if (instance.destroying) continue;
|
|
1287
|
+
const count = instance.actors.size;
|
|
1288
|
+
if (count < this.#config.actorsPerInstance && count > bestCount) {
|
|
1289
|
+
bestInstance = instance;
|
|
1290
|
+
bestCount = count;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (!bestInstance) {
|
|
1294
|
+
const wasmModule = await this.#getModule();
|
|
1295
|
+
if (this.#shuttingDown) {
|
|
1296
|
+
throw new Error("SqliteVfsPool is shutting down");
|
|
1297
|
+
}
|
|
1298
|
+
const existingHandleAfterAwait = this.#actorToHandle.get(actorId);
|
|
1299
|
+
if (existingHandleAfterAwait) {
|
|
1300
|
+
return existingHandleAfterAwait;
|
|
1301
|
+
}
|
|
1302
|
+
for (const instance of this.#instances) {
|
|
1303
|
+
if (instance.destroying) continue;
|
|
1304
|
+
const count = instance.actors.size;
|
|
1305
|
+
if (count < this.#config.actorsPerInstance && count > bestCount) {
|
|
1306
|
+
bestInstance = instance;
|
|
1307
|
+
bestCount = count;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (!bestInstance) {
|
|
1311
|
+
const vfs = new SqliteVfs(wasmModule);
|
|
1312
|
+
bestInstance = {
|
|
1313
|
+
vfs,
|
|
1314
|
+
actors: /* @__PURE__ */ new Set(),
|
|
1315
|
+
shortNameCounter: 0,
|
|
1316
|
+
actorShortNames: /* @__PURE__ */ new Map(),
|
|
1317
|
+
availableShortNames: /* @__PURE__ */ new Set(),
|
|
1318
|
+
poisonedShortNames: /* @__PURE__ */ new Set(),
|
|
1319
|
+
opsInFlight: 0,
|
|
1320
|
+
idleTimer: null,
|
|
1321
|
+
destroying: false
|
|
1322
|
+
};
|
|
1323
|
+
this.#instances.add(bestInstance);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
this.#cancelIdleTimer(bestInstance);
|
|
1327
|
+
let shortName;
|
|
1328
|
+
const recycled = bestInstance.availableShortNames.values().next();
|
|
1329
|
+
if (!recycled.done) {
|
|
1330
|
+
shortName = recycled.value;
|
|
1331
|
+
bestInstance.availableShortNames.delete(shortName);
|
|
1332
|
+
} else {
|
|
1333
|
+
shortName = String(bestInstance.shortNameCounter++);
|
|
1334
|
+
}
|
|
1335
|
+
bestInstance.actors.add(actorId);
|
|
1336
|
+
bestInstance.actorShortNames.set(actorId, shortName);
|
|
1337
|
+
this.#actorToInstance.set(actorId, bestInstance);
|
|
1338
|
+
const handle = new PooledSqliteHandle(shortName, actorId, this);
|
|
1339
|
+
this.#actorToHandle.set(actorId, handle);
|
|
1340
|
+
return handle;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Release an actor's assignment from the pool. Force-closes all database
|
|
1344
|
+
* handles for the actor, recycles or poisons the short name, and
|
|
1345
|
+
* decrements the instance refcount.
|
|
1346
|
+
*/
|
|
1347
|
+
async release(actorId) {
|
|
1348
|
+
const instance = this.#actorToInstance.get(actorId);
|
|
1349
|
+
if (!instance) {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
const shortName = instance.actorShortNames.get(actorId);
|
|
1353
|
+
if (shortName === void 0) {
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const { allSucceeded } = await instance.vfs.forceCloseByFileName(shortName);
|
|
1357
|
+
if (allSucceeded) {
|
|
1358
|
+
instance.availableShortNames.add(shortName);
|
|
1359
|
+
} else {
|
|
1360
|
+
instance.poisonedShortNames.add(shortName);
|
|
1361
|
+
}
|
|
1362
|
+
instance.actors.delete(actorId);
|
|
1363
|
+
instance.actorShortNames.delete(actorId);
|
|
1364
|
+
this.#actorToInstance.delete(actorId);
|
|
1365
|
+
this.#actorToHandle.delete(actorId);
|
|
1366
|
+
if (instance.actors.size === 0 && instance.opsInFlight === 0 && !this.#shuttingDown) {
|
|
1367
|
+
this.#startIdleTimer(instance);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Track an in-flight operation on an instance. Increments opsInFlight
|
|
1372
|
+
* before running fn, decrements after using try/finally to prevent
|
|
1373
|
+
* drift from exceptions. If the decrement brings opsInFlight to 0
|
|
1374
|
+
* with refcount also 0, starts the idle timer.
|
|
1375
|
+
*/
|
|
1376
|
+
async #trackOp(instance, fn) {
|
|
1377
|
+
instance.opsInFlight++;
|
|
1378
|
+
try {
|
|
1379
|
+
return await fn();
|
|
1380
|
+
} finally {
|
|
1381
|
+
instance.opsInFlight--;
|
|
1382
|
+
if (instance.actors.size === 0 && instance.opsInFlight === 0 && !instance.destroying && !this.#shuttingDown) {
|
|
1383
|
+
this.#startIdleTimer(instance);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Open a database on behalf of an actor, tracked as an in-flight
|
|
1389
|
+
* operation. Used by PooledSqliteHandle to avoid exposing PoolInstance.
|
|
1390
|
+
*/
|
|
1391
|
+
async openForActor(actorId, shortName, options) {
|
|
1392
|
+
const instance = this.#actorToInstance.get(actorId);
|
|
1393
|
+
if (!instance) {
|
|
1394
|
+
throw new Error(
|
|
1395
|
+
`Actor ${actorId} is not assigned to any pool instance`
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
return this.#trackOp(
|
|
1399
|
+
instance,
|
|
1400
|
+
() => instance.vfs.open(shortName, options)
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Track an in-flight database operation for the given actor. Resolves the
|
|
1405
|
+
* actor's pool instance and wraps the operation with opsInFlight tracking.
|
|
1406
|
+
* If the actor has already been released, the operation runs without
|
|
1407
|
+
* tracking since the instance may already be destroyed.
|
|
1408
|
+
*/
|
|
1409
|
+
async trackOpForActor(actorId, fn) {
|
|
1410
|
+
const instance = this.#actorToInstance.get(actorId);
|
|
1411
|
+
if (!instance) {
|
|
1412
|
+
return fn();
|
|
1413
|
+
}
|
|
1414
|
+
return this.#trackOp(instance, fn);
|
|
1415
|
+
}
|
|
1416
|
+
#startIdleTimer(instance) {
|
|
1417
|
+
if (instance.idleTimer || instance.destroying) return;
|
|
1418
|
+
const idleDestroyMs = this.#config.idleDestroyMs ?? 3e4;
|
|
1419
|
+
instance.idleTimer = setTimeout(() => {
|
|
1420
|
+
instance.idleTimer = null;
|
|
1421
|
+
if (instance.actors.size === 0 && instance.opsInFlight === 0 && !instance.destroying) {
|
|
1422
|
+
this.#destroyInstance(instance);
|
|
1423
|
+
}
|
|
1424
|
+
}, idleDestroyMs);
|
|
1425
|
+
}
|
|
1426
|
+
#cancelIdleTimer(instance) {
|
|
1427
|
+
if (instance.idleTimer) {
|
|
1428
|
+
clearTimeout(instance.idleTimer);
|
|
1429
|
+
instance.idleTimer = null;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
async #destroyInstance(instance) {
|
|
1433
|
+
instance.destroying = true;
|
|
1434
|
+
this.#cancelIdleTimer(instance);
|
|
1435
|
+
this.#instances.delete(instance);
|
|
1436
|
+
try {
|
|
1437
|
+
await instance.vfs.forceCloseAll();
|
|
1438
|
+
await instance.vfs.destroy();
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
console.warn("SqliteVfsPool: failed to destroy instance", error);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Graceful shutdown. Rejects new acquire() calls, cancels idle timers,
|
|
1445
|
+
* force-closes all databases, destroys all VFS instances, and clears pool
|
|
1446
|
+
* state.
|
|
1447
|
+
*/
|
|
1448
|
+
async shutdown() {
|
|
1449
|
+
this.#shuttingDown = true;
|
|
1450
|
+
const instances = [...this.#instances];
|
|
1451
|
+
for (const instance of instances) {
|
|
1452
|
+
this.#cancelIdleTimer(instance);
|
|
1453
|
+
this.#instances.delete(instance);
|
|
1454
|
+
if (instance.opsInFlight > 0) {
|
|
1455
|
+
console.warn(
|
|
1456
|
+
`SqliteVfsPool: shutting down instance with ${instance.opsInFlight} in-flight operation(s). Concurrent close is safe due to Database.close() idempotency.`
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
try {
|
|
1460
|
+
await instance.vfs.forceCloseAll();
|
|
1461
|
+
await instance.vfs.destroy();
|
|
1462
|
+
} catch (error) {
|
|
1463
|
+
console.warn(
|
|
1464
|
+
"SqliteVfsPool: failed to destroy instance during shutdown",
|
|
1465
|
+
error
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
this.#actorToInstance.clear();
|
|
1470
|
+
this.#actorToHandle.clear();
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
var TrackedDatabase = class {
|
|
1474
|
+
#inner;
|
|
1475
|
+
#pool;
|
|
1476
|
+
#actorId;
|
|
1477
|
+
constructor(inner, pool, actorId) {
|
|
1478
|
+
this.#inner = inner;
|
|
1479
|
+
this.#pool = pool;
|
|
1480
|
+
this.#actorId = actorId;
|
|
1481
|
+
}
|
|
1482
|
+
async exec(...args) {
|
|
1483
|
+
return this.#pool.trackOpForActor(
|
|
1484
|
+
this.#actorId,
|
|
1485
|
+
() => this.#inner.exec(...args)
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
async run(...args) {
|
|
1489
|
+
return this.#pool.trackOpForActor(
|
|
1490
|
+
this.#actorId,
|
|
1491
|
+
() => this.#inner.run(...args)
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
async query(...args) {
|
|
1495
|
+
return this.#pool.trackOpForActor(
|
|
1496
|
+
this.#actorId,
|
|
1497
|
+
() => this.#inner.query(...args)
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
async close() {
|
|
1501
|
+
return this.#pool.trackOpForActor(
|
|
1502
|
+
this.#actorId,
|
|
1503
|
+
() => this.#inner.close()
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
get fileName() {
|
|
1507
|
+
return this.#inner.fileName;
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
var PooledSqliteHandle = class {
|
|
1511
|
+
#shortName;
|
|
1512
|
+
#actorId;
|
|
1513
|
+
#pool;
|
|
1514
|
+
#released = false;
|
|
1515
|
+
constructor(shortName, actorId, pool) {
|
|
1516
|
+
this.#shortName = shortName;
|
|
1517
|
+
this.#actorId = actorId;
|
|
1518
|
+
this.#pool = pool;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Open a database on the shared instance. Uses the pool-assigned short
|
|
1522
|
+
* name as the VFS file path, with the caller's KvVfsOptions for KV
|
|
1523
|
+
* routing. The open call itself is tracked as an in-flight operation,
|
|
1524
|
+
* and the returned Database is wrapped so that exec(), run(), query(),
|
|
1525
|
+
* and close() are also tracked via opsInFlight.
|
|
1526
|
+
*/
|
|
1527
|
+
async open(_fileName, options) {
|
|
1528
|
+
if (this.#released) {
|
|
1529
|
+
throw new Error("PooledSqliteHandle has been released");
|
|
1530
|
+
}
|
|
1531
|
+
const db = await this.#pool.openForActor(
|
|
1532
|
+
this.#actorId,
|
|
1533
|
+
this.#shortName,
|
|
1534
|
+
options
|
|
1535
|
+
);
|
|
1536
|
+
return new TrackedDatabase(db, this.#pool, this.#actorId);
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Release this actor's assignment back to the pool. Idempotent: calling
|
|
1540
|
+
* destroy() more than once is a no-op, preventing double-release from
|
|
1541
|
+
* decrementing the instance refcount below actual.
|
|
1542
|
+
*/
|
|
1543
|
+
async destroy() {
|
|
1544
|
+
if (this.#released) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
this.#released = true;
|
|
1548
|
+
await this.#pool.release(this.#actorId);
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
export {
|
|
1552
|
+
Database,
|
|
1553
|
+
PooledSqliteHandle,
|
|
1554
|
+
SqliteVfs,
|
|
1555
|
+
SqliteVfsPool
|
|
1556
|
+
};
|
|
1557
|
+
//# sourceMappingURL=index.js.map
|