Source: lib/core/reactive/proxyHandler.js

/**
 * @file proxyHandler.js
 * @description Factory for creating proxy handlers used in reactive state.
 * Handles normal property access and computed property redirection.
 */

import { track, trigger, parentMap, AvenxWatcher, depMap, getPropertyPath, setDebugReactivity, isDebugReactivityEnabled } from './watcher.js';

import {
  RAW_SYMBOL,
  IS_REACTIVE_PROXY,
  PROXY_REF_SYMBOL,
  NON_REACTIVE_SYMBOL,
} from './symbols.js';
import { tracer } from '../trace/tracer.js';
import { traceWrite, traceCollectionWrite } from '../trace/reactive.js';
import { journal, setPathResolver } from './journal.js';

// The journal names a conflicted path in its report; the naming rules already
// live in the watcher. Injecting the resolver keeps the journal free of the
// watcher's surface while still producing "cart.items.0.qty" rather than "qty".
setPathResolver(getPropertyPath);

export { setDebugReactivity, isDebugReactivityEnabled };

// The symbols moved to ./symbols.js so consumers that only need to recognise a
// reactive value can import them without pulling in the proxy factory. They are
// re-exported here unchanged, so every existing importer is unaffected.
export { RAW_SYMBOL, IS_REACTIVE_PROXY, PROXY_REF_SYMBOL, NON_REACTIVE_SYMBOL };

const mutatingArrayMethods = new Set([
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
  'copyWithin',
  'fill',
]);

/**
 * Checks if the value is a candidate for reactive wrapping.
 * We restrict this to plain objects and arrays to avoid issues with
 * built-in classes (Date, RegExp, Map, Set, Promise) and custom class
 * instances that may contain private fields or internal slots.
 * @param {any} value - The value to check.
 * @returns {boolean} True if the value should be reactive, false otherwise.
 */
export function isReactiveTarget(value) {
  if (value === null || typeof value !== 'object') {
    return false;
  }
  if (value[NON_REACTIVE_SYMBOL]) {
    return false;
  }
  const proto = Object.getPrototypeOf(value);
  return (
    proto === null ||
    proto === Object.prototype ||
    proto === Array.prototype ||
    value instanceof Set ||
    value instanceof Map
  );
}

/**
 * Returns the underlying raw object for a reactive proxy.
 * @param {any} target
 * @returns {any}
 */
export function toRaw(target) {
  if (target === null || typeof target !== 'object') {
    return target;
  }
  return target[RAW_SYMBOL] || target;
}

/**
 * Returns true when value is an Avenx reactive proxy.
 * @param {any} value
 * @returns {boolean}
 */
export function isReactive(value) {
  return !!(value && typeof value === 'object' && value[IS_REACTIVE_PROXY]);
}

/**
 * Marks an object so it will not be wrapped by reactive proxies.
 * @template T
 * @param {T} target
 * @returns {T}
 */
export function markRaw(target) {
  if (target === null || typeof target !== 'object') {
    return target;
  }
  if (!Object.isExtensible(target)) {
    return target;
  }
  if (!target[NON_REACTIVE_SYMBOL]) {
    Object.defineProperty(target, NON_REACTIVE_SYMBOL, {
      value: true,
      writable: false,
      enumerable: false,
      configurable: false,
    });
  }
  return target;
}

/**
 * Recursively cleans up parentMap entries for a detached reactive target and its nested properties.
 * @param {any} value
 * @param {Set<any>} [seen]
 */
export function cleanupParentMap(value, seen = new Set()) {
  if (value === null || typeof value !== 'object') {
    return;
  }
  const rawValue = value[RAW_SYMBOL] || value;
  if (seen.has(rawValue)) {
    return;
  }
  seen.add(rawValue);

  if (parentMap.has(rawValue)) {
    parentMap.delete(rawValue);
  }

  if (rawValue instanceof Map) {
    for (const val of rawValue.values()) {
      cleanupParentMap(val, seen);
    }
  } else if (rawValue instanceof Set) {
    for (const val of rawValue.values()) {
      cleanupParentMap(val, seen);
    }
  } else if (Array.isArray(rawValue)) {
    for (let i = 0; i < rawValue.length; i++) {
      cleanupParentMap(rawValue[i], seen);
    }
  } else {
    // Plain object
    for (const val of Object.values(rawValue)) {
      cleanupParentMap(val, seen);
    }
  }
}



