You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-**State** (`createState`): Mutable signals for primitive values and objects
30
30
-**Sensor** (`createSensor`): Read-only signals for external input streams with automatic state updates. Use `SKIP_EQUALITY` for mutable object observation.
31
-
-**Memo** (`createMemo`): Synchronous derived computations with memoization and reducer capabilities
32
-
-**Task** (`createTask`): Async derived computations with automatic abort/cancellation
31
+
-**Memo** (`createMemo`): Synchronous derived computations with memoization, reducer capabilities, and optional `watched(invalidate)` for external invalidation
32
+
-**Task** (`createTask`): Async derived computations with automatic abort/cancellation and optional `watched(invalidate)` for external invalidation
33
33
-**Store** (`createStore`): Mutable object signals with individually reactive properties via Proxy
34
34
-**List** (`createList`): Mutable array signals with stable keys and reactive items
35
35
-**Collection** (`createCollection`): Reactive collections — either externally-driven with watched lifecycle, or derived from List/Collection with item-level memoization
Copy file name to clipboardExpand all lines: .github/copilot-instructions.md
+15-5Lines changed: 15 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,8 +15,8 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
15
15
### Signal Types (all in `src/nodes/`)
16
16
-**State** (`createState`): Mutable signals for values (`get`, `set`, `update`)
17
17
-**Sensor** (`createSensor`): Read-only signals for external input with automatic state updates. Use `SKIP_EQUALITY` for mutable object observation.
18
-
-**Memo** (`createMemo`): Synchronous derived computations with memoization and reducer capabilities
19
-
-**Task** (`createTask`): Async derived computations with automatic AbortController cancellation
18
+
-**Memo** (`createMemo`): Synchronous derived computations with memoization, reducer capabilities, and optional `watched(invalidate)` for external invalidation
19
+
-**Task** (`createTask`): Async derived computations with automatic AbortController cancellation and optional `watched(invalidate)` for external invalidation
20
20
-**Store** (`createStore`): Proxy-based reactive objects with per-property State/Store signals
21
21
-**List** (`createList`): Reactive arrays with stable keys and per-item State signals
22
22
-**Collection** (`createCollection`): Reactive collections — either externally-driven with watched lifecycle, or derived from List/Collection with item-level memoization
@@ -72,7 +72,8 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
72
72
- All signals have `.get()` for value access
73
73
- Mutable signals (State) have `.set(value)` and `.update(fn)`
74
74
- Store properties are automatically reactive signals via Proxy
75
-
- Sensor/Collection use a start callback returning Cleanup (lazy activation)
75
+
- Sensor/Collection use a watched callback returning Cleanup (lazy activation)
76
+
- Memo/Task use optional `watched(invalidate)` callback in options for external invalidation
76
77
- Store/List use optional `watched` callback in options returning Cleanup
Copy file name to clipboardExpand all lines: ARCHITECTURE.md
+15-13Lines changed: 15 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -178,14 +178,14 @@ A mutable value container. The simplest signal type — `get()` links and return
178
178
179
179
**Graph node**: `StateNode<T>` (source only)
180
180
181
-
A read-only signal that tracks external input. The `start` callback receives a `set` function that updates the node's value via `setState()`. Sensors cover two patterns:
181
+
A read-only signal that tracks external input. The `watched` callback receives a `set` function that updates the node's value via `setState()`. Sensors cover two patterns:
2.**Observing mutable objects** (with `SKIP_EQUALITY`): Holds a stable reference to a mutable object (DOM element, Map, Set). `set(sameRef)` with `equals: SKIP_EQUALITY` always propagates, notifying consumers that the object's internals have changed.
185
185
186
-
The value starts undefined unless `options.value` is provided. Reading a sensor before its `start` callback has called `set()` (and without `options.value`) throws `UnsetSignalValueError`.
186
+
The value starts undefined unless `options.value` is provided. Reading a sensor before its `watched` callback has called `set()` (and without `options.value`) throws `UnsetSignalValueError`.
187
187
188
-
**Lazy lifecycle**: The `start` callback is invoked on first sink attachment. The returned cleanup is stored as `node.stop` and called when the last sink detaches (via `unlink()`).
188
+
**Lazy lifecycle**: The `watched` callback is invoked on first sink attachment. The returned cleanup is stored as `node.stop` and called when the last sink detaches (via `unlink()`).
189
189
190
190
### Memo (`src/nodes/memo.ts`)
191
191
@@ -199,6 +199,8 @@ The `error` field preserves thrown errors: if `fn` throws, the error is stored a
199
199
200
200
**Reducer pattern**: The `prev` parameter enables state accumulation across recomputations without writable state.
201
201
202
+
**Watched lifecycle**: An optional `watched` callback in options provides lazy external invalidation. The callback receives an `invalidate` function and is invoked on first sink attachment. Calling `invalidate()` marks the node `FLAG_DIRTY`, propagates to sinks, and flushes — triggering re-evaluation of the memo's `fn` without changing any tracked dependency. The returned cleanup is stored as `node.stop` and called when the last sink detaches. This enables patterns like DOM observation (MutationObserver) where the memo re-derives its value in response to external events.
203
+
202
204
### Task (`src/nodes/task.ts`)
203
205
204
206
**Graph node**: `TaskNode<T>` (source + sink)
@@ -209,6 +211,8 @@ During dependency tracking, only the synchronous preamble of `fn` is tracked (be
209
211
210
212
`isPending()` returns `true` while a computation is in flight. `abort()` cancels the current computation manually. Errors are preserved like Memo, but old values are retained on errors (the last successful result remains accessible).
211
213
214
+
**Watched lifecycle**: Same pattern as Memo — an optional `watched` callback receives `invalidate` and is invoked on first sink attachment. Calling `invalidate()` marks the node dirty and triggers re-execution, which aborts any in-flight computation via the existing `AbortController` mechanism before starting a new one.
215
+
212
216
### Effect (`src/nodes/effect.ts`)
213
217
214
218
**Graph node**: `EffectNode` (sink only)
@@ -247,28 +251,26 @@ A reactive array with stable keys and per-item reactivity. Each item becomes a `
247
251
248
252
Collection implements two creation patterns that share the same `Collection<T>` interface:
An externally-driven reactive collection with a watched lifecycle, mirroring `createSensor(start, options?)`. The `start` callback receives an `applyChanges(diffResult)` function for granular add/change/remove operations. Initial items are provided via `options.value` (default `[]`).
258
+
An externally-driven reactive collection with a watched lifecycle, mirroring `createSensor(watched, options?)`. The `watched` callback receives an `applyChanges(diffResult)` function for granular add/change/remove operations. Initial items are provided via `options.value` (default `[]`).
255
259
256
-
**Lazy lifecycle**: Like Sensor, the `start` callback is invoked on first sink attachment. The returned cleanup is stored as `node.stop` and called when the last sink detaches (via `unlink()`). The `startWatching()` guard ensures `start` fires before `link()` so synchronous mutations inside `start` update `node.value` before the activating effect reads it.
260
+
**Lazy lifecycle**: Like Sensor, the `watched` callback is invoked on first sink attachment. The returned cleanup is stored as `node.stop` and called when the last sink detaches (via `unlink()`). The `startWatching()` guard ensures `watched` fires before `link()` so synchronous mutations inside `watched` update `node.value` before the activating effect reads it.
257
261
258
262
**External mutation via `applyChanges`**: Additions create new item signals (via configurable `createItem` factory, default `createState`). Changes update existing `State` signals. Removals delete signals and keys. Structural changes null out `node.sources` to force edge re-establishment. The node uses `equals: () => false` since structural changes are managed externally rather than detected by diffing.
259
263
260
264
**Two-path access**: Same pattern as Store/List — first `get()` uses `refresh()` to establish edges from child signals to the collection node; subsequent reads use `untrack(buildValue)` to avoid re-linking.
An internal factory (not exported from the public API) that creates a read-only derived transformation of a List or another Collection. Exposed to users via the `.deriveCollection(callback)` method on List and Collection. Each source item is individually memoized: sync callbacks create `Memo` signals, async callbacks create `Task` signals.
267
271
268
-
**Two-level reactivity**: The derived collection's `MemoNode` tracks structural changes only — its `fn` (`syncKeys`) reads `source.keys()` to detect additions and removals. Value-level changes flow through the individual per-item `Memo`/`Task` signals, which independently track their source item.
269
-
270
-
**Key differences from Store/List**: The `MemoNode.value` is a `string[]` (the keys array), not the collection's actual values. The `equals` function is a shallow string array comparison (`keysEqual`). The node starts `FLAG_DIRTY` (unlike Store/List which start clean after initialization) to ensure the first `refresh()` establishes the edge from source to collection.
272
+
**Consistent with Store/List/createCollection**: The `MemoNode.value` is a `T[]` (cached computed values), and keys are tracked in a separate local `string[]` variable. The `equals` function uses shallow reference equality on array elements to prevent unnecessary downstream propagation when re-evaluation produces the same item references. The node starts `FLAG_DIRTY` to ensure the first `refresh()` establishes edges.
271
273
272
-
**No `invalidateEdges`**: Unlike Store/List, the derived collection never needs to re-establish its source edge because it has exactly one source (the parent List or Collection) that never changes identity. Structural changes (adding/removing per-item signals) happen inside `syncKeys` without affecting the source edge.
274
+
**Two-path access with structural fallback**: Same pattern as Store/List — first `get()` uses `refresh()` to establish edges; subsequent reads use `untrack(buildValue)`. A `syncKeys()` step inside `buildValue` syncs the signals map with `source.keys()`. If keys changed, `syncKeys` nulls `node.sources` to force edge re-establishment via `refresh()`, ensuring new child signals are properly linked.
273
275
274
-
**Chaining**: `.deriveCollection()` creates a new derived collection from an existing one, forming a pipeline. Each level in the chain has its own `MemoNode` for structural tracking and its own set of per-item derived signals.
276
+
**Chaining**: `.deriveCollection()` creates a new derived collection from an existing one, forming a pipeline. Each level in the chain has its own `MemoNode` for value caching and its own set of per-item derived signals.
-**Memo `watched(invalidate)` option**: `createMemo(fn, { watched })` accepts a lazy lifecycle callback that receives an `invalidate` function. Calling `invalidate()` marks the memo dirty and triggers re-evaluation. The callback is invoked on first sink attachment and cleaned up when the last sink detaches. This enables patterns like DOM observation where a memo re-derives its value in response to external events (e.g., MutationObserver) without needing a separate Sensor.
8
+
-**Task `watched(invalidate)` option**: Same pattern as Memo. Calling `invalidate()` aborts any in-flight computation and triggers re-execution.
9
+
-**`CollectionChanges<T>` type**: New typed interface for collection mutations with `add?: T[]`, `change?: T[]`, `remove?: T[]` arrays. Replaces the untyped `DiffResult` records previously used by `CollectionCallback`.
10
+
-**`SensorOptions<T>` type**: Dedicated options type for `createSensor`, extending `SignalOptions<T>` with optional `value`.
11
+
-**`CollectionChanges` export** from public API (`index.ts`).
12
+
-**`SensorOptions` export** from public API (`index.ts`).
13
+
14
+
### Changed
15
+
16
+
-**`createSensor` parameter renamed**: `start` → `watched` for consistency with Store/List lifecycle terminology.
17
+
-**`createSensor` options type**: `ComputedOptions<T>` → `SensorOptions<T>`. This decouples Sensor options from `ComputedOptions`, which now carries the `watched(invalidate)` field for Memo/Task.
18
+
-**`createCollection` parameter renamed**: `start` → `watched` for consistency.
19
+
-**`CollectionCallback` is now generic**: `CollectionCallback` → `CollectionCallback<T>`. The `applyChanges` parameter accepts `CollectionChanges<T>` instead of `DiffResult`.
20
+
-**`CollectionOptions.createItem` signature**: `(key: string, value: T) => Signal<T>` → `(value: T) => Signal<T>`. Key generation is now handled internally.
21
+
-**`KeyConfig<T>` return type relaxed**: Key functions may now return `string | undefined`. Returning `undefined` falls back to synthetic key generation.
22
+
23
+
### Removed
24
+
25
+
-**`DiffResult` removed from public API**: No longer re-exported from `index.ts`. The type remains available from `src/nodes/list.ts` for internal use but is superseded by `CollectionChanges<T>` for collection mutations.
26
+
27
+
## 0.18.0
28
+
29
+
Baseline release. Factory function API (`createState`, `createMemo`, `createTask`, `createEffect`, `createStore`, `createList`, `createCollection`, `createSensor`) with linked-list graph engine.
Sensorand Collection signals use a **start callback** pattern for lazy resource management. Resources are allocated only when a signal is first accessed by an effect and automatically cleaned up when no effects are watching:
127
+
Sensor, Collection, Memo (with `watched` option), and Task (with `watched` option) use a **watched callback** pattern for lazy resource management. Resources are allocated only when a signal is first accessed by an effect and automatically cleaned up when no effects are watching:
128
128
129
129
```typescript
130
130
// Sensor: track external input with state updates
0 commit comments