@livestore/wa-sqlite 0.4.0-dev.21 → 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,19 +1,19 @@
|
|
|
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
6
|
// Options for navigator.locks.request().
|
|
7
|
-
/** @type {LockOptions} */ const SHARED = { mode: 'shared' }
|
|
8
|
-
/** @type {LockOptions} */ const POLL_SHARED = { ifAvailable: true, mode: 'shared' }
|
|
9
|
-
/** @type {LockOptions} */ const POLL_EXCLUSIVE = { ifAvailable: true, mode: 'exclusive' }
|
|
7
|
+
/** @type {LockOptions} */ const SHARED = { mode: 'shared' }
|
|
8
|
+
/** @type {LockOptions} */ const POLL_SHARED = { ifAvailable: true, mode: 'shared' }
|
|
9
|
+
/** @type {LockOptions} */ const POLL_EXCLUSIVE = { ifAvailable: true, mode: 'exclusive' }
|
|
10
10
|
|
|
11
11
|
// Default number of transactions between flushing the OPFS file and
|
|
12
12
|
// reclaiming free page offsets. Used only when synchronous! = 'full'.
|
|
13
|
-
const DEFAULT_FLUSH_INTERVAL = 64
|
|
13
|
+
const DEFAULT_FLUSH_INTERVAL = 64
|
|
14
14
|
|
|
15
15
|
// Used only for debug logging.
|
|
16
|
-
const contextId = Math.random().toString(36).slice(2)
|
|
16
|
+
const contextId = Math.random().toString(36).slice(2)
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @typedef {Object} Transaction
|
|
@@ -30,195 +30,195 @@ const contextId = Math.random().toString(36).slice(2);
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
class File {
|
|
33
|
-
/** @type {string} */ path
|
|
34
|
-
/** @type {number} */ flags
|
|
35
|
-
/** @type {FileSystemSyncAccessHandle} */ accessHandle
|
|
33
|
+
/** @type {string} */ path
|
|
34
|
+
/** @type {number} */ flags
|
|
35
|
+
/** @type {FileSystemSyncAccessHandle} */ accessHandle
|
|
36
36
|
|
|
37
37
|
// Members below are only used for SQLITE_OPEN_MAIN_DB.
|
|
38
38
|
|
|
39
|
-
/** @type {number} */ pageSize
|
|
40
|
-
/** @type {number} */ fileSize
|
|
39
|
+
/** @type {number} */ pageSize
|
|
40
|
+
/** @type {number} */ fileSize // virtual file size exposed to SQLite
|
|
41
41
|
|
|
42
|
-
/** @type {IDBDatabase} */ idb
|
|
42
|
+
/** @type {IDBDatabase} */ idb
|
|
43
43
|
|
|
44
|
-
/** @type {Transaction} */ viewTx
|
|
45
|
-
/** @type {function?} */ viewReleaser
|
|
44
|
+
/** @type {Transaction} */ viewTx // last transaction incorporated
|
|
45
|
+
/** @type {function?} */ viewReleaser
|
|
46
46
|
|
|
47
|
-
/** @type {BroadcastChannel} */ broadcastChannel
|
|
48
|
-
/** @type {(Transaction|AccessRequest)[]} */ broadcastReceived
|
|
47
|
+
/** @type {BroadcastChannel} */ broadcastChannel
|
|
48
|
+
/** @type {(Transaction|AccessRequest)[]} */ broadcastReceived
|
|
49
49
|
|
|
50
|
-
/** @type {Map<number, number>} */ mapPageToOffset
|
|
51
|
-
/** @type {Map<number, Transaction>} */ mapTxToPending
|
|
52
|
-
/** @type {Set<number>} */ freeOffsets
|
|
50
|
+
/** @type {Map<number, number>} */ mapPageToOffset
|
|
51
|
+
/** @type {Map<number, Transaction>} */ mapTxToPending
|
|
52
|
+
/** @type {Set<number>} */ freeOffsets
|
|
53
53
|
|
|
54
|
-
/** @type {number} */ lockState
|
|
55
|
-
/** @type {{read?: function, write?: function, reserved?: function, hint?: function}} */ locks
|
|
54
|
+
/** @type {number} */ lockState
|
|
55
|
+
/** @type {{read?: function, write?: function, reserved?: function, hint?: function}} */ locks
|
|
56
56
|
|
|
57
|
-
/** @type {AbortController} */ abortController
|
|
57
|
+
/** @type {AbortController} */ abortController
|
|
58
58
|
|
|
59
|
-
/** @type {Transaction?} */ txActive
|
|
60
|
-
/** @type {number} */ txRealFileSize
|
|
61
|
-
/** @type {boolean} */ txIsOverwrite
|
|
62
|
-
/** @type {boolean} */ txWriteHint
|
|
59
|
+
/** @type {Transaction?} */ txActive // transaction in progress
|
|
60
|
+
/** @type {number} */ txRealFileSize // physical file size
|
|
61
|
+
/** @type {boolean} */ txIsOverwrite // VACUUM in progress
|
|
62
|
+
/** @type {boolean} */ txWriteHint
|
|
63
63
|
|
|
64
|
-
/** @type {'full'|'normal'} */ synchronous
|
|
65
|
-
/** @type {number} */ flushInterval
|
|
64
|
+
/** @type {'full'|'normal'} */ synchronous
|
|
65
|
+
/** @type {number} */ flushInterval
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* @param {string} pathname
|
|
69
|
-
* @param {number} flags
|
|
68
|
+
* @param {string} pathname
|
|
69
|
+
* @param {number} flags
|
|
70
70
|
*/
|
|
71
71
|
constructor(pathname, flags) {
|
|
72
|
-
this.path = pathname
|
|
73
|
-
this.flags = flags
|
|
72
|
+
this.path = pathname
|
|
73
|
+
this.flags = flags
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
* @param {string} pathname
|
|
78
|
-
* @param {number} flags
|
|
79
|
-
* @returns
|
|
77
|
+
* @param {string} pathname
|
|
78
|
+
* @param {number} flags
|
|
79
|
+
* @returns
|
|
80
80
|
*/
|
|
81
81
|
static async create(pathname, flags) {
|
|
82
|
-
const file = new File(pathname, flags)
|
|
82
|
+
const file = new File(pathname, flags)
|
|
83
83
|
|
|
84
|
-
const create = !!(flags & VFS.SQLITE_OPEN_CREATE)
|
|
85
|
-
const [directory, filename] = await getPathComponents(pathname, create)
|
|
86
|
-
const handle = await directory.getFileHandle(filename, { create })
|
|
84
|
+
const create = !!(flags & VFS.SQLITE_OPEN_CREATE)
|
|
85
|
+
const [directory, filename] = await getPathComponents(pathname, create)
|
|
86
|
+
const handle = await directory.getFileHandle(filename, { create })
|
|
87
87
|
// @ts-ignore
|
|
88
|
-
file.accessHandle = await handle.createSyncAccessHandle({ mode: 'readwrite-unsafe' })
|
|
88
|
+
file.accessHandle = await handle.createSyncAccessHandle({ mode: 'readwrite-unsafe' })
|
|
89
89
|
|
|
90
90
|
if (flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
91
91
|
file.idb = await new Promise((resolve, reject) => {
|
|
92
|
-
const request = indexedDB.open(pathname)
|
|
92
|
+
const request = indexedDB.open(pathname)
|
|
93
93
|
request.onupgradeneeded = () => {
|
|
94
|
-
const db = request.result
|
|
95
|
-
db.createObjectStore('pages', { keyPath: 'i' })
|
|
96
|
-
db.createObjectStore('pending', { keyPath: 'txId'})
|
|
97
|
-
}
|
|
98
|
-
request.onsuccess = () => resolve(request.result)
|
|
99
|
-
request.onerror = () => reject(request.error)
|
|
100
|
-
})
|
|
94
|
+
const db = request.result
|
|
95
|
+
db.createObjectStore('pages', { keyPath: 'i' })
|
|
96
|
+
db.createObjectStore('pending', { keyPath: 'txId' })
|
|
97
|
+
}
|
|
98
|
+
request.onsuccess = () => resolve(request.result)
|
|
99
|
+
request.onerror = () => reject(request.error)
|
|
100
|
+
})
|
|
101
101
|
}
|
|
102
|
-
return file
|
|
102
|
+
return file
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export class OPFSPermutedVFS extends FacadeVFS {
|
|
107
|
-
/** @type {Map<number, File>} */ #mapIdToFile = new Map()
|
|
108
|
-
#lastError = null
|
|
107
|
+
/** @type {Map<number, File>} */ #mapIdToFile = new Map()
|
|
108
|
+
#lastError = null
|
|
109
109
|
|
|
110
|
-
log = null
|
|
110
|
+
log = null // (...args) => console.debug(contextId, ...args);
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* @param {string} name
|
|
114
|
-
* @param {*} module
|
|
115
|
-
* @returns
|
|
113
|
+
* @param {string} name
|
|
114
|
+
* @param {*} module
|
|
115
|
+
* @returns
|
|
116
116
|
*/
|
|
117
117
|
static async create(name, module) {
|
|
118
|
-
const vfs = new OPFSPermutedVFS(name, module)
|
|
119
|
-
await vfs.isReady()
|
|
120
|
-
return vfs
|
|
118
|
+
const vfs = new OPFSPermutedVFS(name, module)
|
|
119
|
+
await vfs.isReady()
|
|
120
|
+
return vfs
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
* @param {string?} zName
|
|
125
|
-
* @param {number} fileId
|
|
126
|
-
* @param {number} flags
|
|
127
|
-
* @param {DataView} pOutFlags
|
|
124
|
+
* @param {string?} zName
|
|
125
|
+
* @param {number} fileId
|
|
126
|
+
* @param {number} flags
|
|
127
|
+
* @param {DataView} pOutFlags
|
|
128
128
|
* @returns {Promise<number>}
|
|
129
129
|
*/
|
|
130
130
|
async jOpen(zName, fileId, flags, pOutFlags) {
|
|
131
|
-
/** @type {(() => void)[]} */ const onFinally = []
|
|
131
|
+
/** @type {(() => void)[]} */ const onFinally = []
|
|
132
132
|
try {
|
|
133
|
-
const url = new URL(zName || Math.random().toString(36).slice(2), 'file://')
|
|
134
|
-
const path = url.pathname
|
|
133
|
+
const url = new URL(zName || Math.random().toString(36).slice(2), 'file://')
|
|
134
|
+
const path = url.pathname
|
|
135
135
|
|
|
136
|
-
const file = await File.create(path, flags)
|
|
136
|
+
const file = await File.create(path, flags)
|
|
137
137
|
if (flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
138
|
-
file.pageSize = 0
|
|
139
|
-
file.fileSize = 0
|
|
140
|
-
file.viewTx = { txId: 0 }
|
|
141
|
-
file.broadcastChannel = new BroadcastChannel(`permuted:${path}`)
|
|
142
|
-
file.broadcastReceived = []
|
|
143
|
-
file.mapPageToOffset = new Map()
|
|
144
|
-
file.mapTxToPending = new Map()
|
|
145
|
-
file.freeOffsets = new Set()
|
|
146
|
-
file.lockState = VFS.SQLITE_LOCK_NONE
|
|
147
|
-
file.locks = {}
|
|
148
|
-
file.abortController = new AbortController()
|
|
149
|
-
file.txIsOverwrite = false
|
|
150
|
-
file.txActive = null
|
|
151
|
-
file.synchronous = 'full'
|
|
152
|
-
file.flushInterval = DEFAULT_FLUSH_INTERVAL
|
|
138
|
+
file.pageSize = 0
|
|
139
|
+
file.fileSize = 0
|
|
140
|
+
file.viewTx = { txId: 0 }
|
|
141
|
+
file.broadcastChannel = new BroadcastChannel(`permuted:${path}`)
|
|
142
|
+
file.broadcastReceived = []
|
|
143
|
+
file.mapPageToOffset = new Map()
|
|
144
|
+
file.mapTxToPending = new Map()
|
|
145
|
+
file.freeOffsets = new Set()
|
|
146
|
+
file.lockState = VFS.SQLITE_LOCK_NONE
|
|
147
|
+
file.locks = {}
|
|
148
|
+
file.abortController = new AbortController()
|
|
149
|
+
file.txIsOverwrite = false
|
|
150
|
+
file.txActive = null
|
|
151
|
+
file.synchronous = 'full'
|
|
152
|
+
file.flushInterval = DEFAULT_FLUSH_INTERVAL
|
|
153
153
|
|
|
154
154
|
// Take the write lock so no other connection changes state
|
|
155
155
|
// during our initialization.
|
|
156
|
-
await this.#lock(file, 'write')
|
|
157
|
-
onFinally.push(() => file.locks.write())
|
|
156
|
+
await this.#lock(file, 'write')
|
|
157
|
+
onFinally.push(() => file.locks.write())
|
|
158
158
|
|
|
159
159
|
// Load the initial page map from the database.
|
|
160
|
-
const tx = file.idb.transaction(['pages', 'pending'])
|
|
161
|
-
const pages = await idbX(tx.objectStore('pages').getAll())
|
|
162
|
-
file.pageSize = this.#getPageSize(file)
|
|
163
|
-
file.fileSize = pages.length * file.pageSize
|
|
160
|
+
const tx = file.idb.transaction(['pages', 'pending'])
|
|
161
|
+
const pages = await idbX(tx.objectStore('pages').getAll())
|
|
162
|
+
file.pageSize = this.#getPageSize(file)
|
|
163
|
+
file.fileSize = pages.length * file.pageSize
|
|
164
164
|
|
|
165
165
|
// Begin with adding all file offsets to the free list.
|
|
166
|
-
const opfsFileSize = file.accessHandle.getSize()
|
|
166
|
+
const opfsFileSize = file.accessHandle.getSize()
|
|
167
167
|
for (let i = 0; i < opfsFileSize; i += file.pageSize) {
|
|
168
|
-
file.freeOffsets.add(i)
|
|
168
|
+
file.freeOffsets.add(i)
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// Incorporate the page map data.
|
|
172
172
|
for (const { i, o } of pages) {
|
|
173
|
-
file.mapPageToOffset.set(i, o)
|
|
174
|
-
file.freeOffsets.delete(o)
|
|
173
|
+
file.mapPageToOffset.set(i, o)
|
|
174
|
+
file.freeOffsets.delete(o)
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
// Incorporate pending transactions.
|
|
178
178
|
try {
|
|
179
179
|
/** @type {Transaction[]} */
|
|
180
|
-
const transactions = await idbX(tx.objectStore('pending').getAll())
|
|
180
|
+
const transactions = await idbX(tx.objectStore('pending').getAll())
|
|
181
181
|
for (const transaction of transactions) {
|
|
182
182
|
// Verify checksums for all pages in this transaction.
|
|
183
183
|
for (const [index, { offset, digest }] of transaction.pages) {
|
|
184
|
-
const data = new Uint8Array(file.pageSize)
|
|
185
|
-
file.accessHandle.read(data, { at: offset })
|
|
184
|
+
const data = new Uint8Array(file.pageSize)
|
|
185
|
+
file.accessHandle.read(data, { at: offset })
|
|
186
186
|
if (checksum(data).some((v, i) => v !== digest[i])) {
|
|
187
|
-
throw Object.assign(new Error('checksum error'), { txId: transaction.txId })
|
|
187
|
+
throw Object.assign(new Error('checksum error'), { txId: transaction.txId })
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
this.#acceptTx(file, transaction)
|
|
191
|
-
file.viewTx = transaction
|
|
190
|
+
this.#acceptTx(file, transaction)
|
|
191
|
+
file.viewTx = transaction
|
|
192
192
|
}
|
|
193
193
|
} catch (e) {
|
|
194
194
|
if (e.message === 'checksum error') {
|
|
195
195
|
console.warn(`Checksum error, removing tx ${e.txId}+`)
|
|
196
|
-
const tx = file.idb.transaction('pending', 'readwrite')
|
|
196
|
+
const tx = file.idb.transaction('pending', 'readwrite')
|
|
197
197
|
const txCommit = new Promise((resolve, reject) => {
|
|
198
|
-
tx.oncomplete = resolve
|
|
199
|
-
tx.onabort = () => reject(tx.error)
|
|
200
|
-
})
|
|
201
|
-
const range = IDBKeyRange.lowerBound(e.txId)
|
|
202
|
-
tx.objectStore('pending').delete(range)
|
|
203
|
-
tx.commit()
|
|
204
|
-
await txCommit
|
|
198
|
+
tx.oncomplete = resolve
|
|
199
|
+
tx.onabort = () => reject(tx.error)
|
|
200
|
+
})
|
|
201
|
+
const range = IDBKeyRange.lowerBound(e.txId)
|
|
202
|
+
tx.objectStore('pending').delete(range)
|
|
203
|
+
tx.commit()
|
|
204
|
+
await txCommit
|
|
205
205
|
} else {
|
|
206
|
-
throw e
|
|
206
|
+
throw e
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
// Publish our view of the database. This prevents other connections
|
|
211
211
|
// from overwriting file data we still need.
|
|
212
|
-
await this.#setView(file, file.viewTx)
|
|
212
|
+
await this.#setView(file, file.viewTx)
|
|
213
213
|
|
|
214
214
|
// Listen for broadcasts. Messages are cached until the database
|
|
215
215
|
// is unlocked.
|
|
216
|
-
file.broadcastChannel.addEventListener('message', event => {
|
|
217
|
-
file.broadcastReceived.push(event.data)
|
|
216
|
+
file.broadcastChannel.addEventListener('message', (event) => {
|
|
217
|
+
file.broadcastReceived.push(event.data)
|
|
218
218
|
if (file.lockState === VFS.SQLITE_LOCK_NONE) {
|
|
219
|
-
this.#processBroadcasts(file)
|
|
219
|
+
this.#processBroadcasts(file)
|
|
220
220
|
}
|
|
221
|
-
})
|
|
221
|
+
})
|
|
222
222
|
|
|
223
223
|
// Connections usually hold this shared read lock so they don't
|
|
224
224
|
// acquire and release it for every transaction. The only time
|
|
@@ -227,354 +227,352 @@ export class OPFSPermutedVFS extends FacadeVFS {
|
|
|
227
227
|
await this.#lock(file, 'read', SHARED)
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
pOutFlags.setInt32(0, flags, true)
|
|
231
|
-
this.#mapIdToFile.set(fileId, file)
|
|
232
|
-
return VFS.SQLITE_OK
|
|
230
|
+
pOutFlags.setInt32(0, flags, true)
|
|
231
|
+
this.#mapIdToFile.set(fileId, file)
|
|
232
|
+
return VFS.SQLITE_OK
|
|
233
233
|
} catch (e) {
|
|
234
|
-
this.#lastError = e
|
|
235
|
-
return VFS.SQLITE_CANTOPEN
|
|
234
|
+
this.#lastError = e
|
|
235
|
+
return VFS.SQLITE_CANTOPEN
|
|
236
236
|
} finally {
|
|
237
237
|
while (onFinally.length) {
|
|
238
|
-
await onFinally.pop()()
|
|
238
|
+
await onFinally.pop()()
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
/**
|
|
244
|
-
* @param {string} zName
|
|
245
|
-
* @param {number} syncDir
|
|
244
|
+
* @param {string} zName
|
|
245
|
+
* @param {number} syncDir
|
|
246
246
|
* @returns {Promise<number>}
|
|
247
247
|
*/
|
|
248
248
|
async jDelete(zName, syncDir) {
|
|
249
249
|
try {
|
|
250
|
-
const url = new URL(zName, 'file://')
|
|
251
|
-
const pathname = url.pathname
|
|
252
|
-
|
|
253
|
-
const [directoryHandle, name] = await getPathComponents(pathname, false)
|
|
254
|
-
const result = directoryHandle.removeEntry(name, { recursive: false })
|
|
250
|
+
const url = new URL(zName, 'file://')
|
|
251
|
+
const pathname = url.pathname
|
|
252
|
+
|
|
253
|
+
const [directoryHandle, name] = await getPathComponents(pathname, false)
|
|
254
|
+
const result = directoryHandle.removeEntry(name, { recursive: false })
|
|
255
255
|
if (syncDir) {
|
|
256
|
-
await result
|
|
256
|
+
await result
|
|
257
257
|
}
|
|
258
|
-
return VFS.SQLITE_OK
|
|
258
|
+
return VFS.SQLITE_OK
|
|
259
259
|
} catch (e) {
|
|
260
|
-
return VFS.SQLITE_IOERR_DELETE
|
|
260
|
+
return VFS.SQLITE_IOERR_DELETE
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
/**
|
|
265
|
-
* @param {string} zName
|
|
266
|
-
* @param {number} flags
|
|
267
|
-
* @param {DataView} pResOut
|
|
265
|
+
* @param {string} zName
|
|
266
|
+
* @param {number} flags
|
|
267
|
+
* @param {DataView} pResOut
|
|
268
268
|
* @returns {Promise<number>}
|
|
269
269
|
*/
|
|
270
270
|
async jAccess(zName, flags, pResOut) {
|
|
271
271
|
try {
|
|
272
|
-
const url = new URL(zName, 'file://')
|
|
273
|
-
const pathname = url.pathname
|
|
272
|
+
const url = new URL(zName, 'file://')
|
|
273
|
+
const pathname = url.pathname
|
|
274
274
|
|
|
275
|
-
const [directoryHandle, dbName] = await getPathComponents(pathname, false)
|
|
276
|
-
await directoryHandle.getFileHandle(dbName, { create: false })
|
|
277
|
-
pResOut.setInt32(0, 1, true)
|
|
278
|
-
return VFS.SQLITE_OK
|
|
275
|
+
const [directoryHandle, dbName] = await getPathComponents(pathname, false)
|
|
276
|
+
await directoryHandle.getFileHandle(dbName, { create: false })
|
|
277
|
+
pResOut.setInt32(0, 1, true)
|
|
278
|
+
return VFS.SQLITE_OK
|
|
279
279
|
} catch (e) {
|
|
280
280
|
if (e.name === 'NotFoundError') {
|
|
281
|
-
pResOut.setInt32(0, 0, true)
|
|
282
|
-
return VFS.SQLITE_OK
|
|
281
|
+
pResOut.setInt32(0, 0, true)
|
|
282
|
+
return VFS.SQLITE_OK
|
|
283
283
|
}
|
|
284
|
-
this.#lastError = e
|
|
285
|
-
return VFS.SQLITE_IOERR_ACCESS
|
|
284
|
+
this.#lastError = e
|
|
285
|
+
return VFS.SQLITE_IOERR_ACCESS
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
/**
|
|
290
|
-
* @param {number} fileId
|
|
290
|
+
* @param {number} fileId
|
|
291
291
|
* @returns {Promise<number>}
|
|
292
292
|
*/
|
|
293
293
|
async jClose(fileId) {
|
|
294
294
|
try {
|
|
295
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
296
|
-
this.#mapIdToFile.delete(fileId)
|
|
297
|
-
file?.accessHandle?.close()
|
|
295
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
296
|
+
this.#mapIdToFile.delete(fileId)
|
|
297
|
+
file?.accessHandle?.close()
|
|
298
298
|
|
|
299
299
|
if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
300
|
-
file.broadcastChannel.close()
|
|
301
|
-
file.viewReleaser?.()
|
|
300
|
+
file.broadcastChannel.close()
|
|
301
|
+
file.viewReleaser?.()
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
|
305
|
-
const [directoryHandle, name] = await getPathComponents(file.path, false)
|
|
306
|
-
await directoryHandle.removeEntry(name, { recursive: false })
|
|
305
|
+
const [directoryHandle, name] = await getPathComponents(file.path, false)
|
|
306
|
+
await directoryHandle.removeEntry(name, { recursive: false })
|
|
307
307
|
}
|
|
308
|
-
return VFS.SQLITE_OK
|
|
308
|
+
return VFS.SQLITE_OK
|
|
309
309
|
} catch (e) {
|
|
310
|
-
return VFS.SQLITE_IOERR_CLOSE
|
|
310
|
+
return VFS.SQLITE_IOERR_CLOSE
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/**
|
|
315
|
-
* @param {number} fileId
|
|
316
|
-
* @param {Uint8Array} pData
|
|
315
|
+
* @param {number} fileId
|
|
316
|
+
* @param {Uint8Array} pData
|
|
317
317
|
* @param {number} iOffset
|
|
318
318
|
* @returns {number}
|
|
319
319
|
*/
|
|
320
320
|
jRead(fileId, pData, iOffset) {
|
|
321
321
|
try {
|
|
322
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
322
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
323
323
|
|
|
324
|
-
let bytesRead = 0
|
|
324
|
+
let bytesRead = 0
|
|
325
325
|
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
326
|
-
file.abortController.signal.throwIfAborted()
|
|
326
|
+
file.abortController.signal.throwIfAborted()
|
|
327
327
|
|
|
328
328
|
// Look up the page location in the file. Check the pages in
|
|
329
329
|
// any active write transaction first, then the main map.
|
|
330
|
-
const pageIndex = file.pageSize ?
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
file.txActive.pages.get(pageIndex).offset :
|
|
335
|
-
file.mapPageToOffset.get(pageIndex);
|
|
330
|
+
const pageIndex = file.pageSize ? Math.trunc(iOffset / file.pageSize) + 1 : 1
|
|
331
|
+
const pageOffset = file.txActive?.pages.has(pageIndex)
|
|
332
|
+
? file.txActive.pages.get(pageIndex).offset
|
|
333
|
+
: file.mapPageToOffset.get(pageIndex)
|
|
336
334
|
if (pageOffset >= 0) {
|
|
337
|
-
this.log?.(`read page ${pageIndex} at ${pageOffset}`)
|
|
338
|
-
bytesRead = file.accessHandle.read(
|
|
339
|
-
|
|
340
|
-
|
|
335
|
+
this.log?.(`read page ${pageIndex} at ${pageOffset}`)
|
|
336
|
+
bytesRead = file.accessHandle.read(pData.subarray(), {
|
|
337
|
+
at: pageOffset + (file.pageSize ? iOffset % file.pageSize : 0),
|
|
338
|
+
})
|
|
341
339
|
}
|
|
342
340
|
|
|
343
341
|
// Get page size if not already known.
|
|
344
342
|
if (!file.pageSize && iOffset <= 16 && iOffset + bytesRead >= 18) {
|
|
345
|
-
const dataView = new DataView(pData.slice(16 - iOffset, 18 - iOffset).buffer)
|
|
346
|
-
file.pageSize = dataView.getUint16(0)
|
|
343
|
+
const dataView = new DataView(pData.slice(16 - iOffset, 18 - iOffset).buffer)
|
|
344
|
+
file.pageSize = dataView.getUint16(0)
|
|
347
345
|
if (file.pageSize === 1) {
|
|
348
|
-
file.pageSize = 65536
|
|
346
|
+
file.pageSize = 65536
|
|
349
347
|
}
|
|
350
|
-
this.log?.(`set page size ${file.pageSize}`)
|
|
348
|
+
this.log?.(`set page size ${file.pageSize}`)
|
|
351
349
|
}
|
|
352
350
|
} else {
|
|
353
351
|
// On Chrome (at least), passing pData to accessHandle.read() is
|
|
354
352
|
// an error because pData is a Proxy of a Uint8Array. Calling
|
|
355
353
|
// subarray() produces a real Uint8Array and that works.
|
|
356
|
-
bytesRead = file.accessHandle.read(pData.subarray(), { at: iOffset })
|
|
354
|
+
bytesRead = file.accessHandle.read(pData.subarray(), { at: iOffset })
|
|
357
355
|
}
|
|
358
356
|
|
|
359
357
|
if (bytesRead < pData.byteLength) {
|
|
360
|
-
pData.fill(0, bytesRead)
|
|
361
|
-
return VFS.SQLITE_IOERR_SHORT_READ
|
|
358
|
+
pData.fill(0, bytesRead)
|
|
359
|
+
return VFS.SQLITE_IOERR_SHORT_READ
|
|
362
360
|
}
|
|
363
|
-
return VFS.SQLITE_OK
|
|
361
|
+
return VFS.SQLITE_OK
|
|
364
362
|
} catch (e) {
|
|
365
|
-
this.#lastError = e
|
|
366
|
-
return VFS.SQLITE_IOERR_READ
|
|
363
|
+
this.#lastError = e
|
|
364
|
+
return VFS.SQLITE_IOERR_READ
|
|
367
365
|
}
|
|
368
366
|
}
|
|
369
367
|
|
|
370
368
|
/**
|
|
371
|
-
* @param {number} fileId
|
|
372
|
-
* @param {Uint8Array} pData
|
|
369
|
+
* @param {number} fileId
|
|
370
|
+
* @param {Uint8Array} pData
|
|
373
371
|
* @param {number} iOffset
|
|
374
372
|
* @returns {number}
|
|
375
373
|
*/
|
|
376
374
|
jWrite(fileId, pData, iOffset) {
|
|
377
375
|
try {
|
|
378
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
376
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
379
377
|
|
|
380
378
|
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
381
|
-
file.abortController.signal.throwIfAborted()
|
|
379
|
+
file.abortController.signal.throwIfAborted()
|
|
382
380
|
if (!file.pageSize) {
|
|
383
381
|
this.log?.(`set page size ${pData.byteLength}`)
|
|
384
|
-
file.pageSize = pData.byteLength
|
|
382
|
+
file.pageSize = pData.byteLength
|
|
385
383
|
}
|
|
386
384
|
|
|
387
385
|
// The first write begins a transaction. Note that xLock/xUnlock
|
|
388
386
|
// is not a good way to determine transaction boundaries because
|
|
389
387
|
// PRAGMA locking_mode can change the behavior.
|
|
390
388
|
if (!file.txActive) {
|
|
391
|
-
this.#beginTx(file)
|
|
389
|
+
this.#beginTx(file)
|
|
392
390
|
}
|
|
393
391
|
|
|
394
392
|
// Choose the offset in the file to write this page.
|
|
395
|
-
let pageOffset
|
|
396
|
-
const pageIndex = Math.trunc(iOffset / file.pageSize) + 1
|
|
393
|
+
let pageOffset
|
|
394
|
+
const pageIndex = Math.trunc(iOffset / file.pageSize) + 1
|
|
397
395
|
if (file.txIsOverwrite) {
|
|
398
396
|
// For VACUUM, use the identity mapping to write each page
|
|
399
397
|
// at its canonical offset.
|
|
400
|
-
pageOffset = iOffset
|
|
398
|
+
pageOffset = iOffset
|
|
401
399
|
} else if (file.txActive.pages.has(pageIndex)) {
|
|
402
400
|
// This page has already been written in this transaction.
|
|
403
401
|
// Use the same offset.
|
|
404
|
-
pageOffset = file.txActive.pages.get(pageIndex).offset
|
|
405
|
-
this.log?.(`overwrite page ${pageIndex} at ${pageOffset}`)
|
|
402
|
+
pageOffset = file.txActive.pages.get(pageIndex).offset
|
|
403
|
+
this.log?.(`overwrite page ${pageIndex} at ${pageOffset}`)
|
|
406
404
|
} else if (pageIndex === 1 && file.freeOffsets.delete(0)) {
|
|
407
405
|
// Offset 0 is available for page 1.
|
|
408
|
-
pageOffset = 0
|
|
409
|
-
this.log?.(`write page ${pageIndex} at ${pageOffset}`)
|
|
406
|
+
pageOffset = 0
|
|
407
|
+
this.log?.(`write page ${pageIndex} at ${pageOffset}`)
|
|
410
408
|
} else {
|
|
411
409
|
// Use the first unused non-zero offset within the file.
|
|
412
410
|
for (const maybeOffset of file.freeOffsets) {
|
|
413
411
|
if (maybeOffset) {
|
|
414
412
|
if (maybeOffset < file.txRealFileSize) {
|
|
415
|
-
pageOffset = maybeOffset
|
|
416
|
-
file.freeOffsets.delete(pageOffset)
|
|
417
|
-
this.log?.(`write page ${pageIndex} at ${pageOffset}`)
|
|
418
|
-
break
|
|
413
|
+
pageOffset = maybeOffset
|
|
414
|
+
file.freeOffsets.delete(pageOffset)
|
|
415
|
+
this.log?.(`write page ${pageIndex} at ${pageOffset}`)
|
|
416
|
+
break
|
|
419
417
|
} else {
|
|
420
418
|
// This offset is beyond the end of the file.
|
|
421
|
-
file.freeOffsets.delete(maybeOffset)
|
|
419
|
+
file.freeOffsets.delete(maybeOffset)
|
|
422
420
|
}
|
|
423
421
|
}
|
|
424
422
|
}
|
|
425
423
|
|
|
426
424
|
if (pageOffset === undefined) {
|
|
427
425
|
// Write to the end of the file.
|
|
428
|
-
pageOffset = file.txRealFileSize
|
|
429
|
-
this.log?.(`append page ${pageIndex} at ${pageOffset}`)
|
|
426
|
+
pageOffset = file.txRealFileSize
|
|
427
|
+
this.log?.(`append page ${pageIndex} at ${pageOffset}`)
|
|
430
428
|
}
|
|
431
429
|
}
|
|
432
|
-
file.accessHandle.write(pData.subarray(), { at: pageOffset })
|
|
430
|
+
file.accessHandle.write(pData.subarray(), { at: pageOffset })
|
|
433
431
|
|
|
434
432
|
// Update the transaction.
|
|
435
433
|
file.txActive.pages.set(pageIndex, {
|
|
436
434
|
offset: pageOffset,
|
|
437
|
-
digest: checksum(pData.subarray())
|
|
438
|
-
})
|
|
439
|
-
file.txActive.fileSize = Math.max(file.txActive.fileSize, pageIndex * file.pageSize)
|
|
435
|
+
digest: checksum(pData.subarray()),
|
|
436
|
+
})
|
|
437
|
+
file.txActive.fileSize = Math.max(file.txActive.fileSize, pageIndex * file.pageSize)
|
|
440
438
|
|
|
441
439
|
// Track the actual file size.
|
|
442
|
-
file.txRealFileSize = Math.max(file.txRealFileSize, pageOffset + pData.byteLength)
|
|
440
|
+
file.txRealFileSize = Math.max(file.txRealFileSize, pageOffset + pData.byteLength)
|
|
443
441
|
} else {
|
|
444
442
|
// On Chrome (at least), passing pData to accessHandle.write() is
|
|
445
443
|
// an error because pData is a Proxy of a Uint8Array. Calling
|
|
446
444
|
// subarray() produces a real Uint8Array and that works.
|
|
447
|
-
file.accessHandle.write(pData.subarray(), { at: iOffset })
|
|
445
|
+
file.accessHandle.write(pData.subarray(), { at: iOffset })
|
|
448
446
|
}
|
|
449
|
-
return VFS.SQLITE_OK
|
|
447
|
+
return VFS.SQLITE_OK
|
|
450
448
|
} catch (e) {
|
|
451
|
-
this.#lastError = e
|
|
452
|
-
return VFS.SQLITE_IOERR_WRITE
|
|
449
|
+
this.#lastError = e
|
|
450
|
+
return VFS.SQLITE_IOERR_WRITE
|
|
453
451
|
}
|
|
454
452
|
}
|
|
455
453
|
|
|
456
454
|
/**
|
|
457
|
-
* @param {number} fileId
|
|
458
|
-
* @param {number} iSize
|
|
455
|
+
* @param {number} fileId
|
|
456
|
+
* @param {number} iSize
|
|
459
457
|
* @returns {number}
|
|
460
458
|
*/
|
|
461
459
|
jTruncate(fileId, iSize) {
|
|
462
460
|
try {
|
|
463
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
464
|
-
if (
|
|
465
|
-
file.abortController.signal.throwIfAborted()
|
|
461
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
462
|
+
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB && !file.txIsOverwrite) {
|
|
463
|
+
file.abortController.signal.throwIfAborted()
|
|
466
464
|
if (!file.txActive) {
|
|
467
|
-
this.#beginTx(file)
|
|
465
|
+
this.#beginTx(file)
|
|
468
466
|
}
|
|
469
|
-
file.txActive.fileSize = iSize
|
|
467
|
+
file.txActive.fileSize = iSize
|
|
470
468
|
|
|
471
469
|
// Remove now obsolete pages from file.txActive.pages
|
|
472
470
|
for (const [index, { offset }] of file.txActive.pages) {
|
|
473
471
|
// Page indices are 1-based.
|
|
474
472
|
if (index * file.pageSize > iSize) {
|
|
475
|
-
file.txActive.pages.delete(index)
|
|
476
|
-
file.freeOffsets.add(offset)
|
|
473
|
+
file.txActive.pages.delete(index)
|
|
474
|
+
file.freeOffsets.add(offset)
|
|
477
475
|
}
|
|
478
476
|
}
|
|
479
|
-
return VFS.SQLITE_OK
|
|
477
|
+
return VFS.SQLITE_OK
|
|
480
478
|
}
|
|
481
|
-
file.accessHandle.truncate(iSize)
|
|
482
|
-
return VFS.SQLITE_OK
|
|
479
|
+
file.accessHandle.truncate(iSize)
|
|
480
|
+
return VFS.SQLITE_OK
|
|
483
481
|
} catch (e) {
|
|
484
|
-
console.error(e)
|
|
485
|
-
this.lastError = e
|
|
486
|
-
return VFS.SQLITE_IOERR_TRUNCATE
|
|
482
|
+
console.error(e)
|
|
483
|
+
this.lastError = e
|
|
484
|
+
return VFS.SQLITE_IOERR_TRUNCATE
|
|
487
485
|
}
|
|
488
486
|
}
|
|
489
487
|
|
|
490
488
|
/**
|
|
491
|
-
* @param {number} fileId
|
|
492
|
-
* @param {number} flags
|
|
489
|
+
* @param {number} fileId
|
|
490
|
+
* @param {number} flags
|
|
493
491
|
* @returns {number}
|
|
494
492
|
*/
|
|
495
493
|
jSync(fileId, flags) {
|
|
496
494
|
try {
|
|
497
495
|
// Main DB sync is handled by SQLITE_FCNTL_SYNC.
|
|
498
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
496
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
499
497
|
if (!(file.flags & VFS.SQLITE_OPEN_MAIN_DB)) {
|
|
500
|
-
file.accessHandle.flush()
|
|
498
|
+
file.accessHandle.flush()
|
|
501
499
|
}
|
|
502
|
-
return VFS.SQLITE_OK
|
|
500
|
+
return VFS.SQLITE_OK
|
|
503
501
|
} catch (e) {
|
|
504
|
-
this.#lastError = e
|
|
505
|
-
return VFS.SQLITE_IOERR_FSYNC
|
|
502
|
+
this.#lastError = e
|
|
503
|
+
return VFS.SQLITE_IOERR_FSYNC
|
|
506
504
|
}
|
|
507
505
|
}
|
|
508
506
|
|
|
509
507
|
/**
|
|
510
|
-
* @param {number} fileId
|
|
511
|
-
* @param {DataView} pSize64
|
|
508
|
+
* @param {number} fileId
|
|
509
|
+
* @param {DataView} pSize64
|
|
512
510
|
* @returns {number}
|
|
513
511
|
*/
|
|
514
512
|
jFileSize(fileId, pSize64) {
|
|
515
513
|
try {
|
|
516
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
514
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
517
515
|
|
|
518
|
-
let size
|
|
516
|
+
let size
|
|
519
517
|
if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
|
|
520
|
-
file.abortController.signal.throwIfAborted()
|
|
521
|
-
size = file.txActive?.fileSize ?? file.fileSize
|
|
518
|
+
file.abortController.signal.throwIfAborted()
|
|
519
|
+
size = file.txActive?.fileSize ?? file.fileSize
|
|
522
520
|
} else {
|
|
523
|
-
size = file.accessHandle.getSize()
|
|
521
|
+
size = file.accessHandle.getSize()
|
|
524
522
|
}
|
|
525
523
|
|
|
526
|
-
pSize64.setBigInt64(0, BigInt(size), true)
|
|
527
|
-
return VFS.SQLITE_OK
|
|
524
|
+
pSize64.setBigInt64(0, BigInt(size), true)
|
|
525
|
+
return VFS.SQLITE_OK
|
|
528
526
|
} catch (e) {
|
|
529
|
-
this.#lastError = e
|
|
530
|
-
return VFS.SQLITE_IOERR_FSTAT
|
|
527
|
+
this.#lastError = e
|
|
528
|
+
return VFS.SQLITE_IOERR_FSTAT
|
|
531
529
|
}
|
|
532
530
|
}
|
|
533
531
|
|
|
534
532
|
/**
|
|
535
|
-
* @param {number} fileId
|
|
536
|
-
* @param {number} lockType
|
|
533
|
+
* @param {number} fileId
|
|
534
|
+
* @param {number} lockType
|
|
537
535
|
* @returns {Promise<number>}
|
|
538
536
|
*/
|
|
539
537
|
async jLock(fileId, lockType) {
|
|
540
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
541
|
-
if (lockType <= file.lockState) return VFS.SQLITE_OK
|
|
538
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
539
|
+
if (lockType <= file.lockState) return VFS.SQLITE_OK
|
|
542
540
|
switch (lockType) {
|
|
543
541
|
case VFS.SQLITE_LOCK_SHARED:
|
|
544
542
|
if (file.txWriteHint) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
543
|
+
// xFileControl() has hinted that this transaction will
|
|
544
|
+
// write. Acquire the hint lock, which is required to reach
|
|
545
|
+
// the RESERVED state.
|
|
546
|
+
if (!(await this.#lock(file, 'hint'))) {
|
|
547
|
+
return VFS.SQLITE_BUSY
|
|
548
|
+
}
|
|
551
549
|
}
|
|
552
550
|
|
|
553
551
|
if (!file.locks.read) {
|
|
554
552
|
// Reacquire lock if it was released by a broadcast request.
|
|
555
|
-
await this.#lock(file, 'read', SHARED)
|
|
553
|
+
await this.#lock(file, 'read', SHARED)
|
|
556
554
|
}
|
|
557
|
-
break
|
|
555
|
+
break
|
|
558
556
|
case VFS.SQLITE_LOCK_RESERVED:
|
|
559
557
|
// Ideally we should already have the hint lock, but if not
|
|
560
558
|
// poll for it here.
|
|
561
|
-
if (!file.locks.hint && !await this.#lock(file, 'hint', POLL_EXCLUSIVE)) {
|
|
562
|
-
return VFS.SQLITE_BUSY
|
|
559
|
+
if (!file.locks.hint && !(await this.#lock(file, 'hint', POLL_EXCLUSIVE))) {
|
|
560
|
+
return VFS.SQLITE_BUSY
|
|
563
561
|
}
|
|
564
562
|
|
|
565
|
-
if (!await this.#lock(file, 'reserved', POLL_EXCLUSIVE)) {
|
|
566
|
-
file.locks.hint()
|
|
567
|
-
return VFS.SQLITE_BUSY
|
|
563
|
+
if (!(await this.#lock(file, 'reserved', POLL_EXCLUSIVE))) {
|
|
564
|
+
file.locks.hint()
|
|
565
|
+
return VFS.SQLITE_BUSY
|
|
568
566
|
}
|
|
569
567
|
|
|
570
568
|
// In order to write, our view of the database must be up to date.
|
|
571
569
|
// To check this, first fetch all transactions in IndexedDB equal to
|
|
572
570
|
// or greater than our view.
|
|
573
|
-
const tx = file.idb.transaction(['pending'])
|
|
574
|
-
const range = IDBKeyRange.lowerBound(file.viewTx.txId)
|
|
571
|
+
const tx = file.idb.transaction(['pending'])
|
|
572
|
+
const range = IDBKeyRange.lowerBound(file.viewTx.txId)
|
|
575
573
|
|
|
576
574
|
/** @type {Transaction[]} */
|
|
577
|
-
const entries = await idbX(tx.objectStore('pending').getAll(range))
|
|
575
|
+
const entries = await idbX(tx.objectStore('pending').getAll(range))
|
|
578
576
|
|
|
579
577
|
// Ideally the fetched list of transactions should contain one
|
|
580
578
|
// entry matching our view. If not then our view is out of date.
|
|
@@ -582,68 +580,68 @@ export class OPFSPermutedVFS extends FacadeVFS {
|
|
|
582
580
|
// There are newer transactions in IndexedDB that we haven't
|
|
583
581
|
// seen via broadcast. Ensure that they are incorporated on unlock,
|
|
584
582
|
// and force the application to retry.
|
|
585
|
-
file.broadcastReceived.push(...entries)
|
|
586
|
-
file.locks.reserved()
|
|
583
|
+
file.broadcastReceived.push(...entries)
|
|
584
|
+
file.locks.reserved()
|
|
587
585
|
return VFS.SQLITE_BUSY
|
|
588
586
|
}
|
|
589
|
-
break
|
|
587
|
+
break
|
|
590
588
|
case VFS.SQLITE_LOCK_EXCLUSIVE:
|
|
591
|
-
await this.#lock(file, 'write')
|
|
592
|
-
break
|
|
589
|
+
await this.#lock(file, 'write')
|
|
590
|
+
break
|
|
593
591
|
}
|
|
594
|
-
file.lockState = lockType
|
|
595
|
-
return VFS.SQLITE_OK
|
|
592
|
+
file.lockState = lockType
|
|
593
|
+
return VFS.SQLITE_OK
|
|
596
594
|
}
|
|
597
595
|
|
|
598
596
|
/**
|
|
599
|
-
* @param {number} fileId
|
|
600
|
-
* @param {number} lockType
|
|
597
|
+
* @param {number} fileId
|
|
598
|
+
* @param {number} lockType
|
|
601
599
|
* @returns {number}
|
|
602
600
|
*/
|
|
603
601
|
jUnlock(fileId, lockType) {
|
|
604
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
605
|
-
if (lockType >= file.lockState) return VFS.SQLITE_OK
|
|
602
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
603
|
+
if (lockType >= file.lockState) return VFS.SQLITE_OK
|
|
606
604
|
switch (lockType) {
|
|
607
605
|
case VFS.SQLITE_LOCK_SHARED:
|
|
608
|
-
file.locks.write?.()
|
|
609
|
-
file.locks.reserved?.()
|
|
610
|
-
file.locks.hint?.()
|
|
611
|
-
break
|
|
606
|
+
file.locks.write?.()
|
|
607
|
+
file.locks.reserved?.()
|
|
608
|
+
file.locks.hint?.()
|
|
609
|
+
break
|
|
612
610
|
case VFS.SQLITE_LOCK_NONE:
|
|
613
611
|
// Don't release the read lock here. It will be released on demand
|
|
614
612
|
// when a broadcast notifies us that another connections wants to
|
|
615
613
|
// VACUUM.
|
|
616
|
-
this.#processBroadcasts(file)
|
|
617
|
-
file.locks.write?.()
|
|
618
|
-
file.locks.reserved?.()
|
|
619
|
-
file.locks.hint?.()
|
|
620
|
-
break
|
|
614
|
+
this.#processBroadcasts(file)
|
|
615
|
+
file.locks.write?.()
|
|
616
|
+
file.locks.reserved?.()
|
|
617
|
+
file.locks.hint?.()
|
|
618
|
+
break
|
|
621
619
|
}
|
|
622
|
-
file.lockState = lockType
|
|
623
|
-
return VFS.SQLITE_OK
|
|
620
|
+
file.lockState = lockType
|
|
621
|
+
return VFS.SQLITE_OK
|
|
624
622
|
}
|
|
625
623
|
|
|
626
624
|
/**
|
|
627
625
|
* @param {number} fileId
|
|
628
|
-
* @param {DataView} pResOut
|
|
626
|
+
* @param {DataView} pResOut
|
|
629
627
|
* @returns {Promise<number>}
|
|
630
628
|
*/
|
|
631
629
|
async jCheckReservedLock(fileId, pResOut) {
|
|
632
630
|
try {
|
|
633
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
631
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
634
632
|
if (await this.#lock(file, 'reserved', POLL_SHARED)) {
|
|
635
633
|
// This looks backwards, but if we get the lock then no one
|
|
636
634
|
// else had it.
|
|
637
|
-
pResOut.setInt32(0, 0, true)
|
|
638
|
-
file.locks.reserved()
|
|
635
|
+
pResOut.setInt32(0, 0, true)
|
|
636
|
+
file.locks.reserved()
|
|
639
637
|
} else {
|
|
640
|
-
pResOut.setInt32(0, 1, true)
|
|
638
|
+
pResOut.setInt32(0, 1, true)
|
|
641
639
|
}
|
|
642
|
-
return VFS.SQLITE_OK
|
|
640
|
+
return VFS.SQLITE_OK
|
|
643
641
|
} catch (e) {
|
|
644
|
-
console.error(e)
|
|
645
|
-
this.lastError = e
|
|
646
|
-
return VFS.SQLITE_IOERR_LOCK
|
|
642
|
+
console.error(e)
|
|
643
|
+
this.lastError = e
|
|
644
|
+
return VFS.SQLITE_IOERR_LOCK
|
|
647
645
|
}
|
|
648
646
|
}
|
|
649
647
|
|
|
@@ -655,19 +653,19 @@ export class OPFSPermutedVFS extends FacadeVFS {
|
|
|
655
653
|
*/
|
|
656
654
|
async jFileControl(fileId, op, pArg) {
|
|
657
655
|
try {
|
|
658
|
-
const file = this.#mapIdToFile.get(fileId)
|
|
656
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
659
657
|
switch (op) {
|
|
660
658
|
case VFS.SQLITE_FCNTL_PRAGMA:
|
|
661
|
-
const key = cvtString(pArg, 4)
|
|
662
|
-
const value = cvtString(pArg, 8)
|
|
663
|
-
this.log?.('xFileControl', file.path, 'PRAGMA', key, value)
|
|
659
|
+
const key = cvtString(pArg, 4)
|
|
660
|
+
const value = cvtString(pArg, 8)
|
|
661
|
+
this.log?.('xFileControl', file.path, 'PRAGMA', key, value)
|
|
664
662
|
switch (key.toLowerCase()) {
|
|
665
663
|
case 'page_size':
|
|
666
664
|
// Don't allow changing the page size.
|
|
667
665
|
if (value && file.pageSize && Number(value) !== file.pageSize) {
|
|
668
|
-
return VFS.SQLITE_ERROR
|
|
666
|
+
return VFS.SQLITE_ERROR
|
|
669
667
|
}
|
|
670
|
-
break
|
|
668
|
+
break
|
|
671
669
|
case 'synchronous':
|
|
672
670
|
// This VFS only recognizes 'full' and not 'full'.
|
|
673
671
|
if (value) {
|
|
@@ -676,71 +674,69 @@ export class OPFSPermutedVFS extends FacadeVFS {
|
|
|
676
674
|
case '2':
|
|
677
675
|
case 'extra':
|
|
678
676
|
case '3':
|
|
679
|
-
file.synchronous = 'full'
|
|
680
|
-
break
|
|
677
|
+
file.synchronous = 'full'
|
|
678
|
+
break
|
|
681
679
|
default:
|
|
682
|
-
file.synchronous = 'normal'
|
|
683
|
-
break
|
|
680
|
+
file.synchronous = 'normal'
|
|
681
|
+
break
|
|
684
682
|
}
|
|
685
683
|
}
|
|
686
|
-
break
|
|
684
|
+
break
|
|
687
685
|
case 'flush_interval':
|
|
688
686
|
if (value) {
|
|
689
|
-
const interval = Number(value)
|
|
687
|
+
const interval = Number(value)
|
|
690
688
|
if (interval > 0) {
|
|
691
|
-
file.flushInterval = Number(value)
|
|
689
|
+
file.flushInterval = Number(value)
|
|
692
690
|
} else {
|
|
693
|
-
return VFS.SQLITE_ERROR
|
|
691
|
+
return VFS.SQLITE_ERROR
|
|
694
692
|
}
|
|
695
693
|
} else {
|
|
696
694
|
// Report current value.
|
|
697
|
-
const buffer = new TextEncoder().encode(file.flushInterval.toString())
|
|
698
|
-
const s = this._module._sqlite3_malloc64(buffer.byteLength + 1)
|
|
699
|
-
new Uint8Array(this._module.HEAPU8.buffer, s, buffer.byteLength + 1)
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
pArg.setUint32(0, s, true);
|
|
704
|
-
return VFS.SQLITE_OK;
|
|
695
|
+
const buffer = new TextEncoder().encode(file.flushInterval.toString())
|
|
696
|
+
const s = this._module._sqlite3_malloc64(buffer.byteLength + 1)
|
|
697
|
+
new Uint8Array(this._module.HEAPU8.buffer, s, buffer.byteLength + 1).fill(0).set(buffer)
|
|
698
|
+
|
|
699
|
+
pArg.setUint32(0, s, true)
|
|
700
|
+
return VFS.SQLITE_OK
|
|
705
701
|
}
|
|
706
|
-
break
|
|
702
|
+
break
|
|
707
703
|
case 'write_hint':
|
|
708
|
-
return this.jFileControl(fileId, WebLocksMixin.WRITE_HINT_OP_CODE, null)
|
|
709
|
-
|
|
710
|
-
break
|
|
704
|
+
return this.jFileControl(fileId, WebLocksMixin.WRITE_HINT_OP_CODE, null)
|
|
705
|
+
}
|
|
706
|
+
break
|
|
711
707
|
case VFS.SQLITE_FCNTL_BEGIN_ATOMIC_WRITE:
|
|
712
|
-
this.log?.('xFileControl', 'BEGIN_ATOMIC_WRITE', file.path)
|
|
713
|
-
return VFS.SQLITE_OK
|
|
708
|
+
this.log?.('xFileControl', 'BEGIN_ATOMIC_WRITE', file.path)
|
|
709
|
+
return VFS.SQLITE_OK
|
|
714
710
|
case VFS.SQLITE_FCNTL_COMMIT_ATOMIC_WRITE:
|
|
715
|
-
this.log?.('xFileControl', 'COMMIT_ATOMIC_WRITE', file.path)
|
|
716
|
-
return VFS.SQLITE_OK
|
|
711
|
+
this.log?.('xFileControl', 'COMMIT_ATOMIC_WRITE', file.path)
|
|
712
|
+
return VFS.SQLITE_OK
|
|
717
713
|
case VFS.SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE:
|
|
718
|
-
this.log?.('xFileControl', 'ROLLBACK_ATOMIC_WRITE', file.path)
|
|
719
|
-
this.#rollbackTx(file)
|
|
720
|
-
return VFS.SQLITE_OK
|
|
714
|
+
this.log?.('xFileControl', 'ROLLBACK_ATOMIC_WRITE', file.path)
|
|
715
|
+
this.#rollbackTx(file)
|
|
716
|
+
return VFS.SQLITE_OK
|
|
721
717
|
case VFS.SQLITE_FCNTL_OVERWRITE:
|
|
722
718
|
// This is a VACUUM.
|
|
723
|
-
this.log?.('xFileControl', 'OVERWRITE', file.path)
|
|
724
|
-
await this.#prepareOverwrite(file)
|
|
725
|
-
break
|
|
719
|
+
this.log?.('xFileControl', 'OVERWRITE', file.path)
|
|
720
|
+
await this.#prepareOverwrite(file)
|
|
721
|
+
break
|
|
726
722
|
case VFS.SQLITE_FCNTL_COMMIT_PHASETWO:
|
|
727
723
|
// Finish any transaction. Note that the transaction may not
|
|
728
724
|
// exist if there is a BEGIN IMMEDIATE...COMMIT block that
|
|
729
725
|
// does not actually call xWrite.
|
|
730
|
-
this.log?.('xFileControl', 'COMMIT_PHASETWO', file.path)
|
|
726
|
+
this.log?.('xFileControl', 'COMMIT_PHASETWO', file.path)
|
|
731
727
|
if (file.txActive) {
|
|
732
|
-
await this.#commitTx(file)
|
|
728
|
+
await this.#commitTx(file)
|
|
733
729
|
}
|
|
734
|
-
break
|
|
730
|
+
break
|
|
735
731
|
case WebLocksMixin.WRITE_HINT_OP_CODE:
|
|
736
|
-
file.txWriteHint = true
|
|
737
|
-
break
|
|
732
|
+
file.txWriteHint = true
|
|
733
|
+
break
|
|
738
734
|
}
|
|
739
735
|
} catch (e) {
|
|
740
|
-
this.#lastError = e
|
|
741
|
-
return VFS.SQLITE_IOERR
|
|
736
|
+
this.#lastError = e
|
|
737
|
+
return VFS.SQLITE_IOERR
|
|
742
738
|
}
|
|
743
|
-
return VFS.SQLITE_NOTFOUND
|
|
739
|
+
return VFS.SQLITE_NOTFOUND
|
|
744
740
|
}
|
|
745
741
|
|
|
746
742
|
/**
|
|
@@ -748,109 +744,109 @@ export class OPFSPermutedVFS extends FacadeVFS {
|
|
|
748
744
|
* @returns {number|Promise<number>}
|
|
749
745
|
*/
|
|
750
746
|
jDeviceCharacteristics(fileId) {
|
|
751
|
-
return 0
|
|
752
|
-
| VFS.SQLITE_IOCAP_BATCH_ATOMIC
|
|
753
|
-
| VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
|
|
747
|
+
return 0 | VFS.SQLITE_IOCAP_BATCH_ATOMIC | VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
|
|
754
748
|
}
|
|
755
749
|
|
|
756
750
|
/**
|
|
757
|
-
* @param {Uint8Array} zBuf
|
|
751
|
+
* @param {Uint8Array} zBuf
|
|
758
752
|
* @returns {number}
|
|
759
753
|
*/
|
|
760
754
|
jGetLastError(zBuf) {
|
|
761
755
|
if (this.#lastError) {
|
|
762
|
-
console.error(this.#lastError)
|
|
763
|
-
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1)
|
|
764
|
-
const { written } = new TextEncoder().encodeInto(this.#lastError.message, outputArray)
|
|
765
|
-
zBuf[written] = 0
|
|
756
|
+
console.error(this.#lastError)
|
|
757
|
+
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1)
|
|
758
|
+
const { written } = new TextEncoder().encodeInto(this.#lastError.message, outputArray)
|
|
759
|
+
zBuf[written] = 0
|
|
766
760
|
}
|
|
767
761
|
return VFS.SQLITE_OK
|
|
768
762
|
}
|
|
769
763
|
|
|
770
764
|
/**
|
|
771
765
|
* Return the database page size, or 0 if not yet known.
|
|
772
|
-
* @param {File} file
|
|
766
|
+
* @param {File} file
|
|
773
767
|
* @returns {number}
|
|
774
768
|
*/
|
|
775
769
|
#getPageSize(file) {
|
|
776
770
|
// Offset 0 will always contain a page 1. Even if it is out of
|
|
777
771
|
// date it will have a valid page size.
|
|
778
772
|
// https://sqlite.org/fileformat.html#page_size
|
|
779
|
-
const header = new DataView(new ArrayBuffer(2))
|
|
780
|
-
const n = file.accessHandle.read(header, { at: 16 })
|
|
781
|
-
if (n !== header.byteLength) return 0
|
|
782
|
-
const pageSize = header.getUint16(0)
|
|
773
|
+
const header = new DataView(new ArrayBuffer(2))
|
|
774
|
+
const n = file.accessHandle.read(header, { at: 16 })
|
|
775
|
+
if (n !== header.byteLength) return 0
|
|
776
|
+
const pageSize = header.getUint16(0)
|
|
783
777
|
switch (pageSize) {
|
|
784
778
|
case 1:
|
|
785
|
-
return 65536
|
|
779
|
+
return 65536
|
|
786
780
|
default:
|
|
787
|
-
return pageSize
|
|
781
|
+
return pageSize
|
|
788
782
|
}
|
|
789
783
|
}
|
|
790
784
|
|
|
791
785
|
/**
|
|
792
786
|
* Acquire one of the database file internal Web Locks.
|
|
793
|
-
* @param {File} file
|
|
794
|
-
* @param {'read'|'write'|'reserved'|'hint'} name
|
|
795
|
-
* @param {LockOptions} options
|
|
787
|
+
* @param {File} file
|
|
788
|
+
* @param {'read'|'write'|'reserved'|'hint'} name
|
|
789
|
+
* @param {LockOptions} options
|
|
796
790
|
* @returns {Promise<boolean>}
|
|
797
791
|
*/
|
|
798
792
|
#lock(file, name, options = {}) {
|
|
799
|
-
return new Promise(resolve => {
|
|
800
|
-
const lockName = `${file.path}@@${name}
|
|
801
|
-
navigator.locks
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
793
|
+
return new Promise((resolve) => {
|
|
794
|
+
const lockName = `${file.path}@@${name}`
|
|
795
|
+
navigator.locks
|
|
796
|
+
.request(lockName, options, (lock) => {
|
|
797
|
+
if (lock) {
|
|
798
|
+
return new Promise((release) => {
|
|
799
|
+
file.locks[name] = () => {
|
|
800
|
+
release()
|
|
801
|
+
file.locks[name] = null
|
|
802
|
+
}
|
|
803
|
+
resolve(true)
|
|
804
|
+
})
|
|
805
|
+
} else {
|
|
806
|
+
file.locks[name] = null
|
|
807
|
+
resolve(false)
|
|
808
|
+
}
|
|
809
|
+
})
|
|
810
|
+
.catch((e) => {
|
|
811
|
+
if (e.name !== 'AbortError') throw e
|
|
812
|
+
})
|
|
813
|
+
})
|
|
818
814
|
}
|
|
819
815
|
|
|
820
816
|
/**
|
|
821
|
-
* @param {File} file
|
|
822
|
-
* @param {Transaction} tx
|
|
817
|
+
* @param {File} file
|
|
818
|
+
* @param {Transaction} tx
|
|
823
819
|
*/
|
|
824
820
|
async #setView(file, tx) {
|
|
825
821
|
// Publish our view of the database with a lock name that includes
|
|
826
822
|
// the transaction id. As long as we hold the lock, no other connection
|
|
827
823
|
// will overwrite data we are using.
|
|
828
|
-
file.viewTx = tx
|
|
829
|
-
const lockName = `${file.path}@@[${tx.txId}]
|
|
830
|
-
const newReleaser = await new Promise(resolve => {
|
|
831
|
-
navigator.locks.request(lockName, SHARED, lock => {
|
|
832
|
-
return new Promise(release => {
|
|
833
|
-
resolve(release)
|
|
834
|
-
})
|
|
835
|
-
})
|
|
836
|
-
})
|
|
824
|
+
file.viewTx = tx
|
|
825
|
+
const lockName = `${file.path}@@[${tx.txId}]`
|
|
826
|
+
const newReleaser = await new Promise((resolve) => {
|
|
827
|
+
navigator.locks.request(lockName, SHARED, (lock) => {
|
|
828
|
+
return new Promise((release) => {
|
|
829
|
+
resolve(release)
|
|
830
|
+
})
|
|
831
|
+
})
|
|
832
|
+
})
|
|
837
833
|
|
|
838
834
|
// The new lock is acquired so release the old one.
|
|
839
|
-
file.viewReleaser?.()
|
|
840
|
-
file.viewReleaser = newReleaser
|
|
835
|
+
file.viewReleaser?.()
|
|
836
|
+
file.viewReleaser = newReleaser
|
|
841
837
|
}
|
|
842
838
|
|
|
843
839
|
/**
|
|
844
840
|
* Handle prevously received messages from other connections.
|
|
845
|
-
* @param {File} file
|
|
841
|
+
* @param {File} file
|
|
846
842
|
*/
|
|
847
843
|
#processBroadcasts(file) {
|
|
848
844
|
// Sort transaction messages by id. Move other messages to the front.
|
|
849
845
|
// @ts-ignore
|
|
850
|
-
file.broadcastReceived.sort((a, b) => (a.txId ?? -1) - (b.txId ?? -1))
|
|
846
|
+
file.broadcastReceived.sort((a, b) => (a.txId ?? -1) - (b.txId ?? -1))
|
|
851
847
|
|
|
852
|
-
let nHandled = 0
|
|
853
|
-
let newTx = file.viewTx
|
|
848
|
+
let nHandled = 0
|
|
849
|
+
let newTx = file.viewTx
|
|
854
850
|
for (const message of file.broadcastReceived) {
|
|
855
851
|
if (Object.hasOwn(message, 'txId')) {
|
|
856
852
|
const messageTx = /** @type {Transaction} */ (message)
|
|
@@ -858,280 +854,276 @@ export class OPFSPermutedVFS extends FacadeVFS {
|
|
|
858
854
|
// This transaction is already incorporated into our view.
|
|
859
855
|
} else if (messageTx.txId === newTx.txId + 1) {
|
|
860
856
|
// This is the next expected transaction.
|
|
861
|
-
this.log?.(`accept tx ${messageTx.txId}`)
|
|
862
|
-
this.#acceptTx(file, messageTx)
|
|
863
|
-
newTx = messageTx
|
|
857
|
+
this.log?.(`accept tx ${messageTx.txId}`)
|
|
858
|
+
this.#acceptTx(file, messageTx)
|
|
859
|
+
newTx = messageTx
|
|
864
860
|
} else {
|
|
865
861
|
// There is a gap in the transaction sequence.
|
|
866
|
-
console.warn(`missing tx ${newTx.txId + 1} (got ${messageTx.txId})`)
|
|
867
|
-
break
|
|
862
|
+
console.warn(`missing tx ${newTx.txId + 1} (got ${messageTx.txId})`)
|
|
863
|
+
break
|
|
868
864
|
}
|
|
869
865
|
} else if (Object.hasOwn(message, 'exclusive')) {
|
|
870
866
|
// Release the read lock if we have it.
|
|
871
|
-
this.log?.('releasing read lock')
|
|
872
|
-
console.assert(file.lockState === VFS.SQLITE_LOCK_NONE)
|
|
873
|
-
file.locks.read?.()
|
|
867
|
+
this.log?.('releasing read lock')
|
|
868
|
+
console.assert(file.lockState === VFS.SQLITE_LOCK_NONE)
|
|
869
|
+
file.locks.read?.()
|
|
874
870
|
}
|
|
875
|
-
nHandled
|
|
871
|
+
nHandled++
|
|
876
872
|
}
|
|
877
873
|
|
|
878
874
|
// Remove handled messages from the list.
|
|
879
|
-
file.broadcastReceived.splice(0, nHandled)
|
|
875
|
+
file.broadcastReceived.splice(0, nHandled)
|
|
880
876
|
|
|
881
877
|
// Tell other connections about a change in our view.
|
|
882
878
|
if (newTx.txId > file.viewTx.txId) {
|
|
883
879
|
// No need to await here.
|
|
884
|
-
this.#setView(file, newTx)
|
|
880
|
+
this.#setView(file, newTx)
|
|
885
881
|
}
|
|
886
882
|
}
|
|
887
883
|
|
|
888
884
|
/**
|
|
889
|
-
* @param {File} file
|
|
890
|
-
* @param {Transaction} message
|
|
885
|
+
* @param {File} file
|
|
886
|
+
* @param {Transaction} message
|
|
891
887
|
*/
|
|
892
888
|
#acceptTx(file, message) {
|
|
893
|
-
file.pageSize = file.pageSize || this.#getPageSize(file)
|
|
889
|
+
file.pageSize = file.pageSize || this.#getPageSize(file)
|
|
894
890
|
|
|
895
891
|
// Add list of pages made obsolete by this transaction. These pages
|
|
896
892
|
// can be moved to the free list when all connections have reached
|
|
897
893
|
// this point.
|
|
898
|
-
message.reclaimable = []
|
|
894
|
+
message.reclaimable = []
|
|
899
895
|
|
|
900
896
|
// Update page mapping with transaction pages.
|
|
901
897
|
for (const [index, { offset }] of message.pages) {
|
|
902
898
|
if (file.mapPageToOffset.has(index)) {
|
|
903
899
|
// Remember overwritten pages that can be reused when all
|
|
904
900
|
// connections have seen this transaction.
|
|
905
|
-
message.reclaimable.push(file.mapPageToOffset.get(index))
|
|
901
|
+
message.reclaimable.push(file.mapPageToOffset.get(index))
|
|
906
902
|
}
|
|
907
|
-
file.mapPageToOffset.set(index, offset)
|
|
908
|
-
file.freeOffsets.delete(offset)
|
|
903
|
+
file.mapPageToOffset.set(index, offset)
|
|
904
|
+
file.freeOffsets.delete(offset)
|
|
909
905
|
}
|
|
910
906
|
|
|
911
907
|
// Remove mappings for truncated pages.
|
|
912
|
-
const oldPageCount = file.fileSize / file.pageSize
|
|
913
|
-
const newPageCount = message.fileSize / file.pageSize
|
|
908
|
+
const oldPageCount = file.fileSize / file.pageSize
|
|
909
|
+
const newPageCount = message.fileSize / file.pageSize
|
|
914
910
|
for (let index = newPageCount + 1; index <= oldPageCount; index++) {
|
|
915
|
-
message.reclaimable.push(file.mapPageToOffset.get(index))
|
|
916
|
-
file.mapPageToOffset.delete(index)
|
|
911
|
+
message.reclaimable.push(file.mapPageToOffset.get(index))
|
|
912
|
+
file.mapPageToOffset.delete(index)
|
|
917
913
|
}
|
|
918
914
|
|
|
919
|
-
file.fileSize = message.fileSize
|
|
920
|
-
file.mapTxToPending.set(message.txId, message)
|
|
915
|
+
file.fileSize = message.fileSize
|
|
916
|
+
file.mapTxToPending.set(message.txId, message)
|
|
921
917
|
if (message.oldestTxId) {
|
|
922
918
|
// Finalize pending transactions that are no longer needed.
|
|
923
919
|
for (const tx of file.mapTxToPending.values()) {
|
|
924
|
-
if (tx.txId > message.oldestTxId) break
|
|
920
|
+
if (tx.txId > message.oldestTxId) break
|
|
925
921
|
|
|
926
922
|
// Return no longer referenced pages to the free list.
|
|
927
923
|
for (const offset of tx.reclaimable) {
|
|
928
|
-
this.log?.(`reclaim offset ${offset}`)
|
|
929
|
-
file.freeOffsets.add(offset)
|
|
924
|
+
this.log?.(`reclaim offset ${offset}`)
|
|
925
|
+
file.freeOffsets.add(offset)
|
|
930
926
|
}
|
|
931
|
-
file.mapTxToPending.delete(tx.txId)
|
|
927
|
+
file.mapTxToPending.delete(tx.txId)
|
|
932
928
|
}
|
|
933
929
|
}
|
|
934
930
|
}
|
|
935
931
|
|
|
936
932
|
/**
|
|
937
|
-
* @param {File} file
|
|
933
|
+
* @param {File} file
|
|
938
934
|
*/
|
|
939
935
|
#beginTx(file) {
|
|
940
936
|
// Start a new transaction.
|
|
941
937
|
file.txActive = {
|
|
942
938
|
txId: file.viewTx.txId + 1,
|
|
943
939
|
pages: new Map(),
|
|
944
|
-
fileSize: file.fileSize
|
|
945
|
-
}
|
|
946
|
-
file.txRealFileSize = file.accessHandle.getSize()
|
|
947
|
-
this.log?.(`begin transaction ${file.txActive.txId}`)
|
|
940
|
+
fileSize: file.fileSize,
|
|
941
|
+
}
|
|
942
|
+
file.txRealFileSize = file.accessHandle.getSize()
|
|
943
|
+
this.log?.(`begin transaction ${file.txActive.txId}`)
|
|
948
944
|
}
|
|
949
945
|
|
|
950
946
|
/**
|
|
951
|
-
* @param {File} file
|
|
947
|
+
* @param {File} file
|
|
952
948
|
*/
|
|
953
949
|
async #commitTx(file) {
|
|
954
950
|
// Determine whether to finalize pending transactions, i.e. transfer
|
|
955
951
|
// them to the IndexedDB pages store.
|
|
956
|
-
if (file.synchronous === 'full' ||
|
|
957
|
-
|
|
958
|
-
(file.txActive.txId % file.flushInterval) === 0) {
|
|
959
|
-
file.txActive.oldestTxId = await this.#getOldestTxInUse(file);
|
|
952
|
+
if (file.synchronous === 'full' || file.txIsOverwrite || file.txActive.txId % file.flushInterval === 0) {
|
|
953
|
+
file.txActive.oldestTxId = await this.#getOldestTxInUse(file)
|
|
960
954
|
}
|
|
961
955
|
|
|
962
|
-
const tx = file.idb.transaction(
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
{ durability: file.synchronous === 'full' ? 'strict' : 'relaxed'});
|
|
956
|
+
const tx = file.idb.transaction(['pages', 'pending'], 'readwrite', {
|
|
957
|
+
durability: file.synchronous === 'full' ? 'strict' : 'relaxed',
|
|
958
|
+
})
|
|
966
959
|
|
|
967
960
|
if (file.txActive.oldestTxId) {
|
|
968
961
|
// Ensure that all pending data is safely on storage.
|
|
969
962
|
if (file.txIsOverwrite) {
|
|
970
|
-
file.accessHandle.truncate(file.txActive.fileSize)
|
|
963
|
+
file.accessHandle.truncate(file.txActive.fileSize)
|
|
971
964
|
}
|
|
972
|
-
file.accessHandle.flush()
|
|
973
|
-
|
|
965
|
+
file.accessHandle.flush()
|
|
966
|
+
|
|
974
967
|
// Transfer page mappings to the pages store for all pending
|
|
975
968
|
// transactions that are no longer in use.
|
|
976
|
-
const pageStore = tx.objectStore('pages')
|
|
969
|
+
const pageStore = tx.objectStore('pages')
|
|
977
970
|
for (const tx of file.mapTxToPending.values()) {
|
|
978
|
-
if (tx.txId > file.txActive.oldestTxId) break
|
|
971
|
+
if (tx.txId > file.txActive.oldestTxId) break
|
|
979
972
|
|
|
980
973
|
for (const [index, { offset }] of tx.pages) {
|
|
981
|
-
pageStore.put({ i: index, o: offset })
|
|
974
|
+
pageStore.put({ i: index, o: offset })
|
|
982
975
|
}
|
|
983
976
|
}
|
|
984
977
|
|
|
985
978
|
// Delete pending store entries that are no longer needed.
|
|
986
|
-
tx.objectStore('pending')
|
|
987
|
-
.delete(IDBKeyRange.upperBound(file.txActive.oldestTxId));
|
|
979
|
+
tx.objectStore('pending').delete(IDBKeyRange.upperBound(file.txActive.oldestTxId))
|
|
988
980
|
}
|
|
989
981
|
|
|
990
982
|
// Publish the transaction via broadcast and IndexedDB.
|
|
991
|
-
this.log?.(`commit transaction ${file.txActive.txId}`)
|
|
992
|
-
tx.objectStore('pending').put(file.txActive)
|
|
983
|
+
this.log?.(`commit transaction ${file.txActive.txId}`)
|
|
984
|
+
tx.objectStore('pending').put(file.txActive)
|
|
993
985
|
|
|
994
986
|
const txComplete = new Promise((resolve, reject) => {
|
|
995
|
-
const message = file.txActive
|
|
987
|
+
const message = file.txActive
|
|
996
988
|
tx.oncomplete = () => {
|
|
997
|
-
file.broadcastChannel.postMessage(message)
|
|
998
|
-
resolve()
|
|
999
|
-
}
|
|
989
|
+
file.broadcastChannel.postMessage(message)
|
|
990
|
+
resolve()
|
|
991
|
+
}
|
|
1000
992
|
tx.onabort = () => {
|
|
1001
|
-
file.abortController.abort()
|
|
1002
|
-
reject(tx.error)
|
|
1003
|
-
}
|
|
1004
|
-
tx.commit()
|
|
1005
|
-
})
|
|
993
|
+
file.abortController.abort()
|
|
994
|
+
reject(tx.error)
|
|
995
|
+
}
|
|
996
|
+
tx.commit()
|
|
997
|
+
})
|
|
1006
998
|
|
|
1007
999
|
if (file.synchronous === 'full') {
|
|
1008
|
-
await txComplete
|
|
1000
|
+
await txComplete
|
|
1009
1001
|
}
|
|
1010
1002
|
|
|
1011
1003
|
// Advance our own view. Even if we received our own broadcasts (we
|
|
1012
1004
|
// don't), we want our view to be updated synchronously.
|
|
1013
|
-
this.#acceptTx(file, file.txActive)
|
|
1014
|
-
this.#setView(file, file.txActive)
|
|
1015
|
-
file.txActive = null
|
|
1016
|
-
file.txWriteHint = false
|
|
1005
|
+
this.#acceptTx(file, file.txActive)
|
|
1006
|
+
this.#setView(file, file.txActive)
|
|
1007
|
+
file.txActive = null
|
|
1008
|
+
file.txWriteHint = false
|
|
1017
1009
|
|
|
1018
1010
|
if (file.txIsOverwrite) {
|
|
1019
1011
|
// Wait until all connections have seen the transaction.
|
|
1020
|
-
while (file.viewTx.txId !== await this.#getOldestTxInUse(file)) {
|
|
1021
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
1012
|
+
while (file.viewTx.txId !== (await this.#getOldestTxInUse(file))) {
|
|
1013
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
1022
1014
|
}
|
|
1023
1015
|
|
|
1024
1016
|
// Downgrade the exclusive read lock to a shared lock.
|
|
1025
|
-
file.locks.read()
|
|
1026
|
-
await this.#lock(file, 'read', SHARED)
|
|
1017
|
+
file.locks.read()
|
|
1018
|
+
await this.#lock(file, 'read', SHARED)
|
|
1027
1019
|
|
|
1028
1020
|
// There should be no extra space in the file now.
|
|
1029
|
-
file.freeOffsets.clear()
|
|
1021
|
+
file.freeOffsets.clear()
|
|
1030
1022
|
|
|
1031
|
-
file.txIsOverwrite = false
|
|
1023
|
+
file.txIsOverwrite = false
|
|
1032
1024
|
}
|
|
1033
1025
|
}
|
|
1034
1026
|
|
|
1035
1027
|
/**
|
|
1036
|
-
* @param {File} file
|
|
1028
|
+
* @param {File} file
|
|
1037
1029
|
*/
|
|
1038
1030
|
#rollbackTx(file) {
|
|
1039
1031
|
// Return offsets to the free list.
|
|
1040
|
-
this.log?.(`rollback transaction ${file.txActive.txId}`)
|
|
1032
|
+
this.log?.(`rollback transaction ${file.txActive.txId}`)
|
|
1041
1033
|
for (const { offset } of file.txActive.pages.values()) {
|
|
1042
|
-
file.freeOffsets.add(offset)
|
|
1034
|
+
file.freeOffsets.add(offset)
|
|
1043
1035
|
}
|
|
1044
|
-
file.txActive = null
|
|
1045
|
-
file.txWriteHint = false
|
|
1036
|
+
file.txActive = null
|
|
1037
|
+
file.txWriteHint = false
|
|
1046
1038
|
}
|
|
1047
1039
|
|
|
1048
1040
|
/**
|
|
1049
|
-
* @param {File} file
|
|
1041
|
+
* @param {File} file
|
|
1050
1042
|
*/
|
|
1051
1043
|
async #prepareOverwrite(file) {
|
|
1052
1044
|
// Get an exclusive read lock to prevent other connections from
|
|
1053
1045
|
// seeing the database in an inconsistent state.
|
|
1054
|
-
file.locks.read?.()
|
|
1055
|
-
if (!await this.#lock(file, 'read', POLL_EXCLUSIVE)) {
|
|
1046
|
+
file.locks.read?.()
|
|
1047
|
+
if (!(await this.#lock(file, 'read', POLL_EXCLUSIVE))) {
|
|
1056
1048
|
// We didn't get the read lock because other connections have
|
|
1057
1049
|
// it. Notify them that we want the lock and wait.
|
|
1058
|
-
const lockRequest = this.#lock(file, 'read')
|
|
1059
|
-
file.broadcastChannel.postMessage({ exclusive: true })
|
|
1060
|
-
await lockRequest
|
|
1050
|
+
const lockRequest = this.#lock(file, 'read')
|
|
1051
|
+
file.broadcastChannel.postMessage({ exclusive: true })
|
|
1052
|
+
await lockRequest
|
|
1061
1053
|
}
|
|
1062
1054
|
|
|
1063
1055
|
// Create a intermediate transaction to copy all current page data to
|
|
1064
|
-
// an offset past fileSize.
|
|
1056
|
+
// an offset past fileSize.
|
|
1065
1057
|
file.txActive = {
|
|
1066
1058
|
txId: file.viewTx.txId + 1,
|
|
1067
1059
|
pages: new Map(),
|
|
1068
|
-
fileSize: file.fileSize
|
|
1069
|
-
}
|
|
1060
|
+
fileSize: file.fileSize,
|
|
1061
|
+
}
|
|
1070
1062
|
|
|
1071
1063
|
// This helper generator provides offsets above fileSize.
|
|
1072
|
-
const offsetGenerator = (function*() {
|
|
1064
|
+
const offsetGenerator = (function* () {
|
|
1073
1065
|
for (const offset of file.freeOffsets) {
|
|
1074
1066
|
if (offset >= file.fileSize) {
|
|
1075
|
-
yield offset
|
|
1067
|
+
yield offset
|
|
1076
1068
|
}
|
|
1077
1069
|
}
|
|
1078
1070
|
|
|
1079
1071
|
while (true) {
|
|
1080
|
-
yield file.accessHandle.getSize()
|
|
1072
|
+
yield file.accessHandle.getSize()
|
|
1081
1073
|
}
|
|
1082
|
-
})()
|
|
1074
|
+
})()
|
|
1083
1075
|
|
|
1084
|
-
const pageBuffer = new Uint8Array(file.pageSize)
|
|
1076
|
+
const pageBuffer = new Uint8Array(file.pageSize)
|
|
1085
1077
|
for (let offset = 0; offset < file.fileSize; offset += file.pageSize) {
|
|
1086
|
-
const pageIndex = offset / file.pageSize + 1
|
|
1087
|
-
const oldOffset = file.mapPageToOffset.get(pageIndex)
|
|
1078
|
+
const pageIndex = offset / file.pageSize + 1
|
|
1079
|
+
const oldOffset = file.mapPageToOffset.get(pageIndex)
|
|
1088
1080
|
if (oldOffset < file.fileSize) {
|
|
1089
1081
|
// This page is stored below fileSize. Read it into memory.
|
|
1090
1082
|
if (file.accessHandle.read(pageBuffer, { at: oldOffset }) !== file.pageSize) {
|
|
1091
|
-
throw new Error('Failed to read page')
|
|
1083
|
+
throw new Error('Failed to read page')
|
|
1092
1084
|
}
|
|
1093
|
-
|
|
1085
|
+
|
|
1094
1086
|
// Perform the copy.
|
|
1095
|
-
const newOffset = offsetGenerator.next().value
|
|
1087
|
+
const newOffset = offsetGenerator.next().value
|
|
1096
1088
|
if (file.accessHandle.write(pageBuffer, { at: newOffset }) !== file.pageSize) {
|
|
1097
|
-
throw new Error('Failed to write page')
|
|
1089
|
+
throw new Error('Failed to write page')
|
|
1098
1090
|
}
|
|
1099
1091
|
|
|
1100
1092
|
file.txActive.pages.set(pageIndex, {
|
|
1101
1093
|
offset: newOffset,
|
|
1102
|
-
digest: checksum(pageBuffer)
|
|
1103
|
-
})
|
|
1094
|
+
digest: checksum(pageBuffer),
|
|
1095
|
+
})
|
|
1104
1096
|
}
|
|
1105
1097
|
}
|
|
1106
|
-
file.accessHandle.flush()
|
|
1107
|
-
file.freeOffsets.clear()
|
|
1108
|
-
|
|
1098
|
+
file.accessHandle.flush()
|
|
1099
|
+
file.freeOffsets.clear()
|
|
1100
|
+
|
|
1109
1101
|
// Publish transaction for others.
|
|
1110
|
-
file.broadcastChannel.postMessage(file.txActive)
|
|
1111
|
-
const tx = file.idb.transaction('pending', 'readwrite')
|
|
1102
|
+
file.broadcastChannel.postMessage(file.txActive)
|
|
1103
|
+
const tx = file.idb.transaction('pending', 'readwrite')
|
|
1112
1104
|
const txComplete = new Promise((resolve, reject) => {
|
|
1113
|
-
tx.oncomplete = resolve
|
|
1114
|
-
tx.onabort = () => reject(tx.error)
|
|
1115
|
-
})
|
|
1116
|
-
tx.objectStore('pending').put(file.txActive)
|
|
1117
|
-
tx.commit()
|
|
1118
|
-
await txComplete
|
|
1105
|
+
tx.oncomplete = resolve
|
|
1106
|
+
tx.onabort = () => reject(tx.error)
|
|
1107
|
+
})
|
|
1108
|
+
tx.objectStore('pending').put(file.txActive)
|
|
1109
|
+
tx.commit()
|
|
1110
|
+
await txComplete
|
|
1119
1111
|
|
|
1120
1112
|
// Incorporate the transaction into our view.
|
|
1121
|
-
this.#acceptTx(file, file.txActive)
|
|
1122
|
-
this.#setView(file, file.txActive)
|
|
1123
|
-
file.txActive = null
|
|
1113
|
+
this.#acceptTx(file, file.txActive)
|
|
1114
|
+
this.#setView(file, file.txActive)
|
|
1115
|
+
file.txActive = null
|
|
1124
1116
|
|
|
1125
1117
|
// Now all pages are in the file above fileSize. The VACUUM operation
|
|
1126
1118
|
// will now copy the pages below fileSize in the proper order. After
|
|
1127
1119
|
// that once all connections are up to date the file can be truncated.
|
|
1128
1120
|
|
|
1129
1121
|
// This flag tells xWrite to write pages at their canonical offset.
|
|
1130
|
-
file.txIsOverwrite = true
|
|
1122
|
+
file.txIsOverwrite = true
|
|
1131
1123
|
}
|
|
1132
1124
|
|
|
1133
1125
|
/**
|
|
1134
|
-
* @param {File} file
|
|
1126
|
+
* @param {File} file
|
|
1135
1127
|
* @returns {Promise<number>}
|
|
1136
1128
|
*/
|
|
1137
1129
|
async #getOldestTxInUse(file) {
|
|
@@ -1139,79 +1131,76 @@ export class OPFSPermutedVFS extends FacadeVFS {
|
|
|
1139
1131
|
// the latest transaction it knows about. We can find the oldest
|
|
1140
1132
|
// transaction by listing the those locks and extracting the earliest
|
|
1141
1133
|
// transaction id.
|
|
1142
|
-
const TX_LOCK_REGEX = /^(.*)@@\[(\d+)\]
|
|
1143
|
-
let oldestTxId = file.viewTx.txId
|
|
1144
|
-
const locks = await navigator.locks.query()
|
|
1134
|
+
const TX_LOCK_REGEX = /^(.*)@@\[(\d+)\]$/
|
|
1135
|
+
let oldestTxId = file.viewTx.txId
|
|
1136
|
+
const locks = await navigator.locks.query()
|
|
1145
1137
|
for (const { name } of locks.held) {
|
|
1146
|
-
const m = TX_LOCK_REGEX.exec(name)
|
|
1138
|
+
const m = TX_LOCK_REGEX.exec(name)
|
|
1147
1139
|
if (m && m[1] === file.path) {
|
|
1148
|
-
oldestTxId = Math.min(oldestTxId, Number(m[2]))
|
|
1140
|
+
oldestTxId = Math.min(oldestTxId, Number(m[2]))
|
|
1149
1141
|
}
|
|
1150
1142
|
}
|
|
1151
|
-
return oldestTxId
|
|
1143
|
+
return oldestTxId
|
|
1152
1144
|
}
|
|
1153
1145
|
}
|
|
1154
1146
|
|
|
1155
1147
|
/**
|
|
1156
1148
|
* Wrap IndexedDB request with a Promise.
|
|
1157
|
-
* @param {IDBRequest} request
|
|
1158
|
-
* @returns
|
|
1149
|
+
* @param {IDBRequest} request
|
|
1150
|
+
* @returns
|
|
1159
1151
|
*/
|
|
1160
1152
|
function idbX(request) {
|
|
1161
1153
|
return new Promise((resolve, reject) => {
|
|
1162
|
-
request.onsuccess = () => resolve(request.result)
|
|
1163
|
-
request.onerror = () => reject(request.error)
|
|
1164
|
-
})
|
|
1154
|
+
request.onsuccess = () => resolve(request.result)
|
|
1155
|
+
request.onerror = () => reject(request.error)
|
|
1156
|
+
})
|
|
1165
1157
|
}
|
|
1166
1158
|
|
|
1167
1159
|
/**
|
|
1168
1160
|
* Given a path, return the directory handle and filename.
|
|
1169
|
-
* @param {string} path
|
|
1170
|
-
* @param {boolean} create
|
|
1161
|
+
* @param {string} path
|
|
1162
|
+
* @param {boolean} create
|
|
1171
1163
|
* @returns {Promise<[FileSystemDirectoryHandle, string]>}
|
|
1172
1164
|
*/
|
|
1173
1165
|
async function getPathComponents(path, create) {
|
|
1174
|
-
const components = path.split('/')
|
|
1175
|
-
const filename = components.pop()
|
|
1176
|
-
let directory = await navigator.storage.getDirectory()
|
|
1177
|
-
for (const component of components.filter(s => s)) {
|
|
1178
|
-
directory = await directory.getDirectoryHandle(component, { create })
|
|
1166
|
+
const components = path.split('/')
|
|
1167
|
+
const filename = components.pop()
|
|
1168
|
+
let directory = await navigator.storage.getDirectory()
|
|
1169
|
+
for (const component of components.filter((s) => s)) {
|
|
1170
|
+
directory = await directory.getDirectoryHandle(component, { create })
|
|
1179
1171
|
}
|
|
1180
|
-
return [directory, filename]
|
|
1172
|
+
return [directory, filename]
|
|
1181
1173
|
}
|
|
1182
1174
|
|
|
1183
1175
|
/**
|
|
1184
1176
|
* Extract a C string from WebAssembly memory.
|
|
1185
|
-
* @param {DataView} dataView
|
|
1186
|
-
* @param {number} offset
|
|
1187
|
-
* @returns
|
|
1177
|
+
* @param {DataView} dataView
|
|
1178
|
+
* @param {number} offset
|
|
1179
|
+
* @returns
|
|
1188
1180
|
*/
|
|
1189
1181
|
function cvtString(dataView, offset) {
|
|
1190
|
-
const p = dataView.getUint32(offset, true)
|
|
1182
|
+
const p = dataView.getUint32(offset, true)
|
|
1191
1183
|
if (p) {
|
|
1192
|
-
const chars = new Uint8Array(dataView.buffer, p)
|
|
1193
|
-
return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)))
|
|
1184
|
+
const chars = new Uint8Array(dataView.buffer, p)
|
|
1185
|
+
return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)))
|
|
1194
1186
|
}
|
|
1195
|
-
return null
|
|
1187
|
+
return null
|
|
1196
1188
|
}
|
|
1197
1189
|
|
|
1198
1190
|
/**
|
|
1199
1191
|
* Compute a page checksum.
|
|
1200
|
-
* @param {ArrayBufferView} data
|
|
1192
|
+
* @param {ArrayBufferView} data
|
|
1201
1193
|
* @returns {Uint32Array}
|
|
1202
1194
|
*/
|
|
1203
1195
|
function checksum(data) {
|
|
1204
|
-
const array = new Uint32Array(
|
|
1205
|
-
data.buffer,
|
|
1206
|
-
data.byteOffset,
|
|
1207
|
-
data.byteLength / Uint32Array.BYTES_PER_ELEMENT);
|
|
1196
|
+
const array = new Uint32Array(data.buffer, data.byteOffset, data.byteLength / Uint32Array.BYTES_PER_ELEMENT)
|
|
1208
1197
|
|
|
1209
1198
|
// https://en.wikipedia.org/wiki/Fletcher%27s_checksum
|
|
1210
|
-
let h1 = 0
|
|
1211
|
-
let h2 = 0
|
|
1199
|
+
let h1 = 0
|
|
1200
|
+
let h2 = 0
|
|
1212
1201
|
for (const value of array) {
|
|
1213
|
-
h1 = (h1 + value) % 4294967295
|
|
1214
|
-
h2 = (h2 + h1) % 4294967295
|
|
1202
|
+
h1 = (h1 + value) % 4294967295
|
|
1203
|
+
h2 = (h2 + h1) % 4294967295
|
|
1215
1204
|
}
|
|
1216
|
-
return new Uint32Array([h1, h2])
|
|
1217
|
-
}
|
|
1205
|
+
return new Uint32Array([h1, h2])
|
|
1206
|
+
}
|