Skip to content

fix: fork animations to run concurrently instead of serializing reconcile loop#29

Merged
jonlaing merged 2 commits into
mainfrom
fix/animation-serialization-v2
Jul 2, 2026
Merged

fix: fork animations to run concurrently instead of serializing reconcile loop#29
jonlaing merged 2 commits into
mainfrom
fix/animation-serialization-v2

Conversation

@jonlaing

@jonlaing jonlaing commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

Both ClientControlCtx.addSlot and HydrationControlCtx.addSlot were awaiting runEnterAnimation inline with yield*. Same for runExitAnimation in removeSlot. Because reconcile's sync loop yields sequentially, this caused every animation to serialize:

  • A list added as [a, b, c] at once took 3 × animationDuration wall time. Users saw items appear one after another when they'd configured all three to enter at the same time.
  • stagger was effectively broken — the configured stagger delay applied on top of each preceding animation's full duration, since addSlot didn't return until the animation completed.
  • Batch removals had the same shape: exit animations cascaded serially instead of playing together.

Fix

  • Enter animations are forked into the slot's own scope. addSlot returns as soon as the slot is registered and DOM inserted. If the slot is removed mid-enter, closing the slot scope interrupts the in-flight animation.
  • Exit animations are forked into the parent scope so removeSlot returns immediately. The forked fiber closes the slot's scope first (interrupting any in-flight enter), then plays the exit animation, then removes the DOM node. Running on the parent scope means the exit continues even if a fresh slot re-uses the same key immediately.

Applied to:

  • packages/dom/src/Control/ClientControlCtx.ts (both addSlot and removeSlot)
  • packages/dom/src/Control/HydrationControlCtx.ts — both factories (createClientLikeControlCtx and createHydrationControlCtx), both addSlot and removeSlot

Interface signature

removeSlot uses Effect.scope to get the parent scope for Effect.forkIn, which pulls Scope.Scope into R. The interface declares Effect.Effect<void> (concrete no-deps signature), so a cast is applied at the end — same pattern the existing subscribe method uses in these files. Callers of removeSlot (i.e., reconcile) always have Scope.Scope in their runtime context, so this is safe.

Depends on

This PR is stacked on top of #23. It targets fix/hydration-animation-config-lazy-read as base, so once #23 merges, this can rebase to main.

Test plan

  • pnpm exec tsc --noEmit — clean
  • All 50 existing Control / hydrate / Animation tests pass
  • Manually verify list animations play concurrently on a real client-rendered list

Closes #25

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying effex with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3f3a839
Status: ✅  Deploy successful!
Preview URL: https://27ba56b6.effex.pages.dev
Branch Preview URL: https://fix-animation-serialization.effex.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying effex-api with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3f3a839
Status: ✅  Deploy successful!
Preview URL: https://310dfdd4.effex-api.pages.dev
Branch Preview URL: https://fix-animation-serialization.effex-api.pages.dev

View logs

…cile loop

Both ClientControlCtx and HydrationControlCtx were awaiting runEnterAnimation and runExitAnimation inline, which caused the sync loop in reconcile to serialize: item N's animation blocked item N+1's addSlot from starting. Made stagger configs functionally broken and exit animations cascade one-at-a-time instead of in parallel.

Enter animations now fork into the slot's own scope. Exit animations fork into the parent scope so removeSlot returns immediately while the exit still plays; the slot's scope is closed inside the fork before the exit runs so any in-flight enter is interrupted first.

All 50 existing Control/hydrate/Animation tests pass.

Closes #25

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jonlaing jonlaing force-pushed the fix/animation-serialization-v2 branch from e090853 to 0dd6440 Compare July 1, 2026 20:32
@jonlaing jonlaing changed the base branch from fix/hydration-animation-config-lazy-read to main July 1, 2026 20:32
Pulls the config-read + fork-enter and close-scope + config-read + exit + remove-DOM blocks (repeated three times each across ClientControlCtx and both HydrationControlCtx factories) into forkSlotEnter and forkSlotRemoval in a new slotAnimation.ts module. Net -116 lines across the two ControlCtx files. All 704 tests still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jonlaing jonlaing merged commit 63cc6df into main Jul 2, 2026
4 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.

Inline animations serialize the reconcile loop

1 participant