@zeix/cause-effect 0.16.1 → 0.17.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 (66) hide show
  1. package/.ai-context.md +85 -21
  2. package/.cursorrules +11 -5
  3. package/.github/copilot-instructions.md +64 -13
  4. package/CLAUDE.md +143 -163
  5. package/LICENSE +1 -1
  6. package/README.md +248 -333
  7. package/archive/benchmark.ts +688 -0
  8. package/archive/collection.ts +312 -0
  9. package/{src → archive}/computed.ts +21 -21
  10. package/archive/list.ts +551 -0
  11. package/archive/memo.ts +139 -0
  12. package/{src → archive}/state.ts +13 -11
  13. package/archive/store.ts +368 -0
  14. package/archive/task.ts +194 -0
  15. package/eslint.config.js +1 -0
  16. package/index.dev.js +938 -509
  17. package/index.js +1 -1
  18. package/index.ts +50 -23
  19. package/package.json +1 -1
  20. package/src/classes/collection.ts +282 -0
  21. package/src/classes/composite.ts +176 -0
  22. package/src/classes/computed.ts +333 -0
  23. package/src/classes/list.ts +305 -0
  24. package/src/classes/ref.ts +68 -0
  25. package/src/classes/state.ts +98 -0
  26. package/src/classes/store.ts +210 -0
  27. package/src/diff.ts +26 -53
  28. package/src/effect.ts +9 -9
  29. package/src/errors.ts +71 -25
  30. package/src/match.ts +5 -12
  31. package/src/resolve.ts +3 -2
  32. package/src/signal.ts +58 -41
  33. package/src/system.ts +79 -42
  34. package/src/util.ts +16 -34
  35. package/test/batch.test.ts +15 -17
  36. package/test/benchmark.test.ts +4 -4
  37. package/test/collection.test.ts +853 -0
  38. package/test/computed.test.ts +138 -130
  39. package/test/diff.test.ts +2 -2
  40. package/test/effect.test.ts +36 -35
  41. package/test/list.test.ts +754 -0
  42. package/test/match.test.ts +25 -25
  43. package/test/ref.test.ts +227 -0
  44. package/test/resolve.test.ts +17 -19
  45. package/test/signal.test.ts +70 -119
  46. package/test/state.test.ts +44 -44
  47. package/test/store.test.ts +253 -929
  48. package/types/index.d.ts +12 -9
  49. package/types/src/classes/collection.d.ts +46 -0
  50. package/types/src/classes/composite.d.ts +15 -0
  51. package/types/src/classes/computed.d.ts +97 -0
  52. package/types/src/classes/list.d.ts +41 -0
  53. package/types/src/classes/ref.d.ts +39 -0
  54. package/types/src/classes/state.d.ts +52 -0
  55. package/types/src/classes/store.d.ts +51 -0
  56. package/types/src/diff.d.ts +8 -12
  57. package/types/src/errors.d.ts +17 -11
  58. package/types/src/signal.d.ts +27 -14
  59. package/types/src/system.d.ts +41 -20
  60. package/types/src/util.d.ts +6 -4
  61. package/src/store.ts +0 -474
  62. package/types/src/collection.d.ts +0 -26
  63. package/types/src/computed.d.ts +0 -33
  64. package/types/src/scheduler.d.ts +0 -55
  65. package/types/src/state.d.ts +0 -24
  66. package/types/src/store.d.ts +0 -65
