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) :
@@ -6161,9 +6161,15 @@
6161
6161
  if (err !== null) {
6162
6162
  me.__error = err;
6163
6163
  }
6164
- if (me.__requestsFinished) {
6164
+ if (me.__requestsFinished && err !== null) {
6165
6165
  // The transaction has already completed, so we can't call "onerror" or "onabort".
6166
- // So throw the error instead.
6166
+ // So throw the error instead. `err` is only ever `null` here via
6167
+ // `IDBTransaction.prototype.abort`'s own `__abortTransaction(null)`
6168
+ // call, which now checks `__requestsFinished` itself first and
6169
+ // throws `InvalidStateError` synchronously before ever reaching
6170
+ // this point -- so this guard is just defense in depth against
6171
+ // `err` somehow being `null` some other way, not something this
6172
+ // path should see in practice.
6167
6173
  setTimeout(function () {
6168
6174
  throw err;
6169
6175
  }, 0);
@@ -6283,6 +6289,14 @@
6283
6289
  if (me.__committed) {
6284
6290
  throw createDOMException('InvalidStateError', 'The transaction has already been committed');
6285
6291
  }
6292
+ if (me.__requestsFinished) {
6293
+ // All requests have already finished and the transaction is
6294
+ // auto-committing (the async SQL commit round trip just hasn't
6295
+ // resolved yet) -- too late to abort per spec, even though
6296
+ // `__committed` itself isn't set until that round trip actually
6297
+ // finishes.
6298
+ throw createDOMException('InvalidStateError', 'The transaction is already committing');
6299
+ }
6286
6300
  me.__abortTransaction(null);
6287
6301
  };
6288
6302
 
@@ -11097,7 +11111,29 @@
11097
11111
  return new SyncPromise(function (resolve) {
11098
11112
  setTimeout(function () {
11099
11113
  entry.dispatchEvent(e); // No need to catch errors
11100
- resolve(undefined);
11114
+ // Unlike a native `Promise`, `SyncPromise#then` chains
11115
+ // synchronously off `resolve()` (verified directly:
11116
+ // `SyncPromise.resolve().then(cb)` runs `cb` before
11117
+ // even the calling code below it finishes) -- so
11118
+ // resolving immediately here would let the
11119
+ // `connectionsClosed()` check below run before a
11120
+ // same-task-but-microtask-later continuation of a
11121
+ // `versionchange` listener above (e.g. an `await`-
11122
+ // based one that then calls `db.close()`, as in
11123
+ // `transaction-lifetime.any.js`) ever gets a turn,
11124
+ // incorrectly firing `blocked` even though the
11125
+ // connection was about to close in time. Give real
11126
+ // microtask-deferred continuations a small, bounded
11127
+ // number of turns first -- same pattern as
11128
+ // `IDBTransaction.js`'s `checkQueueEntry`.
11129
+ var attemptsLeft = 10;
11130
+ (function wait() {
11131
+ if (attemptsLeft-- <= 0) {
11132
+ resolve(undefined);
11133
+ return;
11134
+ }
11135
+ queueMicrotask(wait);
11136
+ })();
11101
11137
  }, 0);
11102
11138
  });
11103
11139
  });
@@ -11157,6 +11193,23 @@
11157
11193
  */
11158
11194
  var websqlDBCache = {};
11159
11195
 
11196
+ /**
11197
+ * Tracks databases with a creation/upgrade currently in flight but not yet
11198
+ * committed or aborted -- keyed by (unescaped) database name. The
11199
+ * `dbVersions` row in `sysdb` these entries shadow is written *before*
11200
+ * `upgradeneeded` is even dispatched to user code (as the success
11201
+ * callback of that very `INSERT`/`UPDATE`), on the same shared `sysdb`
11202
+ * connection `databases()` itself reads from -- so, absent this, a
11203
+ * `databases()` call made while an upgrade is still pending would see
11204
+ * the new row (or new version) immediately, rather than only once the
11205
+ * corresponding `versionchange` transaction has genuinely committed, as
11206
+ * required by `get-databases.any.js`'s "doesn't pick up changes that
11207
+ * haven't committed" test. `IDBFactory.prototype.databases` filters
11208
+ * its SQL results against this map.
11209
+ * @type {Map<string, {oldVersion: Integer, newVersion: Integer}>}
11210
+ */
11211
+ var pendingVersionChanges = new Map();
11212
+
11160
11213
  /** @type {import('websql-configurable/lib/websql/WebSQLDatabase.js').default} */
