indexeddbshim 17.0.0 → 17.1.0

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 (51) hide show
  1. package/README.md +62 -0
  2. package/badges/licenses-badge-dev.svg +1 -1
  3. package/badges/licenses-badge.svg +1 -1
  4. package/dist/CFG.d.ts +1 -0
  5. package/dist/CFG.d.ts.map +1 -1
  6. package/dist/IDBCursor.d.ts +9 -2
  7. package/dist/IDBCursor.d.ts.map +1 -1
  8. package/dist/IDBFactory.d.ts.map +1 -1
  9. package/dist/IDBIndex.d.ts.map +1 -1
  10. package/dist/IDBTransaction.d.ts +21 -0
  11. package/dist/IDBTransaction.d.ts.map +1 -1
  12. package/dist/Key.d.ts.map +1 -1
  13. package/dist/indexeddbshim-Key.js +25 -6
  14. package/dist/indexeddbshim-Key.js.map +1 -1
  15. package/dist/indexeddbshim-Key.min.js +2 -2
  16. package/dist/indexeddbshim-Key.min.js.map +1 -1
  17. package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs +1402 -818
  18. package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs.map +1 -1
  19. package/dist/indexeddbshim-UnicodeIdentifiers.js +467 -298
  20. package/dist/indexeddbshim-UnicodeIdentifiers.js.map +1 -1
  21. package/dist/indexeddbshim-UnicodeIdentifiers.min.js +4 -4
  22. package/dist/indexeddbshim-UnicodeIdentifiers.min.js.map +1 -1
  23. package/dist/indexeddbshim-node.cjs +1402 -818
  24. package/dist/indexeddbshim-node.cjs.map +1 -1
  25. package/dist/indexeddbshim-noninvasive.js +467 -298
  26. package/dist/indexeddbshim-noninvasive.js.map +1 -1
  27. package/dist/indexeddbshim-noninvasive.min.js +4 -4
  28. package/dist/indexeddbshim-noninvasive.min.js.map +1 -1
  29. package/dist/indexeddbshim.js +471 -295
  30. package/dist/indexeddbshim.js.map +1 -1
  31. package/dist/indexeddbshim.min.js +4 -4
  32. package/dist/indexeddbshim.min.js.map +1 -1
  33. package/dist/node-UnicodeIdentifiers.d.ts.map +1 -1
  34. package/dist/node.d.ts.map +1 -1
  35. package/dist/nodeSQLiteDatabase.d.ts +65 -0
  36. package/dist/nodeSQLiteDatabase.d.ts.map +1 -0
  37. package/dist/nodeWebSQL.d.ts.map +1 -1
  38. package/dist/util.d.ts +5 -0
  39. package/dist/util.d.ts.map +1 -1
  40. package/package.json +17 -13
  41. package/src/CFG.js +3 -0
  42. package/src/IDBCursor.js +136 -86
  43. package/src/IDBFactory.js +10 -6
  44. package/src/IDBIndex.js +1 -2
  45. package/src/IDBTransaction.js +32 -2
  46. package/src/Key.js +10 -2
  47. package/src/node-UnicodeIdentifiers.js +5 -1
  48. package/src/node.js +5 -1
  49. package/src/nodeSQLiteDatabase.js +174 -0
  50. package/src/nodeWebSQL.js +1 -3
  51. package/src/util.js +62 -3
package/src/Key.js CHANGED
@@ -283,14 +283,22 @@ const types = {
283
283
  encoded[i] = encodedItem;
284
284
  }
285
285
  encoded.push(keyTypeToEncodedChar.invalid + '-'); // append an extra item, so empty arrays sort correctly
286
- return keyTypeToEncodedChar.array + '-' + JSON.stringify(encoded);
286
+ let encodedKey = JSON.stringify(encoded);
287
+ if (CFG.escapeNULForSQLiteStatements === false) {
288
+ encodedKey = encodedKey.replaceAll(String.raw`\u0000`, '\0');
289
+ }
290
+ return keyTypeToEncodedChar.array + '-' + encodedKey;
287
291
  },
288
292
  /**
289
293
  * @param {string} key
290
294
  * @returns {ValueTypeArray}
291
295
  */
