fix(webcontainer): prevent double initialization and race conditions when booting WebContainer and loading editor workspace/file tree
This commit is contained in:
BIN
.playwright-mcp/workspace-working.png
Normal file
BIN
.playwright-mcp/workspace-working.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user