From 7d6248173d29d7592676b5878b33da19fda41277 Mon Sep 17 00:00:00 2001 From: kpal Date: Wed, 15 Apr 2026 13:05:17 +0100 Subject: [PATCH 1/4] fix: guard code-editor document load on null type (PLAY-CANVAS-G3DM) When the collab server's checkpoint cleanup nulls a document's _data in MongoDB, ShareDB delivers a snapshot with type=null. The load handler in documents-load.ts was missing the guard already present in sharedb.ts and userdata-realtime.ts, causing downstream consumers to crash on null doc.data. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/code-editor/documents/documents-load.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/code-editor/documents/documents-load.ts b/src/code-editor/documents/documents-load.ts index 950279578..8ba623010 100644 --- a/src/code-editor/documents/documents-load.ts +++ b/src/code-editor/documents/documents-load.ts @@ -75,6 +75,10 @@ editor.once('load', () => { // ready to sync doc.on('load', () => { + if (!doc.type) { + return; + } + // check if closed by the user if (!documentsIndex[id]) { return; From d8fbc720f7011275a1bef121021b926f132cdace Mon Sep 17 00:00:00 2001 From: kpal Date: Wed, 15 Apr 2026 13:05:48 +0100 Subject: [PATCH 2/4] fix: filter unresolved assets in ESM dependency loading (PLAY-CANVAS-G3EA) assets:getByVirtualPath can return undefined for import paths that don't resolve to a loaded asset. Filter these out before passing to loadDocument() which expects a valid Observer with .get(). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/code-editor/documents/documents-load.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/code-editor/documents/documents-load.ts b/src/code-editor/documents/documents-load.ts index 8ba623010..32f961992 100644 --- a/src/code-editor/documents/documents-load.ts +++ b/src/code-editor/documents/documents-load.ts @@ -107,7 +107,7 @@ editor.once('load', () => { if (importingAssetPath) { // Return the immediate dependencies of the asset const deps = editor.call('utils:deps-from-string', content, importingAssetPath); - const depsAsAsset = Array.from(deps).map(path => editor.call('assets:getByVirtualPath', path)); + const depsAsAsset = Array.from(deps).map(path => editor.call('assets:getByVirtualPath', path)).filter(Boolean); // And load them, ensuring that Monaco can resolve dependencies depsAsAsset.forEach(asset => loadDocument(asset, false)); From ea10cd942e0d63153e2441390d466eaaa43f69f5 Mon Sep 17 00:00:00 2001 From: kpal Date: Wed, 15 Apr 2026 13:06:14 +0100 Subject: [PATCH 3/4] fix: eliminate TOCTOU race in code-editor path resolution (PLAY-CANVAS-G3CG) Replace the two-pass check-then-map pattern with a single-pass loop that bails on the first missing asset. The previous pattern had a window between the some() check and the subsequent map() where an asset could be removed by a concurrent event. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/code-editor/monaco/document.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/code-editor/monaco/document.ts b/src/code-editor/monaco/document.ts index 9dad82a6d..b1b57145e 100644 --- a/src/code-editor/monaco/document.ts +++ b/src/code-editor/monaco/document.ts @@ -124,12 +124,12 @@ editor.once('load', () => { } const assetPath = asset.get('path'); - const pathAssets = assetPath.map(id => editor.call('assets:get', id)); - if (pathAssets.some(a => !a)) { - // Parent folder(s) have been deleted, skip loading - return; + const pathSegments = []; + for (const id of assetPath) { + const a = editor.call('assets:get', id); + if (!a) return; + pathSegments.push(a.get('name')); } - const pathSegments = pathAssets.map(a => a.get('name')); const path = [...pathSegments, asset.get('file').filename].join('/'); const uri = monaco.Uri.parse(`${path}`); From 637af91e846027243bfd24174d41c4b9b3b0e1c8 Mon Sep 17 00:00:00 2001 From: kpal Date: Wed, 15 Apr 2026 13:06:37 +0100 Subject: [PATCH 4/4] fix: check destroyed before selecting file tree item (PLAY-CANVAS-G3E0) When an asset is deleted, the TreeViewItem is destroyed but remains in the idToItem map briefly. A racing files:select call (from tab close) could set selected on the destroyed item, accessing classList on a null DOM element. Add the same !item.destroyed guard already used in the documents:close handler. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/code-editor/files-panel/files-panel.ts | 2 +- src/code-editor/monaco/document.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/code-editor/files-panel/files-panel.ts b/src/code-editor/files-panel/files-panel.ts index 793021b8c..a94aadaef 100644 --- a/src/code-editor/files-panel/files-panel.ts +++ b/src/code-editor/files-panel/files-panel.ts @@ -376,7 +376,7 @@ editor.once('load', () => { // Select file by id (which can be passed as a string or number) editor.method('files:select', (id: number|string) => { const item = idToItem.get(String(id)); - if (item) { + if (item && !item.destroyed) { tree.deselect(); item.selected = true; } diff --git a/src/code-editor/monaco/document.ts b/src/code-editor/monaco/document.ts index b1b57145e..1a66a7c9a 100644 --- a/src/code-editor/monaco/document.ts +++ b/src/code-editor/monaco/document.ts @@ -127,7 +127,9 @@ editor.once('load', () => { const pathSegments = []; for (const id of assetPath) { const a = editor.call('assets:get', id); - if (!a) return; + if (!a) { + return; + } pathSegments.push(a.get('name')); } const path = [...pathSegments, asset.get('file').filename].join('/');