import { ReactiveFlags } from "./contents"; import { computed, signal, isSignal } from "./core"; /** * Implementation overview (supplementary to header docs): * * Data structures: * - proxyToSignals: WeakMap> maps each proxied object to its per-key signal. * - objToProxy: WeakMap ensures stable proxy instance per raw object. * - arrayToArrayOfSignals: WeakMap holds special `$` array meta proxy (index signals + length). * - ignore: WeakSet marks objects already proxied or shallow-wrapped to avoid double proxying. * - objToIterable: WeakMap> used to re-trigger enumeration dependent computed/effects. * - proxyMeta: WeakMap chain info for patch path reconstruction. * * Key decisions: * - Signals are created lazily: first read of value or its `$` accessor. * - Getter properties become computed signals (readonly) so derived values stay consistent. * - Setting via `$prop` enforces passing a signal (allows external signal assignment path). * - Deep patches reconstruct path on mutation (O(depth)); no bookkeeping per property upfront. * - Arrays use `$` (returns array-of-signals proxy) & `$length` (length signal) while skipping function-valued entries. * * Performance characteristics: * - Read of untouched property: ~O(1) creating one signal + potential child proxy. * - Mutation: signal update + optional patch path build; batching coalesces multiple patches in same microtask. * - Enumeration tracking: ownKeys/for..in increments a dedicated counter signal (objToIterable) to invalidate dependents. */ /** * deepSignal() * ===================================================================== * Core idea: Wrap a plain object/array tree in a Proxy that lazily creates * per-property signals on first access. Property access without a `$` prefix * returns the underlying value (tracking the read). Access with `$` (e.g. obj.$foo) * returns the signal function itself. Arrays have special `$` for index signals and * `$length` for the length signal. * * Why function signals? Native alien-signals v2 returns a function you call to read, * call with an argument to write. We keep that but provide `.value` in tagging layer. * * Getter logic summary: * 1. Determine if the caller wants a signal (prefix `$` or array meta) vs value. * 2. If property is a getter on the original object, wrap it in a computed signal. * 3. Otherwise create (once) a writable signal seeded with (possibly proxied) child value. * 4. Return either the signal function or its invocation (value) depending on caller form. * * Setter logic summary: * - Writes update the raw target via Reflect.set. * - If the property signal already exists, update it; otherwise create it. * - Array length & object key enumeration signals are nudged (length / ownKeys tracking). * - A deep mutation patch is queued capturing: root id, type, path, new value. * * Patch batching: * - Mutations push a patch into a per-microtask buffer (pendingPatches). * - A queued microtask flush delivers the accumulated array to each subscriber. * - This enables consumers (framework adapters) to materialize minimal changes. * * Metadata chain (proxyMeta): parent + key + root symbol; used to reconstruct full path * for patches without storing full paths on every node. */ /** * Deep mutation patch system (for Svelte rune integration / granular updates) * ------------------------------------------------------------------------- * This augmention adds an optional patch stream that reports every deep mutation * (set/delete) with a property path relative to the root deepSignal() object. * * Rationale: The core library already has per-property signals and is efficient * for effect re-execution. However, consumers that want to mirror the entire * nested structure into another reactive container (e.g. Svelte $state) gain * from receiving a batch of fine-grained patches instead of re-cloning. * * Design: * 1. Each proxy created by deepSignal has lightweight metadata (parent, key, root id). * 2. On each mutation (set/delete) we reconstruct the path by walking parents. * 3. Patches are batched in a microtask and delivered to subscribers. * 4. Zero cost for projects that never call subscribeDeepMutations(): only minimal * metadata storage and O(depth) path build per mutation. */ /** * A granular description of a deep mutation originating from a {@link deepSignal} root. * Patches are batched per microtask and delivered in order of occurrence. * * Invariants: * - `path` is never empty (the root object itself is not represented by a patch without a key) * - For `type === "delete"` the `value` field is omitted * - For `type === "set"` the `value` is the post‑mutation (proxied if object/array/set) value snapshot */ export interface DeepPatch { /** Unique identifier for the deep signal root which produced this patch. */ root: symbol; /** Mutation kind applied at the resolved `path`. */ type: "set" | "delete"; /** Property path (array indices, object keys, synthetic Set entry ids) from the root to the mutated location. */ path: (string | number)[]; /** New value for `set` mutations (omitted for `delete`). */ value?: any; } /** Function signature for subscribers passed to {@link subscribeDeepMutations}. */ export type DeepPatchSubscriber = (patches: DeepPatch[]) => void; /** * Lightweight metadata stored per proxy enabling reconstruction of a property's full path * at mutation time without eager bookkeeping of every descendant. */ interface ProxyMeta { /** Parent proxy in the object graph (undefined for root). */ parent?: object; /** Key within the parent pointing to this proxy (undefined for root). */ key?: string | number; /** Stable root id symbol shared by the entire deepSignal tree. */ root: symbol; } /** Internal lookup from proxy -> {@link ProxyMeta}. */ const proxyMeta = new WeakMap(); /** Global registry of batch mutation subscribers (filtered per root at delivery time). */ const mutationSubscribers = new Set(); let pendingPatches: DeepPatch[] | null = null; let microtaskScheduled = false; /** Sentinal constant for root id retrieval (exported for external helpers). */ /** * Sentinel symbol used internally / by helpers to retrieve a deepSignal root id. * You normally obtain a concrete root id via {@link getDeepSignalRootId} on a proxy instance. */ export const DEEP_SIGNAL_ROOT_ID = Symbol("alienDeepSignalRootId"); function buildPath( startProxy: object, leafKey: string | number ): (string | number)[] { const path: (string | number)[] = [leafKey]; let cur: object | undefined = startProxy; while (cur) { const meta = proxyMeta.get(cur); if (!meta) break; // Defensive: metadata should always exist. if (meta.key === undefined) break; // Reached root (no key recorded). path.unshift(meta.key); cur = meta.parent; } return path; } function queuePatch(patch: DeepPatch) { if (!pendingPatches) pendingPatches = []; pendingPatches.push(patch); if (!microtaskScheduled) { microtaskScheduled = true; queueMicrotask(() => { microtaskScheduled = false; const batch = pendingPatches; pendingPatches = null; if (!batch || batch.length === 0) return; mutationSubscribers.forEach((sub) => sub(batch)); }); } } /** * Register a mutation batch listener for all active deepSignal roots. * * Each microtask in which one or more deep mutations occur produces at most one callback * invocation containing the ordered list of {@link DeepPatch} objects. * * @param sub Callback receiving a batch array (never empty) of deep patches. * @returns An unsubscribe function that detaches the listener when invoked. */ export function subscribeDeepMutations(sub: DeepPatchSubscriber): () => void { mutationSubscribers.add(sub); return () => mutationSubscribers.delete(sub); } /** * Obtain the stable root id symbol for a given deepSignal proxy (or any nested proxy). * Returns `undefined` if the value is not a deepSignal-managed proxy. * * @example * const state = deepSignal({ a: { b: 1 } }); * const id1 = getDeepSignalRootId(state); // symbol * const id2 = getDeepSignalRootId(state.a); // same symbol as id1 * getDeepSignalRootId({}) // undefined */ export function getDeepSignalRootId(obj: any): symbol | undefined { return proxyMeta.get(obj)?.root; } // Proxy -> Map of property name -> signal function /** Proxy instance -> map of property name -> signal function (created lazily). */ const proxyToSignals = new WeakMap(); // Raw object/array/Set -> its stable proxy const objToProxy = new WeakMap(); // Raw array -> special `$` proxy giving index signals const arrayToArrayOfSignals = new WeakMap(); // Objects already proxied or intentionally shallow const ignore = new WeakSet(); // Object -> signal counter used for key enumeration invalidation const objToIterable = new WeakMap(); const rg = /^\$/; const descriptor = Object.getOwnPropertyDescriptor; let peeking = false; // (Synthetic ID helper declarations were restored further below before usage.) /** * Deep array interface expressed as a type intersection to avoid structural extend * incompatibilities with the native Array while still refining callback parameter * types to their DeepSignal forms. */ type DeepArray = Array & { map: ( callbackfn: ( value: DeepSignal, index: number, array: DeepSignalArray ) => U, thisArg?: any ) => U[]; forEach: ( callbackfn: ( value: DeepSignal, index: number, array: DeepSignalArray ) => void, thisArg?: any ) => void; concat(...items: ConcatArray[]): DeepSignalArray; concat(...items: (T | ConcatArray)[]): DeepSignalArray; reverse(): DeepSignalArray; shift(): DeepSignal | undefined; slice(start?: number, end?: number): DeepSignalArray; splice(start: number, deleteCount?: number): DeepSignalArray; splice( start: number, deleteCount: number, ...items: T[] ): DeepSignalArray; filter( predicate: ( value: DeepSignal, index: number, array: DeepSignalArray ) => value is DeepSignal, thisArg?: any ): DeepSignalArray; filter( predicate: ( value: DeepSignal, index: number, array: DeepSignalArray ) => unknown, thisArg?: any ): DeepSignalArray; reduce( callbackfn: ( previousValue: DeepSignal, currentValue: DeepSignal, currentIndex: number, array: DeepSignalArray ) => T ): DeepSignal; reduce( callbackfn: ( previousValue: DeepSignal, currentValue: DeepSignal, currentIndex: number, array: DeepSignalArray ) => DeepSignal, initialValue: T ): DeepSignal; reduce( callbackfn: ( previousValue: U, currentValue: DeepSignal, currentIndex: number, array: DeepSignalArray ) => U, initialValue: U ): U; reduceRight( callbackfn: ( previousValue: DeepSignal, currentValue: DeepSignal, currentIndex: number, array: DeepSignalArray ) => T ): DeepSignal; reduceRight( callbackfn: ( previousValue: DeepSignal, currentValue: DeepSignal, currentIndex: number, array: DeepSignalArray ) => DeepSignal, initialValue: T ): DeepSignal; reduceRight( callbackfn: ( previousValue: U, currentValue: DeepSignal, currentIndex: number, array: DeepSignalArray ) => U, initialValue: U ): U; }; // --- Synthetic ID helpers & ergonomics for Set entry patching (restored) --- let __blankNodeCounter = 0; /** User or auto-assigned synthetic id bookkeeping for Set entry objects. */ const setObjectIds = new WeakMap(); const assignBlankNodeId = (obj: any) => { if (setObjectIds.has(obj)) return setObjectIds.get(obj)!; const id = `_b${++__blankNodeCounter}`; setObjectIds.set(obj, id); return id; }; /** Assign (or override) synthetic identifier for an object prior to Set.add(). */ export function setSetEntrySyntheticId(obj: object, id: string | number) { setObjectIds.set(obj, String(id)); } const getSetEntryKey = (val: any): string | number => { if (val && typeof val === "object") { if (setObjectIds.has(val)) return setObjectIds.get(val)!; if ( typeof (val as any).id === "string" || typeof (val as any).id === "number" ) return (val as any).id; if ( typeof (val as any)["@id"] === "string" || typeof (val as any)["@id"] === "number" ) return (val as any)["@id"]; return assignBlankNodeId(val); } return val as any; }; /** * Insert into a (possibly proxied) Set with a desired synthetic id; returns proxied entry (objects) or primitive. */ export function addWithId( set: Set, entry: T, id: string | number ): DeepSignal; export function addWithId(set: Set, entry: T, id: string | number): T; export function addWithId(set: Set, entry: any, id: string | number) { if (entry && typeof entry === "object") setSetEntrySyntheticId(entry, id); (set as any).add(entry); if (entry && typeof entry === "object" && objToProxy.has(entry)) return objToProxy.get(entry); return entry; } /** Determine whether a given value is a deepSignal-managed proxy (any depth). */ export const isDeepSignal = (source: any) => { return proxyToSignals.has(source); }; /** Predicate indicating whether a value was explicitly marked via {@link shallow}. */ export const isShallow = (source: any) => { return ignore.has(source); }; /** * Create (or retrieve) a deep reactive proxy for the supplied plain object / array / Set. * All nested objects / arrays / Sets are wrapped lazily on first access; primitives are passed through. * * Root identity: multiple invocations with the same object return the same proxy; each distinct root * owns a unique symbol id available via {@link getDeepSignalRootId} and present on every emitted patch. * */ export const deepSignal = (obj: T): DeepSignal => { if (!shouldProxy(obj)) throw new Error("This object can't be observed."); if (!objToProxy.has(obj)) { // Create a unique root id symbol to identify this deep signal tree in patches. const rootId = Symbol("deepSignalRoot"); const proxy = createProxy(obj, objectHandlers, rootId) as DeepSignal; const meta = proxyMeta.get(proxy)!; meta.parent = undefined; // root has no parent meta.key = undefined; // root not addressed by a key meta.root = rootId; // ensure root id stored (explicit) // Pre-register an empty signals map so isDeepSignal() is true before any property access. if (!proxyToSignals.has(proxy)) proxyToSignals.set(proxy, new Map()); objToProxy.set(obj, proxy); } return objToProxy.get(obj); }; /** * Read a property on a deepSignal proxy without establishing reactive tracking. * Equivalent conceptually to a non-tracking read / untracked() pattern. * * @param obj deepSignal proxy object * @param key property key to read * @returns The raw (possibly proxied) property value without dependency collection. */ export const peek = < T extends DeepSignalObject, K extends keyof RevertDeepSignalObject >( obj: T, key: K ): RevertDeepSignal[K]> => { peeking = true; const value = obj[key]; try { peeking = false; } catch (e) {} return value as RevertDeepSignal[K]>; }; const shallowFlag = Symbol(ReactiveFlags.IS_SHALLOW); /** * Mark an object so that it will not be deeply proxied when used as a property value of * another deepSignal tree. This is a shallow escape hatch for performance or semantic reasons. * * NOTE: The returned object itself is not reactive; only top-level assignment of the reference * produces patches when attached to a deepSignal structure. */ export function shallow(obj: T): Shallow { ignore.add(obj); return obj as Shallow; } const createProxy = ( target: object, handlers: ProxyHandler, rootId?: symbol ) => { const proxy = new Proxy(target, handlers); ignore.add(proxy); // Initialize proxy metadata if not present. Root proxies provide a stable root id. if (!proxyMeta.has(proxy)) { proxyMeta.set(proxy, { root: rootId || Symbol("deepSignalDetachedRoot") }); } return proxy; }; const throwOnMutation = () => { throw new Error( "Don't mutate the signals directly (use the underlying property/value instead)." ); }; /** * Unified get trap factory. * @param isArrayOfSignals indicates we are resolving properties on the special array `$` proxy. */ const get = (isArrayOfSignals: boolean) => (target: object, fullKey: string, receiver: object): unknown => { if (peeking) return Reflect.get(target, fullKey, receiver); let returnSignal = isArrayOfSignals || fullKey[0] === "$"; // Special handling for Set instances: treat as atomic & emit structural + per-entry patches if (target instanceof Set && typeof fullKey === "string") { const raw = target as Set; const key = fullKey; const meta = proxyMeta.get(receiver); // Helper to proxy a single entry const ensureEntryProxy = (entry: any) => { if ( entry && typeof entry === "object" && shouldProxy(entry) && !objToProxy.has(entry) ) { const synthetic = getSetEntryKey(entry); const childProxy = createProxy(entry, objectHandlers, meta!.root); const childMeta = proxyMeta.get(childProxy)!; childMeta.parent = receiver; childMeta.key = synthetic; objToProxy.set(entry, childProxy); return childProxy; } if (objToProxy.has(entry)) return objToProxy.get(entry); return entry; }; // Pre-pass to ensure any existing non-proxied object entries are proxied if (meta) raw.forEach(ensureEntryProxy); if (key === "add" || key === "delete" || key === "clear") { const fn: Function = (raw as any)[key]; return function (this: any, ...args: any[]) { const sizeBefore = raw.size; const result = fn.apply(raw, args); if (raw.size !== sizeBefore) { const metaNow = proxyMeta.get(receiver); if ( metaNow && metaNow.parent !== undefined && metaNow.key !== undefined ) { const containerPath = buildPath(metaNow.parent, metaNow.key); if (key === "add") { const entry = args[0]; let synthetic = getSetEntryKey(entry); if (entry && typeof entry === "object") { for (const existing of raw.values()) { if (existing === entry) continue; if (getSetEntryKey(existing) === synthetic) { synthetic = assignBlankNodeId(entry); break; } } } let entryVal = entry; if ( entryVal && typeof entryVal === "object" && shouldProxy(entryVal) && !objToProxy.has(entryVal) ) { const childProxy = createProxy( entryVal, objectHandlers, metaNow.root ); const childMeta = proxyMeta.get(childProxy)!; childMeta.parent = receiver; childMeta.key = synthetic; objToProxy.set(entryVal, childProxy); entryVal = childProxy; } queuePatch({ root: metaNow.root, type: "set", path: [...containerPath, synthetic], value: entryVal, }); } else if (key === "delete") { const entry = args[0]; const synthetic = getSetEntryKey(entry); queuePatch({ root: metaNow.root, type: "delete", path: [...containerPath, synthetic], }); } else if (key === "clear") { queuePatch({ root: metaNow.root, type: "set", path: containerPath, value: raw, }); } } } return result; }; } const makeIterator = (pair: boolean) => { return function thisIter(this: any) { const iterable = raw.values(); return { [Symbol.iterator]() { return { next() { const n = iterable.next(); if (n.done) return n; const entry = ensureEntryProxy(n.value); return { value: pair ? [entry, entry] : entry, done: false }; }, }; }, } as Iterable; }; }; if (key === "values" || key === "keys") return makeIterator(false); if (key === "entries") return makeIterator(true); if (key === "forEach") { return function thisForEach(this: any, cb: any, thisArg?: any) { raw.forEach((entry: any) => { cb.call( thisArg, ensureEntryProxy(entry), ensureEntryProxy(entry), raw ); }); }; } if (key === Symbol.iterator.toString()) { // string form access of iterator symbol; pass through } const val = Reflect.get(raw, key, raw); if (typeof val === "function") return val.bind(raw); return val; } if (!isArrayOfSignals && returnSignal && Array.isArray(target)) { if (fullKey === "$") { if (!arrayToArrayOfSignals.has(target)) arrayToArrayOfSignals.set( target, createProxy( target, arrayHandlers, proxyMeta.get(receiver)?.root // propagate root id to $ array proxy ) ); return arrayToArrayOfSignals.get(target); } returnSignal = fullKey === "$length"; } if (!proxyToSignals.has(receiver)) proxyToSignals.set(receiver, new Map()); // allocate map lazily const signals = proxyToSignals.get(receiver); const key = returnSignal ? fullKey.replace(rg, "") : fullKey; if ( !signals.has(key) && typeof descriptor(target, key)?.get === "function" ) { signals.set( key, computed(() => Reflect.get(target, key, receiver)) ); } else { let value = Reflect.get(target, key, receiver); if (returnSignal && typeof value === "function") return; // functions never wrapped as signals if (typeof key === "symbol" && wellKnownSymbols.has(key)) return value; if (!signals.has(key)) { if (shouldProxy(value)) { if (!objToProxy.has(value)) { // Child object/array lazily wrapped: link to parent for path reconstruction. const parentMeta = proxyMeta.get(receiver)!; const childProxy = createProxy( value, objectHandlers, parentMeta.root ); const childMeta = proxyMeta.get(childProxy)!; childMeta.parent = receiver; childMeta.key = key as string; objToProxy.set(value, childProxy); } value = objToProxy.get(value); } signals.set(key, signal(value)); } } // deep getter: function signals are callable; non-signal access returns current value. // We intentionally return the raw function (signal) when `$`-prefixed so callers can set `.value` or invoke. const sig = signals.get(key); return returnSignal ? sig : sig(); }; // Handlers for standard object/array (non `$` array meta proxy) const objectHandlers = { get: get(false), set(target: object, fullKey: string, val: any, receiver: object): boolean { // Respect original getter/setter semantics if (typeof descriptor(target, fullKey)?.set === "function") return Reflect.set(target, fullKey, val, receiver); if (!proxyToSignals.has(receiver)) proxyToSignals.set(receiver, new Map()); const signals = proxyToSignals.get(receiver); if (fullKey[0] === "$") { if (!isSignal(val)) throwOnMutation(); const key = fullKey.replace(rg, ""); signals.set(key, val); return Reflect.set(target, key, val.peek(), receiver); } else { let internal = val; if (shouldProxy(val)) { if (!objToProxy.has(val)) { // Link newly wrapped child to its parent for path reconstruction. const parentMeta = proxyMeta.get(receiver)!; const childProxy = createProxy(val, objectHandlers, parentMeta.root); const childMeta = proxyMeta.get(childProxy)!; childMeta.parent = receiver; childMeta.key = fullKey; objToProxy.set(val, childProxy); } internal = objToProxy.get(val); } const isNew = !(fullKey in target); const result = Reflect.set(target, fullKey, val, receiver); if (!signals.has(fullKey)) { // First write after structure change -> create signal. signals.set(fullKey, signal(internal)); } else { // Subsequent writes -> update underlying signal. signals.get(fullKey).set(internal); } if (isNew && objToIterable.has(target)) objToIterable.get(target).value++; if (Array.isArray(target) && signals.has("length")) signals.get("length").set(target.length); // Emit patch (after mutation) so subscribers get final value snapshot. const meta = proxyMeta.get(receiver); if (meta) { queuePatch({ root: meta.root, type: "set", path: buildPath(receiver, fullKey), value: val, }); } return result; } }, deleteProperty(target: object, key: string): boolean { if (key[0] === "$") throwOnMutation(); const signals = proxyToSignals.get(objToProxy.get(target)); const result = Reflect.deleteProperty(target, key); if (signals && signals.has(key)) signals.get(key).value = undefined; objToIterable.has(target) && objToIterable.get(target).value++; // Emit deletion patch const receiverProxy = objToProxy.get(target); const meta = receiverProxy && proxyMeta.get(receiverProxy); if (meta) { queuePatch({ root: meta.root, type: "delete", path: buildPath(receiverProxy, key), }); } return result; }, ownKeys(target: object): (string | symbol)[] { if (!objToIterable.has(target)) objToIterable.set(target, signal(0)); (objToIterable as any)._ = objToIterable.get(target).get(); return Reflect.ownKeys(target); }, }; // Handlers for special `$` proxy wrapping an array (index signals only) const arrayHandlers = { get: get(true), set: throwOnMutation, deleteProperty: throwOnMutation, }; const wellKnownSymbols = new Set( Object.getOwnPropertyNames(Symbol) .map((key) => Symbol[key as WellKnownSymbols]) .filter((value) => typeof value === "symbol") ); // Support Set so structural mutations can emit patches (Map still unsupported for now) const supported = new Set([Object, Array, Set]); const shouldProxy = (val: any): boolean => { if (typeof val !== "object" || val === null) return false; return supported.has(val.constructor) && !ignore.has(val); }; /** TYPES **/ /** * Structural deep reactive view of an input type. Functions and values marked with {@link Shallow} * are passed through untouched; arrays and plain objects become recursively deep-signal aware; * Sets are proxied so structural & deep entry mutations emit patches. */ export type DeepSignal = T extends Function ? T : T extends { [shallowFlag]: true } ? T : T extends Array ? DeepSignalArray : T extends object ? DeepSignalObject : T; /** Recursive mapped type converting an object graph into its deepSignal proxy shape. */ export type DeepSignalObject = { [P in keyof T & string as `$${P}`]?: T[P] extends Function ? never : ReturnType>; } & { [P in keyof T]: DeepSignal; }; /** Extract element type from an array. */ type ArrayType = T extends Array ? I : T; /** DeepSignal-enhanced array type (numeric indices & `$` meta accessors). */ type DeepSignalArray = DeepArray> & { [key: number]: DeepSignal>; $?: { [key: number]: ReturnType>> }; $length?: ReturnType>; }; /** Marker utility type for objects passed through without deep proxying. */ export type Shallow = T & { [shallowFlag]: true }; /** Framework adapter hook (declared for consumers) that returns a {@link DeepSignal} proxy. */ export declare const useDeepSignal: (obj: T) => DeepSignal; // @ts-ignore /** Utility: strip `$`-prefixed synthetic signal accessors from key union. */ type FilterSignals = K extends `$${string}` ? never : K; /** Reverse of {@link DeepSignalObject}: remove signal accessors to obtain original object shape. */ type RevertDeepSignalObject = Pick>; /** Reverse of {@link DeepSignalArray}: omit meta accessors. */ type RevertDeepSignalArray = Omit; /** Inverse mapped type that removes deepSignal wrapper affordances (`$` accessors). */ export type RevertDeepSignal = T extends Array ? RevertDeepSignalArray : T extends object ? RevertDeepSignalObject : T; /** Subset of ECMAScript well-known symbols we explicitly pass through without proxy wrapping. */ type WellKnownSymbols = | "asyncIterator" | "hasInstance" | "isConcatSpreadable" | "iterator" | "match" | "matchAll" | "replace" | "search" | "species" | "split" | "toPrimitive" | "toStringTag" | "unscopables";