/**
 * Whether anything depends on a key, or on the path that reaches it.
 *
 * A dependency may be recorded at any level: a template reading `cart.items`
 * registers against `items` on the root target, while a write to
 * `cart.items[0].qty` lands on the element object. Walking up `parentMap` is
 * what connects the two.
 * @param {object} target - The raw target that changed.
 * @param {string|symbol|Array<string|symbol>} key - The key or keys that changed.
 * @returns {boolean} True when some watcher depends on the change.
 */
function hasDependentOnPath(target, key) {
  const keys = Array.isArray(key) ? key : [key];

  const keysMap = depMap.get(target);
  if (keysMap) {
    for (const k of keys) {
      const watchers = keysMap.get(k);
      if (watchers && watchers.size > 0) {
        return true;
      }
    }
  }

  // Walk to the root, asking whether the property that reaches this object is
  // itself observed. A guard on depth is unnecessary: cleanupParentMap keeps
  // the chain acyclic, but a visited set costs nothing and makes that explicit.
  const seen = new Set();
  let current = target;
  while (current && !seen.has(current)) {
    seen.add(current);
    const relation = parentMap.get(current);
    if (!relation) {
      return false;
    }
    const parentKeys = depMap.get(relation.parentTarget);
    if (parentKeys) {
      const watchers = parentKeys.get(relation.parentKey);
      if (watchers && watchers.size > 0) {
        return true;
      }
    }
    current = relation.parentTarget;
  }

  return false;
}

/**
 * Factory for creating and managing Proxy handlers for reactive state objects.
 */
export class ProxyHandlerFactory {
  /**
   * @param {object} [options] - Configuration options.
   * @param {string[]} [options.computedKeys] - List of keys that should be treated as computed properties.
   * @param {Function} [options.onChange] - Callback triggered when a property is set.
   * @param {Function|null} [options.onKeysChanged] - Callback triggered when the
   *   root target gains or loses a key, for consumers that cache its key set.
   * @param {boolean|function(): boolean} [options.onlyNotifyObserved] - When true, onChange is
   *   suppressed for writes that no watcher depends on, at any level of the
   *   property path. Off by default so the standalone reactive primitive keeps
   *   notifying for every mutation.
   * @param {Function} [options.getComputedValue] - Function to evaluate a computed property.
   * @param {object} [options.instance] - The component instance for fallback property lookups.
   * @param {boolean} [options.bypassSymbol] - Bypasses Symbol check during lookup.
   * @param {string[]} [options.persist] - List of state keys to persist in localStorage.
   */
  constructor({
    computedKeys = [],
    onChange = () => {},
    onKeysChanged = null,
    onlyNotifyObserved = false,
    getComputedValue = () => undefined,
    instance = null,
    bypassSymbol = false,
    persist = [],
  } = {}) {
    /** @type {Set<string>} */
    this.computedKeys = new Set(computedKeys);
    /** @type {Function} */
    this.onChange = onChange;
    /**
     * Called when the root target gains or loses a key.
     *
     * Distinct from `onChange`, which fires for a *value* change. A consumer
     * that caches the set of names state binds -- the template scope does --
     * needs to know when that set changed, and cannot tell from a value
     * notification. Without it the cache has to be rebuilt on every read, which
     * is `Object.keys()` over the whole state object per expression evaluated.
     * @type {Function|null}
     */
    this.onKeysChanged = onKeysChanged;
    /**
     * Whether onChange is suppressed for writes nothing depends on.
     *
     * Off by default: the standalone reactive primitive promises a
     * notification for every mutation. Components turn it on so that a write
     * to a key no expression reads does not schedule a render.
     * May be a predicate, so a consumer can enable filtering only once it has
     * actually collected dependencies.
     * @type {boolean|function(): boolean}
     */
    this.onlyNotifyObserved = onlyNotifyObserved;
    /** @type {Function} */
    this.getComputedValueReal = getComputedValue;
    /** @type {Set<string>} */
    this.persistKeys = new Set(persist);
    /** @type {WeakMap<object, Proxy>} */
    this.proxyCache = new WeakMap();
    /** @type {Map<string, AvenxWatcher>} */
    this.computedWatchers = new Map();
    /** @type {object|null} */
    this.instance = instance;
    /** @type {boolean} */
    this.bypassSymbol =
      bypassSymbol ||
      (typeof globalThis !== 'undefined' &&
        !!globalThis.__avenx_bypass_symbol__);
    /** @type {object|null} */
    this.rootTarget = null;
  }
  
