goroutine.mjs raw

   1  // MoxieJS Runtime — Goroutine Scheduler
   2  // Cooperative async/await scheduler with microtask queue.
   3  
   4  const runQueue = [];
   5  let running = false;
   6  let goroutineId = 0;
   7  
   8  // Error tracking — last N goroutine crashes, accessible via onError callback.
   9  const _errorLog = [];
  10  const _maxErrors = 32;
  11  let _onError = null;
  12  
  13  class Goroutine {
  14    constructor(id, fn, caller) {
  15      this.id = id;
  16      this.fn = fn;
  17      this.caller = caller || '';
  18      this.done = false;
  19      this.error = null;
  20    }
  21  }
  22  
  23  // Register a callback for goroutine errors: fn(goroutineId, callerInfo, error).
  24  export function onError(fn) {
  25    _onError = fn;
  26  }
  27  
  28  // Return recent errors as [{id, caller, error, time}].
  29  export function errors() {
  30    return _errorLog.slice();
  31  }
  32  
  33  function recordError(g, e) {
  34    const entry = { id: g.id, caller: g.caller, error: e, time: Date.now() };
  35    _errorLog.push(entry);
  36    if (_errorLog.length > _maxErrors) _errorLog.shift();
  37  
  38    if (typeof e === 'object' && e !== null && e.name === 'GoPanic') {
  39      console.error(`goroutine ${g.id} [${g.caller}]: panic: ${e.message}`);
  40      if (e.stack) console.error(e.stack);
  41    } else {
  42      console.error(`goroutine ${g.id} [${g.caller}]: unhandled error:`, e);
  43    }
  44  
  45    if (_onError) {
  46      try { _onError(g.id, g.caller, e); } catch (_) {}
  47    }
  48  }
  49  
  50  // Spawn a new goroutine. caller is an optional string identifying the spawn site.
  51  export function spawn(fn, caller) {
  52    const id = ++goroutineId;
  53    const g = new Goroutine(id, fn, caller || '');
  54    runQueue.push(g);
  55    scheduleRun();
  56    return g;
  57  }
  58  
  59  let scheduled = false;
  60  
  61  function scheduleRun() {
  62    if (!scheduled) {
  63      scheduled = true;
  64      // Use queueMicrotask for tight scheduling, setTimeout(0) for fairness.
  65      queueMicrotask(tick);
  66    }
  67  }
  68  
  69  async function tick() {
  70    if (running) { scheduled = false; return; }
  71    running = true;
  72  
  73    while (runQueue.length > 0) {
  74      const g = runQueue.shift();
  75      if (g.done) continue;
  76  
  77      try {
  78        const result = g.fn();
  79        if (result instanceof Promise) {
  80          await result;
  81        }
  82        g.done = true;
  83      } catch (e) {
  84        g.done = true;
  85        g.error = e;
  86        // Record the error but keep the scheduler alive.
  87        recordError(g, e);
  88        // Continue processing remaining goroutines.
  89      }
  90    }
  91  
  92    running = false;
  93    scheduled = false;
  94  }
  95  
  96  // Yield current goroutine to let others run.
  97  export function gosched() {
  98    return new Promise(resolve => {
  99      queueMicrotask(resolve);
 100    });
 101  }
 102  
 103  // Sleep for a duration (nanoseconds, but we convert to ms).
 104  // ns may be BigInt (int64) or Number.
 105  export function sleep(ns) {
 106    const ms = Math.max(0, Number(ns) / 1_000_000);
 107    return new Promise(resolve => setTimeout(resolve, ms));
 108  }
 109  
 110  // Run the main function and wait for all goroutines.
 111  export async function runMain(mainFn) {
 112    try {
 113      const result = mainFn();
 114      if (result instanceof Promise) await result;
 115    } catch (e) {
 116      const entry = { id: 0, caller: 'main', error: e, time: Date.now() };
 117      _errorLog.push(entry);
 118      if (e && e.name === 'GoPanic') {
 119        console.error(`main: panic: ${e.message}`);
 120        if (e.stack) console.error(e.stack);
 121      } else {
 122        console.error('main: unhandled error:', e);
 123      }
 124      if (_onError) {
 125        try { _onError(0, 'main', e); } catch (_) {}
 126      }
 127      // Don't re-throw — let the scheduler drain remaining goroutines.
 128    }
 129  
 130    // Drain remaining goroutines.
 131    while (runQueue.length > 0) {
 132      await tick();
 133      // Small yield to let any newly-spawned goroutines enqueue.
 134      await new Promise(r => setTimeout(r, 0));
 135    }
 136  }
 137