@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,29 +1,20 @@
1
- import { diff, isEqual, type UnknownArray } from '../diff'
1
+ import { type DiffResult, diff, isEqual, type UnknownArray } from '../diff'
2
2
  import {
3
3
  DuplicateKeyError,
4
- InvalidHookError,
4
+ guardMutableSignal,
5
5
  validateSignalValue,
6
6
  } from '../errors'
7
7
  import {
8
- type Cleanup,
9
- HOOK_ADD,
10
- HOOK_CHANGE,
11
- HOOK_REMOVE,
12
- HOOK_SORT,
13
- HOOK_WATCH,
14
- type Hook,
15
- type HookCallback,
16
- type HookCallbacks,
17
- isHandledHook,
18
- notifyWatchers,
19
- subscribeActiveWatcher,
20
- triggerHook,
8
+ batch,
9
+ notifyOf,
10
+ registerWatchCallbacks,
11
+ type SignalOptions,
12
+ subscribeTo,
21
13
  UNSET,
22
- type Watcher,
14
+ unsubscribeAllFrom,
23
15
  } from '../system'
24
16
  import { isFunction, isNumber, isObjectOfType, isString } from '../util'
25
17
  import { type CollectionCallback, DerivedCollection } from './collection'
26
- import { Composite } from './composite'
27
18
  import { State } from './state'
28
19
 
29
20
  /* === Types === */
@@ -33,6 +24,9 @@ type ArrayToRecord<T extends UnknownArray> = {
33
24
  }
34
25
 
35
26
  type KeyConfig<T> = string | ((item: T) => string)
27
+ type ListOptions<T extends {}> = SignalOptions<T> & {
28
+ keyConfig?: KeyConfig<T>
29
+ }
36
30
 
37
31
  /* === Constants === */
38
32
 
@@ -41,30 +35,39 @@ const TYPE_LIST = 'List' as const
41
35
  /* === Class === */
42
36
 
43
37
  class List<T extends {}> {
44
- #composite: Composite<Record<string, T>, State<T>>
45
- #watchers = new Set<Watcher>()
46
- #hookCallbacks: HookCallbacks = {}
47
- #order: string[] = []
38
+ #signals = new Map<string, State<T>>()
39
+ #keys: string[] = []
48
40
  #generateKey: (item: T) => string
41
+ #validate: (key: string, value: unknown) => value is T
49
42
 
50
- constructor(initialValue: T[], keyConfig?: KeyConfig<T>) {
43
+ constructor(initialValue: T[], options?: ListOptions<T>) {
51
44
  validateSignalValue(TYPE_LIST, initialValue, Array.isArray)
52
45
 
53
46
  let keyCounter = 0
47
+ const keyConfig = options?.keyConfig
54
48
  this.#generateKey = isString(keyConfig)
55
49
  ? () => `${keyConfig}${keyCounter++}`
56
50
  : isFunction<string>(keyConfig)
57
51
  ? (item: T) => keyConfig(item)
58
52
  : () => String(keyCounter++)
59
53
 
60
- this.#composite = new Composite<ArrayToRecord<T[]>, State<T>>(
61
- this.#toRecord(initialValue),
62
- (key: string, value: unknown): value is T => {
63
- validateSignalValue(`${TYPE_LIST} for key "${key}"`, value)
64
- return true
65
- },
66
- value => new State(value),
67
- )
54
+ this.#validate = (key: string, value: unknown): value is T => {
55
+ validateSignalValue(
56
+ `${TYPE_LIST} item for key "${key}"`,
57
+ value,
58
+ options?.guard,
59
+ )
60
+ return true
61
+ }
62
+
63
+ this.#change({
64
+ add: this.#toRecord(initialValue),
65
+ change: {},
66
+ remove: {},
67
+ changed: true,
68
+ })
69
+ if (options?.watched)
70
+ registerWatchCallbacks(this, options.watched, options.unwatched)
68
71
  }
69
72
 
70
73
  // Convert array to record with stable keys
@@ -75,19 +78,66 @@ class List<T extends {}> {
75
78
  const value = array[i]
76
79
  if (value === undefined) continue // Skip sparse array positions
77
80
 
78
- let key = this.#order[i]
81
+ let key = this.#keys[i]
79
82
  if (!key) {
80
83
  key = this.#generateKey(value)
81
- this.#order[i] = key
84
+ this.#keys[i] = key
82
85
  }
83
86
  record[key] = value
84
87
  }
