@zeix/cause-effect 0.17.2 → 0.18.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.
Files changed (94) hide show
  1. package/.ai-context.md +163 -226
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/.zed/settings.json +3 -0
  5. package/ARCHITECTURE.md +274 -0
  6. package/CLAUDE.md +197 -202
  7. package/COLLECTION_REFACTORING.md +161 -0
  8. package/GUIDE.md +298 -0
  9. package/README.md +241 -220
  10. package/REQUIREMENTS.md +100 -0
  11. package/bench/reactivity.bench.ts +577 -0
  12. package/index.dev.js +1326 -1174
  13. package/index.js +1 -1
  14. package/index.ts +58 -85
  15. package/package.json +9 -6
  16. package/src/errors.ts +118 -70
  17. package/src/graph.ts +601 -0
  18. package/src/nodes/collection.ts +474 -0
  19. package/src/nodes/effect.ts +149 -0
  20. package/src/nodes/list.ts +588 -0
  21. package/src/nodes/memo.ts +120 -0
  22. package/src/nodes/sensor.ts +139 -0
  23. package/src/nodes/state.ts +135 -0
  24. package/src/nodes/store.ts +383 -0
  25. package/src/nodes/task.ts +146 -0
  26. package/src/signal.ts +112 -64
  27. package/src/util.ts +26 -57
  28. package/test/batch.test.ts +96 -69
  29. package/test/benchmark.test.ts +473 -485
  30. package/test/collection.test.ts +455 -955
  31. package/test/effect.test.ts +293 -696
  32. package/test/list.test.ts +332 -857
  33. package/test/memo.test.ts +380 -0
  34. package/test/regression.test.ts +156 -0
  35. package/test/scope.test.ts +191 -0
  36. package/test/sensor.test.ts +454 -0
  37. package/test/signal.test.ts +220 -213
  38. package/test/state.test.ts +217 -271
  39. package/test/store.test.ts +346 -898
  40. package/test/task.test.ts +395 -0
  41. package/test/untrack.test.ts +167 -0
  42. package/test/util/dependency-graph.ts +2 -2
  43. package/tsconfig.build.json +11 -0
  44. package/tsconfig.json +5 -7
  45. package/types/index.d.ts +13 -15
  46. package/types/src/errors.d.ts +73 -19
  47. package/types/src/graph.d.ts +208 -0
  48. package/types/src/nodes/collection.d.ts +64 -0
  49. package/types/src/nodes/effect.d.ts +48 -0
  50. package/types/src/nodes/list.d.ts +65 -0
  51. package/types/src/nodes/memo.d.ts +57 -0
  52. package/types/src/nodes/sensor.d.ts +75 -0
  53. package/types/src/nodes/state.d.ts +78 -0
  54. package/types/src/nodes/store.d.ts +51 -0
  55. package/types/src/nodes/task.d.ts +73 -0
  56. package/types/src/signal.d.ts +43 -28
  57. package/types/src/util.d.ts +9 -16
  58. package/archive/benchmark.ts +0 -688
  59. package/archive/collection.ts +0 -310
  60. package/archive/computed.ts +0 -198
  61. package/archive/list.ts +0 -544
  62. package/archive/memo.ts +0 -140
  63. package/archive/state.ts +0 -90
  64. package/archive/store.ts +0 -357
  65. package/archive/task.ts +0 -191
  66. package/src/classes/collection.ts +0 -298
  67. package/src/classes/composite.ts +0 -171
  68. package/src/classes/computed.ts +0 -392
  69. package/src/classes/list.ts +0 -310
  70. package/src/classes/ref.ts +0 -96
  71. package/src/classes/state.ts +0 -131
  72. package/src/classes/store.ts +0 -227
  73. package/src/diff.ts +0 -138
  74. package/src/effect.ts +0 -96
  75. package/src/match.ts +0 -45
  76. package/src/resolve.ts +0 -49
  77. package/src/system.ts +0 -275
  78. package/test/computed.test.ts +0 -1126
  79. package/test/diff.test.ts +0 -955
  80. package/test/match.test.ts +0 -388
  81. package/test/ref.test.ts +0 -381
  82. package/test/resolve.test.ts +0 -154
  83. package/types/src/classes/collection.d.ts +0 -47
  84. package/types/src/classes/composite.d.ts +0 -15
  85. package/types/src/classes/computed.d.ts +0 -114
  86. package/types/src/classes/list.d.ts +0 -41
  87. package/types/src/classes/ref.d.ts +0 -48
  88. package/types/src/classes/state.d.ts +0 -61
  89. package/types/src/classes/store.d.ts +0 -51
  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 -81
