@planet-matrix/mobius-model 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +21 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -2
  6. package/dist/index.js.map +12 -6
  7. package/dist/reactor/index.d.ts +3 -0
  8. package/dist/reactor/index.d.ts.map +1 -0
  9. package/dist/reactor/reactor-core/flags.d.ts.map +1 -0
  10. package/dist/reactor/reactor-core/index.d.ts.map +1 -0
  11. package/dist/reactor/reactor-core/primitive.d.ts +276 -0
  12. package/dist/reactor/reactor-core/primitive.d.ts.map +1 -0
  13. package/dist/{signal/signal-core → reactor/reactor-core}/reactive-system.d.ts +102 -22
  14. package/dist/reactor/reactor-core/reactive-system.d.ts.map +1 -0
  15. package/dist/reactor/reactor-operators/branch.d.ts +19 -0
  16. package/dist/reactor/reactor-operators/branch.d.ts.map +1 -0
  17. package/dist/reactor/reactor-operators/convert.d.ts +30 -0
  18. package/dist/reactor/reactor-operators/convert.d.ts.map +1 -0
  19. package/dist/reactor/reactor-operators/create.d.ts +26 -0
  20. package/dist/reactor/reactor-operators/create.d.ts.map +1 -0
  21. package/dist/reactor/reactor-operators/filter.d.ts +269 -0
  22. package/dist/reactor/reactor-operators/filter.d.ts.map +1 -0
  23. package/dist/reactor/reactor-operators/index.d.ts +8 -0
  24. package/dist/reactor/reactor-operators/index.d.ts.map +1 -0
  25. package/dist/reactor/reactor-operators/join.d.ts +48 -0
  26. package/dist/reactor/reactor-operators/join.d.ts.map +1 -0
  27. package/dist/reactor/reactor-operators/map.d.ts +165 -0
  28. package/dist/reactor/reactor-operators/map.d.ts.map +1 -0
  29. package/dist/reactor/reactor-operators/utility.d.ts +48 -0
  30. package/dist/reactor/reactor-operators/utility.d.ts.map +1 -0
  31. package/package.json +9 -12
  32. package/src/index.ts +1 -1
  33. package/src/reactor/README.md +18 -0
  34. package/src/reactor/index.ts +2 -0
  35. package/src/reactor/reactor-core/primitive.ts +1046 -0
  36. package/src/{signal/signal-core → reactor/reactor-core}/reactive-system.ts +392 -93
  37. package/src/reactor/reactor-operators/branch.ts +66 -0
  38. package/src/reactor/reactor-operators/convert.ts +70 -0
  39. package/src/reactor/reactor-operators/create.ts +66 -0
  40. package/src/reactor/reactor-operators/filter.ts +988 -0
  41. package/src/reactor/reactor-operators/index.ts +7 -0
  42. package/src/reactor/reactor-operators/join.ts +174 -0
  43. package/src/reactor/reactor-operators/map.ts +599 -0
  44. package/src/reactor/reactor-operators/utility.ts +102 -0
  45. package/tests/unit/{signal/computed.spec.ts → reactor/alien-signals-computed.spec.ts} +15 -10
  46. package/tests/unit/reactor/alien-signals-effect-scope.spec.ts +86 -0
  47. package/tests/unit/reactor/alien-signals-effect.spec.ts +395 -0
  48. package/tests/unit/reactor/alien-signals-topology.spec.ts +361 -0
  49. package/tests/unit/reactor/alien-signals-trigger.spec.ts +75 -0
  50. package/tests/unit/reactor/alien-signals-untrack.spec.ts +91 -0
  51. package/tests/unit/reactor/preact-signal.spec.ts +73 -0
  52. package/tests/unit/reactor/reactor-core.spec.ts +219 -0
  53. package/tests/unit/reactor/reactor-operators-branch.spec.ts +33 -0
  54. package/tests/unit/reactor/reactor-operators-convert.spec.ts +31 -0
  55. package/tests/unit/reactor/reactor-operators-create.spec.ts +47 -0
  56. package/tests/unit/reactor/reactor-operators-filter.spec.ts +604 -0
  57. package/tests/unit/reactor/reactor-operators-join.spec.ts +94 -0
  58. package/tests/unit/reactor/reactor-operators-map.spec.ts +327 -0
  59. package/tests/unit/reactor/reactor-operators-utility.spec.ts +55 -0
  60. package/dist/signal/index.d.ts +0 -3
  61. package/dist/signal/index.d.ts.map +0 -1
  62. package/dist/signal/signal-core/flags.d.ts.map +0 -1
  63. package/dist/signal/signal-core/index.d.ts.map +0 -1
  64. package/dist/signal/signal-core/primitive.d.ts +0 -67
  65. package/dist/signal/signal-core/primitive.d.ts.map +0 -1
  66. package/dist/signal/signal-core/reactive-system.d.ts.map +0 -1
  67. package/dist/signal/signal-operators/index.d.ts +0 -4
  68. package/dist/signal/signal-operators/index.d.ts.map +0 -1
  69. package/src/signal/index.ts +0 -2
  70. package/src/signal/signal-core/README.md +0 -4
  71. package/src/signal/signal-core/primitive.ts +0 -275
  72. package/src/signal/signal-operators/index.ts +0 -19
  73. package/tests/unit/signal/effect.spec.ts +0 -108
  74. /package/dist/{signal/signal-core → reactor/reactor-core}/flags.d.ts +0 -0
  75. /package/dist/{signal/signal-core → reactor/reactor-core}/index.d.ts +0 -0
  76. /package/src/{signal/signal-core → reactor/reactor-core}/flags.ts +0 -0
  77. /package/src/{signal/signal-core → reactor/reactor-core}/index.ts +0 -0
