@@ -13,8 +13,10 @@ import { TIMEOUTS } from '$lib/constants/python';
1313import { STATUS_MESSAGES } from '$lib/constants/messages' ;
1414import { PYTHON_PACKAGES } from '$lib/constants/dependencies' ;
1515
16- /** Polling interval for stream results (ms) */
17- const STREAM_POLL_INTERVAL = 30 ;
16+ /** Delay between polls (ms). The server uses long-polling (blocks up
17+ * to 100 ms until data arrives), so data delivery is near-instant.
18+ * This interval is just a safety gap between consecutive requests. */
19+ const STREAM_POLL_INTERVAL = 5 ;
1820
1921/** BroadcastChannel name for cross-tab session coordination */
2022const SESSION_CHANNEL = 'flask-session' ;
@@ -86,28 +88,7 @@ export class FlaskBackend implements Backend {
8688
8789 backendState . update ( ( s ) => ( { ...s , progress : 'Initializing Python worker...' } ) ) ;
8890
89- const initResp = await fetch ( `${ this . host } /api/init` , {
90- method : 'POST' ,
91- headers : {
92- 'Content-Type' : 'application/json' ,
93- 'X-Session-ID' : this . sessionId
94- } ,
95- body : JSON . stringify ( { packages : PYTHON_PACKAGES } ) ,
96- signal : AbortSignal . timeout ( TIMEOUTS . INIT )
97- } ) ;
98- const initData = await initResp . json ( ) ;
99-
100- if ( initData . type === 'error' ) throw new Error ( initData . error ) ;
101-
102- if ( initData . messages ) {
103- for ( const msg of initData . messages ) {
104- if ( msg . type === 'stdout' && this . stdoutCallback ) this . stdoutCallback ( msg . value ) ;
105- if ( msg . type === 'stderr' && this . stderrCallback ) this . stderrCallback ( msg . value ) ;
106- if ( msg . type === 'progress' ) {
107- backendState . update ( ( s ) => ( { ...s , progress : msg . value } ) ) ;
108- }
109- }
110- }
91+ await this . postInit ( { updateProgress : true } ) ;
11192
11293 this . serverInitPromise = Promise . resolve ( ) ;
11394
@@ -126,6 +107,10 @@ export class FlaskBackend implements Backend {
126107
127108 terminate ( ) : void {
128109 this . stopStreaming ( ) ;
110+ if ( this . streamPollTimer ) {
111+ clearTimeout ( this . streamPollTimer ) ;
112+ this . streamPollTimer = null ;
113+ }
129114 this . _isStreaming = false ;
130115 this . streamState = { onData : null , onDone : null , onError : null } ;
131116
@@ -172,25 +157,7 @@ export class FlaskBackend implements Backend {
172157 private ensureServerInit ( ) : Promise < void > {
173158 if ( this . serverInitPromise ) return this . serverInitPromise ;
174159
175- this . serverInitPromise = ( async ( ) => {
176- const resp = await fetch ( `${ this . host } /api/init` , {
177- method : 'POST' ,
178- headers : {
179- 'Content-Type' : 'application/json' ,
180- 'X-Session-ID' : this . sessionId
181- } ,
182- body : JSON . stringify ( { packages : PYTHON_PACKAGES } ) ,
183- signal : AbortSignal . timeout ( TIMEOUTS . INIT )
184- } ) ;
185- const data = await resp . json ( ) ;
186- if ( data . type === 'error' ) throw new Error ( data . error ) ;
187- if ( data . messages ) {
188- for ( const msg of data . messages ) {
189- if ( msg . type === 'stdout' && this . stdoutCallback ) this . stdoutCallback ( msg . value ) ;
190- if ( msg . type === 'stderr' && this . stderrCallback ) this . stderrCallback ( msg . value ) ;
191- }
192- }
193- } ) ( ) ;
160+ this . serverInitPromise = this . postInit ( { updateProgress : false } ) ;
194161
195162 // Clear on failure so subsequent calls retry instead of returning the rejected promise
196163 this . serverInitPromise . catch ( ( ) => {
@@ -271,6 +238,13 @@ export class FlaskBackend implements Backend {
271238 this . stopStreaming ( ) ;
272239 }
273240
241+ // Clear any lingering poll timer from a previous stream so it
242+ // doesn't pick up a stale stream-done and fire the NEW onDone.
243+ if ( this . streamPollTimer ) {
244+ clearTimeout ( this . streamPollTimer ) ;
245+ this . streamPollTimer = null ;
246+ }
247+
274248 const id = this . generateId ( ) ;
275249 this . _isStreaming = true ;
276250 this . streamState = {
@@ -296,7 +270,6 @@ export class FlaskBackend implements Backend {
296270 if ( data . type === 'error' ) {
297271 throw new Error ( data . error ) ;
298272 }
299- // Start polling loop — same as Pyodide worker's onmessage dispatching
300273 this . pollStreamResults ( ) ;
301274 } )
302275 . catch ( ( error ) => {
@@ -309,29 +282,19 @@ export class FlaskBackend implements Backend {
309282 stopStreaming ( ) : void {
310283 if ( ! this . _isStreaming ) return ;
311284
312- // Stop polling timer — the server will send stream-done which triggers onDone
313- if ( this . streamPollTimer ) {
314- clearTimeout ( this . streamPollTimer ) ;
315- this . streamPollTimer = null ;
316- }
317-
318- // Tell server to stop, then do one final poll to get the stream-done message
285+ // Just send the stop signal — don't disrupt the polling loop.
286+ // The poll loop will naturally pick up stream-done and clean up,
287+ // matching how Pyodide's stopStreaming just sends stream-stop and
288+ // lets worker.onmessage handle the rest.
319289 fetch ( `${ this . host } /api/stream/stop` , {
320290 method : 'POST' ,
321291 headers : {
322292 'Content-Type' : 'application/json' ,
323293 'X-Session-ID' : this . sessionId
324294 }
325- } )
326- . then ( ( ) => this . pollStreamResults ( ) )
327- . catch ( ( ) => {
328- // If final poll fails, clean up locally
329- this . _isStreaming = false ;
330- if ( this . streamState . onDone ) {
331- this . streamState . onDone ( ) ;
332- }
333- this . streamState = { onData : null , onDone : null , onError : null } ;
334- } ) ;
295+ } ) . catch ( ( ) => {
296+ // Network failure — poll loop will also fail and clean up
297+ } ) ;
335298 }
336299
337300 isStreaming ( ) : boolean {
@@ -374,6 +337,33 @@ export class FlaskBackend implements Backend {
374337 return `repl_${ ++ this . messageId } ` ;
375338 }
376339
340+ /**
341+ * POST /api/init with packages and forward worker messages to callbacks.
342+ * Shared by init() (first load with progress UI) and ensureServerInit() (lazy re-init).
343+ */
344+ private async postInit ( opts : { updateProgress : boolean } ) : Promise < void > {
345+ const resp = await fetch ( `${ this . host } /api/init` , {
346+ method : 'POST' ,
347+ headers : {
348+ 'Content-Type' : 'application/json' ,
349+ 'X-Session-ID' : this . sessionId
350+ } ,
351+ body : JSON . stringify ( { packages : PYTHON_PACKAGES } ) ,
352+ signal : AbortSignal . timeout ( TIMEOUTS . INIT )
353+ } ) ;
354+ const data = await resp . json ( ) ;
355+ if ( data . type === 'error' ) throw new Error ( data . error ) ;
356+ if ( data . messages ) {
357+ for ( const msg of data . messages ) {
358+ if ( msg . type === 'stdout' && this . stdoutCallback ) this . stdoutCallback ( msg . value ) ;
359+ if ( msg . type === 'stderr' && this . stderrCallback ) this . stderrCallback ( msg . value ) ;
360+ if ( msg . type === 'progress' && opts . updateProgress ) {
361+ backendState . update ( ( s ) => ( { ...s , progress : msg . value } ) ) ;
362+ }
363+ }
364+ }
365+ }
366+
377367 /**
378368 * Check if a response indicates the worker crashed or timed out.
379369 * If so, clear serverInitPromise so the next request triggers re-init.
0 commit comments