Skip to content

Commit 764e2dd

Browse files
Wait for MakeCode to be ready after language change (#581)
We might try to push logic for this into makecode-embed as it's getting kinda messy dealing with it at the app level.
1 parent 0d56850 commit 764e2dd

File tree

4 files changed

+103
-28
lines changed

4 files changed

+103
-28
lines changed

src/components/LanguageDialog.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import {
1414
} from "@chakra-ui/modal";
1515
import { HStack, Icon, Link, SimpleGrid, Text, VStack } from "@chakra-ui/react";
1616
import { useCallback } from "react";
17-
import { FormattedMessage } from "react-intl";
18-
import { Language, supportedLanguages } from "../settings";
19-
import { useSettings } from "../store";
2017
import { RiExternalLinkLine } from "react-icons/ri";
18+
import { FormattedMessage } from "react-intl";
2119
import { deployment } from "../deployment";
20+
import { Language, supportedLanguages } from "../settings";
21+
import { useStore } from "../store";
2222

2323
interface LanguageDialogProps {
2424
isOpen: boolean;
@@ -34,13 +34,13 @@ export const LanguageDialog = ({
3434
onClose,
3535
finalFocusRef,
3636
}: LanguageDialogProps) => {
37-
const [, setSettings] = useSettings();
37+
const setLanguage = useStore((s) => s.setLanguage);
3838
const handleChooseLanguage = useCallback(
3939
(languageId: string) => {
40-
setSettings({ languageId });
40+
setLanguage(languageId);
4141
onClose();
4242
},
43-
[setSettings, onClose]
43+
[onClose, setLanguage]
4444
);
4545
const hasPreviewLanguages = supportedLanguages.some((l) => l.preview);
4646
return (

src/hooks/project-hooks.tsx

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import {
4242
readFileAsText,
4343
} from "../utils/fs-util";
4444
import { useDownloadActions } from "./download-hooks";
45-
import { usePromiseRef } from "./use-promise-ref";
4645

4746
class CodeEditorError extends Error {}
4847

@@ -136,11 +135,17 @@ export const ProjectProvider = ({
136135
const checkIfProjectNeedsFlush = useStore((s) => s.checkIfProjectNeedsFlush);
137136
const getCurrentProject = useStore((s) => s.getCurrentProject);
138137
const setPostImportDialogState = useStore((s) => s.setPostImportDialogState);
138+
const { editorReadyPromise, editorContentLoadedPromise } = useStore(
139+
(s) => s.editorPromises
140+
);
141+
const startUpTimestamp = useStore((s) => s.editorStartUpTimestamp);
142+
const langChangeFlushedToEditor = useStore(
143+
(s) => s.langChangeFlushedToEditor
144+
);
145+
const checkIfLangChanged = useStore((s) => s.checkIfLangChanged);
139146
const navigate = useNavigate();
140147

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

152157
const startUpTimeout = 90000;
153-
const startUpTimestamp = useRef<number>(Date.now());
154158

155159
const onWorkspaceLoaded = useCallback(async () => {
156160
logging.log("[MakeCode] Workspace loaded");
157-
await editorContentLoadedPromiseRef.current.promise;
161+
await editorContentLoadedPromise.promise;
158162
// Get latest start up state and only mark editor ready if editor has not timed out.
159163
getEditorStartUp() !== "timed out" && editorReady();
160-
editorReadyPromiseRef.current.resolve();
164+
editorReadyPromise.resolve();
161165
}, [
162-
editorContentLoadedPromiseRef,
166+
editorContentLoadedPromise,
163167
editorReady,
164-
editorReadyPromiseRef,
168+
editorReadyPromise,
165169
getEditorStartUp,
166170
logging,
167171
]);
168172

169173
const onEditorContentLoaded = useCallback(() => {
170174
logging.log("[MakeCode] Editor content loaded");
171-
editorContentLoadedPromiseRef.current.resolve();
172-
}, [editorContentLoadedPromiseRef, logging]);
175+
editorContentLoadedPromise.resolve();
176+
}, [editorContentLoadedPromise, logging]);
173177

174178
const checkIfEditorStartUpTimedOut = useCallback(
175179
async (promise: Promise<void> | undefined) => {
176-
const elapsedTimeSinceStartup = Date.now() - startUpTimestamp.current;
180+
const elapsedTimeSinceStartup = Date.now() - startUpTimestamp;
177181
const remainingTimeout = startUpTimeout - elapsedTimeSinceStartup;
178182
if (
179183
// Editor has already timed out.
@@ -195,31 +199,39 @@ export const ProjectProvider = ({
195199
: []),
196200
]);
197201
},
198-
[editorStartUp]
202+
[editorStartUp, startUpTimestamp]
199203
);
200204

201205
const doAfterEditorUpdatePromise = useRef<Promise<void>>();
202206
const doAfterEditorUpdate = useCallback(
203207
async (action: () => Promise<void>) => {
204-
if (!doAfterEditorUpdatePromise.current && checkIfProjectNeedsFlush()) {
208+
if (
209+
!doAfterEditorUpdatePromise.current &&
210+
(checkIfProjectNeedsFlush() || checkIfLangChanged())
211+
) {
205212
doAfterEditorUpdatePromise.current = (async () => {
206213
// driverRef.current is not defined on first render.
207214
// Only an issue when navigating to code page directly.
208215
if (!driverRef.current) {
209216
throw new CodeEditorError("MakeCode iframe ref is undefined");
210-
} else {
217+
} else if (checkIfProjectNeedsFlush()) {
211218
logging.log("[MakeCode] Importing project");
212-
await editorReadyPromiseRef.current.promise;
219+
await editorReadyPromise.promise;
213220
const project = getCurrentProject();
214221
expectChangedHeader();
215222
try {
216223
await driverRef.current.importProject({ project });
217224
logging.log("[MakeCode] Project import succeeded");
218225
projectFlushedToEditor();
226+
langChangeFlushedToEditor();
219227
} catch (e) {
220228
logging.log("[MakeCode] Project import failed");
221229
throw e;
222230
}
231+
} else {
232+
logging.log("[MakeCode] Waiting for editor after language change");
233+
await editorReadyPromise.promise;
234+
langChangeFlushedToEditor();
223235
}
224236
})();
225237
}
@@ -243,12 +255,14 @@ export const ProjectProvider = ({
243255
},
244256
[
245257
checkIfProjectNeedsFlush,
258+
checkIfLangChanged,
246259
driverRef,
247260
logging,
248-
editorReadyPromiseRef,
261+
editorReadyPromise.promise,
249262
getCurrentProject,
250263
expectChangedHeader,
251264
projectFlushedToEditor,
265+
langChangeFlushedToEditor,
252266
checkIfEditorStartUpTimedOut,
253267
editorTimedOut,
254268
]
@@ -311,7 +325,7 @@ export const ProjectProvider = ({
311325
// Check if is a MakeCode hex, otherwise show error dialog.
312326
if (hex.includes(makeCodeMagicMark)) {
313327
const hasTimedOut = await checkIfEditorStartUpTimedOut(
314-
editorReadyPromiseRef.current.promise
328+
editorReadyPromise.promise
315329
);
316330
if (hasTimedOut) {
317331
openEditorTimedOutDialog();
@@ -332,7 +346,7 @@ export const ProjectProvider = ({
332346
[
333347
checkIfEditorStartUpTimedOut,
334348
driverRef,
335-
editorReadyPromiseRef,
349+
editorReadyPromise,
336350
loadDataset,
337351
logging,
338352
navigate,
@@ -412,9 +426,14 @@ export const ProjectProvider = ({
412426
const editorChange = useStore((s) => s.editorChange);
413427
const onWorkspaceSave = useCallback(
414428
(event: EditorWorkspaceSaveRequest) => {
415-
editorChange(event.project);
429+
if (!checkIfLangChanged()) {
430+
// We don't want to handle these events until MakeCode has been
431+
// reinitialised after a language change.
432+
// We should reinitialise with the latest project.
433+
editorChange(event.project);
434+
}
416435
},
417-
[editorChange]
436+
[checkIfLangChanged, editorChange]
418437
);
419438

420439
const onBack = useCallback(() => {

src/hooks/use-promise-ref.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ interface PromiseCallbacks<T> {
1010
reject: (reason?: unknown) => void;
1111
}
1212

13-
interface PromiseInfo<T> extends PromiseCallbacks<T> {
13+
export interface PromiseInfo<T> extends PromiseCallbacks<T> {
1414
promise: Promise<T>;
1515
}
1616

17-
const createPromise = <T>(): PromiseInfo<T> => {
17+
export const createPromise = <T>(): PromiseInfo<T> => {
1818
let callbacks: PromiseCallbacks<T> | undefined;
1919
const promise = new Promise<T>((resolve, reject) => {
2020
callbacks = { resolve, reject };

src/store.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { mlSettings } from "./mlConfig";
4444
import { BufferedData } from "./buffered-data";
4545
import { getDetectedAction } from "./utils/prediction";
4646
import { getTour as getTourSpec } from "./tours";
47+
import { createPromise, PromiseInfo } from "./hooks/use-promise-ref";
4748

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

@@ -163,7 +164,13 @@ export interface State {
163164
isEditorOpen: boolean;
164165
isEditorReady: boolean;
165166
editorStartUp: EditorStartUp;
167+
editorStartUpTimestamp: number;
168+
editorPromises: {
169+
editorReadyPromise: PromiseInfo<void>;
170+
editorContentLoadedPromise: PromiseInfo<void>;
171+
};
166172
isEditorTimedOutDialogOpen: boolean;
173+
langChanged: boolean;
167174

168175
download: DownloadState;
169176
downloadFlashingProgress: number;
@@ -222,6 +229,7 @@ export interface Actions {
222229
closeTrainModelDialogs: () => void;
223230
trainModel(): Promise<boolean>;
224231
setSettings(update: Partial<Settings>): void;
232+
setLanguage(languageId: string): void;
225233

226234
/**
227235
* Resets the project.
@@ -238,6 +246,8 @@ export interface Actions {
238246
*/
239247
getCurrentProject(): Project;
240248
checkIfProjectNeedsFlush(): boolean;
249+
checkIfLangChanged(): boolean;
250+
langChangeFlushedToEditor(): void;
241251
editorChange(project: Project): void;
242252
editorReady(): void;
243253
editorTimedOut(): void;
@@ -307,7 +317,13 @@ const createMlStore = (logging: Logging) => {
307317
isEditorOpen: false,
308318
isEditorReady: false,
309319
editorStartUp: "in-progress",
320+
editorStartUpTimestamp: Date.now(),
321+
editorPromises: {
322+
editorReadyPromise: createPromise<void>(),
323+
editorContentLoadedPromise: createPromise<void>(),
324+
},
310325
isEditorTimedOutDialogOpen: false,
326+
langChanged: false,
311327
appEditNeedsFlushToEditor: true,
312328
changedHeaderExpected: false,
313329
// This dialog flow spans two pages
@@ -342,6 +358,33 @@ const createMlStore = (logging: Logging) => {
342358
);
343359
},
344360

361+
setLanguage(languageId: string) {
362+
const currLanguageId = get().settings.languageId;
363+
if (languageId === currLanguageId) {
364+
// No need to update language if language is the same.
365+
// MakeCode does not reload.
366+
return;
367+
}
368+
set(
369+
({ settings }) => ({
370+
settings: {
371+
...settings,
372+
languageId,
373+
},
374+
editorPromises: {
375+
editorReadyPromise: createPromise<void>(),
376+
editorContentLoadedPromise: createPromise<void>(),
377+
},
378+
isEditorReady: false,
379+
editorStartUp: "in-progress",
380+
editorStartUpTimestamp: Date.now(),
381+
langChanged: true,
382+
}),
383+
false,
384+
"setLanguage"
385+
);
386+
},
387+
345388
newSession(projectName?: string) {
346389
const untitledProject = createUntitledProject();
347390
set(
@@ -768,6 +811,10 @@ const createMlStore = (logging: Logging) => {
768811
return get().appEditNeedsFlushToEditor;
769812
},
770813

814+
checkIfLangChanged() {
815+
return get().langChanged;
816+
},
817+
771818
getCurrentProject() {
772819
return get().project;
773820
},
@@ -892,6 +939,15 @@ const createMlStore = (logging: Logging) => {
892939
"setChangedHeaderExpected"
893940
);
894941
},
942+
langChangeFlushedToEditor() {
943+
set(
944+
{
945+
langChanged: false,
946+
},
947+
false,
948+
"langChangeFlushedToEditor"
949+
);
950+
},
895951
projectFlushedToEditor() {
896952
set(
897953
{

0 commit comments

Comments
 (0)