From 2903ba9d8f839d7e4626c5449895961085a8f1de Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 3 Jul 2026 15:39:16 +0200 Subject: [PATCH] fork fix --- .../src/internal/client/dom/blocks/portal.js | 27 ++++---- .../samples/async-portal-fork/_config.js | 19 ++++++ .../samples/async-portal-fork/main.svelte | 20 ++++++ .../async-portal-key-changing-fork/_config.js | 63 +++++++++++++++++++ .../main.svelte | 35 +++++++++++ 5 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-portal-fork/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-portal-fork/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-portal-key-changing-fork/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-portal-key-changing-fork/main.svelte diff --git a/packages/svelte/src/internal/client/dom/blocks/portal.js b/packages/svelte/src/internal/client/dom/blocks/portal.js index c0d73687b2..c690866300 100644 --- a/packages/svelte/src/internal/client/dom/blocks/portal.js +++ b/packages/svelte/src/internal/client/dom/blocks/portal.js @@ -42,11 +42,17 @@ import { queue_micro_task } from '../task.js'; /** * All known outlets (and portals waiting for outlets) for a given key. - * `pending` is only used during hydration — it contains render functions of - * `{#portal ...}` blocks that were created before their outlet, so that the - * outlet can have them claim their server-rendered content upon initialisation. + * - `outlets` is the reactive source portals depend on + * - `all` is the canonical registry across all 'worlds' (in-flight batches and + * forks). It is the base for the read-modify-writes to `outlets`: outlets add + * and remove themselves as their effects are created and destroyed, so every + * write carries the full union and registrations of other batches are never + * lost. Batch-aware reads of `outlets` still scope each batch to its own view. + * - `pending` it contains render functions of `{#portal ...}` blocks that were + * created before their outlet, so that content is rendered upon initialisation. * @typedef {{ * outlets: Source, + * all: Outlet[], * pending: Set<(outlet: Outlet) => void> * }} OutletEntry */ @@ -68,7 +74,7 @@ function get_outlet_entry(key) { let entry = outlet_map.get(key); if (entry === undefined) { - entry = { outlets: source([]), pending: new Set() }; + entry = { outlets: source([]), all: [], pending: new Set() }; outlet_map.set(key, entry); } @@ -151,19 +157,15 @@ export function portal_outlet(node, get_id) { const entry = get_outlet_entry(id); - internal_set(entry.outlets, [...entry.outlets.v, outlet]); + internal_set(entry.outlets, (entry.all = [...entry.all, outlet])); - // During hydration, portals that were created before this outlet claim - // their server-rendered content now, while the hydration position is known. + // portals that were created before this outlet claim their server-rendered content now for (const render of entry.pending) { render(outlet); } const unregister = () => { - internal_set( - entry.outlets, - entry.outlets.v.filter((o) => o !== outlet) - ); + internal_set(entry.outlets, (entry.all = entry.all.filter((o) => o !== outlet))); }; return () => { @@ -503,6 +505,7 @@ export function portal(get_target, content) { } for (const k of keys) { + // Do the get outside the below if-block so we're notified of changes to any outlets source var outlets = get(get_outlet_entry(k).outlets); if (k === key) { @@ -516,7 +519,7 @@ export function portal(get_target, content) { if (target instanceof Element) { targets.add(target); } else if (hydrating) { - // an outlet with our key may appear later during this hydration + // An outlet with our key may appear later during this hydration // pass (`{#portal ...}` before `{@portal ...}` in the markup). // Register a callback so it can have us claim our server-rendered // content at its position. diff --git a/packages/svelte/tests/runtime-runes/samples/async-portal-fork/_config.js b/packages/svelte/tests/runtime-runes/samples/async-portal-fork/_config.js new file mode 100644 index 0000000000..ea50dee816 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-portal-fork/_config.js @@ -0,0 +1,19 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + solo: true, + async test({ assert, target }) { + const btn = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, ``); + + btn?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + `
portaled
portaled
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-portal-fork/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-portal-fork/main.svelte new file mode 100644 index 0000000000..adc831da90 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-portal-fork/main.svelte @@ -0,0 +1,20 @@ + + + + +{#portal 'target'} + portaled +{/portal} + +{#if show} +
{@portal 'target'}
+
{@portal 'target'}
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-portal-key-changing-fork/_config.js b/packages/svelte/tests/runtime-runes/samples/async-portal-key-changing-fork/_config.js new file mode 100644 index 0000000000..f0d0e04100 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-portal-key-changing-fork/_config.js @@ -0,0 +1,63 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [togglePortalKey, toggleOutletKey, shift, pop, commit] = + target.querySelectorAll('button'); + + togglePortalKey.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' ab' + ); + + toggleOutletKey.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' ab' + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' bb hi' + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' bb hi' + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' ba' + ); + + toggleOutletKey.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' ba' + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' bb hi' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-portal-key-changing-fork/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-portal-key-changing-fork/main.svelte new file mode 100644 index 0000000000..d90b739f30 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-portal-key-changing-fork/main.svelte @@ -0,0 +1,35 @@ + + + + + + + + +{await push(portalKey + outletKey)} + +{#portal portalKey} + hi +{/portal} + +{@portal outletKey}