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
package/src/Key.js
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {unknown[]} arr
|
|
3
|
+
* @param {number} index
|
|
4
|
+
* @param {unknown} val
|
|
5
|
+
* @returns {void}
|
|
6
|
+
*/
|
|
7
|
+
const setArrayValue = (arr, index, val) => {
|
|
8
|
+
if (Reflect.has(Array.prototype, index)) {
|
|
9
|
+
Object.defineProperty(arr, index, {value: val, enumerable: true, writable: true, configurable: true});
|
|
10
|
+
} else {
|
|
11
|
+
arr[index] = val;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* @param {unknown[]} arr
|
|
16
|
+
* @param {unknown} val
|
|
17
|
+
* @returns {void}
|
|
18
|
+
*/
|
|
19
|
+
const safePush = (arr, val) => setArrayValue(arr, arr.length, val);
|
|
1
20
|
import {createDOMException} from './DOMException.js';
|
|
2
21
|
import * as util from './util.js';
|
|
3
22
|
import cmp from './cmp.js';
|
|
@@ -281,12 +300,13 @@ const types = {
|
|
|
281
300
|
* @returns {string}
|
|
282
301
|
*/
|
|
283
302
|
encode (key) {
|
|
303
|
+
/** @type {(string|null)[]} */
|
|
284
304
|
const encoded = [];
|
|
285
305
|
for (const [i, item] of key.entries()) {
|
|
286
306
|
const encodedItem = encode(item, true); // encode the array item
|
|
287
|
-
encoded
|
|
307
|
+
setArrayValue(encoded, i, encodedItem);
|
|
288
308
|
}
|
|
289
|
-
encoded
|
|
309
|
+
safePush(encoded, keyTypeToEncodedChar.invalid + '-'); // append an extra item, so empty arrays sort correctly
|
|
290
310
|
let encodedKey = JSON.stringify(encoded);
|
|
291
311
|
if (CFG.escapeNULForSQLiteStatements === false) {
|
|
292
312
|
encodedKey = encodedKey.replaceAll(String.raw`\u0000`, '\0');
|
|
@@ -307,7 +327,7 @@ const types = {
|
|
|
307
327
|
for (let i = 0; i < decoded.length; i++) {
|
|
308
328
|
const item = decoded[i];
|
|
309
329
|
const decodedItem = decode(item, true); // decode the item
|
|
310
|
-
decoded
|
|
330
|
+
setArrayValue(decoded, i, decodedItem);
|
|
311
331
|
}
|
|
312
332
|
return decoded;
|
|
313
333
|
}
|
|
@@ -577,7 +597,7 @@ function convertValueToKeyValueDecoded (input, seen, multiEntry, fullKeys) {
|
|
|
577
597
|
} case 'array': { // May throw (from binary)
|
|
578
598
|
const arr = /** @type {Array<any>} */ (input);
|
|
579
599
|
const len = arr.length;
|
|
580
|
-
seen
|
|
600
|
+
safePush(seen, input);
|
|
581
601
|
|
|
582
602
|
/** @type {(KeyValueObject|Value)[]} */
|
|
583
603
|
const keys = [];
|
|
@@ -598,7 +618,7 @@ function convertValueToKeyValueDecoded (input, seen, multiEntry, fullKeys) {
|
|
|
598
618
|
(!fullKeys && keys.every((k) => cmp(k, key.value) !== 0)) ||
|
|
599
619
|
(fullKeys && keys.every((k) => cmp(k, key) !== 0))
|
|
600
620
|
) {
|
|
601
|
-
keys
|
|
621
|
+
safePush(keys, fullKeys ? key : key.value);
|
|
602
622
|
}
|
|
603
623
|
} catch (err) {
|
|
604
624
|
if (!multiEntry) {
|
|
@@ -732,7 +752,7 @@ function evaluateKeyPathOnValueToDecodedValue (value, keyPath, multiEntry, fullK
|
|
|
732
752
|
if (key.failure) {
|
|
733
753
|
return true;
|
|
734
754
|
}
|
|
735
|
-
result
|
|
755
|
+
safePush(result, key.value);
|
|
736
756
|
return false;
|
|
737
757
|
})
|
|
738
758
|
? {failure: true}
|
|
@@ -793,11 +813,11 @@ function injectKeyIntoValueUsingKeyPath (value, key, keyPath) {
|
|
|
793
813
|
identifiers.forEach((identifier) => {
|
|
794
814
|
const hop = Object.hasOwn(value, identifier);
|
|
795
815
|
if (!hop) {
|
|
796
|
-
value
|
|
816
|
+
Object.defineProperty(value, identifier, {value: {}, enumerable: true, writable: true, configurable: true});
|
|
797
817
|
}
|
|
798
818
|
value = value[identifier];
|
|
799
819
|
});
|
|
800
|
-
value
|
|
820
|
+
Object.defineProperty(value, /** @type {string} */ (last), {value: key, enumerable: true, writable: true, configurable: true}); // key is already a `keyValue` in our processing so no need to convert
|
|
801
821
|
}
|
|
802
822
|
|
|
803
823
|
/**
|
|
@@ -883,6 +903,7 @@ function isMultiEntryMatch (encodedEntry, encodedKey) {
|
|
|
883
903
|
* @returns {Key[]}
|
|
884
904
|
*/
|
|
885
905
|
function findMultiEntryMatches (keyEntry, range) {
|
|
906
|
+
/** @type {unknown[]} */
|
|
886
907
|
const matches = [];
|
|
887
908
|
|
|
888
909
|
if (Array.isArray(keyEntry)) {
|
|
@@ -896,18 +917,18 @@ function findMultiEntryMatches (keyEntry, range) {
|
|
|
896
917
|
} else {
|
|
897
918
|
const nested = findMultiEntryMatches(key, range);
|
|
898
919
|
if (nested.length > 0) {
|
|
899
|
-
matches
|
|
920
|
+
safePush(matches, key);
|
|
900
921
|
}
|
|
901
922
|
continue;
|
|
902
923
|
}
|
|
903
924
|
}
|
|
904
925
|
|
|
905
926
|
if (util.isNullish(range) || isKeyInRange(key, range, true)) {
|
|
906
|
-
matches
|
|
927
|
+
safePush(matches, key);
|
|
907
928
|
}
|
|
908
929
|
}
|
|
909
930
|
} else if (util.isNullish(range) || isKeyInRange(keyEntry, range, true)) {
|
|
910
|
-
matches
|
|
931
|
+
safePush(matches, keyEntry);
|
|
911
932
|
}
|
|
912
933
|
return matches;
|
|
913
934
|
}
|
|
@@ -924,12 +945,13 @@ function convertKeyToValue (key) {
|
|
|
924
945
|
case 'number': case 'string': {
|
|
925
946
|
return value;
|
|
926
947
|
} case 'array': {
|
|
948
|
+
/** @type {ValueType[]} */
|
|
927
949
|
const array = [];
|
|
928
950
|
const len = value.length;
|
|
929
951
|
let index = 0;
|
|
930
952
|
while (index < len) {
|
|
931
953
|
const entry = convertKeyToValue(value[index]);
|
|
932
|
-
array
|
|
954
|
+
setArrayValue(array, index, entry);
|
|
933
955
|
index++;
|
|
934
956
|
}
|
|
935
957
|
return array;
|
package/src/Sca.js
CHANGED
|
@@ -1,12 +1,62 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Typeson, hasConstructorOf,
|
|
2
|
+
Typeson, hasConstructorOf, structuredCloningForStorage
|
|
3
3
|
} from 'typeson-registry';
|
|
4
4
|
|
|
5
5
|
import {createDOMException, ShimDOMException} from './DOMException.js';
|
|
6
6
|
|
|
7
7
|
// See: https://stackoverflow.com/questions/42170826/categories-for-rejection-by-the-structured-cloning-algorithm
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Although typeson-registry already has a FileList type in its structured cloning presets,
|
|
10
|
+
// we need to override it so it works with our tests
|
|
11
|
+
|
|
12
|
+
const specSet = structuredCloningForStorage
|
|
13
|
+
.flatMap((preset) => (Array.isArray(preset) ? preset : [preset]))
|
|
14
|
+
.find((preset) => preset && !Array.isArray(preset) && 'filelist' in preset);
|
|
15
|
+
|
|
16
|
+
const origFileList = specSet && !Array.isArray(specSet) && 'filelist' in specSet
|
|
17
|
+
? specSet.filelist
|
|
18
|
+
: undefined;
|
|
19
|
+
|
|
20
|
+
const origTest = origFileList && typeof origFileList === 'object' && 'test' in origFileList && typeof origFileList.test === 'function'
|
|
21
|
+
? origFileList.test
|
|
22
|
+
: undefined;
|
|
23
|
+
|
|
24
|
+
const origRevive = origFileList && typeof origFileList === 'object' && 'revive' in origFileList && typeof origFileList.revive === 'function'
|
|
25
|
+
? origFileList.revive
|
|
26
|
+
: undefined;
|
|
27
|
+
|
|
28
|
+
const customFileList = origFileList
|
|
29
|
+
? {
|
|
30
|
+
...origFileList,
|
|
31
|
+
/**
|
|
32
|
+
* @param {unknown} x
|
|
33
|
+
* @param {import('typeson').StateObject} state
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
test (x, state) {
|
|
37
|
+
if (typeof FileList !== 'undefined') {
|
|
38
|
+
return x instanceof FileList;
|
|
39
|
+
}
|
|
40
|
+
return typeof origTest === 'function' ? origTest(x, state) : false;
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* @param {unknown} x
|
|
44
|
+
* @param {import('typeson').StateObject} state
|
|
45
|
+
* @returns {unknown}
|
|
46
|
+
*/
|
|
47
|
+
revive (x, state) {
|
|
48
|
+
if (typeof FileList !== 'undefined') {
|
|
49
|
+
return Reflect.construct(FileList, [x]);
|
|
50
|
+
}
|
|
51
|
+
return typeof origRevive === 'function' ? origRevive(x, state) : undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
56
|
+
let typeson = new Typeson().register([
|
|
57
|
+
structuredCloningForStorage,
|
|
58
|
+
customFileList ? {filelist: customFileList} : {}
|
|
59
|
+
]);
|
|
10
60
|
|
|
11
61
|
/**
|
|
12
62
|
* @param {(preset: import('typeson-registry').Preset) =>
|
|
@@ -15,7 +65,7 @@ let typeson = new Typeson().register(structuredCloningThrowing);
|
|
|
15
65
|
*/
|
|
16
66
|
function register (func) {
|
|
17
67
|
// eslint-disable-next-line unicorn/no-top-level-assignment-in-function -- Should be one-time cache
|
|
18
|
-
typeson = new Typeson().register(func(
|
|
68
|
+
typeson = new Typeson().register(func(structuredCloningForStorage));
|
|
19
69
|
}
|
|
20
70
|
|
|
21
71
|
/**
|
|
@@ -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;
|
package/src/nodeWebSQL.js
CHANGED
|
@@ -40,5 +40,12 @@ function wrappedSQLiteDatabase (name) {
|
|
|
40
40
|
return db;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// `concurrentReaders` is off by default in `websql-configurable` itself (to
|
|
44
|
+
// preserve the WebSQL spec's strict, one-at-a-time transaction ordering
|
|
45
|
+
// that library's own test suite depends on), but IndexedDBShim only ever
|
|
46
|
+
// uses it as an internal SQL execution engine -- it doesn't need or expose
|
|
47
|
+
// that ordering guarantee itself -- so it's safe, and needed, to opt in
|
|
48
|
+
// here: without it, two same-scope `readonly` IDBTransactions can deadlock
|
|
49
|
+
// waiting on each other (see `transaction-scheduling-within-database.any.js`).
|
|
50
|
+
const nodeWebSQL = customOpenDatabase(/** @type {SQLiteDatabaseConstructor} */ (/** @type {unknown} */ (wrappedSQLiteDatabase)), {websql: {concurrentReaders: true}});
|
|
44
51
|
export default nodeWebSQL;
|