@livestore/wa-sqlite 0.4.0-dev.22 → 0.4.0-dev.24
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 +46 -36
- package/dist/README.md +13 -13
- package/dist/fts5/wa-sqlite.mjs +1 -1
- package/dist/fts5/wa-sqlite.node.mjs +1 -1
- package/dist/fts5/wa-sqlite.node.wasm +0 -0
- package/dist/fts5/wa-sqlite.wasm +0 -0
- package/dist/wa-sqlite-async.mjs +1 -1
- package/dist/wa-sqlite-async.wasm +0 -0
- package/dist/wa-sqlite-jspi.mjs +1 -1
- package/dist/wa-sqlite-jspi.wasm +0 -0
- package/dist/wa-sqlite.mjs +1 -1
- package/dist/wa-sqlite.node.mjs +1 -1
- package/dist/wa-sqlite.node.wasm +0 -0
- package/dist/wa-sqlite.wasm +0 -0
- package/package.json +40 -29
- package/src/FacadeVFS.js +252 -261
- package/src/VFS.js +84 -85
- package/src/WebLocksMixin.js +357 -351
- package/src/examples/AccessHandlePoolVFS.js +185 -194
- package/src/examples/IDBBatchAtomicVFS.js +429 -409
- package/src/examples/IDBMirrorVFS.js +402 -409
- package/src/examples/MemoryAsyncVFS.js +32 -37
- package/src/examples/MemoryVFS.js +71 -75
- package/src/examples/OPFSAdaptiveVFS.js +206 -206
- package/src/examples/OPFSAnyContextVFS.js +141 -140
- package/src/examples/OPFSCoopSyncVFS.js +297 -299
- package/src/examples/OPFSPermutedVFS.js +529 -540
- package/src/examples/README.md +27 -15
- package/src/examples/tag.js +27 -27
- package/src/sqlite-api.js +910 -941
- package/src/sqlite-constants.js +246 -232
- package/src/types/globals.d.ts +52 -52
- package/src/types/index.d.ts +586 -576
- package/test/AccessHandlePoolVFS.test.js +21 -21
- package/test/IDBBatchAtomicVFS.test.js +69 -69
- package/test/IDBMirrorVFS.test.js +21 -21
- package/test/MemoryAsyncVFS.test.js +21 -21
- package/test/MemoryVFS.test.js +21 -21
- package/test/OPFSAdaptiveVFS.test.js +21 -21
- package/test/OPFSAnyContextVFS.test.js +21 -21
- package/test/OPFSCoopSyncVFS.test.js +21 -21
- package/test/OPFSPermutedVFS.test.js +21 -21
- package/test/TestContext.js +44 -41
- package/test/WebLocksMixin.test.js +369 -360
- package/test/api.test.js +23 -23
- package/test/api_exec.js +72 -61
- package/test/api_misc.js +53 -54
- package/test/api_statements.js +271 -279
- package/test/callbacks.test.js +492 -478
- package/test/data/idbv5.json +1135 -1
- package/test/sql.test.js +30 -30
- package/test/sql_0001.js +49 -33
- package/test/sql_0002.js +55 -34
- package/test/sql_0003.js +85 -49
- package/test/sql_0004.js +76 -47
- package/test/sql_0005.js +60 -44
- package/test/test-worker.js +171 -163
- package/test/vfs_xAccess.js +1 -2
- package/test/vfs_xClose.js +50 -49
- package/test/vfs_xOpen.js +73 -72
- package/test/vfs_xRead.js +31 -31
- package/test/vfs_xWrite.js +30 -29
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
// Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
|
|
2
|
-
import { FacadeVFS } from '../FacadeVFS.js'
|
|
3
|
-
import * as VFS from '../VFS.js'
|
|
4
|
-
import { WebLocksMixin } from '../WebLocksMixin.js'
|
|
2
|
+
import { FacadeVFS } from '../FacadeVFS.js'
|
|
3
|
+
import * as VFS from '../VFS.js'
|
|
4
|
+
import { WebLocksMixin } from '../WebLocksMixin.js'
|
|
5
5
|
|
|
6
|
-
const RETRYABLE_ERRORS = new Set([
|
|
7
|
-
'TransactionInactiveError',
|
|
8
|
-
'InvalidStateError'
|
|
9
|
-
]);
|
|
6
|
+
const RETRYABLE_ERRORS = new Set(['TransactionInactiveError', 'InvalidStateError'])
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* @typedef Metadata
|
|
@@ -17,414 +14,419 @@ const RETRYABLE_ERRORS = new Set([
|
|
|
17
14
|
*/
|
|
18
15
|
|
|
19
16
|
class File {
|
|
20
|
-
/** @type {string} */ path
|
|
21
|
-
/** @type {number} */ flags
|
|
17
|
+
/** @type {string} */ path
|
|
18
|
+
/** @type {number} */ flags
|
|
22
19
|
|
|
23
|
-
/** @type {Metadata} */ metadata
|
|
24
|
-
/** @type {number} */ fileSize = 0
|
|
20
|
+
/** @type {Metadata} */ metadata
|
|
21
|
+
/** @type {number} */ fileSize = 0
|
|
25
22
|
|
|
26
|
-
/** @type {boolean} */ needsMetadataSync = false
|
|
27
|
-
/** @type {Metadata} */ rollback = null
|
|
28
|
-
/** @type {Set<number>} */ changedPages = new Set()
|
|
23
|
+
/** @type {boolean} */ needsMetadataSync = false
|
|
24
|
+
/** @type {Metadata} */ rollback = null
|
|
25
|
+
/** @type {Set<number>} */ changedPages = new Set()
|
|
29
26
|
|
|
30
|
-
/** @type {string} */ synchronous = 'full'
|
|
31
|
-
/** @type {IDBTransactionOptions} */ txOptions = { durability: 'strict' }
|
|
27
|
+
/** @type {string} */ synchronous = 'full'
|
|
28
|
+
/** @type {IDBTransactionOptions} */ txOptions = { durability: 'strict' }
|
|
32
29
|
|
|
33
30
|
constructor(path, flags, metadata) {
|
|
34
|
-
this.path = path
|
|
35
|
-
this.flags = flags
|
|
36
|
-
this.metadata = metadata
|
|
31
|
+
this.path = path
|
|
32
|
+
this.flags = flags
|
|
33
|
+
this.metadata = metadata
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
export class IDBBatchAtomicVFS extends WebLocksMixin(FacadeVFS) {
|
|
41
|
-
/** @type {Map<number, File>} */ mapIdToFile = new Map()
|
|
42
|
-
lastError = null
|
|
38
|
+
/** @type {Map<number, File>} */ mapIdToFile = new Map()
|
|
39
|
+
lastError = null
|
|
43
40
|
|
|
44
|
-
log = null
|
|
41
|
+
log = null // console.log
|
|
45
42
|
|
|
46
|
-
/** @type {Promise} */ #isReady
|
|
47
|
-
/** @type {IDBContext} */ #idb
|
|
43
|
+
/** @type {Promise} */ #isReady
|
|
44
|
+
/** @type {IDBContext} */ #idb
|
|
48
45
|
|
|
49
46
|
static async create(name, module, options) {
|
|
50
|
-
const vfs = new IDBBatchAtomicVFS(name, module, options)
|
|
51
|
-
await vfs.isReady()
|
|
52
|
-
return vfs
|
|
47
|
+
const vfs = new IDBBatchAtomicVFS(name, module, options)
|
|
48
|
+
await vfs.isReady()
|
|
49
|
+
return vfs
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
constructor(name, module, options = {}) {
|
|
56
|
-
super(name, module, options)
|
|
57
|
-
this.#isReady = this.#initialize(options.idbName ?? name)
|
|
53
|
+
super(name, module, options)
|
|
54
|
+
this.#isReady = this.#initialize(options.idbName ?? name)
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
async #initialize(name) {
|
|
61
|
-
this.#idb = await IDBContext.create(name)
|
|
58
|
+
this.#idb = await IDBContext.create(name)
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
close() {
|
|
65
|
-
this.#idb.close()
|
|
62
|
+
this.#idb.close()
|
|
66
63
|
}
|
|
67
|
-
|
|
64
|
+
|
|
68
65
|
async isReady() {
|
|
69
|
-
await super.isReady()
|
|
70
|
-
await this.#isReady
|
|
66
|
+
await super.isReady()
|
|
67
|
+
await this.#isReady
|
|
71
68
|
}
|
|
72
69
|
|
|
73
70
|
getFilename(fileId) {
|
|
74
|
-
const pathname = this.mapIdToFile.get(fileId).path
|
|
71
|
+
const pathname = this.mapIdToFile.get(fileId).path
|
|
75
72
|
return `IDB(${this.name}):${pathname}`
|
|
76
73
|
}
|
|
77
|
-
|
|
74
|
+
|
|
78
75
|
/**
|
|
79
|
-
* @param {string?} zName
|
|
80
|
-
* @param {number} fileId
|
|
81
|
-
* @param {number} flags
|
|
82
|
-
* @param {DataView} pOutFlags
|
|
76
|
+
* @param {string?} zName
|
|
77
|
+
* @param {number} fileId
|
|
78
|
+
* @param {number} flags
|
|
79
|
+
* @param {DataView} pOutFlags
|
|
83
80
|
* @returns {Promise<number>}
|
|
84
81
|
*/
|
|
85
82
|
async jOpen(zName, fileId, flags, pOutFlags) {
|
|
86
83
|
try {
|
|
87
|
-
const url = new URL(zName || Math.random().toString(36).slice(2), 'file://')
|
|
88
|
-
const path = url.pathname
|
|
84
|
+
const url = new URL(zName || Math.random().toString(36).slice(2), 'file://')
|
|
85
|
+
const path = url.pathname
|
|
89
86
|
|
|
90
|
-
let meta = await this.#idb.q(({ metadata }) => metadata.get(path))
|
|
91
|
-
if (!meta &&
|
|
87
|
+
let meta = await this.#idb.q(({ metadata }) => metadata.get(path))
|
|
88
|
+
if (!meta && flags & VFS.SQLITE_OPEN_CREATE) {
|
|
92
89
|
meta = {
|
|
93
90
|
name: path,
|
|
94
91
|
fileSize: 0,
|
|
95
|
-
version: 0
|
|
96
|
-
}
|
|
97
|
-
await this.#idb.q(({ metadata }) => metadata.put(meta), 'rw')
|
|
92
|
+
version: 0,
|
|
93
|
+
}
|
|
94
|
+
await this.#idb.q(({ metadata }) => metadata.put(meta), 'rw')
|
|
98
95
|
}
|
|
99
|
-
|
|
96
|
+
|
|
100
97
|
if (!meta) {
|
|
101
|
-
throw new Error(`File ${path} not found`)
|
|
98
|
+
throw new Error(`File ${path} not found`)
|
|
102
99
|
}
|
|
103
100
|
|
|
104
|
-
const file = new File(path, flags, meta)
|
|
105
|
-
this.mapIdToFile.set(fileId, file)
|
|
106
|
-
pOutFlags.setInt32(0, flags, true)
|
|
107
|
-
return VFS.SQLITE_OK
|
|
101
|
+
const file = new File(path, flags, meta)
|
|
102
|
+
this.mapIdToFile.set(fileId, file)
|
|
103
|
+
pOutFlags.setInt32(0, flags, true)
|
|
104
|
+
return VFS.SQLITE_OK
|
|
108
105
|
} catch (e) {
|
|
109
|
-
this.lastError = e
|
|
110
|
-
return VFS.SQLITE_CANTOPEN
|
|
106
|
+
this.lastError = e
|
|
107
|
+
return VFS.SQLITE_CANTOPEN
|
|
111
108
|
}
|
|
112
109
|
}
|
|
113
110
|
|
|
114
111
|
/**
|
|
115
|
-
* @param {string} zName
|
|
116
|
-
* @param {number} syncDir
|
|
112
|
+
* @param {string} zName
|
|
113
|
+
* @param {number} syncDir
|
|
117
114
|
* @returns {Promise<number>}
|
|
118
115
|
*/
|
|
119
116
|
async jDelete(zName, syncDir) {
|
|
120
117
|
try {
|
|
121
|
-
const url = new URL(zName, 'file://')
|
|
122
|
-
const path = url.pathname
|
|
118
|
+
const url = new URL(zName, 'file://')
|
|
119
|
+
const path = url.pathname
|
|
123
120
|
|
|
124
121
|
this.#idb.q(({ metadata, blocks }) => {
|
|
125
|
-
const range = IDBKeyRange.bound([path, -Infinity], [path, Infinity])
|
|
126
|
-
blocks.delete(range)
|
|
127
|
-
metadata.delete(path)
|
|
128
|
-
}, 'rw')
|
|
122
|
+
const range = IDBKeyRange.bound([path, -Infinity], [path, Infinity])
|
|
123
|
+
blocks.delete(range)
|
|
124
|
+
metadata.delete(path)
|
|
125
|
+
}, 'rw')
|
|
129
126
|
|
|
130
127
|
if (syncDir) {
|
|
131
|
-
await this.#idb.sync(false)
|
|
128
|
+
await this.#idb.sync(false)
|
|
132
129
|
}
|
|
133
|
-
return VFS.SQLITE_OK
|
|
130
|
+
return VFS.SQLITE_OK
|
|
134
131
|
} catch (e) {
|
|
135
|
-
this.lastError = e
|
|
136
|
-
return VFS.SQLITE_IOERR_DELETE
|
|
132
|
+
this.lastError = e
|
|
133
|
+
return VFS.SQLITE_IOERR_DELETE
|
|
137
134
|
}
|
|
138
135
|
}
|
|
139
136
|
|
|
140
137
|
/**
|
|
141
|
-
* @param {string} zName
|
|
142
|
-
* @param {number} flags
|
|
143
|
-
* @param {DataView} pResOut
|
|
138
|
+
* @param {string} zName
|
|
139
|
+
* @param {number} flags
|
|
140
|
+
* @param {DataView} pResOut
|
|
144
141
|
* @returns {Promise<number>}
|
|
145
142
|
*/
|
|
146
143
|
async jAccess(zName, flags, pResOut) {
|
|
147
144
|
try {
|
|
148
|
-
const url = new URL(zName, 'file://')
|
|
149
|
-
const path = url.pathname
|
|
145
|
+
const url = new URL(zName, 'file://')
|
|
146
|
+
const path = url.pathname
|
|
150
147
|
|
|
151
|
-
const meta = await this.#idb.q(({ metadata }) => metadata.get(path))
|
|
152
|
-
pResOut.setInt32(0, meta ? 1 : 0, true)
|
|
153
|
-
return VFS.SQLITE_OK
|
|
148
|
+
const meta = await this.#idb.q(({ metadata }) => metadata.get(path))
|
|
149
|
+
pResOut.setInt32(0, meta ? 1 : 0, true)
|
|
150
|
+
return VFS.SQLITE_OK
|
|
154
151
|
} catch (e) {
|
|
155
|
-
this.lastError = e
|
|
156
|
-
return VFS.SQLITE_IOERR_ACCESS
|
|
157
|
-
}
|
|
152
|
+
this.lastError = e
|
|
153
|
+
return VFS.SQLITE_IOERR_ACCESS
|
|
154
|
+
}
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
/**
|
|
161
|
-
* @param {number} fileId
|
|
158
|
+
* @param {number} fileId
|
|
162
159
|
* @returns {Promise<number>}
|
|
163
160
|
*/
|
|
164
161
|
async jClose(fileId) {
|
|
165
162
|
try {
|
|
166
|
-
const file = this.mapIdToFile.get(fileId)
|
|
167
|
-
this.mapIdToFile.delete(fileId)
|
|
163
|
+
const file = this.mapIdToFile.get(fileId)
|
|
164
|
+
this.mapIdToFile.delete(fileId)
|
|
168
165
|
|
|
169
166
|
if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
|
170
167
|
await this.#idb.q(({ metadata, blocks }) => {
|
|
171
|
-
metadata.delete(file.path)
|
|
172
|
-
blocks.delete(IDBKeyRange.bound([file.path, 0], [file.path, Infinity]))
|
|
173
|
-
}, 'rw')
|
|
168
|
+
metadata.delete(file.path)
|
|
169
|
+
blocks.delete(IDBKeyRange.bound([file.path, 0], [file.path, Infinity]))
|
|
170
|
+
}, 'rw')
|
|
174
171
|
}
|
|
175
172
|
|
|
176
173
|
if (file.needsMetadataSync) {
|
|
177
|
-
this.#idb.q(({ metadata }) => metadata.put(file.metadata), 'rw')
|
|
174
|
+
this.#idb.q(({ metadata }) => metadata.put(file.metadata), 'rw')
|
|
178
175
|
}
|
|
179
|
-
await this.#idb.sync(file.synchronous === 'full')
|
|
180
|
-
return VFS.SQLITE_OK
|
|
176
|
+
await this.#idb.sync(file.synchronous === 'full')
|
|
177
|
+
return VFS.SQLITE_OK
|
|
181
178
|
} catch (e) {
|
|
182
|
-
this.lastError = e
|
|
183
|
-
return VFS.SQLITE_IOERR_CLOSE
|
|
179
|
+
this.lastError = e
|
|
180
|
+
return VFS.SQLITE_IOERR_CLOSE
|
|
184
181
|
}
|
|
185
182
|
}
|
|
186
183
|
|
|
187
184
|
/**
|
|
188
|
-
* @param {number} fileId
|
|
189
|
-
* @param {Uint8Array} pData
|
|
185
|
+
* @param {number} fileId
|
|
186
|
+
* @param {Uint8Array} pData
|
|
190
187
|
* @param {number} iOffset
|
|
191
188
|
* @returns {Promise<number>}
|
|
192
189
|
*/
|
|
193
190
|
async jRead(fileId, pData, iOffset) {
|
|
194
191
|
try {
|
|
195
|
-
const file = this.mapIdToFile.get(fileId)
|
|
192
|
+
const file = this.mapIdToFile.get(fileId)
|
|
196
193
|
|
|
197
|
-
let pDataOffset = 0
|
|
194
|
+
let pDataOffset = 0
|
|
198
195
|
while (pDataOffset < pData.byteLength) {
|
|
199
196
|
// Fetch the IndexedDB block for this file location.
|
|
200
|
-
const fileOffset = iOffset + pDataOffset
|
|
197
|
+
const fileOffset = iOffset + pDataOffset
|
|
201
198
|
const block = await this.#idb.q(({ blocks }) => {
|
|
202
|
-
const range = IDBKeyRange.bound([file.path, -fileOffset], [file.path, Infinity])
|
|
203
|
-
return blocks.get(range)
|
|
204
|
-
})
|
|
205
|
-
|
|
199
|
+
const range = IDBKeyRange.bound([file.path, -fileOffset], [file.path, Infinity])
|
|
200
|
+
return blocks.get(range)
|
|
201
|
+
})
|
|
202
|
+
|
|
206
203
|
if (!block || block.data.byteLength - block.offset <= fileOffset) {
|
|
207
|
-
pData.fill(0, pDataOffset)
|
|
208
|
-
return VFS.SQLITE_IOERR_SHORT_READ
|
|
204
|
+
pData.fill(0, pDataOffset)
|
|
205
|
+
return VFS.SQLITE_IOERR_SHORT_READ
|
|
209
206
|
}
|
|
210
207
|
|
|
211
208
|
// Copy block data.
|
|
212
|
-
const dst = pData.subarray(pDataOffset)
|
|
213
|
-
const srcOffset = fileOffset + block.offset
|
|
214
|
-
const nBytesToCopy = Math.min(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
dst.set(block.data.subarray(srcOffset, srcOffset + nBytesToCopy));
|
|
218
|
-
pDataOffset += nBytesToCopy;
|
|
209
|
+
const dst = pData.subarray(pDataOffset)
|
|
210
|
+
const srcOffset = fileOffset + block.offset
|
|
211
|
+
const nBytesToCopy = Math.min(Math.max(block.data.byteLength - srcOffset, 0), dst.byteLength)
|
|
212
|
+
dst.set(block.data.subarray(srcOffset, srcOffset + nBytesToCopy))
|
|
213
|
+
pDataOffset += nBytesToCopy
|
|
219
214
|
}
|
|
220
|
-
return VFS.SQLITE_OK
|
|
215
|
+
return VFS.SQLITE_OK
|
|
221
216
|
} catch (e) {
|
|
222
|
-
this.lastError = e
|
|
223
|
-
return VFS.SQLITE_IOERR_READ
|
|
217
|
+
this.lastError = e
|
|
218
|
+
return VFS.SQLITE_IOERR_READ
|
|
224
219
|
}
|
|
225
220
|
}
|
|
226
221
|
|
|
227
222
|
/**
|
|
228
|
-
* @param {number} fileId
|
|
229
|
-
* @param {Uint8Array} pData
|
|
223
|
+
* @param {number} fileId
|
|
224
|
+
* @param {Uint8Array} pData
|
|
230
225
|
* @param {number} iOffset
|
|
231
226
|
* @returns {number}
|
|
232
227
|
*/
|
|
233
228
|
jWrite(fileId, pData, iOffset) {
|
|
234
229
|
try {
|
|
235
|
-
const file = this.mapIdToFile.get(fileId)
|
|
230
|
+
const file = this.mapIdToFile.get(fileId)
|
|
236
231
|
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
237
232
|
if (!file.rollback) {
|
|
238
233
|
// Begin a new write transaction.
|
|
239
234
|
// Add pendingVersion to the metadata in IndexedDB. If we crash
|
|
240
235
|
// during the transaction, this lets subsequent connections
|
|
241
236
|
// know to remove blocks from the failed transaction.
|
|
242
|
-
const pending = Object.assign(
|
|
243
|
-
|
|
244
|
-
file.metadata);
|
|
245
|
-
this.#idb.q(({ metadata }) => metadata.put(pending), 'rw', file.txOptions);
|
|
237
|
+
const pending = Object.assign({ pendingVersion: file.metadata.version - 1 }, file.metadata)
|
|
238
|
+
this.#idb.q(({ metadata }) => metadata.put(pending), 'rw', file.txOptions)
|
|
246
239
|
|
|
247
|
-
file.rollback = Object.assign({}, file.metadata)
|
|
248
|
-
file.metadata.version
|
|
240
|
+
file.rollback = Object.assign({}, file.metadata)
|
|
241
|
+
file.metadata.version--
|
|
249
242
|
}
|
|
250
243
|
}
|
|
251
244
|
|
|
252
245
|
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
253
|
-
file.changedPages.add(iOffset)
|
|
246
|
+
file.changedPages.add(iOffset)
|
|
254
247
|
}
|
|
255
248
|
|
|
256
|
-
const data = pData.slice()
|
|
257
|
-
const version = file.metadata.version
|
|
258
|
-
const isOverwrite = iOffset < file.metadata.fileSize
|
|
259
|
-
if (!isOverwrite ||
|
|
260
|
-
file.flags & VFS.SQLITE_OPEN_MAIN_DB ||
|
|
261
|
-
file.flags & VFS.SQLITE_OPEN_TEMP_DB) {
|
|
249
|
+
const data = pData.slice()
|
|
250
|
+
const version = file.metadata.version
|
|
251
|
+
const isOverwrite = iOffset < file.metadata.fileSize
|
|
252
|
+
if (!isOverwrite || file.flags & VFS.SQLITE_OPEN_MAIN_DB || file.flags & VFS.SQLITE_OPEN_TEMP_DB) {
|
|
262
253
|
const block = {
|
|
263
254
|
path: file.path,
|
|
264
255
|
offset: -iOffset,
|
|
265
256
|
version: version,
|
|
266
|
-
data: pData.slice()
|
|
267
|
-
}
|
|
268
|
-
this.#idb.q(
|
|
269
|
-
blocks
|
|
270
|
-
|
|
271
|
-
|
|
257
|
+
data: pData.slice(),
|
|
258
|
+
}
|
|
259
|
+
this.#idb.q(
|
|
260
|
+
({ blocks }) => {
|
|
261
|
+
blocks.put(block)
|
|
262
|
+
file.changedPages.add(iOffset)
|
|
263
|
+
},
|
|
264
|
+
'rw',
|
|
265
|
+
file.txOptions,
|
|
266
|
+
)
|
|
272
267
|
} else {
|
|
273
|
-
this.#idb.q(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
[file.path, -iOffset],
|
|
277
|
-
|
|
278
|
-
const block = await blocks.get(range);
|
|
279
|
-
|
|
280
|
-
// Modify the block data.
|
|
281
|
-
// @ts-ignore
|
|
282
|
-
block.data.subarray(iOffset + block.offset).set(data);
|
|
283
|
-
|
|
284
|
-
// Write back.
|
|
285
|
-
blocks.put(block);
|
|
286
|
-
}, 'rw', file.txOptions);
|
|
268
|
+
this.#idb.q(
|
|
269
|
+
async ({ blocks }) => {
|
|
270
|
+
// Read the existing block.
|
|
271
|
+
const range = IDBKeyRange.bound([file.path, -iOffset], [file.path, Infinity])
|
|
272
|
+
const block = await blocks.get(range)
|
|
287
273
|
|
|
274
|
+
// Modify the block data.
|
|
275
|
+
// @ts-ignore
|
|
276
|
+
block.data.subarray(iOffset + block.offset).set(data)
|
|
277
|
+
|
|
278
|
+
// Write back.
|
|
279
|
+
blocks.put(block)
|
|
280
|
+
},
|
|
281
|
+
'rw',
|
|
282
|
+
file.txOptions,
|
|
283
|
+
)
|
|
288
284
|
}
|
|
289
285
|
|
|
290
286
|
if (file.metadata.fileSize < iOffset + pData.length) {
|
|
291
|
-
file.metadata.fileSize = iOffset + pData.length
|
|
292
|
-
file.needsMetadataSync = true
|
|
287
|
+
file.metadata.fileSize = iOffset + pData.length
|
|
288
|
+
file.needsMetadataSync = true
|
|
293
289
|
}
|
|
294
|
-
return VFS.SQLITE_OK
|
|
290
|
+
return VFS.SQLITE_OK
|
|
295
291
|
} catch (e) {
|
|
296
|
-
this.lastError = e
|
|
297
|
-
return VFS.SQLITE_IOERR_WRITE
|
|
292
|
+
this.lastError = e
|
|
293
|
+
return VFS.SQLITE_IOERR_WRITE
|
|
298
294
|
}
|
|
299
295
|
}
|
|
300
296
|
|
|
301
297
|
/**
|
|
302
|
-
* @param {number} fileId
|
|
303
|
-
* @param {number} iSize
|
|
298
|
+
* @param {number} fileId
|
|
299
|
+
* @param {number} iSize
|
|
304
300
|
* @returns {number}
|
|
305
301
|
*/
|
|
306
302
|
jTruncate(fileId, iSize) {
|
|
307
303
|
try {
|
|
308
|
-
const file = this.mapIdToFile.get(fileId)
|
|
304
|
+
const file = this.mapIdToFile.get(fileId)
|
|
309
305
|
if (iSize < file.metadata.fileSize) {
|
|
310
|
-
this.#idb.q(
|
|
311
|
-
|
|
312
|
-
[file.path, -Infinity],
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
306
|
+
this.#idb.q(
|
|
307
|
+
({ blocks }) => {
|
|
308
|
+
const range = IDBKeyRange.bound([file.path, -Infinity], [file.path, -iSize, Infinity])
|
|
309
|
+
blocks.delete(range)
|
|
310
|
+
},
|
|
311
|
+
'rw',
|
|
312
|
+
file.txOptions,
|
|
313
|
+
)
|
|
314
|
+
file.metadata.fileSize = iSize
|
|
315
|
+
file.needsMetadataSync = true
|
|
318
316
|
}
|
|
319
|
-
return VFS.SQLITE_OK
|
|
317
|
+
return VFS.SQLITE_OK
|
|
320
318
|
} catch (e) {
|
|
321
|
-
this.lastError = e
|
|
322
|
-
return VFS.SQLITE_IOERR_TRUNCATE
|
|
319
|
+
this.lastError = e
|
|
320
|
+
return VFS.SQLITE_IOERR_TRUNCATE
|
|
323
321
|
}
|
|
324
322
|
}
|
|
325
323
|
|
|
326
324
|
/**
|
|
327
|
-
* @param {number} fileId
|
|
328
|
-
* @param {number} flags
|
|
325
|
+
* @param {number} fileId
|
|
326
|
+
* @param {number} flags
|
|
329
327
|
* @returns {Promise<number>}
|
|
330
328
|
*/
|
|
331
329
|
async jSync(fileId, flags) {
|
|
332
330
|
try {
|
|
333
|
-
const file = this.mapIdToFile.get(fileId)
|
|
331
|
+
const file = this.mapIdToFile.get(fileId)
|
|
334
332
|
if (file.needsMetadataSync) {
|
|
335
|
-
this.#idb.q(({ metadata }) => metadata.put(file.metadata), 'rw', file.txOptions)
|
|
336
|
-
file.needsMetadataSync = false
|
|
333
|
+
this.#idb.q(({ metadata }) => metadata.put(file.metadata), 'rw', file.txOptions)
|
|
334
|
+
file.needsMetadataSync = false
|
|
337
335
|
}
|
|
338
336
|
|
|
339
337
|
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
340
338
|
// Sync is only needed here for durability. Visibility for other
|
|
341
339
|
// connections is ensured in jUnlock().
|
|
342
340
|
if (file.synchronous === 'full') {
|
|
343
|
-
await this.#idb.sync(true)
|
|
341
|
+
await this.#idb.sync(true)
|
|
344
342
|
}
|
|
345
343
|
} else {
|
|
346
|
-
await this.#idb.sync(file.synchronous === 'full')
|
|
344
|
+
await this.#idb.sync(file.synchronous === 'full')
|
|
347
345
|
}
|
|
348
|
-
return VFS.SQLITE_OK
|
|
346
|
+
return VFS.SQLITE_OK
|
|
349
347
|
} catch (e) {
|
|
350
|
-
this.lastError = e
|
|
351
|
-
return VFS.SQLITE_IOERR_FSYNC
|
|
348
|
+
this.lastError = e
|
|
349
|
+
return VFS.SQLITE_IOERR_FSYNC
|
|
352
350
|
}
|
|
353
351
|
}
|
|
354
352
|
|
|
355
353
|
/**
|
|
356
|
-
* @param {number} fileId
|
|
357
|
-
* @param {DataView} pSize64
|
|
354
|
+
* @param {number} fileId
|
|
355
|
+
* @param {DataView} pSize64
|
|
358
356
|
* @returns {number}
|
|
359
357
|
*/
|
|
360
358
|
jFileSize(fileId, pSize64) {
|
|
361
359
|
try {
|
|
362
|
-
const file = this.mapIdToFile.get(fileId)
|
|
363
|
-
pSize64.setBigInt64(0, BigInt(file.metadata.fileSize), true)
|
|
364
|
-
return VFS.SQLITE_OK
|
|
360
|
+
const file = this.mapIdToFile.get(fileId)
|
|
361
|
+
pSize64.setBigInt64(0, BigInt(file.metadata.fileSize), true)
|
|
362
|
+
return VFS.SQLITE_OK
|
|
365
363
|
} catch (e) {
|
|
366
|
-
this.lastError = e
|
|
367
|
-
return VFS.SQLITE_IOERR_FSTAT
|
|
364
|
+
this.lastError = e
|
|
365
|
+
return VFS.SQLITE_IOERR_FSTAT
|
|
368
366
|
}
|
|
369
367
|
}
|
|
370
368
|
|
|
371
369
|
/**
|
|
372
|
-
* @param {number} fileId
|
|
373
|
-
* @param {number} lockType
|
|
370
|
+
* @param {number} fileId
|
|
371
|
+
* @param {number} lockType
|
|
374
372
|
* @returns {Promise<number>}
|
|
375
373
|
*/
|
|
376
374
|
async jLock(fileId, lockType) {
|
|
377
375
|
// Call the actual lock implementation.
|
|
378
|
-
const file = this.mapIdToFile.get(fileId)
|
|
379
|
-
const result = await super.jLock(fileId, lockType)
|
|
376
|
+
const file = this.mapIdToFile.get(fileId)
|
|
377
|
+
const result = await super.jLock(fileId, lockType)
|
|
380
378
|
|
|
381
379
|
if (lockType === VFS.SQLITE_LOCK_SHARED) {
|
|
382
380
|
// Update metadata.
|
|
383
|
-
file.metadata = await this.#idb.q(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
381
|
+
file.metadata = await this.#idb.q(
|
|
382
|
+
async ({ metadata, blocks }) => {
|
|
383
|
+
// @ts-ignore
|
|
384
|
+
/** @type {Metadata} */ const m = await metadata.get(file.path)
|
|
385
|
+
if (m.pendingVersion) {
|
|
386
|
+
console.warn(`removing failed transaction ${m.pendingVersion}`)
|
|
387
|
+
await new Promise((resolve, reject) => {
|
|
388
|
+
const range = IDBKeyRange.bound([m.name, -Infinity], [m.name, Infinity])
|
|
389
|
+
const request = blocks.openCursor(range)
|
|
390
|
+
request.onsuccess = () => {
|
|
391
|
+
const cursor = request.result
|
|
392
|
+
if (cursor) {
|
|
393
|
+
const block = cursor.value
|
|
394
|
+
if (block.version < m.version) {
|
|
395
|
+
cursor.delete()
|
|
396
|
+
}
|
|
397
|
+
cursor.continue()
|
|
398
|
+
} else {
|
|
399
|
+
resolve()
|
|
397
400
|
}
|
|
398
|
-
cursor.continue();
|
|
399
|
-
} else {
|
|
400
|
-
resolve();
|
|
401
401
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
})
|
|
402
|
+
request.onerror = () => reject(request.error)
|
|
403
|
+
})
|
|
405
404
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
405
|
+
delete m.pendingVersion
|
|
406
|
+
metadata.put(m)
|
|
407
|
+
}
|
|
408
|
+
return m
|
|
409
|
+
},
|
|
410
|
+
'rw',
|
|
411
|
+
file.txOptions,
|
|
412
|
+
)
|
|
411
413
|
}
|
|
412
|
-
return result
|
|
414
|
+
return result
|
|
413
415
|
}
|
|
414
416
|
|
|
415
417
|
/**
|
|
416
|
-
* @param {number} fileId
|
|
417
|
-
* @param {number} lockType
|
|
418
|
+
* @param {number} fileId
|
|
419
|
+
* @param {number} lockType
|
|
418
420
|
* @returns {Promise<number>}
|
|
419
421
|
*/
|
|
420
422
|
async jUnlock(fileId, lockType) {
|
|
421
423
|
if (lockType === VFS.SQLITE_LOCK_NONE) {
|
|
422
|
-
const file = this.mapIdToFile.get(fileId)
|
|
423
|
-
await this.#idb.sync(file.synchronous === 'full')
|
|
424
|
+
const file = this.mapIdToFile.get(fileId)
|
|
425
|
+
await this.#idb.sync(file.synchronous === 'full')
|
|
424
426
|
}
|
|
425
427
|
|
|
426
428
|
// Call the actual unlock implementation.
|
|
427
|
-
return super.jUnlock(fileId, lockType)
|
|
429
|
+
return super.jUnlock(fileId, lockType)
|
|
428
430
|
}
|
|
429
431
|
|
|
430
432
|
/**
|
|
@@ -435,267 +437,282 @@ export class IDBBatchAtomicVFS extends WebLocksMixin(FacadeVFS) {
|
|
|
435
437
|
*/
|
|
436
438
|
jFileControl(fileId, op, pArg) {
|
|
437
439
|
try {
|
|
438
|
-
const file = this.mapIdToFile.get(fileId)
|
|
440
|
+
const file = this.mapIdToFile.get(fileId)
|
|
439
441
|
switch (op) {
|
|
440
442
|
case VFS.SQLITE_FCNTL_PRAGMA:
|
|
441
|
-
const key = extractString(pArg, 4)
|
|
442
|
-
const value = extractString(pArg, 8)
|
|
443
|
-
this.log?.('xFileControl', file.path, 'PRAGMA', key, value)
|
|
444
|
-
const setPragmaResponse = response => {
|
|
445
|
-
const encoded = new TextEncoder().encode(response)
|
|
446
|
-
const out = this._module._sqlite3_malloc(encoded.byteLength)
|
|
447
|
-
const outArray = this._module.HEAPU8.subarray(out, out + encoded.byteLength)
|
|
448
|
-
outArray.set(encoded)
|
|
449
|
-
pArg.setUint32(0, out, true)
|
|
450
|
-
return VFS.SQLITE_ERROR
|
|
451
|
-
}
|
|
443
|
+
const key = extractString(pArg, 4)
|
|
444
|
+
const value = extractString(pArg, 8)
|
|
445
|
+
this.log?.('xFileControl', file.path, 'PRAGMA', key, value)
|
|
446
|
+
const setPragmaResponse = (response) => {
|
|
447
|
+
const encoded = new TextEncoder().encode(response)
|
|
448
|
+
const out = this._module._sqlite3_malloc(encoded.byteLength)
|
|
449
|
+
const outArray = this._module.HEAPU8.subarray(out, out + encoded.byteLength)
|
|
450
|
+
outArray.set(encoded)
|
|
451
|
+
pArg.setUint32(0, out, true)
|
|
452
|
+
return VFS.SQLITE_ERROR
|
|
453
|
+
}
|
|
452
454
|
switch (key.toLowerCase()) {
|
|
453
455
|
case 'page_size':
|
|
454
456
|
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
455
457
|
// Don't allow changing the page size.
|
|
456
458
|
if (value && file.metadata.fileSize) {
|
|
457
|
-
return VFS.SQLITE_ERROR
|
|
459
|
+
return VFS.SQLITE_ERROR
|
|
458
460
|
}
|
|
459
461
|
}
|
|
460
|
-
break
|
|
462
|
+
break
|
|
461
463
|
case 'synchronous':
|
|
462
464
|
if (value) {
|
|
463
465
|
switch (value.toLowerCase()) {
|
|
464
466
|
case '0':
|
|
465
467
|
case 'off':
|
|
466
|
-
file.synchronous = 'off'
|
|
467
|
-
file.txOptions = { durability: 'relaxed' }
|
|
468
|
-
break
|
|
468
|
+
file.synchronous = 'off'
|
|
469
|
+
file.txOptions = { durability: 'relaxed' }
|
|
470
|
+
break
|
|
469
471
|
case '1':
|
|
470
472
|
case 'normal':
|
|
471
|
-
file.synchronous = 'normal'
|
|
472
|
-
file.txOptions = { durability: 'relaxed' }
|
|
473
|
-
break
|
|
473
|
+
file.synchronous = 'normal'
|
|
474
|
+
file.txOptions = { durability: 'relaxed' }
|
|
475
|
+
break
|
|
474
476
|
case '2':
|
|
475
477
|
case '3':
|
|
476
478
|
case 'full':
|
|
477
479
|
case 'extra':
|
|
478
|
-
file.synchronous = 'full'
|
|
479
|
-
file.txOptions = { durability: 'strict' }
|
|
480
|
-
break
|
|
480
|
+
file.synchronous = 'full'
|
|
481
|
+
file.txOptions = { durability: 'strict' }
|
|
482
|
+
break
|
|
481
483
|
}
|
|
482
484
|
}
|
|
483
|
-
break
|
|
485
|
+
break
|
|
484
486
|
case 'write_hint':
|
|
485
|
-
return super.jFileControl(fileId, WebLocksMixin.WRITE_HINT_OP_CODE, null)
|
|
486
|
-
|
|
487
|
-
break
|
|
487
|
+
return super.jFileControl(fileId, WebLocksMixin.WRITE_HINT_OP_CODE, null)
|
|
488
|
+
}
|
|
489
|
+
break
|
|
488
490
|
case VFS.SQLITE_FCNTL_SYNC:
|
|
489
|
-
this.log?.('xFileControl', file.path, 'SYNC')
|
|
491
|
+
this.log?.('xFileControl', file.path, 'SYNC')
|
|
490
492
|
if (file.rollback) {
|
|
491
|
-
const commitMetadata = Object.assign({}, file.metadata)
|
|
493
|
+
const commitMetadata = Object.assign({}, file.metadata)
|
|
492
494
|
const prevFileSize = file.rollback.fileSize
|
|
493
|
-
this.#idb.q(
|
|
494
|
-
metadata
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
495
|
+
this.#idb.q(
|
|
496
|
+
({ metadata, blocks }) => {
|
|
497
|
+
metadata.put(commitMetadata)
|
|
498
|
+
|
|
499
|
+
// Remove old page versions.
|
|
500
|
+
for (const offset of file.changedPages) {
|
|
501
|
+
if (offset < prevFileSize) {
|
|
502
|
+
const range = IDBKeyRange.bound(
|
|
503
|
+
[file.path, -offset, commitMetadata.version],
|
|
504
|
+
[file.path, -offset, Infinity],
|
|
505
|
+
true,
|
|
506
|
+
)
|
|
507
|
+
blocks.delete(range)
|
|
508
|
+
}
|
|
504
509
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
+
file.changedPages.clear()
|
|
511
|
+
},
|
|
512
|
+
'rw',
|
|
513
|
+
file.txOptions,
|
|
514
|
+
)
|
|
515
|
+
file.needsMetadataSync = false
|
|
516
|
+
file.rollback = null
|
|
510
517
|
}
|
|
511
|
-
break
|
|
518
|
+
break
|
|
512
519
|
case VFS.SQLITE_FCNTL_BEGIN_ATOMIC_WRITE:
|
|
513
520
|
// Every write transaction is atomic, so this is a no-op.
|
|
514
|
-
this.log?.('xFileControl', file.path, 'BEGIN_ATOMIC_WRITE')
|
|
515
|
-
return VFS.SQLITE_OK
|
|
521
|
+
this.log?.('xFileControl', file.path, 'BEGIN_ATOMIC_WRITE')
|
|
522
|
+
return VFS.SQLITE_OK
|
|
516
523
|
case VFS.SQLITE_FCNTL_COMMIT_ATOMIC_WRITE:
|
|
517
524
|
// Every write transaction is atomic, so this is a no-op.
|
|
518
|
-
this.log?.('xFileControl', file.path, 'COMMIT_ATOMIC_WRITE')
|
|
519
|
-
return VFS.SQLITE_OK
|
|
525
|
+
this.log?.('xFileControl', file.path, 'COMMIT_ATOMIC_WRITE')
|
|
526
|
+
return VFS.SQLITE_OK
|
|
520
527
|
case VFS.SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE:
|
|
521
|
-
this.log?.('xFileControl', file.path, 'ROLLBACK_ATOMIC_WRITE')
|
|
522
|
-
file.metadata = file.rollback
|
|
523
|
-
const rollbackMetadata = Object.assign({}, file.metadata)
|
|
524
|
-
this.#idb.q(
|
|
525
|
-
metadata
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
528
|
+
this.log?.('xFileControl', file.path, 'ROLLBACK_ATOMIC_WRITE')
|
|
529
|
+
file.metadata = file.rollback
|
|
530
|
+
const rollbackMetadata = Object.assign({}, file.metadata)
|
|
531
|
+
this.#idb.q(
|
|
532
|
+
({ metadata, blocks }) => {
|
|
533
|
+
metadata.put(rollbackMetadata)
|
|
534
|
+
|
|
535
|
+
// Remove pages.
|
|
536
|
+
for (const offset of file.changedPages) {
|
|
537
|
+
blocks.delete([file.path, -offset, rollbackMetadata.version - 1])
|
|
538
|
+
}
|
|
539
|
+
file.changedPages.clear()
|
|
540
|
+
},
|
|
541
|
+
'rw',
|
|
542
|
+
file.txOptions,
|
|
543
|
+
)
|
|
544
|
+
file.needsMetadataSync = false
|
|
545
|
+
file.rollback = null
|
|
546
|
+
return VFS.SQLITE_OK
|
|
536
547
|
}
|
|
537
548
|
} catch (e) {
|
|
538
|
-
this.lastError = e
|
|
539
|
-
return VFS.SQLITE_IOERR
|
|
549
|
+
this.lastError = e
|
|
550
|
+
return VFS.SQLITE_IOERR
|
|
540
551
|
}
|
|
541
|
-
return super.jFileControl(fileId, op, pArg)
|
|
552
|
+
return super.jFileControl(fileId, op, pArg)
|
|
542
553
|
}
|
|
543
|
-
|
|
554
|
+
|
|
544
555
|
/**
|
|
545
556
|
* @param {number} pFile
|
|
546
557
|
* @returns {number|Promise<number>}
|
|
547
558
|
*/
|
|
548
559
|
jDeviceCharacteristics(pFile) {
|
|
549
|
-
return 0
|
|
550
|
-
| VFS.SQLITE_IOCAP_BATCH_ATOMIC
|
|
551
|
-
| VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
|
|
560
|
+
return 0 | VFS.SQLITE_IOCAP_BATCH_ATOMIC | VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
|
|
552
561
|
}
|
|
553
562
|
|
|
554
563
|
/**
|
|
555
|
-
* @param {Uint8Array} zBuf
|
|
564
|
+
* @param {Uint8Array} zBuf
|
|
556
565
|
* @returns {number|Promise<number>}
|
|
557
566
|
*/
|
|
558
567
|
jGetLastError(zBuf) {
|
|
559
568
|
if (this.lastError) {
|
|
560
|
-
console.error(this.lastError)
|
|
561
|
-
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1)
|
|
562
|
-
const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray)
|
|
563
|
-
zBuf[written] = 0
|
|
569
|
+
console.error(this.lastError)
|
|
570
|
+
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1)
|
|
571
|
+
const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray)
|
|
572
|
+
zBuf[written] = 0
|
|
564
573
|
}
|
|
565
574
|
return VFS.SQLITE_OK
|
|
566
575
|
}
|
|
567
576
|
}
|
|
568
577
|
|
|
569
578
|
function extractString(dataView, offset) {
|
|
570
|
-
const p = dataView.getUint32(offset, true)
|
|
579
|
+
const p = dataView.getUint32(offset, true)
|
|
571
580
|
if (p) {
|
|
572
|
-
const chars = new Uint8Array(dataView.buffer, p)
|
|
573
|
-
return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)))
|
|
581
|
+
const chars = new Uint8Array(dataView.buffer, p)
|
|
582
|
+
return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)))
|
|
574
583
|
}
|
|
575
|
-
return null
|
|
584
|
+
return null
|
|
576
585
|
}
|
|
577
586
|
|
|
578
587
|
export class IDBContext {
|
|
579
|
-
/** @type {IDBDatabase} */ #database
|
|
588
|
+
/** @type {IDBDatabase} */ #database
|
|
589
|
+
|
|
590
|
+
/** @type {Promise} */ #chain = null
|
|
591
|
+
/** @type {Promise<any>} */ #txComplete = Promise.resolve()
|
|
592
|
+
/** @type {IDBRequest?} */ #request = null
|
|
593
|
+
/** @type {WeakSet<IDBTransaction>} */ #txPending = new WeakSet()
|
|
580
594
|
|
|
581
|
-
|
|
582
|
-
/** @type {Promise<any>} */ #txComplete = Promise.resolve();
|
|
583
|
-
/** @type {IDBRequest?} */ #request = null;
|
|
584
|
-
/** @type {WeakSet<IDBTransaction>} */ #txPending = new WeakSet();
|
|
585
|
-
|
|
586
|
-
log = null;
|
|
595
|
+
log = null
|
|
587
596
|
|
|
588
597
|
static async create(name) {
|
|
589
598
|
const database = await new Promise((resolve, reject) => {
|
|
590
|
-
const request = indexedDB.open(name, 6)
|
|
591
|
-
request.onupgradeneeded = async event => {
|
|
592
|
-
const db = request.result
|
|
599
|
+
const request = indexedDB.open(name, 6)
|
|
600
|
+
request.onupgradeneeded = async (event) => {
|
|
601
|
+
const db = request.result
|
|
593
602
|
if (event.oldVersion) {
|
|
594
|
-
console.log(`Upgrading IndexedDB from version ${event.oldVersion}`)
|
|
603
|
+
console.log(`Upgrading IndexedDB from version ${event.oldVersion}`)
|
|
595
604
|
}
|
|
596
605
|
switch (event.oldVersion) {
|
|
597
606
|
case 0:
|
|
598
607
|
// Start with the original schema.
|
|
599
|
-
db.createObjectStore('blocks', { keyPath: ['path', 'offset', 'version']})
|
|
600
|
-
|
|
601
|
-
|
|
608
|
+
db.createObjectStore('blocks', { keyPath: ['path', 'offset', 'version'] }).createIndex('version', [
|
|
609
|
+
'path',
|
|
610
|
+
'version',
|
|
611
|
+
])
|
|
612
|
+
// fall through intentionally
|
|
602
613
|
case 5:
|
|
603
|
-
const tx = request.transaction
|
|
604
|
-
const blocks = tx.objectStore('blocks')
|
|
605
|
-
blocks.deleteIndex('version')
|
|
606
|
-
const metadata = db.createObjectStore('metadata', { keyPath: 'name' })
|
|
614
|
+
const tx = request.transaction
|
|
615
|
+
const blocks = tx.objectStore('blocks')
|
|
616
|
+
blocks.deleteIndex('version')
|
|
617
|
+
const metadata = db.createObjectStore('metadata', { keyPath: 'name' })
|
|
607
618
|
|
|
608
619
|
await new Promise((resolve, reject) => {
|
|
609
620
|
// Iterate over all the blocks.
|
|
610
|
-
let lastBlock = {}
|
|
611
|
-
const request = tx.objectStore('blocks').openCursor()
|
|
621
|
+
let lastBlock = {}
|
|
622
|
+
const request = tx.objectStore('blocks').openCursor()
|
|
612
623
|
request.onsuccess = () => {
|
|
613
|
-
const cursor = request.result
|
|
624
|
+
const cursor = request.result
|
|
614
625
|
if (cursor) {
|
|
615
|
-
const block = cursor.value
|
|
616
|
-
if (
|
|
617
|
-
|
|
626
|
+
const block = cursor.value
|
|
627
|
+
if (
|
|
628
|
+
typeof block.offset !== 'number' ||
|
|
629
|
+
(block.path === lastBlock.path && block.offset === lastBlock.offset)
|
|
630
|
+
) {
|
|
618
631
|
// Remove superceded block (or the "purge" info).
|
|
619
|
-
cursor.delete()
|
|
632
|
+
cursor.delete()
|
|
620
633
|
} else if (block.offset === 0) {
|
|
621
634
|
// Move metadata to its own store.
|
|
622
635
|
metadata.put({
|
|
623
636
|
name: block.path,
|
|
624
637
|
fileSize: block.fileSize,
|
|
625
|
-
version: block.version
|
|
626
|
-
})
|
|
638
|
+
version: block.version,
|
|
639
|
+
})
|
|
627
640
|
|
|
628
|
-
delete block.fileSize
|
|
629
|
-
cursor.update(block)
|
|
641
|
+
delete block.fileSize
|
|
642
|
+
cursor.update(block)
|
|
630
643
|
}
|
|
631
|
-
lastBlock = block
|
|
632
|
-
cursor.continue()
|
|
644
|
+
lastBlock = block
|
|
645
|
+
cursor.continue()
|
|
633
646
|
} else {
|
|
634
|
-
resolve()
|
|
647
|
+
resolve()
|
|
635
648
|
}
|
|
636
|
-
}
|
|
637
|
-
request.onerror = () => reject(request.error)
|
|
638
|
-
})
|
|
639
|
-
break
|
|
649
|
+
}
|
|
650
|
+
request.onerror = () => reject(request.error)
|
|
651
|
+
})
|
|
652
|
+
break
|
|
640
653
|
}
|
|
641
|
-
}
|
|
642
|
-
request.onsuccess = () => resolve(request.result)
|
|
643
|
-
request.onerror = () => reject(request.error)
|
|
644
|
-
})
|
|
645
|
-
return new IDBContext(database)
|
|
654
|
+
}
|
|
655
|
+
request.onsuccess = () => resolve(request.result)
|
|
656
|
+
request.onerror = () => reject(request.error)
|
|
657
|
+
})
|
|
658
|
+
return new IDBContext(database)
|
|
646
659
|
}
|
|
647
660
|
|
|
648
661
|
constructor(database) {
|
|
649
|
-
this.#database = database
|
|
662
|
+
this.#database = database
|
|
650
663
|
}
|
|
651
664
|
|
|
652
665
|
close() {
|
|
653
|
-
this.#database.close()
|
|
666
|
+
this.#database.close()
|
|
654
667
|
}
|
|
655
668
|
|
|
656
669
|
/**
|
|
657
|
-
* @param {(stores: Object.<string, IDBObjectStore>) => any} f
|
|
658
|
-
* @param {'ro'|'rw'} mode
|
|
670
|
+
* @param {(stores: Object.<string, IDBObjectStore>) => any} f
|
|
671
|
+
* @param {'ro'|'rw'} mode
|
|
659
672
|
* @returns {Promise<any>}
|
|
660
673
|
*/
|
|
661
674
|
q(f, mode = 'ro', options = {}) {
|
|
662
675
|
/** @type {IDBTransactionMode} */
|
|
663
|
-
const txMode = mode === 'ro' ? 'readonly' : 'readwrite'
|
|
664
|
-
const txOptions = Object.assign(
|
|
665
|
-
|
|
666
|
-
|
|
676
|
+
const txMode = mode === 'ro' ? 'readonly' : 'readwrite'
|
|
677
|
+
const txOptions = Object.assign(
|
|
678
|
+
{
|
|
679
|
+
/** @type {IDBTransactionDurability} */ durability: 'default',
|
|
680
|
+
},
|
|
681
|
+
options,
|
|
682
|
+
)
|
|
667
683
|
|
|
668
684
|
// Ensure that queries run sequentially. If any function rejects,
|
|
669
685
|
// or any request has an error, or the transaction does not commit,
|
|
670
686
|
// then no subsequent functions will run until sync() or reset().
|
|
671
|
-
this.#chain = (this.#chain || Promise.resolve())
|
|
672
|
-
|
|
673
|
-
return this.#chain;
|
|
687
|
+
this.#chain = (this.#chain || Promise.resolve()).then(() => this.#q(f, txMode, txOptions))
|
|
688
|
+
return this.#chain
|
|
674
689
|
}
|
|
675
690
|
|
|
676
691
|
/**
|
|
677
|
-
* @param {(stores: Object.<string, IDBObjectStore>) => any} f
|
|
678
|
-
* @param {IDBTransactionMode} mode
|
|
692
|
+
* @param {(stores: Object.<string, IDBObjectStore>) => any} f
|
|
693
|
+
* @param {IDBTransactionMode} mode
|
|
679
694
|
* @param {IDBTransactionOptions} options
|
|
680
695
|
* @returns {Promise<any>}
|
|
681
696
|
*/
|
|
682
697
|
async #q(f, mode, options) {
|
|
683
|
-
/** @type {IDBTransaction} */ let tx
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
698
|
+
/** @type {IDBTransaction} */ let tx
|
|
699
|
+
if (
|
|
700
|
+
this.#request &&
|
|
701
|
+
this.#txPending.has(this.#request.transaction) &&
|
|
702
|
+
this.#request.transaction.mode >= mode &&
|
|
703
|
+
this.#request.transaction.durability === options.durability
|
|
704
|
+
) {
|
|
688
705
|
// The previous request transaction is compatible and has
|
|
689
706
|
// not yet completed.
|
|
690
|
-
tx = this.#request.transaction
|
|
707
|
+
tx = this.#request.transaction
|
|
691
708
|
|
|
692
709
|
// If the previous request is pending, wait for it to complete.
|
|
693
710
|
// This ensures that the transaction will be active.
|
|
694
711
|
if (this.#request.readyState === 'pending') {
|
|
695
|
-
await new Promise(resolve => {
|
|
696
|
-
this.#request.addEventListener('success', resolve, { once: true })
|
|
697
|
-
this.#request.addEventListener('error', resolve, { once: true })
|
|
698
|
-
})
|
|
712
|
+
await new Promise((resolve) => {
|
|
713
|
+
this.#request.addEventListener('success', resolve, { once: true })
|
|
714
|
+
this.#request.addEventListener('error', resolve, { once: true })
|
|
715
|
+
})
|
|
699
716
|
}
|
|
700
717
|
}
|
|
701
718
|
|
|
@@ -704,45 +721,45 @@ export class IDBContext {
|
|
|
704
721
|
// The current transaction is missing or doesn't match so
|
|
705
722
|
// replace it with a new one. wait for the previous
|
|
706
723
|
// transaction to complete so the lifetimes do not overlap.
|
|
707
|
-
await this.#txComplete
|
|
724
|
+
await this.#txComplete
|
|
708
725
|
|
|
709
726
|
// Create the new transaction.
|
|
710
727
|
// @ts-ignore
|
|
711
|
-
tx = this.#database.transaction(this.#database.objectStoreNames, mode, options)
|
|
712
|
-
this.log?.('IDBTransaction open', mode)
|
|
713
|
-
this.#txPending.add(tx)
|
|
728
|
+
tx = this.#database.transaction(this.#database.objectStoreNames, mode, options)
|
|
729
|
+
this.log?.('IDBTransaction open', mode)
|
|
730
|
+
this.#txPending.add(tx)
|
|
714
731
|
this.#txComplete = new Promise((resolve, reject) => {
|
|
715
732
|
tx.addEventListener('complete', () => {
|
|
716
|
-
this.log?.('IDBTransaction complete')
|
|
717
|
-
this.#txPending.delete(tx)
|
|
718
|
-
resolve()
|
|
719
|
-
})
|
|
733
|
+
this.log?.('IDBTransaction complete')
|
|
734
|
+
this.#txPending.delete(tx)
|
|
735
|
+
resolve()
|
|
736
|
+
})
|
|
720
737
|
tx.addEventListener('abort', () => {
|
|
721
|
-
this.#txPending.delete(tx)
|
|
722
|
-
reject(new Error('transaction aborted'))
|
|
723
|
-
})
|
|
724
|
-
})
|
|
738
|
+
this.#txPending.delete(tx)
|
|
739
|
+
reject(new Error('transaction aborted'))
|
|
740
|
+
})
|
|
741
|
+
})
|
|
725
742
|
}
|
|
726
743
|
|
|
727
744
|
try {
|
|
728
745
|
// @ts-ignore
|
|
729
746
|
// Create object store proxies.
|
|
730
|
-
const objectStores = [...tx.objectStoreNames].map(name => {
|
|
731
|
-
return [name, this.proxyStoreOrIndex(tx.objectStore(name))]
|
|
732
|
-
})
|
|
747
|
+
const objectStores = [...tx.objectStoreNames].map((name) => {
|
|
748
|
+
return [name, this.proxyStoreOrIndex(tx.objectStore(name))]
|
|
749
|
+
})
|
|
733
750
|
|
|
734
751
|
// Execute the function.
|
|
735
|
-
return await f(Object.fromEntries(objectStores))
|
|
752
|
+
return await f(Object.fromEntries(objectStores))
|
|
736
753
|
} catch (e) {
|
|
737
754
|
// Use a new transaction if this one was inactive. This will
|
|
738
755
|
// happen if the last request in the transaction completed
|
|
739
756
|
// in a previous task but the transaction has not yet committed.
|
|
740
757
|
if (!i && RETRYABLE_ERRORS.has(e.name)) {
|
|
741
|
-
this.log?.(`${e.name}, retrying`)
|
|
742
|
-
tx = null
|
|
743
|
-
continue
|
|
758
|
+
this.log?.(`${e.name}, retrying`)
|
|
759
|
+
tx = null
|
|
760
|
+
continue
|
|
744
761
|
}
|
|
745
|
-
throw e
|
|
762
|
+
throw e
|
|
746
763
|
}
|
|
747
764
|
}
|
|
748
765
|
}
|
|
@@ -751,16 +768,16 @@ export class IDBContext {
|
|
|
751
768
|
* Object store methods that return an IDBRequest, except for cursor
|
|
752
769
|
* creation, are wrapped to return a Promise. In addition, the
|
|
753
770
|
* request is used internally for chaining.
|
|
754
|
-
* @param {IDBObjectStore} objectStore
|
|
755
|
-
* @returns
|
|
771
|
+
* @param {IDBObjectStore} objectStore
|
|
772
|
+
* @returns
|
|
756
773
|
*/
|
|
757
774
|
proxyStoreOrIndex(objectStore) {
|
|
758
775
|
return new Proxy(objectStore, {
|
|
759
776
|
get: (target, property, receiver) => {
|
|
760
|
-
const result = Reflect.get(target, property, receiver)
|
|
777
|
+
const result = Reflect.get(target, property, receiver)
|
|
761
778
|
if (typeof result === 'function') {
|
|
762
779
|
return (...args) => {
|
|
763
|
-
const maybeRequest = Reflect.apply(result, target, args)
|
|
780
|
+
const maybeRequest = Reflect.apply(result, target, args)
|
|
764
781
|
// @ts-ignore
|
|
765
782
|
if (maybeRequest instanceof IDBRequest && !property.endsWith('Cursor')) {
|
|
766
783
|
// // Debug logging.
|
|
@@ -771,57 +788,60 @@ export class IDBContext {
|
|
|
771
788
|
// maybeRequest.addEventListener('error', () => {
|
|
772
789
|
// this.log?.(`${target.name}.${String(property)} error`, maybeRequest.error);
|
|
773
790
|
// });
|
|
774
|
-
|
|
791
|
+
|
|
775
792
|
// Save the request.
|
|
776
|
-
this.#request = maybeRequest
|
|
793
|
+
this.#request = maybeRequest
|
|
777
794
|
|
|
778
795
|
// Abort the transaction on error.
|
|
779
|
-
maybeRequest.addEventListener(
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
796
|
+
maybeRequest.addEventListener(
|
|
797
|
+
'error',
|
|
798
|
+
() => {
|
|
799
|
+
console.error(maybeRequest.error)
|
|
800
|
+
maybeRequest.transaction.abort()
|
|
801
|
+
},
|
|
802
|
+
{ once: true },
|
|
803
|
+
)
|
|
783
804
|
|
|
784
805
|
// Return a Promise.
|
|
785
|
-
return wrap(maybeRequest)
|
|
806
|
+
return wrap(maybeRequest)
|
|
786
807
|
}
|
|
787
|
-
return maybeRequest
|
|
808
|
+
return maybeRequest
|
|
788
809
|
}
|
|
789
810
|
}
|
|
790
|
-
return result
|
|
791
|
-
}
|
|
792
|
-
})
|
|
811
|
+
return result
|
|
812
|
+
},
|
|
813
|
+
})
|
|
793
814
|
}
|
|
794
815
|
|
|
795
816
|
/**
|
|
796
|
-
* @param {boolean} durable
|
|
817
|
+
* @param {boolean} durable
|
|
797
818
|
*/
|
|
798
819
|
async sync(durable) {
|
|
799
820
|
if (this.#chain) {
|
|
800
821
|
// This waits for all IndexedDB calls to be made.
|
|
801
|
-
await this.#chain
|
|
822
|
+
await this.#chain
|
|
802
823
|
if (durable) {
|
|
803
824
|
// This waits for the final transaction to commit.
|
|
804
|
-
await this.#txComplete
|
|
825
|
+
await this.#txComplete
|
|
805
826
|
}
|
|
806
|
-
this.reset()
|
|
827
|
+
this.reset()
|
|
807
828
|
}
|
|
808
829
|
}
|
|
809
830
|
|
|
810
831
|
reset() {
|
|
811
|
-
this.#chain = null
|
|
812
|
-
this.#txComplete = Promise.resolve()
|
|
813
|
-
this.#request = null
|
|
832
|
+
this.#chain = null
|
|
833
|
+
this.#txComplete = Promise.resolve()
|
|
834
|
+
this.#request = null
|
|
814
835
|
}
|
|
815
836
|
}
|
|
816
837
|
|
|
817
838
|
/**
|
|
818
|
-
* @param {IDBRequest} request
|
|
839
|
+
* @param {IDBRequest} request
|
|
819
840
|
* @returns {Promise}
|
|
820
841
|
*/
|
|
821
842
|
function wrap(request) {
|
|
822
843
|
return new Promise((resolve, reject) => {
|
|
823
|
-
request.onsuccess = () => resolve(request.result)
|
|
824
|
-
request.onerror = () => reject(request.error)
|
|
825
|
-
})
|
|
844
|
+
request.onsuccess = () => resolve(request.result)
|
|
845
|
+
request.onerror = () => reject(request.error)
|
|
846
|
+
})
|
|
826
847
|
}
|
|
827
|
-
|