indexeddbshim 17.2.1 → 17.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +8 -5
  2. package/dist/CFG.d.ts +1 -1
  3. package/dist/CFG.d.ts.map +1 -1
  4. package/dist/DOMException.d.ts +21 -7
  5. package/dist/DOMException.d.ts.map +1 -1
  6. package/dist/DOMStringList.d.ts.map +1 -1
  7. package/dist/Event.d.ts +12 -6
  8. package/dist/Event.d.ts.map +1 -1
  9. package/dist/IDBCursor.d.ts +125 -7
  10. package/dist/IDBCursor.d.ts.map +1 -1
  11. package/dist/IDBDatabase.d.ts +17 -10
  12. package/dist/IDBDatabase.d.ts.map +1 -1
  13. package/dist/IDBFactory.d.ts +5 -0
  14. package/dist/IDBFactory.d.ts.map +1 -1
  15. package/dist/IDBIndex.d.ts +43 -13
  16. package/dist/IDBIndex.d.ts.map +1 -1
  17. package/dist/IDBKeyRange.d.ts +6 -2
  18. package/dist/IDBKeyRange.d.ts.map +1 -1
  19. package/dist/IDBObjectStore.d.ts +66 -17
  20. package/dist/IDBObjectStore.d.ts.map +1 -1
  21. package/dist/IDBRecord.d.ts +44 -0
  22. package/dist/IDBRecord.d.ts.map +1 -0
  23. package/dist/IDBRequest.d.ts +2 -2
  24. package/dist/IDBRequest.d.ts.map +1 -1
  25. package/dist/IDBTransaction.d.ts +43 -15
  26. package/dist/IDBTransaction.d.ts.map +1 -1
  27. package/dist/IDBVersionChangeEvent.d.ts +23 -12
  28. package/dist/IDBVersionChangeEvent.d.ts.map +1 -1
  29. package/dist/Key.d.ts +7 -6
  30. package/dist/Key.d.ts.map +1 -1
  31. package/dist/Sca.d.ts +15 -0
  32. package/dist/Sca.d.ts.map +1 -1
  33. package/dist/indexeddbshim-Key.js +59 -23
  34. package/dist/indexeddbshim-Key.js.map +1 -1
  35. package/dist/indexeddbshim-Key.min.js +2 -2
  36. package/dist/indexeddbshim-Key.min.js.map +1 -1
  37. package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs +3123 -1169
  38. package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs.map +1 -1
  39. package/dist/indexeddbshim-UnicodeIdentifiers.js +2683 -979
  40. package/dist/indexeddbshim-UnicodeIdentifiers.js.map +1 -1
  41. package/dist/indexeddbshim-UnicodeIdentifiers.min.js +4 -4
  42. package/dist/indexeddbshim-UnicodeIdentifiers.min.js.map +1 -1
  43. package/dist/indexeddbshim-node.cjs +3123 -1169
  44. package/dist/indexeddbshim-node.cjs.map +1 -1
  45. package/dist/indexeddbshim-noninvasive.js +2683 -979
  46. package/dist/indexeddbshim-noninvasive.js.map +1 -1
  47. package/dist/indexeddbshim-noninvasive.min.js +4 -4
  48. package/dist/indexeddbshim-noninvasive.min.js.map +1 -1
  49. package/dist/indexeddbshim.js +2683 -979
  50. package/dist/indexeddbshim.js.map +1 -1
  51. package/dist/indexeddbshim.min.js +4 -4
  52. package/dist/indexeddbshim.min.js.map +1 -1
  53. package/dist/node-UnicodeIdentifiers.d.ts +2 -2
  54. package/dist/node-UnicodeIdentifiers.d.ts.map +1 -1
  55. package/dist/node.d.ts +2 -2
  56. package/dist/node.d.ts.map +1 -1
  57. package/dist/nodeSQLiteDatabase.d.ts +13 -14
  58. package/dist/nodeSQLiteDatabase.d.ts.map +1 -1
  59. package/dist/nodeWebSQL.d.ts +17 -1
  60. package/dist/nodeWebSQL.d.ts.map +1 -1
  61. package/dist/setGlobalVars.d.ts +2 -2
  62. package/dist/setGlobalVars.d.ts.map +1 -1
  63. package/dist/util.d.ts +15 -0
  64. package/dist/util.d.ts.map +1 -1
  65. package/examples/IDBKeyUtils.html +0 -1
  66. package/package.json +28 -21
  67. package/src/CFG.js +2 -1
  68. package/src/DOMException.js +38 -12
  69. package/src/DOMStringList.js +21 -2
  70. package/src/Event.js +9 -4
  71. package/src/IDBCursor.js +346 -18
  72. package/src/IDBDatabase.js +33 -16
  73. package/src/IDBFactory.js +162 -62
  74. package/src/IDBIndex.js +78 -45
  75. package/src/IDBKeyRange.js +8 -2
  76. package/src/IDBObjectStore.js +89 -65
  77. package/src/IDBRecord.js +96 -0
  78. package/src/IDBRequest.js +7 -7
  79. package/src/IDBTransaction.js +342 -61
  80. package/src/IDBVersionChangeEvent.js +37 -30
  81. package/src/Key.js +36 -18
  82. package/src/Sca.js +22 -1
  83. package/src/browser-UnicodeIdentifiers.js +3 -1
  84. package/src/browser-noninvasive.js +3 -1
  85. package/src/browser.js +3 -1
  86. package/src/node-UnicodeIdentifiers.js +1 -1
  87. package/src/node.js +1 -1
  88. package/src/nodeSQLiteDatabase.js +67 -2
  89. package/src/nodeWebSQL.js +22 -2
  90. package/src/setGlobalVars.js +5 -5
  91. package/src/util.js +29 -1
  92. package/typings/shimIndexedDB.d.ts +1 -1