11161
11214
  var sysdb;
11162
11215
  var nameCounter = 0;
@@ -11459,6 +11512,19 @@
11459
11512
  if (calledDbCreateError) {
11460
11513
  return false;
11461
11514
  }
11515
+ // Defensive cleanup: `pendingVersionChanges.set(name, ...)` (see
11516
+ // below) runs *before* the `dbVersions` `INSERT`/`UPDATE` it
11517
+ // guards is even attempted, but only `versionSet`'s own
11518
+ // `on__beforecomplete`/`on__preabort` handlers ever clear it --
11519
+ // and `versionSet` never runs at all if that `INSERT`/`UPDATE`,
11520
+ // or an earlier step in this same `open()` flow, fails first.
11521
+ // Without this, such a failure would leave the entry orphaned
11522
+ // forever, silently corrupting `databases()` for any *other*,
11523
+ // unrelated later `open()` call that happens to reuse the same
11524
+ // database name (common in WPT tests, e.g. generic names like
11525
+ // "DB1"/"TestDatabase" reused across different test files in
11526
+ // the same process). A no-op if no entry was ever set.
11527
+ pendingVersionChanges.delete(name);
11462
11528
  var er = err ? webSQLErrback(err) : (/** @type {Error} */tx);
11463
11529
  calledDbCreateError = true;
11464
11530
  // Re: why bubbling here (and how cancelable is only really relevant for `window.onerror`) see: https://github.com/w3c/IndexedDB/issues/86
@@ -11570,22 +11636,43 @@
11570
11636
  // open/close the transaction's active-handler window itself.
11571
11637
  req.transaction.__handlerActive = true;
11572
11638
  req.dispatchEvent(e);
11573
- // Give any same-tick microtask scheduled from within the
11639
+ // Give any microtask-scheduled continuation of the
11574
11640
  // `upgradeneeded` handler (e.g. a plain
