diff --git a/src/hooks/project-hooks.tsx b/src/hooks/project-hooks.tsx index 83cb6e04e..59fc36cf3 100644 --- a/src/hooks/project-hooks.tsx +++ b/src/hooks/project-hooks.tsx @@ -29,6 +29,7 @@ import { PostImportDialogState, SaveStep, SaveType, + WaitForEditorResult, } from "../model"; import { untitledProjectName as untitled, @@ -202,29 +203,56 @@ export const ProjectProvider = ({ editorContentLoadedPromise.resolve(); }, [editorContentLoadedPromise, logging]); - const checkIfEditorStartUpTimedOut = useCallback( - async (promise: Promise | undefined) => { + const waitForEditor = useCallback( + async ( + promise: Promise | undefined + ): Promise => { const elapsedTimeSinceStartup = Date.now() - startUpTimestamp; const remainingTimeout = startUpTimeout - elapsedTimeSinceStartup; if ( - // Editor has already timed out. (editorStartUp === "in-progress" && remainingTimeout <= 0) || editorStartUp === "timed out" ) { - return true; + return WaitForEditorResult.TimedOut; + } + // Races the editor ready promise against failure conditions. + if (!promise) { + return WaitForEditorResult.Ready; + } + const raceEntries: Promise[] = [ + promise.then(() => WaitForEditorResult.Ready), + ]; + if (remainingTimeout > 0) { + raceEntries.push( + new Promise((resolve) => + setTimeout( + () => resolve(WaitForEditorResult.TimedOut), + remainingTimeout + ) + ) + ); + } + // MakeCode requires a network connection to load so there's no point + // waiting if we're offline. + const offlineAbort = new AbortController(); + raceEntries.push( + new Promise((resolve) => { + if (!navigator.onLine) { + resolve(WaitForEditorResult.Offline); + } else { + window.addEventListener( + "offline", + () => resolve(WaitForEditorResult.Offline), + { once: true, signal: offlineAbort.signal } + ); + } + }) + ); + try { + return await Promise.race(raceEntries); + } finally { + offlineAbort.abort(); } - return await Promise.race([ - promise, - ...(remainingTimeout > 0 - ? [ - new Promise((resolve) => - setTimeout(() => { - resolve(true); - }, remainingTimeout) - ), - ] - : []), - ]); }, [editorStartUp, startUpTimestamp] ); @@ -264,12 +292,10 @@ export const ProjectProvider = ({ } try { - const hasTimedOut = await checkIfEditorStartUpTimedOut( - doAfterEditorUpdatePromise.current - ); - if (hasTimedOut) { + const result = await waitForEditor(doAfterEditorUpdatePromise.current); + if (result !== WaitForEditorResult.Ready) { editorTimedOut(); - logging.log("[MakeCode] Load timed out"); + logging.log(`[MakeCode] Load failed: ${result}`); logging.event({ type: "makecode-load-failed", }); @@ -290,7 +316,7 @@ export const ProjectProvider = ({ setEditorImportingState, projectFlushedToEditor, langChangeFlushedToEditor, - checkIfEditorStartUpTimedOut, + waitForEditor, editorTimedOut, ] ); @@ -352,10 +378,8 @@ export const ProjectProvider = ({ setPostImportDialogState(PostImportDialogState.Error); return; } - const hasTimedOut = await checkIfEditorStartUpTimedOut( - editorReadyPromise.promise - ); - if (hasTimedOut) { + const result = await waitForEditor(editorReadyPromise.promise); + if (result !== WaitForEditorResult.Ready) { openEditorTimedOutDialog(); return; } @@ -367,7 +391,7 @@ export const ProjectProvider = ({ }); }, [ - checkIfEditorStartUpTimedOut, + waitForEditor, driverRef, editorReadyPromise.promise, openEditorTimedOutDialog, diff --git a/src/model.ts b/src/model.ts index 7590ef6df..cbf4754ae 100644 --- a/src/model.ts +++ b/src/model.ts @@ -236,3 +236,9 @@ export enum PostImportDialogState { } export type EditorStartUp = "in-progress" | "timed out" | "done"; + +export enum WaitForEditorResult { + Ready = "ready", + TimedOut = "timed-out", + Offline = "offline", +}