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 +1 @@
1
- {"version":3,"file":"nodeSQLiteDatabase.d.ts","sourceRoot":"","sources":["../src/nodeSQLiteDatabase.js"],"names":[],"mappings":";iCAyBa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI;+BAIvC,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAC,SAAS;AAsB9C;;;;;;GAMG;AACH;SAHgB,GAAG;gBAAc,MAAM;SAF5B,MAAM,SACN;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,kBAAkB,CAAA;CAAC,GAEjF,IAAI,CAmEhB;;IAxED;;;;;;OAMG;IACH;aAHgB,GAAG;oBAAc,MAAM;aAF5B,MAAM,SACN;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,kBAAkB,CAAA;KAAC,EAqE7F;IAhEG,mBAAsB;IAkBtB,SA6CE;IAyBN;;;;;OAKG;IACH,cALW;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,EAAE,CAAA;KAAC,EAAE,YAChC,OAAO,YACP,CAAC,GAAG,EAAE,KAAK,GAAC,IAAI,EAAE,OAAO,CAAC,EAAE,YAAY,EAAE,KAAK,IAAI,GACjD,IAAI,CAqFhB;;AA1OD;;GAEG;AACH;IACI;;;;;OAKG;IACH,mBALW,KAAK,GAAC,IAAI,GAAC,SAAS,aACpB,MAAM,GAAC,SAAS,iBAChB,MAAM,SACN,MAAM,EAAE,EAOlB;IAJG,gCAAkB;IAClB,6BAAwB;IACxB,iCAAgC;IAChC,2BAAgB;CAEvB"}
1
+ {"version":3,"file":"nodeSQLiteDatabase.d.ts","sourceRoot":"","sources":["../src/nodeSQLiteDatabase.js"],"names":[],"mappings":";iCAyBa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI;+BAIvC,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAC,SAAS;AAkC9C;;;;;;GAMG;AACH;SAHgB,GAAG;gBAAc,MAAM;SAF5B,MAAM,SACN;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,kBAAkB,CAAA;CAAC,GAEjF,IAAI,CAmEhB;;IAxED;;;;;;OAMG;IACH;aAHgB,GAAG;oBAAc,MAAM;aAF5B,MAAM,SACN;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,kBAAkB,CAAA;KAAC,EAqE7F;IAhEG,mBAAsB;IAkBtB,SA6CE;IAyBN;;;;;OAKG;IACH,cALW;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,EAAE,CAAA;KAAC,EAAE,YAChC,OAAO,YACP,CAAC,GAAG,EAAE,KAAK,GAAC,IAAI,EAAE,OAAO,CAAC,EAAE,YAAY,EAAE,KAAK,IAAI,GACjD,IAAI,CA2IhB;;AA5SD;;GAEG;AACH;IACI;;;;;OAKG;IACH,mBALW,KAAK,GAAC,IAAI,GAAC,SAAS,aACpB,MAAM,GAAC,SAAS,iBAChB,MAAM,SACN,MAAM,EAAE,EAOlB;IAJG,gCAAkB;IAClB,6BAAwB;IACxB,iCAAgC;IAChC,2BAAgB;CAEvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"nodeWebSQL.d.ts","sourceRoot":"","sources":["../src/nodeWebSQL.js"],"names":[],"mappings":";qCAKa;IACR,GAAG,EAAE,GAAG,CAAC;IACT,IAAI,EAAE,CAAA,OAAO,cAAc,EAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;CACjD;;;;;;;;;;;wCAYS,KAAK,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,sBAAsB;AAsBrE,qHAAqI;2BAzC1G,yBAAyB"}
1
+ {"version":3,"file":"nodeWebSQL.d.ts","sourceRoot":"","sources":["../src/nodeWebSQL.js"],"names":[],"mappings":";qCAKa;IACR,GAAG,EAAE,GAAG,CAAC;IACT,IAAI,EAAE,CAAA,OAAO,cAAc,EAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;CACjD;;;;;;;;;;;wCAYS,KAAK,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,sBAAsB;AA6BrE,qHAAsK;2BAhD3I,yBAAyB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indexeddbshim",
3
- "version": "17.3.2",
3
+ "version": "17.3.3",
4
4
  "type": "module",
5
5
  "author": "Parashuram <code@nparashuram.com>",
6
6
  "contributors": [
@@ -173,7 +173,7 @@
173
173
  "sync-promise-expanded": "^2.0.0",
174
174
  "typeson": "10.2.0",
175
175
  "typeson-registry": "14.2.2",
176
- "websql-configurable": "^3.0.6"
176
+ "websql-configurable": "^3.1.0"
177
177
  },
178
178
  "devDependencies": {
179
179
  "@babel/core": "^8.0.1",
package/src/IDBCursor.js CHANGED
@@ -260,9 +260,33 @@ IDBCursor.prototype.__findBasic = function (key, primaryKey, tx, success, error,
260
260
  // Key.convertValueToKey(key); // Already checked by `continue` or `continuePrimaryKey`
261
261
  sqlValues.push(/** @type {string} */ (Key.encode(key)));
262
262
  } else if (continueCall && me.__key !== undefined) {
263
- sql.push('AND', quotedKeyColumnName, op + ' ?');
264
263
  // Key.convertValueToKey(me.__key); // Already checked when stored
265
- sqlValues.push(/** @type {string} */ (Key.encode(me.__key)));
264
+ if (!me.__unique && me.__keyColumnName !== 'key' && me.__primaryKey !== undefined) {
265
+ // A plain `continue()` on a non-unique index cursor must find
266
+ // the next record strictly after the (key, primaryKey) pair
267
+ // this cursor last returned -- a scalar `key > lastKey` alone
268
+ // would wrongly exclude a *different*, not-yet-visited record
269
+ // that's still tied on key with the last one (e.g. another
270
+ // record that always had that same indexed value), and would
271
+ // also wrongly re-admit the *same* record forever if a
272
+ // same-transaction `update()` bumped its own key back above
273
+ // the threshold, since a scalar comparison can't distinguish
274
+ // "some other record newly tied" from "this record moved
275
+ // past its own last position." Comparing the full tuple (via
276
+ // this OR) against both key and primary key resolves both.
277
+ sql.push(
278
+ 'AND (', quotedKeyColumnName, op, '?',
279
+ 'OR (', quotedKeyColumnName, '= ?', 'AND', quotedKey, op, '?))'
280
+ );
281
+ sqlValues.push(
282
+ /** @type {string} */ (Key.encode(me.__key)),
283
+ /** @type {string} */ (Key.encode(me.__key)),
284
+ /** @type {string} */ (Key.encode(me.__primaryKey))
285
+ );
286
+ } else {
287
+ sql.push('AND', quotedKeyColumnName, op + ' ?');
288
+ sqlValues.push(/** @type {string} */ (Key.encode(me.__key)));
289
+ }
266
290
  }
267
291
 
268
292
  if (!me.__count) {
@@ -912,9 +936,15 @@ IDBCursor.prototype.update = function (valueToUpdate) {
912
936
  * @returns {void}
913
937
  */
914
938
  function addToQueue (clonedValue) {
915
- // We set the `invalidateCache` argument to `false` since the old value shouldn't be accessed
939
+ // `invalidateCache: true` so any cursor on this store (including
940
+ // this one) drops its prefetched row batch, forcing its next
941
+ // `continue()` to re-query live rather than serve rows snapshotted
942
+ // before this update -- needed for the compound-tuple
943
+ // continuation logic in `__findBasic` to see this update's
944
+ // effect on ordering (see "Modify records during cursor
945
+ // iteration" in idbcursor_update_index.any.js).
916
946
  // @ts-ignore -- API (not erring in TS 6)
917
- IDBObjectStore.__storingRecordObjectStore(request, me.__store, false, clonedValue, false, key);
947
+ IDBObjectStore.__storingRecordObjectStore(request, me.__store, true, clonedValue, false, key);
918
948
  }
919
949
  if (me.__store.keyPath !== null) {
920
950
  const [evaluatedKey, clonedValue] = me.__store.__validateKeyAndValueAndCloneValue(valueToUpdate, undefined, true);
package/src/IDBFactory.js CHANGED
@@ -156,7 +156,29 @@ function triggerAnyVersionChangeAndBlockedEvents (openConnections, req, oldVersi
156
156
  return new SyncPromise(function (resolve) {
157
157
  setTimeout(() => {
158
158
  entry.dispatchEvent(e); // No need to catch errors
159
- resolve(undefined);
159
+ // Unlike a native `Promise`, `SyncPromise#then` chains
160
+ // synchronously off `resolve()` (verified directly:
161
+ // `SyncPromise.resolve().then(cb)` runs `cb` before
162
+ // even the calling code below it finishes) -- so
163
+ // resolving immediately here would let the
164
+ // `connectionsClosed()` check below run before a
165
+ // same-task-but-microtask-later continuation of a
166
+ // `versionchange` listener above (e.g. an `await`-
167
+ // based one that then calls `db.close()`, as in
168
+ // `transaction-lifetime.any.js`) ever gets a turn,
169
+ // incorrectly firing `blocked` even though the
170
+ // connection was about to close in time. Give real
171
+ // microtask-deferred continuations a small, bounded
172
+ // number of turns first -- same pattern as
173
+ // `IDBTransaction.js`'s `checkQueueEntry`.
174
+ let attemptsLeft = 10;
175
+ (function wait () {
176
+ if (attemptsLeft-- <= 0) {
177
+ resolve(undefined);
178
+ return;
179
+ }
180
+ queueMicrotask(wait);
181
+ }());
160
182
  }, 0);
161
183
  });
162
184
  });
@@ -214,6 +236,23 @@ function triggerAnyVersionChangeAndBlockedEvents (openConnections, req, oldVersi
214
236
  */
215
237
  const websqlDBCache = {};
216
238
 
239
+ /**
240
+ * Tracks databases with a creation/upgrade currently in flight but not yet
241
+ * committed or aborted -- keyed by (unescaped) database name. The
242
+ * `dbVersions` row in `sysdb` these entries shadow is written *before*
243
+ * `upgradeneeded` is even dispatched to user code (as the success
244
+ * callback of that very `INSERT`/`UPDATE`), on the same shared `sysdb`
245
+ * connection `databases()` itself reads from -- so, absent this, a
246
+ * `databases()` call made while an upgrade is still pending would see
247
+ * the new row (or new version) immediately, rather than only once the
248
+ * corresponding `versionchange` transaction has genuinely committed, as
249
+ * required by `get-databases.any.js`'s "doesn't pick up changes that
250
+ * haven't committed" test. `IDBFactory.prototype.databases` filters
251
+ * its SQL results against this map.
252
+ * @type {Map<string, {oldVersion: Integer, newVersion: Integer}>}
253
+ */
254
+ const pendingVersionChanges = new Map();
255
+
217
256
  /** @type {import('websql-configurable/lib/websql/WebSQLDatabase.js').default} */
218
257
  let sysdb;
219
258
  let nameCounter = 0;
@@ -549,6 +588,19 @@ IDBFactory.prototype.open = function (name /* , version */) {
549
588
  if (calledDbCreateError) {
550
589
  return false;
551
590
  }
591
+ // Defensive cleanup: `pendingVersionChanges.set(name, ...)` (see
592
+ // below) runs *before* the `dbVersions` `INSERT`/`UPDATE` it
593
+ // guards is even attempted, but only `versionSet`'s own
594
+ // `on__beforecomplete`/`on__preabort` handlers ever clear it --
595
+ // and `versionSet` never runs at all if that `INSERT`/`UPDATE`,
596
+ // or an earlier step in this same `open()` flow, fails first.
597
+ // Without this, such a failure would leave the entry orphaned
598
+ // forever, silently corrupting `databases()` for any *other*,
599
+ // unrelated later `open()` call that happens to reuse the same
600
+ // database name (common in WPT tests, e.g. generic names like
601
+ // "DB1"/"TestDatabase" reused across different test files in
602
+ // the same process). A no-op if no entry was ever set.
603
+ pendingVersionChanges.delete(name);
552
604
  const er = err ? webSQLErrback(err) : /** @type {Error} */ (tx);
553
605
  calledDbCreateError = true;
554
606
  // Re: why bubbling here (and how cancelable is only really relevant for `window.onerror`) see: https://github.com/w3c/IndexedDB/issues/86
@@ -668,22 +720,43 @@ IDBFactory.prototype.open = function (name /* , version */) {
668
720
  // open/close the transaction's active-handler window itself.
669
721
  req.transaction.__handlerActive = true;
670
722
  req.dispatchEvent(e);
671
- // Give any same-tick microtask scheduled from within the
723
+ // Give any microtask-scheduled continuation of the
672
724
  // `upgradeneeded` handler (e.g. a plain
673
- // `Promise.resolve().then(...)`) a chance to run -- and still
674
- // observe the transaction as active -- before we deactivate it
675
- // again, per https://github.com/w3c/IndexedDB/issues/87. Only
676
- // the flag reset itself is deferred here -- unlike
677
- // `IDBTransaction.js`'s `advanceAfterDispatch`, `finished()`
678
- // (this transaction's own queue-advancement/completion signal)
679
- // still runs synchronously, at exactly its previous timing: an
680
- // earlier attempt at deferring `finished()` too raced against
681
- // unrelated test setup that assumed a freshly deleted/created
682
- // database's upgrade transaction had already fully completed by
683
- // the time this function returns.
684
- queueMicrotask(() => {
685
- req.transaction.__handlerActive = false;
686
- });
725
+ // `Promise.resolve().then(...)`, or an `await`-based
726
+ // continuation of a promise resolved from within the handler,
727
+ // such as testharness.js's own `EventWatcher`) a chance to
728
+ // run -- and still observe the transaction as active -- before
729
+ // we deactivate it again, per
730
+ // https://github.com/w3c/IndexedDB/issues/87. A single deferred
731
+ // tick isn't always enough: an `await`-based continuation can
732
+ // take more than one microtask turn to resume (e.g.
733
+ // `transaction-lifetime.any.js`'s `EventWatcher`-based
734
+ // `await eventWatcher.wait_for('upgradeneeded')`), so retry a
735
+ // small, bounded number of times first -- same pattern as
736
+ // `IDBTransaction.js`'s `checkQueueEntry` -- rather than
737
+ // resetting after only one. Only the flag reset itself is
738
+ // deferred here -- unlike `IDBTransaction.js`'s
739
+ // `advanceAfterDispatch`, `finished()` (this transaction's own
740
+ // queue-advancement/completion signal) still runs
741
+ // synchronously, at exactly its previous timing: an earlier
742
+ // attempt at deferring `finished()` too raced against unrelated
743
+ // test setup that assumed a freshly deleted/created database's
744
+ // upgrade transaction had already fully completed by the time
745
+ // this function returns.
746
+ /**
747
+ * @param {Integer} attemptsLeft
748
+ * @returns {void}
749
+ */
750
+ function deferHandlerActiveReset (attemptsLeft) {
751
+ if (attemptsLeft <= 0) {
752
+ req.transaction.__handlerActive = false;
753
+ return;
754
+ }
755
+ queueMicrotask(() => {
756
+ deferHandlerActiveReset(attemptsLeft - 1);
757
+ });
758
+ }
759
+ deferHandlerActiveReset(10);
687
760
 
688
761
  if (e.__legacyOutputDidListenersThrowError) {
689
762
  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
@@ -701,6 +774,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
701
774
  * @returns {void}
702
775
  */
703
776
  function (ev) {
777
+ pendingVersionChanges.delete(name);
704
778
  connection.__upgradeTransaction = null;
705
779
  /** @type {import('./IDBDatabase.js').IDBDatabaseFull} */ (
706
780
  req.__result
@@ -716,6 +790,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
716
790
 
717
791
  // eslint-disable-next-line camelcase -- Clear API
718
792
  req.transaction.on__preabort = function () {
793
+ pendingVersionChanges.delete(name);
719
794
  connection.__upgradeTransaction = null;
720
795
  // We ensure any cache is deleted before any request error events fire and try to reopen
721
796
  if (useDatabaseCache) {
@@ -789,6 +864,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
789
864
  };
790
865
  }
791
866
 
867
+ pendingVersionChanges.set(name, {oldVersion, newVersion: version});
792
868
  if (oldVersion === 0) {
793
869
  systx.executeSql('INSERT INTO dbVersions VALUES (?,?)', [sqlSafeName, version], versionSet, dbCreateError);
794
870
  } else {
@@ -1102,6 +1178,15 @@ IDBFactory.prototype.cmp = function (key1, key2) {
1102
1178
  IDBFactory.prototype.databases = function () {
1103
1179
  const me = this;
1104
1180
  let calledDbCreateError = false;
1181
+ // Snapshotted *now*, synchronously, at call time -- not read later
1182
+ // from inside the SQL query's callback below, which runs on a
1183
+ // deferred macrotask (see `nodeSQLiteDatabase.js`'s `exec`) and so
1184
+ // could otherwise race against (and lose to) an in-flight upgrade's
1185
+ // own commit/abort handler clearing its `pendingVersionChanges`
1186
+ // entry in the meantime -- which would make this method incorrectly
1187
+ // reflect a since-committed change that hadn't committed yet when
1188
+ // it was actually called.
1189
+ const pendingVersionChangesSnapshot = new Map(pendingVersionChanges);
1105
1190
  return new Promise(function (resolve, reject) { // eslint-disable-line promise/avoid-new -- Own polyfill
1106
1191
  if (!(me instanceof IDBFactory)) {
1107
1192
  throw new TypeError('Illegal invocation');
@@ -1129,11 +1214,24 @@ IDBFactory.prototype.databases = function () {
1129
1214
  sysReadTx.executeSql('SELECT "name", "version" FROM dbVersions', [], function (sysReadTx, data) {
1130
1215
  const dbNames = [];
1131
1216
  for (let i = 0; i < data.rows.length; i++) {
1132
- const {name, version} = /** @type {{name: string, version: Integer}} */ (data.rows.item(i));
1133
- dbNames.push({
1134
- name: util.unescapeSQLiteResponse(name),
1135
- version
1136
- });
1217
+ const {name: encodedName, version} = /** @type {{name: string, version: Integer}} */ (data.rows.item(i));
1218
+ const name = util.unescapeSQLiteResponse(encodedName);
1219
+ // A row for a database whose creation/upgrade hasn't
1220
+ // committed yet (see `pendingVersionChanges`) must
1221
+ // not reflect that in-flight change: a brand new
1222
+ // database (`oldVersion === 0`) isn't reported at
1223
+ // all until its creation commits, and an existing
1224
+ // database being upgraded is still reported, but
1225
+ // with its pre-upgrade version.
1226
+ const pending = pendingVersionChangesSnapshot.get(name);
1227
+ if (pending) {
1228
+ if (pending.oldVersion === 0) {
1229
+ continue;
1230
+ }
1231
+ dbNames.push({name, version: pending.oldVersion});
1232
+ continue;
1233
+ }
1234
+ dbNames.push({name, version});
1137
1235
  }
1138
1236
  resolve(dbNames);
1139
1237
  }, dbGetDatabaseNamesError);
@@ -860,9 +860,15 @@ IDBTransaction.prototype.__abortTransaction = function (err) {
860
860
  me.__error = err;
861
861
  }
862
862
 
863
- if (me.__requestsFinished) {
863
+ if (me.__requestsFinished && err !== null) {
864
864
  // The transaction has already completed, so we can't call "onerror" or "onabort".
865
- // So throw the error instead.
865
+ // So throw the error instead. `err` is only ever `null` here via
866
+ // `IDBTransaction.prototype.abort`'s own `__abortTransaction(null)`
867
+ // call, which now checks `__requestsFinished` itself first and
868
+ // throws `InvalidStateError` synchronously before ever reaching
869
+ // this point -- so this guard is just defense in depth against
870
+ // `err` somehow being `null` some other way, not something this
871
+ // path should see in practice.
866
872
  setTimeout(() => {
867
873
  throw err;
868
874
  }, 0);
@@ -970,6 +976,14 @@ IDBTransaction.prototype.abort = function () {
970
976
  if (me.__committed) {
971
977
  throw createDOMException('InvalidStateError', 'The transaction has already been committed');
972
978
  }
979
+ if (me.__requestsFinished) {
980
+ // All requests have already finished and the transaction is
981
+ // auto-committing (the async SQL commit round trip just hasn't
982
+ // resolved yet) -- too late to abort per spec, even though
983
+ // `__committed` itself isn't set until that round trip actually
984
+ // finishes.
985
+ throw createDOMException('InvalidStateError', 'The transaction is already committing');
986
+ }
973
987
  me.__abortTransaction(null);
974
988
  };
975
989
 
@@ -36,16 +36,28 @@ const READ_ONLY_ERROR = new Error(
36
36
  // connection and a newly-opened one during an upgrade. `PRAGMA
37
37
  // busy_timeout` can't safely arbitrate between them here: it blocks the
38
38
  // single JS thread synchronously while it retries, but the lock holder's
39
- // own release is itself scheduled via `setTimeout` below, which can never
40
- // fire while that retry loop is blocking the same thread -- so instead of
41
- // waiting, the second connection's write just fails with "database is
42
- // locked". Serialize writes per file path instead, so a second
43
- // connection's `BEGIN` waits for the first connection's transaction to
44
- // actually finish rather than colliding with it.
39
+ // own release is itself scheduled via `setImmediate` below, which can
40
+ // never fire while that retry loop is blocking the same thread -- so
41
+ // instead of waiting, a second connection's write just fails with
42
+ // "database is locked".
43
+ //
44
+ // `fileReaders`/`fileWriter` implement a simple (not starvation-proof --
45
+ // see the release loop below) reader/writer lock per file path instead:
46
+ // any number of `readonly` transactions may hold the file concurrently,
47
+ // matching what SQLite itself already natively supports (multiple
48
+ // readers never need to exclude each other, only a writer needs
49
+ // exclusivity), while a non-`readonly` transaction still waits for
50
+ // exclusive access -- no concurrent readers, no concurrent writer. A
51
+ // simple single-owner mutex (this file's prior implementation) would
52
+ // otherwise serialize even purely-concurrent-reader scenarios that
53
+ // never touch a writer at all, for no reason `better-sqlite3`/SQLite
54
+ // itself requires.
55
+ /** @type {Map<string, Set<{_db: any, _qFilePath: string}>>} */
56
+ const fileReaders = new Map();
45
57
  /** @type {Map<string, {_db: any, _qFilePath: string}>} */
46
- const fileLockOwners = new Map();
47
- /** @type {Map<string, (() => void)[]>} */
48
- const fileLockWaiters = new Map();
58
+ const fileWriter = new Map();
59
+ /** @type {Map<string, {isReader: boolean, resume: () => void}[]>} */
60
+ const fileWaiters = new Map();
49
61
  const beginRe = /^\s*BEGIN\b/iu;
50
62
  const endRe = /^\s*(END|COMMIT|ROLLBACK)\b/iu;
51
63
 
@@ -158,14 +170,29 @@ SQLiteDatabase.prototype.exec = function exec (queries, readOnly, callback) {
158
170
  // already fully independent -- so there is nothing to serialize.
159
171
  const filePath = this._qFilePath === ':memory:' ? null : this._qFilePath;
160
172
  if (filePath && queries[0] && beginRe.test(queries[0].sql)) {
161
- const owner = fileLockOwners.get(filePath);
162
- if (owner && owner !== this) {
163
- const waiters = fileLockWaiters.get(filePath) || [];
164
- waiters.push(() => this.exec(queries, readOnly, callback));
165
- fileLockWaiters.set(filePath, waiters);
173
+ const activeReaders = fileReaders.get(filePath);
174
+ const hasActiveReaders = Boolean(activeReaders && activeReaders.size);
175
+ const activeWriter = fileWriter.get(filePath);
176
+ const blockedByWriter = Boolean(activeWriter && activeWriter !== this);
177
+ const blocked = blockedByWriter || (!readOnly && hasActiveReaders);
178
+ if (blocked) {
179
+ const waiters = fileWaiters.get(filePath) || [];
180
+ waiters.push({
181
+ isReader: readOnly,
182
+ resume: () => this.exec(queries, readOnly, callback)
183
+ });
184
+ fileWaiters.set(filePath, waiters);
166
185
  return;
167
186
  }
168
- fileLockOwners.set(filePath, this);
187
+ if (readOnly) {
188
+ if (activeReaders) {
189
+ activeReaders.add(this);
190
+ } else {
191
+ fileReaders.set(filePath, new Set([this]));
192
+ }
193
+ } else {
194
+ fileWriter.set(filePath, this);
195
+ }
169
196
  }
170
197
 
171
198
  const db = this._db._db;
@@ -204,13 +231,15 @@ SQLiteDatabase.prototype.exec = function exec (queries, readOnly, callback) {
204
231
  }
205
232
  }
206
233
  }
207
- // A real timer (not `queueMicrotask`) so this yields to the macrotask
208
- // queue: code that synchronously issues a new request from within
209
- // each request's callback (e.g. to keep a transaction alive) would
234
+ // A real macrotask (not `queueMicrotask`) so this yields properly:
235
+ // code that synchronously issues a new request from within each
236
+ // request's callback (e.g. to keep a transaction alive) would
210
237
  // otherwise chain microtask to microtask forever, starving out any
211
238
  // `setTimeout`-based code (including IndexedDB's own internal request
212
- // scheduling) that never gets a turn to run.
213
- setTimeout(() => {
239
+ // scheduling) that never gets a turn to run. `setImmediate` (Node's
240
+ // "check" phase) still yields the same way `setTimeout(..., 0)` did,
241
+ // but runs sooner in Node's event loop.
242
+ setImmediate(() => {
214
243
  // Release the file lock (if held) and hand it to the next waiting
215
244
  // connection, if any, only once this transaction has genuinely
216
245
  // finished -- and only here, on its own turn, so a resumed
@@ -225,15 +254,52 @@ SQLiteDatabase.prototype.exec = function exec (queries, readOnly, callback) {
225
254
  // and never see the matching release, deadlocking every later
226
255
  // connection to the same file.
227
256
  if (filePath && queries.some((q) => endRe.test(q.sql))) {
228
- fileLockOwners.delete(filePath);
229
- const waiters = fileLockWaiters.get(filePath);
230
- const nextWaitingExec = waiters && waiters.shift();
231
- if (nextWaitingExec) {
232
- nextWaitingExec();
257
+ const activeReaders = fileReaders.get(filePath);
258
+ if (activeReaders) {
259
+ activeReaders.delete(this);
260
+ if (!activeReaders.size) {
261
+ fileReaders.delete(filePath);
262
+ }
263
+ }
264
+ if (fileWriter.get(filePath) === this) {
265
+ fileWriter.delete(filePath);
266
+ }
267
+ // Resume as many waiters as the lock now genuinely allows:
268
+ // any leading run of readers (concurrent reads are always
269
+ // fine), then at most one writer, since a writer needs
270
+ // exclusivity -- stop there rather than looking past it.
271
+ // Not starvation-proof: a steady stream of arriving readers
272
+ // could in principle keep a waiting writer waiting
273
+ // indefinitely, but that's an acceptable tradeoff here over
274
+ // the added complexity of tracking arrival order across
275
+ // reader/writer kinds.
276
+ const waiters = fileWaiters.get(filePath);
277
+ if (waiters) {
278
+ while (waiters.length) {
279
+ if (fileWriter.has(filePath)) {
280
+ break;
281
+ }
282
+ const next = waiters[0];
283
+ const stillHasReaders = fileReaders.get(filePath);
284
+ if (!next.isReader && stillHasReaders && stillHasReaders.size) {
285
+ break;
286
+ }
287
+ waiters.shift();
288
+ next.resume();
289
+ if (!next.isReader) {
290
+ // A writer just took exclusive ownership (inside
291
+ // `resume()`, synchronously, via the acquire
292
+ // logic above) -- nothing else may proceed now.
293
+ break;
294
+ }
295
+ }
296
+ if (!waiters.length) {
297
+ fileWaiters.delete(filePath);
298
+ }
233
299
  }
234
300
  }
235
301
  callback(null, results);
236
- }, 0);
302
+ });
237
303
  };
238
304
 
239
305
  export default SQLiteDatabase;
package/src/nodeWebSQL.js CHANGED
@@ -40,5 +40,12 @@ function wrappedSQLiteDatabase (name) {
40
40
  return db;
41
41
  }
42
42
 
43
- const nodeWebSQL = customOpenDatabase(/** @type {SQLiteDatabaseConstructor} */ (/** @type {unknown} */ (wrappedSQLiteDatabase)), {});
43
+ // `concurrentReaders` is off by default in `websql-configurable` itself (to
44
+ // preserve the WebSQL spec's strict, one-at-a-time transaction ordering
45
+ // that library's own test suite depends on), but IndexedDBShim only ever
46
+ // uses it as an internal SQL execution engine -- it doesn't need or expose
47
+ // that ordering guarantee itself -- so it's safe, and needed, to opt in
48
+ // here: without it, two same-scope `readonly` IDBTransactions can deadlock
49
+ // waiting on each other (see `transaction-scheduling-within-database.any.js`).
50
+ const nodeWebSQL = customOpenDatabase(/** @type {SQLiteDatabaseConstructor} */ (/** @type {unknown} */ (wrappedSQLiteDatabase)), {websql: {concurrentReaders: true}});
44
51
  export default nodeWebSQL;