Version 1.4.0
What's Changed
Changed
- Package distribution metadata overhauled (
package.json): The published entry point is now the unminified ESM bundle (index.js) instead of a minified artifact — consumers' bundlers minify anyway, and readable source improves debugging and bug reports. An explicitexportsmap (.:types→bun→default) replaces the baremain/modulepair: the"bun"condition resolves TypeScript source directly for Bun consumers, while"default"serves the bundledindex.jsto all other toolchains. The"module": "index.ts"field is removed — it pointed at raw TypeScript, which broke webpack and older Rollup configs. TypeScript is now an optional peer dependency (peerDependenciesMeta), so JS-only consumers no longer get an unresolvable-peer warning. Afilesallowlist replaces the fragile.npmignorenegation patterns; the unusedindex.dev.jsartifact is removed. Migration: This is a minor version bump. Theexportsmap blocks deep imports into package internals — code relying on these must switch to named imports from the package root. If your toolchain read themodulefield to consume TypeScript source, switch to the"bun"exports condition or import the bundledindex.js.
Added
EffectConvergenceError: New error class (exported from the package root), thrown when queued effects keep re-triggering each other without settling within 1000 flush passes. Typical triggers: an effect that unconditionally writes a signal it reads (createEffect(() => count.set(count.get() + 1))), or two effects that write each other's dependencies. The error surfaces synchronously from the call that triggered the runaway; other queued effects still run before it is thrown.InvalidStoreMutationError: New error class (exported from the package root), aTypeErrorsubclass thrown when aStoreproperty is assigned, deleted, or defined directly via the proxy (store.name = 'Bob',delete store.name,Object.defineProperty(store, …),Object.assign(store, …)). The message names the correct reactive alternative (store.key.set(value),store.set(next),store.add(key, value), orstore.remove(key)). See ADR-0017.
Fixed
- Direct property assignment on a
Storeproxy silently corrupted the store (src/nodes/store.ts): Previously, the proxy defined noset,deleteProperty, ordefinePropertytraps — sostore.name = 'Bob'wrote the raw value onto the proxy target, shadowing the childStatesignal. From then onstore.namereturned the raw value whilestore.get()returned the reactive value, diverging silently. Now the three traps throwInvalidStoreMutationError, leavingstore.get()and the child signal intact. Migration: code that assigned through the proxy must use the reactive API (store.key.set(),store.set(),store.add(),store.remove()). This is a minor version bump — the throw replaces previously-undetected silent corruption. See ADR-0017. - A throwing effect no longer skips sibling effects in the same flush (
src/graph.ts): Previously, an exception from one effect propagated out offlush()immediately — effects queued after it were silently skipped, and the throwing effect was left withFLAG_RUNNINGset, causing its nextrefresh()to throw a spuriousCircularDependencyError. Nowflush()catches per-effect errors, drains the entire queue, and rethrows: a single error as-is (identity preserved), multiple errors wrapped inAggregateError.runEffect()clearsFLAG_RUNNINGin itsfinally, so a previously-throwing effect re-runs normally on the next update. - Effects that write signals they depend on now converge (
src/graph.ts,src/nodes/effect.ts): Previously,runEffect()overwrote the node's flags withFLAG_CLEANafter running, clobbering the dirty re-mark set by the effect's own write — so a converging clamp effect (if (v > 10) s.set(10)) ended one run stale, having rendered the pre-clamp value. Nowflush()drainsqueuedEffectsin passes over snapshots (effects re-queued during a pass run in the next pass),propagate()preservesFLAG_RUNNINGon effects, andrunEffect()preservesFLAG_DIRTY/FLAG_CHECKfrom the effect's own writes — a self-writing effect re-runs until the graph settles and its last run observes the final signal values. Creation-time self-writes converge via a new internalscheduleEffect(), which also removes a re-entrantrunEffecthazard. - Mutual effect writes no longer hang the process (
src/graph.ts): Previously, two effects writing each other's dependencies re-queued each other forever inside oneflush(), growing the queue until heap exhaustion. Now the flush-pass cap (1000) converts this into anEffectConvergenceErrorwhile sibling effects still run. ListandStoremutation methods leaked dependency edges into the caller's effect (src/nodes/list.ts,src/nodes/store.ts,src/nodes/collection.ts):List.set(),List.sort(),List.splice(),List.update(),Store.update(), andcreateCollection'sonChangeschange branch read child item signals withoutuntrack(). Calling any of them inside an effect silently subscribed that effect to every item signal touched — causing over-broad re-runs on unrelated item mutations (persistent leak) or a spurious one-time re-run during setup (transient leak). These methods now wrap reads inuntrack(), matching the pattern already used byStore.set()andList.replace(). The public read APIs (get(),at(),byKey(),keys(),length, iterator) remain deliberately tracking per ADR-0015.
Full Changelog: v1.3.4...v1.4.0