292
296
  decode (key) {
293
- const decoded = JSON.parse(key.slice(2));
297
+ let decodedKey = key.slice(2);
298
+ if (CFG.escapeNULForSQLiteStatements === false) {
299
+ decodedKey = decodedKey.replaceAll('\0', String.raw`\u0000`);
300
+ }
301
+ const decoded = JSON.parse(decodedKey);
294
302
  decoded.pop(); // remove the extra item
295
303
  for (let i = 0; i < decoded.length; i++) {
296
304
  const item = decoded[i];
@@ -13,7 +13,11 @@ CFG.win = {openDatabase: nodeWebSQL};
13
13
  * @returns {import('./setGlobalVars.js').ShimmedObject|Window}
14
14
  */
15
15
  const __setGlobalVars = function (idb, initialConfig = {}) {
16
- const obj = setGlobalVars(idb, {fs, ...initialConfig});
16
+ const obj = setGlobalVars(idb, {
17
+ fs,
18
+ escapeNULForSQLiteStatements: false,
19
+ ...initialConfig
20
+ });
17
21
  /* istanbul ignore next -- TS guard */
18
22
  if (!obj.shimIndexedDB) {
19
23
  return obj;
package/src/node.js CHANGED
@@ -12,7 +12,11 @@ CFG.win = {openDatabase: nodeWebSQL};
12
12
  * @returns {import('./setGlobalVars.js').ShimmedObject|Window}
13
13
  */
14
14
  const __setGlobalVars = function (idb, initialConfig = {}) {
15
- return setGlobalVars(idb, {fs, ...initialConfig});
15
+ return setGlobalVars(idb, {
16
+ fs,
17
+ escapeNULForSQLiteStatements: false,
18
+ ...initialConfig
19
+ });
16
20
  };
17
21
 
18
22
  export default __setGlobalVars;
@@ -0,0 +1,174 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ /**
4
+ *
5
+ */
6
+ class SQLiteResult {
7
+ /**
8
+ * @param {Error|null|undefined} error
9
+ * @param {number|undefined} [insertId]
10
+ * @param {number} [rowsAffected]
11
+ * @param {object[]} [rows]
12
+ */
13
+ constructor (error, insertId, rowsAffected, rows) {
14
+ this.error = error;
15
+ this.insertId = insertId;
16
+ this.rowsAffected = rowsAffected;
17
+ this.rows = rows;
18
+ }
19
+ }
20
+
21
+ const READ_ONLY_ERROR = new Error(
22
+ 'could not prepare statement (23 not authorized)'
23
+ );
24
+
25
+ /**
26
+ * @typedef {(sql: string, duration: number) => void} SQLProfileCallback
27
+ */
28
+
29
+ /**
30
+ * @typedef {((sql: string) => void)|undefined} SQLTraceCallback
31
+ */
32
+
33
+ /**
34
+ * @param {string} name
35
+ * @param {{busyTimeout?: number, trace?: (sql: string) => void, profile?: SQLProfileCallback}} [opts]
36
+ * @returns {void}
37
+ */
38
+ function SQLiteDatabase (name, opts = {}) {
39
+ /** @type {import('better-sqlite3').Database} */
40
+ const db = new Database(name);
41
+
42
+ /** @type {SQLTraceCallback} */
43
+ // eslint-disable-next-line prefer-destructuring -- TS
44
+ let trace = opts.trace;
45
+ /** @type {SQLProfileCallback|undefined} */
46
+ // eslint-disable-next-line prefer-destructuring -- TS
47
+ let profile = opts.profile;
48
+
49
+ if (opts.busyTimeout) {
50
+ db.pragma('busy_timeout = ' + Number(opts.busyTimeout));
51
+ }
52
+
53
+ // Kept untyped (rather than the better-sqlite3 `Database` type) since that
54
+ // type is internal to `@types/better-sqlite3` and can't be named in this
55
+ // file's emitted declaration.
56
+ this._db = /** @type {any} */ ({
57
+ _db: db,
58
+ /**
59
+ * Compatibility with node-sqlite3's configure API.
60
+ * @param {'busyTimeout'|'trace'|'profile'} option
61
+ * @param {number|((sql: string, duration?: number) => void)} value
62
+ * @returns {void}
63
+ */
64
+ configure (option, value) {
65
+ if (option === 'busyTimeout') {
66
+ db.pragma('busy_timeout = ' + Number(/** @type {number} */ (value)));
67
+ return;
68
+ }
69
+ if (option === 'trace') {
70
+ trace = /** @type {(sql: string) => void} */ (value);
71
+ return;
72
+ }
73
+ if (option === 'profile') {
74
+ profile = /** @type {SQLProfileCallback} */ (value);
75
+ }
76
+ },
77
+ /**
78
+ * Compatibility with callback-oriented close semantics.
79
+ * @param {(err?: Error|null) => void} [cb]
80
+ * @returns {void}
81
+ */
82
+ close (cb) {
83
+ try {
84
+ db.close();
85
+ if (cb) {
86
+ return cb(null);
87
+ }
88
+ } catch (err) {
89
+ if (cb) {
90
+ return cb(/** @type {Error} */ (err));
91
+ }
92
+ }
93
+ return undefined;
94
+ },
95
+ getTrace () {
96
+ return trace;
97
+ },
98
+ getProfile () {
99
+ return profile;
100
+ }
101
+ });
102
+ }
103
+
104
+ /**
105
+ * @param {import('better-sqlite3').Database} db
106
+ * @param {string} sql
107
+ * @param {unknown[]} args
108
+ * @returns {object[]}
109
+ */
110
+ function runSelect (db, sql, args) {
111
+ const stmt = db.prepare(sql);
112
+ return stmt.reader ? /** @type {object[]} */ (stmt.all(...args)) : [];
113
+ }
114
+
115
+ /**
116
+ * @param {import('better-sqlite3').Database} db
117
+ * @param {string} sql
118
+ * @param {unknown[]} args
119
+ * @returns {import('better-sqlite3').RunResult}
120
+ */
121
+ function runNonSelect (db, sql, args) {
122
+ const stmt = db.prepare(sql);
123
+ return stmt.run(...args);
124
+ }
125
+
126
+ /**
127
+ * @param {{sql: string, args: unknown[]}[]} queries
128
+ * @param {boolean} readOnly
129
+ * @param {(err: Error|null, results?: SQLiteResult[]) => void} callback
130
+ * @returns {void}
131
+ */
132
+ SQLiteDatabase.prototype.exec = function exec (queries, readOnly, callback) {
133
+ const db = this._db._db;
134
+ const len = queries.length;
135
+ const results = Array.from({length: len});
136
+
137
+ for (let i = 0; i < len; i++) {
138
+ const query = queries[i];
139
+ const {sql, args} = query;
140
+ const isSelect = (/^\s*SELECT\b/iu).test(sql);
141
+ if (readOnly && !isSelect) {
142
+ results[i] = new SQLiteResult(READ_ONLY_ERROR);
143
+ continue;
144
+ }
145
+ const trace = this._db.getTrace();
146
+ const profile = this._db.getProfile();
147
+ // eslint-disable-next-line unicorn/prefer-bigint-literals -- `0n` needs ES2020+ target for tsc
148
+ const start = profile ? process.hrtime.bigint() : BigInt(0);
149
+ try {
150
+ if (trace) {
151
+ trace(sql);
152
+ }
153
+ if (isSelect) {
154
+ const rows = runSelect(db, sql, args);
155
+ results[i] = new SQLiteResult(null, undefined, 0, rows);
156
+ } else {
157
+ const executionResult = runNonSelect(db, sql, args);
158
+ const insertId = Number(executionResult.lastInsertRowid);
159
+ results[i] = new SQLiteResult(null, insertId, executionResult.changes, []);
160
+ }
161
+ } catch (err) {
162
+ results[i] = new SQLiteResult(/** @type {Error} */ (err));
163
+ } finally {
164
+ if (profile) {
165
+ profile(sql, Number(process.hrtime.bigint() - start));
166
+ }
167
+ }
168
+ }
169
+ queueMicrotask(() => {
170
+ callback(null, results);
171
+ });
172
+ };
173
+
174
+ export default SQLiteDatabase;
package/src/nodeWebSQL.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import customOpenDatabase from 'websql-configurable/custom/index.js';
2
- import SQLiteDatabase from 'websql-configurable/lib/sqlite/SQLiteDatabase.js';
2
+ import SQLiteDatabase from './nodeSQLiteDatabase.js';
3
3
  import CFG from './CFG.js';
4
4
 
5
5
  /**
@@ -12,11 +12,9 @@ function wrappedSQLiteDatabase (name) {
12
12
  db._db.configure('busyTimeout', /** @type {number} */ (CFG.sqlBusyTimeout)); // Default is 1000
13
13
  }
14
14
  if (CFG.sqlTrace) {
15
- // @ts-expect-error native API?
16
15
  db._db.configure('trace', CFG.sqlTrace);
17
16
  }
18
17
  if (CFG.sqlProfile) {
19
- // @ts-expect-error native API?
20
18
  db._db.configure('profile', CFG.sqlProfile);
21
19
  }
22
20
  return db;
package/src/util.js CHANGED
@@ -51,7 +51,12 @@ function escapeNameForSQLiteIdentifier (arg) {
51
51
  * @returns {string}
52
52
  */
53
53
  function escapeSQLiteStatement (arg) {
54
- return escapeUnmatchedSurrogates(arg.replaceAll('^', '^^').replaceAll('\0', '^0'));
54
+ const escaped = arg.replaceAll('^', '^^');
55
+ return escapeUnmatchedSurrogates(
56
+ CFG.escapeNULForSQLiteStatements === false
57
+ ? escaped
58
+ : escaped.replaceAll('\0', '^0')
59
+ );
55
60
  }
56
61
 
57
62
  /**
@@ -59,7 +64,11 @@ function escapeSQLiteStatement (arg) {
59
64
  * @returns {string}
60
65
  */
61
66
  function unescapeSQLiteResponse (arg) {
62
- return unescapeUnmatchedSurrogates(arg)
67
+ const unescaped = unescapeUnmatchedSurrogates(arg);
68
+ if (CFG.escapeNULForSQLiteStatements === false) {
69
+ return unescaped.replaceAll('^^', '^');
70
+ }
71
+ return unescaped
63
72
  .replaceAll(/(\^+)0/gu, (_, esc) => {
64
73
  return esc.length % 2
65
74
  ? esc.slice(1) + '\0'
@@ -548,6 +557,56 @@ function isNullish (v) {
548
557
  return v === null || v === undefined;
549
558
  }
550
559
 
560
+ /**
561
+ * Cursor/request continuation chains can call each other synchronously
562
+ * (e.g. walking a large prefetched buffer, or advancing many interleaved
563
+ * cursors in one transaction) which, for large enough record counts, can
564
+ * exceed the JS engine's call stack ("Maximum call stack size exceeded").
565
+ *
566
+ * This can't be fixed by deferring continuations to a microtask/macrotask:
567
+ * the transaction machinery synchronously checks (once the current call
568
+ * stack unwinds) whether there are any pending requests left in order to
569
+ * decide it's safe to commit; deferring a continuation opens a real gap in
570
+ * which that check runs first and the transaction completes prematurely,
571
+ * with the deferred continuation then acting on an already-finished
572
+ * transaction (a hang, since it can never resolve).
573
+ *
574
+ * Instead, this implements a true trampoline: the first call starts a
575
+ * synchronous work queue and drains it in a flat loop; any call made while
576
+ * already draining (i.e. a continuation triggering another continuation)
577
+ * is simply appended to that same queue and returns immediately instead of
578
+ * recursing. This keeps everything synchronous (so no completion-check
579
+ * race is introduced) while preventing the call stack from growing with
580
+ * each successive continuation.
581
+ */
582
+ const continuationState = {
583
+ /** @type {Array<() => void>|null} */
584
+ queue: null
585
+ };
586
+
587
+ /**
588
+ * @param {() => void} fn
589
+ * @returns {void}
590
+ */
591
+ function runContinuationSafely (fn) {
592
+ if (continuationState.queue) {
593
+ continuationState.queue.push(fn);
594
+ return;
595
+ }
596
+ const queue = [fn];
597
+ continuationState.queue = queue;
598
+ try {
599
+ while (queue.length) {
600
+ const next = /** @type {() => void} */ (queue.shift());
601
+ next();
602
+ }
603
+ } finally {
604
+ // Ensure a thrown exception can't leave the queue permanently
605
+ // stuck (which would silently swallow all future continuations).
606
+ continuationState.queue = null;
607
+ }
608
+ }
609
+
551
610
  export {escapeSQLiteStatement, unescapeSQLiteResponse,
552
611
  escapeDatabaseNameForSQLAndFiles, unescapeDatabaseNameForSQLAndFiles,
553
612
  escapeStoreNameForSQL, escapeIndexNameForSQL, escapeIndexNameForSQLKeyColumn,
@@ -558,4 +617,4 @@ export {escapeSQLiteStatement, unescapeSQLiteResponse,
558
617
  defineListenerProperties, defineReadonlyProperties,
559
618
  isValidKeyPath, enforceRange,
560
619
  convertToDOMString, convertToSequenceDOMString,
561
- isNullish};
620
+ isNullish, runContinuationSafely};