Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 0 additions & 52 deletions .npmignore

This file was deleted.

18 changes: 11 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# Changelog

## [Unreleased]
## 1.4.0

### 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 (`.`: `types` → `bun` → `default`) 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 `set()`/`update()`/`batch()`/`createEffect()` call that triggered the runaway; other queued effects still run before it is thrown.
- **`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](adr/0017-store-proxy-rejects-direct-writes.md).

### 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 object, shadowing the child `State` signal. From then on, `store.name` returned the raw value while `store.get()` returned the reactive value, and the two diverged silently. `delete store.name` similarly bypassed `remove()` semantics. Now the three traps throw `InvalidStoreMutationError`, leaving `store.get()` and the child signal intact. **Migration:** code that previously assigned through the proxy (and silently corrupted state) must use the reactive API (`store.key.set()`, `store.set()`, `store.add()`, `store.remove()`). This is a **minor** version bump, not a patch — the throw replaces previously-undetected silent corruption. See [ADR-0017](adr/0017-store-proxy-rejects-direct-writes.md).
- **A throwing effect no longer skips sibling effects in the same flush** (`src/graph.ts`): Previously, an exception from one effect's callback propagated out of `flush()` immediately — all effects queued after it were silently skipped (their DOM updates and subscriptions lost until some arbitrary later flush), and the throwing effect was left stuck with `FLAG_RUNNING` set, so its next `refresh()` threw a spurious `CircularDependencyError`. Now `flush()` catches per-effect errors, drains the entire queue, and rethrows after: a single error is rethrown as-is (error identity preserved for existing `catch` code), multiple errors are wrapped in an `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()` unconditionally overwrote the node's flags with `FLAG_CLEAN` after running, clobbering the dirty re-mark set by the effect's own write — so even a converging clamp effect (`if (v > 10) s.set(10)`) ended one run stale, having rendered the pre-clamp value while the signal held the clamped one; two subscribers of the same signal could disagree. 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 always observes the final signal values. Creation-time self-writes converge through the same path via a new internal `scheduleEffect()`, which also removes a re-entrant `runEffect` hazard (a write during an effect's creation run previously re-entered the still-running effect via the nested flush).
- **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 — there was no loop guard. Now the flush-pass cap (1000) converts this into a loud `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, removed by `trimSources` on the next run). `Store.set()` and `List.replace()` already had the correct `untrack` pattern; these methods now match it. The public read APIs (`get()`, `at()`, `byKey()`, `keys()`, `length`, iterator) remain deliberately tracking per [ADR-0015](adr/0015-composite-lookup-methods-track-structural-changes.md).
- **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](adr/0017-store-proxy-rejects-direct-writes.md).
- **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](adr/0015-composite-lookup-methods-track-structural-changes.md).

## 1.3.4

Expand Down
124 changes: 0 additions & 124 deletions PLAN-ci-workflow.md

This file was deleted.

Loading
Loading