@zeix/cause-effect 0.18.0 → 0.18.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.
- package/.ai-context.md +14 -3
- package/.github/copilot-instructions.md +15 -5
- package/ARCHITECTURE.md +15 -13
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +9 -7
- package/README.md +23 -5
- package/context7.json +4 -0
- package/examples/events-sensor.ts +187 -0
- package/examples/selector-sensor.ts +173 -0
- package/index.dev.js +276 -222
- package/index.js +1 -1
- package/index.ts +4 -2
- package/package.json +2 -2
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/skills/changelog-keeper/agents/openai.yaml +4 -0
- package/src/graph.ts +13 -2
- package/src/nodes/collection.ts +166 -128
- package/src/nodes/list.ts +105 -104
- package/src/nodes/memo.ts +31 -3
- package/src/nodes/sensor.ts +27 -17
- package/src/nodes/state.ts +2 -2
- package/src/nodes/store.ts +55 -60
- package/src/nodes/task.ts +31 -3
- package/test/collection.test.ts +40 -51
- package/test/memo.test.ts +194 -0
- package/test/task.test.ts +134 -0
- package/types/index.d.ts +5 -5
- package/types/src/graph.d.ts +12 -2
- package/types/src/nodes/collection.d.ts +12 -7
- package/types/src/nodes/list.d.ts +12 -11
- package/types/src/nodes/memo.d.ts +6 -0
- package/types/src/nodes/sensor.d.ts +15 -9
- package/types/src/nodes/store.d.ts +4 -4
- package/types/src/nodes/task.d.ts +6 -0
- package/COLLECTION_REFACTORING.md +0 -161
package/src/nodes/list.ts
CHANGED
|
@@ -39,7 +39,7 @@ type DiffResult = {
|
|
|
39
39
|
remove: UnknownRecord
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
type KeyConfig<T> = string | ((item: T) => string)
|
|
42
|
+
type KeyConfig<T> = string | ((item: T) => string | undefined)
|
|
43
43
|
|
|
44
44
|
type ListOptions<T extends {}> = {
|
|
45
45
|
keyConfig?: KeyConfig<T>
|
|
@@ -52,8 +52,8 @@ type List<T extends {}> = {
|
|
|
52
52
|
[Symbol.iterator](): IterableIterator<State<T>>
|
|
53
53
|
readonly length: number
|
|
54
54
|
get(): T[]
|
|
55
|
-
set(
|
|
56
|
-
update(fn: (
|
|
55
|
+
set(next: T[]): void
|
|
56
|
+
update(fn: (prev: T[]) => T[]): void
|
|
57
57
|
at(index: number): State<T> | undefined
|
|
58
58
|
keys(): IterableIterator<string>
|
|
59
59
|
byKey(key: string): State<T> | undefined
|
|
@@ -77,19 +77,11 @@ type List<T extends {}> = {
|
|
|
77
77
|
* Checks if two values are equal with cycle detection
|
|
78
78
|
*
|
|
79
79
|
* @since 0.15.0
|
|
80
|
-
* @param
|
|
81
|
-
* @param
|
|
82
|
-
* @param
|
|
83
|
-
* @returns
|
|
80
|
+
* @param a - First value to compare
|
|
81
|
+
* @param b - Second value to compare
|
|
82
|
+
* @param visited - Set to track visited objects for cycle detection
|
|
83
|
+
* @returns Whether the two values are equal
|
|
84
84
|
*/
|
|
85
|
-
|
|
86
|
-
/** Shallow equality check for string arrays */
|
|
87
|
-
function keysEqual(a: string[], b: string[]): boolean {
|
|
88
|
-
if (a.length !== b.length) return false
|
|
89
|
-
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
|
90
|
-
return true
|
|
91
|
-
}
|
|
92
|
-
|
|
93
85
|
function isEqual<T>(a: T, b: T, visited?: WeakSet<object>): boolean {
|
|
94
86
|
// Fast paths
|
|
95
87
|
if (Object.is(a, b)) return true
|
|
@@ -117,9 +109,8 @@ function isEqual<T>(a: T, b: T, visited?: WeakSet<object>): boolean {
|
|
|
117
109
|
const aa = a as unknown[]
|
|
118
110
|
const ba = b as unknown[]
|
|
119
111
|
if (aa.length !== ba.length) return false
|
|
120
|
-
for (let i = 0; i < aa.length; i++)
|
|
112
|
+
for (let i = 0; i < aa.length; i++)
|
|
121
113
|
if (!isEqual(aa[i], ba[i], visited)) return false
|
|
122
|
-
}
|
|
123
114
|
return true
|
|
124
115
|
}
|
|
125
116
|
|
|
@@ -144,23 +135,45 @@ function isEqual<T>(a: T, b: T, visited?: WeakSet<object>): boolean {
|
|
|
144
135
|
}
|
|
145
136
|
}
|
|
146
137
|
|
|
138
|
+
/** Shallow equality check for string arrays */
|
|
139
|
+
function keysEqual(a: string[], b: string[]): boolean {
|
|
140
|
+
if (a.length !== b.length) return false
|
|
141
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getKeyGenerator<T extends {}>(
|
|
146
|
+
keyConfig?: KeyConfig<T>,
|
|
147
|
+
): [(item: T) => string, boolean] {
|
|
148
|
+
let keyCounter = 0
|
|
149
|
+
const contentBased = typeof keyConfig === 'function'
|
|
150
|
+
return [
|
|
151
|
+
typeof keyConfig === 'string'
|
|
152
|
+
? () => `${keyConfig}${keyCounter++}`
|
|
153
|
+
: contentBased
|
|
154
|
+
? (item: T) => keyConfig(item) || String(keyCounter++)
|
|
155
|
+
: () => String(keyCounter++),
|
|
156
|
+
contentBased,
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
|
|
147
160
|
/**
|
|
148
161
|
* Compares two arrays using existing keys and returns differences as a DiffResult.
|
|
149
162
|
* Avoids object conversion by working directly with arrays and keys.
|
|
150
163
|
*
|
|
151
164
|
* @since 0.18.0
|
|
152
|
-
* @param
|
|
153
|
-
* @param
|
|
154
|
-
* @param
|
|
155
|
-
* @param
|
|
156
|
-
* @param
|
|
165
|
+
* @param prev - The old array
|
|
166
|
+
* @param next - The new array
|
|
167
|
+
* @param prevKeys - Current keys array (may be sparse or shorter than oldArray)
|
|
168
|
+
* @param generateKey - Function to generate keys for new items
|
|
169
|
+
* @param contentBased - When true, always use generateKey (content-based keys);
|
|
157
170
|
* when false, reuse positional keys from currentKeys (synthetic keys)
|
|
158
|
-
* @returns
|
|
171
|
+
* @returns The differences in DiffResult format plus updated keys array
|
|
159
172
|
*/
|
|
160
173
|
function diffArrays<T>(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
prev: T[],
|
|
175
|
+
next: T[],
|
|
176
|
+
prevKeys: string[],
|
|
164
177
|
generateKey: (item: T) => string,
|
|
165
178
|
contentBased: boolean,
|
|
166
179
|
): DiffResult & { newKeys: string[] } {
|
|
@@ -168,50 +181,46 @@ function diffArrays<T>(
|
|
|
168
181
|
const add = {} as UnknownRecord
|
|
169
182
|
const change = {} as UnknownRecord
|
|
170
183
|
const remove = {} as UnknownRecord
|
|
171
|
-
const
|
|
184
|
+
const nextKeys: string[] = []
|
|
172
185
|
let changed = false
|
|
173
186
|
|
|
174
187
|
// Build a map of old values by key for quick lookup
|
|
175
|
-
const
|
|
176
|
-
for (let i = 0; i <
|
|
177
|
-
const key =
|
|
178
|
-
if (key &&
|
|
188
|
+
const prevByKey = new Map<string, T>()
|
|
189
|
+
for (let i = 0; i < prev.length; i++) {
|
|
190
|
+
const key = prevKeys[i]
|
|
191
|
+
if (key && prev[i]) prevByKey.set(key, prev[i])
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
// Track which old keys we've seen
|
|
182
195
|
const seenKeys = new Set<string>()
|
|
183
196
|
|
|
184
197
|
// Process new array and build new keys array
|
|
185
|
-
for (let i = 0; i <
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
198
|
+
for (let i = 0; i < next.length; i++) {
|
|
199
|
+
const val = next[i]
|
|
200
|
+
if (val === undefined) continue
|
|
188
201
|
|
|
189
202
|
// Content-based keys: always derive from item; synthetic keys: reuse by position
|
|
190
203
|
const key = contentBased
|
|
191
|
-
? generateKey(
|
|
192
|
-
: (
|
|
204
|
+
? generateKey(val)
|
|
205
|
+
: (prevKeys[i] ?? generateKey(val))
|
|
193
206
|
|
|
194
|
-
if (seenKeys.has(key))
|
|
195
|
-
throw new DuplicateKeyError(TYPE_LIST, key, newValue)
|
|
207
|
+
if (seenKeys.has(key)) throw new DuplicateKeyError(TYPE_LIST, key, val)
|
|
196
208
|
|
|
197
|
-
|
|
209
|
+
nextKeys.push(key)
|
|
198
210
|
seenKeys.add(key)
|
|
199
211
|
|
|
200
212
|
// Check if this key existed before
|
|
201
|
-
if (!
|
|
202
|
-
add[key] =
|
|
213
|
+
if (!prevByKey.has(key)) {
|
|
214
|
+
add[key] = val
|
|
215
|
+
changed = true
|
|
216
|
+
} else if (!isEqual(prevByKey.get(key), val, visited)) {
|
|
217
|
+
change[key] = val
|
|
203
218
|
changed = true
|
|
204
|
-
} else {
|
|
205
|
-
const oldValue = oldByKey.get(key)
|
|
206
|
-
if (!isEqual(oldValue, newValue, visited)) {
|
|
207
|
-
change[key] = newValue
|
|
208
|
-
changed = true
|
|
209
|
-
}
|
|
210
219
|
}
|
|
211
220
|
}
|
|
212
221
|
|
|
213
222
|
// Find removed keys (existed in old but not in new)
|
|
214
|
-
for (const [key] of
|
|
223
|
+
for (const [key] of prevByKey) {
|
|
215
224
|
if (!seenKeys.has(key)) {
|
|
216
225
|
remove[key] = null
|
|
217
226
|
changed = true
|
|
@@ -219,37 +228,29 @@ function diffArrays<T>(
|
|
|
219
228
|
}
|
|
220
229
|
|
|
221
230
|
// Detect reorder even when no values changed
|
|
222
|
-
if (!changed && !keysEqual(
|
|
231
|
+
if (!changed && !keysEqual(prevKeys, nextKeys)) changed = true
|
|
223
232
|
|
|
224
|
-
return { add, change, remove, newKeys, changed }
|
|
233
|
+
return { add, change, remove, newKeys: nextKeys, changed }
|
|
225
234
|
}
|
|
226
235
|
|
|
227
236
|
/**
|
|
228
237
|
* Creates a reactive list with stable keys and per-item reactivity.
|
|
229
238
|
*
|
|
230
239
|
* @since 0.18.0
|
|
231
|
-
* @param
|
|
240
|
+
* @param value - Initial array of items
|
|
232
241
|
* @param options - Optional configuration for key generation and watch lifecycle
|
|
233
242
|
* @returns A List signal
|
|
234
243
|
*/
|
|
235
244
|
function createList<T extends {}>(
|
|
236
|
-
|
|
245
|
+
value: T[],
|
|
237
246
|
options?: ListOptions<T>,
|
|
238
247
|
): List<T> {
|
|
239
|
-
validateSignalValue(TYPE_LIST,
|
|
248
|
+
validateSignalValue(TYPE_LIST, value, Array.isArray)
|
|
240
249
|
|
|
241
250
|
const signals = new Map<string, State<T>>()
|
|
242
251
|
let keys: string[] = []
|
|
243
252
|
|
|
244
|
-
|
|
245
|
-
const keyConfig = options?.keyConfig
|
|
246
|
-
const contentBased = isFunction<string>(keyConfig)
|
|
247
|
-
const generateKey: (item: T) => string =
|
|
248
|
-
typeof keyConfig === 'string'
|
|
249
|
-
? () => `${keyConfig}${keyCounter++}`
|
|
250
|
-
: contentBased
|
|
251
|
-
? (item: T) => keyConfig(item)
|
|
252
|
-
: () => String(keyCounter++)
|
|
253
|
+
const [generateKey, contentBased] = getKeyGenerator(options?.keyConfig)
|
|
253
254
|
|
|
254
255
|
// --- Internal helpers ---
|
|
255
256
|
|
|
@@ -265,7 +266,7 @@ function createList<T extends {}>(
|
|
|
265
266
|
// Mutation methods (add/remove/set/splice) null out sources to force re-establishment.
|
|
266
267
|
const node: MemoNode<T[]> = {
|
|
267
268
|
fn: buildValue,
|
|
268
|
-
value
|
|
269
|
+
value,
|
|
269
270
|
flags: FLAG_DIRTY,
|
|
270
271
|
sources: null,
|
|
271
272
|
sourcesTail: null,
|
|
@@ -278,14 +279,14 @@ function createList<T extends {}>(
|
|
|
278
279
|
const toRecord = (array: T[]): Record<string, T> => {
|
|
279
280
|
const record = {} as Record<string, T>
|
|
280
281
|
for (let i = 0; i < array.length; i++) {
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
282
|
+
const val = array[i]
|
|
283
|
+
if (val === undefined) continue
|
|
283
284
|
let key = keys[i]
|
|
284
285
|
if (!key) {
|
|
285
|
-
key = generateKey(
|
|
286
|
+
key = generateKey(val)
|
|
286
287
|
keys[i] = key
|
|
287
288
|
}
|
|
288
|
-
record[key] =
|
|
289
|
+
record[key] = val
|
|
289
290
|
}
|
|
290
291
|
return record
|
|
291
292
|
}
|
|
@@ -295,9 +296,9 @@ function createList<T extends {}>(
|
|
|
295
296
|
|
|
296
297
|
// Additions
|
|
297
298
|
for (const key in changes.add) {
|
|
298
|
-
const
|
|
299
|
-
validateSignalValue(`${TYPE_LIST} item for key "${key}"`,
|
|
300
|
-
signals.set(key, createState(
|
|
299
|
+
const val = changes.add[key] as T
|
|
300
|
+
validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val)
|
|
301
|
+
signals.set(key, createState(val))
|
|
301
302
|
structural = true
|
|
302
303
|
}
|
|
303
304
|
|
|
@@ -305,13 +306,13 @@ function createList<T extends {}>(
|
|
|
305
306
|
if (Object.keys(changes.change).length) {
|
|
306
307
|
batch(() => {
|
|
307
308
|
for (const key in changes.change) {
|
|
308
|
-
const
|
|
309
|
+
const val = changes.change[key]
|
|
309
310
|
validateSignalValue(
|
|
310
311
|
`${TYPE_LIST} item for key "${key}"`,
|
|
311
|
-
|
|
312
|
+
val,
|
|
312
313
|
)
|
|
313
314
|
const signal = signals.get(key)
|
|
314
|
-
if (signal) signal.set(
|
|
315
|
+
if (signal) signal.set(val as T)
|
|
315
316
|
}
|
|
316
317
|
})
|
|
317
318
|
}
|
|
@@ -332,17 +333,29 @@ function createList<T extends {}>(
|
|
|
332
333
|
return changes.changed
|
|
333
334
|
}
|
|
334
335
|
|
|
336
|
+
const watched = options?.watched
|
|
337
|
+
const subscribe = watched
|
|
338
|
+
? () => {
|
|
339
|
+
if (activeSink) {
|
|
340
|
+
if (!node.sinks) node.stop = watched()
|
|
341
|
+
link(node, activeSink)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
: () => {
|
|
345
|
+
if (activeSink) link(node, activeSink)
|
|
346
|
+
}
|
|
347
|
+
|
|
335
348
|
// --- Initialize ---
|
|
336
|
-
const initRecord = toRecord(
|
|
349
|
+
const initRecord = toRecord(value)
|
|
337
350
|
for (const key in initRecord) {
|
|
338
|
-
const
|
|
339
|
-
validateSignalValue(`${TYPE_LIST} item for key "${key}"`,
|
|
340
|
-
signals.set(key, createState(
|
|
351
|
+
const val = initRecord[key]
|
|
352
|
+
validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val)
|
|
353
|
+
signals.set(key, createState(val))
|
|
341
354
|
}
|
|
342
355
|
|
|
343
356
|
// Starts clean: mutation methods (add/remove/set/splice) explicitly call
|
|
344
357
|
// propagate() + invalidate edges, so refresh() on first get() is not needed.
|
|
345
|
-
node.value =
|
|
358
|
+
node.value = value
|
|
346
359
|
node.flags = 0
|
|
347
360
|
|
|
348
361
|
// --- List object ---
|
|
@@ -358,20 +371,12 @@ function createList<T extends {}>(
|
|
|
358
371
|
},
|
|
359
372
|
|
|
360
373
|
get length() {
|
|
361
|
-
|
|
362
|
-
if (!node.sinks && options?.watched)
|
|
363
|
-
node.stop = options.watched()
|
|
364
|
-
link(node, activeSink)
|
|
365
|
-
}
|
|
374
|
+
subscribe()
|
|
366
375
|
return keys.length
|
|
367
376
|
},
|
|
368
377
|
|
|
369
378
|
get() {
|
|
370
|
-
|
|
371
|
-
if (!node.sinks && options?.watched)
|
|
372
|
-
node.stop = options.watched()
|
|
373
|
-
link(node, activeSink)
|
|
374
|
-
}
|
|
379
|
+
subscribe()
|
|
375
380
|
if (node.sources) {
|
|
376
381
|
// Fast path: edges already established, rebuild value directly
|
|
377
382
|
if (node.flags) {
|
|
@@ -386,12 +391,11 @@ function createList<T extends {}>(
|
|
|
386
391
|
return node.value
|
|
387
392
|
},
|
|
388
393
|
|
|
389
|
-
set(
|
|
390
|
-
const
|
|
391
|
-
node.flags & FLAG_DIRTY ? buildValue() : node.value
|
|
394
|
+
set(next: T[]) {
|
|
395
|
+
const prev = node.flags & FLAG_DIRTY ? buildValue() : node.value
|
|
392
396
|
const changes = diffArrays(
|
|
393
|
-
|
|
394
|
-
|
|
397
|
+
prev,
|
|
398
|
+
next,
|
|
395
399
|
keys,
|
|
396
400
|
generateKey,
|
|
397
401
|
contentBased,
|
|
@@ -399,13 +403,13 @@ function createList<T extends {}>(
|
|
|
399
403
|
if (changes.changed) {
|
|
400
404
|
keys = changes.newKeys
|
|
401
405
|
applyChanges(changes)
|
|
402
|
-
propagate(node as unknown as SinkNode)
|
|
403
406
|
node.flags |= FLAG_DIRTY
|
|
407
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
404
408
|
if (batchDepth === 0) flush()
|
|
405
409
|
}
|
|
406
410
|
},
|
|
407
411
|
|
|
408
|
-
update(fn: (
|
|
412
|
+
update(fn: (prev: T[]) => T[]) {
|
|
409
413
|
list.set(fn(list.get()))
|
|
410
414
|
},
|
|
411
415
|
|
|
@@ -414,11 +418,7 @@ function createList<T extends {}>(
|
|
|
414
418
|
},
|
|
415
419
|
|
|
416
420
|
keys() {
|
|
417
|
-
|
|
418
|
-
if (!node.sinks && options?.watched)
|
|
419
|
-
node.stop = options.watched()
|
|
420
|
-
link(node, activeSink)
|
|
421
|
-
}
|
|
421
|
+
subscribe()
|
|
422
422
|
return keys.values()
|
|
423
423
|
},
|
|
424
424
|
|
|
@@ -443,8 +443,8 @@ function createList<T extends {}>(
|
|
|
443
443
|
signals.set(key, createState(value))
|
|
444
444
|
node.sources = null
|
|
445
445
|
node.sourcesTail = null
|
|
446
|
-
propagate(node as unknown as SinkNode)
|
|
447
446
|
node.flags |= FLAG_DIRTY
|
|
447
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
448
448
|
if (batchDepth === 0) flush()
|
|
449
449
|
return key
|
|
450
450
|
},
|
|
@@ -461,8 +461,8 @@ function createList<T extends {}>(
|
|
|
461
461
|
if (index >= 0) keys.splice(index, 1)
|
|
462
462
|
node.sources = null
|
|
463
463
|
node.sourcesTail = null
|
|
464
|
-
propagate(node as unknown as SinkNode)
|
|
465
464
|
node.flags |= FLAG_DIRTY
|
|
465
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
466
466
|
if (batchDepth === 0) flush()
|
|
467
467
|
}
|
|
468
468
|
},
|
|
@@ -479,8 +479,8 @@ function createList<T extends {}>(
|
|
|
479
479
|
|
|
480
480
|
if (!keysEqual(keys, newOrder)) {
|
|
481
481
|
keys = newOrder
|
|
482
|
-
propagate(node as unknown as SinkNode)
|
|
483
482
|
node.flags |= FLAG_DIRTY
|
|
483
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
484
484
|
if (batchDepth === 0) flush()
|
|
485
485
|
}
|
|
486
486
|
},
|
|
@@ -538,8 +538,8 @@ function createList<T extends {}>(
|
|
|
538
538
|
changed,
|
|
539
539
|
})
|
|
540
540
|
keys = newOrder
|
|
541
|
-
propagate(node as unknown as SinkNode)
|
|
542
541
|
node.flags |= FLAG_DIRTY
|
|
542
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
543
543
|
if (batchDepth === 0) flush()
|
|
544
544
|
}
|
|
545
545
|
|
|
@@ -583,6 +583,7 @@ export {
|
|
|
583
583
|
createList,
|
|
584
584
|
isEqual,
|
|
585
585
|
isList,
|
|
586
|
+
getKeyGenerator,
|
|
586
587
|
keysEqual,
|
|
587
588
|
TYPE_LIST,
|
|
588
589
|
}
|
package/src/nodes/memo.ts
CHANGED
|
@@ -5,12 +5,15 @@ import {
|
|
|
5
5
|
} from '../errors'
|
|
6
6
|
import {
|
|
7
7
|
activeSink,
|
|
8
|
+
batchDepth,
|
|
8
9
|
type ComputedOptions,
|
|
9
|
-
|
|
10
|
+
DEFAULT_EQUALITY,
|
|
10
11
|
FLAG_DIRTY,
|
|
12
|
+
flush,
|
|
11
13
|
link,
|
|
12
14
|
type MemoCallback,
|
|
13
15
|
type MemoNode,
|
|
16
|
+
propagate,
|
|
14
17
|
refresh,
|
|
15
18
|
type SinkNode,
|
|
16
19
|
TYPE_MEMO,
|
|
@@ -49,6 +52,12 @@ type Memo<T extends {}> = {
|
|
|
49
52
|
* @template T - The type of value computed by the memo
|
|
50
53
|
* @param fn - The computation function that receives the previous value
|
|
51
54
|
* @param options - Optional configuration for the memo
|
|
55
|
+
* @param options.value - Optional initial value for reducer patterns
|
|
56
|
+
* @param options.equals - Optional equality function. Defaults to strict equality (`===`)
|
|
57
|
+
* @param options.guard - Optional type guard to validate values
|
|
58
|
+
* @param options.watched - Optional callback invoked when the memo is first watched by an effect.
|
|
59
|
+
* Receives an `invalidate` function to mark the memo dirty and trigger recomputation.
|
|
60
|
+
* Must return a cleanup function called when no effects are watching.
|
|
52
61
|
* @returns A Memo object with a get() method
|
|
53
62
|
*
|
|
54
63
|
* @example
|
|
@@ -90,14 +99,33 @@ function createMemo<T extends {}>(
|
|
|
90
99
|
sourcesTail: null,
|
|
91
100
|
sinks: null,
|
|
92
101
|
sinksTail: null,
|
|
93
|
-
equals: options?.equals ??
|
|
102
|
+
equals: options?.equals ?? DEFAULT_EQUALITY,
|
|
94
103
|
error: undefined,
|
|
104
|
+
stop: undefined,
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
const watched = options?.watched
|
|
108
|
+
const subscribe = watched
|
|
109
|
+
? () => {
|
|
110
|
+
if (activeSink) {
|
|
111
|
+
if (!node.sinks)
|
|
112
|
+
node.stop = watched(() => {
|
|
113
|
+
node.flags |= FLAG_DIRTY
|
|
114
|
+
for (let e = node.sinks; e; e = e.nextSink)
|
|
115
|
+
propagate(e.sink)
|
|
116
|
+
if (batchDepth === 0) flush()
|
|
117
|
+
})
|
|
118
|
+
link(node, activeSink)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
: () => {
|
|
122
|
+
if (activeSink) link(node, activeSink)
|
|
123
|
+
}
|
|
124
|
+
|
|
97
125
|
return {
|
|
98
126
|
[Symbol.toStringTag]: TYPE_MEMO,
|
|
99
127
|
get() {
|
|
100
|
-
|
|
128
|
+
subscribe()
|
|
101
129
|
refresh(node as unknown as SinkNode)
|
|
102
130
|
if (node.error) throw node.error
|
|
103
131
|
validateReadValue(TYPE_MEMO, node.value)
|
package/src/nodes/sensor.ts
CHANGED
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
activeSink,
|
|
8
8
|
type Cleanup,
|
|
9
|
-
|
|
10
|
-
defaultEquals,
|
|
9
|
+
DEFAULT_EQUALITY,
|
|
11
10
|
link,
|
|
11
|
+
type SignalOptions,
|
|
12
12
|
type StateNode,
|
|
13
13
|
setState,
|
|
14
14
|
TYPE_SENSOR,
|
|
@@ -27,7 +27,6 @@ type Sensor<T extends {}> = {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Gets the current value of the sensor.
|
|
30
|
-
* Updates its state value if the sensor is active.
|
|
31
30
|
* When called inside another reactive context, creates a dependency.
|
|
32
31
|
* @returns The sensor value
|
|
33
32
|
* @throws UnsetSignalValueError If the sensor value is still unset when read.
|
|
@@ -42,6 +41,14 @@ type Sensor<T extends {}> = {
|
|
|
42
41
|
* @param set - A function to set the observed value
|
|
43
42
|
* @returns A cleanup function when the sensor stops being watched
|
|
44
43
|
*/
|
|
44
|
+
type SensorOptions<T extends {}> = SignalOptions<T> & {
|
|
45
|
+
/**
|
|
46
|
+
* Optional initial value. Avoids `UnsetSignalValueError` on first read
|
|
47
|
+
* before the watched callback fires.
|
|
48
|
+
*/
|
|
49
|
+
value?: T
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup
|
|
46
53
|
|
|
47
54
|
/* === Exported Functions === */
|
|
@@ -52,12 +59,12 @@ type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup
|
|
|
52
59
|
* no longer watched. This lazy activation pattern ensures resources are only consumed when needed.
|
|
53
60
|
*
|
|
54
61
|
* @since 0.18.0
|
|
55
|
-
* @template T - The type of value
|
|
56
|
-
* @param
|
|
57
|
-
* @param options - Optional
|
|
62
|
+
* @template T - The type of value produced by the sensor
|
|
63
|
+
* @param watched - The callback invoked when the sensor starts being watched, receives a `set` function and returns a cleanup function.
|
|
64
|
+
* @param options - Optional configuration for the sensor.
|
|
58
65
|
* @param options.value - Optional initial value. Avoids `UnsetSignalValueError` on first read
|
|
59
|
-
* before the
|
|
60
|
-
* @param options.equals - Optional equality function. Defaults to
|
|
66
|
+
* before the watched callback fires. Essential for the mutable-object observation pattern.
|
|
67
|
+
* @param options.equals - Optional equality function. Defaults to strict equality (`===`). Use `SKIP_EQUALITY`
|
|
61
68
|
* for mutable objects where the reference stays the same but internal state changes.
|
|
62
69
|
* @param options.guard - Optional type guard to validate values.
|
|
63
70
|
* @returns A read-only sensor signal.
|
|
@@ -87,10 +94,10 @@ type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup
|
|
|
87
94
|
* ```
|
|
88
95
|
*/
|
|
89
96
|
function createSensor<T extends {}>(
|
|
90
|
-
|
|
91
|
-
options?:
|
|
97
|
+
watched: SensorCallback<T>,
|
|
98
|
+
options?: SensorOptions<T>,
|
|
92
99
|
): Sensor<T> {
|
|
93
|
-
validateCallback(TYPE_SENSOR,
|
|
100
|
+
validateCallback(TYPE_SENSOR, watched, isSyncFunction)
|
|
94
101
|
if (options?.value !== undefined)
|
|
95
102
|
validateSignalValue(TYPE_SENSOR, options.value, options?.guard)
|
|
96
103
|
|
|
@@ -98,7 +105,7 @@ function createSensor<T extends {}>(
|
|
|
98
105
|
value: options?.value as T,
|
|
99
106
|
sinks: null,
|
|
100
107
|
sinksTail: null,
|
|
101
|
-
equals: options?.equals ??
|
|
108
|
+
equals: options?.equals ?? DEFAULT_EQUALITY,
|
|
102
109
|
guard: options?.guard,
|
|
103
110
|
stop: undefined,
|
|
104
111
|
}
|
|
@@ -107,11 +114,8 @@ function createSensor<T extends {}>(
|
|
|
107
114
|
[Symbol.toStringTag]: TYPE_SENSOR,
|
|
108
115
|
get(): T {
|
|
109
116
|
if (activeSink) {
|
|
110
|
-
// Start fires before link: synchronous set() inside start updates
|
|
111
|
-
// node.value without propagation (no sinks yet). The activating
|
|
112
|
-
// effect reads the updated value directly after link.
|
|
113
117
|
if (!node.sinks)
|
|
114
|
-
node.stop =
|
|
118
|
+
node.stop = watched((next: T): void => {
|
|
115
119
|
validateSignalValue(TYPE_SENSOR, next, node.guard)
|
|
116
120
|
setState(node, next)
|
|
117
121
|
})
|
|
@@ -136,4 +140,10 @@ function isSensor<T extends {} = unknown & {}>(
|
|
|
136
140
|
return isObjectOfType(value, TYPE_SENSOR)
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
export {
|
|
143
|
+
export {
|
|
144
|
+
createSensor,
|
|
145
|
+
isSensor,
|
|
146
|
+
type Sensor,
|
|
147
|
+
type SensorCallback,
|
|
148
|
+
type SensorOptions,
|
|
149
|
+
}
|
package/src/nodes/state.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateCallback, validateSignalValue } from '../errors'
|
|
2
2
|
import {
|
|
3
3
|
activeSink,
|
|
4
|
-
|
|
4
|
+
DEFAULT_EQUALITY,
|
|
5
5
|
link,
|
|
6
6
|
type SignalOptions,
|
|
7
7
|
type StateNode,
|
|
@@ -88,7 +88,7 @@ function createState<T extends {}>(
|
|
|
88
88
|
value,
|
|
89
89
|
sinks: null,
|
|
90
90
|
sinksTail: null,
|
|
91
|
-
equals: options?.equals ??
|
|
91
|
+
equals: options?.equals ?? DEFAULT_EQUALITY,
|
|
92
92
|
guard: options?.guard,
|
|
93
93
|
}
|
|
94
94
|
|