@@ -0,0 +1,98 @@
1
+ import { isEqual } from '../diff'
2
+ import { validateCallback, validateSignalValue } from '../errors'
3
+ import { notifyWatchers, subscribeActiveWatcher, type Watcher } from '../system'
4
+ import { isObjectOfType, UNSET } from '../util'
5
+
6
+ /* === Constants === */
7
+
8
+ const TYPE_STATE = 'State' as const
9
+
10
+ /* === Class === */
11
+
12
+ /**
13
+ * Create a new state signal.
14
+ *
15
+ * @since 0.17.0
16
+ */
17
+ class State<T extends {}> {
18
+ #watchers = new Set<Watcher>()
19
+ #value: T
20
+
21
+ /**
22
+ * Create a new state signal.
23
+ *
24
+ * @param {T} initialValue - Initial value of the state
25
+ * @throws {NullishSignalValueError} - If the initial value is null or undefined
26
+ * @throws {InvalidSignalValueError} - If the initial value is invalid
27
+ */
28
+ constructor(initialValue: T) {
29
+ validateSignalValue('state', initialValue)
30
+
31
+ this.#value = initialValue
32
+ }
33
+
34
+ get [Symbol.toStringTag](): string {
35
+ return TYPE_STATE
36
+ }
37
+
38
+ /**
39
+ * Get the current value of the state signal.
40
+ *
41
+ * @returns {T} - Current value of the state
42
+ */
43
+ get(): T {
44
+ subscribeActiveWatcher(this.#watchers)
45
+ return this.#value
46
+ }
47
+
48
+ /**
49
+ * Set the value of the state signal.
50
+ *
51
+ * @param {T} newValue - New value of the state
52
+ * @returns {void}
53
+ * @throws {NullishSignalValueError} - If the initial value is null or undefined
54
+ * @throws {InvalidSignalValueError} - If the initial value is invalid
55
+ */
56
+ set(newValue: T): void {
57
+ validateSignalValue('state', newValue)
58
+
59
+ if (isEqual(this.#value, newValue)) return
60
+ this.#value = newValue
61
+ notifyWatchers(this.#watchers)
62
+
63
+ // Setting to UNSET clears the watchers so the signal can be garbage collected
64
+ if (UNSET === this.#value) this.#watchers.clear()
65
+ }
66
+
67
+ /**
68
+ * Update the value of the state signal.
69
+ *
70
+ * @param {Function} updater - Function that takes the current value and returns the new value
71
+ * @returns {void}
72
+ * @throws {InvalidCallbackError} - If the updater function is not a function
73
+ * @throws {NullishSignalValueError} - If the initial value is null or undefined
74
+ * @throws {InvalidSignalValueError} - If the initial value is invalid
75
+ */
76
+ update(updater: (oldValue: T) => T): void {
77
+ validateCallback('state update', updater)
78
+
79
+ this.set(updater(this.#value))
80
+ }
81
+ }
82
+
83
+ /* === Functions === */
84
+
85
+ /**
86
+ * Check if the provided value is a State instance
87
+ *
88
+ * @since 0.9.0
89
+ * @param {unknown} value - Value to check
90
+ * @returns {boolean} - True if the value is a State instance, false otherwise
91
+ */
92
+ const isState = /*#__PURE__*/ <T extends {}>(
93
+ value: unknown,
94
+ ): value is State<T> => isObjectOfType(value, TYPE_STATE)
95
+
96
+ /* === Exports === */
97
+
98
+ export { TYPE_STATE, isState, State }
@@ -0,0 +1,210 @@
1
+ import { diff, type UnknownRecord } from '../diff'
2
+ import { DuplicateKeyError, validateSignalValue } from '../errors'
3
+ import { createMutableSignal, type MutableSignal, type Signal } from '../signal'
4
+ import {
5
+ type Cleanup,
6
+ type Listener,
7
+ type Listeners,
8
+ notifyWatchers,
9
+ subscribeActiveWatcher,
10
+ type Watcher,
11
+ } from '../system'
12
+ import { isFunction, isObjectOfType, isRecord, isSymbol, UNSET } from '../util'
13
+ import { Composite } from './composite'
14
+ import type { List } from './list'
15
+ import type { State } from './state'
16
+
17
+ /* === Types === */
18
+
19
+ type Store<T extends UnknownRecord> = BaseStore<T> & {
20
+ [K in keyof T]: T[K] extends readonly (infer U extends {})[]
21
+ ? List<U>
22
+ : T[K] extends UnknownRecord
23
+ ? Store<T[K]>
24
+ : State<T[K] & {}>
25
+ }
26
+
27
+ /* === Constants === */
28
+
29
+ const TYPE_STORE = 'Store' as const
30
+
31
+ /* === Store Implementation === */
32
+
33
+ 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] & {}>>(
48
+ 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),
57
+ )
58
+ }
59
+
60
+ get #value(): T {
61
+ const record = {} as UnknownRecord
62
+ for (const [key, signal] of this.#composite.signals.entries())
63
+ record[key] = signal.get()
64
+ return record as T
65
+ }
66
+
67
+ // Public methods
68
+ get [Symbol.toStringTag](): 'Store' {
69
+ return TYPE_STORE
70
+ }
71
+
72
+ get [Symbol.isConcatSpreadable](): boolean {
73
+ return false
74
+ }
75
+
76
+ *[Symbol.iterator](): IterableIterator<
77
+ [string, MutableSignal<T[keyof T] & {}>]
78
+ > {
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)
99
+ }
100
+
101
+ keys(): IterableIterator<string> {
102
+ return this.#composite.signals.keys()
103
+ }
104
+
105
+ byKey<K extends keyof T & string>(
106
+ key: K,
107
+ ): T[K] extends readonly (infer U extends {})[]
108
+ ? List<U>
109
+ : T[K] extends UnknownRecord
110
+ ? Store<T[K]>
111
+ : T[K] extends unknown & {}
112
+ ? State<T[K] & {}>
113
+ : State<T[K] & {}> | undefined {
114
+ return this.#composite.signals.get(
115
+ key,
116
+ ) as T[K] extends readonly (infer U extends {})[]
117
+ ? List<U>
118
+ : T[K] extends UnknownRecord
119
+ ? Store<T[K]>
120
+ : T[K] extends unknown & {}
121
+ ? State<T[K] & {}>
122
+ : State<T[K] & {}> | undefined
123
+ }
124
+
125
+ update(fn: (oldValue: T) => T): void {
126
+ this.set(fn(this.get()))
127
+ }
128
+
129
+ 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)
132
+
133
+ const ok = this.#composite.add(key, value)
134
+ if (ok) notifyWatchers(this.#watchers)
135
+ return key
136
+ }
137
+
138
+ 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)
148
+ }
149
+ }
150
+
151
+ /* === Functions === */
152
+
153
+ /**
154
+ * Create a new store with deeply nested reactive properties
155
+ *
156
+ * @since 0.15.0
157
+ * @param {T} initialValue - Initial object or array value of the store
158
+ * @returns {Store<T>} - New store with reactive properties that preserves the original type T
159
+ */
160
+ const createStore = <T extends UnknownRecord>(initialValue: T): Store<T> => {
161
+ const instance = new BaseStore(initialValue)
162
+
163
+ // Return proxy for property access
164
+ return new Proxy(instance, {
165
+ get(target, prop) {
166
+ if (prop in target) {
167
+ const value = Reflect.get(target, prop)
168
+ return isFunction(value) ? value.bind(target) : value
169
+ }
170
+ if (!isSymbol(prop)) return target.byKey(prop)
171
+ },
172
+ has(target, prop) {
173
+ if (prop in target) return true
174
+ return target.byKey(String(prop)) !== undefined
175
+ },
176
+ ownKeys(target) {
177
+ return Array.from(target.keys())
178
+ },
179
+ getOwnPropertyDescriptor(target, prop) {
180
+ if (prop in target)
181
+ return Reflect.getOwnPropertyDescriptor(target, prop)
182
+ if (isSymbol(prop)) return undefined
183
+
184
+ const signal = target.byKey(String(prop))
185
+ return signal
186
+ ? {
187
+ enumerable: true,
188
+ configurable: true,
189
+ writable: true,
190
+ value: signal,
191
+ }
192
+ : undefined
193
+ },
194
+ }) as Store<T>
195
+ }
196
+
197
+ /**
198
+ * Check if the provided value is a Store instance
199
+ *
200
+ * @since 0.15.0
201
+ * @param {unknown} value - Value to check
202
+ * @returns {boolean} - True if the value is a Store instance, false otherwise
203
+ */
204
+ const isStore = <T extends UnknownRecord>(
205
+ value: unknown,
206
+ ): value is BaseStore<T> => isObjectOfType(value, TYPE_STORE)
207
+
208
+ /* === Exports === */
209
+
210
+ export { createStore, isStore, BaseStore, TYPE_STORE, type Store }
package/src/diff.ts CHANGED
@@ -1,24 +1,16 @@
1
1
  import { CircularDependencyError } from './errors'
