UNPKG

svelte

Version:

Cybernetically enhanced web apps

143 lines (122 loc) 4.07 kB
/** @import { Source, TemplateNode } from '#client' */ import { is_promise } from '../../../shared/utils.js'; import { block } from '../../reactivity/effects.js'; import { internal_set, mutable_source, source } from '../../reactivity/sources.js'; import { hydrate_next, 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 { is_runes } from '../../context.js'; import { Batch, 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; const CATCH = 2; /** @typedef {typeof PENDING | typeof THEN | typeof CATCH} AwaitState */ /** * @template V * @param {TemplateNode} node * @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 * @returns {void} */ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { if (hydrating) { hydrate_next(); } var runes = is_runes(); 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); var branches = new BranchManager(node); 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 `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 set_hydrate_node(skip_nodes()); set_hydrating(false); } if (is_promise(input)) { var restore = capture(); var resolved = false; /** * @param {() => void} fn */ const resolve = (fn) => { if (destroyed) return; resolved = true; // We don't want to restore the previous batch here; {#await} blocks don't follow the async logic // we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak. restore(false); // Make sure we have a batch, since the branch manager expects one to exist Batch.ensure(); if (hydrating) { // `restore()` could set `hydrating` to `true`, which we very much // don't want — we want to restore everything _except_ this set_hydrating(false); } try { fn(); } finally { unset_context(); // 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))); }); }, (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) { 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 then queue_micro_task(() => { if (!resolved) { resolve(() => { branches.ensure(PENDING, pending_fn); }); } }); } } else { internal_set(value, input); branches.ensure(THEN, then_fn && ((target) => then_fn(target, value))); } if (mismatch) { // continue in hydration mode set_hydrating(true); } return () => { destroyed = true; }; }); }