UNPKG

svelte

Version:

Cybernetically enhanced web apps

429 lines (359 loc) 10.7 kB
/** @import { Source } from '#client' */ import { DEV } from 'esm-env'; import { get, active_effect, update_version, active_reaction, set_update_version, set_active_reaction } from './runtime.js'; import { array_prototype, get_descriptor, get_prototype_of, is_array, object_prototype } from '../shared/utils.js'; import { state as source, set, increment, flush_inspect_effects, set_inspect_effects_deferred } from './reactivity/sources.js'; import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; import { get_stack, tag } from './dev/tracing.js'; import { tracing_mode_flag } from '../flags/index.js'; // TODO move all regexes into shared module? const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; /** * @template T * @param {T} value * @returns {T} */ export function proxy(value) { // if non-proxyable, or is already a proxy, return `value` if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) { return value; } const prototype = get_prototype_of(value); if (prototype !== object_prototype && prototype !== array_prototype) { return value; } /** @type {Map<any, Source<any>>} */ var sources = new Map(); var is_proxied_array = is_array(value); var version = source(0); var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; var parent_version = update_version; /** * Executes the proxy in the context of the reaction it was originally created in, if any * @template T * @param {() => T} fn */ var with_parent = (fn) => { if (update_version === parent_version) { return fn(); } // child source is being created after the initial proxy — // prevent it from being associated with the current reaction var reaction = active_reaction; var version = update_version; set_active_reaction(null); set_update_version(parent_version); var result = fn(); set_active_reaction(reaction); set_update_version(version); return result; }; if (is_proxied_array) { // We need to create the length source eagerly to ensure that // mutations to the array are properly synced with our proxy sources.set('length', source(/** @type {any[]} */ (value).length, stack)); if (DEV) { value = /** @type {any} */ (inspectable_array(/** @type {any[]} */ (value))); } } /** Used in dev for $inspect.trace() */ var path = ''; /** @param {string} new_path */ function update_path(new_path) { path = new_path; tag(version, `${path} version`); // rename all child sources and child proxies for (const [prop, source] of sources) { tag(source, get_label(path, prop)); } } return new Proxy(/** @type {any} */ (value), { defineProperty(_, prop, descriptor) { if ( !('value' in descriptor) || descriptor.configurable === false || descriptor.enumerable === false || descriptor.writable === false ) { // we disallow non-basic descriptors, because unless they are applied to the // target object — which we avoid, so that state can be forked — we will run // afoul of the various invariants // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor#invariants e.state_descriptors_fixed(); } var s = sources.get(prop); if (s === undefined) { s = with_parent(() => { var s = source(descriptor.value, stack); sources.set(prop, s); if (DEV && typeof prop === 'string') { tag(s, get_label(path, prop)); } return s; }); } else { set(s, descriptor.value, true); } return true; }, deleteProperty(target, prop) { var s = sources.get(prop); if (s === undefined) { if (prop in target) { const s = with_parent(() => source(UNINITIALIZED, stack)); sources.set(prop, s); increment(version); if (DEV) { tag(s, get_label(path, prop)); } } } else { set(s, UNINITIALIZED); increment(version); } return true; }, get(target, prop, receiver) { if (prop === STATE_SYMBOL) { return value; } if (DEV && prop === PROXY_PATH_SYMBOL) { return update_path; } var s = sources.get(prop); var exists = prop in target; // create a source, but only if it's an own property and not a prototype property if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) { s = with_parent(() => { var p = proxy(exists ? target[prop] : UNINITIALIZED); var s = source(p, stack); if (DEV) { tag(s, get_label(path, prop)); } return s; }); sources.set(prop, s); } if (s !== undefined) { var v = get(s); return v === UNINITIALIZED ? undefined : v; } return Reflect.get(target, prop, receiver); }, getOwnPropertyDescriptor(target, prop) { var descriptor = Reflect.getOwnPropertyDescriptor(target, prop); if (descriptor && 'value' in descriptor) { var s = sources.get(prop); if (s) descriptor.value = get(s); } else if (descriptor === undefined) { var source = sources.get(prop); var value = source?.v; if (source !== undefined && value !== UNINITIALIZED) { return { enumerable: true, configurable: true, value, writable: true }; } } return descriptor; }, has(target, prop) { if (prop === STATE_SYMBOL) { return true; } var s = sources.get(prop); var has = (s !== undefined && s.v !== UNINITIALIZED) || Reflect.has(target, prop); if ( s !== undefined || (active_effect !== null && (!has || get_descriptor(target, prop)?.writable)) ) { if (s === undefined) { s = with_parent(() => { var p = has ? proxy(target[prop]) : UNINITIALIZED; var s = source(p, stack); if (DEV) { tag(s, get_label(path, prop)); } return s; }); sources.set(prop, s); } var value = get(s); if (value === UNINITIALIZED) { return false; } } return has; }, set(target, prop, value, receiver) { var s = sources.get(prop); var has = prop in target; // variable.length = value -> clear all signals with index >= value if (is_proxied_array && prop === 'length') { for (var i = value; i < /** @type {Source<number>} */ (s).v; i += 1) { var other_s = sources.get(i + ''); if (other_s !== undefined) { set(other_s, UNINITIALIZED); } else if (i in target) { // If the item exists in the original, we need to create a uninitialized source, // else a later read of the property would result in a source being created with // the value of the original item at that index. other_s = with_parent(() => source(UNINITIALIZED, stack)); sources.set(i + '', other_s); if (DEV) { tag(other_s, get_label(path, i)); } } } } // If we haven't yet created a source for this property, we need to ensure // we do so otherwise if we read it later, then the write won't be tracked and // the heuristics of effects will be different vs if we had read the proxied // object property before writing to that property. if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { s = with_parent(() => source(undefined, stack)); set(s, proxy(value)); sources.set(prop, s); if (DEV) { tag(s, get_label(path, prop)); } } } else { has = s.v !== UNINITIALIZED; var p = with_parent(() => proxy(value)); set(s, p); } var descriptor = Reflect.getOwnPropertyDescriptor(target, prop); // Set the new value before updating any signals so that any listeners get the new value if (descriptor?.set) { descriptor.set.call(receiver, value); } if (!has) { // If we have mutated an array directly, we might need to // signal that length has also changed. Do it before updating metadata // to ensure that iterating over the array as a result of a metadata update // will not cause the length to be out of sync. if (is_proxied_array && typeof prop === 'string') { var ls = /** @type {Source<number>} */ (sources.get('length')); var n = Number(prop); if (Number.isInteger(n) && n >= ls.v) { set(ls, n + 1); } } increment(version); } return true; }, ownKeys(target) { get(version); var own_keys = Reflect.ownKeys(target).filter((key) => { var source = sources.get(key); return source === undefined || source.v !== UNINITIALIZED; }); for (var [key, source] of sources) { if (source.v !== UNINITIALIZED && !(key in target)) { own_keys.push(key); } } return own_keys; }, setPrototypeOf() { e.state_prototype_fixed(); } }); } /** * @param {string} path * @param {string | symbol} prop */ function get_label(path, prop) { if (typeof prop === 'symbol') return `${path}[Symbol(${prop.description ?? ''})]`; if (regex_is_valid_identifier.test(prop)) return `${path}.${prop}`; return /^\d+$/.test(prop) ? `${path}[${prop}]` : `${path}['${prop}']`; } /** * @param {any} value */ export function get_proxied_value(value) { try { if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) { return value[STATE_SYMBOL]; } } catch { // the above if check can throw an error if the value in question // is the contentWindow of an iframe on another domain, in which // case we want to just return the value (because it's definitely // not a proxied value) so we don't break any JavaScript interacting // with that iframe (such as various payment companies client side // JavaScript libraries interacting with their iframes on the same // domain) } return value; } /** * @param {any} a * @param {any} b */ export function is(a, b) { return Object.is(get_proxied_value(a), get_proxied_value(b)); } const ARRAY_MUTATING_METHODS = new Set([ 'copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ]); /** * Wrap array mutating methods so $inspect is triggered only once and * to prevent logging an array in intermediate state (e.g. with an empty slot) * @param {any[]} array */ function inspectable_array(array) { return new Proxy(array, { get(target, prop, receiver) { var value = Reflect.get(target, prop, receiver); if (!ARRAY_MUTATING_METHODS.has(/** @type {string} */ (prop))) { return value; } /** * @this {any[]} * @param {any[]} args */ return function (...args) { set_inspect_effects_deferred(); var result = value.apply(this, args); flush_inspect_effects(); return result; }; } }); }