idb-smoke.cjs raw
1 // Persistence smoke test: sync with IndexedDB, then load a second wasm
2 // instance against the same database and require it to resume with no
3 // re-download.
4 //
5 // npm install fake-indexeddb # once, in this directory or NODE_PATH
6 // node --experimental-websocket idb-smoke.cjs <nostrino.wasm> <gateway.uri>
7 'use strict';
8
9 const fs = require('fs');
10 const [wasmPath, uriFile] = process.argv.slice(2);
11 if (!wasmPath || !uriFile) {
12 console.error('usage: idb-smoke.cjs <nostrino.wasm> <gateway.uri>');
13 process.exit(2);
14 }
15 if (typeof globalThis.WebSocket === 'undefined') {
16 console.error('this Node has no global WebSocket; run with --experimental-websocket');
17 process.exit(2);
18 }
19 let fake;
20 try {
21 fake = require(process.env.IDB_MODULE || 'fake-indexeddb');
22 } catch (err) {
23 console.error('fake-indexeddb not found; run `npm install fake-indexeddb` or set IDB_MODULE');
24 process.exit(2);
25 }
26 globalThis.indexedDB = fake.indexedDB;
27 globalThis.IDBKeyRange = fake.IDBKeyRange;
28
29 const execPath = process.env.TINYGO_WASM_EXEC || '/usr/local/lib/tinygo/targets/wasm_exec.js';
30 if (!fs.existsSync(execPath)) {
31 console.error('wasm_exec.js not found at ' + execPath + '; set TINYGO_WASM_EXEC');
32 process.exit(2);
33 }
34 globalThis.require = require;
35 globalThis.fs = fs;
36 globalThis.performance = {
37 now() {
38 const [s, n] = process.hrtime();
39 return s * 1000 + n / 1e6;
40 },
41 };
42 require(execPath);
43
44 const uri = fs.readFileSync(uriFile, 'utf8').trim();
45 const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
46 const wasm = fs.readFileSync(wasmPath);
47
48 async function boot() {
49 const go = new Go();
50 const result = await WebAssembly.instantiate(wasm, go.importObject);
51 go.run(result.instance);
52 for (let i = 0; i < 500 && !globalThis.nostrino; i++) await sleep(10);
53 if (!globalThis.nostrino) throw new Error('nostrino global missing');
54 return globalThis.nostrino;
55 }
56
57 async function sync(nostrino, label) {
58 nostrino.sync(uri, 'regtest');
59 let last = '';
60 for (let i = 0; i < 900; i++) {
61 const raw = nostrino.status();
62 if (raw !== last) {
63 console.log(label + ': ' + raw);
64 last = raw;
65 }
66 const st = JSON.parse(raw);
67 if (!st.running) {
68 if (st.status !== 'done') throw new Error(label + ' failed: ' + st.error);
69 return st;
70 }
71 await sleep(100);
72 }
73 throw new Error(label + ' timed out');
74 }
75
76 (async () => {
77 const first = await boot();
78 const one = await sync(first, 'first');
79 if (!one.persist || one.store !== 'indexeddb') {
80 throw new Error('first sync did not use IndexedDB (store=' + one.store + ')');
81 }
82 if (one.tip === 0 || one.filters === 0) throw new Error('first sync had no data');
83
84 // A second module instance simulates a page reload against the same IDB.
85 const second = await boot();
86 const two = await sync(second, 'second');
87 if (two.store !== 'indexeddb') throw new Error('second sync did not use IndexedDB');
88 if (two.tip !== one.tip) throw new Error('tip changed across reload');
89 if (two.added !== 0 || two.filters !== 0) {
90 throw new Error('second sync re-downloaded: added=' + two.added + ' filters=' + two.filters);
91 }
92 const matches = JSON.parse(second.match('626c6f636b2d37'));
93 if (!Array.isArray(matches) || matches.length !== 1 || matches[0] !== 7) {
94 throw new Error('match after reload failed: ' + JSON.stringify(matches));
95 }
96 console.log('PASS: IndexedDB resumed at tip ' + two.tip + ' with no re-download');
97 process.exit(0);
98 })().catch((err) => {
99 console.error('FAIL:', err.message || err);
100 process.exit(1);
101 });
102