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 (global, factory) {
4
4
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
@@ -6156,9 +6156,15 @@
6156
6156
  if (err !== null) {
6157
6157
  me.__error = err;
6158
6158
  }
6159
- if (me.__requestsFinished) {
6159
+ if (me.__requestsFinished && err !== null) {
6160
6160
  // The transaction has already completed, so we can't call "onerror" or "onabort".
6161
- // So throw the error instead.
6161
+ // So throw the error instead. `err` is only ever `null` here via
6162
+ // `IDBTransaction.prototype.abort`'s own `__abortTransaction(null)`
6163
+ // call, which now checks `__requestsFinished` itself first and
6164
+ // throws `InvalidStateError` synchronously before ever reaching
6165
+ // this point -- so this guard is just defense in depth against
6166
+ // `err` somehow being `null` some other way, not something this
6167
+ // path should see in practice.
6162
6168
  setTimeout(function () {
6163
6169
  throw err;
6164
6170
  }, 0);
@@ -6278,6 +6284,14 @@
6278
6284
  if (me.__committed) {
6279
6285
  throw createDOMException('InvalidStateError', 'The transaction has already been committed');
6280
6286
  }
6287
+ if (me.__requestsFinished) {
6288
+ // All requests have already finished and the transaction is
6289
+ // auto-committing (the async SQL commit round trip just hasn't
6290
+ // resolved yet) -- too late to abort per spec, even though
6291
+ // `__committed` itself isn't set until that round trip actually
6292
+ // finishes.
6293
+ throw createDOMException('InvalidStateError', 'The transaction is already committing');
6294
+ }
6281
6295
  me.__abortTransaction(null);
6282
6296
  };
6283
6297
 
@@ -11092,7 +11106,29 @@
11092
11106
  return new SyncPromise(function (resolve) {
11093
11107
  setTimeout(function () {
11094
11108
  entry.dispatchEvent(e); // No need to catch errors
11095
- resolve(undefined);
11109
+ // Unlike a native `Promise`, `SyncPromise#then` chains
11110
+ // synchronously off `resolve()` (verified directly:
11111
+ // `SyncPromise.resolve().then(cb)` runs `cb` before
11112
+ // even the calling code below it finishes) -- so
11113
+ // resolving immediately here would let the
11114
+ // `connectionsClosed()` check below run before a
11115
+ // same-task-but-microtask-later continuation of a
11116
+ // `versionchange` listener above (e.g. an `await`-
11117
+ // based one that then calls `db.close()`, as in
11118
+ // `transaction-lifetime.any.js`) ever gets a turn,
11119
+ // incorrectly firing `blocked` even though the
11120
+ // connection was about to close in time. Give real
11121
+ // microtask-deferred continuations a small, bounded
11122
+ // number of turns first -- same pattern as
11123
+ // `IDBTransaction.js`'s `checkQueueEntry`.
11124
+ var attemptsLeft = 10;
11125
+ (function wait() {
11126
+ if (attemptsLeft-- <= 0) {
11127
+ resolve(undefined);
11128
+ return;
11129
+ }
11130
+ queueMicrotask(wait);
11131
+ })();
11096
11132
  }, 0);
11097
11133
  });
11098
11134
  });
@@ -11152,6 +11188,23 @@
11152
11188
  */
11153
11189
  var websqlDBCache = {};
11154
11190
 
11191
+ /**
11192
+ * Tracks databases with a creation/upgrade currently in flight but not yet
11193
+ * committed or aborted -- keyed by (unescaped) database name. The
11194
+ * `dbVersions` row in `sysdb` these entries shadow is written *before*
11195
+ * `upgradeneeded` is even dispatched to user code (as the success
11196
+ * callback of that very `INSERT`/`UPDATE`), on the same shared `sysdb`
11197
+ * connection `databases()` itself reads from -- so, absent this, a
11198
+ * `databases()` call made while an upgrade is still pending would see
11199
+ * the new row (or new version) immediately, rather than only once the
11200
+ * corresponding `versionchange` transaction has genuinely committed, as
11201
+ * required by `get-databases.any.js`'s "doesn't pick up changes that
11202
+ * haven't committed" test. `IDBFactory.prototype.databases` filters
11203
+ * its SQL results against this map.
11204
+ * @type {Map<string, {oldVersion: Integer, newVersion: Integer}>}
11205
+ */
11206
+ var pendingVersionChanges = new Map();
11207
+
11155
11208
  /** @type {import('websql-configurable/lib/websql/WebSQLDatabase.js').default} */
