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.
Files changed (36) hide show
  1. package/dist/IDBCursor.d.ts.map +1 -1
  2. package/dist/IDBFactory.d.ts.map +1 -1
  3. package/dist/IDBTransaction.d.ts +8 -6
  4. package/dist/IDBTransaction.d.ts.map +1 -1
  5. package/dist/Key.d.ts.map +1 -1
  6. package/dist/Sca.d.ts.map +1 -1
  7. package/dist/indexeddbshim-Key.js +52 -13
  8. package/dist/indexeddbshim-Key.js.map +1 -1
  9. package/dist/indexeddbshim-Key.min.js +2 -2
  10. package/dist/indexeddbshim-Key.min.js.map +1 -1
  11. package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs +508 -134
  12. package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs.map +1 -1
  13. package/dist/indexeddbshim-UnicodeIdentifiers.js +337 -71
  14. package/dist/indexeddbshim-UnicodeIdentifiers.js.map +1 -1
  15. package/dist/indexeddbshim-UnicodeIdentifiers.min.js +3 -3
  16. package/dist/indexeddbshim-UnicodeIdentifiers.min.js.map +1 -1
  17. package/dist/indexeddbshim-node.cjs +508 -134
  18. package/dist/indexeddbshim-node.cjs.map +1 -1
  19. package/dist/indexeddbshim-noninvasive.js +337 -71
  20. package/dist/indexeddbshim-noninvasive.js.map +1 -1
  21. package/dist/indexeddbshim-noninvasive.min.js +3 -3
  22. package/dist/indexeddbshim-noninvasive.min.js.map +1 -1
  23. package/dist/indexeddbshim.js +337 -71
  24. package/dist/indexeddbshim.js.map +1 -1
  25. package/dist/indexeddbshim.min.js +3 -3
  26. package/dist/indexeddbshim.min.js.map +1 -1
  27. package/dist/nodeSQLiteDatabase.d.ts.map +1 -1
  28. package/dist/nodeWebSQL.d.ts.map +1 -1
  29. package/package.json +4 -4
  30. package/src/IDBCursor.js +34 -4
  31. package/src/IDBFactory.js +207 -60
  32. package/src/IDBTransaction.js +23 -3
  33. package/src/Key.js +34 -12
  34. package/src/Sca.js +53 -3
  35. package/src/nodeSQLiteDatabase.js +92 -26
  36. 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[i] = encodedItem;
307
+ setArrayValue(encoded, i, encodedItem);
288
308
  }
289
- encoded.push(keyTypeToEncodedChar.invalid + '-'); // append an extra item, so empty arrays sort correctly
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[i] = decodedItem;
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.push(input);
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.push(fullKeys ? key : key.value);
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.push(key.value);
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[identifier] = {};
816
+ Object.defineProperty(value, identifier, {value: {}, enumerable: true, writable: true, configurable: true});
797
817
  }
798
818
  value = value[identifier];
799
819
  });
800
- value[/** @type {string} */ (last)] = key; // key is already a `keyValue` in our processing so no need to convert
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.push(key);
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.push(key);
927
+ safePush(matches, key);
907
928
  }
908
929
  }
