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