goroutine.mjs raw

   1  // TinyJS 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  class Goroutine {
   9    constructor(id, fn) {
  10      this.id = id;
  11      this.fn = fn;
  12      this.done = false;
  13      this.error = null;
  14    }
  15  }
  16  
  17  // Spawn a new goroutine.
  18  export function spawn(fn) {
  19    const id = ++goroutineId;
  20    const g = new Goroutine(id, fn);
  21    runQueue.push(g);
  22    scheduleRun();
  23    return g;
  24  }
  25  
  26  let scheduled = false;
  27  
  28  function scheduleRun() {
  29    if (!scheduled) {
  30      scheduled = true;
  31      // Use queueMicrotask for tight scheduling, setTimeout(0) for fairness.
  32      queueMicrotask(tick);
  33    }
  34  }
  35  
  36  async function tick() {
  37    scheduled = false;
  38    if (running) return;
  39    running = true;
  40  
  41    while (runQueue.length > 0) {
  42      const g = runQueue.shift();
  43      if (g.done) continue;
  44  
  45      try {
  46        const result = g.fn();
  47        if (result instanceof Promise) {
  48          result.then(
  49            () => { g.done = true; },
  50            (e) => {
  51              g.done = true;
  52              g.error = e;
  53              if (typeof e === 'object' && e !== null && e.name === 'GoPanic') {
  54                console.error(`goroutine ${g.id}: panic: ${e.message}`);
  55              } else {
  56                console.error(`goroutine ${g.id}: unhandled error:`, e);
  57              }
  58              if (typeof process !== 'undefined' && process.exit) process.exit(2);
  59            }
  60          );
  61        } else {
  62          g.done = true;
  63        }
  64      } catch (e) {
  65        g.done = true;
  66        g.error = e;
  67        if (typeof e === 'object' && e !== null && e.name === 'GoPanic') {
  68          console.error(`goroutine ${g.id}: panic: ${e.message}`);
  69        } else {
  70          console.error(`goroutine ${g.id}: unhandled error:`, e);
  71        }
  72        if (typeof process !== 'undefined' && process.exit) process.exit(2);
  73      }
  74    }
  75  
  76    running = false;
  77  }
  78  
  79  // Yield current goroutine to let others run.
  80  export function gosched() {
  81    return new Promise(resolve => {
  82      queueMicrotask(resolve);
  83    });
  84  }
  85  
  86  // Sleep for a duration (nanoseconds, but we convert to ms).
  87  export function sleep(ns) {
  88    const ms = Math.max(0, ns / 1_000_000);
  89    return new Promise(resolve => setTimeout(resolve, ms));
  90  }
  91  
  92  // Run the main function. Goroutines continue asynchronously.
  93  export async function runMain(mainFn) {
  94    try {
  95      const result = mainFn();
  96      if (result instanceof Promise) await result;
  97    } catch (e) {
  98      if (e && e.name === 'GoPanic') {
  99        console.error(`panic: ${e.message}`);
 100        console.error(e.stack);
 101        if (typeof process !== 'undefined' && process.exit) process.exit(2);
 102      }
 103      throw e;
 104    }
 105  
 106    if (runQueue.length > 0) {
 107      scheduleRun();
 108    }
 109  }
 110