@@ -9,7 +9,7 @@ import CFG from './CFG.js';
9
9
 
10
10
  let uniqueID = 0;
11
11
  const listeners = ['onabort', 'oncomplete', 'onerror'];
12
- const readonlyProperties = ['objectStoreNames', 'mode', 'db', 'error'];
12
+ const readonlyProperties = ['objectStoreNames', 'mode', 'durability', 'db', 'error'];
13
13
 
14
14
  /**
15
15
  * @typedef {number} Integer
@@ -26,6 +26,7 @@ const readonlyProperties = ['objectStoreNames', 'mode', 'db', 'error'];
26
26
  /**
27
27
  * @typedef {EventTarget & {
28
28
  * mode: "readonly"|"readwrite"|"versionchange",
29
+ * durability: "default"|"strict"|"relaxed",
29
30
  * db: import('./IDBDatabase.js').IDBDatabaseFull,
30
31
  * on__abort: () => void,
31
32
  * on__complete: () => void,
@@ -35,14 +36,17 @@ const readonlyProperties = ['objectStoreNames', 'mode', 'db', 'error'];
35
36
  * on__preabort: () => void,
36
37
  * __abortTransaction: (err: Error|DOMException|null) => void,
37
38
  * __executeRequests: () => void,
38
- * __tx: SQLTransaction,
39
+ * __tx: import('websql-configurable/lib/websql/WebSQLTransaction.js').default,
39
40
  * __id: Integer,
40
41
  * __active: boolean,
42
+ * __handlerActive: boolean,
41
43
  * __running: boolean,
42
44
  * __errored: boolean,
45
+ * __committed: boolean,
43
46
  * __requests: RequestInfo[],
44
47
  * __db: import('./IDBDatabase.js').IDBDatabaseFull,
45
48
  * __mode: string,
49
+ * __durability: string,
46
50
  * __error: null|DOMException|Error,
47
51
  * __objectStoreNames: import('./DOMStringList.js').DOMStringListFull,
48
52
  * __storeHandles: {
@@ -54,6 +58,7 @@ const readonlyProperties = ['objectStoreNames', 'mode', 'db', 'error'];
54
58
  * __transactionEndCallback: () => void,
55
59
  * __transactionFinished: boolean,
56
60
  * __completed: boolean,
61
+ * __transFinishedCbFired: boolean,
57
62
  * __internal: boolean,
58
63
  * __abortFinished: boolean,
59
64
  * __createRequest: (
@@ -68,6 +73,7 @@ const readonlyProperties = ['objectStoreNames', 'mode', 'db', 'error'];
68
73
  * args?: ObjectArray
69
74
  * ) => void,
70
75
  * __assertActive: () => void,
76
+ * commit: () => void,
71
77
  * __addNonRequestToTransactionQueue: (
72
78
  * callback: SQLCallback,
73
79
  * args?: ObjectArray
@@ -98,9 +104,10 @@ const IDBTransactionAlias = IDBTransaction;
98
104
  * @param {import('./IDBDatabase.js').IDBDatabaseFull} db
99
105
  * @param {import('./DOMStringList.js').DOMStringListFull} storeNames
100
106
  * @param {string} mode
107
+ * @param {string} [durability]
101
108
  * @returns {IDBTransactionFull}
102
109
  */
