@zeix/cause-effect 1.0.0 → 1.0.2

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.
Files changed (62) hide show
  1. package/.github/copilot-instructions.md +5 -1
  2. package/.zed/settings.json +24 -1
  3. package/ARCHITECTURE.md +27 -1
  4. package/CHANGELOG.md +21 -0
  5. package/CLAUDE.md +1 -1
  6. package/GUIDE.md +2 -5
  7. package/README.md +45 -3
  8. package/REQUIREMENTS.md +3 -3
  9. package/eslint.config.js +2 -1
  10. package/index.dev.js +14 -0
  11. package/index.js +1 -1
  12. package/index.ts +1 -1
  13. package/package.json +5 -4
  14. package/skills/cause-effect/SKILL.md +69 -0
  15. package/skills/cause-effect/agents/openai.yaml +4 -0
  16. package/skills/cause-effect/references/api-facts.md +179 -0
  17. package/skills/cause-effect/references/error-classes.md +153 -0
  18. package/skills/cause-effect/references/non-obvious-behaviors.md +195 -0
  19. package/skills/cause-effect/references/signal-types.md +292 -0
  20. package/skills/cause-effect/workflows/answer-question.md +54 -0
  21. package/skills/cause-effect/workflows/debug.md +71 -0
  22. package/skills/cause-effect/workflows/use-api.md +63 -0
  23. package/skills/cause-effect-dev/SKILL.md +61 -100
  24. package/skills/cause-effect-dev/references/api-facts.md +96 -0
  25. package/skills/cause-effect-dev/references/error-classes.md +97 -0
  26. package/skills/cause-effect-dev/references/internal-types.md +54 -0
  27. package/skills/cause-effect-dev/references/non-obvious-behaviors.md +162 -0
  28. package/skills/cause-effect-dev/references/source-map.md +45 -0
  29. package/skills/cause-effect-dev/workflows/answer-question.md +55 -0
  30. package/skills/cause-effect-dev/workflows/fix-bug.md +63 -0
  31. package/skills/cause-effect-dev/workflows/implement-feature.md +46 -0
  32. package/skills/cause-effect-dev/workflows/write-tests.md +64 -0
  33. package/skills/changelog-keeper/SKILL.md +47 -37
  34. package/skills/tech-writer/SKILL.md +94 -0
  35. package/skills/tech-writer/references/document-map.md +199 -0
  36. package/skills/tech-writer/references/tone-guide.md +189 -0
  37. package/skills/tech-writer/workflows/consistency-review.md +98 -0
  38. package/skills/tech-writer/workflows/update-after-change.md +65 -0
  39. package/skills/tech-writer/workflows/update-agent-docs.md +77 -0
  40. package/skills/tech-writer/workflows/update-architecture.md +61 -0
  41. package/skills/tech-writer/workflows/update-jsdoc.md +72 -0
  42. package/skills/tech-writer/workflows/update-public-api.md +59 -0
  43. package/skills/tech-writer/workflows/update-requirements.md +80 -0
  44. package/src/graph.ts +2 -0
  45. package/src/nodes/collection.ts +38 -0
  46. package/src/nodes/effect.ts +13 -1
  47. package/src/nodes/list.ts +41 -2
  48. package/src/nodes/memo.ts +0 -1
  49. package/src/nodes/sensor.ts +10 -4
  50. package/src/nodes/store.ts +11 -0
  51. package/src/signal.ts +6 -0
  52. package/test/list.test.ts +121 -0
  53. package/tsconfig.json +9 -0
  54. package/types/index.d.ts +1 -1
  55. package/types/src/graph.d.ts +2 -0
  56. package/types/src/nodes/collection.d.ts +38 -0
  57. package/types/src/nodes/effect.d.ts +13 -1
  58. package/types/src/nodes/list.d.ts +30 -2
  59. package/types/src/nodes/memo.d.ts +0 -1
  60. package/types/src/nodes/sensor.d.ts +10 -4
  61. package/types/src/nodes/store.d.ts +11 -0
  62. package/types/src/signal.d.ts +6 -0
package/src/nodes/list.ts CHANGED
@@ -40,13 +40,33 @@ type DiffResult = {
40
40
  remove: UnknownRecord
41
41
  }
42
42
 
