@framers/sql-storage-adapter 0.6.13 → 0.6.15
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 +70 -38
- package/dist/adapters/capacitorSqliteAdapter.d.ts +42 -0
- package/dist/adapters/capacitorSqliteAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitorSqliteAdapter.js +139 -21
- package/dist/adapters/capacitorSqliteAdapter.js.map +1 -1
- package/dist/adapters/indexedDbAdapter.d.ts +89 -10
- package/dist/adapters/indexedDbAdapter.d.ts.map +1 -1
- package/dist/adapters/indexedDbAdapter.js +265 -58
- package/dist/adapters/indexedDbAdapter.js.map +1 -1
- package/dist/adapters/postgresAdapter.d.ts +83 -5
- package/dist/adapters/postgresAdapter.d.ts.map +1 -1
- package/dist/adapters/postgresAdapter.js +285 -18
- package/dist/adapters/postgresAdapter.js.map +1 -1
- package/dist/core/database.d.ts +37 -8
- package/dist/core/database.d.ts.map +1 -1
- package/dist/core/database.js +48 -11
- package/dist/core/database.js.map +1 -1
- package/dist/core/resolver.d.ts +34 -5
- package/dist/core/resolver.d.ts.map +1 -1
- package/dist/core/resolver.js +59 -9
- package/dist/core/resolver.js.map +1 -1
- package/dist/shared/connectionGate.d.ts +32 -9
- package/dist/shared/connectionGate.d.ts.map +1 -1
- package/dist/shared/connectionGate.js +31 -12
- package/dist/shared/connectionGate.js.map +1 -1
- package/dist/shared/postgresTarget.d.ts +10 -0
- package/dist/shared/postgresTarget.d.ts.map +1 -0
- package/dist/shared/postgresTarget.js +13 -0
- package/dist/shared/postgresTarget.js.map +1 -0
- package/package.json +2 -1
|
@@ -134,7 +134,32 @@ export declare class IndexedDbAdapter implements StorageAdapter {
|
|
|
134
134
|
private db;
|
|
135
135
|
private idb;
|
|
136
136
|
private saveTimer;
|
|
137
|
-
|
|
137
|
+
/** Bumped by every statement that can change the database. */
|
|
138
|
+
private writeGeneration;
|
|
139
|
+
/** The generation the last completed save exported; the writes after it are unsaved. */
|
|
140
|
+
private savedGeneration;
|
|
141
|
+
/** The save in progress, if any. */
|
|
142
|
+
private saving;
|
|
143
|
+
/** Whether a failed auto-save has been reported since the last one that succeeded. */
|
|
144
|
+
private saveFailureReported;
|
|
145
|
+
/** Set while close() runs: no statement or transaction starts. */
|
|
146
|
+
private closing;
|
|
147
|
+
/** The close() in progress, which a second call waits for. */
|
|
148
|
+
private closeRun;
|
|
149
|
+
private longWaitReported;
|
|
150
|
+
/**
|
|
151
|
+
* The connection is shared, so transaction() runs one callback at a time.
|
|
152
|
+
*/
|
|
153
|
+
private readonly runSerializedTransaction;
|
|
154
|
+
/**
|
|
155
|
+
* Holds statements from outside an open transaction until it ends, so they
|
|
156
|
+
* neither join it (and vanish in its rollback) nor read what it has not
|
|
157
|
+
* committed. Browsers have no async context to tell the callback's own
|
|
158
|
+
* statements apart, so the callback's handle is the only way into an open
|
|
159
|
+
* transaction: a statement through the adapter itself waits.
|
|
160
|
+
*/
|
|
161
|
+
private readonly gate;
|
|
162
|
+
private readonly longWait;
|
|
138
163
|
private readonly dbName;
|
|
139
164
|
private readonly storeName;
|
|
140
165
|
private readonly autoSave;
|
|
@@ -157,6 +182,7 @@ export declare class IndexedDbAdapter implements StorageAdapter {
|
|
|
157
182
|
* @returns Result with changes count and last insert row ID
|
|
158
183
|
*/
|
|
159
184
|
run(statement: string, parameters?: StorageParameters): Promise<StorageRunResult>;
|
|
185
|
+
private runNow;
|
|
160
186
|
/**
|
|
161
187
|
* Executes a SQL query and returns the first row.
|
|
162
188
|
* @param statement SELECT statement
|
|
@@ -171,17 +197,32 @@ export declare class IndexedDbAdapter implements StorageAdapter {
|
|
|
171
197
|
* @returns Array of result rows as objects
|
|
172
198
|
*/
|
|
173
199
|
all<T>(statement: string, parameters?: StorageParameters): Promise<T[]>;
|
|
200
|
+
private allNow;
|
|
174
201
|
/**
|
|
175
202
|
* Executes a script containing multiple SQL statements.
|
|
176
203
|
*/
|
|
177
204
|
exec(script: string): Promise<void>;
|
|
205
|
+
private execNow;
|
|
178
206
|
/**
|
|
179
207
|
* Executes a callback within a database transaction.
|
|
208
|
+
*
|
|
209
|
+
* Callbacks run one at a time, since every transaction shares the one
|
|
210
|
+
* connection. The callback's statements go through the handle it receives;
|
|
211
|
+
* a statement issued through the adapter itself while the callback runs
|
|
212
|
+
* waits until the transaction ends, so a callback must not await one. The
|
|
213
|
+
* auto-save tick does not save while a transaction is open.
|
|
180
214
|
*/
|
|
181
215
|
transaction<T>(fn: (trx: StorageAdapter) => Promise<T>): Promise<T>;
|
|
182
216
|
/**
|
|
183
|
-
*
|
|
184
|
-
* so
|
|
217
|
+
* The statements of one transaction's handle: they carry the transaction's
|
|
218
|
+
* token, so the gate admits them while the transaction is open.
|
|
219
|
+
*/
|
|
220
|
+
private handleStatements;
|
|
221
|
+
/** Rolls back the open transaction; one that already ended (a failed statement, close()) is left alone. */
|
|
222
|
+
private rollbackQuietly;
|
|
223
|
+
/**
|
|
224
|
+
* Begins a transaction the caller ends with commit() or rollback(). It is not
|
|
225
|
+
* the callback form: other callers' statements are not held out of it.
|
|
185
226
|
*/
|
|
186
227
|
beginTransaction(): Promise<void>;
|
|
187
228
|
/**
|
|
@@ -193,18 +234,27 @@ export declare class IndexedDbAdapter implements StorageAdapter {
|
|
|
193
234
|
*/
|
|
194
235
|
rollback(): Promise<void>;
|
|
195
236
|
/**
|
|
196
|
-
* Closes the database and
|
|
237
|
+
* Closes the database and saves its committed state to IndexedDB.
|
|
238
|
+
*
|
|
239
|
+
* A transaction still open is rolled back, as closing a SQLite connection
|
|
240
|
+
* does, before the save; the save then holds committed writes only. A call
|
|
241
|
+
* made while a close is running waits for that close.
|
|
197
242
|
*/
|
|
198
243
|
close(): Promise<void>;
|
|
244
|
+
private closeNow;
|
|
199
245
|
/**
|
|
200
246
|
* Exports the database as a Uint8Array (SQLite file format).
|
|
201
247
|
* Can be downloaded or stored externally.
|
|
248
|
+
*
|
|
249
|
+
* Throws while a transaction is open: sql.js's export closes and reopens the
|
|
250
|
+
* connection, which would end the transaction.
|
|
202
251
|
* @returns SQLite database file as binary
|
|
203
252
|
*/
|
|
204
253
|
exportDatabase(): Uint8Array;
|
|
205
254
|
/**
|
|
206
255
|
* Imports a database from a Uint8Array (SQLite file).
|
|
207
|
-
* Replaces the current database
|
|
256
|
+
* Replaces the current database once any open transaction() has ended, and
|
|
257
|
+
* saves it to IndexedDB before it resolves.
|
|
208
258
|
* @param data SQLite database file
|
|
209
259
|
*/
|
|
210
260
|
importDatabase(data: Uint8Array): Promise<void>;
|
|
@@ -231,22 +281,51 @@ export declare class IndexedDbAdapter implements StorageAdapter {
|
|
|
231
281
|
*/
|
|
232
282
|
private loadFromIndexedDb;
|
|
233
283
|
/**
|
|
234
|
-
*
|
|
284
|
+
* Writes the current image to IndexedDB. Never called while a transaction is
|
|
285
|
+
* open (see saveIfIdle): sql.js's export() closes and reopens the connection,
|
|
286
|
+
* which ends an open transaction.
|
|
287
|
+
*
|
|
288
|
+
* Settles when the IndexedDB transaction completes, not when its put request
|
|
289
|
+
* succeeds: the transaction can still abort after that (a quota error at
|
|
290
|
+
* commit), and the image is then not stored. On abort the writes stay
|
|
291
|
+
* unsaved, so the next save writes them. A write made after the export stays
|
|
292
|
+
* unsaved too.
|
|
235
293
|
*/
|
|
236
294
|
private saveToIndexedDb;
|
|
295
|
+
/** Whether writes were made since the image the last completed save exported. */
|
|
296
|
+
private hasUnsavedWrites;
|
|
297
|
+
/**
|
|
298
|
+
* Whether a transaction is open on the connection: one transaction() is
|
|
299
|
+
* running, or one the caller began with BEGIN or SAVEPOINT. sql.js does not
|
|
300
|
+
* export sqlite3_get_autocommit, so the adapter asks SQLite: BEGIN fails
|
|
301
|
+
* inside a transaction, and outside one it is rolled back at once. A probe
|
|
302
|
+
* that fails for any reason counts as open, so the adapter does not export.
|
|
303
|
+
*/
|
|
304
|
+
private inTransaction;
|
|
305
|
+
/**
|
|
306
|
+
* Starts a save of the unsaved writes, unless a save is running or a
|
|
307
|
+
* transaction is open; the auto-save tick tries again later.
|
|
308
|
+
*/
|
|
309
|
+
private saveIfIdle;
|
|
310
|
+
/**
|
|
311
|
+
* Starts a save at once and keeps it as the save in progress. The caller has
|
|
312
|
+
* made sure no transaction is open: the export would end it.
|
|
313
|
+
*/
|
|
314
|
+
private startSave;
|
|
315
|
+
/** Waits for a save in progress to settle, whether it succeeded or not. */
|
|
316
|
+
private settleSave;
|
|
317
|
+
private reportLongWait;
|
|
237
318
|
/**
|
|
238
319
|
* Wraps a transaction operation with retry logic.
|
|
239
320
|
* Handles "NotFoundError" by reopening IndexedDB connection.
|
|
240
321
|
*/
|
|
241
322
|
private withTransactionRetry;
|
|
242
|
-
/**
|
|
243
|
-
* Persists to IndexedDB if auto-save is enabled.
|
|
244
|
-
*/
|
|
245
|
-
private persistIfNeeded;
|
|
246
323
|
/**
|
|
247
324
|
* Starts auto-save interval.
|
|
248
325
|
*/
|
|
249
326
|
private startAutoSave;
|
|
327
|
+
/** One auto-save tick: a failed save is reported once and tried again on the next tick. */
|
|
328
|
+
private autoSaveTick;
|
|
250
329
|
/**
|
|
251
330
|
* Ensures the database is open.
|
|
252
331
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexedDbAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/indexedDbAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAIH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"indexedDbAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/indexedDbAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import initSqlJs from 'sql.js';
|
|
46
46
|
import { normaliseParameters } from '../shared/parameterUtils.js';
|
|
47
|
+
import { createSerializedTransactionRunner, createSharedConnectionTransactionAdapter, } from '../shared/serializedTransaction.js';
|
|
48
|
+
import { createConnectionGate, whenAdmitted } from '../shared/connectionGate.js';
|
|
49
|
+
/** How long a statement waits for an open transaction before the adapter warns. */
|
|
50
|
+
const LONG_WAIT_MS = 10000;
|
|
47
51
|
// ============================================================================
|
|
48
52
|
// GLOBAL SQL.JS SINGLETON
|
|
49
53
|
// ============================================================================
|
|
@@ -147,7 +151,35 @@ export class IndexedDbAdapter {
|
|
|
147
151
|
this.db = null;
|
|
148
152
|
this.idb = null;
|
|
149
153
|
this.saveTimer = null;
|
|
150
|
-
|
|
154
|
+
/** Bumped by every statement that can change the database. */
|
|
155
|
+
this.writeGeneration = 0;
|
|
156
|
+
/** The generation the last completed save exported; the writes after it are unsaved. */
|
|
157
|
+
this.savedGeneration = 0;
|
|
158
|
+
/** The save in progress, if any. */
|
|
159
|
+
this.saving = null;
|
|
160
|
+
/** Whether a failed auto-save has been reported since the last one that succeeded. */
|
|
161
|
+
this.saveFailureReported = false;
|
|
162
|
+
/** Set while close() runs: no statement or transaction starts. */
|
|
163
|
+
this.closing = false;
|
|
164
|
+
/** The close() in progress, which a second call waits for. */
|
|
165
|
+
this.closeRun = null;
|
|
166
|
+
this.longWaitReported = false;
|
|
167
|
+
/**
|
|
168
|
+
* The connection is shared, so transaction() runs one callback at a time.
|
|
169
|
+
*/
|
|
170
|
+
this.runSerializedTransaction = createSerializedTransactionRunner();
|
|
171
|
+
/**
|
|
172
|
+
* Holds statements from outside an open transaction until it ends, so they
|
|
173
|
+
* neither join it (and vanish in its rollback) nor read what it has not
|
|
174
|
+
* committed. Browsers have no async context to tell the callback's own
|
|
175
|
+
* statements apart, so the callback's handle is the only way into an open
|
|
176
|
+
* transaction: a statement through the adapter itself waits.
|
|
177
|
+
*/
|
|
178
|
+
this.gate = createConnectionGate(null, { withoutContext: 'wait' });
|
|
179
|
+
this.longWait = {
|
|
180
|
+
afterMs: LONG_WAIT_MS,
|
|
181
|
+
onLongWait: () => this.reportLongWait(),
|
|
182
|
+
};
|
|
151
183
|
this.dbName = options.dbName || 'app-db';
|
|
152
184
|
this.storeName = options.storeName || 'sqliteDb';
|
|
153
185
|
this.autoSave = options.autoSave ?? true;
|
|
@@ -190,6 +222,9 @@ export class IndexedDbAdapter {
|
|
|
190
222
|
* @returns Result with changes count and last insert row ID
|
|
191
223
|
*/
|
|
192
224
|
async run(statement, parameters) {
|
|
225
|
+
return whenAdmitted(this.gate, () => this.runNow(statement, parameters), undefined, this.longWait);
|
|
226
|
+
}
|
|
227
|
+
runNow(statement, parameters) {
|
|
193
228
|
this.ensureOpen();
|
|
194
229
|
const stmt = this.db.prepare(statement);
|
|
195
230
|
try {
|
|
@@ -204,8 +239,7 @@ export class IndexedDbAdapter {
|
|
|
204
239
|
const rowIdResult = this.db.exec('SELECT last_insert_rowid() AS id');
|
|
205
240
|
const rawRowId = rowIdResult[0]?.values?.[0]?.[0];
|
|
206
241
|
const lastInsertRowid = this.normaliseRowId(rawRowId);
|
|
207
|
-
this.
|
|
208
|
-
await this.persistIfNeeded();
|
|
242
|
+
this.writeGeneration += 1;
|
|
209
243
|
return {
|
|
210
244
|
changes: this.db.getRowsModified(),
|
|
211
245
|
lastInsertRowid,
|
|
@@ -232,6 +266,9 @@ export class IndexedDbAdapter {
|
|
|
232
266
|
* @returns Array of result rows as objects
|
|
233
267
|
*/
|
|
234
268
|
async all(statement, parameters) {
|
|
269
|
+
return whenAdmitted(this.gate, () => this.allNow(statement, parameters), undefined, this.longWait);
|
|
270
|
+
}
|
|
271
|
+
allNow(statement, parameters) {
|
|
235
272
|
this.ensureOpen();
|
|
236
273
|
const stmt = this.db.prepare(statement);
|
|
237
274
|
try {
|
|
@@ -263,35 +300,91 @@ export class IndexedDbAdapter {
|
|
|
263
300
|
* Executes a script containing multiple SQL statements.
|
|
264
301
|
*/
|
|
265
302
|
async exec(script) {
|
|
303
|
+
return whenAdmitted(this.gate, () => this.execNow(script), undefined, this.longWait);
|
|
304
|
+
}
|
|
305
|
+
execNow(script) {
|
|
266
306
|
this.ensureOpen();
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
307
|
+
try {
|
|
308
|
+
this.db.exec(script);
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
// A script that fails part way keeps the statements before the failure.
|
|
312
|
+
this.writeGeneration += 1;
|
|
313
|
+
}
|
|
270
314
|
}
|
|
271
315
|
/**
|
|
272
316
|
* Executes a callback within a database transaction.
|
|
317
|
+
*
|
|
318
|
+
* Callbacks run one at a time, since every transaction shares the one
|
|
319
|
+
* connection. The callback's statements go through the handle it receives;
|
|
320
|
+
* a statement issued through the adapter itself while the callback runs
|
|
321
|
+
* waits until the transaction ends, so a callback must not await one. The
|
|
322
|
+
* auto-save tick does not save while a transaction is open.
|
|
273
323
|
*/
|
|
274
324
|
async transaction(fn) {
|
|
275
325
|
this.ensureOpen();
|
|
276
|
-
|
|
326
|
+
const queued = setTimeout(() => this.reportLongWait(), LONG_WAIT_MS);
|
|
327
|
+
return this.runSerializedTransaction(() => {
|
|
328
|
+
clearTimeout(queued);
|
|
329
|
+
return this.gate.hold(async (token) => {
|
|
330
|
+
this.ensureOpen();
|
|
331
|
+
// Outside the try: when BEGIN fails, the transaction that is open
|
|
332
|
+
// (a caller's own BEGIN) is not this one to roll back.
|
|
333
|
+
this.db.run('BEGIN TRANSACTION');
|
|
334
|
+
const transactionAdapter = createSharedConnectionTransactionAdapter(this.handleStatements(token));
|
|
335
|
+
try {
|
|
336
|
+
const result = await fn(transactionAdapter);
|
|
337
|
+
this.ensureOpen();
|
|
338
|
+
this.db.run('COMMIT');
|
|
339
|
+
// The callback may have written through all() or get() (INSERT ...
|
|
340
|
+
// RETURNING), which mark nothing unsaved; the commit does.
|
|
341
|
+
this.writeGeneration += 1;
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
this.rollbackQuietly();
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The statements of one transaction's handle: they carry the transaction's
|
|
353
|
+
* token, so the gate admits them while the transaction is open.
|
|
354
|
+
*/
|
|
355
|
+
handleStatements(token) {
|
|
356
|
+
return {
|
|
357
|
+
kind: this.kind,
|
|
358
|
+
capabilities: this.capabilities,
|
|
359
|
+
open: async () => { },
|
|
360
|
+
close: async () => { },
|
|
361
|
+
run: (statement, parameters) => whenAdmitted(this.gate, () => this.runNow(statement, parameters), token),
|
|
362
|
+
get: async (statement, parameters) => {
|
|
363
|
+
const rows = await whenAdmitted(this.gate, () => this.allNow(statement, parameters), token);
|
|
364
|
+
return rows.length > 0 ? rows[0] : null;
|
|
365
|
+
},
|
|
366
|
+
all: (statement, parameters) => whenAdmitted(this.gate, () => this.allNow(statement, parameters), token),
|
|
367
|
+
exec: (script) => whenAdmitted(this.gate, () => this.execNow(script), token),
|
|
368
|
+
// The handle answers a nested transaction() itself and never calls this.
|
|
369
|
+
transaction: (fn) => fn(this),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/** Rolls back the open transaction; one that already ended (a failed statement, close()) is left alone. */
|
|
373
|
+
rollbackQuietly() {
|
|
374
|
+
if (!this.db)
|
|
375
|
+
return;
|
|
277
376
|
try {
|
|
278
|
-
|
|
279
|
-
await this.run('COMMIT');
|
|
280
|
-
this.dirty = true;
|
|
281
|
-
await this.persistIfNeeded();
|
|
282
|
-
return result;
|
|
377
|
+
this.db.run('ROLLBACK');
|
|
283
378
|
}
|
|
284
|
-
catch
|
|
285
|
-
|
|
286
|
-
throw error;
|
|
379
|
+
catch {
|
|
380
|
+
// No transaction is open.
|
|
287
381
|
}
|
|
288
382
|
}
|
|
289
383
|
/**
|
|
290
|
-
* Begins a transaction
|
|
291
|
-
*
|
|
384
|
+
* Begins a transaction the caller ends with commit() or rollback(). It is not
|
|
385
|
+
* the callback form: other callers' statements are not held out of it.
|
|
292
386
|
*/
|
|
293
387
|
async beginTransaction() {
|
|
294
|
-
// sql.js uses auto-commit; explicit BEGIN is optional
|
|
295
388
|
await this.run('BEGIN');
|
|
296
389
|
}
|
|
297
390
|
/**
|
|
@@ -299,8 +392,6 @@ export class IndexedDbAdapter {
|
|
|
299
392
|
*/
|
|
300
393
|
async commit() {
|
|
301
394
|
await this.run('COMMIT');
|
|
302
|
-
this.dirty = true;
|
|
303
|
-
await this.persistIfNeeded();
|
|
304
395
|
}
|
|
305
396
|
/**
|
|
306
397
|
* Rolls back the current transaction.
|
|
@@ -309,19 +400,44 @@ export class IndexedDbAdapter {
|
|
|
309
400
|
await this.run('ROLLBACK');
|
|
310
401
|
}
|
|
311
402
|
/**
|
|
312
|
-
* Closes the database and
|
|
403
|
+
* Closes the database and saves its committed state to IndexedDB.
|
|
404
|
+
*
|
|
405
|
+
* A transaction still open is rolled back, as closing a SQLite connection
|
|
406
|
+
* does, before the save; the save then holds committed writes only. A call
|
|
407
|
+
* made while a close is running waits for that close.
|
|
313
408
|
*/
|
|
314
|
-
|
|
409
|
+
close() {
|
|
410
|
+
if (!this.closeRun) {
|
|
411
|
+
this.closeRun = this.closeNow().finally(() => {
|
|
412
|
+
this.closeRun = null;
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return this.closeRun;
|
|
416
|
+
}
|
|
417
|
+
async closeNow() {
|
|
315
418
|
if (this.saveTimer) {
|
|
316
419
|
clearInterval(this.saveTimer);
|
|
317
420
|
this.saveTimer = null;
|
|
318
421
|
}
|
|
319
|
-
if (this.dirty) {
|
|
320
|
-
await this.saveToIndexedDb();
|
|
321
|
-
}
|
|
322
422
|
if (this.db) {
|
|
323
|
-
|
|
324
|
-
|
|
423
|
+
// From here no statement or transaction starts: a transaction() still
|
|
424
|
+
// running fails at its next statement, and one queued behind it fails
|
|
425
|
+
// when it starts.
|
|
426
|
+
this.closing = true;
|
|
427
|
+
try {
|
|
428
|
+
if (this.inTransaction()) {
|
|
429
|
+
this.rollbackQuietly();
|
|
430
|
+
}
|
|
431
|
+
await this.settleSave();
|
|
432
|
+
if (this.hasUnsavedWrites()) {
|
|
433
|
+
await this.startSave();
|
|
434
|
+
}
|
|
435
|
+
this.db?.close();
|
|
436
|
+
this.db = null;
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
this.closing = false;
|
|
440
|
+
}
|
|
325
441
|
}
|
|
326
442
|
if (this.idb) {
|
|
327
443
|
this.idb.close();
|
|
@@ -331,23 +447,38 @@ export class IndexedDbAdapter {
|
|
|
331
447
|
/**
|
|
332
448
|
* Exports the database as a Uint8Array (SQLite file format).
|
|
333
449
|
* Can be downloaded or stored externally.
|
|
450
|
+
*
|
|
451
|
+
* Throws while a transaction is open: sql.js's export closes and reopens the
|
|
452
|
+
* connection, which would end the transaction.
|
|
334
453
|
* @returns SQLite database file as binary
|
|
335
454
|
*/
|
|
336
455
|
exportDatabase() {
|
|
337
456
|
this.ensureOpen();
|
|
457
|
+
if (this.inTransaction()) {
|
|
458
|
+
throw new Error("IndexedDbAdapter: cannot export the database while a transaction is open; sql.js's export would end it.");
|
|
459
|
+
}
|
|
338
460
|
return this.db.export();
|
|
339
461
|
}
|
|
340
462
|
/**
|
|
341
463
|
* Imports a database from a Uint8Array (SQLite file).
|
|
342
|
-
* Replaces the current database
|
|
464
|
+
* Replaces the current database once any open transaction() has ended, and
|
|
465
|
+
* saves it to IndexedDB before it resolves.
|
|
343
466
|
* @param data SQLite database file
|
|
344
467
|
*/
|
|
345
468
|
async importDatabase(data) {
|
|
346
469
|
this.ensureOpen();
|
|
347
|
-
this.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
470
|
+
await whenAdmitted(this.gate, () => {
|
|
471
|
+
this.ensureOpen();
|
|
472
|
+
// Opened before the current database closes: an image sql.js refuses
|
|
473
|
+
// leaves the adapter on the database it had.
|
|
474
|
+
const imported = new this.SQL.Database(data);
|
|
475
|
+
this.db.close();
|
|
476
|
+
this.db = imported;
|
|
477
|
+
this.writeGeneration += 1;
|
|
478
|
+
// The save exports in this turn, so no transaction can open between
|
|
479
|
+
// the swap and the export and hold the save back.
|
|
480
|
+
return this.startSave();
|
|
481
|
+
}, undefined, this.longWait);
|
|
351
482
|
}
|
|
352
483
|
/**
|
|
353
484
|
* Opens or creates the IndexedDB database.
|
|
@@ -419,24 +550,98 @@ export class IndexedDbAdapter {
|
|
|
419
550
|
});
|
|
420
551
|
}
|
|
421
552
|
/**
|
|
422
|
-
*
|
|
553
|
+
* Writes the current image to IndexedDB. Never called while a transaction is
|
|
554
|
+
* open (see saveIfIdle): sql.js's export() closes and reopens the connection,
|
|
555
|
+
* which ends an open transaction.
|
|
556
|
+
*
|
|
557
|
+
* Settles when the IndexedDB transaction completes, not when its put request
|
|
558
|
+
* succeeds: the transaction can still abort after that (a quota error at
|
|
559
|
+
* commit), and the image is then not stored. On abort the writes stay
|
|
560
|
+
* unsaved, so the next save writes them. A write made after the export stays
|
|
561
|
+
* unsaved too.
|
|
423
562
|
*/
|
|
424
563
|
async saveToIndexedDb() {
|
|
425
564
|
if (!this.idb || !this.db)
|
|
426
565
|
return;
|
|
566
|
+
const generation = this.writeGeneration;
|
|
427
567
|
const data = this.db.export();
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
568
|
+
await this.withTransactionRetry(() => new Promise((resolve, reject) => {
|
|
569
|
+
const tx = this.idb.transaction(this.storeName, 'readwrite');
|
|
570
|
+
tx.objectStore(this.storeName).put(data, 'db');
|
|
571
|
+
tx.oncomplete = () => resolve();
|
|
572
|
+
// A failed request aborts the transaction, so abort covers both.
|
|
573
|
+
tx.onabort = () => reject(tx.error ?? new Error(`IndexedDbAdapter: saving "${this.dbName}" to IndexedDB was aborted.`));
|
|
574
|
+
}));
|
|
575
|
+
this.savedGeneration = Math.max(this.savedGeneration, generation);
|
|
576
|
+
}
|
|
577
|
+
/** Whether writes were made since the image the last completed save exported. */
|
|
578
|
+
hasUnsavedWrites() {
|
|
579
|
+
return this.writeGeneration !== this.savedGeneration;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Whether a transaction is open on the connection: one transaction() is
|
|
583
|
+
* running, or one the caller began with BEGIN or SAVEPOINT. sql.js does not
|
|
584
|
+
* export sqlite3_get_autocommit, so the adapter asks SQLite: BEGIN fails
|
|
585
|
+
* inside a transaction, and outside one it is rolled back at once. A probe
|
|
586
|
+
* that fails for any reason counts as open, so the adapter does not export.
|
|
587
|
+
*/
|
|
588
|
+
inTransaction() {
|
|
589
|
+
if (this.gate.openToken() !== null)
|
|
590
|
+
return true;
|
|
591
|
+
if (!this.db)
|
|
592
|
+
return false;
|
|
593
|
+
try {
|
|
594
|
+
this.db.run('BEGIN');
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
this.db.run('ROLLBACK');
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// The probe's own transaction did not roll back; it is open.
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Starts a save of the unsaved writes, unless a save is running or a
|
|
610
|
+
* transaction is open; the auto-save tick tries again later.
|
|
611
|
+
*/
|
|
612
|
+
saveIfIdle() {
|
|
613
|
+
if (this.saving)
|
|
614
|
+
return this.saving;
|
|
615
|
+
if (!this.db || !this.idb || !this.hasUnsavedWrites() || this.inTransaction()) {
|
|
616
|
+
return Promise.resolve();
|
|
617
|
+
}
|
|
618
|
+
return this.startSave();
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Starts a save at once and keeps it as the save in progress. The caller has
|
|
622
|
+
* made sure no transaction is open: the export would end it.
|
|
623
|
+
*/
|
|
624
|
+
startSave() {
|
|
625
|
+
const saving = this.saveToIndexedDb().finally(() => {
|
|
626
|
+
if (this.saving === saving)
|
|
627
|
+
this.saving = null;
|
|
439
628
|
});
|
|
629
|
+
this.saving = saving;
|
|
630
|
+
return saving;
|
|
631
|
+
}
|
|
632
|
+
/** Waits for a save in progress to settle, whether it succeeded or not. */
|
|
633
|
+
async settleSave() {
|
|
634
|
+
if (this.saving) {
|
|
635
|
+
await this.saving.catch(() => undefined);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
reportLongWait() {
|
|
639
|
+
if (this.longWaitReported)
|
|
640
|
+
return;
|
|
641
|
+
this.longWaitReported = true;
|
|
642
|
+
console.warn(`[IndexedDbAdapter] A statement on "${this.dbName}" has waited ${LONG_WAIT_MS / 1000} s for an open transaction to end. ` +
|
|
643
|
+
"Inside a transaction() callback, run statements through the callback's handle: one run through the adapter " +
|
|
644
|
+
'itself waits for the transaction, and the transaction waits for it.');
|
|
440
645
|
}
|
|
441
646
|
/**
|
|
442
647
|
* Wraps a transaction operation with retry logic.
|
|
@@ -472,30 +677,32 @@ export class IndexedDbAdapter {
|
|
|
472
677
|
}
|
|
473
678
|
throw lastError;
|
|
474
679
|
}
|
|
475
|
-
/**
|
|
476
|
-
* Persists to IndexedDB if auto-save is enabled.
|
|
477
|
-
*/
|
|
478
|
-
async persistIfNeeded() {
|
|
479
|
-
if (this.autoSave && !this.saveTimer) {
|
|
480
|
-
// Immediate save on first write, then batched
|
|
481
|
-
await this.saveToIndexedDb();
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
680
|
/**
|
|
485
681
|
* Starts auto-save interval.
|
|
486
682
|
*/
|
|
487
683
|
startAutoSave() {
|
|
488
|
-
this.saveTimer = setInterval(
|
|
489
|
-
|
|
490
|
-
await this.saveToIndexedDb();
|
|
491
|
-
}
|
|
684
|
+
this.saveTimer = setInterval(() => {
|
|
685
|
+
void this.autoSaveTick();
|
|
492
686
|
}, this.saveIntervalMs);
|
|
493
687
|
}
|
|
688
|
+
/** One auto-save tick: a failed save is reported once and tried again on the next tick. */
|
|
689
|
+
async autoSaveTick() {
|
|
690
|
+
try {
|
|
691
|
+
await this.saveIfIdle();
|
|
692
|
+
this.saveFailureReported = false;
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
if (!this.saveFailureReported) {
|
|
696
|
+
this.saveFailureReported = true;
|
|
697
|
+
console.warn(`[IndexedDbAdapter] Saving "${this.dbName}" to IndexedDB failed; the next tick tries again.`, error);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
494
701
|
/**
|
|
495
702
|
* Ensures the database is open.
|
|
496
703
|
*/
|
|
497
704
|
ensureOpen() {
|
|
498
|
-
if (!this.db) {
|
|
705
|
+
if (!this.db || this.closing) {
|
|
499
706
|
throw new Error('IndexedDbAdapter: Database not open. Call open() first.');
|
|
500
707
|
}
|
|
501
708
|
}
|