evg_observable 3.0.0 → 3.1.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.
- package/README.md +298 -21
- package/package.json +24 -15
- package/repo/evg_observable.js +1 -1
- package/src/outLib/FunctionLibs.js +5 -4
- package/src/outLib/Observable.d.ts +1 -0
- package/src/outLib/Observable.js +9 -0
- package/src/outLib/OrderedSubscribeObject.d.ts +3 -0
- package/src/outLib/OrderedSubscribeObject.js +9 -0
- package/src/outLib/Pipe.d.ts +8 -1
- package/src/outLib/Pipe.js +121 -2
- package/src/outLib/SubscribeObject.js +17 -40
- package/src/outLib/Types.d.ts +48 -2
- package/BENCHMARK_BUNDLE_RESULTS.md +0 -149
- package/BENCHMARK_OBSERVABLE_FNS_RESULTS.md +0 -197
- package/BREAKING_CHANGES.md +0 -70
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
[](https://socket.dev/npm/package/evg_observable)
|
|
2
|
+
<a href="https://www.npmjs.com/package/evg_observable"><img alt="npm version" src="https://img.shields.io/npm/v/evg_observable.svg?style=flat-square"></a>
|
|
3
|
+
<a href="https://bundlephobia.com/result?p=evg_observable"><img alt="Bundle size" src="https://badgen.net/bundlephobia/min/evg_observable"></a>
|
|
2
4
|
<h1 align=center style="color: saddlebrown">
|
|
3
5
|
EVG Observable
|
|
4
6
|
</h1>
|
|
@@ -20,6 +22,13 @@ EVG Observable - is a light library for simple use.
|
|
|
20
22
|
- [pipe().once()](#pipeonce)
|
|
21
23
|
- [pipe().unsubscribeBy()](#pipeunsubscribebycondition)
|
|
22
24
|
- [pipe().and()](#pipeandcondition)
|
|
25
|
+
- [pipe().throttle()](#pipethrottlems)
|
|
26
|
+
- [pipe().debounce()](#pipedebouncems)
|
|
27
|
+
- [pipe().distinctUntilChanged()](#pipedistinctuntilchangedcomparator)
|
|
28
|
+
- [pipe().tap()](#pipetapfn)
|
|
29
|
+
- [pipe().take()](#pipetaken)
|
|
30
|
+
- [pipe().skip()](#pipeskipn)
|
|
31
|
+
- [pipe().scan()](#pipescankfn-seed)
|
|
23
32
|
- [pipe().toJson()](#pipetojson)
|
|
24
33
|
- [pipe().fromJson()](#pipefromjsonk)
|
|
25
34
|
- [pipe().in()](#pipeink-v)
|
|
@@ -41,14 +50,26 @@ EVG Observable - is a light library for simple use.
|
|
|
41
50
|
|
|
42
51
|
| Metric | EVG Observable | RxJS |
|
|
43
52
|
|--------|----------------|------|
|
|
44
|
-
| **Bundle size** | **
|
|
45
|
-
| **Size advantage** | **
|
|
46
|
-
| **Operations** | ~
|
|
47
|
-
| **Performance** | **2-
|
|
53
|
+
| **Bundle size** | **9.4 kB** | 63.6 kB |
|
|
54
|
+
| **Size advantage** | **6.8x smaller** | - |
|
|
55
|
+
| **Operations** | ~43 | 100+ |
|
|
56
|
+
| **Performance** | **2-6x faster** | baseline |
|
|
48
57
|
|
|
49
58
|
### Performance Comparison (Bundle vs Bundle)
|
|
50
59
|
|
|
51
|
-
Benchmarked with minified bundles on Node.js
|
|
60
|
+
Benchmarked with minified bundles on Node.js v20.18.3 (v3.1.0 API):
|
|
61
|
+
|
|
62
|
+
| Test | EVG Observable | RxJS | Advantage |
|
|
63
|
+
|------|----------------|------|-----------|
|
|
64
|
+
| Emit 100 values | 2,955K ops/sec | 508K ops/sec | **5.8x faster** |
|
|
65
|
+
| Filter + transform | 615K ops/sec | 237K ops/sec | **2.6x faster** |
|
|
66
|
+
| 10 subscribers | 21,881K ops/sec | 7,556K ops/sec | **2.9x faster** |
|
|
67
|
+
| 100 subscribers | 2,284K ops/sec | 907K ops/sec | **2.5x faster** |
|
|
68
|
+
| 1000 subscribers | 181K ops/sec | 81K ops/sec | **2.2x faster** |
|
|
69
|
+
| Large payload | 1,455K ops/sec | 370K ops/sec | **3.9x faster** |
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary>Previous results (v3.0.0 API, Node.js v22.17.1, averaged over 3 clean runs)</summary>
|
|
52
73
|
|
|
53
74
|
| Test | EVG Observable | RxJS | Advantage |
|
|
54
75
|
|------|----------------|------|-----------|
|
|
@@ -60,20 +81,6 @@ Benchmarked with minified bundles on Node.js v22.17.1 (v3.0.0 API, averaged over
|
|
|
60
81
|
| Batch emission - of(100) | 906K ops/sec | 176K ops/sec | **5.1x faster** |
|
|
61
82
|
| 5 chained filters | 19K ops/sec | 9K ops/sec | **2.1x faster** |
|
|
62
83
|
| Large payload | 879K ops/sec | 184K ops/sec | **4.8x faster** |
|
|
63
|
-
|
|
64
|
-
<details>
|
|
65
|
-
<summary>Previous results (v2.x API, measured in different conditions)</summary>
|
|
66
|
-
|
|
67
|
-
| Test | EVG Observable | RxJS | Advantage |
|
|
68
|
-
|------|----------------|------|-----------|
|
|
69
|
-
| Emit 100 values | 1,548K ops/sec | 240K ops/sec | **6.4x faster** |
|
|
70
|
-
| Filter + transform | 353K ops/sec | 164K ops/sec | **2.1x faster** |
|
|
71
|
-
| 10 subscribers | 9,078K ops/sec | 2,900K ops/sec | **3.1x faster** |
|
|
72
|
-
| 100 subscribers | 1,245K ops/sec | 336K ops/sec | **3.7x faster** |
|
|
73
|
-
| 1000 subscribers | 122K ops/sec | 33K ops/sec | **3.7x faster** |
|
|
74
|
-
| Large payload | 865K ops/sec | 199K ops/sec | **4.3x faster** |
|
|
75
|
-
|
|
76
|
-
**Note**: v3.0.0 performance is equal or better than v2.x (emit: +7%, 10 subs: +10%). The API redesign with more flexible pipe system maintains excellent performance while providing enhanced functionality.
|
|
77
84
|
</details>
|
|
78
85
|
|
|
79
86
|
### EVG Observable Advantages
|
|
@@ -90,7 +97,7 @@ Benchmarked with minified bundles on Node.js v22.17.1 (v3.0.0 API, averaged over
|
|
|
90
97
|
|
|
91
98
|
### When to use RxJS instead
|
|
92
99
|
|
|
93
|
-
RxJS is better when you need specialized operators like `
|
|
100
|
+
RxJS is better when you need specialized operators like `switchMap`, `mergeMap`, `combineLatest`, `withLatestFrom`, or schedulers for async control.
|
|
94
101
|
|
|
95
102
|
**For 80% of reactive programming tasks, EVG Observable provides sufficient functionality with significant performance and size benefits.**
|
|
96
103
|
|
|
@@ -103,7 +110,7 @@ Comparison with lightweight libraries in the same weight category (observable-fn
|
|
|
103
110
|
| Metric | EVG Observable | observable-fns |
|
|
104
111
|
|--------|----------------|----------------|
|
|
105
112
|
| **Weekly downloads** | Growing | 67K |
|
|
106
|
-
| **Bundle size (minified)** |
|
|
113
|
+
| **Bundle size (minified)** | 9.4 kB | 9.9 kB |
|
|
107
114
|
| **Implementation** | Original architecture | zen-observable re-implementation |
|
|
108
115
|
| **Dependencies** | 0 | 0 |
|
|
109
116
|
| **Architecture** | True hot observables | Cold observables (zen-observable API) |
|
|
@@ -334,6 +341,269 @@ observable$.next({message: "some message3", isNeedUnsubscribe: true});
|
|
|
334
341
|
|
|
335
342
|
Observable will send a value to the listener only if condition returns "true". There is no automatic unsubscription.
|
|
336
343
|
|
|
344
|
+
### pipe().throttle(ms)
|
|
345
|
+
|
|
346
|
+
Throttle emissions using leading-edge strategy. The first value passes immediately; subsequent values within the cooldown interval are silently dropped.
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import {Observable} from "evg_observable";
|
|
350
|
+
|
|
351
|
+
const observable$ = new Observable<string>('');
|
|
352
|
+
const received: string[] = [];
|
|
353
|
+
|
|
354
|
+
observable$
|
|
355
|
+
.pipe()
|
|
356
|
+
.throttle(300) // Only allow one value per 300ms
|
|
357
|
+
.subscribe((value: string) => received.push(value));
|
|
358
|
+
|
|
359
|
+
observable$.next("first"); // passes immediately
|
|
360
|
+
observable$.next("second"); // dropped (within 300ms)
|
|
361
|
+
observable$.next("third"); // dropped (within 300ms)
|
|
362
|
+
// After 300ms...
|
|
363
|
+
observable$.next("fourth"); // passes (interval expired)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Throttle can be combined with other pipe operators:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
observable$
|
|
370
|
+
.pipe()
|
|
371
|
+
.and(str => str.length > 1) // filter short strings
|
|
372
|
+
.throttle(500) // throttle remaining values
|
|
373
|
+
.map<number>(str => str.length) // transform to length
|
|
374
|
+
.subscribe(listener);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### pipe().debounce(ms)
|
|
378
|
+
|
|
379
|
+
Debounce emissions using trailing-edge strategy. Each new value resets the timer. The value is emitted after `ms` milliseconds of silence (no new values).
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import {Observable} from "evg_observable";
|
|
383
|
+
|
|
384
|
+
const search$ = new Observable<string>('');
|
|
385
|
+
const results: string[] = [];
|
|
386
|
+
|
|
387
|
+
search$
|
|
388
|
+
.pipe()
|
|
389
|
+
.debounce(300) // Wait 300ms of silence before emitting
|
|
390
|
+
.subscribe((value: string) => results.push(value));
|
|
391
|
+
|
|
392
|
+
search$.next("h"); // timer starts
|
|
393
|
+
search$.next("he"); // timer resets
|
|
394
|
+
search$.next("hello"); // timer resets
|
|
395
|
+
// After 300ms of silence → "hello" is emitted
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Debounce can be combined with other pipe operators:
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
observable$
|
|
402
|
+
.pipe()
|
|
403
|
+
.and(str => str.length > 2) // filter short strings first
|
|
404
|
+
.debounce(200) // debounce remaining values
|
|
405
|
+
.map<number>(str => str.length) // transform to length
|
|
406
|
+
.subscribe(listener);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### pipe().distinctUntilChanged(comparator?)
|
|
410
|
+
|
|
411
|
+
Suppresses consecutive duplicate values. A value is emitted only when it differs from the previously emitted value. The first value always passes through.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
import {Observable} from "evg_observable";
|
|
415
|
+
|
|
416
|
+
const observable$ = new Observable<number>(0);
|
|
417
|
+
const results: number[] = [];
|
|
418
|
+
|
|
419
|
+
observable$
|
|
420
|
+
.pipe()
|
|
421
|
+
.distinctUntilChanged()
|
|
422
|
+
.subscribe((value: number) => results.push(value));
|
|
423
|
+
|
|
424
|
+
observable$.next(1); // emitted (first value)
|
|
425
|
+
observable$.next(1); // suppressed (same as previous)
|
|
426
|
+
observable$.next(2); // emitted (different)
|
|
427
|
+
observable$.next(2); // suppressed (same as previous)
|
|
428
|
+
observable$.next(1); // emitted (different from 2)
|
|
429
|
+
// results: [1, 2, 1]
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
With custom comparator for objects:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
const users$ = new Observable<{id: number; name: string}>({id: 0, name: ''});
|
|
436
|
+
|
|
437
|
+
users$
|
|
438
|
+
.pipe()
|
|
439
|
+
.distinctUntilChanged((prev, curr) => prev.id === curr.id)
|
|
440
|
+
.subscribe(user => console.log(user.name));
|
|
441
|
+
|
|
442
|
+
users$.next({id: 1, name: 'Alice'}); // emitted
|
|
443
|
+
users$.next({id: 1, name: 'Alice Updated'}); // suppressed (same id)
|
|
444
|
+
users$.next({id: 2, name: 'Bob'}); // emitted
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### pipe().tap(fn)
|
|
448
|
+
|
|
449
|
+
Executes a side-effect function on the current value without modifying it. The value passes through unchanged to the next operator. Useful for logging, debugging, or triggering external actions mid-pipeline.
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import {Observable} from "evg_observable";
|
|
453
|
+
|
|
454
|
+
const observable$ = new Observable<number>(0);
|
|
455
|
+
|
|
456
|
+
observable$
|
|
457
|
+
.pipe()
|
|
458
|
+
.tap(value => console.log('before filter:', value))
|
|
459
|
+
.and(value => value > 0)
|
|
460
|
+
.tap(value => console.log('after filter:', value))
|
|
461
|
+
.map<string>(value => `Result: ${value}`)
|
|
462
|
+
.subscribe(result => console.log(result));
|
|
463
|
+
|
|
464
|
+
observable$.next(5);
|
|
465
|
+
// before filter: 5
|
|
466
|
+
// after filter: 5
|
|
467
|
+
// Result: 5
|
|
468
|
+
|
|
469
|
+
observable$.next(-1);
|
|
470
|
+
// before filter: -1
|
|
471
|
+
// (filtered out by .and(), tap after filter is not called)
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### Global debug flag pattern
|
|
475
|
+
|
|
476
|
+
Use a debug flag to enable/disable all taps at once — zero runtime cost when disabled:
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
const DEBUG = true; // set to false in production
|
|
480
|
+
|
|
481
|
+
const observable$ = new Observable<number>(0);
|
|
482
|
+
|
|
483
|
+
observable$
|
|
484
|
+
.pipe()
|
|
485
|
+
.tap(value => DEBUG && console.log('[filter-in]:', value))
|
|
486
|
+
.and(value => value > 0)
|
|
487
|
+
.tap(value => DEBUG && console.log('[filter-out]:', value))
|
|
488
|
+
.map<number>(value => value * 2)
|
|
489
|
+
.tap(value => DEBUG && console.log('[mapped]:', value))
|
|
490
|
+
.subscribe(listener);
|
|
491
|
+
|
|
492
|
+
observable$.next(5);
|
|
493
|
+
// DEBUG=true: [filter-in]: 5 → [filter-out]: 5 → [mapped]: 10
|
|
494
|
+
// DEBUG=false: (nothing logged, V8 optimizes `false && ...` to no-op)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
You can also use named taps for clarity in complex pipelines:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
const tap_log = (name: string) => (value: any) =>
|
|
501
|
+
DEBUG && console.log(`[${name}]:`, value);
|
|
502
|
+
|
|
503
|
+
observable$
|
|
504
|
+
.pipe()
|
|
505
|
+
.tap(tap_log('raw'))
|
|
506
|
+
.and(value => value > 0)
|
|
507
|
+
.tap(tap_log('filtered'))
|
|
508
|
+
.distinctUntilChanged()
|
|
509
|
+
.tap(tap_log('unique'))
|
|
510
|
+
.subscribe(listener);
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### pipe().take(n)
|
|
514
|
+
|
|
515
|
+
Passes the first N values through the pipe, then automatically unsubscribes. Generalization of `once()` — `once()` is equivalent to `take(1)`.
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
import {Observable} from "evg_observable";
|
|
519
|
+
|
|
520
|
+
const observable$ = new Observable<string>('');
|
|
521
|
+
const received: string[] = [];
|
|
522
|
+
|
|
523
|
+
observable$
|
|
524
|
+
.pipe()
|
|
525
|
+
.take(3)
|
|
526
|
+
.subscribe((value: string) => received.push(value));
|
|
527
|
+
|
|
528
|
+
observable$.next("a"); // received: ["a"]
|
|
529
|
+
observable$.next("b"); // received: ["a", "b"]
|
|
530
|
+
observable$.next("c"); // received: ["a", "b", "c"] — auto-unsubscribed
|
|
531
|
+
observable$.next("d"); // not received
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Combine with filters and transforms:
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
observable$
|
|
538
|
+
.pipe()
|
|
539
|
+
.and(str => str.length > 2) // filter short strings
|
|
540
|
+
.take(2) // take first 2 that pass
|
|
541
|
+
.subscribe(listener);
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### pipe().skip(n)
|
|
545
|
+
|
|
546
|
+
Ignores the first N values in the pipe, then passes all subsequent values through. Mirror of `take(n)`.
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
import {Observable} from "evg_observable";
|
|
550
|
+
|
|
551
|
+
const observable$ = new Observable<string>('');
|
|
552
|
+
const received: string[] = [];
|
|
553
|
+
|
|
554
|
+
observable$
|
|
555
|
+
.pipe()
|
|
556
|
+
.skip(2)
|
|
557
|
+
.subscribe((value: string) => received.push(value));
|
|
558
|
+
|
|
559
|
+
observable$.next("a"); // skipped
|
|
560
|
+
observable$.next("b"); // skipped
|
|
561
|
+
observable$.next("c"); // received: ["c"]
|
|
562
|
+
observable$.next("d"); // received: ["c", "d"]
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Combine skip + take for window slicing:
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
observable$
|
|
569
|
+
.pipe()
|
|
570
|
+
.skip(2) // skip first 2
|
|
571
|
+
.take(3) // take next 3, then unsubscribe
|
|
572
|
+
.subscribe(listener);
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### pipe().scan<K>(fn, seed)
|
|
576
|
+
|
|
577
|
+
Accumulator operator — each value passes through a reducer function, the accumulated result is emitted. Like `Array.reduce()` for streams.
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
import {Observable} from "evg_observable";
|
|
581
|
+
|
|
582
|
+
const observable$ = new Observable<number>(0);
|
|
583
|
+
const sums: number[] = [];
|
|
584
|
+
|
|
585
|
+
observable$
|
|
586
|
+
.pipe()
|
|
587
|
+
.scan<number>((acc, val) => acc + val, 0)
|
|
588
|
+
.subscribe((sum: number) => sums.push(sum));
|
|
589
|
+
|
|
590
|
+
observable$.next(1); // sums: [1]
|
|
591
|
+
observable$.next(2); // sums: [1, 3]
|
|
592
|
+
observable$.next(3); // sums: [1, 3, 6]
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Type-changing scan (string → number):
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
const observable$ = new Observable<string>('');
|
|
599
|
+
|
|
600
|
+
observable$
|
|
601
|
+
.pipe()
|
|
602
|
+
.scan<number>((acc, str) => acc + str.length, 0)
|
|
603
|
+
.and(total => total > 5)
|
|
604
|
+
.subscribe(total => console.log('Total chars:', total));
|
|
605
|
+
```
|
|
606
|
+
|
|
337
607
|
### pipe().toJson()
|
|
338
608
|
|
|
339
609
|
To convert the observable's data to JSON format, you can use the serialize method. This method turns the observer's
|
|
@@ -805,7 +1075,14 @@ observable$.next(-1); // Listener 1 throws, listener 2 still receives
|
|
|
805
1075
|
| `.choice()` | SwitchCase object | transitions the pipe into switch-case mode. In this mode, only the first condition that returns a positive result is triggered, and all others are ignored. This allows you to handle multiple cases more conveniently. |
|
|
806
1076
|
| `.or(*condition)` | PipeCase object | Adds a condition to the chain of cases. The entire chain operates on the principle of "OR". This is different from other pipe methods which, when chained, operate on the principle of "AND". |
|
|
807
1077
|
| `.anyOf(*conditions)` | PipeCase object | This method allows you to add a group of conditions for filtering cases data in the pipeline chain. |
|
|
1078
|
+
| `.throttle(ms: number)` | pipe object | Throttles emissions using leading-edge strategy. First value passes immediately, subsequent values within `ms` interval are dropped. |
|
|
1079
|
+
| `.debounce(ms: number)` | pipe object | Debounces emissions using trailing-edge strategy. Each new value resets the timer. Emits after `ms` milliseconds of silence. |
|
|
1080
|
+
| `.distinctUntilChanged(comparator?)` | pipe object | Suppresses consecutive duplicate values. Optional `comparator(prev, curr) => boolean` for custom equality. Defaults to `===`. |
|
|
1081
|
+
| `.tap(fn: ICallback<T>)` | pipe object | Executes a side-effect function without modifying the value. The value passes through unchanged. |
|
|
1082
|
+
| `.take(n: number)` | pipe object | Passes the first N values, then auto-unsubscribes. Generalization of `once()`. |
|
|
1083
|
+
| `.skip(n: number)` | pipe object | Ignores the first N values, then passes all subsequent values through. Mirror of `take()`. |
|
|
808
1084
|
| `.map<K>(transform: ICallback<T>)` | Observable instance with new data type | This method allows transforming payload data in the pipe chain by applying user callback function. `transform` should be a function that takes the current data and returns transformed data of possibly another type. |
|
|
1085
|
+
| `.scan<K>(fn, seed)` | pipe object with new type K | Accumulator — each value passes through reducer `fn(acc, val) => newAcc`, emits accumulated result. Like `Array.reduce()` for streams. |
|
|
809
1086
|
| `.toJson()` | pipe object | Converts the observers data into a JSON string. |
|
|
810
1087
|
| `.fromJson<K>()` | pipe object | Converts a JSON string into an object of type K. |
|
|
811
1088
|
| `.of<K, V>(transform?: ICallback<K>)`| pipe object | Iterates over array elements. For each element, emits it to subscribers. Optional transform function processes each element before emission. |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evg_observable",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "Lightweight reactive Observable library (zero dependencies) — 2-7x faster than RxJS, 1.5-3x faster than observable-fns. Pipe operators: throttle, debounce, distinctUntilChanged, map, tap. Original hot-observable architecture, not a fork or wrapper.",
|
|
5
5
|
"main": "src/outLib/index.js",
|
|
6
6
|
"types": "src/outLib/index.d.ts",
|
|
7
7
|
"directories": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"benchmark:patterns-edge": "ts-node benchmarks/benchmark-patterns-edge-cases.ts",
|
|
21
21
|
"benchmark:patterns-clean": "ts-node benchmarks/benchmark-patterns-clean.ts",
|
|
22
22
|
"benchmark:patterns-final": "ts-node benchmarks/benchmark-patterns-fixed.ts",
|
|
23
|
-
"benchmark:patterns-vs-competitors": "ts-node benchmarks/benchmark-patterns-vs-competitors.ts"
|
|
23
|
+
"benchmark:patterns-vs-competitors": "ts-node benchmarks/benchmark-patterns-vs-competitors.ts",
|
|
24
|
+
"bundle": "esbuild src/browser-entry.ts --bundle --minify --format=iife --outfile=repo/evg_observable.js",
|
|
25
|
+
"bundle:watch": "esbuild src/browser-entry.ts --bundle --minify --format=iife --outfile=repo/evg_observable.js --watch"
|
|
24
26
|
},
|
|
25
27
|
"repository": {
|
|
26
28
|
"type": "git",
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
"@types/chai": "^4.3.4",
|
|
39
41
|
"benchmark": "^2.1.4",
|
|
40
42
|
"chai": "^4.3.7",
|
|
43
|
+
"esbuild": "^0.27.4",
|
|
41
44
|
"microtime": "^3.1.1",
|
|
42
45
|
"mocha": "^11.7.5",
|
|
43
46
|
"nyc": "^17.1.0",
|
|
@@ -47,17 +50,23 @@
|
|
|
47
50
|
"typescript": "^5.4.5"
|
|
48
51
|
},
|
|
49
52
|
"keywords": [
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
53
|
+
"observable",
|
|
54
|
+
"reactive",
|
|
55
|
+
"event-emitter",
|
|
56
|
+
"rxjs-alternative",
|
|
57
|
+
"lightweight",
|
|
58
|
+
"pub-sub",
|
|
59
|
+
"subscribe",
|
|
60
|
+
"pipe",
|
|
61
|
+
"filter",
|
|
62
|
+
"map",
|
|
63
|
+
"throttle",
|
|
64
|
+
"debounce",
|
|
65
|
+
"distinctUntilChanged",
|
|
66
|
+
"tap",
|
|
67
|
+
"state-management",
|
|
68
|
+
"event-stream",
|
|
69
|
+
"typescript",
|
|
70
|
+
"zero-dependencies"
|
|
62
71
|
]
|
|
63
72
|
}
|
package/repo/evg_observable.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";(()=>{function y(t,e){return t.order>e.order?1:t.order<e.order?-1:0}function v(t,e){return t.order>e.order?-1:t.order<e.order?1:0}function o(t,e){let r=t.indexOf(e);return r===-1?!1:(t.splice(r,1),!0)}function O(t){if(Array.isArray(t)){let e=t.length,r=new Array(e);for(let i=0;i<e;i++)r[i]=k(t[i]);return i=>{for(let s=0;s<e;s++)r[s](i)}}return k(t)}function k(t){return"next"in t?e=>t.next(e):t}var c=class{pipe;counter;constructor(e){this.pipe=e,this.counter=e.chain.length?e.chain.length:0}or(e){this.counter++;let r=this.counter,i=this.pipe.chain;return i.push(s=>{s.isAvailable=!0,e(s.payload)&&(s.isBreak=!0),r===i.length&&!s.isBreak&&(s.isAvailable=!1)}),this}anyOf(e){if(!Array.isArray(e))return this;for(let r=0;r<e.length;r++)this.or(e[r]);return this}};var I=class{chain=[];flow={isBreak:!1,isUnsubscribe:!1,isAvailable:!1,debounceMs:0,debounceTimer:0,debounceValue:void 0,debounceIndex:0,payload:null};push(e){return this.chain.push(e),this}once(){return this.push(e=>{this.listener(e.payload),e.isUnsubscribe=!0})}take(e){e<0&&(e=0);let r=0;return this.push(i=>{if(r>=e){i.isUnsubscribe=!0;return}r++,this.listener(i.payload),r>=e&&(i.isUnsubscribe=!0)})}skip(e){e<0&&(e=0);let r=0;return this.push(i=>{if(r<e){r++;return}i.isAvailable=!0})}unsubscribeBy(e){return this.push(r=>{r.isAvailable=!0,e(r.payload)&&(r.isUnsubscribe=!0)})}and(e){return this.push(r=>e(r.payload)&&(r.isAvailable=!0))}allOf(e){if(!Array.isArray(e))return this;for(let r=0;r<e.length;r++)this.and(e[r]);return this}choice(){return new T(this)}map(e){return this.push(r=>{r.payload=e(r.payload),r.isAvailable=!0})}scan(e,r){let i=r;return this.push(s=>{i=e(i,s.payload),s.payload=i,s.isAvailable=!0})}tap(e){return this.push(r=>{e(r.payload),r.isAvailable=!0})}throttle(e){let r=0;return this.push(i=>{let s=Date.now();s-r>=e&&(r=s,i.isAvailable=!0)})}debounce(e){return this.push(r=>{r.isAvailable=!0,r.debounceMs=e})}distinctUntilChanged(e){let r=!1,i;return this.push(s=>{let n=s.payload;r&&(e?e(i,n):i===n)||(r=!0,i=n,s.isAvailable=!0)})}toJson(){return this.push(e=>{e.payload=JSON.stringify(e.payload),e.isAvailable=!0})}fromJson(){return this.push(e=>{e.payload=JSON.parse(e.payload),e.isAvailable=!0})}group(){return this}processChain(e){let r=this.chain,i=this.flow,s=r.length;for(let n=0;n<s;n++){if(i.isUnsubscribe=!1,i.isAvailable=!1,i.debounceMs=0,r[n](i),i.isUnsubscribe)return this.unsubscribe();if(i.debounceMs>0){i.debounceValue=i.payload,i.debounceIndex=n+1;let l=()=>{i.debounceTimer=0,i.payload=i.debounceValue,i.isBreak=!1;for(let a=i.debounceIndex;a<s;a++){if(i.isUnsubscribe=!1,i.isAvailable=!1,i.debounceMs=0,r[a](i),i.isUnsubscribe)return this.unsubscribe();if(i.debounceMs>0){i.debounceValue=i.payload,i.debounceIndex=a+1,clearTimeout(i.debounceTimer),i.debounceTimer=setTimeout(l,i.debounceMs);return}if(!i.isAvailable)return;if(i.isBreak)break}e&&e(i.payload)};clearTimeout(i.debounceTimer),i.debounceTimer=setTimeout(l,i.debounceMs);return}if(!i.isAvailable)return;if(i.isBreak)break}i.isAvailable=!0,e&&e(i.payload)}},T=class extends c{subscribe(e,r){return this.pipe.subscribe(e,r)}group(){return this.pipe}};var u=class extends I{observer;listener;errorHandler=(e,r)=>{console.log(`(Unit of SubscribeObject).send(${e}) ERROR:`,r)};_order=0;paused=!1;piped=!1;listeners;errorHandlers;constructor(e,r){super(),this.observer=e,this.piped=!!r}subscribe(e,r){return this.listener=O(e),r&&(this.errorHandler=r),this}add(e,r){if(this.listeners||(this.listeners=[],this.errorHandlers=[]),Array.isArray(e))for(let i=0;i<e.length;i++){this.listeners.push(e[i]);let s=r&&Array.isArray(r)?r[i]??this.errorHandler:r||this.errorHandler;this.errorHandlers.push(s)}else{this.listeners.push(e);let i=r&&!Array.isArray(r)?r:this.errorHandler;this.errorHandlers.push(i)}return this}unsubscribe(){this.observer&&(clearTimeout(this.flow.debounceTimer),this.observer.unSubscribe(this),this.observer=null,this.listener=null,this.chain.length=0)}send(e){let r=this.listener,i=this.listeners&&this.listeners.length>0;if(!r&&!i){this.unsubscribe();return}if(!(!this.observer||this.paused)){if(!this.piped){if(r)try{r(e)}catch(s){this.errorHandler(e,s)}return}try{if(this.flow.payload=e,this.flow.isBreak=!1,i){let s=this.listeners,n=this.errorHandlers;this.processChain(l=>{r&&r(l);for(let a=0;a<s.length;a++)try{s[a](l)}catch(C){n[a](l,C)}})}else this.processChain(r)}catch(s){this.errorHandler(e,s)}}}resume(){this.paused=!1}pause(){this.paused=!0}get order(){return this._order}set order(e){this._order=e}};var f=class{chain=[];flow={isBreak:!1,isAvailable:!1,payload:null};response={isOK:!1,payload:void 0};errHandler;get isEmpty(){return!this.chain.length}push(e){return this.chain.push(e),this}and(e){return this.push(r=>e(r.payload)&&(r.isAvailable=!0))}allOf(e){if(!Array.isArray(e))return this;for(let r=0;r<e.length;r++)this.and(e[r]);return this}choice(){return new m(this)}processChain(e){let r=this.chain,i=this.flow,s=this.response;s.isOK=!1,s.payload=void 0,i.payload=e,i.isBreak=!1;try{let n=r.length;for(let l=0;l<n;l++){if(i.isAvailable=!1,r[l](i),!i.isAvailable)return s;if(i.isBreak)break}}catch(n){return this.errHandler?this.errHandler(n,"Filter.processChain ERROR:"):console.log("Filter.processChain ERROR:",n),s}return s.isOK=!0,s.payload=i.payload,s}addErrorHandler(e){this.errHandler=e}},m=class extends c{};var b=class{subs=[];enabled=!0;killed=!1;process=!1;trash=[];filters=new f;_value;constructor(e){this._value=e}addFilter(e){return e&&this.filters.addErrorHandler(e),this.filters}disable(){this.enabled=!1}enable(){this.enabled=!0}get isEnable(){return this.enabled}next(e){if(this.killed||!this.enabled||!this.subs.length||!this.filters.isEmpty&&!this.filters.processChain(e).isOK)return;this.process=!0,this._value=e;let r=this.subs,i=r.length;for(let s=0;s<i;s++)r[s].send(e);this.process=!1,this.trash.length&&this.clearTrash()}of(e){if(!this.killed&&this.enabled)for(let r=0;r<e.length;r++)this.next(e[r])}in(e){if(!this.killed&&this.enabled)for(let r in e)Object.hasOwn(e,r)&&this.next([r,e[r]])}clearTrash(){let e=this.trash.length;for(let r=0;r<e;r++)this.unSubscribe(this.trash[r]);this.trash.length=0}unSubscribe(e){if(!this.killed){if(this.process&&e){this.trash.push(e);return}this.subs&&o(this.subs,e)}}destroy(){if(!this.killed){if(this.killed=!0,!this.process){this.clearDebounceTimers(),this._value=null,this.subs.length=0;return}Promise.resolve().then(()=>{this.clearDebounceTimers(),this._value=null,this.subs.length=0})}}unsubscribeAll(){if(!this.killed){if(this.process){this.clearDebounceTimers();let e=this.subs;for(let r=0;r<e.length;r++)this.trash.push(e[r]);return}this.clearDebounceTimers(),this.subs.length=0}}clearDebounceTimers(){let e=this.subs;for(let r=0;r<e.length;r++)clearTimeout(e[r].flow.debounceTimer)}getValue(){if(!this.killed)return this._value}size(){return this.killed?0:this.subs.length}subscribe(e,r){if(this.killed||!this.isListener(e))return;let i=new u(this,!1);return this.addObserver(i,e,r),i}addObserver(e,r,i){e.subscribe(r,i),this.subs.push(e)}isListener(e){return this.killed?!1:!!e}pipe(){if(this.killed)return;let e=new u(this,!0);return this.subs.push(e),e}get isDestroyed(){return this.killed}};var d=class extends u{constructor(e,r){super(e,r)}get order(){return this._order}set order(e){if(!this.observer||this.observer&&this.observer.isDestroyed){this._order=void 0;return}this._order=e,this.observer.sortByOrder()}subscribe(e,r){return super.subscribe(e,r),this}once(){return super.once()}take(e){return super.take(e)}skip(e){return super.skip(e)}scan(e,r){return super.scan(e,r)}};var h=class extends b{sortDirection=y;ascendingSort(){return this.sortDirection=y,this.sortByOrder()}descendingSort(){return this.sortDirection=v,this.sortByOrder()}sortByOrder(){return this.killed?!1:(this.subs.sort(this.sortDirection),!0)}subscribe(e,r){if(!this.isListener(e))return;let i=new d(this,!1);return this.addObserver(i,e,r),i}pipe(){if(this.killed)return;let e=new d(this,!0);return this.subs.push(e),e}unSubscribe(e){if(!this.killed){if(this.process&&e){this.trash.push(e);return}this.subs&&o(this.subs,e)}}};var p=class{arr=[];killed=!1;collect(...e){this.killed||this.arr.push(...e)}unsubscribe(e){this.killed||(e?.unsubscribe(),o(this.arr,e))}unsubscribeAll(){if(this.killed)return;let e=this.arr;for(let r=0;r<e.length;r++)e[r].unsubscribe();e.length=0}size(){return this.killed?0:this.arr.length}destroy(){this.unsubscribeAll(),this.arr.length=0,this.arr=0,this.killed=!0}get isDestroyed(){return this.killed}};var S=window;S.Observable=b;S.OrderedObservable=h;S.Collector=p;})();
|
|
@@ -36,11 +36,12 @@ function quickDeleteFromArray(arr, component) {
|
|
|
36
36
|
}
|
|
37
37
|
function getListener(listenerGroup) {
|
|
38
38
|
if (Array.isArray(listenerGroup)) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const len = listenerGroup.length;
|
|
40
|
+
const group = new Array(len);
|
|
41
|
+
for (let i = 0; i < len; i++)
|
|
42
|
+
group[i] = wrapListener(listenerGroup[i]);
|
|
42
43
|
return (data) => {
|
|
43
|
-
for (let i = 0; i <
|
|
44
|
+
for (let i = 0; i < len; i++)
|
|
44
45
|
group[i](data);
|
|
45
46
|
};
|
|
46
47
|
}
|
|
@@ -21,6 +21,7 @@ export declare class Observable<T> implements IObserver<T>, IStream<T>, IAddFilt
|
|
|
21
21
|
unSubscribe(listener: ISubscriptionLike): void;
|
|
22
22
|
destroy(): void;
|
|
23
23
|
unsubscribeAll(): void;
|
|
24
|
+
protected clearDebounceTimers(): void;
|
|
24
25
|
getValue(): T | undefined;
|
|
25
26
|
size(): number;
|
|
26
27
|
subscribe(observer: ISubscribeGroup<T>, errorHandler?: IErrorCallback): ISubscriptionLike | undefined;
|
package/src/outLib/Observable.js
CHANGED
|
@@ -86,11 +86,13 @@ class Observable {
|
|
|
86
86
|
return;
|
|
87
87
|
this.killed = true;
|
|
88
88
|
if (!this.process) {
|
|
89
|
+
this.clearDebounceTimers();
|
|
89
90
|
this._value = null;
|
|
90
91
|
this.subs.length = 0;
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
Promise.resolve().then(() => {
|
|
95
|
+
this.clearDebounceTimers();
|
|
94
96
|
this._value = null;
|
|
95
97
|
this.subs.length = 0;
|
|
96
98
|
});
|
|
@@ -99,13 +101,20 @@ class Observable {
|
|
|
99
101
|
if (this.killed)
|
|
100
102
|
return;
|
|
101
103
|
if (this.process) {
|
|
104
|
+
this.clearDebounceTimers();
|
|
102
105
|
const subs = this.subs;
|
|
103
106
|
for (let i = 0; i < subs.length; i++)
|
|
104
107
|
this.trash.push(subs[i]);
|
|
105
108
|
return;
|
|
106
109
|
}
|
|
110
|
+
this.clearDebounceTimers();
|
|
107
111
|
this.subs.length = 0;
|
|
108
112
|
}
|
|
113
|
+
clearDebounceTimers() {
|
|
114
|
+
const subs = this.subs;
|
|
115
|
+
for (let i = 0; i < subs.length; i++)
|
|
116
|
+
clearTimeout(subs[i].flow.debounceTimer);
|
|
117
|
+
}
|
|
109
118
|
getValue() {
|
|
110
119
|
if (this.killed)
|
|
111
120
|
return undefined;
|
|
@@ -7,4 +7,7 @@ export declare class OrderedSubscribeObject<T> extends SubscribeObject<T> implem
|
|
|
7
7
|
set order(value: number);
|
|
8
8
|
subscribe(observer: IListener<T> | ISetObservableValue, errorHandler?: IErrorCallback): IOrderedSubscriptionLike;
|
|
9
9
|
once(): IOrderedSubscribe<T>;
|
|
10
|
+
take(n: number): IOrderedSubscribe<T>;
|
|
11
|
+
skip(n: number): IOrderedSetup<T>;
|
|
12
|
+
scan<K>(fn: (accumulator: K, value: T) => K, seed: K): IOrderedSetup<K>;
|
|
10
13
|
}
|
|
@@ -25,5 +25,14 @@ class OrderedSubscribeObject extends SubscribeObject_1.SubscribeObject {
|
|
|
25
25
|
once() {
|
|
26
26
|
return super.once();
|
|
27
27
|
}
|
|
28
|
+
take(n) {
|
|
29
|
+
return super.take(n);
|
|
30
|
+
}
|
|
31
|
+
skip(n) {
|
|
32
|
+
return super.skip(n);
|
|
33
|
+
}
|
|
34
|
+
scan(fn, seed) {
|
|
35
|
+
return super.scan(fn, seed);
|
|
36
|
+
}
|
|
28
37
|
}
|
|
29
38
|
exports.OrderedSubscribeObject = OrderedSubscribeObject;
|
package/src/outLib/Pipe.d.ts
CHANGED
|
@@ -6,15 +6,22 @@ export declare abstract class Pipe<T> implements ISubscribe<T> {
|
|
|
6
6
|
abstract subscribe(listener: IListener<T> | ISetObservableValue, errorHandler?: IErrorCallback): ISubscriptionLike | undefined;
|
|
7
7
|
private push;
|
|
8
8
|
once(): ISubscribe<T>;
|
|
9
|
+
take(n: number): ISubscribe<T>;
|
|
10
|
+
skip(n: number): ISetup<T>;
|
|
9
11
|
unsubscribeBy(condition: ICallback<T>): ISetup<T>;
|
|
10
12
|
and(condition: ICallback<T>): ISetup<T>;
|
|
11
13
|
allOf(conditions: ICallback<any>[]): ISetup<T>;
|
|
12
14
|
choice(): PipeSwitchCase<T>;
|
|
13
15
|
map<K>(condition: ICallback<T>): ISetup<K>;
|
|
16
|
+
scan<K>(fn: (accumulator: K, value: T) => K, seed: K): ISetup<K>;
|
|
17
|
+
tap(fn: ICallback<T>): ISetup<T>;
|
|
18
|
+
throttle(ms: number): ISetup<T>;
|
|
19
|
+
debounce(ms: number): ISetup<T>;
|
|
20
|
+
distinctUntilChanged(comparator?: (previous: T, current: T) => boolean): ISetup<T>;
|
|
14
21
|
toJson(): ISetup<string>;
|
|
15
22
|
fromJson<K>(): ISetup<K>;
|
|
16
23
|
group(): IGroupSubscription<T>;
|
|
17
|
-
processChain(listener
|
|
24
|
+
processChain(listener?: IListener<T>): void;
|
|
18
25
|
}
|
|
19
26
|
export declare class PipeSwitchCase<T> extends SwitchCase<T, Pipe<T>, IPipeCase<T>> implements ISubscribe<T> {
|
|
20
27
|
subscribe(listener: IListener<T> | ISetObservableValue, errorHandler?: IErrorCallback): ISubscriptionLike | undefined;
|
package/src/outLib/Pipe.js
CHANGED
|
@@ -4,7 +4,16 @@ exports.PipeSwitchCase = exports.Pipe = void 0;
|
|
|
4
4
|
const AbstractSwitchCase_1 = require("./AbstractSwitchCase");
|
|
5
5
|
class Pipe {
|
|
6
6
|
chain = [];
|
|
7
|
-
flow = {
|
|
7
|
+
flow = {
|
|
8
|
+
isBreak: false,
|
|
9
|
+
isUnsubscribe: false,
|
|
10
|
+
isAvailable: false,
|
|
11
|
+
debounceMs: 0,
|
|
12
|
+
debounceTimer: 0,
|
|
13
|
+
debounceValue: undefined,
|
|
14
|
+
debounceIndex: 0,
|
|
15
|
+
payload: null
|
|
16
|
+
};
|
|
8
17
|
push(callback) {
|
|
9
18
|
this.chain.push(callback);
|
|
10
19
|
return this;
|
|
@@ -15,6 +24,33 @@ class Pipe {
|
|
|
15
24
|
data.isUnsubscribe = true;
|
|
16
25
|
});
|
|
17
26
|
}
|
|
27
|
+
take(n) {
|
|
28
|
+
if (n < 0)
|
|
29
|
+
n = 0;
|
|
30
|
+
let count = 0;
|
|
31
|
+
return this.push((data) => {
|
|
32
|
+
if (count >= n) {
|
|
33
|
+
data.isUnsubscribe = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
count++;
|
|
37
|
+
this.listener(data.payload);
|
|
38
|
+
if (count >= n)
|
|
39
|
+
data.isUnsubscribe = true;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
skip(n) {
|
|
43
|
+
if (n < 0)
|
|
44
|
+
n = 0;
|
|
45
|
+
let count = 0;
|
|
46
|
+
return this.push((data) => {
|
|
47
|
+
if (count < n) {
|
|
48
|
+
count++;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
data.isAvailable = true;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
18
54
|
unsubscribeBy(condition) {
|
|
19
55
|
return this.push((data) => {
|
|
20
56
|
data.isAvailable = true;
|
|
@@ -41,6 +77,53 @@ class Pipe {
|
|
|
41
77
|
data.isAvailable = true;
|
|
42
78
|
});
|
|
43
79
|
}
|
|
80
|
+
scan(fn, seed) {
|
|
81
|
+
let accumulator = seed;
|
|
82
|
+
return this.push((data) => {
|
|
83
|
+
accumulator = fn(accumulator, data.payload);
|
|
84
|
+
data.payload = accumulator;
|
|
85
|
+
data.isAvailable = true;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
tap(fn) {
|
|
89
|
+
return this.push((data) => {
|
|
90
|
+
fn(data.payload);
|
|
91
|
+
data.isAvailable = true;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
throttle(ms) {
|
|
95
|
+
let lastEmitTime = 0;
|
|
96
|
+
return this.push((data) => {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
if (now - lastEmitTime >= ms) {
|
|
99
|
+
lastEmitTime = now;
|
|
100
|
+
data.isAvailable = true;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
debounce(ms) {
|
|
105
|
+
return this.push((data) => {
|
|
106
|
+
data.isAvailable = true;
|
|
107
|
+
data.debounceMs = ms;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
distinctUntilChanged(comparator) {
|
|
111
|
+
let hasPrevious = false;
|
|
112
|
+
let previousValue;
|
|
113
|
+
return this.push((data) => {
|
|
114
|
+
const current = data.payload;
|
|
115
|
+
if (hasPrevious) {
|
|
116
|
+
const isSame = comparator
|
|
117
|
+
? comparator(previousValue, current)
|
|
118
|
+
: previousValue === current;
|
|
119
|
+
if (isSame)
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
hasPrevious = true;
|
|
123
|
+
previousValue = current;
|
|
124
|
+
data.isAvailable = true;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
44
127
|
toJson() {
|
|
45
128
|
return this.push((data) => {
|
|
46
129
|
data.payload = JSON.stringify(data.payload);
|
|
@@ -63,15 +146,51 @@ class Pipe {
|
|
|
63
146
|
for (let i = 0; i < len; i++) {
|
|
64
147
|
data.isUnsubscribe = false;
|
|
65
148
|
data.isAvailable = false;
|
|
149
|
+
data.debounceMs = 0;
|
|
66
150
|
chain[i](data);
|
|
67
151
|
if (data.isUnsubscribe)
|
|
68
152
|
return this.unsubscribe();
|
|
153
|
+
if (data.debounceMs > 0) {
|
|
154
|
+
data.debounceValue = data.payload;
|
|
155
|
+
data.debounceIndex = i + 1;
|
|
156
|
+
const continueChain = () => {
|
|
157
|
+
data.debounceTimer = 0;
|
|
158
|
+
data.payload = data.debounceValue;
|
|
159
|
+
data.isBreak = false;
|
|
160
|
+
for (let j = data.debounceIndex; j < len; j++) {
|
|
161
|
+
data.isUnsubscribe = false;
|
|
162
|
+
data.isAvailable = false;
|
|
163
|
+
data.debounceMs = 0;
|
|
164
|
+
chain[j](data);
|
|
165
|
+
if (data.isUnsubscribe)
|
|
166
|
+
return this.unsubscribe();
|
|
167
|
+
if (data.debounceMs > 0) {
|
|
168
|
+
data.debounceValue = data.payload;
|
|
169
|
+
data.debounceIndex = j + 1;
|
|
170
|
+
clearTimeout(data.debounceTimer);
|
|
171
|
+
data.debounceTimer = setTimeout(continueChain, data.debounceMs);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!data.isAvailable)
|
|
175
|
+
return;
|
|
176
|
+
if (data.isBreak)
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (listener)
|
|
180
|
+
listener(data.payload);
|
|
181
|
+
};
|
|
182
|
+
clearTimeout(data.debounceTimer);
|
|
183
|
+
data.debounceTimer = setTimeout(continueChain, data.debounceMs);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
69
186
|
if (!data.isAvailable)
|
|
70
187
|
return;
|
|
71
188
|
if (data.isBreak)
|
|
72
189
|
break;
|
|
73
190
|
}
|
|
74
|
-
|
|
191
|
+
data.isAvailable = true;
|
|
192
|
+
if (listener)
|
|
193
|
+
listener(data.payload);
|
|
75
194
|
}
|
|
76
195
|
}
|
|
77
196
|
exports.Pipe = Pipe;
|
|
@@ -50,6 +50,7 @@ class SubscribeObject extends Pipe_1.Pipe {
|
|
|
50
50
|
unsubscribe() {
|
|
51
51
|
if (!this.observer)
|
|
52
52
|
return;
|
|
53
|
+
clearTimeout(this.flow.debounceTimer);
|
|
53
54
|
this.observer.unSubscribe(this);
|
|
54
55
|
this.observer = null;
|
|
55
56
|
this.listener = null;
|
|
@@ -73,53 +74,29 @@ class SubscribeObject extends Pipe_1.Pipe {
|
|
|
73
74
|
this.errorHandler(value, err);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
|
-
if (hasGroupListeners) {
|
|
77
|
-
for (let i = 0; i < this.listeners.length; i++) {
|
|
78
|
-
try {
|
|
79
|
-
this.listeners[i](value);
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
this.errorHandlers[i](value, err);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
77
|
return;
|
|
87
78
|
}
|
|
88
79
|
try {
|
|
89
80
|
this.flow.payload = value;
|
|
90
81
|
this.flow.isBreak = false;
|
|
91
|
-
if (listener) {
|
|
92
|
-
this.processChain(listener);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
const chain = this.chain;
|
|
96
|
-
const data = this.flow;
|
|
97
|
-
const len = chain.length;
|
|
98
|
-
data.isAvailable = len === 0;
|
|
99
|
-
for (let i = 0; i < len; i++) {
|
|
100
|
-
data.isUnsubscribe = false;
|
|
101
|
-
data.isAvailable = false;
|
|
102
|
-
chain[i](data);
|
|
103
|
-
if (data.isUnsubscribe) {
|
|
104
|
-
this.unsubscribe();
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (!data.isAvailable)
|
|
108
|
-
return;
|
|
109
|
-
if (data.isBreak)
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
82
|
if (hasGroupListeners) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
83
|
+
const groupListeners = this.listeners;
|
|
84
|
+
const groupErrorHandlers = this.errorHandlers;
|
|
85
|
+
this.processChain((value) => {
|
|
86
|
+
if (listener)
|
|
87
|
+
listener(value);
|
|
88
|
+
for (let i = 0; i < groupListeners.length; i++) {
|
|
89
|
+
try {
|
|
90
|
+
groupListeners[i](value);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
groupErrorHandlers[i](value, err);
|
|
94
|
+
}
|
|
121
95
|
}
|
|
122
|
-
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.processChain(listener);
|
|
123
100
|
}
|
|
124
101
|
}
|
|
125
102
|
catch (err) {
|
package/src/outLib/Types.d.ts
CHANGED
|
@@ -31,6 +31,24 @@ export type IOnce<T> = {
|
|
|
31
31
|
export type IOrderedOnce<T> = {
|
|
32
32
|
once(): IOrderedSubscribe<T>;
|
|
33
33
|
};
|
|
34
|
+
export type ITake<T> = {
|
|
35
|
+
take(n: number): ISubscribe<T>;
|
|
36
|
+
};
|
|
37
|
+
export type IOrderedTake<T> = {
|
|
38
|
+
take(n: number): IOrderedSubscribe<T>;
|
|
39
|
+
};
|
|
40
|
+
export type ISkip<T> = {
|
|
41
|
+
skip(n: number): ISetup<T>;
|
|
42
|
+
};
|
|
43
|
+
export type IOrderedSkip<T> = {
|
|
44
|
+
skip(n: number): IOrderedSetup<T>;
|
|
45
|
+
};
|
|
46
|
+
export type IScan<T> = {
|
|
47
|
+
scan<K>(fn: (accumulator: K, value: T) => K, seed: K): ISetup<K>;
|
|
48
|
+
};
|
|
49
|
+
export type IOrderedScan<T> = {
|
|
50
|
+
scan<K>(fn: (accumulator: K, value: T) => K, seed: K): IOrderedSetup<K>;
|
|
51
|
+
};
|
|
34
52
|
export type ISetObservableValue = {
|
|
35
53
|
next(value: any): void;
|
|
36
54
|
};
|
|
@@ -40,8 +58,8 @@ export type ISubscriptionLike = {
|
|
|
40
58
|
export type IGroupSubscription<T> = ISubscriptionLike & {
|
|
41
59
|
add(listener: IListener<T> | IListener<T>[], errorHandler?: IErrorCallback | IErrorCallback[]): IGroupSubscription<T>;
|
|
42
60
|
};
|
|
43
|
-
export type ISetup<T> = IUnsubscribeByPositive<T> & IEmitByPositive<T> & IOnce<T> & ISwitch<T> & ITransform<T> & ISerialisation & IGroup<T> & ISubscribe<T>;
|
|
44
|
-
export type IOrderedSetup<T> = IOrderedUnsubscribeByPositive<T> & IOrderedEmitByPositive<T> & IOrderedOnce<T> & IOrderedSwitch<T> & IOrderedTransform<T> & IOrderedSerialisation & IOrderedGroup<T> & IOrderedSubscribe<T>;
|
|
61
|
+
export type ISetup<T> = IUnsubscribeByPositive<T> & IEmitByPositive<T> & IOnce<T> & ITake<T> & ISkip<T> & IScan<T> & ISwitch<T> & ITransform<T> & IThrottle<T> & IDebounce<T> & IDistinctUntilChanged<T> & ITap<T> & ISerialisation & IGroup<T> & ISubscribe<T>;
|
|
62
|
+
export type IOrderedSetup<T> = IOrderedUnsubscribeByPositive<T> & IOrderedEmitByPositive<T> & IOrderedOnce<T> & IOrderedTake<T> & IOrderedSkip<T> & IOrderedScan<T> & IOrderedSwitch<T> & IOrderedTransform<T> & IOrderedThrottle<T> & IOrderedDebounce<T> & IOrderedDistinctUntilChanged<T> & IOrderedTap<T> & IOrderedSerialisation & IOrderedGroup<T> & IOrderedSubscribe<T>;
|
|
45
63
|
export type ISubscribeObject<T> = ISubscriptionLike & IPause & IOrder & ISend<T> & ISetup<T>;
|
|
46
64
|
export type ISubscribeCounter = {
|
|
47
65
|
size(): number;
|
|
@@ -100,6 +118,9 @@ export type IEmitByPositive<T> = {
|
|
|
100
118
|
export type ITransform<T> = {
|
|
101
119
|
map<K>(condition: ICallback<T>): ISetup<K>;
|
|
102
120
|
};
|
|
121
|
+
export type IThrottle<T> = {
|
|
122
|
+
throttle(ms: number): ISetup<T>;
|
|
123
|
+
};
|
|
103
124
|
export type ISerialisation = {
|
|
104
125
|
toJson(): ISetup<string>;
|
|
105
126
|
fromJson<K>(): ISetup<K>;
|
|
@@ -111,6 +132,27 @@ export type IOrderedEmitByPositive<T> = {
|
|
|
111
132
|
export type IOrderedTransform<T> = {
|
|
112
133
|
map<K>(condition: ICallback<T>): ISetup<K>;
|
|
113
134
|
};
|
|
135
|
+
export type IOrderedThrottle<T> = {
|
|
136
|
+
throttle(ms: number): ISetup<T>;
|
|
137
|
+
};
|
|
138
|
+
export type IDebounce<T> = {
|
|
139
|
+
debounce(ms: number): ISetup<T>;
|
|
140
|
+
};
|
|
141
|
+
export type IOrderedDebounce<T> = {
|
|
142
|
+
debounce(ms: number): ISetup<T>;
|
|
143
|
+
};
|
|
144
|
+
export type IDistinctUntilChanged<T> = {
|
|
145
|
+
distinctUntilChanged(comparator?: (previous: T, current: T) => boolean): ISetup<T>;
|
|
146
|
+
};
|
|
147
|
+
export type IOrderedDistinctUntilChanged<T> = {
|
|
148
|
+
distinctUntilChanged(comparator?: (previous: T, current: T) => boolean): ISetup<T>;
|
|
149
|
+
};
|
|
150
|
+
export type ITap<T> = {
|
|
151
|
+
tap(fn: ICallback<T>): ISetup<T>;
|
|
152
|
+
};
|
|
153
|
+
export type IOrderedTap<T> = {
|
|
154
|
+
tap(fn: ICallback<T>): ISetup<T>;
|
|
155
|
+
};
|
|
114
156
|
export type IOrderedSerialisation = {
|
|
115
157
|
toJson(): ISetup<string>;
|
|
116
158
|
fromJson<K>(): ISetup<K>;
|
|
@@ -141,6 +183,10 @@ export type IPipePayload = {
|
|
|
141
183
|
isBreak: boolean;
|
|
142
184
|
isUnsubscribe: boolean;
|
|
143
185
|
isAvailable: boolean;
|
|
186
|
+
debounceMs: number;
|
|
187
|
+
debounceTimer: any;
|
|
188
|
+
debounceValue: any;
|
|
189
|
+
debounceIndex: number;
|
|
144
190
|
payload: any;
|
|
145
191
|
};
|
|
146
192
|
export type IChainCallback = (data: IPipePayload) => void;
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
# Benchmark Results: Bundle vs TypeScript vs RxJS
|
|
2
|
-
|
|
3
|
-
Date: 2026-01-10
|
|
4
|
-
|
|
5
|
-
## Methodology
|
|
6
|
-
|
|
7
|
-
Performance comparison of three variants:
|
|
8
|
-
|
|
9
|
-
1. **Bundle** - Minified JavaScript from `repo/evg_observable.js` (7.2 kB)
|
|
10
|
-
2. **TypeScript** - On-the-fly compilation via ts-node
|
|
11
|
-
3. **RxJS** - From node_modules (88 kB UMD bundle)
|
|
12
|
-
|
|
13
|
-
**Size comparison:** EVG Observable is **12.2x smaller** than RxJS (7.2 kB vs 88 kB)
|
|
14
|
-
|
|
15
|
-
## Test Results
|
|
16
|
-
|
|
17
|
-
Results averaged over 3 clean benchmark runs (no background processes):
|
|
18
|
-
|
|
19
|
-
### 1. Observable Creation
|
|
20
|
-
|
|
21
|
-
| Variant | Ops/sec | Relative Performance |
|
|
22
|
-
|---------|---------|---------------------|
|
|
23
|
-
| Bundle | 122,701,000 | **baseline** |
|
|
24
|
-
| TypeScript | 52,574,000 | 2.3x slower |
|
|
25
|
-
| RxJS | 53,639,000 | 2.3x slower |
|
|
26
|
-
|
|
27
|
-
**Winner: Bundle**
|
|
28
|
-
|
|
29
|
-
### 2. Emit 100 Values (1 subscriber)
|
|
30
|
-
|
|
31
|
-
| Variant | Ops/sec | Relative Performance |
|
|
32
|
-
|---------|---------|---------------------|
|
|
33
|
-
| Bundle | 1,662,000 | **baseline** |
|
|
34
|
-
| TypeScript | 1,079,000 | 1.5x slower |
|
|
35
|
-
| RxJS | 239,000 | 7.0x slower |
|
|
36
|
-
|
|
37
|
-
**Winner: Bundle**
|
|
38
|
-
|
|
39
|
-
### 3. Filter and Transform (pipe chain)
|
|
40
|
-
|
|
41
|
-
| Variant | Ops/sec | Relative Performance |
|
|
42
|
-
|---------|---------|---------------------|
|
|
43
|
-
| Bundle | 340,000 | **baseline** |
|
|
44
|
-
| TypeScript | 324,000 | ~equal (5% difference) |
|
|
45
|
-
| RxJS | 149,000 | 2.3x slower |
|
|
46
|
-
|
|
47
|
-
**Winner: Bundle** (marginal advantage)
|
|
48
|
-
|
|
49
|
-
### 4. Multiple Subscribers
|
|
50
|
-
|
|
51
|
-
#### 10 Subscribers
|
|
52
|
-
|
|
53
|
-
| Variant | Ops/sec | Relative Performance |
|
|
54
|
-
|---------|---------|---------------------|
|
|
55
|
-
| Bundle | 9,946,000 | **baseline** |
|
|
56
|
-
| TypeScript | 8,119,000 | 1.2x slower |
|
|
57
|
-
| RxJS | 3,500,000 | 2.8x slower |
|
|
58
|
-
|
|
59
|
-
#### 100 Subscribers
|
|
60
|
-
|
|
61
|
-
| Variant | Ops/sec | Relative Performance |
|
|
62
|
-
|---------|---------|---------------------|
|
|
63
|
-
| Bundle | 1,236,000 | **baseline** |
|
|
64
|
-
| TypeScript | 991,000 | 1.2x slower |
|
|
65
|
-
| RxJS | 432,000 | 2.9x slower |
|
|
66
|
-
|
|
67
|
-
#### 1000 Subscribers
|
|
68
|
-
|
|
69
|
-
| Variant | Ops/sec | Relative Performance |
|
|
70
|
-
|---------|---------|---------------------|
|
|
71
|
-
| Bundle | 124,000 | **baseline** |
|
|
72
|
-
| TypeScript | 98,000 | 1.3x slower |
|
|
73
|
-
| RxJS | 41,000 | 3.0x slower |
|
|
74
|
-
|
|
75
|
-
**Winner: Bundle** (all cases)
|
|
76
|
-
|
|
77
|
-
### 5. Batch Emission - of(100)
|
|
78
|
-
|
|
79
|
-
| Variant | Ops/sec | Relative Performance |
|
|
80
|
-
|---------|---------|---------------------|
|
|
81
|
-
| Bundle | 906,000 | **baseline** |
|
|
82
|
-
| TypeScript | 752,000 | 1.2x slower |
|
|
83
|
-
| RxJS | 176,000 | 5.1x slower |
|
|
84
|
-
|
|
85
|
-
**Winner: Bundle**
|
|
86
|
-
|
|
87
|
-
### 6. Five Chained Filters
|
|
88
|
-
|
|
89
|
-
| Variant | Ops/sec | Relative Performance |
|
|
90
|
-
|---------|---------|---------------------|
|
|
91
|
-
| Bundle | 18,900 | **baseline** |
|
|
92
|
-
| TypeScript | 18,200 | ~equal (4% difference) |
|
|
93
|
-
| RxJS | 9,200 | 2.1x slower |
|
|
94
|
-
|
|
95
|
-
**Winner: Bundle** (marginal advantage)
|
|
96
|
-
|
|
97
|
-
### 7. Large Payload (complex objects)
|
|
98
|
-
|
|
99
|
-
| Variant | Ops/sec | Relative Performance |
|
|
100
|
-
|---------|---------|---------------------|
|
|
101
|
-
| Bundle | 879,000 | **baseline** |
|
|
102
|
-
| TypeScript | 755,000 | 1.2x slower |
|
|
103
|
-
| RxJS | 184,000 | 4.8x slower |
|
|
104
|
-
|
|
105
|
-
**Winner: Bundle**
|
|
106
|
-
|
|
107
|
-
## Summary
|
|
108
|
-
|
|
109
|
-
### Bundle vs TypeScript
|
|
110
|
-
|
|
111
|
-
The minified bundle shows **consistent advantage** over TypeScript compilation:
|
|
112
|
-
|
|
113
|
-
- **Object creation**: 2.3x faster - significant advantage
|
|
114
|
-
- **Data emission**: 1.5x faster - noticeable advantage
|
|
115
|
-
- **Filtering/transformation**: ~equal performance (within 5%)
|
|
116
|
-
- **Multiple subscribers**: 1.2-1.3x faster - stable advantage
|
|
117
|
-
- **Batch emission**: 1.2x faster
|
|
118
|
-
- **Large objects**: 1.2x faster
|
|
119
|
-
|
|
120
|
-
**Reasons for bundle's advantage:**
|
|
121
|
-
1. Minification and dead code elimination
|
|
122
|
-
2. Better V8 JIT optimization for compiled code
|
|
123
|
-
3. Reduced module loading overhead
|
|
124
|
-
4. Inlining of small functions
|
|
125
|
-
|
|
126
|
-
### EVG Observable vs RxJS
|
|
127
|
-
|
|
128
|
-
Both variants (bundle and TypeScript) **significantly outperform RxJS**:
|
|
129
|
-
|
|
130
|
-
- **Creation**: ~2.3x faster
|
|
131
|
-
- **Simple emission**: 5-7x faster
|
|
132
|
-
- **Filtering**: 2.3x faster
|
|
133
|
-
- **Multiple subscribers**: 2.8-3.0x faster
|
|
134
|
-
- **Batch emission**: 4.5-5.1x faster
|
|
135
|
-
- **Complex filtering**: ~2.1x faster
|
|
136
|
-
- **Large objects**: 4.7-4.8x faster
|
|
137
|
-
|
|
138
|
-
### Recommendations
|
|
139
|
-
|
|
140
|
-
1. **For production**: Use minified bundle for maximum performance
|
|
141
|
-
2. **For development**: TypeScript compilation provides nearly equal performance with type safety convenience
|
|
142
|
-
3. **Migration from RxJS**: Expect 2-7x performance improvement depending on usage patterns
|
|
143
|
-
|
|
144
|
-
## Technical Details
|
|
145
|
-
|
|
146
|
-
- **Node.js**: v22.17.1
|
|
147
|
-
- **Benchmark.js**: Standard settings
|
|
148
|
-
- **Minimum runs**: 71-90 per test
|
|
149
|
-
- **Margin of Error**: ±0.71% - ±5.21%
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# Benchmark Results: EVG Observable vs observable-fns
|
|
2
|
-
|
|
3
|
-
Date: 2026-01-10
|
|
4
|
-
|
|
5
|
-
## Methodology
|
|
6
|
-
|
|
7
|
-
Performance comparison of two lightweight Observable libraries:
|
|
8
|
-
|
|
9
|
-
1. **EVG Observable** - True hot observables with original architecture (7.2 kB minified)
|
|
10
|
-
2. **observable-fns** - Based on zen-observable, provides Subject for hot observables (10.8 kB minified)
|
|
11
|
-
|
|
12
|
-
**Size comparison:** EVG Observable is **1.5x smaller** than observable-fns (7.2 kB vs 10.8 kB)
|
|
13
|
-
|
|
14
|
-
Benchmarked on Node.js v22.17.1, results averaged over 3 clean runs (no background processes).
|
|
15
|
-
|
|
16
|
-
## Test Results
|
|
17
|
-
|
|
18
|
-
### 1. Observable Creation
|
|
19
|
-
|
|
20
|
-
| Library | Ops/sec | Relative Performance |
|
|
21
|
-
|---------|---------|---------------------|
|
|
22
|
-
| EVG Observable | 54,138,000 | **baseline** |
|
|
23
|
-
| observable-fns | 17,226,000 | 3.1x slower |
|
|
24
|
-
|
|
25
|
-
**Winner: EVG Observable** (3.1x faster)
|
|
26
|
-
|
|
27
|
-
### 2. Single Emission Performance Matrix
|
|
28
|
-
|
|
29
|
-
#### 1 Emission × Multiple Subscribers
|
|
30
|
-
|
|
31
|
-
| Subscribers | EVG Observable | observable-fns | Advantage |
|
|
32
|
-
|-------------|----------------|----------------|-----------|
|
|
33
|
-
| 1 | 53,802,000 ops/sec | 36,757,000 | **1.5x faster** |
|
|
34
|
-
| 10 | 14,318,000 ops/sec | 6,283,000 | **2.3x faster** |
|
|
35
|
-
| 100 | 1,700,000 ops/sec | 733,000 | **2.3x faster** |
|
|
36
|
-
| 1,000 | 176,000 ops/sec | 73,000 | **2.4x faster** |
|
|
37
|
-
| 10,000 | 16,100 ops/sec | 6,900 | **2.3x faster** |
|
|
38
|
-
|
|
39
|
-
**Winner: EVG Observable** (consistent 1.5-2.4x advantage)
|
|
40
|
-
|
|
41
|
-
#### 10 Emissions × Multiple Subscribers
|
|
42
|
-
|
|
43
|
-
| Subscribers | EVG Observable | observable-fns | Advantage |
|
|
44
|
-
|-------------|----------------|----------------|-----------|
|
|
45
|
-
| 1 | 9,881,000 ops/sec | 5,568,000 | **1.8x faster** |
|
|
46
|
-
| 10 | 1,651,000 ops/sec | 677,000 | **2.4x faster** |
|
|
47
|
-
| 100 | 176,000 ops/sec | 75,000 | **2.3x faster** |
|
|
48
|
-
| 1,000 | 17,800 ops/sec | 7,400 | **2.4x faster** |
|
|
49
|
-
| 10,000 | 1,600 ops/sec | 700 | **2.3x faster** |
|
|
50
|
-
|
|
51
|
-
**Winner: EVG Observable** (consistent 1.8-2.4x advantage)
|
|
52
|
-
|
|
53
|
-
#### 100 Emissions × Multiple Subscribers
|
|
54
|
-
|
|
55
|
-
| Subscribers | EVG Observable | observable-fns | Advantage |
|
|
56
|
-
|-------------|----------------|----------------|-----------|
|
|
57
|
-
| 1 | 1,022,000 ops/sec | 552,000 | **1.9x faster** |
|
|
58
|
-
| 10 | 169,000 ops/sec | 69,000 | **2.4x faster** |
|
|
59
|
-
| 100 | 17,500 ops/sec | 7,300 | **2.4x faster** |
|
|
60
|
-
| 1,000 | 1,770 ops/sec | 730 | **2.4x faster** |
|
|
61
|
-
| 10,000 | 166 ops/sec | 65 | **2.6x faster** |
|
|
62
|
-
|
|
63
|
-
**Winner: EVG Observable** (consistent 1.9-2.6x advantage)
|
|
64
|
-
|
|
65
|
-
#### 1000 Emissions × Multiple Subscribers
|
|
66
|
-
|
|
67
|
-
| Subscribers | EVG Observable | observable-fns | Advantage |
|
|
68
|
-
|-------------|----------------|----------------|-----------|
|
|
69
|
-
| 1 | 98,400 ops/sec | 54,400 | **1.8x faster** |
|
|
70
|
-
| 10 | 16,900 ops/sec | 7,000 | **2.4x faster** |
|
|
71
|
-
| 100 | 1,750 ops/sec | 730 | **2.4x faster** |
|
|
72
|
-
| 1,000 | 179 ops/sec | 75 | **2.4x faster** |
|
|
73
|
-
| 10,000 | 15.8 ops/sec | 6.1 | **2.6x faster** |
|
|
74
|
-
|
|
75
|
-
**Winner: EVG Observable** (consistent 1.8-2.6x advantage)
|
|
76
|
-
|
|
77
|
-
### 3. Filter & Transform (pipe chain with 100 emissions)
|
|
78
|
-
|
|
79
|
-
| Subscribers | EVG Observable | observable-fns | Advantage |
|
|
80
|
-
|-------------|----------------|----------------|-----------|
|
|
81
|
-
| 1 | 292,000 ops/sec | 127,000 | **2.3x faster** |
|
|
82
|
-
| 10 | 275,000 ops/sec | 18,700 | **14.7x faster** |
|
|
83
|
-
| 100 | 280,000 ops/sec | 1,880 | **149x faster** |
|
|
84
|
-
| 1,000 | 273,000 ops/sec | 143 | **1,909x faster** |
|
|
85
|
-
| 10,000 | 277,000 ops/sec | 13.7 | **20,219x faster** |
|
|
86
|
-
|
|
87
|
-
**Winner: EVG Observable** (dramatic advantage at higher subscriber counts)
|
|
88
|
-
|
|
89
|
-
**Analysis:** EVG Observable's pipe architecture is extremely efficient with multiple subscribers. observable-fns creates separate pipe chains per subscriber, causing exponential slowdown.
|
|
90
|
-
|
|
91
|
-
### 4. Five Chained Filters (100 emissions)
|
|
92
|
-
|
|
93
|
-
| Subscribers | EVG Observable | observable-fns | Advantage |
|
|
94
|
-
|-------------|----------------|----------------|-----------|
|
|
95
|
-
| 1 | 119,000 ops/sec | 71,300 | **1.7x faster** |
|
|
96
|
-
| 10 | 116,400 ops/sec | 7,280 | **16.0x faster** |
|
|
97
|
-
| 100 | 113,500 ops/sec | 709 | **160x faster** |
|
|
98
|
-
| 1,000 | 112,400 ops/sec | 57.6 | **1,951x faster** |
|
|
99
|
-
| 10,000 | 114,100 ops/sec | 2.1 | **54,333x faster** |
|
|
100
|
-
|
|
101
|
-
**Winner: EVG Observable** (exponential advantage at higher subscriber counts)
|
|
102
|
-
|
|
103
|
-
**Analysis:** Complex pipe chains show EVG Observable's architectural superiority. The advantage grows exponentially with subscriber count.
|
|
104
|
-
|
|
105
|
-
### 5. Large Payload (complex objects, 100 emissions)
|
|
106
|
-
|
|
107
|
-
| Library | Ops/sec | Relative Performance |
|
|
108
|
-
|---------|---------|---------------------|
|
|
109
|
-
| EVG Observable | 749,000 | **baseline** |
|
|
110
|
-
| observable-fns | 549,000 | 1.4x slower |
|
|
111
|
-
|
|
112
|
-
**Winner: EVG Observable** (1.4x faster)
|
|
113
|
-
|
|
114
|
-
### 6. Subscribe/Unsubscribe Churn (1000 cycles)
|
|
115
|
-
|
|
116
|
-
| Library | Ops/sec | Relative Performance |
|
|
117
|
-
|---------|---------|---------------------|
|
|
118
|
-
| EVG Observable | 4,406 | **~equal** |
|
|
119
|
-
| observable-fns | 4,537 | ~equal |
|
|
120
|
-
|
|
121
|
-
**Winner: Tie** (within margin of error)
|
|
122
|
-
|
|
123
|
-
**Analysis:** Both libraries have similar overhead for subscription management.
|
|
124
|
-
|
|
125
|
-
## Summary
|
|
126
|
-
|
|
127
|
-
### Key Findings
|
|
128
|
-
|
|
129
|
-
1. **Simple Emissions (1 subscriber)**: EVG Observable is **1.5-1.9x faster**
|
|
130
|
-
- Consistent advantage across all emission counts
|
|
131
|
-
- Both scale linearly
|
|
132
|
-
|
|
133
|
-
2. **Multiple Subscribers (10-10,000)**: EVG Observable is **2.3-2.6x faster**
|
|
134
|
-
- Advantage increases slightly with subscriber count
|
|
135
|
-
- EVG's true hot observable architecture shines
|
|
136
|
-
|
|
137
|
-
3. **Pipe Operations**: EVG Observable has **dramatic advantage**
|
|
138
|
-
- 1 subscriber: 1.7-2.3x faster
|
|
139
|
-
- 10 subscribers: 14.7-16.0x faster
|
|
140
|
-
- 100 subscribers: 149-160x faster
|
|
141
|
-
- 1,000 subscribers: 1,900-1,950x faster
|
|
142
|
-
- 10,000 subscribers: 20,000-54,000x faster
|
|
143
|
-
|
|
144
|
-
4. **Observable Creation**: EVG Observable is **3.1x faster**
|
|
145
|
-
- Lower instantiation overhead
|
|
146
|
-
|
|
147
|
-
5. **Large Payloads**: EVG Observable is **1.4x faster**
|
|
148
|
-
- Efficient data passing
|
|
149
|
-
|
|
150
|
-
### Architectural Insights
|
|
151
|
-
|
|
152
|
-
**EVG Observable's advantages:**
|
|
153
|
-
- True hot observable architecture (single pipe chain for all subscribers)
|
|
154
|
-
- Efficient subscription management for multi-subscriber scenarios
|
|
155
|
-
- Optimized pipe operations that don't duplicate work
|
|
156
|
-
- Lower memory overhead per subscriber
|
|
157
|
-
|
|
158
|
-
**observable-fns characteristics:**
|
|
159
|
-
- Based on zen-observable (cold observables)
|
|
160
|
-
- Each subscriber creates separate pipe chain
|
|
161
|
-
- Good for single-subscriber patterns
|
|
162
|
-
- Struggles with multi-subscriber pipe operations
|
|
163
|
-
|
|
164
|
-
### Recommendations
|
|
165
|
-
|
|
166
|
-
**Choose EVG Observable when:**
|
|
167
|
-
- You have multiple subscribers (most real-world scenarios)
|
|
168
|
-
- Using pipe operators (filter, map, etc.)
|
|
169
|
-
- Performance is critical
|
|
170
|
-
- Real-time broadcasting (WebSocket, SSE, event emitters)
|
|
171
|
-
- Need hot observable semantics
|
|
172
|
-
|
|
173
|
-
**Choose observable-fns when:**
|
|
174
|
-
- You need zen-observable API compatibility
|
|
175
|
-
- Single subscriber patterns
|
|
176
|
-
- Cold observable semantics required
|
|
177
|
-
|
|
178
|
-
### Performance Summary Table
|
|
179
|
-
|
|
180
|
-
| Operation Type | Typical Advantage | Best Case | Worst Case |
|
|
181
|
-
|----------------|-------------------|-----------|------------|
|
|
182
|
-
| Simple emission | 1.5-2.4x | 2.6x | 1.5x |
|
|
183
|
-
| Pipe (1 sub) | 1.7-2.3x | 2.3x | 1.7x |
|
|
184
|
-
| Pipe (10+ subs) | 15-20,000x | 54,333x | 14.7x |
|
|
185
|
-
| Creation | 3.1x | 3.1x | 3.1x |
|
|
186
|
-
| Large payloads | 1.4x | 1.4x | 1.4x |
|
|
187
|
-
| Sub/Unsub | ~equal | - | - |
|
|
188
|
-
|
|
189
|
-
## Technical Details
|
|
190
|
-
|
|
191
|
-
- **Node.js**: v22.17.1
|
|
192
|
-
- **Benchmark.js**: Standard settings
|
|
193
|
-
- **Runs**: 3 clean benchmark runs, averaged
|
|
194
|
-
- **Per-test samples**: 70-94 runs per scenario
|
|
195
|
-
- **Margin of Error**: ±1.2% - ±2.9% (typical)
|
|
196
|
-
- **Total scenarios**: 33 benchmark scenarios
|
|
197
|
-
- **Verification**: All pipe operations include result verification to ensure correct behavior
|
package/BREAKING_CHANGES.md
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# Breaking Changes - Performance Optimizations Branch
|
|
2
|
-
|
|
3
|
-
## Version 2.15.0
|
|
4
|
-
|
|
5
|
-
### Internal Field Renaming
|
|
6
|
-
|
|
7
|
-
**Affected field:** `value` renamed to `_value`
|
|
8
|
-
|
|
9
|
-
The internal field storing the current observable value has been renamed from `value` to `_value` to follow TypeScript conventions for protected/private members.
|
|
10
|
-
|
|
11
|
-
**Before:**
|
|
12
|
-
```typescript
|
|
13
|
-
export class Observable<T> {
|
|
14
|
-
constructor(private value: T) {}
|
|
15
|
-
}
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
**After:**
|
|
19
|
-
```typescript
|
|
20
|
-
export class Observable<T> {
|
|
21
|
-
protected _value: T;
|
|
22
|
-
constructor(value: T) {
|
|
23
|
-
this._value = value;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
**Migration:**
|
|
29
|
-
- If you were extending `Observable` and accessing `this.value`, change to `this._value`
|
|
30
|
-
- Use the public `getValue()` method for reading the current value (recommended)
|
|
31
|
-
|
|
32
|
-
### Visibility Changes
|
|
33
|
-
|
|
34
|
-
The following fields changed from `private` to `protected` to enable better inheritance:
|
|
35
|
-
|
|
36
|
-
| Field | Before | After |
|
|
37
|
-
|-------|--------|-------|
|
|
38
|
-
| `enabled` | private | protected |
|
|
39
|
-
| `filters` | private | protected |
|
|
40
|
-
| `_value` (formerly `value`) | private | protected |
|
|
41
|
-
|
|
42
|
-
**Impact:** This is not a breaking change for most users, but allows subclasses to access these fields.
|
|
43
|
-
|
|
44
|
-
### Recommended Migration Path
|
|
45
|
-
|
|
46
|
-
Instead of accessing internal fields directly, use the public API:
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
// Instead of:
|
|
50
|
-
// @ts-ignore
|
|
51
|
-
const val = observable._value;
|
|
52
|
-
|
|
53
|
-
// Use:
|
|
54
|
-
const val = observable.getValue();
|
|
55
|
-
|
|
56
|
-
// Instead of:
|
|
57
|
-
// @ts-ignore
|
|
58
|
-
const count = observable.subs.length;
|
|
59
|
-
|
|
60
|
-
// Use:
|
|
61
|
-
const count = observable.size();
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### New Behavior
|
|
65
|
-
|
|
66
|
-
1. **`destroy()` now uses `Promise.resolve()`** instead of `setInterval()` for async cleanup when called during emission
|
|
67
|
-
2. **`unsubscribeAll()` is now safe to call during `next()`** - uses deferred cleanup mechanism
|
|
68
|
-
3. **Early exit optimization** - `next()` returns immediately if there are no subscribers
|
|
69
|
-
|
|
70
|
-
These behavioral changes improve performance and memory management but should not require code changes.
|