43
+ /**
44
+ * Key generation strategy for `createList` items.
45
+ * A string value is used as a prefix for auto-incremented keys (`prefix0`, `prefix1`, …).
46
+ * A function receives each item and returns a stable string key, or `undefined` to fall back to auto-increment.
47
+ *
48
+ * @template T - The type of items in the list
49
+ */
43
50
  type KeyConfig<T> = string | ((item: T) => string | undefined)
44
51
 
52
+ /**
53
+ * Configuration options for `createList`.
54
+ *
55
+ * @template T - The type of items in the list
56
+ */
45
57
  type ListOptions<T extends {}> = {
58
+ /** Key generation strategy. A string prefix or a function `(item) => string | undefined`. Defaults to auto-increment. */
46
59
  keyConfig?: KeyConfig<T>
60
+ /** Lifecycle callback invoked when the list gains its first downstream subscriber. Must return a cleanup function. */
47
61
  watched?: () => Cleanup
48
62
  }
49
63
 
64
+ /**
65
+ * A reactive ordered array with stable keys and per-item reactivity.
66
+ * Each item is a `State<T>` signal; structural changes (add/remove/sort) propagate reactively.
67
+ *
68
+ * @template T - The type of items in the list
69
+ */
50
70
  type List<T extends {}> = {
51
71
  readonly [Symbol.toStringTag]: 'List'
52
72
  readonly [Symbol.isConcatSpreadable]: true
@@ -62,6 +82,13 @@ type List<T extends {}> = {
62
82
  indexOfKey(key: string): number
63
83
  add(value: T): string
64
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
65
92
  sort(compareFn?: (a: T, b: T) => number): void
66
93
  splice(start: number, deleteCount?: number, ...items: T[]): T[]
67
94
  deriveCollection<R extends {}>(
@@ -240,8 +267,9 @@ function diffArrays<T>(
240
267
  *
241
268
  * @since 0.18.0
242
269
  * @param value - Initial array of items
243
- * @param options - Optional configuration for key generation and watch lifecycle
244
- * @returns A List signal
270
+ * @param options.keyConfig - Key generation strategy: string prefix or `(item) => string | undefined`. Defaults to auto-increment.
271
+ * @param options.watched - Lifecycle callback invoked on first subscriber; must return a cleanup function called on last unsubscribe.
272
+ * @returns A `List` signal with reactive per-item `State` signals
245
273
  */
246
274
  function createList<T extends {}>(
247
275
  value: T[],
@@ -474,6 +502,17 @@ function createList<T extends {}>(
474
502
  }
475
503
  },
476
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
+
477
516
  sort(compareFn?: (a: T, b: T) => number) {
478
517
  const entries = keys
479
518
  .map(key => [key, signals.get(key)?.get()] as [string, T])
package/src/nodes/memo.ts CHANGED
@@ -36,7 +36,6 @@ type Memo<T extends {}> = {
36
36
  * Recomputes if dependencies have changed since last access.
37
37
  * When called inside another reactive context, creates a dependency.
38
38
  * @returns The computed value
39
- * @throws UnsetSignalValueError If the memo value is still unset when read.
40
39
  */
41
40
  get(): T
42
41
  }
@@ -35,11 +35,9 @@ type Sensor<T extends {}> = {
35
35
  }
36
36
 
37
37
  /**
38
- * A callback function for sensors when the sensor starts being watched.
38
+ * Configuration options for `createSensor`.
39
39
  *
40
- * @template T - The type of value observed
41
- * @param set - A function to set the observed value
42
- * @returns A cleanup function when the sensor stops being watched
40
+ * @template T - The type of value produced by the sensor
43
41
  */
44
42
  type SensorOptions<T extends {}> = SignalOptions<T> & {
45
43
  /**
@@ -49,6 +47,14 @@ type SensorOptions<T extends {}> = SignalOptions<T> & {
49
47
  value?: T
50
48
  }
51
49
 
50
+ /**
51
+ * Setup callback for `createSensor`. Invoked when the sensor gains its first downstream
52
+ * subscriber; receives a `set` function to push new values into the graph.
53
+ *
54
+ * @template T - The type of value produced by the sensor
55
+ * @param set - Updates the sensor value and propagates the change to subscribers
56
+ * @returns A cleanup function invoked when the sensor loses all subscribers
57
+ */
52
58
  type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup
53
59
 
54
60
  /* === Exported Functions === */
@@ -28,7 +28,11 @@ import { createState, type State } from './state'
28
28
 
29
29
  /* === Types === */
30
30
 
31
+ /**
32
+ * Configuration options for `createStore`.
33
+ */
31
34
  type StoreOptions = {
35
+ /** Invoked when the store gains its first downstream subscriber; returns a cleanup called when the last one unsubscribes. */
32
36
  watched?: () => Cleanup
33
37
  }
34
38
 
@@ -58,6 +62,13 @@ type BaseStore<T extends UnknownRecord> = {
58
62
  remove(key: string): void
59
63
  }
60
64
 
65
+ /**
66
+ * A reactive object with per-property reactivity.
67
+ * Each property is wrapped as a `State`, nested `Store`, or `List` signal, accessible directly via proxy.
68
+ * Updating one property only re-runs effects that read that property.
69
+ *
70
+ * @template T - The plain-object type whose properties become reactive signals
71
+ */
61
72
  type Store<T extends UnknownRecord> = BaseStore<T> & {
62
73
  [K in keyof T]: T[K] extends readonly (infer U extends {})[]
63
74
  ? List<U>
package/src/signal.ts CHANGED
@@ -22,6 +22,12 @@ import { isAsyncFunction, isFunction, isRecord, isUniformArray } from './util'
22
22
 
23
23
  /* === Types === */
24
24
 
25
+ /**
26
+ * A readable and writable signal — the type union of `State`, `Store`, and `List`.
27
+ * Use as a parameter type for generic code that accepts any writable signal.
28
+ *
29
+ * @template T - The type of value held by the signal
30
+ */
25
31
  type MutableSignal<T extends {}> = {
26
32
  get(): T
27
33
  set(value: T): void
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/tsconfig.json CHANGED
@@ -12,6 +12,10 @@
12
12
  "moduleResolution": "bundler",
13
13
  "allowImportingTsExtensions": true,
14
14
  "verbatimModuleSyntax": true,
15
+ "erasableSyntaxOnly": true,
16
+ "isolatedModules": true,
17
+ "resolveJsonModule": true,
18
+ "types": ["bun-types"],
15
19
 
16
20
  // Editor-only mode - no emit
17
21
  "noEmit": true,
@@ -24,11 +28,16 @@
24
28
  "useUnknownInCatchVariables": true,
25
29
  "noUncheckedSideEffectImports": true,
26
30
  "noFallthroughCasesInSwitch": true,
31
+ "forceConsistentCasingInFileNames": true,
27
32
 
28
33
  // Some stricter flags (disabled by default)
29
34
  "noUnusedLocals": false,
30
35
  "noUnusedParameters": false,
31
36
  "noPropertyAccessFromIndexSignature": false,
37
+
38
+ /* Performance */
39
+ "incremental": true,
40
+ "tsBuildInfoFile": "./.tsbuildinfo",
32
41
  },
33
42
  "include": ["./**/*.ts"],
34
43
  "exclude": ["node_modules", "types", "index.js"],
package/types/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 1.0.0
3
+ * @version 1.0.2
4
4
  * @author Esther Brunner
5
5
  */
6
6
  export { CircularDependencyError, type Guard, InvalidCallbackError, InvalidSignalValueError, NullishSignalValueError, ReadonlySignalError, RequiredOwnerError, UnsetSignalValueError, } from './src/errors';
@@ -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,19 @@
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). */
8
17
  nil?: () => MaybePromise<MaybeCleanup>;
9
18
  };
10
19
  /**
@@ -38,10 +47,13 @@ type MatchHandlers<T extends readonly Signal<unknown & {}>[]> = {
38
47
  */
39
48
  declare function createEffect(fn: EffectCallback): Cleanup;
40
49
  /**
41
- * Runs handlers based on the current values of signals.
50
+ * Reads one or more signals and dispatches to the appropriate handler based on their state.
42
51
  * Must be called within an active owner (effect or scope) so async cleanup can be registered.
43
52
  *
44
53
  * @since 0.15.0
54
+ * @param signals - Tuple of signals to read; all must have a value for `ok` to run.
55
+ * @param handlers - Object with an `ok` branch and optional `err` and `nil` branches.
56
+ * @returns An optional cleanup function if the active handler returns one.
45
57
  * @throws RequiredOwnerError If called without an active owner.
46
58
  */
47
59
  declare function match<T extends readonly Signal<unknown & {}>[]>(signals: readonly [...T], handlers: MatchHandlers<T>): MaybeCleanup;
@@ -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;