11575
- // `Promise.resolve().then(...)`) a chance to run -- and still
11576
- // observe the transaction as active -- before we deactivate it
11577
- // again, per https://github.com/w3c/IndexedDB/issues/87. Only
11578
- // the flag reset itself is deferred here -- unlike
11579
- // `IDBTransaction.js`'s `advanceAfterDispatch`, `finished()`
11580
- // (this transaction's own queue-advancement/completion signal)
11581
- // still runs synchronously, at exactly its previous timing: an
11582
- // earlier attempt at deferring `finished()` too raced against
11583
- // unrelated test setup that assumed a freshly deleted/created
11584
- // database's upgrade transaction had already fully completed by
11585
- // the time this function returns.
11586
- queueMicrotask(function () {
11587
- req.transaction.__handlerActive = false;
11588
- });
11641
+ // `Promise.resolve().then(...)`, or an `await`-based
11642
+ // continuation of a promise resolved from within the handler,
11643
+ // such as testharness.js's own `EventWatcher`) a chance to
11644
+ // run -- and still observe the transaction as active -- before
11645
+ // we deactivate it again, per
11646
+ // https://github.com/w3c/IndexedDB/issues/87. A single deferred
11647
+ // tick isn't always enough: an `await`-based continuation can
11648
+ // take more than one microtask turn to resume (e.g.
11649
+ // `transaction-lifetime.any.js`'s `EventWatcher`-based
11650
+ // `await eventWatcher.wait_for('upgradeneeded')`), so retry a
11651
+ // small, bounded number of times first -- same pattern as
11652
+ // `IDBTransaction.js`'s `checkQueueEntry` -- rather than
11653
+ // resetting after only one. Only the flag reset itself is
11654
+ // deferred here -- unlike `IDBTransaction.js`'s
11655
+ // `advanceAfterDispatch`, `finished()` (this transaction's own
11656
+ // queue-advancement/completion signal) still runs
11657
+ // synchronously, at exactly its previous timing: an earlier
11658
+ // attempt at deferring `finished()` too raced against unrelated
11659
+ // test setup that assumed a freshly deleted/created database's
11660
+ // upgrade transaction had already fully completed by the time
11661
+ // this function returns.
11662
+ /**
11663
+ * @param {Integer} attemptsLeft
11664
+ * @returns {void}
11665
+ */
11666
+ function deferHandlerActiveReset(attemptsLeft) {
11667
+ if (attemptsLeft <= 0) {
11668
+ req.transaction.__handlerActive = false;
11669
+ return;
11670
+ }
11671
+ queueMicrotask(function () {
11672
+ deferHandlerActiveReset(attemptsLeft - 1);
11673
+ });
11674
+ }
11675
+ deferHandlerActiveReset(10);
11589
11676
  if (e.__legacyOutputDidListenersThrowError) {
11590
11677
  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
11591
11678
  req.transaction.__abortTransaction(createDOMException('AbortError', 'A request was aborted.'));
@@ -11601,6 +11688,7 @@
11601
11688
  * @returns {void}
11602
11689
  */
11603
11690
  function (ev) {
11691
+ pendingVersionChanges.delete(name);
11604
11692
  connection.__upgradeTransaction = null;
11605
11693
  /** @type {import('./IDBDatabase.js').IDBDatabaseFull} */
11606
11694
  req.__result.__versionTransaction = null;
@@ -11615,6 +11703,7 @@
11615
11703
 
11616
11704
  // eslint-disable-next-line camelcase -- Clear API
11617
11705
  req.transaction.on__preabort = function () {
11706
+ pendingVersionChanges.delete(name);
11618
11707
  connection.__upgradeTransaction = null;
11619
11708
  // We ensure any cache is deleted before any request error events fire and try to reopen
11620
11709
  if (useDatabaseCache) {
@@ -11677,6 +11766,10 @@
11677
11766
  // });
11678
11767
  };
11679
11768
  }
11769
+ pendingVersionChanges.set(name, {
11770
+ oldVersion: oldVersion,
11771
+ newVersion: version
11772
+ });
11680
11773
  if (oldVersion === 0) {
11681
11774
  systx.executeSql('INSERT INTO dbVersions VALUES (?,?)', [sqlSafeName, version], versionSet, dbCreateError);
11682
11775
  } else {
@@ -11979,6 +12072,15 @@
11979
12072
  IDBFactory.prototype.databases = function () {
11980
12073
  var me = this;
11981
12074
  var calledDbCreateError = false;
12075
+ // Snapshotted *now*, synchronously, at call time -- not read later
12076
+ // from inside the SQL query's callback below, which runs on a
12077
+ // deferred macrotask (see `nodeSQLiteDatabase.js`'s `exec`) and so
12078
+ // could otherwise race against (and lose to) an in-flight upgrade's
12079
+ // own commit/abort handler clearing its `pendingVersionChanges`
12080
+ // entry in the meantime -- which would make this method incorrectly
12081
+ // reflect a since-committed change that hadn't committed yet when
12082
+ // it was actually called.
12083
+ var pendingVersionChangesSnapshot = new Map(pendingVersionChanges);
11982
12084
  return new Promise(function (resolve, reject) {
11983
12085
  // eslint-disable-line promise/avoid-new -- Own polyfill
11984
12086
  if (!(me instanceof IDBFactory)) {
@@ -12008,10 +12110,29 @@
12008
12110
  var dbNames = [];
12009
12111
  for (var i = 0; i < data.rows.length; i++) {
12010
12112
  var _data$rows$item2 = /** @type {{name: string, version: Integer}} */data.rows.item(i),
12011
- name = _data$rows$item2.name,
12113
+ encodedName = _data$rows$item2.name,
12012
12114
  version = _data$rows$item2.version;
12115
+ var name = unescapeSQLiteResponse(encodedName);
12116
+ // A row for a database whose creation/upgrade hasn't
12117
+ // committed yet (see `pendingVersionChanges`) must
12118
+ // not reflect that in-flight change: a brand new
12119
+ // database (`oldVersion === 0`) isn't reported at
12120
+ // all until its creation commits, and an existing
12121
+ // database being upgraded is still reported, but
12122
+ // with its pre-upgrade version.
12123
+ var pending = pendingVersionChangesSnapshot.get(name);
12124
+ if (pending) {
12125
+ if (pending.oldVersion === 0) {
12126
+ continue;
12127
+ }
12128
+ dbNames.push({
12129
+ name: name,
12130
+ version: pending.oldVersion
12131
+ });
12132
+ continue;
12133
+ }
12013
12134
  dbNames.push({
12014
- name: unescapeSQLiteResponse(name),
12135
+ name: name,
12015
12136
  version: version
12016
12137
  });
12017
12138
  }
@@ -12431,9 +12552,26 @@
12431
12552
  // Key.convertValueToKey(key); // Already checked by `continue` or `continuePrimaryKey`
12432
12553
  sqlValues.push(/** @type {string} */_encode(key));
12433
12554
  } else if (continueCall && me.__key !== undefined) {
12434
- sql.push('AND', quotedKeyColumnName, op + ' ?');
12435
12555
  // Key.convertValueToKey(me.__key); // Already checked when stored
12436
- sqlValues.push(/** @type {string} */_encode(me.__key));
12556
+ if (!me.__unique && me.__keyColumnName !== 'key' && me.__primaryKey !== undefined) {
12557
+ // A plain `continue()` on a non-unique index cursor must find
12558
+ // the next record strictly after the (key, primaryKey) pair
12559
+ // this cursor last returned -- a scalar `key > lastKey` alone
12560
+ // would wrongly exclude a *different*, not-yet-visited record
12561
+ // that's still tied on key with the last one (e.g. another
12562
+ // record that always had that same indexed value), and would
12563
+ // also wrongly re-admit the *same* record forever if a
12564
+ // same-transaction `update()` bumped its own key back above
12565
+ // the threshold, since a scalar comparison can't distinguish
12566
+ // "some other record newly tied" from "this record moved
12567
+ // past its own last position." Comparing the full tuple (via
12568
+ // this OR) against both key and primary key resolves both.
12569
+ sql.push('AND (', quotedKeyColumnName, op, '?', 'OR (', quotedKeyColumnName, '= ?', 'AND', quotedKey, op, '?))');
12570
+ sqlValues.push(/** @type {string} */_encode(me.__key), /** @type {string} */_encode(me.__key), /** @type {string} */_encode(me.__primaryKey));
12571
+ } else {
12572
+ sql.push('AND', quotedKeyColumnName, op + ' ?');
12573
+ sqlValues.push(/** @type {string} */_encode(me.__key));
12574
+ }
12437
12575
  }
12438
12576
  if (!me.__count) {
12439
12577
  // 1. Sort by key
@@ -13063,9 +13201,15 @@
13063
13201
  * @returns {void}
13064
13202
  */
13065
13203
  function addToQueue(clonedValue) {
13066
- // We set the `invalidateCache` argument to `false` since the old value shouldn't be accessed
13204
+ // `invalidateCache: true` so any cursor on this store (including
13205
+ // this one) drops its prefetched row batch, forcing its next
13206
+ // `continue()` to re-query live rather than serve rows snapshotted
13207
+ // before this update -- needed for the compound-tuple
13208
+ // continuation logic in `__findBasic` to see this update's
13209
+ // effect on ordering (see "Modify records during cursor
13210
+ // iteration" in idbcursor_update_index.any.js).
13067
13211
  // @ts-ignore -- API (not erring in TS 6)
13068
- IDBObjectStore.__storingRecordObjectStore(request, me.__store, false, clonedValue, false, key);
13212
+ IDBObjectStore.__storingRecordObjectStore(request, me.__store, true, clonedValue, false, key);
13069
13213
  }
13070
13214
  if (me.__store.keyPath !== null) {
13071
13215
  var _me$__store$__validat = me.__store.__validateKeyAndValueAndCloneValue(valueToUpdate, undefined, true),