2
- import { isRecord, isRecordOrArray, UNSET } from './util'
2
+ import { isNonNullObject, isRecord, isRecordOrArray, UNSET } from './util'
3
3
 
4
4
  /* === Types === */
5
5
 
6
- type UnknownRecord = Record<string, unknown & {}>
6
+ type UnknownRecord = Record<string, unknown>
7
7
  type UnknownArray = ReadonlyArray<unknown & {}>
8
8
 
9
- type ArrayToRecord<T extends UnknownArray> = {
10
- [key: string]: T extends Array<infer U extends {}> ? U : never
11
- }
12
-
13
- type PartialRecord<T> = T extends UnknownArray
14
- ? Partial<ArrayToRecord<T>>
15
- : Partial<T>
16
-
17
- type DiffResult<T extends UnknownRecord | UnknownArray = UnknownRecord> = {
9
+ type DiffResult = {
18
10
  changed: boolean
19
- add: PartialRecord<T>
20
- change: PartialRecord<T>
21
- remove: PartialRecord<T>
11
+ add: UnknownRecord
12
+ change: UnknownRecord
13
+ remove: UnknownRecord
22
14
  }
23
15
 
24
16
  /* === Functions === */
@@ -36,14 +28,14 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
36
28
  // Fast paths
37
29
  if (Object.is(a, b)) return true
38
30
  if (typeof a !== typeof b) return false
39
- if (typeof a !== 'object' || a === null || b === null) return false
31
+ if (!isNonNullObject(a) || !isNonNullObject(b)) return false
40
32
 
41
33
  // Cycle detection
42
34
  if (!visited) visited = new WeakSet()
43
35
  if (visited.has(a as object) || visited.has(b as object))
44
36
  throw new CircularDependencyError('isEqual')
45
- visited.add(a as object)
46
- visited.add(b as object)
37
+ visited.add(a)
38
+ visited.add(b)
47
39
 
48
40
  try {
49
41
  if (Array.isArray(a) && Array.isArray(b)) {
@@ -63,14 +55,7 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
63
55
  if (aKeys.length !== bKeys.length) return false
64
56
  for (const key of aKeys) {
65
57
  if (!(key in b)) return false
66
- if (
67
- !isEqual(
68
- (a as Record<string, unknown>)[key],
69
- (b as Record<string, unknown>)[key],
70
- visited,
71
- )
72
- )
73
- return false
58
+ if (!isEqual(a[key], b[key], visited)) return false
74
59
  }
75
60
  return true
76
61
  }
@@ -79,8 +64,8 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
79
64
  // (which would have been caught by Object.is at the beginning)
80
65
  return false
81
66
  } finally {
82
- visited.delete(a as object)
83
- visited.delete(b as object)
67
+ visited.delete(a)
68
+ visited.delete(b)
84
69
  }
85
70
  }
