From 824b3f41a4562641e75f2fdfe99654085e246d93 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 21 Nov 2025 23:50:43 +0100 Subject: [PATCH] feat: add `isCommitted/isDiscarded/run` to `fork` This gives more insight into the state of a fork and also gives you the ability to run another function in the context of the fork. SvelteKit needs this --- .changeset/lemon-walls-judge.md | 5 +++ packages/svelte/src/index.d.ts | 12 +++++++ .../src/internal/client/reactivity/batch.js | 31 +++++++++++++++++++ packages/svelte/types/index.d.ts | 12 +++++++ 4 files changed, 60 insertions(+) create mode 100644 .changeset/lemon-walls-judge.md diff --git a/.changeset/lemon-walls-judge.md b/.changeset/lemon-walls-judge.md new file mode 100644 index 000000000000..cf4f7e6ed13a --- /dev/null +++ b/.changeset/lemon-walls-judge.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `isCommitted/isDiscarded/run` to `fork` diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index a1782f5b61a5..d6e013f0e73f 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -366,6 +366,18 @@ export interface Fork { * Discard the fork */ discard(): void; + /** + * Whether the fork has been committed + */ + isCommitted(): boolean; + /** + * Whether the fork has been discarded + */ + isDiscarded(): boolean; + /** + * Run a function within the forked context. + */ + run(fn: () => void): Promise; } export * from './index-client.js'; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 22526df7c1f2..800fb1a854cb 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1014,6 +1014,37 @@ export function fork(fn) { batches.delete(batch); batch.discard(); } + }, + isCommitted: () => committed, + isDiscarded: () => !committed && !batches.has(batch), + run: async (fn) => { + if (committed) { + // TODO error instead? + await settled; + fn(); + } else { + // We want to start with the current state of the world, not the + // state back when the fork was created. Also important to + // correctly revert state changes later + const previous = batch.previous; + batch.previous = new Map(); + batch.activate(); + + flushSync(fn); + + // revert state changes + for (var [source, value] of batch.previous) { + source.v = value; + } + + // merge 'previous' map + // TODO is this correct? + for (const [source, value] of previous) { + if (!batch.previous.has(source)) { + batch.previous.set(source, value); + } + } + } } }; } diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 5e3ca77eb5cd..f75bd4876d29 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -363,6 +363,18 @@ declare module 'svelte' { * Discard the fork */ discard(): void; + /** + * Whether the fork has been committed + */ + isCommitted(): boolean; + /** + * Whether the fork has been discarded + */ + isDiscarded(): boolean; + /** + * Run a function within the forked context. + */ + run(fn: () => void): Promise; } /** * Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed.