fix(webcontainer): prevent double initialization and race conditions when booting WebContainer and loading editor workspace/file tree

This commit is contained in:
2025-12-30 15:47:15 +00:00
parent 26759a5b90
commit c27b532aaa
6 changed files with 63 additions and 9 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,5 +1,13 @@
# Changelog # Changelog
## 2025-12-30 - 3.13.1 - fix(webcontainer)
prevent double initialization and race conditions when booting WebContainer and loading editor workspace/file tree
- Add loadTreeStarted flag in dees-editor-filetree to avoid double-loading the file tree and reset it on refresh or on error to allow retries.
- Add initializationStarted flag in dees-editor-workspace to prevent duplicate workspace initialization and reset it on initialization failure to allow retry.
- Make WebContainerEnvironment use a shared singleton container and a bootPromise so only one WebContainer boot runs per page; instances wait for an ongoing boot instead of booting again.
- Reset bootPromise/sharedContainer on boot failure and clear them on teardown so subsequent attempts can retry cleanly.
## 2025-12-30 - 3.13.0 - feat(editor/runtime) ## 2025-12-30 - 3.13.0 - feat(editor/runtime)
Replace bare editor with Monaco-based editor and add runtime + workspace/filetree integration Replace bare editor with Monaco-based editor and add runtime + workspace/filetree integration

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-catalog', name: '@design.estate/dees-catalog',
version: '3.13.0', version: '3.13.1',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
} }

View File

@@ -55,6 +55,7 @@ export class DeesEditorFiletree extends DeesElement {
accessor errorMessage: string = ''; accessor errorMessage: string = '';
private expandedPaths: Set<string> = new Set(); private expandedPaths: Set<string> = new Set();
private loadTreeStarted: boolean = false;
public static styles = [ public static styles = [
themeDefaultStyles, themeDefaultStyles,
@@ -484,6 +485,10 @@ export class DeesEditorFiletree extends DeesElement {
private async loadTree() { private async loadTree() {
if (!this.executionEnvironment) return; if (!this.executionEnvironment) return;
// Prevent double loading on initial render
if (this.loadTreeStarted) return;
this.loadTreeStarted = true;
this.isLoading = true; this.isLoading = true;
this.errorMessage = ''; this.errorMessage = '';
@@ -503,6 +508,8 @@ export class DeesEditorFiletree extends DeesElement {
} catch (error) { } catch (error) {
this.errorMessage = `Failed to load files: ${error}`; this.errorMessage = `Failed to load files: ${error}`;
console.error('Failed to load file tree:', error); console.error('Failed to load file tree:', error);
// Reset flag to allow retry
this.loadTreeStarted = false;
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }
@@ -521,6 +528,7 @@ export class DeesEditorFiletree extends DeesElement {
public async refresh() { public async refresh() {
this.expandedPaths.clear(); this.expandedPaths.clear();
this.loadTreeStarted = false; // Reset to allow loading
await this.loadTree(); await this.loadTree();
} }

View File

@@ -74,6 +74,7 @@ export class DeesEditorWorkspace extends DeesElement {
accessor isInitializing: boolean = true; accessor isInitializing: boolean = true;
private editorElement: DeesEditorMonaco | null = null; private editorElement: DeesEditorMonaco | null = null;
private initializationStarted: boolean = false;
public static styles = [ public static styles = [
themeDefaultStyles, themeDefaultStyles,
@@ -442,6 +443,10 @@ export class DeesEditorWorkspace extends DeesElement {
private async initializeWorkspace() { private async initializeWorkspace() {
if (!this.executionEnvironment) return; if (!this.executionEnvironment) return;
// Prevent double initialization
if (this.initializationStarted) return;
this.initializationStarted = true;
this.isInitializing = true; this.isInitializing = true;
try { try {
@@ -450,6 +455,8 @@ export class DeesEditorWorkspace extends DeesElement {
} }
} catch (error) { } catch (error) {
console.error('Failed to initialize workspace:', error); console.error('Failed to initialize workspace:', error);
// Reset flag to allow retry
this.initializationStarted = false;
} finally { } finally {
this.isInitializing = false; this.isInitializing = false;
} }

View File

@@ -6,7 +6,10 @@ import type { IExecutionEnvironment, IFileEntry, IProcessHandle } from '../inter
* Runs Node.js and shell commands in the browser using WebContainer API. * Runs Node.js and shell commands in the browser using WebContainer API.
*/ */
export class WebContainerEnvironment implements IExecutionEnvironment { export class WebContainerEnvironment implements IExecutionEnvironment {
private container: webcontainer.WebContainer | null = null; // Static shared state - WebContainer only allows ONE boot per page
private static sharedContainer: webcontainer.WebContainer | null = null;
private static bootPromise: Promise<webcontainer.WebContainer> | null = null;
private _ready: boolean = false; private _ready: boolean = false;
public readonly type = 'webcontainer' as const; public readonly type = 'webcontainer' as const;
@@ -15,11 +18,29 @@ export class WebContainerEnvironment implements IExecutionEnvironment {
return this._ready; return this._ready;
} }
private get container(): webcontainer.WebContainer | null {
return WebContainerEnvironment.sharedContainer;
}
// ============ Lifecycle ============ // ============ Lifecycle ============
public async init(): Promise<void> { public async init(): Promise<void> {
if (this._ready && this.container) { // Already initialized (this instance)
return; // Already initialized if (this._ready && WebContainerEnvironment.sharedContainer) {
return;
}
// If boot is in progress (by any instance), wait for it
if (WebContainerEnvironment.bootPromise) {
await WebContainerEnvironment.bootPromise;
this._ready = true;
return;
}
// If already booted by another instance, just mark ready
if (WebContainerEnvironment.sharedContainer) {
this._ready = true;
return;
} }
// Check if SharedArrayBuffer is available (required for WebContainer) // Check if SharedArrayBuffer is available (required for WebContainer)
@@ -32,14 +53,24 @@ export class WebContainerEnvironment implements IExecutionEnvironment {
); );
} }
this.container = await webcontainer.WebContainer.boot(); // Start boot process
this._ready = true; WebContainerEnvironment.bootPromise = webcontainer.WebContainer.boot();
try {
WebContainerEnvironment.sharedContainer = await WebContainerEnvironment.bootPromise;
this._ready = true;
} catch (error) {
// Reset promise on failure so retry is possible
WebContainerEnvironment.bootPromise = null;
throw error;
}
} }
public async destroy(): Promise<void> { public async destroy(): Promise<void> {
if (this.container) { if (WebContainerEnvironment.sharedContainer) {
this.container.teardown(); WebContainerEnvironment.sharedContainer.teardown();
this.container = null; WebContainerEnvironment.sharedContainer = null;
WebContainerEnvironment.bootPromise = null;
this._ready = false; this._ready = false;
} }
} }