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