@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
package/archive/store.ts DELETED
@@ -1,298 +0,0 @@
1
- import { type DiffResult, diff, type UnknownRecord } from '../src/diff'
2
- import {
3
- DuplicateKeyError,
4
- InvalidSignalValueError,
5
- NullishSignalValueError,
6
- ReadonlySignalError,
7
- } from '../src/errors'
8
- import { isMutableSignal, type MutableSignal } from '../src/signal'
9
- import {
10
- batch,
11
- notifyWatchers,
12
- subscribeActiveWatcher,
13
- UNSET,
14
- type Watcher,
15
- } from '../src/system'
16
- import { isFunction, isObjectOfType, isRecord, isSymbol } from '../src/util'
17
- import { isComputed } from './computed'
18
- import { createList, isList, type List } from './list'
19
- import { createState, isState, type State } from './state'
20
-
21
- /* === Types === */
22
-
23
- type StoreKeySignal<T extends {}> = T extends readonly (infer U extends {})[]
24
- ? List<U>
25
- : T extends UnknownRecord
26
- ? Store<T>
27
- : State<T>
28
-
29
- type Store<T extends UnknownRecord> = {
30
- [K in keyof T]: T[K] extends readonly (infer U extends {})[]
31
- ? List<U>
32
- : T extends Record<string, unknown>
33
- ? Store<T[K] & {}>
34
- : State<T[K] & {}>
35
- } & {
36
- readonly [Symbol.toStringTag]: 'Store'
37
- [Symbol.iterator](): IterableIterator<
38
- [
39
- Extract<keyof T, string>,
40
- StoreKeySignal<T[Extract<keyof T, string>] & {}>,
41
- ]
42
- >
43
- add<K extends Extract<keyof T, string>>(key: K, value: T[K]): K
44
- byKey<K extends Extract<keyof T, string>>(key: K): StoreKeySignal<T[K] & {}>
45
- get(): T
46
- keyAt(index: number): string | undefined
47
- indexOfKey(key: string): number
48
- set(value: T): void
49
- update(fn: (value: T) => T): void
50
- sort<U = T[Extract<keyof T, string>]>(
51
- compareFn?: (a: U, b: U) => number,
52
- ): void
53
- remove<K extends Extract<keyof T, string>>(key: K): void
54
- }
55
-
56
- /* === Constants === */
57
-
58
- const TYPE_STORE = 'Store' as const
59
-
60
- /* === Functions === */
61
-
62
- /**
63
- * Create a new store with deeply nested reactive properties
64
- *
65
- * Supports both objects and arrays as initial values. Arrays are converted
66
- * to records internally for storage but maintain their array type through
67
- * the .get() method, which automatically converts objects with consecutive
68
- * numeric keys back to arrays.
69
- *
70
- * For array-like stores, an optional keyConfig parameter can be provided to
71
- * generate stable keys for array items. This creates persistent references
72
- * that remain stable across sort and compact operations.
73
- *
74
- * @since 0.15.0
75
- * @param {T} initialValue - initial object or array value of the store
76
- * @returns {Store<T>} - new store with reactive properties that preserves the original type T
77
- */
78
- const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
79
- if (initialValue == null) throw new NullishSignalValueError('store')
80
-
81
- const watchers = new Set<Watcher>()
82
- const signals = new Map<
83
- string,
84
- MutableSignal<T[Extract<keyof T, string>] & {}>
85
- >()
86
- const ownWatchers = new Map<string, Watcher>()
87
-
88
- // Get current record
89
- const current = (): T => {
90
- const record = {} as Record<string, unknown>
91
- for (const [key, signal] of signals) record[key] = signal.get()
92
- return record as T
93
- }
94
-
95
- // Validate input
96
- const isValidValue = <T>(
97
- key: string,
98
- value: T,
99
- ): value is NonNullable<T> => {
100
- if (value == null)
101
- throw new NullishSignalValueError(`store for key "${key}"`)
102
- if (value === UNSET) return true
103
- if (isSymbol(value) || isFunction(value) || isComputed(value))
104
- throw new InvalidSignalValueError(`store for key "${key}"`, value)
105
- return true
106
- }
107
-
108
- // Add nested signal and effect
109
- const addProperty = <K extends keyof T & string>(
110
- key: K,
111
- value: T[K],
112
- single = false,
113
- ): boolean => {
114
- if (!isValidValue(key, value)) return false
115
-
116
- // Create signal for key
117
- // @ts-expect-error non-matching signal types
118
- const signal: MutableSignal<T[K] & {}> =
119
- isState(value) || isStore(value) || isList(value)
120
- ? (value as unknown as MutableSignal<T[K] & {}>)
121
- : isRecord(value)
122
- ? createStore(value)
123
- : Array.isArray(value)
124
- ? createList(value)
125
- : createState(value)
126
-
127
- // Set internal states
128
- // @ts-expect-error non-matching signal types
129
- signals.set(key, signal)
130
-
131
- if (single) {
132
- notifyWatchers(watchers)
133
- }
134
- return true
135
- }
136
-
137
- // Remove nested signal and effect
138
- const removeProperty = (key: string, single = false) => {
139
- // Remove signal for key
140
- const ok = signals.delete(key)
141
- if (!ok) return
142
-
143
- // Clean up internal states
144
- const watcher = ownWatchers.get(key)
145
- if (watcher) {
146
- watcher.stop()
147
- ownWatchers.delete(key)
148
- }
149
-
150
- if (single) {
151
- notifyWatchers(watchers)
152
- }
153
- }
154
-
155
- // Commit batched changes and emit notifications
156
- const batchChanges = (changes: DiffResult) => {
157
- // Additions
158
- if (Object.keys(changes.add).length) {
159
- for (const key in changes.add)
160
- addProperty(
161
- key,
162
- changes.add[key] as T[Extract<keyof T, string>] & {},
163
- false,
164
- )
165
- }
166
-
167
- // Changes
168
- if (Object.keys(changes.change).length) {
169
- batch(() => {
170
- for (const key in changes.change) {
171
- const value = changes.change[key] as T[Extract<
172
- keyof T,
173
- string
174
- >]
175
- if (!isValidValue(key, value)) continue
176
-
177
- const signal = signals.get(key)
178
- if (isMutableSignal(signal)) signal.set(value)
179
- else throw new ReadonlySignalError(key, value)
180
- }
181
- })
182
- }
183
-
184
- // Removals
185
- if (Object.keys(changes.remove).length) {
186
- for (const key in changes.remove) removeProperty(key)
187
- }
188
-
189
- return changes.changed
190
- }
191
-
192
- // Reconcile data and dispatch events
193
- const reconcile = (oldValue: T, newValue: T): boolean =>
194
- batchChanges(diff(oldValue, newValue))
195
-
196
- // Initialize data
197
- reconcile({} as T, initialValue)
198
-
199
- // Methods and Properties
200
- const prototype: Record<PropertyKey, unknown> = {}
201
- Object.defineProperties(prototype, {
202
- [Symbol.toStringTag]: {
203
- value: TYPE_STORE,
204
- },
205
- [Symbol.iterator]: {
206
- value: function* () {
207
- for (const [key, signal] of signals) yield [key, signal]
208
- },
209
- },
210
- add: {
211
- value: <K extends Extract<keyof T, string>>(
212
- key: K,
213
- value: T[K],
214
- ): K => {
215
- if (signals.has(key))
216
- throw new DuplicateKeyError('store', key, value)
217
-
218
- addProperty(key, value, true)
219
- return key
220
- },
221
- },
222
- byKey: {
223
- value: (key: string) => {
224
- return signals.get(key)
225
- },
226
- },
227
- get: {
228
- value: (): T => {
229
- subscribeActiveWatcher(watchers)
230
- return current()
231
- },
232
- },
233
- remove: {
234
- value: (key: string): void => {
235
- if (signals.has(key)) removeProperty(key, true)
236
- },
237
- },
238
- set: {
239
- value: (newValue: T): void => {
240
- if (reconcile(current(), newValue)) {
241
- notifyWatchers(watchers)
242
- if (UNSET === newValue) watchers.clear()
243
- }
244
- },
245
- },
246
- update: {
247
- value: (fn: (oldValue: T) => T): void => {
248
- store.set(fn(current()))
249
- },
250
- },
251
- })
252
-
253
- // Return proxy directly with integrated signal methods
254
- const store = new Proxy(prototype as Store<T>, {
255
- get(target, prop) {
256
- if (prop in target) return Reflect.get(target, prop)
257
- if (!isSymbol(prop)) return signals.get(prop)
258
- },
259
- has(target, prop) {
260
- if (prop in target) return true
261
- return signals.has(String(prop))
262
- },
263
- ownKeys(target) {
264
- const staticKeys = Reflect.ownKeys(target)
265
- return [...new Set([...signals.keys(), ...staticKeys])]
266
- },
267
- getOwnPropertyDescriptor(target, prop) {
268
- if (prop in target)
269
- return Reflect.getOwnPropertyDescriptor(target, prop)
270
- if (isSymbol(prop)) return undefined
271
-
272
- const signal = signals.get(prop)
273
- return signal
274
- ? {
275
- enumerable: true,
276
- configurable: true,
277
- writable: true,
278
- value: signal,
279
- }
280
- : undefined
281
- },
282
- })
283
- return store
284
- }
285
-
286
- /**
287
- * Check if the provided value is a Store instance
288
- *
289
- * @since 0.15.0
290
- * @param {unknown} value - Value to check
291
- * @returns {boolean} - True if the value is a Store instance, false otherwise
292
- */
293
- const isStore = <T extends UnknownRecord>(value: unknown): value is Store<T> =>
294
- isObjectOfType(value, TYPE_STORE)
295
-
296
- /* === Exports === */
297
-
298
- export { TYPE_STORE, isStore, createStore, type Store }
package/archive/task.ts DELETED
@@ -1,189 +0,0 @@
1
- import { isEqual } from '../src/diff'
2
- import {
3
- CircularDependencyError,
4
- createError,
5
- InvalidCallbackError,
6
- NullishSignalValueError,
7
- } from '../src/errors'
8
- import {
9
- createWatcher,
10
- flush,
11
- notifyWatchers,
12
- subscribeActiveWatcher,
13
- UNSET,
14
- type Watcher,
15
- } from '../src/system'
16
- import { isAbortError, isAsyncFunction, isObjectOfType } from '../src/util'
17
-
18
- /* === Types === */
19
-
20
- type Task<T extends {}> = {
21
- readonly [Symbol.toStringTag]: 'Task'
22
- get(): T
23
- }
24
-
25
- type TaskCallback<T extends {} & { then?: undefined }> = (
26
- oldValue: T,
27
- abort: AbortSignal,
28
- ) => Promise<T>
29
-
30
- /* === Constants === */
31
-
32
- const TYPE_TASK = 'Task' as const
33
-
34
- /* === Functions === */
35
-
36
- /**
37
- * Create a derived signal from existing signals
38
- *
39
- * @since 0.9.0
40
- * @param {TaskCallback<T>} callback - Computation callback function
41
- * @returns {Task<T>} - Computed signal
42
- */
43
- const createTask = <T extends {}>(
44
- callback: TaskCallback<T>,
45
- initialValue: T = UNSET,
46
- ): Task<T> => {
47
- if (!isTaskCallback(callback))
48
- throw new InvalidCallbackError('task', callback)
49
- if (initialValue == null) throw new NullishSignalValueError('task')
50
-
51
- const watchers: Set<Watcher> = new Set()
52
-
53
- // Internal state
54
- let value: T = initialValue
55
- let error: Error | undefined
56
- let controller: AbortController | undefined
57
- let dirty = true
58
- let changed = false
59
- let computing = false
60
-
61
- // Functions to update internal state
62
- const ok = (v: T): undefined => {
63
- if (!isEqual(v, value)) {
64
- value = v
65
- changed = true
66
- }
67
- error = undefined
68
- dirty = false
69
- }
70
- const nil = (): undefined => {
71
- changed = UNSET !== value
72
- value = UNSET
73
- error = undefined
74
- }
75
- const err = (e: unknown): undefined => {
76
- const newError = createError(e)
77
- changed =
78
- !error ||
79
- newError.name !== error.name ||
80
- newError.message !== error.message
81
- value = UNSET
82
- error = newError
83
- }
84
- const settle =
85
- <T>(fn: (arg: T) => void) =>
86
- (arg: T) => {
87
- computing = false
88
- controller = undefined
89
- fn(arg)
90
- if (changed) notifyWatchers(watchers)
91
- }
92
-
93
- // Own watcher: called when notified from sources (push)
94
- const watcher = createWatcher(
95
- () => {
96
- dirty = true
97
- controller?.abort()
98
- if (watchers.size) notifyWatchers(watchers)
99
- else watcher.stop()
100
- },
101
- () => {
102
- if (computing) throw new CircularDependencyError('computed')
103
- changed = false
104
- // Return current value until promise resolves
105
- if (controller) return value
106
-
107
- controller = new AbortController()
108
- controller.signal.addEventListener(
109
- 'abort',
110
- () => {
111
- computing = false
112
- controller = undefined
113
- watcher.run() // Retry computation with updated state
114
- },
115
- {
116
- once: true,
117
- },
118
- )
119
- let result: T | Promise<T>
120
- computing = true
121
- try {
122
- result = callback(value, controller.signal)
123
- } catch (e) {
124
- if (isAbortError(e)) nil()
125
- else err(e)
126
- computing = false
127
- return
128
- }
129
-
130
- if (result instanceof Promise) result.then(settle(ok), settle(err))
131
- else if (null == result || UNSET === result) nil()
132
- else ok(result)
133
- computing = false
134
- },
135
- )
136
- watcher.onCleanup(() => {
137
- controller?.abort()
138
- })
139
-
140
- const task: Record<PropertyKey, unknown> = {}
141
- Object.defineProperties(task, {
142
- [Symbol.toStringTag]: {
143
- value: TYPE_TASK,
144
- },
145
- get: {
146
- value: (): T => {
147
- subscribeActiveWatcher(watchers)
148
- flush()
149
-
150
- if (dirty) watcher.run()
151
- if (error) throw error
152
- return value
153
- },
154
- },
155
- })
156
- return task as Task<T>
157
- }
158
-
159
- /**
160
- * Check if a value is a task signal
161
- *
162
- * @since 0.9.0
163
- * @param {unknown} value - Value to check
164
- * @returns {boolean} - True if value is a task signal, false otherwise
165
- */
166
- const isTask = /*#__PURE__*/ <T extends {}>(value: unknown): value is Task<T> =>
167
- isObjectOfType(value, TYPE_TASK)
168
-
169
- /**
170
- * Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
171
- *
172
- * @since 0.12.0
173
- * @param {unknown} value - Value to check
174
- * @returns {boolean} - True if value is an async callback, false otherwise
175
- */
176
- const isTaskCallback = /*#__PURE__*/ <T extends {}>(
177
- value: unknown,
178
- ): value is TaskCallback<T> => isAsyncFunction(value) && value.length < 3
179
-
180
- /* === Exports === */
181
-
182
- export {
183
- TYPE_TASK,
184
- createTask,
185
- isTask,
186
- isTaskCallback,
187
- type Task,
188
- type TaskCallback,
189
- }