init
This commit is contained in:
+432
@@ -0,0 +1,432 @@
|
||||
/** @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_eager_effects,
|
||||
set_eager_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 { tag } from './dev/tracing.js';
|
||||
import { get_error } from '../shared/dev.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_error('created at') : 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 = '';
|
||||
let updating = false;
|
||||
/** @param {string} new_path */
|
||||
function update_path(new_path) {
|
||||
if (updating) return;
|
||||
updating = true;
|
||||
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));
|
||||
}
|
||||
updating = false;
|
||||
}
|
||||
|
||||
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 an 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));
|
||||
|
||||
if (DEV) {
|
||||
tag(s, get_label(path, prop));
|
||||
}
|
||||
set(s, proxy(value));
|
||||
|
||||
sources.set(prop, s);
|
||||
}
|
||||
} 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_eager_effects_deferred();
|
||||
var result = value.apply(this, args);
|
||||
flush_eager_effects();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user