1import assert from 'node:assert/strict';
2import { readFile } from 'node:fs/promises';
3import test from 'node:test';
4import vm from 'node:vm';
5
6const source = await readFile(new URL('../../crates/snowbound/web/glue.js', import.meta.url), 'utf8');
7
8test('an OPFS flush rejection retains ordered writes until storage recovers', async () => {
9 const files = new Map([['existing', new Uint8Array([0])]]);
10 const flushed = [];
11 const replies = [];
12 let unavailable = false;
13 const root = {
14 async *entries() {
15 for (const [name, bytes] of files) {
16 yield [name, { kind: 'file', getFile: async () => ({ arrayBuffer: async () => bytes.buffer }) }];
17 }
18 },
19 async getFileHandle(path) {
20 let bytes = files.get(path)?.slice() ?? new Uint8Array();
21 return {
22 async createSyncAccessHandle() {
23 return {
24 write(part, { at }) {
25 if (at + part.length > bytes.length) {
26 const extended = new Uint8Array(at + part.length);
27 extended.set(bytes);
28 bytes = extended;
29 }
30 bytes.set(part, at);
31 return part.length;
32 },
33 truncate(length) { bytes = bytes.slice(0, length); },
34 flush() {
35 if (unavailable) throw new Error('Injected OPFS outage');
36 files.set(path, bytes.slice());
37 flushed.push([path, [...bytes]]);
38 },
39 };
40 },
41 };
42 },
43 };
44 const context = vm.createContext({
45 navigator: { storage: { getDirectory: async () => root } },
46 postMessage: (message) => replies.push(message),
47 console: { error() {} },
48 });
49 vm.runInContext(source.replace(/\bexport /g, '').replaceAll('import.meta.url', JSON.stringify(import.meta.url)), context);
50 vm.runInContext('storageWorker()', context);
51 const send = async (data) => {
52 context.onmessage({ data });
53 const expected = replies.length + 1;
54 if (data.kind === 'store') context.onmessage({ data: { kind: 'settle' } });
55 for (let turns = 0; replies.length < expected; turns++) {
56 assert.ok(turns < 100, 'storage worker did not answer');
57 await Promise.resolve();
58 }
59 return replies.at(-1);
60 };
61 await send({ kind: 'load' });
62 unavailable = true;
63 const first = await send({ kind: 'store', changes: [['replica-wal', 2, [[0, new Uint8Array([1, 2])]]]] });
64 assert.match(first.error, /Injected OPFS outage/);
65 await send({ kind: 'store', changes: [['replica-wal', 2, [[1, new Uint8Array([3])]]], ['replica', 1, [[0, new Uint8Array([4])]]]] });
66 assert.equal(files.has('replica'), false);
67 assert.equal(flushed.length, 0);
68 unavailable = false;
69 assert.equal((await send({ kind: 'settle' })).error, null);
70 assert.deepEqual(flushed, [['replica-wal', [1, 2]], ['replica-wal', [1, 3]], ['replica', [4]]]);
71 assert.equal((await send({ kind: 'settle' })).error, null);
72 assert.equal(flushed.length, 3);
73});