Skip to content

Version 1.4.0

Choose a tag to compare

@estherbrunner estherbrunner released this 10 Jul 22:28
· 69 commits to main since this release
2c8af38

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 explicit exports map (.: typesbundefault) replaces the bare main/module pair: the "bun" condition resolves TypeScript source directly for Bun consumers, while "default" serves the bundled index.js to 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. A files allowlist replaces the fragile .npmignore negation patterns; the unused index.dev.js artifact is removed. Migration: This is a minor version bump. The exports map blocks deep imports into package internals — code relying on these must switch to named imports from the package root. If your toolchain read the module field to consume TypeScript source, switch to the "bun" exports condition or import the bundled index.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), a TypeError subclass thrown when a Store property 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), or store.remove(key)). See ADR-0017.

Fixed

  • Direct property assignment on a Store proxy silently corrupted the store (src/nodes/store.ts): Previously, the proxy defined no set, deleteProperty, or defineProperty traps — so store.name = 'Bob' wrote the raw value onto the proxy target, shadowing the child State signal. From then on store.name returned the raw value while store.get() returned the reactive value, diverging silently. Now the three traps throw InvalidStoreMutationError, leaving store.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 of flush() immediately — effects queued after it were silently skipped, and the throwing effect was left with FLAG_RUNNING set, causing its next refresh() to throw a spurious CircularDependencyError. Now flush() catches per-effect errors, drains the entire queue, and rethrows: a single error as-is (identity preserved), multiple errors wrapped in AggregateError. runEffect() clears FLAG_RUNNING in its finally, 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 with FLAG_CLEAN after 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. Now flush() drains queuedEffects in passes over snapshots (effects re-queued during a pass run in the next pass), propagate() preserves FLAG_RUNNING on effects, and runEffect() preserves FLAG_DIRTY/FLAG_CHECK from 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 internal scheduleEffect(), which also removes a re-entrant runEffect hazard.
  • 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 one flush(), growing the queue until heap exhaustion. Now the flush-pass cap (1000) converts this into an EffectConvergenceError while sibling effects still run.
  • List and Store mutation 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(), and createCollection's onChanges change branch read child item signals without untrack(). 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 in untrack(), matching the pattern already used by Store.set() and List.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