Skip to content

Conversation

@justinvdm
Copy link
Collaborator

@justinvdm justinvdm commented Oct 21, 2025

This addresses two distinct but related sources of instability in the Vite dev server, both of which are triggered by Vite's dependency re-optimization process.

Problem 1: Module-Level State is Discarded on Re-optimization

The framework's runtime relies on long-lived, module-level state for critical features like request context tracking via AsyncLocalStorage. However, Vite's dependency re-optimization process, designed for browser-based hot reloading, is fundamentally incompatible with this. When a new dependency is discovered, Vite discards and re-instantiates the entire module graph. This process wipes out our module-level state, leading to unpredictable runtime errors and application crashes.

Solution: A Virtual State Module

A virtual state module, rwsdk/__state, is introduced to act as a centralized, persistent store for framework-level state.

  • A new Vite plugin (statePlugin) marks this virtual module as external to Vite's dependency optimizer for the worker environment. This insulates it from the re-optimization and reload process.
  • The plugin resolves rwsdk/__state to a physical module (the built module in dist/ for sdk/src/runtime/state.ts) that contains the state container and management APIs - i.e. we bypass the dep optimizer for this specific module, so we have a stable path outside of dep optimization bundles
  • Framework code is refactored to use this module (e.g., defineRwState(...)), making the state resilient to reloads.

This solves the state-loss problem and centralizes state management within the framework.

Problem 2: Race Conditions Cause "Stale Pre-bundle" Errors

In a standard Vite setup, handling stale dependencies is a routine process. When a re-optimization occurs, the browser might request a module with an old version hash. The Vite server correctly throws a "stale pre-bundle" error, which is caught by Vite's client-side script in the browser. This script then automatically retries the request or performs a full page reload, seamlessly recovering from the transient error.

However, our architecture introduces several layers of complexity that make this standard recovery model insufficient. The "client" making these requests is not a browser, but the Cloudflare CustomModuleRunner executing server-side within Miniflare. Furthermore, our SSR Bridge architecture means this runner interacts with a virtual module subgraph. When it needs to render an SSR component, it makes a request for a virtual module which, via our plugin, triggers a server-to-server fetchModule call from the worker environment to the isolated ssr environment.

This unique, cross-environment request pattern for virtual modules is at the heart of the instability. When a re-optimization happens in the ssr environment, the standard recovery mechanisms are not equipped to handle the resulting state desynchronization. The failure manifests as a perfect storm of three deeper, interconnected issues:

  1. Stale Resolution: After an SSR re-optimization, Vite's internal resolver would continue to use a stale "ghost node" from its module graph to resolve our virtual ssr_bridge module, leading to a request for a dependency with an old, invalid version hash.
  2. Desynchronized Environments: The full-reload HMR event triggered by the SSR optimizer was not being propagated to the worker environment. This meant the worker's own caches (especially the CustomModuleRunner's execution cache) were never cleared and continued to use stale modules, creating an infinite error loop.
  3. Premature Re-import: Even with synchronized invalidation, the worker's module runner re-imports its entry points immediately after clearing its cache. This happens too quickly, hitting the Vite server while its own internal state is still being updated, re-triggering the stale dependency error.

Solution: A Multi-Layered Approach to Synchronization and Stability

A combination of fixes was implemented to address this race condition:

  1. Manual Hash Resolution: The ssrBridgePlugin was modified to no longer rely on Vite's internal, faulty resolution for virtual modules. It now manually resolves the correct, up-to-date version hash for any optimized dependency from the SSR optimizer's metadata before fetching it. This bypasses the "ghost node" problem.
  2. HMR Propagation: The ssrBridgePlugin now intercepts the full-reload HMR event from the SSR environment and propagates it to the worker environment. This ensures the worker's module runner and module graph are correctly invalidated when the SSR environment changes.
  3. Debounced Stability Plugin (staleDepRetryPlugin): A new error-handling middleware was introduced. When it catches the inevitable "stale pre-bundle" error from the runner's premature re-import, it does not immediately retry. Instead, it waits for the server to become "stable" by monitoring the transform hook. Once a quiet period with no module transformation activity is detected, it signals the client to perform a full reload and gracefully redirects the failed request.

@justinvdm justinvdm changed the title Optimize dep resilience fix(dev): Make dev server resilient to dependency re-optimization Oct 21, 2025
@justinvdm justinvdm changed the title fix(dev): Make dev server resilient to dependency re-optimization fix: Make dev server resilient to dependency re-optimization Oct 21, 2025
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Oct 21, 2025

Deploying redwood-sdk-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 223636f
Status: ✅  Deploy successful!
Preview URL: https://fd7d989a.redwood-sdk-docs.pages.dev
Branch Preview URL: https://optimize-dep-resilience.redwood-sdk-docs.pages.dev

View logs

@justinvdm justinvdm merged commit 43aa564 into main Oct 22, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant