indexeddbshim 17.3.2 → 17.3.4

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 (36) hide show
  1. package/dist/IDBCursor.d.ts.map +1 -1
  2. package/dist/IDBFactory.d.ts.map +1 -1
  3. package/dist/IDBTransaction.d.ts +8 -6
  4. package/dist/IDBTransaction.d.ts.map +1 -1
  5. package/dist/Key.d.ts.map +1 -1
  6. package/dist/Sca.d.ts.map +1 -1
  7. package/dist/indexeddbshim-Key.js +52 -13
  8. package/dist/indexeddbshim-Key.js.map +1 -1
  9. package/dist/indexeddbshim-Key.min.js +2 -2
  10. package/dist/indexeddbshim-Key.min.js.map +1 -1
  11. package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs +508 -134
  12. package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs.map +1 -1
  13. package/dist/indexeddbshim-UnicodeIdentifiers.js +337 -71
  14. package/dist/indexeddbshim-UnicodeIdentifiers.js.map +1 -1
  15. package/dist/indexeddbshim-UnicodeIdentifiers.min.js +3 -3
  16. package/dist/indexeddbshim-UnicodeIdentifiers.min.js.map +1 -1
  17. package/dist/indexeddbshim-node.cjs +508 -134
  18. package/dist/indexeddbshim-node.cjs.map +1 -1
  19. package/dist/indexeddbshim-noninvasive.js +337 -71
  20. package/dist/indexeddbshim-noninvasive.js.map +1 -1
  21. package/dist/indexeddbshim-noninvasive.min.js +3 -3
  22. package/dist/indexeddbshim-noninvasive.min.js.map +1 -1
  23. package/dist/indexeddbshim.js +337 -71
  24. package/dist/indexeddbshim.js.map +1 -1
  25. package/dist/indexeddbshim.min.js +3 -3
  26. package/dist/indexeddbshim.min.js.map +1 -1
  27. package/dist/nodeSQLiteDatabase.d.ts.map +1 -1
  28. package/dist/nodeWebSQL.d.ts.map +1 -1
  29. package/package.json +4 -4
  30. package/src/IDBCursor.js +34 -4
  31. package/src/IDBFactory.js +207 -60
  32. package/src/IDBTransaction.js +23 -3
  33. package/src/Key.js +34 -12
  34. package/src/Sca.js +53 -3
  35. package/src/nodeSQLiteDatabase.js +92 -26
  36. package/src/nodeWebSQL.js +8 -1
@@ -1,4 +1,4 @@
1
- /*! indexeddbshim - v17.3.2 - 8/24/2026 */
1
+ /*! indexeddbshim - v17.3.4 - 8/25/2026 */
2
2
 
3
3
  'use strict';
4
4
 
@@ -3159,6 +3159,31 @@ function cmp(first, second) {
3159
3159
  return result;
3160
3160
  }
3161
3161
 
3162
+ /**
3163
+ * @param {unknown[]} arr
3164
+ * @param {number} index
3165
+ * @param {unknown} val
3166
+ * @returns {void}
3167
+ */
3168
+ const setArrayValue = (arr, index, val) => {
3169
+ if (Reflect.has(Array.prototype, index)) {
3170
+ Object.defineProperty(arr, index, {
3171
+ value: val,
3172
+ enumerable: true,
3173
+ writable: true,
3174
+ configurable: true
3175
+ });
3176
+ } else {
3177
+ arr[index] = val;
3178
+ }
3179
+ };
3180
+ /**
3181
+ * @param {unknown[]} arr
3182
+ * @param {unknown} val
3183
+ * @returns {void}
3184
+ */
3185
+ const safePush = (arr, val) => setArrayValue(arr, arr.length, val);
3186
+
3162
3187
  /**
3163
3188
  * @typedef {NodeJS.TypedArray|DataView} ArrayBufferView
3164
3189
  */
