indexeddbshim 17.3.1 → 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/IDBObjectStore.d.ts +28 -0
- package/dist/IDBObjectStore.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 +507 -150
- package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs.map +1 -1
- package/dist/indexeddbshim-UnicodeIdentifiers.js +334 -89
- 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 +507 -150
- package/dist/indexeddbshim-node.cjs.map +1 -1
- package/dist/indexeddbshim-noninvasive.js +334 -89
- 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 +334 -89
- 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 +4 -3
- package/src/IDBCursor.js +34 -4
- package/src/IDBFactory.js +119 -21
- package/src/IDBObjectStore.js +103 -1
- package/src/IDBRecord.js +8 -0
- package/src/IDBTransaction.js +16 -2
- package/src/nodeSQLiteDatabase.js +92 -26
- package/src/nodeWebSQL.js +8 -1
|
@@ -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;
|
|
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"}
|
package/dist/nodeWebSQL.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
3
|
+
"version": "17.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Parashuram <code@nparashuram.com>",
|
|
6
6
|
"contributors": [
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"w3c-test": "run-s clean-w3c test-and-start:w3c",
|
|
103
103
|
"w3c-good-only": "NODE_OPTIONS=--max_old_space_size=16384 node test-support/node-idb-test.js good",
|
|
104
104
|
"w3c-bad-only": "NODE_OPTIONS=--max_old_space_size=16384 node test-support/node-idb-test.js bad",
|
|
105
|
+
"w3c-good-worker-only": "NODE_OPTIONS=--max_old_space_size=16384 node test-support/node-idb-test.js good-worker",
|
|
105
106
|
"w3c-good": "run-s w3c-build w3c-good-only clean-w3c",
|
|
106
107
|
"w3c-bad": "run-s w3c-build w3c-bad-only clean-w3c",
|
|
107
108
|
"w3c-good-test": "run-s w3c-good-only clean-w3c",
|
|
@@ -171,8 +172,8 @@
|
|
|
171
172
|
"eventtargeter": "0.12.0",
|
|
172
173
|
"sync-promise-expanded": "^2.0.0",
|
|
173
174
|
"typeson": "10.2.0",
|
|
174
|
-
"typeson-registry": "14.2.
|
|
175
|
-
"websql-configurable": "^3.0
|
|
175
|
+
"typeson-registry": "14.2.2",
|
|
176
|
+
"websql-configurable": "^3.1.0"
|
|
176
177
|
},
|
|
177
178
|
"devDependencies": {
|
|
178
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
|
-
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
|
723
|
+
// Give any microtask-scheduled continuation of the
|
|
672
724
|
// `upgradeneeded` handler (e.g. a plain
|
|
673
|
-
// `Promise.resolve().then(...)
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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);
|
package/src/IDBObjectStore.js
CHANGED
|
@@ -6,6 +6,7 @@ import DOMStringList from './DOMStringList.js';
|
|
|
6
6
|
import * as util from './util.js';
|
|
7
7
|
import * as Key from './Key.js';
|
|
8
8
|
import {executeFetchIndexData, buildFetchIndexDataSQL, IDBIndex} from './IDBIndex.js';
|
|
9
|
+
import cmp from './cmp.js';
|
|
9
10
|
import IDBTransaction from './IDBTransaction.js';
|
|
10
11
|
import * as Sca from './Sca.js';
|
|
11
12
|
import CFG from './CFG.js';
|
|
@@ -69,6 +70,11 @@ const IDBObjectStoreAlias = IDBObjectStore;
|
|
|
69
70
|
* success: (key: import('./Key.js').Key, cn?: Integer) => void,
|
|
70
71
|
* failCb: import('./Key.js').SQLFailureCallback
|
|
71
72
|
* ) => void,
|
|
73
|
+
* __checkIndexConstraints: (
|
|
74
|
+
* tx: WebSQLTransaction,
|
|
75
|
+
* value: import('./Key.js').Value,
|
|
76
|
+
* excludeKey: import('./Key.js').Key|Integer|undefined
|
|
77
|
+
* ) => SyncPromise,
|
|
72
78
|
* __insertData: (
|
|
73
79
|
* tx: WebSQLTransaction,
|
|
74
80
|
* encoded: string,
|
|
@@ -518,6 +524,77 @@ IDBObjectStore.prototype.__deriveKey = function (tx, value, key, success, failCb
|
|
|
518
524
|
}
|
|
519
525
|
};
|
|
520
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Validates `value`'s unique index entries against the table's *current*
|
|
529
|
+
* state without mutating anything -- used by `__storingRecordObjectStore`
|
|
530
|
+
* to check a `put()`'s constraints *before* `__overwrite` deletes the
|
|
531
|
+
* existing row for that key, so a rejected `put()` can't end up losing
|
|
532
|
+
* the record it was trying to replace (see
|
|
533
|
+
* `idbobjectstore-put-unique-index-constraint-is-atomic.any.js`).
|
|
534
|
+
* `excludeKey`, when given, treats a conflicting record as a non-conflict
|
|
535
|
+
* if it's the very record being overwritten (its own current entry would
|
|
536
|
+
* otherwise self-conflict on every no-op `put()` of an unchanged value).
|
|
537
|
+
* `__insertData` below still runs its own (now redundant, but harmless --
|
|
538
|
+
* the old row is gone by then) equivalent check as its first step; this
|
|
539
|
+
* isn't merged into it because `__insertData` also performs the actual
|
|
540
|
+
* `INSERT`, which cannot safely run before `__overwrite`'s `DELETE` when
|
|
541
|
+
* overwriting an existing key.
|
|
542
|
+
* @param {WebSQLTransaction} tx
|
|
543
|
+
* @param {import('./Key.js').Value} value
|
|
544
|
+
* @param {import('./Key.js').Key|Integer|undefined} excludeKey
|
|
545
|
+
* @this {IDBObjectStoreFull}
|
|
546
|
+
* @returns {SyncPromise}
|
|
547
|
+
*/
|
|
548
|
+
IDBObjectStore.prototype.__checkIndexConstraints = function (tx, value, excludeKey) {
|
|
549
|
+
const me = this;
|
|
550
|
+
const indexPromises = Object.keys(me.__indexes).map((indexName) => {
|
|
551
|
+
return new SyncPromise((resolve, reject) => {
|
|
552
|
+
const index = me.__indexes[indexName];
|
|
553
|
+
if (index.__pendingCreate || index.__deleted || !index.unique) {
|
|
554
|
+
resolve(undefined);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* @type {import('./Key.js').KeyValueObject|
|
|
559
|
+
* import('./Key.js').KeyPathEvaluateValue}
|
|
560
|
+
*/
|
|
561
|
+
let indexKey;
|
|
562
|
+
try {
|
|
563
|
+
indexKey = Key.extractKeyValueDecodedFromValueUsingKeyPath(value, index.keyPath, index.multiEntry);
|
|
564
|
+
if (
|
|
565
|
+
('invalid' in indexKey && indexKey.invalid) ||
|
|
566
|
+
('failure' in indexKey && indexKey.failure)
|
|
567
|
+
) {
|
|
568
|
+
throw new Error('Go to catch');
|
|
569
|
+
}
|
|
570
|
+
} catch (err) {
|
|
571
|
+
resolve(undefined);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
indexKey = indexKey.value;
|
|
575
|
+
if (indexKey === undefined) {
|
|
576
|
+
resolve(undefined);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const multiCheck = index.multiEntry && Array.isArray(indexKey);
|
|
580
|
+
const fetchArgs = buildFetchIndexDataSQL(true, index, indexKey, 'key', multiCheck);
|
|
581
|
+
executeFetchIndexData(null, ...fetchArgs, tx, null, function success (key) {
|
|
582
|
+
if (key === undefined || (excludeKey !== undefined && cmp(key, excludeKey) === 0)) {
|
|
583
|
+
resolve(undefined);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
reject(createDOMException(
|
|
587
|
+
'ConstraintError',
|
|
588
|
+
'Index already contains a record equal to ' +
|
|
589
|
+
(multiCheck ? 'one of the subkeys of' : '') +
|
|
590
|
+
'`indexKey`'
|
|
591
|
+
));
|
|
592
|
+
}, reject);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
return SyncPromise.all(indexPromises);
|
|
596
|
+
};
|
|
597
|
+
|
|
521
598
|
/**
|
|
522
599
|
*
|
|
523
600
|
* @param {WebSQLTransaction} tx
|
|
@@ -794,7 +871,32 @@ IDBObjectStore.__storingRecordObjectStore = function (request, store, invalidate
|
|
|
794
871
|
}, error);
|
|
795
872
|
}
|
|
796
873
|
if (!noOverwrite) {
|
|
797
|
-
|
|
874
|
+
// Validate unique index constraints *before* `__overwrite`
|
|
875
|
+
// deletes the existing row at this key -- otherwise a
|
|
876
|
+
// `put()` that ends up rejected still permanently loses
|
|
877
|
+
// the record it was trying to replace (see
|
|
878
|
+
// `idbobjectstore-put-unique-index-constraint-is-atomic.any.js`).
|
|
879
|
+
store.__checkIndexConstraints(tx, value, clonedKeyOrCurrentNumber).then(() => {
|
|
880
|
+
store.__overwrite(tx, clonedKeyOrCurrentNumber, insert, error);
|
|
881
|
+
return undefined;
|
|
882
|
+
/**
|
|
883
|
+
* @param {Error|DOMException} err
|
|
884
|
+
* @returns {null}
|
|
885
|
+
*/
|
|
886
|
+
}).catch(function (err) {
|
|
887
|
+
/**
|
|
888
|
+
* @returns {void}
|
|
889
|
+
*/
|
|
890
|
+
function fail () {
|
|
891
|
+
error(err);
|
|
892
|
+
}
|
|
893
|
+
if (typeof oldCn === 'number') {
|
|
894
|
+
Key.assignCurrentNumber(tx, store, oldCn, fail, fail);
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
fail();
|
|
898
|
+
return null;
|
|
899
|
+
});
|
|
798
900
|
return;
|
|
799
901
|
}
|
|
800
902
|
insert(tx);
|
package/src/IDBRecord.js
CHANGED
|
@@ -77,6 +77,14 @@ readonlyProperties.forEach((prop) => {
|
|
|
77
77
|
* @returns {import('./Key.js').Key|import('./Key.js').Value}
|
|
78
78
|
*/
|
|
79
79
|
get [prop] () {
|
|
80
|
+
// This is defined on the *shared* prototype (see above), so,
|
|
81
|
+
// unlike a per-instance getter, it's reachable by calling it
|
|
82
|
+
// directly on `IDBRecord.prototype` itself (not a real
|
|
83
|
+
// instance) -- WebIDL requires that to throw.
|
|
84
|
+
if (!(this instanceof IDBRecordAlias)) {
|
|
85
|
+
throw new TypeError('Illegal invocation');
|
|
86
|
+
}
|
|
87
|
+
// @ts-ignore `this` is a real instance past the check above (not an issue in TS7)
|
|
80
88
|
return this['__' + prop];
|
|
81
89
|
}
|
|
82
90
|
};
|
package/src/IDBTransaction.js
CHANGED
|
@@ -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 `
|
|
40
|
-
// fire while that retry loop is blocking the same thread -- so
|
|
41
|
-
// waiting,
|
|
42
|
-
// locked".
|
|
43
|
-
//
|
|
44
|
-
//
|
|
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
|
|
47
|
-
/** @type {Map<string, (
|
|
48
|
-
const
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
|
208
|
-
//
|
|
209
|
-
//
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
}
|
|
302
|
+
});
|
|
237
303
|
};
|
|
238
304
|
|
|
239
305
|
export default SQLiteDatabase;
|