@zeix/cause-effect 0.17.1 → 0.17.3

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 (57) hide show
  1. package/.ai-context.md +13 -0
  2. package/.github/copilot-instructions.md +4 -0
  3. package/.zed/settings.json +3 -0
  4. package/CLAUDE.md +41 -7
  5. package/README.md +48 -25
  6. package/archive/benchmark.ts +0 -5
  7. package/archive/collection.ts +6 -65
  8. package/archive/composite.ts +85 -0
  9. package/archive/computed.ts +18 -20
  10. package/archive/list.ts +7 -75
  11. package/archive/memo.ts +15 -15
  12. package/archive/state.ts +2 -1
  13. package/archive/store.ts +8 -78
  14. package/archive/task.ts +20 -25
  15. package/index.dev.js +508 -526
  16. package/index.js +1 -1
  17. package/index.ts +9 -11
  18. package/package.json +6 -6
  19. package/src/classes/collection.ts +70 -107
  20. package/src/classes/computed.ts +165 -149
  21. package/src/classes/list.ts +145 -107
  22. package/src/classes/ref.ts +19 -17
  23. package/src/classes/state.ts +21 -17
  24. package/src/classes/store.ts +125 -73
  25. package/src/diff.ts +2 -1
  26. package/src/effect.ts +17 -10
  27. package/src/errors.ts +14 -1
  28. package/src/resolve.ts +1 -1
  29. package/src/signal.ts +3 -2
  30. package/src/system.ts +159 -61
  31. package/src/util.ts +0 -6
  32. package/test/batch.test.ts +4 -11
  33. package/test/benchmark.test.ts +4 -2
  34. package/test/collection.test.ts +106 -107
  35. package/test/computed.test.ts +351 -112
  36. package/test/effect.test.ts +2 -2
  37. package/test/list.test.ts +62 -102
  38. package/test/ref.test.ts +128 -2
  39. package/test/state.test.ts +16 -22
  40. package/test/store.test.ts +101 -108
  41. package/test/util/dependency-graph.ts +2 -2
  42. package/tsconfig.build.json +11 -0
  43. package/tsconfig.json +5 -7
  44. package/types/index.d.ts +3 -3
  45. package/types/src/classes/collection.d.ts +9 -10
  46. package/types/src/classes/computed.d.ts +17 -20
  47. package/types/src/classes/list.d.ts +8 -6
  48. package/types/src/classes/ref.d.ts +8 -12
  49. package/types/src/classes/state.d.ts +5 -8
  50. package/types/src/classes/store.d.ts +14 -13
  51. package/types/src/effect.d.ts +1 -2
  52. package/types/src/errors.d.ts +2 -1
  53. package/types/src/signal.d.ts +3 -2
  54. package/types/src/system.d.ts +47 -34
  55. package/types/src/util.d.ts +1 -2
  56. package/src/classes/composite.ts +0 -176
  57. package/types/src/classes/composite.d.ts +0 -15
@@ -1,16 +1,20 @@
1
- import { diff, type UnknownRecord } from '../diff'
2
- import { DuplicateKeyError, validateSignalValue } from '../errors'
3
- import { createMutableSignal, type MutableSignal, type Signal } from '../signal'
1
+ import { type DiffResult, diff, type UnknownRecord } from '../diff'
4
2
  import {
5
- type Cleanup,
6
- type Listener,
7
- type Listeners,
8
- notifyWatchers,
9
- subscribeActiveWatcher,
10
- type Watcher,
3
+ DuplicateKeyError,
4
+ guardMutableSignal,
5
+ validateSignalValue,
6
+ } from '../errors'
7
+ import { createMutableSignal, type MutableSignal } from '../signal'
8
+ import {
9
+ batch,
10
+ notifyOf,
11
+ registerWatchCallbacks,
12
+ type SignalOptions,
13
+ subscribeTo,
14
+ UNSET,
15
+ unsubscribeAllFrom,
11
16
  } from '../system'
