@moyodb/sdk 1.1.0 → 1.4.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/CONTRIBUTING.md +46 -0
- package/README.md +144 -0
- package/dist/assets/browser-worker-Bj2ErRvC.js +4 -0
- package/dist/assets/browser-worker-Bj2ErRvC.js.map +1 -0
- package/dist/assets/shared-worker-CMeTcusa.js +2 -0
- package/dist/assets/shared-worker-CMeTcusa.js.map +1 -0
- package/dist/chunks/{indexing-BzmFhWXs.js → indexing-B0duqlUs.js} +131 -124
- package/dist/chunks/indexing-B0duqlUs.js.map +1 -0
- package/dist/compression.d.ts +16 -2
- package/dist/compression.d.ts.map +1 -1
- package/dist/demo.d.ts.map +1 -1
- package/dist/engine/README.md +32 -3
- package/dist/engine/moyodb_engine.d.ts +36 -10
- package/dist/engine/moyodb_engine.js +261 -51
- package/dist/engine/moyodb_engine_bg.wasm +0 -0
- package/dist/engine/moyodb_engine_bg.wasm.d.ts +18 -10
- package/dist/engine/package.json +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/indexing.d.ts.map +1 -1
- package/dist/internal.d.ts +6 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/moyodb-sdk.js +1069 -835
- package/dist/moyodb-sdk.js.map +1 -1
- package/dist/node-engine.js +731 -111
- package/dist/node-engine.js.map +1 -1
- package/dist/node-storage.d.mts +4 -0
- package/dist/node-storage.mjs +38 -3
- package/dist/node-worker-client.d.ts +1 -0
- package/dist/node-worker-client.d.ts.map +1 -1
- package/dist/node-worker.mjs +2 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +302 -67
- package/dist/node.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/shared-worker-client.d.ts +4 -0
- package/dist/shared-worker-client.d.ts.map +1 -1
- package/dist/shared-worker.d.ts.map +1 -1
- package/dist/snappy.d.ts +3 -0
- package/dist/snappy.d.ts.map +1 -0
- package/dist/sql-parser.d.ts.map +1 -1
- package/dist/types.d.ts +33 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-api.d.ts +19 -6
- package/dist/worker-api.d.ts.map +1 -1
- package/dist/worker-client.d.ts +7 -2
- package/dist/worker-client.d.ts.map +1 -1
- package/dist/worker-protocol.d.ts +18 -3
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-server.d.ts.map +1 -1
- package/dist/worker.d.ts +33 -4
- package/dist/worker.d.ts.map +1 -1
- package/package.json +6 -5
- package/dist/assets/browser-worker-DP6Mjz7c.js +0 -4
- package/dist/assets/browser-worker-DP6Mjz7c.js.map +0 -1
- package/dist/assets/shared-worker-DKzhauVf.js +0 -2
- package/dist/assets/shared-worker-DKzhauVf.js.map +0 -1
- package/dist/chunks/indexing-BzmFhWXs.js.map +0 -1
- package/dist/engine/snippets/moyodb-engine-cff0a0e2441a7a8f/js/opfs_shim.js +0 -513
- /package/dist/engine/snippets/{moyodb-engine-8ebe6af0cc2e81b7 → moyodb-engine-1d53039816cbc2f7}/js/opfs_shim.js +0 -0
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
const DB_ROOT_DIR = 'stackdb';
|
|
2
|
-
const FILE_NAMES = ['manifest.bin', 'main.bin', 'wal.bin'];
|
|
3
|
-
const CONTROL_FILE_NAME = 'root-manifest.bin';
|
|
4
|
-
const CONTROL_SLOT_SIZE = 4096;
|
|
5
|
-
const CONTROL_VERSION = 1;
|
|
6
|
-
const CONTROL_CHECKSUM_OFFSET = 24;
|
|
7
|
-
const CONTROL_NAME_OFFSET = 32;
|
|
8
|
-
const CONTROL_MAGIC = new Uint8Array([66, 68, 66, 82, 79, 79, 84, 49]);
|
|
9
|
-
const OPFS_WRITE_CHUNK_SIZE = 256 * 1024;
|
|
10
|
-
const TEXT_ENCODER = new TextEncoder();
|
|
11
|
-
const TEXT_DECODER = new TextDecoder();
|
|
12
|
-
let rootDir = null;
|
|
13
|
-
let nextSessionId = 1;
|
|
14
|
-
const sessions = new Map();
|
|
15
|
-
function namedError(name, message) {
|
|
16
|
-
const error = new Error(message);
|
|
17
|
-
error.name = name;
|
|
18
|
-
error.code = name;
|
|
19
|
-
return error;
|
|
20
|
-
}
|
|
21
|
-
function corruptionError(message) {
|
|
22
|
-
return namedError('CorruptionError', message);
|
|
23
|
-
}
|
|
24
|
-
async function getRootDir() {
|
|
25
|
-
if (!navigator?.storage?.getDirectory) {
|
|
26
|
-
throw new Error('navigator.storage.getDirectory is unavailable');
|
|
27
|
-
}
|
|
28
|
-
if (!rootDir) {
|
|
29
|
-
rootDir = await navigator.storage.getDirectory();
|
|
30
|
-
}
|
|
31
|
-
return rootDir;
|
|
32
|
-
}
|
|
33
|
-
async function getOrCreateStackdbRoot() {
|
|
34
|
-
const root = await getRootDir();
|
|
35
|
-
return await root.getDirectoryHandle(DB_ROOT_DIR, { create: true });
|
|
36
|
-
}
|
|
37
|
-
function isNotFoundError(error) {
|
|
38
|
-
return error?.name === 'NotFoundError' || error?.name === 'TypeMismatchError';
|
|
39
|
-
}
|
|
40
|
-
async function lookupDirectoryHandle(parent, name) {
|
|
41
|
-
try {
|
|
42
|
-
return await parent.getDirectoryHandle(name, { create: false });
|
|
43
|
-
} catch (err) {
|
|
44
|
-
if (isNotFoundError(err)) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
throw err;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
async function lookupFileHandle(parent, name) {
|
|
51
|
-
try {
|
|
52
|
-
return await parent.getFileHandle(name, { create: false });
|
|
53
|
-
} catch (err) {
|
|
54
|
-
if (isNotFoundError(err)) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
throw err;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function sessionPath(encodedDbName, generationName = null) {
|
|
61
|
-
return generationName ? `${encodedDbName}/${generationName}` : encodedDbName;
|
|
62
|
-
}
|
|
63
|
-
function slotBytesView(bytes) {
|
|
64
|
-
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
65
|
-
}
|
|
66
|
-
function writeU64(view, offset, value) {
|
|
67
|
-
view.setBigUint64(offset, BigInt(value), true);
|
|
68
|
-
}
|
|
69
|
-
function readU64(view, offset) {
|
|
70
|
-
return Number(view.getBigUint64(offset, true));
|
|
71
|
-
}
|
|
72
|
-
function fnv1a32(bytes, zeroOffset = -1, zeroLength = 0) {
|
|
73
|
-
let hash = 0x811c9dc5;
|
|
74
|
-
const zeroEnd = zeroOffset >= 0 ? zeroOffset + zeroLength : -1;
|
|
75
|
-
for (let index = 0; index < bytes.length; index += 1) {
|
|
76
|
-
const value = zeroOffset >= 0 && index >= zeroOffset && index < zeroEnd ? 0 : (bytes[index] ?? 0);
|
|
77
|
-
hash ^= value;
|
|
78
|
-
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
79
|
-
}
|
|
80
|
-
return hash >>> 0;
|
|
81
|
-
}
|
|
82
|
-
function isGenerationNameValid(name) {
|
|
83
|
-
return typeof name === 'string' && /^gen-[a-z0-9]+-[a-z0-9]+$/i.test(name);
|
|
84
|
-
}
|
|
85
|
-
// Every byte must reach the file or the call fails: a short write that returns
|
|
86
|
-
// normally would let callers publish state that is not on disk.
|
|
87
|
-
export function writeAll(handle, bytes, at) {
|
|
88
|
-
let writtenTotal = 0;
|
|
89
|
-
while (writtenTotal < bytes.length) {
|
|
90
|
-
const chunk = bytes.subarray(writtenTotal, writtenTotal + OPFS_WRITE_CHUNK_SIZE);
|
|
91
|
-
const rawWritten = handle.write(chunk, { at: at + writtenTotal });
|
|
92
|
-
const written = Number(rawWritten);
|
|
93
|
-
if (!Number.isSafeInteger(written) || written < 0 || written > chunk.length) {
|
|
94
|
-
throw namedError('StorageError', `opfs write failed: invalid byte count ${String(rawWritten)}`);
|
|
95
|
-
}
|
|
96
|
-
if (written === 0 && chunk.length > 0) {
|
|
97
|
-
throw namedError('StorageError', 'opfs write failed: wrote 0 bytes');
|
|
98
|
-
}
|
|
99
|
-
writtenTotal += written;
|
|
100
|
-
}
|
|
101
|
-
return writtenTotal;
|
|
102
|
-
}
|
|
103
|
-
// Fills `buffer` from `at`, stopping only at end of file. Bytes past EOF stay zero.
|
|
104
|
-
export function readAll(handle, buffer, at) {
|
|
105
|
-
let readTotal = 0;
|
|
106
|
-
while (readTotal < buffer.length) {
|
|
107
|
-
const rawRead = handle.read(buffer.subarray(readTotal), { at: at + readTotal });
|
|
108
|
-
const read = Number(rawRead);
|
|
109
|
-
if (!Number.isSafeInteger(read) || read < 0 || read > buffer.length - readTotal) {
|
|
110
|
-
throw namedError('StorageError', `opfs read failed: invalid byte count ${String(rawRead)}`);
|
|
111
|
-
}
|
|
112
|
-
if (read === 0) {
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
readTotal += read;
|
|
116
|
-
}
|
|
117
|
-
return readTotal;
|
|
118
|
-
}
|
|
119
|
-
export function encodeControlSlot(generationCounter, activeGeneration) {
|
|
120
|
-
if (!isGenerationNameValid(activeGeneration)) {
|
|
121
|
-
throw new Error(`invalid generation name ${activeGeneration}`);
|
|
122
|
-
}
|
|
123
|
-
const nameBytes = TEXT_ENCODER.encode(activeGeneration);
|
|
124
|
-
if (CONTROL_NAME_OFFSET + nameBytes.length > CONTROL_SLOT_SIZE) {
|
|
125
|
-
throw new Error(`generation name too long: ${nameBytes.length}`);
|
|
126
|
-
}
|
|
127
|
-
const slot = new Uint8Array(CONTROL_SLOT_SIZE);
|
|
128
|
-
const view = slotBytesView(slot);
|
|
129
|
-
slot.set(CONTROL_MAGIC, 0);
|
|
130
|
-
view.setUint32(8, CONTROL_VERSION, true);
|
|
131
|
-
writeU64(view, 12, generationCounter);
|
|
132
|
-
view.setUint16(20, nameBytes.length, true);
|
|
133
|
-
view.setUint16(22, 0, true);
|
|
134
|
-
view.setUint32(CONTROL_CHECKSUM_OFFSET, 0, true);
|
|
135
|
-
view.setUint32(28, 0, true);
|
|
136
|
-
slot.set(nameBytes, CONTROL_NAME_OFFSET);
|
|
137
|
-
const checksum = fnv1a32(slot, CONTROL_CHECKSUM_OFFSET, 4);
|
|
138
|
-
view.setUint32(CONTROL_CHECKSUM_OFFSET, checksum, true);
|
|
139
|
-
return slot;
|
|
140
|
-
}
|
|
141
|
-
// Returns null for any slot whose bytes do not checksum. Fields are interpreted
|
|
142
|
-
// only after the checksum passes, so a torn slot never masks its valid sibling.
|
|
143
|
-
export function decodeControlSlot(slotIndex, bytes) {
|
|
144
|
-
if (bytes.length < CONTROL_SLOT_SIZE) {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
for (let index = 0; index < CONTROL_MAGIC.length; index += 1) {
|
|
148
|
-
if (bytes[index] !== CONTROL_MAGIC[index]) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const view = slotBytesView(bytes);
|
|
153
|
-
const expectedChecksum = fnv1a32(bytes.subarray(0, CONTROL_SLOT_SIZE), CONTROL_CHECKSUM_OFFSET, 4);
|
|
154
|
-
const storedChecksum = view.getUint32(CONTROL_CHECKSUM_OFFSET, true);
|
|
155
|
-
if (expectedChecksum !== storedChecksum) {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
const version = view.getUint32(8, true);
|
|
159
|
-
if (version !== CONTROL_VERSION) {
|
|
160
|
-
throw corruptionError(`unsupported control file version ${version}`);
|
|
161
|
-
}
|
|
162
|
-
const nameLength = view.getUint16(20, true);
|
|
163
|
-
const nameEnd = CONTROL_NAME_OFFSET + nameLength;
|
|
164
|
-
if (nameEnd > CONTROL_SLOT_SIZE) {
|
|
165
|
-
throw corruptionError(`control slot name length out of bounds: ${nameLength}`);
|
|
166
|
-
}
|
|
167
|
-
const activeGeneration = TEXT_DECODER.decode(bytes.subarray(CONTROL_NAME_OFFSET, nameEnd));
|
|
168
|
-
if (!isGenerationNameValid(activeGeneration)) {
|
|
169
|
-
throw corruptionError(`invalid generation in control slot: ${activeGeneration}`);
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
slotIndex,
|
|
173
|
-
generationCounter: readU64(view, 12),
|
|
174
|
-
activeGeneration
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
// `absent` — the file is empty: no generation has ever been published.
|
|
178
|
-
// `invalid` — the file has bytes but no slot checksums.
|
|
179
|
-
// `valid` — `control` is the newest checksummed slot.
|
|
180
|
-
export function readControlStateFromAccessHandle(accessHandle) {
|
|
181
|
-
const size = Number(accessHandle.getSize());
|
|
182
|
-
if (!Number.isSafeInteger(size) || size < 0) {
|
|
183
|
-
throw namedError('StorageError', `invalid control file size ${String(size)}`);
|
|
184
|
-
}
|
|
185
|
-
if (size === 0) {
|
|
186
|
-
return { status: 'absent', control: null };
|
|
187
|
-
}
|
|
188
|
-
const buffer = new Uint8Array(CONTROL_SLOT_SIZE * 2);
|
|
189
|
-
readAll(accessHandle, buffer.subarray(0, Math.min(buffer.length, size)), 0);
|
|
190
|
-
const slot0 = decodeControlSlot(0, buffer.subarray(0, CONTROL_SLOT_SIZE));
|
|
191
|
-
const slot1 = decodeControlSlot(1, buffer.subarray(CONTROL_SLOT_SIZE, CONTROL_SLOT_SIZE * 2));
|
|
192
|
-
if (slot0 && slot1) {
|
|
193
|
-
return { status: 'valid', control: slot0.generationCounter >= slot1.generationCounter ? slot0 : slot1 };
|
|
194
|
-
}
|
|
195
|
-
const control = slot0 ?? slot1;
|
|
196
|
-
return control ? { status: 'valid', control } : { status: 'invalid', control: null };
|
|
197
|
-
}
|
|
198
|
-
async function readControlFile(dbRoot) {
|
|
199
|
-
const fileHandle = await lookupFileHandle(dbRoot, CONTROL_FILE_NAME);
|
|
200
|
-
if (!fileHandle) {
|
|
201
|
-
return { status: 'absent', control: null };
|
|
202
|
-
}
|
|
203
|
-
const accessHandle = await fileHandle.createSyncAccessHandle();
|
|
204
|
-
try {
|
|
205
|
-
return readControlStateFromAccessHandle(accessHandle);
|
|
206
|
-
} finally {
|
|
207
|
-
accessHandle.close();
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
async function hasLegacyDataFiles(dbRoot) {
|
|
211
|
-
for (const fileName of FILE_NAMES) {
|
|
212
|
-
if (await lookupFileHandle(dbRoot, fileName)) {
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
218
|
-
// Resolves which directory holds the live database, failing closed on damage.
|
|
219
|
-
//
|
|
220
|
-
// Legacy root files are removed before the first write into a published
|
|
221
|
-
// generation (see opfsCleanupInactiveEntries and opfsOpenActiveDb). So an
|
|
222
|
-
// unreadable control file next to legacy files can only be a torn first
|
|
223
|
-
// publication, and legacy is still authoritative. Without legacy files it is
|
|
224
|
-
// real corruption of the pointer to live data.
|
|
225
|
-
async function resolveActiveGeneration(dbRoot) {
|
|
226
|
-
const state = await readControlFile(dbRoot);
|
|
227
|
-
if (state.status === 'valid') {
|
|
228
|
-
return state.control.activeGeneration;
|
|
229
|
-
}
|
|
230
|
-
if (state.status === 'absent') {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
if (await hasLegacyDataFiles(dbRoot)) {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
throw corruptionError(`control file ${CONTROL_FILE_NAME} is corrupt: no slot has a valid checksum`);
|
|
237
|
-
}
|
|
238
|
-
async function writeControlState(dbRoot, activeGeneration, expectedCurrentGeneration) {
|
|
239
|
-
const fileHandle = await dbRoot.getFileHandle(CONTROL_FILE_NAME, { create: true });
|
|
240
|
-
const accessHandle = await fileHandle.createSyncAccessHandle();
|
|
241
|
-
try {
|
|
242
|
-
const current = readControlStateFromAccessHandle(accessHandle);
|
|
243
|
-
if (current.status === 'invalid' && !(await hasLegacyDataFiles(dbRoot))) {
|
|
244
|
-
throw corruptionError('refusing to publish a generation over a corrupt control file');
|
|
245
|
-
}
|
|
246
|
-
const currentGeneration = current.control?.activeGeneration ?? null;
|
|
247
|
-
if (expectedCurrentGeneration !== undefined && currentGeneration !== expectedCurrentGeneration) {
|
|
248
|
-
throw namedError(
|
|
249
|
-
'DatabaseBusyError',
|
|
250
|
-
`active generation changed underneath compaction: expected ${String(expectedCurrentGeneration)}, found ${String(currentGeneration)}`
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
const nextSlot = current.control ? (current.control.slotIndex === 0 ? 1 : 0) : 0;
|
|
254
|
-
const nextGenerationCounter = (current.control?.generationCounter ?? 0) + 1;
|
|
255
|
-
const encoded = encodeControlSlot(nextGenerationCounter, activeGeneration);
|
|
256
|
-
writeAll(accessHandle, encoded, nextSlot * CONTROL_SLOT_SIZE);
|
|
257
|
-
accessHandle.flush();
|
|
258
|
-
const published = readControlStateFromAccessHandle(accessHandle);
|
|
259
|
-
if (
|
|
260
|
-
published.status !== 'valid' ||
|
|
261
|
-
published.control.activeGeneration !== activeGeneration ||
|
|
262
|
-
published.control.generationCounter !== nextGenerationCounter
|
|
263
|
-
) {
|
|
264
|
-
throw namedError('StorageError', `control file does not select ${activeGeneration} after publication`);
|
|
265
|
-
}
|
|
266
|
-
} finally {
|
|
267
|
-
accessHandle.close();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
function createGenerationName() {
|
|
271
|
-
const timePart = Date.now().toString(36);
|
|
272
|
-
const randomBytes = new Uint8Array(4);
|
|
273
|
-
crypto.getRandomValues(randomBytes);
|
|
274
|
-
const randomPart = Array.from(randomBytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
275
|
-
return `gen-${timePart}-${randomPart}`;
|
|
276
|
-
}
|
|
277
|
-
async function getDbRoot(encodedDbName, createIfMissing) {
|
|
278
|
-
const stackdb = await getOrCreateStackdbRoot();
|
|
279
|
-
let dbRoot = await lookupDirectoryHandle(stackdb, encodedDbName);
|
|
280
|
-
if (!dbRoot) {
|
|
281
|
-
if (!createIfMissing) {
|
|
282
|
-
throw new Error(`database ${encodedDbName} does not exist`);
|
|
283
|
-
}
|
|
284
|
-
dbRoot = await stackdb.getDirectoryHandle(encodedDbName, { create: true });
|
|
285
|
-
}
|
|
286
|
-
return dbRoot;
|
|
287
|
-
}
|
|
288
|
-
async function removeLegacyDataFiles(dbRoot) {
|
|
289
|
-
for (const fileName of FILE_NAMES) {
|
|
290
|
-
try {
|
|
291
|
-
await dbRoot.removeEntry(fileName);
|
|
292
|
-
} catch (err) {
|
|
293
|
-
if (!isNotFoundError(err)) {
|
|
294
|
-
throw err;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
async function resolveActiveDataDir(dbRoot) {
|
|
300
|
-
const activeGeneration = await resolveActiveGeneration(dbRoot);
|
|
301
|
-
if (!activeGeneration) {
|
|
302
|
-
return {
|
|
303
|
-
dirHandle: dbRoot,
|
|
304
|
-
generationName: null,
|
|
305
|
-
path: dbRoot.name
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
const activeDir = await lookupDirectoryHandle(dbRoot, activeGeneration);
|
|
309
|
-
if (!activeDir) {
|
|
310
|
-
throw corruptionError(`active generation ${activeGeneration} is missing`);
|
|
311
|
-
}
|
|
312
|
-
// A crash after publication but before legacy cleanup leaves stale root
|
|
313
|
-
// files. They must be gone before this generation accepts writes.
|
|
314
|
-
await removeLegacyDataFiles(dbRoot);
|
|
315
|
-
return {
|
|
316
|
-
dirHandle: activeDir,
|
|
317
|
-
generationName: activeGeneration,
|
|
318
|
-
path: `${dbRoot.name}/${activeGeneration}`
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
function closeSession(sessionId) {
|
|
322
|
-
const session = sessions.get(sessionId);
|
|
323
|
-
if (!session) {
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
for (const handle of session.handles.values()) {
|
|
327
|
-
try {
|
|
328
|
-
handle.close();
|
|
329
|
-
} catch (_err) {}
|
|
330
|
-
}
|
|
331
|
-
sessions.delete(sessionId);
|
|
332
|
-
}
|
|
333
|
-
function closeSessionsForDb(encodedDbName) {
|
|
334
|
-
const prefix = `${encodedDbName}/`;
|
|
335
|
-
for (const [sessionId, session] of Array.from(sessions.entries())) {
|
|
336
|
-
if (session.path === encodedDbName || session.path.startsWith(prefix)) {
|
|
337
|
-
closeSession(sessionId);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
async function openSessionForDir(dbPath, dirHandle, generationName) {
|
|
342
|
-
const handles = new Map();
|
|
343
|
-
try {
|
|
344
|
-
for (let fileKind = 0; fileKind < FILE_NAMES.length; fileKind += 1) {
|
|
345
|
-
const fileName = FILE_NAMES[fileKind];
|
|
346
|
-
const fileHandle = await dirHandle.getFileHandle(fileName, { create: true });
|
|
347
|
-
if (typeof fileHandle.createSyncAccessHandle !== 'function') {
|
|
348
|
-
throw new Error('createSyncAccessHandle is unavailable');
|
|
349
|
-
}
|
|
350
|
-
const accessHandle = await fileHandle.createSyncAccessHandle();
|
|
351
|
-
handles.set(fileKind, accessHandle);
|
|
352
|
-
}
|
|
353
|
-
} catch (error) {
|
|
354
|
-
for (const handle of handles.values()) {
|
|
355
|
-
try {
|
|
356
|
-
handle.close();
|
|
357
|
-
} catch (_err) {}
|
|
358
|
-
}
|
|
359
|
-
throw error;
|
|
360
|
-
}
|
|
361
|
-
const sessionId = nextSessionId;
|
|
362
|
-
nextSessionId += 1;
|
|
363
|
-
sessions.set(sessionId, {
|
|
364
|
-
path: dbPath,
|
|
365
|
-
handles
|
|
366
|
-
});
|
|
367
|
-
return { sessionId, generationName };
|
|
368
|
-
}
|
|
369
|
-
function getSession(sessionId) {
|
|
370
|
-
const session = sessions.get(sessionId);
|
|
371
|
-
if (!session) {
|
|
372
|
-
throw namedError('StorageError', `no OPFS session ${sessionId}`);
|
|
373
|
-
}
|
|
374
|
-
return session;
|
|
375
|
-
}
|
|
376
|
-
function getAccessHandle(sessionId, fileKind) {
|
|
377
|
-
const session = getSession(sessionId);
|
|
378
|
-
const handle = session.handles.get(fileKind);
|
|
379
|
-
if (!handle) {
|
|
380
|
-
throw namedError('StorageError', `no OPFS access handle for session ${sessionId} file kind ${fileKind}`);
|
|
381
|
-
}
|
|
382
|
-
return handle;
|
|
383
|
-
}
|
|
384
|
-
function lookupOpenFileSize(path) {
|
|
385
|
-
for (const session of sessions.values()) {
|
|
386
|
-
for (let fileKind = 0; fileKind < FILE_NAMES.length; fileKind += 1) {
|
|
387
|
-
if (`${session.path}/${FILE_NAMES[fileKind]}` === path) {
|
|
388
|
-
return Number(session.handles.get(fileKind)?.getSize() ?? 0);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
async function sumDirectorySize(dirHandle, pathPrefix) {
|
|
395
|
-
let total = 0;
|
|
396
|
-
for await (const [name, handle] of dirHandle.entries()) {
|
|
397
|
-
const entryPath = pathPrefix ? `${pathPrefix}/${name}` : name;
|
|
398
|
-
if (handle.kind === 'directory') {
|
|
399
|
-
total += await sumDirectorySize(handle, entryPath);
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
const openSize = lookupOpenFileSize(entryPath);
|
|
403
|
-
if (openSize !== null) {
|
|
404
|
-
total += openSize;
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
const file = await handle.getFile();
|
|
408
|
-
total += file.size;
|
|
409
|
-
}
|
|
410
|
-
return total;
|
|
411
|
-
}
|
|
412
|
-
export async function opfsOpenActiveDb(encodedDbName, createIfMissing = true) {
|
|
413
|
-
const dbRoot = await getDbRoot(encodedDbName, createIfMissing);
|
|
414
|
-
const active = await resolveActiveDataDir(dbRoot);
|
|
415
|
-
return await openSessionForDir(active.path, active.dirHandle, active.generationName);
|
|
416
|
-
}
|
|
417
|
-
export async function opfsOpenGenerationDb(encodedDbName, generationName, createIfMissing = true) {
|
|
418
|
-
if (!isGenerationNameValid(generationName)) {
|
|
419
|
-
throw new Error(`invalid generation name ${generationName}`);
|
|
420
|
-
}
|
|
421
|
-
const dbRoot = await getDbRoot(encodedDbName, createIfMissing);
|
|
422
|
-
const generationDir = await dbRoot.getDirectoryHandle(generationName, { create: createIfMissing });
|
|
423
|
-
return await openSessionForDir(sessionPath(encodedDbName, generationName), generationDir, generationName);
|
|
424
|
-
}
|
|
425
|
-
export async function opfsReadActiveGeneration(encodedDbName) {
|
|
426
|
-
const dbRoot = await getDbRoot(encodedDbName, false);
|
|
427
|
-
return await resolveActiveGeneration(dbRoot);
|
|
428
|
-
}
|
|
429
|
-
export function opfsReadAt(sessionId, fileKind, offset, len) {
|
|
430
|
-
const handle = getAccessHandle(sessionId, fileKind);
|
|
431
|
-
const buffer = new Uint8Array(len);
|
|
432
|
-
readAll(handle, buffer, Number(offset));
|
|
433
|
-
return buffer;
|
|
434
|
-
}
|
|
435
|
-
export function opfsWriteAt(sessionId, fileKind, offset, bytes) {
|
|
436
|
-
return writeAll(getAccessHandle(sessionId, fileKind), bytes, Number(offset));
|
|
437
|
-
}
|
|
438
|
-
export function opfsFlush(sessionId, fileKind) {
|
|
439
|
-
getAccessHandle(sessionId, fileKind).flush();
|
|
440
|
-
}
|
|
441
|
-
export function opfsLen(sessionId, fileKind) {
|
|
442
|
-
return BigInt(getAccessHandle(sessionId, fileKind).getSize());
|
|
443
|
-
}
|
|
444
|
-
export function opfsTruncate(sessionId, fileKind, size) {
|
|
445
|
-
getAccessHandle(sessionId, fileKind).truncate(Number(size));
|
|
446
|
-
}
|
|
447
|
-
export function opfsCloseSession(sessionId) {
|
|
448
|
-
closeSession(sessionId);
|
|
449
|
-
}
|
|
450
|
-
export async function opfsPrepareRebuildTarget(encodedDbName) {
|
|
451
|
-
const dbRoot = await getDbRoot(encodedDbName, true);
|
|
452
|
-
let generationName = createGenerationName();
|
|
453
|
-
while (await lookupDirectoryHandle(dbRoot, generationName)) {
|
|
454
|
-
generationName = createGenerationName();
|
|
455
|
-
}
|
|
456
|
-
await dbRoot.getDirectoryHandle(generationName, { create: true });
|
|
457
|
-
return { generationName };
|
|
458
|
-
}
|
|
459
|
-
// Publishes `generationName` as the live database. Resolves only after the
|
|
460
|
-
// control file has been flushed and read back selecting the new generation;
|
|
461
|
-
// the caller may switch its live engine only after that.
|
|
462
|
-
export async function opfsSwapActiveGeneration(encodedDbName, generationName, expectedCurrentGeneration) {
|
|
463
|
-
if (!isGenerationNameValid(generationName)) {
|
|
464
|
-
throw new Error(`invalid generation name ${generationName}`);
|
|
465
|
-
}
|
|
466
|
-
const dbRoot = await getDbRoot(encodedDbName, false);
|
|
467
|
-
const generationDir = await lookupDirectoryHandle(dbRoot, generationName);
|
|
468
|
-
if (!generationDir) {
|
|
469
|
-
throw new Error(`generation ${generationName} does not exist`);
|
|
470
|
-
}
|
|
471
|
-
await writeControlState(dbRoot, generationName, expectedCurrentGeneration ?? null);
|
|
472
|
-
}
|
|
473
|
-
// Legacy root files are removed strictly (see resolveActiveGeneration); stale
|
|
474
|
-
// generation directories are best effort because nothing can select them.
|
|
475
|
-
export async function opfsCleanupInactiveEntries(encodedDbName) {
|
|
476
|
-
const dbRoot = await lookupDirectoryHandle(await getOrCreateStackdbRoot(), encodedDbName);
|
|
477
|
-
if (!dbRoot) {
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const activeGeneration = await resolveActiveGeneration(dbRoot);
|
|
481
|
-
if (activeGeneration) {
|
|
482
|
-
await removeLegacyDataFiles(dbRoot);
|
|
483
|
-
}
|
|
484
|
-
const staleDirectories = [];
|
|
485
|
-
for await (const [name, handle] of dbRoot.entries()) {
|
|
486
|
-
if (handle.kind === 'directory' && name.startsWith('gen-') && name !== activeGeneration) {
|
|
487
|
-
staleDirectories.push(name);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
for (const name of staleDirectories) {
|
|
491
|
-
try {
|
|
492
|
-
await dbRoot.removeEntry(name, { recursive: true });
|
|
493
|
-
} catch (_err) {}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
export async function opfsDbDirectorySize(encodedDbName) {
|
|
497
|
-
const dbRoot = await lookupDirectoryHandle(await getOrCreateStackdbRoot(), encodedDbName);
|
|
498
|
-
if (!dbRoot) {
|
|
499
|
-
return 0;
|
|
500
|
-
}
|
|
501
|
-
return await sumDirectorySize(dbRoot, encodedDbName);
|
|
502
|
-
}
|
|
503
|
-
export async function opfsRemoveDb(encodedDbName) {
|
|
504
|
-
closeSessionsForDb(encodedDbName);
|
|
505
|
-
const stackdb = await getOrCreateStackdbRoot();
|
|
506
|
-
try {
|
|
507
|
-
await stackdb.removeEntry(encodedDbName, { recursive: true });
|
|
508
|
-
} catch (err) {
|
|
509
|
-
if (!isNotFoundError(err)) {
|
|
510
|
-
throw err;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
File without changes
|