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          // Don't await — let the goroutine run asynchronously so other
  49          // goroutines can interleave via channel awaits. Awaiting here would
  50          // deadlock two goroutines communicating via a channel (the sender
  51          // blocks, tick() waits for it, the receiver never starts).
  52          result.then(
  53            () => { g.done = true; },
  54            (e) => {
  55              g.done = true;
  56              g.error = e;
  57              if (typeof e === 'object' && e !== null && e.name === 'GoPanic') {
  58                console.error(`goroutine ${g.id}: panic: ${e.message}`);
  59              } else {
  60                console.error(`goroutine ${g.id}: unhandled error:`, e);
  61              }
  62              if (typeof process !== 'undefined' && process.exit) process.exit(2);
  63            }
  64          );
  65        } else {
  66          g.done = true;
  67        }
  68      } catch (e) {
  69        g.done = true;
  70        g.error = e;
  71        if (typeof e === 'object' && e !== null && e.name === 'GoPanic') {
  72          console.error(`goroutine ${g.id}: panic: ${e.message}`);
  73        } else {
  74          console.error(`goroutine ${g.id}: unhandled error:`, e);
  75        }
  76        if (typeof process !== 'undefined' && process.exit) process.exit(2);
  77      }
  78    }
  79  
  80    running = false;
  81  }
  82  
  83  // Yield current goroutine to let others run.
  84  export function gosched() {
  85    return new Promise(resolve => {
  86      queueMicrotask(resolve);
  87    });
  88  }
  89  
  90  // Sleep for a duration (nanoseconds, but we convert to ms).
  91  export function sleep(ns) {
  92    const ms = Math.max(0, ns / 1_000_000);
  93    return new Promise(resolve => setTimeout(resolve, ms));
  94  }
  95  
  96  // Run the main function. Goroutines continue asynchronously.
  97  export async function runMain(mainFn) {
  98    try {
  99      const result = mainFn();
 100      if (result instanceof Promise) await result;
 101    } catch (e) {
 102      if (e && e.name === 'GoPanic') {
 103        console.error(`panic: ${e.message}`);
 104        console.error(e.stack);
 105        if (typeof process !== 'undefined' && process.exit) process.exit(2);
 106      }
 107      throw e;
 108    }
 109  
 110    // Kick the scheduler to start any queued goroutines.
 111    if (runQueue.length > 0) {
 112      scheduleRun();
 113    }
 114  }
 115