12
- import { isFunction, isObjectOfType, isRecord, isSymbol, UNSET } from '../util'
13
- import { Composite } from './composite'
17
+ import { isFunction, isObjectOfType, isRecord, isSymbol } from '../util'
14
18
  import type { List } from './list'
15
19
  import type { State } from './state'
16
20
 
@@ -30,40 +34,93 @@ const TYPE_STORE = 'Store' as const
30
34
 
31
35
  /* === Store Implementation === */
32
36
 
37
+ /**
38
+ * Create a new store with the given initial value.
39
+ *
40
+ * @since 0.17.0
41
+ * @param {T} initialValue - The initial value of the store
42
+ * @throws {NullishSignalValueError} - If the initial value is null or undefined
43
+ * @throws {InvalidSignalValueError} - If the initial value is not an object
44
+ */
33
45
  class BaseStore<T extends UnknownRecord> {
34
- #composite: Composite<T, Signal<T[keyof T] & {}>>
35
- #watchers = new Set<Watcher>()
36
-
37
- /**
38
- * Create a new store with the given initial value.
39
- *
40
- * @param {T} initialValue - The initial value of the store
41
- * @throws {NullishSignalValueError} - If the initial value is null or undefined
42
- * @throws {InvalidSignalValueError} - If the initial value is not an object
43
- */
44
- constructor(initialValue: T) {
45
- validateSignalValue('store', initialValue, isRecord)
46
-
47
- this.#composite = new Composite<T, Signal<T[keyof T] & {}>>(
46
+ #signals = new Map<keyof T & string, MutableSignal<T[keyof T] & {}>>()
47
+
48
+ constructor(initialValue: T, options?: SignalOptions<T>) {
49
+ validateSignalValue(
50
+ TYPE_STORE,
48
51
  initialValue,
49
- <K extends keyof T & string>(
50
- key: K,
51
- value: unknown,
52
- ): value is T[K] & {} => {
53
- validateSignalValue(`store for key "${key}"`, value)
54
- return true
55
- },
56
- value => createMutableSignal(value),
52
+ options?.guard ?? isRecord,
57
53
  )
54
+
55
+ this.#change({
56
+ add: initialValue,
57
+ change: {},
58
+ remove: {},
59
+ changed: true,
60
+ })
61
+ if (options?.watched)
62
+ registerWatchCallbacks(this, options.watched, options.unwatched)
58
63
  }
59
64
 
60
65
  get #value(): T {
61
66
  const record = {} as UnknownRecord
62
- for (const [key, signal] of this.#composite.signals.entries())
67
+ for (const [key, signal] of this.#signals.entries())
63
68
  record[key] = signal.get()
64
69
  return record as T
65
70
  }
66
71
 
72
+ #validate<K extends keyof T & string>(
73
+ key: K,
74
+ value: unknown,
75
+ ): value is T[K] & {} {
76
+ validateSignalValue(`${TYPE_STORE} for key "${key}"`, value)
77
+ return true
78
+ }
79
+
80
+ #add<K extends keyof T & string>(key: K, value: T[K]): boolean {
81
+ if (!this.#validate(key, value)) return false
82
+
83
+ this.#signals.set(
84
+ key,
85
+ createMutableSignal(value) as unknown as MutableSignal<
86
+ T[keyof T] & {}
87
+ >,
88
+ )
89
+ return true
90
+ }
91
+
92
+ #change(changes: DiffResult): boolean {
93
+ // Additions
94
+ if (Object.keys(changes.add).length) {
95
+ for (const key in changes.add)
96
+ this.#add(
97
+ key,
98
+ changes.add[key] as T[Extract<keyof T, string>] & {},
99
+ )
100
+ }
101
+
102
+ // Changes
103
+ if (Object.keys(changes.change).length) {
104
+ batch(() => {
105
+ for (const key in changes.change) {
106
+ const value = changes.change[key]
107
+ if (!this.#validate(key, value)) continue
108
+
109
+ const signal = this.#signals.get(key)
110
+ if (guardMutableSignal(`list item "${key}"`, value, signal))
111
+ signal.set(value)
112
+ }
113
+ })
114
+ }
115
+
116
+ // Removals
117
+ if (Object.keys(changes.remove).length) {
118
+ for (const key in changes.remove) this.remove(key)
119
+ }
120
+
121
+ return changes.changed
122
+ }
123
+
67
124
  // Public methods