86
71
 
@@ -90,12 +75,9 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
90
75
  * @since 0.15.0
91
76
  * @param {T} oldObj - The old record to compare
92
77
  * @param {T} newObj - The new record to compare
93
- * @returns {DiffResult<T>} The result of the comparison
78
+ * @returns {DiffResult} The result of the comparison
94
79
  */
95
- const diff = <T extends UnknownRecord | UnknownArray>(
96
- oldObj: T extends UnknownArray ? ArrayToRecord<T> : T,
97
- newObj: T extends UnknownArray ? ArrayToRecord<T> : T,
98
- ): DiffResult<T> => {
80
+ const diff = <T extends UnknownRecord>(oldObj: T, newObj: T): DiffResult => {
99
81
  // Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
100
82
  const oldValid = isRecordOrArray(oldObj)
101
83
  const newValid = isRecordOrArray(newObj)
@@ -104,17 +86,17 @@ const diff = <T extends UnknownRecord | UnknownArray>(
104
86
  const changed = !Object.is(oldObj, newObj)
105
87
  return {
106
88
  changed,
107
- add: changed && newValid ? newObj : ({} as PartialRecord<T>),
108
- change: {} as PartialRecord<T>,
109
- remove: changed && oldValid ? oldObj : ({} as PartialRecord<T>),
89
+ add: changed && newValid ? newObj : {},
90
+ change: {},
91
+ remove: changed && oldValid ? oldObj : {},
110
92
  }
111
93
  }
112
94
 
113
95
  const visited = new WeakSet()
114
96
 
115
- const add = {} as PartialRecord<T>
116
- const change = {} as PartialRecord<T>
117
- const remove = {} as PartialRecord<T>
97
+ const add = {} as UnknownRecord
98
+ const change = {} as UnknownRecord
99
+ const remove = {} as UnknownRecord
118
100
 
119
101
  const oldKeys = Object.keys(oldObj)
120
102
  const newKeys = Object.keys(newObj)
@@ -138,27 +120,18 @@ const diff = <T extends UnknownRecord | UnknownArray>(
138
120
  if (!isEqual(oldValue, newValue, visited)) change[key] = newValue
139
121
  }
140
122
 
141
- const changed =
142
- Object.keys(add).length > 0 ||
143
- Object.keys(change).length > 0 ||
144
- Object.keys(remove).length > 0
145
-
146
123
  return {
147
- changed,
148
124
  add,
149
125
  change,
150
126
  remove,
127
+ changed: !!(
128
+ Object.keys(add).length ||
129
+ Object.keys(change).length ||
130
+ Object.keys(remove).length
131
+ ),
151
132
  }
152
133
  }
153
134
 
154
135
  /* === Exports === */
155
136
 
156
- export {
157
- type ArrayToRecord,
158
- type DiffResult,
159
- diff,
160
- isEqual,
161
- type UnknownRecord,
162
- type UnknownArray,
163
- type PartialRecord,
164
- }
137
+ export { type DiffResult, diff, isEqual, type UnknownRecord, type UnknownArray }
package/src/effect.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CircularDependencyError, InvalidCallbackError } from './errors'
2
- import { type Cleanup, createWatcher, observe } from './system'
3
- import { isAbortError, isAsyncFunction, isFunction, valueString } from './util'
2
+ import { type Cleanup, createWatcher, trackSignalReads } from './system'
3
+ import { isAbortError, isAsyncFunction, isFunction } from './util'
4
4
 
