@zeix/cause-effect 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -120,6 +120,9 @@ const users = createList(
120
120
  [{ id: 'alice', name: 'Alice' }],
121
121
  { keyConfig: u => u.id }
122
122
  )
123
+ const key = users.add({ id: 'bob', name: 'Bob' })
124
+ users.replace(key, { id: 'bob', name: 'Bobby' }) // update item, propagates to all subscribers
125
+ users.remove(key)
123
126
 
124
127
  // Memo for synchronous derived values
125
128
  const doubled = createMemo(() => count.get() * 2)
package/ARCHITECTURE.md CHANGED
@@ -253,6 +253,8 @@ A reactive object where each property is its own signal. Properties are automati
253
253
 
254
254
  **Two-path access with `FLAG_RELINK`**: On first `get()`, `refresh()` executes `buildValue()` with `activeSink = storeNode`, establishing edges from each child signal to the store. Subsequent reads use a fast path: `untrack(buildValue)` rebuilds the value without re-establishing edges. Structural mutations (`add`/`remove`/`set` with additions or removals) set `FLAG_DIRTY | FLAG_RELINK` on the node. The next `get()` detects `FLAG_RELINK` and forces a tracked `refresh()` after rebuilding the value, so `recomputeMemo()` links new child signals and trims removed ones without orphaning edges.
255
255
 
256
+ **Same two-path propagation asymmetry as List**: `store.name.set(v)` (via proxy, equivalent to `byKey('name').set(v)`) propagates only if `childSignal → storeNode` edges exist — which requires `store.get()` to have been called at least once. Effects that subscribe only via `store.keys()` or the iterator, without ever calling `store.get()`, will not be notified of child signal mutations. In practice this is uncommon for Store because typical consumers either call `store.get()` (whole object) or `store.name.get()` (single property) — both of which establish the necessary edges. Effects that use `store.set({...})` rather than `store.name.set(v)` are always safe: `set()` explicitly propagates through `node.sinks` after `applyChanges()`.
257
+
256
258
  **Diff-based updates**: `store.set(newObj)` diffs the new object against the current state, applying only the granular changes to child signals. This preserves identity of unchanged child signals and their downstream edges.
257
259
 
258
260
  **Watched lifecycle**: An optional `watched` callback in options provides lazy resource allocation, following the same pattern as Sensor — activated on first sink, cleaned up when the last sink detaches.
@@ -269,6 +271,30 @@ A reactive array with stable keys and per-item reactivity. Each item becomes a `
269
271
 
270
272
  **Diff-based updates**: `list.set(newArray)` uses `diffArrays()` to compute granular additions, changes, and removals. Changed items update their existing `State` signals; structural changes (add/remove) set `FLAG_DIRTY | FLAG_RELINK`.
271
273
 
274
+ **Two propagation paths — and the asymmetry between them**
275
+
276
+ The List node has two distinct propagation paths. Understanding when each applies is essential for correct usage and for implementing `replace()`:
277
+
278
+ | Path | Trigger | How it works | When it fires |
279
+ |------|---------|-------------|---------------|
280
+ | Structural | `add()`, `remove()`, `sort()`, `splice()`, `set()` | Calls `propagate(e.sink)` directly on `node.sinks` | Always, as soon as any consumer has subscribed via `keys()`, `length`, `get()`, or iterator |
281
+ | Value | `byKey(k).set(v)` on a raw item signal | `setState(itemNode)` → propagates to `itemNode.sinks` → listNode (if linked) → downstream | Only if `recomputeMemo(listNode)` has previously run with that item, establishing the `itemSignal → listNode` edge |
282
+
283
+ The value path has a prerequisite: `list.get()` must have been called at least once after the item was present, so that `buildValue()` executed with `activeSink = listNode` and called `link(itemSignal, listNode)` for each item. This happens automatically for any consumer that reads `list.get()` directly. It does **not** happen for consumers that subscribe only via structural methods (`list.keys()`, `list.length`, the `[Symbol.iterator]`): those call `subscribe()` which links `listNode → effectNode`, but `buildValue()` is never run with `activeSink = listNode`, so no `itemSignal → listNode` edges are established.
284
+
285
+ Consequence: code that calls `byKey(k).set(v)` to update an item will silently fail to notify effects that subscribe via structural accessors. The item signal propagates to nothing because `listNode` is absent from its sinks.
286
+
287
+ **`list.replace(key, value)` — the correct API for item mutation with guaranteed propagation**
288
+
289
+ To close this gap, `List` should expose a `replace(key, value)` method that combines both paths:
290
+
291
+ 1. Updates the item signal: `signals.get(key)?.set(value)` — this propagates through item signal edges to any consumers that subscribed directly (e.g. effects reading `byKey(k).get()`).
292
+ 2. Explicitly marks the list dirty and propagates through `node.sinks`: `node.flags |= FLAG_DIRTY; for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)` — this notifies structural consumers regardless of edge state.
293
+
294
+ This mirrors what `list.set(newArray)` already does internally: `applyChanges()` calls `signal.set(val)` on changed items, then `list.set()` calls `propagate(e.sink)` on `node.sinks` unconditionally. `replace(key, value)` is a single-item version of the same invariant.
295
+
296
+ `byKey(key).set(value)` remains valid for effects that subscribe directly to the item signal. It is **not** a safe pattern for effects that subscribe to the list via structural accessors. This asymmetry should be documented in the public API.
297
+
272
298
  ### Collection (`src/nodes/collection.ts`)
273
299
 
274
300
  Collection implements two creation patterns that share the same `Collection<T>` interface:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Added
6
+
7
+ - **Single-signal overload for `match()`**: `match(signal, handlers)` now accepts a bare signal (not wrapped in an array). The `ok` handler receives the resolved value directly as `(value: T)`, and `err` receives a single `Error` rather than `readonly Error[]`. The existing tuple form is unchanged. This eliminates the boilerplate of wrapping a single source in `[source]`, destructuring `values[0]` in `ok`, and unwrapping `errors[0]!` in `err`.
8
+ - **`SingleMatchHandlers<T>` type**: New exported type that describes the handler object for the single-signal overload. Counterpart to the existing `MatchHandlers<T>` for tuple usage.
9
+
10
+ ### Changed
11
+
12
+ - **Async handler documentation**: Added `@remarks` to the `match()` JSDoc and an expanded section in `README.md` clarifying that async `ok`/`err` handlers are intended for external side effects only (logging, DOM writes, analytics). Any async work that needs to drive reactive state should use a `Task` node, which receives an `AbortSignal` and is auto-cancelled on re-run. Documents the known limitation that rejected async handlers from stale (superseded) runs still call `err`, since the library cannot cancel operations it did not initiate.
13
+
14
+ ## 1.0.2
15
+
16
+ ### Added
17
+
18
+ - **`List.replace(key, value)` — guaranteed item mutation**: Updates the value of an existing item in place, propagating to all subscribers regardless of how they subscribed. `byKey(key).set(value)` only propagates through `itemSignal → listNode` edges, which are established lazily when `list.get()` is called; effects that subscribed via `list.keys()`, `list.length`, or the iterator never trigger that path and receive no notification. `replace()` closes this gap by also walking `node.sinks` directly — the same structural propagation path used by `add()`, `remove()`, and `sort()`. Signal identity is preserved: the `State<T>` returned by `byKey(key)` is the same object before and after. No-op if the key does not exist or the value is reference-equal to the current value.
19
+
3
20
  ## 1.0.1
4
21
 
5
22
  ### Added
package/CLAUDE.md CHANGED
@@ -30,7 +30,7 @@ Scope — owner only (createScope)
30
30
 
31
31
  **`T extends {}` excludes `null` and `undefined` at the type level.** Every signal generic uses this constraint. Signals cannot hold nullish values — use a wrapper type or a union with a sentinel if you need to represent absence.
32
32
 
33
- **`byKey()`, `at()`, `keyAt()`, and `indexOfKey()` do NOT create graph edges.** They are direct lookups. An effect that only calls `collection.byKey('x')?.get()` will react to value changes of key `'x'` but will *not* re-run if that key is added or removed. To track structural changes, read `get()`, `keys()`, or `length`.
33
+ **`byKey()`, `at()`, `keyAt()`, and `indexOfKey()` do NOT create graph edges.** They are direct lookups. An effect that only calls `collection.byKey('x')?.get()` will react to value changes of key `'x'` but will *not* re-run if that key is added or removed. To track structural changes, read `get()`, `keys()`, or `length`. When updating an existing item's value, use `list.replace(key, value)` rather than `byKey(key).set(value)`. `replace()` guarantees propagation to all subscribers regardless of graph edge state. `byKey(key).set(value)` is only safe for effects that directly call `byKey(key).get()` inside their body.
34
34
 
35
35
  **Conditional reads delay `watched` activation.** Dependencies are tracked only for `.get()` calls that actually execute during a given effect run. If a signal read is inside a branch that doesn't execute (e.g. the `ok` arm of `match()` while a Task is still pending), no edge is created and `watched` does not fire. Read signals eagerly before conditional logic when lifecycle activation matters:
36
36
 
package/GUIDE.md CHANGED
@@ -224,14 +224,11 @@ const todos = createList([
224
224
  { id: 't2', text: 'Build app', done: false }
225
225
  ], { keyConfig: todo => todo.id })
226
226
 
227
- // Get a stable reference to a specific item
228
- const first = todos.byKey('t1')
229
-
230
227
  todos.sort((a, b) => a.text.localeCompare(b.text))
231
- // first still points to "Learn signals", regardless of position
228
+ // 'Learn signals' is still at key 't1', regardless of position
232
229
 
233
230
  // Update a single item without replacing the array
234
- first?.set({ id: 't1', text: 'Learn signals', done: true })
231
+ todos.replace('t1', { id: 't1', text: 'Learn signals', done: true })
235
232
  ```
236
233
 
237
234
  Each item is its own signal. Sorting reorders keys without destroying signals or their downstream dependencies. Adding and removing items is granular — unaffected items and their effects don't re-run.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Cause & Effect
2
2
 
3
- Version 1.0.0
3
+ Version 1.1.0
4
4
 
5
5
  **Cause & Effect** is a reactive state management primitives library for TypeScript. It provides the foundational building blocks for managing complex, dynamic, composite, and asynchronous state — correctly and performantly — in a unified signal graph.
6
6
 
@@ -242,7 +242,7 @@ items.splice(1, 1, 'orange')
242
242
  items.sort()
243
243
  ```
244
244
 
245
- Access items by key using `.byKey()` or by index using `.at()`. `.indexOfKey()` returns the current index of an item in the list, while `.keyAt()` returns the key of an item at a given position.
245
+ Access items by key using `.byKey()` or by index using `.at()`. `.indexOfKey()` returns the current index of an item in the list, while `.keyAt()` returns the key of an item at a given position. To update an existing item, use `.replace(key, value)` — this propagates to all subscribers regardless of how they subscribed to the list.
246
246
 
247
247
  Keys are stable across reordering. Use `keyConfig` in options to control key generation:
248
248
 
@@ -260,10 +260,11 @@ const users = createList(
260
260
  const key = items.add('orange')
261
261
  items.sort()
262
262
  console.log(items.byKey(key)?.get()) // 'orange'
263
+ items.replace(key, 'ORANGE') // update in place
263
264
  console.log(items.indexOfKey(key)) // current index
264
265
  ```
265
266
 
266
- Lists have `.keys()`, `.add()`, and `.remove()` methods like stores. Additionally, they have `.sort()`, `.splice()`, and a reactive `.length` property. But unlike stores, deeply nested properties in items are not converted to individual signals.
267
+ Lists have `.keys()`, `.add()`, and `.remove()` methods like stores. Additionally, they have `.replace()`, `.sort()`, `.splice()`, and a reactive `.length` property. But unlike stores, deeply nested properties in items are not converted to individual signals.
267
268
 
268
269
  ### Collection
269
270
 
@@ -391,14 +392,40 @@ const userData = createTask(async (_, abort) => {
391
392
  })
392
393
 