68
125
  get [Symbol.toStringTag](): 'Store' {
69
126
  return TYPE_STORE
@@ -76,30 +133,12 @@ class BaseStore<T extends UnknownRecord> {
76
133
  *[Symbol.iterator](): IterableIterator<
77
134
  [string, MutableSignal<T[keyof T] & {}>]
78
135
  > {
79
- for (const [key, signal] of this.#composite.signals.entries())
80
- yield [key, signal as MutableSignal<T[keyof T] & {}>]
81
- }
82
-
83
- get(): T {
84
- subscribeActiveWatcher(this.#watchers)
85
- return this.#value
86
- }
87
-
88
- set(newValue: T): void {
89
- if (UNSET === newValue) {
90
- this.#composite.clear()
91
- notifyWatchers(this.#watchers)
92
- this.#watchers.clear()
93
- return
94
- }
95
-
96
- const oldValue = this.#value
97
- const changed = this.#composite.change(diff(oldValue, newValue))
98
- if (changed) notifyWatchers(this.#watchers)
136
+ for (const [key, signal] of this.#signals.entries()) yield [key, signal]
99
137
  }
100
138
 
101
139
  keys(): IterableIterator<string> {
102
- return this.#composite.signals.keys()
140
+ subscribeTo(this)
141
+ return this.#signals.keys()
103
142
  }
104
143
 
105
144
  byKey<K extends keyof T & string>(
@@ -111,9 +150,8 @@ class BaseStore<T extends UnknownRecord> {
111
150
  : T[K] extends unknown & {}
112
151
  ? State<T[K] & {}>
113
152
  : State<T[K] & {}> | undefined {
114
- return this.#composite.signals.get(
115
- key,
116
- ) as T[K] extends readonly (infer U extends {})[]
153
+ return this.#signals.get(key) as T[K] extends readonly (infer U extends
154
+ {})[]
117
155
  ? List<U>
118
156
  : T[K] extends UnknownRecord
119
157
  ? Store<T[K]>
@@ -122,29 +160,39 @@ class BaseStore<T extends UnknownRecord> {
122
160
  : State<T[K] & {}> | undefined
123
161
  }
124
162
 
163
+ get(): T {
164
+ subscribeTo(this)
165
+ return this.#value
166
+ }
167
+
168
+ set(newValue: T): void {
169
+ if (UNSET === newValue) {
170
+ this.#signals.clear()
171
+ notifyOf(this)
172
+ unsubscribeAllFrom(this)
173
+ return
174
+ }
175
+
176
+ const changed = this.#change(diff(this.#value, newValue))
177
+ if (changed) notifyOf(this)
178
+ }
179
+
125
180
  update(fn: (oldValue: T) => T): void {
126
181
  this.set(fn(this.get()))
127
182
  }
128
183
 
129
184
  add<K extends keyof T & string>(key: K, value: T[K]): K {
130
- if (this.#composite.signals.has(key))
131
- throw new DuplicateKeyError('store', key, value)
185
+ if (this.#signals.has(key))
186
+ throw new DuplicateKeyError(TYPE_STORE, key, value)
132
187
 
133
- const ok = this.#composite.add(key, value)
134
- if (ok) notifyWatchers(this.#watchers)
188
+ const ok = this.#add(key, value)
189
+ if (ok) notifyOf(this)
135
190
  return key
136
191
  }
137
192
 
138
193
  remove(key: string): void {
139
- const ok = this.#composite.remove(key)
140
- if (ok) notifyWatchers(this.#watchers)
141
- }
142
-
143
- on<K extends keyof Omit<Listeners, 'sort'>>(
144
- type: K,
145
- listener: Listener<K>,
146
- ): Cleanup {
147
- return this.#composite.on(type, listener)
194
+ const ok = this.#signals.delete(key)
195
+ if (ok) notifyOf(this)
148
196
  }
149
197
  }
150
198
 
@@ -155,10 +203,14 @@ class BaseStore<T extends UnknownRecord> {
155
203
  *
156
204
  * @since 0.15.0
157
205
  * @param {T} initialValue - Initial object or array value of the store
206
+ * @param {SignalOptions<T>} options - Options for the store
158
207
  * @returns {Store<T>} - New store with reactive properties that preserves the original type T
159
208
  */
160
- const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
161
- const instance = new BaseStore(initialValue)
209
+ const createStore = <T extends UnknownRecord>(
210
+ initialValue: T,
211
+ options?: SignalOptions<T>,
212
+ ): Store<T> => {
213
+ const instance = new BaseStore(initialValue, options)
162
214
 
163
215
  // Return proxy for property access
164
216
  return new Proxy(instance, {
package/src/diff.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CircularDependencyError } from './errors'
2
- import { isNonNullObject, isRecord, isRecordOrArray, UNSET } from './util'
2
+ import { UNSET } from './system'
3
+ import { isNonNullObject, isRecord, isRecordOrArray } from './util'
3
4
 
4
5
  /* === Types === */
5
6
 
package/src/effect.ts CHANGED
@@ -1,12 +1,9 @@
1
1
  import { CircularDependencyError, InvalidCallbackError } from './errors'
2
- import { type Cleanup, createWatcher, trackSignalReads } from './system'
2
+ import { type Cleanup, createWatcher, type MaybeCleanup } from './system'
3
3
  import { isAbortError, isAsyncFunction, isFunction } from './util'
4
4
 
5
5
  /* === Types === */
6
6
 
7
- // biome-ignore lint/suspicious/noConfusingVoidType: optional Cleanup return type
8
- type MaybeCleanup = Cleanup | undefined | void
9
-
10
7
  type EffectCallback =
11
8
  | (() => MaybeCleanup)
12
9
  | ((abort: AbortSignal) => Promise<MaybeCleanup>)
@@ -32,8 +29,11 @@ const createEffect = (callback: EffectCallback): Cleanup => {
32
29
  let running = false
33
30
  let controller: AbortController | undefined
34
31
 
35
- const watcher = createWatcher(() =>
36
- trackSignalReads(watcher, () => {
32
+ const watcher = createWatcher(
33
+ () => {
34
+ watcher.run()
35
+ },
36
+ () => {
37
37
  if (running) throw new CircularDependencyError('effect')
38
38
  running = true
39
39
 
@@ -59,7 +59,10 @@ const createEffect = (callback: EffectCallback): Cleanup => {
59
59
  })
60
60
  .catch(error => {
61
61
  if (!isAbortError(error))
62
- console.error('Async effect error:', error)
62
+ console.error(
63
+ 'Error in async effect callback:',
64
+ error,
65
+ )
63
66
  })
64
67
  } else {
65
68
  cleanup = callback()
@@ -67,17 +70,21 @@ const createEffect = (callback: EffectCallback): Cleanup => {
67
70
  }
68
71
  } catch (error) {
69
72
  if (!isAbortError(error))
70
- console.error('Effect callback error:', error)
73
+ console.error('Error in effect callback:', error)
71
74
  }
72
75
 
73
76
  running = false
74
- }),
77
+ },
75
78
  )
76
79
 
77
80
  watcher()
78
81
  return () => {
79
82
  controller?.abort()
80
- watcher.stop()
83
+ try {
84
+ watcher.stop()
85
+ } catch (error) {
86
+ console.error('Error in effect cleanup:', error)
87
+ }
81
88
  }
82
89
  }
83
90
 
package/src/errors.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { isMutableSignal, type MutableSignal } from './signal'
2
- import { isFunction, isSymbol, UNSET, valueString } from './util'
2
+ import { UNSET } from './system'
3
+ import { isFunction, isSymbol, valueString } from './util'
3
4
 
4
5
  /* === Types === */
5
6
 
@@ -25,6 +26,13 @@ class DuplicateKeyError extends Error {
25
26
  }
26
27
  }
27
28
 
29
+ class FailedAssertionError extends Error {
30
+ constructor(message: string = 'unexpected condition') {
31
+ super(`Assertion failed: ${message}`)
32
+ this.name = 'FailedAssertionError'
33
+ }
34
+ }
35
+
28
36
  class InvalidCallbackError extends TypeError {
29
37
  constructor(where: string, value: unknown) {
30
38
  super(`Invalid ${where} callback ${valueString(value)}`)
@@ -64,6 +72,10 @@ class ReadonlySignalError extends Error {
64
72
 
65
73
  /* === Functions === */
66
74
 
75
+ function assert(condition: unknown, msg?: string): asserts condition {
76
+ if (!condition) throw new FailedAssertionError(msg)
77
+ }
78
+
67
79
  const createError = /*#__PURE__*/ (reason: unknown): Error =>
68
80
  reason instanceof Error ? reason : Error(String(reason))
69
81
 
@@ -103,6 +115,7 @@ export {
103
115
  InvalidSignalValueError,
104
116
  NullishSignalValueError,
105
117
  ReadonlySignalError,
118
+ assert,
106
119
  createError,
107
120
  validateCallback,
108
121
  validateSignalValue,
package/src/resolve.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { UnknownRecord } from './diff'
2
2
  import { createError } from './errors'
3
3
  import type { SignalValues, UnknownSignalRecord } from './signal'
4
- import { UNSET } from './util'
4
+ import { UNSET } from './system'
5
5
 
6
6
  /* === Types === */
7
7
 
package/src/signal.ts CHANGED
@@ -10,7 +10,6 @@ import { isList, List } from './classes/list'
10
10
  import { isState, State } from './classes/state'
11
11
  import { createStore, isStore, type Store } from './classes/store'
12
12
  import type { UnknownRecord } from './diff'
13
- // import type { Collection } from './signals/collection'
14
13
  import { isRecord, isUniformArray } from './util'
15
14
 
16
15
  /* === Types === */
@@ -19,6 +18,7 @@ type Signal<T extends {}> = {
19
18
  get(): T
20
19
  }
21
20
 
21
+ type UnknownSignal = Signal<unknown & {}>
22
22
  type MutableSignal<T extends {}> = T extends readonly (infer U extends {})[]
23
23
  ? List<U>
24
24
  : T extends UnknownRecord
@@ -26,7 +26,7 @@ type MutableSignal<T extends {}> = T extends readonly (infer U extends {})[]
26
26
  : State<T>
27
27
  type ReadonlySignal<T extends {}> = Computed<T> // | Collection<T>
28
28
 
29
- type UnknownSignalRecord = Record<string, Signal<unknown & {}>>
29
+ type UnknownSignalRecord = Record<string, UnknownSignal>
30
30
 
31
31
  type SignalValues<S extends UnknownSignalRecord> = {
32
32
  [K in keyof S]: S[K] extends Signal<infer T> ? T : never
@@ -101,5 +101,6 @@ export {
101
101
  type ReadonlySignal,
102
102
  type Signal,
103
103
  type SignalValues,
104
+ type UnknownSignal,
104
105
  type UnknownSignalRecord,
105
106
  }