  /**
   * Evaluates and caches a computed property using an internal AvenxWatcher.
   * @param {string} key - The computed key.
   * @param {object} receiver - The proxy receiver.
   * @returns {any}
   */
  evaluateComputedWatcher(key, receiver) {
    const target = receiver[RAW_SYMBOL] || receiver;
    track(target, key);

    let watcher = this.computedWatchers.get(key);
    if (!watcher) {
      watcher = new AvenxWatcher(
        () => this.getComputedValueReal(key, receiver),
        () => {
          trigger(target, key);
          this.notifyChange(target, key);
        },
        { lazy: true },
      );
      this.computedWatchers.set(key, watcher);
    }
    return watcher.evaluate();
  }
  
  /**
   * Cleans up all computed watchers created by this proxy handler.
   */
  teardown() {
    for (const watcher of this.computedWatchers.values()) {
      watcher.teardown();
    }
    this.computedWatchers.clear();
  }

  /**
   * Checks if the target object is still connected to the root state.
   * @param {object} target - The target raw object.
   * @returns {boolean}
   */
  isConnected(target) {
    if (!this.rootTarget) {
      return true;
    }
    let current = target;
    while (current) {
      if (current === this.rootTarget) {
        return true;
      }
      const relation = parentMap.get(current);
      if (!relation) {
        break;
      }
      current = relation.parentTarget;
    }
    return false;
  }

  /**
   * Notifies about a change if anything actually depends on it.
   *
   * This used to call `onChange()` for every write to any connected target,
   * which scheduled a component update whether or not a single expression read
   * the key. The per-key dependency graph the Proxy layer maintains was
   * therefore discarded at the last step: a component re-rendered in full when
   * a state key nothing referenced changed.
   *
   * `trigger()` alone is not a sufficient replacement. It wakes watchers
   * registered for exactly one target and key, and a nested write such as
   * `items[0].qty = 3` triggers on the inner object while the template's
   * dependency was recorded against `items` on the root. Walking the parent
   * chain is what keeps deep reactivity working while still allowing a write
   * that nothing observes -- at any level of the path -- to be skipped.
   *
   * Filtering is opt-in via `onlyNotifyObserved`. `StateFactory.create(obj,
   * { onChange })` is a public primitive whose documented contract is that
   * onChange fires for every mutation, watchers or not; components opt in
   * because for them onChange means "schedule a re-render", and a re-render
   * for a key no expression reads is exactly the waste being removed.
   * @param {object} target - The target raw object.
   * @param {string|symbol|Array<string|symbol>} [key] - The key that changed.
   *   Omitted for structural changes whose affected keys are not known, which
   *   notify unconditionally.
   */
  notifyChange(target, key) {
    if (!this.isConnected(target)) {
      return;
    }
    // The filter is only sound once the consumer has collected dependencies.
    // Before a component's first render nothing has been read, so every key
    // would look unobserved and no update would ever be scheduled.
    const filtering =
      typeof this.onlyNotifyObserved === 'function' ? this.onlyNotifyObserved() : this.onlyNotifyObserved;

    if (filtering && key !== undefined && !hasDependentOnPath(target, key)) {
      return;
    }
    this.onChange();
  }

