slightly better approach

pull/17004/head
Rich Harris 2 weeks ago
parent 63fa53064c
commit 4469cc5c2c

@ -12,17 +12,20 @@ import {
ROOT_EFFECT, ROOT_EFFECT,
MAYBE_DIRTY, MAYBE_DIRTY,
DERIVED, DERIVED,
BOUNDARY_EFFECT BOUNDARY_EFFECT,
INSPECT_EFFECT
} from '#client/constants'; } from '#client/constants';
import { async_mode_flag } from '../../flags/index.js'; import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js'; import { deferred, define_property } from '../../shared/utils.js';
import { import {
active_effect, active_effect,
get, get,
increment_write_version,
is_dirty, is_dirty,
is_updating_effect, is_updating_effect,
set_is_updating_effect, set_is_updating_effect,
set_signal_status, set_signal_status,
tick,
update_effect update_effect
} from '../runtime.js'; } from '../runtime.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
@ -123,13 +126,6 @@ export class Batch {
*/ */
#deferred = null; #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 * Deferred effects (which run after async work has completed) that are DIRTY
* @type {Effect[]} * @type {Effect[]}
@ -309,8 +305,9 @@ export class Batch {
} }
flush() { flush() {
if (queued_root_effects.length > 0) {
this.activate(); this.activate();
if (queued_root_effects.length > 0) {
flush_effects(); flush_effects();
if (current_batch !== null && current_batch !== this) { if (current_batch !== null && current_batch !== this) {
@ -341,14 +338,10 @@ export class Batch {
this.#callbacks.clear(); this.#callbacks.clear();
} }
if (this.#pending === 0) { if (this.#pending === 0 && !this.is_fork) {
if (this.is_fork) {
this.#fork_deferred?.resolve();
} else {
this.#commit(); this.#commit();
} }
} }
}
#commit() { #commit() {
// If there are other pending batches, they now need to be 'rebased' — // If there are other pending batches, they now need to be 'rebased' —
@ -477,10 +470,6 @@ export class Batch {
return (this.#deferred ??= deferred()).promise; return (this.#deferred ??= deferred()).promise;
} }
fork_settled() {
return (this.#fork_deferred ??= deferred()).promise;
}
static ensure() { static ensure() {
if (current_batch === null) { if (current_batch === null) {
const batch = (current_batch = new Batch()); const batch = (current_batch = new Batch());
@ -736,6 +725,28 @@ function mark_effects(value, sources) {
} }
} }
/**
* When committing a fork, we need to trigger inspect effects so that
* any `$state.eager(...)` expressions update immediately. This
* function allows us to discover them
* @param {Value} value
* @param {Set<Effect>} effects
*/
function mark_inspect_effects(value, effects) {
if (value.reactions !== null) {
for (const reaction of value.reactions) {
const flags = reaction.f;
if ((flags & DERIVED) !== 0) {
mark_inspect_effects(/** @type {Derived} */ (reaction), effects);
} else if ((flags & INSPECT_EFFECT) !== 0) {
set_signal_status(reaction, DIRTY);
effects.add(/** @type {Effect} */ (reaction));
}
}
}
}
/** /**
* @param {Reaction} reaction * @param {Reaction} reaction
* @param {Source[]} sources * @param {Source[]} sources
@ -841,13 +852,6 @@ export function eager(fn) {
return value; return value;
} }
/**
* Forcibly remove all current batches, to prevent cross-talk between tests
*/
export function clear() {
batches.clear();
}
/** /**
* @param {() => void} fn * @param {() => void} fn
* @returns {{ commit: () => void, discard: () => void }} * @returns {{ commit: () => void, discard: () => void }}
@ -860,10 +864,9 @@ export function fork(fn) {
const batch = Batch.ensure(); const batch = Batch.ensure();
batch.is_fork = true; batch.is_fork = true;
const promise = batch.fork_settled(); const settled = batch.settled();
flushSync(fn); flushSync(fn);
const deferred_inspect_effects = inspect_effects;
// revert state changes // revert state changes
for (const [source, value] of batch.previous) { for (const [source, value] of batch.previous) {
@ -876,36 +879,41 @@ export function fork(fn) {
throw new Error('Cannot commit this batch'); // TODO better error throw new Error('Cannot commit this batch'); // TODO better error
} }
batch.is_fork = false;
// delete all other forks // delete all other forks
for (const b of batches) { for (const b of batches) {
if (b !== batch && b.is_fork) { if (b.is_fork) batches.delete(b);
batches.delete(b);
}
} }
await promise; // apply changes
for (const [source, value] of batch.current) { for (const [source, value] of batch.current) {
source.v = value; source.v = value;
} }
const previous_inspect_effects = inspect_effects; // trigger any `$state.eager(...)` expressions with the new state
flushSync(() => {
const inspect_effects = new Set();
for (const source of batch.current.keys()) {
mark_inspect_effects(source, inspect_effects);
}
try { set_inspect_effects(inspect_effects);
if (DEV && deferred_inspect_effects.size > 0) {
set_inspect_effects(deferred_inspect_effects);
flush_inspect_effects(); flush_inspect_effects();
} });
batch.is_fork = false;
batch.activate();
batch.revive(); batch.revive();
} finally { await settled;
set_inspect_effects(previous_inspect_effects);
}
}, },
discard: () => { discard: () => {
batches.delete(batch); batches.delete(batch);
} }
}; };
} }
/**
* Forcibly remove all current batches, to prevent cross-talk between tests
*/
export function clear() {
batches.clear();
}

Loading…
Cancel
Save