Version 1.2.0
What's Changed
Added
stalehandler formatch(): BothMatchHandlers<T>andSingleMatchHandlers<T>now accept an optionalstale?: () => MaybePromise<MaybeCleanup>branch. It fires when all signals have a retained value but at least oneTasksignal is currently executing (isPending() === true). Routing precedence isnil>err>stale>ok; omittingstalefalls back took, showing the retained value unchanged while the task re-fetches. Any cleanup returned bystaleis registered on the owner and runs before the next handler dispatches — the right place to remove a refresh indicator or dim overlay. In React Query terms:nilmaps toisLoading(no data yet);stalemaps toisFetchingwith existing data.isSignalOfType<T>(value, type)utility: New exported function that replacesisObjectOfTypefor signal type guards. Checksvalue != null && value[Symbol.toStringTag] === typedirectly — zero string allocations, O(1). All eight internalis*()guards (isState,isMemo,isTask,isSensor,isSlot,isStore,isList,isCollection) now use it.DEEP_EQUALITYequality preset: New exported constant for deep structural comparison of plain objects and arrays. UsesObject.isas a fast path, then recursively compares array elements by index and own enumerable keys of plain-object records (Object.getPrototypeOf(v) === Object.prototype). Non-plain objects (class instances,Map,Set) are never structurally equal unless they are the same reference. Pass to theequalsoption to suppress propagation when a signal holding an object or array recomputes to a structurally identical value.DEFAULT_EQUALITYexported fromindex.ts: The===-based equality preset was already used internally throughout the library but was not part of the public API. It is now exported, allowing callers to restore the default explicitly when composing or selectively overridingSignalOptions.
Changed
isSignaluses a module-levelSetwith directSymbol.toStringTagaccess: Previously allocated two strings per call viaObject.prototype.toString.call(value).slice(8, -1)and scanned an inline array withArray.includes(). Now checksSIGNAL_TYPES.has(value[Symbol.toStringTag])— one hash lookup, zero allocations,Setbuilt once at module load.isRecorduses a prototype check instead ofObject.prototype.toString: PreviouslyObject.prototype.toString.call(value) === '[object Object]', which returnstruefor class instances without a customSymbol.toStringTag. Now checksObject.getPrototypeOf(value) === Object.prototype, which excludes class instances. AffectscreateSignalandcreateMutableSignal: a class instance with noSymbol.toStringTagpreviously resolved to aStore; now it falls through tocreateState. Class instances are not plain records, so this is the correct behavior.isEqual/DEEP_EQUALITYcycle detection removed: Previously, the deep equality function inlist.tsandstore.tsallocated aWeakSeton everyList.set()/Store.set()call, added both operands before recursing, and threwCircularDependencyErroron a circular reference. Thetry/finallyblock cleaned up theWeakSetentries after each call. All of this is removed — the implementation is now plain recursion (deepEqualingraph.ts) with no allocations. Circular data causes a stack overflow rather than a thrown error. Signal values are expected to be plain JSON-like data; circular references are a programming error.- Equality presets unified in
graph.ts:DEFAULT_EQUALITY,SKIP_EQUALITY, andDEEP_EQUALITYare all defined ingraph.tsalongsideSignalOptions. PreviouslyisEqual(the deep equality implementation) lived inlist.tsas a private function and was imported bystore.ts. Both files now importDEEP_EQUALITYfromgraph.ts; theCircularDependencyErrorimport inlist.tsis removed.
Deprecated
isObjectOfType(value, type): Marked@deprecated. Allocates two strings per call (Object.prototype.toString.call()plus a template literal). UseisSignalOfType(value, type)for signal type guards instead. The function remains exported for backward compatibility and will be removed in a future release.isEqual: Deprecated alias forDEEP_EQUALITY. Previously the private deep equality implementation inlist.ts, now re-exported fromindex.tsas a deprecated alias pointing toDEEP_EQUALITYingraph.ts. Replace all uses withDEEP_EQUALITY.
Fixed
createScopeeffect leak on throw: Previously, iffn()threw after creating child effects,disposewas never created or registered with the parent owner — child effects leaked and continued running indefinitely. Nowdisposeis created before thetryblock and registered withprevOwnerin thefinallyclause, so cleanup always executes regardless of whetherfn()throws.list.replace()spurious dependency edge: Previously, callingreplace()from inside an effect linked the item signal to the calling effect as a dependency (via the unguardedsignal.get()equality check). The effect re-ran — and permanently acquired the dependency — after eachreplace()call. Now the check usesuntrack(() => signal.get()), so no edge is created during the early-exit test.list.splice()signal corruption on same-key replace: Previously, splicing out an item and inserting a new item with the same content-based key left the key inkeysbut absent fromsignals—byKey()returnedundefinedsilently. Nowsplicedetects the key overlap and routes tochangeinstead of an add+remove pair.match()errcleanup silently dropped on thrown errors: Previously, the catch branch callederr([...])without capturing the return value — cleanup functions orPromise<MaybeCleanup>returned byerrwere silently discarded (memory leak in the error path). Nowout = err([...])captures the return value for cleanup registration, matching the try-branch behavior.
Full Changelog: v1.1.1...v1.2.0