  /**
   * Returns a custom implementation of a Map method.
   * @param {Map} target - The raw Map.
   * @param {string|symbol} key - The property/method key.
   * @param {Proxy} receiver - The Map proxy.
   * @returns {any}
   */
  getMapMethod(target, key, receiver) {
    if (key === 'size') {
      track(target, 'size');
      return target.size;
    }
    if (key === 'get') {
      return (k) => {
        track(target, k);
        const val = target.get(k);
        if (isReactiveTarget(val)) {
          parentMap.set(val, { parentTarget: target, parentKey: k });
          return this.getOrCreateProxy(val);
        }
        return val;
      };
    }
    if (key === 'has') {
      return (k) => {
        track(target, k);
        return target.has(k);
      };
    }
    if (key === 'set') {
      return (k, v) => {
        const rawVal = v && v[RAW_SYMBOL] ? v[RAW_SYMBOL] : v;
        if (journal.active) {
          journal.recordCollection(target, this, 'Map');
        }
        const hasKey = target.has(k);
        const oldValue = target.get(k);
        const valueChanged = oldValue !== rawVal;

        target.set(k, rawVal);

        if (isReactiveTarget(oldValue)) {
          cleanupParentMap(oldValue);
        }

        if (isReactiveTarget(rawVal)) {
          parentMap.set(rawVal, { parentTarget: target, parentKey: k });
        }

        if (valueChanged || !hasKey) {
          const token = tracer.on ? traceCollectionWrite(target, k, 'map.set', target.size) : -1;
          try {
            trigger(target, !hasKey ? [k, 'size'] : k);
            this.notifyChange(target, !hasKey ? [k, 'size'] : k);
          } finally {
            if (token >= 0) {
              tracer.leave(token);
            }
          }
        }
        return receiver;
      };
    }
    if (key === 'delete') {
      return (k) => {
        if (journal.active) {
          journal.recordCollection(target, this, 'Map');
        }
        const hasKey = target.has(k);
        const oldValue = target.get(k);

        const result = target.delete(k);

        if (hasKey) {
          if (isReactiveTarget(oldValue)) {
            cleanupParentMap(oldValue);
          }

          const token = tracer.on ? traceCollectionWrite(target, k, 'map.delete', target.size) : -1;
          try {
            trigger(target, [k, 'size']);
            this.notifyChange(target, [k, 'size']);
          } finally {
            if (token >= 0) {
              tracer.leave(token);
            }
          }
        }

        return result;
      };
    }
    if (key === 'clear') {
      return () => {
        if (journal.active) {
          journal.recordCollection(target, this, 'Map');
        }
        const size = target.size;
        if (size > 0) {
          const keysMap = depMap.get(target);
          const keysToTrigger = ['size'];
          if (keysMap) {
            keysToTrigger.push(...keysMap.keys());
          }
          target.clear();
          const token = tracer.on ? traceCollectionWrite(target, undefined, 'clear', 0) : -1;
          try {
            trigger(target, keysToTrigger);
            this.notifyChange(target, keysToTrigger);
          } finally {
            if (token >= 0) {
              tracer.leave(token);
            }
          }
        }
      };
    }
    if (key === 'forEach') {
      return (callback, thisArg) => {
        track(target, 'size');
        target.forEach((val, k) => {
          callback.call(
            thisArg,
            isReactiveTarget(val) ? this.getOrCreateProxy(val) : val,
            isReactiveTarget(k) ? this.getOrCreateProxy(k) : k,
            receiver,
          );
        });
      };
    }
    if (key === 'keys') {
      return () => {
        track(target, 'size');
        const iterator = target.keys();
        const self = this;
        return {
          next() {
            const { value, done } = iterator.next();
            return {
              value: done ? undefined : isReactiveTarget(value) ? self.getOrCreateProxy(value) : value,
              done,
            };
          },
          [Symbol.iterator]() {
            return this;
          },
        };
      };
    }
    if (key === 'values') {
      return () => {
        track(target, 'size');
        const iterator = target.values();
        const self = this;
        return {
          next() {
            const { value, done } = iterator.next();
            return {
              value: done ? undefined : isReactiveTarget(value) ? self.getOrCreateProxy(value) : value,
              done,
            };
          },
          [Symbol.iterator]() {
            return this;
          },
        };
      };
    }
    if (key === 'entries' || key === Symbol.iterator) {
      return () => {
        track(target, 'size');
        const iterator = target.entries();
        const self = this;
        return {
          next() {
            const { value, done } = iterator.next();
            let unwrapped;
            if (done) {
              unwrapped = undefined;
            } else {
              unwrapped = [
                isReactiveTarget(value[0]) ? self.getOrCreateProxy(value[0]) : value[0],
                isReactiveTarget(value[1]) ? self.getOrCreateProxy(value[1]) : value[1],
              ];
            }
            return {
              value: unwrapped,
              done,
            };
          },
          [Symbol.iterator]() {
            return this;
          },
        };
      };
    }

    const value = Reflect.get(target, key, target);
    if (typeof value === 'function') {
      return value.bind(target);
    }
    return value;
  }

