@livestore/wa-sqlite 1.0.1-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/wa-sqlite-async.mjs +16 -0
- package/dist/wa-sqlite-async.wasm +0 -0
- package/dist/wa-sqlite-jspi.mjs +16 -0
- package/dist/wa-sqlite-jspi.wasm +0 -0
- package/dist/wa-sqlite.mjs +16 -0
- package/dist/wa-sqlite.wasm +0 -0
- package/package.json +45 -0
- package/src/FacadeVFS.js +508 -0
- package/src/VFS.js +222 -0
- package/src/WebLocksMixin.js +412 -0
- package/src/examples/AccessHandlePoolVFS.js +458 -0
- package/src/examples/IDBBatchAtomicVFS.js +820 -0
- package/src/examples/IDBMirrorVFS.js +875 -0
- package/src/examples/MemoryAsyncVFS.js +100 -0
- package/src/examples/MemoryVFS.js +176 -0
- package/src/examples/OPFSAdaptiveVFS.js +437 -0
- package/src/examples/OPFSAnyContextVFS.js +300 -0
- package/src/examples/OPFSCoopSyncVFS.js +590 -0
- package/src/examples/OPFSPermutedVFS.js +1214 -0
- package/src/examples/README.md +89 -0
- package/src/examples/tag.js +82 -0
- package/src/sqlite-api.js +914 -0
- package/src/sqlite-constants.js +275 -0
- package/src/types/globals.d.ts +60 -0
- package/src/types/index.d.ts +1302 -0
- package/src/types/tsconfig.json +6 -0
- package/test/AccessHandlePoolVFS.test.js +27 -0
- package/test/IDBBatchAtomicVFS.test.js +97 -0
- package/test/IDBMirrorVFS.test.js +27 -0
- package/test/MemoryAsyncVFS.test.js +27 -0
- package/test/MemoryVFS.test.js +27 -0
- package/test/OPFSAdaptiveVFS.test.js +27 -0
- package/test/OPFSAnyContextVFS.test.js +27 -0
- package/test/OPFSCoopSyncVFS.test.js +27 -0
- package/test/OPFSPermutedVFS.test.js +27 -0
- package/test/TestContext.js +96 -0
- package/test/WebLocksMixin.test.js +521 -0
- package/test/api.test.js +49 -0
- package/test/api_exec.js +89 -0
- package/test/api_misc.js +63 -0
- package/test/api_statements.js +426 -0
- package/test/callbacks.test.js +373 -0
- package/test/sql.test.js +64 -0
- package/test/sql_0001.js +49 -0
- package/test/sql_0002.js +52 -0
- package/test/sql_0003.js +83 -0
- package/test/sql_0004.js +81 -0
- package/test/sql_0005.js +76 -0
- package/test/test-worker.js +204 -0
- package/test/vfs_xAccess.js +2 -0
- package/test/vfs_xClose.js +52 -0
- package/test/vfs_xOpen.js +91 -0
- package/test/vfs_xRead.js +38 -0
- package/test/vfs_xWrite.js +36 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
|
|
2
|
+
import { MemoryVFS } from './MemoryVFS.js';
|
|
3
|
+
|
|
4
|
+
// Sample asynchronous in-memory filesystem. This filesystem requires an
|
|
5
|
+
// asynchronous WebAssembly build (Asyncify or JSPI).
|
|
6
|
+
export class MemoryAsyncVFS extends MemoryVFS {
|
|
7
|
+
|
|
8
|
+
static async create(name, module) {
|
|
9
|
+
const vfs = new MemoryVFS(name, module);
|
|
10
|
+
await vfs.isReady();
|
|
11
|
+
return vfs;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
constructor(name, module) {
|
|
15
|
+
super(name, module);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async close() {
|
|
19
|
+
for (const fileId of this.mapIdToFile.keys()) {
|
|
20
|
+
await this.xClose(fileId);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string?} name
|
|
26
|
+
* @param {number} fileId
|
|
27
|
+
* @param {number} flags
|
|
28
|
+
* @param {DataView} pOutFlags
|
|
29
|
+
* @returns {Promise<number>}
|
|
30
|
+
*/
|
|
31
|
+
async jOpen(name, fileId, flags, pOutFlags) {
|
|
32
|
+
return super.jOpen(name, fileId, flags, pOutFlags);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {number} fileId
|
|
37
|
+
* @returns {Promise<number>}
|
|
38
|
+
*/
|
|
39
|
+
async jClose(fileId) {
|
|
40
|
+
return super.jClose(fileId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {number} fileId
|
|
45
|
+
* @param {Uint8Array} pData
|
|
46
|
+
* @param {number} iOffset
|
|
47
|
+
* @returns {Promise<number>}
|
|
48
|
+
*/
|
|
49
|
+
async jRead(fileId, pData, iOffset) {
|
|
50
|
+
return super.jRead(fileId, pData, iOffset);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {number} fileId
|
|
55
|
+
* @param {Uint8Array} pData
|
|
56
|
+
* @param {number} iOffset
|
|
57
|
+
* @returns {Promise<number>}
|
|
58
|
+
*/
|
|
59
|
+
async jWrite(fileId, pData, iOffset) {
|
|
60
|
+
return super.jWrite(fileId, pData, iOffset);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {number} fileId
|
|
65
|
+
* @param {number} iSize
|
|
66
|
+
* @returns {Promise<number>}
|
|
67
|
+
*/
|
|
68
|
+
async xTruncate(fileId, iSize) {
|
|
69
|
+
return super.jTruncate(fileId, iSize);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {number} fileId
|
|
74
|
+
* @param {DataView} pSize64
|
|
75
|
+
* @returns {Promise<number>}
|
|
76
|
+
*/
|
|
77
|
+
async jFileSize(fileId, pSize64) {
|
|
78
|
+
return super.jFileSize(fileId, pSize64);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
*
|
|
83
|
+
* @param {string} name
|
|
84
|
+
* @param {number} syncDir
|
|
85
|
+
* @returns {Promise<number>}
|
|
86
|
+
*/
|
|
87
|
+
async jDelete(name, syncDir) {
|
|
88
|
+
return super.jDelete(name, syncDir);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} name
|
|
93
|
+
* @param {number} flags
|
|
94
|
+
* @param {DataView} pResOut
|
|
95
|
+
* @returns {Promise<number>}
|
|
96
|
+
*/
|
|
97
|
+
async jAccess(name, flags, pResOut) {
|
|
98
|
+
return super.jAccess(name, flags, pResOut);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
|
|
2
|
+
import { FacadeVFS } from '../FacadeVFS.js';
|
|
3
|
+
import * as VFS from '../VFS.js';
|
|
4
|
+
|
|
5
|
+
// Sample in-memory filesystem.
|
|
6
|
+
export class MemoryVFS extends FacadeVFS {
|
|
7
|
+
// Map of existing files, keyed by filename.
|
|
8
|
+
mapNameToFile = new Map();
|
|
9
|
+
|
|
10
|
+
// Map of open files, keyed by id (sqlite3_file pointer).
|
|
11
|
+
mapIdToFile = new Map();
|
|
12
|
+
|
|
13
|
+
static async create(name, module) {
|
|
14
|
+
const vfs = new MemoryVFS(name, module);
|
|
15
|
+
await vfs.isReady();
|
|
16
|
+
return vfs;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor(name, module) {
|
|
20
|
+
super(name, module);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
close() {
|
|
24
|
+
for (const fileId of this.mapIdToFile.keys()) {
|
|
25
|
+
this.jClose(fileId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string?} filename
|
|
31
|
+
* @param {number} fileId
|
|
32
|
+
* @param {number} flags
|
|
33
|
+
* @param {DataView} pOutFlags
|
|
34
|
+
* @returns {number|Promise<number>}
|
|
35
|
+
*/
|
|
36
|
+
jOpen(filename, fileId, flags, pOutFlags) {
|
|
37
|
+
const url = new URL(filename || Math.random().toString(36).slice(2), 'file://');
|
|
38
|
+
const pathname = url.pathname;
|
|
39
|
+
|
|
40
|
+
let file = this.mapNameToFile.get(pathname);
|
|
41
|
+
if (!file) {
|
|
42
|
+
if (flags & VFS.SQLITE_OPEN_CREATE) {
|
|
43
|
+
// Create a new file object.
|
|
44
|
+
file = {
|
|
45
|
+
pathname,
|
|
46
|
+
flags,
|
|
47
|
+
size: 0,
|
|
48
|
+
data: new ArrayBuffer(0)
|
|
49
|
+
};
|
|
50
|
+
this.mapNameToFile.set(pathname, file);
|
|
51
|
+
} else {
|
|
52
|
+
return VFS.SQLITE_CANTOPEN;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Put the file in the opened files map.
|
|
57
|
+
this.mapIdToFile.set(fileId, file);
|
|
58
|
+
pOutFlags.setInt32(0, flags, true);
|
|
59
|
+
return VFS.SQLITE_OK;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {number} fileId
|
|
64
|
+
* @returns {number|Promise<number>}
|
|
65
|
+
*/
|
|
66
|
+
jClose(fileId) {
|
|
67
|
+
const file = this.mapIdToFile.get(fileId);
|
|
68
|
+
this.mapIdToFile.delete(fileId);
|
|
69
|
+
|
|
70
|
+
if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
|
71
|
+
this.mapNameToFile.delete(file.pathname);
|
|
72
|
+
}
|
|
73
|
+
return VFS.SQLITE_OK;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {number} fileId
|
|
78
|
+
* @param {Uint8Array} pData
|
|
79
|
+
* @param {number} iOffset
|
|
80
|
+
* @returns {number|Promise<number>}
|
|
81
|
+
*/
|
|
82
|
+
jRead(fileId, pData, iOffset) {
|
|
83
|
+
const file = this.mapIdToFile.get(fileId);
|
|
84
|
+
|
|
85
|
+
// Clip the requested read to the file boundary.
|
|
86
|
+
const bgn = Math.min(iOffset, file.size);
|
|
87
|
+
const end = Math.min(iOffset + pData.byteLength, file.size);
|
|
88
|
+
const nBytes = end - bgn;
|
|
89
|
+
|
|
90
|
+
if (nBytes) {
|
|
91
|
+
pData.set(new Uint8Array(file.data, bgn, nBytes));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (nBytes < pData.byteLength) {
|
|
95
|
+
// Zero unused area of read buffer.
|
|
96
|
+
pData.fill(0, nBytes);
|
|
97
|
+
return VFS.SQLITE_IOERR_SHORT_READ;
|
|
98
|
+
}
|
|
99
|
+
return VFS.SQLITE_OK;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {number} fileId
|
|
104
|
+
* @param {Uint8Array} pData
|
|
105
|
+
* @param {number} iOffset
|
|
106
|
+
* @returns {number|Promise<number>}
|
|
107
|
+
*/
|
|
108
|
+
jWrite(fileId, pData, iOffset) {
|
|
109
|
+
const file = this.mapIdToFile.get(fileId);
|
|
110
|
+
if (iOffset + pData.byteLength > file.data.byteLength) {
|
|
111
|
+
// Resize the ArrayBuffer to hold more data.
|
|
112
|
+
const newSize = Math.max(iOffset + pData.byteLength, 2 * file.data.byteLength);
|
|
113
|
+
const data = new ArrayBuffer(newSize);
|
|
114
|
+
new Uint8Array(data).set(new Uint8Array(file.data, 0, file.size));
|
|
115
|
+
file.data = data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Copy data.
|
|
119
|
+
new Uint8Array(file.data, iOffset, pData.byteLength).set(pData);
|
|
120
|
+
file.size = Math.max(file.size, iOffset + pData.byteLength);
|
|
121
|
+
return VFS.SQLITE_OK;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {number} fileId
|
|
126
|
+
* @param {number} iSize
|
|
127
|
+
* @returns {number|Promise<number>}
|
|
128
|
+
*/
|
|
129
|
+
jTruncate(fileId, iSize) {
|
|
130
|
+
const file = this.mapIdToFile.get(fileId);
|
|
131
|
+
|
|
132
|
+
// For simplicity we don't make the ArrayBuffer smaller.
|
|
133
|
+
file.size = Math.min(file.size, iSize);
|
|
134
|
+
return VFS.SQLITE_OK;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {number} fileId
|
|
139
|
+
* @param {DataView} pSize64
|
|
140
|
+
* @returns {number|Promise<number>}
|
|
141
|
+
*/
|
|
142
|
+
jFileSize(fileId, pSize64) {
|
|
143
|
+
const file = this.mapIdToFile.get(fileId);
|
|
144
|
+
|
|
145
|
+
pSize64.setBigInt64(0, BigInt(file.size), true);
|
|
146
|
+
return VFS.SQLITE_OK;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} name
|
|
151
|
+
* @param {number} syncDir
|
|
152
|
+
* @returns {number|Promise<number>}
|
|
153
|
+
*/
|
|
154
|
+
jDelete(name, syncDir) {
|
|
155
|
+
const url = new URL(name, 'file://');
|
|
156
|
+
const pathname = url.pathname;
|
|
157
|
+
|
|
158
|
+
this.mapNameToFile.delete(pathname);
|
|
159
|
+
return VFS.SQLITE_OK;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {string} name
|
|
164
|
+
* @param {number} flags
|
|
165
|
+
* @param {DataView} pResOut
|
|
166
|
+
* @returns {number|Promise<number>}
|
|
167
|
+
*/
|
|
168
|
+
jAccess(name, flags, pResOut) {
|
|
169
|
+
const url = new URL(name, 'file://');
|
|
170
|
+
const pathname = url.pathname;
|
|
171
|
+
|
|
172
|
+
const file = this.mapNameToFile.get(pathname);
|
|
173
|
+
pResOut.setInt32(0, file ? 1 : 0, true);
|
|
174
|
+
return VFS.SQLITE_OK;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,437 @@
|
|
|
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';
|
|
5
|
+
|
|
6
|
+
const LOCK_NOTIFY_INTERVAL = 1000;
|
|
7
|
+
|
|
8
|
+
const hasUnsafeAccessHandle =
|
|
9
|
+
globalThis.FileSystemSyncAccessHandle.prototype.hasOwnProperty('mode');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} pathname
|
|
13
|
+
* @param {boolean} create
|
|
14
|
+
* @returns {Promise<[FileSystemDirectoryHandle, string]>}
|
|
15
|
+
*/
|
|
16
|
+
async function getPathComponents(pathname, create) {
|
|
17
|
+
const [_, directories, filename] = pathname.match(/[/]?(.*)[/](.*)$/);
|
|
18
|
+
|
|
19
|
+
let directoryHandle = await navigator.storage.getDirectory();
|
|
20
|
+
for (const directory of directories.split('/')) {
|
|
21
|
+
if (directory) {
|
|
22
|
+
directoryHandle = await directoryHandle.getDirectoryHandle(directory, { create });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [directoryHandle, filename];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
class File {
|
|
29
|
+
/** @type {string} */ pathname;
|
|
30
|
+
/** @type {number} */ flags;
|
|
31
|
+
/** @type {FileSystemFileHandle} */ fileHandle;
|
|
32
|
+
/** @type {FileSystemSyncAccessHandle} */ accessHandle;
|
|
33
|
+
|
|
34
|
+
// The rest of the properties are for platforms without readwrite-unsafe
|
|
35
|
+
// access handles. Only one connection can have an open access handle
|
|
36
|
+
// so coordination is needed in addition to the SQLite locking model.
|
|
37
|
+
//
|
|
38
|
+
// Opening and closing the access handle is expensive so we leave the
|
|
39
|
+
// handle open unless another connection signals on BroadcastChannel.
|
|
40
|
+
/** @type {BroadcastChannel} */ handleRequestChannel;
|
|
41
|
+
/** @type {function} */ handleLockReleaser = null;
|
|
42
|
+
/** @type {boolean} */ isHandleRequested = false;
|
|
43
|
+
/** @type {boolean} */ isFileLocked = false;
|
|
44
|
+
|
|
45
|
+
// SQLite makes one read on file open that is not protected by a lock.
|
|
46
|
+
// This needs to be handled as a special case.
|
|
47
|
+
/** @type {function} */ openLockReleaser = null;
|
|
48
|
+
|
|
49
|
+
constructor(pathname, flags) {
|
|
50
|
+
this.pathname = pathname;
|
|
51
|
+
this.flags = flags;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class OPFSAdaptiveVFS extends WebLocksMixin(FacadeVFS) {
|
|
56
|
+
/** @type {Map<number, File>} */ mapIdToFile = new Map();
|
|
57
|
+
lastError = null;
|
|
58
|
+
|
|
59
|
+
log = null;
|
|
60
|
+
|
|
61
|
+
static async create(name, module, options) {
|
|
62
|
+
const vfs = new OPFSAdaptiveVFS(name, module, options);
|
|
63
|
+
await vfs.isReady();
|
|
64
|
+
return vfs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
constructor(name, module, options = {}) {
|
|
68
|
+
super(name, module, options);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getFilename(fileId) {
|
|
72
|
+
const pathname = this.mapIdToFile.get(fileId).pathname;
|
|
73
|
+
return `OPFS:${pathname}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string?} zName
|
|
78
|
+
* @param {number} fileId
|
|
79
|
+
* @param {number} flags
|
|
80
|
+
* @param {DataView} pOutFlags
|
|
81
|
+
* @returns {Promise<number>}
|
|
82
|
+
*/
|
|
83
|
+
async jOpen(zName, fileId, flags, pOutFlags) {
|
|
84
|
+
try {
|
|
85
|
+
const url = new URL(zName || Math.random().toString(36).slice(2), 'file://');
|
|
86
|
+
const pathname = url.pathname;
|
|
87
|
+
|
|
88
|
+
const file = new File(pathname, flags);
|
|
89
|
+
this.mapIdToFile.set(fileId, file);
|
|
90
|
+
|
|
91
|
+
const create = !!(flags & VFS.SQLITE_OPEN_CREATE);
|
|
92
|
+
const [directoryHandle, filename] = await getPathComponents(pathname, create);
|
|
93
|
+
file.fileHandle = await directoryHandle.getFileHandle(filename, { create });
|
|
94
|
+
|
|
95
|
+
if ((flags & VFS.SQLITE_OPEN_MAIN_DB) && !hasUnsafeAccessHandle) {
|
|
96
|
+
file.handleRequestChannel = new BroadcastChannel(this.getFilename(fileId));
|
|
97
|
+
|
|
98
|
+
// Acquire the access handle lock. The first read of a database
|
|
99
|
+
// file is done outside xLock/xUnlock so we get that lock here.
|
|
100
|
+
function notify() {
|
|
101
|
+
file.handleRequestChannel.postMessage(null);
|
|
102
|
+
}
|
|
103
|
+
const notifyId = setInterval(notify, LOCK_NOTIFY_INTERVAL);
|
|
104
|
+
setTimeout(notify);
|
|
105
|
+
|
|
106
|
+
file.openLockReleaser = await new Promise((resolve, reject) => {
|
|
107
|
+
navigator.locks.request(this.getFilename(fileId), lock => {
|
|
108
|
+
clearInterval(notifyId);
|
|
109
|
+
if (!lock) return reject();
|
|
110
|
+
return new Promise(release => {
|
|
111
|
+
resolve(release);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
this.log?.('access handle acquired for open');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// @ts-ignore
|
|
119
|
+
file.accessHandle = await file.fileHandle.createSyncAccessHandle({
|
|
120
|
+
mode: 'readwrite-unsafe'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
pOutFlags.setInt32(0, flags, true);
|
|
124
|
+
return VFS.SQLITE_OK;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
this.lastError = e;
|
|
127
|
+
return VFS.SQLITE_CANTOPEN;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} zName
|
|
133
|
+
* @param {number} syncDir
|
|
134
|
+
* @returns {Promise<number>}
|
|
135
|
+
*/
|
|
136
|
+
async jDelete(zName, syncDir) {
|
|
137
|
+
try {
|
|
138
|
+
const url = new URL(zName, 'file://');
|
|
139
|
+
const pathname = url.pathname;
|
|
140
|
+
|
|
141
|
+
const [directoryHandle, name] = await getPathComponents(pathname, false);
|
|
142
|
+
const result = directoryHandle.removeEntry(name, { recursive: false });
|
|
143
|
+
if (syncDir) {
|
|
144
|
+
await result;
|
|
145
|
+
}
|
|
146
|
+
return VFS.SQLITE_OK;
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return VFS.SQLITE_IOERR_DELETE;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} zName
|
|
154
|
+
* @param {number} flags
|
|
155
|
+
* @param {DataView} pResOut
|
|
156
|
+
* @returns {Promise<number>}
|
|
157
|
+
*/
|
|
158
|
+
async jAccess(zName, flags, pResOut) {
|
|
159
|
+
try {
|
|
160
|
+
const url = new URL(zName, 'file://');
|
|
161
|
+
const pathname = url.pathname;
|
|
162
|
+
|
|
163
|
+
const [directoryHandle, dbName] = await getPathComponents(pathname, false);
|
|
164
|
+
const fileHandle = await directoryHandle.getFileHandle(dbName, { create: false });
|
|
165
|
+
pResOut.setInt32(0, 1, true);
|
|
166
|
+
return VFS.SQLITE_OK;
|
|
167
|
+
} catch (e) {
|
|
168
|
+
if (e.name === 'NotFoundError') {
|
|
169
|
+
pResOut.setInt32(0, 0, true);
|
|
170
|
+
return VFS.SQLITE_OK;
|
|
171
|
+
}
|
|
172
|
+
this.lastError = e;
|
|
173
|
+
return VFS.SQLITE_IOERR_ACCESS;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {number} fileId
|
|
179
|
+
* @returns {Promise<number>}
|
|
180
|
+
*/
|
|
181
|
+
async jClose(fileId) {
|
|
182
|
+
try {
|
|
183
|
+
const file = this.mapIdToFile.get(fileId);
|
|
184
|
+
this.mapIdToFile.delete(fileId);
|
|
185
|
+
await file?.accessHandle?.close();
|
|
186
|
+
|
|
187
|
+
if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
|
|
188
|
+
const [directoryHandle, name] = await getPathComponents(file.pathname, false);
|
|
189
|
+
await directoryHandle.removeEntry(name, { recursive: false });
|
|
190
|
+
}
|
|
191
|
+
return VFS.SQLITE_OK;
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return VFS.SQLITE_IOERR_DELETE;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {number} fileId
|
|
199
|
+
* @param {Uint8Array} pData
|
|
200
|
+
* @param {number} iOffset
|
|
201
|
+
* @returns {number}
|
|
202
|
+
*/
|
|
203
|
+
jRead(fileId, pData, iOffset) {
|
|
204
|
+
try {
|
|
205
|
+
const file = this.mapIdToFile.get(fileId);
|
|
206
|
+
|
|
207
|
+
// On Chrome (at least), passing pData to accessHandle.read() is
|
|
208
|
+
// an error because pData is a Proxy of a Uint8Array. Calling
|
|
209
|
+
// subarray() produces a real Uint8Array and that works.
|
|
210
|
+
const bytesRead = file.accessHandle.read(pData.subarray(), { at: iOffset });
|
|
211
|
+
if (file.openLockReleaser) {
|
|
212
|
+
// We obtained the access handle on file open.
|
|
213
|
+
file.accessHandle.close();
|
|
214
|
+
file.accessHandle = null;
|
|
215
|
+
file.openLockReleaser();
|
|
216
|
+
file.openLockReleaser = null;
|
|
217
|
+
this.log?.('access handle released for open');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (bytesRead < pData.byteLength) {
|
|
221
|
+
pData.fill(0, bytesRead);
|
|
222
|
+
return VFS.SQLITE_IOERR_SHORT_READ;
|
|
223
|
+
}
|
|
224
|
+
return VFS.SQLITE_OK;
|
|
225
|
+
} catch (e) {
|
|
226
|
+
this.lastError = e;
|
|
227
|
+
return VFS.SQLITE_IOERR_READ;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {number} fileId
|
|
233
|
+
* @param {Uint8Array} pData
|
|
234
|
+
* @param {number} iOffset
|
|
235
|
+
* @returns {number}
|
|
236
|
+
*/
|
|
237
|
+
jWrite(fileId, pData, iOffset) {
|
|
238
|
+
try {
|
|
239
|
+
const file = this.mapIdToFile.get(fileId);
|
|
240
|
+
|
|
241
|
+
// On Chrome (at least), passing pData to accessHandle.write() is
|
|
242
|
+
// an error because pData is a Proxy of a Uint8Array. Calling
|
|
243
|
+
// subarray() produces a real Uint8Array and that works.
|
|
244
|
+
file.accessHandle.write(pData.subarray(), { at: iOffset });
|
|
245
|
+
return VFS.SQLITE_OK;
|
|
246
|
+
} catch (e) {
|
|
247
|
+
this.lastError = e;
|
|
248
|
+
return VFS.SQLITE_IOERR_WRITE;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @param {number} fileId
|
|
254
|
+
* @param {number} iSize
|
|
255
|
+
* @returns {number}
|
|
256
|
+
*/
|
|
257
|
+
jTruncate(fileId, iSize) {
|
|
258
|
+
try {
|
|
259
|
+
const file = this.mapIdToFile.get(fileId);
|
|
260
|
+
file.accessHandle.truncate(iSize);
|
|
261
|
+
return VFS.SQLITE_OK;
|
|
262
|
+
} catch (e) {
|
|
263
|
+
this.lastError = e;
|
|
264
|
+
return VFS.SQLITE_IOERR_TRUNCATE;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @param {number} fileId
|
|
270
|
+
* @param {number} flags
|
|
271
|
+
* @returns {number}
|
|
272
|
+
*/
|
|
273
|
+
jSync(fileId, flags) {
|
|
274
|
+
try {
|
|
275
|
+
const file = this.mapIdToFile.get(fileId);
|
|
276
|
+
file.accessHandle.flush();
|
|
277
|
+
return VFS.SQLITE_OK;
|
|
278
|
+
} catch (e) {
|
|
279
|
+
this.lastError = e;
|
|
280
|
+
return VFS.SQLITE_IOERR_FSYNC;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {number} fileId
|
|
286
|
+
* @param {DataView} pSize64
|
|
287
|
+
* @returns {number}
|
|
288
|
+
*/
|
|
289
|
+
jFileSize(fileId, pSize64) {
|
|
290
|
+
try {
|
|
291
|
+
const file = this.mapIdToFile.get(fileId);
|
|
292
|
+
const size = file.accessHandle.getSize();
|
|
293
|
+
pSize64.setBigInt64(0, BigInt(size), true);
|
|
294
|
+
return VFS.SQLITE_OK;
|
|
295
|
+
} catch (e) {
|
|
296
|
+
this.lastError = e;
|
|
297
|
+
return VFS.SQLITE_IOERR_FSTAT;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {number} fileId
|
|
303
|
+
* @param {number} lockType
|
|
304
|
+
* @returns {Promise<number>}
|
|
305
|
+
*/
|
|
306
|
+
async jLock(fileId, lockType) {
|
|
307
|
+
if (hasUnsafeAccessHandle) return super.jLock(fileId, lockType);
|
|
308
|
+
|
|
309
|
+
const file = this.mapIdToFile.get(fileId);
|
|
310
|
+
if (!file.isFileLocked) {
|
|
311
|
+
file.isFileLocked = true;
|
|
312
|
+
if (!file.handleLockReleaser) {
|
|
313
|
+
// Listen for other connections wanting the access handle.
|
|
314
|
+
file.handleRequestChannel.onmessage = event => {
|
|
315
|
+
if(!file.isFileLocked) {
|
|
316
|
+
// We have the access handle but the file is not locked.
|
|
317
|
+
// Release the access handle for the requester.
|
|
318
|
+
file.accessHandle.close();
|
|
319
|
+
file.accessHandle = null;
|
|
320
|
+
file.handleLockReleaser();
|
|
321
|
+
file.handleLockReleaser = null;
|
|
322
|
+
this.log?.('access handle requested and released');
|
|
323
|
+
} else {
|
|
324
|
+
// We're still using the access handle, so mark it to be
|
|
325
|
+
// released when we're done.
|
|
326
|
+
file.isHandleRequested = true;
|
|
327
|
+
this.log?.('access handle requested');
|
|
328
|
+
}
|
|
329
|
+
file.handleRequestChannel.onmessage = null;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// We don't have the access handle. First acquire the lock.
|
|
333
|
+
file.handleLockReleaser = await new Promise((resolve, reject) => {
|
|
334
|
+
// Tell everyone we want the access handle.
|
|
335
|
+
function notify() {
|
|
336
|
+
file.handleRequestChannel.postMessage(null);
|
|
337
|
+
}
|
|
338
|
+
const notifyId = setInterval(notify, LOCK_NOTIFY_INTERVAL);
|
|
339
|
+
setTimeout(notify);
|
|
340
|
+
|
|
341
|
+
navigator.locks.request(this.getFilename(fileId), lock => {
|
|
342
|
+
clearInterval(notifyId);
|
|
343
|
+
if (!lock) return reject();
|
|
344
|
+
return new Promise(release => {
|
|
345
|
+
resolve(release);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// The access handle should now be available.
|
|
351
|
+
file.accessHandle = await file.fileHandle.createSyncAccessHandle();
|
|
352
|
+
this.log?.('access handle acquired');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
}
|
|
356
|
+
return VFS.SQLITE_OK;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {number} fileId
|
|
361
|
+
* @param {number} lockType
|
|
362
|
+
* @returns {Promise<number>}
|
|
363
|
+
*/
|
|
364
|
+
async jUnlock(fileId, lockType) {
|
|
365
|
+
if (hasUnsafeAccessHandle) return super.jUnlock(fileId, lockType);
|
|
366
|
+
|
|
367
|
+
if (lockType === VFS.SQLITE_LOCK_NONE) {
|
|
368
|
+
const file = this.mapIdToFile.get(fileId);
|
|
369
|
+
if (file.isHandleRequested) {
|
|
370
|
+
if (file.handleLockReleaser) {
|
|
371
|
+
// Another connection wants the access handle.
|
|
372
|
+
file.accessHandle.close();
|
|
373
|
+
file.accessHandle = null;
|
|
374
|
+
file.handleLockReleaser();
|
|
375
|
+
file.handleLockReleaser = null;
|
|
376
|
+
this.log?.('access handle released');
|
|
377
|
+
}
|
|
378
|
+
file.isHandleRequested = false;
|
|
379
|
+
}
|
|
380
|
+
file.isFileLocked = false;
|
|
381
|
+
}
|
|
382
|
+
return VFS.SQLITE_OK;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @param {number} fileId
|
|
387
|
+
* @param {number} op
|
|
388
|
+
* @param {DataView} pArg
|
|
389
|
+
* @returns {number|Promise<number>}
|
|
390
|
+
*/
|
|
391
|
+
jFileControl(fileId, op, pArg) {
|
|
392
|
+
try {
|
|
393
|
+
const file = this.mapIdToFile.get(fileId);
|
|
394
|
+
switch (op) {
|
|
395
|
+
case VFS.SQLITE_FCNTL_PRAGMA:
|
|
396
|
+
const key = extractString(pArg, 4);
|
|
397
|
+
const value = extractString(pArg, 8);
|
|
398
|
+
this.log?.('xFileControl', file.pathname, 'PRAGMA', key, value);
|
|
399
|
+
switch (key.toLowerCase()) {
|
|
400
|
+
case 'journal_mode':
|
|
401
|
+
if (value &&
|
|
402
|
+
!hasUnsafeAccessHandle &&
|
|
403
|
+
!['off', 'memory', 'delete', 'wal'].includes(value.toLowerCase())) {
|
|
404
|
+
throw new Error('journal_mode must be "off", "memory", "delete", or "wal"');
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
case 'write_hint':
|
|
408
|
+
return super.jFileControl(fileId, WebLocksMixin.WRITE_HINT_OP_CODE, null);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
} catch (e) {
|
|
413
|
+
this.lastError = e;
|
|
414
|
+
return VFS.SQLITE_IOERR;
|
|
415
|
+
}
|
|
416
|
+
return VFS.SQLITE_NOTFOUND;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
jGetLastError(zBuf) {
|
|
420
|
+
if (this.lastError) {
|
|
421
|
+
console.error(this.lastError);
|
|
422
|
+
const outputArray = zBuf.subarray(0, zBuf.byteLength - 1);
|
|
423
|
+
const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray);
|
|
424
|
+
zBuf[written] = 0;
|
|
425
|
+
}
|
|
426
|
+
return VFS.SQLITE_OK
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function extractString(dataView, offset) {
|
|
431
|
+
const p = dataView.getUint32(offset, true);
|
|
432
|
+
if (p) {
|
|
433
|
+
const chars = new Uint8Array(dataView.buffer, p);
|
|
434
|
+
return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)));
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|