From 72f30dd8921bceb7c6fdc15d55ffef7bce668e71 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Jan 2025 14:35:18 -0500 Subject: [PATCH] WIP --- packages/svelte/src/index-client.js | 2 + packages/svelte/src/index-server.js | 2 + .../internal/client/dom/blocks/boundary.js | 10 ++ packages/svelte/src/internal/client/fork.js | 129 ++++++++++++++++++ .../internal/client/reactivity/deriveds.js | 15 +- .../src/internal/client/reactivity/sources.js | 35 ++++- .../svelte/src/internal/client/runtime.js | 1 + .../svelte/src/internal/client/types.d.ts | 13 ++ packages/svelte/types/index.d.ts | 3 + 9 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 packages/svelte/src/internal/client/fork.js diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 587d766233..3df3391417 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -191,3 +191,5 @@ export { } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; + +export { fork } from './internal/client/fork.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 0f1aff8f5a..b77a8c7125 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -35,6 +35,8 @@ export function unmount() { export async function tick() {} +export async function fork() {} + export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js'; export { createRawSnippet } from './internal/server/blocks/snippet.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index d832c4d354..c3fd282cdf 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -29,6 +29,7 @@ import { import { get_next_sibling } from '../operations.js'; import { queue_boundary_micro_task, queue_micro_task } from '../task.js'; import * as e from '../../../shared/errors.js'; +import { active_fork, decrement_fork, increment_fork, set_active_fork } from '../../fork.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); @@ -249,11 +250,13 @@ export function capture() { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; + var previous_fork = active_fork; return function restore() { set_active_effect(previous_effect); set_active_reaction(previous_reaction); set_component_context(previous_component_context); + set_active_fork(previous_fork); // prevent the active effect from outstaying its welcome queue_micro_task(exit); @@ -269,6 +272,12 @@ export function is_pending_boundary(boundary) { } export function suspend() { + if (active_fork !== null) { + var fork = active_fork; + increment_fork(fork); + return () => decrement_fork(fork); + } + var boundary = active_effect; while (boundary !== null) { @@ -311,4 +320,5 @@ function exit() { set_active_effect(null); set_active_reaction(null); set_component_context(null); + set_active_fork(null); } diff --git a/packages/svelte/src/internal/client/fork.js b/packages/svelte/src/internal/client/fork.js new file mode 100644 index 0000000000..99a856c500 --- /dev/null +++ b/packages/svelte/src/internal/client/fork.js @@ -0,0 +1,129 @@ +/** @import { Derived, Effect, Fork, Value } from '#client' */ +import { BLOCK_EFFECT, DERIVED, DIRTY, TEMPLATE_EFFECT } from './constants.js'; +import { queue_micro_task } from './dom/task.js'; +import { flush_sync, schedule_effect, set_signal_status } from './runtime.js'; + +/** @type {Fork | null} */ +export let active_fork = null; + +/** + * @param {Fork | null} fork + */ +export function set_active_fork(fork) { + active_fork = fork; +} + +/** + * @param {(error?: Error) => void} callback + * @returns {Fork} + */ +function create_fork(callback) { + return { + pending: 0, + sources: new Map(), + callback + }; +} + +/** + * @param {Fork} fork + */ +export function increment_fork(fork) { + fork.pending += 1; +} + +/** + * @param {Fork} fork + */ +export function decrement_fork(fork) { + fork.pending -= 1; + + queue_micro_task(() => { + if (fork.pending === 0) { + // TODO if the state that was originally set inside the + // fork callback was updated in the meantime, reject + // the fork. Also need to handle a case like + // `{#if a || b}` where the fork makes `a` + // truthy but `b` became truthy in the + // meantime — requires a 'rebase' + fork.callback(); + } + }); +} + +/** + * @param {Fork} fork + */ +function apply_fork(fork) { + // TODO check the fork is still valid and error otherwise + + for (const [source, saved] of fork.sources) { + source.v = saved.next_v; + source.wv = saved.next_wv; + + mark_effects(source); + } +} + +/** + * @param {Value} value + */ +function mark_effects(value) { + if (value.reactions === null) return; + + for (var reaction of value.reactions) { + var flags = reaction.f; + + if ((flags & DERIVED) !== 0) { + mark_effects(/** @type {Derived} */ (reaction)); + } else { + if ((flags & BLOCK_EFFECT) === 0 || (flags & TEMPLATE_EFFECT) !== 0) { + set_signal_status(reaction, DIRTY); + schedule_effect(/** @type {Effect} */ (reaction)); + } + } + } +} + +/** + * @param {Fork} fork + */ +function revert(fork) { + for (const [source, saved] of fork.sources) { + source.v = saved.v; + source.wv = saved.wv; + } +} + +/** + * @param {() => void} fn + * @returns {Promise<{ apply: () => void }>} + */ +export function fork(fn) { + flush_sync(); + + if (active_fork !== null) { + throw new Error("TODO can't fork inside a fork, you mad fool"); + } + + try { + return new Promise((fulfil, reject) => { + var f = (active_fork = create_fork((error) => { + if (error) { + reject(error); + } else { + fulfil({ + apply: () => apply_fork(f) + }); + } + })); + + fn(); + flush_sync(); + + revert(active_fork); + }); + } finally { + active_fork = null; + } +} diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index bff8f32d34..d8d7ea6871 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -21,7 +21,8 @@ import { set_active_effect, component_context, handle_error, - get + get, + flush_sync } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; @@ -32,6 +33,7 @@ import { get_stack } from '../dev/tracing.js'; import { tracing_mode_flag } from '../../flags/index.js'; import { capture, suspend } from '../dom/blocks/boundary.js'; import { flush_boundary_micro_tasks } from '../dom/task.js'; +import { active_fork } from '../fork.js'; /** * @template V @@ -114,7 +116,18 @@ export function async_derived(fn) { if (promise === current) { restore(); + + var prev = { v: value.v, wv: value.wv }; + internal_set(value, v); + + if (active_fork) { + flush_sync(); + + // revert + value.v = prev.v; + value.wv = prev.wv; + } } } catch (e) { handle_error(e, parent, null, parent.ctx); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index c2448c9ee5..4da26db826 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -30,11 +30,13 @@ import { UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, - ROOT_EFFECT + ROOT_EFFECT, + TEMPLATE_EFFECT } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack } from '../dev/tracing.js'; +import { active_fork } from '../fork.js'; export let inspect_effects = new Set(); @@ -171,9 +173,27 @@ export function set(source, value) { export function internal_set(source, value) { if (!source.equals(value)) { var old_value = source.v; + var old_write_version = source.wv; + source.v = value; source.wv = increment_write_version(); + if (active_fork !== null) { + var source_fork = active_fork.sources.get(source); + + if (source_fork) { + source_fork.next_v = value; + source_fork.next_wv = source.wv; + } else { + active_fork.sources.set(source, { + v: old_value, + wv: old_write_version, + next_v: value, + next_wv: source.wv + }); + } + } + if (DEV && tracing_mode_flag) { source.updated = get_stack('UpdatedAt'); if (active_effect != null) { @@ -254,13 +274,22 @@ function mark_reactions(signal, status) { continue; } - set_signal_status(reaction, status); + // in a fork, only schedule block effects (that are not also template effects) + // TODO we refer to too many things as 'blocks', it's confusing + var skip = + active_fork !== null && + (flags & DERIVED) === 0 && + ((flags & BLOCK_EFFECT) === 0 || (flags & TEMPLATE_EFFECT) !== 0); + + if (!skip) { + set_signal_status(reaction, status); + } // If the signal a) was previously clean or b) is an unowned derived, then mark it if ((flags & (CLEAN | UNOWNED)) !== 0) { if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else { + } else if (!skip) { schedule_effect(/** @type {Effect} */ (reaction)); } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 40a52a4aec..6fefbf1741 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -44,6 +44,7 @@ import { FILENAME } from '../../constants.js'; import { legacy_mode_flag, tracing_mode_flag } from '../flags/index.js'; import { tracing_expressions, get_stack } from './dev/tracing.js'; import { is_pending_boundary } from './dom/blocks/boundary.js'; +import { active_fork } from './fork.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 7208ed7783..95a9fafefb 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -177,6 +177,19 @@ export type TaskCallback = (now: number) => boolean | void; export type TaskEntry = { c: TaskCallback; f: () => void }; +export interface SourceFork { + v: any; + wv: number; + next_v: any; + next_wv: number; +} + +export interface Fork { + sources: Map; + pending: number; + callback: (error?: Error) => void; +} + /** Dev-only */ export interface ProxyMetadata { /** The components that 'own' this state, if any. `null` means no owners, i.e. everyone can mutate this state. */ diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 7b27d0ddb7..bfa3bc7cae 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -419,6 +419,9 @@ declare module 'svelte' { render: () => string; setup?: (element: Element) => void | (() => void); }): Snippet; + export function fork(fn: () => void): Promise<{ + apply: () => void; + }>; /** Anything except a function */ type NotFunction = T extends Function ? never : T; /**