@zeix/cause-effect 0.16.0 → 0.17.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 (62) hide show
  1. package/.ai-context.md +71 -21
  2. package/.cursorrules +3 -2
  3. package/.github/copilot-instructions.md +59 -13
  4. package/CLAUDE.md +170 -24
  5. package/LICENSE +1 -1
  6. package/README.md +156 -52
  7. package/archive/benchmark.ts +688 -0
  8. package/archive/collection.ts +312 -0
  9. package/{src → archive}/computed.ts +33 -34
  10. package/archive/list.ts +551 -0
  11. package/archive/memo.ts +138 -0
  12. package/archive/state.ts +89 -0
  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 +902 -501
  17. package/index.js +1 -1
  18. package/index.ts +42 -22
  19. package/package.json +1 -1
  20. package/src/classes/collection.ts +272 -0
  21. package/src/classes/composite.ts +176 -0
  22. package/src/classes/computed.ts +333 -0
  23. package/src/classes/list.ts +304 -0
  24. package/src/classes/state.ts +98 -0
  25. package/src/classes/store.ts +210 -0
  26. package/src/diff.ts +28 -52
  27. package/src/effect.ts +9 -9
  28. package/src/errors.ts +50 -25
  29. package/src/signal.ts +58 -41
  30. package/src/system.ts +79 -42
  31. package/src/util.ts +16 -34
  32. package/test/batch.test.ts +15 -17
  33. package/test/benchmark.test.ts +4 -4
  34. package/test/collection.test.ts +796 -0
  35. package/test/computed.test.ts +138 -130
  36. package/test/diff.test.ts +2 -2
  37. package/test/effect.test.ts +36 -35
  38. package/test/list.test.ts +754 -0
  39. package/test/match.test.ts +25 -25
  40. package/test/resolve.test.ts +17 -19
  41. package/test/signal.test.ts +72 -121
  42. package/test/state.test.ts +44 -44
  43. package/test/store.test.ts +344 -1663
  44. package/types/index.d.ts +11 -9
  45. package/types/src/classes/collection.d.ts +32 -0
  46. package/types/src/classes/composite.d.ts +15 -0
  47. package/types/src/classes/computed.d.ts +97 -0
  48. package/types/src/classes/list.d.ts +41 -0
  49. package/types/src/classes/state.d.ts +52 -0
  50. package/types/src/classes/store.d.ts +51 -0
  51. package/types/src/diff.d.ts +8 -12
  52. package/types/src/errors.d.ts +12 -11
  53. package/types/src/signal.d.ts +27 -14
  54. package/types/src/system.d.ts +41 -20
  55. package/types/src/util.d.ts +6 -3
  56. package/src/state.ts +0 -98
  57. package/src/store.ts +0 -525
  58. package/types/src/collection.d.ts +0 -26
  59. package/types/src/computed.d.ts +0 -33
  60. package/types/src/scheduler.d.ts +0 -55
  61. package/types/src/state.d.ts +0 -24
  62. package/types/src/store.d.ts +0 -66
@@ -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,20 +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
- type ArrayToRecord<T extends UnknownArray> = {
9
- [key: string]: T extends Array<infer U extends {}> ? U : never
10
- }
11
- type UnknownRecordOrArray = UnknownRecord | ArrayToRecord<UnknownArray>
12
8
 
