@ -1,30 +1,397 @@
import { ReactiveFlags } from "./contents" ;
import { computed , Signal , signal } from "./core" ;
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 )
}
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 ) )
objToProxy . set ( obj , createProxy ( obj , objectHandlers ) as DeepSignal < T > ) ;
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 >
@ -36,40 +403,202 @@ export const peek = <
const value = obj [ key ] ;
try {
peeking = false ;
} catch ( e ) { }
} 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 > ) = > {
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." ) ;
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 ) ) ;
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 ( ) ) ;
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 (
@ -82,48 +611,85 @@ const get =
) ;
} else {
let value = Reflect . get ( target , key , receiver ) ;
if ( returnSignal && typeof value === "function" ) return ;
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 ) )
objToProxy . set ( value , createProxy ( value , objectHandlers ) ) ;
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
return returnSignal ? signals . get ( key ) : signals . get ( key ) . get ( ) ;
// 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 ( ! ( val instanceof Sign al) ) throwOnMutation ( ) ;
if ( ! isSignal ( v al) ) 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 ) )
objToProxy . set ( val , createProxy ( val , objectHandlers ) ) ;
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 ) ) signals . set ( fullKey , signal ( internal ) ) ;
else signals . get ( fullKey ) . set ( internal ) ;
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 ;
}
} ,
@ -133,6 +699,16 @@ const objectHandlers = {
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 ) [ ] {
@ -142,6 +718,7 @@ const objectHandlers = {
} ,
} ;
// Handlers for special `$` proxy wrapping an array (index signals only)
const arrayHandlers = {
get : get ( true ) ,
set : throwOnMutation ,
@ -150,10 +727,11 @@ const arrayHandlers = {
const wellKnownSymbols = new Set (
Object . getOwnPropertyNames ( Symbol )
. map ( key = > Symbol [ key as WellKnownSymbols ] )
. filter ( value = > typeof value === "symbol" )
. map ( ( key ) = > Symbol [ key as WellKnownSymbols ] )
. filter ( ( value ) = > typeof value === "symbol" )
) ;
const supported = new Set ( [ Object , Array ] ) ;
// 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 ) ;
@ -161,6 +739,11 @@ const shouldProxy = (val: any): boolean => {
/** 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 }
@ -171,133 +754,45 @@ export type DeepSignal<T> = T extends Function
? DeepSignalObject < T >
: T ;
type DeepSignalObject < T extends object > = {
/** 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
: Signal < T [ P ] > ;
: ReturnType < typeof signal < T [ P ] > > ;
} & {
[ P in keyof T ] : DeepSignal < T [ P ] > ;
} ;
/** @ts-expect-error **/
interface DeepArray < T > extends 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 ;
}
/** 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 ] : Signal < ArrayType < T > > } ;
$length? : Signal < number > ;
$ ? : { [ 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
type FilterSignals < K > = K extends ` $ ${ infer P } ` ? never : K ;
/** 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"