5
5
  /* === Types === */
6
6
 
@@ -26,14 +26,14 @@ type EffectCallback =
26
26
  */
27
27
  const createEffect = (callback: EffectCallback): Cleanup => {
28
28
  if (!isFunction(callback) || callback.length > 1)
29
- throw new InvalidCallbackError('effect', valueString(callback))
29
+ throw new InvalidCallbackError('effect', callback)
30
30
 
31
31
  const isAsync = isAsyncFunction(callback)
32
32
  let running = false
33
33
  let controller: AbortController | undefined
34
34
 
35
35
  const watcher = createWatcher(() =>
36
- observe(() => {
36
+ trackSignalReads(watcher, () => {
37
37
  if (running) throw new CircularDependencyError('effect')
38
38
  running = true
39
39
 
@@ -55,15 +55,15 @@ const createEffect = (callback: EffectCallback): Cleanup => {
55
55
  isFunction(cleanup) &&
56
56
  controller === currentController
57
57
  )
58
- watcher.unwatch(cleanup)
58
+ watcher.onCleanup(cleanup)
59
59
  })
60
60
  .catch(error => {
61
61
  if (!isAbortError(error))
62
62
  console.error('Async effect error:', error)
63
63
  })
64
64
  } else {
65
- cleanup = (callback as () => MaybeCleanup)()
66
- if (isFunction(cleanup)) watcher.unwatch(cleanup)
65
+ cleanup = callback()
66
+ if (isFunction(cleanup)) watcher.onCleanup(cleanup)
67
67
  }
68
68
  } catch (error) {
69
69
  if (!isAbortError(error))
@@ -71,13 +71,13 @@ const createEffect = (callback: EffectCallback): Cleanup => {
71
71
  }
72
72
 
73
73
  running = false
74
- }, watcher),
74
+ }),
75
75
  )
