2424import { parentPort } from "node:worker_threads" ;
2525
2626if ( ! parentPort ) {
27- throw new Error ( "runtime/worker.js must be loaded as a worker_thread." ) ;
27+ throw new Error ( "runtime/worker.ts must be loaded as a worker_thread." ) ;
2828}
2929
30+ const port = parentPort ;
31+
32+ type WorkerPostMessage =
33+ | { type : "ready" }
34+ | { type : "changes" ; changes : unknown [ ] }
35+ | { type : "error" ; error : SerializedError ; source : string }
36+ | { type : "console" ; level : ConsoleLevel ; text : string }
37+ | { type : "heartbeat" ; t : number }
38+ | { type : "online" } ;
39+
40+ type WorkerCommand =
41+ | { type : "init" ; code : string ; options ?: RuntimeOptions }
42+ | { type : "setCode" ; code : string }
43+ | { type : "setIsRunning" ; value : boolean }
44+ | { type : "run" }
45+ | { type : "destroy" } ;
46+
47+ type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug" ;
48+ type RuntimeOptions = { cellTimeoutMs ?: number } ;
49+ type SerializedError = { message : string ; name : string ; stack : string | null ; code : unknown } ;
50+ type RuntimeInstance = {
51+ destroy ?: ( ) => void ;
52+ onChanges : ( callback : ( event : { changes : unknown [ ] } ) => void ) => void ;
53+ onError : ( callback : ( event : { error : unknown ; source ?: string } ) => void ) => void ;
54+ setIsRunning : ( value : boolean ) => void ;
55+ setCode : ( code : string ) => void ;
56+ run : ( ) => void ;
57+ } ;
58+
3059// -- Capture console & stderr BEFORE loading the runtime, so anything the
3160// stdlib (or its observer.rejected path) writes during boot is forwarded.
32- function safePost ( msg ) {
61+ function safePost ( msg : WorkerPostMessage ) {
3362 try {
34- parentPort . postMessage ( msg ) ;
63+ port . postMessage ( msg ) ;
3564 } catch {
3665 /* parent gone */
3766 }
3867}
3968
40- function stringifyArgs ( args ) {
69+ function stringifyArgs ( args : unknown [ ] ) : string {
4170 return args
4271 . map ( ( a ) => {
4372 if ( a instanceof Error ) return ( a . stack ? a . stack : a . message ) || String ( a ) ;
@@ -51,28 +80,33 @@ function stringifyArgs(args) {
5180 . join ( " " ) ;
5281}
5382
54- function serializeError ( e ) {
55- if ( ! e ) return { message : "(unknown error)" } ;
83+ function serializeError ( e : unknown ) : SerializedError {
84+ if ( ! e ) return { message : "(unknown error)" , name : "Error" , stack : null , code : null } ;
85+ const error = e as { message ?: string ; name ?: string ; stack ?: string ; code ?: unknown } ;
5686 return {
57- message : e . message || String ( e ) ,
58- name : e . name || "Error" ,
59- stack : e . stack || null ,
60- code : e . code || null ,
87+ message : error . message || String ( e ) ,
88+ name : error . name || "Error" ,
89+ stack : error . stack || null ,
90+ code : error . code || null ,
6191 } ;
6292}
6393
64- for ( const level of [ "log" , "info" , "warn" , "error" , "debug" ] ) {
94+ for ( const level of [ "log" , "info" , "warn" , "error" , "debug" ] satisfies ConsoleLevel [ ] ) {
6595 console [ level ] = ( ...args ) => safePost ( { type : "console" , level, text : stringifyArgs ( args ) } ) ;
6696}
6797
6898const origStderrWrite = process . stderr . write . bind ( process . stderr ) ;
69- process . stderr . write = ( chunk , encoding , callback ) => {
99+ process . stderr . write = ( (
100+ chunk : string | Uint8Array ,
101+ encoding ?: BufferEncoding | ( ( err ?: Error | null ) => void ) ,
102+ callback ?: ( err ?: Error | null ) => void ,
103+ ) => {
70104 const text = typeof chunk === "string" ? chunk : ( chunk ?. toString ?.( ) ?? String ( chunk ) ) ;
71105 if ( text . trim ( ) ) safePost ( { type : "console" , level : "error" , text : text . replace ( / \n + $ / , "" ) } ) ;
72106 if ( typeof encoding === "function" ) encoding ( ) ;
73107 else if ( typeof callback === "function" ) callback ( ) ;
74108 return true ;
75- } ;
109+ } ) as typeof process . stderr . write ;
76110
77111// Process-level catch-alls — async failures in notebook code shouldn't kill
78112// the worker (and even if they do, the main thread will just respawn).
@@ -90,16 +124,16 @@ process.on("uncaughtException", (e) => {
90124const HEARTBEAT_MS = 200 ;
91125setInterval ( ( ) => safePost ( { type : "heartbeat" , t : Date . now ( ) } ) , HEARTBEAT_MS ) . unref ( ) ;
92126
93- let rt = null ;
127+ let rt : RuntimeInstance | null = null ;
94128
95- function bootRuntime ( code , options ) {
129+ function bootRuntime ( code : string , options ?: RuntimeOptions ) {
96130 rt ?. destroy ?.( ) ;
97131 rt = null ;
98132
99133 // Lazy import so the heartbeat above is already running when the runtime
100134 // (and its dependencies) get evaluated.
101135 return import ( "./index.js" ) . then ( ( { createRuntime} ) => {
102- rt = createRuntime ( code , options || { } ) ;
136+ rt = createRuntime ( code , options || { } ) as RuntimeInstance ;
103137 rt . onChanges ( ( evt ) => safePost ( { type : "changes" , changes : evt . changes } ) ) ;
104138 rt . onError ( ( evt ) => safePost ( { type : "error" , error : serializeError ( evt . error ) , source : evt . source || "runtime" } ) ) ;
105139 rt . setIsRunning ( true ) ;
@@ -112,7 +146,7 @@ function bootRuntime(code, options) {
112146 } ) ;
113147}
114148
115- parentPort . on ( "message" , ( msg ) => {
149+ port . on ( "message" , ( msg : WorkerCommand ) => {
116150 switch ( msg ?. type ) {
117151 case "init" :
118152 bootRuntime ( msg . code , msg . options ) . catch ( ( e ) =>
@@ -151,7 +185,7 @@ parentPort.on("message", (msg) => {
151185 default :
152186 // Unknown message — log to original stderr (which we replaced above).
153187 try {
154- origStderrWrite ( `recho-worker: unknown message type ${ JSON . stringify ( msg ? .type ) } \n` ) ;
188+ origStderrWrite ( `recho-worker: unknown message type ${ JSON . stringify ( ( msg as { type ?: unknown } ) . type ) } \n` ) ;
155189 } catch {
156190 /* ignore */
157191 }
0 commit comments