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