@zeix/cause-effect 0.17.3 → 0.18.1

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 (94) hide show
  1. package/.ai-context.md +169 -227
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +176 -116
  4. package/ARCHITECTURE.md +276 -0
  5. package/CHANGELOG.md +29 -0
  6. package/CLAUDE.md +201 -143
  7. package/GUIDE.md +298 -0
  8. package/README.md +246 -193
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/context7.json +4 -0
  12. package/examples/events-sensor.ts +187 -0
  13. package/examples/selector-sensor.ts +173 -0
  14. package/index.dev.js +1390 -1008
  15. package/index.js +1 -1
  16. package/index.ts +60 -74
  17. package/package.json +5 -2
  18. package/skills/changelog-keeper/SKILL.md +59 -0
  19. package/skills/changelog-keeper/agents/openai.yaml +4 -0
  20. package/src/errors.ts +118 -74
  21. package/src/graph.ts +612 -0
  22. package/src/nodes/collection.ts +512 -0
  23. package/src/nodes/effect.ts +149 -0
  24. package/src/nodes/list.ts +589 -0
  25. package/src/nodes/memo.ts +148 -0
  26. package/src/nodes/sensor.ts +149 -0
  27. package/src/nodes/state.ts +135 -0
  28. package/src/nodes/store.ts +378 -0
  29. package/src/nodes/task.ts +174 -0
  30. package/src/signal.ts +112 -66
  31. package/src/util.ts +26 -57
  32. package/test/batch.test.ts +96 -62
  33. package/test/benchmark.test.ts +473 -487
  34. package/test/collection.test.ts +456 -707
  35. package/test/effect.test.ts +293 -696
  36. package/test/list.test.ts +335 -592
  37. package/test/memo.test.ts +574 -0
  38. package/test/regression.test.ts +156 -0
  39. package/test/scope.test.ts +191 -0
  40. package/test/sensor.test.ts +454 -0
  41. package/test/signal.test.ts +220 -213
  42. package/test/state.test.ts +217 -265
  43. package/test/store.test.ts +346 -446
  44. package/test/task.test.ts +529 -0
  45. package/test/untrack.test.ts +167 -0
  46. package/types/index.d.ts +13 -15
  47. package/types/src/errors.d.ts +73 -17
  48. package/types/src/graph.d.ts +218 -0
  49. package/types/src/nodes/collection.d.ts +69 -0
  50. package/types/src/nodes/effect.d.ts +48 -0
  51. package/types/src/nodes/list.d.ts +66 -0
  52. package/types/src/nodes/memo.d.ts +63 -0
  53. package/types/src/nodes/sensor.d.ts +81 -0
  54. package/types/src/nodes/state.d.ts +78 -0
  55. package/types/src/nodes/store.d.ts +51 -0
  56. package/types/src/nodes/task.d.ts +79 -0
  57. package/types/src/signal.d.ts +43 -29
  58. package/types/src/util.d.ts +9 -16
  59. package/archive/benchmark.ts +0 -683
  60. package/archive/collection.ts +0 -253
  61. package/archive/composite.ts +0 -85
  62. package/archive/computed.ts +0 -195
  63. package/archive/list.ts +0 -483
  64. package/archive/memo.ts +0 -139
  65. package/archive/state.ts +0 -90
  66. package/archive/store.ts +0 -298
  67. package/archive/task.ts +0 -189
  68. package/src/classes/collection.ts +0 -245
  69. package/src/classes/computed.ts +0 -349
  70. package/src/classes/list.ts +0 -343
  71. package/src/classes/ref.ts +0 -70
  72. package/src/classes/state.ts +0 -102
  73. package/src/classes/store.ts +0 -262
  74. package/src/diff.ts +0 -138
  75. package/src/effect.ts +0 -93
  76. package/src/match.ts +0 -45
  77. package/src/resolve.ts +0 -49
  78. package/src/system.ts +0 -257
  79. package/test/computed.test.ts +0 -1108
  80. package/test/diff.test.ts +0 -955
  81. package/test/match.test.ts +0 -388
  82. package/test/ref.test.ts +0 -353
  83. package/test/resolve.test.ts +0 -154
  84. package/types/src/classes/collection.d.ts +0 -45
  85. package/types/src/classes/computed.d.ts +0 -94
  86. package/types/src/classes/list.d.ts +0 -43
  87. package/types/src/classes/ref.d.ts +0 -35
  88. package/types/src/classes/state.d.ts +0 -49
  89. package/types/src/classes/store.d.ts +0 -52
  90. package/types/src/diff.d.ts +0 -28
  91. package/types/src/effect.d.ts +0 -15
  92. package/types/src/match.d.ts +0 -21
  93. package/types/src/resolve.d.ts +0 -29
  94. package/types/src/system.d.ts +0 -78
