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
## 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)
Replace bare editor with Monaco-based editor and add runtime + workspace/filetree integration

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
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.'
}

View File

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

View File

@@ -74,6 +74,7 @@ export class DeesEditorWorkspace extends DeesElement {
accessor isInitializing: boolean = true;
private editorElement: DeesEditorMonaco | null = null;
private initializationStarted: boolean = false;
public static styles = [
themeDefaultStyles,
@@ -442,6 +443,10 @@ export class DeesEditorWorkspace extends DeesElement {
private async initializeWorkspace() {
if (!this.executionEnvironment) return;
// Prevent double initialization
if (this.initializationStarted) return;
this.initializationStarted = true;
this.isInitializing = true;
try {
@@ -450,6 +455,8 @@ export class DeesEditorWorkspace extends DeesElement {
}
} catch (error) {
console.error('Failed to initialize workspace:', error);
// Reset flag to allow retry
this.initializationStarted = false;
} finally {
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.
*/
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;
public readonly type = 'webcontainer' as const;
@@ -15,11 +18,29 @@ export class WebContainerEnvironment implements IExecutionEnvironment {
return this._ready;
}
private get container(): webcontainer.WebContainer | null {
return WebContainerEnvironment.sharedContainer;
}
// ============ Lifecycle ============
public async init(): Promise<void> {
if (this._ready && this.container) {
return; // Already initialized
// Already initialized (this instance)
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)
@@ -32,14 +53,24 @@ export class WebContainerEnvironment implements IExecutionEnvironment {
);
}
this.container = await webcontainer.WebContainer.boot();
this._ready = true;
// Start boot process
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> {
if (this.container) {
this.container.teardown();
this.container = null;
if (WebContainerEnvironment.sharedContainer) {
WebContainerEnvironment.sharedContainer.teardown();
WebContainerEnvironment.sharedContainer = null;
WebContainerEnvironment.bootPromise = null;
this._ready = false;
}
}