11156
11209
  var sysdb;
11157
11210
  var nameCounter = 0;
@@ -11454,6 +11507,19 @@
11454
11507
  if (calledDbCreateError) {
11455
11508
  return false;
11456
11509
  }
11510
+ // Defensive cleanup: `pendingVersionChanges.set(name, ...)` (see
11511
+ // below) runs *before* the `dbVersions` `INSERT`/`UPDATE` it
11512
+ // guards is even attempted, but only `versionSet`'s own
11513
+ // `on__beforecomplete`/`on__preabort` handlers ever clear it --
11514
+ // and `versionSet` never runs at all if that `INSERT`/`UPDATE`,
11515
+ // or an earlier step in this same `open()` flow, fails first.
11516
+ // Without this, such a failure would leave the entry orphaned
11517
+ // forever, silently corrupting `databases()` for any *other*,
11518
+ // unrelated later `open()` call that happens to reuse the same
11519
+ // database name (common in WPT tests, e.g. generic names like
11520
+ // "DB1"/"TestDatabase" reused across different test files in
11521
+ // the same process). A no-op if no entry was ever set.
11522
+ pendingVersionChanges.delete(name);
11457
11523
  var er = err ? webSQLErrback(err) : (/** @type {Error} */tx);
11458
11524
  calledDbCreateError = true;
11459
11525
  // Re: why bubbling here (and how cancelable is only really relevant for `window.onerror`) see: https://github.com/w3c/IndexedDB/issues/86
@@ -11565,22 +11631,43 @@
11565
11631
  // open/close the transaction's active-handler window itself.
11566
11632
  req.transaction.__handlerActive = true;
11567
11633
  req.dispatchEvent(e);
11568
- // Give any same-tick microtask scheduled from within the
11634
+ // Give any microtask-scheduled continuation of the
11569
11635
  // `upgradeneeded` handler (e.g. a plain