909
930
  } else if (util.isNullish(range) || isKeyInRange(keyEntry, range, true)) {
910
- matches.push(keyEntry);
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[index] = entry;
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, structuredCloningThrowing
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
- let typeson = new Typeson().register(structuredCloningThrowing);
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(structuredCloningThrowing));
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 `setTimeout` below, which can never
40
- // fire while that retry loop is blocking the same thread -- so instead of
41
- // waiting, the second connection's write just fails with "database is
42
- // locked". Serialize writes per file path instead, so a second
43
- // connection's `BEGIN` waits for the first connection's transaction to
44
- // actually finish rather than colliding with it.
39
+ // own release is itself scheduled via `setImmediate` below, which can
40
+ // never fire while that retry loop is blocking the same thread -- so
41
+ // instead of waiting, a second connection's write just fails with
42
+ // "database is locked".
43
+ //
44
+ // `fileReaders`/`fileWriter` implement a simple (not starvation-proof --
45
+ // see the release loop below) reader/writer lock per file path instead:
46
+ // any number of `readonly` transactions may hold the file concurrently,
47
+ // matching what SQLite itself already natively supports (multiple
48
+ // readers never need to exclude each other, only a writer needs
49
+ // exclusivity), while a non-`readonly` transaction still waits for
50
+ // exclusive access -- no concurrent readers, no concurrent writer. A
51
+ // simple single-owner mutex (this file's prior implementation) would
52
+ // otherwise serialize even purely-concurrent-reader scenarios that
53
+ // never touch a writer at all, for no reason `better-sqlite3`/SQLite
54
+ // itself requires.
55
+ /** @type {Map<string, Set<{_db: any, _qFilePath: string}>>} */
56
+ const fileReaders = new Map();
45
57
  /** @type {Map<string, {_db: any, _qFilePath: string}>} */
46
- const fileLockOwners = new Map();
47
- /** @type {Map<string, (() => void)[]>} */
48
- const fileLockWaiters = new Map();
58
+ const fileWriter = new Map();
59
+ /** @type {Map<string, {isReader: boolean, resume: () => void}[]>} */
60
+ const fileWaiters = new Map();
49
61
  const beginRe = /^\s*BEGIN\b/iu;
50
62
  const endRe = /^\s*(END|COMMIT|ROLLBACK)\b/iu;
51
63
 
@@ -158,14 +170,29 @@ SQLiteDatabase.prototype.exec = function exec (queries, readOnly, callback) {
158
170
  // already fully independent -- so there is nothing to serialize.
159
171
  const filePath = this._qFilePath === ':memory:' ? null : this._qFilePath;
160
172
  if (filePath && queries[0] && beginRe.test(queries[0].sql)) {
161
- const owner = fileLockOwners.get(filePath);
162
- if (owner && owner !== this) {
163
- const waiters = fileLockWaiters.get(filePath) || [];
164
- waiters.push(() => this.exec(queries, readOnly, callback));
165
- fileLockWaiters.set(filePath, waiters);
173
+ const activeReaders = fileReaders.get(filePath);
174
+ const hasActiveReaders = Boolean(activeReaders && activeReaders.size);
175
+ const activeWriter = fileWriter.get(filePath);
176
+ const blockedByWriter = Boolean(activeWriter && activeWriter !== this);
177
+ const blocked = blockedByWriter || (!readOnly && hasActiveReaders);
178
+ if (blocked) {
179
+ const waiters = fileWaiters.get(filePath) || [];
180
+ waiters.push({
181
+ isReader: readOnly,
182
+ resume: () => this.exec(queries, readOnly, callback)
183
+ });
184
+ fileWaiters.set(filePath, waiters);
166
185
  return;
167
186
  }
168
- fileLockOwners.set(filePath, this);
187
+ if (readOnly) {
188
+ if (activeReaders) {
189
+ activeReaders.add(this);
190
+ } else {
191
+ fileReaders.set(filePath, new Set([this]));
192
+ }
193
+ } else {
194
+ fileWriter.set(filePath, this);
195
+ }
169
196
  }
170
197
 
171
198
  const db = this._db._db;
@@ -204,13 +231,15 @@ SQLiteDatabase.prototype.exec = function exec (queries, readOnly, callback) {
204
231
  }
205
232
  }
206
233
  }
207
- // A real timer (not `queueMicrotask`) so this yields to the macrotask
208
- // queue: code that synchronously issues a new request from within
209
- // each request's callback (e.g. to keep a transaction alive) would
234
+ // A real macrotask (not `queueMicrotask`) so this yields properly:
235
+ // code that synchronously issues a new request from within each
236
+ // request's callback (e.g. to keep a transaction alive) would
210
237
  // otherwise chain microtask to microtask forever, starving out any
211
238
  // `setTimeout`-based code (including IndexedDB's own internal request