85
88
  return record
86
89
  }
87
90
 
91
+ #add(key: string, value: T) {
92
+ if (!this.#validate(key, value)) return false
93
+
94
+ this.#signals.set(key, new State(value))
95
+ return true
96
+ }
97
+
98
+ #change(changes: DiffResult) {
99
+ // Additions
100
+ if (Object.keys(changes.add).length) {
101
+ for (const key in changes.add) this.#add(key, changes.add[key] as T)
102
+ }
103
+
104
+ // Changes
105
+ if (Object.keys(changes.change).length) {
106
+ batch(() => {
107
+ for (const key in changes.change) {
108
+ const value = changes.change[key]
109
+ if (!this.#validate(key as keyof T & string, value))
110
+ continue
111
+
112
+ const signal = this.#signals.get(key)
113
+ if (
114
+ guardMutableSignal(
115
+ `${TYPE_LIST} item "${key}"`,
116
+ value,
117
+ signal,
118
+ )
119
+ )
120
+ signal.set(value)
121
+ }
122
+ })
123
+ }
124
+
125
+ // Removals
126
+ if (Object.keys(changes.remove).length) {
127
+ for (const key in changes.remove) {
128
+ this.#signals.delete(key)
129
+ const index = this.#keys.indexOf(key)
130
+ if (index !== -1) this.#keys.splice(index, 1)
131
+ }
132
+ this.#keys = this.#keys.filter(() => true)
133
+ }
134
+
135
+ return changes.changed
136
+ }
137
+
88
138
  get #value(): T[] {
89
- return this.#order
90
- .map(key => this.#composite.signals.get(key)?.get())
139
+ return this.#keys
140
+ .map(key => this.#signals.get(key)?.get())
91
141
  .filter(v => v !== undefined) as T[]
92
142
  }
93
143
 
@@ -101,43 +151,35 @@ class List<T extends {}> {
101
151
  }
102
152
 
103
153
  *[Symbol.iterator](): IterableIterator<State<T>> {
104
- for (const key of this.#order) {
105
- const signal = this.#composite.signals.get(key)
154
+ for (const key of this.#keys) {
155
+ const signal = this.#signals.get(key)
106
156
  if (signal) yield signal as State<T>
107
157
  }
108
158
  }
109
159
 
110
160
  get length(): number {
111
- subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH])
112
- return this.#order.length
161
+ subscribeTo(this)
162
+ return this.#keys.length
113
163
  }
114
164
 
115
165
  get(): T[] {
116
- subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH])
166
+ subscribeTo(this)
117
167
  return this.#value
118
168
  }
119
169
 