@@ -3421,12 +3446,13 @@ const types = {
3421
3446
  * @returns {string}
3422
3447
  */
3423
3448
  encode(key) {
3449
+ /** @type {(string|null)[]} */
3424
3450
  const encoded = [];
3425
3451
  for (const [i, item] of key.entries()) {
3426
3452
  const encodedItem = encode$1(item, true); // encode the array item
3427
- encoded[i] = encodedItem;
3453
+ setArrayValue(encoded, i, encodedItem);
3428
3454
  }
3429
- encoded.push(keyTypeToEncodedChar.invalid + '-'); // append an extra item, so empty arrays sort correctly
3455
+ safePush(encoded, keyTypeToEncodedChar.invalid + '-'); // append an extra item, so empty arrays sort correctly
3430
3456
  let encodedKey = JSON.stringify(encoded);
3431
3457
  if (CFG.escapeNULForSQLiteStatements === false) {
3432
3458
  encodedKey = encodedKey.replaceAll(String.raw`\u0000`, '\0');
@@ -3447,7 +3473,7 @@ const types = {
3447
3473
  for (let i = 0; i < decoded.length; i++) {
3448
3474
  const item = decoded[i];
3449
3475
  const decodedItem = decode$1(item, true); // decode the item
3450
- decoded[i] = decodedItem;
3476
+ setArrayValue(decoded, i, decodedItem);
3451
3477
  }
3452
3478
  return decoded;
3453
3479
  }
@@ -3733,7 +3759,7 @@ function convertValueToKeyValueDecoded(input, seen, multiEntry, fullKeys) {
3733
3759
  // May throw (from binary)
3734
3760
  const arr = /** @type {Array<any>} */input;
3735
3761
  const len = arr.length;
3736
- seen.push(input);
3762
+ safePush(seen, input);
3737
3763
 
3738
3764
  /** @type {(KeyValueObject|Value)[]} */
3739
3765
  const keys = [];
@@ -3760,7 +3786,7 @@ function convertValueToKeyValueDecoded(input, seen, multiEntry, fullKeys) {
3760
3786
  };
3761
3787
  }
3762
3788
  if (!multiEntry || !fullKeys && keys.every(k => cmp(k, key.value) !== 0) || fullKeys && keys.every(k => cmp(k, key) !== 0)) {
3763
- keys.push(fullKeys ? key : key.value);
3789
+ safePush(keys, fullKeys ? key : key.value);
3764
3790
  }
3765
3791
  } catch (err) {
3766
3792
  if (!multiEntry) {
@@ -3914,7 +3940,7 @@ function evaluateKeyPathOnValueToDecodedValue(value, keyPath, multiEntry, fullKe
3914
3940
  if (key.failure) {
3915
3941
  return true;
3916
3942
  }
3917
- result.push(key.value);
3943
+ safePush(result, key.value);
3918
3944
  return false;
3919
3945
  }) ? {
3920
3946
  failure: true
@@ -3975,11 +4001,21 @@ function injectKeyIntoValueUsingKeyPath(value, key, keyPath) {
3975
4001
  identifiers.forEach(identifier => {
3976
4002
  const hop = Object.hasOwn(value, identifier);
3977
4003
  if (!hop) {
3978
- value[identifier] = {};
4004
+ Object.defineProperty(value, identifier, {
4005
+ value: {},
4006
+ enumerable: true,
4007
+ writable: true,
4008
+ configurable: true
4009
+ });
3979
4010
  }
3980
4011
  value = value[identifier];
3981
4012
  });
3982
- value[(/** @type {string} */last)] = key; // key is already a `keyValue` in our processing so no need to convert
4013
+ Object.defineProperty(value, /** @type {string} */last, {
4014
+ value: key,
4015
+ enumerable: true,
4016
+ writable: true,
4017
+ configurable: true
4018
+ }); // key is already a `keyValue` in our processing so no need to convert
3983
4019
  }
3984
4020
 
3985
4021
  /**
@@ -4048,6 +4084,7 @@ function isMultiEntryMatch(encodedEntry, encodedKey) {
4048
4084
  * @returns {Key[]}
4049
4085
  */
4050
4086
  function findMultiEntryMatches(keyEntry, range) {
4087
+ /** @type {unknown[]} */
4051
4088
  const matches = [];
4052
4089
  if (Array.isArray(keyEntry)) {
4053
4090
  for (let key of keyEntry) {
@@ -4060,17 +4097,17 @@ function findMultiEntryMatches(keyEntry, range) {
4060
4097
  } else {
4061
4098
  const nested = findMultiEntryMatches(key, range);
4062
4099
  if (nested.length > 0) {
4063
- matches.push(key);
4100
+ safePush(matches, key);
4064
4101
  }
4065
4102
  continue;
4066
4103
  }
4067
4104
  }
4068
4105
  if (isNullish(range) || isKeyInRange(key, range, true)) {
4069
- matches.push(key);
4106
+ safePush(matches, key);
4070
4107
  }
4071
4108
  }
4072
4109
  } else if (isNullish(range) || isKeyInRange(keyEntry, range, true)) {
4073
- matches.push(keyEntry);
4110
+ safePush(matches, keyEntry);
4074
4111
  }
4075
4112
  return matches;
4076
4113
  }
@@ -4094,12 +4131,13 @@ function convertKeyToValue(key) {
4094
4131
  }
4095
4132
  case 'array':
4096
4133
  {
4134
+ /** @type {ValueType[]} */
4097
4135
  const array = [];
4098
4136
  const len = value.length;
4099
4137
  let index = 0;
4100
4138
  while (index < len) {
4101
4139
  const entry = convertKeyToValue(value[index]);
4102
- array[index] = entry;
4140
+ setArrayValue(array, index, entry);
4103
4141
  index++;
4104
4142
  }
4105
4143
  return array;
@@ -4862,6 +4900,7 @@ if (cleanInterface) {
4862
4900
  }
4863
4901
 
4864
4902
  let uniqueID = 0;
4903
+ const activeTransactions = new Set();
4865
4904
  const listeners$1 = ['onabort', 'oncomplete', 'onerror'];
4866
4905
  const readonlyProperties$4 = ['objectStoreNames', 'mode', 'durability', 'db', 'error'];
4867
4906
 
@@ -4991,6 +5030,7 @@ IDBTransaction.__createInstance = function (db, storeNames, mode, durability = '
4991
5030
  me.__mode = mode;
4992
5031
  me.__durability = durability;
4993
5032
  me.__db = db;
5033
+ activeTransactions.add(me);
4994
5034
  me.__error = null;
4995
5035
  // @ts-expect-error Part of `ShimEventTarget`
4996
5036
  me.__setOptions({
@@ -5498,6 +5538,7 @@ IDBTransaction.prototype.__executeRequests = function () {
5498
5538
  me.__errored = true;
5499
5539
  throw e;
5500
5540
  } finally {
5541
+ activeTransactions.delete(me);
5501
5542
  me.__storeHandles = {};
5502
5543
  }
5503
5544
  }
@@ -5704,13 +5745,19 @@ IDBTransaction.prototype.__abortTransaction = function (err) {
5704
5745
  });
5705
5746
  }
5706
5747
  me.__active = false; // Setting here and in requestsFinished for https://github.com/w3c/IndexedDB/issues/87
5707
-
5748
+ activeTransactions.delete(me);
5708
5749
  if (err !== null) {
5709
5750
  me.__error = err;
5710
5751
  }
5711
- if (me.__requestsFinished) {
5752
+ if (me.__requestsFinished && err !== null) {
5712
5753
  // The transaction has already completed, so we can't call "onerror" or "onabort".
5713
- // So throw the error instead.
5754
+ // So throw the error instead. `err` is only ever `null` here via
5755
+ // `IDBTransaction.prototype.abort`'s own `__abortTransaction(null)`
5756
+ // call, which now checks `__requestsFinished` itself first and
5757
+ // throws `InvalidStateError` synchronously before ever reaching
5758
+ // this point -- so this guard is just defense in depth against
5759
+ // `err` somehow being `null` some other way, not something this
5760
+ // path should see in practice.
5714
5761
  setTimeout(() => {
5715
5762
  throw err;
5716
5763
  }, 0);
@@ -5828,6 +5875,14 @@ IDBTransaction.prototype.abort = function () {
5828
5875
  if (me.__committed) {
5829
5876
  throw createDOMException('InvalidStateError', 'The transaction has already been committed');
5830
5877
  }
5878
+ if (me.__requestsFinished) {
5879
+ // All requests have already finished and the transaction is
5880
+ // auto-committing (the async SQL commit round trip just hasn't
5881
+ // resolved yet) -- too late to abort per spec, even though
5882
+ // `__committed` itself isn't set until that round trip actually
5883
+ // finishes.
5884
+ throw createDOMException('InvalidStateError', 'The transaction is already committing');
5885
+ }
5831
5886
  me.__abortTransaction(null);
5832
5887
  };
5833
5888
 
@@ -5909,7 +5964,7 @@ IDBTransaction.__assertNotFinishedObjectStoreMethod = function (tx) {
5909
5964
  * @returns {void}
5910
5965
  */
5911
5966
  IDBTransaction.__assertActive = function (tx) {
5912
- if (!tx || !tx.__active || tx.__committed) {
5967
+ if (!tx || !tx.__active || !tx.__handlerActive || tx.__committed) {
5913
5968
  throw createDOMException('TransactionInactiveError', 'A request was placed against a transaction which is currently not active, or which is finished');
5914
5969
  }
5915
5970
  };
@@ -5936,6 +5991,9 @@ Object.defineProperty(IDBTransaction.prototype, 'constructor', {
5936
5991
  Object.defineProperty(IDBTransaction, 'prototype', {
5937
5992
  writable: false
5938
5993
  });
5994
+ /* eslint-enable unicorn/no-top-level-side-effects -- Would be good */
5995
+
5996
+ IDBTransaction.activeTransactions = activeTransactions;
5939
5997
 
5940
5998
  class TypesonPromise {
5941
5999
  constructor(e) {
@@ -7081,14 +7139,14 @@ const B = {
7081
7139
  revive: () => 1 / 0
7082
7140
  }
7083
7141
  },
7084
- k = {
7142
+ D = {
7085
7143
  map: {
7086
7144
  test: e => "Map" === toStringTag(e),
7087
7145
  replace: e => e.entries().toArray(),
7088
7146
  revive: e => new Map(e)
7089
7147
  }
7090
7148
  },
7091
- D = {
7149
+ R = {
7092
7150
  nan: {
7093
7151
  test: e => Number.isNaN(e),
7094
7152
  replace: () => "NaN",
@@ -7343,8 +7401,8 @@ const Y = {
7343
7401
  revive() {}
7344
7402
  }
7345
7403
  }],
7346
- ne = [D, M, L, F],
7347
- ce = [Z, Y, re, K, ne, O, z, U, _, B, I, h, N, C].concat("function" == typeof Map ? k : [], "function" == typeof Set ? H : [], "function" == typeof ArrayBuffer ? p : [], "function" == typeof Uint8Array ? X : [], "function" == typeof DataView ? w : [], "undefined" != typeof crypto ? v : [], "undefined" != typeof BigInt ? [m, d] : [], "undefined" != typeof DOMException ? A : [], "undefined" != typeof QuotaExceededError ? W : [], "undefined" != typeof WebTransportError ? te : [], "undefined" != typeof DOMRect ? x : [], "undefined" != typeof DOMPoint ? S : [], "undefined" != typeof DOMQuad ? P : [], "undefined" != typeof DOMMatrix ? T : [], "undefined" != typeof AudioData ? f : [], "undefined" != typeof EncodedAudioChunk ? E : [], "undefined" != typeof EncodedVideoChunk ? j : [], "undefined" != typeof VideoFrame ? ee : []);
7404
+ ne = [R, M, L, F],
7405
+ ce = [Z, Y, re, K, ne, O, z, U, _, B, I, h, N, C].concat("function" == typeof Map ? D : [], "function" == typeof Set ? H : [], "function" == typeof ArrayBuffer ? p : [], "function" == typeof Uint8Array ? X : [], "function" == typeof DataView ? w : [], "undefined" != typeof crypto ? v : [], "undefined" != typeof BigInt ? [m, d] : [], "undefined" != typeof DOMException ? A : [], "undefined" != typeof QuotaExceededError ? W : [], "undefined" != typeof WebTransportError ? te : [], "undefined" != typeof DOMRect ? x : [], "undefined" != typeof DOMPoint ? S : [], "undefined" != typeof DOMQuad ? P : [], "undefined" != typeof DOMMatrix ? T : [], "undefined" != typeof AudioData ? f : [], "undefined" != typeof EncodedAudioChunk ? E : [], "undefined" != typeof EncodedVideoChunk ? j : [], "undefined" != typeof VideoFrame ? ee : []);
7348
7406
  const ue = ce.concat({
7349
7407
  checkDataCloneException: {
7350
7408
  test(e) {
@@ -7361,11 +7419,53 @@ const ue = ce.concat({
7361
7419
  return false;
7362
7420
  }
7363
7421
  }
7422
+ }),
7423
+ ye = ue.concat({
7424
+ checkSharedArrayBufferException: {
7425
+ test(e) {
7426
+ if ("SharedArrayBuffer" === {}.toString.call(e).slice(8, -1)) throw new DOMException("The object cannot be cloned.", "DataCloneError");
7427
+ return false;
7428
+ }
7429
+ }
7364
7430
  });
7365
7431
 
7366
7432
  // See: https://stackoverflow.com/questions/42170826/categories-for-rejection-by-the-structured-cloning-algorithm
7367
7433
 
7368
- let typeson = new Typeson().register(ue);
7434
+ // Although typeson-registry already has a FileList type in its structured cloning presets,
7435
+ // we need to override it so it works with our tests
7436
+
7437
+ const specSet = ye.flatMap(preset => Array.isArray(preset) ? preset : [preset]).find(preset => preset && !Array.isArray(preset) && 'filelist' in preset);
7438
+ const origFileList = specSet && !Array.isArray(specSet) && 'filelist' in specSet ? specSet.filelist : undefined;
7439
+ const origTest = origFileList && typeof origFileList === 'object' && 'test' in origFileList && typeof origFileList.test === 'function' ? origFileList.test : undefined;
7440
+ const origRevive = origFileList && typeof origFileList === 'object' && 'revive' in origFileList && typeof origFileList.revive === 'function' ? origFileList.revive : undefined;
7441
+ const customFileList = origFileList ? {
7442
+ ...origFileList,
7443
+ /**
7444
+ * @param {unknown} x
7445
+ * @param {import('typeson').StateObject} state
7446
+ * @returns {boolean}
7447
+ */
7448
+ test(x, state) {
7449
+ if (typeof FileList !== 'undefined') {
7450
+ return x instanceof FileList;
7451
+ }
7452
+ return typeof origTest === 'function' ? origTest(x, state) : false;
7453
+ },
7454
+ /**
7455
+ * @param {unknown} x
7456
+ * @param {import('typeson').StateObject} state
7457
+ * @returns {unknown}
7458
+ */
7459
+ revive(x, state) {
7460
+ if (typeof FileList !== 'undefined') {
7461
+ return Reflect.construct(FileList, [x]);
7462
+ }
7463
+ return typeof origRevive === 'function' ? origRevive(x, state) : undefined;
7464
+ }
7465
+ } : undefined;
7466
+ let typeson = new Typeson().register([ye, customFileList ? {
7467
+ filelist: customFileList
7468
+ } : {}]);
7369
7469
 
7370
7470
  /**
7371
7471
  * @param {(preset: import('typeson-registry').Preset) =>
@@ -7374,7 +7474,7 @@ let typeson = new Typeson().register(ue);
7374
7474
  */
7375
7475
  function register(func) {
7376
7476
  // eslint-disable-next-line unicorn/no-top-level-assignment-in-function -- Should be one-time cache
7377
- typeson = new Typeson().register(func(ue));
7477
+ typeson = new Typeson().register(func(ye));
7378
7478
  }
7379
7479
 
7380
7480
  /**
@@ -10063,7 +10163,29 @@ function triggerAnyVersionChangeAndBlockedEvents(openConnections, req, oldVersio
10063
10163
  return new SyncPromise(function (resolve) {
10064
10164
  setTimeout(() => {
10065
10165
  entry.dispatchEvent(e); // No need to catch errors
10066
- resolve(undefined);
10166
+ // Unlike a native `Promise`, `SyncPromise#then` chains
10167
+ // synchronously off `resolve()` (verified directly:
10168
+ // `SyncPromise.resolve().then(cb)` runs `cb` before
10169
+ // even the calling code below it finishes) -- so
10170
+ // resolving immediately here would let the
10171
+ // `connectionsClosed()` check below run before a
10172
+ // same-task-but-microtask-later continuation of a
10173
+ // `versionchange` listener above (e.g. an `await`-
10174
+ // based one that then calls `db.close()`, as in
10175
+ // `transaction-lifetime.any.js`) ever gets a turn,
10176
+ // incorrectly firing `blocked` even though the
10177
+ // connection was about to close in time. Give real
10178
+ // microtask-deferred continuations a small, bounded
10179
+ // number of turns first -- same pattern as
10180
+ // `IDBTransaction.js`'s `checkQueueEntry`.
10181
+ let attemptsLeft = 10;
10182
+ (function wait() {
10183
+ if (attemptsLeft-- <= 0) {
10184
+ resolve(undefined);
10185
+ return;
10186
+ }
10187
+ queueMicrotask(wait);
10188
+ })();
10067
10189
  }, 0);
10068
10190
  });
10069
10191
  });
@@ -10123,6 +10245,23 @@ function triggerAnyVersionChangeAndBlockedEvents(openConnections, req, oldVersio
10123
10245
  */
10124
10246
  const websqlDBCache = {};
10125
10247
 
10248
+ /**
10249
+ * Tracks databases with a creation/upgrade currently in flight but not yet
10250
+ * committed or aborted -- keyed by (unescaped) database name. The
10251
+ * `dbVersions` row in `sysdb` these entries shadow is written *before*
10252
+ * `upgradeneeded` is even dispatched to user code (as the success
10253
+ * callback of that very `INSERT`/`UPDATE`), on the same shared `sysdb`
10254
+ * connection `databases()` itself reads from -- so, absent this, a
10255
+ * `databases()` call made while an upgrade is still pending would see
10256
+ * the new row (or new version) immediately, rather than only once the
10257
+ * corresponding `versionchange` transaction has genuinely committed, as
10258
+ * required by `get-databases.any.js`'s "doesn't pick up changes that
10259
+ * haven't committed" test. `IDBFactory.prototype.databases` filters
10260
+ * its SQL results against this map.
10261
+ * @type {Map<string, {oldVersion: Integer, newVersion: Integer}>}
10262
+ */
10263
+ const pendingVersionChanges = new Map();
10264
+
10126
10265
  /** @type {import('websql-configurable/lib/websql/WebSQLDatabase.js').default} */
10127
10266
  let sysdb;
10128
10267
  let nameCounter = 0;
@@ -10396,6 +10535,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
10396
10535
  }
10397
10536
  const req = IDBOpenDBRequest.__createInstance();
10398
10537
  let calledDbCreateError = false;
10538
+ let isRevertingSysdb = false;
10399
10539
  if (CFG.autoName && name === '') {
10400
10540
  // eslint-disable-next-line unicorn/no-top-level-assignment-in-function -- Necessary?
10401
10541
  name = 'autoNamedDatabase_' + nameCounter++;
@@ -10422,9 +10562,22 @@ IDBFactory.prototype.open = function (name /* , version */) {
10422
10562
  * @returns {boolean}
10423
10563
  */
10424
10564
  function dbCreateError(tx, err) {
10425
- if (calledDbCreateError) {
10565
+ if (calledDbCreateError || isRevertingSysdb) {
10426
10566
  return false;
10427
10567
  }
10568
+ // Defensive cleanup: `pendingVersionChanges.set(name, ...)` (see
10569
+ // below) runs *before* the `dbVersions` `INSERT`/`UPDATE` it
10570
+ // guards is even attempted, but only `versionSet`'s own
10571
+ // `on__beforecomplete`/`on__preabort` handlers ever clear it --
10572
+ // and `versionSet` never runs at all if that `INSERT`/`UPDATE`,
10573
+ // or an earlier step in this same `open()` flow, fails first.
10574
+ // Without this, such a failure would leave the entry orphaned
10575
+ // forever, silently corrupting `databases()` for any *other*,
10576
+ // unrelated later `open()` call that happens to reuse the same
10577
+ // database name (common in WPT tests, e.g. generic names like
10578
+ // "DB1"/"TestDatabase" reused across different test files in
10579
+ // the same process). A no-op if no entry was ever set.
10580
+ pendingVersionChanges.delete(name);
10428
10581
  const er = err ? webSQLErrback(err) : (/** @type {Error} */tx);
10429
10582
  calledDbCreateError = true;
10430
10583
  // Re: why bubbling here (and how cancelable is only really relevant for `window.onerror`) see: https://github.com/w3c/IndexedDB/issues/86
@@ -10473,36 +10626,46 @@ IDBFactory.prototype.open = function (name /* , version */) {
10473
10626
  */
10474
10627
  let sysdbFinishedCb = function (systx, err, cb) {
10475
10628
  if (err) {
10476
- try {
10477
- systx.executeSql('ROLLBACK', [], cb, cb);
10478
- } catch (err) {
10479
- // Browser may fail with expired transaction above so
10480
- // no choice but to manually revert
10629
+ /**
10630
+ * @param {any} [errorToShow]
10631
+ * @returns {void}
10632
+ */
10633
+ const manualRevert = function (errorToShow) {
10634
+ /**
10635
+ * @param {string} [msg]
10636
+ * @throws {Error}
10637
+ * @returns {never}
10638
+ */
10639
+ function reportError(msg) {
10640
+ throw new Error('Unable to roll back upgrade transaction!' + (msg || ''));
10641
+ }
10481
10642
  sysdb.transaction(function (systx) {
10482
- /**
10483
- *
10484
- * @param {string} msg
10485
- * @throws {Error}
10486
- * @returns {never}
10487
- */
10488
- function reportError(msg) {
10489
- throw new Error('Unable to roll back upgrade transaction!' + (msg || ''));
10490
- }
10491
-
10492
10643
  // Attempt to revert
10493
10644
  if (oldVersion === 0) {
10494
- systx.executeSql('DELETE FROM dbVersions WHERE "name" = ?', [sqlSafeName], function () {
10495
- // @ts-expect-error Force to work
10496
- cb(reportError); // eslint-disable-line promise/no-callback-in-promise -- Convenient
10497
- },
10498
- // @ts-expect-error Force to work
10499
- reportError);
10645
+ systx.executeSql('DELETE FROM dbVersions WHERE "name" = ?', [sqlSafeName]);
10500
10646
  } else {
10501
- systx.executeSql('UPDATE dbVersions SET "version" = ? WHERE "name" = ?', [oldVersion, sqlSafeName], cb,
10502
- // @ts-expect-error Force to work
10503
- reportError);
10647
+ systx.executeSql('UPDATE dbVersions SET "version" = ? WHERE "name" = ?', [oldVersion, sqlSafeName]);
10504
10648
  }
10649
+ }, function (sqlErr) {
10650
+ isRevertingSysdb = false;
10651
+ cb(sqlErr); // eslint-disable-line promise/no-callback-in-promise -- Convenient
10652
+ }, function () {
10653
+ isRevertingSysdb = false;
10654
+ cb(errorToShow || reportError); // eslint-disable-line promise/no-callback-in-promise -- Convenient
10505
10655
  });
10656
+ };
10657
+ try {
10658
+ systx.executeSql('ROLLBACK', [], function () {
10659
+ cb(); // eslint-disable-line promise/no-callback-in-promise -- Convenient
10660
+ }, function (tx, sqlErr) {
10661
+ // Browser/Node may fail with expired transaction, so manually revert
10662
+ manualRevert(sqlErr);
10663
+ return false;
10664
+ });
10665
+ } catch (e) {
10666
+ // Browser may fail with expired transaction above so
10667
+ // no choice but to manually revert
10668
+ manualRevert(e);
10506
10669
  }
10507
10670
  return;
10508
10671
  }
@@ -10536,22 +10699,43 @@ IDBFactory.prototype.open = function (name /* , version */) {
10536
10699
  // open/close the transaction's active-handler window itself.
10537
10700
  req.transaction.__handlerActive = true;
10538
10701
  req.dispatchEvent(e);
10539
- // Give any same-tick microtask scheduled from within the
10702
+ // Give any microtask-scheduled continuation of the
10540
10703
  // `upgradeneeded` handler (e.g. a plain
10541
- // `Promise.resolve().then(...)`) a chance to run -- and still
10542
- // observe the transaction as active -- before we deactivate it
10543
- // again, per https://github.com/w3c/IndexedDB/issues/87. Only
10544
- // the flag reset itself is deferred here -- unlike
10545
- // `IDBTransaction.js`'s `advanceAfterDispatch`, `finished()`
10546
- // (this transaction's own queue-advancement/completion signal)
10547
- // still runs synchronously, at exactly its previous timing: an
10548
- // earlier attempt at deferring `finished()` too raced against
10549
- // unrelated test setup that assumed a freshly deleted/created
10550
- // database's upgrade transaction had already fully completed by
10551
- // the time this function returns.
10552
- queueMicrotask(() => {
10553
- req.transaction.__handlerActive = false;
10554
- });
10704
+ // `Promise.resolve().then(...)`, or an `await`-based
10705
+ // continuation of a promise resolved from within the handler,
10706
+ // such as testharness.js's own `EventWatcher`) a chance to
10707
+ // run -- and still observe the transaction as active -- before
10708
+ // we deactivate it again, per
10709
+ // https://github.com/w3c/IndexedDB/issues/87. A single deferred
10710
+ // tick isn't always enough: an `await`-based continuation can
10711
+ // take more than one microtask turn to resume (e.g.
10712
+ // `transaction-lifetime.any.js`'s `EventWatcher`-based
10713
+ // `await eventWatcher.wait_for('upgradeneeded')`), so retry a
10714
+ // small, bounded number of times first -- same pattern as
10715
+ // `IDBTransaction.js`'s `checkQueueEntry` -- rather than
10716
+ // resetting after only one. Only the flag reset itself is
10717
+ // deferred here -- unlike `IDBTransaction.js`'s
10718
+ // `advanceAfterDispatch`, `finished()` (this transaction's own
10719
+ // queue-advancement/completion signal) still runs
10720
+ // synchronously, at exactly its previous timing: an earlier
10721
+ // attempt at deferring `finished()` too raced against unrelated
10722
+ // test setup that assumed a freshly deleted/created database's
10723
+ // upgrade transaction had already fully completed by the time
10724
+ // this function returns.
10725
+ /**
10726
+ * @param {Integer} attemptsLeft
10727
+ * @returns {void}
10728
+ */
10729
+ function deferHandlerActiveReset(attemptsLeft) {
10730
+ if (attemptsLeft <= 0) {
10731
+ req.transaction.__handlerActive = false;
10732
+ return;
10733
+ }
10734
+ queueMicrotask(() => {
10735
+ deferHandlerActiveReset(attemptsLeft - 1);
10736
+ });
10737
+ }
10738
+ deferHandlerActiveReset(10);
10555
10739
  if (e.__legacyOutputDidListenersThrowError) {
10556
10740
  logError('Error', 'An error occurred in an upgradeneeded handler attached to request chain', /** @type {Error} */e.__legacyOutputDidListenersThrowError); // We do nothing else with this error as per spec
10557
10741
  req.transaction.__abortTransaction(createDOMException('AbortError', 'A request was aborted.'));
@@ -10567,6 +10751,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
10567
10751
  * @returns {void}
10568
10752
  */
10569
10753
  function (ev) {
10754
+ pendingVersionChanges.delete(name);
10570
10755
  connection.__upgradeTransaction = null;
10571
10756
  /** @type {import('./IDBDatabase.js').IDBDatabaseFull} */
10572
10757
  req.__result.__versionTransaction = null;
@@ -10581,6 +10766,8 @@ IDBFactory.prototype.open = function (name /* , version */) {
10581
10766
 
10582
10767
  // eslint-disable-next-line camelcase -- Clear API
10583
10768
  req.transaction.on__preabort = function () {
10769
+ isRevertingSysdb = true;
10770
+ pendingVersionChanges.delete(name);
10584
10771
  connection.__upgradeTransaction = null;
10585
10772
  // We ensure any cache is deleted before any request error events fire and try to reopen
10586
10773
  if (useDatabaseCache) {
@@ -10597,6 +10784,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
10597
10784
  req.__result = undefined;
10598
10785
  req.__done = false;
10599
10786
  connection.close();
10787
+ isRevertingSysdb = true;
10600
10788
  setTimeout(() => {
10601
10789
  const err = createDOMException('AbortError', 'The upgrade transaction was aborted.');
10602
10790
  sysdbFinishedCb(systx, err, function (reportError) {
@@ -10643,6 +10831,10 @@ IDBFactory.prototype.open = function (name /* , version */) {
10643
10831
  // });
10644
10832
  };
10645
10833
  }
10834
+ pendingVersionChanges.set(name, {
10835
+ oldVersion,
10836
+ newVersion: version
10837
+ });
10646
10838
  if (oldVersion === 0) {
10647
10839
  systx.executeSql('INSERT INTO dbVersions VALUES (?,?)', [sqlSafeName, version], versionSet, dbCreateError);
10648
10840
  } else {
@@ -10654,7 +10846,26 @@ IDBFactory.prototype.open = function (name /* , version */) {
10654
10846
  }
10655
10847
  sysdbFinishedCb = function (systx, err, cb) {
10656
10848
  if (err) {
10657
- rollback(err, cb);
10849
+ rollback(err,
10850
+ /**
10851
+ * @param {Error} [reportError]
10852
+ * @returns {void}
10853
+ */
10854
+ function (reportError) {
10855
+ sysdb.transaction(function (systx) {
10856
+ if (oldVersion === 0) {
10857
+ systx.executeSql('DELETE FROM dbVersions WHERE "name" = ?', [sqlSafeName]);
10858
+ } else {
10859
+ systx.executeSql('UPDATE dbVersions SET "version" = ? WHERE "name" = ?', [oldVersion, sqlSafeName]);
10860
+ }
10861
+ }, function (sqlErr) {
10862
+ isRevertingSysdb = false;
10863
+ cb(sqlErr); // eslint-disable-line promise/no-callback-in-promise -- Convenient
10864
+ }, function () {
10865
+ isRevertingSysdb = false;
10866
+ cb(reportError); // eslint-disable-line promise/no-callback-in-promise -- Convenient
10867
+ });
10868
+ });
10658
10869
  } else {
10659
10870
  commit(cb);
10660
10871
  }
@@ -10946,6 +11157,15 @@ IDBFactory.prototype.cmp = function (key1, key2) {
10946
11157
  IDBFactory.prototype.databases = function () {
10947
11158
  const me = this;
10948
11159
  let calledDbCreateError = false;
11160
+ // Snapshotted *now*, synchronously, at call time -- not read later
11161
+ // from inside the SQL query's callback below, which runs on a
11162
+ // deferred macrotask (see `nodeSQLiteDatabase.js`'s `exec`) and so
11163
+ // could otherwise race against (and lose to) an in-flight upgrade's
11164
+ // own commit/abort handler clearing its `pendingVersionChanges`
11165
+ // entry in the meantime -- which would make this method incorrectly
11166
+ // reflect a since-committed change that hadn't committed yet when
11167
+ // it was actually called.
11168
+ const pendingVersionChangesSnapshot = new Map(pendingVersionChanges);
10949
11169
  return new Promise(function (resolve, reject) {
10950
11170
  // eslint-disable-line promise/avoid-new -- Own polyfill
10951
11171
  if (!(me instanceof IDBFactory)) {
@@ -10975,11 +11195,30 @@ IDBFactory.prototype.databases = function () {
10975
11195
  const dbNames = [];
10976
11196
  for (let i = 0; i < data.rows.length; i++) {
10977
11197
  const {
10978
- name,
11198
+ name: encodedName,
10979
11199
  version
10980
11200
  } = /** @type {{name: string, version: Integer}} */data.rows.item(i);
11201
+ const name = unescapeSQLiteResponse(encodedName);
11202
+ // A row for a database whose creation/upgrade hasn't
11203
+ // committed yet (see `pendingVersionChanges`) must
11204
+ // not reflect that in-flight change: a brand new
11205
+ // database (`oldVersion === 0`) isn't reported at
11206
+ // all until its creation commits, and an existing
11207
+ // database being upgraded is still reported, but
11208
+ // with its pre-upgrade version.
11209
+ const pending = pendingVersionChangesSnapshot.get(name);
11210
+ if (pending) {
11211
+ if (pending.oldVersion === 0) {
11212
+ continue;
11213
+ }
11214
+ dbNames.push({
11215
+ name,
11216
+ version: pending.oldVersion
11217
+ });
11218
+ continue;
11219
+ }
10981
11220
  dbNames.push({
10982
- name: unescapeSQLiteResponse(name),
11221
+ name,
10983
11222
  version
10984
11223
  });
10985
11224
  }
@@ -11392,9 +11631,26 @@ IDBCursor.prototype.__findBasic = function (key, primaryKey, tx, success, error,
11392
11631
  // Key.convertValueToKey(key); // Already checked by `continue` or `continuePrimaryKey`
11393
11632
  sqlValues.push(/** @type {string} */encode$1(key));
11394
11633
  } else if (continueCall && me.__key !== undefined) {
11395
- sql.push('AND', quotedKeyColumnName, op + ' ?');
11396
11634
  // Key.convertValueToKey(me.__key); // Already checked when stored
11397
- sqlValues.push(/** @type {string} */encode$1(me.__key));
11635
+ if (!me.__unique && me.__keyColumnName !== 'key' && me.__primaryKey !== undefined) {
11636
+ // A plain `continue()` on a non-unique index cursor must find
11637
+ // the next record strictly after the (key, primaryKey) pair
11638
+ // this cursor last returned -- a scalar `key > lastKey` alone
11639
+ // would wrongly exclude a *different*, not-yet-visited record
11640
+ // that's still tied on key with the last one (e.g. another
11641
+ // record that always had that same indexed value), and would
11642
+ // also wrongly re-admit the *same* record forever if a
11643
+ // same-transaction `update()` bumped its own key back above
11644
+ // the threshold, since a scalar comparison can't distinguish
11645
+ // "some other record newly tied" from "this record moved
11646
+ // past its own last position." Comparing the full tuple (via
11647
+ // this OR) against both key and primary key resolves both.
11648
+ sql.push('AND (', quotedKeyColumnName, op, '?', 'OR (', quotedKeyColumnName, '= ?', 'AND', quotedKey, op, '?))');
11649
+ sqlValues.push(/** @type {string} */encode$1(me.__key), /** @type {string} */encode$1(me.__key), /** @type {string} */encode$1(me.__primaryKey));
11650
+ } else {
11651
+ sql.push('AND', quotedKeyColumnName, op + ' ?');
11652
+ sqlValues.push(/** @type {string} */encode$1(me.__key));
11653
+ }
11398
11654
  }
11399
11655
  if (!me.__count) {
11400
11656
  // 1. Sort by key
@@ -12008,9 +12264,15 @@ IDBCursor.prototype.update = function (valueToUpdate) {
12008
12264
  * @returns {void}
12009
12265
  */
12010
12266
  function addToQueue(clonedValue) {
12011
- // We set the `invalidateCache` argument to `false` since the old value shouldn't be accessed
12267
+ // `invalidateCache: true` so any cursor on this store (including
12268
+ // this one) drops its prefetched row batch, forcing its next
12269
+ // `continue()` to re-query live rather than serve rows snapshotted
12270
+ // before this update -- needed for the compound-tuple
12271
+ // continuation logic in `__findBasic` to see this update's
12272
+ // effect on ordering (see "Modify records during cursor
12273
+ // iteration" in idbcursor_update_index.any.js).
12012
12274
  // @ts-ignore -- API (not erring in TS 6)
12013
- IDBObjectStore.__storingRecordObjectStore(request, me.__store, false, clonedValue, false, key);
12275
+ IDBObjectStore.__storingRecordObjectStore(request, me.__store, true, clonedValue, false, key);
12014
12276
  }
12015
12277
  if (me.__store.keyPath !== null) {
12016
12278
  const [evaluatedKey, clonedValue] = me.__store.__validateKeyAndValueAndCloneValue(valueToUpdate, undefined, true);
@@ -13132,11 +13394,9 @@ function runBatch(self, batch) {
13132
13394
  self._running = false;
13133
13395
  runAllSql(self);
13134
13396
  }
13135
- const currentTask = /** @type {import('./WebSQLDatabase.js').TransactionTask} */
13136
- self._websqlDatabase._currentTask;
13137
13397
  const {
13138
13398
  readOnly
13139
- } = currentTask;
13399
+ } = self._task;
13140
13400
  self._websqlDatabase._db.exec(batch, readOnly, function (err, results) {
13141
13401
  /* c8 ignore next */
13142
13402
  if (err || !results) {
@@ -13224,10 +13484,12 @@ function executeSql(self, sql, args, sqlCallback, sqlErrorCallback, executeDelay
13224
13484
  class WebSQLTransaction {
13225
13485
  /**
13226
13486
  * @param {import('./WebSQLDatabase.js').default} websqlDatabase
13487
+ * @param {import('./WebSQLDatabase.js').TransactionTask} task
13227
13488
  * @param {import('../types.js').Delay} [executeDelay]
13228
13489
  */
13229
- constructor(websqlDatabase, executeDelay) {
13490
+ constructor(websqlDatabase, task, executeDelay) {
13230
13491
  this._websqlDatabase = websqlDatabase;
13492
+ this._task = task;
13231
13493
  /** @type {Error | null} */
13232
13494
  this._error = null;
13233
13495
  this._complete = false;
@@ -13236,11 +13498,14 @@ class WebSQLTransaction {
13236
13498
  this._executeDelay = executeDelay || immediate;
13237
13499
  /** @type {import('tiny-queue').default<SQLTask>} */
13238
13500
  this._sqlQueue = new Queue();
13239
- const currentTask = /** @type {import('./WebSQLDatabase.js').TransactionTask} */
13240
- websqlDatabase._currentTask;
13241
- if (!currentTask.readOnly) {
13242
- // Since we serialize all access to the database, there is no need to
13243
- // run read-only tasks in a transaction. This is a perf boost.
13501
+ if (!task.readOnly) {
13502
+ // A read-only task never needs a transaction wrapper for its own
13503
+ // sake (reads don't need atomicity against each other) -- true
13504
+ // whether or not `concurrentReaders` (see `WebSQLDatabase`) lets
13505
+ // multiple read-only tasks actually run at once. This is a perf
13506
+ // boost either way. A non-read-only task always still gets one, and
13507
+ // always still runs with full exclusivity (see
13508
+ // `WebSQLDatabase#runNextTransaction`).
13244
13509
  this._sqlQueue.push(new SQLTask('BEGIN;', [], noop, noop));
13245
13510
  }
13246
13511
  }
@@ -13321,39 +13586,68 @@ class WebSQLDatabase {
13321
13586
  this._db = db;
13322
13587
  /** @type {import('tiny-queue').default<TransactionTask>} */
13323
13588
  this._txnQueue = new Queue();
13324
- this._running = false;
13589
+ // Off by default: queued transactions -- read or write alike -- run
13590
+ // strictly one at a time, in the order requested, per the WebSQL spec
13591
+ // (see `#runNextTransaction`). When true, any number of read-only
13592
+ // tasks may instead run concurrently; a non-read-only task still
13593
+ // always runs with full exclusivity either way.
13594
+ this._concurrentReaders = Boolean(webSQLOverrides.concurrentReaders);
13595
+ /** @type {Set<TransactionTask>} */
13596
+ this._activeReaders = new Set();
13325
13597
  /** @type {TransactionTask | null} */
13326
- this._currentTask = null;
13598
+ this._activeWriter = null;
13327
13599
  this._transactionDelay = webSQLOverrides.transactionDelay || immediate;
13328
13600
  this._executeDelay = webSQLOverrides.executeDelay || immediate;
13329
13601
  }
13330
13602
 
13331
13603
  /**
13332
- *
13604
+ * @param {TransactionTask} task
13333
13605
  */
13334
- #runTransaction() {
13335
- const txn = new WebSQLTransaction(this, this._executeDelay);
13606
+ #runTransaction(task) {
13607
+ const txn = new WebSQLTransaction(this, task, this._executeDelay);
13336
13608
  this._transactionDelay(() => {
13337
- const currentTask = /** @type {TransactionTask} */this._currentTask;
13338
- currentTask.txnCallback(txn);
13609
+ task.txnCallback(txn);
13339
13610
  txn._checkDone();
13340
13611
  });
13341
13612
  }
13342
13613
 
13343
13614
  /**
13344
- *
13615
+ * Starts as many queued tasks as the current lock state allows. With
13616
+ * `concurrentReaders` off (the default), a task only starts once
13617
+ * nothing else is active at all -- full mutual exclusion, matching the
13618
+ * WebSQL spec's strict one-at-a-time, in-request-order guarantee (see
13619
+ * this file's own test suite, "callback order 2"). With it on, any
13620
+ * leading run of read-only tasks can all start together instead
13621
+ * (concurrent reads are always fine); a non-read-only task still needs
13622
+ * exclusivity regardless, so nothing past it may start until it
13623
+ * finishes. Not starvation-proof in the `concurrentReaders` case -- a
13624
+ * steady stream of arriving readers could in principle keep a waiting
13625
+ * writer waiting indefinitely -- but that's an acceptable tradeoff over
13626
+ * the added complexity of tracking arrival order across reader/writer
13627
+ * kinds.
13345
13628
  */
13346
13629
  #runNextTransaction() {
13347
- if (this._running) {
13348
- return;
13349
- }
13350
- const task = this._txnQueue.shift();
13351
- if (!task) {
13352
- return;
13630
+ for (;;) {
13631
+ const [nextTask] = this._txnQueue.slice(0, 1);
13632
+ if (!nextTask) {
13633
+ return;
13634
+ }
13635
+ const readerMayShare = this._concurrentReaders && nextTask.readOnly;
13636
+ if (readerMayShare) {
13637
+ if (this._activeWriter) {
13638
+ return;
13639
+ }
13640
+ } else if (this._activeWriter || this._activeReaders.size) {
13641
+ return;
13642
+ }
13643
+ this._txnQueue.shift();
13644
+ if (nextTask.readOnly) {
13645
+ this._activeReaders.add(nextTask);
13646
+ } else {
13647
+ this._activeWriter = nextTask;
13648
+ }
13649
+ this.#runTransaction(nextTask);
13353
13650
  }
13354
- this._currentTask = task;
13355
- this._running = true;
13356
- this.#runTransaction();
13357
13651
  }
13358
13652
 
13359
13653
  /**
@@ -13381,6 +13675,7 @@ class WebSQLDatabase {
13381
13675
  */
13382
13676
  // eslint-disable-next-line unicorn/prefer-private-class-fields -- see above
13383
13677
  _onTransactionComplete(err, transaction) {
13678
+ const task = transaction._task;
13384
13679
  /**
13385
13680
  * @param {Error | boolean | null} [er]
13386
13681
  */
@@ -13395,14 +13690,15 @@ class WebSQLDatabase {
13395
13690
  transaction._complete = true;
13396
13691
  }
13397
13692
  if (er) {
13398
- if (this._currentTask) {
13399
- this._currentTask.errorCallback(/** @type {Error} */er);
13400
- }
13401
- } else if (this._currentTask) {
13402
- this._currentTask.successCallback();
13693
+ task.errorCallback(/** @type {Error} */er);
13694
+ } else {
13695
+ task.successCallback();
13696
+ }
13697
+ if (task.readOnly) {
13698
+ this._activeReaders.delete(task);
13699
+ } else if (this._activeWriter === task) {
13700
+ this._activeWriter = null;
13403
13701
  }
13404
- this._running = false;
13405
- this._currentTask = null;
13406
13702
  this.#runNextTransaction();
13407
13703
  };
13408
13704
  /**
@@ -13428,13 +13724,13 @@ class WebSQLDatabase {
13428
13724
  }
13429
13725
  });
13430
13726
  };
13431
- if (this._currentTask && this._currentTask.nonstandardTransCb) {
13432
- const cont = this._currentTask.nonstandardTransCb.call(this, this._currentTask, err, done, rollback, commit);
13727
+ if (task.nonstandardTransCb) {
13728
+ const cont = task.nonstandardTransCb.call(this, task, err, done, rollback, commit);
13433
13729
  if (!cont) {
13434
13730
  return;
13435
13731
  }
13436
13732
  }
13437
- if (this._currentTask && this._currentTask.readOnly) {
13733
+ if (task.readOnly) {
13438
13734
  done(err); // read-only doesn't require a transaction
13439
13735
  } else if (err) {
13440
13736
  rollback(err);
@@ -14392,16 +14688,28 @@ const READ_ONLY_ERROR = new Error('could not prepare statement (23 not authorize
14392
14688
  // connection and a newly-opened one during an upgrade. `PRAGMA
14393
14689
  // busy_timeout` can't safely arbitrate between them here: it blocks the
14394
14690
  // single JS thread synchronously while it retries, but the lock holder's
14395
- // own release is itself scheduled via `setTimeout` below, which can never
14396
- // fire while that retry loop is blocking the same thread -- so instead of
14397
- // waiting, the second connection's write just fails with "database is
14398
- // locked". Serialize writes per file path instead, so a second
14399
- // connection's `BEGIN` waits for the first connection's transaction to
14400
- // actually finish rather than colliding with it.
14691
+ // own release is itself scheduled via `setImmediate` below, which can
14692
+ // never fire while that retry loop is blocking the same thread -- so
14693
+ // instead of waiting, a second connection's write just fails with
14694
+ // "database is locked".
14695
+ //
14696
+ // `fileReaders`/`fileWriter` implement a simple (not starvation-proof --
14697
+ // see the release loop below) reader/writer lock per file path instead:
14698
+ // any number of `readonly` transactions may hold the file concurrently,
14699
+ // matching what SQLite itself already natively supports (multiple
14700
+ // readers never need to exclude each other, only a writer needs
14701
+ // exclusivity), while a non-`readonly` transaction still waits for
14702
+ // exclusive access -- no concurrent readers, no concurrent writer. A
14703
+ // simple single-owner mutex (this file's prior implementation) would
14704
+ // otherwise serialize even purely-concurrent-reader scenarios that
14705
+ // never touch a writer at all, for no reason `better-sqlite3`/SQLite
14706
+ // itself requires.
14707
+ /** @type {Map<string, Set<{_db: any, _qFilePath: string}>>} */
14708
+ const fileReaders = new Map();
14401
14709
  /** @type {Map<string, {_db: any, _qFilePath: string}>} */
14402
- const fileLockOwners = new Map();
14403
- /** @type {Map<string, (() => void)[]>} */
14404
- const fileLockWaiters = new Map();
14710
+ const fileWriter = new Map();
14711
+ /** @type {Map<string, {isReader: boolean, resume: () => void}[]>} */
14712
+ const fileWaiters = new Map();
14405
14713
  const beginRe = /^\s*BEGIN\b/iu;
14406
14714
  const endRe = /^\s*(END|COMMIT|ROLLBACK)\b/iu;
14407
14715
 
@@ -14513,14 +14821,29 @@ SQLiteDatabase.prototype.exec = function exec(queries, readOnly, callback) {
14513
14821
  // already fully independent -- so there is nothing to serialize.
14514
14822
  const filePath = this._qFilePath === ':memory:' ? null : this._qFilePath;
14515
14823
  if (filePath && queries[0] && beginRe.test(queries[0].sql)) {
14516
- const owner = fileLockOwners.get(filePath);
14517
- if (owner && owner !== this) {
14518
- const waiters = fileLockWaiters.get(filePath) || [];
14519
- waiters.push(() => this.exec(queries, readOnly, callback));
14520
- fileLockWaiters.set(filePath, waiters);
14824
+ const activeReaders = fileReaders.get(filePath);
14825
+ const hasActiveReaders = Boolean(activeReaders && activeReaders.size);
14826
+ const activeWriter = fileWriter.get(filePath);
14827
+ const blockedByWriter = Boolean(activeWriter && activeWriter !== this);
14828
+ const blocked = blockedByWriter || !readOnly && hasActiveReaders;
14829
+ if (blocked) {
14830
+ const waiters = fileWaiters.get(filePath) || [];
14831
+ waiters.push({
14832
+ isReader: readOnly,
14833
+ resume: () => this.exec(queries, readOnly, callback)
14834
+ });
14835
+ fileWaiters.set(filePath, waiters);
14521
14836
  return;
14522
14837
  }
14523
- fileLockOwners.set(filePath, this);
14838
+ if (readOnly) {
14839
+ if (activeReaders) {
14840
+ activeReaders.add(this);
14841
+ } else {
14842
+ fileReaders.set(filePath, new Set([this]));
14843
+ }
14844
+ } else {
14845
+ fileWriter.set(filePath, this);
14846
+ }
14524
14847
  }
14525
14848
  const db = this._db._db;
14526
14849
  const len = queries.length;
@@ -14562,13 +14885,15 @@ SQLiteDatabase.prototype.exec = function exec(queries, readOnly, callback) {
14562
14885
  }
14563
14886
  }
14564
14887
  }
14565
- // A real timer (not `queueMicrotask`) so this yields to the macrotask
14566
- // queue: code that synchronously issues a new request from within
14567
- // each request's callback (e.g. to keep a transaction alive) would
14888
+ // A real macrotask (not `queueMicrotask`) so this yields properly:
14889
+ // code that synchronously issues a new request from within each
14890
+ // request's callback (e.g. to keep a transaction alive) would
14568
14891
  // otherwise chain microtask to microtask forever, starving out any
14569
14892
  // `setTimeout`-based code (including IndexedDB's own internal request
14570
- // scheduling) that never gets a turn to run.
14571
- setTimeout(() => {
14893
+ // scheduling) that never gets a turn to run. `setImmediate` (Node's
14894
+ // "check" phase) still yields the same way `setTimeout(..., 0)` did,
14895
+ // but runs sooner in Node's event loop.
14896
+ setImmediate(() => {
14572
14897
  // Release the file lock (if held) and hand it to the next waiting
14573
14898
  // connection, if any, only once this transaction has genuinely
14574
14899
  // finished -- and only here, on its own turn, so a resumed
@@ -14583,15 +14908,52 @@ SQLiteDatabase.prototype.exec = function exec(queries, readOnly, callback) {
14583
14908
  // and never see the matching release, deadlocking every later
14584
14909
  // connection to the same file.
14585
14910
  if (filePath && queries.some(q => endRe.test(q.sql))) {
14586
- fileLockOwners.delete(filePath);
14587
- const waiters = fileLockWaiters.get(filePath);
14588
- const nextWaitingExec = waiters && waiters.shift();
14589
- if (nextWaitingExec) {
14590
- nextWaitingExec();
14911
+ const activeReaders = fileReaders.get(filePath);
14912
+ if (activeReaders) {
14913
+ activeReaders.delete(this);
14914
+ if (!activeReaders.size) {
14915
+ fileReaders.delete(filePath);
14916
+ }
14917
+ }
14918
+ if (fileWriter.get(filePath) === this) {
14919
+ fileWriter.delete(filePath);
14920
+ }
14921
+ // Resume as many waiters as the lock now genuinely allows:
14922
+ // any leading run of readers (concurrent reads are always
14923
+ // fine), then at most one writer, since a writer needs
14924
+ // exclusivity -- stop there rather than looking past it.
14925
+ // Not starvation-proof: a steady stream of arriving readers
14926
+ // could in principle keep a waiting writer waiting
14927
+ // indefinitely, but that's an acceptable tradeoff here over
14928
+ // the added complexity of tracking arrival order across
14929
+ // reader/writer kinds.
14930
+ const waiters = fileWaiters.get(filePath);
14931
+ if (waiters) {
14932
+ while (waiters.length) {
14933
+ if (fileWriter.has(filePath)) {
14934
+ break;
14935
+ }
14936
+ const next = waiters[0];
14937
+ const stillHasReaders = fileReaders.get(filePath);
14938
+ if (!next.isReader && stillHasReaders && stillHasReaders.size) {
14939
+ break;
14940
+ }
14941
+ waiters.shift();
14942
+ next.resume();
14943
+ if (!next.isReader) {
14944
+ // A writer just took exclusive ownership (inside
14945
+ // `resume()`, synchronously, via the acquire
14946
+ // logic above) -- nothing else may proceed now.
14947
+ break;
14948
+ }
14949
+ }
14950
+ if (!waiters.length) {
14951
+ fileWaiters.delete(filePath);
14952
+ }
14591
14953
  }
14592
14954
  }
14593
14955
  callback(null, results);
14594
- }, 0);
14956
+ });
14595
14957
  };
14596
14958
 
14597
14959
  /**
@@ -14631,7 +14993,19 @@ function wrappedSQLiteDatabase(name) {
14631
14993
  }
14632
14994
  return db;
14633
14995
  }
14634
- const nodeWebSQL = customOpenDatabase(/** @type {SQLiteDatabaseConstructor} */ /** @type {unknown} */wrappedSQLiteDatabase, {});
14996
+
14997
+ // `concurrentReaders` is off by default in `websql-configurable` itself (to
14998
+ // preserve the WebSQL spec's strict, one-at-a-time transaction ordering
14999
+ // that library's own test suite depends on), but IndexedDBShim only ever
15000
+ // uses it as an internal SQL execution engine -- it doesn't need or expose
15001
+ // that ordering guarantee itself -- so it's safe, and needed, to opt in
15002
+ // here: without it, two same-scope `readonly` IDBTransactions can deadlock
15003
+ // waiting on each other (see `transaction-scheduling-within-database.any.js`).
15004
+ const nodeWebSQL = customOpenDatabase(/** @type {SQLiteDatabaseConstructor} */ /** @type {unknown} */wrappedSQLiteDatabase, {
15005
+ websql: {
15006
+ concurrentReaders: true
15007
+ }
15008
+ });
14635
15009
 
14636
15010
  // ID_Start (includes Other_ID_Start)
14637
15011
  const UnicodeIDStart = String.raw`(?:[$A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D])`;