@zeix/cause-effect 0.17.0 → 0.17.2
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.
- package/.ai-context.md +26 -5
- package/.cursorrules +8 -3
- package/.github/copilot-instructions.md +13 -4
- package/CLAUDE.md +191 -262
- package/README.md +268 -420
- package/archive/collection.ts +23 -25
- package/archive/computed.ts +5 -4
- package/archive/list.ts +21 -28
- package/archive/memo.ts +4 -2
- package/archive/state.ts +2 -1
- package/archive/store.ts +21 -32
- package/archive/task.ts +6 -9
- package/index.dev.js +411 -220
- package/index.js +1 -1
- package/index.ts +25 -8
- package/package.json +1 -1
- package/src/classes/collection.ts +103 -77
- package/src/classes/composite.ts +28 -33
- package/src/classes/computed.ts +90 -31
- package/src/classes/list.ts +39 -33
- package/src/classes/ref.ts +96 -0
- package/src/classes/state.ts +41 -8
- package/src/classes/store.ts +47 -30
- package/src/diff.ts +2 -1
- package/src/effect.ts +19 -9
- package/src/errors.ts +31 -1
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/signal.ts +0 -1
- package/src/system.ts +159 -43
- package/src/util.ts +0 -10
- package/test/collection.test.ts +383 -67
- package/test/computed.test.ts +268 -11
- package/test/effect.test.ts +2 -2
- package/test/list.test.ts +249 -21
- package/test/ref.test.ts +381 -0
- package/test/state.test.ts +13 -13
- package/test/store.test.ts +473 -28
- package/types/index.d.ts +6 -5
- package/types/src/classes/collection.d.ts +27 -12
- package/types/src/classes/composite.d.ts +4 -4
- package/types/src/classes/computed.d.ts +17 -0
- package/types/src/classes/list.d.ts +6 -6
- package/types/src/classes/ref.d.ts +48 -0
- package/types/src/classes/state.d.ts +9 -0
- package/types/src/classes/store.d.ts +4 -4
- package/types/src/effect.d.ts +1 -2
- package/types/src/errors.d.ts +9 -1
- package/types/src/system.d.ts +40 -24
- package/types/src/util.d.ts +1 -3
package/src/classes/computed.ts
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import { isEqual } from '../diff'
|
|
2
2
|
import {
|
|
3
3
|
CircularDependencyError,
|
|
4
|
+
createError,
|
|
5
|
+
InvalidHookError,
|
|
4
6
|
validateCallback,
|
|
5
7
|
validateSignalValue,
|
|
6
8
|
} from '../errors'
|
|
7
9
|
import {
|
|
10
|
+
type Cleanup,
|
|
8
11
|
createWatcher,
|
|
9
12
|
flushPendingReactions,
|
|
13
|
+
HOOK_CLEANUP,
|
|
14
|
+
HOOK_WATCH,
|
|
15
|
+
type HookCallback,
|
|
10
16
|
notifyWatchers,
|
|
11
17
|
subscribeActiveWatcher,
|
|
12
18
|
trackSignalReads,
|
|
19
|
+
UNSET,
|
|
13
20
|
type Watcher,
|
|
21
|
+
type WatchHook,
|
|
14
22
|
} from '../system'
|
|
15
23
|
import {
|
|
16
24
|
isAbortError,
|
|
17
25
|
isAsyncFunction,
|
|
18
26
|
isObjectOfType,
|
|
19
27
|
isSyncFunction,
|
|
20
|
-
toError,
|
|
21
|
-
UNSET,
|
|
22
28
|
} from '../util'
|
|
23
29
|
|
|
24
30
|
/* === Types === */
|
|
@@ -53,7 +59,8 @@ class Memo<T extends {}> {
|
|
|
53
59
|
#error: Error | undefined
|
|
54
60
|
#dirty = true
|
|
55
61
|
#computing = false
|
|
56
|
-
#watcher: Watcher
|
|
62
|
+
#watcher: Watcher | undefined
|
|
63
|
+
#watchHookCallbacks: Set<HookCallback> | undefined
|
|
57
64
|
|
|
58
65
|
/**
|
|
59
66
|
* Create a new memoized signal.
|
|
@@ -64,18 +71,25 @@ class Memo<T extends {}> {
|
|
|
64
71
|
* @throws {InvalidSignalValueError} If the initial value is not valid
|
|
65
72
|
*/
|
|
66
73
|
constructor(callback: MemoCallback<T>, initialValue: T = UNSET) {
|
|
67
|
-
validateCallback(
|
|
68
|
-
validateSignalValue(
|
|
74
|
+
validateCallback(this.constructor.name, callback, isMemoCallback)
|
|
75
|
+
validateSignalValue(this.constructor.name, initialValue)
|
|
69
76
|
|
|
70
77
|
this.#callback = callback
|
|
71
78
|
this.#value = initialValue
|
|
79
|
+
}
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
this.#watcher
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
#getWatcher(): Watcher {
|
|
82
|
+
if (!this.#watcher) {
|
|
83
|
+
// Own watcher: called by notifyWatchers() in upstream signals (push)
|
|
84
|
+
this.#watcher = createWatcher(() => {
|
|
85
|
+
this.#dirty = true
|
|
86
|
+
if (!notifyWatchers(this.#watchers)) this.#watcher?.stop()
|
|
87
|
+
})
|
|
88
|
+
this.#watcher.on(HOOK_CLEANUP, () => {
|
|
89
|
+
this.#watcher = undefined
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
return this.#watcher
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
get [Symbol.toStringTag](): 'Computed' {
|
|
@@ -90,11 +104,12 @@ class Memo<T extends {}> {
|
|
|
90
104
|
* @throws {Error} If an error occurs during computation
|
|
91
105
|
*/
|
|
92
106
|
get(): T {
|
|
93
|
-
subscribeActiveWatcher(this.#watchers)
|
|
107
|
+
subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
|
|
94
108
|
flushPendingReactions()
|
|
95
109
|
|
|
96
110
|
if (this.#dirty) {
|
|
97
|
-
|
|
111
|
+
const watcher = this.#getWatcher()
|
|
112
|
+
trackSignalReads(watcher, () => {
|
|
98
113
|
if (this.#computing) throw new CircularDependencyError('memo')
|
|
99
114
|
|
|
100
115
|
let result: T
|
|
@@ -104,7 +119,7 @@ class Memo<T extends {}> {
|
|
|
104
119
|
} catch (e) {
|
|
105
120
|
// Err track
|
|
106
121
|
this.#value = UNSET
|
|
107
|
-
this.#error =
|
|
122
|
+
this.#error = createError(e)
|
|
108
123
|
this.#computing = false
|
|
109
124
|
return
|
|
110
125
|
}
|
|
@@ -126,6 +141,24 @@ class Memo<T extends {}> {
|
|
|
126
141
|
if (this.#error) throw this.#error
|
|
127
142
|
return this.#value
|
|
128
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Register a callback to be called when HOOK_WATCH is triggered.
|
|
147
|
+
*
|
|
148
|
+
* @param {WatchHook} type - The type of hook to register the callback for; only HOOK_WATCH is supported
|
|
149
|
+
* @param {HookCallback} callback - The callback to register
|
|
150
|
+
* @returns {Cleanup} - A function to unregister the callback
|
|
151
|
+
*/
|
|
152
|
+
on(type: WatchHook, callback: HookCallback): Cleanup {
|
|
153
|
+
if (type === HOOK_WATCH) {
|
|
154
|
+
this.#watchHookCallbacks ||= new Set()
|
|
155
|
+
this.#watchHookCallbacks.add(callback)
|
|
156
|
+
return () => {
|
|
157
|
+
this.#watchHookCallbacks?.delete(callback)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
throw new InvalidHookError(this.constructor.name, type)
|
|
161
|
+
}
|
|
129
162
|
}
|
|
130
163
|
|
|
131
164
|
/**
|
|
@@ -141,8 +174,9 @@ class Task<T extends {}> {
|
|
|
141
174
|
#dirty = true
|
|
142
175
|
#computing = false
|
|
143
176
|
#changed = false
|
|
144
|
-
#watcher: Watcher
|
|
177
|
+
#watcher: Watcher | undefined
|
|
145
178
|
#controller: AbortController | undefined
|
|
179
|
+
#watchHookCallbacks: Set<HookCallback> | undefined
|
|
146
180
|
|
|
147
181
|
/**
|
|
148
182
|
* Create a new task signal for an asynchronous function.
|
|
@@ -153,22 +187,28 @@ class Task<T extends {}> {
|
|
|
153
187
|
* @throws {InvalidSignalValueError} If the initial value is not valid
|
|
154
188
|
*/
|
|
155
189
|
constructor(callback: TaskCallback<T>, initialValue: T = UNSET) {
|
|
156
|
-
validateCallback(
|
|
157
|
-
validateSignalValue(
|
|
190
|
+
validateCallback(this.constructor.name, callback, isTaskCallback)
|
|
191
|
+
validateSignalValue(this.constructor.name, initialValue)
|
|
158
192
|
|
|
159
193
|
this.#callback = callback
|
|
160
194
|
this.#value = initialValue
|
|
195
|
+
}
|
|
161
196
|
|
|
162
|
-
|
|
163
|
-
this.#watcher
|
|
164
|
-
|
|
165
|
-
this.#
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this.#
|
|
171
|
-
|
|
197
|
+
#getWatcher(): Watcher {
|
|
198
|
+
if (!this.#watcher) {
|
|
199
|
+
// Own watcher: called by notifyWatchers() in upstream signals (push)
|
|
200
|
+
this.#watcher = createWatcher(() => {
|
|
201
|
+
this.#dirty = true
|
|
202
|
+
this.#controller?.abort()
|
|
203
|
+
if (!notifyWatchers(this.#watchers)) this.#watcher?.stop()
|
|
204
|
+
})
|
|
205
|
+
this.#watcher.on(HOOK_CLEANUP, () => {
|
|
206
|
+
this.#controller?.abort()
|
|
207
|
+
this.#controller = undefined
|
|
208
|
+
this.#watcher = undefined
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
return this.#watcher
|
|
172
212
|
}
|
|
173
213
|
|
|
174
214
|
get [Symbol.toStringTag](): 'Computed' {
|
|
@@ -183,7 +223,7 @@ class Task<T extends {}> {
|
|
|
183
223
|
* @throws {Error} If an error occurs during computation
|
|
184
224
|
*/
|
|
185
225
|
get(): T {
|
|
186
|
-
subscribeActiveWatcher(this.#watchers)
|
|
226
|
+
subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
|
|
187
227
|
flushPendingReactions()
|
|
188
228
|
|
|
189
229
|
// Functions to update internal state
|
|
@@ -201,7 +241,7 @@ class Task<T extends {}> {
|
|
|
201
241
|
this.#error = undefined
|
|
202
242
|
}
|
|
203
243
|
const err = (e: unknown): undefined => {
|
|
204
|
-
const newError =
|
|
244
|
+
const newError = createError(e)
|
|
205
245
|
this.#changed =
|
|
206
246
|
!this.#error ||
|
|
207
247
|
newError.name !== this.#error.name ||
|
|
@@ -215,11 +255,12 @@ class Task<T extends {}> {
|
|
|
215
255
|
this.#computing = false
|
|
216
256
|
this.#controller = undefined
|
|
217
257
|
fn(arg)
|
|
218
|
-
if (this.#changed
|
|
258
|
+
if (this.#changed && !notifyWatchers(this.#watchers))
|
|
259
|
+
this.#watcher?.stop()
|
|
219
260
|
}
|
|
220
261
|
|
|
221
262
|
const compute = () =>
|
|
222
|
-
trackSignalReads(this.#
|
|
263
|
+
trackSignalReads(this.#getWatcher(), () => {
|
|
223
264
|
if (this.#computing) throw new CircularDependencyError('task')
|
|
224
265
|
this.#changed = false
|
|
225
266
|
|
|
@@ -266,6 +307,24 @@ class Task<T extends {}> {
|
|
|
266
307
|
if (this.#error) throw this.#error
|
|
267
308
|
return this.#value
|
|
268
309
|
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Register a callback to be called when HOOK_WATCH is triggered.
|
|
313
|
+
*
|
|
314
|
+
* @param {WatchHook} type - The type of hook to register the callback for; only HOOK_WATCH is supported
|
|
315
|
+
* @param {HookCallback} callback - The callback to register
|
|
316
|
+
* @returns {Cleanup} - A function to unregister the callback
|
|
317
|
+
*/
|
|
318
|
+
on(type: WatchHook, callback: HookCallback): Cleanup {
|
|
319
|
+
if (type === HOOK_WATCH) {
|
|
320
|
+
this.#watchHookCallbacks ||= new Set()
|
|
321
|
+
this.#watchHookCallbacks.add(callback)
|
|
322
|
+
return () => {
|
|
323
|
+
this.#watchHookCallbacks?.delete(callback)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
throw new InvalidHookError(this.constructor.name, type)
|
|
327
|
+
}
|
|
269
328
|
}
|
|
270
329
|
|
|
271
330
|
/* === Functions === */
|
package/src/classes/list.ts
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { diff, isEqual, type UnknownArray } from '../diff'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DuplicateKeyError,
|
|
4
|
+
InvalidHookError,
|
|
5
|
+
validateSignalValue,
|
|
6
|
+
} from '../errors'
|
|
3
7
|
import {
|
|
4
8
|
type Cleanup,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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,
|
|
9
18
|
notifyWatchers,
|
|
10
19
|
subscribeActiveWatcher,
|
|
20
|
+
triggerHook,
|
|
21
|
+
UNSET,
|
|
11
22
|
type Watcher,
|
|
12
23
|
} from '../system'
|
|
13
|
-
import { isFunction, isNumber, isObjectOfType, isString
|
|
14
|
-
import {
|
|
24
|
+
import { isFunction, isNumber, isObjectOfType, isString } from '../util'
|
|
25
|
+
import { type CollectionCallback, DerivedCollection } from './collection'
|
|
15
26
|
import { Composite } from './composite'
|
|
16
27
|
import { State } from './state'
|
|
17
28
|
|
|
@@ -32,14 +43,12 @@ const TYPE_LIST = 'List' as const
|
|
|
32
43
|
class List<T extends {}> {
|
|
33
44
|
#composite: Composite<Record<string, T>, State<T>>
|
|
34
45
|
#watchers = new Set<Watcher>()
|
|
35
|
-
#
|
|
36
|
-
sort: new Set<Listener<'sort'>>(),
|
|
37
|
-
}
|
|
46
|
+
#hookCallbacks: HookCallbacks = {}
|
|
38
47
|
#order: string[] = []
|
|
39
48
|
#generateKey: (item: T) => string
|
|
40
49
|
|
|
41
50
|
constructor(initialValue: T[], keyConfig?: KeyConfig<T>) {
|
|
42
|
-
validateSignalValue(
|
|
51
|
+
validateSignalValue(TYPE_LIST, initialValue, Array.isArray)
|
|
43
52
|
|
|
44
53
|
let keyCounter = 0
|
|
45
54
|
this.#generateKey = isString(keyConfig)
|
|
@@ -51,7 +60,7 @@ class List<T extends {}> {
|
|
|
51
60
|
this.#composite = new Composite<ArrayToRecord<T[]>, State<T>>(
|
|
52
61
|
this.#toRecord(initialValue),
|
|
53
62
|
(key: string, value: unknown): value is T => {
|
|
54
|
-
validateSignalValue(
|
|
63
|
+
validateSignalValue(`${TYPE_LIST} for key "${key}"`, value)
|
|
55
64
|
return true
|
|
56
65
|
},
|
|
57
66
|
value => new State(value),
|
|
@@ -87,7 +96,7 @@ class List<T extends {}> {
|
|
|
87
96
|
return TYPE_LIST
|
|
88
97
|
}
|
|
89
98
|
|
|
90
|
-
get [Symbol.isConcatSpreadable]():
|
|
99
|
+
get [Symbol.isConcatSpreadable](): true {
|
|
91
100
|
return true
|
|
92
101
|
}
|
|
93
102
|
|
|
@@ -99,12 +108,12 @@ class List<T extends {}> {
|
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
get length(): number {
|
|
102
|
-
subscribeActiveWatcher(this.#watchers)
|
|
111
|
+
subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH])
|
|
103
112
|
return this.#order.length
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
get(): T[] {
|
|
107
|
-
subscribeActiveWatcher(this.#watchers)
|
|
116
|
+
subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH])
|
|
108
117
|
return this.#value
|
|
109
118
|
}
|
|
110
119
|
|
|
@@ -198,7 +207,7 @@ class List<T extends {}> {
|
|
|
198
207
|
if (!isEqual(this.#order, newOrder)) {
|
|
199
208
|
this.#order = newOrder
|
|
200
209
|
notifyWatchers(this.#watchers)
|
|
201
|
-
|
|
210
|
+
triggerHook(this.#hookCallbacks.sort, this.#order)
|
|
202
211
|
}
|
|
203
212
|
}
|
|
204
213
|
|
|
@@ -258,32 +267,29 @@ class List<T extends {}> {
|
|
|
258
267
|
return Object.values(remove)
|
|
259
268
|
}
|
|
260
269
|
|
|
261
|
-
on
|
|
262
|
-
if (type
|
|
263
|
-
this.#
|
|
264
|
-
|
|
265
|
-
|
|
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)
|
|
266
279
|
}
|
|
267
|
-
|
|
268
|
-
// For other types, delegate to the composite
|
|
269
|
-
return this.#composite.on(
|
|
270
|
-
type,
|
|
271
|
-
listener as Listener<
|
|
272
|
-
keyof Pick<Notifications, 'add' | 'remove' | 'change'>
|
|
273
|
-
>,
|
|
274
|
-
)
|
|
280
|
+
throw new InvalidHookError(TYPE_LIST, type)
|
|
275
281
|
}
|
|
276
282
|
|
|
277
283
|
deriveCollection<R extends {}>(
|
|
278
284
|
callback: (sourceValue: T) => R,
|
|
279
|
-
):
|
|
285
|
+
): DerivedCollection<R, T>
|
|
280
286
|
deriveCollection<R extends {}>(
|
|
281
287
|
callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
|
|
282
|
-
):
|
|
288
|
+
): DerivedCollection<R, T>
|
|
283
289
|
deriveCollection<R extends {}>(
|
|
284
290
|
callback: CollectionCallback<R, T>,
|
|
285
|
-
):
|
|
286
|
-
return new
|
|
291
|
+
): DerivedCollection<R, T> {
|
|
292
|
+
return new DerivedCollection(this, callback)
|
|
287
293
|
}
|
|
288
294
|
}
|
|
289
295
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type Guard, InvalidHookError, validateSignalValue } from '../errors'
|
|
2
|
+
import {
|
|
3
|
+
type Cleanup,
|
|
4
|
+
HOOK_WATCH,
|
|
5
|
+
type HookCallback,
|
|
6
|
+
notifyWatchers,
|
|
7
|
+
subscribeActiveWatcher,
|
|
8
|
+
type Watcher,
|
|
9
|
+
type WatchHook,
|
|
10
|
+
} from '../system'
|
|
11
|
+
import { isObjectOfType } from '../util'
|
|
12
|
+
|
|
13
|
+
/* === Constants === */
|
|
14
|
+
|
|
15
|
+
const TYPE_REF = 'Ref'
|
|
16
|
+
|
|
17
|
+
/* === Class === */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a new ref signal.
|
|
21
|
+
*
|
|
22
|
+
* @since 0.17.1
|
|
23
|
+
*/
|
|
24
|
+
class Ref<T extends {}> {
|
|
25
|
+
#watchers = new Set<Watcher>()
|
|
26
|
+
#value: T
|
|
27
|
+
#watchHookCallbacks: Set<HookCallback> | undefined
|
|
28
|
+
|
|
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)
|
|
39
|
+
|
|
40
|
+
this.#value = value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get [Symbol.toStringTag](): string {
|
|
44
|
+
return TYPE_REF
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the value of the ref signal.
|
|
49
|
+
*
|
|
50
|
+
* @returns {T} - Object reference
|
|
51
|
+
*/
|
|
52
|
+
get(): T {
|
|
53
|
+
subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
|
|
54
|
+
|
|
55
|
+
return this.#value
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Notify watchers of relevant changes in the external reference.
|
|
60
|
+
*/
|
|
61
|
+
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)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* === Functions === */
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if the provided value is a Ref instance
|
|
88
|
+
*
|
|
89
|
+
* @since 0.17.1
|
|
90
|
+
* @param {unknown} value - Value to check
|
|
91
|
+
* @returns {boolean} - True if the value is a Ref instance, false otherwise
|
|
92
|
+
*/
|
|
93
|
+
const isRef = /*#__PURE__*/ <T extends {}>(value: unknown): value is Ref<T> =>
|
|
94
|
+
isObjectOfType(value, TYPE_REF)
|
|
95
|
+
|
|
96
|
+
export { TYPE_REF, Ref, isRef }
|
package/src/classes/state.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { isEqual } from '../diff'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
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,
|
|
13
|
+
UNSET,
|
|
14
|
+
type Watcher,
|
|
15
|
+
type WatchHook,
|
|
16
|
+
} from '../system'
|
|
17
|
+
import { isObjectOfType } from '../util'
|
|
5
18
|
|
|
6
19
|
/* === Constants === */
|
|
7
20
|
|
|
@@ -17,6 +30,7 @@ const TYPE_STATE = 'State' as const
|
|
|
17
30
|
class State<T extends {}> {
|
|
18
31
|
#watchers = new Set<Watcher>()
|
|
19
32
|
#value: T
|
|
33
|
+
#watchHookCallbacks: Set<HookCallback> | undefined
|
|
20
34
|
|
|
21
35
|
/**
|
|
22
36
|
* Create a new state signal.
|
|
@@ -26,7 +40,7 @@ class State<T extends {}> {
|
|
|
26
40
|
* @throws {InvalidSignalValueError} - If the initial value is invalid
|
|
27
41
|
*/
|
|
28
42
|
constructor(initialValue: T) {
|
|
29
|
-
validateSignalValue(
|
|
43
|
+
validateSignalValue(TYPE_STATE, initialValue)
|
|
30
44
|
|
|
31
45
|
this.#value = initialValue
|
|
32
46
|
}
|
|
@@ -41,7 +55,8 @@ class State<T extends {}> {
|
|
|
41
55
|
* @returns {T} - Current value of the state
|
|
42
56
|
*/
|
|
43
57
|
get(): T {
|
|
44
|
-
subscribeActiveWatcher(this.#watchers)
|
|
58
|
+
subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
|
|
59
|
+
|
|
45
60
|
return this.#value
|
|
46
61
|
}
|
|
47
62
|
|
|
@@ -54,11 +69,11 @@ class State<T extends {}> {
|
|
|
54
69
|
* @throws {InvalidSignalValueError} - If the initial value is invalid
|
|
55
70
|
*/
|
|
56
71
|
set(newValue: T): void {
|
|
57
|
-
validateSignalValue(
|
|
72
|
+
validateSignalValue(TYPE_STATE, newValue)
|
|
58
73
|
|
|
59
74
|
if (isEqual(this.#value, newValue)) return
|
|
60
75
|
this.#value = newValue
|
|
61
|
-
notifyWatchers(this.#watchers)
|
|
76
|
+
if (this.#watchers.size) notifyWatchers(this.#watchers)
|
|
62
77
|
|
|
63
78
|
// Setting to UNSET clears the watchers so the signal can be garbage collected
|
|
64
79
|
if (UNSET === this.#value) this.#watchers.clear()
|
|
@@ -74,10 +89,28 @@ class State<T extends {}> {
|
|
|
74
89
|
* @throws {InvalidSignalValueError} - If the initial value is invalid
|
|
75
90
|
*/
|
|
76
91
|
update(updater: (oldValue: T) => T): void {
|
|
77
|
-
validateCallback(
|
|
92
|
+
validateCallback(`${TYPE_STATE} update`, updater)
|
|
78
93
|
|
|
79
94
|
this.set(updater(this.#value))
|
|
80
95
|
}
|
|
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
|
+
}
|
|
81
114
|
}
|
|
82
115
|
|
|
83
116
|
/* === Functions === */
|
package/src/classes/store.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { diff, type UnknownRecord } from '../diff'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DuplicateKeyError,
|
|
4
|
+
InvalidHookError,
|
|
5
|
+
validateSignalValue,
|
|
6
|
+
} from '../errors'
|
|
3
7
|
import { createMutableSignal, type MutableSignal, type Signal } from '../signal'
|
|
4
8
|
import {
|
|
5
9
|
type Cleanup,
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
HOOK_ADD,
|
|
11
|
+
HOOK_CHANGE,
|
|
12
|
+
HOOK_REMOVE,
|
|
13
|
+
HOOK_WATCH,
|
|
14
|
+
type Hook,
|
|
15
|
+
type HookCallback,
|
|
16
|
+
isHandledHook,
|
|
8
17
|
notifyWatchers,
|
|
9
18
|
subscribeActiveWatcher,
|
|
19
|
+
UNSET,
|
|
10
20
|
type Watcher,
|
|
11
21
|
} from '../system'
|
|
12
|
-
import { isFunction, isObjectOfType, isRecord, isSymbol
|
|
22
|
+
import { isFunction, isObjectOfType, isRecord, isSymbol } from '../util'
|
|
13
23
|
import { Composite } from './composite'
|
|
14
24
|
import type { List } from './list'
|
|
15
25
|
import type { State } from './state'
|
|
@@ -33,6 +43,7 @@ const TYPE_STORE = 'Store' as const
|
|
|
33
43
|
class BaseStore<T extends UnknownRecord> {
|
|
34
44
|
#composite: Composite<T, Signal<T[keyof T] & {}>>
|
|
35
45
|
#watchers = new Set<Watcher>()
|
|
46
|
+
#watchHookCallbacks: Set<HookCallback> | undefined
|
|
36
47
|
|
|
37
48
|
/**
|
|
38
49
|
* Create a new store with the given initial value.
|
|
@@ -42,7 +53,7 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
42
53
|
* @throws {InvalidSignalValueError} - If the initial value is not an object
|
|
43
54
|
*/
|
|
44
55
|
constructor(initialValue: T) {
|
|
45
|
-
validateSignalValue(
|
|
56
|
+
validateSignalValue(TYPE_STORE, initialValue, isRecord)
|
|
46
57
|
|
|
47
58
|
this.#composite = new Composite<T, Signal<T[keyof T] & {}>>(
|
|
48
59
|
initialValue,
|
|
@@ -50,7 +61,7 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
50
61
|
key: K,
|
|
51
62
|
value: unknown,
|
|
52
63
|
): value is T[K] & {} => {
|
|
53
|
-
validateSignalValue(
|
|
64
|
+
validateSignalValue(`${TYPE_STORE} for key "${key}"`, value)
|
|
54
65
|
return true
|
|
55
66
|
},
|
|
56
67
|
value => createMutableSignal(value),
|
|
@@ -80,24 +91,6 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
80
91
|
yield [key, signal as MutableSignal<T[keyof T] & {}>]
|
|
81
92
|
}
|
|
82
93
|
|
|
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
94
|
keys(): IterableIterator<string> {
|
|
102
95
|
return this.#composite.signals.keys()
|
|
103
96
|
}
|
|
@@ -122,13 +115,31 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
122
115
|
: State<T[K] & {}> | undefined
|
|
123
116
|
}
|
|
124
117
|
|
|
118
|
+
get(): T {
|
|
119
|
+
subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks)
|
|
120
|
+
return this.#value
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
set(newValue: T): void {
|
|
124
|
+
if (UNSET === newValue) {
|
|
125
|
+
this.#composite.clear()
|
|
126
|
+
notifyWatchers(this.#watchers)
|
|
127
|
+
this.#watchers.clear()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const oldValue = this.#value
|
|
132
|
+
const changed = this.#composite.change(diff(oldValue, newValue))
|
|
133
|
+
if (changed) notifyWatchers(this.#watchers)
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
update(fn: (oldValue: T) => T): void {
|
|
126
137
|
this.set(fn(this.get()))
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
add<K extends keyof T & string>(key: K, value: T[K]): K {
|
|
130
141
|
if (this.#composite.signals.has(key))
|
|
131
|
-
throw new DuplicateKeyError(
|
|
142
|
+
throw new DuplicateKeyError(TYPE_STORE, key, value)
|
|
132
143
|
|
|
133
144
|
const ok = this.#composite.add(key, value)
|
|
134
145
|
if (ok) notifyWatchers(this.#watchers)
|
|
@@ -140,11 +151,17 @@ class BaseStore<T extends UnknownRecord> {
|
|
|
140
151
|
if (ok) notifyWatchers(this.#watchers)
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
on
|
|
144
|
-
type
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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)
|
|
148
165
|
}
|
|
149
166
|
}
|
|
150
167
|
|