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
  (function (factory) {
4
4
  typeof define === 'function' && define.amd ? define(factory) :
@@ -6148,9 +6148,15 @@
6148
6148
  if (err !== null) {
6149
6149
  me.__error = err;
6150
6150
  }
6151
- if (me.__requestsFinished) {
6151
+ if (me.__requestsFinished && err !== null) {
6152
6152
  // The transaction has already completed, so we can't call "onerror" or "onabort".
6153
- // So throw the error instead.
6153
+ // So throw the error instead. `err` is only ever `null` here via
6154
+ // `IDBTransaction.prototype.abort`'s own `__abortTransaction(null)`
6155
+ // call, which now checks `__requestsFinished` itself first and
6156
+ // throws `InvalidStateError` synchronously before ever reaching
6157
+ // this point -- so this guard is just defense in depth against
6158
+ // `err` somehow being `null` some other way, not something this
6159
+ // path should see in practice.
6154
6160
  setTimeout(function () {
6155
6161
  throw err;
6156
6162
  }, 0);
@@ -6270,6 +6276,14 @@
6270
6276
  if (me.__committed) {
6271
6277
  throw createDOMException('InvalidStateError', 'The transaction has already been committed');
6272
6278
  }
6279
+ if (me.__requestsFinished) {
6280
+ // All requests have already finished and the transaction is
6281
+ // auto-committing (the async SQL commit round trip just hasn't
6282
+ // resolved yet) -- too late to abort per spec, even though
6283
+ // `__committed` itself isn't set until that round trip actually
6284
+ // finishes.
6285
+ throw createDOMException('InvalidStateError', 'The transaction is already committing');
6286
+ }
6273
6287
  me.__abortTransaction(null);
6274
6288
  };
6275
6289
 
@@ -11084,7 +11098,29 @@
11084
11098
  return new SyncPromise(function (resolve) {
11085
11099
  setTimeout(function () {
11086
11100
  entry.dispatchEvent(e); // No need to catch errors
11087
- resolve(undefined);
11101
+ // Unlike a native `Promise`, `SyncPromise#then` chains
11102
+ // synchronously off `resolve()` (verified directly:
11103
+ // `SyncPromise.resolve().then(cb)` runs `cb` before
11104
+ // even the calling code below it finishes) -- so
11105
+ // resolving immediately here would let the
11106
+ // `connectionsClosed()` check below run before a
11107
+ // same-task-but-microtask-later continuation of a
11108
+ // `versionchange` listener above (e.g. an `await`-
11109
+ // based one that then calls `db.close()`, as in
11110
+ // `transaction-lifetime.any.js`) ever gets a turn,
11111
+ // incorrectly firing `blocked` even though the
11112
+ // connection was about to close in time. Give real
11113
+ // microtask-deferred continuations a small, bounded
11114
+ // number of turns first -- same pattern as
11115
+ // `IDBTransaction.js`'s `checkQueueEntry`.
11116
+ var attemptsLeft = 10;
11117
+ (function wait() {
11118
+ if (attemptsLeft-- <= 0) {
11119
+ resolve(undefined);
11120
+ return;
11121
+ }
11122
+ queueMicrotask(wait);
11123
+ })();
11088
11124
  }, 0);
11089
11125
  });
11090
11126
  });
@@ -11144,6 +11180,23 @@
11144
11180
  */
11145
11181
  var websqlDBCache = {};
11146
11182
 
11183
+ /**
11184
+ * Tracks databases with a creation/upgrade currently in flight but not yet
11185
+ * committed or aborted -- keyed by (unescaped) database name. The
11186
+ * `dbVersions` row in `sysdb` these entries shadow is written *before*
11187
+ * `upgradeneeded` is even dispatched to user code (as the success
11188
+ * callback of that very `INSERT`/`UPDATE`), on the same shared `sysdb`
11189
+ * connection `databases()` itself reads from -- so, absent this, a
11190
+ * `databases()` call made while an upgrade is still pending would see
11191
+ * the new row (or new version) immediately, rather than only once the
11192
+ * corresponding `versionchange` transaction has genuinely committed, as
11193
+ * required by `get-databases.any.js`'s "doesn't pick up changes that
11194
+ * haven't committed" test. `IDBFactory.prototype.databases` filters
11195
+ * its SQL results against this map.
11196
+ * @type {Map<string, {oldVersion: Integer, newVersion: Integer}>}
11197
+ */
11198
+ var pendingVersionChanges = new Map();
11199
+
11147
11200
  /** @type {import('websql-configurable/lib/websql/WebSQLDatabase.js').default} */
11148
11201
  var sysdb;
11149
11202
  var nameCounter = 0;
@@ -11446,6 +11499,19 @@
11446
11499
  if (calledDbCreateError) {
11447
11500
  return false;
11448
11501
  }
11502
+ // Defensive cleanup: `pendingVersionChanges.set(name, ...)` (see
11503
+ // below) runs *before* the `dbVersions` `INSERT`/`UPDATE` it
11504
+ // guards is even attempted, but only `versionSet`'s own
11505
+ // `on__beforecomplete`/`on__preabort` handlers ever clear it --
11506
+ // and `versionSet` never runs at all if that `INSERT`/`UPDATE`,
11507
+ // or an earlier step in this same `open()` flow, fails first.
11508
+ // Without this, such a failure would leave the entry orphaned
11509
+ // forever, silently corrupting `databases()` for any *other*,
11510
+ // unrelated later `open()` call that happens to reuse the same
11511
+ // database name (common in WPT tests, e.g. generic names like
11512
+ // "DB1"/"TestDatabase" reused across different test files in
11513
+ // the same process). A no-op if no entry was ever set.
11514
+ pendingVersionChanges.delete(name);
11449
11515
  var er = err ? webSQLErrback(err) : (/** @type {Error} */tx);
11450
11516
  calledDbCreateError = true;
11451
11517
  // Re: why bubbling here (and how cancelable is only really relevant for `window.onerror`) see: https://github.com/w3c/IndexedDB/issues/86
@@ -11557,22 +11623,43 @@
11557
11623
  // open/close the transaction's active-handler window itself.
11558
11624
  req.transaction.__handlerActive = true;
11559
11625
  req.dispatchEvent(e);
11560
- // Give any same-tick microtask scheduled from within the
11626
+ // Give any microtask-scheduled continuation of the
11561
11627
  // `upgradeneeded` handler (e.g. a plain
11562
- // `Promise.resolve().then(...)`) a chance to run -- and still
11563
- // observe the transaction as active -- before we deactivate it
11564
- // again, per https://github.com/w3c/IndexedDB/issues/87. Only
11565
- // the flag reset itself is deferred here -- unlike
11566
- // `IDBTransaction.js`'s `advanceAfterDispatch`, `finished()`
11567
- // (this transaction's own queue-advancement/completion signal)
11568
- // still runs synchronously, at exactly its previous timing: an
11569
- // earlier attempt at deferring `finished()` too raced against
11570
- // unrelated test setup that assumed a freshly deleted/created
11571
- // database's upgrade transaction had already fully completed by
11572
- // the time this function returns.
11573
- queueMicrotask(function () {
11574
- req.transaction.__handlerActive = false;
11575
- });
11628
+ // `Promise.resolve().then(...)`, or an `await`-based
11629
+ // continuation of a promise resolved from within the handler,
11630
+ // such as testharness.js's own `EventWatcher`) a chance to
11631
+ // run -- and still observe the transaction as active -- before
11632
+ // we deactivate it again, per
11633
+ // https://github.com/w3c/IndexedDB/issues/87. A single deferred
11634
+ // tick isn't always enough: an `await`-based continuation can
11635
+ // take more than one microtask turn to resume (e.g.
11636
+ // `transaction-lifetime.any.js`'s `EventWatcher`-based
11637
+ // `await eventWatcher.wait_for('upgradeneeded')`), so retry a
11638
+ // small, bounded number of times first -- same pattern as
11639
+ // `IDBTransaction.js`'s `checkQueueEntry` -- rather than
11640
+ // resetting after only one. Only the flag reset itself is
11641
+ // deferred here -- unlike `IDBTransaction.js`'s
11642
+ // `advanceAfterDispatch`, `finished()` (this transaction's own
11643
+ // queue-advancement/completion signal) still runs
11644
+ // synchronously, at exactly its previous timing: an earlier
11645
+ // attempt at deferring `finished()` too raced against unrelated
11646
+ // test setup that assumed a freshly deleted/created database's
11647
+ // upgrade transaction had already fully completed by the time
11648
+ // this function returns.
11649
+ /**
11650
+ * @param {Integer} attemptsLeft
11651
+ * @returns {void}
11652
+ */
11653
+ function deferHandlerActiveReset(attemptsLeft) {
11654
+ if (attemptsLeft <= 0) {
11655
+ req.transaction.__handlerActive = false;
11656
+ return;
11657
+ }
11658
+ queueMicrotask(function () {
11659
+ deferHandlerActiveReset(attemptsLeft - 1);
11660
+ });
11661
+ }
11662
+ deferHandlerActiveReset(10);
11576
11663
  if (e.__legacyOutputDidListenersThrowError) {
11577
11664
  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
11578
11665
  req.transaction.__abortTransaction(createDOMException('AbortError', 'A request was aborted.'));
@@ -11588,6 +11675,7 @@
11588
11675
  * @returns {void}
11589
11676
  */
11590
11677
  function (ev) {
11678
+ pendingVersionChanges.delete(name);
11591
11679
  connection.__upgradeTransaction = null;
11592
11680
  /** @type {import('./IDBDatabase.js').IDBDatabaseFull} */
11593
11681
  req.__result.__versionTransaction = null;
@@ -11602,6 +11690,7 @@
11602
11690
 
11603
11691
  // eslint-disable-next-line camelcase -- Clear API
11604
11692
  req.transaction.on__preabort = function () {
11693
+ pendingVersionChanges.delete(name);
11605
11694
  connection.__upgradeTransaction = null;
11606
11695
  // We ensure any cache is deleted before any request error events fire and try to reopen
11607
11696
  if (useDatabaseCache) {
@@ -11664,6 +11753,10 @@
11664
11753
  // });
11665
11754
  };
11666
11755
  }
11756
+ pendingVersionChanges.set(name, {
11757
+ oldVersion: oldVersion,
11758
+ newVersion: version
11759
+ });
11667
11760
  if (oldVersion === 0) {
11668
11761
  systx.executeSql('INSERT INTO dbVersions VALUES (?,?)', [sqlSafeName, version], versionSet, dbCreateError);
11669
11762
  } else {
@@ -11966,6 +12059,15 @@
11966
12059
  IDBFactory.prototype.databases = function () {
11967
12060
  var me = this;
11968
12061
  var calledDbCreateError = false;
12062
+ // Snapshotted *now*, synchronously, at call time -- not read later
12063
+ // from inside the SQL query's callback below, which runs on a
12064
+ // deferred macrotask (see `nodeSQLiteDatabase.js`'s `exec`) and so
12065
+ // could otherwise race against (and lose to) an in-flight upgrade's
12066
+ // own commit/abort handler clearing its `pendingVersionChanges`
12067
+ // entry in the meantime -- which would make this method incorrectly
12068
+ // reflect a since-committed change that hadn't committed yet when
12069
+ // it was actually called.
12070
+ var pendingVersionChangesSnapshot = new Map(pendingVersionChanges);
11969
12071
  return new Promise(function (resolve, reject) {
11970
12072
  // eslint-disable-line promise/avoid-new -- Own polyfill
11971
12073
  if (!(me instanceof IDBFactory)) {
@@ -11995,10 +12097,29 @@
11995
12097
  var dbNames = [];
11996
12098
  for (var i = 0; i < data.rows.length; i++) {
11997
12099
  var _data$rows$item2 = /** @type {{name: string, version: Integer}} */data.rows.item(i),
11998
- name = _data$rows$item2.name,
12100
+ encodedName = _data$rows$item2.name,
11999
12101
  version = _data$rows$item2.version;
12102
+ var name = unescapeSQLiteResponse(encodedName);
12103
+ // A row for a database whose creation/upgrade hasn't
12104
+ // committed yet (see `pendingVersionChanges`) must
12105
+ // not reflect that in-flight change: a brand new
12106
+ // database (`oldVersion === 0`) isn't reported at
12107
+ // all until its creation commits, and an existing
12108
+ // database being upgraded is still reported, but
12109
+ // with its pre-upgrade version.
12110
+ var pending = pendingVersionChangesSnapshot.get(name);
12111
+ if (pending) {
12112
+ if (pending.oldVersion === 0) {
12113
+ continue;
12114
+ }
12115
+ dbNames.push({
12116
+ name: name,
12117
+ version: pending.oldVersion
12118
+ });
12119
+ continue;
12120
+ }
12000
12121
  dbNames.push({
12001
- name: unescapeSQLiteResponse(name),
12122
+ name: name,
12002
12123
  version: version
12003
12124
  });
12004
12125
  }
@@ -12418,9 +12539,26 @@
12418
12539
  // Key.convertValueToKey(key); // Already checked by `continue` or `continuePrimaryKey`
12419
12540
  sqlValues.push(/** @type {string} */_encode(key));
12420
12541
  } else if (continueCall && me.__key !== undefined) {
12421
- sql.push('AND', quotedKeyColumnName, op + ' ?');
12422
12542
  // Key.convertValueToKey(me.__key); // Already checked when stored
12423
- sqlValues.push(/** @type {string} */_encode(me.__key));
12543
+ if (!me.__unique && me.__keyColumnName !== 'key' && me.__primaryKey !== undefined) {
12544
+ // A plain `continue()` on a non-unique index cursor must find
12545
+ // the next record strictly after the (key, primaryKey) pair
12546
+ // this cursor last returned -- a scalar `key > lastKey` alone
12547
+ // would wrongly exclude a *different*, not-yet-visited record
12548
+ // that's still tied on key with the last one (e.g. another
12549
+ // record that always had that same indexed value), and would
12550
+ // also wrongly re-admit the *same* record forever if a
12551
+ // same-transaction `update()` bumped its own key back above
12552
+ // the threshold, since a scalar comparison can't distinguish
12553
+ // "some other record newly tied" from "this record moved
12554
+ // past its own last position." Comparing the full tuple (via
12555
+ // this OR) against both key and primary key resolves both.
12556
+ sql.push('AND (', quotedKeyColumnName, op, '?', 'OR (', quotedKeyColumnName, '= ?', 'AND', quotedKey, op, '?))');
12557
+ sqlValues.push(/** @type {string} */_encode(me.__key), /** @type {string} */_encode(me.__key), /** @type {string} */_encode(me.__primaryKey));
12558
+ } else {
12559
+ sql.push('AND', quotedKeyColumnName, op + ' ?');
12560
+ sqlValues.push(/** @type {string} */_encode(me.__key));
12561
+ }
12424
12562
  }
12425
12563
  if (!me.__count) {
12426
12564
  // 1. Sort by key
@@ -13050,9 +13188,15 @@
13050
13188
  * @returns {void}
13051
13189
  */
13052
13190
  function addToQueue(clonedValue) {
13053
- // We set the `invalidateCache` argument to `false` since the old value shouldn't be accessed
13191
+ // `invalidateCache: true` so any cursor on this store (including
13192
+ // this one) drops its prefetched row batch, forcing its next
13193
+ // `continue()` to re-query live rather than serve rows snapshotted
13194
+ // before this update -- needed for the compound-tuple
13195
+ // continuation logic in `__findBasic` to see this update's
13196
+ // effect on ordering (see "Modify records during cursor
13197
+ // iteration" in idbcursor_update_index.any.js).
13054
13198
  // @ts-ignore -- API (not erring in TS 6)
13055
- IDBObjectStore.__storingRecordObjectStore(request, me.__store, false, clonedValue, false, key);
13199
+ IDBObjectStore.__storingRecordObjectStore(request, me.__store, true, clonedValue, false, key);
13056
13200
  }
13057
13201
  if (me.__store.keyPath !== null) {
13058
13202
  var _me$__store$__validat = me.__store.__validateKeyAndValueAndCloneValue(valueToUpdate, undefined, true),