  /**
   * Returns a custom implementation of a Set method.
   * @param {Set} target - The raw Set.
   * @param {string|symbol} key - The property/method key.
   * @param {Proxy} receiver - The Set proxy.
   * @returns {any}
   */
  getSetMethod(target, key, receiver) {
    if (key === 'size') {
      track(target, 'size');
      return target.size;
    }
    if (key === 'has') {
      return (val) => {
        const rawVal = val && val[RAW_SYMBOL] ? val[RAW_SYMBOL] : val;
        track(target, rawVal);
        return target.has(rawVal);
      };
    }
    if (key === 'add') {
      return (val) => {
        if (journal.active) {
          journal.recordCollection(target, this, 'Set');
        }
        const rawVal = val && val[RAW_SYMBOL] ? val[RAW_SYMBOL] : val;
        const hasVal = target.has(rawVal);
        if (!hasVal) {
          target.add(rawVal);
          if (isReactiveTarget(rawVal)) {
            parentMap.set(rawVal, { parentTarget: target, parentKey: rawVal });
          }
          const token = tracer.on ? traceCollectionWrite(target, undefined, 'set.add', target.size) : -1;
          try {
            trigger(target, [rawVal, 'size']);
            this.notifyChange(target, [rawVal, 'size']);
          } finally {
            if (token >= 0) {
              tracer.leave(token);
            }
          }
        }
        return receiver;
      };
    }
    if (key === 'delete') {
      return (val) => {
        if (journal.active) {
          journal.recordCollection(target, this, 'Set');
        }
        const rawVal = val && val[RAW_SYMBOL] ? val[RAW_SYMBOL] : val;
        const hasVal = target.has(rawVal);

        const result = target.delete(rawVal);

        if (hasVal) {
          if (isReactiveTarget(rawVal)) {
            cleanupParentMap(rawVal);
          }

          const token = tracer.on ? traceCollectionWrite(target, undefined, 'set.delete', target.size) : -1;
          try {
            trigger(target, [rawVal, 'size']);
            this.notifyChange(target, [rawVal, 'size']);
          } finally {
            if (token >= 0) {
              tracer.leave(token);
            }
          }
        }

        return result;
      };
    }
    if (key === 'clear') {
      return () => {
        if (journal.active) {
          journal.recordCollection(target, this, 'Set');
        }
        const size = target.size;
        if (size > 0) {
          const keysMap = depMap.get(target);
          const keysToTrigger = ['size'];
          if (keysMap) {
            keysToTrigger.push(...keysMap.keys());
          }
          target.clear();
          const token = tracer.on ? traceCollectionWrite(target, undefined, 'clear', 0) : -1;
          try {
            trigger(target, keysToTrigger);
            this.notifyChange(target, keysToTrigger);
          } finally {
            if (token >= 0) {
              tracer.leave(token);
            }
          }
        }
      };
    }
    if (key === 'forEach') {
      return (callback, thisArg) => {
        track(target, 'size');
        target.forEach((val) => {
          if (isReactiveTarget(val)) {
            parentMap.set(val, { parentTarget: target, parentKey: val });
          }
          callback.call(
            thisArg,
            isReactiveTarget(val) ? this.getOrCreateProxy(val) : val,
            isReactiveTarget(val) ? this.getOrCreateProxy(val) : val,
            receiver,
          );
        });
      };
    }
    if (key === 'values' || key === 'keys' || key === Symbol.iterator) {
      return () => {
        track(target, 'size');
        const iterator = target.values();
        const self = this;
        return {
          next() {
            const { value, done } = iterator.next();
            if (!done && isReactiveTarget(value)) {
              parentMap.set(value, { parentTarget: target, parentKey: value });
            }
            return {
              value: done ? undefined : isReactiveTarget(value) ? self.getOrCreateProxy(value) : value,
              done,
            };
          },
          [Symbol.iterator]() {
            return this;
          },
        };
      };
    }
    if (key === 'entries') {
      return () => {
        track(target, 'size');
        const iterator = target.entries();
        const self = this;
        return {
          next() {
            const { value, done } = iterator.next();
            let unwrapped;
            if (done) {
              unwrapped = undefined;
            } else {
              if (isReactiveTarget(value[0])) {
                parentMap.set(value[0], { parentTarget: target, parentKey: value[0] });
              }
              unwrapped = [
                isReactiveTarget(value[0]) ? self.getOrCreateProxy(value[0]) : value[0],
                isReactiveTarget(value[1]) ? self.getOrCreateProxy(value[1]) : value[1],
              ];
            }
            return {
              value: unwrapped,
              done,
            };
          },
          [Symbol.iterator]() {
            return this;
          },
        };
      };
    }

    const value = Reflect.get(target, key, target);
    if (typeof value === 'function') {
      return value.bind(target);
    }
    return value;
  }

