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