@@ -0,0 +1,599 @@
1
+ import type { AnyValueReactor, Signal, ValueOfValueReactor, ValueReactor } from "../reactor-core/index.ts"
2
+
3
+ import { castValueInitializer } from "./utility.ts"
4
+ import { effect, signal } from "../reactor-core/index.ts"
5
+
6
+ export interface TapOptions<R extends AnyValueReactor> {
7
+ target: R;
8
+ tapper?: ((value: ValueOfValueReactor<R>) => void) | undefined;
9
+ }
10
+ /**
11
+ * Taps into the value changes of a ValueReactor without modifying its value.
12
+ */
13
+ export const tap = <R extends AnyValueReactor>(
14
+ options: TapOptions<R>
15
+ ): R => {
16
+ const { target, tapper = (value: ValueOfValueReactor<R>): void => console.log(value) } = options;
17
+
18
+ effect(() => {
19
+ // oxlint-disable-next-line no-unsafe-type-assertion
20
+ const value = target.get() as ValueOfValueReactor<R>
21
+ tapper(value)
22
+ })
23
+
24
+ return target
25
+ }
26
+
27
+ export interface WithHistoryOptions<V> {
28
+ target: ValueReactor<V>;
29
+ }
30
+ /**
31
+ * Creates a signal that maintains a history of values from a ValueReactor.
32
+ * The history is stored in an array, with each new value appended to the end.
33
+ *
34
+ * Note: This implementation does not limit the size of the history array. Use
35
+ * with caution to avoid excessive memory usage.
36
+ */
37
+ export const withHistory = <V>(
38
+ options: WithHistoryOptions<V>
39
+ ): Signal<V[]> => {
40
+ const { target } = options;
41
+
42
+ const result = signal(castValueInitializer<V[]>(), {
43
+ onDispose: () => {
44
+ eTarget.dispose()
45
+ }
46
+ })
47
+
48
+ const history: V[] = []
49
+ const eTarget = effect(() => {
50
+ history.push(target.get())
51
+ result.set([...history])
52
+ })
53
+
54
+ return result
55
+ }
56
+
57
+ export interface BufferByCountOptions<V> {
58
+ target: ValueReactor<V>;
59
+ count: number;
60
+ }
61
+ /**
62
+ * Buffers values from a ValueReactor and emits them as an array
63
+ * when the number of buffered values reaches the specified count.
64
+ */
65
+ export const bufferByCount = <V>(
66
+ options: BufferByCountOptions<V>
67
+ ): Signal<V[]> => {
68
+ const { target, count } = options;
69
+
70
+ if (count <= 0) {
71
+ throw new Error("bufferByCount operator requires count to be greater than 0")
72
+ }
73
+
74
+ const result = signal<V[]>(() => [], {
75
+ onDispose: () => {
76
+ eTarget.dispose()
77
+ }
78
+ })
79
+
80
+ const bufferedValues: V[] = []
81
+ const eTarget = effect(() => {
82
+ bufferedValues.push(target.get())
83
+ if (bufferedValues.length >= count) {
84
+ result.set([...bufferedValues])
85
+ bufferedValues.length = 0
86
+ }
87
+ })
88
+
89
+ return result
90
+ }
91
+
92
+ export interface BufferByTimeOptions<V> {
93
+ target: ValueReactor<V>;
94
+ time: number;
95
+ }
96
+ /**
97
+ * Buffers values from a ValueReactor and emits them as an array
98
+ * at specified time intervals.
99
+ */
100
+ export const bufferByTime = <V>(
101
+ options: BufferByTimeOptions<V>
102
+ ): Signal<V[]> => {
103
+ const { target, time } = options;
104
+
105
+ if (time <= 0) {
106
+ throw new Error("bufferByTime operator requires timeInMilliseconds to be greater than 0")
107
+ }
108
+
109
+ const result = signal<V[]>(() => [], {
110
+ onDispose: () => {
111
+ eTarget.dispose()
112
+ clearInterval(intervalId)
113
+ }
114
+ })
115
+
116
+ const bufferedValues: V[] = []
117
+ const eTarget = effect(() => {
118
+ bufferedValues.push(target.get())
119
+ })
120
+
121
+ const intervalId = setInterval(() => {
122
+ result.set([...bufferedValues])
123
+ bufferedValues.length = 0
124
+ }, time)
125
+
126
+ return result
127
+ }
128
+
129
+ export interface BufferByTriggerOptions<VTarget, VTrigger> {
130
+ target: ValueReactor<VTarget>;
131
+ trigger: ValueReactor<VTrigger>;
132
+ }
133
+ /**
134
+ * Buffers values from a ValueReactor and emits them as an array when the trigger ValueReactor changes.
135
+ */
136
+ export const bufferByTrigger = <VTarget, VTrigger>(
137
+ options: BufferByTriggerOptions<VTarget, VTrigger>
138
+ ): Signal<VTarget[]> => {
139
+ const { target, trigger } = options;
140
+
141
+ const result = signal(castValueInitializer<VTarget[]>(), {
142
+ onDispose: () => {
143
+ eTarget.dispose()
144
+ eTrigger.dispose()
145
+ }
146
+ })
147
+
148
+ const bufferedValues: VTarget[] = []
149
+ const eTarget = effect(() => {
150
+ bufferedValues.push(target.get())
151
+ })
152
+
153
+ const eTrigger = effect(() => {
154
+ trigger.get()
155
+ result.set([...bufferedValues])
156
+ bufferedValues.length = 0
157
+ })
158
+
159
+ return result
160
+ }
161
+
162
+ export interface BufferByToggleOptions<VTarget, VOpen, VClose> {
163
+ target: ValueReactor<VTarget>;
164
+ open: ValueReactor<VOpen>;
165
+ close: ValueReactor<VClose>;
166
+ }
167
+ /**
168
+ * Buffers values from a ValueReactor and emits them as an array
169
+ * when the open ValueReactor emits a value, and stops buffering
170
+ * when the close ValueReactor emits a value.
171
+ */
172
+ export const bufferByToggle = <VTarget, VOpen, VClose>(
173
+ options: BufferByToggleOptions<VTarget, VOpen, VClose>
174
+ ): Signal<VTarget[]> => {
175
+ const { target, open, close } = options;
176
+
177
+ const result = signal(castValueInitializer<VTarget[]>(), {
178
+ onDispose: () => {
179
+ eTarget.dispose()
180
+ eOpen.dispose()
181
+ eClose.dispose()
182
+ }
183
+ })
184
+
185
+ const bufferedValues: VTarget[] = []
186
+ let buffering = false
187
+
188
+ const eOpen = effect(() => {
189
+ open.get()
190
+ buffering = true
191
+ })
192
+
193
+ const eTarget = effect(() => {
194
+ const value = target.get()
195
+ if (buffering === true) {
196
+ bufferedValues.push(value)
197
+ }
198
+ })
199
+
200
+ const eClose = effect(() => {
201
+ close.get()
202
+ if (buffering === true) {
203
+ result.set([...bufferedValues])
204
+ bufferedValues.length = 0
205
+ buffering = false
206
+ }
207
+ })
208
+
209
+ return result
210
+ }
211
+
212
+ export interface BufferByDynamicOptions<VTarget, VDynamic> {
213
+ target: ValueReactor<VTarget>;
214
+ dynamic: (value: VTarget) => ValueReactor<VDynamic>;
215
+ }
216
+ /**
217
+ * Buffers values from a ValueReactor and emits them as an array
218
+ * when the ValueReactor returned by the `dynamic` function emits a value.
219
+ * The `dynamic` function is called each time the buffer is emitted
220
+ * to get a new ValueReactor for the next buffer.
221
+ */
222
+ export const bufferByDynamic = <VTarget, VDynamic>(
223
+ options: BufferByDynamicOptions<VTarget, VDynamic>
224
+ ): Signal<VTarget[]> => {
225
+ const { target, dynamic } = options;
226
+
227
+ const result = signal(castValueInitializer<VTarget[]>(), {
228
+ onDispose: () => {
229
+ eTarget.dispose()
230
+ eDynamic.dispose()
231
+ }
232
+ })
233
+
234
+ const bufferedValues: VTarget[] = []
235
+ const eTarget = effect(() => {
236
+ const value = target.get()
237
+ bufferedValues.push(value)
238
+ })
239
+
240
+ let trigger: ValueReactor<VDynamic> | undefined = undefined
241
+ const eDynamic = effect(() => {
242
+ trigger?.dispose()
243
+ const targetValue = target.getWithoutTrack()
244
+ trigger = dynamic(targetValue)
245
+ trigger.get()
246
+ result.set([...bufferedValues])
247
+ bufferedValues.length = 0
248
+ })
249
+
250
+ return result
251
+ }
252
+
253
+ export interface GroupToArrayByOptions<V, K> {
254
+ target: ValueReactor<V>;
255
+ keyGetter: (value: V) => K;
256
+ }
257
+ /**
258
+ * Groups values from a ValueReactor into an array of arrays based on a key
259
+ * extracted by the keyGetter function.
260
+ */
261
+ export const groupToArrayBy = <V, K>(
262
+ options: GroupToArrayByOptions<V, K>
263
+ ): Signal<V[][]> => {
264
+ const { target, keyGetter } = options;
265
+
266
+ const result = signal<V[][]>(() => [], {
267
+ onDispose: () => {
268
+ eTarget.dispose()
269
+ }
270
+ })
271
+
272
+ const groups = new Map<K, V[]>()
273
+ const eTarget = effect(() => {
274
+ const value = target.get()
275
+ const key = keyGetter(value)
276
+ if (groups.has(key) === false) {
277
+ groups.set(key, [])
278
+ result.set([...groups.values()])
279
+ }
280
+ const groupedArray = groups.get(key)!
281
+ groupedArray.push(value)
282
+ })
283
+
284
+ return result
285
+ }
286
+
287
+ export interface GroupToObjectByOptions<V, K extends string | number | symbol> {
288
+ target: ValueReactor<V>;
289
+ keyGetter: (value: V) => K;
290
+ }
291
+ /**
292
+ * Groups values from a ValueReactor into an object where each key maps to an array
293
+ * of values corresponding to that key.
294
+ */
295
+ export const groupToObjectBy = <V, K extends string | number | symbol>(
296
+ options: GroupToObjectByOptions<V, K>
297
+ ): Signal<Record<K, V[]>> => {
298
+ const { target, keyGetter } = options;
299
+
300
+ const result = signal<Record<K, V[]>>(() => {
301
+ // oxlint-disable-next-line no-unsafe-type-assertion
302
+ return {} as Record<K, V[]>
303
+ }, {
304
+ onDispose: () => {
305
+ eTarget.dispose()
306
+ }
307
+ })
308
+
309
+ const groups = new Map<K, V[]>()
310
+ const eTarget = effect(() => {
311
+ const value = target.get()
312
+ const key = keyGetter(value)
313
+ if (groups.has(key) === false) {
314
+ groups.set(key, [])
315
+ // oxlint-disable-next-line no-unsafe-type-assertion
316
+ result.set(Object.fromEntries(groups.entries()) as Record<K, V[]>)
317
+ }
318
+ const groupedArray = groups.get(key)!
319
+ groupedArray.push(value)
320
+ })
321
+
322
+ return result
323
+ }
324
+
325
+ /**
326
+ * Groups values from a ValueReactor into an array of Signals based on a key
327
+ * extracted by the keyGetter function.
328
+ */
329
+ export const groupToSignalArrayBy = <V, K>(
330
+ target: ValueReactor<V>, keyGetter: (value: V) => K
331
+ ): Signal<Array<Signal<V[]>>> => {
332
+ const result = signal<Array<Signal<V[]>>>(() => [], {
333
+ onDispose: () => {
334
+ eTarget.dispose()
335
+ }
336
+ })
337
+
338
+ const groups = new Map<K, Signal<V[]>>()
339
+ const eTarget = effect(() => {
340
+ const value = target.get()
341
+ const key = keyGetter(value)
342
+ if (groups.has(key) === false) {
343
+ const newGroup = signal<V[]>(() => [])
344
+ groups.set(key, newGroup)
345
+ result.set([...groups.values()])
346
+ }
347
+ const groupedSignal = groups.get(key)!
348
+ const currentArray = groupedSignal.get()
349
+ groupedSignal.set([...currentArray, value])
350
+ })
351
+
352
+ return result
353
+ }
354
+
355
+ export interface GroupToSignalObjectByOptions<V, K extends string | number | symbol> {
356
+ target: ValueReactor<V>;
357
+ keyGetter: (value: V) => K;
358
+ }
359
+ /**
360
+ * Groups values from a ValueReactor into a Signal of an object where each key maps to a Signal
361
+ * of an array of values corresponding to that key.
362
+ */
363
+ export const groupToSignalObjectBy = <V, K extends string | number | symbol>(
364
+ options: GroupToSignalObjectByOptions<V, K>
365
+ ): Signal<Record<K, Signal<V[]>>> => {
366
+ const { target, keyGetter } = options;
367
+
368
+ const result = signal<Record<K, Signal<V[]>>>(() => {
369
+ // oxlint-disable-next-line no-unsafe-type-assertion
370
+ return {} as Record<K, Signal<V[]>>
371
+ }, {
372
+ onDispose: () => {
373
+ eTarget.dispose()
374
+ }
375
+ })
376
+
377
+ const groups = new Map<K, Signal<V[]>>()
378
+ const eTarget = effect(() => {
379
+ const value = target.get()
380
+ const key = keyGetter(value)
381
+ if (groups.has(key) === false) {
382
+ const newGroup = signal<V[]>(() => [])
383
+ groups.set(key, newGroup)
384
+ // oxlint-disable-next-line no-unsafe-type-assertion
385
+ result.set(Object.fromEntries(groups.entries()) as Record<K, Signal<V[]>>)
386
+ }
387
+ const groupedSignal = groups.get(key)!
388
+ const currentArray = groupedSignal.get()
389
+ groupedSignal.set([...currentArray, value])
390
+ })
391
+
392
+ return result
393
+ }
394
+
395
+ export interface MapOptions<VTarget, VMapped> {
396
+ target: ValueReactor<VTarget>;
397
+ mapper: (value: VTarget) => VMapped;
398
+ }
399
+ /**
400
+ * Maps values from a ValueReactor using the provided mapper function and
401
+ * emits the mapped values as a new Signal.
402
+ */
403
+ export const map = <VTarget, VMapped>(
404
+ options: MapOptions<VTarget, VMapped>
405
+ ): Signal<VMapped> => {
406
+ const { target, mapper } = options;
407
+
408
+ const result = signal(castValueInitializer<VMapped>(), {
409
+ onDispose: () => {
410
+ eTarget.dispose()
411
+ }
412
+ })
413
+
414
+ const eTarget = effect(() => {
415
+ const value = target.get()
416
+ const newValue = mapper(value)
417
+ result.set(newValue)
418
+ })
419
+
420
+ return result
421
+ }
422
+
423
+ export interface ContextualMapInitializingMapperContext<VTarget> {
424
+ isInitializingRun: true;
425
+ targetValue: VTarget
426
+ }
427
+ export interface ContextualMapUpdatingMapperContext<VTarget, VMapped> {
428
+ isInitializingRun: false;
429
+ targetValue: VTarget
430
+ previousMappedValue: VMapped
431
+ }
432
+ export type ContextualMapMapperContextOf<VTarget, VMapped> =
433
+ | ContextualMapInitializingMapperContext<VTarget>
434
+ | ContextualMapUpdatingMapperContext<VTarget, VMapped>;
435
+
436
+ export interface ContextualMapOptions<VTarget, VMapped> {
437
+ target: ValueReactor<VTarget>;
438
+ mapper: (context: ContextualMapMapperContextOf<VTarget, VMapped>) => VMapped;
439
+ }
440
+ /**
441
+ * Maps values from a ValueReactor using the provided contextual mapper function and
442
+ * emits the mapped values as a new Signal.
443
+ */
444
+ export const contextualMap = <VTarget, VMapped>(
445
+ options: ContextualMapOptions<VTarget, VMapped>
446
+ ): Signal<VMapped> => {
447
+ const { target, mapper } = options;
448
+
449
+ const result = signal(castValueInitializer<VMapped>(), {
450
+ onDispose: () => {
451
+ eTarget.dispose()
452
+ }
453
+ })
454
+
455
+ const eTarget = effect((context) => {
456
+ const targetValue = target.get()
457
+ const { isInitializingRun } = context
458
+ if (isInitializingRun === true) {
459
+ const newValue = mapper({
460
+ isInitializingRun,
461
+ targetValue
462
+ })
463
+ result.set(newValue)
464
+ } else {
465
+ const previousMappedValue = result.getWithoutTrack()
466
+ const newValue = mapper({
467
+ isInitializingRun,
468
+ targetValue,
469
+ previousMappedValue
470
+ })
471
+ result.set(newValue)
472
+ }
473
+ })
474
+
475
+ return result
476
+ }
477
+
478
+ export interface MergeMapOptions<VTarget, VMapped> {
479
+ target: ValueReactor<VTarget>;
480
+ mapper: (value: VTarget) => ValueReactor<VMapped>;
481
+ }
482
+ /**
483
+ * Maps each value from a ValueReactor<T> to a ValueReactor<R> using the provided `mapper`,
484
+ * subscribes to every mapped reactor, and merges their emitted values into a single `Signal<R>`.
485
+ */
486
+ export const mergeMap = <VTarget, VMapped>(
487
+ options: MergeMapOptions<VTarget, VMapped>
488
+ ): Signal<VMapped> => {
489
+ const { target, mapper } = options;
490
+
491
+ const result = signal(castValueInitializer<VMapped>(), {
492
+ onDispose: () => {
493
+ eTarget.dispose()
494
+ }
495
+ })
496
+
497
+ const eTarget = effect(() => {
498
+ const value = target.get()
499
+ const currentMappedReactor = mapper(value);
500
+
501
+ effect(() => {
502
+ const mappedValue = currentMappedReactor.get()
503
+ result.set(mappedValue)
504
+ })
505
+ })
506
+
507
+ return result
508
+ }
509
+
510
+ export interface SwitchMapOptions<VTarget, VMapped> {
511
+ target: ValueReactor<VTarget>;
512
+ mapper: (value: VTarget) => ValueReactor<VMapped>;
513
+ }
514
+ /**
515
+ * Maps each value from a ValueReactor<T> to a ValueReactor<R> using the provided `mapper`,
516
+ * subscribes only to the latest mapped reactor, and emits its values as a `Signal<R>`.
517
+ */
518
+ export const switchMap = <VTarget, VMapped>(
519
+ options: SwitchMapOptions<VTarget, VMapped>
520
+ ): Signal<VMapped> => {
521
+ const { target, mapper } = options;
522
+
523
+ const result = signal(castValueInitializer<VMapped>(), {
524
+ onDispose: () => {
525
+ eTarget.dispose()
526
+ }
527
+ })
528
+
529
+ let lastMappedReactor: ValueReactor<VMapped> | undefined = undefined
530
+
531
+ const eTarget = effect(() => {
532
+ const value = target.get()
533
+ lastMappedReactor?.dispose()
534
+ lastMappedReactor = mapper(value);
535
+ effect(() => {
536
+ const mappedValue = lastMappedReactor!.get()
537
+ result.set(mappedValue)
538
+ })
539
+ })
540
+
541
+ return result
542
+ }
543
+
544
+ export interface ScanOptions<VTarget, VAccumulated> {
545
+ target: ValueReactor<VTarget>;
546
+ accumulator: (acc: VAccumulated, value: VTarget) => VAccumulated;
547
+ seed: VAccumulated;
548
+ }
549
+ /**
550
+ * Accumulates values from a ValueReactor using the provided accumulator function
551
+ * and emits the accumulated value as a Signal.
552
+ */
553
+ export const scan = <VTarget, VAccumulated>(
554
+ options: ScanOptions<VTarget, VAccumulated>
555
+ ): Signal<VAccumulated> => {
556
+ const { target, accumulator, seed } = options;
557
+
558
+ const result = signal(() => seed, {
559
+ onDispose: () => {
560
+ eTarget.dispose()
561
+ }
562
+ })
563
+
564
+ const eTarget = effect(() => {
565
+ const value = target.get()
566
+ const newValue = accumulator(result.getWithoutTrack(), value)
567
+ result.set(newValue)
568
+ })
569
+
570
+ return result
571
+ }
572
+
573
+ export interface PairwiseOptions<V> {
574
+ target: ValueReactor<V>;
575
+ }
576
+ /**
577
+ * Emits the previous and current values from a ValueReactor as a tuple.
578
+ * The first emitted tuple will have `undefined` as the previous value.
579
+ */
580
+ export const pairwise = <V>(
581
+ options: PairwiseOptions<V>
582
+ ): Signal<[V | undefined, V]> => {
583
+ const { target } = options;
584
+
585
+ const result = signal<[V | undefined, V]>(() => [undefined, target.get()], {
586
+ onDispose: () => {
587
+ eTarget.dispose()
588
+ }
589
+ })
590
+
591
+ let previousValue: V | undefined = undefined
592
+ const eTarget = effect(() => {
593
+ const currentValue = target.get()
594
+ result.set([previousValue, currentValue])
595
+ previousValue = currentValue
596
+ })
597
+
598
+ return result
599
+ }
@@ -0,0 +1,102 @@
1
+ import type { ValueReactor } from "../reactor-core/index.ts"
2
+
3
+ import { effect } from "../reactor-core/index.ts"
4
+
5
+ export type ValueInitializer<V> = () => V;
6
+ /**
7
+ * This initializer can be used to create a Signal with an undefined initial value which
8
+ * is casted to the desired type T. So the real value getter function will not be called
9
+ * until the Signal is actually used.
10
+ *
11
+ * Every signal should always have a valid value (or value getter which returns a valid value).
12
+ * When using the castValueInitializer, the real value should be set as soon as possible.
13
+ */
14
+ export const castValueInitializer = <V>(): ValueInitializer<V> => {
15
+ return (): V => {
16
+ // oxlint-disable-next-line no-unsafe-return no-unsafe-type-assertion
17
+ return undefined as unknown as V
18
+ }
19
+ }
20
+
21
+ export type Unsubscribe = () => void
22
+ export interface SubscribeOptions<V> {
23
+ target: ValueReactor<V>;
24
+ subscriber: (value: V) => void;
25
+ }
26
+ /**
27
+ * Subscribes to a ValueReactor and invokes the subscriber when the reactor's value
28
+ * changes.
29
+ *
30
+ * The initial value is skipped; the subscriber is called only on subsequent updates.
31
+ */
32
+ export const subscribe = <V>(
33
+ options: SubscribeOptions<V>
34
+ ): Unsubscribe => {
35
+ const { target, subscriber } = options
36
+
37
+ const e = effect((context) => {
38
+ const { isInitializingRun } = context
39
+ const value = target.get()
40
+ if (isInitializingRun === false) {
41
+ subscriber(value)
42
+ }
43
+ })
44
+ const unsubscribe = (): void => {
45
+ e.dispose()
46
+ }
47
+ return unsubscribe
48
+ }
49
+
50
+ // oxlint-disable-next-line no-explicit-any
51
+ export type AnyValueReactorArray = Array<ValueReactor<any>>;
52
+ export type GetValuesInArray<T extends AnyValueReactorArray> = {
53
+ [K in keyof T]: T[K] extends ValueReactor<infer U> ? U : never
54
+ }
55
+ export interface GetValuesInArrayOptions<T extends AnyValueReactorArray> {
56
+ target: T;
57
+ withoutTrack?: boolean;
58
+ }
59
+ /**
60
+ * Gets the current values from an array of ValueReactors.
61
+ */
62
+ export const getValuesInArray = <T extends AnyValueReactorArray>(
63
+ options: GetValuesInArrayOptions<T>
64
+ ): GetValuesInArray<T> => {
65
+ const valueReactors = options.target;
66
+ const withoutTrack = options?.withoutTrack ?? false;
67
+ // oxlint-disable-next-line no-unsafe-type-assertion
68
+ const values = valueReactors.map((vr) => {
69
+ // oxlint-disable-next-line no-unsafe-return
70
+ return withoutTrack ? vr.getWithoutTrack() : vr.get()
71
+ }) as unknown as GetValuesInArray<T>
72
+ return values
73
+ }
74
+
75
+ // oxlint-disable-next-line no-explicit-any
76
+ export type AnyValueReactorObject = Record<string, ValueReactor<any>>;
77
+ export type GetValuesInObject<T extends AnyValueReactorObject> = {
78
+ [K in keyof T]: T[K] extends ValueReactor<infer U> ? U : never
79
+ }
80
+ export interface GetValueInObjectOptions<T extends AnyValueReactorObject> {
81
+ target: T;
82
+ withoutTrack?: boolean;
83
+ }
84
+ /**
85
+ * Gets the current values from an object of ValueReactors.
86
+ */
87
+ export const getValuesInObject = <T extends AnyValueReactorObject>(
88
+ options: GetValueInObjectOptions<T>
89
+ ): GetValuesInObject<T> => {
90
+ const valueReactors = options.target;
91
+ const withoutTrack = options?.withoutTrack ?? false;
92
+ // oxlint-disable-next-line no-unsafe-type-assertion
93
+ const values = {} as GetValuesInObject<T>
94
+ for (const key of Object.keys(valueReactors)) {
95
+ const valueReactor = valueReactors[key]!
96
+ // oxlint-disable-next-line no-unsafe-assignment
97
+ const value = withoutTrack ? valueReactor.getWithoutTrack() : valueReactor.get()
98
+ // oxlint-disable-next-line no-unsafe-assignment no-unsafe-member-access no-unsafe-type-assertion
99
+ values[key as unknown as keyof GetValuesInObject<T>] = value
100
+ }
101
+ return values
102
+ }