  /**
   * Creates the proxy handler object.
   * @returns {ProxyHandler<object>}
   */
  /**
   * Creates the proxy handler object.
   * @returns {ProxyHandler<object>}
   */
  create() {
    if (!this._sharedHandler) {
      this._sharedHandler = {
        set: (target, key, value) => {
          if (!this.rootTarget) {
            this.rootTarget = target;
          }
          return this.set(target, key, value);
        },
        get: (target, key, receiver) => {
          if (!this.rootTarget) {
            this.rootTarget = target;
          }
          return this.get(target, key, receiver);
        },
        ownKeys: (target) => {
          if (!this.rootTarget) {
            this.rootTarget = target;
          }
          return this.ownKeys(target);
        },
        getOwnPropertyDescriptor: (target, key) => {
          if (!this.rootTarget) {
            this.rootTarget = target;
          }
          return this.getOwnPropertyDescriptor(target, key);
        },
        deleteProperty: (target, key) => {
          if (!this.rootTarget) {
            this.rootTarget = target;
          }
          return this.deleteProperty(target, key);
        },
      };
    }
    return this._sharedHandler;
  }

  /**
   * Proxy 'set' trap.
   * @param {object} target - The target object.
   * @param {string|symbol} key - The property key.
   * @param {any} value - The new value.
   * @returns {boolean}
   */
  set(target, key, value) {
    if (value && value[RAW_SYMBOL]) {
      value = value[RAW_SYMBOL];
    }

    const isNewRootKey =
      this.onKeysChanged !== null &&
      target === this.rootTarget &&
      typeof key !== 'symbol' &&
      !Object.prototype.hasOwnProperty.call(target, key);

    const oldValue = target[key];
    const changed =
      typeof key !== 'symbol' &&
      (oldValue !== value || (Array.isArray(target) && key === 'length'));

    // Journaled before the assignment, because whether the key already existed
    // is the difference between restoring a value and deleting one, and that
    // cannot be told apart once the write has landed.
    if (changed && journal.active) {
      if (Array.isArray(target) && key === 'length') {
        // `arr.length = 0` discards elements that no single before/after pair
        // describes, so the whole array is saved instead.
        journal.recordCollection(target, this, 'array');
      } else {
        journal.recordWrite(
          target,
          key,
          oldValue,
          value,
          Object.prototype.hasOwnProperty.call(target, key),
          this
        );
      }
    }

    if (isReactiveTarget(oldValue)) {
      cleanupParentMap(oldValue);
    }

    target[key] = value;

    if (isReactiveTarget(value)) {
      parentMap.set(value, { parentTarget: target, parentKey: key });
    }

    // Persist configured root-level state values to localStorage.
    if (
      changed &&
      typeof key !== 'symbol' &&
      target === this.rootTarget &&
      this.persistKeys.has(key) &&
      typeof localStorage !== 'undefined'
    ) {
      try {
        localStorage.setItem(key, JSON.stringify(value));
      } catch {
        // Ignore storage and serialization errors so persistence
        // never breaks normal reactive state updates.
      }
    }

    if (isNewRootKey) {
      this.onKeysChanged();
    }

    if (changed) {
      const token = tracer.on ? traceWrite(target, key, oldValue, value) : -1;

      try {
        trigger(target, key, oldValue, value);
        this.notifyChange(target, key);
      } finally {
        if (token >= 0) {
          tracer.leave(token);
        }
      }
    }

    return true;
  }
  /**
   * Proxy 'get' trap.
   * Redirects to evaluateComputedWatcher if the key is a computed property.
   * @param {object} target - The target object.
   * @param {string|symbol} key - The property key.
   * @param {object} receiver - The proxy or object inheriting from the proxy.
   * @returns {any}
   */
  get(target, key, receiver) {
    if (key === IS_REACTIVE_PROXY) {
      return true;
    }
    if (key === RAW_SYMBOL) {
      return target;
    }
    if (key === PROXY_REF_SYMBOL) {
      return target[PROXY_REF_SYMBOL];
    }
    if (this.computedKeys.has(key)) {
      return this.evaluateComputedWatcher(key, receiver);
    }

    if (target instanceof Map) {
      return this.getMapMethod(target, key, receiver);
    }
    if (target instanceof Set) {
      return this.getSetMethod(target, key, receiver);
    }

    if (!(key in target) && this.instance && key in this.instance) {
      const val = this.instance[key];
      if (typeof val === 'function') {
        return val.bind(this.instance);
      }
      return val;
    }

    // Track property access
    if (typeof key !== 'symbol') {
      track(target, key);
    }

    const value = Reflect.get(target, key, receiver);
    if (typeof value === 'function') {
      if (Array.isArray(target) && mutatingArrayMethods.has(key)) {
        return (...args) => {
          // `push`, `splice` and `sort` have no single before-and-after pair to
          // journal, so the transaction keeps a copy of the array instead —
          // once, the first time it touches it.
          if (journal.active) {
            journal.recordCollection(target, this, `array.${String(key)}`);
          }
          const result = target[key](...args);
          // The array's contents are deliberately not captured here: cloning
          // the whole array on every push would make tracing a growing list
          // quadratic. The length after the call is enough to explain it.
          const token = tracer.on ? traceCollectionWrite(target, 'length', key, target.length) : -1;
          try {
            trigger(target, 'length');
            this.notifyChange(target, 'length');
          } finally {
            if (token >= 0) {
              tracer.leave(token);
            }
          }
          return result;
        };
      }
      return value.bind(receiver);
    }
    if (isReactiveTarget(value)) {
      parentMap.set(value, { parentTarget: target, parentKey: key });
      if (!this.bypassSymbol && value[PROXY_REF_SYMBOL]) {
        return value[PROXY_REF_SYMBOL];
      }
      return this.getOrCreateProxy(value);
    }
    return value;
  }

