UNPKG

ripple

Version:

Ripple is an elegant TypeScript UI framework

1,794 lines (1,605 loc) 42.2 kB
/** * @import { Component, Dependency, Block, TryBlockWithCatch } from '#server'; * @import { NestedArray } from '#helpers'; * @import { Props } from '#public'; * @import { RenderResult, BaseRenderOptions, RenderStreamResult, Stream, StreamSink } from 'ripple/server'; */ // Export-only Types /** @typedef {Output} OutputInterface */ // Internal Types /** @typedef {(props?: Props) => void} RenderComponent */ /** @typedef {{ tag: string; parent: undefined | ElementContext; filename: undefined | string; line: number; column: number; }} ElementContext */ /** @typedef {{ cancel: () => void }} RegisteredAsyncOperation */ // Both /** @typedef {TrackedValue} Tracked */ /** @typedef {DerivedValue} Derived */ import { DERIVED, UNINITIALIZED, TRACKED, SUSPENSE_PENDING, SUSPENSE_REJECTED, ASYNC_DERIVED_READ_THROWN, TRACKED_UPDATED, } from '../client/constants.js'; import { DEV } from 'esm-env'; import { is_ripple_object } from '../client/utils.js'; import { array_slice, is_array } from '@tsrx/core/runtime/language-helpers'; import { escape, escape_script, is_boolean_attribute, normalize_css_property_name, } from '@tsrx/core/runtime/html'; import { clsx } from 'clsx'; import { create_ref_prop } from '@tsrx/core/runtime/ref'; import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../constants.js'; import { is_tsrx_element, normalize_children, tsrx_element } from '../../element.js'; import { is_tag_valid_with_parent, is_tag_valid_with_ancestor, } from '../../../html-tree-validation.js'; import { get_async_track_result } from '../../../utils/async.js'; import { get_track_async_script_id } from '../../../utils/track-async-serialization.js'; import * as devalue from 'devalue'; import { cancel_async_operations, component_block, get_closest_catch_block, try_block, } from './blocks.js'; import { COMPONENT_BLOCK, TRY_BLOCK } from './constants.js'; export { escape }; export { register_component_css as register_css } from './css-registry.js'; export { simple_hash, strong_hash } from '@tsrx/core/runtime/hash'; export { context } from './context.js'; export { try_block, component_block, regular_block } from './blocks.js'; export { array_slice }; export { tsrx_element, normalize_children }; export { create_ref_prop }; /** @extends Error */ export class TrackAsyncRunError extends Error { /** @type {Tracked} */ tracked; /** @type {Error} */ cause; /** * @param {string} message * @param {{tracked: Tracked, cause: Error}} options */ constructor(message, options) { super(message); this.name = 'TrackAsyncRunError'; this.tracked = options.tracked; this.cause = options.cause; } } export function noop() {} /** * @param {any[]} value * @returns {void} */ function render_tsrx_collection(value) { for (var i = 0; i < value.length; i++) { var item = value[i]; if (is_tsrx_element(item)) { item.render({}); } else if (is_array(item)) { render_tsrx_collection(item); } else if (item != null) { output_push(escape(item)); } } } /** * @param {any} value * @returns {void} */ export function render_expression(value) { output_push(BLOCK_OPEN); if (is_tsrx_element(value)) { value.render({}); } else if (is_array(value)) { render_tsrx_collection(value); } else { output_push(escape(value ?? '')); } output_push(BLOCK_CLOSE); } /** * @returns {Stream} */ export function create_ssr_stream() { /** @type {ReadableStreamDefaultController<Uint8Array> | null} */ var c = null; /** @type {ReadableStream<Uint8Array>} */ var stream = new ReadableStream({ start(controller) { // this runs synchronously c = controller; }, }); var encoder = new TextEncoder(); var is_closed = false; var controller = /** @type {ReadableStreamDefaultController<Uint8Array>} */ ( /** @type {unknown} */ (c) ); var close = controller.close; var error = controller.error; controller.close = function (...args) { is_closed = true; close.call(controller, ...args); }; controller.error = function (...args) { is_closed = true; error.call(controller, ...args); }; return { controller, textEncoder: encoder, stream, sink: { push(chunk) { if (is_closed) { return; } controller.enqueue(encoder.encode(chunk)); }, close() { controller.close(); }, error(reason) { controller.error(reason); }, }, }; } /** @type {null | Component} */ export let active_component = null; /** @type {null | Block} */ export let active_block = null; export let tracking = false; /** @type {null | Dependency} */ let active_dependency = null; let inside_async_track = false; /** @type {ElementContext | undefined} */ let current_element; /** @type {Set<string>} */ let seen_warnings = new Set(); /** * @returns {void} */ export function reset_state() { active_component = null; active_block = null; active_dependency = null; inside_async_track = false; tracking = false; seen_warnings = new Set(); current_element = undefined; } /** @type {number} */ let clock = 0; /** * @returns {number} */ function increment_clock() { return ++clock; } /** * @param {Block} block */ export function set_active_block(block) { active_block = block; } /** * @param {Tracked | Derived} tracked * @returns {Dependency} */ function create_dependency(tracked) { return { c: tracked.c, t: tracked, n: null, }; } /** * @param {Tracked | Derived} tracked */ function register_dependency(tracked) { var dependency = active_dependency; if (dependency === null) { dependency = create_dependency(tracked); active_dependency = dependency; } else { var current = dependency; while (current !== null) { if (current.t === tracked) { current.c = tracked.c; return; } var next = current.n; if (next === null) { break; } current = next; } dependency = create_dependency(tracked); current.n = dependency; } } /** * @param {Dependency | null} tracking */ function is_tracking_dirty(tracking) { if (tracking === null) { return false; } while (tracking !== null) { var tracked = tracking.t; if ((tracked.f & DERIVED) !== 0) { update_derived(/** @type {Derived} **/ (tracked)); } if (tracked.c > tracking.c) { return true; } tracking = tracking.n; } return false; } /** * @template T * @param {() => T} fn * @returns {T} */ export function untrack(fn) { var previous_tracking = tracking; var previous_dependency = active_dependency; tracking = false; active_dependency = null; try { return fn(); } finally { tracking = previous_tracking; active_dependency = previous_dependency; } } /** * @param {Derived} computed */ function update_derived(computed) { var value = computed.v; if (value === UNINITIALIZED || is_tracking_dirty(computed.d)) { value = run_derived(computed); if (value !== computed.v) { computed.v = value; computed.c = increment_clock(); } } } /** * @param {Tracked} computed * @param {any} value */ function update_tracked_value_clock(computed, value) { computed.v = value; computed.c = increment_clock(); } /** * @param {Derived} computed */ function run_derived(computed) { var previous_tracking = tracking; var previous_dependency = active_dependency; var previous_component = active_component; try { tracking = true; active_dependency = null; active_component = computed.co; var value = computed.fn(); computed.d = active_dependency; return value; } catch (error) { computed.d = active_dependency; if (error === ASYNC_DERIVED_READ_THROWN) { // Check if any dependency is rejected — if so, propagate rejection var dep = active_dependency; while (dep !== null) { if (dep.t.v === SUSPENSE_REJECTED) { return SUSPENSE_REJECTED; } dep = dep.n; } return SUSPENSE_PENDING; } throw error; } finally { tracking = previous_tracking; active_dependency = previous_dependency; active_component = previous_component; } } /** * `<div translate={false}>` should be rendered as `<div translate="no">` and _not_ * `<div translate="false">`, which is equivalent to `<div translate="yes">`. There * may be other odd cases that need to be added to this list in future * @type {Record<string, Map<any, string>>} */ const replacements = { translate: new Map([ [true, 'yes'], [false, 'no'], ]), }; export class Output { /** @type {Output} */ #root; /** @type {NestedArray<string>} */ #head = []; /** @type {NestedArray<string>} */ #body = []; /** @type {Set<string>} */ #css = new Set(); /** @type {null | Output} */ #parent = null; /** @type {StreamSink | null} */ #streamOutput = null; #stream_started = false; #stream_finished = false; /** @type {null | number} */ #pending_count = null; /** @type {null | Promise<void>} */ #promise = null; /** @type {null | (() => void)} */ #promise_resolve = null; /** @type {null | ((reason?: any) => void)} */ #promise_reject = null; #is_root = false; #sync_run = false; /** @type {Set<RegisteredAsyncOperation>} */ #async_operations = new Set(); /** @type {null | 'head'} */ target = null; get root() { return this.#root; } get body() { return this.#body; } get head() { return this.#head; } get css() { return this.#css; } get promise() { if (this.#is_root) { return /** @type {Promise<void>} */ (this.#promise); } throw new Error('getPromise() can only be called on the root Output'); } /** * @param {Output | null} parent */ constructor(parent) { if (!parent) { this.#root = this; this.#is_root = true; this.#promise = new Promise((resolve, reject) => { this.#promise_resolve = resolve; this.#promise_reject = reject; }); this.#pending_count = 1; this.#sync_run = true; } else { this.#root = parent.root; this.#parent = parent; this.#parent.body.push(this.body); this.#parent.head.push(this.head); } } /** * @param {string} str * @param {boolean} [is_root=false] * @param {boolean} [is_prepend=false] * @returns {void} */ #push(str, is_root = false, is_prepend = false) { if (this.isStreamMode() && !this.isSyncRun()) { // TODO - we need to wrap the resulting block output into something that // the client-side can understand and append them appropriately, // or actually, first append and hydrate when the full block is finished // without waiting for the all blocks to finish streaming to make hydration faster /** @type {StreamSink} */ (this.#root.#streamOutput).push(str); return; } var instance = is_root ? this.#root : this; // we never write to `head` in the root instance if (instance !== this.#root && instance.target === 'head') { if (is_prepend) { instance.#head.unshift(str); } else { instance.#head.push(str); } return; } if (is_prepend) { instance.#body.unshift(str); } else { instance.#body.push(str); } } /** * @param {string} str * @returns {void} */ push(str) { this.#push(str); } /** * @param {string} str * @returns {void} */ push_serialized_error(str) { // prepend to the root block to avoid messing up the hydration markers // writing to the root to avoid being cleared in the local instance when an error occurs this.#push(str, true, true); } /** * @param {string} str * @returns {void} */ push_serialized_result(str) { this.#push(str); } clear() { this.#head.length = 0; this.#body.length = 0; this.#css.clear(); } /** * @param {string} hash * @returns {void} */ register_css(hash) { if (this.isStreamMode() && !this.isSyncRun()) { // TODO - when we're in the streaming mode and finished the sync render, // We should wrap the css into something that the client-side can understand // and append them into the head immediately return; } this.#css.add(hash); } /** * @param {RegisteredAsyncOperation} operation * @return {void} */ registerAsync(operation) { this.#async_operations.add(operation); this.#root._incrementPending(); } /** * @param {RegisteredAsyncOperation} operation * @returns {void} */ resolveAsync(operation) { this.#async_operations.delete(operation); this.#root._decrementPending(); } cancelAsyncOperations() { for (const operation of this.#async_operations) { operation.cancel(); this.#async_operations.delete(operation); this.clear(); this.#root._decrementPending(); } } _incrementPending() { if (this.#is_root) { /** @type {number} */ (this.#pending_count)++; return; } throw new Error('_incrementPending() is an internal method.'); } _decrementPending() { if (this.#is_root) { /** @type {number} */ (this.#pending_count)--; if (this.#pending_count === 0) { this.#promise_resolve?.(); } return; } throw new Error('_decrementPending() is an internal method.'); } _finishSyncRun() { if (this.#is_root) { this.#sync_run = false; return; } throw new Error('_finishSyncRun() is an internal method.'); } /** * @param {StreamSink} stream */ _setStream(stream) { if (this.#is_root) { this.#streamOutput = stream; return; } throw new Error('_setStream() is an internal method.'); } _startStream() { if (this.#is_root) { this.#stream_started = true; return; } throw new Error('_startStream() is an internal method.'); } _closeStream() { if (this.#is_root) { if (this.#streamOutput && this.#stream_started && !this.#stream_finished) { this.#stream_finished = true; this.#streamOutput.close(); } return; } throw new Error('_closeStream() is an internal method.'); } /** * @param {unknown} reason * @returns {void} */ _errorStream(reason) { if (this.#is_root) { if (this.#streamOutput && this.#stream_started && !this.#stream_finished) { this.#stream_finished = true; this.#streamOutput.error(reason); } return; } throw new Error('_errorStream() is an internal method.'); } isStreamMode() { return this.#root.#streamOutput !== null; } isSyncRun() { return this.#root.#sync_run; } branch() { return new Output(this); } } /** * @param {RenderComponent} component * @param {BaseRenderOptions} [passed_in_options] * @returns {Promise<RenderResult | RenderStreamResult>} */ export async function render(component, passed_in_options = {}) { /** @type {BaseRenderOptions} */ var options = { ...(passed_in_options.stream ? { closeStream: true } : {}), ...passed_in_options, }; /** @type {Error | null } */ var top_level_error = null; var head = ''; var body = ''; /** @type {Set<string>} */ var css = new Set(); /** @type {Block | null} */ var root_block = null; // Reset dev-mode element tracking state at the start of each render reset_state(); try_block( // since there is no `active_block` yet, the usual automatic block run will be skipped () => { // this will run only once and immediately when we call the `try_block` root_block = /** @type {Block} */ (active_block); const output = root_block.o; if (options.stream) { output._setStream(options.stream); } component({}); output._decrementPending(); output._finishSyncRun(); if (output.isStreamMode()) { sync_buffers_to_string(output); output._startStream(); output.push(head); output.push(body); // TODO - how do we handle css?, in needs to be inside the head // We probably can allocate a buffer inside the head for this // We should have the same order of insertion as for the full async render } }, (error) => { // TODO - allow a global error template in ripple.config.ts // We're not going to send the error in the stream stream.error() // as we should send sent the error template // store the error to be returned top_level_error = error; console.error(error); }, () => { // TODO - allow a global pending in ripple.config.ts // pending would be implemented as part of the streaming rendering support }, ); await /** @type {Block} */ (/** @type {unknown} */ (root_block)).o.promise; reset_state(); const output = /** @type {Block} */ (/** @type {unknown} */ (root_block)).o; if (output.isStreamMode() && options.closeStream) { output._closeStream(); } if (!output.isStreamMode()) { sync_buffers_to_string(output); } return options.stream ? { stream: options.stream, topLevelError: top_level_error } : { head, body, css, topLevelError: top_level_error }; /** * @param {Output} output * @returns {void} */ function sync_buffers_to_string(output) { head = /** @type {string[]} */ (output.head).flat(Infinity).join(''); body = BLOCK_OPEN + /** @type {string[]} */ (output.body).flat(Infinity).join('') + BLOCK_CLOSE; css = output.css; } } /** * @returns {void} */ export function push_component() { active_component = { c: null, p: active_component, }; active_block = component_block(() => {}); } /** * @returns {void} */ export function pop_component() { active_component = /** @type {Component} */ (active_component).p; active_block = /** @type {Block} */ (active_block).p; } /** * @param {string} str * @returns {void} */ export function output_push(str) { /** @type {Block} */ (active_block).o.push(str); } /** * @param {string} str * @returns {void} */ export function output_push_serialized_error(str) { /** @type {Block} */ (active_block).o.push_serialized_error(str); } /** * @param {Output['target']} target */ export function set_output_target(target) { /** @type {Block} */ (active_block).o.target = target; } /** * @param {string} hash * @returns {void} */ export function output_register_css(hash) { /** @type {Block} */ (active_block).o.register_css(hash); } /** * @param {string} message */ function print_nesting_error(message) { message = `node_invalid_placement_ssr: ${message}\n\n` + 'This can cause content to shift around as the browser repairs the HTML, and will likely result in a hydration mismatch.'; if (seen_warnings.has(message)) return; seen_warnings.add(message); // eslint-disable-next-line no-console console.error(message); } /** * Pushes an element onto the element stack and validates its nesting. * Used during DEV mode SSR to detect invalid HTML nesting that would cause * the browser to repair the HTML, breaking hydration. * @param {string} tag * @param {string} filename * @param {number} line * @param {number} column * @returns {void} */ export function push_element(tag, filename, line, column) { var parent = current_element; var element = { tag, parent, filename, line, column }; if (parent !== undefined) { var ancestor = parent.parent; var ancestors = [parent.tag]; const child_loc = filename ? `${filename}:${line}:${column}` : undefined; const parent_loc = parent.filename ? `${parent.filename}:${parent.line}:${parent.column}` : undefined; const message = is_tag_valid_with_parent(tag, parent.tag, child_loc, parent_loc); if (message) print_nesting_error(message); while (ancestor != null) { ancestors.push(ancestor.tag); const ancestor_loc = ancestor.filename ? `${ancestor.filename}:${ancestor.line}:${ancestor.column}` : undefined; const ancestor_message = is_tag_valid_with_ancestor(tag, ancestors, child_loc, ancestor_loc); if (ancestor_message) print_nesting_error(ancestor_message); ancestor = ancestor.parent; } } current_element = element; } /** * Pops the current element from the element stack. * @returns {void} */ export function pop_element() { if (current_element !== undefined) { current_element = current_element.parent; } } /** * @param {any} tracked * @returns {any} */ export function get(tracked) { if (!is_ripple_object(tracked)) { return tracked; } if ((tracked.f & DERIVED) !== 0) { update_derived(/** @type {Derived} **/ (tracked)); if (tracking) { register_dependency(tracked); } } else if (tracking) { register_dependency(tracked); } if (tracked.v === SUSPENSE_PENDING || tracked.v === SUSPENSE_REJECTED) { var is_try_block = false; if ( !inside_async_track && (!active_block || active_block.f & COMPONENT_BLOCK || (is_try_block = (active_block.f & TRY_BLOCK) !== 0)) ) { throw new Error( `Reads on pending tracked or derived values directly inside ${is_try_block ? 'try' : 'component'} body are prohibited. Use trackPending() test for safe access or create another derived instead.`, ); } // this will be caught by the run_block and the block will be re-run // once the async tracked dependency's promise resolves throw ASYNC_DERIVED_READ_THROWN; } var g = tracked.a.get; return g ? g(tracked.v) : tracked.v; } /** * @param {Derived | Tracked} tracked * @param {any} value */ export function set(tracked, value) { var old_value = tracked.v; if (value !== old_value) { var s = tracked.a.set; tracked.v = s ? s(value, tracked.v) : value; tracked.c = increment_clock(); } } /** * @param {Tracked} tracked * @param {number} [d] * @returns {number} */ export function update(tracked, d = 1) { var value = get(tracked); var result = d === 1 ? value++ : value--; set(tracked, value); return result; } /** * @param {Tracked} tracked * @param {number} [d] * @returns {number} */ export function update_pre(tracked, d = 1) { var value = get(tracked); var new_value = d === 1 ? ++value : --value; set(tracked, new_value); return new_value; } /** * @param {any} obj * @param {string | number | symbol} property * @param {any} value * @returns {void} */ export function set_property(obj, property, value) { var tracked = obj[property]; set(tracked, value); } /** * @param {any} obj * @param {string | number | symbol} property * @param {boolean} [chain=false] * @returns {any} */ export function get_property(obj, property, chain = false) { if (chain && obj == null) { return undefined; } var tracked = obj[property]; if (tracked == null) { return tracked; } return get(tracked); } /** * @param {any} obj * @param {string | number | symbol} property * @param {number} [d=1] * @returns {number} */ export function update_property(obj, property, d = 1) { var tracked = obj[property]; var value = get(tracked); var new_value = d === 1 ? value++ : value--; set(tracked, value); return new_value; } /** * @param {any} obj * @param {string | number | symbol} property * @param {number} [d=1] * @returns {number} */ export function update_pre_property(obj, property, d = 1) { var tracked = obj[property]; var value = get(tracked); var new_value = d === 1 ? ++value : --value; set(tracked, new_value); return new_value; } /** * @template V * @param {string} name * @param {V} value * @param {boolean} [is_boolean] * @returns {string} */ export function attr(name, value, is_boolean = false) { if (name === 'hidden' && value !== 'until-found') { is_boolean = true; } if (value == null || (!value && is_boolean)) return ''; const normalized = (name in replacements && replacements[name].get(value)) || value; let value_to_escape = name === 'class' ? clsx(normalized) : normalized; value_to_escape = name === 'style' ? typeof value !== 'string' ? get_styles(value) : String(normalized).trim() : value_to_escape; const assignment = is_boolean ? '' : `="${escape(value_to_escape, true)}"`; return ` ${name}${assignment}`; } /** * @param {Record<string, string | number>} styles * @returns {string} */ function get_styles(styles) { var result = ''; for (const key in styles) { const css_prop = normalize_css_property_name(key); const value = String(styles[key]).trim(); result += `${css_prop}: ${value}; `; } return result.trim(); } /** * @param {Record<string, any>} attrs * @param {string | undefined} css_hash * @returns {string} */ export function spread_attrs(attrs, css_hash) { let attr_str = ''; let name; for (name in attrs) { var value = attrs[name]; if (name === 'children' || typeof value === 'function' || is_tsrx_element(value)) continue; if (is_ripple_object(value)) { value = get(value); } if (name === 'class' && css_hash) { value = value == null || value === css_hash ? css_hash : [value, css_hash]; } attr_str += attr(name, value, is_boolean_attribute(name)); } return attr_str; } var empty_get_set = { get: undefined, set: undefined }; class TrackedValue { /** * @param {any} v * @param {{ get?: Function; set?: Function }} a * @param {string} hash */ constructor(v, a, hash) { /** @type {{ get?: Function; set?: Function }} */ this.a = a; /** @type {AbortController | null} */ this.aa = null; /** @type {PromiseLike<any> | null} */ this.ap = null; /** @type {Block} */ this.b = /** @type {Block} */ (active_block); /** @type {number} */ this.c = 0; /** @type {number} */ this.f = TRACKED; /** @type {string} */ this.h = hash; /** @type {any} */ this.v = v; } /** @returns {any} */ get [0]() { return get(/** @type {Tracked} */ (this)); } /** @param {any} v */ set [0](v) { set(/** @type {Tracked} */ (this), v); } /** @returns {Tracked} */ get [1]() { return /** @type {Tracked} */ (this); } /** @returns {any} */ get value() { return get(/** @type {Tracked} */ (this)); } /** @param {any} v */ set value(v) { set(/** @type {Tracked} */ (this), v); } /** @returns {2} */ get length() { return 2; } /** @returns {Iterator<any | Tracked>} */ *[Symbol.iterator]() { yield get(/** @type {Tracked} */ (this)); yield this; } } class DerivedValue { /** * @param {Function} fn * @param {{ get?: Function; set?: Function }} a * @param {string} hash */ constructor(fn, a, hash) { /** @type {{ get?: Function; set?: Function }} */ this.a = a; // we always should have an active block /** @type {Block} */ this.b = /** @type {Block} */ (active_block); /** @type {number} */ this.c = 0; /** @type {Component | null} */ this.co = active_component; /** @type {Dependency | null} */ this.d = null; /** @type {number} */ this.f = DERIVED; /** @type {Function} */ this.fn = fn; /** @type {string} */ this.h = hash; /** @type {any} */ this.v = UNINITIALIZED; } /** @returns {any} */ get [0]() { return get(/** @type {Derived} */ (this)); } /** @param {any} v */ set [0](v) { set(/** @type {Derived} */ (this), v); } /** @returns {Derived} */ get [1]() { return /** @type {Derived} */ (this); } /** @returns {any} */ get value() { return get(/** @type {Derived} */ (this)); } /** @param {any} v */ set value(v) { set(/** @type {Derived} */ (this), v); } /** @returns {2} */ get length() { return 2; } /** @returns {Iterator<any | Derived>} */ *[Symbol.iterator]() { yield get(/** @type {Derived} */ (this)); yield this; } } /** * @param {any} v * @param {string} hash * @param {(value: any) => any} [get] * @param {(next: any, prev: any) => any} [set] * @returns {Tracked} */ function tracked(v, hash, get, set) { return /** @type {Tracked} */ ( new TrackedValue(v, get || set ? { get, set } : empty_get_set, hash) ); } /** * @param {Record<string, unknown>} obj * @param {string[]} exclude_keys * @returns {Record<string, unknown>} */ export function exclude_from_object(obj, exclude_keys) { /** @type {Record<string, unknown>} */ var new_obj = {}; for (const key of Object.keys(obj)) { if (!exclude_keys.includes(key)) { new_obj[key] = obj[key]; } } return new_obj; } /** * @param {any} v * @param {string} hash * @param {(value: any) => any} [get] * @param {(next: any, prev: any) => any} [set] * @returns {Derived} */ function derived(v, hash, get, set) { return /** @type {Derived} */ ( new DerivedValue(v, get || set ? { get, set } : empty_get_set, hash) ); } /** * @param {any} v * @param {string} hash * @param {(value: any) => any} [get] * @param {(next: any, prev: any) => any} [set] * @returns {Tracked | Derived} */ export function track(v, hash, get, set) { var is_tracked = is_ripple_object(v); if (is_tracked) { return v; } if (typeof v === 'function') { return derived(v, hash, get, set); } return tracked(v, hash, get, set); } /** * Serializes a resolved trackAsync result as a script tag for hydration. * @param {OutputInterface} output - The output push function captured at call time * @param {string} hash - The unique hash for this trackAsync call * @param {any} value - The resolved value * @param {string[] | null} [deps] - Hashes of direct reactive dependencies read by fn() * @returns {void} */ function serialize_track_async_result(output, hash, value, deps) { /** @type {{ ok: true, payload: string, deps?: string[] }} */ var envelope = { ok: true, payload: devalue.stringify(value) }; if (deps && deps.length > 0) { envelope.deps = deps; } push_script_for_hydration((str) => output.push_serialized_result(str), hash, envelope); } /** * Serializes a rejected trackAsync error as a script tag for hydration. * Must be called after route_error_to_catch_block so active_block is the catch block. * @param {string} hash * @param {any} error * @returns {void} */ export function serialize_track_async_error(hash, error) { var error_message = get_public_track_async_error_message(error); // we can just use the output_push_serialized directly so it's added to the root block // if we here then the try's block failed to render and the output was cleared // so we're writing to the root otherwise it will be cleared in the local output push_script_for_hydration(output_push_serialized_error, hash, { ok: false, error: { message: error_message }, }); } /** * @param {string} hash * @param {any} error * @returns {void} */ export function route_track_async_error_to_catch_block(hash, error) { route_track_async_error_to_catch_block_with_boundary( get_closest_catch_block(/** @type {Block} */ (active_block)), hash, error, ); } /** * @param {any} error * @returns {any} */ export function create_public_track_async_error(error) { if (DEV) { return error; } return new Error(get_public_track_async_error_message(error)); } /** * We avoid leaking arbitrary server errors in production while still keeping * rich error messages in development and tests. * @param {any} error * @returns {string} */ function get_public_track_async_error_message(error) { if (DEV) { return error?.message ?? String(error); } return 'An error occurred during async rendering'; } /** * Routes trackAsync errors to a catch boundary and serializes the same * public error for hydration, preventing SSR/hydration message mismatches. * @param {TryBlockWithCatch} catch_block * @param {string} hash * @param {any} error * @returns {void} */ function route_track_async_error_to_catch_block_with_boundary(catch_block, hash, error) { var public_error = create_public_track_async_error(error); route_error_to_catch_block(catch_block, public_error); // has to run after routing as it sets the active_block to the catch block serialize_track_async_error(hash, public_error); } /** * @param {(str: string) => void} push_fn * @param {string} hash * @param {object} envelope - The envelope containing the serialized data * @envelope {ok: boolean, payload?: any, error?: { message: string } } * @returns {void} */ function push_script_for_hydration(push_fn, hash, envelope) { var serialized_envelope = escape_script(JSON.stringify(envelope)); push_fn( '<script id="' + get_track_async_script_id(hash) + '" type="application/json">' + serialized_envelope + '</script>', ); } /** * Runs the async tracked function, handling sync results, async results, * and chained cases where fn() reads a pending dependency. * @param {Tracked} t * @param {() => any} fn * @param {Block} block * @param {((value?: any) => void) | null} dr * @param {((reason?: any) => void) | null} dj */ function run_track_async(t, fn, block, dr, dj) { var previous_tracking = tracking; var previous_dependency = active_dependency; var previous_inside = inside_async_track; tracking = true; active_dependency = null; inside_async_track = true; var result; /** @type {Dependency | null} */ var caught_dep = null; /** @type {Dependency | null} */ var direct_deps = null; var caught = false; try { result = fn(); direct_deps = active_dependency; } catch (error) { caught_dep = active_dependency; caught = true; if (error !== ASYNC_DERIVED_READ_THROWN) { throw new TrackAsyncRunError('Error thrown during trackAsync execution', { cause: /** @type {Error} */ (error), tracked: t, }); } } finally { tracking = previous_tracking; active_dependency = previous_dependency; inside_async_track = previous_inside; } if (caught) { // Chained case: fn() read a pending tracked/derived dependency // Check if any dependency is rejected var dep = /** @type {Dependency | null} */ (caught_dep); while (dep !== null) { if (dep.t.v === SUSPENSE_REJECTED) { update_tracked_value_clock(t, SUSPENSE_REJECTED); if (dj) { dj(new Error('Upstream dependency rejected')); } return; } dep = dep.n; } // Create synthetic promise if first time (for downstream chaining) if (!dr) { t.ap = new Promise((resolve, reject) => { dr = resolve; dj = reject; }); } // Find the pending dependency with a promise and chain on it dep = /** @type {Dependency | null} */ (caught_dep); while (dep !== null) { var dep_tracked = /** @type {Tracked} */ (dep.t); if ((dep_tracked.f & TRACKED) !== 0 && dep_tracked.v === SUSPENSE_PENDING && dep_tracked.ap) { /** @type {PromiseLike<any>} */ (dep_tracked.ap).then( () => run_track_async(t, fn, block, dr, dj), (error) => { update_tracked_value_clock(t, SUSPENSE_REJECTED); if (dj) { dj(error); } route_track_async_error_to_catch_block_with_boundary( get_closest_catch_block(block), t.h, error, ); }, ); return; } dep = dep.n; } return; } var dep_hashes = collect_dep_hashes(direct_deps); // Handle the result var async_result = get_async_track_result(result); if (async_result === null) { // Sync result update_tracked_value_clock(t, result); serialize_track_async_result(t.b.o, t.h, result, dep_hashes); if (dr) { dr(result); } return; } t.aa = async_result.abort_controller; if (!dr) { // First run, no chaining — set real promise directly t.ap = async_result.promise; } async_result.promise.then( (resolved) => { update_tracked_value_clock(t, resolved); serialize_track_async_result(t.b.o, t.h, resolved, dep_hashes); if (dr) { dr(resolved); } }, (error) => { update_tracked_value_clock(t, SUSPENSE_REJECTED); if (dj) { dj(error); } route_track_async_error_to_catch_block_with_boundary( get_closest_catch_block(block), t.h, error, ); }, ); } /** * Walks a dependency chain and collects the hashes of dependencies that have * one (i.e. were created from a compile-time track/trackAsync call). * @param {Dependency | null} head * @returns {string[] | null} */ function collect_dep_hashes(head) { /** @type {string[] | null} */ var hashes = null; var dep = head; while (dep !== null) { var h = /** @type {{ h?: string }} */ (dep.t).h; if (h !== undefined) { if (hashes === null) hashes = []; hashes.push(h); } dep = dep.n; } return hashes; } /** * @param {any} v * @param {string} hash - Unique hash for SSR serialization/hydration * @returns {Tracked | void} */ export function track_async(v, hash) { if (is_ripple_object(v)) { return v; } if (typeof v !== 'function') { throw new TypeError( 'trackAsync() only accepts function arguments that return a promise or an object with a promise property', ); } var t = tracked(SUSPENSE_PENDING, hash); run_track_async(t, v, t.b, null, null); return t; } /** * @param {(Derived | Tracked) | (() => any)} t * @returns {boolean} */ export function is_tracked_pending(t) { try { if (typeof t === 'function') { t(); } else { get(t); } return false; } catch (error) { if (error === ASYNC_DERIVED_READ_THROWN) { return true; } throw error; } } /** * @param {Tracked | Derived} tracked * @return {any} */ export function peek_tracked(tracked) { if (!is_ripple_object(tracked)) { return tracked; } return tracked.v; } /** * Routes an error to the nearest catch boundary: clears output, cancels * pending async work, and invokes the catch handler if one exists. * @param {TryBlockWithCatch} catch_block * @param {any} error */ function route_error_to_catch_block(catch_block, error) { // cancel async should also clear the output // for this block and all its children cancel_async_operations(catch_block); reset_state(); set_active_block(catch_block); catch_block.s.c(error); } /** * @param {Block} block * @returns {void} */ function register_block_rerun(block) { // Find the pending dependency with a promise in the dependency chain. var dep_entry = active_dependency; // tracked async must exist as otherwise we wouldn't have thrown the ASYNC_DERIVED_READ_THROWN /** @type {Tracked | null} */ var t = null; while (dep_entry !== null) { var d = /** @type {Tracked} */ (dep_entry.t); if ((d.f & TRACKED) !== 0 && d.v === SUSPENSE_PENDING && d.ap) { t = d; break; } dep_entry = dep_entry.n; } var cancelled = false; var try_catch_block = get_closest_catch_block(block); var operation = { cancel: () => { cancelled = true; if (t && t.aa) { t.aa.abort(TRACKED_UPDATED); t.aa = null; t.ap = null; } }, }; try_catch_block.o.registerAsync(operation); /** @type {PromiseLike<any>} */ (/** @type {Tracked} */ (t).ap).then( () => { if (cancelled) { return; } reset_state(); try { run_block(block); try_catch_block.o.resolveAsync(operation); } catch (error) { if (error instanceof TrackAsyncRunError) { var { cause, tracked: { h: hash }, } = /** @type {InstanceType<typeof TrackAsyncRunError>} */ (error); error = cause; route_track_async_error_to_catch_block_with_boundary(try_catch_block, hash, error); } else { route_error_to_catch_block(try_catch_block, error); } } }, (error) => { if (cancelled) { return; } route_track_async_error_to_catch_block_with_boundary( try_catch_block, /** @type {Tracked} */ (t).h, error, ); }, ); // clear all output buffers as we'll rerun the block rendering block.o.clear(); } /** * @param {Block} block */ export function run_block(block) { var previous_block = active_block; var previous_component = active_component; var previous_tracking = tracking; var previous_dependency = active_dependency; var previous_element = current_element; try { active_block = block; active_component = block.co; tracking = true; active_dependency = null; block.fn(block.o); } catch (error) { var output = block.o; if (error === ASYNC_DERIVED_READ_THROWN) { // regardless of the render mode (stream, etc.) // we need to rerun the block when the dependency's promise resolves register_block_rerun(block); if (output.isStreamMode() && output.isSyncRun()) { // rethrowing so that the pending block catches it // we should only render fallback/pending in the streaming mode // when in the synchronous phase throw error; } } else { // always re-throw real errors // during sync, try_block's catch handles it; // during async, the register_block_rerun() try/catch handles it throw error; } } finally { active_block = previous_block; active_component = previous_component; tracking = previous_tracking; active_dependency = previous_dependency; current_element = previous_element; } } /** * @param {any} _ * @param {ConstructorParameters<typeof URL>} params * @returns {URL} */ export function ripple_url(_, ...params) { return new URL(...params); } /** * @param {any} _ * @param {ConstructorParameters<typeof URLSearchParams>} params * @returns {URLSearchParams} */ export function ripple_url_search_params(_, ...params) { return new URLSearchParams(...params); } /** * @param {ConstructorParameters<typeof Date>} params * @returns {Date} */ export function ripple_date(...params) { return new Date(...params); } /** * @param {string} query * @param {boolean} [matches] * @returns {boolean} */ export function media_query(query, matches = false) { void query; return matches; } /** * @param {() => void} _fn * @returns {void} */ export function effect(_fn) { return; } /** * @template T * @param {...T} elements * @returns {T[]} */ export function ripple_array(...elements) { return new Array(...elements); } /** * @template T * @param {ArrayLike<T> | Iterable<T>} arrayLike * @param {(v: T, k: number) => any | undefined} [map_fn] * @param {any} [thisArg] * @returns {T[]} */ ripple_array.from = function (arrayLike, map_fn, thisArg) { return map_fn ? Array.from(arrayLike, map_fn, thisArg) : Array.from(arrayLike); }; /** * @template T * @param {...T} items * @returns {T[]} */ ripple_array.of = function (...items) { return Array.of(...items); }; /** * @template T * @param {ArrayLike<T> | Iterable<T>} arrayLike * @param {(v: T, k: number) => any | undefined} [map_fn] * @param {any} [thisArg] * @returns {Promise<T[]>} */ ripple_array.from_async = async function (arrayLike, map_fn, thisArg) { return map_fn ? Array.fromAsync(arrayLike, map_fn, thisArg) : Array.fromAsync(arrayLike); }; /** * @param {object} obj * @returns {object} */ export function ripple_object(obj) { return obj; } /** * @template K, V * @param {Iterable<readonly [K, V]>} [iterable] * @returns {Map<K, V>} */ export function ripple_map(iterable) { return new Map(iterable); } /** * Returns the fallback value if the given value is undefined. * @template T * @param {T | undefined} value * @param {T} fallback * @returns {T} */ export function fallback(value, fallback) { return value === undefined ? fallback : value; }