@planet-matrix/mobius-model 0.1.4 → 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 +46 -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 -9
  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,988 @@
1
+ import type { Signal, SignalValueInitializer, 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 CurrentOptions<V> {
7
+ target: ValueReactor<V>;
8
+ }
9
+ /**
10
+ * Returns a Signal that holds the current value of the target ValueReactor.
11
+ */
12
+ export const current = <V>(
13
+ options: CurrentOptions<V>
14
+ ): Signal<V> => {
15
+ const { target } = options;
16
+
17
+ const result = signal<V>(() => {
18
+ return target.get()
19
+ })
20
+
21
+ return result
22
+ }
23
+
24
+ export interface NextOptions<V> {
25
+ target: ValueReactor<V>;
26
+ valueInitializer: SignalValueInitializer<V>
27
+ }
28
+ /**
29
+ * Returns a Signal that holds the next value of the target ValueReactor
30
+ * after the initial value.
31
+ *
32
+ * The Signal will update its value only once, upon the next change of
33
+ * the target ValueReactor, and then it will not update anymore.
34
+ */
35
+ export const next = <V>(
36
+ options: NextOptions<V>
37
+ ): Signal<V> => {
38
+ const { target, valueInitializer } = options;
39
+
40
+ const result = signal(valueInitializer, {
41
+ onDispose: () => {
42
+ eTarget.dispose()
43
+ }
44
+ })
45
+
46
+ const eTarget = effect((context) => {
47
+ const { isInitializingRun } = context
48
+
49
+ const value = target.get()
50
+ if (isInitializingRun === false) {
51
+ result.set(value)
52
+ eTarget.dispose()
53
+ }
54
+ })
55
+
56
+ return result
57
+ }
58
+
59
+ export interface FilterInitializingFalsyValueGetterContext<V> {
60
+ isInitializingRun: true;
61
+ targetValue: V
62
+ }
63
+ export interface FilterUpdatingFalsyValueGetterContext<V> {
64
+ isInitializingRun: false;
65
+ targetValue: V
66
+ previousValue: V
67
+ }
68
+ export type FilterFalsyValueGetterContextOf<V> =
69
+ | FilterInitializingFalsyValueGetterContext<V>
70
+ | FilterUpdatingFalsyValueGetterContext<V>
71
+ export interface FilterOptions<V> {
72
+ target: ValueReactor<V>;
73
+ truthyPredicate: (value: V) => boolean;
74
+ falsyValueGetter: (context: FilterFalsyValueGetterContextOf<V>) => V;
75
+ }
76
+ /**
77
+ * Returns a Signal that only updates its value from the target
78
+ * ValueReactor when the predicate function returns true.
79
+ */
80
+ export const filter = <V>(
81
+ options: FilterOptions<V>
82
+ ): Signal<V> => {
83
+ const { target, truthyPredicate, falsyValueGetter } = options;
84
+
85
+ const result = signal<V>(castValueInitializer<V>(), {
86
+ onDispose: () => {
87
+ eTarget.dispose()
88
+ }
89
+ })
90
+
91
+ const eTarget = effect((context) => {
92
+ const value = target.get()
93
+ if (truthyPredicate(value) === true) {
94
+ result.set(value)
95
+ } else {
96
+ const { isInitializingRun } = context
97
+ if (isInitializingRun === true) {
98
+ const fallbackValue = falsyValueGetter({
99
+ isInitializingRun,
100
+ targetValue: value
101
+ })
102
+ result.set(fallbackValue)
103
+ } else {
104
+ const fallbackValue = falsyValueGetter({
105
+ isInitializingRun,
106
+ targetValue: value,
107
+ previousValue: result.getWithoutTrack()
108
+ })
109
+ result.set(fallbackValue)
110
+ }
111
+ }
112
+ })
113
+
114
+ return result
115
+ }
116
+
117
+ export interface AuditByCountOptions<V> {
118
+ target: ValueReactor<V>;
119
+ count: number;
120
+ }
121
+ /**
122
+ * Returns a Signal that updates its value with the latest value from
123
+ * the target ValueReactor after every specified count of emissions.
124
+ */
125
+ export const auditByCount = <V>(
126
+ options: AuditByCountOptions<V>
127
+ ): Signal<V> => {
128
+ const { target, count } = options;
129
+
130
+ if (count <= 0) {
131
+ throw new Error("auditByCount operator requires count to be greater than 0")
132
+ }
133
+
134
+
135
+ const result = signal(castValueInitializer<V>(), {
136
+ onDispose: () => {
137
+ eTarget.dispose()
138
+ }
139
+ })
140
+
141
+ let emissionCount = 0
142
+ const eTarget = effect((context) => {
143
+ const latestValue = target.get()
144
+
145
+ if (context.isInitializingRun === true) {
146
+ result.set(latestValue)
147
+ } else {
148
+ emissionCount = emissionCount + 1
149
+ if (emissionCount >= count) {
150
+ result.set(latestValue)
151
+ emissionCount = 0
152
+ }
153
+ }
154
+ })
155
+
156
+ return result
157
+ }
158
+
159
+ export interface AuditByTimeOptions<V> {
160
+ target: ValueReactor<V>;
161
+ time: number;
162
+ }
163
+ /**
164
+ * Returns a Signal that updates its value with the latest value from
165
+ * the target ValueReactor at specified time intervals.
166
+ */
167
+ export const auditByTime = <V>(
168
+ options: AuditByTimeOptions<V>
169
+ ): Signal<V> => {
170
+ const { target, time } = options;
171
+
172
+ if (time <= 0) {
173
+ throw new Error("auditByTime operator requires timeInMilliseconds to be greater than 0")
174
+ }
175
+
176
+ const result = signal<V>(() => target.getWithoutTrack(), {
177
+ onDispose: () => {
178
+ clearInterval(intervalId)
179
+ }
180
+ })
181
+
182
+ const intervalId = setInterval(() => {
183
+ result.set(target.getWithoutTrack())
184
+ }, time)
185
+
186
+ return result
187
+ }
188
+
189
+ export interface AuditByTriggerOptions<VTarget, VTrigger> {
190
+ target: ValueReactor<VTarget>;
191
+ trigger: ValueReactor<VTrigger>;
192
+ }
193
+ /**
194
+ * Returns a Signal that only updates its value when the trigger
195
+ * ValueReactor emits a value, using the latest value from the target
196
+ * ValueReactor.
197
+ */
198
+ export const auditByTrigger = <VTarget, VTrigger>(
199
+ options: AuditByTriggerOptions<VTarget, VTrigger>
200
+ ): Signal<VTarget> => {
201
+ const { target, trigger } = options;
202
+
203
+ const result = signal<VTarget>(castValueInitializer<VTarget>(), {
204
+ onDispose: () => {
205
+ eTrigger.dispose()
206
+ }
207
+ })
208
+
209
+ const eTrigger = effect(() => {
210
+ trigger.get()
211
+ result.set(target.getWithoutTrack())
212
+ })
213
+
214
+ return result
215
+ }
216
+
217
+ export interface AuditByToggleOptions<VTarget, VOpen, VClose> {
218
+ target: ValueReactor<VTarget>;
219
+ open: ValueReactor<VOpen>;
220
+ close: ValueReactor<VClose>;
221
+ }
222
+ /**
223
+ * Returns a Signal that updates its value with the latest value from
224
+ * the target ValueReactor when the close ValueReactor emits, but only
225
+ * if auditing was started by the open ValueReactor.
226
+ */
227
+ export const auditByToggle = <VTarget, VOpen, VClose>(
228
+ options: AuditByToggleOptions<VTarget, VOpen, VClose>
229
+ ): Signal<VTarget> => {
230
+ const { target, open, close } = options;
231
+
232
+ const result = signal(castValueInitializer<VTarget>(), {
233
+ onDispose: () => {
234
+ eTarget.dispose()
235
+ eOpen.dispose()
236
+ eClose.dispose()
237
+ }
238
+ })
239
+
240
+ let auditValue: VTarget | undefined = undefined
241
+ let auditing = false
242
+
243
+ const eOpen = effect(() => {
244
+ open.get()
245
+ auditing = true
246
+ })
247
+
248
+ const eTarget = effect(() => {
249
+ const value = target.get()
250
+ if (auditing === true) {
251
+ auditValue = value
252
+ }
253
+ })
254
+
255
+ const eClose = effect(() => {
256
+ close.get()
257
+ if (auditing === true) {
258
+ if (auditValue !== undefined) {
259
+ result.set(auditValue)
260
+ }
261
+ auditing = false
262
+ auditValue = undefined
263
+ }
264
+ })
265
+
266
+ return result
267
+ }
268
+
269
+ export interface AuditByDynamicOptions<VTarget, VDynamic> {
270
+ target: ValueReactor<VTarget>;
271
+ dynamic: (value: VTarget) => ValueReactor<VDynamic>;
272
+ }
273
+ /**
274
+ * Returns a Signal that updates its value with the latest value from
275
+ * the target ValueReactor when the ValueReactor returned by the `dynamic`
276
+ * function emits. The `dynamic` function is called each time after emitting
277
+ * to get a new ValueReactor for the next audit period.
278
+ */
279
+ export const auditByDynamic = <VTarget, VDynamic>(
280
+ options: AuditByDynamicOptions<VTarget, VDynamic>
281
+ ): Signal<VTarget> => {
282
+ const { target, dynamic } = options;
283
+
284
+ const result = signal(castValueInitializer<VTarget>(), {
285
+ onDispose: () => {
286
+ eDynamic.dispose()
287
+ }
288
+ })
289
+
290
+ let trigger: ValueReactor<VDynamic> | undefined = undefined
291
+ const eDynamic = effect(() => {
292
+ trigger?.dispose()
293
+ const targetValue = target.getWithoutTrack()
294
+ trigger = dynamic(targetValue)
295
+ trigger.get()
296
+ result.set(targetValue)
297
+ })
298
+
299
+ return result
300
+ }
301
+
302
+ export interface DebounceByCountOptions<V> {
303
+ target: ValueReactor<V>;
304
+ count: number;
305
+ }
306
+ /**
307
+ * Returns a Signal that updates its value with the latest value from
308
+ * the target ValueReactor after the specified count of emissions
309
+ * without being updated.
310
+ */
311
+ export const debounceByCount = <V>(
312
+ options: DebounceByCountOptions<V>
313
+ ): Signal<V> => {
314
+ const { target, count } = options;
315
+
316
+ if (count <= 0) {
317
+ throw new Error("debounceByCount operator requires count to be greater than 0")
318
+ }
319
+
320
+ const result = signal(castValueInitializer<V>(), {
321
+ onDispose: () => {
322
+ eTarget.dispose()
323
+ }
324
+ })
325
+
326
+ let emissionCount = 0
327
+ const eTarget = effect((context) => {
328
+ const latestValue = target.get()
329
+
330
+ if (context.isInitializingRun === true) {
331
+ result.set(latestValue)
332
+ } else {
333
+ emissionCount = emissionCount + 1
334
+ if (emissionCount >= count) {
335
+ result.set(latestValue)
336
+ emissionCount = 0
337
+ }
338
+ }
339
+ })
340
+
341
+ return result
342
+ }
343
+
344
+ export interface DebounceByTimeOptions<V> {
345
+ target: ValueReactor<V>;
346
+ time: number;
347
+ }
348
+ /**
349
+ * Returns a Signal that updates its value with the latest value from
350
+ * the target ValueReactor after the specified time in milliseconds
351
+ * has passed since the last update.
352
+ */
353
+ export const debounceByTime = <V>(
354
+ options: DebounceByTimeOptions<V>
355
+ ): Signal<V> => {
356
+ const { target, time } = options;
357
+
358
+ if (time <= 0) {
359
+ throw new Error("debounceTime operator requires timeInMilliseconds to be greater than 0")
360
+ }
361
+
362
+ let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
363
+
364
+ const result = signal<V>(castValueInitializer<V>(), {
365
+ onDispose: () => {
366
+ eTarget.dispose()
367
+ clearTimeout(timeoutId)
368
+ }
369
+ })
370
+
371
+ const eTarget = effect((context) => {
372
+ const latestValue = target.get()
373
+
374
+ if (context.isInitializingRun === true) {
375
+ result.set(latestValue)
376
+ } else {
377
+ clearTimeout(timeoutId)
378
+ timeoutId = setTimeout(() => {
379
+ result.set(latestValue)
380
+ timeoutId = undefined
381
+ }, time)
382
+ }
383
+ })
384
+
385
+ return result
386
+ }
387
+
388
+ export interface DebounceByTriggerOptions<VTarget, VTrigger> {
389
+ target: ValueReactor<VTarget>;
390
+ trigger: ValueReactor<VTrigger>;
391
+ }
392
+ /**
393
+ * Returns a Signal that updates its value with the latest value from
394
+ * the target ValueReactor whenever the trigger ValueReactor
395
+ * emits a value.
396
+ */
397
+ export const debounceByTrigger = <VTarget, VTrigger>(
398
+ options: DebounceByTriggerOptions<VTarget, VTrigger>
399
+ ): Signal<VTarget> => {
400
+ const { target, trigger } = options;
401
+
402
+ const result = signal<VTarget>(castValueInitializer<VTarget>(), {
403
+ onDispose: () => {
404
+ eTrigger.dispose()
405
+ }
406
+ })
407
+
408
+ const eTrigger = effect(() => {
409
+ trigger.get()
410
+ result.set(target.getWithoutTrack())
411
+ })
412
+
413
+ return result
414
+ }
415
+
416
+ export interface DebounceByToggleOptions<VTarget, VOpen, VClose> {
417
+ target: ValueReactor<VTarget>;
418
+ open: ValueReactor<VOpen>;
419
+ close: ValueReactor<VClose>;
420
+ }
421
+ /**
422
+ * Returns a Signal that updates its value with the latest value from
423
+ * the target ValueReactor when the close ValueReactor emits, but only
424
+ * if debouncing was started by the open ValueReactor.
425
+ */
426
+ export const debounceByToggle = <VTarget, VOpen, VClose>(
427
+ options: DebounceByToggleOptions<VTarget, VOpen, VClose>
428
+ ): Signal<VTarget> => {
429
+ const { target, open, close } = options;
430
+
431
+ const result = signal(castValueInitializer<VTarget>(), {
432
+ onDispose: () => {
433
+ eTarget.dispose()
434
+ eOpen.dispose()
435
+ eClose.dispose()
436
+ }
437
+ })
438
+
439
+ let debounceValue: VTarget | undefined = undefined
440
+ let debouncing = false
441
+
442
+ const eOpen = effect(() => {
443
+ open.get()
444
+ debouncing = true
445
+ })
446
+
447
+ const eTarget = effect(() => {
448
+ const value = target.get()
449
+ if (debouncing === true) {
450
+ debounceValue = value
451
+ }
452
+ })
453
+
454
+ const eClose = effect(() => {
455
+ close.get()
456
+ if (debouncing === true) {
457
+ if (debounceValue !== undefined) {
458
+ result.set(debounceValue)
459
+ }
460
+ debouncing = false
461
+ debounceValue = undefined
462
+ }
463
+ })
464
+
465
+ return result
466
+ }
467
+
468
+ export interface DebounceByDynamicOptions<VTarget, VDynamic> {
469
+ target: ValueReactor<VTarget>;
470
+ dynamic: (value: VTarget) => ValueReactor<VDynamic>;
471
+ }
472
+ /**
473
+ * Returns a Signal that updates its value with the latest value from
474
+ * the target ValueReactor when the ValueReactor returned by the `dynamic`
475
+ * function emits. The `dynamic` function is called each time after emitting
476
+ * to get a new ValueReactor for the next debounce period.
477
+ */
478
+ export const debounceByDynamic = <VTarget, VDynamic>(
479
+ options: DebounceByDynamicOptions<VTarget, VDynamic>
480
+ ): Signal<VTarget> => {
481
+ const { target, dynamic } = options;
482
+
483
+ const result = signal(castValueInitializer<VTarget>(), {
484
+ onDispose: () => {
485
+ eDynamic.dispose()
486
+ }
487
+ })
488
+
489
+ let trigger: ValueReactor<VDynamic> | undefined = undefined
490
+ const eDynamic = effect(() => {
491
+ trigger?.dispose()
492
+ const targetValue = target.getWithoutTrack()
493
+ trigger = dynamic(targetValue)
494
+ trigger.get()
495
+ result.set(targetValue)
496
+ })
497
+
498
+ return result
499
+ }
500
+
501
+ export interface ThrottleByCountOptions<V> {
502
+ target: ValueReactor<V>;
503
+ count: number;
504
+ }
505
+ /**
506
+ * Returns a Signal that updates its value with the latest value from
507
+ * the target ValueReactor at most once every specified count of emissions.
508
+ */
509
+ export const throttleByCount = <V>(
510
+ options: ThrottleByCountOptions<V>
511
+ ): Signal<V> => {
512
+ const { target, count } = options;
513
+
514
+ if (count <= 0) {
515
+ throw new Error("throttleByCount operator requires count to be greater than 0")
516
+ }
517
+
518
+ const result = signal(castValueInitializer<V>(), {
519
+ onDispose: () => {
520
+ eTarget.dispose()
521
+ }
522
+ })
523
+
524
+ let emissionCount = 0
525
+ const eTarget = effect(() => {
526
+ const latestValue = target.get()
527
+ if (emissionCount === 0) {
528
+ result.set(latestValue)
529
+ }
530
+ emissionCount = emissionCount + 1
531
+ if (emissionCount >= count) {
532
+ emissionCount = 0
533
+ }
534
+ })
535
+
536
+ return result
537
+ }
538
+
539
+ export interface ThrottleByTimeOptions<V> {
540
+ target: ValueReactor<V>;
541
+ time: number;
542
+ }
543
+ /**
544
+ * Returns a Signal that updates its value with the latest value from
545
+ * the target ValueReactor at most once every specified time in milliseconds.
546
+ */
547
+ export const throttleByTime = <V>(
548
+ options: ThrottleByTimeOptions<V>
549
+ ): Signal<V> => {
550
+ const { target, time } = options;
551
+
552
+ if (time <= 0) {
553
+ throw new Error("throttleByTime operator requires timeInMilliseconds to be greater than 0")
554
+ }
555
+
556
+ const result = signal<V>(castValueInitializer<V>(), {
557
+ onDispose: () => {
558
+ eTarget.dispose()
559
+ }
560
+ })
561
+
562
+ let canEmit = true
563
+ const eTarget = effect(() => {
564
+ const latestValue = target.get()
565
+
566
+ if (canEmit === true) {
567
+ result.set(latestValue)
568
+ canEmit = false
569
+ setTimeout(() => {
570
+ canEmit = true
571
+ }, time)
572
+ }
573
+ })
574
+
575
+ return result
576
+ }
577
+
578
+ export interface ThrottleByTriggerOptions<VTarget, VTrigger> {
579
+ target: ValueReactor<VTarget>;
580
+ trigger: ValueReactor<VTrigger>;
581
+ }
582
+ /**
583
+ * Returns a Signal that updates its value with the latest value from
584
+ * the target ValueReactor at most once whenever the trigger
585
+ * ValueReactor emits.
586
+ */
587
+ export const throttleByTrigger = <VTarget, VTrigger>(
588
+ options: ThrottleByTriggerOptions<VTarget, VTrigger>
589
+ ): Signal<VTarget> => {
590
+ const { target, trigger } = options;
591
+
592
+ const result = signal<VTarget>(castValueInitializer<VTarget>(), {
593
+ onDispose: () => {
594
+ eTrigger.dispose()
595
+ eTarget.dispose()
596
+ }
597
+ })
598
+
599
+ let canEmit = false
600
+ const eTrigger = effect(() => {
601
+ trigger.get()
602
+ canEmit = true
603
+ })
604
+ const eTarget = effect(() => {
605
+ const latestValue = target.get()
606
+ if (canEmit === true) {
607
+ result.set(latestValue)
608
+ canEmit = false
609
+ }
610
+ })
611
+
612
+ return result
613
+ }
614
+
615
+ export interface ThrottleByToggleOptions<VTarget, VOpen, VClose> {
616
+ target: ValueReactor<VTarget>;
617
+ open: ValueReactor<VOpen>;
618
+ close: ValueReactor<VClose>;
619
+ }
620
+ /**
621
+ * Returns a Signal that updates its value with the latest value from
622
+ * the target ValueReactor when the close ValueReactor emits, but only
623
+ * if throttling was started by the open ValueReactor.
624
+ */
625
+ export const throttleByToggle = <VTarget, VOpen, VClose>(
626
+ options: ThrottleByToggleOptions<VTarget, VOpen, VClose>
627
+ ): Signal<VTarget> => {
628
+ const { target, open, close } = options;
629
+
630
+ const result = signal(castValueInitializer<VTarget>(), {
631
+ onDispose: () => {
632
+ eTarget.dispose()
633
+ eOpen.dispose()
634
+ eClose.dispose()
635
+ }
636
+ })
637
+
638
+ let throttling = false
639
+
640
+ const eOpen = effect(() => {
641
+ open.get()
642
+ throttling = true
643
+ })
644
+
645
+ const eTarget = effect(() => {
646
+ const value = target.get()
647
+ if (throttling === true) {
648
+ result.set(value)
649
+ throttling = false
650
+ }
651
+ })
652
+
653
+ const eClose = effect(() => {
654
+ close.get()
655
+ throttling = false
656
+ })
657
+
658
+ return result
659
+ }
660
+
661
+ export interface ThrottleByDynamicOptions<VTarget, VDynamic> {
662
+ target: ValueReactor<VTarget>;
663
+ dynamic: (value: VTarget) => ValueReactor<VDynamic>;
664
+ }
665
+ /**
666
+ * Returns a Signal that updates its value with the latest value from
667
+ * the target ValueReactor when the ValueReactor returned by the `dynamic`
668
+ * function emits. The `dynamic` function is called each time after emitting
669
+ * to get a new ValueReactor for the next throttle period.
670
+ */
671
+ export const throttleByDynamic = <VTarget, VDynamic>(
672
+ options: ThrottleByDynamicOptions<VTarget, VDynamic>
673
+ ): Signal<VTarget> => {
674
+ const { target, dynamic } = options;
675
+
676
+ const result = signal(castValueInitializer<VTarget>(), {
677
+ onDispose: () => {
678
+ eTrigger.dispose()
679
+ eTarget.dispose()
680
+ }
681
+ })
682
+
683
+ let trigger: ValueReactor<VDynamic> | undefined = undefined
684
+ let canEmit = true
685
+ const eTrigger = effect(() => {
686
+ trigger?.dispose()
687
+ const targetValue = target.getWithoutTrack()
688
+ trigger = dynamic(targetValue)
689
+ trigger.get()
690
+ canEmit = true
691
+ })
692
+
693
+ const eTarget = effect(() => {
694
+ const latestValue = target.get()
695
+ if (canEmit === true) {
696
+ result.set(latestValue)
697
+ canEmit = false
698
+ }
699
+ })
700
+
701
+ return result
702
+ }
703
+
704
+ export interface DistinctOptions<V, K> {
705
+ target: ValueReactor<V>;
706
+ keyGetter?: (value: V) => K;
707
+ }
708
+ /**
709
+ * Returns a Signal that only updates its value with unique values
710
+ * from the target ValueReactor, determined by the key returned from
711
+ * the keyGetter function.
712
+ */
713
+ export const distinct = <V, K>(
714
+ options: DistinctOptions<V, K>
715
+ ): Signal<V> => {
716
+ const { target, keyGetter } = options;
717
+
718
+ const result = signal<V>(castValueInitializer<V>(), {
719
+ onDispose: () => {
720
+ eTarget.dispose()
721
+ }
722
+ })
723
+
724
+ // oxlint-disable-next-line no-unsafe-type-assertion
725
+ const preparedKeyGetter = keyGetter ?? ((value: V): K => (value as unknown) as K)
726
+
727
+ const keySet = new Set<K>()
728
+ const eTarget = effect(() => {
729
+ const newValue = target.get()
730
+ const key = preparedKeyGetter(newValue);
731
+ if (keySet.has(key) === false) {
732
+ result.set(newValue)
733
+ keySet.add(key)
734
+ }
735
+ })
736
+
737
+ return result
738
+ }
739
+
740
+ export interface DistinctUntilChangedOptions<V, K> {
741
+ target: ValueReactor<V>;
742
+ keyGetter?: (value: V) => K;
743
+ }
744
+ /**
745
+ * Returns a Signal that only updates its value when the key returned
746
+ * from the keyGetter function changes between emissions.
747
+ */
748
+ export const distinctUntilChanged = <V, K>(
749
+ options: DistinctUntilChangedOptions<V, K>
750
+ ): Signal<V> => {
751
+ const { target, keyGetter } = options;
752
+
753
+ const result = signal<V>(castValueInitializer<V>(), {
754
+ onDispose: () => {
755
+ eTarget.dispose()
756
+ }
757
+ })
758
+
759
+ // oxlint-disable-next-line no-unsafe-type-assertion
760
+ const preparedKeyGetter = keyGetter ?? ((value: V): K => (value as unknown) as K)
761
+
762
+ // oxlint-disable-next-line no-unsafe-type-assertion
763
+ let lastKey: K = Symbol("initial-key-for-distinct-until-changed") as unknown as K
764
+ const eTarget = effect(() => {
765
+ const newValue = target.get()
766
+ const key = preparedKeyGetter(newValue);
767
+ if (key !== lastKey) {
768
+ result.set(newValue)
769
+ lastKey = key
770
+ }
771
+ })
772
+
773
+ return result
774
+ }
775
+
776
+ export interface TakeByPredicateOptions<V> {
777
+ target: ValueReactor<V>;
778
+ predicate: (value: V) => boolean;
779
+ }
780
+ /**
781
+ * Returns a Signal that updates its value with the latest value from
782
+ * the target ValueReactor, but only while the predicate function
783
+ * returns true.
784
+ */
785
+ export const takeByPredicate = <V>(
786
+ options: TakeByPredicateOptions<V>
787
+ ): Signal<V> => {
788
+ const { target, predicate } = options;
789
+
790
+ const result = signal<V>(castValueInitializer<V>(), {
791
+ onDispose: () => {
792
+ eTarget.dispose()
793
+ }
794
+ })
795
+
796
+ const eTarget = effect(() => {
797
+ const latestValue = target.get()
798
+ if (predicate(latestValue) === true) {
799
+ result.set(latestValue)
800
+ } else {
801
+ eTarget.dispose()
802
+ }
803
+ })
804
+
805
+ return result
806
+ }
807
+
808
+ export interface TakeByCountOptions<V> {
809
+ target: ValueReactor<V>;
810
+ count: number;
811
+ }
812
+ /**
813
+ * Returns a Signal that updates its value with the latest value from
814
+ * the target ValueReactor, but only for the first `count` emissions.
815
+ */
816
+ export const takeByCount = <V>(
817
+ options: TakeByCountOptions<V>
818
+ ): Signal<V> => {
819
+ const { target, count } = options;
820
+
821
+ if (count <= 0) {
822
+ throw new Error("takeByCount operator requires count to be greater than 0")
823
+ }
824
+
825
+ const result = signal<V>(castValueInitializer<V>(), {
826
+ onDispose: () => {
827
+ eTarget.dispose()
828
+ }
829
+ })
830
+
831
+ let takenCount = 0
832
+ const eTarget = effect(() => {
833
+ const latestValue = target.get()
834
+ if (takenCount < count) {
835
+ result.set(latestValue)
836
+ takenCount = takenCount + 1
837
+ }
838
+ if (takenCount >= count) {
839
+ eTarget.dispose()
840
+ }
841
+ })
842
+
843
+ return result
844
+ }
845
+
846
+ export interface TakeByTimeOptions<T> {
847
+ target: ValueReactor<T>;
848
+ time: number;
849
+ }
850
+ /**
851
+ * Returns a Signal that updates its value with the latest value from
852
+ * the target ValueReactor, but only for the specified time in milliseconds.
853
+ */
854
+ export const takeByTime = <T>(
855
+ options: TakeByTimeOptions<T>
856
+ ): Signal<T> => {
857
+ const { target, time } = options;
858
+
859
+ if (time <= 0) {
860
+ throw new Error("takeByTime operator requires timeInMilliseconds to be greater than 0")
861
+ }
862
+
863
+ const result = signal<T>(castValueInitializer<T>(), {
864
+ onDispose: () => {
865
+ eTarget.dispose()
866
+ clearTimeout(timeoutId)
867
+ }
868
+ })
869
+
870
+ const timeoutId = setTimeout(() => {
871
+ eTarget.dispose()
872
+ }, time)
873
+
874
+ const eTarget = effect(() => {
875
+ const latestValue = target.get()
876
+ result.set(latestValue)
877
+ })
878
+
879
+ return result
880
+ }
881
+
882
+ export interface TakeByTriggerOptions<VTarget, VTrigger> {
883
+ target: ValueReactor<VTarget>;
884
+ trigger: ValueReactor<VTrigger>;
885
+ }
886
+ /**
887
+ * Returns a Signal that updates its value with the latest value from
888
+ * the target ValueReactor whenever the trigger ValueReactor emits a value.
889
+ */
890
+ export const takeByTrigger = <VTarget, VTrigger>(
891
+ options: TakeByTriggerOptions<VTarget, VTrigger>
892
+ ): Signal<VTarget> => {
893
+ const { target, trigger } = options;
894
+
895
+ const result = signal<VTarget>(castValueInitializer<VTarget>(), {
896
+ onDispose: () => {
897
+ eTrigger.dispose()
898
+ }
899
+ })
900
+
901
+ const eTrigger = effect(() => {
902
+ trigger.get()
903
+ result.set(target.getWithoutTrack())
904
+ })
905
+
906
+ return result
907
+ }
908
+
909
+ export interface TakeByToggleOptions<VTarget, VOpen, VClose> {
910
+ target: ValueReactor<VTarget>;
911
+ open: ValueReactor<VOpen>;
912
+ close: ValueReactor<VClose>;
913
+ }
914
+ /**
915
+ * Returns a Signal that updates its value with the latest value from
916
+ * the target ValueReactor when the close ValueReactor emits, but only
917
+ * if taking was started by the open ValueReactor.
918
+ */
919
+ export const takeByToggle = <VTarget, VOpen, VClose>(
920
+ options: TakeByToggleOptions<VTarget, VOpen, VClose>
921
+ ): Signal<VTarget> => {
922
+ const { target, open, close } = options;
923
+
924
+ const result = signal<VTarget>(castValueInitializer<VTarget>(), {
925
+ onDispose: () => {
926
+ eTarget.dispose()
927
+ eOpen.dispose()
928
+ eClose.dispose()
929
+ }
930
+ })
931
+
932
+ let taking = false
933
+
934
+ const eOpen = effect(() => {
935
+ open.get()
936
+ taking = true
937
+ })
938
+
939
+ const eTarget = effect(() => {
940
+ const value = target.get()
941
+ if (taking === true) {
942
+ result.set(value)
943
+ }
944
+ })
945
+
946
+ const eClose = effect(() => {
947
+ close.get()
948
+ taking = false
949
+ })
950
+
951
+ return result
952
+ }
953
+
954
+ export interface TakeUntilTriggerOptions<VTarget, VTrigger> {
955
+ target: ValueReactor<VTarget>;
956
+ trigger: ValueReactor<VTrigger>;
957
+ }
958
+ /**
959
+ * Returns a Signal that updates its value with the latest value from
960
+ * the target ValueReactor until the trigger ValueReactor emits a value.
961
+ */
962
+ export const takeUntilTrigger = <VTarget, VTrigger>(
963
+ options: TakeUntilTriggerOptions<VTarget, VTrigger>
964
+ ): Signal<VTarget> => {
965
+ const { target, trigger } = options;
966
+
967
+ const result = signal<VTarget>(castValueInitializer<VTarget>(), {
968
+ onDispose: () => {
969
+ eTarget.dispose()
970
+ eTrigger.dispose()
971
+ }
972
+ })
973
+
974
+ const eTarget = effect(() => {
975
+ const latestValue = target.get()
976
+ result.set(latestValue)
977
+ })
978
+
979
+ const eTrigger = effect((context) => {
980
+ trigger.get()
981
+ if (context.isInitializingRun === false) {
982
+ eTarget.dispose()
983
+ eTrigger.dispose()
984
+ }
985
+ })
986
+
987
+ return result
988
+ }