11570
- // `Promise.resolve().then(...)`) a chance to run -- and still
11571
- // observe the transaction as active -- before we deactivate it
11572
- // again, per https://github.com/w3c/IndexedDB/issues/87. Only
11573
- // the flag reset itself is deferred here -- unlike
11574
- // `IDBTransaction.js`'s `advanceAfterDispatch`, `finished()`
11575
- // (this transaction's own queue-advancement/completion signal)
11576
- // still runs synchronously, at exactly its previous timing: an
11577
- // earlier attempt at deferring `finished()` too raced against
11578
- // unrelated test setup that assumed a freshly deleted/created
11579
- // database's upgrade transaction had already fully completed by
11580
- // the time this function returns.
11581
- queueMicrotask(function () {
11582
- req.transaction.__handlerActive = false;
11583
- });
11636
+ // `Promise.resolve().then(...)`, or an `await`-based
11637
+ // continuation of a promise resolved from within the handler,
11638
+ // such as testharness.js's own `EventWatcher`) a chance to
11639
+ // run -- and still observe the transaction as active -- before
11640
+ // we deactivate it again, per
11641
+ // https://github.com/w3c/IndexedDB/issues/87. A single deferred
11642
+ // tick isn't always enough: an `await`-based continuation can
11643
+ // take more than one microtask turn to resume (e.g.
11644
+ // `transaction-lifetime.any.js`'s `EventWatcher`-based
11645
+ // `await eventWatcher.wait_for('upgradeneeded')`), so retry a
11646
+ // small, bounded number of times first -- same pattern as
11647
+ // `IDBTransaction.js`'s `checkQueueEntry` -- rather than
11648
+ // resetting after only one. Only the flag reset itself is
11649
+ // deferred here -- unlike `IDBTransaction.js`'s
11650
+ // `advanceAfterDispatch`, `finished()` (this transaction's own
11651
+ // queue-advancement/completion signal) still runs
11652
+ // synchronously, at exactly its previous timing: an earlier
11653
+ // attempt at deferring `finished()` too raced against unrelated
11654
+ // test setup that assumed a freshly deleted/created database's
11655
+ // upgrade transaction had already fully completed by the time
11656
+ // this function returns.
11657
+ /**
11658
+ * @param {Integer} attemptsLeft
11659
+ * @returns {void}
11660
+ */
11661
+ function deferHandlerActiveReset(attemptsLeft) {
11662
+ if (attemptsLeft <= 0) {
11663
+ req.transaction.__handlerActive = false;
11664
+ return;
11665
+ }
11666
+ queueMicrotask(function () {
11667
+ deferHandlerActiveReset(attemptsLeft - 1);
11668
+ });
11669
+ }
11670
+ deferHandlerActiveReset(10);
11584
11671
  if (e.__legacyOutputDidListenersThrowError) {
11585
11672
  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
11586
11673
  req.transaction.__abortTransaction(createDOMException('AbortError', 'A request was aborted.'));
@@ -11596,6 +11683,7 @@
11596
11683
  * @returns {void}
11597
11684
  */
11598
11685
  function (ev) {
11686
+ pendingVersionChanges.delete(name);
11599
11687
  connection.__upgradeTransaction = null;
11600
11688
  /** @type {import('./IDBDatabase.js').IDBDatabaseFull} */
11601
11689
  req.__result.__versionTransaction = null;
@@ -11610,6 +11698,7 @@
11610
11698
 
11611
11699
  // eslint-disable-next-line camelcase -- Clear API
11612
11700
  req.transaction.on__preabort = function () {
11701
+ pendingVersionChanges.delete(name);
11613
11702
  connection.__upgradeTransaction = null;
11614
11703
  // We ensure any cache is deleted before any request error events fire and try to reopen
11615
11704
  if (useDatabaseCache) {
@@ -11672,6 +11761,10 @@
11672
11761
  // });
11673
11762
  };
11674
11763
  }
11764
+ pendingVersionChanges.set(name, {
11765
+ oldVersion: oldVersion,
11766
+ newVersion: version
11767
+ });
11675
11768
  if (oldVersion === 0) {
11676
11769
  systx.executeSql('INSERT INTO dbVersions VALUES (?,?)', [sqlSafeName, version], versionSet, dbCreateError);
11677
11770
  } else {
@@ -11974,6 +12067,15 @@
11974
12067
  IDBFactory.prototype.databases = function () {
11975
12068
  var me = this;
11976
12069
  var calledDbCreateError = false;
12070
+ // Snapshotted *now*, synchronously, at call time -- not read later
12071
+ // from inside the SQL query's callback below, which runs on a
12072
+ // deferred macrotask (see `nodeSQLiteDatabase.js`'s `exec`) and so
12073
+ // could otherwise race against (and lose to) an in-flight upgrade's
12074
+ // own commit/abort handler clearing its `pendingVersionChanges`
12075
+ // entry in the meantime -- which would make this method incorrectly
12076
+ // reflect a since-committed change that hadn't committed yet when
12077
+ // it was actually called.
12078
+ var pendingVersionChangesSnapshot = new Map(pendingVersionChanges);
11977
12079
  return new Promise(function (resolve, reject) {
11978
12080
  // eslint-disable-line promise/avoid-new -- Own polyfill
11979
12081
  if (!(me instanceof IDBFactory)) {
@@ -12003,10 +12105,29 @@
12003
12105
  var dbNames = [];
12004
12106
  for (var i = 0; i < data.rows.length; i++) {
12005
12107
  var _data$rows$item2 = /** @type {{name: string, version: Integer}} */data.rows.item(i),
12006
- name = _data$rows$item2.name,
12108
+ encodedName = _data$rows$item2.name,
12007
12109
  version = _data$rows$item2.version;
12110
+ var name = unescapeSQLiteResponse(encodedName);
12111
+ // A row for a database whose creation/upgrade hasn't
12112
+ // committed yet (see `pendingVersionChanges`) must
12113
+ // not reflect that in-flight change: a brand new
12114
+ // database (`oldVersion === 0`) isn't reported at
12115
+ // all until its creation commits, and an existing
12116
+ // database being upgraded is still reported, but
12117
+ // with its pre-upgrade version.
12118
+ var pending = pendingVersionChangesSnapshot.get(name);
12119
+ if (pending) {
12120
+ if (pending.oldVersion === 0) {
12121
+ continue;
12122
+ }
12123
+ dbNames.push({
12124
+ name: name,
12125
+ version: pending.oldVersion
12126
+ });
12127
+ continue;
12128
+ }
12008
12129
  dbNames.push({
12009
- name: unescapeSQLiteResponse(name),
12130
+ name: name,
12010
12131
  version: version
12011
12132
  });
12012
12133
  }
@@ -12426,9 +12547,26 @@
12426
12547
  // Key.convertValueToKey(key); // Already checked by `continue` or `continuePrimaryKey`
12427
12548
  sqlValues.push(/** @type {string} */_encode(key));
12428
12549
  } else if (continueCall && me.__key !== undefined) {
12429
- sql.push('AND', quotedKeyColumnName, op + ' ?');
12430
12550
  // Key.convertValueToKey(me.__key); // Already checked when stored
12431
- sqlValues.push(/** @type {string} */_encode(me.__key));
12551
+ if (!me.__unique && me.__keyColumnName !== 'key' && me.__primaryKey !== undefined) {
12552
+ // A plain `continue()` on a non-unique index cursor must find
12553
+ // the next record strictly after the (key, primaryKey) pair
12554
+ // this cursor last returned -- a scalar `key > lastKey` alone
12555
+ // would wrongly exclude a *different*, not-yet-visited record
12556
+ // that's still tied on key with the last one (e.g. another
12557
+ // record that always had that same indexed value), and would
12558
+ // also wrongly re-admit the *same* record forever if a
12559
+ // same-transaction `update()` bumped its own key back above
12560
+ // the threshold, since a scalar comparison can't distinguish
12561
+ // "some other record newly tied" from "this record moved
12562
+ // past its own last position." Comparing the full tuple (via
12563
+ // this OR) against both key and primary key resolves both.
12564
+ sql.push('AND (', quotedKeyColumnName, op, '?', 'OR (', quotedKeyColumnName, '= ?', 'AND', quotedKey, op, '?))');
12565
+ sqlValues.push(/** @type {string} */_encode(me.__key), /** @type {string} */_encode(me.__key), /** @type {string} */_encode(me.__primaryKey));
12566
+ } else {
12567
+ sql.push('AND', quotedKeyColumnName, op + ' ?');
12568
+ sqlValues.push(/** @type {string} */_encode(me.__key));
12569
+ }
12432
12570
  }
12433
12571
  if (!me.__count) {
12434
12572
  // 1. Sort by key
@@ -13058,9 +13196,15 @@
13058
13196
  * @returns {void}
13059
13197
  */
13060
13198
  function addToQueue(clonedValue) {
13061
- // We set the `invalidateCache` argument to `false` since the old value shouldn't be accessed
13199
+ // `invalidateCache: true` so any cursor on this store (including
13200
+ // this one) drops its prefetched row batch, forcing its next
13201
+ // `continue()` to re-query live rather than serve rows snapshotted
13202
+ // before this update -- needed for the compound-tuple
13203
+ // continuation logic in `__findBasic` to see this update's
13204
+ // effect on ordering (see "Modify records during cursor
13205
+ // iteration" in idbcursor_update_index.any.js).
13062
13206
  // @ts-ignore -- API (not erring in TS 6)
13063
- IDBObjectStore.__storingRecordObjectStore(request, me.__store, false, clonedValue, false, key);
13207
+ IDBObjectStore.__storingRecordObjectStore(request, me.__store, true, clonedValue, false, key);
13064
13208
  }
13065
13209
  if (me.__store.keyPath !== null) {
13066
13210
  var _me$__store$__validat = me.__store.__validateKeyAndValueAndCloneValue(valueToUpdate, undefined, true),