@zeix/cause-effect 0.18.0 → 0.18.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 +14 -3
- package/.github/copilot-instructions.md +15 -5
- package/ARCHITECTURE.md +43 -19
- package/CHANGELOG.md +43 -0
- package/CLAUDE.md +33 -7
- package/GUIDE.md +1 -1
- package/README.md +36 -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 +28 -5
- package/src/nodes/collection.ts +185 -133
- package/src/nodes/list.ts +121 -116
- 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 +71 -72
- package/src/nodes/task.ts +31 -3
- package/test/collection.test.ts +40 -51
- package/test/list.test.ts +192 -0
- 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
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type Cleanup,
|
|
11
11
|
FLAG_CLEAN,
|
|
12
12
|
FLAG_DIRTY,
|
|
13
|
+
FLAG_RELINK,
|
|
13
14
|
flush,
|
|
14
15
|
link,
|
|
15
16
|
type MemoNode,
|
|
@@ -39,7 +40,7 @@ type DiffResult = {
|
|
|
39
40
|
remove: UnknownRecord
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
type KeyConfig<T> = string | ((item: T) => string)
|
|
43
|
+
type KeyConfig<T> = string | ((item: T) => string | undefined)
|
|
43
44
|
|
|
44
45
|
type ListOptions<T extends {}> = {
|
|
45
46
|
keyConfig?: KeyConfig<T>
|
|
@@ -52,8 +53,8 @@ type List<T extends {}> = {
|
|
|
52
53
|
[Symbol.iterator](): IterableIterator<State<T>>
|
|
53
54
|
readonly length: number
|
|
54
55
|
get(): T[]
|
|
55
|
-
set(
|
|
56
|
-
update(fn: (
|
|
56
|
+
set(next: T[]): void
|
|
57
|
+
update(fn: (prev: T[]) => T[]): void
|
|
57
58
|
at(index: number): State<T> | undefined
|
|
58
59
|
keys(): IterableIterator<string>
|
|
59
60
|
byKey(key: string): State<T> | undefined
|
|
@@ -77,19 +78,11 @@ type List<T extends {}> = {
|
|
|
77
78
|
* Checks if two values are equal with cycle detection
|
|
78
79
|
*
|
|
79
80
|
* @since 0.15.0
|
|
80
|
-
* @param
|
|
81
|
-
* @param
|
|
82
|
-
* @param
|
|
83
|
-
* @returns
|
|
81
|
+
* @param a - First value to compare
|
|
82
|
+
* @param b - Second value to compare
|
|
83
|
+
* @param visited - Set to track visited objects for cycle detection
|
|
84
|
+
* @returns Whether the two values are equal
|
|
84
85
|
*/
|
|
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
86
|
function isEqual<T>(a: T, b: T, visited?: WeakSet<object>): boolean {
|
|
94
87
|
// Fast paths
|
|
95
88
|
if (Object.is(a, b)) return true
|
|
@@ -117,9 +110,8 @@ function isEqual<T>(a: T, b: T, visited?: WeakSet<object>): boolean {
|
|
|
117
110
|
const aa = a as unknown[]
|
|
118
111
|
const ba = b as unknown[]
|
|
119
112
|
if (aa.length !== ba.length) return false
|
|
120
|
-
for (let i = 0; i < aa.length; i++)
|
|
113
|
+
for (let i = 0; i < aa.length; i++)
|
|
121
114
|
if (!isEqual(aa[i], ba[i], visited)) return false
|
|
122
|
-
}
|
|
123
115
|
return true
|
|
124
116
|
}
|
|
125
117
|
|
|
@@ -144,23 +136,45 @@ function isEqual<T>(a: T, b: T, visited?: WeakSet<object>): boolean {
|
|
|
144
136
|
}
|
|
145
137
|
}
|
|
146
138
|
|
|
139
|
+
/** Shallow equality check for string arrays */
|
|
140
|
+
function keysEqual(a: string[], b: string[]): boolean {
|
|
141
|
+
if (a.length !== b.length) return false
|
|
142
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
|
143
|
+
return true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getKeyGenerator<T extends {}>(
|
|
147
|
+
keyConfig?: KeyConfig<T>,
|
|
148
|
+
): [(item: T) => string, boolean] {
|
|
149
|
+
let keyCounter = 0
|
|
150
|
+
const contentBased = typeof keyConfig === 'function'
|
|
151
|
+
return [
|
|
152
|
+
typeof keyConfig === 'string'
|
|
153
|
+
? () => `${keyConfig}${keyCounter++}`
|
|
154
|
+
: contentBased
|
|
155
|
+
? (item: T) => keyConfig(item) || String(keyCounter++)
|
|
156
|
+
: () => String(keyCounter++),
|
|
157
|
+
contentBased,
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
|
|
147
161
|
/**
|
|
148
162
|
* Compares two arrays using existing keys and returns differences as a DiffResult.
|
|
149
163
|
* Avoids object conversion by working directly with arrays and keys.
|
|
150
164
|
*
|
|
151
165
|
* @since 0.18.0
|
|
152
|
-
* @param
|
|
153
|
-
* @param
|
|
154
|
-
* @param
|
|
155
|
-
* @param
|
|
156
|
-
* @param
|
|
166
|
+
* @param prev - The old array
|
|
167
|
+
* @param next - The new array
|
|
168
|
+
* @param prevKeys - Current keys array (may be sparse or shorter than oldArray)
|
|
169
|
+
* @param generateKey - Function to generate keys for new items
|
|
170
|
+
* @param contentBased - When true, always use generateKey (content-based keys);
|
|
157
171
|
* when false, reuse positional keys from currentKeys (synthetic keys)
|
|
158
|
-
* @returns
|
|
172
|
+
* @returns The differences in DiffResult format plus updated keys array
|
|
159
173
|
*/
|
|
160
174
|
function diffArrays<T>(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
175
|
+
prev: T[],
|
|
176
|
+
next: T[],
|
|
177
|
+
prevKeys: string[],
|
|
164
178
|
generateKey: (item: T) => string,
|
|
165
179
|
contentBased: boolean,
|
|
166
180
|
): DiffResult & { newKeys: string[] } {
|
|
@@ -168,50 +182,46 @@ function diffArrays<T>(
|
|
|
168
182
|
const add = {} as UnknownRecord
|
|
169
183
|
const change = {} as UnknownRecord
|
|
170
184
|
const remove = {} as UnknownRecord
|
|
171
|
-
const
|
|
185
|
+
const nextKeys: string[] = []
|
|
172
186
|
let changed = false
|
|
173
187
|
|
|
174
188
|
// 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 &&
|
|
189
|
+
const prevByKey = new Map<string, T>()
|
|
190
|
+
for (let i = 0; i < prev.length; i++) {
|
|
191
|
+
const key = prevKeys[i]
|
|
192
|
+
if (key && prev[i]) prevByKey.set(key, prev[i])
|
|
179
193
|
}
|
|
180
194
|
|
|
181
195
|
// Track which old keys we've seen
|
|
182
196
|
const seenKeys = new Set<string>()
|
|
183
197
|
|
|
184
198
|
// Process new array and build new keys array
|
|
185
|
-
for (let i = 0; i <
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
199
|
+
for (let i = 0; i < next.length; i++) {
|
|
200
|
+
const val = next[i]
|
|
201
|
+
if (val === undefined) continue
|
|
188
202
|
|
|
189
203
|
// Content-based keys: always derive from item; synthetic keys: reuse by position
|
|
190
204
|
const key = contentBased
|
|
191
|
-
? generateKey(
|
|
192
|
-
: (
|
|
205
|
+
? generateKey(val)
|
|
206
|
+
: (prevKeys[i] ?? generateKey(val))
|
|
193
207
|
|
|
194
|
-
if (seenKeys.has(key))
|
|
195
|
-
throw new DuplicateKeyError(TYPE_LIST, key, newValue)
|
|
208
|
+
if (seenKeys.has(key)) throw new DuplicateKeyError(TYPE_LIST, key, val)
|
|
196
209
|
|
|
197
|
-
|
|
210
|
+
nextKeys.push(key)
|
|
198
211
|
seenKeys.add(key)
|
|
199
212
|
|
|
200
213
|
// Check if this key existed before
|
|
201
|
-
if (!
|
|
202
|
-
add[key] =
|
|
214
|
+
if (!prevByKey.has(key)) {
|
|
215
|
+
add[key] = val
|
|
216
|
+
changed = true
|
|
217
|
+
} else if (!isEqual(prevByKey.get(key), val, visited)) {
|
|
218
|
+
change[key] = val
|
|
203
219
|
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
220
|
}
|
|
211
221
|
}
|
|
212
222
|
|
|
213
223
|
// Find removed keys (existed in old but not in new)
|
|
214
|
-
for (const [key] of
|
|
224
|
+
for (const [key] of prevByKey) {
|
|
215
225
|
if (!seenKeys.has(key)) {
|
|
216
226
|
remove[key] = null
|
|
217
227
|
changed = true
|
|
@@ -219,37 +229,29 @@ function diffArrays<T>(
|
|
|
219
229
|
}
|
|
220
230
|
|
|
221
231
|
// Detect reorder even when no values changed
|
|
222
|
-
if (!changed && !keysEqual(
|
|
232
|
+
if (!changed && !keysEqual(prevKeys, nextKeys)) changed = true
|
|
223
233
|
|
|
224
|
-
return { add, change, remove, newKeys, changed }
|
|
234
|
+
return { add, change, remove, newKeys: nextKeys, changed }
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
/**
|
|
228
238
|
* Creates a reactive list with stable keys and per-item reactivity.
|
|
229
239
|
*
|
|
230
240
|
* @since 0.18.0
|
|
231
|
-
* @param
|
|
241
|
+
* @param value - Initial array of items
|
|
232
242
|
* @param options - Optional configuration for key generation and watch lifecycle
|
|
233
243
|
* @returns A List signal
|
|
234
244
|
*/
|
|
235
245
|
function createList<T extends {}>(
|
|
236
|
-
|
|
246
|
+
value: T[],
|
|
237
247
|
options?: ListOptions<T>,
|
|
238
248
|
): List<T> {
|
|
239
|
-
validateSignalValue(TYPE_LIST,
|
|
249
|
+
validateSignalValue(TYPE_LIST, value, Array.isArray)
|
|
240
250
|
|
|
241
251
|
const signals = new Map<string, State<T>>()
|
|
242
252
|
let keys: string[] = []
|
|
243
253
|
|
|
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++)
|
|
254
|
+
const [generateKey, contentBased] = getKeyGenerator(options?.keyConfig)
|
|
253
255
|
|
|
254
256
|
// --- Internal helpers ---
|
|
255
257
|
|
|
@@ -262,10 +264,10 @@ function createList<T extends {}>(
|
|
|
262
264
|
// Structural tracking node — not a general-purpose Memo.
|
|
263
265
|
// On first get(): refresh() establishes edges from child signals.
|
|
264
266
|
// On subsequent get(): untrack(buildValue) rebuilds without re-linking.
|
|
265
|
-
// Mutation methods
|
|
267
|
+
// Mutation methods set FLAG_RELINK to force re-establishment on next read.
|
|
266
268
|
const node: MemoNode<T[]> = {
|
|
267
269
|
fn: buildValue,
|
|
268
|
-
value
|
|
270
|
+
value,
|
|
269
271
|
flags: FLAG_DIRTY,
|
|
270
272
|
sources: null,
|
|
271
273
|
sourcesTail: null,
|
|
@@ -278,14 +280,14 @@ function createList<T extends {}>(
|
|
|
278
280
|
const toRecord = (array: T[]): Record<string, T> => {
|
|
279
281
|
const record = {} as Record<string, T>
|
|
280
282
|
for (let i = 0; i < array.length; i++) {
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
+
const val = array[i]
|
|
284
|
+
if (val === undefined) continue
|
|
283
285
|
let key = keys[i]
|
|
284
286
|
if (!key) {
|
|
285
|
-
key = generateKey(
|
|
287
|
+
key = generateKey(val)
|
|
286
288
|
keys[i] = key
|
|
287
289
|
}
|
|
288
|
-
record[key] =
|
|
290
|
+
record[key] = val
|
|
289
291
|
}
|
|
290
292
|
return record
|
|
291
293
|
}
|
|
@@ -295,9 +297,9 @@ function createList<T extends {}>(
|
|
|
295
297
|
|
|
296
298
|
// Additions
|
|
297
299
|
for (const key in changes.add) {
|
|
298
|
-
const
|
|
299
|
-
validateSignalValue(`${TYPE_LIST} item for key "${key}"`,
|
|
300
|
-
signals.set(key, createState(
|
|
300
|
+
const val = changes.add[key] as T
|
|
301
|
+
validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val)
|
|
302
|
+
signals.set(key, createState(val))
|
|
301
303
|
structural = true
|
|
302
304
|
}
|
|
303
305
|
|
|
@@ -305,13 +307,13 @@ function createList<T extends {}>(
|
|
|
305
307
|
if (Object.keys(changes.change).length) {
|
|
306
308
|
batch(() => {
|
|
307
309
|
for (const key in changes.change) {
|
|
308
|
-
const
|
|
310
|
+
const val = changes.change[key]
|
|
309
311
|
validateSignalValue(
|
|
310
312
|
`${TYPE_LIST} item for key "${key}"`,
|
|
311
|
-
|
|
313
|
+
val,
|
|
312
314
|
)
|
|
313
315
|
const signal = signals.get(key)
|
|
314
|
-
if (signal) signal.set(
|
|
316
|
+
if (signal) signal.set(val as T)
|
|
315
317
|
}
|
|
316
318
|
})
|
|
317
319
|
}
|
|
@@ -324,25 +326,34 @@ function createList<T extends {}>(
|
|
|
324
326
|
structural = true
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
if (structural)
|
|
328
|
-
node.sources = null
|
|
329
|
-
node.sourcesTail = null
|
|
330
|
-
}
|
|
329
|
+
if (structural) node.flags |= FLAG_RELINK
|
|
331
330
|
|
|
332
331
|
return changes.changed
|
|
333
332
|
}
|
|
334
333
|
|
|
334
|
+
const watched = options?.watched
|
|
335
|
+
const subscribe = watched
|
|
336
|
+
? () => {
|
|
337
|
+
if (activeSink) {
|
|
338
|
+
if (!node.sinks) node.stop = watched()
|
|
339
|
+
link(node, activeSink)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
: () => {
|
|
343
|
+
if (activeSink) link(node, activeSink)
|
|
344
|
+
}
|
|
345
|
+
|
|
335
346
|
// --- Initialize ---
|
|
336
|
-
const initRecord = toRecord(
|
|
347
|
+
const initRecord = toRecord(value)
|
|
337
348
|
for (const key in initRecord) {
|
|
338
|
-
const
|
|
339
|
-
validateSignalValue(`${TYPE_LIST} item for key "${key}"`,
|
|
340
|
-
signals.set(key, createState(
|
|
349
|
+
const val = initRecord[key]
|
|
350
|
+
validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val)
|
|
351
|
+
signals.set(key, createState(val))
|
|
341
352
|
}
|
|
342
353
|
|
|
343
354
|
// Starts clean: mutation methods (add/remove/set/splice) explicitly call
|
|
344
355
|
// propagate() + invalidate edges, so refresh() on first get() is not needed.
|
|
345
|
-
node.value =
|
|
356
|
+
node.value = value
|
|
346
357
|
node.flags = 0
|
|
347
358
|
|
|
348
359
|
// --- List object ---
|
|
@@ -358,25 +369,27 @@ function createList<T extends {}>(
|
|
|
358
369
|
},
|
|
359
370
|
|
|
360
371
|
get length() {
|
|
361
|
-
|
|
362
|
-
if (!node.sinks && options?.watched)
|
|
363
|
-
node.stop = options.watched()
|
|
364
|
-
link(node, activeSink)
|
|
365
|
-
}
|
|
372
|
+
subscribe()
|
|
366
373
|
return keys.length
|
|
367
374
|
},
|
|
368
375
|
|
|
369
376
|
get() {
|
|
370
|
-
|
|
371
|
-
if (!node.sinks && options?.watched)
|
|
372
|
-
node.stop = options.watched()
|
|
373
|
-
link(node, activeSink)
|
|
374
|
-
}
|
|
377
|
+
subscribe()
|
|
375
378
|
if (node.sources) {
|
|
376
379
|
// Fast path: edges already established, rebuild value directly
|
|
377
380
|
if (node.flags) {
|
|
381
|
+
const relink = node.flags & FLAG_RELINK
|
|
378
382
|
node.value = untrack(buildValue)
|
|
379
|
-
|
|
383
|
+
if (relink) {
|
|
384
|
+
// Structural mutation added/removed child signals —
|
|
385
|
+
// tracked recompute so link() adds new edges and
|
|
386
|
+
// trimSources() removes stale ones without orphaning.
|
|
387
|
+
node.flags = FLAG_DIRTY
|
|
388
|
+
refresh(node as unknown as SinkNode)
|
|
389
|
+
if (node.error) throw node.error
|
|
390
|
+
} else {
|
|
391
|
+
node.flags = FLAG_CLEAN
|
|
392
|
+
}
|
|
380
393
|
}
|
|
381
394
|
} else {
|
|
382
395
|
// First access: use refresh() to establish child → list edges
|
|
@@ -386,12 +399,11 @@ function createList<T extends {}>(
|
|
|
386
399
|
return node.value
|
|
387
400
|
},
|
|
388
401
|
|
|
389
|
-
set(
|
|
390
|
-
const
|
|
391
|
-
node.flags & FLAG_DIRTY ? buildValue() : node.value
|
|
402
|
+
set(next: T[]) {
|
|
403
|
+
const prev = node.flags & FLAG_DIRTY ? buildValue() : node.value
|
|
392
404
|
const changes = diffArrays(
|
|
393
|
-
|
|
394
|
-
|
|
405
|
+
prev,
|
|
406
|
+
next,
|
|
395
407
|
keys,
|
|
396
408
|
generateKey,
|
|
397
409
|
contentBased,
|
|
@@ -399,13 +411,13 @@ function createList<T extends {}>(
|
|
|
399
411
|
if (changes.changed) {
|
|
400
412
|
keys = changes.newKeys
|
|
401
413
|
applyChanges(changes)
|
|
402
|
-
propagate(node as unknown as SinkNode)
|
|
403
414
|
node.flags |= FLAG_DIRTY
|
|
415
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
404
416
|
if (batchDepth === 0) flush()
|
|
405
417
|
}
|
|
406
418
|
},
|
|
407
419
|
|
|
408
|
-
update(fn: (
|
|
420
|
+
update(fn: (prev: T[]) => T[]) {
|
|
409
421
|
list.set(fn(list.get()))
|
|
410
422
|
},
|
|
411
423
|
|
|
@@ -414,11 +426,7 @@ function createList<T extends {}>(
|
|
|
414
426
|
},
|
|
415
427
|
|
|
416
428
|
keys() {
|
|
417
|
-
|
|
418
|
-
if (!node.sinks && options?.watched)
|
|
419
|
-
node.stop = options.watched()
|
|
420
|
-
link(node, activeSink)
|
|
421
|
-
}
|
|
429
|
+
subscribe()
|
|
422
430
|
return keys.values()
|
|
423
431
|
},
|
|
424
432
|
|
|
@@ -441,10 +449,8 @@ function createList<T extends {}>(
|
|
|
441
449
|
if (!keys.includes(key)) keys.push(key)
|
|
442
450
|
validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value)
|
|
443
451
|
signals.set(key, createState(value))
|
|
444
|
-
node.
|
|
445
|
-
node.
|
|
446
|
-
propagate(node as unknown as SinkNode)
|
|
447
|
-
node.flags |= FLAG_DIRTY
|
|
452
|
+
node.flags |= FLAG_DIRTY | FLAG_RELINK
|
|
453
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
448
454
|
if (batchDepth === 0) flush()
|
|
449
455
|
return key
|
|
450
456
|
},
|
|
@@ -459,10 +465,8 @@ function createList<T extends {}>(
|
|
|
459
465
|
? keyOrIndex
|
|
460
466
|
: keys.indexOf(key)
|
|
461
467
|
if (index >= 0) keys.splice(index, 1)
|
|
462
|
-
node.
|
|
463
|
-
node.
|
|
464
|
-
propagate(node as unknown as SinkNode)
|
|
465
|
-
node.flags |= FLAG_DIRTY
|
|
468
|
+
node.flags |= FLAG_DIRTY | FLAG_RELINK
|
|
469
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
466
470
|
if (batchDepth === 0) flush()
|
|
467
471
|
}
|
|
468
472
|
},
|
|
@@ -479,8 +483,8 @@ function createList<T extends {}>(
|
|
|
479
483
|
|
|
480
484
|
if (!keysEqual(keys, newOrder)) {
|
|
481
485
|
keys = newOrder
|
|
482
|
-
propagate(node as unknown as SinkNode)
|
|
483
486
|
node.flags |= FLAG_DIRTY
|
|
487
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
484
488
|
if (batchDepth === 0) flush()
|
|
485
489
|
}
|
|
486
490
|
},
|
|
@@ -538,8 +542,8 @@ function createList<T extends {}>(
|
|
|
538
542
|
changed,
|
|
539
543
|
})
|
|
540
544
|
keys = newOrder
|
|
541
|
-
propagate(node as unknown as SinkNode)
|
|
542
545
|
node.flags |= FLAG_DIRTY
|
|
546
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
543
547
|
if (batchDepth === 0) flush()
|
|
544
548
|
}
|
|
545
549
|
|
|
@@ -583,6 +587,7 @@ export {
|
|
|
583
587
|
createList,
|
|
584
588
|
isEqual,
|
|
585
589
|
isList,
|
|
590
|
+
getKeyGenerator,
|
|
586
591
|
keysEqual,
|
|
587
592
|
TYPE_LIST,
|
|
588
593
|
}
|
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
|
|