@@ -0,0 +1,148 @@
1
+ import {
2
+ validateCallback,
3
+ validateReadValue,
4
+ validateSignalValue,
5
+ } from '../errors'
6
+ import {
7
+ activeSink,
8
+ batchDepth,
9
+ type ComputedOptions,
10
+ DEFAULT_EQUALITY,
11
+ FLAG_DIRTY,
12
+ flush,
13
+ link,
14
+ type MemoCallback,
15
+ type MemoNode,
16
+ propagate,
17
+ refresh,
18
+ type SinkNode,
19
+ TYPE_MEMO,
20
+ } from '../graph'
21
+ import { isObjectOfType, isSyncFunction } from '../util'
22
+
23
+ /* === Types === */
24
+
25
+ /**
26
+ * A derived reactive computation that caches its result.
27
+ * Automatically tracks dependencies and recomputes when they change.
28
+ *
29
+ * @template T - The type of value computed by the memo
30
+ */
31
+ type Memo<T extends {}> = {
32
+ readonly [Symbol.toStringTag]: 'Memo'
33
+
34
+ /**
35
+ * Gets the current value of the memo.
36
+ * Recomputes if dependencies have changed since last access.
37
+ * When called inside another reactive context, creates a dependency.
38
+ * @returns The computed value
39
+ * @throws UnsetSignalValueError If the memo value is still unset when read.
40
+ */
41
+ get(): T
42
+ }
43
+
44
+ /* === Exported Functions === */
45
+
46
+ /**
47
+ * Creates a derived reactive computation that caches its result.
48
+ * The computation automatically tracks dependencies and recomputes when they change.
49
+ * Uses lazy evaluation - only computes when the value is accessed.
50
+ *
51
+ * @since 0.18.0
52
+ * @template T - The type of value computed by the memo
53
+ * @param fn - The computation function that receives the previous value
54
+ * @param options - Optional configuration for the memo
55
+ * @param options.value - Optional initial value for reducer patterns
56
+ * @param options.equals - Optional equality function. Defaults to strict equality (`===`)
57
+ * @param options.guard - Optional type guard to validate values
58
+ * @param options.watched - Optional callback invoked when the memo is first watched by an effect.
59
+ * Receives an `invalidate` function to mark the memo dirty and trigger recomputation.
60
+ * Must return a cleanup function called when no effects are watching.
61
+ * @returns A Memo object with a get() method
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const count = createState(0);
66
+ * const doubled = createMemo(() => count.get() * 2);
67
+ * console.log(doubled.get()); // 0
68
+ * count.set(5);
69
+ * console.log(doubled.get()); // 10
70
+ * ```
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * // Using previous value
75
+ * const sum = createMemo((prev) => prev + count.get(), { value: 0, equals: Object.is });
76
+ * ```
77
+ */
78
+ function createMemo<T extends {}>(
79
+ fn: (prev: T) => T,
80
+ options: ComputedOptions<T> & { value: T },
81
+ ): Memo<T>
82
+ function createMemo<T extends {}>(
83
+ fn: MemoCallback<T>,
84
+ options?: ComputedOptions<T>,
85
+ ): Memo<T>
86
+ function createMemo<T extends {}>(
87
+ fn: MemoCallback<T>,
88
+ options?: ComputedOptions<T>,
89
+ ): Memo<T> {
90
+ validateCallback(TYPE_MEMO, fn, isSyncFunction)
91
+ if (options?.value !== undefined)
92
+ validateSignalValue(TYPE_MEMO, options.value, options?.guard)
93
+
94
+ const node: MemoNode<T> = {
95
+ fn,
96
+ value: options?.value as T,
97
+ flags: FLAG_DIRTY,
98
+ sources: null,
99
+ sourcesTail: null,
100
+ sinks: null,
101
+ sinksTail: null,
102
+ equals: options?.equals ?? DEFAULT_EQUALITY,
103
+ error: undefined,
104
+ stop: undefined,
105
+ }
106
+
107
+ const watched = options?.watched
108
+ const subscribe = watched
109
+ ? () => {
110
+ if (activeSink) {
111
+ if (!node.sinks)
112
+ node.stop = watched(() => {
113
+ node.flags |= FLAG_DIRTY
114
+ for (let e = node.sinks; e; e = e.nextSink)
115
+ propagate(e.sink)
116
+ if (batchDepth === 0) flush()
117
+ })
118
+ link(node, activeSink)
119
+ }
120
+ }
121
+ : () => {
122
+ if (activeSink) link(node, activeSink)
123
+ }
124
+
125
+ return {
126
+ [Symbol.toStringTag]: TYPE_MEMO,
127
+ get() {
128
+ subscribe()
129
+ refresh(node as unknown as SinkNode)
130
+ if (node.error) throw node.error
131
+ validateReadValue(TYPE_MEMO, node.value)
132
+ return node.value
133
+ },
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Checks if a value is a Memo signal.
139
+ *
140
+ * @since 0.18.0
141
+ * @param value - The value to check
142
+ * @returns True if the value is a Memo
143
+ */
144
+ function isMemo<T extends {} = unknown & {}>(value: unknown): value is Memo<T> {
145
+ return isObjectOfType(value, TYPE_MEMO)
146
+ }
147
+
148
+ export { createMemo, isMemo, type Memo }
@@ -0,0 +1,149 @@
1
+ import {
2
+ validateCallback,
3
+ validateReadValue,
4
+ validateSignalValue,
5
+ } from '../errors'
6
+ import {
7
+ activeSink,
8
+ type Cleanup,
9
+ DEFAULT_EQUALITY,
10
+ link,
11
+ type SignalOptions,
12
+ type StateNode,
13
+ setState,
14
+ TYPE_SENSOR,
15
+ } from '../graph'
16
+ import { isObjectOfType, isSyncFunction } from '../util'
17
+
18
+ /* === Types === */
19
+
20
+ /**
21
+ * A read-only signal that tracks external input and updates a state value as long as it is active.
22
+ *
23
+ * @template T - The type of value produced by the sensor
24
+ */
25
+ type Sensor<T extends {}> = {
26
+ readonly [Symbol.toStringTag]: 'Sensor'
27
+
28
+ /**
29
+ * Gets the current value of the sensor.
30
+ * When called inside another reactive context, creates a dependency.
31
+ * @returns The sensor value
32
+ * @throws UnsetSignalValueError If the sensor value is still unset when read.
33
+ */
34
+ get(): T
35
+ }
36
+
37
+ /**
38
+ * A callback function for sensors when the sensor starts being watched.
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
43
+ */
44
+ type SensorOptions<T extends {}> = SignalOptions<T> & {
45
+ /**
46
+ * Optional initial value. Avoids `UnsetSignalValueError` on first read
47
+ * before the watched callback fires.
48
+ */
49
+ value?: T
50
+ }
51
+
52
+ type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup
53
+
54
+ /* === Exported Functions === */
55
+
56
+ /**
57
+ * Creates a sensor that tracks external input and updates a state value as long as it is active.
58
+ * Sensors get activated when they are first accessed by an effect and deactivated when they are
59
+ * no longer watched. This lazy activation pattern ensures resources are only consumed when needed.
60
+ *
61
+ * @since 0.18.0
62
+ * @template T - The type of value produced by the sensor
63
+ * @param watched - The callback invoked when the sensor starts being watched, receives a `set` function and returns a cleanup function.
64
+ * @param options - Optional configuration for the sensor.
65
+ * @param options.value - Optional initial value. Avoids `UnsetSignalValueError` on first read
66
+ * before the watched callback fires. Essential for the mutable-object observation pattern.
67
+ * @param options.equals - Optional equality function. Defaults to strict equality (`===`). Use `SKIP_EQUALITY`
68
+ * for mutable objects where the reference stays the same but internal state changes.
69
+ * @param options.guard - Optional type guard to validate values.
70
+ * @returns A read-only sensor signal.
71
+ *
72
+ * @example Tracking external values
73
+ * ```ts
74
+ * const mousePos = createSensor<{ x: number; y: number }>((set) => {
75
+ * const handler = (e: MouseEvent) => {
76
+ * set({ x: e.clientX, y: e.clientY });
77
+ * };
78
+ * window.addEventListener('mousemove', handler);
79
+ * return () => window.removeEventListener('mousemove', handler);
80
+ * });
81
+ * ```
82
+ *
83
+ * @example Observing a mutable object
84
+ * ```ts
85
+ * import { createSensor, SKIP_EQUALITY } from 'cause-effect';
86
+ *
87
+ * const el = createSensor<HTMLElement>((set) => {
88
+ * const node = document.getElementById('box')!;
89
+ * set(node);
90
+ * const obs = new MutationObserver(() => set(node));
91
+ * obs.observe(node, { attributes: true });
92
+ * return () => obs.disconnect();
93
+ * }, { value: node, equals: SKIP_EQUALITY });
94
+ * ```
95
+ */
96
+ function createSensor<T extends {}>(
97
+ watched: SensorCallback<T>,
98
+ options?: SensorOptions<T>,
99
+ ): Sensor<T> {
100
+ validateCallback(TYPE_SENSOR, watched, isSyncFunction)
101
+ if (options?.value !== undefined)
102
+ validateSignalValue(TYPE_SENSOR, options.value, options?.guard)
103
+
104
+ const node: StateNode<T> = {
105
+ value: options?.value as T,
106
+ sinks: null,
107
+ sinksTail: null,
108
+ equals: options?.equals ?? DEFAULT_EQUALITY,
109
+ guard: options?.guard,
110
+ stop: undefined,
111
+ }
112
+
113
+ return {
114
+ [Symbol.toStringTag]: TYPE_SENSOR,
115
+ get(): T {
116
+ if (activeSink) {
117
+ if (!node.sinks)
118
+ node.stop = watched((next: T): void => {
119
+ validateSignalValue(TYPE_SENSOR, next, node.guard)
120
+ setState(node, next)
121
+ })
122
+ link(node, activeSink)
123
+ }
124
+ validateReadValue(TYPE_SENSOR, node.value)
125
+ return node.value
126
+ },
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Checks if a value is a Sensor signal.
132
+ *
133
+ * @since 0.18.0
134
+ * @param value - The value to check
135
+ * @returns True if the value is a Sensor
136
+ */
137
+ function isSensor<T extends {} = unknown & {}>(
138
+ value: unknown,
139
+ ): value is Sensor<T> {
140
+ return isObjectOfType(value, TYPE_SENSOR)
141
+ }
142
+
143
+ export {
144
+ createSensor,
145
+ isSensor,
146
+ type Sensor,
147
+ type SensorCallback,
148
+ type SensorOptions,
149
+ }
@@ -0,0 +1,135 @@
1
+ import { validateCallback, validateSignalValue } from '../errors'
2
+ import {
3
+ activeSink,
4
+ DEFAULT_EQUALITY,
5
+ link,
6
+ type SignalOptions,
7
+ type StateNode,
8
+ setState,
9
+ TYPE_STATE,
10
+ } from '../graph'
11
+ import { isObjectOfType } from '../util'
12
+
13
+ /* === Types === */
14
+
15
+ /**
16
+ * A callback function for states that updates a value based on the previous value.
17
+ *
18
+ * @template T - The type of value
19
+ * @param prev - The previous state value
20
+ * @returns The new state value
21
+ */
22
+ type UpdateCallback<T extends {}> = (prev: T) => T
23
+
24
+ /**
25
+ * A mutable reactive state container.
26
+ * Changes to the state will automatically propagate to dependent computations and effects.
27
+ *
28
+ * @template T - The type of value stored in the state
29
+ */
30
+ type State<T extends {}> = {
31
+ readonly [Symbol.toStringTag]: 'State'
32
+
33
+ /**
34
+ * Gets the current value of the state.
35
+ * When called inside a memo, task, or effect, creates a dependency.
36
+ * @returns The current value
37
+ */
38
+ get(): T
39
+
40
+ /**
41
+ * Sets a new value for the state.
42
+ * If the new value is different (according to the equality function), all dependents will be notified.
43
+ * @param next - The new value to set
44
+ */
45
+ set(next: T): void
46
+
47
+ /**
48
+ * Updates the state with a new value computed by a callback function.
49
+ * The callback receives the current value as an argument.
50
+ * @param fn - The callback function to compute the new value
51
+ */
52
+ update(fn: UpdateCallback<T>): void
53
+ }
54
+
55
+ /* === Exported Functions === */
56
+
57
+ /**
58
+ * Creates a mutable reactive state container.
59
+ *
60
+ * @since 0.9.0
61
+ * @template T - The type of value stored in the state
62
+ * @param value - The initial value
63
+ * @param options - Optional configuration for the state
64
+ * @returns A State object with get() and set() methods
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const count = createState(0);
69
+ * count.set(1);
70
+ * console.log(count.get()); // 1
71
+ * ```
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * // With type guard
76
+ * const count = createState(0, {
77
+ * guard: (v): v is number => typeof v === 'number'
78
+ * });
79
+ * ```
80
+ */
81
+ function createState<T extends {}>(
82
+ value: T,
83
+ options?: SignalOptions<T>,
84
+ ): State<T> {
85
+ validateSignalValue(TYPE_STATE, value, options?.guard)
86
+
87
+ const node: StateNode<T> = {
88
+ value,
89
+ sinks: null,
90
+ sinksTail: null,
91
+ equals: options?.equals ?? DEFAULT_EQUALITY,
92
+ guard: options?.guard,
93
+ }
94
+
95
+ return {
96
+ [Symbol.toStringTag]: TYPE_STATE,
97
+ get(): T {
98
+ if (activeSink) link(node, activeSink)
99
+ return node.value
100
+ },
101
+ set(next: T): void {
102
+ validateSignalValue(TYPE_STATE, next, node.guard)
103
+ setState(node, next)
104
+ },
105
+ update(fn: UpdateCallback<T>): void {
106
+ validateCallback(TYPE_STATE, fn)
107
+ const next = fn(node.value)
108
+ validateSignalValue(TYPE_STATE, next, node.guard)
109
+ setState(node, next)
110
+ },
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Checks if a value is a State signal.
116
+ *
117
+ * @since 0.9.0
118
+ * @param value - The value to check
119
+ * @returns True if the value is a State
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * const state = createState(0);
124
+ * if (isState(state)) {
125
+ * state.set(1); // TypeScript knows state has set()
126
+ * }
127
+ * ```
128
+ */
129
+ function isState<T extends {} = unknown & {}>(
130
+ value: unknown,
131
+ ): value is State<T> {
132
+ return isObjectOfType(value, TYPE_STATE)
133
+ }
134
+
135
+ export { createState, isState, type State, type UpdateCallback }