393
394
  createEffect(() => {
394
- match([userData], {
395
- ok: ([user]) => console.log('User:', user),
395
+ match(userData, {
396
+ ok: user => console.log('User:', user),
396
397
  nil: () => console.log('Loading...'),
397
- err: errors => console.error(errors[0])
398
+ err: error => console.error(error)
398
399
  })
399
400
  })
400
401
  ```
401
402
 
403
+ **When to make a handler async.** The `ok` (and `err`) handler may return a `Promise`. Use this for *external* side effects whose result does not need to drive reactive state — sending analytics, writing to IndexedDB, triggering a toast notification, or any fire-and-forget call. A cleanup function returned by the resolved Promise is registered and called synchronously before the next re-run.
404
+
405
+ **Do not set signal state inside an async handler.** If the async result needs to update the graph, model it as a `Task` instead. `Task` receives an `AbortSignal`, is auto-cancelled when its dependencies change, and exposes its pending / resolved / error states as first-class reactive values that compose naturally with `nil` and `err`.
406
+
407
+ ```js
408
+ // ✗ Don't: async handler that writes back into the graph
409
+ createEffect(() => match(trigger, {
410
+ ok: async () => {
411
+ const data = await fetch('/api/data').then(r => r.json())
412
+ result.set(data) // ← side-channel write, not tracked, no cancellation
413
+ }
414
+ }))
415
+
416
+ // ✓ Do: derive the async value as a Task, read it in match()
417
+ const result = createTask(async (_, signal) =>
418
+ fetch('/api/data', { signal }).then(r => r.json()))
419
+
420
+ createEffect(() => match(result, {
421
+ ok: data => render(data),
422
+ nil: () => showSpinner(),
423
+ err: e => showError(e)
424
+ }))
425
+ ```
426
+
427
+ **Stale-run rejections still reach `err`.** When a signal changes and the effect re-runs, the in-flight async handler from the previous run cannot be cancelled (the library did not initiate the underlying operation). If that stale operation eventually rejects, `err` will be called even though a newer run is already active. This is another reason to keep async handlers free of state writes — routing errors to `err` is safe when `err` is a pure side effect (logging, displaying a notification), but it becomes incorrect if `err` calls `.set()` on a signal that run 2 has already updated.
428
+
402
429
  ### Utilities
403
430
 
404
431
  Polymorphic factories and type predicates for generic and library-author code.
package/index.dev.js CHANGED
@@ -727,6 +727,20 @@ function createList(value, options) {
727
727
  flush();
728
728
  }
729
729
  },
730
+ replace(key, value2) {
731
+ const signal = signals.get(key);
732
+ if (!signal)
733
+ return;
734
+ validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value2);
735
+ if (signal.get() === value2)
736
+ return;
737
+ signal.set(value2);
738
+ node.flags |= FLAG_DIRTY;
739
+ for (let e = node.sinks;e; e = e.nextSink)
740
+ propagate(e.sink);
741
+ if (batchDepth === 0)
742
+ flush();
743
+ },
730
744
  sort(compareFn) {
731
745
  const entries = keys.map((key) => [key, signals.get(key)?.get()]).sort(isFunction(compareFn) ? (a, b) => compareFn(a[1], b[1]) : (a, b) => String(a[1]).localeCompare(String(b[1])));
732
746
  const newOrder = entries.map(([key]) => key);
@@ -1215,10 +1229,14 @@ function createEffect(fn) {
1215
1229
  runEffect(node);
1216
1230
  return dispose;
1217
1231
  }
1218
- function match(signals, handlers) {
1232
+ function match(signalOrSignals, handlers) {
1219
1233
  if (!activeOwner)
1220
1234
  throw new RequiredOwnerError("match");
1221
- const { ok, err = console.error, nil } = handlers;
1235
+ const isSingle = !Array.isArray(signalOrSignals);
1236
+ const signals = isSingle ? [signalOrSignals] : signalOrSignals;
1237
+ const { nil } = handlers;
1238
+ const ok = isSingle ? (values2) => handlers.ok(values2[0]) : (values2) => handlers.ok(values2);
1239
+ const err = isSingle && handlers.err ? (errors2) => handlers.err(errors2[0]) : handlers.err ?? console.error;
1222
1240
  let errors;
1223
1241
  let pending = false;
1224
1242
  const values = new Array(signals.length);
package/index.js CHANGED
@@ -1 +1 @@
1
- function g($){return typeof $==="function"}function o($){return g($)&&$.constructor.name==="AsyncFunction"}function X$($){return g($)&&$.constructor.name!=="AsyncFunction"}function Y($,J){return Object.prototype.toString.call($)===`[object ${J}]`}function T($){return Y($,"Object")}function Y$($,J=(z)=>z!=null){return Array.isArray($)&&$.every(J)}function D$($){return typeof $==="string"?`"${$}"`:!!$&&typeof $==="object"?JSON.stringify($):String($)}class Z$ extends Error{constructor($){super(`[${$}] Circular dependency detected`);this.name="CircularDependencyError"}}class A$ extends TypeError{constructor($){super(`[${$}] Signal value cannot be null or undefined`);this.name="NullishSignalValueError"}}class i extends Error{constructor($){super(`[${$}] Signal value is unset`);this.name="UnsetSignalValueError"}}class j$ extends TypeError{constructor($,J){super(`[${$}] Signal value ${D$(J)} is invalid`);this.name="InvalidSignalValueError"}}class b$ extends TypeError{constructor($,J){super(`[${$}] Callback ${D$(J)} is invalid`);this.name="InvalidCallbackError"}}class P$ extends Error{constructor($){super(`[${$}] Signal is read-only`);this.name="ReadonlySignalError"}}class G$ extends Error{constructor($){super(`[${$}] Active owner is required`);this.name="RequiredOwnerError"}}class e extends Error{constructor($,J,z){super(`[${$}] Could not add key "${J}"${z?` with value ${JSON.stringify(z)}`:""} because it already exists`);this.name="DuplicateKeyError"}}function O($,J,z){if(J==null)throw new A$($);if(z&&!z(J))throw new j$($,J)}function W$($,J){if(J==null)throw new i($)}function E($,J,z=g){if(!z(J))throw new b$($,J)}var c="State",u="Memo",d="Task",t="Sensor",S="List",s="Collection",l="Store",r="Slot",p=0,$$=1,G=2,U$=4,b=8,N=null,_=null,C$=[],x=0,_$=!1,k=($,J)=>$===J,L$=($,J)=>!1;function g$($,J){let z=J.sourcesTail;if(z){let X=J.sources;while(X){if(X===$)return!0;if(X===z)break;X=X.nextSource}}return!1}function R($,J){let z=J.sourcesTail;if(z?.source===$)return;let X=null,W=J.flags&U$;if(W){if(X=z?z.nextSource:J.sources,X?.source===$){J.sourcesTail=X;return}}let B=$.sinksTail;if(B?.sink===J&&(!W||g$(B,J)))return;let D={source:$,sink:J,nextSource:X,prevSink:B,nextSink:null};if(J.sourcesTail=$.sinksTail=D,z)z.nextSource=D;else J.sources=D;if(B)B.nextSink=D;else $.sinks=D}function k$($){let{source:J,nextSource:z,nextSink:X,prevSink:W}=$;if(X)X.prevSink=W;else J.sinksTail=W;if(W)W.nextSink=X;else J.sinks=X;if(!J.sinks){if(J.stop)J.stop(),J.stop=void 0;if("sources"in J&&J.sources){let B=J;B.sourcesTail=null,H$(B)}}return z}function H$($){let J=$.sourcesTail,z=J?J.nextSource:$.sources;while(z)z=k$(z);if(J)J.nextSource=null;else $.sources=null}function F($,J=G){let z=$.flags;if("sinks"in $){if((z&(G|$$))>=J)return;if($.flags=z|J,"controller"in $&&$.controller)$.controller.abort(),$.controller=void 0;for(let X=$.sinks;X;X=X.nextSink)F(X.sink,$$)}else{if((z&(G|$$))>=J)return;let X=z&(G|$$);if($.flags=J,!X)C$.push($)}}function N$($,J){if($.equals($.value,J))return;$.value=J;for(let z=$.sinks;z;z=z.nextSink)F(z.sink);if(x===0)I()}function J$($,J){if(!$.cleanup)$.cleanup=J;else if(Array.isArray($.cleanup))$.cleanup.push(J);else $.cleanup=[$.cleanup,J]}function O$($){if(!$.cleanup)return;if(Array.isArray($.cleanup))for(let J=0;J<$.cleanup.length;J++)$.cleanup[J]();else $.cleanup();$.cleanup=null}function v$($){let J=N;N=$,$.sourcesTail=null,$.flags=U$;let z=!1;try{let X=$.fn($.value);if($.error||!$.equals(X,$.value))$.value=X,$.error=void 0,z=!0}catch(X){z=!0,$.error=X instanceof Error?X:Error(String(X))}finally{N=J,H$($)}if(z){for(let X=$.sinks;X;X=X.nextSink)if(X.sink.flags&$$)X.sink.flags|=G}$.flags=p}function c$($){$.controller?.abort();let J=new AbortController;$.controller=J,$.error=void 0;let z=N;N=$,$.sourcesTail=null,$.flags=U$;let X;try{X=$.fn($.value,J.signal)}catch(W){$.controller=void 0,$.error=W instanceof Error?W:Error(String(W));return}finally{N=z,H$($)}X.then((W)=>{if(J.signal.aborted)return;if($.controller=void 0,$.error||!$.equals(W,$.value)){$.value=W,$.error=void 0;for(let B=$.sinks;B;B=B.nextSink)F(B.sink);if(x===0)I()}},(W)=>{if(J.signal.aborted)return;$.controller=void 0;let B=W instanceof Error?W:Error(String(W));if(!$.error||B.name!==$.error.name||B.message!==$.error.message){$.error=B;for(let D=$.sinks;D;D=D.nextSink)F(D.sink);if(x===0)I()}}),$.flags=p}function T$($){O$($);let J=N,z=_;N=_=$,$.sourcesTail=null,$.flags=U$;try{let X=$.fn();if(typeof X==="function")J$($,X)}finally{N=J,_=z,H$($)}$.flags=p}function f($){if($.flags&$$)for(let J=$.sources;J;J=J.nextSource){if("fn"in J.source)f(J.source);if($.flags&G)break}if($.flags&U$)throw new Z$("controller"in $?d:("value"in $)?u:"Effect");if($.flags&G)if("controller"in $)c$($);else if("value"in $)v$($);else T$($);else $.flags=p}function I(){if(_$)return;_$=!0;try{for(let $=0;$<C$.length;$++){let J=C$[$];if(J.flags&(G|$$))f(J)}C$.length=0}finally{_$=!1}}function z$($){x++;try{$()}finally{if(x--,x===0)I()}}function v($){let J=N;N=null;try{return $()}finally{N=J}}function u$($){let J=_,z={cleanup:null};_=z;try{let X=$();if(typeof X==="function")J$(z,X);let W=()=>O$(z);if(J)J$(J,W);return W}finally{_=J}}function d$($){let J=_;_=null;try{return $()}finally{_=J}}function y($,J){O(c,$,J?.guard);let z={value:$,sinks:null,sinksTail:null,equals:J?.equals??k,guard:J?.guard};return{[Symbol.toStringTag]:c,get(){if(N)R(z,N);return z.value},set(X){O(c,X,z.guard),N$(z,X)},update(X){E(c,X);let W=X(z.value);O(c,W,z.guard),N$(z,W)}}}function m$($){return Y($,c)}function n($,J,z){if(Object.is($,J))return!0;if(typeof $!==typeof J)return!1;if($==null||typeof $!=="object"||J==null||typeof J!=="object")return!1;if(!z)z=new WeakSet;if(z.has($)||z.has(J))throw new Z$("isEqual");z.add($),z.add(J);try{let X=Array.isArray($);if(X!==Array.isArray(J))return!1;if(X){let W=$,B=J;if(W.length!==B.length)return!1;for(let D=0;D<W.length;D++)if(!n(W[D],B[D],z))return!1;return!0}if(T($)&&T(J)){let W=Object.keys($),B=Object.keys(J);if(W.length!==B.length)return!1;for(let D of W){if(!(D in J))return!1;if(!n($[D],J[D],z))return!1}return!0}return!1}finally{z.delete($),z.delete(J)}}function K$($,J){if($.length!==J.length)return!1;for(let z=0;z<$.length;z++)if($[z]!==J[z])return!1;return!0}function E$($){let J=0,z=typeof $==="function";return[typeof $==="string"?()=>`${$}${J++}`:z?(X)=>$(X)||String(J++):()=>String(J++),z]}function t$($,J,z,X,W){let B=new WeakSet,D={},M={},C={},U=[],Q=!1,j=new Map;for(let q=0;q<$.length;q++){let Z=z[q],H=$[q];if(Z&&H!==void 0)j.set(Z,H)}let P=new Set;for(let q=0;q<J.length;q++){let Z=J[q];if(Z===void 0)continue;let H=W?X(Z):z[q]??X(Z);if(P.has(H))throw new e(S,H,Z);if(U.push(H),P.add(H),!j.has(H))D[H]=Z,Q=!0;else if(!n(j.get(H),Z,B))M[H]=Z,Q=!0}for(let[q]of j)if(!P.has(q))C[q]=null,Q=!0;if(!Q&&!K$(z,U))Q=!0;return{add:D,change:M,remove:C,newKeys:U,changed:Q}}function Q$($,J){O(S,$,Array.isArray);let z=new Map,X=[],[W,B]=E$(J?.keyConfig),D=()=>X.map((Z)=>z.get(Z)?.get()).filter((Z)=>Z!==void 0),M={fn:D,value:$,flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:n,error:void 0},C=(Z)=>{let H={};for(let m=0;m<Z.length;m++){let V=Z[m];if(V===void 0)continue;let K=X[m];if(!K)K=W(V),X[m]=K;H[K]=V}return H},U=(Z)=>{let H=!1;for(let m in Z.add){let V=Z.add[m];O(`${S} item for key "${m}"`,V),z.set(m,y(V)),H=!0}if(Object.keys(Z.change).length)z$(()=>{for(let m in Z.change){let V=Z.change[m];O(`${S} item for key "${m}"`,V);let K=z.get(m);if(K)K.set(V)}});for(let m in Z.remove){z.delete(m);let V=X.indexOf(m);if(V!==-1)X.splice(V,1);H=!0}if(H)M.flags|=b;return Z.changed},Q=J?.watched,j=Q?()=>{if(N){if(!M.sinks)M.stop=Q();R(M,N)}}:()=>{if(N)R(M,N)},P=C($);for(let Z in P){let H=P[Z];O(`${S} item for key "${Z}"`,H),z.set(Z,y(H))}M.value=$,M.flags=0;let q={[Symbol.toStringTag]:S,[Symbol.isConcatSpreadable]:!0,*[Symbol.iterator](){for(let Z of X){let H=z.get(Z);if(H)yield H}},get length(){return j(),X.length},get(){if(j(),M.sources){if(M.flags){let Z=M.flags&b;if(M.value=v(D),Z){if(M.flags=G,f(M),M.error)throw M.error}else M.flags=p}}else if(f(M),M.error)throw M.error;return M.value},set(Z){let H=M.flags&G?D():M.value,m=t$(H,Z,X,W,B);if(m.changed){X=m.newKeys,U(m),M.flags|=G;for(let V=M.sinks;V;V=V.nextSink)F(V.sink);if(x===0)I()}},update(Z){q.set(Z(q.get()))},at(Z){let H=X[Z];return H!==void 0?z.get(H):void 0},keys(){return j(),X.values()},byKey(Z){return z.get(Z)},keyAt(Z){return X[Z]},indexOfKey(Z){return X.indexOf(Z)},add(Z){let H=W(Z);if(z.has(H))throw new e(S,H,Z);if(!X.includes(H))X.push(H);O(`${S} item for key "${H}"`,Z),z.set(H,y(Z)),M.flags|=G|b;for(let m=M.sinks;m;m=m.nextSink)F(m.sink);if(x===0)I();return H},remove(Z){let H=typeof Z==="number"?X[Z]:Z;if(H===void 0)return;if(z.delete(H)){let V=typeof Z==="number"?Z:X.indexOf(H);if(V>=0)X.splice(V,1);M.flags|=G|b;for(let K=M.sinks;K;K=K.nextSink)F(K.sink);if(x===0)I()}},sort(Z){let m=X.map((V)=>[V,z.get(V)?.get()]).sort(g(Z)?(V,K)=>Z(V[1],K[1]):(V,K)=>String(V[1]).localeCompare(String(K[1]))).map(([V])=>V);if(!K$(X,m)){X=m,M.flags|=G;for(let V=M.sinks;V;V=V.nextSink)F(V.sink);if(x===0)I()}},splice(Z,H,...m){let V=X.length,K=Z<0?Math.max(0,V+Z):Math.min(Z,V),w=Math.max(0,Math.min(H??Math.max(0,V-Math.max(0,K)),V-K)),A={},L={};for(let h=0;h<w;h++){let a=K+h,w$=X[a];if(w$){let y$=z.get(w$);if(y$)L[w$]=y$.get()}}let f$=X.slice(0,K);for(let h of m){let a=W(h);if(z.has(a)&&!(a in L))throw new e(S,a,h);f$.push(a),A[a]=h}f$.push(...X.slice(K+w));let h$=!!(Object.keys(A).length||Object.keys(L).length);if(h$){U({add:A,change:{},remove:L,changed:h$}),X=f$,M.flags|=G;for(let h=M.sinks;h;h=h.nextSink)F(h.sink);if(x===0)I()}return Object.values(L)},deriveCollection(Z){return x$(q,Z)}};return q}function R$($){return Y($,S)}function B$($,J){if(E(u,$,X$),J?.value!==void 0)O(u,J.value,J?.guard);let z={fn:$,value:J?.value,flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:J?.equals??k,error:void 0,stop:void 0},X=J?.watched,W=X?()=>{if(N){if(!z.sinks)z.stop=X(()=>{if(F(z),x===0)I()});R(z,N)}}:()=>{if(N)R(z,N)};return{[Symbol.toStringTag]:u,get(){if(W(),f(z),z.error)throw z.error;return W$(u,z.value),z.value}}}function S$($){return Y($,u)}function q$($,J){if(E(d,$,o),J?.value!==void 0)O(d,J.value,J?.guard);let z={fn:$,value:J?.value,sources:null,sourcesTail:null,sinks:null,sinksTail:null,flags:G,equals:J?.equals??k,controller:void 0,error:void 0,stop:void 0},X=J?.watched,W=X?()=>{if(N){if(!z.sinks)z.stop=X(()=>{if(F(z),x===0)I()});R(z,N)}}:()=>{if(N)R(z,N)};return{[Symbol.toStringTag]:d,get(){if(W(),f(z),z.error)throw z.error;return W$(d,z.value),z.value},isPending(){return!!z.controller},abort(){z.controller?.abort(),z.controller=void 0}}}function p$($){return Y($,d)}function x$($,J){E(s,J);let z=o(J),X=new Map,W=[],B=(q)=>{let Z=z?q$(async(H,m)=>{let V=$.byKey(q)?.get();if(V==null)return H;return J(V,m)}):B$(()=>{let H=$.byKey(q)?.get();if(H==null)return;return J(H)});X.set(q,Z)};function D(q){if(!K$(W,q)){let Z=new Set(W),H=new Set(q);for(let m of W)if(!H.has(m))X.delete(m);for(let m of q)if(!Z.has(m))B(m);W=q,U.flags|=b}}function M(){D(Array.from($.keys()));let q=[];for(let Z of W)try{let H=X.get(Z)?.get();if(H!=null)q.push(H)}catch(H){if(!(H instanceof i))throw H}return q}let U={fn:M,value:[],flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:(q,Z)=>{if(q.length!==Z.length)return!1;for(let H=0;H<q.length;H++)if(q[H]!==Z[H])return!1;return!0},error:void 0};function Q(){if(U.sources){if(U.flags)if(U.value=v(M),U.flags&b){if(U.flags=G,f(U),U.error)throw U.error}else U.flags=p}else if(U.sinks){if(f(U),U.error)throw U.error}else U.value=v(M)}let j=Array.from(v(()=>$.keys()));for(let q of j)B(q);W=j;let P={[Symbol.toStringTag]:s,[Symbol.isConcatSpreadable]:!0,*[Symbol.iterator](){for(let q of W){let Z=X.get(q);if(Z)yield Z}},get length(){if(N)R(U,N);return Q(),W.length},keys(){if(N)R(U,N);return Q(),W.values()},get(){if(N)R(U,N);return Q(),U.value},at(q){let Z=W[q];return Z!==void 0?X.get(Z):void 0},byKey(q){return X.get(q)},keyAt(q){return W[q]},indexOfKey(q){return W.indexOf(q)},deriveCollection(q){return x$(P,q)}};return P}function s$($,J){let z=J?.value??[];if(z.length)O(s,z,Array.isArray);E(s,$,X$);let X=new Map,W=[],B=new Map,[D,M]=E$(J?.keyConfig),C=(Z)=>B.get(Z)??(M?D(Z):void 0),U=J?.createItem??y;function Q(){let Z=[];for(let H of W)try{let m=X.get(H)?.get();if(m!=null)Z.push(m)}catch(m){if(!(m instanceof i))throw m}return Z}let j={fn:Q,value:z,flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:L$,error:void 0};for(let Z of z){let H=D(Z);X.set(H,U(Z)),B.set(Z,H),W.push(H)}j.value=z,j.flags=G;function P(){if(N){if(!j.sinks)j.stop=$((Z)=>{let{add:H,change:m,remove:V}=Z;if(!H?.length&&!m?.length&&!V?.length)return;let K=!1;z$(()=>{if(H)for(let w of H){let A=D(w);if(X.set(A,U(w)),B.set(w,A),!W.includes(A))W.push(A);K=!0}if(m)for(let w of m){let A=C(w);if(!A)continue;let L=X.get(A);if(L&&m$(L))B.delete(L.get()),L.set(w),B.set(w,A)}if(V)for(let w of V){let A=C(w);if(!A)continue;B.delete(w),X.delete(A);let L=W.indexOf(A);if(L!==-1)W.splice(L,1);K=!0}j.flags=G|(K?b:0);for(let w=j.sinks;w;w=w.nextSink)F(w.sink)})});R(j,N)}}let q={[Symbol.toStringTag]:s,[Symbol.isConcatSpreadable]:!0,*[Symbol.iterator](){for(let Z of W){let H=X.get(Z);if(H)yield H}},get length(){return P(),W.length},keys(){return P(),W.values()},get(){if(P(),j.sources){if(j.flags){let Z=j.flags&b;if(j.value=v(Q),Z){if(j.flags=G,f(j),j.error)throw j.error}else j.flags=p}}else if(f(j),j.error)throw j.error;return j.value},at(Z){let H=W[Z];return H!==void 0?X.get(H):void 0},byKey(Z){return X.get(Z)},keyAt(Z){return W[Z]},indexOfKey(Z){return W.indexOf(Z)},deriveCollection(Z){return x$(q,Z)}};return q}function l$($){return Y($,s)}function r$($){E("Effect",$);let J={fn:$,flags:G,sources:null,sourcesTail:null,cleanup:null},z=()=>{O$(J),J.fn=void 0,J.flags=p,J.sourcesTail=null,H$(J)};if(_)J$(_,z);return T$(J),z}function o$($,J){if(!_)throw new G$("match");let{ok:z,err:X=console.error,nil:W}=J,B,D=!1,M=Array($.length);for(let U=0;U<$.length;U++)try{M[U]=$[U].get()}catch(Q){if(Q instanceof i){D=!0;continue}if(!B)B=[];B.push(Q instanceof Error?Q:Error(String(Q)))}let C;try{if(D)C=W?.();else if(B)C=X(B);else C=z(M)}catch(U){X([U instanceof Error?U:Error(String(U))])}if(typeof C==="function")return C;if(C instanceof Promise){let U=_,Q=new AbortController;J$(U,()=>Q.abort()),C.then((j)=>{if(!Q.signal.aborted&&typeof j==="function")J$(U,j)}).catch((j)=>{X([j instanceof Error?j:Error(String(j))])})}}function i$($,J){if(E(t,$,X$),J?.value!==void 0)O(t,J.value,J?.guard);let z={value:J?.value,sinks:null,sinksTail:null,equals:J?.equals??k,guard:J?.guard,stop:void 0};return{[Symbol.toStringTag]:t,get(){if(N){if(!z.sinks)z.stop=$((X)=>{O(t,X,z.guard),N$(z,X)});R(z,N)}return W$(t,z.value),z.value}}}function n$($){return Y($,t)}function a$($,J){let z=T($)||Array.isArray($),X=T(J)||Array.isArray(J);if(!z||!X){let j=!Object.is($,J);return{changed:j,add:j&&X?J:{},change:{},remove:j&&z?$:{}}}let W=new WeakSet,B={},D={},M={},C=!1,U=Object.keys($),Q=Object.keys(J);for(let j of Q)if(j in $){if(!n($[j],J[j],W))D[j]=J[j],C=!0}else B[j]=J[j],C=!0;for(let j of U)if(!(j in J))M[j]=void 0,C=!0;return{add:B,change:D,remove:M,changed:C}}function V$($,J){O(l,$,T);let z=new Map,X=(Q,j)=>{if(O(`${l} for key "${Q}"`,j),Array.isArray(j))z.set(Q,Q$(j));else if(T(j))z.set(Q,V$(j));else z.set(Q,y(j))},W=()=>{let Q={};return z.forEach((j,P)=>{Q[P]=j.get()}),Q},B={fn:W,value:$,flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:n,error:void 0},D=(Q)=>{let j=!1;for(let P in Q.add)X(P,Q.add[P]),j=!0;if(Object.keys(Q.change).length)z$(()=>{for(let P in Q.change){let q=Q.change[P];O(`${l} for key "${P}"`,q);let Z=z.get(P);if(Z)if(T(q)!==F$(Z))X(P,q),j=!0;else Z.set(q)}});for(let P in Q.remove)z.delete(P),j=!0;if(j)B.flags|=b;return Q.changed},M=J?.watched,C=M?()=>{if(N){if(!B.sinks)B.stop=M();R(B,N)}}:()=>{if(N)R(B,N)};for(let Q of Object.keys($))X(Q,$[Q]);let U={[Symbol.toStringTag]:l,[Symbol.isConcatSpreadable]:!1,*[Symbol.iterator](){for(let Q of Array.from(z.keys())){let j=z.get(Q);if(j)yield[Q,j]}},keys(){return C(),z.keys()},byKey(Q){return z.get(Q)},get(){if(C(),B.sources){if(B.flags){let Q=B.flags&b;if(B.value=v(W),Q){if(B.flags=G,f(B),B.error)throw B.error}else B.flags=p}}else if(f(B),B.error)throw B.error;return B.value},set(Q){let j=B.flags&G?W():B.value,P=a$(j,Q);if(D(P)){B.flags|=G;for(let q=B.sinks;q;q=q.nextSink)F(q.sink);if(x===0)I()}},update(Q){U.set(Q(U.get()))},add(Q,j){if(z.has(Q))throw new e(l,Q,j);X(Q,j),B.flags|=G|b;for(let P=B.sinks;P;P=P.nextSink)F(P.sink);if(x===0)I();return Q},remove(Q){if(z.delete(Q)){B.flags|=G|b;for(let P=B.sinks;P;P=P.nextSink)F(P.sink);if(x===0)I()}}};return new Proxy(U,{get(Q,j){if(j in Q)return Reflect.get(Q,j);if(typeof j!=="symbol")return Q.byKey(j)},has(Q,j){if(j in Q)return!0;return Q.byKey(String(j))!==void 0},ownKeys(Q){return Array.from(Q.keys())},getOwnPropertyDescriptor(Q,j){if(j in Q)return Reflect.getOwnPropertyDescriptor(Q,j);if(typeof j==="symbol")return;let P=Q.byKey(String(j));return P?{enumerable:!0,configurable:!0,writable:!0,value:P}:void 0}})}function F$($){return Y($,l)}function e$($,J){return o($)?q$($,J):B$($,J)}function $J($){if(M$($))return $;if($==null)throw new j$("createSignal",$);if(o($))return q$($);if(g($))return B$($);if(Y$($))return Q$($);if(T($))return V$($);return y($)}function JJ($){if(I$($))return $;if($==null||g($)||M$($))throw new j$("createMutableSignal",$);if(Y$($))return Q$($);if(T($))return V$($);return y($)}function zJ($){return S$($)||p$($)}function M$($){let J=[c,u,d,t,r,S,s,l],z=Object.prototype.toString.call($).slice(8,-1);return J.includes(z)}function I$($){return m$($)||F$($)||R$($)}function XJ($,J){O(r,$,M$);let z=$,X=J?.guard,W={fn:()=>z.get(),value:void 0,flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:J?.equals??k,error:void 0},B=()=>{if(N)R(W,N);if(f(W),W.error)throw W.error;return W.value},D=(C)=>{if(!I$(z))throw new P$(r);O(r,C,X),z.set(C)},M=(C)=>{O(r,C,M$),z=C,W.flags|=G;for(let U=W.sinks;U;U=U.nextSink)F(U.sink);if(x===0)I()};return{[Symbol.toStringTag]:r,configurable:!0,enumerable:!0,get:B,set:D,replace:M,current:()=>z}}function ZJ($){return Y($,r)}export{D$ as valueString,v as untrack,d$ as unown,o$ as match,p$ as isTask,F$ as isStore,m$ as isState,ZJ as isSlot,M$ as isSignal,n$ as isSensor,T as isRecord,Y as isObjectOfType,I$ as isMutableSignal,S$ as isMemo,R$ as isList,g as isFunction,n as isEqual,zJ as isComputed,l$ as isCollection,o as isAsyncFunction,q$ as createTask,V$ as createStore,y as createState,XJ as createSlot,$J as createSignal,i$ as createSensor,u$ as createScope,JJ as createMutableSignal,B$ as createMemo,Q$ as createList,r$ as createEffect,e$ as createComputed,s$ as createCollection,z$ as batch,i as UnsetSignalValueError,L$ as SKIP_EQUALITY,G$ as RequiredOwnerError,P$ as ReadonlySignalError,A$ as NullishSignalValueError,j$ as InvalidSignalValueError,b$ as InvalidCallbackError,Z$ as CircularDependencyError};
1
+ function g($){return typeof $==="function"}function o($){return g($)&&$.constructor.name==="AsyncFunction"}function X$($){return g($)&&$.constructor.name!=="AsyncFunction"}function Y($,J){return Object.prototype.toString.call($)===`[object ${J}]`}function E($){return Y($,"Object")}function Y$($,J=(z)=>z!=null){return Array.isArray($)&&$.every(J)}function D$($){return typeof $==="string"?`"${$}"`:!!$&&typeof $==="object"?JSON.stringify($):String($)}class Z$ extends Error{constructor($){super(`[${$}] Circular dependency detected`);this.name="CircularDependencyError"}}class A$ extends TypeError{constructor($){super(`[${$}] Signal value cannot be null or undefined`);this.name="NullishSignalValueError"}}class i extends Error{constructor($){super(`[${$}] Signal value is unset`);this.name="UnsetSignalValueError"}}class j$ extends TypeError{constructor($,J){super(`[${$}] Signal value ${D$(J)} is invalid`);this.name="InvalidSignalValueError"}}class b$ extends TypeError{constructor($,J){super(`[${$}] Callback ${D$(J)} is invalid`);this.name="InvalidCallbackError"}}class P$ extends Error{constructor($){super(`[${$}] Signal is read-only`);this.name="ReadonlySignalError"}}class G$ extends Error{constructor($){super(`[${$}] Active owner is required`);this.name="RequiredOwnerError"}}class e extends Error{constructor($,J,z){super(`[${$}] Could not add key "${J}"${z?` with value ${JSON.stringify(z)}`:""} because it already exists`);this.name="DuplicateKeyError"}}function C($,J,z){if(J==null)throw new A$($);if(z&&!z(J))throw new j$($,J)}function W$($,J){if(J==null)throw new i($)}function S($,J,z=g){if(!z(J))throw new b$($,J)}var c="State",u="Memo",d="Task",t="Sensor",L="List",s="Collection",r="Store",l="Slot",p=0,$$=1,G=2,U$=4,b=8,V=null,_=null,C$=[],O=0,_$=!1,k=($,J)=>$===J,L$=($,J)=>!1;function g$($,J){let z=J.sourcesTail;if(z){let X=J.sources;while(X){if(X===$)return!0;if(X===z)break;X=X.nextSource}}return!1}function F($,J){let z=J.sourcesTail;if(z?.source===$)return;let X=null,W=J.flags&U$;if(W){if(X=z?z.nextSource:J.sources,X?.source===$){J.sourcesTail=X;return}}let B=$.sinksTail;if(B?.sink===J&&(!W||g$(B,J)))return;let D={source:$,sink:J,nextSource:X,prevSink:B,nextSink:null};if(J.sourcesTail=$.sinksTail=D,z)z.nextSource=D;else J.sources=D;if(B)B.nextSink=D;else $.sinks=D}function k$($){let{source:J,nextSource:z,nextSink:X,prevSink:W}=$;if(X)X.prevSink=W;else J.sinksTail=W;if(W)W.nextSink=X;else J.sinks=X;if(!J.sinks){if(J.stop)J.stop(),J.stop=void 0;if("sources"in J&&J.sources){let B=J;B.sourcesTail=null,H$(B)}}return z}function H$($){let J=$.sourcesTail,z=J?J.nextSource:$.sources;while(z)z=k$(z);if(J)J.nextSource=null;else $.sources=null}function x($,J=G){let z=$.flags;if("sinks"in $){if((z&(G|$$))>=J)return;if($.flags=z|J,"controller"in $&&$.controller)$.controller.abort(),$.controller=void 0;for(let X=$.sinks;X;X=X.nextSink)x(X.sink,$$)}else{if((z&(G|$$))>=J)return;let X=z&(G|$$);if($.flags=J,!X)C$.push($)}}function N$($,J){if($.equals($.value,J))return;$.value=J;for(let z=$.sinks;z;z=z.nextSink)x(z.sink);if(O===0)w()}function J$($,J){if(!$.cleanup)$.cleanup=J;else if(Array.isArray($.cleanup))$.cleanup.push(J);else $.cleanup=[$.cleanup,J]}function K$($){if(!$.cleanup)return;if(Array.isArray($.cleanup))for(let J=0;J<$.cleanup.length;J++)$.cleanup[J]();else $.cleanup();$.cleanup=null}function v$($){let J=V;V=$,$.sourcesTail=null,$.flags=U$;let z=!1;try{let X=$.fn($.value);if($.error||!$.equals(X,$.value))$.value=X,$.error=void 0,z=!0}catch(X){z=!0,$.error=X instanceof Error?X:Error(String(X))}finally{V=J,H$($)}if(z){for(let X=$.sinks;X;X=X.nextSink)if(X.sink.flags&$$)X.sink.flags|=G}$.flags=p}function c$($){$.controller?.abort();let J=new AbortController;$.controller=J,$.error=void 0;let z=V;V=$,$.sourcesTail=null,$.flags=U$;let X;try{X=$.fn($.value,J.signal)}catch(W){$.controller=void 0,$.error=W instanceof Error?W:Error(String(W));return}finally{V=z,H$($)}X.then((W)=>{if(J.signal.aborted)return;if($.controller=void 0,$.error||!$.equals(W,$.value)){$.value=W,$.error=void 0;for(let B=$.sinks;B;B=B.nextSink)x(B.sink);if(O===0)w()}},(W)=>{if(J.signal.aborted)return;$.controller=void 0;let B=W instanceof Error?W:Error(String(W));if(!$.error||B.name!==$.error.name||B.message!==$.error.message){$.error=B;for(let D=$.sinks;D;D=D.nextSink)x(D.sink);if(O===0)w()}}),$.flags=p}function T$($){K$($);let J=V,z=_;V=_=$,$.sourcesTail=null,$.flags=U$;try{let X=$.fn();if(typeof X==="function")J$($,X)}finally{V=J,_=z,H$($)}$.flags=p}function I($){if($.flags&$$)for(let J=$.sources;J;J=J.nextSource){if("fn"in J.source)I(J.source);if($.flags&G)break}if($.flags&U$)throw new Z$("controller"in $?d:("value"in $)?u:"Effect");if($.flags&G)if("controller"in $)c$($);else if("value"in $)v$($);else T$($);else $.flags=p}function w(){if(_$)return;_$=!0;try{for(let $=0;$<C$.length;$++){let J=C$[$];if(J.flags&(G|$$))I(J)}C$.length=0}finally{_$=!1}}function z$($){O++;try{$()}finally{if(O--,O===0)w()}}function v($){let J=V;V=null;try{return $()}finally{V=J}}function u$($){let J=_,z={cleanup:null};_=z;try{let X=$();if(typeof X==="function")J$(z,X);let W=()=>K$(z);if(J)J$(J,W);return W}finally{_=J}}function d$($){let J=_;_=null;try{return $()}finally{_=J}}function y($,J){C(c,$,J?.guard);let z={value:$,sinks:null,sinksTail:null,equals:J?.equals??k,guard:J?.guard};return{[Symbol.toStringTag]:c,get(){if(V)F(z,V);return z.value},set(X){C(c,X,z.guard),N$(z,X)},update(X){S(c,X);let W=X(z.value);C(c,W,z.guard),N$(z,W)}}}function V$($){return Y($,c)}function n($,J,z){if(Object.is($,J))return!0;if(typeof $!==typeof J)return!1;if($==null||typeof $!=="object"||J==null||typeof J!=="object")return!1;if(!z)z=new WeakSet;if(z.has($)||z.has(J))throw new Z$("isEqual");z.add($),z.add(J);try{let X=Array.isArray($);if(X!==Array.isArray(J))return!1;if(X){let W=$,B=J;if(W.length!==B.length)return!1;for(let D=0;D<W.length;D++)if(!n(W[D],B[D],z))return!1;return!0}if(E($)&&E(J)){let W=Object.keys($),B=Object.keys(J);if(W.length!==B.length)return!1;for(let D of W){if(!(D in J))return!1;if(!n($[D],J[D],z))return!1}return!0}return!1}finally{z.delete($),z.delete(J)}}function R$($,J){if($.length!==J.length)return!1;for(let z=0;z<$.length;z++)if($[z]!==J[z])return!1;return!0}function E$($){let J=0,z=typeof $==="function";return[typeof $==="string"?()=>`${$}${J++}`:z?(X)=>$(X)||String(J++):()=>String(J++),z]}function t$($,J,z,X,W){let B=new WeakSet,D={},M={},R={},P=[],Q=!1,j=new Map;for(let q=0;q<$.length;q++){let Z=z[q],H=$[q];if(Z&&H!==void 0)j.set(Z,H)}let m=new Set;for(let q=0;q<J.length;q++){let Z=J[q];if(Z===void 0)continue;let H=W?X(Z):z[q]??X(Z);if(m.has(H))throw new e(L,H,Z);if(P.push(H),m.add(H),!j.has(H))D[H]=Z,Q=!0;else if(!n(j.get(H),Z,B))M[H]=Z,Q=!0}for(let[q]of j)if(!m.has(q))R[q]=null,Q=!0;if(!Q&&!R$(z,P))Q=!0;return{add:D,change:M,remove:R,newKeys:P,changed:Q}}function Q$($,J){C(L,$,Array.isArray);let z=new Map,X=[],[W,B]=E$(J?.keyConfig),D=()=>X.map((Z)=>z.get(Z)?.get()).filter((Z)=>Z!==void 0),M={fn:D,value:$,flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:n,error:void 0},R=(Z)=>{let H={};for(let U=0;U<Z.length;U++){let N=Z[U];if(N===void 0)continue;let K=X[U];if(!K)K=W(N),X[U]=K;H[K]=N}return H},P=(Z)=>{let H=!1;for(let U in Z.add){let N=Z.add[U];C(`${L} item for key "${U}"`,N),z.set(U,y(N)),H=!0}if(Object.keys(Z.change).length)z$(()=>{for(let U in Z.change){let N=Z.change[U];C(`${L} item for key "${U}"`,N);let K=z.get(U);if(K)K.set(N)}});for(let U in Z.remove){z.delete(U);let N=X.indexOf(U);if(N!==-1)X.splice(N,1);H=!0}if(H)M.flags|=b;return Z.changed},Q=J?.watched,j=Q?()=>{if(V){if(!M.sinks)M.stop=Q();F(M,V)}}:()=>{if(V)F(M,V)},m=R($);for(let Z in m){let H=m[Z];C(`${L} item for key "${Z}"`,H),z.set(Z,y(H))}M.value=$,M.flags=0;let q={[Symbol.toStringTag]:L,[Symbol.isConcatSpreadable]:!0,*[Symbol.iterator](){for(let Z of X){let H=z.get(Z);if(H)yield H}},get length(){return j(),X.length},get(){if(j(),M.sources){if(M.flags){let Z=M.flags&b;if(M.value=v(D),Z){if(M.flags=G,I(M),M.error)throw M.error}else M.flags=p}}else if(I(M),M.error)throw M.error;return M.value},set(Z){let H=M.flags&G?D():M.value,U=t$(H,Z,X,W,B);if(U.changed){X=U.newKeys,P(U),M.flags|=G;for(let N=M.sinks;N;N=N.nextSink)x(N.sink);if(O===0)w()}},update(Z){q.set(Z(q.get()))},at(Z){let H=X[Z];return H!==void 0?z.get(H):void 0},keys(){return j(),X.values()},byKey(Z){return z.get(Z)},keyAt(Z){return X[Z]},indexOfKey(Z){return X.indexOf(Z)},add(Z){let H=W(Z);if(z.has(H))throw new e(L,H,Z);if(!X.includes(H))X.push(H);C(`${L} item for key "${H}"`,Z),z.set(H,y(Z)),M.flags|=G|b;for(let U=M.sinks;U;U=U.nextSink)x(U.sink);if(O===0)w();return H},remove(Z){let H=typeof Z==="number"?X[Z]:Z;if(H===void 0)return;if(z.delete(H)){let N=typeof Z==="number"?Z:X.indexOf(H);if(N>=0)X.splice(N,1);M.flags|=G|b;for(let K=M.sinks;K;K=K.nextSink)x(K.sink);if(O===0)w()}},replace(Z,H){let U=z.get(Z);if(!U)return;if(C(`${L} item for key "${Z}"`,H),U.get()===H)return;U.set(H),M.flags|=G;for(let N=M.sinks;N;N=N.nextSink)x(N.sink);if(O===0)w()},sort(Z){let U=X.map((N)=>[N,z.get(N)?.get()]).sort(g(Z)?(N,K)=>Z(N[1],K[1]):(N,K)=>String(N[1]).localeCompare(String(K[1]))).map(([N])=>N);if(!R$(X,U)){X=U,M.flags|=G;for(let N=M.sinks;N;N=N.nextSink)x(N.sink);if(O===0)w()}},splice(Z,H,...U){let N=X.length,K=Z<0?Math.max(0,N+Z):Math.min(Z,N),f=Math.max(0,Math.min(H??Math.max(0,N-Math.max(0,K)),N-K)),A={},T={};for(let h=0;h<f;h++){let a=K+h,f$=X[a];if(f$){let y$=z.get(f$);if(y$)T[f$]=y$.get()}}let I$=X.slice(0,K);for(let h of U){let a=W(h);if(z.has(a)&&!(a in T))throw new e(L,a,h);I$.push(a),A[a]=h}I$.push(...X.slice(K+f));let h$=!!(Object.keys(A).length||Object.keys(T).length);if(h$){P({add:A,change:{},remove:T,changed:h$}),X=I$,M.flags|=G;for(let h=M.sinks;h;h=h.nextSink)x(h.sink);if(O===0)w()}return Object.values(T)},deriveCollection(Z){return x$(q,Z)}};return q}function O$($){return Y($,L)}function B$($,J){if(S(u,$,X$),J?.value!==void 0)C(u,J.value,J?.guard);let z={fn:$,value:J?.value,flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:J?.equals??k,error:void 0,stop:void 0},X=J?.watched,W=X?()=>{if(V){if(!z.sinks)z.stop=X(()=>{if(x(z),O===0)w()});F(z,V)}}:()=>{if(V)F(z,V)};return{[Symbol.toStringTag]:u,get(){if(W(),I(z),z.error)throw z.error;return W$(u,z.value),z.value}}}function S$($){return Y($,u)}function q$($,J){if(S(d,$,o),J?.value!==void 0)C(d,J.value,J?.guard);let z={fn:$,value:J?.value,sources:null,sourcesTail:null,sinks:null,sinksTail:null,flags:G,equals:J?.equals??k,controller:void 0,error:void 0,stop:void 0},X=J?.watched,W=X?()=>{if(V){if(!z.sinks)z.stop=X(()=>{if(x(z),O===0)w()});F(z,V)}}:()=>{if(V)F(z,V)};return{[Symbol.toStringTag]:d,get(){if(W(),I(z),z.error)throw z.error;return W$(d,z.value),z.value},isPending(){return!!z.controller},abort(){z.controller?.abort(),z.controller=void 0}}}function p$($){return Y($,d)}function x$($,J){S(s,J);let z=o(J),X=new Map,W=[],B=(q)=>{let Z=z?q$(async(H,U)=>{let N=$.byKey(q)?.get();if(N==null)return H;return J(N,U)}):B$(()=>{let H=$.byKey(q)?.get();if(H==null)return;return J(H)});X.set(q,Z)};function D(q){if(!R$(W,q)){let Z=new Set(W),H=new Set(q);for(let U of W)if(!H.has(U))X.delete(U);for(let U of q)if(!Z.has(U))B(U);W=q,P.flags|=b}}function M(){D(Array.from($.keys()));let q=[];for(let Z of W)try{let H=X.get(Z)?.get();if(H!=null)q.push(H)}catch(H){if(!(H instanceof i))throw H}return q}let P={fn:M,value:[],flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:(q,Z)=>{if(q.length!==Z.length)return!1;for(let H=0;H<q.length;H++)if(q[H]!==Z[H])return!1;return!0},error:void 0};function Q(){if(P.sources){if(P.flags)if(P.value=v(M),P.flags&b){if(P.flags=G,I(P),P.error)throw P.error}else P.flags=p}else if(P.sinks){if(I(P),P.error)throw P.error}else P.value=v(M)}let j=Array.from(v(()=>$.keys()));for(let q of j)B(q);W=j;let m={[Symbol.toStringTag]:s,[Symbol.isConcatSpreadable]:!0,*[Symbol.iterator](){for(let q of W){let Z=X.get(q);if(Z)yield Z}},get length(){if(V)F(P,V);return Q(),W.length},keys(){if(V)F(P,V);return Q(),W.values()},get(){if(V)F(P,V);return Q(),P.value},at(q){let Z=W[q];return Z!==void 0?X.get(Z):void 0},byKey(q){return X.get(q)},keyAt(q){return W[q]},indexOfKey(q){return W.indexOf(q)},deriveCollection(q){return x$(m,q)}};return m}function s$($,J){let z=J?.value??[];if(z.length)C(s,z,Array.isArray);S(s,$,X$);let X=new Map,W=[],B=new Map,[D,M]=E$(J?.keyConfig),R=(Z)=>B.get(Z)??(M?D(Z):void 0),P=J?.createItem??y;function Q(){let Z=[];for(let H of W)try{let U=X.get(H)?.get();if(U!=null)Z.push(U)}catch(U){if(!(U instanceof i))throw U}return Z}let j={fn:Q,value:z,flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:L$,error:void 0};for(let Z of z){let H=D(Z);X.set(H,P(Z)),B.set(Z,H),W.push(H)}j.value=z,j.flags=G;function m(){if(V){if(!j.sinks)j.stop=$((Z)=>{let{add:H,change:U,remove:N}=Z;if(!H?.length&&!U?.length&&!N?.length)return;let K=!1;z$(()=>{if(H)for(let f of H){let A=D(f);if(X.set(A,P(f)),B.set(f,A),!W.includes(A))W.push(A);K=!0}if(U)for(let f of U){let A=R(f);if(!A)continue;let T=X.get(A);if(T&&V$(T))B.delete(T.get()),T.set(f),B.set(f,A)}if(N)for(let f of N){let A=R(f);if(!A)continue;B.delete(f),X.delete(A);let T=W.indexOf(A);if(T!==-1)W.splice(T,1);K=!0}j.flags=G|(K?b:0);for(let f=j.sinks;f;f=f.nextSink)x(f.sink)})});F(j,V)}}let q={[Symbol.toStringTag]:s,[Symbol.isConcatSpreadable]:!0,*[Symbol.iterator](){for(let Z of W){let H=X.get(Z);if(H)yield H}},get length(){return m(),W.length},keys(){return m(),W.values()},get(){if(m(),j.sources){if(j.flags){let Z=j.flags&b;if(j.value=v(Q),Z){if(j.flags=G,I(j),j.error)throw j.error}else j.flags=p}}else if(I(j),j.error)throw j.error;return j.value},at(Z){let H=W[Z];return H!==void 0?X.get(H):void 0},byKey(Z){return X.get(Z)},keyAt(Z){return W[Z]},indexOfKey(Z){return W.indexOf(Z)},deriveCollection(Z){return x$(q,Z)}};return q}function r$($){return Y($,s)}function l$($){S("Effect",$);let J={fn:$,flags:G,sources:null,sourcesTail:null,cleanup:null},z=()=>{K$(J),J.fn=void 0,J.flags=p,J.sourcesTail=null,H$(J)};if(_)J$(_,z);return T$(J),z}function o$($,J){if(!_)throw new G$("match");let z=!Array.isArray($),X=z?[$]:$,{nil:W}=J,B=z?(j)=>J.ok(j[0]):(j)=>J.ok(j),D=z&&J.err?(j)=>J.err(j[0]):J.err??console.error,M,R=!1,P=Array(X.length);for(let j=0;j<X.length;j++)try{P[j]=X[j].get()}catch(m){if(m instanceof i){R=!0;continue}if(!M)M=[];M.push(m instanceof Error?m:Error(String(m)))}let Q;try{if(R)Q=W?.();else if(M)Q=D(M);else Q=B(P)}catch(j){D([j instanceof Error?j:Error(String(j))])}if(typeof Q==="function")return Q;if(Q instanceof Promise){let j=_,m=new AbortController;J$(j,()=>m.abort()),Q.then((q)=>{if(!m.signal.aborted&&typeof q==="function")J$(j,q)}).catch((q)=>{D([q instanceof Error?q:Error(String(q))])})}}function i$($,J){if(S(t,$,X$),J?.value!==void 0)C(t,J.value,J?.guard);let z={value:J?.value,sinks:null,sinksTail:null,equals:J?.equals??k,guard:J?.guard,stop:void 0};return{[Symbol.toStringTag]:t,get(){if(V){if(!z.sinks)z.stop=$((X)=>{C(t,X,z.guard),N$(z,X)});F(z,V)}return W$(t,z.value),z.value}}}function n$($){return Y($,t)}function a$($,J){let z=E($)||Array.isArray($),X=E(J)||Array.isArray(J);if(!z||!X){let j=!Object.is($,J);return{changed:j,add:j&&X?J:{},change:{},remove:j&&z?$:{}}}let W=new WeakSet,B={},D={},M={},R=!1,P=Object.keys($),Q=Object.keys(J);for(let j of Q)if(j in $){if(!n($[j],J[j],W))D[j]=J[j],R=!0}else B[j]=J[j],R=!0;for(let j of P)if(!(j in J))M[j]=void 0,R=!0;return{add:B,change:D,remove:M,changed:R}}function m$($,J){C(r,$,E);let z=new Map,X=(Q,j)=>{if(C(`${r} for key "${Q}"`,j),Array.isArray(j))z.set(Q,Q$(j));else if(E(j))z.set(Q,m$(j));else z.set(Q,y(j))},W=()=>{let Q={};return z.forEach((j,m)=>{Q[m]=j.get()}),Q},B={fn:W,value:$,flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:n,error:void 0},D=(Q)=>{let j=!1;for(let m in Q.add)X(m,Q.add[m]),j=!0;if(Object.keys(Q.change).length)z$(()=>{for(let m in Q.change){let q=Q.change[m];C(`${r} for key "${m}"`,q);let Z=z.get(m);if(Z)if(E(q)!==F$(Z))X(m,q),j=!0;else Z.set(q)}});for(let m in Q.remove)z.delete(m),j=!0;if(j)B.flags|=b;return Q.changed},M=J?.watched,R=M?()=>{if(V){if(!B.sinks)B.stop=M();F(B,V)}}:()=>{if(V)F(B,V)};for(let Q of Object.keys($))X(Q,$[Q]);let P={[Symbol.toStringTag]:r,[Symbol.isConcatSpreadable]:!1,*[Symbol.iterator](){for(let Q of Array.from(z.keys())){let j=z.get(Q);if(j)yield[Q,j]}},keys(){return R(),z.keys()},byKey(Q){return z.get(Q)},get(){if(R(),B.sources){if(B.flags){let Q=B.flags&b;if(B.value=v(W),Q){if(B.flags=G,I(B),B.error)throw B.error}else B.flags=p}}else if(I(B),B.error)throw B.error;return B.value},set(Q){let j=B.flags&G?W():B.value,m=a$(j,Q);if(D(m)){B.flags|=G;for(let q=B.sinks;q;q=q.nextSink)x(q.sink);if(O===0)w()}},update(Q){P.set(Q(P.get()))},add(Q,j){if(z.has(Q))throw new e(r,Q,j);X(Q,j),B.flags|=G|b;for(let m=B.sinks;m;m=m.nextSink)x(m.sink);if(O===0)w();return Q},remove(Q){if(z.delete(Q)){B.flags|=G|b;for(let m=B.sinks;m;m=m.nextSink)x(m.sink);if(O===0)w()}}};return new Proxy(P,{get(Q,j){if(j in Q)return Reflect.get(Q,j);if(typeof j!=="symbol")return Q.byKey(j)},has(Q,j){if(j in Q)return!0;return Q.byKey(String(j))!==void 0},ownKeys(Q){return Array.from(Q.keys())},getOwnPropertyDescriptor(Q,j){if(j in Q)return Reflect.getOwnPropertyDescriptor(Q,j);if(typeof j==="symbol")return;let m=Q.byKey(String(j));return m?{enumerable:!0,configurable:!0,writable:!0,value:m}:void 0}})}function F$($){return Y($,r)}function e$($,J){return o($)?q$($,J):B$($,J)}function $J($){if(M$($))return $;if($==null)throw new j$("createSignal",$);if(o($))return q$($);if(g($))return B$($);if(Y$($))return Q$($);if(E($))return m$($);return y($)}function JJ($){if(w$($))return $;if($==null||g($)||M$($))throw new j$("createMutableSignal",$);if(Y$($))return Q$($);if(E($))return m$($);return y($)}function zJ($){return S$($)||p$($)}function M$($){let J=[c,u,d,t,l,L,s,r],z=Object.prototype.toString.call($).slice(8,-1);return J.includes(z)}function w$($){return V$($)||F$($)||O$($)}function XJ($,J){C(l,$,M$);let z=$,X=J?.guard,W={fn:()=>z.get(),value:void 0,flags:G,sources:null,sourcesTail:null,sinks:null,sinksTail:null,equals:J?.equals??k,error:void 0},B=()=>{if(V)F(W,V);if(I(W),W.error)throw W.error;return W.value},D=(R)=>{if(!w$(z))throw new P$(l);C(l,R,X),z.set(R)},M=(R)=>{C(l,R,M$),z=R,W.flags|=G;for(let P=W.sinks;P;P=P.nextSink)x(P.sink);if(O===0)w()};return{[Symbol.toStringTag]:l,configurable:!0,enumerable:!0,get:B,set:D,replace:M,current:()=>z}}function ZJ($){return Y($,l)}export{D$ as valueString,v as untrack,d$ as unown,o$ as match,p$ as isTask,F$ as isStore,V$ as isState,ZJ as isSlot,M$ as isSignal,n$ as isSensor,E as isRecord,Y as isObjectOfType,w$ as isMutableSignal,S$ as isMemo,O$ as isList,g as isFunction,n as isEqual,zJ as isComputed,r$ as isCollection,o as isAsyncFunction,q$ as createTask,m$ as createStore,y as createState,XJ as createSlot,$J as createSignal,i$ as createSensor,u$ as createScope,JJ as createMutableSignal,B$ as createMemo,Q$ as createList,l$ as createEffect,e$ as createComputed,s$ as createCollection,z$ as batch,i as UnsetSignalValueError,L$ as SKIP_EQUALITY,G$ as RequiredOwnerError,P$ as ReadonlySignalError,A$ as NullishSignalValueError,j$ as InvalidSignalValueError,b$ as InvalidCallbackError,Z$ as CircularDependencyError};
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 1.0.0
3
+ * @version 1.1.0
4
4
  * @author Esther Brunner
5
5
  */
6
6
 
@@ -41,6 +41,7 @@ export {
41
41
  export {
42
42
  createEffect,
43
43
  type MatchHandlers,
44
+ type SingleMatchHandlers,
44
45
  type MaybePromise,
45
46
  match,
46
47
  } from './src/nodes/effect'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeix/cause-effect",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "author": "Esther Brunner",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -8,10 +8,10 @@
8
8
  "types": "types/index.d.ts",
9
9
  "devDependencies": {
10
10
  "@biomejs/biome": "2.4.6",
11
- "bun-types": "latest",
11
+ "@types/bun": "latest",
12
12
  "mitata": "^1.0.34",
13
13
  "random": "^5.4.1",
14
- "typescript": "latest"
14
+ "typescript": "^6.0.2"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "typescript": ">=5.8.0"
@@ -36,6 +36,28 @@ createEffect(() => {
36
36
  ```
37
37
  </direct_lookups_do_not_track>
38
38
 
39
+ <bykey_set_does_not_propagate_to_structural_subscribers>
40
+ **`byKey(key).set(value)` does not propagate to effects that subscribed via `list.keys()`,
41
+ `list.length`, or the iterator.** Those effects subscribe to the list's structural node but
42
+ do not establish item-level edges, so a direct item signal mutation reaches them only if
43
+ `list.get()` has previously been called to link the item signal to the list node.
44
+
45
+ Use `list.replace(key, value)` for imperative item updates. It propagates through both paths
46
+ — item-level edges and the structural node — regardless of how subscribers are attached.
47
+
48
+ ```typescript
49
+ // Wrong — silently does nothing for effects that subscribed via list.keys()
50
+ list.byKey(key)?.set(newValue)
51
+
52
+ // Correct — guaranteed propagation to all subscribers
53
+ list.replace(key, newValue)
54
+ ```
55
+
56
+ `byKey(key).set(value)` is safe only when the consuming effect directly calls
57
+ `byKey(key).get()` inside its body — that creates a direct edge from the item signal to the
58
+ effect, bypassing the list node entirely.
59
+ </bykey_set_does_not_propagate_to_structural_subscribers>
60
+
39
61
  <conditional_reads_delay_watched>
40
62
  **Conditional signal reads delay `watched` activation.** The `watched` callback on a State
41
63
  or Sensor fires when the first downstream effect subscribes. If a signal is only read inside
@@ -150,15 +150,19 @@ user.name = 'Bob' // only effects reading `user.name` re-run
150
150
  - You need to react to structural changes (items added, removed, reordered) as well as value changes
151
151
 
152
152
  **Key facts:**
153
- - Items are identified by a key; keys must be unique
153
+ - Items are identified by a stable key; keys survive sorting and reordering
154
154
  - `byKey()`, `at()`, `keyAt()`, and `indexOfKey()` are direct lookups — they **do not create graph edges**
155
155
  - To react to structural changes, read `get()`, `keys()`, or `length` instead
156
+ - To update an existing item, use `list.replace(key, value)` — **not** `byKey(key).set(value)`. `replace()` propagates to all subscribers; `byKey().set()` silently misses effects that subscribed via `keys()`, `length`, or the iterator
156
157
 
157
158
  ```typescript
158
- const todos = createList<Todo, 'id'>('id', [
159
- { id: '1', text: 'Buy milk', done: false },
160
- ])
161
- todos.push({ id: '2', text: 'Walk dog', done: false })
159
+ const todos = createList(
160
+ [{ id: 't1', text: 'Buy milk', done: false }],
161
+ { keyConfig: todo => todo.id }
162
+ )
163
+ todos.add({ id: 't2', text: 'Walk dog', done: false })
164
+ todos.replace('t1', { id: 't1', text: 'Buy milk', done: true }) // update in place
165
+ todos.remove('t2')
162
166
  ```
163
167
  </List>
164
168
 
@@ -30,6 +30,22 @@ createEffect(() => {
30
30
  ```
31
31
  </direct_lookups_do_not_track>
32
32
 
33
+ <bykey_set_does_not_propagate_to_structural_subscribers>
34
+ **`byKey(key).set(value)` does not propagate through `listNode.sinks` unless `itemSignal → listNode` edges exist.** Those edges are established only when `recomputeMemo(listNode)` runs — which requires `list.get()` to have been called. Effects that subscribed via `list.keys()`, `list.length`, or the iterator link to `listNode.sinks` but never trigger `recomputeMemo`, so the item-level edges are never created.
35
+
36
+ `list.replace(key, value)` is the correct API for imperative item mutations. It calls `signal.set(value)` (value path) then explicitly walks `node.sinks` (structural path), guaranteeing propagation regardless of edge state.
37
+
38
+ ```typescript
39
+ // Wrong — misses structural subscribers if list.get() was never called
40
+ list.byKey(key)?.set(newValue)
41
+
42
+ // Correct — propagates through both paths unconditionally
43
+ list.replace(key, newValue)
44
+ ```
45
+
46
+ `byKey(key).set(value)` is safe only when the consuming effect directly reads `byKey(key).get()` inside its body, establishing a direct `itemSignal → effectNode` edge.
47
+ </bykey_set_does_not_propagate_to_structural_subscribers>
48
+
33
49
  <conditional_reads_delay_watched>
34
50
  **Conditional signal reads delay `watched` activation.** The `watched` callback on a State or Sensor fires when the first downstream effect subscribes. If a signal is only read inside a branch that hasn't executed yet, `watched` does not fire until that branch runs.
35
51
 
@@ -39,6 +39,20 @@ type MatchHandlers<T extends readonly Signal<unknown & {}>[]> = {
39
39
  nil?: () => MaybePromise<MaybeCleanup>
40
40
  }
41
41
 
42
+ /**
43
+ * Handlers for a single signal passed to `match()`.
44
+ *
45
+ * @template T - The value type of the signal being matched
46
+ */
47
+ type SingleMatchHandlers<T extends {}> = {
48
+ /** Called when the signal has a value. Receives the resolved value directly. */
49
+ ok: (value: T) => MaybePromise<MaybeCleanup>
50
+ /** Called when the signal holds an error. Receives the error directly. Defaults to `console.error`. */
51
+ err?: (error: Error) => MaybePromise<MaybeCleanup>
52
+ /** Called when the signal is unset (pending). */
53
+ nil?: () => MaybePromise<MaybeCleanup>
54
+ }
55
+
42
56
  /* === Exported Functions === */
43
57
 
44
58
  /**
@@ -96,6 +110,20 @@ function createEffect(fn: EffectCallback): Cleanup {
96
110
  return dispose
97
111
  }
98
112
 
113
+ /**
114
+ * Reads one or more signals and dispatches to the appropriate handler based on their state.
115
+ * Must be called within an active owner (effect or scope) so async cleanup can be registered.
116
+ *
117
+ * @since 1.1
118
+ * @param signal - A single signal to read.
119
+ * @param handlers - Object with an `ok` branch (receives the value directly) and optional `err` and `nil` branches.
120
+ * @returns An optional cleanup function if the active handler returns one.
121
+ * @throws RequiredOwnerError If called without an active owner.
122
+ */
123
+ function match<T extends {}>(
124
+ signal: Signal<T>,
125
+ handlers: SingleMatchHandlers<T>,
126
+ ): MaybeCleanup
99
127
  /**
100
128
  * Reads one or more signals and dispatches to the appropriate handler based on their state.
101
129
  * Must be called within an active owner (effect or scope) so async cleanup can be registered.
@@ -105,13 +133,41 @@ function createEffect(fn: EffectCallback): Cleanup {
105
133
  * @param handlers - Object with an `ok` branch and optional `err` and `nil` branches.
106
134
  * @returns An optional cleanup function if the active handler returns one.
107
135
  * @throws RequiredOwnerError If called without an active owner.
136
+ *
137
+ * @remarks
138
+ * **Async handlers are for external side effects only** — DOM mutations, analytics, logging,
139
+ * or any fire-and-forget API call whose result does not need to drive reactive state.
140
+ * Do not call `.set()` on a signal inside an async handler: use a `Task` node instead,
141
+ * which receives an `AbortSignal`, is auto-cancelled on re-run, and integrates cleanly
142
+ * with `nil` and `err` branches.
143
+ *
144
+ * Rejections from async handlers are always routed to `err`, including rejections from
145
+ * stale runs that were already superseded by a newer signal value. The library cannot
146
+ * cancel external operations it did not start.
108
147
  */
109
148
  function match<T extends readonly Signal<unknown & {}>[]>(
110
149
  signals: readonly [...T],
111
150
  handlers: MatchHandlers<T>,
151
+ ): MaybeCleanup
152
+ function match(
153
+ signalOrSignals: Signal<unknown & {}> | readonly Signal<unknown & {}>[],
154
+ // biome-ignore lint/suspicious/noExplicitAny: implementation overload, not part of the public API
155
+ handlers: any,
112
156
  ): MaybeCleanup {
113
157
  if (!activeOwner) throw new RequiredOwnerError('match')
114
- const { ok, err = console.error, nil } = handlers
158
+
159
+ const isSingle = !Array.isArray(signalOrSignals)
160
+ const signals = isSingle ? [signalOrSignals] : signalOrSignals
161
+
162
+ const { nil } = handlers
163
+ const ok = isSingle
164
+ ? (values: unknown[]) => handlers.ok(values[0])
165
+ : (values: unknown[]) => handlers.ok(values)
166
+ const err: (errors: readonly Error[]) => MaybePromise<MaybeCleanup> =
167
+ isSingle && handlers.err
168
+ ? (errors: readonly Error[]) => handlers.err(errors[0])
169
+ : (handlers.err ?? console.error)
170
+
115
171
  let errors: Error[] | undefined
116
172
  let pending = false
117
173
  const values = new Array(signals.length)
@@ -133,12 +189,7 @@ function match<T extends readonly Signal<unknown & {}>[]>(
133
189
  try {
134
190
  if (pending) out = nil?.()
135
191
  else if (errors) out = err(errors)
136
- else
137
- out = ok(
138
- values as {
139
- [K in keyof T]: T[K] extends Signal<infer V> ? V : never
140
- },
141
- )
192
+ else out = ok(values)
142
193
  } catch (e) {
143
194
  err([e instanceof Error ? e : new Error(String(e))])
144
195
  }
@@ -158,4 +209,10 @@ function match<T extends readonly Signal<unknown & {}>[]>(
158
209
  }
159
210
  }
160
211
 
161
- export { type MaybePromise, type MatchHandlers, createEffect, match }
212
+ export {
213
+ type MaybePromise,
214
+ type MatchHandlers,
215
+ type SingleMatchHandlers,
216
+ createEffect,
217
+ match,
218
+ }
package/src/nodes/list.ts CHANGED
@@ -82,6 +82,13 @@ type List<T extends {}> = {
82
82
  indexOfKey(key: string): number
83
83
  add(value: T): string
84
84
  remove(keyOrIndex: string | number): void
85
+ /**
86
+ * Updates an existing item by key, propagating to all subscribers.
87
+ * No-op if the key does not exist or the value is reference-equal to the current value.
88
+ * @param key - Stable key of the item to update
89
+ * @param value - New value for the item
90
+ */
91
+ replace(key: string, value: T): void
85
92
  sort(compareFn?: (a: T, b: T) => number): void
86
93
  splice(start: number, deleteCount?: number, ...items: T[]): T[]
87
94
  deriveCollection<R extends {}>(
@@ -495,6 +502,17 @@ function createList<T extends {}>(
495
502
  }
496
503
  },
497
504
 
505
+ replace(key: string, value: T) {
506
+ const signal = signals.get(key)
507
+ if (!signal) return
508
+ validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value)
509
+ if (signal.get() === value) return
510
+ signal.set(value)
511
+ node.flags |= FLAG_DIRTY
512
+ for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
513
+ if (batchDepth === 0) flush()
514
+ },
515
+
498
516
  sort(compareFn?: (a: T, b: T) => number) {
499
517
  const entries = keys
500
518
  .map(key => [key, signals.get(key)?.get()] as [string, T])
@@ -475,6 +475,94 @@ describe('match', () => {
475
475
  expect(() => match([], { ok: () => {} })).toThrow(RequiredOwnerError)
476
476
  })
477
477
 
478
+ describe('Single-signal overload', () => {
479
+ test('should call ok with unwrapped value', () => {
480
+ const s = createState(42)
481
+ let result = 0
482
+ createEffect(() =>
483
+ match(s, {
484
+ ok: value => {
485
+ result = value
486
+ },
487
+ }),
488
+ )
489
+ expect(result).toBe(42)
490
+ s.set(99)
491
+ expect(result).toBe(99)
492
+ })
493
+
494
+ test('should call nil handler when signal is unset', async () => {
495
+ const task = createTask(async () => {
496
+ await wait(50)
497
+ return 42
498
+ })
499
+ let okCount = 0
500
+ let nilCount = 0
501
+ createEffect(() =>
502
+ match(task, {
503
+ ok: value => {
504
+ okCount++
505
+ expect(value).toBe(42)
506
+ },
507
+ nil: () => {
508
+ nilCount++
509
+ },
510
+ }),
511
+ )
512
+ expect(okCount).toBe(0)
513
+ expect(nilCount).toBe(1)
514
+ await wait(60)
515
+ expect(okCount).toBeGreaterThan(0)
516
+ expect(nilCount).toBe(1)
517
+ })
518
+
519
+ test('should call err with unwrapped Error', () => {
520
+ const a = createState(1)
521
+ const b = createMemo(() => {
522
+ if (a.get() > 5) throw new Error('Too high')
523
+ return a.get() * 2
524
+ })
525
+ let okCount = 0
526
+ let errCount = 0
527
+ createEffect(() =>
528
+ match(b, {
529
+ ok: () => {
530
+ okCount++
531
+ },
532
+ err: error => {
533
+ errCount++
534
+ expect(error.message).toBe('Too high')
535
+ },
536
+ }),
537
+ )
538
+ expect(okCount).toBe(1)
539
+ a.set(6)
540
+ expect(errCount).toBe(1)
541
+ a.set(3)
542
+ expect(okCount).toBe(2)
543
+ expect(errCount).toBe(1)
544
+ })
545
+
546
+ test('should fall back to console.error for single signal without err handler', () => {
547
+ const originalConsoleError = console.error
548
+ const mockConsoleError = mock(() => {})
549
+ console.error = mockConsoleError
550
+
551
+ try {
552
+ const a = createState(1)
553
+ const b = createMemo(() => {
554
+ if (a.get() > 5) throw new Error('Too high')
555
+ return a.get() * 2
556
+ })
557
+ createEffect(() => match(b, { ok: () => {} }))
558
+ a.set(6)
559
+ expect(mockConsoleError).toHaveBeenCalled()
560
+ } finally {
561
+ console.error = originalConsoleError
562
+ }
563
+ })
564
+ })
565
+
478
566
  test('should resolve multiple async tasks without waterfalls', async () => {
479
567
  const a = createTask(async () => {
480
568
  await wait(20)
package/test/list.test.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import {
3
+ batch,
3
4
  createEffect,
4
5
  createList,
5
6
  createMemo,
@@ -209,6 +210,126 @@ describe('List', () => {
209
210
  })
210
211
  })
211
212
 
213
+ describe('replace', () => {
214
+ test('should update the item signal value', () => {
215
+ const list = createList(['a', 'b', 'c'])
216
+ // biome-ignore lint/style/noNonNullAssertion: index is within bounds
217
+ const key = list.keyAt(1)!
218
+ list.replace(key, 'B')
219
+ expect(list.byKey(key)?.get()).toBe('B')
220
+ })
221
+
222
+ test('structural subscriber via keys() re-runs after replace(); byKey().set() does NOT', () => {
223
+ const list = createList(['x', 'y'])
224
+ // biome-ignore lint/style/noNonNullAssertion: index is within bounds
225
+ const key = list.keyAt(0)!
226
+ let effectCount = 0
227
+
228
+ // This effect subscribes structurally via keys() only — no list.get() call
229
+ createEffect(() => {
230
+ void [...list.keys()]
231
+ effectCount++
232
+ })
233
+
234
+ expect(effectCount).toBe(1)
235
+
236
+ // replace() propagates through node.sinks — structural subscriber re-runs
237
+ list.replace(key, 'X')
238
+ expect(effectCount).toBe(2)
239
+
240
+ // byKey().set() does NOT propagate through node.sinks — structural subscriber does NOT re-run
241
+ list.byKey(key)?.set('XX')
242
+ expect(effectCount).toBe(2)
243
+ })
244
+
245
+ test('direct subscriber via byKey().get() re-runs after replace()', () => {
246
+ const list = createList(['a', 'b'])
247
+ // biome-ignore lint/style/noNonNullAssertion: index is within bounds
248
+ const key = list.keyAt(0)!
249
+ let lastValue = ''
250
+ let effectCount = 0
251
+
252
+ createEffect(() => {
253
+ // biome-ignore lint/style/noNonNullAssertion: key is valid
254
+ lastValue = list.byKey(key)!.get()
255
+ effectCount++
256
+ })
257
+
258
+ expect(effectCount).toBe(1)
259
+ expect(lastValue).toBe('a')
260
+
261
+ list.replace(key, 'A')
262
+ expect(effectCount).toBe(2)
263
+ expect(lastValue).toBe('A')
264
+ })
265
+
266
+ test('no-op on equal value (same reference)', () => {
267
+ const obj = { id: 1 }
268
+ const list = createList([obj])
269
+ // biome-ignore lint/style/noNonNullAssertion: index is within bounds
270
+ const key = list.keyAt(0)!
271
+ let effectCount = 0
272
+
273
+ createEffect(() => {
274
+ list.get()
275
+ effectCount++
276
+ })
277
+
278
+ expect(effectCount).toBe(1)
279
+
280
+ list.replace(key, obj)
281
+ expect(effectCount).toBe(1)
282
+ })
283
+
284
+ test('no-op on missing key — does not throw and does not trigger effects', () => {
285
+ const list = createList([1, 2, 3])
286
+ let effectCount = 0
287
+
288
+ createEffect(() => {
289
+ list.get()
290
+ effectCount++
291
+ })
292
+
293
+ expect(effectCount).toBe(1)
294
+ expect(() => list.replace('nonexistent', 99)).not.toThrow()
295
+ expect(effectCount).toBe(1)
296
+ })
297
+
298
+ test('batch compatibility — effects run only once inside batch()', () => {
299
+ const list = createList(['a', 'b', 'c'])
300
+ // biome-ignore lint/style/noNonNullAssertion: index is within bounds
301
+ const key0 = list.keyAt(0)!
302
+ // biome-ignore lint/style/noNonNullAssertion: index is within bounds
303
+ const key1 = list.keyAt(1)!
304
+ let effectCount = 0
305
+
306
+ createEffect(() => {
307
+ list.get()
308
+ effectCount++
309
+ })
310
+
311
+ expect(effectCount).toBe(1)
312
+
313
+ batch(() => {
314
+ list.replace(key0, 'A')
315
+ list.replace(key1, 'B')
316
+ })
317
+
318
+ expect(effectCount).toBe(2)
319
+ expect(list.get()).toEqual(['A', 'B', 'c'])
320
+ })
321
+
322
+ test('signal identity preserved — byKey() returns same signal before and after replace()', () => {
323
+ const list = createList([10, 20])
324
+ // biome-ignore lint/style/noNonNullAssertion: index is within bounds
325
+ const key = list.keyAt(0)!
326
+ const signalBefore = list.byKey(key)
327
+ list.replace(key, 99)
328
+ const signalAfter = list.byKey(key)
329
+ expect(signalBefore).toBe(signalAfter)
330
+ })
331
+ })
332
+
212
333
  describe('sort', () => {
213
334
  test('should sort with default string comparison', () => {
214
335
  const list = createList([3, 1, 2])
package/types/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 1.0.0
3
+ * @version 1.1.0
4
4
  * @author Esther Brunner
5
5
  */
6
6
  export { CircularDependencyError, type Guard, InvalidCallbackError, InvalidSignalValueError, NullishSignalValueError, ReadonlySignalError, RequiredOwnerError, UnsetSignalValueError, } from './src/errors';
7
7
  export { batch, type Cleanup, type ComputedOptions, createScope, type EffectCallback, type MaybeCleanup, type MemoCallback, type Signal, type SignalOptions, SKIP_EQUALITY, type TaskCallback, unown, untrack, } from './src/graph';
8
8
  export { type Collection, type CollectionCallback, type CollectionChanges, type CollectionOptions, createCollection, type DeriveCollectionCallback, isCollection, } from './src/nodes/collection';
9
- export { createEffect, type MatchHandlers, type MaybePromise, match, } from './src/nodes/effect';
9
+ export { createEffect, type MatchHandlers, type SingleMatchHandlers, type MaybePromise, match, } from './src/nodes/effect';
10
10
  export { createList, isEqual, isList, type KeyConfig, type List, type ListOptions, } from './src/nodes/list';
11
11
  export { createMemo, isMemo, type Memo } from './src/nodes/memo';
12
12
  export { createSensor, isSensor, type Sensor, type SensorCallback, type SensorOptions, } from './src/nodes/sensor';
@@ -226,6 +226,8 @@ declare function createScope(fn: () => MaybeCleanup): Cleanup;
226
226
  * reactive graph.
227
227
  *
228
228
  * @since 0.18.5
229
+ * @param fn - The function to execute without an active owner
230
+ * @returns The return value of `fn`
229
231
  */
230
232
  declare function unown<T>(fn: () => T): T;
231
233
  export { type Cleanup, type ComputedOptions, type EffectCallback, type EffectNode, type MaybeCleanup, type MemoCallback, type MemoNode, type Scope, type Signal, type SignalOptions, type SinkNode, type StateNode, type TaskCallback, type TaskNode, activeOwner, activeSink, batch, batchDepth, createScope, DEFAULT_EQUALITY, SKIP_EQUALITY, FLAG_CHECK, FLAG_CLEAN, FLAG_DIRTY, FLAG_RELINK, flush, link, propagate, refresh, registerCleanup, runCleanup, runEffect, setState, trimSources, TYPE_COLLECTION, TYPE_LIST, TYPE_MEMO, TYPE_SENSOR, TYPE_STATE, TYPE_SLOT, TYPE_STORE, TYPE_TASK, unlink, unown, untrack, };
@@ -1,7 +1,21 @@
1
1
  import { type Cleanup, type Signal } from '../graph';
2
2
  import { type KeyConfig, type List } from './list';
3
3
  type CollectionSource<T extends {}> = List<T> | Collection<T>;
4
+ /**
5
+ * Transformation callback for `deriveCollection` — sync or async.
6
+ * Sync callbacks produce a `Memo<T>` per item; async callbacks produce a `Task<T>`
7
+ * with automatic cancellation when the source item changes.
8
+ *
9
+ * @template T - The type of derived items
10
+ * @template U - The type of source items
11
+ */
4
12
  type DeriveCollectionCallback<T extends {}, U extends {}> = ((sourceValue: U) => T) | ((sourceValue: U, abort: AbortSignal) => Promise<T>);
13
+ /**
14
+ * A read-only reactive keyed collection with per-item reactivity.
15
+ * Created by `createCollection` (externally driven) or via `.deriveCollection()` on a `List` or `Collection`.
16
+ *
17
+ * @template T - The type of items in the collection
18
+ */
5
19
  type Collection<T extends {}> = {
6
20
  readonly [Symbol.toStringTag]: 'Collection';
7
21
  readonly [Symbol.isConcatSpreadable]: true;
@@ -16,16 +30,40 @@ type Collection<T extends {}> = {
16
30
  deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>): Collection<R>;
17
31
  readonly length: number;
18
32
  };
33
+ /**
34
+ * Granular mutation descriptor passed to the `applyChanges` callback inside a `CollectionCallback`.
35
+ *
36
+ * @template T - The type of items in the collection
37
+ */
19
38
  type CollectionChanges<T> = {
39
+ /** Items to add. Each item is assigned a new key via the configured `keyConfig`. */
20
40
  add?: T[];
41
+ /** Items whose values have changed. Matched to existing entries by key. */
21
42
  change?: T[];
43
+ /** Items to remove. Matched to existing entries by key. */
22
44
  remove?: T[];
23
45
  };
46
+ /**
47
+ * Configuration options for `createCollection`.
48
+ *
49
+ * @template T - The type of items in the collection
50
+ */
24
51
  type CollectionOptions<T extends {}> = {
52
+ /** Initial items. Defaults to `[]`. */
25
53
  value?: T[];
54
+ /** Key generation strategy. See `KeyConfig`. Defaults to auto-increment. */
26
55
  keyConfig?: KeyConfig<T>;
56
+ /** Factory for per-item signals. Defaults to `createState`. */
27
57
  createItem?: (value: T) => Signal<T>;
28
58
  };
59
+ /**
60
+ * Setup callback for `createCollection`. Invoked when the collection gains its first downstream
61
+ * subscriber; receives an `applyChanges` function to push granular mutations into the graph.
62
+ *
63
+ * @template T - The type of items in the collection
64
+ * @param apply - Call with a `CollectionChanges` object to add, update, or remove items
65
+ * @returns A cleanup function invoked when the collection loses all subscribers
66
+ */
29
67
  type CollectionCallback<T extends {}> = (apply: (changes: CollectionChanges<T>) => void) => Cleanup;
30
68
  /**
31
69
  * Creates a derived Collection from a List or another Collection with item-level memoization.
@@ -1,10 +1,32 @@
1
1
  import { type Cleanup, type EffectCallback, type MaybeCleanup, type Signal } from '../graph';
2
+ /** A value that is either synchronous or a `Promise` — used for handler return types in `match()`. */
2
3
  type MaybePromise<T> = T | Promise<T>;
4
+ /**
5
+ * Handlers for all states of one or more signals passed to `match()`.
6
+ *
7
+ * @template T - Tuple of `Signal` types being matched
8
+ */
3
9
  type MatchHandlers<T extends readonly Signal<unknown & {}>[]> = {
10
+ /** Called when all signals have a value. Receives a tuple of resolved values. */
4
11
  ok: (values: {
5
12
  [K in keyof T]: T[K] extends Signal<infer V> ? V : never;
6
13
  }) => MaybePromise<MaybeCleanup>;
14
+ /** Called when one or more signals hold an error. Defaults to `console.error`. */
7
15
  err?: (errors: readonly Error[]) => MaybePromise<MaybeCleanup>;
16
+ /** Called when one or more signals are unset (pending). */
17
+ nil?: () => MaybePromise<MaybeCleanup>;
18
+ };
19
+ /**
20
+ * Handlers for a single signal passed to `match()`.
21
+ *
22
+ * @template T - The value type of the signal being matched
23
+ */
24
+ type SingleMatchHandlers<T extends {}> = {
25
+ /** Called when the signal has a value. Receives the resolved value directly. */
26
+ ok: (value: T) => MaybePromise<MaybeCleanup>;
27
+ /** Called when the signal holds an error. Receives the error directly. Defaults to `console.error`. */
28
+ err?: (error: Error) => MaybePromise<MaybeCleanup>;
29
+ /** Called when the signal is unset (pending). */
8
30
  nil?: () => MaybePromise<MaybeCleanup>;
9
31
  };
10
32
  /**
@@ -38,11 +60,36 @@ type MatchHandlers<T extends readonly Signal<unknown & {}>[]> = {
38
60
  */
39
61
  declare function createEffect(fn: EffectCallback): Cleanup;
40
62
  /**
41
- * Runs handlers based on the current values of signals.
63
+ * Reads one or more signals and dispatches to the appropriate handler based on their state.
64
+ * Must be called within an active owner (effect or scope) so async cleanup can be registered.
65
+ *
66
+ * @since 1.1
67
+ * @param signal - A single signal to read.
68
+ * @param handlers - Object with an `ok` branch (receives the value directly) and optional `err` and `nil` branches.
69
+ * @returns An optional cleanup function if the active handler returns one.
70
+ * @throws RequiredOwnerError If called without an active owner.
71
+ */
72
+ declare function match<T extends {}>(signal: Signal<T>, handlers: SingleMatchHandlers<T>): MaybeCleanup;
73
+ /**
74
+ * Reads one or more signals and dispatches to the appropriate handler based on their state.
42
75
  * Must be called within an active owner (effect or scope) so async cleanup can be registered.
43
76
  *
44
77
  * @since 0.15.0
78
+ * @param signals - Tuple of signals to read; all must have a value for `ok` to run.
79
+ * @param handlers - Object with an `ok` branch and optional `err` and `nil` branches.
80
+ * @returns An optional cleanup function if the active handler returns one.
45
81
  * @throws RequiredOwnerError If called without an active owner.
82
+ *
83
+ * @remarks
84
+ * **Async handlers are for external side effects only** — DOM mutations, analytics, logging,
85
+ * or any fire-and-forget API call whose result does not need to drive reactive state.
86
+ * Do not call `.set()` on a signal inside an async handler: use a `Task` node instead,
87
+ * which receives an `AbortSignal`, is auto-cancelled on re-run, and integrates cleanly
88
+ * with `nil` and `err` branches.
89
+ *
90
+ * Rejections from async handlers are always routed to `err`, including rejections from
91
+ * stale runs that were already superseded by a newer signal value. The library cannot
92
+ * cancel external operations it did not start.
46
93
  */
47
94
  declare function match<T extends readonly Signal<unknown & {}>[]>(signals: readonly [...T], handlers: MatchHandlers<T>): MaybeCleanup;
48
- export { type MaybePromise, type MatchHandlers, createEffect, match };
95
+ export { type MaybePromise, type MatchHandlers, type SingleMatchHandlers, createEffect, match, };
@@ -8,11 +8,31 @@ type DiffResult = {
8
8
  change: UnknownRecord;
9
9
  remove: UnknownRecord;
10
10
  };
11
+ /**
12
+ * Key generation strategy for `createList` items.
13
+ * A string value is used as a prefix for auto-incremented keys (`prefix0`, `prefix1`, …).
14
+ * A function receives each item and returns a stable string key, or `undefined` to fall back to auto-increment.
15
+ *
16
+ * @template T - The type of items in the list
17
+ */
11
18
  type KeyConfig<T> = string | ((item: T) => string | undefined);
19
+ /**
20
+ * Configuration options for `createList`.
21
+ *
22
+ * @template T - The type of items in the list
23
+ */
12
24
  type ListOptions<T extends {}> = {
25
+ /** Key generation strategy. A string prefix or a function `(item) => string | undefined`. Defaults to auto-increment. */
13
26
  keyConfig?: KeyConfig<T>;
27
+ /** Lifecycle callback invoked when the list gains its first downstream subscriber. Must return a cleanup function. */
14
28
  watched?: () => Cleanup;
15
29
  };
30
+ /**
31
+ * A reactive ordered array with stable keys and per-item reactivity.
32
+ * Each item is a `State<T>` signal; structural changes (add/remove/sort) propagate reactively.
33
+ *
34
+ * @template T - The type of items in the list
35
+ */
16
36
  type List<T extends {}> = {
17
37
  readonly [Symbol.toStringTag]: 'List';
18
38
  readonly [Symbol.isConcatSpreadable]: true;
@@ -28,6 +48,13 @@ type List<T extends {}> = {
28
48
  indexOfKey(key: string): number;
29
49
  add(value: T): string;
30
50
  remove(keyOrIndex: string | number): void;
51
+ /**
52
+ * Updates an existing item by key, propagating to all subscribers.
53
+ * No-op if the key does not exist or the value is reference-equal to the current value.
54
+ * @param key - Stable key of the item to update
55
+ * @param value - New value for the item
56
+ */
57
+ replace(key: string, value: T): void;
31
58
  sort(compareFn?: (a: T, b: T) => number): void;
32
59
  splice(start: number, deleteCount?: number, ...items: T[]): T[];
33
60
  deriveCollection<R extends {}>(callback: (sourceValue: T) => R): Collection<R>;
@@ -51,8 +78,9 @@ declare function getKeyGenerator<T extends {}>(keyConfig?: KeyConfig<T>): [(item
51
78
  *
52
79
  * @since 0.18.0
53
80
  * @param value - Initial array of items
54
- * @param options - Optional configuration for key generation and watch lifecycle
55
- * @returns A List signal
81
+ * @param options.keyConfig - Key generation strategy: string prefix or `(item) => string | undefined`. Defaults to auto-increment.
82
+ * @param options.watched - Lifecycle callback invoked on first subscriber; must return a cleanup function called on last unsubscribe.
83
+ * @returns A `List` signal with reactive per-item `State` signals
56
84
  */
57
85
  declare function createList<T extends {}>(value: T[], options?: ListOptions<T>): List<T>;
58
86
  /**
@@ -12,7 +12,6 @@ type Memo<T extends {}> = {
12
12
  * Recomputes if dependencies have changed since last access.
13
13
  * When called inside another reactive context, creates a dependency.
14
14
  * @returns The computed value
15
- * @throws UnsetSignalValueError If the memo value is still unset when read.
16
15
  */
17
16
  get(): T;
18
17
  };
@@ -15,11 +15,9 @@ type Sensor<T extends {}> = {
15
15
  get(): T;
16
16
  };
17
17
  /**
18
- * A callback function for sensors when the sensor starts being watched.
18
+ * Configuration options for `createSensor`.
19
19
  *
20
- * @template T - The type of value observed
21
- * @param set - A function to set the observed value
22
- * @returns A cleanup function when the sensor stops being watched
20
+ * @template T - The type of value produced by the sensor
23
21
  */
24
22
  type SensorOptions<T extends {}> = SignalOptions<T> & {
25
23
  /**
@@ -28,6 +26,14 @@ type SensorOptions<T extends {}> = SignalOptions<T> & {
28
26
  */
29
27
  value?: T;
30
28
  };
29
+ /**
30
+ * Setup callback for `createSensor`. Invoked when the sensor gains its first downstream
31
+ * subscriber; receives a `set` function to push new values into the graph.
32
+ *
33
+ * @template T - The type of value produced by the sensor
34
+ * @param set - Updates the sensor value and propagates the change to subscribers
35
+ * @returns A cleanup function invoked when the sensor loses all subscribers
36
+ */
31
37
  type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup;
32
38
  /**
33
39
  * Creates a sensor that tracks external input and updates a state value as long as it is active.
@@ -1,7 +1,11 @@
1
1
  import { type Cleanup, TYPE_STORE } from '../graph';
2
2
  import { type List, type UnknownRecord } from './list';
3
3
  import { type State } from './state';
4
+ /**
5
+ * Configuration options for `createStore`.
6
+ */
4
7
  type StoreOptions = {
8
+ /** Invoked when the store gains its first downstream subscriber; returns a cleanup called when the last one unsubscribes. */
5
9
  watched?: () => Cleanup;
6
10
  };
7
11
  type BaseStore<T extends UnknownRecord> = {
@@ -19,6 +23,13 @@ type BaseStore<T extends UnknownRecord> = {
19
23
  add<K extends keyof T & string>(key: K, value: T[K]): K;
20
24
  remove(key: string): void;
21
25
  };
26
+ /**
27
+ * A reactive object with per-property reactivity.
28
+ * Each property is wrapped as a `State`, nested `Store`, or `List` signal, accessible directly via proxy.
29
+ * Updating one property only re-runs effects that read that property.
30
+ *
31
+ * @template T - The plain-object type whose properties become reactive signals
32
+ */
22
33
  type Store<T extends UnknownRecord> = BaseStore<T> & {
23
34
  [K in keyof T]: T[K] extends readonly (infer U extends {})[] ? List<U> : T[K] extends UnknownRecord ? Store<T[K]> : T[K] extends unknown & {} ? State<T[K] & {}> : State<T[K] & {}> | undefined;
24
35
  };
@@ -4,6 +4,12 @@ import { type Memo } from './nodes/memo';
4
4
  import { type State } from './nodes/state';
5
5
  import { type Store } from './nodes/store';
6
6
  import { type Task } from './nodes/task';
7
+ /**
8
+ * A readable and writable signal — the type union of `State`, `Store`, and `List`.
9
+ * Use as a parameter type for generic code that accepts any writable signal.
10
+ *
11
+ * @template T - The type of value held by the signal
12
+ */
7
13
  type MutableSignal<T extends {}> = {
8
14
  get(): T;
9
15
  set(value: T): void;