Skip to content

Commit

Permalink
Wait for MakeCode to be ready after language change (#581)
Browse files Browse the repository at this point in the history
We might try to push logic for this into makecode-embed as it's getting kinda messy dealing with it at the app level.
  • Loading branch information
microbit-robert authored Jan 13, 2025
1 parent 0d56850 commit 764e2dd
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 28 deletions.
12 changes: 6 additions & 6 deletions src/components/LanguageDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import {
} from "@chakra-ui/modal";
import { HStack, Icon, Link, SimpleGrid, Text, VStack } from "@chakra-ui/react";
import { useCallback } from "react";
import { FormattedMessage } from "react-intl";
import { Language, supportedLanguages } from "../settings";
import { useSettings } from "../store";
import { RiExternalLinkLine } from "react-icons/ri";
import { FormattedMessage } from "react-intl";
import { deployment } from "../deployment";
import { Language, supportedLanguages } from "../settings";
import { useStore } from "../store";

interface LanguageDialogProps {
isOpen: boolean;
Expand All @@ -34,13 +34,13 @@ export const LanguageDialog = ({
onClose,
finalFocusRef,
}: LanguageDialogProps) => {
const [, setSettings] = useSettings();
const setLanguage = useStore((s) => s.setLanguage);
const handleChooseLanguage = useCallback(
(languageId: string) => {
setSettings({ languageId });
setLanguage(languageId);
onClose();
},
[setSettings, onClose]
[onClose, setLanguage]
);
const hasPreviewLanguages = supportedLanguages.some((l) => l.preview);
return (
Expand Down
59 changes: 39 additions & 20 deletions src/hooks/project-hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import {
readFileAsText,
} from "../utils/fs-util";
import { useDownloadActions } from "./download-hooks";
import { usePromiseRef } from "./use-promise-ref";

class CodeEditorError extends Error {}

Expand Down Expand Up @@ -136,11 +135,17 @@ export const ProjectProvider = ({
const checkIfProjectNeedsFlush = useStore((s) => s.checkIfProjectNeedsFlush);
const getCurrentProject = useStore((s) => s.getCurrentProject);
const setPostImportDialogState = useStore((s) => s.setPostImportDialogState);
const { editorReadyPromise, editorContentLoadedPromise } = useStore(
(s) => s.editorPromises
);
const startUpTimestamp = useStore((s) => s.editorStartUpTimestamp);
const langChangeFlushedToEditor = useStore(
(s) => s.langChangeFlushedToEditor
);
const checkIfLangChanged = useStore((s) => s.checkIfLangChanged);
const navigate = useNavigate();

const project = useStore((s) => s.project);
const editorReadyPromiseRef = usePromiseRef<void>();
const editorContentLoadedPromiseRef = usePromiseRef<void>();
const initialProjects = useCallback(() => {
logging.log(
`[MakeCode] Initialising with header ID: ${project.header?.id}`
Expand All @@ -150,30 +155,29 @@ export const ProjectProvider = ({
}, [logging, project]);

const startUpTimeout = 90000;
const startUpTimestamp = useRef<number>(Date.now());

const onWorkspaceLoaded = useCallback(async () => {
logging.log("[MakeCode] Workspace loaded");
await editorContentLoadedPromiseRef.current.promise;
await editorContentLoadedPromise.promise;
// Get latest start up state and only mark editor ready if editor has not timed out.
getEditorStartUp() !== "timed out" && editorReady();
editorReadyPromiseRef.current.resolve();
editorReadyPromise.resolve();
}, [
editorContentLoadedPromiseRef,
editorContentLoadedPromise,
editorReady,
editorReadyPromiseRef,
editorReadyPromise,
getEditorStartUp,
logging,
]);

const onEditorContentLoaded = useCallback(() => {
logging.log("[MakeCode] Editor content loaded");
editorContentLoadedPromiseRef.current.resolve();
}, [editorContentLoadedPromiseRef, logging]);
editorContentLoadedPromise.resolve();
}, [editorContentLoadedPromise, logging]);

const checkIfEditorStartUpTimedOut = useCallback(
async (promise: Promise<void> | undefined) => {
const elapsedTimeSinceStartup = Date.now() - startUpTimestamp.current;
const elapsedTimeSinceStartup = Date.now() - startUpTimestamp;
const remainingTimeout = startUpTimeout - elapsedTimeSinceStartup;
if (
// Editor has already timed out.
Expand All @@ -195,31 +199,39 @@ export const ProjectProvider = ({
: []),
]);
},
[editorStartUp]
[editorStartUp, startUpTimestamp]
);

const doAfterEditorUpdatePromise = useRef<Promise<void>>();
const doAfterEditorUpdate = useCallback(
async (action: () => Promise<void>) => {
if (!doAfterEditorUpdatePromise.current && checkIfProjectNeedsFlush()) {
if (
!doAfterEditorUpdatePromise.current &&
(checkIfProjectNeedsFlush() || checkIfLangChanged())
) {
doAfterEditorUpdatePromise.current = (async () => {
// driverRef.current is not defined on first render.
// Only an issue when navigating to code page directly.
if (!driverRef.current) {
throw new CodeEditorError("MakeCode iframe ref is undefined");
} else {
} else if (checkIfProjectNeedsFlush()) {
logging.log("[MakeCode] Importing project");
await editorReadyPromiseRef.current.promise;
await editorReadyPromise.promise;
const project = getCurrentProject();
expectChangedHeader();
try {
await driverRef.current.importProject({ project });
logging.log("[MakeCode] Project import succeeded");
projectFlushedToEditor();
langChangeFlushedToEditor();
} catch (e) {
logging.log("[MakeCode] Project import failed");
throw e;
}
} else {
logging.log("[MakeCode] Waiting for editor after language change");
await editorReadyPromise.promise;
langChangeFlushedToEditor();
}
})();
}
Expand All @@ -243,12 +255,14 @@ export const ProjectProvider = ({
},
[
checkIfProjectNeedsFlush,
checkIfLangChanged,
driverRef,
logging,
editorReadyPromiseRef,
editorReadyPromise.promise,
getCurrentProject,
expectChangedHeader,
projectFlushedToEditor,
langChangeFlushedToEditor,
checkIfEditorStartUpTimedOut,
editorTimedOut,
]
Expand Down Expand Up @@ -311,7 +325,7 @@ export const ProjectProvider = ({
// Check if is a MakeCode hex, otherwise show error dialog.
if (hex.includes(makeCodeMagicMark)) {
const hasTimedOut = await checkIfEditorStartUpTimedOut(
editorReadyPromiseRef.current.promise
editorReadyPromise.promise
);
if (hasTimedOut) {
openEditorTimedOutDialog();
Expand All @@ -332,7 +346,7 @@ export const ProjectProvider = ({
[
checkIfEditorStartUpTimedOut,
driverRef,
editorReadyPromiseRef,
editorReadyPromise,
loadDataset,
logging,
navigate,
Expand Down Expand Up @@ -412,9 +426,14 @@ export const ProjectProvider = ({
const editorChange = useStore((s) => s.editorChange);
const onWorkspaceSave = useCallback(
(event: EditorWorkspaceSaveRequest) => {
editorChange(event.project);
if (!checkIfLangChanged()) {
// We don't want to handle these events until MakeCode has been
// reinitialised after a language change.
// We should reinitialise with the latest project.
editorChange(event.project);
}
},
[editorChange]
[checkIfLangChanged, editorChange]
);

const onBack = useCallback(() => {
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/use-promise-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ interface PromiseCallbacks<T> {
reject: (reason?: unknown) => void;
}

interface PromiseInfo<T> extends PromiseCallbacks<T> {
export interface PromiseInfo<T> extends PromiseCallbacks<T> {
promise: Promise<T>;
}

const createPromise = <T>(): PromiseInfo<T> => {
export const createPromise = <T>(): PromiseInfo<T> => {
let callbacks: PromiseCallbacks<T> | undefined;
const promise = new Promise<T>((resolve, reject) => {
callbacks = { resolve, reject };
Expand Down
56 changes: 56 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { mlSettings } from "./mlConfig";
import { BufferedData } from "./buffered-data";
import { getDetectedAction } from "./utils/prediction";
import { getTour as getTourSpec } from "./tours";
import { createPromise, PromiseInfo } from "./hooks/use-promise-ref";

export const modelUrl = "indexeddb://micro:bit-ai-creator-model";

Expand Down Expand Up @@ -163,7 +164,13 @@ export interface State {
isEditorOpen: boolean;
isEditorReady: boolean;
editorStartUp: EditorStartUp;
editorStartUpTimestamp: number;
editorPromises: {
editorReadyPromise: PromiseInfo<void>;
editorContentLoadedPromise: PromiseInfo<void>;
};
isEditorTimedOutDialogOpen: boolean;
langChanged: boolean;

download: DownloadState;
downloadFlashingProgress: number;
Expand Down Expand Up @@ -222,6 +229,7 @@ export interface Actions {
closeTrainModelDialogs: () => void;
trainModel(): Promise<boolean>;
setSettings(update: Partial<Settings>): void;
setLanguage(languageId: string): void;

/**
* Resets the project.
Expand All @@ -238,6 +246,8 @@ export interface Actions {
*/
getCurrentProject(): Project;
checkIfProjectNeedsFlush(): boolean;
checkIfLangChanged(): boolean;
langChangeFlushedToEditor(): void;
editorChange(project: Project): void;
editorReady(): void;
editorTimedOut(): void;
Expand Down Expand Up @@ -307,7 +317,13 @@ const createMlStore = (logging: Logging) => {
isEditorOpen: false,
isEditorReady: false,
editorStartUp: "in-progress",
editorStartUpTimestamp: Date.now(),
editorPromises: {
editorReadyPromise: createPromise<void>(),
editorContentLoadedPromise: createPromise<void>(),
},
isEditorTimedOutDialogOpen: false,
langChanged: false,
appEditNeedsFlushToEditor: true,
changedHeaderExpected: false,
// This dialog flow spans two pages
Expand Down Expand Up @@ -342,6 +358,33 @@ const createMlStore = (logging: Logging) => {
);
},

setLanguage(languageId: string) {
const currLanguageId = get().settings.languageId;
if (languageId === currLanguageId) {
// No need to update language if language is the same.
// MakeCode does not reload.
return;
}
set(
({ settings }) => ({
settings: {
...settings,
languageId,
},
editorPromises: {
editorReadyPromise: createPromise<void>(),
editorContentLoadedPromise: createPromise<void>(),
},
isEditorReady: false,
editorStartUp: "in-progress",
editorStartUpTimestamp: Date.now(),
langChanged: true,
}),
false,
"setLanguage"
);
},

newSession(projectName?: string) {
const untitledProject = createUntitledProject();
set(
Expand Down Expand Up @@ -768,6 +811,10 @@ const createMlStore = (logging: Logging) => {
return get().appEditNeedsFlushToEditor;
},

checkIfLangChanged() {
return get().langChanged;
},

getCurrentProject() {
return get().project;
},
Expand Down Expand Up @@ -892,6 +939,15 @@ const createMlStore = (logging: Logging) => {
"setChangedHeaderExpected"
);
},
langChangeFlushedToEditor() {
set(
{
langChanged: false,
},
false,
"langChangeFlushedToEditor"
);
},
projectFlushedToEditor() {
set(
{
Expand Down

0 comments on commit 764e2dd

Please sign in to comment.