UNPKG

@tinymce/tinymce-svelte

Version:

TinyMCE Svelte Component

1,880 lines (1,542 loc) 135 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Editor = factory()); })(this, (function () { 'use strict'; // generated during release, do not modify const PUBLIC_VERSION = '5'; if (typeof window !== 'undefined') { // @ts-expect-error ((window.__svelte ??= {}).v ??= new Set()).add(PUBLIC_VERSION); } const PROPS_IS_IMMUTABLE = 1; const PROPS_IS_UPDATED = 1 << 2; const PROPS_IS_BINDABLE = 1 << 3; const PROPS_IS_LAZY_INITIAL = 1 << 4; const TEMPLATE_FRAGMENT = 1; const TEMPLATE_USE_IMPORT_NODE = 1 << 1; const UNINITIALIZED = Symbol(); // Dev-time component properties const FILENAME = Symbol('filename'); const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml'; const node_env = globalThis.process?.env?.NODE_ENV; var DEV = node_env && !node_env.toLowerCase().startsWith('prod'); // Store the references to globals in case someone tries to monkey patch these, causing the below // to de-opt (this occurs often when using popular extensions). var is_array = Array.isArray; var index_of = Array.prototype.indexOf; var includes = Array.prototype.includes; var define_property = Object.defineProperty; var get_descriptor = Object.getOwnPropertyDescriptor; var get_descriptors = Object.getOwnPropertyDescriptors; var object_prototype = Object.prototype; var array_prototype = Array.prototype; var get_prototype_of = Object.getPrototypeOf; const noop = () => {}; /** @param {Array<() => void>} arr */ function run_all(arr) { for (var i = 0; i < arr.length; i++) { arr[i](); } } /** * TODO replace with Promise.withResolvers once supported widely enough * @template [T=void] */ function deferred() { /** @type {(value: T) => void} */ var resolve; /** @type {(reason: any) => void} */ var reject; /** @type {Promise<T>} */ var promise = new Promise((res, rej) => { resolve = res; reject = rej; }); // @ts-expect-error return { promise, resolve, reject }; } // General flags const DERIVED = 1 << 1; const EFFECT = 1 << 2; const RENDER_EFFECT = 1 << 3; /** * An effect that does not destroy its child effects when it reruns. * Runs as part of render effects, i.e. not eagerly as part of tree traversal or effect flushing. */ const MANAGED_EFFECT = 1 << 24; /** * An effect that does not destroy its child effects when it reruns (like MANAGED_EFFECT). * Runs eagerly as part of tree traversal or effect flushing. */ const BLOCK_EFFECT = 1 << 4; const BRANCH_EFFECT = 1 << 5; const ROOT_EFFECT = 1 << 6; const BOUNDARY_EFFECT = 1 << 7; /** * Indicates that a reaction is connected to an effect root — either it is an effect, * or it is a derived that is depended on by at least one effect. If a derived has * no dependents, we can disconnect it from the graph, allowing it to either be * GC'd or reconnected later if an effect comes to depend on it again */ const CONNECTED = 1 << 9; const CLEAN = 1 << 10; const DIRTY = 1 << 11; const MAYBE_DIRTY = 1 << 12; const INERT = 1 << 13; const DESTROYED = 1 << 14; /** Set once a reaction has run for the first time */ const REACTION_RAN = 1 << 15; // Flags exclusive to effects /** * 'Transparent' effects do not create a transition boundary. * This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned */ const EFFECT_TRANSPARENT = 1 << 16; const EAGER_EFFECT = 1 << 17; const HEAD_EFFECT = 1 << 18; const EFFECT_PRESERVED = 1 << 19; const USER_EFFECT = 1 << 20; // Flags exclusive to deriveds /** * Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase. * Will be lifted during execution of the derived and during checking its dirty state (both are necessary * because a derived might be checked but not executed). */ const WAS_MARKED = 1 << 16; // Flags used for async const REACTION_IS_UPDATING = 1 << 21; const ASYNC = 1 << 22; const ERROR_VALUE = 1 << 23; const STATE_SYMBOL = Symbol('$state'); const LEGACY_PROPS = Symbol('legacy props'); const LOADING_ATTR_SYMBOL = Symbol(''); const PROXY_PATH_SYMBOL = Symbol('proxy path'); /** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */ const STALE_REACTION = new (class StaleReactionError extends Error { name = 'StaleReactionError'; message = 'The reaction that called `getAbortSignal()` was re-run or destroyed'; })(); /* This file is generated by scripts/process-messages/index.js. Do not edit! */ /** * Cannot create a `$derived(...)` with an `await` expression outside of an effect tree * @returns {never} */ function async_derived_orphan() { if (DEV) { const error = new Error(`async_derived_orphan\nCannot create a \`$derived(...)\` with an \`await\` expression outside of an effect tree\nhttps://svelte.dev/e/async_derived_orphan`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/async_derived_orphan`); } } /** * A derived value cannot reference itself recursively * @returns {never} */ function derived_references_self() { if (DEV) { const error = new Error(`derived_references_self\nA derived value cannot reference itself recursively\nhttps://svelte.dev/e/derived_references_self`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/derived_references_self`); } } /** * `%rune%` cannot be used inside an effect cleanup function * @param {string} rune * @returns {never} */ function effect_in_teardown(rune) { if (DEV) { const error = new Error(`effect_in_teardown\n\`${rune}\` cannot be used inside an effect cleanup function\nhttps://svelte.dev/e/effect_in_teardown`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/effect_in_teardown`); } } /** * Effect cannot be created inside a `$derived` value that was not itself created inside an effect * @returns {never} */ function effect_in_unowned_derived() { if (DEV) { const error = new Error(`effect_in_unowned_derived\nEffect cannot be created inside a \`$derived\` value that was not itself created inside an effect\nhttps://svelte.dev/e/effect_in_unowned_derived`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/effect_in_unowned_derived`); } } /** * `%rune%` can only be used inside an effect (e.g. during component initialisation) * @param {string} rune * @returns {never} */ function effect_orphan(rune) { if (DEV) { const error = new Error(`effect_orphan\n\`${rune}\` can only be used inside an effect (e.g. during component initialisation)\nhttps://svelte.dev/e/effect_orphan`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/effect_orphan`); } } /** * Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state * @returns {never} */ function effect_update_depth_exceeded() { if (DEV) { const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state\nhttps://svelte.dev/e/effect_update_depth_exceeded`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/effect_update_depth_exceeded`); } } /** * Cannot do `bind:%key%={undefined}` when `%key%` has a fallback value * @param {string} key * @returns {never} */ function props_invalid_value(key) { if (DEV) { const error = new Error(`props_invalid_value\nCannot do \`bind:${key}={undefined}\` when \`${key}\` has a fallback value\nhttps://svelte.dev/e/props_invalid_value`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/props_invalid_value`); } } /** * Rest element properties of `$props()` such as `%property%` are readonly * @param {string} property * @returns {never} */ function props_rest_readonly(property) { if (DEV) { const error = new Error(`props_rest_readonly\nRest element properties of \`$props()\` such as \`${property}\` are readonly\nhttps://svelte.dev/e/props_rest_readonly`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/props_rest_readonly`); } } /** * The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files * @param {string} rune * @returns {never} */ function rune_outside_svelte(rune) { if (DEV) { const error = new Error(`rune_outside_svelte\nThe \`${rune}\` rune is only available inside \`.svelte\` and \`.svelte.js/ts\` files\nhttps://svelte.dev/e/rune_outside_svelte`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/rune_outside_svelte`); } } /** * Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`. * @returns {never} */ function state_descriptors_fixed() { if (DEV) { const error = new Error(`state_descriptors_fixed\nProperty descriptors defined on \`$state\` objects must contain \`value\` and always be \`enumerable\`, \`configurable\` and \`writable\`.\nhttps://svelte.dev/e/state_descriptors_fixed`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/state_descriptors_fixed`); } } /** * Cannot set prototype of `$state` object * @returns {never} */ function state_prototype_fixed() { if (DEV) { const error = new Error(`state_prototype_fixed\nCannot set prototype of \`$state\` object\nhttps://svelte.dev/e/state_prototype_fixed`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/state_prototype_fixed`); } } /** * Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state` * @returns {never} */ function state_unsafe_mutation() { if (DEV) { const error = new Error(`state_unsafe_mutation\nUpdating state inside \`$derived(...)\`, \`$inspect(...)\` or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`); error.name = 'Svelte error'; throw error; } else { throw new Error(`https://svelte.dev/e/state_unsafe_mutation`); } } /* This file is generated by scripts/process-messages/index.js. Do not edit! */ var bold$1 = 'font-weight: bold'; var normal$1 = 'font-weight: normal'; /** * An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app * @param {string} name * @param {string} location */ function await_waterfall(name, location) { if (DEV) { console.warn(`%c[svelte] await_waterfall\n%cAn async derived, \`${name}\` (${location}) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app\nhttps://svelte.dev/e/await_waterfall`, bold$1, normal$1); } else { console.warn(`https://svelte.dev/e/await_waterfall`); } } /** @import { Equals } from '#client' */ /** @type {Equals} */ function equals(value) { return value === this.v; } /** * @param {unknown} a * @param {unknown} b * @returns {boolean} */ function safe_not_equal(a, b) { return a != a ? b == b : a !== b || (a !== null && typeof a === 'object') || typeof a === 'function'; } /** @type {Equals} */ function safe_equals(value) { return !safe_not_equal(value, this.v); } /** True if experimental.async=true */ let async_mode_flag = false; /** True if we're not certain that we only have Svelte 5 code in the compilation */ let legacy_mode_flag = false; /** True if $inspect.trace is used */ let tracing_mode_flag = false; /* This file is generated by scripts/process-messages/index.js. Do not edit! */ var bold = 'font-weight: bold'; var normal = 'font-weight: normal'; /** * The following properties cannot be cloned with `$state.snapshot` — the return value contains the originals: * * %properties% * @param {string | undefined | null} [properties] */ function state_snapshot_uncloneable(properties) { if (DEV) { console.warn( `%c[svelte] state_snapshot_uncloneable\n%c${properties ? `The following properties cannot be cloned with \`$state.snapshot\` — the return value contains the originals: ${properties}` : 'Value cannot be cloned with `$state.snapshot` — the original value was returned'}\nhttps://svelte.dev/e/state_snapshot_uncloneable`, bold, normal ); } else { console.warn(`https://svelte.dev/e/state_snapshot_uncloneable`); } } /** @import { Snapshot } from './types' */ /** * In dev, we keep track of which properties could not be cloned. In prod * we don't bother, but we keep a dummy array around so that the * signature stays the same * @type {string[]} */ const empty = []; /** * @template T * @param {T} value * @param {boolean} [skip_warning] * @param {boolean} [no_tojson] * @returns {Snapshot<T>} */ function snapshot(value, skip_warning = false, no_tojson = false) { if (DEV && !skip_warning) { /** @type {string[]} */ const paths = []; const copy = clone(value, new Map(), '', paths, null, no_tojson); if (paths.length === 1 && paths[0] === '') { // value could not be cloned state_snapshot_uncloneable(); } else if (paths.length > 0) { // some properties could not be cloned const slice = paths.length > 10 ? paths.slice(0, 7) : paths.slice(0, 10); const excess = paths.length - slice.length; let uncloned = slice.map((path) => `- <value>${path}`).join('\n'); if (excess > 0) uncloned += `\n- ...and ${excess} more`; state_snapshot_uncloneable(uncloned); } return copy; } return clone(value, new Map(), '', empty, null, no_tojson); } /** * @template T * @param {T} value * @param {Map<T, Snapshot<T>>} cloned * @param {string} path * @param {string[]} paths * @param {null | T} [original] The original value, if `value` was produced from a `toJSON` call * @param {boolean} [no_tojson] * @returns {Snapshot<T>} */ function clone(value, cloned, path, paths, original = null, no_tojson = false) { if (typeof value === 'object' && value !== null) { var unwrapped = cloned.get(value); if (unwrapped !== undefined) return unwrapped; if (value instanceof Map) return /** @type {Snapshot<T>} */ (new Map(value)); if (value instanceof Set) return /** @type {Snapshot<T>} */ (new Set(value)); if (is_array(value)) { var copy = /** @type {Snapshot<any>} */ (Array(value.length)); cloned.set(value, copy); if (original !== null) { cloned.set(original, copy); } for (var i = 0; i < value.length; i += 1) { var element = value[i]; if (i in value) { copy[i] = clone(element, cloned, DEV ? `${path}[${i}]` : path, paths, null, no_tojson); } } return copy; } if (get_prototype_of(value) === object_prototype) { /** @type {Snapshot<any>} */ copy = {}; cloned.set(value, copy); if (original !== null) { cloned.set(original, copy); } for (var key of Object.keys(value)) { copy[key] = clone( // @ts-expect-error value[key], cloned, DEV ? `${path}.${key}` : path, paths, null, no_tojson ); } return copy; } if (value instanceof Date) { return /** @type {Snapshot<T>} */ (structuredClone(value)); } if (typeof (/** @type {T & { toJSON?: any } } */ (value).toJSON) === 'function' && !no_tojson) { return clone( /** @type {T & { toJSON(): any } } */ (value).toJSON(), cloned, DEV ? `${path}.toJSON()` : path, paths, // Associate the instance with the toJSON clone value ); } } if (value instanceof EventTarget) { // can't be cloned return /** @type {Snapshot<T>} */ (value); } try { return /** @type {Snapshot<T>} */ (structuredClone(value)); } catch (e) { if (DEV) { paths.push(path); } return /** @type {Snapshot<T>} */ (value); } } /** @import { Derived, Reaction, Value } from '#client' */ /** * @param {Value} source * @param {string} label */ function tag(source, label) { source.label = label; tag_proxy(source.v, label); return source; } /** * @param {unknown} value * @param {string} label */ function tag_proxy(value, label) { // @ts-expect-error value?.[PROXY_PATH_SYMBOL]?.(label); return value; } /** * @param {string} label * @returns {Error & { stack: string } | null} */ function get_error(label) { const error = new Error(); const stack = get_stack(); if (stack.length === 0) { return null; } stack.unshift('\n'); define_property(error, 'stack', { value: stack.join('\n') }); define_property(error, 'name', { value: label }); return /** @type {Error & { stack: string }} */ (error); } /** * @returns {string[]} */ function get_stack() { // @ts-ignore - doesn't exist everywhere const limit = Error.stackTraceLimit; // @ts-ignore - doesn't exist everywhere Error.stackTraceLimit = Infinity; const stack = new Error().stack; // @ts-ignore - doesn't exist everywhere Error.stackTraceLimit = limit; if (!stack) return []; const lines = stack.split('\n'); const new_lines = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const posixified = line.replaceAll('\\', '/'); if (line.trim() === 'Error') { continue; } if (line.includes('validate_each_keys')) { return []; } if (posixified.includes('svelte/src/internal') || posixified.includes('node_modules/.vite')) { continue; } new_lines.push(line); } return new_lines; } /** @import { ComponentContext, DevStackEntry, Effect } from '#client' */ /** @type {ComponentContext | null} */ let component_context = null; /** @param {ComponentContext | null} context */ function set_component_context(context) { component_context = context; } /** @type {DevStackEntry | null} */ let dev_stack = null; /** @param {DevStackEntry | null} stack */ function set_dev_stack(stack) { dev_stack = stack; } /** * The current component function. Different from current component context: * ```html * <!-- App.svelte --> * <Foo> * <Bar /> <!-- context == Foo.svelte, function == App.svelte --> * </Foo> * ``` * @type {ComponentContext['function']} */ let dev_current_component_function = null; /** @param {ComponentContext['function']} fn */ function set_dev_current_component_function(fn) { dev_current_component_function = fn; } /** * @param {Record<string, unknown>} props * @param {any} runes * @param {Function} [fn] * @returns {void} */ function push(props, runes = false, fn) { component_context = { p: component_context, i: false, c: null, e: null, s: props, x: null, l: null }; if (DEV) { // component function component_context.function = fn; dev_current_component_function = fn; } } /** * @template {Record<string, any>} T * @param {T} [component] * @returns {T} */ function pop(component) { var context = /** @type {ComponentContext} */ (component_context); var effects = context.e; if (effects !== null) { context.e = null; for (var fn of effects) { create_user_effect(fn); } } if (component !== undefined) { context.x = component; } context.i = true; component_context = context.p; if (DEV) { dev_current_component_function = component_context?.function ?? null; } return component ?? /** @type {T} */ ({}); } /** @returns {boolean} */ function is_runes() { return !legacy_mode_flag ; } /** @type {Array<() => void>} */ let micro_tasks = []; function run_micro_tasks() { var tasks = micro_tasks; micro_tasks = []; run_all(tasks); } /** * @param {() => void} fn */ function queue_micro_task(fn) { if (micro_tasks.length === 0 && !is_flushing_sync) { var tasks = micro_tasks; queueMicrotask(() => { // If this is false, a flushSync happened in the meantime. Do _not_ run new scheduled microtasks in that case // as the ordering of microtasks would be broken at that point - consider this case: // - queue_micro_task schedules microtask A to flush task X // - synchronously after, flushSync runs, processing task X // - synchronously after, some other microtask B is scheduled, but not through queue_micro_task but for example a Promise.resolve() in user code // - synchronously after, queue_micro_task schedules microtask C to flush task Y // - one tick later, microtask A now resolves, flushing task Y before microtask B, which is incorrect // This if check prevents that race condition (that realistically will only happen in tests) if (tasks === micro_tasks) run_micro_tasks(); }); } micro_tasks.push(fn); } /** @import { Derived, Effect } from '#client' */ const adjustments = new WeakMap(); /** * @param {unknown} error */ function handle_error(error) { var effect = active_effect; // for unowned deriveds, don't throw until we read the value if (effect === null) { /** @type {Derived} */ (active_reaction).f |= ERROR_VALUE; return error; } if (DEV && error instanceof Error && !adjustments.has(error)) { adjustments.set(error, get_adjustments(error, effect)); } // if the error occurred while creating this subtree, we let it // bubble up until it hits a boundary that can handle it, unless // it's an $effect in which case it doesn't run immediately if ((effect.f & REACTION_RAN) === 0 && (effect.f & EFFECT) === 0) { if (DEV && !effect.parent && error instanceof Error) { apply_adjustments(error); } throw error; } // otherwise we bubble up the effect tree ourselves invoke_error_boundary(error, effect); } /** * @param {unknown} error * @param {Effect | null} effect */ function invoke_error_boundary(error, effect) { while (effect !== null) { if ((effect.f & BOUNDARY_EFFECT) !== 0) { if ((effect.f & REACTION_RAN) === 0) { // we are still creating the boundary effect throw error; } try { /** @type {Boundary} */ (effect.b).error(error); return; } catch (e) { error = e; } } effect = effect.parent; } if (DEV && error instanceof Error) { apply_adjustments(error); } throw error; } /** * Add useful information to the error message/stack in development * @param {Error} error * @param {Effect} effect */ function get_adjustments(error, effect) { const message_descriptor = get_descriptor(error, 'message'); // if the message was already changed and it's not configurable we can't change it // or it will throw a different error swallowing the original error if (message_descriptor && !message_descriptor.configurable) return; var indent = '\t'; var component_stack = `\n${indent}in ${effect.fn?.name || '<unknown>'}`; var context = effect.ctx; while (context !== null) { component_stack += `\n${indent}in ${context.function?.[FILENAME].split('/').pop()}`; context = context.p; } return { message: error.message + `\n${component_stack}\n`, stack: error.stack ?.split('\n') .filter((line) => !line.includes('svelte/src/internal')) .join('\n') }; } /** * @param {Error} error */ function apply_adjustments(error) { const adjusted = adjustments.get(error); if (adjusted) { define_property(error, 'message', { value: adjusted.message }); define_property(error, 'stack', { value: adjusted.stack }); } } /** @import { Derived, Signal } from '#client' */ const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN); /** * @param {Signal} signal * @param {number} status */ function set_signal_status(signal, status) { signal.f = (signal.f & STATUS_MASK) | status; } /** * Set a derived's status to CLEAN or MAYBE_DIRTY based on its connection state. * @param {Derived} derived */ function update_derived_status(derived) { // Only mark as MAYBE_DIRTY if disconnected and has dependencies. if ((derived.f & CONNECTED) !== 0 || derived.deps === null) { set_signal_status(derived, CLEAN); } else { set_signal_status(derived, MAYBE_DIRTY); } } /** @import { Derived, Effect, Value } from '#client' */ /** * @param {Value[] | null} deps */ function clear_marked(deps) { if (deps === null) return; for (const dep of deps) { if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) { continue; } dep.f ^= WAS_MARKED; clear_marked(/** @type {Derived} */ (dep).deps); } } /** * @param {Effect} effect * @param {Set<Effect>} dirty_effects * @param {Set<Effect>} maybe_dirty_effects */ function defer_effect(effect, dirty_effects, maybe_dirty_effects) { if ((effect.f & DIRTY) !== 0) { dirty_effects.add(effect); } else if ((effect.f & MAYBE_DIRTY) !== 0) { maybe_dirty_effects.add(effect); } // Since we're not executing these effects now, we need to clear any WAS_MARKED flags // so that other batches can correctly reach these effects during their own traversal clear_marked(effect.deps); // mark as clean so they get scheduled if they depend on pending async state set_signal_status(effect, CLEAN); } /** @import { StoreReferencesContainer } from '#client' */ /** * We set this to `true` when updating a store so that we correctly * schedule effects if the update takes place inside a `$:` effect */ let legacy_is_updating_store = false; /** * Whether or not the prop currently being read is a store binding, as in * `<Child bind:x={$y} />`. If it is, we treat the prop as mutable even in * runes mode, and skip `binding_property_non_reactive` validation */ let is_store_binding = false; /** * Returns a tuple that indicates whether `fn()` reads a prop that is a store binding. * Used to prevent `binding_property_non_reactive` validation false positives and * ensure that these props are treated as mutable even in runes mode * @template T * @param {() => T} fn * @returns {[T, boolean]} */ function capture_store_binding(fn) { var previous_is_store_binding = is_store_binding; try { is_store_binding = false; return [fn(), is_store_binding]; } finally { is_store_binding = previous_is_store_binding; } } /** @import { Fork } from 'svelte' */ /** @type {Set<Batch>} */ const batches = new Set(); /** @type {Batch | null} */ let current_batch = null; /** * When time travelling (i.e. working in one batch, while other batches * still have ongoing work), we ignore the real values of affected * signals in favour of their values within the batch * @type {Map<Value, any> | null} */ let batch_values = null; /** @type {Effect | null} */ let last_scheduled_effect = null; let is_flushing_sync = false; let is_processing = false; /** * During traversal, this is an array. Newly created effects are (if not immediately * executed) pushed to this array, rather than going through the scheduling * rigamarole that would cause another turn of the flush loop. * @type {Effect[] | null} */ let collected_effects = null; /** * An array of effects that are marked during traversal as a result of a `set` * (not `internal_set`) call. These will be added to the next batch and * trigger another `batch.process()` * @type {Effect[] | null} * @deprecated when we get rid of legacy mode and stores, we can get rid of this */ let legacy_updates = null; var flush_count = 0; var source_stacks = DEV ? new Set() : null; let uid = 1; class Batch { // for debugging. TODO remove once async is stable id = uid++; /** * The current values of any sources that are updated in this batch * They keys of this map are identical to `this.#previous` * @type {Map<Source, any>} */ current = new Map(); /** * The values of any sources that are updated in this batch _before_ those updates took place. * They keys of this map are identical to `this.#current` * @type {Map<Source, any>} */ previous = new Map(); /** * When the batch is committed (and the DOM is updated), we need to remove old branches * and append new ones by calling the functions added inside (if/each/key/etc) blocks * @type {Set<(batch: Batch) => void>} */ #commit_callbacks = new Set(); /** * If a fork is discarded, we need to destroy any effects that are no longer needed * @type {Set<(batch: Batch) => void>} */ #discard_callbacks = new Set(); /** * The number of async effects that are currently in flight */ #pending = 0; /** * The number of async effects that are currently in flight, _not_ inside a pending boundary */ #blocking_pending = 0; /** * A deferred that resolves when the batch is committed, used with `settled()` * TODO replace with Promise.withResolvers once supported widely enough * @type {{ promise: Promise<void>, resolve: (value?: any) => void, reject: (reason: unknown) => void } | null} */ #deferred = null; /** * The root effects that need to be flushed * @type {Effect[]} */ #roots = []; /** * Deferred effects (which run after async work has completed) that are DIRTY * @type {Set<Effect>} */ #dirty_effects = new Set(); /** * Deferred effects that are MAYBE_DIRTY * @type {Set<Effect>} */ #maybe_dirty_effects = new Set(); /** * A map of branches that still exist, but will be destroyed when this batch * is committed — we skip over these during `process`. * The value contains child effects that were dirty/maybe_dirty before being reset, * so they can be rescheduled if the branch survives. * @type {Map<Effect, { d: Effect[], m: Effect[] }>} */ #skipped_branches = new Map(); is_fork = false; #decrement_queued = false; #is_deferred() { return this.is_fork || this.#blocking_pending > 0; } /** * Add an effect to the #skipped_branches map and reset its children * @param {Effect} effect */ skip_effect(effect) { if (!this.#skipped_branches.has(effect)) { this.#skipped_branches.set(effect, { d: [], m: [] }); } } /** * Remove an effect from the #skipped_branches map and reschedule * any tracked dirty/maybe_dirty child effects * @param {Effect} effect */ unskip_effect(effect) { var tracked = this.#skipped_branches.get(effect); if (tracked) { this.#skipped_branches.delete(effect); for (var e of tracked.d) { set_signal_status(e, DIRTY); this.schedule(e); } for (e of tracked.m) { set_signal_status(e, MAYBE_DIRTY); this.schedule(e); } } } #process() { if (flush_count++ > 1000) { infinite_loop_guard(); } const roots = this.#roots; this.#roots = []; this.apply(); /** @type {Effect[]} */ var effects = (collected_effects = []); /** @type {Effect[]} */ var render_effects = []; /** * @type {Effect[]} * @deprecated when we get rid of legacy mode and stores, we can get rid of this */ var updates = (legacy_updates = []); for (const root of roots) { this.#traverse(root, effects, render_effects); } // any writes should take effect in a subsequent batch current_batch = null; if (updates.length > 0) { var batch = Batch.ensure(); for (const e of updates) { batch.schedule(e); } } collected_effects = null; legacy_updates = null; if (this.#is_deferred()) { this.#defer_effects(render_effects); this.#defer_effects(effects); for (const [e, t] of this.#skipped_branches) { reset_branch(e, t); } } else { // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches. this.#dirty_effects.clear(); this.#maybe_dirty_effects.clear(); // append/remove branches for (const fn of this.#commit_callbacks) fn(this); this.#commit_callbacks.clear(); flush_queued_effects(render_effects); flush_queued_effects(effects); if (this.#pending === 0) { this.#commit(); } this.#deferred?.resolve(); } var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch)); if (next_batch !== null) { batches.add(next_batch); if (DEV) { for (const source of this.current.keys()) { /** @type {Set<Source>} */ (source_stacks).add(source); } } next_batch.#process(); } } /** * Traverse the effect tree, executing effects or stashing * them for later execution as appropriate * @param {Effect} root * @param {Effect[]} effects * @param {Effect[]} render_effects */ #traverse(root, effects, render_effects) { root.f ^= CLEAN; var effect = root.first; while (effect !== null) { var flags = effect.f; var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; var skip = is_skippable_branch || (flags & INERT) !== 0 || this.#skipped_branches.has(effect); if (!skip && effect.fn !== null) { if (is_branch) { effect.f ^= CLEAN; } else if ((flags & EFFECT) !== 0) { effects.push(effect); } else if (is_dirty(effect)) { if ((flags & BLOCK_EFFECT) !== 0) this.#maybe_dirty_effects.add(effect); update_effect(effect); } var child = effect.first; if (child !== null) { effect = child; continue; } } while (effect !== null) { var next = effect.next; if (next !== null) { effect = next; break; } effect = effect.parent; } } } /** * @param {Effect[]} effects */ #defer_effects(effects) { for (var i = 0; i < effects.length; i += 1) { defer_effect(effects[i], this.#dirty_effects, this.#maybe_dirty_effects); } } /** * Associate a change to a given source with the current * batch, noting its previous and current values * @param {Source} source * @param {any} value */ capture(source, value) { if (value !== UNINITIALIZED && !this.previous.has(source)) { this.previous.set(source, value); } // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get` if ((source.f & ERROR_VALUE) === 0) { this.current.set(source, source.v); batch_values?.set(source, source.v); } } activate() { current_batch = this; } deactivate() { current_batch = null; batch_values = null; } flush() { var source_stacks = DEV ? new Set() : null; try { is_processing = true; current_batch = this; // we only reschedule previously-deferred effects if we expect // to be able to run them after processing the batch if (!this.#is_deferred()) { for (const e of this.#dirty_effects) { this.#maybe_dirty_effects.delete(e); set_signal_status(e, DIRTY); this.schedule(e); } for (const e of this.#maybe_dirty_effects) { set_signal_status(e, MAYBE_DIRTY); this.schedule(e); } } this.#process(); } finally { flush_count = 0; last_scheduled_effect = null; collected_effects = null; legacy_updates = null; is_processing = false; current_batch = null; batch_values = null; old_values.clear(); if (DEV) { for (const source of /** @type {Set<Source>} */ (source_stacks)) { source.updated = null; } } } } discard() { for (const fn of this.#discard_callbacks) fn(this); this.#discard_callbacks.clear(); } #commit() { // If there are other pending batches, they now need to be 'rebased' — // in other words, we re-run block/async effects with the newly // committed state, unless the batch in question has a more // recent value for a given source if (batches.size > 1) { this.previous.clear(); var previous_batch = current_batch; var previous_batch_values = batch_values; var is_earlier = true; for (const batch of batches) { if (batch === this) { is_earlier = false; continue; } /** @type {Source[]} */ const sources = []; for (const [source, value] of this.current) { if (batch.current.has(source)) { if (is_earlier && value !== batch.current.get(source)) { // bring the value up to date batch.current.set(source, value); } else { // same value or later batch has more recent value, // no need to re-run these effects continue; } } sources.push(source); } if (sources.length === 0) { continue; } // Re-run async/block effects that depend on distinct values changed in both batches const others = [...batch.current.keys()].filter((s) => !this.current.has(s)); if (others.length > 0) { batch.activate(); /** @type {Set<Value>} */ const marked = new Set(); /** @type {Map<Reaction, boolean>} */ const checked = new Map(); for (const source of sources) { mark_effects(source, others, marked, checked); } if (batch.#roots.length > 0) { batch.apply(); for (const root of batch.#roots) { batch.#traverse(root, [], []); } // TODO do we need to do anything with the dummy effect arrays? } batch.deactivate(); } } current_batch = previous_batch; batch_values = previous_batch_values; } this.#skipped_branches.clear(); batches.delete(this); } /** * * @param {boolean} blocking */ increment(blocking) { this.#pending += 1; if (blocking) this.#blocking_pending += 1; } /** * @param {boolean} blocking * @param {boolean} skip - whether to skip updates (because this is triggered by a stale reaction) */ decrement(blocking, skip) { this.#pending -= 1; if (blocking) this.#blocking_pending -= 1; if (this.#decrement_queued || skip) return; this.#decrement_queued = true; queue_micro_task(() => { this.#decrement_queued = false; this.flush(); }); } /** @param {(batch: Batch) => void} fn */ oncommit(fn) { this.#commit_callbacks.add(fn); } /** @param {(batch: Batch) => void} fn */ ondiscard(fn) { this.#discard_callbacks.add(fn); } settled() { return (this.#deferred ??= deferred()).promise; } static ensure() { if (current_batch === null) { const batch = (current_batch = new Batch()); if (!is_processing) { batches.add(current_batch); { queue_micro_task(() => { if (current_batch !== batch) { // a flushSync happened in the meantime return; } batch.flush(); }); } } } return current_batch; } apply() { return; } /** * * @param {Effect} effect */ schedule(effect) { last_scheduled_effect = effect; // defer render effects inside a pending boundary // TODO the `REACTION_RAN` check is only necessary because of legacy `$:` effects AFAICT — we can remove later if ( effect.b?.is_pending && (effect.f & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0 && (effect.f & REACTION_RAN) === 0 ) { effect.b.defer_effect(effect); return; } var e = effect; while (e.parent !== null) { e = e.parent; var flags = e.f; // if the effect is being scheduled because a parent (each/await/etc) block // updated an internal source, or because a branch is being unskipped, // bail out or we'll cause a second flush if (collected_effects !== null && e === active_effect) { // in sync mode, render effects run during traversal. in an extreme edge case // — namely that we're setting a value inside a derived read during traversal — // they can be made dirty after they have already been visited, in which // case we shouldn't bail out. we also shouldn't bail out if we're // updating a store inside a `$:`, since this might invalidate // effects that were already visited if ( (active_reaction === null || (active_reaction.f & DERIVED) === 0) && !legacy_is_updating_store ) { return; } } if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { if ((flags & CLEAN) === 0) { // branch is already dirty, bail return; } e.f ^= CLEAN; } } this.#roots.push(e); } } function infinite_loop_guard() { if (DEV) { var updates = new Map(); for (const source of /** @type {Batch} */ (current_batch).current.keys()) { for (const [stack, update] of source.updated ?? []) { var entry = updates.get(stack); if (!entry) { entry = { error: update.error, count: 0 }; updates.set(stack, entry); } entry.count += update.count; } } for (const update of updates.values()) { if (update.error) { // eslint-disable-next-line no-console console.error(update.error); } } } try { effect_update_depth_exceeded(); } catch (error) { if (DEV) { // stack contains no useful information, replace it define_property(error, 'stack', { value: '' }); } // Best effort: invoke the boundary nearest the most recent // effect and hope that it's relevant to the infinite loop invoke_error_boundary(error, last_scheduled_effect); } } /** @type {Set<Effect> | null} */ let eager_block_effects = null; /** * @param {Array<Effect>} effects * @returns {void} */ function flush_queued_effects(effects) { var length = effects.length; if (length === 0) return; var i = 0; while (i < length) { var effect = effects[i++]; if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) { eager_block_effects = new Set(); update_effect(effect); // Effects with no dependencies or teardown do not get added to the effect tree. // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we // don't know if we need to keep them until they are executed. Doing the check // here (rather than in `update_effect`) allows us to skip the work for // immediate effects. if ( effect.deps === null && effect.first === null && effect.nodes === null && effect.teardown === null && effect.ac === null ) { // remove this effect from the graph unlink_effect(effect); } // If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(), // which already handled this logic and did set eager_block_effects to null. if (eager_block_effects?.size > 0) { old_values.clear(); for (const e of eager_block_effects) { // Skip eager effects that have already been unmounted if ((e.f & (DESTROYED | INERT)) !== 0) continue; // Run effects in order from ancestor to descendant, else we could run into nullpointers /** @type {Effect[]} */ const ordered_effects = [e]; let ancestor = e.parent; while (ancestor !== null) { if (eager_block_effects.has(ancestor)) { eager_block_effects.delete(ancestor); ordered_effects.push(ancestor); } ancestor = ancestor.parent; } for (let j = ordered_effects.length - 1; j >= 0; j--) { const e = ordered_effects[j]; // Skip eager effects that have already been unmounted if ((e.f & (DESTROYED | INERT)) !== 0) continue; update_effect(e); } } eager_block_effects.clear(); } } } eager_block_effects = null; } /** * This is similar to `mark_reactions`, but it only marks async/block effects * depending on `value` and at least one of the other `sources`, so that * these effects can re-run after another batch has been committed * @param {Value} value * @param {Source[]} sources * @param {Set<Value>} marked * @param {Map<Reaction, boolean>} checked */ function mark_effects(value, sources, marked, checked) { if (marked.has(value)) return; marked.add(value); if (value.reactions !== null) { for (const reaction of value.reactions) { const flags = reaction.f; if ((flags & DERIVED) !== 0) { mark_effects(/** @type {Derived} */ (reaction), sources, marked, checked); } else if ( (flags & (ASYNC | BLOCK_EFFECT)) !== 0 && (flags & DIRTY) === 0 && depends_on(reaction, sources, checked) ) { set_signal_status(reaction, DIRTY); schedule_effect(/** @type {Effect} */ (reaction)); } } } } /** * @param {Reaction} reaction * @param {Source[]} sources * @param {Map<Reaction, boolean>} checked */ function depends_on(reaction, sources, checked) { const depends = checked.get(reaction); if (depends !== undefined) return depends; if (reaction.deps !== null) { for (const dep of reaction.deps) { if (includes.call(sources, dep)) { return true; } if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources, checked)) { checked.set(/** @type {Derived} */ (dep), true); return true; } } } checked.set(reaction, false); return false; } /** * @param {Effect} effect * @returns {void} */ function schedule_effect(effect) { /** @type {Batch} */ (current_batch).schedule(effect); } /** * Mark all the effects inside a skipped branch CLEAN, so that * they can be correctly rescheduled later. Tracks dirty and maybe_dirty * effects so they can be rescheduled if the branch survives. * @param {Effect} effect * @param {{ d: Effect[], m: Effect[] }} tracked */ function reset_branch(effect, tracked) { // clean branch = nothing dirty inside, no need to traverse further if ((effect.f & BRANCH_EFFECT) !== 0 && (effect.f & CLEAN) !== 0) { return; } if ((effect.f & DIRTY) !== 0) { tracked.d.push(effect); } else if ((effect.f & MAYBE_DIRTY) !== 0) { tracked.m.push(effect); } set_signal_status(effect, CLEAN); var e = effect.first; while (e !== null) { reset_branch(e, tracked); e = e.next; } } /** @import { Blocker, Effect, Value } from '#client' */ /** * @param {Blocker[]} blockers * @param {Array<() => any>} sync * @param {Array<() => Promise<any>>} async * @param {(values: Value[]) => any} fn */ function flatten(blockers, sync, async, fn) { const d = derived ; // Filter out already-settled blockers - no need to wait for them var pending = blockers.filter((b) => !b.settled); if (async.length === 0 && pending.length === 0) { fn(sync.map(d)); return; } var parent = /** @type {Effect} */ (active_effect); var restore = capture(); var blocker_promise = pending.length === 1 ? pending[0].promise : pending.length > 1 ? Promise.all(pending.map((b) => b.promise)) : null; /** @param {Value[]} values */ function finish(values) { restore(); try { fn(values); } catch (error) { if ((parent.f & DESTROYED) === 0) { invoke_error_boundary(error, parent); } } unset_context(); } // Fast path: blockers but no async expressions if (async.length === 0) { /** @type {Promise<any>} */ (blocker_promise).then(() => finish(sync.map(d))); return; } var decrement_pending = increment_pending(); // Full path: has async expressions function run() { Promise.all(async.map((expression) => async_derived(expression))) .then((result) => finish([...sync.map(d), ...result])) .catch((error) => invoke_error_boundary(error, parent)) .finally(() => decrement_pending()); } if (blocker_promise) { blocker_promise.then(() => { restore(); run(); unset_context(); }); } else { run(); } } /** * Captures the current effect context so that we can restore it after * some asynchronous work has happened (so that e.g. `await a + b` * causes `b` to be registered as a dependency). */ function capture() { var previous_effect = /** @type {Effect} */ (active_effect); var previous_reaction = active_reaction; var previous_component_context = component_context; var previous_batch = /** @type {Batch} */ (current_batch); if (DEV) { var previous_dev_stack = dev_stack; } return function restore(activate_batch = true) { set_active_effect(previous_effect); set_active_reaction(previous_reaction); set_component_context(previous_component_context); if (activate_batch && (previous_effect.f & DESTROYED) === 0) { // TODO we only need optional chaining here because `{#await ...}` blocks // are anomalous. Once we retire them we can get rid of it previous_batch?.activate(); previous_batch?.apply(); } if (DEV) { set_dev_stack(previous_dev_stack); } }; } function unset_context(deactivate_batch = true) { set_active_effect(null); set_active_reaction(null); set_component_context(null); if (deactivate_batch) current_batch?.deactivate(); if (DEV) { set_dev_stack(null); } } /** * @returns {(skip?: boolean) => void} */ function increment_pending() { var boundary = /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b); var batch = /** @type {Batch} */ (current_batch); var blocking = boundary.is_rendered(); boundary.update_pending_count(1, batch); batch.increment(blocking); return (skip = false)