13
- type DiffResult<T extends UnknownRecordOrArray = UnknownRecord> = {
9
+ type DiffResult = {
14
10
  changed: boolean
15
- add: Partial<T>
16
- change: Partial<T>
17
- remove: Partial<T>
11
+ add: UnknownRecord
12
+ change: UnknownRecord
13
+ remove: UnknownRecord
18
14
  }
19
15
 
20
16
  /* === Functions === */
@@ -32,14 +28,14 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
32
28
  // Fast paths
33
29
  if (Object.is(a, b)) return true
34
30
  if (typeof a !== typeof b) return false
35
- if (typeof a !== 'object' || a === null || b === null) return false
31
+ if (!isNonNullObject(a) || !isNonNullObject(b)) return false
36
32
 
37
33
  // Cycle detection
38
34
  if (!visited) visited = new WeakSet()
39
35
  if (visited.has(a as object) || visited.has(b as object))
40
36
  throw new CircularDependencyError('isEqual')
41
- visited.add(a as object)
42
- visited.add(b as object)
37
+ visited.add(a)
38
+ visited.add(b)
43
39
 
44
40
  try {
45
41
  if (Array.isArray(a) && Array.isArray(b)) {
@@ -59,14 +55,7 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
59
55
  if (aKeys.length !== bKeys.length) return false
60
56
  for (const key of aKeys) {
61
57
  if (!(key in b)) return false
62
- if (
63
- !isEqual(
64
- (a as Record<string, unknown>)[key],
65
- (b as Record<string, unknown>)[key],
66
- visited,
67
- )
68
- )
69
- return false
58
+ if (!isEqual(a[key], b[key], visited)) return false
70
59
  }
71
60
  return true
72
61
  }
@@ -75,8 +64,8 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
75
64
  // (which would have been caught by Object.is at the beginning)
76
65
  return false
77
66
  } finally {
78
- visited.delete(a as object)
79
- visited.delete(b as object)
67
+ visited.delete(a)
68
+ visited.delete(b)
80
69
  }
81
70
  }
82
71
 
@@ -86,12 +75,9 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
86
75
  * @since 0.15.0
87
76
  * @param {T} oldObj - The old record to compare
88
77
  * @param {T} newObj - The new record to compare
89
- * @returns {DiffResult<T>} The result of the comparison
78
+ * @returns {DiffResult} The result of the comparison
90
79
  */
