@rivetkit/sqlite-vfs 2.1.5 → 2.1.6-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tsup/index.cjs +739 -101
- package/dist/tsup/index.cjs.map +1 -1
- package/dist/tsup/index.d.cts +136 -6
- package/dist/tsup/index.d.ts +136 -6
- package/dist/tsup/index.js +737 -99
- package/dist/tsup/index.js.map +1 -1
- package/package.json +4 -3
- package/src/generated/empty-db-page.ts +23 -0
- package/src/index.ts +3 -0
- package/src/kv.ts +18 -3
- package/src/pool.ts +495 -0
- package/src/vfs.ts +604 -131
- package/src/wasm.d.ts +60 -0
package/src/pool.ts
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite VFS Pool - shares WASM SQLite instances across actors to reduce
|
|
3
|
+
* memory overhead. Instead of one WASM module per actor, multiple actors
|
|
4
|
+
* share a single instance, with short file names routing to separate KV
|
|
5
|
+
* namespaces.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { SqliteVfs } from "./vfs";
|
|
11
|
+
import type { ISqliteVfs, IDatabase } from "./vfs";
|
|
12
|
+
import type { KvVfsOptions } from "./types";
|
|
13
|
+
|
|
14
|
+
export interface SqliteVfsPoolConfig {
|
|
15
|
+
actorsPerInstance: number;
|
|
16
|
+
idleDestroyMs?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Internal state for a single WASM SQLite instance shared by multiple actors.
|
|
21
|
+
*/
|
|
22
|
+
interface PoolInstance {
|
|
23
|
+
vfs: SqliteVfs;
|
|
24
|
+
/** Actor IDs currently assigned to this instance. */
|
|
25
|
+
actors: Set<string>;
|
|
26
|
+
/** Monotonically increasing counter for generating short file names. */
|
|
27
|
+
shortNameCounter: number;
|
|
28
|
+
/** Maps actorId to the short name assigned within this instance. */
|
|
29
|
+
actorShortNames: Map<string, string>;
|
|
30
|
+
/** Short names released by actors that closed successfully, available for reuse. */
|
|
31
|
+
availableShortNames: Set<string>;
|
|
32
|
+
/** Short names that failed to close cleanly. Not reused until instance is destroyed. */
|
|
33
|
+
poisonedShortNames: Set<string>;
|
|
34
|
+
/** Number of in-flight operations (e.g. open calls) on this instance. */
|
|
35
|
+
opsInFlight: number;
|
|
36
|
+
/** Handle for the idle destruction timer, or null if not scheduled. */
|
|
37
|
+
idleTimer: ReturnType<typeof setTimeout> | null;
|
|
38
|
+
/** True once destruction has started. Prevents double-destroy. */
|
|
39
|
+
destroying: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Manages a pool of SqliteVfs instances, assigning actors to instances using
|
|
44
|
+
* bin-packing to maximize density. The WASM module is compiled once and
|
|
45
|
+
* reused across all instances.
|
|
46
|
+
*/
|
|
47
|
+
export class SqliteVfsPool {
|
|
48
|
+
readonly #config: SqliteVfsPoolConfig;
|
|
49
|
+
#modulePromise: Promise<WebAssembly.Module> | null = null;
|
|
50
|
+
readonly #instances: Set<PoolInstance> = new Set();
|
|
51
|
+
readonly #actorToInstance: Map<string, PoolInstance> = new Map();
|
|
52
|
+
readonly #actorToHandle: Map<string, PooledSqliteHandle> = new Map();
|
|
53
|
+
#shuttingDown = false;
|
|
54
|
+
|
|
55
|
+
constructor(config: SqliteVfsPoolConfig) {
|
|
56
|
+
if (
|
|
57
|
+
!Number.isInteger(config.actorsPerInstance) ||
|
|
58
|
+
config.actorsPerInstance < 1
|
|
59
|
+
) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`actorsPerInstance must be a positive integer, got ${config.actorsPerInstance}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
this.#config = config;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compile the WASM module once and cache the promise. Subsequent calls
|
|
69
|
+
* return the same promise, avoiding redundant compilation.
|
|
70
|
+
*/
|
|
71
|
+
#getModule(): Promise<WebAssembly.Module> {
|
|
72
|
+
if (!this.#modulePromise) {
|
|
73
|
+
this.#modulePromise = (async () => {
|
|
74
|
+
const require = createRequire(import.meta.url);
|
|
75
|
+
const wasmPath = require.resolve(
|
|
76
|
+
"@rivetkit/sqlite/dist/wa-sqlite-async.wasm",
|
|
77
|
+
);
|
|
78
|
+
const wasmBinary = readFileSync(wasmPath);
|
|
79
|
+
return WebAssembly.compile(wasmBinary);
|
|
80
|
+
})();
|
|
81
|
+
// Clear the cached promise on rejection so subsequent calls retry
|
|
82
|
+
// compilation instead of returning the same rejected promise forever.
|
|
83
|
+
this.#modulePromise.catch(() => {
|
|
84
|
+
this.#modulePromise = null;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return this.#modulePromise;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Number of live WASM instances in the pool. */
|
|
91
|
+
get instanceCount(): number {
|
|
92
|
+
return this.#instances.size;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Number of actors currently assigned to pool instances. */
|
|
96
|
+
get actorCount(): number {
|
|
97
|
+
return this.#actorToInstance.size;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Acquire a pooled VFS handle for the given actor. Returns a
|
|
102
|
+
* PooledSqliteHandle with sticky assignment. If the actor is already
|
|
103
|
+
* assigned, the existing handle is returned.
|
|
104
|
+
*
|
|
105
|
+
* Bin-packing: picks the instance with the most actors that still has
|
|
106
|
+
* capacity. If all instances are full, creates a new one using the
|
|
107
|
+
* cached WASM module.
|
|
108
|
+
*/
|
|
109
|
+
async acquire(actorId: string): Promise<PooledSqliteHandle> {
|
|
110
|
+
if (this.#shuttingDown) {
|
|
111
|
+
throw new Error("SqliteVfsPool is shutting down");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Sticky assignment: return existing handle.
|
|
115
|
+
const existingHandle = this.#actorToHandle.get(actorId);
|
|
116
|
+
if (existingHandle) {
|
|
117
|
+
return existingHandle;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Bin-packing: pick instance with most actors that still has capacity.
|
|
121
|
+
// Skip instances that are being destroyed.
|
|
122
|
+
let bestInstance: PoolInstance | null = null;
|
|
123
|
+
let bestCount = -1;
|
|
124
|
+
for (const instance of this.#instances) {
|
|
125
|
+
if (instance.destroying) continue;
|
|
126
|
+
const count = instance.actors.size;
|
|
127
|
+
if (count < this.#config.actorsPerInstance && count > bestCount) {
|
|
128
|
+
bestInstance = instance;
|
|
129
|
+
bestCount = count;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// If all instances are full, compile the module and re-check capacity.
|
|
134
|
+
// Multiple concurrent acquire() calls may all reach this point. After
|
|
135
|
+
// awaiting the module, re-scan for capacity that another caller may
|
|
136
|
+
// have created during the await, to avoid creating duplicate instances.
|
|
137
|
+
if (!bestInstance) {
|
|
138
|
+
const wasmModule = await this.#getModule();
|
|
139
|
+
if (this.#shuttingDown) {
|
|
140
|
+
throw new Error("SqliteVfsPool is shutting down");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Re-check sticky assignment: another concurrent acquire() for the
|
|
144
|
+
// same actorId may have completed during the await.
|
|
145
|
+
const existingHandleAfterAwait = this.#actorToHandle.get(actorId);
|
|
146
|
+
if (existingHandleAfterAwait) {
|
|
147
|
+
return existingHandleAfterAwait;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Re-scan for an instance with available capacity that was created
|
|
151
|
+
// by another concurrent acquire() during the module compilation.
|
|
152
|
+
for (const instance of this.#instances) {
|
|
153
|
+
if (instance.destroying) continue;
|
|
154
|
+
const count = instance.actors.size;
|
|
155
|
+
if (count < this.#config.actorsPerInstance && count > bestCount) {
|
|
156
|
+
bestInstance = instance;
|
|
157
|
+
bestCount = count;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!bestInstance) {
|
|
162
|
+
const vfs = new SqliteVfs(wasmModule);
|
|
163
|
+
bestInstance = {
|
|
164
|
+
vfs,
|
|
165
|
+
actors: new Set(),
|
|
166
|
+
shortNameCounter: 0,
|
|
167
|
+
actorShortNames: new Map(),
|
|
168
|
+
availableShortNames: new Set(),
|
|
169
|
+
poisonedShortNames: new Set(),
|
|
170
|
+
opsInFlight: 0,
|
|
171
|
+
idleTimer: null,
|
|
172
|
+
destroying: false,
|
|
173
|
+
};
|
|
174
|
+
this.#instances.add(bestInstance);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Cancel idle timer synchronously since this instance is getting a
|
|
179
|
+
// new actor and should not be destroyed.
|
|
180
|
+
this.#cancelIdleTimer(bestInstance);
|
|
181
|
+
|
|
182
|
+
// Assign actor to instance with a short file name. Prefer recycled
|
|
183
|
+
// names from the available set before generating a new one.
|
|
184
|
+
let shortName: string;
|
|
185
|
+
const recycled = bestInstance.availableShortNames.values().next();
|
|
186
|
+
if (!recycled.done) {
|
|
187
|
+
shortName = recycled.value;
|
|
188
|
+
bestInstance.availableShortNames.delete(shortName);
|
|
189
|
+
} else {
|
|
190
|
+
shortName = String(bestInstance.shortNameCounter++);
|
|
191
|
+
}
|
|
192
|
+
bestInstance.actors.add(actorId);
|
|
193
|
+
bestInstance.actorShortNames.set(actorId, shortName);
|
|
194
|
+
this.#actorToInstance.set(actorId, bestInstance);
|
|
195
|
+
|
|
196
|
+
const handle = new PooledSqliteHandle(
|
|
197
|
+
shortName,
|
|
198
|
+
actorId,
|
|
199
|
+
this,
|
|
200
|
+
);
|
|
201
|
+
this.#actorToHandle.set(actorId, handle);
|
|
202
|
+
|
|
203
|
+
return handle;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Release an actor's assignment from the pool. Force-closes all database
|
|
208
|
+
* handles for the actor, recycles or poisons the short name, and
|
|
209
|
+
* decrements the instance refcount.
|
|
210
|
+
*/
|
|
211
|
+
async release(actorId: string): Promise<void> {
|
|
212
|
+
const instance = this.#actorToInstance.get(actorId);
|
|
213
|
+
if (!instance) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const shortName = instance.actorShortNames.get(actorId);
|
|
218
|
+
if (shortName === undefined) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Force-close all Database handles for this actor's short name.
|
|
223
|
+
const { allSucceeded } =
|
|
224
|
+
await instance.vfs.forceCloseByFileName(shortName);
|
|
225
|
+
|
|
226
|
+
if (allSucceeded) {
|
|
227
|
+
instance.availableShortNames.add(shortName);
|
|
228
|
+
} else {
|
|
229
|
+
instance.poisonedShortNames.add(shortName);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Remove actor from instance tracking.
|
|
233
|
+
instance.actors.delete(actorId);
|
|
234
|
+
instance.actorShortNames.delete(actorId);
|
|
235
|
+
this.#actorToInstance.delete(actorId);
|
|
236
|
+
this.#actorToHandle.delete(actorId);
|
|
237
|
+
|
|
238
|
+
// Start idle timer if instance has no actors and no in-flight ops.
|
|
239
|
+
// Skip if shutting down to avoid leaking timers after shutdown
|
|
240
|
+
// completes.
|
|
241
|
+
if (instance.actors.size === 0 && instance.opsInFlight === 0 && !this.#shuttingDown) {
|
|
242
|
+
this.#startIdleTimer(instance);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Track an in-flight operation on an instance. Increments opsInFlight
|
|
248
|
+
* before running fn, decrements after using try/finally to prevent
|
|
249
|
+
* drift from exceptions. If the decrement brings opsInFlight to 0
|
|
250
|
+
* with refcount also 0, starts the idle timer.
|
|
251
|
+
*/
|
|
252
|
+
async #trackOp<T>(
|
|
253
|
+
instance: PoolInstance,
|
|
254
|
+
fn: () => Promise<T>,
|
|
255
|
+
): Promise<T> {
|
|
256
|
+
instance.opsInFlight++;
|
|
257
|
+
try {
|
|
258
|
+
return await fn();
|
|
259
|
+
} finally {
|
|
260
|
+
instance.opsInFlight--;
|
|
261
|
+
if (
|
|
262
|
+
instance.actors.size === 0 &&
|
|
263
|
+
instance.opsInFlight === 0 &&
|
|
264
|
+
!instance.destroying &&
|
|
265
|
+
!this.#shuttingDown
|
|
266
|
+
) {
|
|
267
|
+
this.#startIdleTimer(instance);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Open a database on behalf of an actor, tracked as an in-flight
|
|
274
|
+
* operation. Used by PooledSqliteHandle to avoid exposing PoolInstance.
|
|
275
|
+
*/
|
|
276
|
+
async openForActor(
|
|
277
|
+
actorId: string,
|
|
278
|
+
shortName: string,
|
|
279
|
+
options: KvVfsOptions,
|
|
280
|
+
): Promise<IDatabase> {
|
|
281
|
+
const instance = this.#actorToInstance.get(actorId);
|
|
282
|
+
if (!instance) {
|
|
283
|
+
throw new Error(`Actor ${actorId} is not assigned to any pool instance`);
|
|
284
|
+
}
|
|
285
|
+
return this.#trackOp(instance, () =>
|
|
286
|
+
instance.vfs.open(shortName, options),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Track an in-flight database operation for the given actor. Resolves the
|
|
292
|
+
* actor's pool instance and wraps the operation with opsInFlight tracking.
|
|
293
|
+
* If the actor has already been released, the operation runs without
|
|
294
|
+
* tracking since the instance may already be destroyed.
|
|
295
|
+
*/
|
|
296
|
+
async trackOpForActor<T>(
|
|
297
|
+
actorId: string,
|
|
298
|
+
fn: () => Promise<T>,
|
|
299
|
+
): Promise<T> {
|
|
300
|
+
const instance = this.#actorToInstance.get(actorId);
|
|
301
|
+
if (!instance) {
|
|
302
|
+
return fn();
|
|
303
|
+
}
|
|
304
|
+
return this.#trackOp(instance, fn);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#startIdleTimer(instance: PoolInstance): void {
|
|
308
|
+
if (instance.idleTimer || instance.destroying) return;
|
|
309
|
+
const idleDestroyMs = this.#config.idleDestroyMs ?? 30_000;
|
|
310
|
+
instance.idleTimer = setTimeout(() => {
|
|
311
|
+
instance.idleTimer = null;
|
|
312
|
+
// Check opsInFlight in addition to actors.size. With tracked
|
|
313
|
+
// database operations (TrackedDatabase), opsInFlight can be >0
|
|
314
|
+
// while actors.size is 0 if the last operation is still in-flight
|
|
315
|
+
// after release. The #trackOp finally block will re-start the
|
|
316
|
+
// idle timer when ops drain to 0.
|
|
317
|
+
if (
|
|
318
|
+
instance.actors.size === 0 &&
|
|
319
|
+
instance.opsInFlight === 0 &&
|
|
320
|
+
!instance.destroying
|
|
321
|
+
) {
|
|
322
|
+
this.#destroyInstance(instance);
|
|
323
|
+
}
|
|
324
|
+
}, idleDestroyMs);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#cancelIdleTimer(instance: PoolInstance): void {
|
|
328
|
+
if (instance.idleTimer) {
|
|
329
|
+
clearTimeout(instance.idleTimer);
|
|
330
|
+
instance.idleTimer = null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async #destroyInstance(instance: PoolInstance): Promise<void> {
|
|
335
|
+
instance.destroying = true;
|
|
336
|
+
this.#cancelIdleTimer(instance);
|
|
337
|
+
// Remove from pool map first so no new actors can be assigned.
|
|
338
|
+
this.#instances.delete(instance);
|
|
339
|
+
try {
|
|
340
|
+
await instance.vfs.forceCloseAll();
|
|
341
|
+
await instance.vfs.destroy();
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.warn("SqliteVfsPool: failed to destroy instance", error);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Graceful shutdown. Rejects new acquire() calls, cancels idle timers,
|
|
349
|
+
* force-closes all databases, destroys all VFS instances, and clears pool
|
|
350
|
+
* state.
|
|
351
|
+
*/
|
|
352
|
+
async shutdown(): Promise<void> {
|
|
353
|
+
this.#shuttingDown = true;
|
|
354
|
+
|
|
355
|
+
// Snapshot instances to array since we mutate the set during iteration.
|
|
356
|
+
const instances = [...this.#instances];
|
|
357
|
+
|
|
358
|
+
for (const instance of instances) {
|
|
359
|
+
this.#cancelIdleTimer(instance);
|
|
360
|
+
this.#instances.delete(instance);
|
|
361
|
+
|
|
362
|
+
// Check for in-flight operations (e.g. a concurrent release() call
|
|
363
|
+
// mid-forceCloseByFileName). Database.close() is idempotent
|
|
364
|
+
// (US-019), so concurrent close from shutdown + release is safe,
|
|
365
|
+
// but log a warning for observability.
|
|
366
|
+
if (instance.opsInFlight > 0) {
|
|
367
|
+
console.warn(
|
|
368
|
+
`SqliteVfsPool: shutting down instance with ${instance.opsInFlight} in-flight operation(s). ` +
|
|
369
|
+
"Concurrent close is safe due to Database.close() idempotency.",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
await instance.vfs.forceCloseAll();
|
|
375
|
+
await instance.vfs.destroy();
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.warn("SqliteVfsPool: failed to destroy instance during shutdown", error);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.#actorToInstance.clear();
|
|
382
|
+
this.#actorToHandle.clear();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Wraps a Database with opsInFlight tracking so the pool's idle timer
|
|
388
|
+
* does not destroy instances while database operations are in-flight.
|
|
389
|
+
* The unwrapped Database remains in SqliteVfs's #openDatabases set
|
|
390
|
+
* for force-close purposes.
|
|
391
|
+
*/
|
|
392
|
+
class TrackedDatabase implements IDatabase {
|
|
393
|
+
readonly #inner: IDatabase;
|
|
394
|
+
readonly #pool: SqliteVfsPool;
|
|
395
|
+
readonly #actorId: string;
|
|
396
|
+
|
|
397
|
+
constructor(inner: IDatabase, pool: SqliteVfsPool, actorId: string) {
|
|
398
|
+
this.#inner = inner;
|
|
399
|
+
this.#pool = pool;
|
|
400
|
+
this.#actorId = actorId;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async exec(
|
|
404
|
+
...args: Parameters<IDatabase["exec"]>
|
|
405
|
+
): ReturnType<IDatabase["exec"]> {
|
|
406
|
+
return this.#pool.trackOpForActor(this.#actorId, () =>
|
|
407
|
+
this.#inner.exec(...args),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async run(
|
|
412
|
+
...args: Parameters<IDatabase["run"]>
|
|
413
|
+
): ReturnType<IDatabase["run"]> {
|
|
414
|
+
return this.#pool.trackOpForActor(this.#actorId, () =>
|
|
415
|
+
this.#inner.run(...args),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async query(
|
|
420
|
+
...args: Parameters<IDatabase["query"]>
|
|
421
|
+
): ReturnType<IDatabase["query"]> {
|
|
422
|
+
return this.#pool.trackOpForActor(this.#actorId, () =>
|
|
423
|
+
this.#inner.query(...args),
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async close(): ReturnType<IDatabase["close"]> {
|
|
428
|
+
return this.#pool.trackOpForActor(this.#actorId, () =>
|
|
429
|
+
this.#inner.close(),
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
get fileName(): string {
|
|
434
|
+
return this.#inner.fileName;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* A pooled VFS handle for a single actor. Implements ISqliteVfs so callers
|
|
440
|
+
* can use it interchangeably with a standalone SqliteVfs. The short name
|
|
441
|
+
* assigned by the pool is used as the VFS file path, while the caller's
|
|
442
|
+
* KvVfsOptions routes data to the correct KV namespace.
|
|
443
|
+
*/
|
|
444
|
+
export class PooledSqliteHandle implements ISqliteVfs {
|
|
445
|
+
readonly #shortName: string;
|
|
446
|
+
readonly #actorId: string;
|
|
447
|
+
readonly #pool: SqliteVfsPool;
|
|
448
|
+
#released = false;
|
|
449
|
+
|
|
450
|
+
constructor(
|
|
451
|
+
shortName: string,
|
|
452
|
+
actorId: string,
|
|
453
|
+
pool: SqliteVfsPool,
|
|
454
|
+
) {
|
|
455
|
+
this.#shortName = shortName;
|
|
456
|
+
this.#actorId = actorId;
|
|
457
|
+
this.#pool = pool;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Open a database on the shared instance. Uses the pool-assigned short
|
|
462
|
+
* name as the VFS file path, with the caller's KvVfsOptions for KV
|
|
463
|
+
* routing. The open call itself is tracked as an in-flight operation,
|
|
464
|
+
* and the returned Database is wrapped so that exec(), run(), query(),
|
|
465
|
+
* and close() are also tracked via opsInFlight.
|
|
466
|
+
*/
|
|
467
|
+
async open(_fileName: string, options: KvVfsOptions): Promise<IDatabase> {
|
|
468
|
+
if (this.#released) {
|
|
469
|
+
throw new Error("PooledSqliteHandle has been released");
|
|
470
|
+
}
|
|
471
|
+
const db = await this.#pool.openForActor(
|
|
472
|
+
this.#actorId,
|
|
473
|
+
this.#shortName,
|
|
474
|
+
options,
|
|
475
|
+
);
|
|
476
|
+
return new TrackedDatabase(
|
|
477
|
+
db,
|
|
478
|
+
this.#pool,
|
|
479
|
+
this.#actorId,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Release this actor's assignment back to the pool. Idempotent: calling
|
|
485
|
+
* destroy() more than once is a no-op, preventing double-release from
|
|
486
|
+
* decrementing the instance refcount below actual.
|
|
487
|
+
*/
|
|
488
|
+
async destroy(): Promise<void> {
|
|
489
|
+
if (this.#released) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
this.#released = true;
|
|
493
|
+
await this.#pool.release(this.#actorId);
|
|
494
|
+
}
|
|
495
|
+
}
|