From ee1b1140d36b572cd6ff67067c6d5e66fbae7f4b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 20 Oct 2025 21:24:27 -0400 Subject: [PATCH] WIP --- packages/svelte/src/index-client.js | 2 +- packages/svelte/src/index-server.js | 4 + .../src/internal/client/reactivity/batch.js | 78 ++++++++++++++++--- packages/svelte/types/index.d.ts | 15 ++-- 4 files changed, 81 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 337cbb500b..4fcfff980d 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -241,7 +241,7 @@ function init_update_callbacks(context) { return (l.u ??= { a: [], b: [], m: [] }); } -export { flushSync } from './internal/client/reactivity/batch.js'; +export { flushSync, fork } from './internal/client/reactivity/batch.js'; export { createContext, getContext, diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 223ce6a4cd..61b0d98c06 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -33,6 +33,10 @@ export function unmount() { e.lifecycle_function_unavailable('unmount'); } +export function fork() { + e.lifecycle_function_unavailable('fork'); +} + export async function tick() {} export async function settled() {} diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 215b992cc4..17ba2c9ed4 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -114,6 +114,13 @@ export class Batch { */ #deferred = null; + /** + * A deferred that resolves when a fork is ready + * TODO replace with Promise.withResolvers once supported widely enough + * @type {{ promise: Promise, resolve: (value?: any) => void, reject: (reason: unknown) => void } | null} + */ + #fork_deferred = null; + /** * Deferred effects (which run after async work has completed) that are DIRTY * @type {Effect[]} @@ -133,6 +140,8 @@ export class Batch { */ skipped_effects = new Set(); + is_fork = false; + /** * * @param {Effect[]} root_effects @@ -159,17 +168,25 @@ export class Batch { // if there is no outstanding async work, commit if (this.#pending === 0) { - // commit before flushing effects, since that may result in - // another batch being created - this.#commit(); + if (this.is_fork) { + this.#fork_deferred?.resolve(); + } else { + // commit before flushing effects, since that may result in + // another batch being created + this.#commit(); + } } - if (this.#blocking_pending > 0) { + if (this.#blocking_pending > 0 || this.is_fork) { this.#defer_effects(target.effects); this.#defer_effects(target.render_effects); this.#defer_effects(target.block_effects); } else { - // TODO append/detach blocks here, not in #commit + for (const fn of this.#callbacks) { + fn(); + } + + this.#callbacks.clear(); // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. @@ -301,7 +318,7 @@ export class Batch { return; } } else if (this.#pending === 0) { - this.#commit(); + this.process([]); // TODO this feels awkward } this.deactivate(); @@ -321,12 +338,6 @@ export class Batch { * Append and remove branches to/from the DOM */ #commit() { - for (const fn of this.#callbacks) { - fn(); - } - - this.#callbacks.clear(); - // If there are other pending batches, they now need to be 'rebased' — // in other words, we re-run block/async effects with the newly // committed state, unless the batch in question has a more @@ -423,6 +434,10 @@ export class Batch { this.#pending -= 1; if (blocking) this.#blocking_pending -= 1; + this.revive(); + } + + revive() { for (const e of this.#dirty_effects) { set_signal_status(e, DIRTY); schedule_effect(e); @@ -448,6 +463,10 @@ export class Batch { return (this.#deferred ??= deferred()).promise; } + fork_settled() { + return (this.#fork_deferred ??= deferred()).promise; + } + static ensure() { if (current_batch === null) { const batch = (current_batch = new Batch()); @@ -795,3 +814,38 @@ export function eager(fn) { export function clear() { batches.clear(); } + +/** + * @param {() => void} fn + * @returns {Promise<{ commit: () => void, discard: () => void }>} + */ +export function fork(fn) { + /** @type {Promise<{ commit: () => void, discard: () => void }>} */ + const promise = new Promise((fulfil) => { + // TODO does qmt guarantee this will run outside a batch? + // because it needs to + queue_micro_task(async () => { + const batch = Batch.ensure(); + batch.is_fork = true; + + fn(); + await batch.fork_settled(); + + // TODO revert state changes + + fulfil({ + commit: () => { + // TODO reapply state changes + batch.is_fork = false; + batch.activate(); + batch.revive(); + }, + discard: () => { + batches.delete(batch); + } + }); + }); + }); + + return promise; +} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d260b738c3..0823fd942b 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -434,11 +434,6 @@ declare module 'svelte' { * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead * */ export function afterUpdate(fn: () => void): void; - /** - * Synchronously flush any pending updates. - * Returns void if no callback is provided, otherwise returns the result of calling the callback. - * */ - export function flushSync(fn?: (() => T) | undefined): T; /** * Create a snippet programmatically * */ @@ -448,6 +443,16 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Synchronously flush any pending updates. + * Returns void if no callback is provided, otherwise returns the result of calling the callback. + * */ + export function flushSync(fn?: (() => T) | undefined): T; + + export function fork(fn: () => void): Promise<{ + commit: () => void; + discard: () => void; + }>; /** * Returns a `[get, set]` pair of functions for working with context in a type-safe way. *