svelte
Version:
Cybernetically enhanced web apps
429 lines (359 loc) • 10.7 kB
JavaScript
/** @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;
};
}
});
}