91
- const diff = <T extends UnknownRecordOrArray>(
92
- oldObj: T,
93
- newObj: T,
94
- ): DiffResult<T> => {
80
+ const diff = <T extends UnknownRecord>(oldObj: T, newObj: T): DiffResult => {
95
81
  // Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
96
82
  const oldValid = isRecordOrArray(oldObj)
97
83
  const newValid = isRecordOrArray(newObj)
@@ -108,9 +94,9 @@ const diff = <T extends UnknownRecordOrArray>(
108
94
 
109
95
  const visited = new WeakSet()
110
96
 
111
- const add: Partial<T> = {}
112
- const change: Partial<T> = {}
113
- const remove: Partial<T> = {}
97
+ const add = {} as UnknownRecord
98
+ const change = {} as UnknownRecord
99
+ const remove = {} as UnknownRecord
114
100
 
115
101
  const oldKeys = Object.keys(oldObj)
116
102
  const newKeys = Object.keys(newObj)
@@ -121,41 +107,31 @@ const diff = <T extends UnknownRecordOrArray>(
121
107
  const newHas = key in newObj
122
108
 
123
109
  if (!oldHas && newHas) {
124
- add[key as keyof T] = newObj[key] as T[keyof T]
110
+ add[key] = newObj[key]
125
111
  continue
126
112
  } else if (oldHas && !newHas) {
127
- remove[key as keyof T] = UNSET
113
+ remove[key] = UNSET
128
114
  continue
129
115
  }
130
116
 
131
- const oldValue = oldObj[key] as T[keyof T]
132
- const newValue = newObj[key] as T[keyof T]
117
+ const oldValue = oldObj[key]
118
+ const newValue = newObj[key]
133
119
 
134
- if (!isEqual(oldValue, newValue, visited))
135
- change[key as keyof T] = newValue
120
+ if (!isEqual(oldValue, newValue, visited)) change[key] = newValue
136
121
  }
137
122
 
138
- const changed =
139
- Object.keys(add).length > 0 ||
140
- Object.keys(change).length > 0 ||
141
- Object.keys(remove).length > 0
142
-
143
123
  return {
144
- changed,
145
124
  add,
146
125
  change,
147
126
  remove,
127
+ changed: !!(
128
+ Object.keys(add).length ||
129
+ Object.keys(change).length ||
130
+ Object.keys(remove).length
131
+ ),
148
132
  }
149
133
  }
150
134
 
151
135
  /* === Exports === */
152
136
 
153
- export {
154
- type ArrayToRecord,
155
- type DiffResult,
156
- diff,
157
- isEqual,
158
- type UnknownRecord,
159
- type UnknownArray,
160
- type UnknownRecordOrArray,
161
- }
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,6 @@
1
+ import { isMutableSignal, type MutableSignal } from './signal'
2
+ import { isFunction, isSymbol, UNSET, valueString } from './util'
3
+
1
4
  class CircularDependencyError extends Error {
2
5
  constructor(where: string) {
3
6
  super(`Circular dependency detected in ${where}`)
@@ -5,16 +8,27 @@ class CircularDependencyError extends Error {
5
8
  }
6
9
  }
7
10
 
11
+ class DuplicateKeyError extends Error {
12
+ constructor(where: string, key: string, value?: unknown) {
13
+ super(
14
+ `Could not add ${where} key "${key}"${
15
+ value ? ` with value ${valueString(value)}` : ''
16
+ } because it already exists`,
17
+ )
18
+ this.name = 'DuplicateKeyError'
19
+ }
20
+ }
21
+
8
22
  class InvalidCallbackError extends TypeError {
9
- constructor(where: string, value: string) {
10
- super(`Invalid ${where} callback ${value}`)
23
+ constructor(where: string, value: unknown) {
24
+ super(`Invalid ${where} callback ${valueString(value)}`)
11
25
  this.name = 'InvalidCallbackError'
12
26
  }
13
27
  }
14
28
 
15
29
  class InvalidSignalValueError extends TypeError {
16
- constructor(where: string, value: string) {
17
- super(`Invalid signal value ${value} in ${where}`)
30
+ constructor(where: string, value: unknown) {
31
+ super(`Invalid signal value ${valueString(value)} in ${where}`)
18
32
  this.name = 'InvalidSignalValueError'
19
33
  }
20
34
  }
@@ -26,39 +40,50 @@ class NullishSignalValueError extends TypeError {
26
40
  }
27
41
  }
28
42
 
29
- class StoreKeyExistsError extends Error {
30
- constructor(key: string, value: string) {
43
+ class ReadonlySignalError extends Error {
44
+ constructor(what: string, value: unknown) {
31
45
  super(
32
- `Could not add store key "${key}" with value ${value} because it already exists`,
46
+ `Could not set ${what} to ${valueString(value)} because signal is read-only`,
33
47
  )
34
- this.name = 'StoreKeyExistsError'
48
+ this.name = 'ReadonlySignalError'
35
49
  }
36
50
  }
37
51
 
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
- }
52
+ const validateCallback = (
53
+ where: string,
54
+ value: unknown,
55
+ guard: (value: unknown) => boolean = isFunction,
56
+ ): void => {
57
+ if (!guard(value)) throw new InvalidCallbackError(where, value)
45
58
  }
46
59
 
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
- }
60
+ const validateSignalValue = (
61
+ where: string,
62
+ value: unknown,
63
+ guard: (value: unknown) => boolean = () =>
64
+ !(isSymbol(value) && value !== UNSET) || isFunction(value),
65
+ ): void => {
66
+ if (value == null) throw new NullishSignalValueError(where)
67
+ if (!guard(value)) throw new InvalidSignalValueError(where, value)
68
+ }
69
+
70
+ const guardMutableSignal = <T extends {}>(
71
+ what: string,
72
+ value: unknown,
73
+ signal: unknown,
74
+ ): signal is MutableSignal<T> => {
75
+ if (!isMutableSignal(signal)) throw new ReadonlySignalError(what, value)
76
+ return true
54
77
  }
55
78
 
56
79
  export {
57
80
  CircularDependencyError,
81
+ DuplicateKeyError,
58
82
  InvalidCallbackError,
59
83
  InvalidSignalValueError,
60
84
  NullishSignalValueError,
61
- StoreKeyExistsError,
62
- StoreKeyRangeError,
63
- StoreKeyReadonlyError,
85
+ ReadonlySignalError,
86
+ validateCallback,
87
+ validateSignalValue,
88
+ guardMutableSignal,
64
89
  }