indexeddbshim 17.3.2 → 17.3.3

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.
@@ -1,4 +1,4 @@
1
- /*! indexeddbshim - v17.3.2 - 8/24/2026 */
1
+ /*! indexeddbshim - v17.3.3 - 8/25/2026 */
2
2
 
3
3
  'use strict';
4
4
 
@@ -5708,9 +5708,15 @@ IDBTransaction.prototype.__abortTransaction = function (err) {
5708
5708
  if (err !== null) {
5709
5709
  me.__error = err;
5710
5710
  }
5711
- if (me.__requestsFinished) {
5711
+ if (me.__requestsFinished && err !== null) {
5712
5712
  // The transaction has already completed, so we can't call "onerror" or "onabort".
5713
- // So throw the error instead.
5713
+ // So throw the error instead. `err` is only ever `null` here via
5714
+ // `IDBTransaction.prototype.abort`'s own `__abortTransaction(null)`
5715
+ // call, which now checks `__requestsFinished` itself first and
5716
+ // throws `InvalidStateError` synchronously before ever reaching
5717
+ // this point -- so this guard is just defense in depth against
5718
+ // `err` somehow being `null` some other way, not something this
5719
+ // path should see in practice.
5714
5720
  setTimeout(() => {
5715
5721
  throw err;
5716
5722
  }, 0);
@@ -5828,6 +5834,14 @@ IDBTransaction.prototype.abort = function () {
5828
5834
  if (me.__committed) {
5829
5835
  throw createDOMException('InvalidStateError', 'The transaction has already been committed');
5830
5836
  }
5837
+ if (me.__requestsFinished) {
5838
+ // All requests have already finished and the transaction is
5839
+ // auto-committing (the async SQL commit round trip just hasn't
5840
+ // resolved yet) -- too late to abort per spec, even though
5841
+ // `__committed` itself isn't set until that round trip actually
5842
+ // finishes.
5843
+ throw createDOMException('InvalidStateError', 'The transaction is already committing');
5844
+ }
5831
5845
  me.__abortTransaction(null);
5832
5846
  };
5833
5847
 
@@ -10063,7 +10077,29 @@ function triggerAnyVersionChangeAndBlockedEvents(openConnections, req, oldVersio
10063
10077
  return new SyncPromise(function (resolve) {
10064
10078
  setTimeout(() => {
10065
10079
  entry.dispatchEvent(e); // No need to catch errors
10066
- resolve(undefined);
10080
+ // Unlike a native `Promise`, `SyncPromise#then` chains
10081
+ // synchronously off `resolve()` (verified directly:
10082
+ // `SyncPromise.resolve().then(cb)` runs `cb` before
10083
+ // even the calling code below it finishes) -- so
10084
+ // resolving immediately here would let the
10085
+ // `connectionsClosed()` check below run before a
10086
+ // same-task-but-microtask-later continuation of a
10087
+ // `versionchange` listener above (e.g. an `await`-
10088
+ // based one that then calls `db.close()`, as in
10089
+ // `transaction-lifetime.any.js`) ever gets a turn,
10090
+ // incorrectly firing `blocked` even though the
10091
+ // connection was about to close in time. Give real
10092
+ // microtask-deferred continuations a small, bounded
10093
+ // number of turns first -- same pattern as
10094
+ // `IDBTransaction.js`'s `checkQueueEntry`.
10095
+ let attemptsLeft = 10;
10096
+ (function wait() {
10097
+ if (attemptsLeft-- <= 0) {
10098
+ resolve(undefined);
10099
+ return;
10100
+ }
10101
+ queueMicrotask(wait);
10102
+ })();
10067
10103
  }, 0);
10068
10104
  });
10069
10105
  });
@@ -10123,6 +10159,23 @@ function triggerAnyVersionChangeAndBlockedEvents(openConnections, req, oldVersio
10123
10159
  */
10124
10160
  const websqlDBCache = {};
10125
10161
 
10162
+ /**
10163
+ * Tracks databases with a creation/upgrade currently in flight but not yet
10164
+ * committed or aborted -- keyed by (unescaped) database name. The
10165
+ * `dbVersions` row in `sysdb` these entries shadow is written *before*
10166
+ * `upgradeneeded` is even dispatched to user code (as the success
10167
+ * callback of that very `INSERT`/`UPDATE`), on the same shared `sysdb`
10168
+ * connection `databases()` itself reads from -- so, absent this, a
10169
+ * `databases()` call made while an upgrade is still pending would see
10170
+ * the new row (or new version) immediately, rather than only once the
10171
+ * corresponding `versionchange` transaction has genuinely committed, as
10172
+ * required by `get-databases.any.js`'s "doesn't pick up changes that
10173
+ * haven't committed" test. `IDBFactory.prototype.databases` filters
10174
+ * its SQL results against this map.
10175
+ * @type {Map<string, {oldVersion: Integer, newVersion: Integer}>}
10176
+ */
10177
+ const pendingVersionChanges = new Map();
10178
+
10126
10179
  /** @type {import('websql-configurable/lib/websql/WebSQLDatabase.js').default} */
10127
10180
  let sysdb;
10128
10181
  let nameCounter = 0;
@@ -10425,6 +10478,19 @@ IDBFactory.prototype.open = function (name /* , version */) {
10425
10478
  if (calledDbCreateError) {
10426
10479
  return false;
10427
10480
  }
10481
+ // Defensive cleanup: `pendingVersionChanges.set(name, ...)` (see
10482
+ // below) runs *before* the `dbVersions` `INSERT`/`UPDATE` it
10483
+ // guards is even attempted, but only `versionSet`'s own
10484
+ // `on__beforecomplete`/`on__preabort` handlers ever clear it --
10485
+ // and `versionSet` never runs at all if that `INSERT`/`UPDATE`,
10486
+ // or an earlier step in this same `open()` flow, fails first.
10487
+ // Without this, such a failure would leave the entry orphaned
10488
+ // forever, silently corrupting `databases()` for any *other*,
10489
+ // unrelated later `open()` call that happens to reuse the same
10490
+ // database name (common in WPT tests, e.g. generic names like
10491
+ // "DB1"/"TestDatabase" reused across different test files in
10492
+ // the same process). A no-op if no entry was ever set.
10493
+ pendingVersionChanges.delete(name);
10428
10494
  const er = err ? webSQLErrback(err) : (/** @type {Error} */tx);
10429
10495
  calledDbCreateError = true;
10430
10496
  // Re: why bubbling here (and how cancelable is only really relevant for `window.onerror`) see: https://github.com/w3c/IndexedDB/issues/86
@@ -10536,22 +10602,43 @@ IDBFactory.prototype.open = function (name /* , version */) {
10536
10602
  // open/close the transaction's active-handler window itself.
10537
10603
  req.transaction.__handlerActive = true;
10538
10604
  req.dispatchEvent(e);
10539
- // Give any same-tick microtask scheduled from within the
10605
+ // Give any microtask-scheduled continuation of the
10540
10606
  // `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
- });
10607
+ // `Promise.resolve().then(...)`, or an `await`-based
10608
+ // continuation of a promise resolved from within the handler,
10609
+ // such as testharness.js's own `EventWatcher`) a chance to
10610
+ // run -- and still observe the transaction as active -- before
10611
+ // we deactivate it again, per
10612
+ // https://github.com/w3c/IndexedDB/issues/87. A single deferred
10613
+ // tick isn't always enough: an `await`-based continuation can
10614
+ // take more than one microtask turn to resume (e.g.
10615
+ // `transaction-lifetime.any.js`'s `EventWatcher`-based
10616
+ // `await eventWatcher.wait_for('upgradeneeded')`), so retry a
10617
+ // small, bounded number of times first -- same pattern as
10618
+ // `IDBTransaction.js`'s `checkQueueEntry` -- rather than
10619
+ // resetting after only one. Only the flag reset itself is
10620
+ // deferred here -- unlike `IDBTransaction.js`'s
10621
+ // `advanceAfterDispatch`, `finished()` (this transaction's own
10622
+ // queue-advancement/completion signal) still runs
10623
+ // synchronously, at exactly its previous timing: an earlier
10624
+ // attempt at deferring `finished()` too raced against unrelated
10625
+ // test setup that assumed a freshly deleted/created database's
10626
+ // upgrade transaction had already fully completed by the time
10627
+ // this function returns.
10628
+ /**
10629
+ * @param {Integer} attemptsLeft
10630
+ * @returns {void}
10631
+ */
10632
+ function deferHandlerActiveReset(attemptsLeft) {
10633
+ if (attemptsLeft <= 0) {
10634
+ req.transaction.__handlerActive = false;
10635
+ return;
10636
+ }
10637
+ queueMicrotask(() => {
10638
+ deferHandlerActiveReset(attemptsLeft - 1);
10639
+ });
10640
+ }
10641
+ deferHandlerActiveReset(10);
10555
10642
  if (e.__legacyOutputDidListenersThrowError) {
10556
10643
  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
10644
  req.transaction.__abortTransaction(createDOMException('AbortError', 'A request was aborted.'));
@@ -10567,6 +10654,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
10567
10654
  * @returns {void}
10568
10655
  */
10569
10656
  function (ev) {
10657
+ pendingVersionChanges.delete(name);
10570
10658
  connection.__upgradeTransaction = null;
10571
10659
  /** @type {import('./IDBDatabase.js').IDBDatabaseFull} */
10572
10660
  req.__result.__versionTransaction = null;
@@ -10581,6 +10669,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
10581
10669
 
10582
10670
  // eslint-disable-next-line camelcase -- Clear API
10583
10671
  req.transaction.on__preabort = function () {
10672
+ pendingVersionChanges.delete(name);
10584
10673
  connection.__upgradeTransaction = null;
10585
10674
  // We ensure any cache is deleted before any request error events fire and try to reopen
10586
10675
  if (useDatabaseCache) {
@@ -10643,6 +10732,10 @@ IDBFactory.prototype.open = function (name /* , version */) {
10643
10732
  // });
10644
10733
  };
10645
10734
  }
10735
+ pendingVersionChanges.set(name, {
10736
+ oldVersion,
10737
+ newVersion: version
10738
+ });
10646
10739
  if (oldVersion === 0) {
10647
10740
  systx.executeSql('INSERT INTO dbVersions VALUES (?,?)', [sqlSafeName, version], versionSet, dbCreateError);
10648
10741
  } else {
@@ -10946,6 +11039,15 @@ IDBFactory.prototype.cmp = function (key1, key2) {
10946
11039
  IDBFactory.prototype.databases = function () {
10947
11040
  const me = this;
10948
11041
  let calledDbCreateError = false;
11042
+ // Snapshotted *now*, synchronously, at call time -- not read later
11043
+ // from inside the SQL query's callback below, which runs on a
11044
+ // deferred macrotask (see `nodeSQLiteDatabase.js`'s `exec`) and so
11045
+ // could otherwise race against (and lose to) an in-flight upgrade's
11046
+ // own commit/abort handler clearing its `pendingVersionChanges`
11047
+ // entry in the meantime -- which would make this method incorrectly
11048
+ // reflect a since-committed change that hadn't committed yet when
11049
+ // it was actually called.
11050
+ const pendingVersionChangesSnapshot = new Map(pendingVersionChanges);
10949
11051
  return new Promise(function (resolve, reject) {
10950
11052
  // eslint-disable-line promise/avoid-new -- Own polyfill
10951
11053
  if (!(me instanceof IDBFactory)) {
@@ -10975,11 +11077,30 @@ IDBFactory.prototype.databases = function () {
10975
11077
  const dbNames = [];
10976
11078
  for (let i = 0; i < data.rows.length; i++) {
10977
11079
  const {
10978
- name,
11080
+ name: encodedName,
10979
11081
  version
10980
11082
  } = /** @type {{name: string, version: Integer}} */data.rows.item(i);
11083
+ const name = unescapeSQLiteResponse(encodedName);
11084
+ // A row for a database whose creation/upgrade hasn't
11085
+ // committed yet (see `pendingVersionChanges`) must
11086
+ // not reflect that in-flight change: a brand new
11087
+ // database (`oldVersion === 0`) isn't reported at
11088
+ // all until its creation commits, and an existing
11089
+ // database being upgraded is still reported, but
11090
+ // with its pre-upgrade version.
11091
+ const pending = pendingVersionChangesSnapshot.get(name);
11092
+ if (pending) {
11093
+ if (pending.oldVersion === 0) {
11094
+ continue;
11095
+ }
11096
+ dbNames.push({
11097
+ name,
11098
+ version: pending.oldVersion
11099
+ });
11100
+ continue;
11101
+ }
10981
11102
  dbNames.push({
10982
- name: unescapeSQLiteResponse(name),
11103
+ name,
10983
11104
  version
10984
11105
  });
10985
11106
  }
@@ -11392,9 +11513,26 @@ IDBCursor.prototype.__findBasic = function (key, primaryKey, tx, success, error,
11392
11513
  // Key.convertValueToKey(key); // Already checked by `continue` or `continuePrimaryKey`
11393
11514
  sqlValues.push(/** @type {string} */encode$1(key));
11394
11515
  } else if (continueCall && me.__key !== undefined) {
11395
- sql.push('AND', quotedKeyColumnName, op + ' ?');
11396
11516
  // Key.convertValueToKey(me.__key); // Already checked when stored
11397
- sqlValues.push(/** @type {string} */encode$1(me.__key));
11517
+ if (!me.__unique && me.__keyColumnName !== 'key' && me.__primaryKey !== undefined) {
11518
+ // A plain `continue()` on a non-unique index cursor must find
11519
+ // the next record strictly after the (key, primaryKey) pair
11520
+ // this cursor last returned -- a scalar `key > lastKey` alone
11521
+ // would wrongly exclude a *different*, not-yet-visited record
11522
+ // that's still tied on key with the last one (e.g. another
11523
+ // record that always had that same indexed value), and would
11524
+ // also wrongly re-admit the *same* record forever if a
11525
+ // same-transaction `update()` bumped its own key back above
11526
+ // the threshold, since a scalar comparison can't distinguish
11527
+ // "some other record newly tied" from "this record moved
11528
+ // past its own last position." Comparing the full tuple (via
11529
+ // this OR) against both key and primary key resolves both.
11530
+ sql.push('AND (', quotedKeyColumnName, op, '?', 'OR (', quotedKeyColumnName, '= ?', 'AND', quotedKey, op, '?))');
11531
+ sqlValues.push(/** @type {string} */encode$1(me.__key), /** @type {string} */encode$1(me.__key), /** @type {string} */encode$1(me.__primaryKey));
11532
+ } else {
11533
+ sql.push('AND', quotedKeyColumnName, op + ' ?');
11534
+ sqlValues.push(/** @type {string} */encode$1(me.__key));
11535
+ }
11398
11536
  }
11399
11537
  if (!me.__count) {
11400
11538
  // 1. Sort by key
@@ -12008,9 +12146,15 @@ IDBCursor.prototype.update = function (valueToUpdate) {
12008
12146
  * @returns {void}
12009
12147
  */
12010
12148
  function addToQueue(clonedValue) {
12011
- // We set the `invalidateCache` argument to `false` since the old value shouldn't be accessed
12149
+ // `invalidateCache: true` so any cursor on this store (including
12150
+ // this one) drops its prefetched row batch, forcing its next
12151
+ // `continue()` to re-query live rather than serve rows snapshotted
12152
+ // before this update -- needed for the compound-tuple
12153
+ // continuation logic in `__findBasic` to see this update's
12154
+ // effect on ordering (see "Modify records during cursor
12155
+ // iteration" in idbcursor_update_index.any.js).
12012
12156
  // @ts-ignore -- API (not erring in TS 6)
12013
- IDBObjectStore.__storingRecordObjectStore(request, me.__store, false, clonedValue, false, key);
12157
+ IDBObjectStore.__storingRecordObjectStore(request, me.__store, true, clonedValue, false, key);
12014
12158
  }
12015
12159
  if (me.__store.keyPath !== null) {
12016
12160
  const [evaluatedKey, clonedValue] = me.__store.__validateKeyAndValueAndCloneValue(valueToUpdate, undefined, true);
@@ -13132,11 +13276,9 @@ function runBatch(self, batch) {
13132
13276
  self._running = false;
13133
13277
  runAllSql(self);
13134
13278
  }
13135
- const currentTask = /** @type {import('./WebSQLDatabase.js').TransactionTask} */
13136
- self._websqlDatabase._currentTask;
13137
13279
  const {
13138
13280
  readOnly
13139
- } = currentTask;
13281
+ } = self._task;
13140
13282
  self._websqlDatabase._db.exec(batch, readOnly, function (err, results) {
13141
13283
  /* c8 ignore next */
13142
13284
  if (err || !results) {
@@ -13224,10 +13366,12 @@ function executeSql(self, sql, args, sqlCallback, sqlErrorCallback, executeDelay
13224
13366
  class WebSQLTransaction {
13225
13367
  /**
13226
13368
  * @param {import('./WebSQLDatabase.js').default} websqlDatabase
13369
+ * @param {import('./WebSQLDatabase.js').TransactionTask} task
13227
13370
  * @param {import('../types.js').Delay} [executeDelay]
13228
13371
  */
13229
- constructor(websqlDatabase, executeDelay) {
13372
+ constructor(websqlDatabase, task, executeDelay) {
13230
13373
  this._websqlDatabase = websqlDatabase;
13374
+ this._task = task;
13231
13375
  /** @type {Error | null} */
13232
13376
  this._error = null;
13233
13377
  this._complete = false;
@@ -13236,11 +13380,14 @@ class WebSQLTransaction {
13236
13380
  this._executeDelay = executeDelay || immediate;
13237
13381
  /** @type {import('tiny-queue').default<SQLTask>} */
13238
13382
  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.
13383
+ if (!task.readOnly) {
13384
+ // A read-only task never needs a transaction wrapper for its own
13385
+ // sake (reads don't need atomicity against each other) -- true
13386
+ // whether or not `concurrentReaders` (see `WebSQLDatabase`) lets
13387
+ // multiple read-only tasks actually run at once. This is a perf
13388
+ // boost either way. A non-read-only task always still gets one, and
13389
+ // always still runs with full exclusivity (see
13390
+ // `WebSQLDatabase#runNextTransaction`).
13244
13391
  this._sqlQueue.push(new SQLTask('BEGIN;', [], noop, noop));
13245
13392
  }
13246
13393
  }
@@ -13321,39 +13468,68 @@ class WebSQLDatabase {
13321
13468
  this._db = db;
13322
13469
  /** @type {import('tiny-queue').default<TransactionTask>} */
13323
13470
  this._txnQueue = new Queue();
13324
- this._running = false;
13471
+ // Off by default: queued transactions -- read or write alike -- run
13472
+ // strictly one at a time, in the order requested, per the WebSQL spec
13473
+ // (see `#runNextTransaction`). When true, any number of read-only
13474
+ // tasks may instead run concurrently; a non-read-only task still
13475
+ // always runs with full exclusivity either way.
13476
+ this._concurrentReaders = Boolean(webSQLOverrides.concurrentReaders);
13477
+ /** @type {Set<TransactionTask>} */
13478
+ this._activeReaders = new Set();
13325
13479
  /** @type {TransactionTask | null} */
13326
- this._currentTask = null;
13480
+ this._activeWriter = null;
13327
13481
  this._transactionDelay = webSQLOverrides.transactionDelay || immediate;
13328
13482
  this._executeDelay = webSQLOverrides.executeDelay || immediate;
13329
13483
  }
13330
13484
 
13331
13485
  /**
13332
- *
13486
+ * @param {TransactionTask} task
13333
13487
  */
13334
- #runTransaction() {
13335
- const txn = new WebSQLTransaction(this, this._executeDelay);
13488
+ #runTransaction(task) {
13489
+ const txn = new WebSQLTransaction(this, task, this._executeDelay);
13336
13490
  this._transactionDelay(() => {
13337
- const currentTask = /** @type {TransactionTask} */this._currentTask;
13338
- currentTask.txnCallback(txn);
13491
+ task.txnCallback(txn);
13339
13492
  txn._checkDone();
13340
13493
  });
13341
13494
  }
13342
13495
 
13343
13496
  /**
13344
- *
13497
+ * Starts as many queued tasks as the current lock state allows. With
13498
+ * `concurrentReaders` off (the default), a task only starts once
13499
+ * nothing else is active at all -- full mutual exclusion, matching the
13500
+ * WebSQL spec's strict one-at-a-time, in-request-order guarantee (see
13501
+ * this file's own test suite, "callback order 2"). With it on, any
13502
+ * leading run of read-only tasks can all start together instead
13503
+ * (concurrent reads are always fine); a non-read-only task still needs
13504
+ * exclusivity regardless, so nothing past it may start until it
13505
+ * finishes. Not starvation-proof in the `concurrentReaders` case -- a
13506
+ * steady stream of arriving readers could in principle keep a waiting
13507
+ * writer waiting indefinitely -- but that's an acceptable tradeoff over
13508
+ * the added complexity of tracking arrival order across reader/writer
13509
+ * kinds.
13345
13510
  */
13346
13511
  #runNextTransaction() {
13347
- if (this._running) {
13348
- return;
13349
- }
13350
- const task = this._txnQueue.shift();
13351
- if (!task) {
13352
- return;
13512
+ for (;;) {
13513
+ const [nextTask] = this._txnQueue.slice(0, 1);
13514
+ if (!nextTask) {
13515
+ return;
13516
+ }
13517
+ const readerMayShare = this._concurrentReaders && nextTask.readOnly;
13518
+ if (readerMayShare) {
13519
+ if (this._activeWriter) {
13520
+ return;
13521
+ }
13522
+ } else if (this._activeWriter || this._activeReaders.size) {
13523
+ return;
13524
+ }
13525
+ this._txnQueue.shift();
13526
+ if (nextTask.readOnly) {
13527
+ this._activeReaders.add(nextTask);
13528
+ } else {
13529
+ this._activeWriter = nextTask;
13530
+ }
13531
+ this.#runTransaction(nextTask);
13353
13532
  }
13354
- this._currentTask = task;
13355
- this._running = true;
13356
- this.#runTransaction();
13357
13533
  }
13358
13534
 
13359
13535
  /**
@@ -13381,6 +13557,7 @@ class WebSQLDatabase {
13381
13557
  */
13382
13558
  // eslint-disable-next-line unicorn/prefer-private-class-fields -- see above
13383
13559
  _onTransactionComplete(err, transaction) {
13560
+ const task = transaction._task;
13384
13561
  /**
13385
13562
  * @param {Error | boolean | null} [er]
13386
13563
  */
@@ -13395,14 +13572,15 @@ class WebSQLDatabase {
13395
13572
  transaction._complete = true;
13396
13573
  }
13397
13574
  if (er) {
13398
- if (this._currentTask) {
13399
- this._currentTask.errorCallback(/** @type {Error} */er);
13400
- }
13401
- } else if (this._currentTask) {
13402
- this._currentTask.successCallback();
13575
+ task.errorCallback(/** @type {Error} */er);
13576
+ } else {
13577
+ task.successCallback();
13578
+ }
13579
+ if (task.readOnly) {
13580
+ this._activeReaders.delete(task);
13581
+ } else if (this._activeWriter === task) {
13582
+ this._activeWriter = null;
13403
13583
  }
13404
- this._running = false;
13405
- this._currentTask = null;
13406
13584
  this.#runNextTransaction();
13407
13585
  };
13408
13586
  /**
@@ -13428,13 +13606,13 @@ class WebSQLDatabase {
13428
13606
  }
13429
13607
  });
13430
13608
  };
13431
- if (this._currentTask && this._currentTask.nonstandardTransCb) {
13432
- const cont = this._currentTask.nonstandardTransCb.call(this, this._currentTask, err, done, rollback, commit);
13609
+ if (task.nonstandardTransCb) {
13610
+ const cont = task.nonstandardTransCb.call(this, task, err, done, rollback, commit);
13433
13611
  if (!cont) {
13434
13612
  return;
13435
13613
  }
13436
13614
  }
13437
- if (this._currentTask && this._currentTask.readOnly) {
13615
+ if (task.readOnly) {
13438
13616
  done(err); // read-only doesn't require a transaction
13439
13617
  } else if (err) {
13440
13618
  rollback(err);
@@ -14392,16 +14570,28 @@ const READ_ONLY_ERROR = new Error('could not prepare statement (23 not authorize
14392
14570
  // connection and a newly-opened one during an upgrade. `PRAGMA
14393
14571
  // busy_timeout` can't safely arbitrate between them here: it blocks the
14394
14572
  // 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.
14573
+ // own release is itself scheduled via `setImmediate` below, which can
14574
+ // never fire while that retry loop is blocking the same thread -- so
14575
+ // instead of waiting, a second connection's write just fails with
14576
+ // "database is locked".
14577
+ //
14578
+ // `fileReaders`/`fileWriter` implement a simple (not starvation-proof --
14579
+ // see the release loop below) reader/writer lock per file path instead:
14580
+ // any number of `readonly` transactions may hold the file concurrently,
14581
+ // matching what SQLite itself already natively supports (multiple
14582
+ // readers never need to exclude each other, only a writer needs
14583
+ // exclusivity), while a non-`readonly` transaction still waits for
14584
+ // exclusive access -- no concurrent readers, no concurrent writer. A
14585
+ // simple single-owner mutex (this file's prior implementation) would
14586
+ // otherwise serialize even purely-concurrent-reader scenarios that
14587
+ // never touch a writer at all, for no reason `better-sqlite3`/SQLite
14588
+ // itself requires.
14589
+ /** @type {Map<string, Set<{_db: any, _qFilePath: string}>>} */
14590
+ const fileReaders = new Map();
14401
14591
  /** @type {Map<string, {_db: any, _qFilePath: string}>} */
14402
- const fileLockOwners = new Map();
14403
- /** @type {Map<string, (() => void)[]>} */
14404
- const fileLockWaiters = new Map();
14592
+ const fileWriter = new Map();
14593
+ /** @type {Map<string, {isReader: boolean, resume: () => void}[]>} */
14594
+ const fileWaiters = new Map();
14405
14595
  const beginRe = /^\s*BEGIN\b/iu;
14406
14596
  const endRe = /^\s*(END|COMMIT|ROLLBACK)\b/iu;
14407
14597
 
@@ -14513,14 +14703,29 @@ SQLiteDatabase.prototype.exec = function exec(queries, readOnly, callback) {
14513
14703
  // already fully independent -- so there is nothing to serialize.
14514
14704
  const filePath = this._qFilePath === ':memory:' ? null : this._qFilePath;
14515
14705
  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);
14706
+ const activeReaders = fileReaders.get(filePath);
14707
+ const hasActiveReaders = Boolean(activeReaders && activeReaders.size);
14708
+ const activeWriter = fileWriter.get(filePath);
14709
+ const blockedByWriter = Boolean(activeWriter && activeWriter !== this);
14710
+ const blocked = blockedByWriter || !readOnly && hasActiveReaders;
14711
+ if (blocked) {
14712
+ const waiters = fileWaiters.get(filePath) || [];
14713
+ waiters.push({
14714
+ isReader: readOnly,
14715
+ resume: () => this.exec(queries, readOnly, callback)
14716
+ });
14717
+ fileWaiters.set(filePath, waiters);
14521
14718
  return;
14522
14719
  }
14523
- fileLockOwners.set(filePath, this);
14720
+ if (readOnly) {
14721
+ if (activeReaders) {
14722
+ activeReaders.add(this);
14723
+ } else {
14724
+ fileReaders.set(filePath, new Set([this]));
14725
+ }
14726
+ } else {
14727
+ fileWriter.set(filePath, this);
14728
+ }
14524
14729
  }
14525
14730
  const db = this._db._db;
14526
14731
  const len = queries.length;
@@ -14562,13 +14767,15 @@ SQLiteDatabase.prototype.exec = function exec(queries, readOnly, callback) {
14562
14767
  }
14563
14768
  }
14564
14769
  }
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
14770
+ // A real macrotask (not `queueMicrotask`) so this yields properly:
14771
+ // code that synchronously issues a new request from within each
14772
+ // request's callback (e.g. to keep a transaction alive) would
14568
14773
  // otherwise chain microtask to microtask forever, starving out any
14569
14774
  // `setTimeout`-based code (including IndexedDB's own internal request
14570
- // scheduling) that never gets a turn to run.
14571
- setTimeout(() => {
14775
+ // scheduling) that never gets a turn to run. `setImmediate` (Node's
14776
+ // "check" phase) still yields the same way `setTimeout(..., 0)` did,
14777
+ // but runs sooner in Node's event loop.
14778
+ setImmediate(() => {
14572
14779
  // Release the file lock (if held) and hand it to the next waiting
14573
14780
  // connection, if any, only once this transaction has genuinely
14574
14781
  // finished -- and only here, on its own turn, so a resumed
@@ -14583,15 +14790,52 @@ SQLiteDatabase.prototype.exec = function exec(queries, readOnly, callback) {
14583
14790
  // and never see the matching release, deadlocking every later
14584
14791
  // connection to the same file.
14585
14792
  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();
14793
+ const activeReaders = fileReaders.get(filePath);
14794
+ if (activeReaders) {
14795
+ activeReaders.delete(this);
14796
+ if (!activeReaders.size) {
14797
+ fileReaders.delete(filePath);
14798
+ }
14799
+ }
14800
+ if (fileWriter.get(filePath) === this) {
14801
+ fileWriter.delete(filePath);
14802
+ }
14803
+ // Resume as many waiters as the lock now genuinely allows:
14804
+ // any leading run of readers (concurrent reads are always
14805
+ // fine), then at most one writer, since a writer needs
14806
+ // exclusivity -- stop there rather than looking past it.
14807
+ // Not starvation-proof: a steady stream of arriving readers
14808
+ // could in principle keep a waiting writer waiting
14809
+ // indefinitely, but that's an acceptable tradeoff here over
14810
+ // the added complexity of tracking arrival order across
14811
+ // reader/writer kinds.
14812
+ const waiters = fileWaiters.get(filePath);
14813
+ if (waiters) {
14814
+ while (waiters.length) {
14815
+ if (fileWriter.has(filePath)) {
14816
+ break;
14817
+ }
14818
+ const next = waiters[0];
14819
+ const stillHasReaders = fileReaders.get(filePath);
14820
+ if (!next.isReader && stillHasReaders && stillHasReaders.size) {
14821
+ break;
14822
+ }
14823
+ waiters.shift();
14824
+ next.resume();
14825
+ if (!next.isReader) {
14826
+ // A writer just took exclusive ownership (inside
14827
+ // `resume()`, synchronously, via the acquire
14828
+ // logic above) -- nothing else may proceed now.
14829
+ break;
14830
+ }
14831
+ }
14832
+ if (!waiters.length) {
14833
+ fileWaiters.delete(filePath);
14834
+ }
14591
14835
  }
14592
14836
  }
14593
14837
  callback(null, results);
14594
- }, 0);
14838
+ });
14595
14839
  };
14596
14840
 
14597
14841
  /**
@@ -14631,7 +14875,19 @@ function wrappedSQLiteDatabase(name) {
14631
14875
  }
14632
14876
  return db;
14633
14877
  }
14634
- const nodeWebSQL = customOpenDatabase(/** @type {SQLiteDatabaseConstructor} */ /** @type {unknown} */wrappedSQLiteDatabase, {});
14878
+
14879
+ // `concurrentReaders` is off by default in `websql-configurable` itself (to
14880
+ // preserve the WebSQL spec's strict, one-at-a-time transaction ordering
14881
+ // that library's own test suite depends on), but IndexedDBShim only ever
14882
+ // uses it as an internal SQL execution engine -- it doesn't need or expose
14883
+ // that ordering guarantee itself -- so it's safe, and needed, to opt in
14884
+ // here: without it, two same-scope `readonly` IDBTransactions can deadlock
14885
+ // waiting on each other (see `transaction-scheduling-within-database.any.js`).
14886
+ const nodeWebSQL = customOpenDatabase(/** @type {SQLiteDatabaseConstructor} */ /** @type {unknown} */wrappedSQLiteDatabase, {
14887
+ websql: {
14888
+ concurrentReaders: true
14889
+ }
14890
+ });
14635
14891
 
14636
14892
  // ID_Start (includes Other_ID_Start)
14637
14893
  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])`;