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('OPFS retries preserve write order and canonical handle ownership', async () => {
9 const files = new Map([['existing', new Uint8Array([0])]]);
10 const flushed = [];
11 const held = new Set();
12 const opened = [];
13 const replies = [];
14 let unavailable = false;
15 const root = {
16 async *entries() {
17 for (const [name, bytes] of files) {
18 yield [name, { kind: 'file', getFile: async () => ({ arrayBuffer: async () => bytes.buffer }) }];
19 }
20 },
21 async removeEntry(path) {
22 assert.equal(held.has(path), false, 'Removal closes the existing access handle');
23 files.delete(path);
24 },
25 async getFileHandle(path) {
26 let bytes = files.get(path)?.slice() ?? new Uint8Array();
27 return {
28 async createSyncAccessHandle() {
29 if (held.has(path)) throw new Error('Another access handle is open for ' + path);
30 held.add(path);
31 opened.push(path);
32 return {
33 close() { held.delete(path); },
34 write(part, { at }) {
35 if (at + part.length > bytes.length) {
36 const extended = new Uint8Array(at + part.length);
37 extended.set(bytes);
38 bytes = extended;
39 }
40 bytes.set(part, at);
41 return part.length;
42 },
43 truncate(length) { bytes = bytes.slice(0, length); },
44 flush() {
45 if (unavailable) throw new Error('Injected OPFS outage');
46 files.set(path, bytes.slice());
47 flushed.push([path, [...bytes]]);
48 },
49 };
50 },
51 };
52 },
53 };
54 const context = vm.createContext({
55 navigator: { storage: { getDirectory: async () => root } },
56 postMessage: (message) => replies.push(message),
57 console: { error() {} },
58 });
59 vm.runInContext(source.replace(/\bexport /g, '').replaceAll('import.meta.url', JSON.stringify(import.meta.url)), context);
60 vm.runInContext('storageWorker()', context);
61 const send = async (data) => {
62 context.onmessage({ data });
63 const expected = replies.length + 1;
64 if (data.kind === 'store') context.onmessage({ data: { kind: 'settle' } });
65 for (let turns = 0; replies.length < expected; turns++) {
66 assert.ok(turns < 100, 'storage worker did not answer');
67 await Promise.resolve();
68 }
69 return replies.at(-1);
70 };
71 await send({ kind: 'load' });
72 unavailable = true;
73 const first = await send({ kind: 'store', changes: [['replica-wal', 2, [[0, new Uint8Array([1, 2])]]]] });
74 assert.match(first.error, /Injected OPFS outage/);
75 await send({ kind: 'store', changes: [['replica-wal', 2, [[1, new Uint8Array([3])]]], ['replica', 1, [[0, new Uint8Array([4])]]]] });
76 assert.equal(files.has('replica'), false);
77 assert.equal(flushed.length, 0);
78 unavailable = false;
79 assert.equal((await send({ kind: 'settle' })).error, null);
80 assert.deepEqual(flushed, [['replica-wal', [1, 2]], ['replica-wal', [1, 3]], ['replica', [4]]]);
81 assert.equal((await send({ kind: 'settle' })).error, null);
82 assert.equal(flushed.length, 3);
83 assert.equal((await send({ kind: 'store', changes: [['/replica/', 1, [[0, new Uint8Array([5])]]]] })).error, null);
84 assert.deepEqual([...files.get('replica')], [5]);
85 assert.equal(opened.filter(path => path === 'replica').length, 1, 'Aliases reuse the existing handle');
86 assert.equal((await send({ kind: 'store', changes: [['/replica/']] })).error, null);
87 assert.equal(files.has('replica'), false);
88 assert.equal((await send({ kind: 'store', changes: [['replica', 1, [[0, new Uint8Array([6])]]]] })).error, null);
89 assert.deepEqual([...files.get('replica')], [6]);
90 assert.equal(opened.filter(path => path === 'replica').length, 2, 'Recreation opens a new handle');
91});