aa-fork
Rich Harris 8 months ago
parent a2cbfe2b15
commit 72f30dd892

@ -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';

@ -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';

@ -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);
}

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

@ -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);

@ -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));
}
}

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

@ -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<Source, SourceFork>;
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. */

@ -419,6 +419,9 @@ declare module 'svelte' {
render: () => string;
setup?: (element: Element) => void | (() => void);
}): Snippet<Params>;
export function fork(fn: () => void): Promise<{
apply: () => void;
}>;
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;
/**

Loading…
Cancel
Save