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.
- package/dist/IDBCursor.d.ts.map +1 -1
- package/dist/IDBFactory.d.ts.map +1 -1
- package/dist/IDBTransaction.d.ts.map +1 -1
- package/dist/indexeddbshim-Key.js +1 -1
- package/dist/indexeddbshim-Key.min.js +1 -1
- package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs +344 -88
- package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs.map +1 -1
- package/dist/indexeddbshim-UnicodeIdentifiers.js +169 -25
- package/dist/indexeddbshim-UnicodeIdentifiers.js.map +1 -1
- package/dist/indexeddbshim-UnicodeIdentifiers.min.js +3 -3
- package/dist/indexeddbshim-UnicodeIdentifiers.min.js.map +1 -1
- package/dist/indexeddbshim-node.cjs +344 -88
- package/dist/indexeddbshim-node.cjs.map +1 -1
- package/dist/indexeddbshim-noninvasive.js +169 -25
- package/dist/indexeddbshim-noninvasive.js.map +1 -1
- package/dist/indexeddbshim-noninvasive.min.js +3 -3
- package/dist/indexeddbshim-noninvasive.min.js.map +1 -1
- package/dist/indexeddbshim.js +169 -25
- package/dist/indexeddbshim.js.map +1 -1
- package/dist/indexeddbshim.min.js +3 -3
- package/dist/indexeddbshim.min.js.map +1 -1
- package/dist/nodeSQLiteDatabase.d.ts.map +1 -1
- package/dist/nodeWebSQL.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/IDBCursor.js +34 -4
- package/src/IDBFactory.js +119 -21
- package/src/IDBTransaction.js +16 -2
- package/src/nodeSQLiteDatabase.js +92 -26
- package/src/nodeWebSQL.js +8 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! indexeddbshim - v17.3.
|
|
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
|
-
|
|
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
|
|
10605
|
+
// Give any microtask-scheduled continuation of the
|
|
10540
10606
|
// `upgradeneeded` handler (e.g. a plain
|
|
10541
|
-
// `Promise.resolve().then(...)
|
|
10542
|
-
//
|
|
10543
|
-
//
|
|
10544
|
-
//
|
|
10545
|
-
//
|
|
10546
|
-
//
|
|
10547
|
-
//
|
|
10548
|
-
//
|
|
10549
|
-
//
|
|
10550
|
-
//
|
|
10551
|
-
//
|
|
10552
|
-
|
|
10553
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
} =
|
|
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
|
-
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
//
|
|
13243
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
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
|
-
|
|
13399
|
-
|
|
13400
|
-
|
|
13401
|
-
}
|
|
13402
|
-
|
|
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 (
|
|
13432
|
-
const cont =
|
|
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 (
|
|
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 `
|
|
14396
|
-
// fire while that retry loop is blocking the same thread -- so
|
|
14397
|
-
// waiting,
|
|
14398
|
-
// locked".
|
|
14399
|
-
//
|
|
14400
|
-
//
|
|
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
|
|
14403
|
-
/** @type {Map<string, (
|
|
14404
|
-
const
|
|
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
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
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
|
-
|
|
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
|
|
14566
|
-
//
|
|
14567
|
-
//
|
|
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
|
-
|
|
14587
|
-
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
|
|
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
|
-
}
|
|
14838
|
+
});
|
|
14595
14839
|
};
|
|
14596
14840
|
|
|
14597
14841
|
/**
|
|
@@ -14631,7 +14875,19 @@ function wrappedSQLiteDatabase(name) {
|
|
|
14631
14875
|
}
|
|
14632
14876
|
return db;
|
|
14633
14877
|
}
|
|
14634
|
-
|
|
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
|
CFG.win = {
|
|
14637
14893
|
openDatabase: nodeWebSQL
|