Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/yellow-shrimps-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

chore: centralise branch management
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,9 @@ export function client_component(analysis, options) {
.../** @type {ESTree.Statement[]} */ (template.body)
]);

component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true))));
component_block.body.push(
b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true)))
);
} else {
component_block.body.push(
...state.instance_level_snippets,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ export function Fragment(node, context) {
}

if (has_await) {
return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]);
return b.block([
b.stmt(
b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true))
)
]);
} else {
return b.block(body);
}
Expand Down
207 changes: 70 additions & 137 deletions packages/svelte/src/internal/client/dom/blocks/await.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
/** @import { Effect, Source, TemplateNode } from '#client' */
import { DEV } from 'esm-env';
/** @import { Source, TemplateNode } from '#client' */
import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { block } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { set_active_effect, set_active_reaction } from '../../runtime.js';
import {
hydrate_next,
hydrate_node,
hydrating,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import {
component_context,
dev_stack,
is_runes,
set_component_context,
set_dev_current_component_function,
set_dev_stack
} from '../../context.js';
import { is_runes } from '../../context.js';
import { flushSync, is_flushing_sync } from '../../reactivity/batch.js';
import { BranchManager } from './branches.js';
import { capture, unset_context } from '../../reactivity/async.js';

const PENDING = 0;
const THEN = 1;
Expand All @@ -33,7 +25,7 @@ const CATCH = 2;
/**
* @template V
* @param {TemplateNode} node
* @param {(() => Promise<V>)} get_input
* @param {(() => any)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: Source<V>) => void)} then_fn
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
Expand All @@ -44,161 +36,102 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
hydrate_next();
}

var anchor = node;
var runes = is_runes();
var active_component_context = component_context;

/** @type {any} */
var component_function = DEV ? component_context?.function : null;
var dev_original_stack = DEV ? dev_stack : null;

/** @type {V | Promise<V> | typeof UNINITIALIZED} */
var input = UNINITIALIZED;

/** @type {Effect | null} */
var pending_effect;

/** @type {Effect | null} */
var then_effect;

/** @type {Effect | null} */
var catch_effect;

var input_source = runes
? source(/** @type {V} */ (undefined))
: mutable_source(/** @type {V} */ (undefined), false, false);
var error_source = runes ? source(undefined) : mutable_source(undefined, false, false);
var resolved = false;
/**
* @param {AwaitState} state
* @param {boolean} restore
*/
function update(state, restore) {
resolved = true;

if (restore) {
set_active_effect(effect);
set_active_reaction(effect); // TODO do we need both?
set_component_context(active_component_context);
if (DEV) {
set_dev_current_component_function(component_function);
set_dev_stack(dev_original_stack);
}
}

try {
if (state === PENDING && pending_fn) {
if (pending_effect) resume_effect(pending_effect);
else pending_effect = branch(() => pending_fn(anchor));
}

if (state === THEN && then_fn) {
if (then_effect) resume_effect(then_effect);
else then_effect = branch(() => then_fn(anchor, input_source));
}

if (state === CATCH && catch_fn) {
if (catch_effect) resume_effect(catch_effect);
else catch_effect = branch(() => catch_fn(anchor, error_source));
}

if (state !== PENDING && pending_effect) {
pause_effect(pending_effect, () => (pending_effect = null));
}

if (state !== THEN && then_effect) {
pause_effect(then_effect, () => (then_effect = null));
}
var v = /** @type {V} */ (UNINITIALIZED);
var value = runes ? source(v) : mutable_source(v, false, false);
var error = runes ? source(v) : mutable_source(v, false, false);

if (state !== CATCH && catch_effect) {
pause_effect(catch_effect, () => (catch_effect = null));
}
} finally {
if (restore) {
if (DEV) {
set_dev_current_component_function(null);
set_dev_stack(null);
}
var branches = new BranchManager(node);

set_component_context(null);
set_active_reaction(null);
set_active_effect(null);

// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
if (!is_flushing_sync) flushSync();
}
}
}

var effect = block(() => {
if (input === (input = get_input())) return;
block(() => {
var input = get_input();
var destroyed = false;

/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
// @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight
let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE);
// @ts-ignore coercing `node` to a `Comment` causes TypeScript and Prettier to fight
let mismatch = hydrating && is_promise(input) === (node.data === HYDRATION_START_ELSE);

if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh
anchor = skip_nodes();

set_hydrate_node(anchor);
set_hydrate_node(skip_nodes());
set_hydrating(false);
mismatch = true;
}

if (is_promise(input)) {
var promise = input;
var restore = capture();
var resolved = false;

/**
* @param {() => void} fn
*/
const resolve = (fn) => {
if (destroyed) return;

resolved = true;
restore();

if (hydrating) {
// we want to restore everything _except_ this
set_hydrating(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to re-enter hydration mode after this? (tbh I don't quite understand why we need to check for hydrating at all here; how can you end up there with hydrating being true?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling restore() will set hydrating back to true if we were initially hydrating. By the time the promise resolves, we've finished hydrating — if we don't set it back to false then all hell breaks loose, because the next time a block effect runs it will enter the wrong code path. Added a comment

}

resolved = false;
try {
fn();
} finally {
unset_context();

promise.then(
(value) => {
if (promise !== input) return;
// we technically could use `set` here since it's on the next microtick
// but let's use internal_set for consistency and just to be safe
internal_set(input_source, value);
update(THEN, true);
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
if (!is_flushing_sync) flushSync();
}
};

input.then(
(v) => {
resolve(() => {
internal_set(value, v);
branches.ensure(THEN, then_fn && ((target) => then_fn(target, value)));
});
},
(error) => {
if (promise !== input) return;
// we technically could use `set` here since it's on the next microtick
// but let's use internal_set for consistency and just to be safe
internal_set(error_source, error);
update(CATCH, true);
if (!catch_fn) {
// Rethrow the error if no catch block exists
throw error_source.v;
}
(e) => {
resolve(() => {
internal_set(error, e);
branches.ensure(THEN, catch_fn && ((target) => catch_fn(target, error)));

if (!catch_fn) {
// Rethrow the error if no catch block exists
throw error.v;
}
});
}
);

if (hydrating) {
if (pending_fn) {
pending_effect = branch(() => pending_fn(anchor));
}
branches.ensure(PENDING, pending_fn);
} else {
// Wait a microtask before checking if we should show the pending state as
// the promise might have resolved by the next microtask.
// the promise might have resolved by then
queue_micro_task(() => {
if (!resolved) update(PENDING, true);
if (!resolved) {
resolve(() => {
branches.ensure(PENDING, pending_fn);
});
}
});
}
} else {
internal_set(input_source, input);
update(THEN, false);
internal_set(value, input);
branches.ensure(THEN, then_fn && ((target) => then_fn(target, value)));
}

if (mismatch) {
// continue in hydration mode
set_hydrating(true);
}

// Set the input to something else, in order to disable the promise callbacks
return () => (input = UNINITIALIZED);
return () => {
destroyed = true;
};
});

if (hydrating) {
anchor = hydrate_node;
}
}
26 changes: 7 additions & 19 deletions packages/svelte/src/internal/client/dom/blocks/boundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import {
import { HYDRATION_START_ELSE } from '../../../../constants.js';
import { component_context, set_component_context } from '../../context.js';
import { handle_error, invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import {
block,
branch,
destroy_effect,
move_effect,
pause_effect
} from '../../reactivity/effects.js';
import {
active_effect,
active_reaction,
Expand Down Expand Up @@ -425,24 +431,6 @@ export class Boundary {
}
}

/**
*
* @param {Effect} effect
* @param {DocumentFragment} fragment
*/
function move_effect(effect, fragment) {
var node = effect.nodes_start;
var end = effect.nodes_end;

while (node !== null) {
/** @type {TemplateNode | null} */
var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));

fragment.append(node);
node = next;
}
}

export function get_boundary() {
return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b);
}
Expand Down
Loading
Loading