indexeddbshim 17.3.2 → 17.3.4
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 +8 -6
- package/dist/IDBTransaction.d.ts.map +1 -1
- package/dist/Key.d.ts.map +1 -1
- package/dist/Sca.d.ts.map +1 -1
- package/dist/indexeddbshim-Key.js +52 -13
- package/dist/indexeddbshim-Key.js.map +1 -1
- package/dist/indexeddbshim-Key.min.js +2 -2
- package/dist/indexeddbshim-Key.min.js.map +1 -1
- package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs +508 -134
- package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs.map +1 -1
- package/dist/indexeddbshim-UnicodeIdentifiers.js +337 -71
- 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 +508 -134
- package/dist/indexeddbshim-node.cjs.map +1 -1
- package/dist/indexeddbshim-noninvasive.js +337 -71
- 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 +337 -71
- 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 -4
- package/src/IDBCursor.js +34 -4
- package/src/IDBFactory.js +207 -60
- package/src/IDBTransaction.js +23 -3
- package/src/Key.js +34 -12
- package/src/Sca.js +53 -3
- package/src/nodeSQLiteDatabase.js +92 -26
- package/src/nodeWebSQL.js +8 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! indexeddbshim - v17.3.
|
|
1
|
+
/*! indexeddbshim - v17.3.4 - 8/25/2026 */
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
@@ -3159,6 +3159,31 @@ function cmp(first, second) {
|
|
|
3159
3159
|
return result;
|
|
3160
3160
|
}
|
|
3161
3161
|
|
|
3162
|
+
/**
|
|
3163
|
+
* @param {unknown[]} arr
|
|
3164
|
+
* @param {number} index
|
|
3165
|
+
* @param {unknown} val
|
|
3166
|
+
* @returns {void}
|
|
3167
|
+
*/
|
|
3168
|
+
const setArrayValue = (arr, index, val) => {
|
|
3169
|
+
if (Reflect.has(Array.prototype, index)) {
|
|
3170
|
+
Object.defineProperty(arr, index, {
|
|
3171
|
+
value: val,
|
|
3172
|
+
enumerable: true,
|
|
3173
|
+
writable: true,
|
|
3174
|
+
configurable: true
|
|
3175
|
+
});
|
|
3176
|
+
} else {
|
|
3177
|
+
arr[index] = val;
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
/**
|
|
3181
|
+
* @param {unknown[]} arr
|
|
3182
|
+
* @param {unknown} val
|
|
3183
|
+
* @returns {void}
|
|
3184
|
+
*/
|
|
3185
|
+
const safePush = (arr, val) => setArrayValue(arr, arr.length, val);
|
|
3186
|
+
|
|
3162
3187
|
/**
|
|
3163
3188
|
* @typedef {NodeJS.TypedArray|DataView} ArrayBufferView
|
|
3164
3189
|
*/
|
|
@@ -3421,12 +3446,13 @@ const types = {
|
|
|
3421
3446
|
* @returns {string}
|
|
3422
3447
|
*/
|
|
3423
3448
|
encode(key) {
|
|
3449
|
+
/** @type {(string|null)[]} */
|
|
3424
3450
|
const encoded = [];
|
|
3425
3451
|
for (const [i, item] of key.entries()) {
|
|
3426
3452
|
const encodedItem = encode$1(item, true); // encode the array item
|
|
3427
|
-
encoded
|
|
3453
|
+
setArrayValue(encoded, i, encodedItem);
|
|
3428
3454
|
}
|
|
3429
|
-
encoded
|
|
3455
|
+
safePush(encoded, keyTypeToEncodedChar.invalid + '-'); // append an extra item, so empty arrays sort correctly
|
|
3430
3456
|
let encodedKey = JSON.stringify(encoded);
|
|
3431
3457
|
if (CFG.escapeNULForSQLiteStatements === false) {
|
|
3432
3458
|
encodedKey = encodedKey.replaceAll(String.raw`\u0000`, '\0');
|
|
@@ -3447,7 +3473,7 @@ const types = {
|
|
|
3447
3473
|
for (let i = 0; i < decoded.length; i++) {
|
|
3448
3474
|
const item = decoded[i];
|
|
3449
3475
|
const decodedItem = decode$1(item, true); // decode the item
|
|
3450
|
-
decoded
|
|
3476
|
+
setArrayValue(decoded, i, decodedItem);
|
|
3451
3477
|
}
|
|
3452
3478
|
return decoded;
|
|
3453
3479
|
}
|
|
@@ -3733,7 +3759,7 @@ function convertValueToKeyValueDecoded(input, seen, multiEntry, fullKeys) {
|
|
|
3733
3759
|
// May throw (from binary)
|
|
3734
3760
|
const arr = /** @type {Array<any>} */input;
|
|
3735
3761
|
const len = arr.length;
|
|
3736
|
-
seen
|
|
3762
|
+
safePush(seen, input);
|
|
3737
3763
|
|
|
3738
3764
|
/** @type {(KeyValueObject|Value)[]} */
|
|
3739
3765
|
const keys = [];
|
|
@@ -3760,7 +3786,7 @@ function convertValueToKeyValueDecoded(input, seen, multiEntry, fullKeys) {
|
|
|
3760
3786
|
};
|
|
3761
3787
|
}
|
|
3762
3788
|
if (!multiEntry || !fullKeys && keys.every(k => cmp(k, key.value) !== 0) || fullKeys && keys.every(k => cmp(k, key) !== 0)) {
|
|
3763
|
-
keys
|
|
3789
|
+
safePush(keys, fullKeys ? key : key.value);
|
|
3764
3790
|
}
|
|
3765
3791
|
} catch (err) {
|
|
3766
3792
|
if (!multiEntry) {
|
|
@@ -3914,7 +3940,7 @@ function evaluateKeyPathOnValueToDecodedValue(value, keyPath, multiEntry, fullKe
|
|
|
3914
3940
|
if (key.failure) {
|
|
3915
3941
|
return true;
|
|
3916
3942
|
}
|
|
3917
|
-
result
|
|
3943
|
+
safePush(result, key.value);
|
|
3918
3944
|
return false;
|
|
3919
3945
|
}) ? {
|
|
3920
3946
|
failure: true
|
|
@@ -3975,11 +4001,21 @@ function injectKeyIntoValueUsingKeyPath(value, key, keyPath) {
|
|
|
3975
4001
|
identifiers.forEach(identifier => {
|
|
3976
4002
|
const hop = Object.hasOwn(value, identifier);
|
|
3977
4003
|
if (!hop) {
|
|
3978
|
-
value
|
|
4004
|
+
Object.defineProperty(value, identifier, {
|
|
4005
|
+
value: {},
|
|
4006
|
+
enumerable: true,
|
|
4007
|
+
writable: true,
|
|
4008
|
+
configurable: true
|
|
4009
|
+
});
|
|
3979
4010
|
}
|
|
3980
4011
|
value = value[identifier];
|
|
3981
4012
|
});
|
|
3982
|
-
value
|
|
4013
|
+
Object.defineProperty(value, /** @type {string} */last, {
|
|
4014
|
+
value: key,
|
|
4015
|
+
enumerable: true,
|
|
4016
|
+
writable: true,
|
|
4017
|
+
configurable: true
|
|
4018
|
+
}); // key is already a `keyValue` in our processing so no need to convert
|
|
3983
4019
|
}
|
|
3984
4020
|
|
|
3985
4021
|
/**
|
|
@@ -4048,6 +4084,7 @@ function isMultiEntryMatch(encodedEntry, encodedKey) {
|
|
|
4048
4084
|
* @returns {Key[]}
|
|
4049
4085
|
*/
|
|
4050
4086
|
function findMultiEntryMatches(keyEntry, range) {
|
|
4087
|
+
/** @type {unknown[]} */
|
|
4051
4088
|
const matches = [];
|
|
4052
4089
|
if (Array.isArray(keyEntry)) {
|
|
4053
4090
|
for (let key of keyEntry) {
|
|
@@ -4060,17 +4097,17 @@ function findMultiEntryMatches(keyEntry, range) {
|
|
|
4060
4097
|
} else {
|
|
4061
4098
|
const nested = findMultiEntryMatches(key, range);
|
|
4062
4099
|
if (nested.length > 0) {
|
|
4063
|
-
matches
|
|
4100
|
+
safePush(matches, key);
|
|
4064
4101
|
}
|
|
4065
4102
|
continue;
|
|
4066
4103
|
}
|
|
4067
4104
|
}
|
|
4068
4105
|
if (isNullish(range) || isKeyInRange(key, range, true)) {
|
|
4069
|
-
matches
|
|
4106
|
+
safePush(matches, key);
|
|
4070
4107
|
}
|
|
4071
4108
|
}
|
|
4072
4109
|
} else if (isNullish(range) || isKeyInRange(keyEntry, range, true)) {
|
|
4073
|
-
matches
|
|
4110
|
+
safePush(matches, keyEntry);
|
|
4074
4111
|
}
|
|
4075
4112
|
return matches;
|
|
4076
4113
|
}
|
|
@@ -4094,12 +4131,13 @@ function convertKeyToValue(key) {
|
|
|
4094
4131
|
}
|
|
4095
4132
|
case 'array':
|
|
4096
4133
|
{
|
|
4134
|
+
/** @type {ValueType[]} */
|
|
4097
4135
|
const array = [];
|
|
4098
4136
|
const len = value.length;
|
|
4099
4137
|
let index = 0;
|
|
4100
4138
|
while (index < len) {
|
|
4101
4139
|
const entry = convertKeyToValue(value[index]);
|
|
4102
|
-
array
|
|
4140
|
+
setArrayValue(array, index, entry);
|
|
4103
4141
|
index++;
|
|
4104
4142
|
}
|
|
4105
4143
|
return array;
|
|
@@ -4862,6 +4900,7 @@ if (cleanInterface) {
|
|
|
4862
4900
|
}
|
|
4863
4901
|
|
|
4864
4902
|
let uniqueID = 0;
|
|
4903
|
+
const activeTransactions = new Set();
|
|
4865
4904
|
const listeners$1 = ['onabort', 'oncomplete', 'onerror'];
|
|
4866
4905
|
const readonlyProperties$4 = ['objectStoreNames', 'mode', 'durability', 'db', 'error'];
|
|
4867
4906
|
|
|
@@ -4991,6 +5030,7 @@ IDBTransaction.__createInstance = function (db, storeNames, mode, durability = '
|
|
|
4991
5030
|
me.__mode = mode;
|
|
4992
5031
|
me.__durability = durability;
|
|
4993
5032
|
me.__db = db;
|
|
5033
|
+
activeTransactions.add(me);
|
|
4994
5034
|
me.__error = null;
|
|
4995
5035
|
// @ts-expect-error Part of `ShimEventTarget`
|
|
4996
5036
|
me.__setOptions({
|
|
@@ -5498,6 +5538,7 @@ IDBTransaction.prototype.__executeRequests = function () {
|
|
|
5498
5538
|
me.__errored = true;
|
|
5499
5539
|
throw e;
|
|
5500
5540
|
} finally {
|
|
5541
|
+
activeTransactions.delete(me);
|
|
5501
5542
|
me.__storeHandles = {};
|
|
5502
5543
|
}
|
|
5503
5544
|
}
|
|
@@ -5704,13 +5745,19 @@ IDBTransaction.prototype.__abortTransaction = function (err) {
|
|
|
5704
5745
|
});
|
|
5705
5746
|
}
|
|
5706
5747
|
me.__active = false; // Setting here and in requestsFinished for https://github.com/w3c/IndexedDB/issues/87
|
|
5707
|
-
|
|
5748
|
+
activeTransactions.delete(me);
|
|
5708
5749
|
if (err !== null) {
|
|
5709
5750
|
me.__error = err;
|
|
5710
5751
|
}
|
|
5711
|
-
if (me.__requestsFinished) {
|
|
5752
|
+
if (me.__requestsFinished && err !== null) {
|
|
5712
5753
|
// The transaction has already completed, so we can't call "onerror" or "onabort".
|
|
5713
|
-
// So throw the error instead.
|
|
5754
|
+
// So throw the error instead. `err` is only ever `null` here via
|
|
5755
|
+
// `IDBTransaction.prototype.abort`'s own `__abortTransaction(null)`
|
|
5756
|
+
// call, which now checks `__requestsFinished` itself first and
|
|
5757
|
+
// throws `InvalidStateError` synchronously before ever reaching
|
|
5758
|
+
// this point -- so this guard is just defense in depth against
|
|
5759
|
+
// `err` somehow being `null` some other way, not something this
|
|
5760
|
+
// path should see in practice.
|
|
5714
5761
|
setTimeout(() => {
|
|
5715
5762
|
throw err;
|
|
5716
5763
|
}, 0);
|
|
@@ -5828,6 +5875,14 @@ IDBTransaction.prototype.abort = function () {
|
|
|
5828
5875
|
if (me.__committed) {
|
|
5829
5876
|
throw createDOMException('InvalidStateError', 'The transaction has already been committed');
|
|
5830
5877
|
}
|
|
5878
|
+
if (me.__requestsFinished) {
|
|
5879
|
+
// All requests have already finished and the transaction is
|
|
5880
|
+
// auto-committing (the async SQL commit round trip just hasn't
|
|
5881
|
+
// resolved yet) -- too late to abort per spec, even though
|
|
5882
|
+
// `__committed` itself isn't set until that round trip actually
|
|
5883
|
+
// finishes.
|
|
5884
|
+
throw createDOMException('InvalidStateError', 'The transaction is already committing');
|
|
5885
|
+
}
|
|
5831
5886
|
me.__abortTransaction(null);
|
|
5832
5887
|
};
|
|
5833
5888
|
|
|
@@ -5909,7 +5964,7 @@ IDBTransaction.__assertNotFinishedObjectStoreMethod = function (tx) {
|
|
|
5909
5964
|
* @returns {void}
|
|
5910
5965
|
*/
|
|
5911
5966
|
IDBTransaction.__assertActive = function (tx) {
|
|
5912
|
-
if (!tx || !tx.__active || tx.__committed) {
|
|
5967
|
+
if (!tx || !tx.__active || !tx.__handlerActive || tx.__committed) {
|
|
5913
5968
|
throw createDOMException('TransactionInactiveError', 'A request was placed against a transaction which is currently not active, or which is finished');
|
|
5914
5969
|
}
|
|
5915
5970
|
};
|
|
@@ -5936,6 +5991,9 @@ Object.defineProperty(IDBTransaction.prototype, 'constructor', {
|
|
|
5936
5991
|
Object.defineProperty(IDBTransaction, 'prototype', {
|
|
5937
5992
|
writable: false
|
|
5938
5993
|
});
|
|
5994
|
+
/* eslint-enable unicorn/no-top-level-side-effects -- Would be good */
|
|
5995
|
+
|
|
5996
|
+
IDBTransaction.activeTransactions = activeTransactions;
|
|
5939
5997
|
|
|
5940
5998
|
class TypesonPromise {
|
|
5941
5999
|
constructor(e) {
|
|
@@ -7081,14 +7139,14 @@ const B = {
|
|
|
7081
7139
|
revive: () => 1 / 0
|
|
7082
7140
|
}
|
|
7083
7141
|
},
|
|
7084
|
-
|
|
7142
|
+
D = {
|
|
7085
7143
|
map: {
|
|
7086
7144
|
test: e => "Map" === toStringTag(e),
|
|
7087
7145
|
replace: e => e.entries().toArray(),
|
|
7088
7146
|
revive: e => new Map(e)
|
|
7089
7147
|
}
|
|
7090
7148
|
},
|
|
7091
|
-
|
|
7149
|
+
R = {
|
|
7092
7150
|
nan: {
|
|
7093
7151
|
test: e => Number.isNaN(e),
|
|
7094
7152
|
replace: () => "NaN",
|
|
@@ -7343,8 +7401,8 @@ const Y = {
|
|
|
7343
7401
|
revive() {}
|
|
7344
7402
|
}
|
|
7345
7403
|
}],
|
|
7346
|
-
ne = [
|
|
7347
|
-
ce = [Z, Y, re, K, ne, O, z, U, _, B, I, h, N, C].concat("function" == typeof Map ?
|
|
7404
|
+
ne = [R, M, L, F],
|
|
7405
|
+
ce = [Z, Y, re, K, ne, O, z, U, _, B, I, h, N, C].concat("function" == typeof Map ? D : [], "function" == typeof Set ? H : [], "function" == typeof ArrayBuffer ? p : [], "function" == typeof Uint8Array ? X : [], "function" == typeof DataView ? w : [], "undefined" != typeof crypto ? v : [], "undefined" != typeof BigInt ? [m, d] : [], "undefined" != typeof DOMException ? A : [], "undefined" != typeof QuotaExceededError ? W : [], "undefined" != typeof WebTransportError ? te : [], "undefined" != typeof DOMRect ? x : [], "undefined" != typeof DOMPoint ? S : [], "undefined" != typeof DOMQuad ? P : [], "undefined" != typeof DOMMatrix ? T : [], "undefined" != typeof AudioData ? f : [], "undefined" != typeof EncodedAudioChunk ? E : [], "undefined" != typeof EncodedVideoChunk ? j : [], "undefined" != typeof VideoFrame ? ee : []);
|
|
7348
7406
|
const ue = ce.concat({
|
|
7349
7407
|
checkDataCloneException: {
|
|
7350
7408
|
test(e) {
|
|
@@ -7361,11 +7419,53 @@ const ue = ce.concat({
|
|
|
7361
7419
|
return false;
|
|
7362
7420
|
}
|
|
7363
7421
|
}
|
|
7422
|
+
}),
|
|
7423
|
+
ye = ue.concat({
|
|
7424
|
+
checkSharedArrayBufferException: {
|
|
7425
|
+
test(e) {
|
|
7426
|
+
if ("SharedArrayBuffer" === {}.toString.call(e).slice(8, -1)) throw new DOMException("The object cannot be cloned.", "DataCloneError");
|
|
7427
|
+
return false;
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7364
7430
|
});
|
|
7365
7431
|
|
|
7366
7432
|
// See: https://stackoverflow.com/questions/42170826/categories-for-rejection-by-the-structured-cloning-algorithm
|
|
7367
7433
|
|
|
7368
|
-
|
|
7434
|
+
// Although typeson-registry already has a FileList type in its structured cloning presets,
|
|
7435
|
+
// we need to override it so it works with our tests
|
|
7436
|
+
|
|
7437
|
+
const specSet = ye.flatMap(preset => Array.isArray(preset) ? preset : [preset]).find(preset => preset && !Array.isArray(preset) && 'filelist' in preset);
|
|
7438
|
+
const origFileList = specSet && !Array.isArray(specSet) && 'filelist' in specSet ? specSet.filelist : undefined;
|
|
7439
|
+
const origTest = origFileList && typeof origFileList === 'object' && 'test' in origFileList && typeof origFileList.test === 'function' ? origFileList.test : undefined;
|
|
7440
|
+
const origRevive = origFileList && typeof origFileList === 'object' && 'revive' in origFileList && typeof origFileList.revive === 'function' ? origFileList.revive : undefined;
|
|
7441
|
+
const customFileList = origFileList ? {
|
|
7442
|
+
...origFileList,
|
|
7443
|
+
/**
|
|
7444
|
+
* @param {unknown} x
|
|
7445
|
+
* @param {import('typeson').StateObject} state
|
|
7446
|
+
* @returns {boolean}
|
|
7447
|
+
*/
|
|
7448
|
+
test(x, state) {
|
|
7449
|
+
if (typeof FileList !== 'undefined') {
|
|
7450
|
+
return x instanceof FileList;
|
|
7451
|
+
}
|
|
7452
|
+
return typeof origTest === 'function' ? origTest(x, state) : false;
|
|
7453
|
+
},
|
|
7454
|
+
/**
|
|
7455
|
+
* @param {unknown} x
|
|
7456
|
+
* @param {import('typeson').StateObject} state
|
|
7457
|
+
* @returns {unknown}
|
|
7458
|
+
*/
|
|
7459
|
+
revive(x, state) {
|
|
7460
|
+
if (typeof FileList !== 'undefined') {
|
|
7461
|
+
return Reflect.construct(FileList, [x]);
|
|
7462
|
+
}
|
|
7463
|
+
return typeof origRevive === 'function' ? origRevive(x, state) : undefined;
|
|
7464
|
+
}
|
|
7465
|
+
} : undefined;
|
|
7466
|
+
let typeson = new Typeson().register([ye, customFileList ? {
|
|
7467
|
+
filelist: customFileList
|
|
7468
|
+
} : {}]);
|
|
7369
7469
|
|
|
7370
7470
|
/**
|
|
7371
7471
|
* @param {(preset: import('typeson-registry').Preset) =>
|
|
@@ -7374,7 +7474,7 @@ let typeson = new Typeson().register(ue);
|
|
|
7374
7474
|
*/
|
|
7375
7475
|
function register(func) {
|
|
7376
7476
|
// eslint-disable-next-line unicorn/no-top-level-assignment-in-function -- Should be one-time cache
|
|
7377
|
-
typeson = new Typeson().register(func(
|
|
7477
|
+
typeson = new Typeson().register(func(ye));
|
|
7378
7478
|
}
|
|
7379
7479
|
|
|
7380
7480
|
/**
|
|
@@ -10063,7 +10163,29 @@ function triggerAnyVersionChangeAndBlockedEvents(openConnections, req, oldVersio
|
|
|
10063
10163
|
return new SyncPromise(function (resolve) {
|
|
10064
10164
|
setTimeout(() => {
|
|
10065
10165
|
entry.dispatchEvent(e); // No need to catch errors
|
|
10066
|
-
|
|
10166
|
+
// Unlike a native `Promise`, `SyncPromise#then` chains
|
|
10167
|
+
// synchronously off `resolve()` (verified directly:
|
|
10168
|
+
// `SyncPromise.resolve().then(cb)` runs `cb` before
|
|
10169
|
+
// even the calling code below it finishes) -- so
|
|
10170
|
+
// resolving immediately here would let the
|
|
10171
|
+
// `connectionsClosed()` check below run before a
|
|
10172
|
+
// same-task-but-microtask-later continuation of a
|
|
10173
|
+
// `versionchange` listener above (e.g. an `await`-
|
|
10174
|
+
// based one that then calls `db.close()`, as in
|
|
10175
|
+
// `transaction-lifetime.any.js`) ever gets a turn,
|
|
10176
|
+
// incorrectly firing `blocked` even though the
|
|
10177
|
+
// connection was about to close in time. Give real
|
|
10178
|
+
// microtask-deferred continuations a small, bounded
|
|
10179
|
+
// number of turns first -- same pattern as
|
|
10180
|
+
// `IDBTransaction.js`'s `checkQueueEntry`.
|
|
10181
|
+
let attemptsLeft = 10;
|
|
10182
|
+
(function wait() {
|
|
10183
|
+
if (attemptsLeft-- <= 0) {
|
|
10184
|
+
resolve(undefined);
|
|
10185
|
+
return;
|
|
10186
|
+
}
|
|
10187
|
+
queueMicrotask(wait);
|
|
10188
|
+
})();
|
|
10067
10189
|
}, 0);
|
|
10068
10190
|
});
|
|
10069
10191
|
});
|
|
@@ -10123,6 +10245,23 @@ function triggerAnyVersionChangeAndBlockedEvents(openConnections, req, oldVersio
|
|
|
10123
10245
|
*/
|
|
10124
10246
|
const websqlDBCache = {};
|
|
10125
10247
|
|
|
10248
|
+
/**
|
|
10249
|
+
* Tracks databases with a creation/upgrade currently in flight but not yet
|
|
10250
|
+
* committed or aborted -- keyed by (unescaped) database name. The
|
|
10251
|
+
* `dbVersions` row in `sysdb` these entries shadow is written *before*
|
|
10252
|
+
* `upgradeneeded` is even dispatched to user code (as the success
|
|
10253
|
+
* callback of that very `INSERT`/`UPDATE`), on the same shared `sysdb`
|
|
10254
|
+
* connection `databases()` itself reads from -- so, absent this, a
|
|
10255
|
+
* `databases()` call made while an upgrade is still pending would see
|
|
10256
|
+
* the new row (or new version) immediately, rather than only once the
|
|
10257
|
+
* corresponding `versionchange` transaction has genuinely committed, as
|
|
10258
|
+
* required by `get-databases.any.js`'s "doesn't pick up changes that
|
|
10259
|
+
* haven't committed" test. `IDBFactory.prototype.databases` filters
|
|
10260
|
+
* its SQL results against this map.
|
|
10261
|
+
* @type {Map<string, {oldVersion: Integer, newVersion: Integer}>}
|
|
10262
|
+
*/
|
|
10263
|
+
const pendingVersionChanges = new Map();
|
|
10264
|
+
|
|
10126
10265
|
/** @type {import('websql-configurable/lib/websql/WebSQLDatabase.js').default} */
|
|
10127
10266
|
let sysdb;
|
|
10128
10267
|
let nameCounter = 0;
|
|
@@ -10396,6 +10535,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
|
|
|
10396
10535
|
}
|
|
10397
10536
|
const req = IDBOpenDBRequest.__createInstance();
|
|
10398
10537
|
let calledDbCreateError = false;
|
|
10538
|
+
let isRevertingSysdb = false;
|
|
10399
10539
|
if (CFG.autoName && name === '') {
|
|
10400
10540
|
// eslint-disable-next-line unicorn/no-top-level-assignment-in-function -- Necessary?
|
|
10401
10541
|
name = 'autoNamedDatabase_' + nameCounter++;
|
|
@@ -10422,9 +10562,22 @@ IDBFactory.prototype.open = function (name /* , version */) {
|
|
|
10422
10562
|
* @returns {boolean}
|
|
10423
10563
|
*/
|
|
10424
10564
|
function dbCreateError(tx, err) {
|
|
10425
|
-
if (calledDbCreateError) {
|
|
10565
|
+
if (calledDbCreateError || isRevertingSysdb) {
|
|
10426
10566
|
return false;
|
|
10427
10567
|
}
|
|
10568
|
+
// Defensive cleanup: `pendingVersionChanges.set(name, ...)` (see
|
|
10569
|
+
// below) runs *before* the `dbVersions` `INSERT`/`UPDATE` it
|
|
10570
|
+
// guards is even attempted, but only `versionSet`'s own
|
|
10571
|
+
// `on__beforecomplete`/`on__preabort` handlers ever clear it --
|
|
10572
|
+
// and `versionSet` never runs at all if that `INSERT`/`UPDATE`,
|
|
10573
|
+
// or an earlier step in this same `open()` flow, fails first.
|
|
10574
|
+
// Without this, such a failure would leave the entry orphaned
|
|
10575
|
+
// forever, silently corrupting `databases()` for any *other*,
|
|
10576
|
+
// unrelated later `open()` call that happens to reuse the same
|
|
10577
|
+
// database name (common in WPT tests, e.g. generic names like
|
|
10578
|
+
// "DB1"/"TestDatabase" reused across different test files in
|
|
10579
|
+
// the same process). A no-op if no entry was ever set.
|
|
10580
|
+
pendingVersionChanges.delete(name);
|
|
10428
10581
|
const er = err ? webSQLErrback(err) : (/** @type {Error} */tx);
|
|
10429
10582
|
calledDbCreateError = true;
|
|
10430
10583
|
// Re: why bubbling here (and how cancelable is only really relevant for `window.onerror`) see: https://github.com/w3c/IndexedDB/issues/86
|
|
@@ -10473,36 +10626,46 @@ IDBFactory.prototype.open = function (name /* , version */) {
|
|
|
10473
10626
|
*/
|
|
10474
10627
|
let sysdbFinishedCb = function (systx, err, cb) {
|
|
10475
10628
|
if (err) {
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10629
|
+
/**
|
|
10630
|
+
* @param {any} [errorToShow]
|
|
10631
|
+
* @returns {void}
|
|
10632
|
+
*/
|
|
10633
|
+
const manualRevert = function (errorToShow) {
|
|
10634
|
+
/**
|
|
10635
|
+
* @param {string} [msg]
|
|
10636
|
+
* @throws {Error}
|
|
10637
|
+
* @returns {never}
|
|
10638
|
+
*/
|
|
10639
|
+
function reportError(msg) {
|
|
10640
|
+
throw new Error('Unable to roll back upgrade transaction!' + (msg || ''));
|
|
10641
|
+
}
|
|
10481
10642
|
sysdb.transaction(function (systx) {
|
|
10482
|
-
/**
|
|
10483
|
-
*
|
|
10484
|
-
* @param {string} msg
|
|
10485
|
-
* @throws {Error}
|
|
10486
|
-
* @returns {never}
|
|
10487
|
-
*/
|
|
10488
|
-
function reportError(msg) {
|
|
10489
|
-
throw new Error('Unable to roll back upgrade transaction!' + (msg || ''));
|
|
10490
|
-
}
|
|
10491
|
-
|
|
10492
10643
|
// Attempt to revert
|
|
10493
10644
|
if (oldVersion === 0) {
|
|
10494
|
-
systx.executeSql('DELETE FROM dbVersions WHERE "name" = ?', [sqlSafeName]
|
|
10495
|
-
// @ts-expect-error Force to work
|
|
10496
|
-
cb(reportError); // eslint-disable-line promise/no-callback-in-promise -- Convenient
|
|
10497
|
-
},
|
|
10498
|
-
// @ts-expect-error Force to work
|
|
10499
|
-
reportError);
|
|
10645
|
+
systx.executeSql('DELETE FROM dbVersions WHERE "name" = ?', [sqlSafeName]);
|
|
10500
10646
|
} else {
|
|
10501
|
-
systx.executeSql('UPDATE dbVersions SET "version" = ? WHERE "name" = ?', [oldVersion, sqlSafeName]
|
|
10502
|
-
// @ts-expect-error Force to work
|
|
10503
|
-
reportError);
|
|
10647
|
+
systx.executeSql('UPDATE dbVersions SET "version" = ? WHERE "name" = ?', [oldVersion, sqlSafeName]);
|
|
10504
10648
|
}
|
|
10649
|
+
}, function (sqlErr) {
|
|
10650
|
+
isRevertingSysdb = false;
|
|
10651
|
+
cb(sqlErr); // eslint-disable-line promise/no-callback-in-promise -- Convenient
|
|
10652
|
+
}, function () {
|
|
10653
|
+
isRevertingSysdb = false;
|
|
10654
|
+
cb(errorToShow || reportError); // eslint-disable-line promise/no-callback-in-promise -- Convenient
|
|
10505
10655
|
});
|
|
10656
|
+
};
|
|
10657
|
+
try {
|
|
10658
|
+
systx.executeSql('ROLLBACK', [], function () {
|
|
10659
|
+
cb(); // eslint-disable-line promise/no-callback-in-promise -- Convenient
|
|
10660
|
+
}, function (tx, sqlErr) {
|
|
10661
|
+
// Browser/Node may fail with expired transaction, so manually revert
|
|
10662
|
+
manualRevert(sqlErr);
|
|
10663
|
+
return false;
|
|
10664
|
+
});
|
|
10665
|
+
} catch (e) {
|
|
10666
|
+
// Browser may fail with expired transaction above so
|
|
10667
|
+
// no choice but to manually revert
|
|
10668
|
+
manualRevert(e);
|
|
10506
10669
|
}
|
|
10507
10670
|
return;
|
|
10508
10671
|
}
|
|
@@ -10536,22 +10699,43 @@ IDBFactory.prototype.open = function (name /* , version */) {
|
|
|
10536
10699
|
// open/close the transaction's active-handler window itself.
|
|
10537
10700
|
req.transaction.__handlerActive = true;
|
|
10538
10701
|
req.dispatchEvent(e);
|
|
10539
|
-
// Give any
|
|
10702
|
+
// Give any microtask-scheduled continuation of the
|
|
10540
10703
|
// `upgradeneeded` handler (e.g. a plain
|
|
10541
|
-
// `Promise.resolve().then(...)
|
|
10542
|
-
//
|
|
10543
|
-
//
|
|
10544
|
-
//
|
|
10545
|
-
//
|
|
10546
|
-
//
|
|
10547
|
-
//
|
|
10548
|
-
//
|
|
10549
|
-
//
|
|
10550
|
-
//
|
|
10551
|
-
//
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10704
|
+
// `Promise.resolve().then(...)`, or an `await`-based
|
|
10705
|
+
// continuation of a promise resolved from within the handler,
|
|
10706
|
+
// such as testharness.js's own `EventWatcher`) a chance to
|
|
10707
|
+
// run -- and still observe the transaction as active -- before
|
|
10708
|
+
// we deactivate it again, per
|
|
10709
|
+
// https://github.com/w3c/IndexedDB/issues/87. A single deferred
|
|
10710
|
+
// tick isn't always enough: an `await`-based continuation can
|
|
10711
|
+
// take more than one microtask turn to resume (e.g.
|
|
10712
|
+
// `transaction-lifetime.any.js`'s `EventWatcher`-based
|
|
10713
|
+
// `await eventWatcher.wait_for('upgradeneeded')`), so retry a
|
|
10714
|
+
// small, bounded number of times first -- same pattern as
|
|
10715
|
+
// `IDBTransaction.js`'s `checkQueueEntry` -- rather than
|
|
10716
|
+
// resetting after only one. Only the flag reset itself is
|
|
10717
|
+
// deferred here -- unlike `IDBTransaction.js`'s
|
|
10718
|
+
// `advanceAfterDispatch`, `finished()` (this transaction's own
|
|
10719
|
+
// queue-advancement/completion signal) still runs
|
|
10720
|
+
// synchronously, at exactly its previous timing: an earlier
|
|
10721
|
+
// attempt at deferring `finished()` too raced against unrelated
|
|
10722
|
+
// test setup that assumed a freshly deleted/created database's
|
|
10723
|
+
// upgrade transaction had already fully completed by the time
|
|
10724
|
+
// this function returns.
|
|
10725
|
+
/**
|
|
10726
|
+
* @param {Integer} attemptsLeft
|
|
10727
|
+
* @returns {void}
|
|
10728
|
+
*/
|
|
10729
|
+
function deferHandlerActiveReset(attemptsLeft) {
|
|
10730
|
+
if (attemptsLeft <= 0) {
|
|
10731
|
+
req.transaction.__handlerActive = false;
|
|
10732
|
+
return;
|
|
10733
|
+
}
|
|
10734
|
+
queueMicrotask(() => {
|
|
10735
|
+
deferHandlerActiveReset(attemptsLeft - 1);
|
|
10736
|
+
});
|
|
10737
|
+
}
|
|
10738
|
+
deferHandlerActiveReset(10);
|
|
10555
10739
|
if (e.__legacyOutputDidListenersThrowError) {
|
|
10556
10740
|
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
10741
|
req.transaction.__abortTransaction(createDOMException('AbortError', 'A request was aborted.'));
|
|
@@ -10567,6 +10751,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
|
|
|
10567
10751
|
* @returns {void}
|
|
10568
10752
|
*/
|
|
10569
10753
|
function (ev) {
|
|
10754
|
+
pendingVersionChanges.delete(name);
|
|
10570
10755
|
connection.__upgradeTransaction = null;
|
|
10571
10756
|
/** @type {import('./IDBDatabase.js').IDBDatabaseFull} */
|
|
10572
10757
|
req.__result.__versionTransaction = null;
|
|
@@ -10581,6 +10766,8 @@ IDBFactory.prototype.open = function (name /* , version */) {
|
|
|
10581
10766
|
|
|
10582
10767
|
// eslint-disable-next-line camelcase -- Clear API
|
|
10583
10768
|
req.transaction.on__preabort = function () {
|
|
10769
|
+
isRevertingSysdb = true;
|
|
10770
|
+
pendingVersionChanges.delete(name);
|
|
10584
10771
|
connection.__upgradeTransaction = null;
|
|
10585
10772
|
// We ensure any cache is deleted before any request error events fire and try to reopen
|
|
10586
10773
|
if (useDatabaseCache) {
|
|
@@ -10597,6 +10784,7 @@ IDBFactory.prototype.open = function (name /* , version */) {
|
|
|
10597
10784
|
req.__result = undefined;
|
|
10598
10785
|
req.__done = false;
|
|
10599
10786
|
connection.close();
|
|
10787
|
+
isRevertingSysdb = true;
|
|
10600
10788
|
setTimeout(() => {
|
|
10601
10789
|
const err = createDOMException('AbortError', 'The upgrade transaction was aborted.');
|
|
10602
10790
|
sysdbFinishedCb(systx, err, function (reportError) {
|
|
@@ -10643,6 +10831,10 @@ IDBFactory.prototype.open = function (name /* , version */) {
|
|
|
10643
10831
|
// });
|
|
10644
10832
|
};
|
|
10645
10833
|
}
|
|
10834
|
+
pendingVersionChanges.set(name, {
|
|
10835
|
+
oldVersion,
|
|
10836
|
+
newVersion: version
|
|
10837
|
+
});
|
|
10646
10838
|
if (oldVersion === 0) {
|
|
10647
10839
|
systx.executeSql('INSERT INTO dbVersions VALUES (?,?)', [sqlSafeName, version], versionSet, dbCreateError);
|
|
10648
10840
|
} else {
|
|
@@ -10654,7 +10846,26 @@ IDBFactory.prototype.open = function (name /* , version */) {
|
|
|
10654
10846
|
}
|
|
10655
10847
|
sysdbFinishedCb = function (systx, err, cb) {
|
|
10656
10848
|
if (err) {
|
|
10657
|
-
rollback(err,
|
|
10849
|
+
rollback(err,
|
|
10850
|
+
/**
|
|
10851
|
+
* @param {Error} [reportError]
|
|
10852
|
+
* @returns {void}
|
|
10853
|
+
*/
|
|
10854
|
+
function (reportError) {
|
|
10855
|
+
sysdb.transaction(function (systx) {
|
|
10856
|
+
if (oldVersion === 0) {
|
|
10857
|
+
systx.executeSql('DELETE FROM dbVersions WHERE "name" = ?', [sqlSafeName]);
|
|
10858
|
+
} else {
|
|
10859
|
+
systx.executeSql('UPDATE dbVersions SET "version" = ? WHERE "name" = ?', [oldVersion, sqlSafeName]);
|
|
10860
|
+
}
|
|
10861
|
+
}, function (sqlErr) {
|
|
10862
|
+
isRevertingSysdb = false;
|
|
10863
|
+
cb(sqlErr); // eslint-disable-line promise/no-callback-in-promise -- Convenient
|
|
10864
|
+
}, function () {
|
|
10865
|
+
isRevertingSysdb = false;
|
|
10866
|
+
cb(reportError); // eslint-disable-line promise/no-callback-in-promise -- Convenient
|
|
10867
|
+
});
|
|
10868
|
+
});
|
|
10658
10869
|
} else {
|
|
10659
10870
|
commit(cb);
|
|
10660
10871
|
}
|
|
@@ -10946,6 +11157,15 @@ IDBFactory.prototype.cmp = function (key1, key2) {
|
|
|
10946
11157
|
IDBFactory.prototype.databases = function () {
|
|
10947
11158
|
const me = this;
|
|
10948
11159
|
let calledDbCreateError = false;
|
|
11160
|
+
// Snapshotted *now*, synchronously, at call time -- not read later
|
|
11161
|
+
// from inside the SQL query's callback below, which runs on a
|
|
11162
|
+
// deferred macrotask (see `nodeSQLiteDatabase.js`'s `exec`) and so
|
|
11163
|
+
// could otherwise race against (and lose to) an in-flight upgrade's
|
|
11164
|
+
// own commit/abort handler clearing its `pendingVersionChanges`
|
|
11165
|
+
// entry in the meantime -- which would make this method incorrectly
|
|
11166
|
+
// reflect a since-committed change that hadn't committed yet when
|
|
11167
|
+
// it was actually called.
|
|
11168
|
+
const pendingVersionChangesSnapshot = new Map(pendingVersionChanges);
|
|
10949
11169
|
return new Promise(function (resolve, reject) {
|
|
10950
11170
|
// eslint-disable-line promise/avoid-new -- Own polyfill
|
|
10951
11171
|
if (!(me instanceof IDBFactory)) {
|
|
@@ -10975,11 +11195,30 @@ IDBFactory.prototype.databases = function () {
|
|
|
10975
11195
|
const dbNames = [];
|
|
10976
11196
|
for (let i = 0; i < data.rows.length; i++) {
|
|
10977
11197
|
const {
|
|
10978
|
-
name,
|
|
11198
|
+
name: encodedName,
|
|
10979
11199
|
version
|
|
10980
11200
|
} = /** @type {{name: string, version: Integer}} */data.rows.item(i);
|
|
11201
|
+
const name = unescapeSQLiteResponse(encodedName);
|
|
11202
|
+
// A row for a database whose creation/upgrade hasn't
|
|
11203
|
+
// committed yet (see `pendingVersionChanges`) must
|
|
11204
|
+
// not reflect that in-flight change: a brand new
|
|
11205
|
+
// database (`oldVersion === 0`) isn't reported at
|
|
11206
|
+
// all until its creation commits, and an existing
|
|
11207
|
+
// database being upgraded is still reported, but
|
|
11208
|
+
// with its pre-upgrade version.
|
|
11209
|
+
const pending = pendingVersionChangesSnapshot.get(name);
|
|
11210
|
+
if (pending) {
|
|
11211
|
+
if (pending.oldVersion === 0) {
|
|
11212
|
+
continue;
|
|
11213
|
+
}
|
|
11214
|
+
dbNames.push({
|
|
11215
|
+
name,
|
|
11216
|
+
version: pending.oldVersion
|
|
11217
|
+
});
|
|
11218
|
+
continue;
|
|
11219
|
+
}
|
|
10981
11220
|
dbNames.push({
|
|
10982
|
-
name
|
|
11221
|
+
name,
|
|
10983
11222
|
version
|
|
10984
11223
|
});
|
|
10985
11224
|
}
|
|
@@ -11392,9 +11631,26 @@ IDBCursor.prototype.__findBasic = function (key, primaryKey, tx, success, error,
|
|
|
11392
11631
|
// Key.convertValueToKey(key); // Already checked by `continue` or `continuePrimaryKey`
|
|
11393
11632
|
sqlValues.push(/** @type {string} */encode$1(key));
|
|
11394
11633
|
} else if (continueCall && me.__key !== undefined) {
|
|
11395
|
-
sql.push('AND', quotedKeyColumnName, op + ' ?');
|
|
11396
11634
|
// Key.convertValueToKey(me.__key); // Already checked when stored
|
|
11397
|
-
|
|
11635
|
+
if (!me.__unique && me.__keyColumnName !== 'key' && me.__primaryKey !== undefined) {
|
|
11636
|
+
// A plain `continue()` on a non-unique index cursor must find
|
|
11637
|
+
// the next record strictly after the (key, primaryKey) pair
|
|
11638
|
+
// this cursor last returned -- a scalar `key > lastKey` alone
|
|
11639
|
+
// would wrongly exclude a *different*, not-yet-visited record
|
|
11640
|
+
// that's still tied on key with the last one (e.g. another
|
|
11641
|
+
// record that always had that same indexed value), and would
|
|
11642
|
+
// also wrongly re-admit the *same* record forever if a
|
|
11643
|
+
// same-transaction `update()` bumped its own key back above
|
|
11644
|
+
// the threshold, since a scalar comparison can't distinguish
|
|
11645
|
+
// "some other record newly tied" from "this record moved
|
|
11646
|
+
// past its own last position." Comparing the full tuple (via
|
|
11647
|
+
// this OR) against both key and primary key resolves both.
|
|
11648
|
+
sql.push('AND (', quotedKeyColumnName, op, '?', 'OR (', quotedKeyColumnName, '= ?', 'AND', quotedKey, op, '?))');
|
|
11649
|
+
sqlValues.push(/** @type {string} */encode$1(me.__key), /** @type {string} */encode$1(me.__key), /** @type {string} */encode$1(me.__primaryKey));
|
|
11650
|
+
} else {
|
|
11651
|
+
sql.push('AND', quotedKeyColumnName, op + ' ?');
|
|
11652
|
+
sqlValues.push(/** @type {string} */encode$1(me.__key));
|
|
11653
|
+
}
|
|
11398
11654
|
}
|
|
11399
11655
|
if (!me.__count) {
|
|
11400
11656
|
// 1. Sort by key
|
|
@@ -12008,9 +12264,15 @@ IDBCursor.prototype.update = function (valueToUpdate) {
|
|
|
12008
12264
|
* @returns {void}
|
|
12009
12265
|
*/
|
|
12010
12266
|
function addToQueue(clonedValue) {
|
|
12011
|
-
//
|
|
12267
|
+
// `invalidateCache: true` so any cursor on this store (including
|
|
12268
|
+
// this one) drops its prefetched row batch, forcing its next
|
|
12269
|
+
// `continue()` to re-query live rather than serve rows snapshotted
|
|
12270
|
+
// before this update -- needed for the compound-tuple
|
|
12271
|
+
// continuation logic in `__findBasic` to see this update's
|
|
12272
|
+
// effect on ordering (see "Modify records during cursor
|
|
12273
|
+
// iteration" in idbcursor_update_index.any.js).
|
|
12012
12274
|
// @ts-ignore -- API (not erring in TS 6)
|
|
12013
|
-
IDBObjectStore.__storingRecordObjectStore(request, me.__store,
|
|
12275
|
+
IDBObjectStore.__storingRecordObjectStore(request, me.__store, true, clonedValue, false, key);
|
|
12014
12276
|
}
|
|
12015
12277
|
if (me.__store.keyPath !== null) {
|
|
12016
12278
|
const [evaluatedKey, clonedValue] = me.__store.__validateKeyAndValueAndCloneValue(valueToUpdate, undefined, true);
|
|
@@ -13132,11 +13394,9 @@ function runBatch(self, batch) {
|
|
|
13132
13394
|
self._running = false;
|
|
13133
13395
|
runAllSql(self);
|
|
13134
13396
|
}
|
|
13135
|
-
const currentTask = /** @type {import('./WebSQLDatabase.js').TransactionTask} */
|
|
13136
|
-
self._websqlDatabase._currentTask;
|
|
13137
13397
|
const {
|
|
13138
13398
|
readOnly
|
|
13139
|
-
} =
|
|
13399
|
+
} = self._task;
|
|
13140
13400
|
self._websqlDatabase._db.exec(batch, readOnly, function (err, results) {
|
|
13141
13401
|
/* c8 ignore next */
|
|
13142
13402
|
if (err || !results) {
|
|
@@ -13224,10 +13484,12 @@ function executeSql(self, sql, args, sqlCallback, sqlErrorCallback, executeDelay
|
|
|
13224
13484
|
class WebSQLTransaction {
|
|
13225
13485
|
/**
|
|
13226
13486
|
* @param {import('./WebSQLDatabase.js').default} websqlDatabase
|
|
13487
|
+
* @param {import('./WebSQLDatabase.js').TransactionTask} task
|
|
13227
13488
|
* @param {import('../types.js').Delay} [executeDelay]
|
|
13228
13489
|
*/
|
|
13229
|
-
constructor(websqlDatabase, executeDelay) {
|
|
13490
|
+
constructor(websqlDatabase, task, executeDelay) {
|
|
13230
13491
|
this._websqlDatabase = websqlDatabase;
|
|
13492
|
+
this._task = task;
|
|
13231
13493
|
/** @type {Error | null} */
|
|
13232
13494
|
this._error = null;
|
|
13233
13495
|
this._complete = false;
|
|
@@ -13236,11 +13498,14 @@ class WebSQLTransaction {
|
|
|
13236
13498
|
this._executeDelay = executeDelay || immediate;
|
|
13237
13499
|
/** @type {import('tiny-queue').default<SQLTask>} */
|
|
13238
13500
|
this._sqlQueue = new Queue();
|
|
13239
|
-
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
//
|
|
13243
|
-
//
|
|
13501
|
+
if (!task.readOnly) {
|
|
13502
|
+
// A read-only task never needs a transaction wrapper for its own
|
|
13503
|
+
// sake (reads don't need atomicity against each other) -- true
|
|
13504
|
+
// whether or not `concurrentReaders` (see `WebSQLDatabase`) lets
|
|
13505
|
+
// multiple read-only tasks actually run at once. This is a perf
|
|
13506
|
+
// boost either way. A non-read-only task always still gets one, and
|
|
13507
|
+
// always still runs with full exclusivity (see
|
|
13508
|
+
// `WebSQLDatabase#runNextTransaction`).
|
|
13244
13509
|
this._sqlQueue.push(new SQLTask('BEGIN;', [], noop, noop));
|
|
13245
13510
|
}
|
|
13246
13511
|
}
|
|
@@ -13321,39 +13586,68 @@ class WebSQLDatabase {
|
|
|
13321
13586
|
this._db = db;
|
|
13322
13587
|
/** @type {import('tiny-queue').default<TransactionTask>} */
|
|
13323
13588
|
this._txnQueue = new Queue();
|
|
13324
|
-
|
|
13589
|
+
// Off by default: queued transactions -- read or write alike -- run
|
|
13590
|
+
// strictly one at a time, in the order requested, per the WebSQL spec
|
|
13591
|
+
// (see `#runNextTransaction`). When true, any number of read-only
|
|
13592
|
+
// tasks may instead run concurrently; a non-read-only task still
|
|
13593
|
+
// always runs with full exclusivity either way.
|
|
13594
|
+
this._concurrentReaders = Boolean(webSQLOverrides.concurrentReaders);
|
|
13595
|
+
/** @type {Set<TransactionTask>} */
|
|
13596
|
+
this._activeReaders = new Set();
|
|
13325
13597
|
/** @type {TransactionTask | null} */
|
|
13326
|
-
this.
|
|
13598
|
+
this._activeWriter = null;
|
|
13327
13599
|
this._transactionDelay = webSQLOverrides.transactionDelay || immediate;
|
|
13328
13600
|
this._executeDelay = webSQLOverrides.executeDelay || immediate;
|
|
13329
13601
|
}
|
|
13330
13602
|
|
|
13331
13603
|
/**
|
|
13332
|
-
*
|
|
13604
|
+
* @param {TransactionTask} task
|
|
13333
13605
|
*/
|
|
13334
|
-
#runTransaction() {
|
|
13335
|
-
const txn = new WebSQLTransaction(this, this._executeDelay);
|
|
13606
|
+
#runTransaction(task) {
|
|
13607
|
+
const txn = new WebSQLTransaction(this, task, this._executeDelay);
|
|
13336
13608
|
this._transactionDelay(() => {
|
|
13337
|
-
|
|
13338
|
-
currentTask.txnCallback(txn);
|
|
13609
|
+
task.txnCallback(txn);
|
|
13339
13610
|
txn._checkDone();
|
|
13340
13611
|
});
|
|
13341
13612
|
}
|
|
13342
13613
|
|
|
13343
13614
|
/**
|
|
13344
|
-
*
|
|
13615
|
+
* Starts as many queued tasks as the current lock state allows. With
|
|
13616
|
+
* `concurrentReaders` off (the default), a task only starts once
|
|
13617
|
+
* nothing else is active at all -- full mutual exclusion, matching the
|
|
13618
|
+
* WebSQL spec's strict one-at-a-time, in-request-order guarantee (see
|
|
13619
|
+
* this file's own test suite, "callback order 2"). With it on, any
|
|
13620
|
+
* leading run of read-only tasks can all start together instead
|
|
13621
|
+
* (concurrent reads are always fine); a non-read-only task still needs
|
|
13622
|
+
* exclusivity regardless, so nothing past it may start until it
|
|
13623
|
+
* finishes. Not starvation-proof in the `concurrentReaders` case -- a
|
|
13624
|
+
* steady stream of arriving readers could in principle keep a waiting
|
|
13625
|
+
* writer waiting indefinitely -- but that's an acceptable tradeoff over
|
|
13626
|
+
* the added complexity of tracking arrival order across reader/writer
|
|
13627
|
+
* kinds.
|
|
13345
13628
|
*/
|
|
13346
13629
|
#runNextTransaction() {
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13630
|
+
for (;;) {
|
|
13631
|
+
const [nextTask] = this._txnQueue.slice(0, 1);
|
|
13632
|
+
if (!nextTask) {
|
|
13633
|
+
return;
|
|
13634
|
+
}
|
|
13635
|
+
const readerMayShare = this._concurrentReaders && nextTask.readOnly;
|
|
13636
|
+
if (readerMayShare) {
|
|
13637
|
+
if (this._activeWriter) {
|
|
13638
|
+
return;
|
|
13639
|
+
}
|
|
13640
|
+
} else if (this._activeWriter || this._activeReaders.size) {
|
|
13641
|
+
return;
|
|
13642
|
+
}
|
|
13643
|
+
this._txnQueue.shift();
|
|
13644
|
+
if (nextTask.readOnly) {
|
|
13645
|
+
this._activeReaders.add(nextTask);
|
|
13646
|
+
} else {
|
|
13647
|
+
this._activeWriter = nextTask;
|
|
13648
|
+
}
|
|
13649
|
+
this.#runTransaction(nextTask);
|
|
13353
13650
|
}
|
|
13354
|
-
this._currentTask = task;
|
|
13355
|
-
this._running = true;
|
|
13356
|
-
this.#runTransaction();
|
|
13357
13651
|
}
|
|
13358
13652
|
|
|
13359
13653
|
/**
|
|
@@ -13381,6 +13675,7 @@ class WebSQLDatabase {
|
|
|
13381
13675
|
*/
|
|
13382
13676
|
// eslint-disable-next-line unicorn/prefer-private-class-fields -- see above
|
|
13383
13677
|
_onTransactionComplete(err, transaction) {
|
|
13678
|
+
const task = transaction._task;
|
|
13384
13679
|
/**
|
|
13385
13680
|
* @param {Error | boolean | null} [er]
|
|
13386
13681
|
*/
|
|
@@ -13395,14 +13690,15 @@ class WebSQLDatabase {
|
|
|
13395
13690
|
transaction._complete = true;
|
|
13396
13691
|
}
|
|
13397
13692
|
if (er) {
|
|
13398
|
-
|
|
13399
|
-
|
|
13400
|
-
|
|
13401
|
-
}
|
|
13402
|
-
|
|
13693
|
+
task.errorCallback(/** @type {Error} */er);
|
|
13694
|
+
} else {
|
|
13695
|
+
task.successCallback();
|
|
13696
|
+
}
|
|
13697
|
+
if (task.readOnly) {
|
|
13698
|
+
this._activeReaders.delete(task);
|
|
13699
|
+
} else if (this._activeWriter === task) {
|
|
13700
|
+
this._activeWriter = null;
|
|
13403
13701
|
}
|
|
13404
|
-
this._running = false;
|
|
13405
|
-
this._currentTask = null;
|
|
13406
13702
|
this.#runNextTransaction();
|
|
13407
13703
|
};
|
|
13408
13704
|
/**
|
|
@@ -13428,13 +13724,13 @@ class WebSQLDatabase {
|
|
|
13428
13724
|
}
|
|
13429
13725
|
});
|
|
13430
13726
|
};
|
|
13431
|
-
if (
|
|
13432
|
-
const cont =
|
|
13727
|
+
if (task.nonstandardTransCb) {
|
|
13728
|
+
const cont = task.nonstandardTransCb.call(this, task, err, done, rollback, commit);
|
|
13433
13729
|
if (!cont) {
|
|
13434
13730
|
return;
|
|
13435
13731
|
}
|
|
13436
13732
|
}
|
|
13437
|
-
if (
|
|
13733
|
+
if (task.readOnly) {
|
|
13438
13734
|
done(err); // read-only doesn't require a transaction
|
|
13439
13735
|
} else if (err) {
|
|
13440
13736
|
rollback(err);
|
|
@@ -14392,16 +14688,28 @@ const READ_ONLY_ERROR = new Error('could not prepare statement (23 not authorize
|
|
|
14392
14688
|
// connection and a newly-opened one during an upgrade. `PRAGMA
|
|
14393
14689
|
// busy_timeout` can't safely arbitrate between them here: it blocks the
|
|
14394
14690
|
// 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
|
-
//
|
|
14691
|
+
// own release is itself scheduled via `setImmediate` below, which can
|
|
14692
|
+
// never fire while that retry loop is blocking the same thread -- so
|
|
14693
|
+
// instead of waiting, a second connection's write just fails with
|
|
14694
|
+
// "database is locked".
|
|
14695
|
+
//
|
|
14696
|
+
// `fileReaders`/`fileWriter` implement a simple (not starvation-proof --
|
|
14697
|
+
// see the release loop below) reader/writer lock per file path instead:
|
|
14698
|
+
// any number of `readonly` transactions may hold the file concurrently,
|
|
14699
|
+
// matching what SQLite itself already natively supports (multiple
|
|
14700
|
+
// readers never need to exclude each other, only a writer needs
|
|
14701
|
+
// exclusivity), while a non-`readonly` transaction still waits for
|
|
14702
|
+
// exclusive access -- no concurrent readers, no concurrent writer. A
|
|
14703
|
+
// simple single-owner mutex (this file's prior implementation) would
|
|
14704
|
+
// otherwise serialize even purely-concurrent-reader scenarios that
|
|
14705
|
+
// never touch a writer at all, for no reason `better-sqlite3`/SQLite
|
|
14706
|
+
// itself requires.
|
|
14707
|
+
/** @type {Map<string, Set<{_db: any, _qFilePath: string}>>} */
|
|
14708
|
+
const fileReaders = new Map();
|
|
14401
14709
|
/** @type {Map<string, {_db: any, _qFilePath: string}>} */
|
|
14402
|
-
const
|
|
14403
|
-
/** @type {Map<string, (
|
|
14404
|
-
const
|
|
14710
|
+
const fileWriter = new Map();
|
|
14711
|
+
/** @type {Map<string, {isReader: boolean, resume: () => void}[]>} */
|
|
14712
|
+
const fileWaiters = new Map();
|
|
14405
14713
|
const beginRe = /^\s*BEGIN\b/iu;
|
|
14406
14714
|
const endRe = /^\s*(END|COMMIT|ROLLBACK)\b/iu;
|
|
14407
14715
|
|
|
@@ -14513,14 +14821,29 @@ SQLiteDatabase.prototype.exec = function exec(queries, readOnly, callback) {
|
|
|
14513
14821
|
// already fully independent -- so there is nothing to serialize.
|
|
14514
14822
|
const filePath = this._qFilePath === ':memory:' ? null : this._qFilePath;
|
|
14515
14823
|
if (filePath && queries[0] && beginRe.test(queries[0].sql)) {
|
|
14516
|
-
const
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14824
|
+
const activeReaders = fileReaders.get(filePath);
|
|
14825
|
+
const hasActiveReaders = Boolean(activeReaders && activeReaders.size);
|
|
14826
|
+
const activeWriter = fileWriter.get(filePath);
|
|
14827
|
+
const blockedByWriter = Boolean(activeWriter && activeWriter !== this);
|
|
14828
|
+
const blocked = blockedByWriter || !readOnly && hasActiveReaders;
|
|
14829
|
+
if (blocked) {
|
|
14830
|
+
const waiters = fileWaiters.get(filePath) || [];
|
|
14831
|
+
waiters.push({
|
|
14832
|
+
isReader: readOnly,
|
|
14833
|
+
resume: () => this.exec(queries, readOnly, callback)
|
|
14834
|
+
});
|
|
14835
|
+
fileWaiters.set(filePath, waiters);
|
|
14521
14836
|
return;
|
|
14522
14837
|
}
|
|
14523
|
-
|
|
14838
|
+
if (readOnly) {
|
|
14839
|
+
if (activeReaders) {
|
|
14840
|
+
activeReaders.add(this);
|
|
14841
|
+
} else {
|
|
14842
|
+
fileReaders.set(filePath, new Set([this]));
|
|
14843
|
+
}
|
|
14844
|
+
} else {
|
|
14845
|
+
fileWriter.set(filePath, this);
|
|
14846
|
+
}
|
|
14524
14847
|
}
|
|
14525
14848
|
const db = this._db._db;
|
|
14526
14849
|
const len = queries.length;
|
|
@@ -14562,13 +14885,15 @@ SQLiteDatabase.prototype.exec = function exec(queries, readOnly, callback) {
|
|
|
14562
14885
|
}
|
|
14563
14886
|
}
|
|
14564
14887
|
}
|
|
14565
|
-
// A real
|
|
14566
|
-
//
|
|
14567
|
-
//
|
|
14888
|
+
// A real macrotask (not `queueMicrotask`) so this yields properly:
|
|
14889
|
+
// code that synchronously issues a new request from within each
|
|
14890
|
+
// request's callback (e.g. to keep a transaction alive) would
|
|
14568
14891
|
// otherwise chain microtask to microtask forever, starving out any
|
|
14569
14892
|
// `setTimeout`-based code (including IndexedDB's own internal request
|
|
14570
|
-
// scheduling) that never gets a turn to run.
|
|
14571
|
-
setTimeout(
|
|
14893
|
+
// scheduling) that never gets a turn to run. `setImmediate` (Node's
|
|
14894
|
+
// "check" phase) still yields the same way `setTimeout(..., 0)` did,
|
|
14895
|
+
// but runs sooner in Node's event loop.
|
|
14896
|
+
setImmediate(() => {
|
|
14572
14897
|
// Release the file lock (if held) and hand it to the next waiting
|
|
14573
14898
|
// connection, if any, only once this transaction has genuinely
|
|
14574
14899
|
// finished -- and only here, on its own turn, so a resumed
|
|
@@ -14583,15 +14908,52 @@ SQLiteDatabase.prototype.exec = function exec(queries, readOnly, callback) {
|
|
|
14583
14908
|
// and never see the matching release, deadlocking every later
|
|
14584
14909
|
// connection to the same file.
|
|
14585
14910
|
if (filePath && queries.some(q => endRe.test(q.sql))) {
|
|
14586
|
-
|
|
14587
|
-
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
|
|
14911
|
+
const activeReaders = fileReaders.get(filePath);
|
|
14912
|
+
if (activeReaders) {
|
|
14913
|
+
activeReaders.delete(this);
|
|
14914
|
+
if (!activeReaders.size) {
|
|
14915
|
+
fileReaders.delete(filePath);
|
|
14916
|
+
}
|
|
14917
|
+
}
|
|
14918
|
+
if (fileWriter.get(filePath) === this) {
|
|
14919
|
+
fileWriter.delete(filePath);
|
|
14920
|
+
}
|
|
14921
|
+
// Resume as many waiters as the lock now genuinely allows:
|
|
14922
|
+
// any leading run of readers (concurrent reads are always
|
|
14923
|
+
// fine), then at most one writer, since a writer needs
|
|
14924
|
+
// exclusivity -- stop there rather than looking past it.
|
|
14925
|
+
// Not starvation-proof: a steady stream of arriving readers
|
|
14926
|
+
// could in principle keep a waiting writer waiting
|
|
14927
|
+
// indefinitely, but that's an acceptable tradeoff here over
|
|
14928
|
+
// the added complexity of tracking arrival order across
|
|
14929
|
+
// reader/writer kinds.
|
|
14930
|
+
const waiters = fileWaiters.get(filePath);
|
|
14931
|
+
if (waiters) {
|
|
14932
|
+
while (waiters.length) {
|
|
14933
|
+
if (fileWriter.has(filePath)) {
|
|
14934
|
+
break;
|
|
14935
|
+
}
|
|
14936
|
+
const next = waiters[0];
|
|
14937
|
+
const stillHasReaders = fileReaders.get(filePath);
|
|
14938
|
+
if (!next.isReader && stillHasReaders && stillHasReaders.size) {
|
|
14939
|
+
break;
|
|
14940
|
+
}
|
|
14941
|
+
waiters.shift();
|
|
14942
|
+
next.resume();
|
|
14943
|
+
if (!next.isReader) {
|
|
14944
|
+
// A writer just took exclusive ownership (inside
|
|
14945
|
+
// `resume()`, synchronously, via the acquire
|
|
14946
|
+
// logic above) -- nothing else may proceed now.
|
|
14947
|
+
break;
|
|
14948
|
+
}
|
|
14949
|
+
}
|
|
14950
|
+
if (!waiters.length) {
|
|
14951
|
+
fileWaiters.delete(filePath);
|
|
14952
|
+
}
|
|
14591
14953
|
}
|
|
14592
14954
|
}
|
|
14593
14955
|
callback(null, results);
|
|
14594
|
-
}
|
|
14956
|
+
});
|
|
14595
14957
|
};
|
|
14596
14958
|
|
|
14597
14959
|
/**
|
|
@@ -14631,7 +14993,19 @@ function wrappedSQLiteDatabase(name) {
|
|
|
14631
14993
|
}
|
|
14632
14994
|
return db;
|
|
14633
14995
|
}
|
|
14634
|
-
|
|
14996
|
+
|
|
14997
|
+
// `concurrentReaders` is off by default in `websql-configurable` itself (to
|
|
14998
|
+
// preserve the WebSQL spec's strict, one-at-a-time transaction ordering
|
|
14999
|
+
// that library's own test suite depends on), but IndexedDBShim only ever
|
|
15000
|
+
// uses it as an internal SQL execution engine -- it doesn't need or expose
|
|
15001
|
+
// that ordering guarantee itself -- so it's safe, and needed, to opt in
|
|
15002
|
+
// here: without it, two same-scope `readonly` IDBTransactions can deadlock
|
|
15003
|
+
// waiting on each other (see `transaction-scheduling-within-database.any.js`).
|
|
15004
|
+
const nodeWebSQL = customOpenDatabase(/** @type {SQLiteDatabaseConstructor} */ /** @type {unknown} */wrappedSQLiteDatabase, {
|
|
15005
|
+
websql: {
|
|
15006
|
+
concurrentReaders: true
|
|
15007
|
+
}
|
|
15008
|
+
});
|
|
14635
15009
|
|
|
14636
15010
|
CFG.win = {
|
|
14637
15011
|
openDatabase: nodeWebSQL
|