From f98aa72b887968c613898fd5d8b0127b82752fca Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Tue, 9 Sep 2025 08:47:16 -0600 Subject: [PATCH] remove docs --- hydration-next.md | 319 ---------------------------------------------- 1 file changed, 319 deletions(-) delete mode 100644 hydration-next.md diff --git a/hydration-next.md b/hydration-next.md deleted file mode 100644 index fd2607e23d..0000000000 --- a/hydration-next.md +++ /dev/null @@ -1,319 +0,0 @@ -# Svelte Async SSR Hydration: Resolved Content Without Pending Snippets (Simplified Design) - -This document outlines the implementation plan for hydrating boundaries when async SSR has already resolved the content on the server and no `pending` snippet exists. This uses a simplified approach that reuses existing hydration infrastructure and allows client-side async re-execution. - -## Problem Statement - -With async SSR, we now have boundaries that can render in two states: - -**Boundary with pending snippet:** - -```svelte - -

{await getData()}

- {#snippet pending()} -

Loading...

- {/snippet} -
-``` - -- **With pending snippet**: Server always renders `

Loading...

` -- **Without pending snippet**: Server waits for `getData()`, renders `

Resolved Data

` - -**The hydration challenge**: How does the client know which content the server rendered? - -## Simplified Design Approach - -### Core Principles - -1. **No promise value serialization** - Client async operations can re-execute if needed -2. **Reuse existing markers** - Leverage the `else` block marker pattern (``) -3. **Binary state model** - Either "pending rendered" or "resolved rendered" -4. **Allow async re-execution** - If promises don't resolve immediately on client, let them run - -### Existing Infrastructure to Reuse - -Svelte already has a pattern for this with `{#if}` blocks: - -```html - - -
if content
- - - - -
else content
- -``` - -We can apply the same pattern to boundaries, where pending is the "else" case: - -```html - - -

Resolved content

- - - - -

Loading...

- -``` - -## Implementation Plan - -### Phase 1: Server-Side Changes - -#### 1.1 Server Boundary Rendering Logic - -Modify `SvelteBoundary` server visitor to use existing marker pattern: - -```javascript -export function SvelteBoundary(node, context) { - const pending_snippet = node.metadata.pending; - - if (pending_snippet) { - // Has pending snippet - render pending content with else marker - context.state.template.push(b.literal(BLOCK_OPEN_ELSE)); // - - if (pending_snippet.type === 'Attribute') { - const value = build_attribute_value(pending_snippet.value, context, false, true); - context.state.template.push(b.call(value, b.id('$$payload'))); - } else if (pending_snippet.type === 'SnippetBlock') { - context.state.template.push(context.visit(pending_snippet.body)); - } - } else { - // No pending snippet - render main content (may be async or sync) - context.state.template.push(b.literal(BLOCK_OPEN)); // - context.state.template.push(context.visit(node.fragment)); - } - - context.state.template.push(b.literal(BLOCK_CLOSE)); // -} -``` - -**Key insight**: The server only cares about whether there's a pending snippet. If there is, render it with the else marker. If not, render the main content with the normal marker - the server will naturally wait for any async operations to resolve during rendering. - -### Phase 2: Client-Side Hydration Changes - -#### 2.1 Hydration State Detection - -Extend boundary constructor to detect server rendering state: - -```javascript -constructor(node, props, children) { - this.#anchor = node; - this.#props = props; - this.#children = children; - this.#hydrate_open = hydrate_node; - - // NEW: Detect what the server rendered - this.#server_rendered_pending = this.#detect_server_state(); - - this.parent = active_effect.b; - this.pending = !!this.#props.pending; - - // Main effect logic... -} - -#detect_server_state() { - if (!hydrating || !this.#hydrate_open) return false; - - const comment = this.#hydrate_open; - if (comment.nodeType === COMMENT_NODE) { - // Check if server rendered pending content (else marker) - return comment.data === HYDRATION_START_ELSE; // '[!' - } - - return false; -} -``` - -#### 2.2 Hydration Flow Logic - -Modify the main boundary effect to handle both cases: - -```javascript -this.#effect = block(() => { - active_effect.b = this; - - if (hydrating) { - hydrate_next(); - - if (this.#server_rendered_pending) { - // Server rendered pending content - existing logic - this.#hydrate_pending_content(); - } else { - // Server rendered resolved content - new logic - this.#hydrate_resolved_content(); - } - } else { - // Client-side rendering - this.#render_client_content(); - } -}, flags); -``` - -#### 2.3 Resolved Content Hydration - -Implement the resolved content hydration path: - -```javascript -#hydrate_resolved_content() { - // Server already rendered resolved content, so hydrate it directly - this.#main_effect = this.#run(() => { - return branch(() => this.#children(this.#anchor)); - }); - - // Start in non-pending state since server rendered resolved content - this.pending = false; - - // Note: Even if client-side async operations are still running, - // we never transition back to pending state. Users can use - // $effect.pending() to track ongoing async work if needed. -} - -#hydrate_pending_content() { - // Existing logic - server rendered pending content - this.#pending_effect = branch(() => this.#props.pending(this.#anchor)); - - Batch.enqueue(() => { - this.#main_effect = this.#run(() => { - Batch.ensure(); - return branch(() => this.#children(this.#anchor)); - }); - - if (this.#pending_count > 0) { - this.#show_pending_snippet(); - } else { - pause_effect(this.#pending_effect, () => { - this.#pending_effect = null; - }); - this.pending = false; - } - }); -} -``` - -### Phase 4: Compiler Integration - -#### 4.1 Analysis Phase - -The analysis already tracks `is_async` on boundaries. We just need to ensure it's set correctly: - -```javascript -// In AwaitExpression visitor - this already exists -if (context.state.async_hoist_boundary && context.state.expression) { - context.state.async_hoist_boundary.metadata.is_async = true; - // ... existing logic -} -``` - -#### 4.2 Server Code Generation - -The server visitor change is minimal - just use the else marker for pending content: - -```javascript -// In server SvelteBoundary visitor -export function SvelteBoundary(node, context) { - const pending_snippet = node.metadata.pending; - - if (pending_snippet) { - // Use else marker for pending content - context.state.template.push(b.literal(BLOCK_OPEN_ELSE)); - // ... render pending content - } else { - // Use normal marker for main content (async or sync) - context.state.template.push(b.literal(BLOCK_OPEN)); - // ... render main content - } - - context.state.template.push(b.literal(BLOCK_CLOSE)); -} -``` - -## Edge Cases and Considerations - -### Edge Case 1: Multiple Async Operations with Different Timing - -```svelte - -

{await fast()}

-

{await slow()}

-
-``` - -If `fast()` resolves on server but `slow()` doesn't, the server still waits for both before rendering resolved content. On client, both may re-execute with different timing. - -**Handling**: The boundary's `#pending_count` system already handles multiple async operations correctly. - -### Edge Case 2: Conditional Async Content - -```svelte - - {#if condition} -

{await getData()}

- {:else} -

No data needed

- {/if} -
-``` - -**Handling**: The `is_async` flag is set if any path contains async operations. Server-side rendering will resolve the condition and any async operations in the taken path. - -### Edge Case 3: Nested Boundaries - -```svelte - -
{await outer()}
-
{await inner()}
-
-``` - -**Handling**: Each boundary is independent. Inner boundary can be resolved while outer is pending, or vice versa. - -## Implementation Context - -### Key Design Philosophy - -- This is the **simplified** approach - we deliberately chose NOT to serialize promise values -- We're reusing existing `if/else` block hydration markers rather than creating new ones -- The server doesn't need to know about `is_async` - it just renders based on pending snippet presence - -### Critical Semantic Understanding - -- `` = pending content (the "else" case when async hasn't resolved) -- `` = resolved content (normal case) -- This inversion makes semantic sense: pending is the fallback/else state - -### Boundary State Rules - -- Boundaries **never** transition back to pending once content is rendered -- Use `$effect.pending()` for tracking ongoing async work, not boundary state -- The `pending` property stays `false` once content is shown - -### Server Logic Simplicity - -- Server only checks: "Does this boundary have a pending snippet?" -- If yes → render pending with `BLOCK_OPEN_ELSE` -- If no → render main content with `BLOCK_OPEN` (async SSR waits naturally) - -### Client Hydration Flow - -- Detect marker type to know what server rendered -- If `HYDRATION_START_ELSE` → server rendered pending, use existing logic -- If normal marker → server rendered resolved, hydrate directly (no complex async handling) - -### What We're NOT Doing - -- No promise serialization/deserialization -- No complex client-server async coordination -- No `error` snippet handling (server never renders errors) -- No distinction between async/sync resolved content - -### Implementation Priority - -The core change is surprisingly small - just swapping which marker the server uses for pending content. The rest leverages existing Svelte hydration infrastructure. - -This approach prioritizes simplicity and reuse over complex optimization, which aligns with Svelte's philosophy of doing more with less code.