pull/17004/head
Rich Harris 6 days ago
parent cfa87a9c37
commit ee1b1140d3

@ -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,

@ -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() {}

@ -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<void>, 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;
}

@ -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<T = void>(fn?: (() => T) | undefined): T;
/**
* Create a snippet programmatically
* */
@ -448,6 +443,16 @@ declare module 'svelte' {
}): Snippet<Params>;
/** Anything except a function */
type NotFunction<T> = 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<T = void>(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.
*

Loading…
Cancel
Save