worker.js raw

   1  // The wasm worker loader, shared by the wallet worker and the light-client workers.
   2  //
   3  // The worker is created with the module and the global it should wait for:
   4  //
   5  //   new Worker('worker.js?wasm=tinywallet.wasm&global=tinywallet')
   6  //
   7  // The page then posts {kind: 'call', fn, args} and gets {kind: 'call', id, result} back. The
   8  // args may carry MessagePorts, which is how the wallet is handed its chain channels.
   9  
  10  const params = new URLSearchParams(self.location.search);
  11  const wasmPath = params.get('wasm') || 'tinywallet.wasm';
  12  const globalName = params.get('global') || 'tinywallet';
  13  
  14  // The worker's console is invisible from the page, and a wasm module that fails to connect
  15  // logs there and nowhere else: a page then shows "opening the workers…" forever with no
  16  // reason. Every console line is forwarded with the ready/call messages so the page can show
  17  // it, and the original output is kept.
  18  for (const level of ['log', 'warn', 'error']) {
  19    const original = console[level].bind(console);
  20    console[level] = (...args) => {
  21      original(...args);
  22      try {
  23        self.postMessage({ kind: 'log', level, text: args.map((arg) => String(arg)).join(' ') });
  24      } catch (error) {
  25        // A worker that cannot post is already gone; nothing to report to.
  26      }
  27    };
  28  }
  29  
  30  let started = null;
  31  
  32  function ready() {
  33    if (self[globalName]) return Promise.resolve();
  34    if (!started) started = start();
  35    return started;
  36  }
  37  
  38  async function start() {
  39    importScripts('wasm_exec.js');
  40  
  41    const go = new Go();
  42    const response = await fetch(wasmPath);
  43    const { instance } = await WebAssembly.instantiateStreaming(response, go.importObject);
  44  
  45    // go.run never settles for a program that parks in select{}, which is what a worker does:
  46    // it starts the module and returns to the event loop.
  47    go.run(instance);
  48  
  49    for (let attempt = 0; attempt < 600; attempt++) {
  50      if (self[globalName]) return;
  51      await new Promise((resolve) => setTimeout(resolve, 50));
  52    }
  53  
  54    throw new Error(wasmPath + ' did not start');
  55  }
  56  
  57  self.onmessage = async (event) => {
  58    const message = event.data;
  59  
  60    if (!message || message.kind !== 'call') return;
  61  
  62    try {
  63      await ready();
  64      const target = self[globalName][message.fn];
  65      if (typeof target !== 'function') throw new Error('no ' + message.fn + ' on ' + globalName);
  66      const result = target.apply(null, message.args || []);
  67      self.postMessage({ kind: 'call', id: message.id, result });
  68    } catch (error) {
  69      self.postMessage({ kind: 'call', id: message.id, error: String(error && error.message ? error.message : error) });
  70    }
  71  };
  72  
  73  // The page needs to know when the module is up, because the first call is the one that
  74  // creates the ports.
  75  ready().then(
  76    () => self.postMessage({ kind: 'ready' }),
  77    (error) => self.postMessage({ kind: 'failed', error: String(error) }),
  78  );
  79