@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/dist/wa-sqlite-async.mjs +16 -0
  4. package/dist/wa-sqlite-async.wasm +0 -0
  5. package/dist/wa-sqlite-jspi.mjs +16 -0
  6. package/dist/wa-sqlite-jspi.wasm +0 -0
  7. package/dist/wa-sqlite.mjs +16 -0
  8. package/dist/wa-sqlite.wasm +0 -0
  9. package/package.json +45 -0
  10. package/src/FacadeVFS.js +508 -0
  11. package/src/VFS.js +222 -0
  12. package/src/WebLocksMixin.js +412 -0
  13. package/src/examples/AccessHandlePoolVFS.js +458 -0
  14. package/src/examples/IDBBatchAtomicVFS.js +820 -0
  15. package/src/examples/IDBMirrorVFS.js +875 -0
  16. package/src/examples/MemoryAsyncVFS.js +100 -0
  17. package/src/examples/MemoryVFS.js +176 -0
  18. package/src/examples/OPFSAdaptiveVFS.js +437 -0
  19. package/src/examples/OPFSAnyContextVFS.js +300 -0
  20. package/src/examples/OPFSCoopSyncVFS.js +590 -0
  21. package/src/examples/OPFSPermutedVFS.js +1214 -0
  22. package/src/examples/README.md +89 -0
  23. package/src/examples/tag.js +82 -0
  24. package/src/sqlite-api.js +914 -0
  25. package/src/sqlite-constants.js +275 -0
  26. package/src/types/globals.d.ts +60 -0
  27. package/src/types/index.d.ts +1302 -0
  28. package/src/types/tsconfig.json +6 -0
  29. package/test/AccessHandlePoolVFS.test.js +27 -0
  30. package/test/IDBBatchAtomicVFS.test.js +97 -0
  31. package/test/IDBMirrorVFS.test.js +27 -0
  32. package/test/MemoryAsyncVFS.test.js +27 -0
  33. package/test/MemoryVFS.test.js +27 -0
  34. package/test/OPFSAdaptiveVFS.test.js +27 -0
  35. package/test/OPFSAnyContextVFS.test.js +27 -0
  36. package/test/OPFSCoopSyncVFS.test.js +27 -0
  37. package/test/OPFSPermutedVFS.test.js +27 -0
  38. package/test/TestContext.js +96 -0
  39. package/test/WebLocksMixin.test.js +521 -0
  40. package/test/api.test.js +49 -0
  41. package/test/api_exec.js +89 -0
  42. package/test/api_misc.js +63 -0
  43. package/test/api_statements.js +426 -0
  44. package/test/callbacks.test.js +373 -0
  45. package/test/sql.test.js +64 -0
  46. package/test/sql_0001.js +49 -0
  47. package/test/sql_0002.js +52 -0
  48. package/test/sql_0003.js +83 -0
  49. package/test/sql_0004.js +81 -0
  50. package/test/sql_0005.js +76 -0
  51. package/test/test-worker.js +204 -0
  52. package/test/vfs_xAccess.js +2 -0
  53. package/test/vfs_xClose.js +52 -0
  54. package/test/vfs_xOpen.js +91 -0
  55. package/test/vfs_xRead.js +38 -0
  56. 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;