  /**
   * Proxy 'deleteProperty' trap.
   * @param {object} target - The target object.
   * @param {string|symbol} key - The property key.
   * @returns {boolean}
   */
  deleteProperty(target, key) {
    const hasKey = Reflect.has(target, key);
    const oldValue = target[key];

    if (hasKey && typeof key !== 'symbol' && journal.active) {
      journal.recordDelete(target, key, oldValue, Object.prototype.hasOwnProperty.call(target, key), this);
    }

    const result = Reflect.deleteProperty(target, key);

    if (isReactiveTarget(oldValue)) {
      cleanupParentMap(oldValue);
    }

    if (hasKey) {
      if (this.onKeysChanged !== null && target === this.rootTarget && typeof key !== 'symbol') {
        this.onKeysChanged();
      }
      const token = tracer.on ? traceWrite(target, key, oldValue, undefined, 'delete') : -1;
      try {
        trigger(target, key);
        this.notifyChange(target, key);
      } finally {
        if (token >= 0) {
          tracer.leave(token);
        }
      }
    }
    return result;
  }

  /**
   * Proxy 'ownKeys' trap.
   * Includes computed keys in the list of keys.
   * @param {object} target - The target object.
   * @returns {Array<string|symbol>}
   */
  ownKeys(target) {
    return [...Reflect.ownKeys(target), ...this.computedKeys];
  }

  /**
   * Proxy 'getOwnPropertyDescriptor' trap.
   * Ensures computed properties appear as own properties.
   * @param {object} target - The target object.
   * @param {string|symbol} key - The property key.
   * @returns {PropertyDescriptor|undefined}
   */
  getOwnPropertyDescriptor(target, key) {
    if (this.computedKeys.has(key)) {
      return { enumerable: true, configurable: true };
    }
    return Reflect.getOwnPropertyDescriptor(target, key);
  }

  /**
   * Returns a cached proxy or creates a new proxy for a nested object/array.
   * @param {object | Array} val - The nested object or array.
   * @returns {Proxy} The reactive proxy.
   * @private
   */
  getOrCreateProxy(val) {
    if (!this.bypassSymbol && val[PROXY_REF_SYMBOL]) {
      return val[PROXY_REF_SYMBOL];
    }
    if (val[IS_REACTIVE_PROXY]) {
      return val;
    }
    if (this.proxyCache.has(val)) {
      return this.proxyCache.get(val);
    }
    const proxy = new Proxy(val, this.create());
    this.proxyCache.set(val, proxy);
    if (!this.bypassSymbol && Object.isExtensible(val)) {
      Object.defineProperty(val, PROXY_REF_SYMBOL, {
        value: proxy,
        writable: false,
        enumerable: false,
        configurable: false,
      });
    }
    return proxy;
  }
}