@@ -1,96 +0,0 @@
1
- import { type Guard, InvalidHookError, validateSignalValue } from '../errors'
2
- import {
3
- type Cleanup,
4
- HOOK_WATCH,
5
- type HookCallback,
6
- notifyWatchers,
7
- subscribeActiveWatcher,
8
- type Watcher,
9
- type WatchHook,
10
- } from '../system'
11
- import { isObjectOfType } from '../util'
12
-
13
- /* === Constants === */
14
-
15
- const TYPE_REF = 'Ref'
16
-
17
- /* === Class === */
18
-
19
- /**
20
- * Create a new ref signal.
21
- *
22
- * @since 0.17.1
23
- */
24
- class Ref<T extends {}> {
25
- #watchers = new Set<Watcher>()
26
- #value: T
27
- #watchHookCallbacks: Set<HookCallback> | undefined
28
-
29
- /**
30
- * Create a new ref signal.
31
- *
32
- * @param {T} value - Reference to external object
33
- * @param {Guard<T>} guard - Optional guard function to validate the value
34
- * @throws {NullishSignalValueError} - If the value is null or undefined
35
- * @throws {InvalidSignalValueError} - If the value is invalid
36
- */
37
- constructor(value: T, guard?: Guard<T>) {
38
- validateSignalValue(TYPE_REF, value, guard)
39
-
40
- this.#value = value
41
- }
42
-
43
- get [Symbol.toStringTag](): string {
44
- return TYPE_REF
45
- }
46
-
47
- /**
48
- * Get the value of the ref signal.
49
- *
50
- * @returns {T} - Object reference
51
- */
52
- get(): T {
53
- subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
54
-
55
- return this.#value
56
- }
57
-
58
- /**
59
- * Notify watchers of relevant changes in the external reference.
60
- */
61
- notify(): void {
62
- notifyWatchers(this.#watchers)
63
- }
64
-
65
- /**
66
- * Register a callback to be called when HOOK_WATCH is triggered.
67
- *
68
- * @param {WatchHook} type - The type of hook to register the callback for; only HOOK_WATCH is supported
69
- * @param {HookCallback} callback - The callback to register
70
- * @returns {Cleanup} - A function to unregister the callback
71
- */
72
- on(type: WatchHook, callback: HookCallback): Cleanup {
73
- if (type === HOOK_WATCH) {
74
- this.#watchHookCallbacks ||= new Set()
75
- this.#watchHookCallbacks.add(callback)
76
- return () => {
77
- this.#watchHookCallbacks?.delete(callback)
78
- }
79
- }
80
- throw new InvalidHookError(TYPE_REF, type)
81
- }
82
- }
83
-
84
- /* === Functions === */
85
-
86
- /**
87
- * Check if the provided value is a Ref instance
88
- *
89
- * @since 0.17.1
90
- * @param {unknown} value - Value to check
91
- * @returns {boolean} - True if the value is a Ref instance, false otherwise
92
- */
93
- const isRef = /*#__PURE__*/ <T extends {}>(value: unknown): value is Ref<T> =>
94
- isObjectOfType(value, TYPE_REF)
95
-
96
- export { TYPE_REF, Ref, isRef }
@@ -1,131 +0,0 @@
1
- import { isEqual } from '../diff'
2
- import {
3
- InvalidHookError,
4
- validateCallback,
5
- validateSignalValue,
6
- } from '../errors'
7
- import {
8
- type Cleanup,
9
- HOOK_WATCH,
10
- type HookCallback,
11
- notifyWatchers,
12
- subscribeActiveWatcher,
13
- UNSET,
14
- type Watcher,
15
- type WatchHook,
16
- } from '../system'
17
- import { isObjectOfType } from '../util'
18
-
19
- /* === Constants === */
20
-
21
- const TYPE_STATE = 'State' as const
22
-
23
- /* === Class === */
24
-
25
- /**
26
- * Create a new state signal.
27
- *
28
- * @since 0.17.0
29
- */
30
- class State<T extends {}> {
31
- #watchers = new Set<Watcher>()
32
- #value: T
33
- #watchHookCallbacks: Set<HookCallback> | undefined
34
-
35
- /**
36
- * Create a new state signal.
37
- *
38
- * @param {T} initialValue - Initial value of the state
39
- * @throws {NullishSignalValueError} - If the initial value is null or undefined
40
- * @throws {InvalidSignalValueError} - If the initial value is invalid
41
- */
42
- constructor(initialValue: T) {
43
- validateSignalValue(TYPE_STATE, initialValue)
44
-
45
- this.#value = initialValue
46
- }
47
-
48
- get [Symbol.toStringTag](): string {
49
- return TYPE_STATE
50
- }
51
-
52
- /**
53
- * Get the current value of the state signal.
54
- *
55
- * @returns {T} - Current value of the state
56
- */
57
- get(): T {
58
- subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
59
-
60
- return this.#value
61
- }
62
-
63
- /**
64
- * Set the value of the state signal.
65
- *
66
- * @param {T} newValue - New value of the state
67
- * @returns {void}
68
- * @throws {NullishSignalValueError} - If the initial value is null or undefined
69
- * @throws {InvalidSignalValueError} - If the initial value is invalid
70
- */
71
- set(newValue: T): void {
72
- validateSignalValue(TYPE_STATE, newValue)
73
-
74
- if (isEqual(this.#value, newValue)) return
75
- this.#value = newValue
76
- if (this.#watchers.size) notifyWatchers(this.#watchers)
77
-
78
- // Setting to UNSET clears the watchers so the signal can be garbage collected
79
- if (UNSET === this.#value) this.#watchers.clear()
80
- }
81
-
82
- /**
83
- * Update the value of the state signal.
84
- *
85
- * @param {Function} updater - Function that takes the current value and returns the new value
86
- * @returns {void}
87
- * @throws {InvalidCallbackError} - If the updater function is not a function
88
- * @throws {NullishSignalValueError} - If the initial value is null or undefined
89
- * @throws {InvalidSignalValueError} - If the initial value is invalid
90
- */
91
- update(updater: (oldValue: T) => T): void {
92
- validateCallback(`${TYPE_STATE} update`, updater)
93
-
94
- this.set(updater(this.#value))
95
- }
96
-
97
- /**
98
- * Register a callback to be called when HOOK_WATCH is triggered.
99
- *
100
- * @param {WatchHook} type - The type of hook to register the callback for; only HOOK_WATCH is supported
101
- * @param {HookCallback} callback - The callback to register
102
- * @returns {Cleanup} - A function to unregister the callback
103
- */
104
- on(type: WatchHook, callback: HookCallback): Cleanup {
105
- if (type === HOOK_WATCH) {
106
- this.#watchHookCallbacks ||= new Set()
107
- this.#watchHookCallbacks.add(callback)
108
- return () => {
109
- this.#watchHookCallbacks?.delete(callback)
110
- }
111
- }
112
- throw new InvalidHookError(this.constructor.name, type)
113
- }
114
- }
115
-
116
- /* === Functions === */
117
-
118
- /**
119
- * Check if the provided value is a State instance
120
- *
121
- * @since 0.9.0
122
- * @param {unknown} value - Value to check
123
- * @returns {boolean} - True if the value is a State instance, false otherwise
124
- */
125
- const isState = /*#__PURE__*/ <T extends {}>(
126
- value: unknown,
127
- ): value is State<T> => isObjectOfType(value, TYPE_STATE)
128
-
129
- /* === Exports === */
130
-
131
- export { TYPE_STATE, isState, State }
@@ -1,227 +0,0 @@
1
- import { diff, type UnknownRecord } from '../diff'
2
- import {
3
- DuplicateKeyError,
4
- InvalidHookError,
5
- validateSignalValue,
6
- } from '../errors'
7
- import { createMutableSignal, type MutableSignal, type Signal } from '../signal'
8
- import {
9
- type Cleanup,
10
- HOOK_ADD,
11
- HOOK_CHANGE,
12
- HOOK_REMOVE,
13
- HOOK_WATCH,
14
- type Hook,
15
- type HookCallback,
16
- isHandledHook,
17
- notifyWatchers,
18
- subscribeActiveWatcher,
19
- UNSET,
20
- type Watcher,
21
- } from '../system'
22
- import { isFunction, isObjectOfType, isRecord, isSymbol } from '../util'
23
- import { Composite } from './composite'
24
- import type { List } from './list'
25
- import type { State } from './state'
26
-
27
- /* === Types === */
28
-
29
- type Store<T extends UnknownRecord> = BaseStore<T> & {
30
- [K in keyof T]: T[K] extends readonly (infer U extends {})[]
31
- ? List<U>
32
- : T[K] extends UnknownRecord
33
- ? Store<T[K]>
34
- : State<T[K] & {}>
35
- }
36
-
37
- /* === Constants === */
38
-
39
- const TYPE_STORE = 'Store' as const
40
-
41
- /* === Store Implementation === */
42
-
43
- class BaseStore<T extends UnknownRecord> {
44
- #composite: Composite<T, Signal<T[keyof T] & {}>>
45
- #watchers = new Set<Watcher>()
46
- #watchHookCallbacks: Set<HookCallback> | undefined
47
-
48
- /**
49
- * Create a new store with the given initial value.
50
- *
51
- * @param {T} initialValue - The initial value of the store
52
- * @throws {NullishSignalValueError} - If the initial value is null or undefined
53
- * @throws {InvalidSignalValueError} - If the initial value is not an object
54
- */
55
- constructor(initialValue: T) {
56
- validateSignalValue(TYPE_STORE, initialValue, isRecord)
57
-
58
- this.#composite = new Composite<T, Signal<T[keyof T] & {}>>(
59
- initialValue,
60
- <K extends keyof T & string>(
61
- key: K,
62
- value: unknown,
63
- ): value is T[K] & {} => {
64
- validateSignalValue(`${TYPE_STORE} for key "${key}"`, value)
65
- return true
66
- },
67
- value => createMutableSignal(value),
68
- )
69
- }
70
-
71
- get #value(): T {
72
- const record = {} as UnknownRecord
73
- for (const [key, signal] of this.#composite.signals.entries())
74
- record[key] = signal.get()
75
- return record as T
76
- }
77
-
78
- // Public methods
79
- get [Symbol.toStringTag](): 'Store' {
80
- return TYPE_STORE
81
- }
82
-
83
- get [Symbol.isConcatSpreadable](): boolean {
84
- return false
85
- }
86
-
87
- *[Symbol.iterator](): IterableIterator<
88
- [string, MutableSignal<T[keyof T] & {}>]
89
- > {
90
- for (const [key, signal] of this.#composite.signals.entries())
91
- yield [key, signal as MutableSignal<T[keyof T] & {}>]
92
- }
93
-
94
- keys(): IterableIterator<string> {
95
- return this.#composite.signals.keys()
96
- }
97
-
98
- byKey<K extends keyof T & string>(
99
- key: K,
100
- ): T[K] extends readonly (infer U extends {})[]
101
- ? List<U>
102
- : T[K] extends UnknownRecord
103
- ? Store<T[K]>
104
- : T[K] extends unknown & {}
105
- ? State<T[K] & {}>
106
- : State<T[K] & {}> | undefined {
107
- return this.#composite.signals.get(
108
- key,
109
- ) as T[K] extends readonly (infer U extends {})[]
110
- ? List<U>
111
- : T[K] extends UnknownRecord
112
- ? Store<T[K]>
113
- : T[K] extends unknown & {}
114
- ? State<T[K] & {}>
115
- : State<T[K] & {}> | undefined
116
- }
117
-
118
- get(): T {
119
- subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
120
- return this.#value
121
- }
122
-
123
- set(newValue: T): void {
124
- if (UNSET === newValue) {
125
- this.#composite.clear()
126
- notifyWatchers(this.#watchers)
127
- this.#watchers.clear()
128
- return
129
- }
130
-
131
- const oldValue = this.#value
132
- const changed = this.#composite.change(diff(oldValue, newValue))
133
- if (changed) notifyWatchers(this.#watchers)
134
- }
135
-
136
- update(fn: (oldValue: T) => T): void {
137
- this.set(fn(this.get()))
138
- }
139
-
140
- add<K extends keyof T & string>(key: K, value: T[K]): K {
141
- if (this.#composite.signals.has(key))
142
- throw new DuplicateKeyError(TYPE_STORE, key, value)
143
-
144
- const ok = this.#composite.add(key, value)
145
- if (ok) notifyWatchers(this.#watchers)
146
- return key
147
- }
148
-
149
- remove(key: string): void {
150
- const ok = this.#composite.remove(key)
151
- if (ok) notifyWatchers(this.#watchers)
152
- }
153
-
154
- on(type: Hook, callback: HookCallback): Cleanup {
155
- if (type === HOOK_WATCH) {
156
- this.#watchHookCallbacks ||= new Set<HookCallback>()
157
- this.#watchHookCallbacks.add(callback)
158
- return () => {
159
- this.#watchHookCallbacks?.delete(callback)
160
- }
161
- } else if (isHandledHook(type, [HOOK_ADD, HOOK_CHANGE, HOOK_REMOVE])) {
162
- return this.#composite.on(type, callback)
163
- }
164
- throw new InvalidHookError(TYPE_STORE, type)
165
- }
166
- }
167
-
168
- /* === Functions === */
169
-
170
- /**
171
- * Create a new store with deeply nested reactive properties
172
- *
173
- * @since 0.15.0
174
- * @param {T} initialValue - Initial object or array value of the store
175
- * @returns {Store<T>} - New store with reactive properties that preserves the original type T
176
- */
177
- const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
178
- const instance = new BaseStore(initialValue)
179
-
180
- // Return proxy for property access
181
- return new Proxy(instance, {
182
- get(target, prop) {
183
- if (prop in target) {
184
- const value = Reflect.get(target, prop)
185
- return isFunction(value) ? value.bind(target) : value
186
- }
187
- if (!isSymbol(prop)) return target.byKey(prop)
188
- },
189
- has(target, prop) {
190
- if (prop in target) return true
191
- return target.byKey(String(prop)) !== undefined
192
- },
193
- ownKeys(target) {
194
- return Array.from(target.keys())
195
- },
196
- getOwnPropertyDescriptor(target, prop) {
197
- if (prop in target)
198
- return Reflect.getOwnPropertyDescriptor(target, prop)
199
- if (isSymbol(prop)) return undefined
200
-
201
- const signal = target.byKey(String(prop))
202
- return signal
203
- ? {
204
- enumerable: true,
205
- configurable: true,
206
- writable: true,
207
- value: signal,
208
- }
209
- : undefined
210
- },
211
- }) as Store<T>
212
- }
213
-
214
- /**
215
- * Check if the provided value is a Store instance
216
- *
217
- * @since 0.15.0
218
- * @param {unknown} value - Value to check
219
- * @returns {boolean} - True if the value is a Store instance, false otherwise
220
- */
221
- const isStore = <T extends UnknownRecord>(
222
- value: unknown,
223
- ): value is BaseStore<T> => isObjectOfType(value, TYPE_STORE)
224
-
225
- /* === Exports === */
226
-
227
- export { createStore, isStore, BaseStore, TYPE_STORE, type Store }
package/src/diff.ts DELETED
@@ -1,138 +0,0 @@
1
- import { CircularDependencyError } from './errors'
2
- import { UNSET } from './system'
3
- import { isNonNullObject, isRecord, isRecordOrArray } from './util'
4
-
5
- /* === Types === */
6
-
7
- type UnknownRecord = Record<string, unknown>
8
- type UnknownArray = ReadonlyArray<unknown & {}>
9
-
10
- type DiffResult = {
11
- changed: boolean
12
- add: UnknownRecord
13
- change: UnknownRecord
14
- remove: UnknownRecord
15
- }
16
-
17
- /* === Functions === */
18
-
19
- /**
20
- * Checks if two values are equal with cycle detection
21
- *
22
- * @since 0.15.0
23
- * @param {T} a - First value to compare
24
- * @param {T} b - Second value to compare
25
- * @param {WeakSet<object>} visited - Set to track visited objects for cycle detection
26
- * @returns {boolean} Whether the two values are equal
27
- */
28
- const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
29
- // Fast paths
30
- if (Object.is(a, b)) return true
31
- if (typeof a !== typeof b) return false
32
- if (!isNonNullObject(a) || !isNonNullObject(b)) return false
33
-
34
- // Cycle detection
35
- if (!visited) visited = new WeakSet()
36
- if (visited.has(a as object) || visited.has(b as object))
37
- throw new CircularDependencyError('isEqual')
38
- visited.add(a)
39
- visited.add(b)
40
-
41
- try {
42
- if (Array.isArray(a) && Array.isArray(b)) {
43
- if (a.length !== b.length) return false
44
- for (let i = 0; i < a.length; i++) {
45
- if (!isEqual(a[i], b[i], visited)) return false
46
- }
47
- return true
48
- }
49
-
50
- if (Array.isArray(a) !== Array.isArray(b)) return false
51
-
52
- if (isRecord(a) && isRecord(b)) {
53
- const aKeys = Object.keys(a)
54
- const bKeys = Object.keys(b)
55
-
56
- if (aKeys.length !== bKeys.length) return false
57
- for (const key of aKeys) {
58
- if (!(key in b)) return false
59
- if (!isEqual(a[key], b[key], visited)) return false
60
- }
61
- return true
62
- }
63
-
64
- // For non-records/non-arrays, they are only equal if they are the same reference
65
- // (which would have been caught by Object.is at the beginning)
66
- return false
67
- } finally {
68
- visited.delete(a)
69
- visited.delete(b)
70
- }
71
- }
72
-
73
- /**
74
- * Compares two records and returns a result object containing the differences.
75
- *
76
- * @since 0.15.0
77
- * @param {T} oldObj - The old record to compare
78
- * @param {T} newObj - The new record to compare
79
- * @returns {DiffResult} The result of the comparison
80
- */
81
- const diff = <T extends UnknownRecord>(oldObj: T, newObj: T): DiffResult => {
82
- // Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
83
- const oldValid = isRecordOrArray(oldObj)
84
- const newValid = isRecordOrArray(newObj)
85
- if (!oldValid || !newValid) {
86
- // For non-objects or non-plain objects, treat as complete change if different
87
- const changed = !Object.is(oldObj, newObj)
88
- return {
89
- changed,
90
- add: changed && newValid ? newObj : {},
91
- change: {},
92
- remove: changed && oldValid ? oldObj : {},
93
- }
94
- }
95
-
96
- const visited = new WeakSet()
97
-
98
- const add = {} as UnknownRecord
99
- const change = {} as UnknownRecord
100
- const remove = {} as UnknownRecord
101
-
102
- const oldKeys = Object.keys(oldObj)
103
- const newKeys = Object.keys(newObj)
104
- const allKeys = new Set([...oldKeys, ...newKeys])
105
-
106
- for (const key of allKeys) {
107
- const oldHas = key in oldObj
108
- const newHas = key in newObj
109
-
110
- if (!oldHas && newHas) {
111
- add[key] = newObj[key]
112
- continue
113
- } else if (oldHas && !newHas) {
114
- remove[key] = UNSET
115
- continue
116
- }
117
-
118
- const oldValue = oldObj[key]
119
- const newValue = newObj[key]
120
-
121
- if (!isEqual(oldValue, newValue, visited)) change[key] = newValue
122
- }
123
-
124
- return {
125
- add,
126
- change,
127
- remove,
128
- changed: !!(
129
- Object.keys(add).length ||
130
- Object.keys(change).length ||
131
- Object.keys(remove).length
132
- ),
133
- }
134
- }
135
-
136
- /* === Exports === */
137
-
138
- export { type DiffResult, diff, isEqual, type UnknownRecord, type UnknownArray }
package/src/effect.ts DELETED
@@ -1,96 +0,0 @@
1
- import { CircularDependencyError, InvalidCallbackError } from './errors'
2
- import {
3
- type Cleanup,
4
- createWatcher,
5
- HOOK_CLEANUP,
6
- type MaybeCleanup,
7
- trackSignalReads,
8
- } from './system'
9
- import { isAbortError, isAsyncFunction, isFunction } from './util'
10
-
11
- /* === Types === */
12
-
13
- type EffectCallback =
14
- | (() => MaybeCleanup)
15
- | ((abort: AbortSignal) => Promise<MaybeCleanup>)
16
-
17
- /* === Functions === */
18
-
19
- /**
20
- * Define what happens when a reactive state changes
21
- *
22
- * The callback can be synchronous or asynchronous. Async callbacks receive
23
- * an AbortSignal parameter, which is automatically aborted when the effect
24
- * re-runs or is cleaned up, preventing stale async operations.
25
- *
26
- * @since 0.1.0
27
- * @param {EffectCallback} callback - Synchronous or asynchronous effect callback
28
- * @returns {Cleanup} - Cleanup function for the effect
29
- */
30
- const createEffect = (callback: EffectCallback): Cleanup => {
31
- if (!isFunction(callback) || callback.length > 1)
32
- throw new InvalidCallbackError('effect', callback)
33
-
34
- const isAsync = isAsyncFunction(callback)
35
- let running = false
36
- let controller: AbortController | undefined
37
-
38
- const watcher = createWatcher(() =>
39
- trackSignalReads(watcher, () => {
40
- if (running) throw new CircularDependencyError('effect')
41
- running = true
42
-
43
- // Abort any previous async operations
44
- controller?.abort()
45
- controller = undefined
46
-
47
- let cleanup: MaybeCleanup | Promise<MaybeCleanup>
48
-
49
- try {
50
- if (isAsync) {
51
- // Create AbortController for async callback
52
- controller = new AbortController()
53
- const currentController = controller
54
- callback(controller.signal)
55
- .then(cleanup => {
56
- // Only register cleanup if this is still the current controller
57
- if (
58
- isFunction(cleanup) &&
59
- controller === currentController
60
- )
61
- watcher.on(HOOK_CLEANUP, cleanup)
62
- })
63
- .catch(error => {
64
- if (!isAbortError(error))
65
- console.error(
66
- 'Error in async effect callback:',
67
- error,
68
- )
69
- })
70
- } else {
71
- cleanup = callback()
72
- if (isFunction(cleanup)) watcher.on(HOOK_CLEANUP, cleanup)
73
- }
74
- } catch (error) {
75
- if (!isAbortError(error))
76
- console.error('Error in effect callback:', error)
77
- }
78
-
79
- running = false
80
- }),
81
- )
82
-
83
- watcher()
84
- return () => {
85
- controller?.abort()
86
- try {
87
- watcher.stop()
88
- } catch (error) {
89
- console.error('Error in effect cleanup:', error)
90
- }
91
- }
92
- }
93
-
94
- /* === Exports === */
95
-
96
- export { type MaybeCleanup, type EffectCallback, createEffect }