import { Target, deepReactive, deepReactiveMap, deepReadonly, deepReadonlyMap, isReadonly, isShallow, shallowReactiveMap, shallowReadonlyMap, toRaw, } from "./reactivity"; import { ITERATE_KEY } from "./tracking"; import { hasChanged, hasOwn, isArray, isIntegerKey, isObject, isSignal, isSymbol, makeMap, } from "./utils"; import { warn } from "./warn"; import { untracked } from "@preact-signals/unified-signals"; import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from "./constants"; import { track, trigger } from "./tracking"; const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__`); const builtInSymbols = new Set( /*#__PURE__*/ Object.getOwnPropertyNames(Symbol) // ios10.x Object.getOwnPropertyNames(Symbol) can enumerate 'arguments' and 'caller' // but accessing them on Symbol leads to TypeError because Symbol is a strict mode // function .filter((key) => key !== "arguments" && key !== "caller") .map((key) => (Symbol as any)[key]) .filter(isSymbol) ); const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations(); function createArrayInstrumentations() { const instrumentations: Record = {}; // instrument identity-sensitive Array methods to account for possible reactive // values (["includes", "indexOf", "lastIndexOf"] as const).forEach((key) => { instrumentations[key] = function (this: unknown[], ...args: unknown[]) { const arr = toRaw(this) as any; for (let i = 0, l = this.length; i < l; i++) { track(arr, TrackOpTypes.GET, i + ""); } // we run the method using the original args first (which may be reactive) const res = arr[key](...args); if (res === -1 || res === false) { // if that didn't work, run it again using raw values. return arr[key](...args.map(toRaw)); } else { return res; } }; }); // instrument length-altering mutation methods to avoid length being tracked // which leads to infinite loops in some cases (#2137) (["push", "pop", "shift", "unshift", "splice"] as const).forEach((key) => { instrumentations[key] = function (this: unknown[], ...args: unknown[]) { return untracked(() => (toRaw(this) as any)[key].apply(this, args)); }; }); return instrumentations; } function hasOwnProperty(this: object, key: string) { const obj = toRaw(this); track(obj, TrackOpTypes.HAS, key); return obj.hasOwnProperty(key); } class BaseReactiveHandler implements ProxyHandler { constructor( protected readonly _isReadonly = false, protected readonly _shallow = false ) {} get(target: Target, key: string | symbol, receiver: object) { const isReadonly = this._isReadonly, shallow = this._shallow; if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly; } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly; } else if (key === ReactiveFlags.IS_SHALLOW) { return shallow; } else if ( key === ReactiveFlags.RAW && receiver === (isReadonly ? shallow ? shallowReadonlyMap : deepReadonlyMap : shallow ? shallowReactiveMap : deepReactiveMap ).get(target) ) { return target; } const targetIsArray = isArray(target); if (!isReadonly) { if (targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver); } if (key === "hasOwnProperty") { return hasOwnProperty; } } const res = Reflect.get(target, key, receiver); if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res; } if (!isReadonly) { track(target, TrackOpTypes.GET, key); } if (shallow) { return res; } if (isSignal(res)) { // ref unwrapping - skip unwrap for Array + integer key. return targetIsArray && isIntegerKey(key) ? res : res.value; } if (isObject(res)) { // Convert returned value into a proxy as well. we do the isObject check // here to avoid invalid value warning. Also need to lazy access readonly // and reactive here to avoid circular dependency. return isReadonly ? deepReadonly(res) : deepReactive(res); } return res; } } class MutableReactiveHandler extends BaseReactiveHandler { constructor(shallow = false) { super(false, shallow); } set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { let oldValue = (target as any)[key]; if (isReadonly(oldValue) && isSignal(oldValue) && !isSignal(value)) { return false; } if (!this._shallow) { if (!isShallow(value) && !isReadonly(value)) { oldValue = toRaw(oldValue); value = toRaw(value); } if (!isArray(target) && isSignal(oldValue) && !isSignal(value)) { oldValue.value = value; return true; } } else { // in shallow mode, objects are set as-is regardless of reactive or not } const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value); } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue); } } return result; } deleteProperty(target: object, key: string | symbol): boolean { const hadKey = hasOwn(target, key); const oldValue = (target as any)[key]; const result = Reflect.deleteProperty(target, key); if (result && hadKey) { trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue); } return result; } has(target: object, key: string | symbol): boolean { const result = Reflect.has(target, key); if (!isSymbol(key) || !builtInSymbols.has(key)) { track(target, TrackOpTypes.HAS, key); } return result; } ownKeys(target: object): (string | symbol)[] { track( target, TrackOpTypes.ITERATE, isArray(target) ? "length" : ITERATE_KEY ); return Reflect.ownKeys(target); } } class ReadonlyReactiveHandler extends BaseReactiveHandler { constructor(shallow = false) { super(true, shallow); } set(target: object, key: string | symbol) { if (__DEV__) { warn( `Set operation on key "${String(key)}" failed: target is readonly.`, target ); } return true; } deleteProperty(target: object, key: string | symbol) { if (__DEV__) { warn( `Delete operation on key "${String(key)}" failed: target is readonly.`, target ); } return true; } } export const mutableHandlers: ProxyHandler = /*#__PURE__*/ new MutableReactiveHandler(); export const readonlyHandlers: ProxyHandler = /*#__PURE__*/ new ReadonlyReactiveHandler(); export const shallowReactiveHandlers = /*#__PURE__*/ new MutableReactiveHandler( true ); // Props handlers are special in the sense that it should not unwrap top-level // refs (in order to allow refs to be explicitly passed down), but should // retain the reactivity of the normal readonly object. export const shallowReadonlyHandlers = /*#__PURE__*/ new ReadonlyReactiveHandler(true);