@livestore/wa-sqlite 0.4.0-dev.22 → 0.4.0-dev.23
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,13 +1,13 @@
|
|
|
1
|
-
import { FacadeVFS } from '../FacadeVFS.js'
|
|
2
|
-
import * as VFS from '../VFS.js'
|
|
1
|
+
import { FacadeVFS } from '../FacadeVFS.js'
|
|
2
|
+
import * as VFS from '../VFS.js'
|
|
3
3
|
|
|
4
4
|
// Options for navigator.locks.request().
|
|
5
|
-
/** @type {LockOptions} */ const SHARED = { mode: 'shared' }
|
|
6
|
-
/** @type {LockOptions} */ const POLL_SHARED = { ifAvailable: true, mode: 'shared' }
|
|
7
|
-
/** @type {LockOptions} */ const POLL_EXCLUSIVE = { ifAvailable: true, mode: 'exclusive' }
|
|
5
|
+
/** @type {LockOptions} */ const SHARED = { mode: 'shared' }
|
|
6
|
+
/** @type {LockOptions} */ const POLL_SHARED = { ifAvailable: true, mode: 'shared' }
|
|
7
|
+
/** @type {LockOptions} */ const POLL_EXCLUSIVE = { ifAvailable: true, mode: 'exclusive' }
|
|
8
8
|
|
|
9
9
|
// Used only for debug logging.
|
|
10
|
-
const contextId = Math.random().toString(36).slice(2)
|
|
10
|
+
const contextId = Math.random().toString(36).slice(2)
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @typedef {Object} Transaction
|
|
@@ -18,434 +18,428 @@ const contextId = Math.random().toString(36).slice(2);
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
class File {
|
|
21
|
-
/** @type {string} */ path
|
|
22
|
-
/** @type {number} */ flags
|
|
21
|
+
/** @type {string} */ path
|
|
22
|
+
/** @type {number} */ flags
|
|
23
23
|
|
|
24
|
-
/** @type {number} */ blockSize
|
|
25
|
-
/** @type {Map<number, Uint8Array>} */ blocks
|
|
24
|
+
/** @type {number} */ blockSize
|
|
25
|
+
/** @type {Map<number, Uint8Array>} */ blocks
|
|
26
26
|
|
|
27
27
|
// Members below are only used for SQLITE_OPEN_MAIN_DB.
|
|
28
28
|
|
|
29
|
-
/** @type {Transaction} */ viewTx
|
|
30
|
-
/** @type {function?} */ viewReleaser
|
|
29
|
+
/** @type {Transaction} */ viewTx // last transaction incorporated
|
|
30
|
+
/** @type {function?} */ viewReleaser
|
|
31
31
|
|
|
32
|
-
/** @type {BroadcastChannel} */ broadcastChannel
|
|
33
|
-
/** @type {Transaction[]} */ broadcastReceived
|
|
32
|
+
/** @type {BroadcastChannel} */ broadcastChannel
|
|
33
|
+
/** @type {Transaction[]} */ broadcastReceived
|
|
34
34
|
|
|
35
|
-
/** @type {number} */ lockState
|
|
36
|
-
/** @type {{write?: function, reserved?: function, hint?: function}} */ locks
|
|
35
|
+
/** @type {number} */ lockState
|
|
36
|
+
/** @type {{write?: function, reserved?: function, hint?: function}} */ locks
|
|
37
37
|
|
|
38
|
-
/** @type {AbortController} */ abortController
|
|
38
|
+
/** @type {AbortController} */ abortController
|
|
39
39
|
|
|
40
|
-
/** @type {Transaction?} */ txActive
|
|
41
|
-
/** @type {boolean} */ txWriteHint
|
|
42
|
-
/** @type {boolean} */ txOverwrite
|
|
40
|
+
/** @type {Transaction?} */ txActive
|
|
41
|
+
/** @type {boolean} */ txWriteHint
|
|
42
|
+
/** @type {boolean} */ txOverwrite
|
|
43
43
|
|
|
44
|
-
/** @type {string} */ synchronous
|
|
44
|
+
/** @type {string} */ synchronous
|
|
45
45
|
|
|
46
46
|
constructor(pathname, flags) {
|
|
47
|
-
this.path = pathname
|
|
48
|
-
this.flags = flags
|
|
47
|
+
this.path = pathname
|
|
48
|
+
this.flags = flags
|
|
49
49
|
|
|
50
|
-
this.blockSize = 0
|
|
51
|
-
this.blocks = new Map()
|
|
50
|
+
this.blockSize = 0
|
|
51
|
+
this.blocks = new Map()
|
|
52
52
|
if (flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
53
|
-
this.viewTx = null
|
|
54
|
-
this.viewReleaser = null
|
|
55
|
-
this.broadcastChannel = new BroadcastChannel('mirror:' + pathname)
|
|
56
|
-
this.broadcastReceived = []
|
|
57
|
-
this.lockState = VFS.SQLITE_LOCK_NONE
|
|
58
|
-
this.locks = {}
|
|
59
|
-
this.txActive = null
|
|
60
|
-
this.txWriteHint = false
|
|
61
|
-
this.txOverwrite = false
|
|
62
|
-
this.synchronous = 'full'
|
|
53
|
+
this.viewTx = null
|
|
54
|
+
this.viewReleaser = null
|
|
55
|
+
this.broadcastChannel = new BroadcastChannel('mirror:' + pathname)
|
|
56
|
+
this.broadcastReceived = []
|
|
57
|
+
this.lockState = VFS.SQLITE_LOCK_NONE
|
|
58
|
+
this.locks = {}
|
|
59
|
+
this.txActive = null
|
|
60
|
+
this.txWriteHint = false
|
|
61
|
+
this.txOverwrite = false
|
|
62
|
+
this.synchronous = 'full'
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export class IDBMirrorVFS extends FacadeVFS {
|
|
68
|
-
/** @type {Map<number, File>} */ #mapIdToFile = new Map()
|
|
69
|
-
/** @type {Map<string, File>} */ #mapPathToFile = new Map()
|
|
70
|
-
#lastError = null
|
|
68
|
+
/** @type {Map<number, File>} */ #mapIdToFile = new Map()
|
|
69
|
+
/** @type {Map<string, File>} */ #mapPathToFile = new Map()
|
|
70
|
+
#lastError = null
|
|
71
71
|
|
|
72
|
-
/** @type {IDBDatabase} */ #idb
|
|
72
|
+
/** @type {IDBDatabase} */ #idb
|
|
73
73
|
|
|
74
|
-
log = null
|
|
74
|
+
log = null // console.log;
|
|
75
75
|
|
|
76
|
-
/** @type {Promise} */ #isReady
|
|
76
|
+
/** @type {Promise} */ #isReady
|
|
77
77
|
|
|
78
78
|
static async create(name, module, options) {
|
|
79
|
-
const instance = new IDBMirrorVFS(name, module, options)
|
|
80
|
-
await instance.isReady()
|
|
81
|
-
return instance
|
|
79
|
+
const instance = new IDBMirrorVFS(name, module, options)
|
|
80
|
+
await instance.isReady()
|
|
81
|
+
return instance
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
constructor(name, module, options = {}) {
|
|
85
|
-
super(name, module)
|
|
86
|
-
this.#isReady = this.#initialize(name)
|
|
85
|
+
super(name, module)
|
|
86
|
+
this.#isReady = this.#initialize(name)
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
async #initialize(name) {
|
|
90
90
|
// Open IndexedDB database, creating it if necessary.
|
|
91
91
|
this.#idb = await new Promise((resolve, reject) => {
|
|
92
|
-
const request = indexedDB.open(name, 1)
|
|
92
|
+
const request = indexedDB.open(name, 1)
|
|
93
93
|
request.onupgradeneeded = (event) => {
|
|
94
|
-
const db = request.result
|
|
94
|
+
const db = request.result
|
|
95
95
|
switch (event.oldVersion) {
|
|
96
96
|
case 0:
|
|
97
|
-
db.createObjectStore('blocks', { keyPath: ['path', 'offset'] })
|
|
98
|
-
db.createObjectStore('tx', { keyPath: ['path', 'txId'] })
|
|
99
|
-
break
|
|
97
|
+
db.createObjectStore('blocks', { keyPath: ['path', 'offset'] })
|
|
98
|
+
db.createObjectStore('tx', { keyPath: ['path', 'txId'] })
|
|
99
|
+
break
|
|
100
100
|
}
|
|
101
|
-
}
|
|
102
|
-
request.onsuccess = () => resolve(request.result)
|
|
103
|
-
request.onerror = () => reject(request.error)
|
|
104
|
-
})
|
|
101
|
+
}
|
|
102
|
+
request.onsuccess = () => resolve(request.result)
|
|
103
|
+
request.onerror = () => reject(request.error)
|
|
104
|
+
})
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
close() {
|
|
108
|
-
return this.#idb.close()
|
|
108
|
+
return this.#idb.close()
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
async isReady() {
|
|
112
|
-
await super.isReady()
|
|
113
|
-
return this.#isReady
|
|
112
|
+
await super.isReady()
|
|
113
|
+
return this.#isReady
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* @param {string?} zName
|
|
118
|
-
* @param {number} fileId
|
|
119
|
-
* @param {number} flags
|
|
120
|
-
* @param {DataView} pOutFlags
|
|
117
|
+
* @param {string?} zName
|
|
118
|
+
* @param {number} fileId
|
|
119
|
+
* @param {number} flags
|
|
120
|
+
* @param {DataView} pOutFlags
|
|
121
121
|
* @returns {Promise<number>}
|
|
122
122
|
*/
|
|
123
123
|
async jOpen(zName, fileId, flags, pOutFlags) {
|
|
124
124
|
try {
|
|
125
|
-
const url = new URL(zName || Math.random().toString(36).slice(2), 'file://')
|
|
126
|
-
const path = url.pathname
|
|
125
|
+
const url = new URL(zName || Math.random().toString(36).slice(2), 'file://')
|
|
126
|
+
const path = url.pathname
|
|
127
127
|
|
|
128
|
-
let file
|
|
128
|
+
let file
|
|
129
129
|
if (flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
130
130
|
// TODO
|
|
131
|
-
file = new File(path, flags)
|
|
131
|
+
file = new File(path, flags)
|
|
132
132
|
|
|
133
|
-
const idbTx = this.#idb.transaction(['blocks', 'tx'], 'readwrite')
|
|
134
|
-
const blocks = idbTx.objectStore('blocks')
|
|
135
|
-
if (await idbX(blocks.count([path, 0])) === 0) {
|
|
133
|
+
const idbTx = this.#idb.transaction(['blocks', 'tx'], 'readwrite')
|
|
134
|
+
const blocks = idbTx.objectStore('blocks')
|
|
135
|
+
if ((await idbX(blocks.count([path, 0]))) === 0) {
|
|
136
136
|
// File does not yet exist.
|
|
137
137
|
if (flags & VFS.SQLITE_OPEN_CREATE) {
|
|
138
|
-
await idbX(blocks.put({ path, offset: 0, data: new Uint8Array(0) }))
|
|
138
|
+
await idbX(blocks.put({ path, offset: 0, data: new Uint8Array(0) }))
|
|
139
139
|
} else {
|
|
140
|
-
throw new Error('File not found')
|
|
140
|
+
throw new Error('File not found')
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
// Load pages into memory from IndexedDB.
|
|
145
145
|
await new Promise((resolve, reject) => {
|
|
146
|
-
const range = IDBKeyRange.bound([path, 0], [path, Infinity])
|
|
147
|
-
const request = blocks.openCursor(range)
|
|
146
|
+
const range = IDBKeyRange.bound([path, 0], [path, Infinity])
|
|
147
|
+
const request = blocks.openCursor(range)
|
|
148
148
|
request.onsuccess = () => {
|
|
149
|
-
const cursor = request.result
|
|
149
|
+
const cursor = request.result
|
|
150
150
|
if (cursor) {
|
|
151
|
-
const { offset, data } = cursor.value
|
|
152
|
-
file.blocks.set(offset, data)
|
|
153
|
-
cursor.continue()
|
|
151
|
+
const { offset, data } = cursor.value
|
|
152
|
+
file.blocks.set(offset, data)
|
|
153
|
+
cursor.continue()
|
|
154
154
|
} else {
|
|
155
|
-
resolve()
|
|
155
|
+
resolve()
|
|
156
156
|
}
|
|
157
|
-
}
|
|
158
|
-
request.onerror = () => reject(request.error)
|
|
159
|
-
})
|
|
160
|
-
file.blockSize = file.blocks.get(0)?.byteLength ?? 0
|
|
157
|
+
}
|
|
158
|
+
request.onerror = () => reject(request.error)
|
|
159
|
+
})
|
|
160
|
+
file.blockSize = file.blocks.get(0)?.byteLength ?? 0
|
|
161
161
|
|
|
162
162
|
// Get the last transaction id.
|
|
163
|
-
const transactions = idbTx.objectStore('tx')
|
|
163
|
+
const transactions = idbTx.objectStore('tx')
|
|
164
164
|
file.viewTx = await new Promise((resolve, reject) => {
|
|
165
|
-
const range = IDBKeyRange.bound([path, 0], [path, Infinity])
|
|
166
|
-
const request = transactions.openCursor(range, 'prev')
|
|
165
|
+
const range = IDBKeyRange.bound([path, 0], [path, Infinity])
|
|
166
|
+
const request = transactions.openCursor(range, 'prev')
|
|
167
167
|
request.onsuccess = () => {
|
|
168
|
-
const cursor = request.result
|
|
168
|
+
const cursor = request.result
|
|
169
169
|
if (cursor) {
|
|
170
|
-
resolve(cursor.value)
|
|
170
|
+
resolve(cursor.value)
|
|
171
171
|
} else {
|
|
172
|
-
resolve({ txId: 0 })
|
|
172
|
+
resolve({ txId: 0 })
|
|
173
173
|
}
|
|
174
|
-
}
|
|
175
|
-
request.onerror = () => reject(request.error)
|
|
176
|
-
})
|
|
174
|
+
}
|
|
175
|
+
request.onerror = () => reject(request.error)
|
|
176
|
+
})
|
|
177
177
|
|
|
178
178
|
// Publish our view of the database. This prevents other connections
|
|
179
179
|
// from overwriting file data we still need.
|
|
180
|
-
await this.#setView(file, file.viewTx)
|
|
180
|
+
await this.#setView(file, file.viewTx)
|
|
181
181
|
|
|
182
182
|
// Listen for broadcasts. Messages are cached until the database
|
|
183
183
|
// is unlocked.
|
|
184
|
-
file.broadcastChannel.addEventListener('message', event => {
|
|
185
|
-
file.broadcastReceived.push(event.data)
|
|
184
|
+
file.broadcastChannel.addEventListener('message', (event) => {
|
|
185
|
+
file.broadcastReceived.push(event.data)
|
|
186
186
|
if (file.lockState === VFS.SQLITE_LOCK_NONE) {
|
|
187
|
-
this.#processBroadcasts(file)
|
|
187
|
+
this.#processBroadcasts(file)
|
|
188
188
|
}
|
|
189
|
-
})
|
|
189
|
+
})
|
|
190
190
|
} else {
|
|
191
191
|
// Not a main database so not stored in IndexedDB.
|
|
192
|
-
file = this.#mapPathToFile.get(path)
|
|
192
|
+
file = this.#mapPathToFile.get(path)
|
|
193
193
|
if (!file) {
|
|
194
194
|
if (flags & VFS.SQLITE_OPEN_CREATE) {
|
|
195
|
-
file = new File(path, flags)
|
|
196
|
-
file.blocks.set(0, new Uint8Array(0))
|
|
195
|
+
file = new File(path, flags)
|
|
196
|
+
file.blocks.set(0, new Uint8Array(0))
|
|
197
197
|
} else {
|
|
198
|
-
throw new Error('File not found')
|
|
198
|
+
throw new Error('File not found')
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
pOutFlags.setInt32(0, flags, true)
|
|
204
|
-
this.#mapIdToFile.set(fileId, file)
|
|
205
|
-
this.#mapPathToFile.set(path, file)
|
|
206
|
-
return VFS.SQLITE_OK
|
|
203
|
+
pOutFlags.setInt32(0, flags, true)
|
|
204
|
+
this.#mapIdToFile.set(fileId, file)
|
|
205
|
+
this.#mapPathToFile.set(path, file)
|
|
206
|
+
return VFS.SQLITE_OK
|
|
207
207
|
} catch (e) {
|
|
208
|
-
this.#lastError = e
|
|
209
|
-
return VFS.SQLITE_CANTOPEN
|
|
208
|
+
this.#lastError = e
|
|
209
|
+
return VFS.SQLITE_CANTOPEN
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
|
-
* @param {string} zName
|
|
215
|
-
* @param {number} syncDir
|
|
214
|
+
* @param {string} zName
|
|
215
|
+
* @param {number} syncDir
|
|
216
216
|
* @returns {Promise<number>}
|
|
217
217
|
*/
|
|
218
218
|
async jDelete(zName, syncDir) {
|
|
219
219
|
try {
|
|
220
|
-
const url = new URL(zName, 'file://')
|
|
221
|
-
const pathname = url.pathname
|
|
220
|
+
const url = new URL(zName, 'file://')
|
|
221
|
+
const pathname = url.pathname
|
|
222
222
|
|
|
223
|
-
const result = await this.#deleteFile(pathname)
|
|
223
|
+
const result = await this.#deleteFile(pathname)
|
|
224
224
|
if (syncDir) {
|
|
225
|
-
await result
|
|
225
|
+
await result
|
|
226
226
|
}
|
|
227
|
-
return VFS.SQLITE_OK
|
|
227
|
+
return VFS.SQLITE_OK
|
|
228
228
|
} catch (e) {
|
|
229
|
-
return VFS.SQLITE_IOERR_DELETE
|
|
229
|
+
return VFS.SQLITE_IOERR_DELETE
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
/**
|
|
234
|
-
* @param {string} zName
|
|
235
|
-
* @param {number} flags
|
|
236
|
-
* @param {DataView} pResOut
|
|
234
|
+
* @param {string} zName
|
|
235
|
+
* @param {number} flags
|
|
236
|
+
* @param {DataView} pResOut
|
|
237
237
|
* @returns {Promise<number>}
|
|
238
238
|
*/
|
|
239
239
|
async jAccess(zName, flags, pResOut) {
|
|
240
240
|
try {
|
|
241
|
-
const url = new URL(zName, 'file://')
|
|
242
|
-
const pathname = url.pathname
|
|
241
|
+
const url = new URL(zName, 'file://')
|
|
242
|
+
const pathname = url.pathname
|
|
243
243
|
|
|
244
244
|
// This test ignores main database files that have not been opened
|
|
245
245
|
// with this connection. SQLite does not call jAccess() on main
|
|
246
246
|
// database files, so avoiding an IndexedDB test saves time.
|
|
247
|
-
const exists = this.#mapPathToFile.has(pathname)
|
|
248
|
-
pResOut.setInt32(0, exists ? 1 : 0, true)
|
|
249
|
-
return VFS.SQLITE_OK
|
|
247
|
+
const exists = this.#mapPathToFile.has(pathname)
|
|
248
|
+
pResOut.setInt32(0, exists ? 1 : 0, true)
|
|
249
|
+
return VFS.SQLITE_OK
|
|
250
250
|
} catch (e) {
|
|
251
|
-
this.#lastError = e
|
|
252
|
-
return VFS.SQLITE_IOERR_ACCESS
|
|
251
|
+
this.#lastError = e
|
|
252
|
+
return VFS.SQLITE_IOERR_ACCESS
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
|
-
* @param {number} fileId
|
|
257
|
+
* @param {number} fileId
|
|
258
258
|
* @returns {Promise<number>}
|
|
259
259
|
*/
|
|
260
260
|
async jClose(fileId) {
|
|
261
261
|
try {
|
|
262
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
263
|
-
this.#mapIdToFile.delete(fileId)
|
|
262
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
263
|
+
this.#mapIdToFile.delete(fileId)
|
|
264
264
|
|
|
265
265
|
if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
266
|
-
file.broadcastChannel.close()
|
|
267
|
-
file.viewReleaser?.()
|
|
266
|
+
file.broadcastChannel.close()
|
|
267
|
+
file.viewReleaser?.()
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
|
271
|
-
this.#deleteFile(file.path)
|
|
271
|
+
this.#deleteFile(file.path)
|
|
272
272
|
}
|
|
273
|
-
return VFS.SQLITE_OK
|
|
273
|
+
return VFS.SQLITE_OK
|
|
274
274
|
} catch (e) {
|
|
275
|
-
return VFS.SQLITE_IOERR_CLOSE
|
|
275
|
+
return VFS.SQLITE_IOERR_CLOSE
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
/**
|
|
280
|
-
* @param {number} fileId
|
|
281
|
-
* @param {Uint8Array} pData
|
|
280
|
+
* @param {number} fileId
|
|
281
|
+
* @param {Uint8Array} pData
|
|
282
282
|
* @param {number} iOffset
|
|
283
283
|
* @returns {number}
|
|
284
284
|
*/
|
|
285
285
|
jRead(fileId, pData, iOffset) {
|
|
286
286
|
try {
|
|
287
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
287
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
288
288
|
|
|
289
|
-
let bytesRead = 0
|
|
290
|
-
let pDataOffset = 0
|
|
289
|
+
let bytesRead = 0
|
|
290
|
+
let pDataOffset = 0
|
|
291
291
|
while (pDataOffset < pData.byteLength) {
|
|
292
292
|
// File data is stored in fixed-size blocks. Get the next block
|
|
293
293
|
// needed.
|
|
294
|
-
const fileOffset = iOffset + pDataOffset
|
|
295
|
-
const blockIndex = Math.floor(fileOffset / file.blockSize)
|
|
296
|
-
const blockOffset = fileOffset % file.blockSize
|
|
294
|
+
const fileOffset = iOffset + pDataOffset
|
|
295
|
+
const blockIndex = Math.floor(fileOffset / file.blockSize)
|
|
296
|
+
const blockOffset = fileOffset % file.blockSize
|
|
297
297
|
const block =
|
|
298
|
-
file.txActive?.blocks.get(blockIndex * file.blockSize) ??
|
|
299
|
-
file.blocks.get(blockIndex * file.blockSize);
|
|
298
|
+
file.txActive?.blocks.get(blockIndex * file.blockSize) ?? file.blocks.get(blockIndex * file.blockSize)
|
|
300
299
|
if (!block) {
|
|
301
|
-
break
|
|
300
|
+
break
|
|
302
301
|
}
|
|
303
302
|
|
|
304
303
|
// Copy block data to the read buffer.
|
|
305
|
-
const blockLength = Math.min(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
pDataOffset += blockLength;
|
|
310
|
-
bytesRead += blockLength;
|
|
304
|
+
const blockLength = Math.min(block.byteLength - blockOffset, pData.byteLength - pDataOffset)
|
|
305
|
+
pData.set(block.subarray(blockOffset, blockOffset + blockLength), pDataOffset)
|
|
306
|
+
pDataOffset += blockLength
|
|
307
|
+
bytesRead += blockLength
|
|
311
308
|
}
|
|
312
309
|
|
|
313
310
|
if (bytesRead < pData.byteLength) {
|
|
314
|
-
pData.fill(0, bytesRead)
|
|
315
|
-
return VFS.SQLITE_IOERR_SHORT_READ
|
|
311
|
+
pData.fill(0, bytesRead)
|
|
312
|
+
return VFS.SQLITE_IOERR_SHORT_READ
|
|
316
313
|
}
|
|
317
|
-
return VFS.SQLITE_OK
|
|
314
|
+
return VFS.SQLITE_OK
|
|
318
315
|
} catch (e) {
|
|
319
|
-
this.#lastError = e
|
|
320
|
-
return VFS.SQLITE_IOERR_READ
|
|
316
|
+
this.#lastError = e
|
|
317
|
+
return VFS.SQLITE_IOERR_READ
|
|
321
318
|
}
|
|
322
319
|
}
|
|
323
320
|
|
|
324
321
|
/**
|
|
325
|
-
* @param {number} fileId
|
|
326
|
-
* @param {Uint8Array} pData
|
|
322
|
+
* @param {number} fileId
|
|
323
|
+
* @param {Uint8Array} pData
|
|
327
324
|
* @param {number} iOffset
|
|
328
325
|
* @returns {number}
|
|
329
326
|
*/
|
|
330
327
|
jWrite(fileId, pData, iOffset) {
|
|
331
328
|
try {
|
|
332
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
329
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
333
330
|
|
|
334
331
|
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
335
|
-
this.#requireTxActive(file)
|
|
332
|
+
this.#requireTxActive(file)
|
|
336
333
|
// SQLite is not necessarily written sequentially, so fill in the
|
|
337
334
|
// unwritten blocks here.
|
|
338
|
-
for (let fillOffset = file.txActive.fileSize;
|
|
339
|
-
|
|
340
|
-
file.txActive.blocks.set(fillOffset, new Uint8Array(pData.byteLength));
|
|
335
|
+
for (let fillOffset = file.txActive.fileSize; fillOffset < iOffset; fillOffset += pData.byteLength) {
|
|
336
|
+
file.txActive.blocks.set(fillOffset, new Uint8Array(pData.byteLength))
|
|
341
337
|
}
|
|
342
|
-
file.txActive.blocks.set(iOffset, pData.slice())
|
|
343
|
-
file.txActive.fileSize = Math.max(file.txActive.fileSize, iOffset + pData.byteLength)
|
|
344
|
-
file.blockSize = pData.byteLength
|
|
338
|
+
file.txActive.blocks.set(iOffset, pData.slice())
|
|
339
|
+
file.txActive.fileSize = Math.max(file.txActive.fileSize, iOffset + pData.byteLength)
|
|
340
|
+
file.blockSize = pData.byteLength
|
|
345
341
|
} else {
|
|
346
342
|
// All files that are not main databases are stored in a single
|
|
347
343
|
// block.
|
|
348
|
-
let block = file.blocks.get(0)
|
|
344
|
+
let block = file.blocks.get(0)
|
|
349
345
|
if (iOffset + pData.byteLength > block.byteLength) {
|
|
350
346
|
// Resize the block buffer.
|
|
351
|
-
const newSize = Math.max(iOffset + pData.byteLength, 2 * block.byteLength)
|
|
352
|
-
const newBlock = new Uint8Array(newSize)
|
|
353
|
-
newBlock.set(block)
|
|
354
|
-
file.blocks.set(0, newBlock)
|
|
355
|
-
block = newBlock
|
|
347
|
+
const newSize = Math.max(iOffset + pData.byteLength, 2 * block.byteLength)
|
|
348
|
+
const newBlock = new Uint8Array(newSize)
|
|
349
|
+
newBlock.set(block)
|
|
350
|
+
file.blocks.set(0, newBlock)
|
|
351
|
+
block = newBlock
|
|
356
352
|
}
|
|
357
|
-
block.set(pData, iOffset)
|
|
358
|
-
file.blockSize = Math.max(file.blockSize, iOffset + pData.byteLength)
|
|
353
|
+
block.set(pData, iOffset)
|
|
354
|
+
file.blockSize = Math.max(file.blockSize, iOffset + pData.byteLength)
|
|
359
355
|
}
|
|
360
|
-
return VFS.SQLITE_OK
|
|
356
|
+
return VFS.SQLITE_OK
|
|
361
357
|
} catch (e) {
|
|
362
|
-
this.lastError = e
|
|
363
|
-
return VFS.SQLITE_IOERR_WRITE
|
|
358
|
+
this.lastError = e
|
|
359
|
+
return VFS.SQLITE_IOERR_WRITE
|
|
364
360
|
}
|
|
365
361
|
}
|
|
366
362
|
|
|
367
363
|
/**
|
|
368
|
-
* @param {number} fileId
|
|
369
|
-
* @param {number} iSize
|
|
364
|
+
* @param {number} fileId
|
|
365
|
+
* @param {number} iSize
|
|
370
366
|
* @returns {number}
|
|
371
367
|
*/
|
|
372
368
|
jTruncate(fileId, iSize) {
|
|
373
369
|
try {
|
|
374
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
370
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
375
371
|
|
|
376
372
|
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
377
|
-
this.#requireTxActive(file)
|
|
378
|
-
file.txActive.fileSize = iSize
|
|
373
|
+
this.#requireTxActive(file)
|
|
374
|
+
file.txActive.fileSize = iSize
|
|
379
375
|
} else {
|
|
380
376
|
// All files that are not main databases are stored in a single
|
|
381
377
|
// block.
|
|
382
378
|
if (iSize < file.blockSize) {
|
|
383
|
-
const block = file.blocks.get(0)
|
|
384
|
-
file.blocks.set(0, block.subarray(0, iSize))
|
|
385
|
-
file.blockSize = iSize
|
|
379
|
+
const block = file.blocks.get(0)
|
|
380
|
+
file.blocks.set(0, block.subarray(0, iSize))
|
|
381
|
+
file.blockSize = iSize
|
|
386
382
|
}
|
|
387
383
|
}
|
|
388
|
-
return VFS.SQLITE_OK
|
|
384
|
+
return VFS.SQLITE_OK
|
|
389
385
|
} catch (e) {
|
|
390
|
-
console.error(e)
|
|
391
|
-
this.lastError = e
|
|
392
|
-
return VFS.SQLITE_IOERR_TRUNCATE
|
|
386
|
+
console.error(e)
|
|
387
|
+
this.lastError = e
|
|
388
|
+
return VFS.SQLITE_IOERR_TRUNCATE
|
|
393
389
|
}
|
|
394
390
|
}
|
|
395
391
|
|
|
396
392
|
/**
|
|
397
|
-
* @param {number} fileId
|
|
398
|
-
* @param {DataView} pSize64
|
|
393
|
+
* @param {number} fileId
|
|
394
|
+
* @param {DataView} pSize64
|
|
399
395
|
* @returns {number|Promise<number>}
|
|
400
396
|
*/
|
|
401
397
|
jFileSize(fileId, pSize64) {
|
|
402
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
403
|
-
const size = file.txActive?.fileSize ?? file.blockSize * file.blocks.size
|
|
404
|
-
pSize64.setBigInt64(0, BigInt(size), true)
|
|
405
|
-
return VFS.SQLITE_OK
|
|
398
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
399
|
+
const size = file.txActive?.fileSize ?? file.blockSize * file.blocks.size
|
|
400
|
+
pSize64.setBigInt64(0, BigInt(size), true)
|
|
401
|
+
return VFS.SQLITE_OK
|
|
406
402
|
}
|
|
407
403
|
|
|
408
404
|
/**
|
|
409
|
-
* @param {number} fileId
|
|
410
|
-
* @param {number} lockType
|
|
405
|
+
* @param {number} fileId
|
|
406
|
+
* @param {number} lockType
|
|
411
407
|
* @returns {Promise<number>}
|
|
412
408
|
*/
|
|
413
409
|
async jLock(fileId, lockType) {
|
|
414
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
415
|
-
if (lockType <= file.lockState) return VFS.SQLITE_OK
|
|
410
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
411
|
+
if (lockType <= file.lockState) return VFS.SQLITE_OK
|
|
416
412
|
switch (lockType) {
|
|
417
413
|
case VFS.SQLITE_LOCK_SHARED:
|
|
418
414
|
if (file.txWriteHint) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
415
|
+
// xFileControl() has hinted that this transaction will
|
|
416
|
+
// write. Acquire the hint lock, which is required to reach
|
|
417
|
+
// the RESERVED state.
|
|
418
|
+
if (!(await this.#lock(file, 'hint'))) {
|
|
419
|
+
return VFS.SQLITE_BUSY
|
|
420
|
+
}
|
|
425
421
|
}
|
|
426
|
-
break
|
|
422
|
+
break
|
|
427
423
|
case VFS.SQLITE_LOCK_RESERVED:
|
|
428
424
|
// Ideally we should already have the hint lock, but if not
|
|
429
425
|
// poll for it here.
|
|
430
|
-
if (!file.locks.hint && !await this.#lock(file, 'hint', POLL_EXCLUSIVE)) {
|
|
431
|
-
return VFS.SQLITE_BUSY
|
|
426
|
+
if (!file.locks.hint && !(await this.#lock(file, 'hint', POLL_EXCLUSIVE))) {
|
|
427
|
+
return VFS.SQLITE_BUSY
|
|
432
428
|
}
|
|
433
429
|
|
|
434
|
-
if (!await this.#lock(file, 'reserved', POLL_EXCLUSIVE)) {
|
|
435
|
-
file.locks.hint()
|
|
436
|
-
return VFS.SQLITE_BUSY
|
|
430
|
+
if (!(await this.#lock(file, 'reserved', POLL_EXCLUSIVE))) {
|
|
431
|
+
file.locks.hint()
|
|
432
|
+
return VFS.SQLITE_BUSY
|
|
437
433
|
}
|
|
438
434
|
|
|
439
435
|
// In order to write, our view of the database must be up to date.
|
|
440
436
|
// To check this, first fetch all transactions in IndexedDB equal to
|
|
441
437
|
// or greater than our view.
|
|
442
|
-
const idbTx = this.#idb.transaction(['blocks', 'tx'])
|
|
443
|
-
const range = IDBKeyRange.bound(
|
|
444
|
-
[file.path, file.viewTx.txId],
|
|
445
|
-
[file.path, Infinity]);
|
|
438
|
+
const idbTx = this.#idb.transaction(['blocks', 'tx'])
|
|
439
|
+
const range = IDBKeyRange.bound([file.path, file.viewTx.txId], [file.path, Infinity])
|
|
446
440
|
|
|
447
441
|
/** @type {Transaction[]} */
|
|
448
|
-
const entries = await idbX(idbTx.objectStore('tx').getAll(range))
|
|
442
|
+
const entries = await idbX(idbTx.objectStore('tx').getAll(range))
|
|
449
443
|
|
|
450
444
|
// Ideally the fetched list of transactions should contain one
|
|
451
445
|
// entry matching our view. If not then our view is out of date.
|
|
@@ -453,80 +447,80 @@ export class IDBMirrorVFS extends FacadeVFS {
|
|
|
453
447
|
// There are newer transactions in IndexedDB that we haven't
|
|
454
448
|
// seen via broadcast. Ensure that they are incorporated on unlock,
|
|
455
449
|
// and force the application to retry.
|
|
456
|
-
const blocks = idbTx.objectStore('blocks')
|
|
450
|
+
const blocks = idbTx.objectStore('blocks')
|
|
457
451
|
for (const entry of entries) {
|
|
458
452
|
// When transactions are stored to IndexedDB, the page data is
|
|
459
453
|
// stripped to save time and space. Restore the page data here.
|
|
460
454
|
for (const offset of Array.from(entry.blocks.keys())) {
|
|
461
|
-
const value = await idbX(blocks.get([file.path, offset]))
|
|
462
|
-
entry.blocks.set(offset, value.data)
|
|
455
|
+
const value = await idbX(blocks.get([file.path, offset]))
|
|
456
|
+
entry.blocks.set(offset, value.data)
|
|
463
457
|
}
|
|
464
458
|
}
|
|
465
|
-
file.broadcastReceived.push(...entries)
|
|
466
|
-
file.locks.reserved()
|
|
459
|
+
file.broadcastReceived.push(...entries)
|
|
460
|
+
file.locks.reserved()
|
|
467
461
|
return VFS.SQLITE_BUSY
|
|
468
462
|
}
|
|
469
463
|
|
|
470
|
-
console.assert(entries[0]?.txId === file.viewTx.txId || !file.viewTx.txId)
|
|
471
|
-
break
|
|
464
|
+
console.assert(entries[0]?.txId === file.viewTx.txId || !file.viewTx.txId)
|
|
465
|
+
break
|
|
472
466
|
case VFS.SQLITE_LOCK_EXCLUSIVE:
|
|
473
|
-
await this.#lock(file, 'write')
|
|
474
|
-
break
|
|
467
|
+
await this.#lock(file, 'write')
|
|
468
|
+
break
|
|
475
469
|
}
|
|
476
|
-
file.lockState = lockType
|
|
477
|
-
return VFS.SQLITE_OK
|
|
470
|
+
file.lockState = lockType
|
|
471
|
+
return VFS.SQLITE_OK
|
|
478
472
|
}
|
|
479
473
|
|
|
480
474
|
/**
|
|
481
|
-
* @param {number} fileId
|
|
482
|
-
* @param {number} lockType
|
|
475
|
+
* @param {number} fileId
|
|
476
|
+
* @param {number} lockType
|
|
483
477
|
* @returns {number}
|
|
484
478
|
*/
|
|
485
479
|
jUnlock(fileId, lockType) {
|
|
486
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
487
|
-
if (lockType >= file.lockState) return VFS.SQLITE_OK
|
|
480
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
481
|
+
if (lockType >= file.lockState) return VFS.SQLITE_OK
|
|
488
482
|
switch (lockType) {
|
|
489
483
|
case VFS.SQLITE_LOCK_SHARED:
|
|
490
|
-
file.locks.write?.()
|
|
491
|
-
file.locks.reserved?.()
|
|
492
|
-
file.locks.hint?.()
|
|
493
|
-
break
|
|
484
|
+
file.locks.write?.()
|
|
485
|
+
file.locks.reserved?.()
|
|
486
|
+
file.locks.hint?.()
|
|
487
|
+
break
|
|
494
488
|
case VFS.SQLITE_LOCK_NONE:
|
|
495
489
|
// Don't release the read lock here. It will be released on demand
|
|
496
490
|
// when a broadcast notifies us that another connections wants to
|
|
497
491
|
// VACUUM.
|
|
498
|
-
this.#processBroadcasts(file)
|
|
499
|
-
file.locks.write?.()
|
|
500
|
-
file.locks.reserved?.()
|
|
501
|
-
file.locks.hint?.()
|
|
502
|
-
break
|
|
492
|
+
this.#processBroadcasts(file)
|
|
493
|
+
file.locks.write?.()
|
|
494
|
+
file.locks.reserved?.()
|
|
495
|
+
file.locks.hint?.()
|
|
496
|
+
break
|
|
503
497
|
}
|
|
504
|
-
file.lockState = lockType
|
|
505
|
-
return VFS.SQLITE_OK
|
|
498
|
+
file.lockState = lockType
|
|
499
|
+
return VFS.SQLITE_OK
|
|
506
500
|
}
|
|
507
501
|
|
|
508
502
|
/**
|
|
509
503
|
* @param {number} fileId
|
|
510
|
-
* @param {DataView} pResOut
|
|
504
|
+
* @param {DataView} pResOut
|
|
511
505
|
* @returns {Promise<number>}
|
|
512
506
|
*/
|
|
513
507
|
async jCheckReservedLock(fileId, pResOut) {
|
|
514
508
|
try {
|
|
515
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
516
|
-
console.assert(file.flags & VFS.SQLITE_OPEN_MAIN_DB)
|
|
509
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
510
|
+
console.assert(file.flags & VFS.SQLITE_OPEN_MAIN_DB)
|
|
517
511
|
if (await this.#lock(file, 'reserved', POLL_SHARED)) {
|
|
518
512
|
// This looks backwards, but if we get the lock then no one
|
|
519
513
|
// else had it.
|
|
520
|
-
pResOut.setInt32(0, 0, true)
|
|
521
|
-
file.locks.reserved()
|
|
514
|
+
pResOut.setInt32(0, 0, true)
|
|
515
|
+
file.locks.reserved()
|
|
522
516
|
} else {
|
|
523
|
-
pResOut.setInt32(0, 1, true)
|
|
517
|
+
pResOut.setInt32(0, 1, true)
|
|
524
518
|
}
|
|
525
|
-
return VFS.SQLITE_OK
|
|
519
|
+
return VFS.SQLITE_OK
|
|
526
520
|
} catch (e) {
|
|
527
|
-
console.error(e)
|
|
528
|
-
this.lastError = e
|
|
529
|
-
return VFS.SQLITE_IOERR_LOCK
|
|
521
|
+
console.error(e)
|
|
522
|
+
this.lastError = e
|
|
523
|
+
return VFS.SQLITE_IOERR_LOCK
|
|
530
524
|
}
|
|
531
525
|
}
|
|
532
526
|
|
|
@@ -538,19 +532,19 @@ export class IDBMirrorVFS extends FacadeVFS {
|
|
|
538
532
|
*/
|
|
539
533
|
async jFileControl(fileId, op, pArg) {
|
|
540
534
|
try {
|
|
541
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
535
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
542
536
|
switch (op) {
|
|
543
537
|
case VFS.SQLITE_FCNTL_PRAGMA:
|
|
544
|
-
const key = cvtString(pArg, 4)
|
|
545
|
-
const value = cvtString(pArg, 8)
|
|
546
|
-
this.log?.('xFileControl', file.path, 'PRAGMA', key, value)
|
|
538
|
+
const key = cvtString(pArg, 4)
|
|
539
|
+
const value = cvtString(pArg, 8)
|
|
540
|
+
this.log?.('xFileControl', file.path, 'PRAGMA', key, value)
|
|
547
541
|
switch (key.toLowerCase()) {
|
|
548
542
|
case 'page_size':
|
|
549
543
|
// Don't allow changing the page size.
|
|
550
544
|
if (value && file.blockSize && Number(value) !== file.blockSize) {
|
|
551
|
-
return VFS.SQLITE_ERROR
|
|
545
|
+
return VFS.SQLITE_ERROR
|
|
552
546
|
}
|
|
553
|
-
break
|
|
547
|
+
break
|
|
554
548
|
case 'synchronous':
|
|
555
549
|
// This VFS only recognizes 'full' and not 'full'.
|
|
556
550
|
if (value) {
|
|
@@ -559,29 +553,29 @@ export class IDBMirrorVFS extends FacadeVFS {
|
|
|
559
553
|
case '2':
|
|
560
554
|
case 'extra':
|
|
561
555
|
case '3':
|
|
562
|
-
file.synchronous = 'full'
|
|
563
|
-
break
|
|
556
|
+
file.synchronous = 'full'
|
|
557
|
+
break
|
|
564
558
|
case 'normal':
|
|
565
559
|
case '1':
|
|
566
|
-
file.synchronous = 'normal'
|
|
567
|
-
break
|
|
560
|
+
file.synchronous = 'normal'
|
|
561
|
+
break
|
|
568
562
|
default:
|
|
569
|
-
console.warn(`unsupported synchronous mode: ${value}`)
|
|
570
|
-
return VFS.SQLITE_ERROR
|
|
571
|
-
|
|
563
|
+
console.warn(`unsupported synchronous mode: ${value}`)
|
|
564
|
+
return VFS.SQLITE_ERROR
|
|
565
|
+
}
|
|
572
566
|
}
|
|
573
|
-
break
|
|
574
|
-
|
|
575
|
-
break
|
|
567
|
+
break
|
|
568
|
+
}
|
|
569
|
+
break
|
|
576
570
|
case VFS.SQLITE_FCNTL_BEGIN_ATOMIC_WRITE:
|
|
577
|
-
this.log?.('xFileControl', 'BEGIN_ATOMIC_WRITE', file.path)
|
|
578
|
-
return VFS.SQLITE_OK
|
|
571
|
+
this.log?.('xFileControl', 'BEGIN_ATOMIC_WRITE', file.path)
|
|
572
|
+
return VFS.SQLITE_OK
|
|
579
573
|
case VFS.SQLITE_FCNTL_COMMIT_ATOMIC_WRITE:
|
|
580
|
-
this.log?.('xFileControl', 'COMMIT_ATOMIC_WRITE', file.path)
|
|
581
|
-
return VFS.SQLITE_OK
|
|
574
|
+
this.log?.('xFileControl', 'COMMIT_ATOMIC_WRITE', file.path)
|
|
575
|
+
return VFS.SQLITE_OK
|
|
582
576
|
case VFS.SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE:
|
|
583
|
-
this.#dropTx(file)
|
|
584
|
-
return VFS.SQLITE_OK
|
|
577
|
+
this.#dropTx(file)
|
|
578
|
+
return VFS.SQLITE_OK
|
|
585
579
|
case VFS.SQLITE_FCNTL_SYNC:
|
|
586
580
|
// Propagate database writes to IndexedDB and other clients. Most
|
|
587
581
|
// often this is a SQLite transaction, but it can also be a
|
|
@@ -589,30 +583,30 @@ export class IDBMirrorVFS extends FacadeVFS {
|
|
|
589
583
|
//
|
|
590
584
|
// If SQLITE_FCNTL_OVERWRITE has been received then propagation is
|
|
591
585
|
// deferred until SQLITE_FCNTL_COMMIT_PHASETWO for file truncation.
|
|
592
|
-
this.log?.('xFileControl', 'SYNC', file.path)
|
|
586
|
+
this.log?.('xFileControl', 'SYNC', file.path)
|
|
593
587
|
if (file.txActive && !file.txOverwrite) {
|
|
594
|
-
await this.#commitTx(file)
|
|
588
|
+
await this.#commitTx(file)
|
|
595
589
|
}
|
|
596
|
-
break
|
|
590
|
+
break
|
|
597
591
|
case VFS.SQLITE_FCNTL_OVERWRITE:
|
|
598
592
|
// Marks the beginning of a VACUUM.
|
|
599
|
-
file.txOverwrite = true
|
|
600
|
-
break
|
|
593
|
+
file.txOverwrite = true
|
|
594
|
+
break
|
|
601
595
|
case VFS.SQLITE_FCNTL_COMMIT_PHASETWO:
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
596
|
+
// Commit database writes for VACUUM. Other writes will already
|
|
597
|
+
// be propagated by SQLITE_FCNTL_SYNC.
|
|
598
|
+
this.log?.('xFileControl', 'COMMIT_PHASETWO', file.path)
|
|
599
|
+
if (file.txActive) {
|
|
600
|
+
await this.#commitTx(file)
|
|
601
|
+
}
|
|
602
|
+
file.txOverwrite = false
|
|
603
|
+
break
|
|
604
|
+
}
|
|
611
605
|
} catch (e) {
|
|
612
|
-
this.#lastError = e
|
|
613
|
-
return VFS.SQLITE_IOERR
|
|
606
|
+
this.#lastError = e
|
|
607
|
+
return VFS.SQLITE_IOERR
|
|
614
608
|
}
|
|
615
|
-
return VFS.SQLITE_NOTFOUND
|
|
609
|
+
return VFS.SQLITE_NOTFOUND
|
|
616
610
|
}
|
|
617
611
|
|
|
618
612
|
/**
|
|
@@ -620,105 +614,101 @@ export class IDBMirrorVFS extends FacadeVFS {
|
|
|
620
614
|
* @returns {number|Promise<number>}
|
|
621
615
|
*/
|
|
622
616
|
jDeviceCharacteristics(fileId) {
|
|
623
|
-
return 0
|
|
624
|
-
| VFS.SQLITE_IOCAP_BATCH_ATOMIC
|
|
625
|
-
| VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
|
|
617
|
+
return 0 | VFS.SQLITE_IOCAP_BATCH_ATOMIC | VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
|
|
626
618
|
}
|
|
627
619
|
|
|
628
620
|
/**
|
|
629
|
-
* @param {Uint8Array} zBuf
|
|
621
|
+
* @param {Uint8Array} zBuf
|
|
630
622
|
* @returns {number}
|
|
631
623
|
*/
|
|
632
624
|
jGetLastError(zBuf) {
|
|
633
625
|
if (this.#lastError) {
|
|
634
|
-
console.error(this.#lastError)
|
|
635
|
-
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1)
|
|
636
|
-
const { written } = new TextEncoder().encodeInto(this.#lastError.message, outputArray)
|
|
637
|
-
zBuf[written] = 0
|
|
626
|
+
console.error(this.#lastError)
|
|
627
|
+
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1)
|
|
628
|
+
const { written } = new TextEncoder().encodeInto(this.#lastError.message, outputArray)
|
|
629
|
+
zBuf[written] = 0
|
|
638
630
|
}
|
|
639
631
|
return VFS.SQLITE_OK
|
|
640
632
|
}
|
|
641
633
|
|
|
642
634
|
/**
|
|
643
|
-
*
|
|
644
|
-
* @param {File} file
|
|
645
|
-
* @param {Transaction} tx
|
|
635
|
+
*
|
|
636
|
+
* @param {File} file
|
|
637
|
+
* @param {Transaction} tx
|
|
646
638
|
*/
|
|
647
639
|
#acceptTx(file, tx) {
|
|
648
640
|
// Add/update transaction pages.
|
|
649
641
|
for (const [offset, data] of tx.blocks) {
|
|
650
|
-
file.blocks.set(offset, data)
|
|
642
|
+
file.blocks.set(offset, data)
|
|
651
643
|
if (file.blockSize === 0) {
|
|
652
|
-
file.blockSize = data.byteLength
|
|
644
|
+
file.blockSize = data.byteLength
|
|
653
645
|
}
|
|
654
646
|
}
|
|
655
647
|
|
|
656
|
-
let truncated = tx.fileSize + file.blockSize
|
|
648
|
+
let truncated = tx.fileSize + file.blockSize
|
|
657
649
|
while (file.blocks.delete(truncated)) {
|
|
658
|
-
truncated += file.blockSize
|
|
650
|
+
truncated += file.blockSize
|
|
659
651
|
}
|
|
660
652
|
|
|
661
|
-
file.viewTx = tx
|
|
653
|
+
file.viewTx = tx
|
|
662
654
|
}
|
|
663
655
|
|
|
664
656
|
/**
|
|
665
|
-
* @param {File} file
|
|
657
|
+
* @param {File} file
|
|
666
658
|
*/
|
|
667
659
|
async #commitTx(file) {
|
|
668
660
|
// Advance our own view. Even if we received our own broadcasts (we
|
|
669
661
|
// don't), we want our view to be updated synchronously.
|
|
670
|
-
this.#acceptTx(file, file.txActive)
|
|
671
|
-
this.#setView(file, file.txActive)
|
|
662
|
+
this.#acceptTx(file, file.txActive)
|
|
663
|
+
this.#setView(file, file.txActive)
|
|
672
664
|
|
|
673
|
-
const oldestTxId = await this.#getOldestTxInUse(file)
|
|
665
|
+
const oldestTxId = await this.#getOldestTxInUse(file)
|
|
674
666
|
|
|
675
667
|
// Update IndexedDB page data.
|
|
676
|
-
const idbTx = this.#idb.transaction(['blocks', 'tx'], 'readwrite')
|
|
677
|
-
const blocks = idbTx.objectStore('blocks')
|
|
668
|
+
const idbTx = this.#idb.transaction(['blocks', 'tx'], 'readwrite')
|
|
669
|
+
const blocks = idbTx.objectStore('blocks')
|
|
678
670
|
for (const [offset, data] of file.txActive.blocks) {
|
|
679
|
-
blocks.put({ path: file.path, offset, data })
|
|
671
|
+
blocks.put({ path: file.path, offset, data })
|
|
680
672
|
}
|
|
681
673
|
|
|
682
674
|
// Delete obsolete transactions no longer needed.
|
|
683
|
-
const oldRange = IDBKeyRange.bound(
|
|
684
|
-
|
|
685
|
-
false, true);
|
|
686
|
-
idbTx.objectStore('tx').delete(oldRange);
|
|
675
|
+
const oldRange = IDBKeyRange.bound([file.path, -Infinity], [file.path, oldestTxId], false, true)
|
|
676
|
+
idbTx.objectStore('tx').delete(oldRange)
|
|
687
677
|
|
|
688
678
|
// Save transaction object. Omit page data as an optimization.
|
|
689
|
-
const txSansData = Object.assign({}, file.txActive)
|
|
690
|
-
txSansData.blocks = new Map(Array.from(file.txActive.blocks, ([k]) => [k, null]))
|
|
691
|
-
idbTx.objectStore('tx').put(txSansData)
|
|
679
|
+
const txSansData = Object.assign({}, file.txActive)
|
|
680
|
+
txSansData.blocks = new Map(Array.from(file.txActive.blocks, ([k]) => [k, null]))
|
|
681
|
+
idbTx.objectStore('tx').put(txSansData)
|
|
692
682
|
|
|
693
683
|
// Broadcast transaction once it commits.
|
|
694
684
|
const complete = new Promise((resolve, reject) => {
|
|
695
|
-
const message = file.txActive
|
|
685
|
+
const message = file.txActive
|
|
696
686
|
idbTx.oncomplete = () => {
|
|
697
|
-
file.broadcastChannel.postMessage(message)
|
|
698
|
-
resolve()
|
|
699
|
-
}
|
|
700
|
-
idbTx.onabort = () => reject(idbTx.error)
|
|
701
|
-
idbTx.commit()
|
|
702
|
-
})
|
|
687
|
+
file.broadcastChannel.postMessage(message)
|
|
688
|
+
resolve()
|
|
689
|
+
}
|
|
690
|
+
idbTx.onabort = () => reject(idbTx.error)
|
|
691
|
+
idbTx.commit()
|
|
692
|
+
})
|
|
703
693
|
|
|
704
694
|
if (file.synchronous === 'full') {
|
|
705
|
-
await complete
|
|
695
|
+
await complete
|
|
706
696
|
}
|
|
707
697
|
|
|
708
|
-
file.txActive = null
|
|
709
|
-
file.txWriteHint = false
|
|
698
|
+
file.txActive = null
|
|
699
|
+
file.txWriteHint = false
|
|
710
700
|
}
|
|
711
701
|
|
|
712
702
|
/**
|
|
713
|
-
* @param {File} file
|
|
703
|
+
* @param {File} file
|
|
714
704
|
*/
|
|
715
705
|
#dropTx(file) {
|
|
716
|
-
file.txActive = null
|
|
717
|
-
file.txWriteHint = false
|
|
706
|
+
file.txActive = null
|
|
707
|
+
file.txWriteHint = false
|
|
718
708
|
}
|
|
719
709
|
|
|
720
710
|
/**
|
|
721
|
-
* @param {File} file
|
|
711
|
+
* @param {File} file
|
|
722
712
|
*/
|
|
723
713
|
#requireTxActive(file) {
|
|
724
714
|
if (!file.txActive) {
|
|
@@ -727,32 +717,33 @@ export class IDBMirrorVFS extends FacadeVFS {
|
|
|
727
717
|
txId: file.viewTx.txId + 1,
|
|
728
718
|
blocks: new Map(),
|
|
729
719
|
fileSize: file.blockSize * file.blocks.size,
|
|
730
|
-
}
|
|
720
|
+
}
|
|
731
721
|
}
|
|
732
722
|
}
|
|
733
723
|
|
|
734
724
|
/**
|
|
735
|
-
* @param {string} path
|
|
725
|
+
* @param {string} path
|
|
736
726
|
* @returns {Promise}
|
|
737
727
|
*/
|
|
738
728
|
async #deleteFile(path) {
|
|
739
|
-
this.#mapPathToFile.delete(path)
|
|
729
|
+
this.#mapPathToFile.delete(path)
|
|
740
730
|
|
|
741
731
|
// Only main databases are stored in IndexedDB and SQLite never
|
|
742
732
|
// deletes main databases, but delete blocks here anyway for
|
|
743
733
|
// standalone use.
|
|
744
|
-
const request = this.#idb
|
|
734
|
+
const request = this.#idb
|
|
735
|
+
.transaction(['blocks'], 'readwrite')
|
|
745
736
|
.objectStore('blocks')
|
|
746
|
-
.delete(IDBKeyRange.bound([path, 0], [path, Infinity]))
|
|
737
|
+
.delete(IDBKeyRange.bound([path, 0], [path, Infinity]))
|
|
747
738
|
await new Promise((resolve, reject) => {
|
|
748
|
-
const idbTx = request.transaction
|
|
749
|
-
idbTx.oncomplete = resolve
|
|
750
|
-
idbTx.onerror = () => reject(idbTx.error)
|
|
751
|
-
})
|
|
739
|
+
const idbTx = request.transaction
|
|
740
|
+
idbTx.oncomplete = resolve
|
|
741
|
+
idbTx.onerror = () => reject(idbTx.error)
|
|
742
|
+
})
|
|
752
743
|
}
|
|
753
744
|
|
|
754
745
|
/**
|
|
755
|
-
* @param {File} file
|
|
746
|
+
* @param {File} file
|
|
756
747
|
* @returns {Promise<number>}
|
|
757
748
|
*/
|
|
758
749
|
async #getOldestTxInUse(file) {
|
|
@@ -760,130 +751,132 @@ export class IDBMirrorVFS extends FacadeVFS {
|
|
|
760
751
|
// the latest transaction it knows about. We can find the oldest
|
|
761
752
|
// transaction by listing the those locks and extracting the earliest
|
|
762
753
|
// transaction id.
|
|
763
|
-
const TX_LOCK_REGEX = /^(.*)@@\[(\d+)\]
|
|
764
|
-
let oldestTxId = file.viewTx.txId
|
|
765
|
-
const locks = await navigator.locks.query()
|
|
754
|
+
const TX_LOCK_REGEX = /^(.*)@@\[(\d+)\]$/
|
|
755
|
+
let oldestTxId = file.viewTx.txId
|
|
756
|
+
const locks = await navigator.locks.query()
|
|
766
757
|
for (const { name } of locks.held) {
|
|
767
|
-
const m = TX_LOCK_REGEX.exec(name)
|
|
758
|
+
const m = TX_LOCK_REGEX.exec(name)
|
|
768
759
|
if (m && m[1] === file.path) {
|
|
769
|
-
oldestTxId = Math.min(oldestTxId, Number(m[2]))
|
|
760
|
+
oldestTxId = Math.min(oldestTxId, Number(m[2]))
|
|
770
761
|
}
|
|
771
762
|
}
|
|
772
|
-
return oldestTxId
|
|
763
|
+
return oldestTxId
|
|
773
764
|
}
|
|
774
765
|
|
|
775
766
|
/**
|
|
776
767
|
* Acquire one of the database file internal Web Locks.
|
|
777
|
-
* @param {File} file
|
|
778
|
-
* @param {'write'|'reserved'|'hint'} name
|
|
779
|
-
* @param {LockOptions} options
|
|
768
|
+
* @param {File} file
|
|
769
|
+
* @param {'write'|'reserved'|'hint'} name
|
|
770
|
+
* @param {LockOptions} options
|
|
780
771
|
* @returns {Promise<boolean>}
|
|
781
772
|
*/
|
|
782
773
|
#lock(file, name, options = {}) {
|
|
783
|
-
return new Promise(resolve => {
|
|
784
|
-
const lockName = `${file.path}@@${name}
|
|
785
|
-
navigator.locks
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
774
|
+
return new Promise((resolve) => {
|
|
775
|
+
const lockName = `${file.path}@@${name}`
|
|
776
|
+
navigator.locks
|
|
777
|
+
.request(lockName, options, (lock) => {
|
|
778
|
+
if (lock) {
|
|
779
|
+
return new Promise((release) => {
|
|
780
|
+
file.locks[name] = () => {
|
|
781
|
+
release()
|
|
782
|
+
file.locks[name] = null
|
|
783
|
+
}
|
|
784
|
+
resolve(true)
|
|
785
|
+
})
|
|
786
|
+
} else {
|
|
787
|
+
file.locks[name] = null
|
|
788
|
+
resolve(false)
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
.catch((e) => {
|
|
792
|
+
if (e.name !== 'AbortError') throw e
|
|
793
|
+
})
|
|
794
|
+
})
|
|
802
795
|
}
|
|
803
796
|
|
|
804
797
|
/**
|
|
805
798
|
* Handle prevously received messages from other connections.
|
|
806
|
-
* @param {File} file
|
|
799
|
+
* @param {File} file
|
|
807
800
|
*/
|
|
808
801
|
#processBroadcasts(file) {
|
|
809
802
|
// Sort transaction messages by id.
|
|
810
|
-
file.broadcastReceived.sort((a, b) => a.txId - b.txId)
|
|
803
|
+
file.broadcastReceived.sort((a, b) => a.txId - b.txId)
|
|
811
804
|
|
|
812
|
-
let nHandled = 0
|
|
813
|
-
let newTx = file.viewTx
|
|
805
|
+
let nHandled = 0
|
|
806
|
+
let newTx = file.viewTx
|
|
814
807
|
for (const message of file.broadcastReceived) {
|
|
815
808
|
if (message.txId <= newTx.txId) {
|
|
816
809
|
// This transaction is already incorporated into our view.
|
|
817
810
|
} else if (message.txId === newTx.txId + 1) {
|
|
818
811
|
// This is the next expected transaction.
|
|
819
|
-
this.log?.(`accept tx ${message.txId}`)
|
|
820
|
-
this.#acceptTx(file, message)
|
|
821
|
-
newTx = message
|
|
812
|
+
this.log?.(`accept tx ${message.txId}`)
|
|
813
|
+
this.#acceptTx(file, message)
|
|
814
|
+
newTx = message
|
|
822
815
|
} else {
|
|
823
816
|
// There is a gap in the transaction sequence.
|
|
824
|
-
console.warn(`missing tx ${newTx.txId + 1} (got ${message.txId})`)
|
|
825
|
-
break
|
|
817
|
+
console.warn(`missing tx ${newTx.txId + 1} (got ${message.txId})`)
|
|
818
|
+
break
|
|
826
819
|
}
|
|
827
|
-
nHandled
|
|
820
|
+
nHandled++
|
|
828
821
|
}
|
|
829
822
|
|
|
830
823
|
// Remove handled messages from the list.
|
|
831
|
-
file.broadcastReceived.splice(0, nHandled)
|
|
824
|
+
file.broadcastReceived.splice(0, nHandled)
|
|
832
825
|
|
|
833
826
|
// Tell other connections about a change in our view.
|
|
834
827
|
if (newTx.txId > file.viewTx.txId) {
|
|
835
828
|
// No need to await here.
|
|
836
|
-
this.#setView(file, newTx)
|
|
829
|
+
this.#setView(file, newTx)
|
|
837
830
|
}
|
|
838
831
|
}
|
|
839
832
|
|
|
840
833
|
/**
|
|
841
|
-
* @param {File} file
|
|
842
|
-
* @param {Transaction} tx
|
|
834
|
+
* @param {File} file
|
|
835
|
+
* @param {Transaction} tx
|
|
843
836
|
*/
|
|
844
837
|
async #setView(file, tx) {
|
|
845
838
|
// Publish our view of the database with a lock name that includes
|
|
846
839
|
// the transaction id. As long as we hold the lock, no other connection
|
|
847
840
|
// will overwrite data we are using.
|
|
848
|
-
file.viewTx = tx
|
|
849
|
-
const lockName = `${file.path}@@[${tx.txId}]
|
|
850
|
-
const newReleaser = await new Promise(resolve => {
|
|
851
|
-
navigator.locks.request(lockName, SHARED, lock => {
|
|
852
|
-
return new Promise(release => {
|
|
853
|
-
resolve(release)
|
|
854
|
-
})
|
|
855
|
-
})
|
|
856
|
-
})
|
|
841
|
+
file.viewTx = tx
|
|
842
|
+
const lockName = `${file.path}@@[${tx.txId}]`
|
|
843
|
+
const newReleaser = await new Promise((resolve) => {
|
|
844
|
+
navigator.locks.request(lockName, SHARED, (lock) => {
|
|
845
|
+
return new Promise((release) => {
|
|
846
|
+
resolve(release)
|
|
847
|
+
})
|
|
848
|
+
})
|
|
849
|
+
})
|
|
857
850
|
|
|
858
851
|
// The new lock is acquired so release the old one.
|
|
859
|
-
file.viewReleaser?.()
|
|
860
|
-
file.viewReleaser = newReleaser
|
|
852
|
+
file.viewReleaser?.()
|
|
853
|
+
file.viewReleaser = newReleaser
|
|
861
854
|
}
|
|
862
855
|
}
|
|
863
856
|
|
|
864
857
|
/**
|
|
865
858
|
* Wrap IndexedDB request with a Promise.
|
|
866
|
-
* @param {IDBRequest} request
|
|
867
|
-
* @returns
|
|
859
|
+
* @param {IDBRequest} request
|
|
860
|
+
* @returns
|
|
868
861
|
*/
|
|
869
862
|
function idbX(request) {
|
|
870
863
|
return new Promise((resolve, reject) => {
|
|
871
|
-
request.onsuccess = () => resolve(request.result)
|
|
872
|
-
request.onerror = () => reject(request.error)
|
|
873
|
-
})
|
|
864
|
+
request.onsuccess = () => resolve(request.result)
|
|
865
|
+
request.onerror = () => reject(request.error)
|
|
866
|
+
})
|
|
874
867
|
}
|
|
875
868
|
|
|
876
869
|
/**
|
|
877
870
|
* Extract a C string from WebAssembly memory.
|
|
878
|
-
* @param {DataView} dataView
|
|
879
|
-
* @param {number} offset
|
|
880
|
-
* @returns
|
|
871
|
+
* @param {DataView} dataView
|
|
872
|
+
* @param {number} offset
|
|
873
|
+
* @returns
|
|
881
874
|
*/
|
|
882
875
|
function cvtString(dataView, offset) {
|
|
883
|
-
const p = dataView.getUint32(offset, true)
|
|
876
|
+
const p = dataView.getUint32(offset, true)
|
|
884
877
|
if (p) {
|
|
885
|
-
const chars = new Uint8Array(dataView.buffer, p)
|
|
886
|
-
return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)))
|
|
878
|
+
const chars = new Uint8Array(dataView.buffer, p)
|
|
879
|
+
return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)))
|
|
887
880
|
}
|
|
888
|
-
return null
|
|
881
|
+
return null
|
|
889
882
|
}
|