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.
- package/README.md +62 -0
- package/badges/licenses-badge-dev.svg +1 -1
- package/badges/licenses-badge.svg +1 -1
- package/dist/CFG.d.ts +1 -0
- package/dist/CFG.d.ts.map +1 -1
- package/dist/IDBCursor.d.ts +9 -2
- package/dist/IDBCursor.d.ts.map +1 -1
- package/dist/IDBFactory.d.ts.map +1 -1
- package/dist/IDBIndex.d.ts.map +1 -1
- package/dist/IDBTransaction.d.ts +21 -0
- package/dist/IDBTransaction.d.ts.map +1 -1
- package/dist/Key.d.ts.map +1 -1
- package/dist/indexeddbshim-Key.js +25 -6
- 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 +1402 -818
- package/dist/indexeddbshim-UnicodeIdentifiers-node.cjs.map +1 -1
- package/dist/indexeddbshim-UnicodeIdentifiers.js +467 -298
- package/dist/indexeddbshim-UnicodeIdentifiers.js.map +1 -1
- package/dist/indexeddbshim-UnicodeIdentifiers.min.js +4 -4
- package/dist/indexeddbshim-UnicodeIdentifiers.min.js.map +1 -1
- package/dist/indexeddbshim-node.cjs +1402 -818
- package/dist/indexeddbshim-node.cjs.map +1 -1
- package/dist/indexeddbshim-noninvasive.js +467 -298
- package/dist/indexeddbshim-noninvasive.js.map +1 -1
- package/dist/indexeddbshim-noninvasive.min.js +4 -4
- package/dist/indexeddbshim-noninvasive.min.js.map +1 -1
- package/dist/indexeddbshim.js +471 -295
- package/dist/indexeddbshim.js.map +1 -1
- package/dist/indexeddbshim.min.js +4 -4
- package/dist/indexeddbshim.min.js.map +1 -1
- package/dist/node-UnicodeIdentifiers.d.ts.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/nodeSQLiteDatabase.d.ts +65 -0
- package/dist/nodeSQLiteDatabase.d.ts.map +1 -0
- package/dist/nodeWebSQL.d.ts.map +1 -1
- package/dist/util.d.ts +5 -0
- package/dist/util.d.ts.map +1 -1
- package/package.json +17 -13
- package/src/CFG.js +3 -0
- package/src/IDBCursor.js +136 -86
- package/src/IDBFactory.js +10 -6
- package/src/IDBIndex.js +1 -2
- package/src/IDBTransaction.js +32 -2
- package/src/Key.js +10 -2
- package/src/node-UnicodeIdentifiers.js +5 -1
- package/src/node.js +5 -1
- package/src/nodeSQLiteDatabase.js +174 -0
- package/src/nodeWebSQL.js +1 -3
- 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
|
-
|
|
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
|
-
|
|
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, {
|
|
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, {
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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};
|