Fork of https://github.com/CCherry07/alien-deepsignals ported to alien-signals v2 with support for per-value modification tracking.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
alien-deepsignals-fork/src/deepSignal.ts

809 lines
30 KiB

import { ReactiveFlags } from "./contents";
import { computed, signal, isSignal } from "./core";
/**
* Implementation overview (supplementary to header docs):
*
* Data structures:
* - proxyToSignals: WeakMap<proxy, Map<prop, signalFn>> maps each proxied object to its per-key signal.
* - objToProxy: WeakMap<rawObject, proxy> ensures stable proxy instance per raw object.
* - arrayToArrayOfSignals: WeakMap<array, proxy> holds special `$` array meta proxy (index signals + length).
* - ignore: WeakSet marks objects already proxied or shallow-wrapped to avoid double proxying.
* - objToIterable: WeakMap<object, signal<number>> used to re-trigger enumeration dependent computed/effects.
* - proxyMeta: WeakMap<proxy, { parent, key, root }> 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<object, ProxyMeta>();
/** Global registry of batch mutation subscribers (filtered per root at delivery time). */
const mutationSubscribers = new Set<DeepPatchSubscriber>();
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<T> = Array<T> & {
map: <U>(
callbackfn: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => U,
thisArg?: any
) => U[];
forEach: (
callbackfn: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => void,
thisArg?: any
) => void;
concat(...items: ConcatArray<T>[]): DeepSignalArray<T[]>;
concat(...items: (T | ConcatArray<T>)[]): DeepSignalArray<T[]>;
reverse(): DeepSignalArray<T[]>;
shift(): DeepSignal<T> | undefined;
slice(start?: number, end?: number): DeepSignalArray<T[]>;
splice(start: number, deleteCount?: number): DeepSignalArray<T[]>;
splice(
start: number,
deleteCount: number,
...items: T[]
): DeepSignalArray<T[]>;
filter<S extends T>(
predicate: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => value is DeepSignal<S>,
thisArg?: any
): DeepSignalArray<S[]>;
filter(
predicate: (
value: DeepSignal<T>,
index: number,
array: DeepSignalArray<T[]>
) => unknown,
thisArg?: any
): DeepSignalArray<T[]>;
reduce(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => T
): DeepSignal<T>;
reduce(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => DeepSignal<T>,
initialValue: T
): DeepSignal<T>;
reduce<U>(
callbackfn: (
previousValue: U,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => U,
initialValue: U
): U;
reduceRight(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => T
): DeepSignal<T>;
reduceRight(
callbackfn: (
previousValue: DeepSignal<T>,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => DeepSignal<T>,
initialValue: T
): DeepSignal<T>;
reduceRight<U>(
callbackfn: (
previousValue: U,
currentValue: DeepSignal<T>,
currentIndex: number,
array: DeepSignalArray<T[]>
) => 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<object, string>();
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<T extends object>(
set: Set<T>,
entry: T,
id: string | number
): DeepSignal<T>;
export function addWithId<T>(set: Set<T>, entry: T, id: string | number): T;
export function addWithId(set: Set<any>, 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 = <T extends object>(obj: T): DeepSignal<T> => {
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<T>;
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<object>,
K extends keyof RevertDeepSignalObject<T>
>(
obj: T,
key: K
): RevertDeepSignal<RevertDeepSignalObject<T>[K]> => {
peeking = true;
const value = obj[key];
try {
peeking = false;
} catch (e) {}
return value as RevertDeepSignal<RevertDeepSignalObject<T>[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<T extends object>(obj: T): Shallow<T> {
ignore.add(obj);
return obj as Shallow<T>;
}
const createProxy = (
target: object,
handlers: ProxyHandler<object>,
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<any>;
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<any>;
};
};
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> = T extends Function
? T
: T extends { [shallowFlag]: true }
? T
: T extends Array<unknown>
? DeepSignalArray<T>
: T extends object
? DeepSignalObject<T>
: T;
/** Recursive mapped type converting an object graph into its deepSignal proxy shape. */
export type DeepSignalObject<T extends object> = {
[P in keyof T & string as `$${P}`]?: T[P] extends Function
? never
: ReturnType<typeof signal<T[P]>>;
} & {
[P in keyof T]: DeepSignal<T[P]>;
};
/** Extract element type from an array. */
type ArrayType<T> = T extends Array<infer I> ? I : T;
/** DeepSignal-enhanced array type (numeric indices & `$` meta accessors). */
type DeepSignalArray<T> = DeepArray<ArrayType<T>> & {
[key: number]: DeepSignal<ArrayType<T>>;
$?: { [key: number]: ReturnType<typeof signal<ArrayType<T>>> };
$length?: ReturnType<typeof signal<number>>;
};
/** Marker utility type for objects passed through without deep proxying. */
export type Shallow<T extends object> = T & { [shallowFlag]: true };
/** Framework adapter hook (declared for consumers) that returns a {@link DeepSignal} proxy. */
export declare const useDeepSignal: <T extends object>(obj: T) => DeepSignal<T>;
// @ts-ignore
/** Utility: strip `$`-prefixed synthetic signal accessors from key union. */
type FilterSignals<K> = K extends `$${string}` ? never : K;
/** Reverse of {@link DeepSignalObject}: remove signal accessors to obtain original object shape. */
type RevertDeepSignalObject<T> = Pick<T, FilterSignals<keyof T>>;
/** Reverse of {@link DeepSignalArray}: omit meta accessors. */
type RevertDeepSignalArray<T> = Omit<T, "$" | "$length">;
/** Inverse mapped type that removes deepSignal wrapper affordances (`$` accessors). */
export type RevertDeepSignal<T> = T extends Array<unknown>
? RevertDeepSignalArray<T>
: T extends object
? RevertDeepSignalObject<T>
: 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";