@@ -55,6 +55,18 @@ export interface EditorShareOptions {
5555 projectName : string ;
5656}
5757
58+ /**
59+ * Reasons the MakeCode editor startup can fail.
60+ */
61+ export type StartUpFailure = 'timeout' | 'offline' | 'load-error' ;
62+
63+ /**
64+ * Result of waitUntilReady().
65+ */
66+ export type StartUpResult =
67+ | { ready : true }
68+ | { ready : false ; reason : StartUpFailure } ;
69+
5870export interface Options {
5971 /**
6072 * A function that provides the initial set of projects to be used when initialising MakeCode.
@@ -132,6 +144,14 @@ export interface Options {
132144 */
133145 onSave ?: ( save : { name : string ; hex : string } ) => void ;
134146
147+ /**
148+ * Called when the iframe fails to load.
149+ *
150+ * This does not cover all failure modes but when it fires it is an
151+ * unambiguous signal of failure.
152+ */
153+ onLoadError ?: ( ) => void ;
154+
135155 /**
136156 * Requests the embedding app handles a press/tap on the back arrow.
137157 *
@@ -146,6 +166,29 @@ export interface Options {
146166 * Applies only with `controller` set to `2`.
147167 */
148168 onBackLongPress ?: ( ) => void ;
169+
170+ /**
171+ * Timeout in milliseconds for the MakeCode editor to reach the
172+ * workspace loaded state. Defaults to 90000 (90 seconds).
173+ * Set to 0 to disable the timeout.
174+ */
175+ startUpTimeout ?: number ;
176+ }
177+
178+ /**
179+ * A one-shot signal that can be resolved once and awaited many times.
180+ */
181+ class Signal {
182+ private resolve ! : ( ) => void ;
183+ readonly promise = new Promise < void > ( ( r ) => {
184+ this . resolve = r ;
185+ } ) ;
186+ resolved = false ;
187+
188+ fire ( ) {
189+ this . resolved = true ;
190+ this . resolve ( ) ;
191+ }
149192}
150193
151194/**
@@ -166,6 +209,11 @@ export class MakeCodeFrameDriver {
166209 }
167210 > ( ) ;
168211
212+ private workspaceLoaded = new Signal ( ) ;
213+ private editorContentLoaded = new Signal ( ) ;
214+ private loadError = new Signal ( ) ;
215+ private startUpTimestamp = 0 ;
216+
169217 private _expectedOrigin : string | undefined ;
170218 private expectedOrigin = ( ) => {
171219 if ( this . _expectedOrigin ) {
@@ -260,6 +308,7 @@ export class MakeCodeFrameDriver {
260308 ) ;
261309 }
262310 case 'workspaceloaded' : {
311+ this . workspaceLoaded . fire ( ) ;
263312 return this . options . onWorkspaceLoaded ?.(
264313 data as EditorWorkspaceSyncRequest
265314 ) ;
@@ -271,6 +320,7 @@ export class MakeCodeFrameDriver {
271320 return ;
272321 }
273322 case 'editorcontentloaded' : {
323+ this . editorContentLoaded . fire ( ) ;
274324 return this . options . onEditorContentLoaded ?.(
275325 data as EditorContentLoadedRequest
276326 ) ;
@@ -311,6 +361,11 @@ export class MakeCodeFrameDriver {
311361 ) { }
312362
313363 initialize ( ) {
364+ this . startUpTimestamp = Date . now ( ) ;
365+ this . workspaceLoaded = new Signal ( ) ;
366+ this . editorContentLoaded = new Signal ( ) ;
367+ this . loadError = new Signal ( ) ;
368+
314369 window . addEventListener ( 'message' , this . listener ) ;
315370 // If the iframe is already loaded this will ensure we still initialize correctly
316371 this . iframe ( ) ?. contentWindow ?. postMessage (
@@ -329,6 +384,92 @@ export class MakeCodeFrameDriver {
329384 window . removeEventListener ( 'message' , this . listener ) ;
330385 }
331386
387+ /**
388+ * Notify the driver that the iframe failed to load.
389+ *
390+ * Called by the React component's onError handler or manually by vanilla
391+ * consumers.
392+ */
393+ notifyLoadError ( ) : void {
394+ this . loadError . fire ( ) ;
395+ this . options . onLoadError ?.( ) ;
396+ }
397+
398+ /**
399+ * Whether the editor workspace has fully loaded since initialize().
400+ */
401+ get isReady ( ) : boolean {
402+ return this . workspaceLoaded . resolved && this . editorContentLoaded . resolved ;
403+ }
404+
405+ /**
406+ * Resolves when the MakeCode editor is fully loaded (both editor content
407+ * loaded and workspace loaded), or resolves with a failure reason if
408+ * startup times out, goes offline, or encounters a load error.
409+ */
410+ async waitUntilReady ( ) : Promise < StartUpResult > {
411+ if ( this . isReady ) {
412+ return { ready : true } ;
413+ }
414+ if ( this . loadError . resolved ) {
415+ return { ready : false , reason : 'load-error' } ;
416+ }
417+
418+ const timeout = this . options . startUpTimeout ?? 90_000 ;
419+ const elapsed = Date . now ( ) - this . startUpTimestamp ;
420+ const remaining = timeout - elapsed ;
421+
422+ if ( timeout > 0 && remaining <= 0 ) {
423+ return { ready : false , reason : 'timeout' } ;
424+ }
425+
426+ const raceEntries : Promise < StartUpResult > [ ] = [
427+ Promise . all ( [
428+ this . editorContentLoaded . promise ,
429+ this . workspaceLoaded . promise ,
430+ ] ) . then ( ( ) : StartUpResult => ( { ready : true } ) ) ,
431+ ] ;
432+
433+ if ( timeout > 0 && remaining > 0 ) {
434+ raceEntries . push (
435+ new Promise ( ( resolve ) =>
436+ setTimeout (
437+ ( ) => resolve ( { ready : false , reason : 'timeout' } ) ,
438+ remaining
439+ )
440+ )
441+ ) ;
442+ }
443+
444+ raceEntries . push (
445+ this . loadError . promise . then (
446+ ( ) : StartUpResult => ( { ready : false , reason : 'load-error' } )
447+ )
448+ ) ;
449+
450+ // MakeCode requires a network connection to load.
451+ const offlineAbort = new AbortController ( ) ;
452+ raceEntries . push (
453+ new Promise ( ( resolve ) => {
454+ if ( ! navigator . onLine ) {
455+ resolve ( { ready : false , reason : 'offline' } ) ;
456+ } else {
457+ window . addEventListener (
458+ 'offline' ,
459+ ( ) => resolve ( { ready : false , reason : 'offline' } ) ,
460+ { once : true , signal : offlineAbort . signal }
461+ ) ;
462+ }
463+ } )
464+ ) ;
465+
466+ try {
467+ return await Promise . race ( raceEntries ) ;
468+ } finally {
469+ offlineAbort . abort ( ) ;
470+ }
471+ }
472+
332473 private sendRequest = (
333474 message : EditorMessageRequestUnion
334475 ) : Promise < unknown > => {
0 commit comments