212
- // scheduling) that never gets a turn to run.
213
- setTimeout(() => {
239
+ // scheduling) that never gets a turn to run. `setImmediate` (Node's
240
+ // "check" phase) still yields the same way `setTimeout(..., 0)` did,
241
+ // but runs sooner in Node's event loop.
242
+ setImmediate(() => {
214
243
  // Release the file lock (if held) and hand it to the next waiting
215
244
  // connection, if any, only once this transaction has genuinely
216
245
  // finished -- and only here, on its own turn, so a resumed
@@ -225,15 +254,52 @@ SQLiteDatabase.prototype.exec = function exec (queries, readOnly, callback) {
225
254
  // and never see the matching release, deadlocking every later
226
255
  // connection to the same file.
227
256
  if (filePath && queries.some((q) => endRe.test(q.sql))) {
228
- fileLockOwners.delete(filePath);
229
- const waiters = fileLockWaiters.get(filePath);
230
- const nextWaitingExec = waiters && waiters.shift();
231
- if (nextWaitingExec) {
232
- nextWaitingExec();
257
+ const activeReaders = fileReaders.get(filePath);
258
+ if (activeReaders) {
259
+ activeReaders.delete(this);
260
+ if (!activeReaders.size) {
261
+ fileReaders.delete(filePath);
262
+ }
263
+ }
264
+ if (fileWriter.get(filePath) === this) {
265
+ fileWriter.delete(filePath);
266
+ }
267
+ // Resume as many waiters as the lock now genuinely allows:
268
+ // any leading run of readers (concurrent reads are always
269
+ // fine), then at most one writer, since a writer needs
270
+ // exclusivity -- stop there rather than looking past it.
271
+ // Not starvation-proof: a steady stream of arriving readers
272
+ // could in principle keep a waiting writer waiting
273
+ // indefinitely, but that's an acceptable tradeoff here over
274
+ // the added complexity of tracking arrival order across
275
+ // reader/writer kinds.
276
+ const waiters = fileWaiters.get(filePath);
277
+ if (waiters) {
278
+ while (waiters.length) {
279
+ if (fileWriter.has(filePath)) {
280
+ break;
281
+ }
282
+ const next = waiters[0];
283
+ const stillHasReaders = fileReaders.get(filePath);
284
+ if (!next.isReader && stillHasReaders && stillHasReaders.size) {
285
+ break;
286
+ }
287
+ waiters.shift();
288
+ next.resume();
289
+ if (!next.isReader) {
290
+ // A writer just took exclusive ownership (inside
291
+ // `resume()`, synchronously, via the acquire
292
+ // logic above) -- nothing else may proceed now.
293
+ break;
294
+ }
295
+ }
296
+ if (!waiters.length) {
297
+ fileWaiters.delete(filePath);
298
+ }
233
299
  }
234
300
  }
235
301
  callback(null, results);
236
- }, 0);
302
+ });
237
303
  };
238
304
 
239
305
  export default SQLiteDatabase;
package/src/nodeWebSQL.js CHANGED
@@ -40,5 +40,12 @@ function wrappedSQLiteDatabase (name) {
40
40
  return db;
41
41
  }
42
42
 
43
- const nodeWebSQL = customOpenDatabase(/** @type {SQLiteDatabaseConstructor} */ (/** @type {unknown} */ (wrappedSQLiteDatabase)), {});
43
+ // `concurrentReaders` is off by default in `websql-configurable` itself (to
44
+ // preserve the WebSQL spec's strict, one-at-a-time transaction ordering
45
+ // that library's own test suite depends on), but IndexedDBShim only ever
46
+ // uses it as an internal SQL execution engine -- it doesn't need or expose
47
+ // that ordering guarantee itself -- so it's safe, and needed, to opt in
48
+ // here: without it, two same-scope `readonly` IDBTransactions can deadlock
49
+ // waiting on each other (see `transaction-scheduling-within-database.any.js`).
50
+ const nodeWebSQL = customOpenDatabase(/** @type {SQLiteDatabaseConstructor} */ (/** @type {unknown} */ (wrappedSQLiteDatabase)), {websql: {concurrentReaders: true}});
44
51
  export default nodeWebSQL;