@livestore/wa-sqlite 1.0.1-dev.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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/wa-sqlite-async.mjs +16 -0
- package/dist/wa-sqlite-async.wasm +0 -0
- package/dist/wa-sqlite-jspi.mjs +16 -0
- package/dist/wa-sqlite-jspi.wasm +0 -0
- package/dist/wa-sqlite.mjs +16 -0
- package/dist/wa-sqlite.wasm +0 -0
- package/package.json +45 -0
- package/src/FacadeVFS.js +508 -0
- package/src/VFS.js +222 -0
- package/src/WebLocksMixin.js +412 -0
- package/src/examples/AccessHandlePoolVFS.js +458 -0
- package/src/examples/IDBBatchAtomicVFS.js +820 -0
- package/src/examples/IDBMirrorVFS.js +875 -0
- package/src/examples/MemoryAsyncVFS.js +100 -0
- package/src/examples/MemoryVFS.js +176 -0
- package/src/examples/OPFSAdaptiveVFS.js +437 -0
- package/src/examples/OPFSAnyContextVFS.js +300 -0
- package/src/examples/OPFSCoopSyncVFS.js +590 -0
- package/src/examples/OPFSPermutedVFS.js +1214 -0
- package/src/examples/README.md +89 -0
- package/src/examples/tag.js +82 -0
- package/src/sqlite-api.js +914 -0
- package/src/sqlite-constants.js +275 -0
- package/src/types/globals.d.ts +60 -0
- package/src/types/index.d.ts +1302 -0
- package/src/types/tsconfig.json +6 -0
- package/test/AccessHandlePoolVFS.test.js +27 -0
- package/test/IDBBatchAtomicVFS.test.js +97 -0
- package/test/IDBMirrorVFS.test.js +27 -0
- package/test/MemoryAsyncVFS.test.js +27 -0
- package/test/MemoryVFS.test.js +27 -0
- package/test/OPFSAdaptiveVFS.test.js +27 -0
- package/test/OPFSAnyContextVFS.test.js +27 -0
- package/test/OPFSCoopSyncVFS.test.js +27 -0
- package/test/OPFSPermutedVFS.test.js +27 -0
- package/test/TestContext.js +96 -0
- package/test/WebLocksMixin.test.js +521 -0
- package/test/api.test.js +49 -0
- package/test/api_exec.js +89 -0
- package/test/api_misc.js +63 -0
- package/test/api_statements.js +426 -0
- package/test/callbacks.test.js +373 -0
- package/test/sql.test.js +64 -0
- package/test/sql_0001.js +49 -0
- package/test/sql_0002.js +52 -0
- package/test/sql_0003.js +83 -0
- package/test/sql_0004.js +81 -0
- package/test/sql_0005.js +76 -0
- package/test/test-worker.js +204 -0
- package/test/vfs_xAccess.js +2 -0
- package/test/vfs_xClose.js +52 -0
- package/test/vfs_xOpen.js +91 -0
- package/test/vfs_xRead.js +38 -0
- package/test/vfs_xWrite.js +36 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import * as VFS from './VFS.js';
|
|
2
|
+
|
|
3
|
+
// Options for navigator.locks.request().
|
|
4
|
+
/** @type {LockOptions} */ const SHARED = { mode: 'shared' };
|
|
5
|
+
/** @type {LockOptions} */ const POLL_SHARED = { ifAvailable: true, mode: 'shared' };
|
|
6
|
+
/** @type {LockOptions} */ const POLL_EXCLUSIVE = { ifAvailable: true, mode: 'exclusive' };
|
|
7
|
+
|
|
8
|
+
const POLICIES = ['exclusive', 'shared', 'shared+hint'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef LockState
|
|
12
|
+
* @property {string} baseName
|
|
13
|
+
* @property {number} type
|
|
14
|
+
* @property {boolean} writeHint
|
|
15
|
+
*
|
|
16
|
+
* These properties are functions that release a specific lock.
|
|
17
|
+
* @property {(() => void)?} [gate]
|
|
18
|
+
* @property {(() => void)?} [access]
|
|
19
|
+
* @property {(() => void)?} [reserved]
|
|
20
|
+
* @property {(() => void)?} [hint]
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Mix-in for FacadeVFS that implements the SQLite VFS locking protocol.
|
|
25
|
+
* @param {*} superclass FacadeVFS (or subclass)
|
|
26
|
+
* @returns
|
|
27
|
+
*/
|
|
28
|
+
export const WebLocksMixin = superclass => class extends superclass {
|
|
29
|
+
#options = {
|
|
30
|
+
lockPolicy: 'exclusive',
|
|
31
|
+
lockTimeout: Infinity
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** @type {Map<number, LockState>} */ #mapIdToState = new Map();
|
|
35
|
+
|
|
36
|
+
constructor(name, module, options) {
|
|
37
|
+
super(name, module, options);
|
|
38
|
+
Object.assign(this.#options, options);
|
|
39
|
+
if (POLICIES.indexOf(this.#options.lockPolicy) === -1) {
|
|
40
|
+
throw new Error(`WebLocksMixin: invalid lock mode: ${options.lockPolicy}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {number} fileId
|
|
46
|
+
* @param {number} lockType
|
|
47
|
+
* @returns {Promise<number>}
|
|
48
|
+
*/
|
|
49
|
+
async jLock(fileId, lockType) {
|
|
50
|
+
try {
|
|
51
|
+
// Create state on first lock.
|
|
52
|
+
if (!this.#mapIdToState.has(fileId)) {
|
|
53
|
+
const name = this.getFilename(fileId);
|
|
54
|
+
const state = {
|
|
55
|
+
baseName: name,
|
|
56
|
+
type: VFS.SQLITE_LOCK_NONE,
|
|
57
|
+
writeHint: false
|
|
58
|
+
};
|
|
59
|
+
this.#mapIdToState.set(fileId, state);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const lockState = this.#mapIdToState.get(fileId);
|
|
63
|
+
if (lockType <= lockState.type) return VFS.SQLITE_OK;
|
|
64
|
+
|
|
65
|
+
switch (this.#options.lockPolicy) {
|
|
66
|
+
case 'exclusive':
|
|
67
|
+
return await this.#lockExclusive(lockState, lockType);
|
|
68
|
+
case 'shared':
|
|
69
|
+
case 'shared+hint':
|
|
70
|
+
return await this.#lockShared(lockState, lockType);
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error('WebLocksMixin: lock error', e);
|
|
74
|
+
return VFS.SQLITE_IOERR_LOCK;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {number} fileId
|
|
80
|
+
* @param {number} lockType
|
|
81
|
+
* @returns {Promise<number>}
|
|
82
|
+
*/
|
|
83
|
+
async jUnlock(fileId, lockType) {
|
|
84
|
+
try {
|
|
85
|
+
const lockState = this.#mapIdToState.get(fileId);
|
|
86
|
+
if (lockType >= lockState.type) return VFS.SQLITE_OK;
|
|
87
|
+
|
|
88
|
+
switch (this.#options.lockPolicy) {
|
|
89
|
+
case 'exclusive':
|
|
90
|
+
return await this.#unlockExclusive(lockState, lockType);
|
|
91
|
+
case 'shared':
|
|
92
|
+
case 'shared+hint':
|
|
93
|
+
return await this.#unlockShared(lockState, lockType);
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.error('WebLocksMixin: unlock error', e);
|
|
97
|
+
return VFS.SQLITE_IOERR_UNLOCK;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {number} fileId
|
|
103
|
+
* @param {DataView} pResOut
|
|
104
|
+
* @returns {Promise<number>}
|
|
105
|
+
*/
|
|
106
|
+
async jCheckReservedLock(fileId, pResOut) {
|
|
107
|
+
try {
|
|
108
|
+
const lockState = this.#mapIdToState.get(fileId);
|
|
109
|
+
switch (this.#options.lockPolicy) {
|
|
110
|
+
case 'exclusive':
|
|
111
|
+
return this.#checkReservedExclusive(lockState, pResOut);
|
|
112
|
+
case 'shared':
|
|
113
|
+
case 'shared+hint':
|
|
114
|
+
return await this.#checkReservedShared(lockState, pResOut);
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error('WebLocksMixin: check reserved lock error', e);
|
|
118
|
+
return VFS.SQLITE_IOERR_CHECKRESERVEDLOCK;
|
|
119
|
+
}
|
|
120
|
+
pResOut.setInt32(0, 0, true);
|
|
121
|
+
return VFS.SQLITE_OK;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {number} pFile
|
|
126
|
+
* @param {number} op
|
|
127
|
+
* @param {DataView} pArg
|
|
128
|
+
* @returns {number|Promise<number>}
|
|
129
|
+
*/
|
|
130
|
+
jFileControl(pFile, op, pArg) {
|
|
131
|
+
const lockState = this.#mapIdToState.get(pFile) ??
|
|
132
|
+
(() => {
|
|
133
|
+
// Call jLock() to create the lock state.
|
|
134
|
+
this.jLock(pFile, VFS.SQLITE_LOCK_NONE);
|
|
135
|
+
return this.#mapIdToState.get(pFile);
|
|
136
|
+
})();
|
|
137
|
+
if (op === WebLocksMixin.WRITE_HINT_OP_CODE &&
|
|
138
|
+
this.#options.lockPolicy === 'shared+hint'){
|
|
139
|
+
lockState.writeHint = true;
|
|
140
|
+
}
|
|
141
|
+
return VFS.SQLITE_NOTFOUND;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {LockState} lockState
|
|
146
|
+
* @param {number} lockType
|
|
147
|
+
* @returns
|
|
148
|
+
*/
|
|
149
|
+
async #lockExclusive(lockState, lockType) {
|
|
150
|
+
if (!lockState.access) {
|
|
151
|
+
if (!await this.#acquire(lockState, 'access')) {
|
|
152
|
+
return VFS.SQLITE_BUSY;
|
|
153
|
+
}
|
|
154
|
+
console.assert(!!lockState.access);
|
|
155
|
+
}
|
|
156
|
+
lockState.type = lockType;
|
|
157
|
+
return VFS.SQLITE_OK;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {LockState} lockState
|
|
162
|
+
* @param {number} lockType
|
|
163
|
+
* @returns {number}
|
|
164
|
+
*/
|
|
165
|
+
#unlockExclusive(lockState, lockType) {
|
|
166
|
+
if (lockType === VFS.SQLITE_LOCK_NONE) {
|
|
167
|
+
lockState.access?.();
|
|
168
|
+
console.assert(!lockState.access);
|
|
169
|
+
}
|
|
170
|
+
lockState.type = lockType;
|
|
171
|
+
return VFS.SQLITE_OK;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {LockState} lockState
|
|
176
|
+
* @param {DataView} pResOut
|
|
177
|
+
* @returns {number}
|
|
178
|
+
*/
|
|
179
|
+
#checkReservedExclusive(lockState, pResOut) {
|
|
180
|
+
pResOut.setInt32(0, 0, true);
|
|
181
|
+
return VFS.SQLITE_OK;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {LockState} lockState
|
|
186
|
+
* @param {number} lockType
|
|
187
|
+
* @returns
|
|
188
|
+
*/
|
|
189
|
+
async #lockShared(lockState, lockType) {
|
|
190
|
+
switch (lockState.type) {
|
|
191
|
+
case VFS.SQLITE_LOCK_NONE:
|
|
192
|
+
switch (lockType) {
|
|
193
|
+
case VFS.SQLITE_LOCK_SHARED:
|
|
194
|
+
if (lockState.writeHint) {
|
|
195
|
+
// xFileControl() has hinted that this transaction will
|
|
196
|
+
// write. Acquire the hint lock, which is required to reach
|
|
197
|
+
// the RESERVED state.
|
|
198
|
+
if (!await this.#acquire(lockState, 'hint')) {
|
|
199
|
+
// Timeout before lock acquired.
|
|
200
|
+
return VFS.SQLITE_BUSY;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Must have the gate lock to request the access lock.
|
|
205
|
+
if (!await this.#acquire(lockState, 'gate', SHARED)) {
|
|
206
|
+
// Timeout before lock acquired.
|
|
207
|
+
lockState.hint?.();
|
|
208
|
+
return VFS.SQLITE_BUSY;
|
|
209
|
+
}
|
|
210
|
+
await this.#acquire(lockState, 'access', SHARED);
|
|
211
|
+
lockState.gate();
|
|
212
|
+
console.assert(!lockState.gate);
|
|
213
|
+
console.assert(!!lockState.access);
|
|
214
|
+
console.assert(!lockState.reserved);
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
default:
|
|
218
|
+
throw new Error('unsupported lock transition');
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
case VFS.SQLITE_LOCK_SHARED:
|
|
222
|
+
switch (lockType) {
|
|
223
|
+
case VFS.SQLITE_LOCK_RESERVED:
|
|
224
|
+
if (this.#options.lockPolicy === 'shared+hint') {
|
|
225
|
+
// Ideally we should already have the hint lock, but if not
|
|
226
|
+
// poll for it here.
|
|
227
|
+
if (!lockState.hint &&
|
|
228
|
+
!await this.#acquire(lockState, 'hint', POLL_EXCLUSIVE)) {
|
|
229
|
+
// Another connection has the hint lock so this is a
|
|
230
|
+
// deadlock. This connection must retry.
|
|
231
|
+
return VFS.SQLITE_BUSY;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Poll for the reserved lock. This should always succeed
|
|
236
|
+
// if all clients use the 'shared+hint' policy.
|
|
237
|
+
if (!await this.#acquire(lockState, 'reserved', POLL_EXCLUSIVE)) {
|
|
238
|
+
// This is a deadlock. The connection holding the reserved
|
|
239
|
+
// lock blocks us, and it can't acquire an exclusive access
|
|
240
|
+
// lock because we hold a shared access lock. This connection
|
|
241
|
+
// must retry.
|
|
242
|
+
lockState.hint?.();
|
|
243
|
+
return VFS.SQLITE_BUSY;
|
|
244
|
+
}
|
|
245
|
+
lockState.access();
|
|
246
|
+
console.assert(!lockState.gate);
|
|
247
|
+
console.assert(!lockState.access);
|
|
248
|
+
console.assert(!!lockState.reserved);
|
|
249
|
+
break;
|
|
250
|
+
|
|
251
|
+
case VFS.SQLITE_LOCK_EXCLUSIVE:
|
|
252
|
+
// Jumping directly from SHARED to EXCLUSIVE without passing
|
|
253
|
+
// through RESERVED is only done with a hot journal.
|
|
254
|
+
if (!await this.#acquire(lockState, 'gate')) {
|
|
255
|
+
// Timeout before lock acquired.
|
|
256
|
+
return VFS.SQLITE_BUSY;
|
|
257
|
+
}
|
|
258
|
+
lockState.access();
|
|
259
|
+
if (!await this.#acquire(lockState, 'access')) {
|
|
260
|
+
// Timeout before lock acquired.
|
|
261
|
+
lockState.gate();
|
|
262
|
+
return VFS.SQLITE_BUSY;
|
|
263
|
+
}
|
|
264
|
+
console.assert(!!lockState.gate);
|
|
265
|
+
console.assert(!!lockState.access);
|
|
266
|
+
console.assert(!lockState.reserved);
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
default:
|
|
270
|
+
throw new Error('unsupported lock transition');
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
case VFS.SQLITE_LOCK_RESERVED:
|
|
274
|
+
switch (lockType) {
|
|
275
|
+
case VFS.SQLITE_LOCK_EXCLUSIVE:
|
|
276
|
+
// Prevent other connections from entering the SHARED state.
|
|
277
|
+
if (!await this.#acquire(lockState, 'gate')) {
|
|
278
|
+
// Timeout before lock acquired.
|
|
279
|
+
return VFS.SQLITE_BUSY;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Block until all other connections exit the SHARED state.
|
|
283
|
+
if (!await this.#acquire(lockState, 'access')) {
|
|
284
|
+
// Timeout before lock acquired.
|
|
285
|
+
lockState.gate();
|
|
286
|
+
return VFS.SQLITE_BUSY;
|
|
287
|
+
}
|
|
288
|
+
console.assert(!!lockState.gate);
|
|
289
|
+
console.assert(!!lockState.access);
|
|
290
|
+
console.assert(!!lockState.reserved);
|
|
291
|
+
break;
|
|
292
|
+
|
|
293
|
+
default:
|
|
294
|
+
throw new Error('unsupported lock transition');
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
lockState.type = lockType;
|
|
299
|
+
return VFS.SQLITE_OK;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {LockState} lockState
|
|
304
|
+
* @param {number} lockType
|
|
305
|
+
* @returns
|
|
306
|
+
*/
|
|
307
|
+
async #unlockShared(lockState, lockType) {
|
|
308
|
+
// lockType can only be SQLITE_LOCK_SHARED or SQLITE_LOCK_NONE.
|
|
309
|
+
if (lockType === VFS.SQLITE_LOCK_NONE) {
|
|
310
|
+
lockState.access?.();
|
|
311
|
+
lockState.gate?.();
|
|
312
|
+
lockState.reserved?.();
|
|
313
|
+
lockState.hint?.();
|
|
314
|
+
lockState.writeHint = false;
|
|
315
|
+
console.assert(!lockState.access);
|
|
316
|
+
console.assert(!lockState.gate);
|
|
317
|
+
console.assert(!lockState.reserved);
|
|
318
|
+
console.assert(!lockState.hint);
|
|
319
|
+
} else { // lockType === VFS.SQLITE_LOCK_SHARED
|
|
320
|
+
switch (lockState.type) {
|
|
321
|
+
case VFS.SQLITE_LOCK_EXCLUSIVE:
|
|
322
|
+
// Release our exclusive access lock and reacquire it with a
|
|
323
|
+
// shared lock. This should always succeed because we hold
|
|
324
|
+
// the gate lock.
|
|
325
|
+
lockState.access();
|
|
326
|
+
await this.#acquire(lockState, 'access', SHARED);
|
|
327
|
+
|
|
328
|
+
// Release our gate and reserved locks. We might not have a
|
|
329
|
+
// reserved lock if we were handling a hot journal.
|
|
330
|
+
lockState.gate();
|
|
331
|
+
lockState.reserved?.();
|
|
332
|
+
lockState.hint?.();
|
|
333
|
+
console.assert(!!lockState.access);
|
|
334
|
+
console.assert(!lockState.gate);
|
|
335
|
+
console.assert(!lockState.reserved);
|
|
336
|
+
break;
|
|
337
|
+
|
|
338
|
+
case VFS.SQLITE_LOCK_RESERVED:
|
|
339
|
+
// This transition is rare, probably only on an I/O error
|
|
340
|
+
// while writing to a journal file.
|
|
341
|
+
await this.#acquire(lockState, 'access', SHARED);
|
|
342
|
+
lockState.reserved();
|
|
343
|
+
lockState.hint?.();
|
|
344
|
+
console.assert(!!lockState.access);
|
|
345
|
+
console.assert(!lockState.gate);
|
|
346
|
+
console.assert(!lockState.reserved);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
lockState.type = lockType;
|
|
351
|
+
return VFS.SQLITE_OK;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @param {LockState} lockState
|
|
356
|
+
* @param {DataView} pResOut
|
|
357
|
+
* @returns {Promise<number>}
|
|
358
|
+
*/
|
|
359
|
+
async #checkReservedShared(lockState, pResOut) {
|
|
360
|
+
if (await this.#acquire(lockState, 'reserved', POLL_SHARED)) {
|
|
361
|
+
// We were able to get the lock so it was not reserved.
|
|
362
|
+
lockState.reserved();
|
|
363
|
+
pResOut.setInt32(0, 0, true);
|
|
364
|
+
} else {
|
|
365
|
+
pResOut.setInt32(0, 1, true);
|
|
366
|
+
}
|
|
367
|
+
return VFS.SQLITE_OK;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @param {LockState} lockState
|
|
372
|
+
* @param {'gate'|'access'|'reserved'|'hint'} name
|
|
373
|
+
* @param {LockOptions} options
|
|
374
|
+
* @returns {Promise<boolean>}
|
|
375
|
+
*/
|
|
376
|
+
#acquire(lockState, name, options = {}) {
|
|
377
|
+
console.assert(!lockState[name]);
|
|
378
|
+
return new Promise(resolve => {
|
|
379
|
+
if (!options.ifAvailable && this.#options.lockTimeout < Infinity) {
|
|
380
|
+
// Add a timeout to the lock request.
|
|
381
|
+
const controller = new AbortController();
|
|
382
|
+
options = Object.assign({}, options, { signal: controller.signal });
|
|
383
|
+
setTimeout(() => {
|
|
384
|
+
controller.abort();
|
|
385
|
+
resolve?.(false);
|
|
386
|
+
}, this.#options.lockTimeout);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const lockName = `lock##${lockState.baseName}##${name}`;
|
|
390
|
+
navigator.locks.request(lockName, options, lock => {
|
|
391
|
+
if (lock) {
|
|
392
|
+
return new Promise(release => {
|
|
393
|
+
lockState[name] = () => {
|
|
394
|
+
release();
|
|
395
|
+
lockState[name] = null;
|
|
396
|
+
};
|
|
397
|
+
resolve(true);
|
|
398
|
+
resolve = null;
|
|
399
|
+
});
|
|
400
|
+
} else {
|
|
401
|
+
lockState[name] = null;
|
|
402
|
+
resolve(false);
|
|
403
|
+
resolve = null;
|
|
404
|
+
}
|
|
405
|
+
}).catch(e => {
|
|
406
|
+
if (e.name !== 'AbortError') throw e;
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
WebLocksMixin.WRITE_HINT_OP_CODE = -9999;
|