@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/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(newValue: T[]): void
56
- update(fn: (oldValue: T[]) => T[]): void
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 {T} a - First value to compare
81
- * @param {T} b - Second value to compare
82
- * @param {WeakSet<object>} visited - Set to track visited objects for cycle detection
83
- * @returns {boolean} Whether the two values are equal
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 {T[]} oldArray - The old array
153
- * @param {T[]} newArray - The new array
154
- * @param {string[]} currentKeys - Current keys array (may be sparse or shorter than oldArray)
155
- * @param {(item: T) => string} generateKey - Function to generate keys for new items
156
- * @param {boolean} contentBased - When true, always use generateKey (content-based keys);
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 {DiffResult & { newKeys: string[] }} The differences in DiffResult format plus updated keys array
172
+ * @returns The differences in DiffResult format plus updated keys array
159
173
  */
160
174
  function diffArrays<T>(
161
- oldArray: T[],
162
- newArray: T[],
163
- currentKeys: string[],
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 newKeys: string[] = []
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 oldByKey = new Map<string, T>()
176
- for (let i = 0; i < oldArray.length; i++) {
177
- const key = currentKeys[i]
178
- if (key && oldArray[i]) oldByKey.set(key, oldArray[i])
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 < newArray.length; i++) {
186
- const newValue = newArray[i]
187
- if (newValue === undefined) continue
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(newValue)
192
- : (currentKeys[i] ?? generateKey(newValue))
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
- newKeys.push(key)
210
+ nextKeys.push(key)
198
211
  seenKeys.add(key)
199
212
 
200
213
  // Check if this key existed before
201
- if (!oldByKey.has(key)) {
202
- add[key] = newValue
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 oldByKey) {
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(currentKeys, newKeys)) changed = true
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 initialValue - Initial array of items
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
- initialValue: T[],
246
+ value: T[],
237
247
  options?: ListOptions<T>,
238
248
  ): List<T> {
239
- validateSignalValue(TYPE_LIST, initialValue, Array.isArray)
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
- let keyCounter = 0
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 (add/remove/set/splice) null out sources to force re-establishment.
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: initialValue,
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 value = array[i]
282
- if (value === undefined) continue
283
+ const val = array[i]
284
+ if (val === undefined) continue
283
285
  let key = keys[i]
284
286
  if (!key) {
285
- key = generateKey(value)
287
+ key = generateKey(val)
286
288
  keys[i] = key
287
289
  }
288
- record[key] = value
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 value = changes.add[key] as T
299
- validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value)
300
- signals.set(key, createState(value))
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 value = changes.change[key]
310
+ const val = changes.change[key]
309
311
  validateSignalValue(
310
312
  `${TYPE_LIST} item for key "${key}"`,
311
- value,
313
+ val,
312
314
  )
313
315
  const signal = signals.get(key)
314
- if (signal) signal.set(value as T)
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(initialValue)
347
+ const initRecord = toRecord(value)
337
348
  for (const key in initRecord) {
338
- const value = initRecord[key]
339
- validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value)
340
- signals.set(key, createState(value))
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 = initialValue
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
- if (activeSink) {
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
- if (activeSink) {
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
- node.flags = FLAG_CLEAN
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(newValue: T[]) {
390
- const currentValue =
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
- currentValue,
394
- newValue,
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: (oldValue: T[]) => T[]) {
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
- if (activeSink) {
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.sources = null
445
- node.sourcesTail = null
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.sources = null
463
- node.sourcesTail = null
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
- defaultEquals,
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 ?? defaultEquals,
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
- if (activeSink) link(node, activeSink)
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)
@@ -6,9 +6,9 @@ import {
6
6
  import {
7
7
  activeSink,
8
8
  type Cleanup,
9
- type ComputedOptions,
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 stored in the state
56
- * @param start - The callback function that starts the sensor and returns a cleanup function.
57
- * @param options - Optional options for the sensor.
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 start callback fires. Essential for the mutable-object observation pattern.
60
- * @param options.equals - Optional equality function. Defaults to `Object.is`. Use `SKIP_EQUALITY`
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
- start: SensorCallback<T>,
91
- options?: ComputedOptions<T>,
97
+ watched: SensorCallback<T>,
98
+ options?: SensorOptions<T>,
92
99
  ): Sensor<T> {
93
- validateCallback(TYPE_SENSOR, start, isSyncFunction)
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 ?? defaultEquals,
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 = start((next: T): void => {
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 { createSensor, isSensor, type Sensor, type SensorCallback }
143
+ export {
144
+ createSensor,
145
+ isSensor,
146
+ type Sensor,
147
+ type SensorCallback,
148
+ type SensorOptions,
149
+ }
@@ -1,7 +1,7 @@
1
1
  import { validateCallback, validateSignalValue } from '../errors'
2
2
  import {
3
3
  activeSink,
4
- defaultEquals,
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 ?? defaultEquals,
91
+ equals: options?.equals ?? DEFAULT_EQUALITY,
92
92
  guard: options?.guard,
93
93
  }
94
94