76
76
 
77
77
  watcher()
78
78
  return () => {
79
79
  controller?.abort()
80
- watcher.cleanup()
80
+ watcher.stop()
81
81
  }
82
82
  }
83
83
 
package/src/errors.ts CHANGED
@@ -1,3 +1,12 @@
1
+ import { isMutableSignal, type MutableSignal } from './signal'
2
+ import { isFunction, isSymbol, UNSET, valueString } from './util'
3
+
4
+ /* === Types === */
5
+
6
+ type Guard<T> = (value: unknown) => value is T
7
+
8
+ /* === Classes === */
9
+
1
10
  class CircularDependencyError extends Error {
2
11
  constructor(where: string) {
3
12
  super(`Circular dependency detected in ${where}`)
@@ -5,16 +14,34 @@ class CircularDependencyError extends Error {
5
14
  }
6
15
  }
7
16
 
17
+ class DuplicateKeyError extends Error {
18
+ constructor(where: string, key: string, value?: unknown) {
19
+ super(
20
+ `Could not add ${where} key "${key}"${
21
+ value ? ` with value ${valueString(value)}` : ''
22
+ } because it already exists`,
23
+ )
24
+ this.name = 'DuplicateKeyError'
25
+ }
26
+ }
27
+
8
28
  class InvalidCallbackError extends TypeError {
9
- constructor(where: string, value: string) {
10
- super(`Invalid ${where} callback ${value}`)
29
+ constructor(where: string, value: unknown) {
30
+ super(`Invalid ${where} callback ${valueString(value)}`)
11
31
  this.name = 'InvalidCallbackError'
12
32
  }
13
33
  }
14
34
 
35
+ class InvalidCollectionSourceError extends TypeError {
36
+ constructor(where: string, value: unknown) {
37
+ super(`Invalid ${where} source ${valueString(value)}`)
38
+ this.name = 'InvalidCollectionSourceError'
39
+ }
40
+ }
41
+
15
42
  class InvalidSignalValueError extends TypeError {
16
- constructor(where: string, value: string) {
17
- super(`Invalid signal value ${value} in ${where}`)
43
+ constructor(where: string, value: unknown) {
44
+ super(`Invalid signal value ${valueString(value)} in ${where}`)
18
45
  this.name = 'InvalidSignalValueError'
19
46
  }
20
47
  }
@@ -26,39 +53,58 @@ class NullishSignalValueError extends TypeError {
26
53
  }
27
54
  }
28
55
 
29
- class StoreKeyExistsError extends Error {
30
- constructor(key: string, value: string) {
56
+ class ReadonlySignalError extends Error {
57
+ constructor(what: string, value: unknown) {
31
58
  super(
32
- `Could not add store key "${key}" with value ${value} because it already exists`,
59
+ `Could not set ${what} to ${valueString(value)} because signal is read-only`,
33
60
  )
34
- this.name = 'StoreKeyExistsError'
61
+ this.name = 'ReadonlySignalError'
35
62
  }
36
63
  }
37
64
 
38
- class StoreKeyRangeError extends RangeError {
39
- constructor(index: number) {
40
- super(
41
- `Could not remove store index ${String(index)} because it is out of range`,
42
- )
43
- this.name = 'StoreKeyRangeError'
44
- }
65
+ /* === Functions === */
66
+
67
+ const createError = /*#__PURE__*/ (reason: unknown): Error =>
68
+ reason instanceof Error ? reason : Error(String(reason))
69
+
70
+ const validateCallback = (
71
+ where: string,
72
+ value: unknown,
73
+ guard: (value: unknown) => boolean = isFunction,
74
+ ): void => {
75
+ if (!guard(value)) throw new InvalidCallbackError(where, value)
45
76
  }
46
77
 
47
- class StoreKeyReadonlyError extends Error {
48
- constructor(key: string, value: string) {
49
- super(
50
- `Could not set store key "${key}" to ${value} because it is readonly`,
51
- )
52
- this.name = 'StoreKeyReadonlyError'
53
- }
78
+ const validateSignalValue = (
79
+ where: string,
80
+ value: unknown,
81
+ guard: (value: unknown) => boolean = () =>
82
+ !(isSymbol(value) && value !== UNSET) || isFunction(value),
83
+ ): void => {
84
+ if (value == null) throw new NullishSignalValueError(where)
85
+ if (!guard(value)) throw new InvalidSignalValueError(where, value)
86
+ }
87
+
88
+ const guardMutableSignal = <T extends {}>(
89
+ what: string,
90
+ value: unknown,
91
+ signal: unknown,
92
+ ): signal is MutableSignal<T> => {
93
+ if (!isMutableSignal(signal)) throw new ReadonlySignalError(what, value)
94
+ return true
54
95
  }
55
96
 
56
97
  export {
98
+ type Guard,
57
99
  CircularDependencyError,
100
+ DuplicateKeyError,
58
101
  InvalidCallbackError,
102
+ InvalidCollectionSourceError,
59
103
  InvalidSignalValueError,
60
104
  NullishSignalValueError,
61
- StoreKeyExistsError,
62
- StoreKeyRangeError,
63
- StoreKeyReadonlyError,
105
+ ReadonlySignalError,
106
+ createError,
107
+ validateCallback,
108
+ validateSignalValue,
109
+ guardMutableSignal,
64
110
  }
package/src/match.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { createError } from './errors'
1
2
  import type { ResolveResult } from './resolve'
2
3
  import type { SignalValues, UnknownSignalRecord } from './signal'
3
- import { toError } from './util'
4
4
 
5
5
  /* === Types === */
6
6
 
@@ -32,17 +32,10 @@ function match<S extends UnknownSignalRecord>(
32
32
  if (result.pending) handlers.nil?.()
33
33
  else if (result.errors) handlers.err?.(result.errors)
34
34
  else if (result.ok) handlers.ok(result.values)
35
- } catch (error) {
36
- // If handler throws, try error handler, otherwise rethrow
37
- if (
38
- handlers.err &&
39
- (!result.errors || !result.errors.includes(toError(error)))
40
- )
41
- handlers.err(
42
- result.errors
43
- ? [...result.errors, toError(error)]
44
- : [toError(error)],
45
- )
35
+ } catch (e) {
36
+ const error = createError(e)
37
+ if (handlers.err && (!result.errors || !result.errors.includes(error)))
38
+ handlers.err(result.errors ? [...result.errors, error] : [error])
46
39
  else throw error
47
40
  }
48
41
  }
package/src/resolve.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { UnknownRecord } from './diff'
2
+ import { createError } from './errors'
2
3
  import type { SignalValues, UnknownSignalRecord } from './signal'
3
- import { toError, UNSET } from './util'
4
+ import { UNSET } from './util'
4
5
 
5
6
  /* === Types === */
6
7
 
@@ -33,7 +34,7 @@ function resolve<S extends UnknownSignalRecord>(signals: S): ResolveResult<S> {
33
34
  if (value === UNSET) pending = true
34
35
  else values[key] = value
35
36
  } catch (e) {
36
- errors.push(toError(e))
37
+ errors.push(createError(e))
37
38
  }
38
39
  }
39
40