@moyodb/sdk 0.0.0-alpha.3 → 1.0.1
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 +202 -0
- package/README.md +10 -2
- package/dist/LICENSE +202 -0
- package/dist/assets/worker-KDc8Khl_.js +4 -0
- package/dist/assets/worker-KDc8Khl_.js.map +1 -0
- package/dist/change-events.d.ts.map +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/compression.d.ts.map +1 -1
- package/dist/engine/README.md +26 -9
- package/dist/engine/moyodb_engine.d.ts +125 -54
- package/dist/engine/moyodb_engine.js +935 -391
- package/dist/engine/moyodb_engine_bg.wasm +0 -0
- package/dist/engine/moyodb_engine_bg.wasm.d.ts +57 -47
- package/dist/engine/package.json +3 -3
- package/dist/engine/snippets/moyodb-engine-8ebe6af0cc2e81b7/js/opfs_shim.js +602 -0
- package/dist/engine/snippets/moyodb-engine-cff0a0e2441a7a8f/js/opfs_shim.js +192 -81
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/indexing.d.ts +8 -0
- package/dist/indexing.d.ts.map +1 -1
- package/dist/moyodb-sdk.js +2034 -1985
- package/dist/moyodb-sdk.js.map +1 -1
- package/dist/registry.d.ts +3 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/subscriptions.d.ts.map +1 -1
- package/dist/types.d.ts +29 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-api.d.ts +50 -37
- package/dist/worker-api.d.ts.map +1 -1
- package/dist/worker-client.d.ts +13 -2
- package/dist/worker-client.d.ts.map +1 -1
- package/dist/worker-protocol.d.ts +42 -3
- package/dist/worker-protocol.d.ts.map +1 -1
- package/dist/worker-server.d.ts.map +1 -1
- package/package.json +13 -9
- package/dist/assets/worker-CaOzbBz1.js +0 -2351
- package/dist/assets/worker-CaOzbBz1.js.map +0 -1
- package/dist/moyodb-sdk.umd.cjs +0 -2
- package/dist/moyodb-sdk.umd.cjs.map +0 -1
|
@@ -12,6 +12,15 @@ const TEXT_DECODER = new TextDecoder();
|
|
|
12
12
|
let rootDir = null;
|
|
13
13
|
let nextSessionId = 1;
|
|
14
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
|
+
}
|
|
15
24
|
async function getRootDir() {
|
|
16
25
|
if (!navigator?.storage?.getDirectory) {
|
|
17
26
|
throw new Error('navigator.storage.getDirectory is unavailable');
|
|
@@ -25,18 +34,27 @@ async function getOrCreateStackdbRoot() {
|
|
|
25
34
|
const root = await getRootDir();
|
|
26
35
|
return await root.getDirectoryHandle(DB_ROOT_DIR, { create: true });
|
|
27
36
|
}
|
|
37
|
+
function isNotFoundError(error) {
|
|
38
|
+
return error?.name === 'NotFoundError' || error?.name === 'TypeMismatchError';
|
|
39
|
+
}
|
|
28
40
|
async function lookupDirectoryHandle(parent, name) {
|
|
29
41
|
try {
|
|
30
42
|
return await parent.getDirectoryHandle(name, { create: false });
|
|
31
|
-
} catch (
|
|
32
|
-
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (isNotFoundError(err)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
50
|
async function lookupFileHandle(parent, name) {
|
|
36
51
|
try {
|
|
37
52
|
return await parent.getFileHandle(name, { create: false });
|
|
38
|
-
} catch (
|
|
39
|
-
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (isNotFoundError(err)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
40
58
|
}
|
|
41
59
|
}
|
|
42
60
|
function sessionPath(encodedDbName, generationName = null) {
|
|
@@ -64,7 +82,41 @@ function fnv1a32(bytes, zeroOffset = -1, zeroLength = 0) {
|
|
|
64
82
|
function isGenerationNameValid(name) {
|
|
65
83
|
return typeof name === 'string' && /^gen-[a-z0-9]+-[a-z0-9]+$/i.test(name);
|
|
66
84
|
}
|
|
67
|
-
|
|
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) {
|
|
68
120
|
if (!isGenerationNameValid(activeGeneration)) {
|
|
69
121
|
throw new Error(`invalid generation name ${activeGeneration}`);
|
|
70
122
|
}
|
|
@@ -86,7 +138,9 @@ function encodeControlSlot(generationCounter, activeGeneration) {
|
|
|
86
138
|
view.setUint32(CONTROL_CHECKSUM_OFFSET, checksum, true);
|
|
87
139
|
return slot;
|
|
88
140
|
}
|
|
89
|
-
|
|
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) {
|
|
90
144
|
if (bytes.length < CONTROL_SLOT_SIZE) {
|
|
91
145
|
return null;
|
|
92
146
|
}
|
|
@@ -96,23 +150,23 @@ function decodeControlSlot(slotIndex, bytes) {
|
|
|
96
150
|
}
|
|
97
151
|
}
|
|
98
152
|
const view = slotBytesView(bytes);
|
|
99
|
-
const
|
|
100
|
-
if (version !== CONTROL_VERSION) {
|
|
101
|
-
throw new Error(`unsupported control file version ${version}`);
|
|
102
|
-
}
|
|
103
|
-
const expectedChecksum = fnv1a32(bytes, CONTROL_CHECKSUM_OFFSET, 4);
|
|
153
|
+
const expectedChecksum = fnv1a32(bytes.subarray(0, CONTROL_SLOT_SIZE), CONTROL_CHECKSUM_OFFSET, 4);
|
|
104
154
|
const storedChecksum = view.getUint32(CONTROL_CHECKSUM_OFFSET, true);
|
|
105
155
|
if (expectedChecksum !== storedChecksum) {
|
|
106
156
|
return null;
|
|
107
157
|
}
|
|
158
|
+
const version = view.getUint32(8, true);
|
|
159
|
+
if (version !== CONTROL_VERSION) {
|
|
160
|
+
throw corruptionError(`unsupported control file version ${version}`);
|
|
161
|
+
}
|
|
108
162
|
const nameLength = view.getUint16(20, true);
|
|
109
163
|
const nameEnd = CONTROL_NAME_OFFSET + nameLength;
|
|
110
164
|
if (nameEnd > CONTROL_SLOT_SIZE) {
|
|
111
|
-
throw
|
|
165
|
+
throw corruptionError(`control slot name length out of bounds: ${nameLength}`);
|
|
112
166
|
}
|
|
113
167
|
const activeGeneration = TEXT_DECODER.decode(bytes.subarray(CONTROL_NAME_OFFSET, nameEnd));
|
|
114
168
|
if (!isGenerationNameValid(activeGeneration)) {
|
|
115
|
-
throw
|
|
169
|
+
throw corruptionError(`invalid generation in control slot: ${activeGeneration}`);
|
|
116
170
|
}
|
|
117
171
|
return {
|
|
118
172
|
slotIndex,
|
|
@@ -120,25 +174,31 @@ function decodeControlSlot(slotIndex, bytes) {
|
|
|
120
174
|
activeGeneration
|
|
121
175
|
};
|
|
122
176
|
}
|
|
123
|
-
|
|
124
|
-
|
|
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) {
|
|
125
181
|
const size = Number(accessHandle.getSize());
|
|
126
|
-
if (size
|
|
127
|
-
|
|
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 };
|
|
128
187
|
}
|
|
129
|
-
const
|
|
130
|
-
accessHandle
|
|
188
|
+
const buffer = new Uint8Array(CONTROL_SLOT_SIZE * 2);
|
|
189
|
+
readAll(accessHandle, buffer.subarray(0, Math.min(buffer.length, size)), 0);
|
|
131
190
|
const slot0 = decodeControlSlot(0, buffer.subarray(0, CONTROL_SLOT_SIZE));
|
|
132
191
|
const slot1 = decodeControlSlot(1, buffer.subarray(CONTROL_SLOT_SIZE, CONTROL_SLOT_SIZE * 2));
|
|
133
192
|
if (slot0 && slot1) {
|
|
134
|
-
return slot0.generationCounter >= slot1.generationCounter ? slot0 : slot1;
|
|
193
|
+
return { status: 'valid', control: slot0.generationCounter >= slot1.generationCounter ? slot0 : slot1 };
|
|
135
194
|
}
|
|
136
|
-
|
|
195
|
+
const control = slot0 ?? slot1;
|
|
196
|
+
return control ? { status: 'valid', control } : { status: 'invalid', control: null };
|
|
137
197
|
}
|
|
138
|
-
async function
|
|
198
|
+
async function readControlFile(dbRoot) {
|
|
139
199
|
const fileHandle = await lookupFileHandle(dbRoot, CONTROL_FILE_NAME);
|
|
140
200
|
if (!fileHandle) {
|
|
141
|
-
return null;
|
|
201
|
+
return { status: 'absent', control: null };
|
|
142
202
|
}
|
|
143
203
|
const accessHandle = await fileHandle.createSyncAccessHandle();
|
|
144
204
|
try {
|
|
@@ -147,16 +207,62 @@ async function readControlState(dbRoot) {
|
|
|
147
207
|
accessHandle.close();
|
|
148
208
|
}
|
|
149
209
|
}
|
|
150
|
-
async function
|
|
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) {
|
|
151
239
|
const fileHandle = await dbRoot.getFileHandle(CONTROL_FILE_NAME, { create: true });
|
|
152
240
|
const accessHandle = await fileHandle.createSyncAccessHandle();
|
|
153
241
|
try {
|
|
154
242
|
const current = readControlStateFromAccessHandle(accessHandle);
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
|
157
255
|
const encoded = encodeControlSlot(nextGenerationCounter, activeGeneration);
|
|
158
|
-
accessHandle
|
|
256
|
+
writeAll(accessHandle, encoded, nextSlot * CONTROL_SLOT_SIZE);
|
|
159
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
|
+
}
|
|
160
266
|
} finally {
|
|
161
267
|
accessHandle.close();
|
|
162
268
|
}
|
|
@@ -179,23 +285,37 @@ async function getDbRoot(encodedDbName, createIfMissing) {
|
|
|
179
285
|
}
|
|
180
286
|
return dbRoot;
|
|
181
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
|
+
}
|
|
182
299
|
async function resolveActiveDataDir(dbRoot) {
|
|
183
|
-
const
|
|
184
|
-
if (!
|
|
300
|
+
const activeGeneration = await resolveActiveGeneration(dbRoot);
|
|
301
|
+
if (!activeGeneration) {
|
|
185
302
|
return {
|
|
186
303
|
dirHandle: dbRoot,
|
|
187
304
|
generationName: null,
|
|
188
305
|
path: dbRoot.name
|
|
189
306
|
};
|
|
190
307
|
}
|
|
191
|
-
const activeDir = await lookupDirectoryHandle(dbRoot,
|
|
308
|
+
const activeDir = await lookupDirectoryHandle(dbRoot, activeGeneration);
|
|
192
309
|
if (!activeDir) {
|
|
193
|
-
throw
|
|
310
|
+
throw corruptionError(`active generation ${activeGeneration} is missing`);
|
|
194
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);
|
|
195
315
|
return {
|
|
196
316
|
dirHandle: activeDir,
|
|
197
|
-
generationName:
|
|
198
|
-
path: `${dbRoot.name}/${
|
|
317
|
+
generationName: activeGeneration,
|
|
318
|
+
path: `${dbRoot.name}/${activeGeneration}`
|
|
199
319
|
};
|
|
200
320
|
}
|
|
201
321
|
function closeSession(sessionId) {
|
|
@@ -218,7 +338,7 @@ function closeSessionsForDb(encodedDbName) {
|
|
|
218
338
|
}
|
|
219
339
|
}
|
|
220
340
|
}
|
|
221
|
-
async function openSessionForDir(dbPath, dirHandle) {
|
|
341
|
+
async function openSessionForDir(dbPath, dirHandle, generationName) {
|
|
222
342
|
const handles = new Map();
|
|
223
343
|
try {
|
|
224
344
|
for (let fileKind = 0; fileKind < FILE_NAMES.length; fileKind += 1) {
|
|
@@ -244,12 +364,12 @@ async function openSessionForDir(dbPath, dirHandle) {
|
|
|
244
364
|
path: dbPath,
|
|
245
365
|
handles
|
|
246
366
|
});
|
|
247
|
-
return { sessionId };
|
|
367
|
+
return { sessionId, generationName };
|
|
248
368
|
}
|
|
249
369
|
function getSession(sessionId) {
|
|
250
370
|
const session = sessions.get(sessionId);
|
|
251
371
|
if (!session) {
|
|
252
|
-
throw
|
|
372
|
+
throw namedError('StorageError', `no OPFS session ${sessionId}`);
|
|
253
373
|
}
|
|
254
374
|
return session;
|
|
255
375
|
}
|
|
@@ -257,7 +377,7 @@ function getAccessHandle(sessionId, fileKind) {
|
|
|
257
377
|
const session = getSession(sessionId);
|
|
258
378
|
const handle = session.handles.get(fileKind);
|
|
259
379
|
if (!handle) {
|
|
260
|
-
throw
|
|
380
|
+
throw namedError('StorageError', `no OPFS access handle for session ${sessionId} file kind ${fileKind}`);
|
|
261
381
|
}
|
|
262
382
|
return handle;
|
|
263
383
|
}
|
|
@@ -292,7 +412,7 @@ async function sumDirectorySize(dirHandle, pathPrefix) {
|
|
|
292
412
|
export async function opfsOpenActiveDb(encodedDbName, createIfMissing = true) {
|
|
293
413
|
const dbRoot = await getDbRoot(encodedDbName, createIfMissing);
|
|
294
414
|
const active = await resolveActiveDataDir(dbRoot);
|
|
295
|
-
return await openSessionForDir(active.path, active.dirHandle);
|
|
415
|
+
return await openSessionForDir(active.path, active.dirHandle, active.generationName);
|
|
296
416
|
}
|
|
297
417
|
export async function opfsOpenGenerationDb(encodedDbName, generationName, createIfMissing = true) {
|
|
298
418
|
if (!isGenerationNameValid(generationName)) {
|
|
@@ -300,43 +420,29 @@ export async function opfsOpenGenerationDb(encodedDbName, generationName, create
|
|
|
300
420
|
}
|
|
301
421
|
const dbRoot = await getDbRoot(encodedDbName, createIfMissing);
|
|
302
422
|
const generationDir = await dbRoot.getDirectoryHandle(generationName, { create: createIfMissing });
|
|
303
|
-
return await openSessionForDir(sessionPath(encodedDbName, generationName), generationDir);
|
|
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);
|
|
304
428
|
}
|
|
305
429
|
export function opfsReadAt(sessionId, fileKind, offset, len) {
|
|
306
430
|
const handle = getAccessHandle(sessionId, fileKind);
|
|
307
431
|
const buffer = new Uint8Array(len);
|
|
308
|
-
handle
|
|
432
|
+
readAll(handle, buffer, Number(offset));
|
|
309
433
|
return buffer;
|
|
310
434
|
}
|
|
311
435
|
export function opfsWriteAt(sessionId, fileKind, offset, bytes) {
|
|
312
|
-
|
|
313
|
-
const startOffset = Number(offset);
|
|
314
|
-
let writtenTotal = 0;
|
|
315
|
-
while (writtenTotal < bytes.length) {
|
|
316
|
-
const chunk = bytes.subarray(writtenTotal, writtenTotal + OPFS_WRITE_CHUNK_SIZE);
|
|
317
|
-
const rawWritten = handle.write(chunk, { at: startOffset + writtenTotal });
|
|
318
|
-
const written = Number(rawWritten);
|
|
319
|
-
if (!Number.isSafeInteger(written) || written < 0 || written > chunk.length) {
|
|
320
|
-
throw new Error(`opfs write failed: invalid byte count ${String(rawWritten)}`);
|
|
321
|
-
}
|
|
322
|
-
if (written === 0 && chunk.length > 0) {
|
|
323
|
-
throw new Error('opfs write failed: wrote 0 bytes');
|
|
324
|
-
}
|
|
325
|
-
writtenTotal += written;
|
|
326
|
-
}
|
|
327
|
-
return writtenTotal;
|
|
436
|
+
return writeAll(getAccessHandle(sessionId, fileKind), bytes, Number(offset));
|
|
328
437
|
}
|
|
329
438
|
export function opfsFlush(sessionId, fileKind) {
|
|
330
|
-
|
|
331
|
-
handle.flush();
|
|
439
|
+
getAccessHandle(sessionId, fileKind).flush();
|
|
332
440
|
}
|
|
333
441
|
export function opfsLen(sessionId, fileKind) {
|
|
334
|
-
|
|
335
|
-
return BigInt(handle.getSize());
|
|
442
|
+
return BigInt(getAccessHandle(sessionId, fileKind).getSize());
|
|
336
443
|
}
|
|
337
444
|
export function opfsTruncate(sessionId, fileKind, size) {
|
|
338
|
-
|
|
339
|
-
handle.truncate(Number(size));
|
|
445
|
+
getAccessHandle(sessionId, fileKind).truncate(Number(size));
|
|
340
446
|
}
|
|
341
447
|
export function opfsCloseSession(sessionId) {
|
|
342
448
|
closeSession(sessionId);
|
|
@@ -350,7 +456,10 @@ export async function opfsPrepareRebuildTarget(encodedDbName) {
|
|
|
350
456
|
await dbRoot.getDirectoryHandle(generationName, { create: true });
|
|
351
457
|
return { generationName };
|
|
352
458
|
}
|
|
353
|
-
|
|
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) {
|
|
354
463
|
if (!isGenerationNameValid(generationName)) {
|
|
355
464
|
throw new Error(`invalid generation name ${generationName}`);
|
|
356
465
|
}
|
|
@@ -359,32 +468,30 @@ export async function opfsSwapActiveGeneration(encodedDbName, generationName) {
|
|
|
359
468
|
if (!generationDir) {
|
|
360
469
|
throw new Error(`generation ${generationName} does not exist`);
|
|
361
470
|
}
|
|
362
|
-
await writeControlState(dbRoot, generationName);
|
|
471
|
+
await writeControlState(dbRoot, generationName, expectedCurrentGeneration ?? null);
|
|
363
472
|
}
|
|
473
|
+
// Legacy root files are removed strictly (see resolveActiveGeneration); stale
|
|
474
|
+
// generation directories are best effort because nothing can select them.
|
|
364
475
|
export async function opfsCleanupInactiveEntries(encodedDbName) {
|
|
365
476
|
const dbRoot = await lookupDirectoryHandle(await getOrCreateStackdbRoot(), encodedDbName);
|
|
366
477
|
if (!dbRoot) {
|
|
367
478
|
return;
|
|
368
479
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
480
|
+
const activeGeneration = await resolveActiveGeneration(dbRoot);
|
|
481
|
+
if (activeGeneration) {
|
|
482
|
+
await removeLegacyDataFiles(dbRoot);
|
|
483
|
+
}
|
|
484
|
+
const staleDirectories = [];
|
|
371
485
|
for await (const [name, handle] of dbRoot.entries()) {
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
if (handle.kind === 'directory') {
|
|
376
|
-
if (activeGeneration && name === activeGeneration) {
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
if (name.startsWith('gen-')) {
|
|
380
|
-
await dbRoot.removeEntry(name, { recursive: true });
|
|
381
|
-
}
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (activeGeneration && FILE_NAMES.includes(name)) {
|
|
385
|
-
await dbRoot.removeEntry(name);
|
|
486
|
+
if (handle.kind === 'directory' && name.startsWith('gen-') && name !== activeGeneration) {
|
|
487
|
+
staleDirectories.push(name);
|
|
386
488
|
}
|
|
387
489
|
}
|
|
490
|
+
for (const name of staleDirectories) {
|
|
491
|
+
try {
|
|
492
|
+
await dbRoot.removeEntry(name, { recursive: true });
|
|
493
|
+
} catch (_err) {}
|
|
494
|
+
}
|
|
388
495
|
}
|
|
389
496
|
export async function opfsDbDirectorySize(encodedDbName) {
|
|
390
497
|
const dbRoot = await lookupDirectoryHandle(await getOrCreateStackdbRoot(), encodedDbName);
|
|
@@ -398,5 +505,9 @@ export async function opfsRemoveDb(encodedDbName) {
|
|
|
398
505
|
const stackdb = await getOrCreateStackdbRoot();
|
|
399
506
|
try {
|
|
400
507
|
await stackdb.removeEntry(encodedDbName, { recursive: true });
|
|
401
|
-
} catch (
|
|
508
|
+
} catch (err) {
|
|
509
|
+
if (!isNotFoundError(err)) {
|
|
510
|
+
throw err;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
402
513
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -64,6 +64,14 @@ export declare class InjectedFailureError extends MoyoDbError {
|
|
|
64
64
|
export declare class ChangeFeedCompactedError extends MoyoDbError {
|
|
65
65
|
constructor(message: string);
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The engine state could not be trusted, or automatic recovery failed.
|
|
69
|
+
* A failed commit may already be durable. The worker attempts recovery before
|
|
70
|
+
* a new transaction and after a failed commit. Recovery discards all open transactions.
|
|
71
|
+
*/
|
|
72
|
+
export declare class RecoveryRequiredError extends MoyoDbError {
|
|
73
|
+
constructor(message: string);
|
|
74
|
+
}
|
|
67
75
|
export declare class InternalError extends MoyoDbError {
|
|
68
76
|
constructor(message: string);
|
|
69
77
|
}
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,WAAY,SAAQ,KAAK;IAClC,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIxC;CACJ;AACD,qBAAa,wBAAyB,SAAQ,WAAW;IACrD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,iBAAkB,SAAQ,WAAW;IAC9C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,eAAgB,SAAQ,WAAW;IAC5C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,gBAAiB,SAAQ,WAAW;IAC7C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,kBAAmB,SAAQ,WAAW;IAC/C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,kBAAmB,SAAQ,WAAW;IAC/C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,eAAgB,SAAQ,WAAW;IAC5C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,0BAA2B,SAAQ,eAAe;IAC3D,YAAY,OAAO,EAAE,MAAM,EAG1B;CACJ;AACD,qBAAa,gCAAiC,SAAQ,WAAW;IAC7D,YAAY,OAAO,SAAmC,EAErD;CACJ;AACD,qBAAa,wBAAyB,SAAQ,WAAW;IACrD,YAAY,OAAO,SAAuD,EAEzE;CACJ;AACD,qBAAa,sBAAuB,SAAQ,WAAW;IACnD,YAAY,OAAO,SAAkC,EAEpD;CACJ;AACD,qBAAa,mBAAoB,SAAQ,WAAW;IAChD,YAAY,OAAO,SAA8B,EAEhD;CACJ;AACD,qBAAa,kBAAmB,SAAQ,WAAW;IAC/C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,gBAAiB,SAAQ,WAAW;IAC7C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,qBAAsB,SAAQ,WAAW;IAClD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,sBAAuB,SAAQ,WAAW;IACnD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,iBAAkB,SAAQ,WAAW;IAC9C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,YAAa,SAAQ,WAAW;IACzC,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,kBAAmB,SAAQ,WAAW;IAC/C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,oBAAqB,SAAQ,WAAW;IACjD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,wBAAyB,SAAQ,WAAW;IACrD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,aAAc,SAAQ,WAAW;IAC1C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,uBAAwB,SAAQ,WAAW;IACpD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,YAAa,SAAQ,WAAW;IACzC,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,WAAY,SAAQ,KAAK;IAClC,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIxC;CACJ;AACD,qBAAa,wBAAyB,SAAQ,WAAW;IACrD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,iBAAkB,SAAQ,WAAW;IAC9C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,eAAgB,SAAQ,WAAW;IAC5C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,gBAAiB,SAAQ,WAAW;IAC7C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,kBAAmB,SAAQ,WAAW;IAC/C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,kBAAmB,SAAQ,WAAW;IAC/C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,eAAgB,SAAQ,WAAW;IAC5C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,0BAA2B,SAAQ,eAAe;IAC3D,YAAY,OAAO,EAAE,MAAM,EAG1B;CACJ;AACD,qBAAa,gCAAiC,SAAQ,WAAW;IAC7D,YAAY,OAAO,SAAmC,EAErD;CACJ;AACD,qBAAa,wBAAyB,SAAQ,WAAW;IACrD,YAAY,OAAO,SAAuD,EAEzE;CACJ;AACD,qBAAa,sBAAuB,SAAQ,WAAW;IACnD,YAAY,OAAO,SAAkC,EAEpD;CACJ;AACD,qBAAa,mBAAoB,SAAQ,WAAW;IAChD,YAAY,OAAO,SAA8B,EAEhD;CACJ;AACD,qBAAa,kBAAmB,SAAQ,WAAW;IAC/C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,gBAAiB,SAAQ,WAAW;IAC7C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,qBAAsB,SAAQ,WAAW;IAClD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,sBAAuB,SAAQ,WAAW;IACnD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,iBAAkB,SAAQ,WAAW;IAC9C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,YAAa,SAAQ,WAAW;IACzC,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,kBAAmB,SAAQ,WAAW;IAC/C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,oBAAqB,SAAQ,WAAW;IACjD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,wBAAyB,SAAQ,WAAW;IACrD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,WAAW;IAClD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,aAAc,SAAQ,WAAW;IAC1C,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,uBAAwB,SAAQ,WAAW;IACpD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AACD,qBAAa,YAAa,SAAQ,WAAW;IACzC,YAAY,OAAO,EAAE,MAAM,EAE1B;CACJ;AA6CD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAcpD"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAMR,EAAE,EAMF,WAAW,EASd,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAMR,EAAE,EAMF,WAAW,EASd,MAAM,SAAS,CAAC;AA+zBjB,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,EAAE,CAAC,CA+DjF;AACD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1D;AACD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG5D;AACD,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,mBAAmB,SAAS,CAAC"}
|
package/dist/indexing.d.ts
CHANGED
|
@@ -27,7 +27,15 @@ export declare function decodeIndexEntryKey(bytes: Uint8Array): DecodedIndexEntr
|
|
|
27
27
|
export declare function indexKeyExactRange(logicalIndexKey: Uint8Array): Range;
|
|
28
28
|
export declare function indexRangeToPhysicalRange(range?: Range): Range;
|
|
29
29
|
export declare function extractLogicalIndexKey(def: NormalizedIndexDef, valueBytes: Uint8Array): Uint8Array | null;
|
|
30
|
+
/**
|
|
31
|
+
* Reuse the decoded document only within one synchronous mutation plan.
|
|
32
|
+
* Lazy decoding preserves validation order. Do not retain the extractor
|
|
33
|
+
* across operations or an await.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createIndexKeyExtractor(valueBytes: Uint8Array): (def: NormalizedIndexDef) => Uint8Array | null;
|
|
36
|
+
export declare function extractLogicalIndexKeyFromDocument(def: NormalizedIndexDef, documentValue: unknown): Uint8Array | null;
|
|
30
37
|
export declare function serializeNormalizedIndexDefinitions(defs: ReadonlyArray<NormalizedIndexDef>): string;
|
|
31
38
|
export declare function cloneNormalizedIndexDefinitions(defs: ReadonlyArray<NormalizedIndexDef>): NormalizedIndexDef[];
|
|
32
39
|
export declare function toPublicIndexDefinitions(defs: ReadonlyArray<NormalizedIndexDef>): IndexDef[];
|
|
40
|
+
export declare function decodeIndexedDocument(bytes: Uint8Array): unknown;
|
|
33
41
|
//# sourceMappingURL=indexing.d.ts.map
|
package/dist/indexing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexing.d.ts","sourceRoot":"","sources":["../src/indexing.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"indexing.d.ts","sourceRoot":"","sources":["../src/indexing.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAU/C,eAAO,MAAM,oBAAoB,wBAAwB,CAAC;AAC1D,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACzB;AACD,MAAM,WAAW,oBAAoB;IACjC,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;CAC1B;AACD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD;AACD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAIxD;AACD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,EAAE,CAuD9E;AACD,wBAAgB,iCAAiC,CAAC,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAc7G;AACD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3E;AACD,wBAAgB,mBAAmB,CAC/B,IAAI,EAAE,aAAa,CAAC,kBAAkB,CAAC,EACvC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GACb,kBAAkB,GAAG,IAAI,CAE3B;AACD,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAE5G;AACD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAE9E;AACD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,kBAAkB,GAAG,UAAU,CAE5E;AACD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,GAAG,kBAAkB,CAS9E;AACD,wBAAgB,mBAAmB,CAAC,eAAe,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,GAAG,UAAU,CAEnG;AACD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,oBAAoB,CAc3E;AACD,wBAAgB,kBAAkB,CAAC,eAAe,EAAE,UAAU,GAAG,KAAK,CAErE;AACD,wBAAgB,yBAAyB,CAAC,KAAK,GAAE,KAAU,GAAG,KAAK,CA4BlE;AACD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,kBAAkB,EAAE,UAAU,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI,CAEzG;AACD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,UAAU,GAAG,CAAC,GAAG,EAAE,kBAAkB,KAAK,UAAU,GAAG,IAAI,CAU9G;AACD,wBAAgB,kCAAkC,CAAC,GAAG,EAAE,kBAAkB,EAAE,aAAa,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI,CAiBrH;AACD,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,MAAM,CAEnG;AACD,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,EAAE,CAS7G;AACD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,QAAQ,EAAE,CAE5F;AAmED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAMhE"}
|