120
170
  set(newValue: T[]): void {
121
171
  if (UNSET === newValue) {
122
- this.#composite.clear()
123
- notifyWatchers(this.#watchers)
124
- this.#watchers.clear()
172
+ this.#signals.clear()
173
+ notifyOf(this)
174
+ unsubscribeAllFrom(this)
125
175
  return
126
176
  }
127
177
 
128
- const oldValue = this.#value
129
- const changes = diff(this.#toRecord(oldValue), this.#toRecord(newValue))
130
- const removedKeys = Object.keys(changes.remove)
131
-
132
- const changed = this.#composite.change(changes)
133
- if (changed) {
134
- for (const key of removedKeys) {
135
- const index = this.#order.indexOf(key)
136
- if (index !== -1) this.#order.splice(index, 1)
137
- }
138
- this.#order = this.#order.filter(() => true)
139
- notifyWatchers(this.#watchers)
140
- }
178
+ const changes = diff(
179
+ this.#toRecord(this.#value),
180
+ this.#toRecord(newValue),
181
+ )
182
+ if (this.#change(changes)) notifyOf(this)
141
183
  }
142
184
 
143
185
  update(fn: (oldValue: T[]) => T[]): void {
@@ -145,58 +187,53 @@ class List<T extends {}> {
145
187
  }
146
188
 
147
189
  at(index: number): State<T> | undefined {
148
- return this.#composite.signals.get(this.#order[index])
190
+ return this.#signals.get(this.#keys[index])
149
191
  }
150
192
 
151
193
  keys(): IterableIterator<string> {
152
- return this.#order.values()
194
+ subscribeTo(this)
195
+ return this.#keys.values()
153
196
  }
154
197
 
155
198
  byKey(key: string): State<T> | undefined {
156
- return this.#composite.signals.get(key)
199
+ return this.#signals.get(key)
157
200
  }
158
201
 
159
202
  keyAt(index: number): string | undefined {
160
- return this.#order[index]
203
+ return this.#keys[index]
161
204
  }
162
205
 
163
206
  indexOfKey(key: string): number {
164
- return this.#order.indexOf(key)
207
+ return this.#keys.indexOf(key)
165
208
  }
166
209
 
167
210
  add(value: T): string {
168
211
  const key = this.#generateKey(value)
169
- if (this.#composite.signals.has(key))
212
+ if (this.#signals.has(key))
170
213
  throw new DuplicateKeyError('store', key, value)
171
214
 
172
- if (!this.#order.includes(key)) this.#order.push(key)
173
- const ok = this.#composite.add(key, value)
174
- if (ok) notifyWatchers(this.#watchers)
215
+ if (!this.#keys.includes(key)) this.#keys.push(key)
216
+ const ok = this.#add(key, value)
217
+ if (ok) notifyOf(this)
175
218
  return key
176
219
  }
177
220
 
178
221
  remove(keyOrIndex: string | number): void {
179
- const key = isNumber(keyOrIndex) ? this.#order[keyOrIndex] : keyOrIndex
180
- const ok = this.#composite.remove(key)
222
+ const key = isNumber(keyOrIndex) ? this.#keys[keyOrIndex] : keyOrIndex
223
+ const ok = this.#signals.delete(key)
181
224
  if (ok) {
182
225
  const index = isNumber(keyOrIndex)
183
226
  ? keyOrIndex
184
- : this.#order.indexOf(key)
185
- if (index >= 0) this.#order.splice(index, 1)
186
- this.#order = this.#order.filter(() => true)
187
- notifyWatchers(this.#watchers)
227
+ : this.#keys.indexOf(key)
228
+ if (index >= 0) this.#keys.splice(index, 1)
229
+ this.#keys = this.#keys.filter(() => true)
230
+ notifyOf(this)
188
231
  }
189
232
  }
190
233
 
191
234
  sort(compareFn?: (a: T, b: T) => number): void {
192
- const entries = this.#order
193
- .map(
194
- key =>
195
- [key, this.#composite.signals.get(key)?.get()] as [
196
- string,
197
- T,
198
- ],
199
- )
235
+ const entries = this.#keys
236
+ .map(key => [key, this.#signals.get(key)?.get()] as [string, T])
200
237
  .sort(
201
238
  isFunction(compareFn)
202
239
  ? (a, b) => compareFn(a[1], b[1])
@@ -204,15 +241,14 @@ class List<T extends {}> {
204
241
  )
205
242
  const newOrder = entries.map(([key]) => key)
206
243
 
207
- if (!isEqual(this.#order, newOrder)) {
208
- this.#order = newOrder
209
- notifyWatchers(this.#watchers)
210
- triggerHook(this.#hookCallbacks.sort, this.#order)
244
+ if (!isEqual(this.#keys, newOrder)) {
245
+ this.#keys = newOrder
246
+ notifyOf(this)
211
247
  }
212
248
  }
213
249
 
214
250
  splice(start: number, deleteCount?: number, ...items: T[]): T[] {
215
- const length = this.#order.length
251
+ const length = this.#keys.length
216
252
  const actualStart =
217
253
  start < 0 ? Math.max(0, length + start) : Math.min(start, length)
218
254
  const actualDeleteCount = Math.max(
@@ -229,15 +265,15 @@ class List<T extends {}> {
229
265
  // Collect items to delete and their keys
230
266
  for (let i = 0; i < actualDeleteCount; i++) {
231
267
  const index = actualStart + i
232
- const key = this.#order[index]
268
+ const key = this.#keys[index]
233
269
  if (key) {
234
- const signal = this.#composite.signals.get(key)
270
+ const signal = this.#signals.get(key)
235
271
  if (signal) remove[key] = signal.get() as T
236
272
  }
237
273
  }
238
274
 
239
275
  // Build new order: items before splice point
240
- const newOrder = this.#order.slice(0, actualStart)
276
+ const newOrder = this.#keys.slice(0, actualStart)
241
277
 
242
278
  // Add new items
243
279
  for (const item of items) {
@@ -247,49 +283,39 @@ class List<T extends {}> {
247
283
  }
248
284
 
249
285
  // Add items after splice point
250
- newOrder.push(...this.#order.slice(actualStart + actualDeleteCount))
286
+ newOrder.push(...this.#keys.slice(actualStart + actualDeleteCount))
251
287
 
252
288
  const changed = !!(
253
289
  Object.keys(add).length || Object.keys(remove).length
254
290
  )
255
291
 
256
292
  if (changed) {
257
- this.#composite.change({
293
+ this.#change({
258
294
  add,
259
295
  change: {} as Record<string, T>,
260
296
  remove,
261
297
  changed,
262
298
  })
263
- this.#order = newOrder.filter(() => true) // Update order array
264
- notifyWatchers(this.#watchers)
299
+ this.#keys = newOrder.filter(() => true) // Update order array
300
+ notifyOf(this)
265
301
  }
266
302
 
267
303
  return Object.values(remove)
268
304
  }
269
305
 
270
- on(type: Hook, callback: HookCallback): Cleanup {
271
- if (isHandledHook(type, [HOOK_SORT, HOOK_WATCH])) {
272
- this.#hookCallbacks[type] ||= new Set<HookCallback>()
273
- this.#hookCallbacks[type].add(callback)
274
- return () => {
275
- this.#hookCallbacks[type]?.delete(callback)
276
- }
277
- } else if (isHandledHook(type, [HOOK_ADD, HOOK_CHANGE, HOOK_REMOVE])) {
278
- return this.#composite.on(type, callback)
279
- }
280
- throw new InvalidHookError(TYPE_LIST, type)
281
- }
282
-
283
306
  deriveCollection<R extends {}>(
284
307
  callback: (sourceValue: T) => R,
308
+ options?: SignalOptions<R[]>,
285
309
  ): DerivedCollection<R, T>
286
310
  deriveCollection<R extends {}>(
287
311
  callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
312
+ options?: SignalOptions<R[]>,
288
313
  ): DerivedCollection<R, T>
289
314
  deriveCollection<R extends {}>(
290
315
  callback: CollectionCallback<R, T>,
316
+ options?: SignalOptions<R[]>,
291
317
  ): DerivedCollection<R, T> {
292
- return new DerivedCollection(this, callback)
318
+ return new DerivedCollection(this, callback, options)
293
319
  }
294
320
  }
295
321
 
@@ -307,4 +333,11 @@ const isList = <T extends {}>(value: unknown): value is List<T> =>
307
333
 
308
334
  /* === Exports === */
309
335
 
310
- export { isList, List, TYPE_LIST, type ArrayToRecord, type KeyConfig }
336
+ export {
337
+ isList,
338
+ List,
339
+ TYPE_LIST,
340
+ type ArrayToRecord,
341
+ type KeyConfig,
342
+ type ListOptions,
343
+ }
@@ -1,12 +1,9 @@
1
- import { type Guard, InvalidHookError, validateSignalValue } from '../errors'
1
+ import { validateSignalValue } from '../errors'
2
2
  import {
3
- type Cleanup,
4
- HOOK_WATCH,
5
- type HookCallback,
6
- notifyWatchers,
7
- subscribeActiveWatcher,
8
- type Watcher,
9
- type WatchHook,
3
+ notifyOf,
4
+ registerWatchCallbacks,
5
+ type SignalOptions,
6
+ subscribeTo,
10
7
  } from '../system'
11
8
  import { isObjectOfType } from '../util'
12
9
 
@@ -20,24 +17,20 @@ const TYPE_REF = 'Ref'
20
17
  * Create a new ref signal.
21
18
  *
22
19
  * @since 0.17.1
20
+ * @param {T} value - Reference to external object
21
+ * @param {Guard<T>} guard - Optional guard function to validate the value
22
+ * @throws {NullishSignalValueError} - If the value is null or undefined
23
+ * @throws {InvalidSignalValueError} - If the value is invalid
23
24
  */
24
25
  class Ref<T extends {}> {
25
- #watchers = new Set<Watcher>()
26
26
  #value: T
27
- #watchHookCallbacks: Set<HookCallback> | undefined
28
27
 
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)
28
+ constructor(value: T, options?: SignalOptions<T>) {
29
+ validateSignalValue(TYPE_REF, value, options?.guard)
39
30
 
40
31
  this.#value = value
32
+ if (options?.watched)
33
+ registerWatchCallbacks(this, options.watched, options.unwatched)
41
34
  }
42
35
 
43
36
  get [Symbol.toStringTag](): string {
@@ -50,8 +43,7 @@ class Ref<T extends {}> {
50
43
  * @returns {T} - Object reference
51
44
  */
52
45
  get(): T {
53
- subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
54
-
46
+ subscribeTo(this)
55
47
  return this.#value
56
48
  }
57
49
 
@@ -59,25 +51,7 @@ class Ref<T extends {}> {
59
51
  * Notify watchers of relevant changes in the external reference.
60
52
  */
61
53
  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)
54
+ notifyOf(this)
81
55
  }
82
56
  }
83
57
 
@@ -88,7 +62,7 @@ class Ref<T extends {}> {
88
62
  *
89
63
  * @since 0.17.1
90
64
  * @param {unknown} value - Value to check
91
- * @returns {boolean} - True if the value is a Ref instance, false otherwise
65
+ * @returns {boolean} - Whether the value is a Ref instance
92
66
  */
93
67
  const isRef = /*#__PURE__*/ <T extends {}>(value: unknown): value is Ref<T> =>
94
68
  isObjectOfType(value, TYPE_REF)
@@ -1,18 +1,12 @@
1
1
  import { isEqual } from '../diff'
2
+ import { validateCallback, validateSignalValue } from '../errors'
2
3
  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,
4
+ type SignalOptions,
5
+ registerWatchCallbacks,
6
+ notifyOf,
7
+ subscribeTo,
13
8
  UNSET,
14
- type Watcher,
15
- type WatchHook,
9
+ unsubscribeAllFrom,
16
10
  } from '../system'
17
11
  import { isObjectOfType } from '../util'
18
12
 
@@ -26,23 +20,19 @@ const TYPE_STATE = 'State' as const
26
20
  * Create a new state signal.
27
21
  *
28
22
  * @since 0.17.0
23
+ * @param {T} initialValue - Initial value of the state
24
+ * @throws {NullishSignalValueError} - If the initial value is null or undefined
25
+ * @throws {InvalidSignalValueError} - If the initial value is invalid
29
26
  */
30
27
  class State<T extends {}> {
31
- #watchers = new Set<Watcher>()
32
28
  #value: T
33
- #watchHookCallbacks: Set<HookCallback> | undefined
34
29
 
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)
30
+ constructor(initialValue: T, options?: SignalOptions<T>) {
31
+ validateSignalValue(TYPE_STATE, initialValue, options?.guard)
44
32
 
45
33
  this.#value = initialValue
34
+ if (options?.watched)
35
+ registerWatchCallbacks(this, options.watched, options.unwatched)
46
36
  }
47
37
 
48
38
  get [Symbol.toStringTag](): string {
@@ -55,8 +45,7 @@ class State<T extends {}> {
55
45
  * @returns {T} - Current value of the state
56
46
  */
57
47
  get(): T {
58
- subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
59
-
48
+ subscribeTo(this)
60
49
  return this.#value
61
50
  }
62
51
 
@@ -73,10 +62,10 @@ class State<T extends {}> {
73
62
 
74
63
  if (isEqual(this.#value, newValue)) return
75
64
  this.#value = newValue
76
- if (this.#watchers.size) notifyWatchers(this.#watchers)
65
+ notifyOf(this)
77
66
 
78
67
  // Setting to UNSET clears the watchers so the signal can be garbage collected
79
- if (UNSET === this.#value) this.#watchers.clear()
68
+ if (UNSET === this.#value) unsubscribeAllFrom(this)
80
69
  }
81
70
 
82
71
  /**
@@ -93,24 +82,6 @@ class State<T extends {}> {
93
82
 
94
83
  this.set(updater(this.#value))
95
84
  }
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
85
  }
115
86
 
116
87
  /* === Functions === */