UNPKG

ripple

Version:

Ripple is an elegant TypeScript UI framework

202 lines (178 loc) 4.72 kB
/** * @import { Block, TryBlock, TryBlockWithCatch } from '#server'; * @import { OutputInterface } from './index.js'; */ /** @typedef {Error} SSRError */ /** @typedef {(__output: OutputInterface) => void} BlockFunction */ /** @typedef {() => void} TryFunction */ /** @typedef {(error: SSRError) => void} CatchFunction */ /** @typedef {() => void} PendingFunction */ import { ASYNC_DERIVED_READ_THROWN } from '../client/constants.js'; import { TRY_CATCH_BLOCK, TRY_PENDING_BLOCK, REGULAR_BLOCK, COMPONENT_BLOCK, ROOT_BLOCK, CAUGHT_ERROR, } from './constants.js'; import { run_block, active_block, active_component, Output, set_active_block, TrackAsyncRunError, create_public_track_async_error, serialize_track_async_error, } from './index.js'; /** * @param {number} flags * @param {BlockFunction} fn * @param {any} [state] * @param {boolean} [skip_run] * @returns {Block} */ function block(flags, fn, state, skip_run = false) { /** @type {Block} */ var block = { co: active_component, f: active_block ? flags : flags | ROOT_BLOCK, fn, o: active_block ? active_block.o.branch() : new Output(null), p: active_block, s: state, first: null, last: null, next: null, prev: null, }; if (active_block) { push_block(block, active_block); } else { set_active_block(block); } if (!skip_run) { run_block(block); } return block; } /** * @param {TryFunction} try_fn * @param {CatchFunction | null} catch_fn * @param {PendingFunction | null} pending_fn * @returns {TryBlock} */ export function try_block(try_fn, catch_fn = null, pending_fn = null) { if (!pending_fn && !catch_fn) { throw new Error('try_block must have either pending or catch state'); } var flags = pending_fn && catch_fn ? TRY_PENDING_BLOCK | TRY_CATCH_BLOCK : catch_fn ? TRY_CATCH_BLOCK : TRY_PENDING_BLOCK; var created_block = block(flags, try_fn, { p: pending_fn, c: catch_fn }, true); var previous_block = /** @type {Block} */ (active_block); set_active_block(created_block); try { try_fn(); } catch (error) { if (error === ASYNC_DERIVED_READ_THROWN && created_block.f & TRY_PENDING_BLOCK) { // we should only end up here in the streaming mode during the sync phase created_block.o.clear(); pending_fn?.(); // continue processing other try blocks return created_block; } if (created_block.f & TRY_CATCH_BLOCK) { created_block.o.clear(); cancel_async_operations(created_block); // make sure to serialize trackAsync error so the client can hydrate them properly // needs to happen after clearing output if (error instanceof TrackAsyncRunError) { var { tracked: t, cause } = /** @type {InstanceType<typeof TrackAsyncRunError>} */ (error); var public_error = create_public_track_async_error(cause); catch_fn?.(public_error); serialize_track_async_error(t.h, public_error); return created_block; } // render the catch catch_fn?.(/** @type {SSRError} */ (error)); } else { // no catch handler, re-throw for an outer boundary to handle throw error; } } finally { set_active_block(previous_block); } return created_block; } /** * @param {BlockFunction} fn * @returns {Block} */ export function regular_block(fn) { return block(REGULAR_BLOCK, fn); } /** * @param {BlockFunction} fn * @returns {Block} */ export function component_block(fn) { return block(COMPONENT_BLOCK, fn, null, true); } /** * @param {Block} block * @returns {TryBlockWithCatch} */ export function get_closest_catch_block(block) { var current = block; while (current !== null) { // there should always be a catch block since we always start with try/catch when rendering if (current.f & TRY_CATCH_BLOCK) { return current; } // we always start with a root block that has the try/catch flag, // so we should never get to null current = /** @type {Block} */ (current.p); } throw new Error('No catch block found'); } /** * @param {Block | null} block * @returns {void} */ export function cancel_async_operations(block) { if (block === null) { return; } if (block.f & TRY_CATCH_BLOCK) { if (block.f & CAUGHT_ERROR) { // already handling an error — skip return; } block.o.cancelAsyncOperations(); block.f |= CAUGHT_ERROR; } var child = block.first; while (child !== null) { cancel_async_operations(child); child = child.next; } } /** * @param {Block} block * @param {Block} parent_block */ function push_block(block, parent_block) { var parent_last = parent_block.last; if (parent_last === null) { parent_block.last = parent_block.first = block; } else { parent_last.next = block; block.prev = parent_last; parent_block.last = block; } }