103
- IDBTransaction.__createInstance = function (db, storeNames, mode) {
110
+ IDBTransaction.__createInstance = function (db, storeNames, mode, durability = 'default') {
104
111
  /**
105
112
  * @class
106
113
  * @this {IDBTransactionFull}
@@ -113,11 +120,22 @@ IDBTransaction.__createInstance = function (db, storeNames, mode) {
113
120
  // eslint-disable-next-line unicorn/no-top-level-assignment-in-function -- Debugging only
114
121
  me.__id = ++uniqueID; // for debugging simultaneous transactions
115
122
  me.__active = true;
123
+ // Tracks the spec's "active" flag for the purpose of validating new
124
+ // requests/`commit()`: true only during the initial synchronous
125
+ // script that created the transaction and during each dispatched
126
+ // request's synchronous `success`/`error` handler. Deliberately
127
+ // kept separate from `__active` above, which additionally (and
128
+ // permanently, once false) signals that request-queue processing
129
+ // has stopped -- `executeNextRequest` relies on that to know
130
+ // whether it's still safe to finish the transaction normally.
131
+ me.__handlerActive = true;
116
132
  me.__running = false;
117
133
  me.__errored = false;
134
+ me.__committed = false;
118
135
  me.__requests = [];
119
136
  me.__objectStoreNames = storeNames;
120
137
  me.__mode = mode;
138
+ me.__durability = durability;
121
139
  me.__db = db;
122
140
  me.__error = null;
123
141
  // @ts-expect-error Part of `ShimEventTarget`
@@ -179,12 +197,43 @@ IDBTransaction.prototype.__transFinishedCb = function (err, cb) {
179
197
  */
180
198
  IDBTransaction.prototype.__callTransFinishedCb = function (err, cb) {
181
199
  const me = this;
200
+ if (me.__completed || me.__transFinishedCbFired) {
201
+ // Either this transaction's `nonstandardTransCb` installation (see
202
+ // `__executeRequests`) already found `__transactionEndCallback` set
203
+ // and fired `__transFinishedCb` itself in the meantime -- a race with
204
+ // this call's own (possibly `setTimeout`-deferred) retry below -- or
205
+ // this same call already fired it on an earlier retry. Either way,
206
+ // don't fire a second commit/rollback for the same transaction.
207
+ // `__transFinishedCbFired` is checked in addition to `__completed`
208
+ // because `__completed` isn't set until the resulting SQL commit/
209
+ // rollback round trip actually finishes, which is too late to stop
210
+ // the other side from *also* firing while that's still in flight.
211
+ return;
212
+ }
182
213
  if (me.__transFinishedCb === IDBTransaction.prototype.__transFinishedCb) {
214
+ // Standard (3-argument) `transaction()`/`readTransaction()` implementations
215
+ // (browser WebSQL, `cordova-plugin-sqlite-2`, etc.) never invoke the
216
+ // non-standard 4th callback that installs the real `__transFinishedCb`,
217
+ // so waiting for it here would defer forever. Detect that via arity
218
+ // (checking whichever of the two methods this transaction's own mode
219
+ // actually uses -- see `__executeRequests`) and, if it's not supported,
220
+ // just call the default (the driver auto-commits on its own).
221
+ const dbConn = me.db && me.db.__db;
222
+ const dbMethodName = me.mode === 'readonly' ? 'readTransaction' : 'transaction';
223
+ const supportsNonstandardTransCb = Boolean(
224
+ dbConn && typeof dbConn[dbMethodName] === 'function' && dbConn[dbMethodName].length >= 4
225
+ );
226
+ if (!supportsNonstandardTransCb) {
227
+ me.__transFinishedCbFired = true;
228
+ me.__transFinishedCb(err, cb);
229
+ return;
230
+ }
183
231
  setTimeout(() => {
184
232
  me.__callTransFinishedCb(err, cb);
185
233
  }, 0);
186
234
  return;
187
235
  }
236
+ me.__transFinishedCbFired = true;
188
237
  me.__transFinishedCb(err, cb);
189
238
  };
190
239
  /**
@@ -198,6 +247,15 @@ IDBTransaction.prototype.__executeRequests = function () {
198
247
  return;
199
248
  }
200
249
 
250
+ // The synchronous script that created this transaction (and
251
+ // synchronously queued whatever requests it wanted to) has now
252
+ // definitely returned control to the event loop -- this callback was
253
+ // itself deferred via `setTimeout(..., 0)` for exactly that reason.
254
+ // So the transaction's initial "active" window closes here, until the
255
+ // first dispatched request's handler (see `success`/`error` below)
256
+ // reopens it.
257
+ me.__handlerActive = false;
258
+
201
259
  me.__running = true;
202
260
 
203
261
  me.db.__db[me.mode === 'readonly' ? 'readTransaction' : 'transaction']( // `readTransaction` is optimized, at least in `node-websql`
@@ -235,20 +293,34 @@ IDBTransaction.prototype.__executeRequests = function () {
235
293
  q.req.__error = null;
236
294
 
237
295
  me.__active = true;
296
+ me.__handlerActive = true;
238
297
  const e = createEvent('success');
239
298
  q.req.dispatchEvent(e);
240
- // Do not set __active flag to false yet: https://github.com/w3c/IndexedDB/issues/87
299
+ // Do not set __active or __handlerActive flags to false yet:
300
+ // https://github.com/w3c/IndexedDB/issues/87 -- a follow-up
301
+ // request queued from an `await`-based continuation of this
302
+ // one (a microtask, not a further synchronous call within
303
+ // this handler) must still pass `__assertActive`'s check of
304
+ // both flags. `checkQueueEntry` (see `executeNextRequest`)
305
+ // is what actually resets `__handlerActive`, once it's
306
+ // confirmed (across its own bounded microtask wait) that no
307
+ // such continuation queued anything.
241
308
  if (e.__legacyOutputDidListenersThrowError) {
242
309
  logError('Error', 'An error occurred in a success handler attached to request chain', e.__legacyOutputDidListenersThrowError); // We do nothing else with this error as per spec
243
- // me.__active = false;
244
- me.__abortTransaction(createDOMException('AbortError', 'A request was aborted (in user handler after success).'));
245
- return;
310
+ if (!me.__committed) { // An explicit `commit()` locks in the commit, so errors thrown afterward must not abort it
311
+ // me.__active = false;
312
+ me.__abortTransaction(createDOMException('AbortError', 'A request was aborted (in user handler after success).'));
313
+ return;
314
+ }
246
315
  }
247
- util.runContinuationSafely(executeNextRequest);
316
+ util.runContinuationSafely(advanceAfterDispatch);
248
317
  }
249
318
 
250
319
  /**
251
- * @param {[tx: SQLTransaction|DOMException|Error|SQLError, err?: SQLError]} args
320
+ * @param {[
321
+ * tx: import('websql-configurable/lib/websql/WebSQLTransaction.js').default|DOMException|Error,
322
+ * err?: Error & {code?: number}
323
+ * ]} args
252
324
  * @returns {void}
253
325
  */
254
326
  function error (...args /* tx, err */) {
@@ -270,11 +342,18 @@ IDBTransaction.prototype.__executeRequests = function () {
270
342
  q.req.__done = true;
271
343
  q.req.__error = err;
272
344
  q.req.__result = undefined; // Must be undefined if an error per `result` getter
273
- q.req.addLateEventListener('error', function (e) {
274
- if (e.cancelable && e.defaultPrevented && !e.__legacyOutputDidListenersThrowError) {
275
- executeNextRequest();
345
+ q.req.addLateEventListener(
346
+ 'error',
347
+ /**
348
+ * @param {Event & {__legacyOutputDidListenersThrowError: boolean}} e
349
+ * @returns {void}
350
+ */
351
+ function (e) {
352
+ if (e.cancelable && e.defaultPrevented && !e.__legacyOutputDidListenersThrowError) {
353
+ advanceAfterDispatch();
354
+ }
276
355
  }
277
- });
356
+ );
278
357
  q.req.addDefaultEventListener('error', function () {
279
358
  if (!q.req) { // TS guard
280
359
  return;
@@ -283,46 +362,187 @@ IDBTransaction.prototype.__executeRequests = function () {
283
362
  });
284
363
 
285
364
  me.__active = true;
365
+ me.__handlerActive = true;
286
366
  const e = createEvent('error', err, {bubbles: true, cancelable: true});
287
367
  q.req.dispatchEvent(e);
288
- // Do not set __active flag to false yet: https://github.com/w3c/IndexedDB/issues/87
368
+ // Do not set __active or __handlerActive flags to false yet --
369
+ // see the matching comment in `success`, above.
289
370
  if (e.__legacyOutputDidListenersThrowError) {
290
371
  logError('Error', 'An error occurred in an error handler attached to request chain', e.__legacyOutputDidListenersThrowError); // We do nothing else with this error as per spec
291
372
  e.preventDefault(); // Prevent 'error' default as steps indicate we should abort with `AbortError` even without cancellation
373
+ if (me.__committed) { // An explicit `commit()` locks in the commit, so errors thrown afterward must not abort it
374
+ util.runContinuationSafely(advanceAfterDispatch);
375
+ return;
376
+ }
292
377
  me.__abortTransaction(createDOMException('AbortError', 'A request was aborted (in user handler after error).'));
293
378
  }
294
379
  }
295
380
 
381
+ /**
382
+ * Sets up `q` for the current queue index and, for a genuine
383
+ * request (not one of our internal non-request queue entries),
384
+ * deactivates the transaction for the duration of its
385
+ * (possibly async) work. Returns `false` if there's nothing to
386
+ * do (an already-aborted request).
387
+ * @returns {boolean}
388
+ */
389
+ function prepareNextRequest () {
390
+ q = me.__requests[i];
391
+ if (!q.req) {
392
+ // Non-standard, non-`IDBRequest` queue entries (e.g.
393
+ // the internal `onupgradeneeded` dispatch op in
394
+ // `IDBFactory.js`) dispatch straight to user code
395
+ // without going through `success`/`error` below --
396
+ // unlike those, they never restore `__active`/
397
+ // `__handlerActive` to `true` before doing so, so
398
+ // they rely on the flags being left as they are
399
+ // (not reset here).
400
+ return true;
401
+ }
402
+ if (q.req.__done) { // Avoid continuing with aborted requests
403
+ return false;
404
+ }
405
+ // We're now handing off to (possibly async) work for
406
+ // this request, so the transaction is no longer active
407
+ // until its own `success`/`error` dispatch (below)
408
+ // sets these flags again -- a check that happens to
409
+ // run during this gap (e.g. `commit()` called from an
410
+ // unrelated `setTimeout`) must see the transaction as
411
+ // inactive, per spec.
412
+ me.__active = false;
413
+ me.__handlerActive = false;
414
+ return true;
415
+ }
416
+
296
417
  /**
297
418
  * @returns {void}
298
419
  */
299
- function executeNextRequest () {
420
+ function launchQueuedOp () {
421
+ try {
422
+ if (!q.req) {
423
+ q.op(tx, q.args, () => util.runContinuationSafely(executeNextRequest), error);
424
+ return;
425
+ }
426
+ q.op(tx, q.args, success, error, executeNextRequest);
427
+ } catch (e) {
428
+ error(/** @type {Error} */ (e));
429
+ }
430
+ }
431
+
432
+ /**
433
+ * @returns {void}
434
+ */
435
+ function runQueuedRequest () {
436
+ if (!prepareNextRequest()) {
437
+ return;
438
+ }
439
+ launchQueuedOp();
440
+ }
441
+
442
+ /**
443
+ * A request's `success`/`error` event fires (and any `await`-based
444
+ * continuation watching it resolves) before this check runs, since
445
+ * that dispatch happens synchronously above, one call frame up.
446
+ * If such a continuation queues a follow-up request, it does so
447
+ * from a microtask -- so if the JS-level queue is merely found
448
+ * empty here, that doesn't yet mean no more work is coming, only
449
+ * that none has been queued *yet*. Re-check across a small, bounded
450
+ * number of further microtask turns (letting a typical `await`
451
+ * chain like `await store.put(...); await store.get(...)` catch
452
+ * up) before finally concluding the transaction is genuinely done.
453
+ * Applies to `readonly` transactions too, not just `readwrite`/
454
+ * `versionchange`: a `readonly` transaction's `complete` event
455
+ * can otherwise fire synchronously, immediately after its last
456
+ * request's `success` handler returns -- before an `await`-based
457
+ * consumer of that same handler (e.g. one that resolves a
458
+ * promise from within `onsuccess` and only attaches `oncomplete`
459
+ * afterward) ever gets a turn to run, so it can miss `complete`
460
+ * entirely. `readonly` requests don't hold a real SQL
461
+ * transaction open, though, so there's no file-lock/connection
462
+ * collision risk in waiting the same bounded amount here.
463
+ * @param {number} attemptsLeft
464
+ * @returns {void}
465
+ */
466
+ function checkQueueEntry (attemptsLeft) {
300
467
  if (me.__errored || me.__requestsFinished) {
301
- // We've already called "onerror", "onabort", or thrown within the transaction, so don't do it again.
302
468
  return;
303
469
  }
304
- i++;
305
- if (i >= me.__requests.length) {
470
+ if (i < me.__requests.length) {
471
+ runQueuedRequest();
472
+ return;
473
+ }
474
+ if (attemptsLeft <= 0) {
306
475
  // All requests in the transaction are done
307
476
  me.__requests = [];
308
477
  if (me.__active) {
309
478
  requestsFinished();
310
479
  }
311
- } else {
312
- try {
313
- q = me.__requests[i];
314
- if (!q.req) {
315
- q.op(tx, q.args, () => util.runContinuationSafely(executeNextRequest), error);
316
- return;
317
- }
318
- if (q.req.__done) { // Avoid continuing with aborted requests
319
- return;
320
- }
321
- q.op(tx, q.args, success, error, executeNextRequest);
322
- } catch (e) {
323
- error(/** @type {Error} */ (e));
324
- }
480
+ return;
325
481
  }
482
+ queueMicrotask(() => {
483
+ checkQueueEntry(attemptsLeft - 1);
484
+ });
485
+ }
486
+
487
+ /**
488
+ * @returns {void}
489
+ */
490
+ function executeNextRequest () {
491
+ if (me.__errored || me.__requestsFinished) {
492
+ // We've already called "onerror", "onabort", or thrown within the transaction, so don't do it again.
493
+ return;
494
+ }
495
+ i++;
496
+ if (i >= me.__requests.length) {
497
+ checkQueueEntry(10);
498
+ return;
499
+ }
500
+ runQueuedRequest();
501
+ }
502
+
503
+ /**
504
+ * Same as `executeNextRequest`, but used specifically as the
505
+ * continuation from a request's own `success`/`error` dispatch
506
+ * (see call sites above): gives same-tick microtasks scheduled
507
+ * from within that handler (e.g. a plain
508
+ * `Promise.resolve().then(...)`) a chance to run -- and still
509
+ * observe the transaction as active -- before we deactivate it
510
+ * again for the next queued request, per
511
+ * https://github.com/w3c/IndexedDB/issues/87.
512
+ *
513
+ * This is safe for `readonly` transactions too only because
514
+ * `__executeRequests` now passes a `nonstandardTransCb` to
515
+ * `readTransaction()` (matching `.transaction()`) and
516
+ * `requestsFinished` defers `readonly` completion through
517
+ * `__transactionEndCallback`/`__callTransFinishedCb` the same
518
+ * way `readwrite` already did: the WebSQL driver's
519
+ * "optimized" `readTransaction` path used to finalize the
520
+ * underlying transaction as soon as no further `executeSql`
521
+ * call was already in flight or queued, which -- before that
522
+ * fix -- meant even a microtask-long gap here would let the
523
+ * driver consider the transaction done, silently dropping
524
+ * every subsequent queued request (never firing
525
+ * `success`/`error` for it at all).
526
+ * @returns {void}
527
+ */
528
+ function advanceAfterDispatch () {
529
+ if (me.__errored || me.__requestsFinished) {
530
+ return;
531
+ }
532
+ i++;
533
+ if (i >= me.__requests.length) {
534
+ checkQueueEntry(10);
535
+ return;
536
+ }
537
+ queueMicrotask(() => {
538
+ if (me.__errored || me.__requestsFinished) {
539
+ return;
540
+ }
541
+ if (!prepareNextRequest()) {
542
+ return;
543
+ }
544
+ launchQueuedOp();
545
+ });
326
546
  }
327
547
 
328
548
  executeNextRequest();
@@ -332,7 +552,7 @@ IDBTransaction.prototype.__executeRequests = function () {
332
552
  if (webSQLErr === true) { // Not a genuine SQL error
333
553
  return;
334
554
  }
335
- const err = webSQLErrback(/** @type {SQLError} */ (webSQLErr));
555
+ const err = webSQLErrback(/** @type {Error & {code?: number}} */ (webSQLErr));
336
556
  me.__abortTransaction(err);
337
557
  },
338
558
  function () {
@@ -346,22 +566,46 @@ IDBTransaction.prototype.__executeRequests = function () {
346
566
  me.__transactionFinished = true;
347
567
  return;
348
568
  }
349
- if (me.__transactionEndCallback && !me.__completed) {
569
+ if (me.__transactionEndCallback && !me.__completed && !me.__transFinishedCbFired) {
570
+ me.__transFinishedCbFired = true;
350
571
  me.__transFinishedCb(me.__errored, me.__transactionEndCallback);
351
572
  }
352
573
  },
353
574
  function (currentTask, err, done, rollback, commit) {
354
- if (currentTask.readOnly || err) {
575
+ if (err) {
355
576
  return true;
356
577
  }
357
- me.__transFinishedCb = function (err, cb) {
358
- if (err) {
359
- rollback(err, cb);
360
- } else {
361
- commit(cb);
578
+ // `readonly` transactions never hold a real SQL transaction open
579
+ // (see `WebSQLTransaction`'s constructor skipping `BEGIN;` for
580
+ // them), so there's no commit/rollback round trip to defer --
581
+ // `done` itself is the "genuinely finished" signal for them,
582
+ // called synchronously once `requestsFinished` (via
583
+ // `__callTransFinishedCb`) confirms no further request is
584
+ // coming.
585
+ me.__transFinishedCb = currentTask.readOnly
586
+ ? function (err, cb) {
587
+ done(err);
588
+ if (cb) {
589
+ cb();
590
+ }
362
591
  }
363
- };
364
- if (me.__transactionEndCallback && !me.__completed) {
592
+ : function (err, cb) {
593
+ if (err) {
594
+ rollback(err, cb);
595
+ } else {
596
+ commit(cb);
597
+ }
598
+ };
599
+ // Guarded by `__transFinishedCbFired`, not just `__completed`: this
600
+ // installation can race `IDBTransaction.prototype.__callTransFinishedCb`'s
601
+ // own `setTimeout` retry (see there) -- both watch for
602
+ // `__transFinishedCb` to become installed and `__transactionEndCallback`
603
+ // to become set, and either could observe both conditions first.
604
+ // `__completed` alone isn't set until the resulting SQL commit/rollback
605
+ // round trip actually finishes, which is too late to prevent the other
606
+ // side from *also* firing in the meantime.
607
+ if (me.__transactionEndCallback && !me.__completed && !me.__transFinishedCbFired) {
608
+ me.__transFinishedCbFired = true;
365
609
  me.__transFinishedCb(me.__errored, me.__transactionEndCallback);
366
610
  }
367
611
  return false;
@@ -373,6 +617,7 @@ IDBTransaction.prototype.__executeRequests = function () {
373
617
  */
374
618
  function requestsFinished () {
375
619
  me.__active = false;
620
+ me.__handlerActive = false;
376
621
  me.__requestsFinished = true;
377
622
 
378
623
  /**
@@ -399,16 +644,27 @@ IDBTransaction.prototype.__executeRequests = function () {
399
644
  me.__storeHandles = {};
400
645
  }
401
646
  }
402
- if (me.mode === 'readwrite') {
647
+ if (me.mode === 'readwrite' || me.mode === 'readonly') {
403
648
  if (me.__transactionFinished) {
404
649
  complete();
405
650
  return;
406
651
  }
407
652
  me.__transactionEndCallback = complete;
408
- return;
409
- }
410
- if (me.mode === 'readonly') {
411
- complete();
653
+ // The underlying SQL driver's own "queue empty" check
654
+ // (`nonstandardTransCb`, above) typically already ran and
655
+ // installed the real `__transFinishedCb` *before* this point --
656
+ // it fires synchronously once the last SQL batch's results are
657
+ // processed, whereas `requestsFinished` can now be reached only
658
+ // after `checkQueueEntry`'s bounded microtask wait, i.e. later.
659
+ // When that happens, its check for `me.__transactionEndCallback`
660
+ // (not yet set at that earlier time) finds nothing to do and
661
+ // just returns, so nothing else will ever re-trigger the actual
662
+ // commit unless this explicitly does so now. For `readonly`,
663
+ // `__transFinishedCb` (installed by the `nonstandardTransCb`
664
+ // passed to `readTransaction()`, above) just calls the WebSQL
665
+ // driver's own `done` -- there's no real commit/rollback SQL
666
+ // step for a `readonly` transaction.
667
+ me.__callTransFinishedCb(me.__errored, complete);
412
668
  return;
413
669
  }
414
670
  const ev = /** @type {Event & {complete: () => void}} */ (
@@ -436,10 +692,13 @@ IDBTransaction.prototype.__createRequest = function (source) {
436
692
 
437
693
  /**
438
694
  * @typedef {(
439
- * tx: SQLTransaction,
695
+ * tx: import('websql-configurable/lib/websql/WebSQLTransaction.js').default,
440
696
  * args: ObjectArray,
441
697
  * success: (result?: any, req?: import('./IDBRequest.js').IDBRequestFull) => void,
442
- * error: (tx: SQLTransaction|Error|DOMException|SQLError, err?: SQLError) => void,
698
+ * error: (
699
+ * tx: import('websql-configurable/lib/websql/WebSQLTransaction.js').default|Error|DOMException,
700
+ * err?: Error & {code?: number}
701
+ * ) => void,
443
702
  * executeNextRequest?: () => void
444
703
  * ) => void} SQLCallback
445
704
  */
@@ -491,10 +750,11 @@ IDBTransaction.prototype.__pushToQueue = function (request, callback, args) {
491
750
 
492
751
  /**
493
752
  * @throws {DOMException}
753
+ * @this {IDBTransactionFull}
494
754
  * @returns {void}
495
755
  */
496
756
  IDBTransaction.prototype.__assertActive = function () {
497
- if (!this.__active) {
757
+ if (!this.__active || !this.__handlerActive || this.__committed) {
498
758
  throw createDOMException('TransactionInactiveError', 'A request was placed against a transaction which is currently not active, or which is finished');
499
759
  }
500
760
  };
@@ -575,7 +835,7 @@ IDBTransaction.prototype.__abortTransaction = function (err) {
575
835
  ).forEach(function (store) {
576
836
  // Store was already created so we restore to name before the rename
577
837
  if ('__pendingName' in store &&
578
- me.db.__oldObjectStoreNames.indexOf(store.__pendingName) > -1 // eslint-disable-line unicorn/prefer-includes -- Not supported
838
+ me.db.__oldObjectStoreNames.indexOf(/** @type {string} */ (store.__pendingName)) > -1 // eslint-disable-line unicorn/prefer-includes -- Not supported
579
839
  ) {
580
840
  store.__name = store.__originalName;
581
841
  }
@@ -586,7 +846,7 @@ IDBTransaction.prototype.__abortTransaction = function (err) {
586
846
  ).forEach(function (index) {
587
847
  // Index was already created so we restore to name before the rename
588
848
  if ('__pendingName' in index &&
589
- store.__oldIndexNames.indexOf(index.__pendingName) > -1 // eslint-disable-line unicorn/prefer-includes -- Not supported
849
+ store.__oldIndexNames.indexOf(/** @type {string} */ (index.__pendingName)) > -1 // eslint-disable-line unicorn/prefer-includes -- Not supported
590
850
  ) {
591
851
  index.__name = index.__originalName;
592
852
  }
@@ -609,8 +869,8 @@ IDBTransaction.prototype.__abortTransaction = function (err) {
609
869
  }
610
870
 
611
871
  /**
612
- * @param {SQLTransaction|null} [tx]
613
- * @param {SQLResultSet|SQLError|{code: 0}} [errOrResult]
872
+ * @param {import('websql-configurable/lib/websql/WebSQLTransaction.js').default|null} [tx]
873
+ * @param {import('websql-configurable/lib/websql/WebSQLResultSet.js').default|(Error & {code?: number})|{code: 0}} [errOrResult]
614
874
  * @returns {void}
615
875
  */
616
876
  function abort (tx, errOrResult) {
@@ -639,7 +899,7 @@ IDBTransaction.prototype.__abortTransaction = function (err) {
639
899
  q.req.__error = createDOMException('AbortError', 'A request was aborted (an unfinished request).');
640
900
  const reqEvt = createEvent('error', q.req.__error, {bubbles: true, cancelable: true});
641
901
  return new SyncPromise(
642
- /** @type {() => void} */
902
+ /** @type {(resolve: (value?: any) => void) => void} */
643
903
  (resolve) => {
644
904
  setTimeout(() => {
645
905
  if (!q.req) { // TS guard
@@ -683,7 +943,7 @@ IDBTransaction.prototype.__abortTransaction = function (err) {
683
943
  'ROLLBACK',
684
944
  [],
685
945
  abort,
686
- /** @type {SQLStatementErrorCallback} */ (abort)
946
+ /** @type {import('websql-configurable/lib/websql/WebSQLTransaction.js').SqlErrorCallback} */ (abort)
687
947
  ); // Not working in some circumstances, even in Node
688
948
  } catch (err) {
689
949
  // Browser errs when transaction has ended and since it most likely already erred here,
@@ -707,14 +967,34 @@ IDBTransaction.prototype.abort = function () {
707
967
  }
708
968
  if (CFG.DEBUG) { console.log('The transaction was aborted', me); }
709
969
  IDBTransaction.__assertNotFinished(me);
970
+ if (me.__committed) {
971
+ throw createDOMException('InvalidStateError', 'The transaction has already been committed');
972
+ }
710
973
  me.__abortTransaction(null);
711
974
  };
712
975
 
976
+ /**
977
+ * @see https://www.w3.org/TR/IndexedDB/#dom-idbtransaction-commit
978
+ * @this {IDBTransactionFull}
979
+ * @returns {void}
980
+ */
981
+ IDBTransaction.prototype.commit = function () {
982
+ const me = this;
983
+ if (!(me instanceof IDBTransaction)) {
984
+ throw new TypeError('Illegal invocation');
985
+ }
986
+ if (!me.__active || !me.__handlerActive || me.__committed) {
987
+ throw createDOMException('InvalidStateError', 'Failed to execute \'commit\' on \'IDBTransaction\': The transaction is not active.');
988
+ }
989
+ if (CFG.DEBUG) { console.log('The transaction was explicitly committed', me); }
990
+ me.__committed = true;
991
+ };
992
+
713
993
  IDBTransaction.prototype[Symbol.toStringTag] = 'IDBTransactionPrototype';
714
994
 
715
995
  /**
716
996
  *
717
- * @param {IDBTransactionFull|undefined} tx
997
+ * @param {IDBTransactionFull|null|undefined} tx
718
998
  * @returns {void}
719
999
  */
720
1000
  IDBTransaction.__assertVersionChange = function (tx) {
@@ -724,7 +1004,7 @@ IDBTransaction.__assertVersionChange = function (tx) {
724
1004
  };
725
1005
  /**
726
1006
  *
727
- * @param {IDBTransactionFull} tx
1007
+ * @param {IDBTransactionFull|null} tx
728
1008
  * @throws {DOMException}
729
1009
  * @returns {void}
730
1010
  */
@@ -736,7 +1016,7 @@ IDBTransaction.__assertNotVersionChange = function (tx) {
736
1016
 
737
1017
  /**
738
1018
  *
739
- * @param {IDBTransactionFull|undefined} tx
1019
+ * @param {IDBTransactionFull|null|undefined} tx
740
1020
  * @throws {DOMException}
741
1021
  * @returns {void}
742
1022
  */
@@ -765,12 +1045,12 @@ IDBTransaction.__assertNotFinishedObjectStoreMethod = function (tx) {
765
1045
 
766
1046
  /**
767
1047
  *
768
- * @param {IDBTransactionFull|undefined} tx
1048
+ * @param {IDBTransactionFull|null|undefined} tx
769
1049
  * @throws {DOMException}
770
1050
  * @returns {void}
771
1051
  */
772
1052
  IDBTransaction.__assertActive = function (tx) {
773
- if (!tx || !tx.__active) {
1053
+ if (!tx || !tx.__active || tx.__committed) {
774
1054
  throw createDOMException('TransactionInactiveError', 'A request was placed against a transaction which is currently not active, or which is finished');
775
1055
  }
776
1056
  };
@@ -787,6 +1067,7 @@ IDBTransaction.prototype.__getParent = function () {
787
1067
  /* eslint-disable unicorn/no-top-level-side-effects -- Would be good */
788
1068
  util.defineOuterInterface(IDBTransaction.prototype, listeners);
789
1069
  util.defineReadonlyOuterInterface(IDBTransaction.prototype, readonlyProperties);
1070
+ util.setOperationNames(IDBTransaction.prototype);
790
1071
 
791
1072
  Object.defineProperty(IDBTransaction.prototype, 'constructor', {
792
1073
  enumerable: false,