fix: centralize WebContainer boot state in Zustand store#95
Conversation
Multiple consumers (terminal, preview) each managed their own boot lifecycle, leading to redundant boots and state drift when navigating between views. Consolidating into a Zustand store with a module-level promise guard ensures a single boot attempt is shared across all concurrent callers.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
WalkthroughThis PR refactors WebContainer lifecycle management by introducing a Zustand store that centralizes boot state, instance reference, and async initialization logic with concurrent request deduplication. The ChangesWebContainer State Centralization
Sequence DiagramsequenceDiagram
participant Component
participant useWebContainer
participant useWebContainerStore
participant WebContainer
Component->>useWebContainer: mount
useWebContainer->>useWebContainerStore: bootWebContainer()
alt Already booted
useWebContainerStore->>useWebContainerStore: return early
else Booting or first call
useWebContainerStore->>WebContainer: WebContainer.boot()
WebContainer-->>useWebContainerStore: instance ready
useWebContainerStore->>useWebContainerStore: set status=booted, instance
end
useWebContainerStore-->>useWebContainer: return {instance, isLoading, error}
useWebContainer-->>Component: return hook result
Component->>useWebContainer: writeFileSync(path, content)
useWebContainer->>WebContainer: instance.fs.mkdir(parent, {recursive})
useWebContainer->>WebContainer: instance.fs.writeFile(path, content)
Component->>useWebContainer: cleanup / destroy
useWebContainer->>useWebContainerStore: teardown()
useWebContainerStore->>WebContainer: instance.teardown()
useWebContainerStore->>useWebContainerStore: reset to idle
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@modules/webcontainers/store/webcontainer-store.ts`:
- Around line 43-69: The boot completion can overwrite store after teardown
because the shared bootPromise may resolve later; add a generation token (e.g.,
bootGeneration number stored in the module/closure or in the store) that you
increment when starting a new boot and again in teardown, capture its value in
the WebContainer.boot() then/catch handlers and only call set(...) or mutate
bootPromise when the captured generation still matches the current generation;
update teardown to increment/reset the generation and clear bootPromise so any
in-flight resolution is ignored, and ensure boot start checks the current
generation to avoid launching parallel boots.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9cf67272-f248-4321-9473-a2bd3c106357
📒 Files selected for processing (2)
modules/webcontainers/hooks/useWebContainer.tsmodules/webcontainers/store/webcontainer-store.ts
| bootPromise = WebContainer.boot() | ||
| .then((instance) => { | ||
| set({ instance, status: "booted", isLoading: false, isBooted: true }); | ||
| }) | ||
| .catch((err) => { | ||
| bootPromise = null; | ||
| set({ | ||
| status: "error", | ||
| isLoading: false, | ||
| error: err instanceof Error ? err.message : "Failed to initialize WebContainer", | ||
| }); | ||
| }); | ||
|
|
||
| await bootPromise; | ||
| }, | ||
|
|
||
| teardown: () => { | ||
| const { instance } = get(); | ||
| if (instance) { | ||
| try { | ||
| instance.teardown(); | ||
| } catch { | ||
| // ignore teardown errors | ||
| } | ||
| } | ||
| bootPromise = null; | ||
| set({ instance: null, status: "idle", isLoading: false, isBooted: false, error: null }); |
There was a problem hiding this comment.
Prevent an in-flight boot from reviving the store after teardown.
If teardown() runs before WebContainer.boot() settles, Line 68 clears the shared promise and Line 69 resets the store, but the original boot can still resolve and Line 45 will flip the store back to booted. That also opens a window for a second bootWebContainer() call to start a parallel boot while the first one is still finishing. Please guard the boot completion with a generation/cancellation token and only commit the result if that boot attempt is still current.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@modules/webcontainers/store/webcontainer-store.ts` around lines 43 - 69, The
boot completion can overwrite store after teardown because the shared
bootPromise may resolve later; add a generation token (e.g., bootGeneration
number stored in the module/closure or in the store) that you increment when
starting a new boot and again in teardown, capture its value in the
WebContainer.boot() then/catch handlers and only call set(...) or mutate
bootPromise when the captured generation still matches the current generation;
update teardown to increment/reset the generation and clear bootPromise so any
in-flight resolution is ignored, and ensure boot start checks the current
generation to avoid launching parallel boots.
piyushdotcomm
left a comment
There was a problem hiding this comment.
you need to check coderabbit and other reviewers comment before directly tagging maintainers
|
i will fix the code |
Summary
Moves WebContainer boot state out of module-level singletons in
useWebContainer.tsand into a Zustand store. Previously,webContainerInstanceandbootPromiselived as file-scope variables in the hook module, meaning state was invisible to other consumers and the boot lifecycle was not globally reactive. The store now owns the full lifecycle, and the hook becomes a thin bridge.Changes
modules/webcontainers/store/webcontainer-store.ts(new): Zustand store withdevtoolsmiddleware holdinginstance,status(idle|booting|booted|error),isLoading,isBooted,error,bootWebContainer, andteardown. A module-levelbootPromisein the store file acts as the idempotency guard — concurrentbootWebContainer()calls share the same in-flight promise instead of triggering parallel boots.modules/webcontainers/hooks/useWebContainer.ts: Removes module-levelwebContainerInstanceandbootPromisesingletons and all localuseStatefor boot state.useEffectnow callsbootWebContainer()from the store.destorydelegates toteardown(). Public return shape (serverUrl,isLoading,error,instance,writeFileSync,destory) is unchanged so callers inapp/playground/[id]/page.tsxandwebcontainer-preview.tsxrequire no edits.Testing
npm testpasses. The WebContainer API requires a browser context so the boot path cannot be exercised in Jest; the existing smoke tests (repo structure checks) continue to pass. Manual verification: boot state is now visible in Redux DevTools under thewebcontainer-storeslice, and navigating away and back to the playground no longer triggers a second boot attempt.Notes
serverUrlremains hardcoded tonullin the hook return — this matched the pre-refactor behaviour (it was never set) and is out of scope for this issue.Closes #47
Summary by CodeRabbit