svelte
Version:
Cybernetically enhanced web apps
305 lines (264 loc) • 8.14 kB
JavaScript
/** @import { ProxyMetadata } from '#client' */
/** @typedef {{ file: string, line: number, column: number }} Location */
import { STATE_SYMBOL_METADATA } from '../constants.js';
import { render_effect, user_pre_effect } from '../reactivity/effects.js';
import { dev_current_component_function } from '../context.js';
import { get_prototype_of } from '../../shared/utils.js';
import * as w from '../warnings.js';
import { FILENAME, UNINITIALIZED } from '../../../constants.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {};
const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/;
const firefox_pattern = /@(.+):(\d+):(\d+)$/;
function get_stack() {
const stack = new Error().stack;
if (!stack) return null;
const entries = [];
for (const line of stack.split('\n')) {
let match = chrome_pattern.exec(line) ?? firefox_pattern.exec(line);
if (match) {
entries.push({
file: match[1],
line: +match[2],
column: +match[3]
});
}
}
return entries;
}
/**
* Determines which `.svelte` component is responsible for a given state change
* @returns {Function | null}
*/
export function get_component() {
// first 4 lines are svelte internals; adjust this number if we change the internal call stack
const stack = get_stack()?.slice(4);
if (!stack) return null;
for (let i = 0; i < stack.length; i++) {
const entry = stack[i];
const modules = boundaries[entry.file];
if (!modules) {
// If the first entry is not a component, that means the modification very likely happened
// within a .svelte.js file, possibly triggered by a component. Since these files are not part
// of the bondaries/component context heuristic, we need to bail in this case, else we would
// have false positives when the .svelte.ts file provides a state creator function, encapsulating
// the state and its mutations, and is being called from a component other than the one who
// called the state creator function.
if (i === 0) return null;
continue;
}
for (const module of modules) {
if (module.end == null) {
return null;
}
if (module.start.line < entry.line && module.end.line > entry.line) {
return module.component;
}
}
}
return null;
}
export const ADD_OWNER = Symbol('ADD_OWNER');
/**
* Together with `mark_module_end`, this function establishes the boundaries of a `.svelte` file,
* such that subsequent calls to `get_component` can tell us which component is responsible
* for a given state change
*/
export function mark_module_start() {
const start = get_stack()?.[2];
if (start) {
(boundaries[start.file] ??= []).push({
start,
// @ts-expect-error
end: null,
// @ts-expect-error we add the component at the end, since HMR will overwrite the function
component: null
});
}
}
/**
* @param {Function} component
*/
export function mark_module_end(component) {
const end = get_stack()?.[2];
if (end) {
const boundaries_file = boundaries[end.file];
const boundary = boundaries_file[boundaries_file.length - 1];
boundary.end = end;
boundary.component = component;
}
}
/**
* @param {any} object
* @param {any | null} owner
* @param {boolean} [global]
* @param {boolean} [skip_warning]
*/
export function add_owner(object, owner, global = false, skip_warning = false) {
if (object && !global) {
const component = dev_current_component_function;
const metadata = object[STATE_SYMBOL_METADATA];
if (metadata && !has_owner(metadata, component)) {
let original = get_owner(metadata);
if (owner && owner[FILENAME] !== component[FILENAME] && !skip_warning) {
w.ownership_invalid_binding(component[FILENAME], owner[FILENAME], original[FILENAME]);
}
}
}
add_owner_to_object(object, owner, new Set());
}
/**
* @param {() => unknown} get_object
* @param {any} Component
* @param {boolean} [skip_warning]
*/
export function add_owner_effect(get_object, Component, skip_warning = false) {
user_pre_effect(() => {
add_owner(get_object(), Component, false, skip_warning);
});
}
/**
* @param {any} _this
* @param {Function} owner
* @param {Array<() => any>} getters
* @param {boolean} skip_warning
*/
export function add_owner_to_class(_this, owner, getters, skip_warning) {
_this[ADD_OWNER].current ||= getters.map(() => UNINITIALIZED);
for (let i = 0; i < getters.length; i += 1) {
const current = getters[i]();
// For performance reasons we only re-add the owner if the state has changed
if (current !== _this[ADD_OWNER][i]) {
_this[ADD_OWNER].current[i] = current;
add_owner(current, owner, false, skip_warning);
}
}
}
/**
* @param {ProxyMetadata | null} from
* @param {ProxyMetadata} to
*/
export function widen_ownership(from, to) {
if (to.owners === null) {
return;
}
while (from) {
if (from.owners === null) {
to.owners = null;
break;
}
for (const owner of from.owners) {
to.owners.add(owner);
}
from = from.parent;
}
}
/**
* @param {any} object
* @param {Function | null} owner If `null`, then the object is globally owned and will not be checked
* @param {Set<any>} seen
*/
function add_owner_to_object(object, owner, seen) {
const metadata = /** @type {ProxyMetadata} */ (object?.[STATE_SYMBOL_METADATA]);
if (metadata) {
// this is a state proxy, add owner directly, if not globally shared
if ('owners' in metadata && metadata.owners != null) {
if (owner) {
metadata.owners.add(owner);
} else {
metadata.owners = null;
}
}
} else if (object && typeof object === 'object') {
if (seen.has(object)) return;
seen.add(object);
if (ADD_OWNER in object && object[ADD_OWNER]) {
// this is a class with state fields. we put this in a render effect
// so that if state is replaced (e.g. `instance.name = { first, last }`)
// the new state is also co-owned by the caller of `getContext`
render_effect(() => {
object[ADD_OWNER](owner);
});
} else {
var proto = get_prototype_of(object);
if (proto === Object.prototype) {
// recurse until we find a state proxy
for (const key in object) {
if (Object.getOwnPropertyDescriptor(object, key)?.get) {
// Similar to the class case; the getter could update with a new state
let current = UNINITIALIZED;
render_effect(() => {
const next = object[key];
if (current !== next) {
current = next;
add_owner_to_object(next, owner, seen);
}
});
} else {
add_owner_to_object(object[key], owner, seen);
}
}
} else if (proto === Array.prototype) {
// recurse until we find a state proxy
for (let i = 0; i < object.length; i += 1) {
add_owner_to_object(object[i], owner, seen);
}
}
}
}
}
/**
* @param {ProxyMetadata} metadata
* @param {Function} component
* @returns {boolean}
*/
function has_owner(metadata, component) {
if (metadata.owners === null) {
return true;
}
return (
metadata.owners.has(component) ||
// This helps avoid false positives when using HMR, where the component function is replaced
(FILENAME in component &&
[...metadata.owners].some(
(owner) => /** @type {any} */ (owner)[FILENAME] === component[FILENAME]
)) ||
(metadata.parent !== null && has_owner(metadata.parent, component))
);
}
/**
* @param {ProxyMetadata} metadata
* @returns {any}
*/
function get_owner(metadata) {
return (
metadata?.owners?.values().next().value ??
get_owner(/** @type {ProxyMetadata} */ (metadata.parent))
);
}
let skip = false;
/**
* @param {() => any} fn
*/
export function skip_ownership_validation(fn) {
skip = true;
fn();
skip = false;
}
/**
* @param {ProxyMetadata} metadata
*/
export function check_ownership(metadata) {
if (skip) return;
const component = get_component();
if (component && !has_owner(metadata, component)) {
let original = get_owner(metadata);
// @ts-expect-error
if (original[FILENAME] !== component[FILENAME]) {
// @ts-expect-error
w.ownership_invalid_mutation(component[FILENAME], original[FILENAME]);
} else {
w.ownership_invalid_mutation();
}
}
}