├── .babelrc ├── .esdoc.json ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── API.md ├── LICENSE.md ├── README.md ├── benchmarks.md ├── benchmarks ├── bm.combine.ts ├── bm.concat.ts ├── bm.create.ts ├── bm.debounce.ts ├── bm.fromArray-map-reduce.ts ├── bm.fromArray-scan-reduce.ts ├── bm.fromArray-takeN.ts ├── bm.mergeMap.ts ├── bm.switch.ts ├── bm.tryCatch.ts ├── lib.ts └── run.ts ├── config └── rollup.config.js ├── extra.ts ├── package.json ├── src ├── internal │ ├── ColdTestObservable.ts │ ├── Container.ts │ ├── Events.ts │ ├── HotTestObservable.ts │ ├── Marble.ts │ ├── Mixins.ts │ ├── Observable.ts │ ├── Observer.ts │ ├── Periodic.ts │ ├── SafeObserver.ts │ ├── SubscriberFunction.ts │ ├── Subscription.ts │ ├── TestObservable.ts │ ├── TestObserver.ts │ ├── TestOptions.ts │ ├── Thrower.ts │ └── Utils.ts ├── main.ts ├── operators │ ├── Combine.ts │ ├── Concat.ts │ ├── Debounce.ts │ ├── Delay.ts │ ├── Filter.ts │ ├── Map.ts │ ├── Merge.ts │ ├── MergeMap.ts │ ├── Multicast.ts │ ├── Operator.ts │ ├── Reduce.ts │ ├── Sample.ts │ ├── Scan.ts │ ├── SkipRepeats.ts │ ├── Slice.ts │ ├── Switch.ts │ └── Unique.ts ├── schedulers │ ├── Scheduler.ts │ └── TestScheduler.ts ├── sinks │ ├── ForEach.ts │ ├── ToNodeStream.ts │ └── ToPromise.ts └── sources │ ├── Create.ts │ ├── Frames.ts │ ├── FromArray.ts │ ├── FromDOM.ts │ ├── FromNodeStream.ts │ ├── FromPromise.ts │ ├── Interval.ts │ └── Subject.ts ├── test.ts ├── test ├── test.Combine.ts ├── test.Concat.ts ├── test.Debounce.ts ├── test.Delay.ts ├── test.ForEach.ts ├── test.Frames.ts ├── test.FromArray.ts ├── test.FromPromise.ts ├── test.Interval.ts ├── test.Map.ts ├── test.Marble.ts ├── test.Merge.ts ├── test.MergeMap.ts ├── test.Multicast.ts ├── test.NodeStream.ts ├── test.Observable.ts ├── test.Sample.ts ├── test.Scan.ts ├── test.SkipRepeats.ts ├── test.Slice.ts ├── test.Subject.ts ├── test.Switch.ts ├── test.TestScheduler.ts ├── test.Unique.ts └── test.Utils.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "babili" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./.dist/es6", 3 | "destination": "./.dist/doc" 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | .idea 42 | src/**/*.js 43 | src/**/*.d.ts 44 | *.map 45 | .dist 46 | *.cfg 47 | *.asm 48 | test/**/*.js 49 | test/**/*.d.ts 50 | typings/ 51 | .vscode/ 52 | *.js 53 | *.d.ts 54 | !config/rollup.config.js 55 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | !src/**/*.js 4 | !.dist/*.js 5 | !test 6 | *.ts 7 | !*.d.ts 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": false, 4 | "singleQuote": true, 5 | "bracketSpacing": false 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | yarn: true 4 | directories: 5 | - ~/.npm 6 | - node_modules 7 | notifications: 8 | email: false 9 | node_js: 10 | - stable 11 | before_install: 12 | - yarn global add greenkeeper-lockfile@1 13 | before_script: 14 | - greenkeeper-lockfile-update 15 | - yarn coverage 16 | - yarn lint 17 | after_success: 18 | - npm run travis-deploy-once "npm run semantic-release" 19 | after_script: 20 | - greenkeeper-lockfile-upload 21 | branches: 22 | except: 23 | - /^v\d+\.\d+\.\d+$/ 24 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | - [Types](#types) 4 | - [Subscription](#Subscription) 5 | - [Scheduler](#Scheduler) 6 | - [Observer](#Observer) 7 | - [Observable](#Observable) 8 | - [Sinks](#sinks) 9 | - [forEach](#forEach) 10 | - [toPromise](#toPromise) 11 | - [toNodeStream](#toNodeStream) 12 | - [Sources](#sources) 13 | - [empty](#empty) 14 | - [frames](#frames) 15 | - [fromArray](#fromArray) 16 | - [fromDOM](#fromDOM) 17 | - [fromNodeStream](#fromNodeStream) 18 | - [fromPromise](#fromPromise) 19 | - [interval](#interval) 20 | - [just](#just) 21 | - [never](#never) 22 | - [subject](#subject) 23 | 24 | - [Operators](#operators) 25 | - [combine](#combine) 26 | - [concat](#concat) 27 | - [concatMap](#concatMap) 28 | - [delay](#delay) 29 | - [filter](#filter) 30 | - [flatMap](#flatMap) 31 | - [join](#join) 32 | - [map](#map) 33 | - [mapTo](#mapTo) 34 | - [merge](#merge) 35 | - [mergeMap](#mergeMap) 36 | - [multicast](#multicast) 37 | - [reduce](#reduce) 38 | - [sample](#sample) 39 | - [scan](#scan) 40 | - [skipRepeats](#skipRepeats) 41 | - [slice](#slice) 42 | - [switchLatest](#switchLatest) 43 | - [switchMap](#switchMap) 44 | - [tap](#tap) 45 | - [unique](#unique) 46 | - [uniqueWith](#uniqueWith) 47 | - [Testing](#testing) 48 | - [TestScheduler](#TestScheduler) 49 | - [marble](#marble) 50 | - [toMarble](#toMarble) 51 | 52 | >All this stuff in just 4kb! 53 | 54 | # Types 55 | 56 | ## Subscription 57 | 58 | ```ts 59 | interface Subscription { 60 | unsubscribe(): void 61 | readonly closed: boolean 62 | } 63 | ``` 64 | 65 | ## Scheduler 66 | A `Scheduler` is an object which has some useful timing utilities as defined in the below interface. 67 | 68 | ```ts 69 | interface Scheduler { 70 | // Runs the task after the given duration 71 | delay(task: () => void, duration: number): Subscription 72 | 73 | // Keeps running the task after every given amount of duration has passed 74 | periodic(task: () => void, duration: number): Subscription 75 | 76 | // Runs the task on the next frame 77 | frame(task: () => void): Subscription 78 | 79 | // Runs the task as soon as possible but in the next event cycle. 80 | asap(task: () => void): Subscription 81 | 82 | // Displays the current time of the scheduler 83 | now(): number 84 | } 85 | ``` 86 | 87 | Observables can have a really complicated logic that is time based. One could use the standard JS functions such as `setTimeout`, `setInterval` etc. for writing such logic. This works for most cases but unfortunately it makes the code really difficult to unit test. This is because the tests now need to actually run for the duration that is specified inside the timing functions. 88 | 89 | Instead a better solution is to adopt [inversion of control] design pattern where we inject some custom implementations of these timing functions into the observable, such that it can be controlled from outside. This is precisely what the `Scheduler` does. 90 | 91 | [inversion of control]: https://en.wikipedia.org/wiki/Inversion_of_control 92 | It is passed on at the time of subscription and is automatically shared with all the other operators and sources inside your observable logic. This gives a much better control over managing time and scheduling tasks. 93 | 94 | **Example:** 95 | 96 | ```ts 97 | import * as O from 'observable-air' 98 | import {createTestScheduler} from 'observable-air/test' 99 | 100 | // source stream 101 | const source = O.interval(1000) 102 | const scheduler = createTestScheduler() 103 | const observer = { 104 | next () { console.log('hi')} 105 | } 106 | source.subscribe(observer, scheduler) 107 | 108 | scheduler.advanceTo(1000) // 109 | ``` 110 | 111 | 112 | ## Observer 113 | ```ts 114 | interface Observer { 115 | // Receives the next value in the sequence 116 | next (val: T): void 117 | 118 | // Receives the sequence error 119 | error (err: Error): void 120 | 121 | // Receives the sequence completion value 122 | complete (): void 123 | } 124 | ``` 125 | 126 | ## Observable 127 | ```ts 128 | interface Observable { 129 | subscribe(observer: Observer, scheduler: Scheduler): Subscription 130 | } 131 | ``` 132 | `Observable` is also exported as class and can be used to create new instances. 133 | 134 | ```ts 135 | function listen (element, eventName) { 136 | return new Observable(observer => { 137 | // Create an event handler which sends data to the sink 138 | let handler = event => observer.next(event) 139 | 140 | // Attach the event handler 141 | element.addEventListener(eventName, handler, true) 142 | 143 | // Return a cleanup function which will cancel the event stream 144 | return () => { 145 | // Detach the event handler from the element 146 | element.removeEventListener(eventName, handler, true) 147 | } 148 | }) 149 | } 150 | ``` 151 | 152 | # Sinks 153 | Sinks are essentially consumers of the observable/streams. By default streams can be consumed by subscribing to them. To create a subscription an observer is needed. 154 | 155 | **Example:** 156 | ```ts 157 | import * as O from 'observable-air' 158 | import {createScheduler} from 'observable-air/src/lib/Scheduler' 159 | 160 | const $ = O.interval(1000) // fires a values every 1 second 161 | const observer = { 162 | next (value) { // triggered on every value 163 | console.log(value) 164 | }, 165 | error (err) { // triggered when there is an exception being thrown 166 | console.error(err) 167 | }, 168 | complete () { 169 | console.log('Complete!') 170 | } 171 | } 172 | 173 | // Subscribing to a stream creates a subscription 174 | const subscription = $.subscribe(observer, createScheduler()) 175 | 176 | 177 | // to unsubscribe 178 | subscription.unsubscribe() 179 | ``` 180 | 181 | ## forEach() 182 | ```ts 183 | function forEach(onValue: (value: any) => void, source: Observable): void 184 | ``` 185 | Since consuming a stream requires a lot of arguments and setting-up a useful helper — `forEach` has been created. The above logic for consuming a stream can be re-written as — 186 | 187 | **Example:** 188 | ```ts 189 | const $ = O.interval(1000) 190 | O.forEach(console.log, $) 191 | ``` 192 | 193 | ## toPromise() 194 | ``ts 195 | function toPromise(observable: Observable): Promise 196 | `` 197 | 198 | Simply converts an observable stream into a promise. The promise is resolved with the last value that is emitted in the observable. 199 | 200 | 201 | ## toNodeStream() 202 | ```ts 203 | function toNodeStream(source: Observable): NodeStream 204 | ``` 205 | 206 | Transforms an observable into [NodeStream]. 207 | 208 | [NodeStream]: https://nodejs.org/api/stream.html 209 | 210 | # Sources 211 | Sources are the functions that emit values, such as — `fromDOM(document, 'click)`, which emits a `ClickEvent` as a part of the stream. Other sources can be — `interval(1000)` which will emit a value every `1000ms`. 212 | 213 | 214 | **Example:** 215 | ```ts 216 | const $ = O.create((observer, scheduler) => 217 | scheduler.delay( 218 | () => { 219 | observer.next('Hello') 220 | observer.complete() 221 | }, 222 | 1000 223 | )) 224 | 225 | // logs 'Hello' after 1 sec and ends 226 | O.forEach(console.log, $) 227 | ``` 228 | 229 | ## empty 230 | 231 | ```ts 232 | function empty(): Observable 233 | ``` 234 | Creates an observable that completes without emitting any value. 235 | 236 | ## frames 237 | 238 | ```ts 239 | function frames(): Observable 240 | ``` 241 | 242 | Simple source that emits a values on every request animation frame. 243 | 244 | **Example:** 245 | ```ts 246 | let i = 0 247 | const $ = O.frames() 248 | 249 | O.forEach( 250 | () => document.innerHTML = i++, 251 | $ 252 | ) 253 | ``` 254 | 255 | 256 | 257 | ## fromArray 258 | 259 | ```ts 260 | function fromArray(list: any[]): Observable 261 | ``` 262 | 263 | Creates an observable from an array of numbers asynchronously — 264 | 265 | **Example:** 266 | ```ts 267 | const $ = O.fromArray([10, 20, 30]) 268 | 269 | // logs 10 ... 20 ... 30 270 | O.forEach(console.log, $) 271 | ``` 272 | 273 | 274 | ## fromDOM 275 | 276 | ```ts 277 | function fromDOM(el: HTMLElement, event: string): Observable 278 | ``` 279 | 280 | Creates an observable of dom events. It takes in two arguments viz. the DOM Element on which an event listener needs to be attached and the name of the event that needs to be listened to. 281 | **Example:** 282 | 283 | ```ts 284 | const $ = O.fromDOM(document.body, 'scroll') 285 | 286 | // Logs the scroll event on every scroll 287 | O.forEach(console.log, $) 288 | ``` 289 | 290 | 291 | ## fromPromise 292 | 293 | ```ts 294 | function fromPromise(fn: () => Promise): Observable 295 | ``` 296 | 297 | Creates an observable from a `Promise`. It takes in a function that returns a promise and then lazily calls the function when the subscription is initiated. 298 | 299 | ```ts 300 | // A stream that emits `Hello World` as its only value and then ends. 301 | const $ = O.fromPromise(() => Promise.resolve('Hello World')) 302 | ``` 303 | 304 | 305 | ## interval 306 | 307 | ```ts 308 | function interval(duration: number): Observable 309 | ``` 310 | 311 | Fires an event on every given `duration` amount of time. 312 | **Example:** 313 | ```ts 314 | const $ = O.interval(1000) // fires in every 1000ms 315 | ``` 316 | 317 | ## just 318 | 319 | ```ts 320 | function just(value: any): Observable 321 | ``` 322 | 323 | Creates an observable that emits only one value 324 | 325 | **Example:** 326 | ```ts 327 | const $ = O.just('AIR') 328 | O.forEach(console.log, $) // logs `AIR` 329 | ``` 330 | 331 | ## never 332 | 333 | ```ts 334 | function never(): Observable 335 | ``` 336 | 337 | ## subject 338 | 339 | ```ts 340 | function subject(): Observable & Observer 341 | ``` 342 | `Subject` is a special type that is both and `Observer` and also an `Observable`. 343 | 344 | **Example:** 345 | ```ts 346 | const $ = O.subject() 347 | 348 | O.forEach(console.log, $) // logs A 349 | 350 | $.next('A') 351 | 352 | ``` 353 | 354 | ## fromNodeStream 355 | 356 | ```ts 357 | function fromNodeStream(stream: Stream): Observable 358 | ``` 359 | 360 | Converts a [NodeStream] into an observable. 361 | 362 | 363 | # Operators 364 | These are functions that take in one or more streams as arguments and returns a another stream as a result. For Eg — `map(x => x + 1, a$)`. Here `map` takes in an *iterator* that increments each value emitted in the stream `a$` and returns a new stream containing the incremented values. 365 | 366 | ## delay 367 | 368 | ```ts 369 | function delay(duration: number, source: IObservabl 370 | e): 371 | Observable``` 372 | Takes in two params viz `duration` and the `source` stream and delays each value by the given duration. 373 | 374 | **Example:** 375 | ```ts 376 | const $ = O.delay(100, O.of('hi')) // emits 'hi' after 100ms 377 | ``` 378 | 379 | 380 | ## filter 381 | ```ts 382 | function filter(predicate: (t: any) => boolean, source: Observable): Observable 383 | ``` 384 | 385 | Filters down the values emitted by the source observable based on the predicate that is passed. 386 | 387 | **Example:** 388 | ```ts 389 | const $ = O.filter( 390 | i => i > 2, 391 | O.fromArray([1, 2, 3, 4]) 392 | ) // emits '3' and '4' only 393 | ``` 394 | 395 | 396 | ## flatMap 397 | 398 | ```ts 399 | function flatMap(fn : (i: any) => Observable, source: Observable): Observable 400 | ``` 401 | 402 | Flattens higher order observables, ie those observables that emit values that themselves are observables. It takes in two arguments — Mapping function and a source observable. The mapping function gets each of the values that is emitted by the source stream and is supposed to return a new object which is of observable type. 403 | 404 | **Example:** 405 | ```ts 406 | const $ = O.flatMap( 407 | () => O.of('Hello'), 408 | O.interval(1000) 409 | ) 410 | ``` 411 | 412 | ## join 413 | 414 | ```ts 415 | function join(source: Observable): Observable 416 | ``` 417 | 418 | It is a special case of `flatMap` where the project function is identity 419 | 420 | **Example:** 421 | ```ts 422 | const $ = O.join( 423 | O.just(O.interval(1000)) 424 | ) 425 | ``` 426 | 427 | It is a special case of [mergeMap](#mergeMap) with concurrency set to `NUMBER.POSITIVE_INFINITY`. 428 | 429 | 430 | ## mergeMap 431 | 432 | ```ts 433 | function mergeMap(concurrency: Observable,project: (s) => Observable,source: Observable): Observable 434 | ``` 435 | `mergeMap()` is a very generic and yet a very powerful operator. It converts a higher order stream into a flattened stream. The `concurrency` helps in keeping a check on the maximum number of subscriptions the operator can have. Unlike `RxJS` the concurrency can be dynamically controlled using the concurrency Observable. 436 | 437 | **Example:** 438 | ```ts 439 | const $ = O.mergeMap( 440 | O.just(1), 441 | (ev) => O.slice(0, 3, O.interval(100)), 442 | O.fromEvent(document, 'click') 443 | ) 444 | ``` 445 | ## concatMap 446 | 447 | ```ts 448 | function concatMap(fn : (i: any) => Observable, source: Observable): Observable 449 | ``` 450 | Its a special case for [mergeMap](#mergeMap) where the `concurrency` is set to `one`. This ensures unless the previous subscriptions end new ones are not created. 451 | 452 | ## combine 453 | ```ts 454 | function combine(a: Selector, s: Observable[]): Observable 455 | ``` 456 | Combines multiple streams into one using a selector. 457 | 458 | 459 | ## concat 460 | ```ts 461 | function concat(a: Observable, b: Observable): Observable 462 | ``` 463 | Initially subscribes to the first Observable and once that is completed, the second observable is subscribed to. 464 | 465 | 466 | ## mapTo 467 | 468 | ```ts 469 | function mapTo(value: any, source: Observable): Observable 470 | ``` 471 | Converts any value that is emitted in a stream to the provided value in the first argument. 472 | 473 | **Example:** 474 | ```ts 475 | const $ = O.mapTo( 476 | 'Foo', 477 | O.of('Bar') 478 | ) // emits `Foo` 479 | ``` 480 | 481 | ## map 482 | 483 | ```ts 484 | function map(fn: (i: any) => any, source: Observable): Observable 485 | ``` 486 | Transforms an input stream of values into another. 487 | 488 | **Example:** 489 | 490 | ```ts 491 | const $ = O.map(i => i + 1, O.of(100)) // emits 101 492 | ``` 493 | 494 | ## merge 495 | 496 | ```ts 497 | function merge(sources: Observable[]): Observable 498 | ``` 499 | 500 | Takes in a list of observables and creates a common observable stream that emits a value whenever any one of them fires. 501 | 502 | **Example:** 503 | ```ts 504 | const $ = O.merge([ 505 | O.delay(1000, O.of('A')), 506 | O.of('B') 507 | ]) // emits 'B' first and the after 1000ms emits `A` 508 | ``` 509 | 510 | ## multicast 511 | ```ts 512 | function multicast(source: Observable): Observable 513 | ``` 514 | Takes in a source stream and returns another. All subscriptions on the returned stream share across a common subscription. This turns out to be relatively a lot more efficient. 515 | 516 | ## reduce 517 | ```ts 518 | function reduce( 519 | reducer: (memory: any, value: any) => any, 520 | seed: any, 521 | source: Observable 522 | ): Observable 523 | ``` 524 | 525 | Like a standard `Array.reduce` operator it keeps calling the reducer function until the source stream end and on completion of the source, the last value is emitted. 526 | 527 | **Example:** 528 | ```ts 529 | const $ = O.reduce( 530 | (a, b) => a + b 531 | 0, 532 | O.fromArray([1, 2, 3, 4]) 533 | ) // emits 10 534 | ``` 535 | 536 | 537 | ## sample 538 | 539 | ```ts 540 | function sample( 541 | fn: (...t: any) => any, 542 | sampler: Observable, 543 | sources: Observable[] 544 | ): Observable 545 | ``` 546 | Takes in multiple sources and a sample source and returns a new observable which emits value whenever the sampler emits one. The emitted value is created by passing the last values of each of the sources to the function that is passed as the first argument to the `sample` function. 547 | 548 | **Example:** 549 | 550 | ```ts 551 | O.sample( 552 | (a, b) => [a, b], 553 | O.interval(1000), 554 | [ 555 | O.mapTo('A', O.interval(100)), 556 | O.mapTo('B', O.interval(200)) 557 | ] 558 | ) // emits ['A', 'B'] every 1000ms 559 | ``` 560 | 561 | ## scan 562 | ```ts 563 | function scan( 564 | reducer: (memory: any, value: any) => any, 565 | seed: any, 566 | source: Observable 567 | ): Observable 568 | ``` 569 | Its like [reduce](#reduce) but it emits all intermediatory results also. 570 | 571 | **Example:** 572 | 573 | ```ts 574 | 575 | O.scan( 576 | (a, b) => a + b, 577 | 0, 578 | O.mapTo(1, O.interval(1000)) 579 | ) // emits 1, 2, 3 and so on every 1000ms 580 | 581 | ``` 582 | 583 | ## skipRepeats 584 | 585 | ```ts 586 | function skipRepeats( 587 | comparator: (previous, current) => boolean, 588 | source: Observable 589 | ): Observable 590 | ``` 591 | 592 | Takes in a source stream and a comparator function. The comparator is called with two arguments - the previous value and the current value and if the return is true the current value is emitted in the resultant stream. 593 | 594 | **Example:** 595 | ```ts 596 | O.skipRepeats( 597 | (a, b) => a * a === b * b, 598 | O.fromArray([1, 2, -2, -3, 3, 3, 2]) 599 | ) // emits 1, 2, -3, 2 600 | ``` 601 | 602 | 603 | ## slice 604 | 605 | ```ts 606 | function slice(start: number, count: number, source: Observable): Observable 607 | ``` 608 | 609 | Just like a normal `Array.slice` it selectively emits values after `start` number of values have been emitted and up until a max of `count` has been reached. 610 | 611 | **Example:** 612 | ```ts 613 | O.slice(2, 2, O.fromArray([1, 2, 3, 4, 5, 6, ,7])) // emits 3, 4 614 | ``` 615 | 616 | ## switchLatest 617 | 618 | It's Exactly like the [join] operator except that it doesn't wait for the internal observables to finish before it subscribe on the next one that is emitted by the source. 619 | 620 | ```ts 621 | function switchLatest(source: Observable): Observable 622 | ``` 623 | 624 | 625 | ## switchMap 626 | 627 | ```ts 628 | function switchMap( 629 | fn: (i: any) => Observable, 630 | Observable 631 | ): Observable 632 | ``` 633 | 634 | Exactly like [flatMap](#flatMap) except that it doesn't wait for the child streams to complete like how [switchLatest](#switchLatest) works. 635 | 636 | 637 | ## tap 638 | 639 | ```ts 640 | function tap(fn: (i) => void, source: Observable): Observable 641 | ``` 642 | 643 | It works like [map](#map) but doesn't ignores the return value of the mapping function. 644 | 645 | **Example:** 646 | 647 | ```ts 648 | 649 | // console.log logs the transmitted values but the return value is ignored 650 | O.tap( 651 | console.log, // console.log() returns undefined which is ignored 652 | O.of(1, 2, 3) // emits 1, 2, 3 653 | ) 654 | ``` 655 | 656 | ## unique 657 | 658 | ```ts 659 | function unique(source: Observable): Observable 660 | ``` 661 | 662 | Emits unique values from a stream. 663 | 664 | **Example:** 665 | 666 | ```ts 667 | O.unique( 668 | O.of(1, 1, 2, 3, 1, 3) // emits 1, 2, 3 669 | ) 670 | ``` 671 | 672 | ## uniqueWith 673 | 674 | ```ts 675 | function uniqueWith(set: Set, source: Observable): Observable 676 | ``` 677 | 678 | Exactly like `unique` but it allows us to pass a custom `Set` to maintain a duplicity record. 679 | 680 | **Example:** 681 | 682 | ```ts 683 | O.uniqueWith( 684 | O.of(1, 1, 2, 3, 1, 3) // emits 1, 2, 3 685 | ) 686 | ``` 687 | 688 | # Testing 689 | 690 | ## TestScheduler 691 | `TestScheduler` is a custom implementation of `Scheduler` which helps in writing unit tests. Apart from the base `Scheduler` functions it has its own functions as well 692 | 693 | ```ts 694 | interface TestScheduler extends Scheduler { 695 | // Updates the system clock internally by 1ms and runs all the pending tasks in the queue 696 | tick (): void 697 | 698 | // Updates the clock by the given duration and keeps executing the tasks as they are scheduled. 699 | advanceBy (duration: number): void 700 | 701 | // Update the clock to an absolute time 702 | advanceTo (duration: number): void 703 | 704 | // Creates a test observer and subscribes to the observable returned by the function 705 | start(func: () => Observable): void 706 | 707 | // Creates a "Cold" TestObservable 708 | Cold (events: Array): Observable 709 | 710 | // Creates a "Hot" Test Observable 711 | Hot (events: Array): Observable 712 | 713 | // Creates a TestObserver. TestObserver keeps log of when and what type of an event was fired. 714 | Observer (): Observer 715 | 716 | // Schedules multiple jobs using an array of tasks 717 | timeline (tasks: [[Number, () => void]]): void 718 | } 719 | ``` 720 | 721 | **Example:** 722 | ```ts 723 | import {compose, add} from 'ramda' 724 | import {createTestScheduler, EVENT} from 'observable-air/test' 725 | import * as assert from 'assert' 726 | import * as O from 'observable-air' 727 | 728 | // create a stream that creates the first 5 even values. 729 | const even = compose( 730 | O.slice(0, 5), 731 | O.scan(add(2), -2), 732 | O.interval 733 | ) 734 | 735 | // creates a stream that emits even values every 100ms 736 | const $ = even(100) 737 | 738 | // logs 0 then after 100ms logs 2 then 4 and so on until 8 739 | O.forEach(console.log, $) // takes 500ms to complete the test 740 | 741 | // Testing using Assert 742 | 743 | const tScheduler = createTestScheduler() 744 | 745 | // runs async code synchronously 746 | const {results} = tScheduler.start(() => even(100)) 747 | 748 | assert.deepEquals(results, [ 749 | EVENT.next(200, 0), 750 | EVENT.next(300, 2), 751 | EVENT.next(400, 4) 752 | EVENT.next(500, 6), 753 | EVENT.next(600, 8), 754 | EVENT.complete(600) 755 | ]) 756 | ``` 757 | ## marble 758 | ```ts 759 | marble(events: string): Array 760 | ``` 761 | 762 | Converts a string of marble events into an array of `ObservableEvent`s which is very useful for testing 763 | 764 | **Example:** 765 | ```ts 766 | marble('-1--2--3--4|') 767 | /* outputs 768 | [ 769 | Event.next(220, '1'), 770 | Event.next(250, '2'), 771 | Event.next(280, '3'), 772 | Event.next(310, '4'), 773 | Event.complete(320) 774 | ] 775 | 776 | */ 777 | 778 | ``` 779 | 780 | ## toMarble 781 | ```ts 782 | toMarble(events: Array): string 783 | ``` 784 | 785 | Opposite of [#marble] it takes in an array of `ObservableEvent` and converts them into a marble string 786 | 787 | **Example:** 788 | ```ts 789 | toMarble([ 790 | Event.next(220, '1'), 791 | Event.next(250, '2'), 792 | Event.next(280, '3'), 793 | Event.next(310, '4'), 794 | Event.complete(320) 795 | ]) 796 | /* outputs 797 | 798 | '-1--2--3--4|' 799 | 800 | */ 801 | 802 | ``` 803 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tushar Mathur 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Observable Air 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/tusharmath/observable-air.svg)](https://greenkeeper.io/) 4 | 5 | [![Build Status](https://travis-ci.org/tusharmath/rwc.svg?branch=master)](https://travis-ci.org/tusharmath/observable-air) 6 | [![npm](https://img.shields.io/npm/v/observable-air.svg)](https://www.npmjs.com/package/observable-air) 7 | [![Coverage Status](https://coveralls.io/repos/github/tusharmath/observable-air/badge.svg)](https://coveralls.io/github/tusharmath/observable-air) 8 | 9 | A **4kb** high performance alternative to [RxJS]. 10 | 11 | If you are new to reactive programming then you should definitely checkout — [The introduction to Reactive Programming you've been missing] 12 | 13 | ## Links 14 | - [Usage](#usage) 15 | - [Example](#example) 16 | - [Installation](#installation) 17 | - [Why an RxJS Alternative?](#why-an-rxjs-alternative) 18 | - [API] 19 | - [Demo] 20 | 21 | [RxJS]: https://github.com/ReactiveX/rxjs 22 | [Observable Proposal]: https://github.com/tc39/proposal-observable 23 | [Ramda]: http://ramdajs.com 24 | [download and parsing]: https://medium.com/@addyosmani/javascript-start-up-performance-69200f43b201#.upm9f4v8u 25 | [The introduction to Reactive Programming you've been missing]: https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 26 | [RxJS Compatibility]: https://github.com/tusharmath/observable-air/blob/master/COMPATIBILITY.md 27 | [Benchmarks]: https://github.com/tusharmath/observable-air/blob/master/BENCHMARK.md 28 | [API]: https://github.com/tusharmath/observable-air/blob/master/API.md 29 | [Demo]: https://jsbin.com/paxudog/edit?js,console 30 | 31 | ## Usage 32 | 33 | **CommonJS** 34 | ```js 35 | const O = require('observable-air') 36 | ``` 37 | **ES6 or Typescript** 38 | ```ts 39 | import * as O from 'observable-air' 40 | ``` 41 | 42 | **HTML** 43 | ```html 44 | 45 | 46 | 52 | ``` 53 | 54 | 55 | ## Example 56 | ```js 57 | import * as O from 'observable-air' 58 | import * as R from 'ramda' 59 | 60 | const timer = R.compose( 61 | O.forEach(console.log), 62 | O.scan(R.add, 0), 63 | O.interval 64 | ) 65 | 66 | timer(100) // outputs 1, 2 ,3, 4 ... every 100ms 67 | timer(1000) // outputs 1, 2 ,3, 4 ... every 1000ms 68 | 69 | ``` 70 | 71 | ## Installation 72 | 73 | ```bash 74 | npm install observable-air --save 75 | ``` 76 | 77 | ## Why an RxJS Alternative? 78 | RxJS is awesome and an inspiration for this a lot of other observable libraries out there. Air is focussed on some fundamental things such as — 79 | 80 | 1. **Smaller Footprint:** Rx has a lot of operators which makes the library quite large in size. Air has a much smaller number of operators and is architected such that more sophisticated operators can be created using the already available ones without any performance overhead of composition. 81 | 82 | 2. **Functional Over Fluidic:** Air embraces a *functional* API rather than a *fludic* one. All the functions come *curried* out of the box and work really well with [ramda]. 83 | 84 | 3. **Performance:** Air is significantly faster than Rx, benchmarks coming up soon. 85 | 86 | 4. **Virtual Time:** In Rx `VirtualTimeScheduler` is passed as an argument to each operator, in Air the `TestScheduler` is passed once at the time of subscription and is internally shared up the chain of its parent operators or sources. 87 | 88 | [ramda]: http://ramdajs.com/docs/ 89 | 90 | -------------------------------------------------------------------------------- /benchmarks.md: -------------------------------------------------------------------------------- 1 | **Node:** v8.3.0 2 | **V8:** 6.0.286.52 3 | ``` 4 | ┌─────────────────────────────────────────────────────┬─────────────────┬─────────┐ 5 | │ name │ ops/sec │ samples │ 6 | ├─────────────────────────────────────────────────────┼─────────────────┼─────────┤ 7 | │ create │ 2,020 (±1.31%) │ 81 │ 8 | ├─────────────────────────────────────────────────────┼─────────────────┼─────────┤ 9 | │ file -> debounce │ 1,016 (±6.18%) │ 58 │ 10 | ├─────────────────────────────────────────────────────┼─────────────────┼─────────┤ 11 | │ file -> combine(sum3, [a, b, c]) -> reduce(sum2, 0) │ 91,646 (±2.73%) │ 76 │ 12 | ├─────────────────────────────────────────────────────┼─────────────────┼─────────┤ 13 | │ file -> map -> reduce │ 59 (±0.94%) │ 69 │ 14 | ├─────────────────────────────────────────────────────┼─────────────────┼─────────┤ 15 | │ file -> scan -> reduce │ 33 (±0.99%) │ 76 │ 16 | ├─────────────────────────────────────────────────────┼─────────────────┼─────────┤ 17 | │ file -> takeN(0, n/10) │ 505 (±1.01%) │ 83 │ 18 | ├─────────────────────────────────────────────────────┼─────────────────┼─────────┤ 19 | │ array(2) -> array(i) -> switchLatest │ 5,255 (±4.45%) │ 66 │ 20 | ├─────────────────────────────────────────────────────┼─────────────────┼─────────┤ 21 | │ tryCatch │ 2,788 (±0.87%) │ 94 │ 22 | ├─────────────────────────────────────────────────────┼─────────────────┼─────────┤ 23 | │ file -> mergeMap │ 52 (±2.07%) │ 78 │ 24 | └─────────────────────────────────────────────────────┴─────────────────┴─────────┘ 25 | ``` 26 | -------------------------------------------------------------------------------- /benchmarks/bm.combine.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 08/12/16. 3 | */ 4 | import {Suite} from 'benchmark' 5 | import {combine} from '../src/operators/Combine' 6 | import {reduce} from '../src/operators/Reduce' 7 | import {fromArray} from '../src/sources/FromArray' 8 | import {array, IDeferred, run} from './lib' 9 | 10 | const a = array(1e2) 11 | const b = array(1e2) 12 | const c = array(1e2) 13 | const sum3 = (a: number, b: number, c: number) => a + b + c 14 | const sum2 = (a: number, b: number) => a + b 15 | 16 | export function bm_fromArray_combine(suite: Suite) { 17 | return suite.add( 18 | 'file -> combine(sum3, [a, b, c]) -> reduce(sum2, 0)', 19 | (d: IDeferred) => 20 | run( 21 | reduce( 22 | sum2, 23 | 0, 24 | combine(sum3, [fromArray(a), fromArray(b), fromArray(c)]) 25 | ), 26 | d 27 | ), 28 | {defer: true} 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /benchmarks/bm.concat.ts: -------------------------------------------------------------------------------- 1 | import {Suite} from 'benchmark' 2 | import {concat} from '../src/operators/Concat' 3 | import {fromArray} from '../src/sources/FromArray' 4 | /** 5 | * Created by tushar on 05/09/17. 6 | */ 7 | import {array, IDeferred, run} from './lib' 8 | 9 | const a = array(1e3) 10 | const b = array(1e3) 11 | 12 | export function bm_concat(suite: Suite) { 13 | return suite.add( 14 | '(file, file) -> concat', 15 | (d: IDeferred) => run(concat(fromArray(a), fromArray(b)), d), 16 | { 17 | defer: true 18 | } 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /benchmarks/bm.create.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 09/12/16. 3 | */ 4 | import {Suite} from 'benchmark' 5 | import {Observable} from '../src/internal/Observable' 6 | import {IObserver} from '../src/internal/Observer' 7 | import {IDeferred, run} from './lib' 8 | 9 | function subscriber(observer: IObserver) { 10 | for (var i = 0; i < 1e6; ++i) { 11 | observer.next(i) 12 | } 13 | observer.complete() 14 | } 15 | 16 | export function bm_create(suite: Suite) { 17 | return suite.add( 18 | 'create', 19 | (d: IDeferred) => run(new Observable(subscriber), d), 20 | { 21 | defer: true 22 | } 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /benchmarks/bm.debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 12/02/17. 3 | */ 4 | import {Suite} from 'benchmark' 5 | import {debounce} from '../src/operators/Debounce' 6 | import {fromArray} from '../src/sources/FromArray' 7 | import {array, IDeferred, run} from './lib' 8 | 9 | const a = array(1e3) 10 | export function bm_debounce(suite: Suite) { 11 | return suite.add( 12 | 'file -> debounce', 13 | (d: IDeferred) => run(debounce(100, fromArray(a)), d), 14 | { 15 | defer: true 16 | } 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /benchmarks/bm.fromArray-map-reduce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 05/11/16. 3 | */ 4 | 5 | import {Suite} from 'benchmark' 6 | import {filter} from '../src/operators/Filter' 7 | import {map} from '../src/operators/Map' 8 | import {reduce} from '../src/operators/Reduce' 9 | import {fromArray} from '../src/sources/FromArray' 10 | import {add1, array, even, IDeferred, run, sum} from './lib' 11 | 12 | const a = array(1e6) 13 | export function bm_fromArray_map_reduce(suite: Suite) { 14 | return suite.add( 15 | 'file -> map -> reduce', 16 | (d: IDeferred) => 17 | run(reduce(sum, 0, map(add1, filter(even, fromArray(a)))), d), 18 | {defer: true} 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /benchmarks/bm.fromArray-scan-reduce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 05/11/16. 3 | */ 4 | 5 | import {Suite} from 'benchmark' 6 | import {reduce} from '../src/operators/Reduce' 7 | import {scan} from '../src/operators/Scan' 8 | import {fromArray} from '../src/sources/FromArray' 9 | import {array, IDeferred, passthrough, run, sum} from './lib' 10 | 11 | const a = array(1e6) 12 | export function bm_fromArray_scan_reduce(suite: Suite) { 13 | return suite.add( 14 | 'file -> scan -> reduce', 15 | (d: IDeferred) => 16 | run(reduce(passthrough, 0, scan(sum, 0, fromArray(a))), d), 17 | {defer: true} 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /benchmarks/bm.fromArray-takeN.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 05/11/16. 3 | */ 4 | 5 | import {Suite} from 'benchmark' 6 | import {slice} from '../src/operators/Slice' 7 | import {fromArray} from '../src/sources/FromArray' 8 | import {array, IDeferred, run} from './lib' 9 | 10 | const a = array(1e6) 11 | export function bm_fromArray_takeN(suite: Suite) { 12 | return suite.add( 13 | 'file -> takeN(0, n/10)', 14 | (d: IDeferred) => run(slice(0, 1e6 / 10, fromArray(a)), d), 15 | {defer: true} 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /benchmarks/bm.mergeMap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 31/08/17. 3 | */ 4 | 5 | import {Suite} from 'benchmark' 6 | import {mergeMap} from '../src/operators/MergeMap' 7 | import {just} from '../src/sources/Create' 8 | import {fromArray} from '../src/sources/FromArray' 9 | import {array, IDeferred, run} from './lib' 10 | 11 | const a = array(1e3).map(i => array(1e3)) 12 | 13 | export function bm_mergeMap(suite: Suite) { 14 | return suite.add( 15 | 'file -> mergeMap', 16 | (d: IDeferred) => run(mergeMap(just(1e2), fromArray, fromArray(a)), d), 17 | { 18 | defer: true 19 | } 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /benchmarks/bm.switch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 16/02/17. 3 | */ 4 | import {Suite} from 'benchmark' 5 | import {map} from '../src/operators/Map' 6 | import {switchLatest} from '../src/operators/Switch' 7 | import {fromArray} from '../src/sources/FromArray' 8 | import {array, IDeferred, run} from './lib' 9 | 10 | const a = array(1e2) 11 | 12 | export function bm_switch(suite: Suite) { 13 | return suite.add( 14 | 'array(2) -> array(i) -> switchLatest', 15 | (d: IDeferred) => 16 | run(switchLatest(map(i => fromArray(array(i)), fromArray(a))), d), 17 | {defer: true} 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /benchmarks/bm.tryCatch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 06/11/16. 3 | */ 4 | import {Suite} from 'benchmark' 5 | import {ISafeValue, tryCatch} from '../src/internal/Utils' 6 | import {array} from './lib' 7 | 8 | function addThis(b: number) { 9 | this.a = this.a + b 10 | if (this.a % 2 === 0) { 11 | throw 'ERROR' 12 | } 13 | return this 14 | } 15 | const safelyAddThis = tryCatch(addThis) 16 | 17 | export function testFunction(arr: number[]): Array> { 18 | const results = [] 19 | for (var i = 0; i < arr.length; ++i) { 20 | results.push(safelyAddThis.call({a: 100}, i)) 21 | } 22 | return results 23 | } 24 | const arr = array(1e3) 25 | export function bm_tryCatch(suite: Suite) { 26 | return suite.add('tryCatch', () => testFunction(arr)) 27 | } 28 | -------------------------------------------------------------------------------- /benchmarks/lib.ts: -------------------------------------------------------------------------------- 1 | import {IObservable} from '../src/internal/Observable' 2 | import {IObserver} from '../src/internal/Observer' 3 | import {createScheduler, IScheduler} from '../src/schedulers/Scheduler' 4 | 5 | /** 6 | * Created by tushar.mathur on 05/11/16. 7 | */ 8 | 9 | const Table = require('cli-table2') 10 | 11 | export interface IDeferred { 12 | resolve(): void 13 | } 14 | export function add1(x: number) { 15 | return x + 1 16 | } 17 | export function even(e: number) { 18 | return e % 2 === 0 19 | } 20 | export function sum(a: number, b: number) { 21 | return a + b 22 | } 23 | 24 | export function passthrough(z: any, x: any) { 25 | return x 26 | } 27 | 28 | export const scheduler = createScheduler() as IScheduler 29 | 30 | class BmObserver implements IObserver { 31 | constructor(private d: IDeferred) {} 32 | 33 | next(val: T): void {} 34 | 35 | error(err: Error): void { 36 | throw err 37 | } 38 | 39 | complete(): void { 40 | this.d.resolve() 41 | } 42 | } 43 | 44 | export function run(observable: IObservable, d: IDeferred) { 45 | observable.subscribe(new BmObserver(d), scheduler) 46 | } 47 | 48 | export function array(n: number) { 49 | const a = new Array(n) 50 | for (var i = 0; i < a.length; ++i) { 51 | a[i] = i 52 | } 53 | return a 54 | } 55 | 56 | const table = new Table({ 57 | head: ['name', 'ops/sec', 'samples'] 58 | }) 59 | 60 | export const onCycle = (event: any) => { 61 | const target = event.target 62 | table.push([ 63 | target.name, 64 | `${Math.floor(target.hz).toLocaleString()} (±${Math.round( 65 | target.stats.rme * 100 66 | ) / 100}%)`, 67 | target.stats.sample.length 68 | ]) 69 | } 70 | 71 | export const onEnd = () => { 72 | console.log('```') 73 | console.log(table.toString()) 74 | console.log('```') 75 | } 76 | -------------------------------------------------------------------------------- /benchmarks/run.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 01/10/16. 3 | */ 4 | import {Suite} from 'benchmark' 5 | import {bm_fromArray_combine} from './bm.combine' 6 | import {bm_concat} from './bm.concat' 7 | import {bm_create} from './bm.create' 8 | import {bm_debounce} from './bm.debounce' 9 | import {bm_fromArray_map_reduce} from './bm.fromArray-map-reduce' 10 | import {bm_fromArray_scan_reduce} from './bm.fromArray-scan-reduce' 11 | import {bm_fromArray_takeN} from './bm.fromArray-takeN' 12 | import {bm_mergeMap} from './bm.mergeMap' 13 | import {bm_switch} from './bm.switch' 14 | import {bm_tryCatch} from './bm.tryCatch' 15 | import {onCycle, onEnd} from './lib' 16 | 17 | // Run All Benchmarks 18 | 19 | console.log('**Node:**', process.version) 20 | console.log('**V8:** ', process.versions.v8) 21 | 22 | const suite = new Suite() 23 | bm_create(suite) 24 | bm_debounce(suite) 25 | bm_fromArray_combine(suite) 26 | bm_fromArray_map_reduce(suite) 27 | bm_fromArray_scan_reduce(suite) 28 | bm_fromArray_takeN(suite) 29 | bm_switch(suite) 30 | bm_tryCatch(suite) 31 | bm_mergeMap(suite) 32 | bm_concat(suite) 33 | suite 34 | .on('cycle', onCycle) 35 | .on('complete', onEnd) 36 | .run() 37 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 11/10/16. 3 | */ 4 | 5 | 'use strict' 6 | 7 | import commonjs from 'rollup-plugin-commonjs' 8 | import uglify from 'rollup-plugin-babili' 9 | 10 | const input = './src/main.js' 11 | export default [ 12 | { 13 | input, 14 | output: { 15 | exports: 'named', 16 | name: 'O', 17 | format: 'umd', 18 | file: './.dist/observable-air.umd.min.js' 19 | }, 20 | plugins: [uglify(), commonjs({})] 21 | }, 22 | { 23 | input, 24 | output: { 25 | exports: 'named', 26 | name: 'O', 27 | format: 'umd', 28 | file: './.dist/observable-air.umd.dev.js' 29 | }, 30 | plugins: [commonjs({})] 31 | }, 32 | { 33 | input, 34 | output: { 35 | format: 'es', 36 | file: './.dist/observable-air.es.dev.js' 37 | }, 38 | plugins: [commonjs({})] 39 | }, 40 | { 41 | input, 42 | output: { 43 | format: 'es', 44 | file: './.dist/observable-air.es.min.js' 45 | }, 46 | plugins: [uglify(), commonjs({})] 47 | } 48 | ] 49 | -------------------------------------------------------------------------------- /extra.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 10/12/16. 3 | */ 4 | 5 | import {Readable, Stream} from 'stream' 6 | import {IObservable} from './src/internal/Observable' 7 | import {IObserver} from './src/internal/Observer' 8 | import {ISubscription} from './src/internal/Subscription' 9 | import * as O from './src/main' 10 | import {combine} from './src/operators/Combine' 11 | import {debounce} from './src/operators/Debounce' 12 | import {createScheduler, IScheduler} from './src/schedulers/Scheduler' 13 | import {toNodeStream} from './src/sinks/ToNodeStream' 14 | import {fromNodeStream} from './src/sources/FromNodeStream' 15 | 16 | export {Observable} from './src/internal/Observable' 17 | 18 | export class Air implements IObservable { 19 | constructor(private src: IObservable) {} 20 | subscribe( 21 | observer: IObserver, 22 | scheduler: IScheduler = createScheduler() 23 | ): ISubscription { 24 | return this.src.subscribe(observer, scheduler) 25 | } 26 | 27 | concatMap(fn: (t: T) => IObservable) { 28 | return new Air(O.concatMap(fn, this)) 29 | } 30 | 31 | concat(src: IObservable) { 32 | return new Air(O.concat(this, src)) 33 | } 34 | 35 | delay(t: number) { 36 | return new Air(O.delay(t, this)) 37 | } 38 | 39 | filter(fn: (t: T) => boolean) { 40 | return new Air(O.filter(fn, this)) 41 | } 42 | 43 | flatMap(fn: (t: T) => IObservable) { 44 | return new Air(O.flatMap(fn, this)) 45 | } 46 | 47 | forEach(fn: (t: T) => void) { 48 | return O.forEach(fn, this) 49 | } 50 | 51 | join() { 52 | return new Air(O.join(this as any)) 53 | } 54 | 55 | mapTo(t: S) { 56 | return new Air(O.mapTo(t, this)) 57 | } 58 | 59 | map(fn: (t: T) => S) { 60 | return new Air(O.map(fn, this)) 61 | } 62 | 63 | mergeMap(n: IObservable, project: (t: T) => IObservable) { 64 | return new Air(O.mergeMap(n, project, this)) 65 | } 66 | 67 | multicast() { 68 | return new Air(O.multicast(this)) 69 | } 70 | 71 | reduce(fn: (memory: R, current: T) => R, seed: R) { 72 | return new Air(O.reduce(fn, seed, this)) 73 | } 74 | 75 | sample(fn: (...e: Array) => T, sources: Array>) { 76 | return new Air(O.sample(fn, this, sources)) 77 | } 78 | 79 | skipRepeats(fn: (a: T, b: T) => boolean) { 80 | return new Air(O.skipRepeats(fn, this)) 81 | } 82 | 83 | slice(start: number, count: number) { 84 | return new Air(O.slice(start, count, this)) 85 | } 86 | 87 | switchLatest() { 88 | return new Air(O.switchLatest(this as any)) 89 | } 90 | 91 | switchMap(fn: (t: T) => IObservable) { 92 | return new Air(O.switchMap(fn, this)) 93 | } 94 | 95 | tap(fn: (t: T) => void) { 96 | return new Air(O.tap(fn, this)) 97 | } 98 | 99 | uniqueWith(set: Set) { 100 | return new Air(O.uniqueWith(set, this)) 101 | } 102 | 103 | unique() { 104 | return new Air(O.unique(this)) 105 | } 106 | 107 | debounce(t: number) { 108 | return new Air(debounce(t, this)) 109 | } 110 | 111 | toNodeStream(): Stream { 112 | return toNodeStream(this) 113 | } 114 | 115 | toPromise() { 116 | return O.toPromise(this) 117 | } 118 | 119 | scan(reducer: (memory: R, current: T) => R, seed: R) { 120 | return new Air(O.scan(reducer, seed, this)) 121 | } 122 | 123 | static combine(selector: (...t: any[]) => T, sources: IObservable[]) { 124 | return new Air(combine(selector, sources)) 125 | } 126 | 127 | static fromNodeStream(node: Readable) { 128 | return new Air(fromNodeStream(node)) 129 | } 130 | 131 | static empty() { 132 | return new Air(O.empty()) 133 | } 134 | 135 | static frames() { 136 | return new Air(O.frames()) 137 | } 138 | 139 | static fromArray(arr: Array) { 140 | return new Air(O.fromArray(arr)) 141 | } 142 | 143 | static fromDOM(el: HTMLElement, event: string) { 144 | return new Air(O.fromDOM(el, event)) 145 | } 146 | 147 | static fromPromise(fn: () => Promise) { 148 | return new Air(O.fromPromise(fn)) 149 | } 150 | 151 | static interval(t: number) { 152 | return new Air(O.interval(t)) 153 | } 154 | 155 | static just(t: T) { 156 | return new Air(O.just(t)) 157 | } 158 | 159 | static never() { 160 | return new Air(O.never()) 161 | } 162 | 163 | static of(...t: T[]) { 164 | return new Air(O.of(...t)) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "observable-air", 3 | "description": "Observables for the calorie conscious", 4 | "main": "./src/main", 5 | "typings": "./src/main.d.ts", 6 | "scripts": { 7 | "benchmark": "tsc; node ./benchmarks/run", 8 | "benchmark:write": "yarn benchmark > benchmarks.md", 9 | "cleanup": "find ./src -type f -name '*.js' -delete && find ./src -type f -name '*.map' -delete", 10 | "coverage": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 11 | "hydra": "node --trace-hydrogen --trace-phase=Z --trace-deopt --code-comments --hydrogen-track-positions --redirect-code-traces --redirect-code-traces-to=code.asm ./benchmarks/run", 12 | "prepublish": "tsc -d && npm run build", 13 | "test": "tsc && mocha --reporter=min", 14 | "test:watch": "mocha --watch", 15 | "build": "rollup -c ./config/rollup.config.js", 16 | "lint": "git ls-files | grep '.ts$' | xargs tslint", 17 | "lint:fix": "git ls-files | grep '.ts$' | xargs tslint --fix", 18 | "prettier": "git ls-files | grep '.ts$' | xargs prettier --write --config=.prettierrc", 19 | "travis-deploy-once": "travis-deploy-once", 20 | "semantic-release": "semantic-release" 21 | }, 22 | "author": "", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@types/benchmark": "^1.0.30", 26 | "@types/mocha": "^5.2.7", 27 | "@types/node": "^12.7.2", 28 | "benchmark": "^2.1.2", 29 | "cli-table2": "^0.2.0", 30 | "coveralls": "^3.0.6", 31 | "cz-conventional-changelog": "^3.0.2", 32 | "ghooks": "^2.0.4", 33 | "mocha": "^6.2.0", 34 | "nyc": "^14.1.1", 35 | "prettier": "^1.18.2", 36 | "prettify": "^0.1.7", 37 | "request-promise": "^4.2.4", 38 | "rollup": "^1.19.4", 39 | "rollup-plugin-babili": "^3.0.0", 40 | "rollup-plugin-commonjs": "^10.0.2", 41 | "rollup-plugin-uglify": "^6.0.2", 42 | "semantic-release": "^15.13.21", 43 | "travis-deploy-once": "^5.0.0", 44 | "ts-node": "^8.3.0", 45 | "tslint": "^5.19.0", 46 | "typescript": "^3.5.3", 47 | "uglify": "^0.1.5", 48 | "validate-commit-msg": "^2.8.2" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/tusharmath/observable-air.git" 53 | }, 54 | "config": { 55 | "commitizen": { 56 | "path": "./node_modules/cz-conventional-changelog" 57 | }, 58 | "ghooks": { 59 | "commit-msg": "validate-commit-msg" 60 | } 61 | }, 62 | "version": "0.0.0-development", 63 | "dependencies": { 64 | "dbl-linked-list-ds": "^2.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/internal/ColdTestObservable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 10/10/16. 3 | */ 4 | 5 | import {IScheduler} from '../schedulers/Scheduler' 6 | import {EventError, EventNext, EventType, IObservableEvent} from './Events' 7 | import {IObserver} from './Observer' 8 | import {TestObservable} from './TestObservable' 9 | 10 | export function ColdTestObservable( 11 | scheduler: IScheduler, 12 | events: Array 13 | ) { 14 | return new TestObservable((observer: IObserver) => { 15 | let closed = false 16 | for (let i = 0; i < events.length && !closed; i++) { 17 | const event = events[i] 18 | if (event.type === EventType.next) { 19 | scheduler.delay( 20 | () => observer.next((>event).value), 21 | event.time 22 | ) 23 | } else if (event.type === EventType.complete) { 24 | scheduler.delay(() => observer.complete(), event.time) 25 | } else if (event.type === EventType.error) { 26 | scheduler.delay( 27 | () => observer.error((event).value), 28 | event.time 29 | ) 30 | } 31 | } 32 | return { 33 | unsubscribe() { 34 | closed = true 35 | }, 36 | get closed() { 37 | return closed 38 | } 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/internal/Container.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 08/12/16. 3 | */ 4 | 5 | export enum StreamStatus { 6 | IDLE, 7 | STARTED, 8 | COMPLETED 9 | } 10 | 11 | export function createArray(size: number, value: T) { 12 | const arr: Array = new Array(size) 13 | for (var i = 0; i < size; ++i) { 14 | arr[i] = value 15 | } 16 | return arr 17 | } 18 | 19 | export interface IContainer { 20 | next(value: T, id: number): void 21 | complete(id: number): boolean 22 | isDone(): boolean 23 | isOn(): boolean 24 | readonly values: any[] 25 | } 26 | 27 | class ValueContainer implements IContainer { 28 | values = new Array(this.total) 29 | private status = createArray(this.total, StreamStatus.IDLE) 30 | private started = 0 31 | private completed = 0 32 | 33 | constructor(private total: number) {} 34 | 35 | next(value: any, id: number) { 36 | if (this.status[id] === StreamStatus.IDLE) { 37 | this.status[id] = StreamStatus.STARTED 38 | this.started++ 39 | } 40 | this.values[id] = value 41 | } 42 | 43 | complete(id: number) { 44 | if (this.status[id] !== StreamStatus.COMPLETED) { 45 | this.status[id] = StreamStatus.COMPLETED 46 | this.completed++ 47 | } 48 | return this.isDone() 49 | } 50 | 51 | isDone() { 52 | return this.completed === this.total 53 | } 54 | 55 | isOn() { 56 | return this.started === this.total 57 | } 58 | } 59 | 60 | export const container = (count: number) => 61 | new ValueContainer(count) as IContainer 62 | -------------------------------------------------------------------------------- /src/internal/Events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 03/10/16. 3 | */ 4 | import {ISubscription} from './Subscription' 5 | 6 | export interface IObservableEvent { 7 | readonly type: EventType 8 | readonly time: number 9 | } 10 | 11 | export enum EventType { 12 | next = 'NEXT', 13 | error = 'ERROR', 14 | complete = 'COMPLETE', 15 | start = 'START', 16 | end = 'END' 17 | } 18 | 19 | export class EventNext implements IObservableEvent { 20 | type = EventType.next 21 | 22 | constructor(public time: number, public value: T) {} 23 | } 24 | 25 | export class EventError implements IObservableEvent { 26 | type = EventType.error 27 | 28 | constructor(public time: number, public value: Error) {} 29 | } 30 | 31 | export class EventComplete implements IObservableEvent { 32 | type = EventType.complete 33 | 34 | constructor(public time: number) {} 35 | } 36 | 37 | export class EventEnd implements IObservableEvent { 38 | type = EventType.end 39 | 40 | constructor(public time: number, public subscription: ISubscription) {} 41 | } 42 | 43 | export class EventStart implements IObservableEvent { 44 | type = EventType.start 45 | 46 | constructor(public time: number, public subscription: ISubscription) {} 47 | } 48 | 49 | export const EVENT = { 50 | next(time: number, value: T): EventNext { 51 | return new EventNext(time, value) 52 | }, 53 | 54 | error(time: number, value: Error) { 55 | return new EventError(time, value) 56 | }, 57 | 58 | complete(time: number) { 59 | return new EventComplete(time) 60 | }, 61 | 62 | start(time: number, subscription: ISubscription) { 63 | return new EventStart(time, subscription) 64 | }, 65 | 66 | end(time: number, subscription: ISubscription) { 67 | return new EventEnd(time, subscription) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/internal/HotTestObservable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 10/10/16. 3 | */ 4 | 5 | import {TestScheduler} from '../schedulers/TestScheduler' 6 | import {EventError, EventNext, EventType, IObservableEvent} from './Events' 7 | import {IObserver} from './Observer' 8 | import {TestObservable} from './TestObservable' 9 | 10 | export function dispatchEvents( 11 | event: IObservableEvent, 12 | observers: Array>, 13 | closed: Array 14 | ) { 15 | observers 16 | .filter((x, i) => !closed[i]) 17 | .forEach(ob => { 18 | if (event.type === EventType.next) 19 | return ob.next((event as EventNext).value) 20 | if (event.type === EventType.complete) return ob.complete() 21 | if (event.type === EventType.error) 22 | return ob.error((event as EventError).value) 23 | }) 24 | } 25 | 26 | export function HotTestObservable( 27 | scheduler: TestScheduler, 28 | events: Array 29 | ) { 30 | const observers: Array> = [] 31 | const closed: Array = [] 32 | events.forEach(ev => { 33 | scheduler.delay(() => dispatchEvents(ev, observers, closed), ev.time, 0) 34 | }) 35 | return new TestObservable((ob: IObserver) => { 36 | const i = observers.push(ob) - 1 37 | closed[i] = false 38 | return { 39 | unsubscribe() { 40 | closed[i] = true 41 | }, 42 | get closed() { 43 | return closed[i] 44 | } 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/internal/Marble.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 23/10/16. 3 | */ 4 | import {EVENT, EventNext, EventType, IObservableEvent} from './Events' 5 | import {DEFAULT_OPTIONS} from './TestOptions' 6 | 7 | export const SIZE = DEFAULT_OPTIONS.marbleSize 8 | export const START = DEFAULT_OPTIONS.subscriptionStart 9 | 10 | export function fromMarble( 11 | message: String, 12 | start = START, 13 | size = SIZE 14 | ): Array { 15 | const events: Array = [] 16 | let time = start 17 | for (let i = 0; i < message.length; ++i) { 18 | switch (message[i]) { 19 | case ' ': 20 | break 21 | case '(': 22 | let value = '' 23 | while (message[++i] !== ')') { 24 | value += message[i] 25 | } 26 | events.push(EVENT.next(time, value)) 27 | time += size 28 | break 29 | case '-': 30 | time += size 31 | break 32 | case '|': 33 | events.push(EVENT.complete(time)) 34 | time += size 35 | break 36 | case '#': 37 | events.push(EVENT.error(time, new Error('#'))) 38 | time += size 39 | break 40 | default: 41 | const m = parseInt(message[i], 10) 42 | if (isNaN(m)) events.push(EVENT.next(time, message[i])) 43 | else events.push(EVENT.next(time, m)) 44 | time += size 45 | break 46 | } 47 | } 48 | return events 49 | } 50 | 51 | export function toMarble( 52 | events: Array, 53 | start = START, 54 | size = SIZE 55 | ) { 56 | let time = start - size 57 | let message = '' 58 | events.forEach(ev => { 59 | if (ev.time % size !== 0) 60 | throw TypeError(`the time (${ev.time}) not a multiple of frame ${size}`) 61 | let count = (ev.time - time) / size 62 | time = ev.time 63 | while (count-- > 1) message += '-' 64 | switch (ev.type) { 65 | case EventType.next: 66 | const s = String((ev as EventNext).value) 67 | message += s.length > 1 ? `(${s})` : s 68 | break 69 | case EventType.error: 70 | message += '#' 71 | break 72 | case EventType.complete: 73 | message += '|' 74 | break 75 | case EventType.start: 76 | message += '^' 77 | break 78 | case EventType.end: 79 | message += '!' 80 | break 81 | } 82 | }) 83 | return message 84 | } 85 | -------------------------------------------------------------------------------- /src/internal/Mixins.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 06/09/17. 3 | */ 4 | 5 | import {IObserver} from './Observer' 6 | 7 | export type Constructor = new (...t: any[]) => T 8 | export class Virgin {} 9 | 10 | /** 11 | * Adds the error() handler 12 | */ 13 | export const ErrorMixin = (Base: TBase) => 14 | class ErrorMixin extends Base { 15 | readonly sink: IObserver 16 | 17 | error(error: Error) { 18 | this.sink.error(error) 19 | } 20 | } 21 | 22 | /** 23 | * Adds the complete() handler 24 | */ 25 | export const CompleteMixin = (Base: TBase) => 26 | class CompleteMixin extends Base { 27 | readonly sink: IObserver 28 | 29 | complete() { 30 | this.sink.complete() 31 | } 32 | } 33 | 34 | /** 35 | * Adds the complete() handler 36 | */ 37 | export const NextMixin = (Base: TBase) => 38 | class NextMixin extends Base { 39 | readonly sink: IObserver 40 | 41 | next(value: any) { 42 | this.sink.next(value) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/internal/Observable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 27/09/16. 3 | */ 4 | import {IScheduler} from '../schedulers/Scheduler' 5 | import {IObservable} from './Observable' 6 | import {IObserver} from './Observer' 7 | import {ISubscriberFunction} from './SubscriberFunction' 8 | import { 9 | CompositeSubscription, 10 | ISubscription, 11 | Subscription 12 | } from './Subscription' 13 | 14 | export interface IObservable { 15 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription 16 | } 17 | 18 | export class Observable implements IObservable { 19 | constructor(private f: ISubscriberFunction) {} 20 | 21 | run( 22 | cSub: CompositeSubscription, 23 | observer: IObserver, 24 | scheduler: IScheduler 25 | ) { 26 | cSub.add(new Subscription(this.f(observer, scheduler))) 27 | } 28 | 29 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 30 | const cSub = new CompositeSubscription() 31 | cSub.add(scheduler.asap(this.run.bind(this, cSub, observer, scheduler))) 32 | return cSub 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/internal/Observer.ts: -------------------------------------------------------------------------------- 1 | import {LinkedList, LinkedListNode} from 'dbl-linked-list-ds' 2 | /** 3 | * Created by tushar.mathur on 27/09/16. 4 | */ 5 | 6 | export interface IObserver { 7 | // Receives the next value in the sequence 8 | next(val: T): void 9 | 10 | // Receives the sequence error 11 | error(err: Error): void 12 | 13 | // Receives the sequence completion value 14 | complete(): void 15 | } 16 | 17 | export class CompositeObserver implements IObserver { 18 | private observers = new LinkedList>() 19 | 20 | get length() { 21 | return this.observers.length 22 | } 23 | 24 | add(observer: IObserver) { 25 | return this.observers.add(observer) 26 | } 27 | 28 | remove(node: LinkedListNode>) { 29 | return this.observers.remove(node) 30 | } 31 | 32 | next(val: T): void { 33 | var node = this.observers.head() 34 | while (node) { 35 | node.value.next(val) 36 | node = node.right 37 | } 38 | } 39 | 40 | error(err: Error): void { 41 | var node = this.observers.head() 42 | while (node) { 43 | node.value.error(err) 44 | node = node.right 45 | } 46 | } 47 | 48 | complete(): void { 49 | var node = this.observers.head() 50 | while (node) { 51 | node.value.complete() 52 | node = node.right 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/internal/Periodic.ts: -------------------------------------------------------------------------------- 1 | import {ISafeObserver} from './SafeObserver' 2 | import {ISubscription} from './Subscription' 3 | 4 | export class Periodic implements ISubscription { 5 | protected sub: ISubscription 6 | protected sink: ISafeObserver 7 | 8 | onEvent() { 9 | this.sink.next(undefined) 10 | if (this.sink.erred) this.unsubscribe() 11 | } 12 | 13 | unsubscribe(): void { 14 | this.sub.unsubscribe() 15 | } 16 | 17 | get closed() { 18 | return this.sub.closed 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/internal/SafeObserver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 29/01/17. 3 | */ 4 | import {CompleteMixin, ErrorMixin, Virgin} from './Mixins' 5 | import {IObserver} from './Observer' 6 | import {ISafeFunction, tryCatch} from './Utils' 7 | 8 | export interface ISafeObserver extends IObserver { 9 | erred: boolean 10 | } 11 | class SafeObserver extends ErrorMixin(CompleteMixin(Virgin)) 12 | implements IObserver { 13 | private _next: ISafeFunction> 14 | erred = false 15 | 16 | constructor(public sink: IObserver) { 17 | super() 18 | this._next = tryCatch(this.sink.next) 19 | } 20 | 21 | next(val: T): void { 22 | const r = this._next.call(this.sink, val) 23 | if (r.isError) this.error(r.getError()) 24 | } 25 | 26 | error(err: Error): void { 27 | this.erred = true 28 | this.sink.error(err) 29 | } 30 | } 31 | 32 | export const safeObserver = (observer: IObserver): ISafeObserver => 33 | observer instanceof SafeObserver ? observer : new SafeObserver(observer) 34 | -------------------------------------------------------------------------------- /src/internal/SubscriberFunction.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 05/10/16. 3 | */ 4 | 5 | import {IScheduler} from '../schedulers/Scheduler' 6 | import {IObserver} from './Observer' 7 | import {ISubscription} from './Subscription' 8 | 9 | export type SubscriberFunctionReturnType = ISubscription | void | (() => void) 10 | 11 | export interface ISubscriberFunction { 12 | (observer: IObserver, scheduler: IScheduler): SubscriberFunctionReturnType 13 | } 14 | -------------------------------------------------------------------------------- /src/internal/Subscription.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 27/09/16. 3 | */ 4 | import {LinkedList, LinkedListNode} from 'dbl-linked-list-ds' 5 | import {SubscriberFunctionReturnType} from './SubscriberFunction' 6 | import {ISubscription} from './Subscription' 7 | export interface ISubscription { 8 | unsubscribe(): void 9 | readonly closed: boolean 10 | } 11 | 12 | export class Subscription implements ISubscription { 13 | closed: boolean = false 14 | 15 | constructor(private dispose: SubscriberFunctionReturnType) {} 16 | 17 | unsubscribe(): void { 18 | if (this.dispose) { 19 | if (typeof this.dispose === 'function') this.dispose() 20 | else if ('unsubscribe' in this.dispose) this.dispose.unsubscribe() 21 | } 22 | this.closed = true 23 | } 24 | } 25 | 26 | export class CompositeSubscription implements ISubscription { 27 | public closed = false 28 | private subscriptions: LinkedList 29 | 30 | constructor() { 31 | this.subscriptions = new LinkedList() 32 | } 33 | 34 | head() { 35 | return this.subscriptions.head() 36 | } 37 | 38 | tail() { 39 | return this.subscriptions.tail() 40 | } 41 | 42 | add(d: ISubscription) { 43 | return this.subscriptions.add(d) 44 | } 45 | 46 | remove(d?: LinkedListNode) { 47 | if (d === undefined) return this.subscriptions.length 48 | d.value.unsubscribe() 49 | return this.subscriptions.remove(d) 50 | } 51 | 52 | length() { 53 | return this.subscriptions.length 54 | } 55 | unsubscribe(): void { 56 | if (this.closed) return 57 | this.closed = true 58 | this.subscriptions.forEach(this.remove, this) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/internal/TestObservable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 06/10/16. 3 | */ 4 | import {IScheduler} from '../schedulers/Scheduler' 5 | import {EVENT, IObservableEvent} from './Events' 6 | import {toMarble} from './Marble' 7 | import {IObservable} from './Observable' 8 | import {IObserver} from './Observer' 9 | import {ISubscription} from './Subscription' 10 | 11 | export class TestObservable implements IObservable { 12 | public readonly subscriptions: Array = [] 13 | 14 | constructor(private func: (observer: IObserver) => ISubscription) {} 15 | 16 | toString() { 17 | return toMarble(this.subscriptions) 18 | } 19 | 20 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 21 | const subscription = this.func(observer) 22 | const connections = this.subscriptions 23 | connections.push(EVENT.start(scheduler.now(), subscription)) 24 | return { 25 | unsubscribe() { 26 | if (subscription.closed) return 27 | subscription.unsubscribe() 28 | connections.push(EVENT.end(scheduler.now(), subscription)) 29 | }, 30 | get closed() { 31 | return subscription.closed 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/internal/TestObserver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 03/10/16. 3 | */ 4 | import {IScheduler} from '../schedulers/Scheduler' 5 | import {EVENT, IObservableEvent} from './Events' 6 | import {toMarble} from './Marble' 7 | import {IObserver} from './Observer' 8 | 9 | export class TestObserver implements IObserver { 10 | results: Array 11 | 12 | toString() { 13 | return toMarble(this.results) 14 | } 15 | 16 | constructor(private scheduler: IScheduler) { 17 | this.results = [] 18 | } 19 | 20 | next(val: T): void { 21 | this.results.push(EVENT.next(this.scheduler.now(), val)) 22 | } 23 | 24 | error(err: Error): void { 25 | this.results.push(EVENT.error(this.scheduler.now(), err)) 26 | } 27 | 28 | complete(): void { 29 | this.results.push(EVENT.complete(this.scheduler.now())) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/internal/TestOptions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 27/01/17. 3 | */ 4 | 5 | export const DEFAULT_OPTIONS = { 6 | subscriptionStart: 200, 7 | subscriptionStop: 2000, 8 | marbleSize: 1, 9 | rafTimeout: 16 10 | } 11 | -------------------------------------------------------------------------------- /src/internal/Thrower.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 06/11/16. 3 | */ 4 | 5 | import {IScheduler} from '../schedulers/Scheduler' 6 | import {CompleteMixin, ErrorMixin, Virgin} from './Mixins' 7 | import {IObservable} from './Observable' 8 | import {IObserver} from './Observer' 9 | import {ISubscription} from './Subscription' 10 | 11 | export const ERROR_MESSAGE = 'Test Exception' 12 | export function throwError(message: string) { 13 | throw Error(message) 14 | } 15 | 16 | class ThrowerObserver extends ErrorMixin(CompleteMixin(Virgin)) 17 | implements IObserver { 18 | constructor(public sink: IObserver) { 19 | super() 20 | } 21 | 22 | next(val: void): void { 23 | throwError(ERROR_MESSAGE) 24 | } 25 | } 26 | 27 | export class Thrower implements IObservable { 28 | constructor(private source: IObservable) {} 29 | 30 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 31 | return this.source.subscribe(new ThrowerObserver(observer), scheduler) 32 | } 33 | } 34 | 35 | export function thrower(ob: IObservable) { 36 | return new Thrower(ob) 37 | } 38 | -------------------------------------------------------------------------------- /src/internal/Utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 15/10/16. 3 | */ 4 | 5 | export function curry(f: Function, l: number = f.length): any { 6 | if (l <= 1) return f 7 | return function curried(...t: any[]) { 8 | if (t.length === 0) return curried 9 | if (t.length >= l) return f.apply(this, arguments) 10 | return curried.bind(this, ...t) 11 | } 12 | } 13 | 14 | export interface ISafeValue { 15 | isError: boolean 16 | getValue(): T 17 | getError(): Error 18 | } 19 | 20 | export interface ISafeFunction { 21 | call(ctx: C, ...t: any[]): ISafeValue 22 | } 23 | 24 | class Guarded implements ISafeValue { 25 | constructor(private value: Error | T, readonly isError: boolean) {} 26 | 27 | getValue() { 28 | return this.value as T 29 | } 30 | 31 | getError() { 32 | return this.value as Error 33 | } 34 | } 35 | 36 | class BaseSafeFunction 37 | implements ISafeFunction { 38 | constructor(private f: T) {} 39 | 40 | call(ctx: C, ...t: any[]): ISafeValue { 41 | try { 42 | return new Guarded(this.f.apply(ctx, t), false) 43 | } catch (e) { 44 | return new Guarded(e, true) 45 | } 46 | } 47 | } 48 | 49 | export function tryCatch(f: T) { 50 | return new BaseSafeFunction(f) as ISafeFunction 51 | } 52 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 17/02/17. 3 | */ 4 | 5 | export {combine} from './operators/Combine' 6 | export {CompositeSubscription} from './internal/Subscription' 7 | export {concatMap} from './operators/MergeMap' 8 | export {concat} from './operators/Concat' 9 | export {delay} from './operators/Delay' 10 | export {empty} from './sources/Create' 11 | export {filter} from './operators/Filter' 12 | export {flatMap} from './operators/MergeMap' 13 | export {forEach} from './sinks/ForEach' 14 | export {frames} from './sources/Frames' 15 | export {fromArray} from './sources/FromArray' 16 | export {fromDOM} from './sources/FromDOM' 17 | export {fromPromise} from './sources/FromPromise' 18 | export {interval} from './sources/Interval' 19 | export {IObservable} from './internal/Observable' 20 | export {IObserver} from './internal/Observer' 21 | export {IScheduler} from './schedulers/Scheduler' 22 | export {ISubscriberFunction} from './internal/SubscriberFunction' 23 | export {ISubscription} from './internal/Subscription' 24 | export {join} from './operators/MergeMap' 25 | export {just} from './sources/Create' 26 | export {mapTo} from './operators/Map' 27 | export {map} from './operators/Map' 28 | export {mergeMap} from './operators/MergeMap' 29 | export {merge} from './operators/Merge' 30 | export {multicast} from './operators/Multicast' 31 | export {never} from './sources/Create' 32 | export {Observable} from './internal/Observable' 33 | export {of} from './sources/FromArray' 34 | export {reduce} from './operators/Reduce' 35 | export {sample} from './operators/Sample' 36 | export {scan} from './operators/Scan' 37 | export {skipRepeats} from './operators/SkipRepeats' 38 | export {slice} from './operators/Slice' 39 | export {subject} from './sources/Subject' 40 | export {switchLatest} from './operators/Switch' 41 | export {switchMap} from './operators/Switch' 42 | export {tap} from './operators/Map' 43 | export {toPromise} from './sinks/ToPromise' 44 | export {uniqueWith} from './operators/Unique' 45 | export {unique} from './operators/Unique' 46 | -------------------------------------------------------------------------------- /src/operators/Combine.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 08/12/16. 3 | */ 4 | 5 | import {container} from '../internal/Container' 6 | import {IObservable} from '../internal/Observable' 7 | import {IObserver} from '../internal/Observer' 8 | import {CompositeSubscription, ISubscription} from '../internal/Subscription' 9 | import {curry} from '../internal/Utils' 10 | import {IScheduler} from '../schedulers/Scheduler' 11 | export type TSelector = {(...e: Array): T} 12 | export type TSource = Array> 13 | export type TResult = IObservable 14 | 15 | class CombineValueObserver implements IObserver { 16 | constructor(private id: number, private sink: CombinedObserver) {} 17 | 18 | next(val: T): void { 19 | this.sink.onNext(val, this.id) 20 | } 21 | 22 | error(err: Error): void { 23 | this.sink.onError(err) 24 | } 25 | 26 | complete(): void { 27 | this.sink.onComplete(this.id) 28 | } 29 | } 30 | 31 | class CombinedObserver { 32 | private collection = container(this.total) 33 | 34 | constructor( 35 | private func: TSelector, 36 | private total: number, 37 | private sink: IObserver 38 | ) {} 39 | 40 | onNext(value: T, id: number) { 41 | this.collection.next(value, id) 42 | if (this.collection.isOn()) { 43 | this.sink.next(this.func.apply(null, this.collection.values)) 44 | } 45 | } 46 | 47 | onComplete(id: number) { 48 | const hasCompleted = this.collection.complete(id) 49 | if (hasCompleted) { 50 | this.sink.complete() 51 | } 52 | } 53 | 54 | onError(err: Error): void { 55 | this.sink.error(err) 56 | } 57 | } 58 | 59 | class CombineObservable implements IObservable { 60 | constructor( 61 | private selector: TSelector, 62 | private sources: Array> 63 | ) {} 64 | 65 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 66 | const cSub = new CompositeSubscription() 67 | const ob = new CombinedObserver( 68 | this.selector, 69 | this.sources.length, 70 | observer 71 | ) 72 | for (var i = 0; i < this.sources.length; ++i) { 73 | cSub.add( 74 | this.sources[i].subscribe(new CombineValueObserver(i, ob), scheduler) 75 | ) 76 | } 77 | return cSub 78 | } 79 | } 80 | 81 | export const combine = curry( 82 | (selector: TSelector, sources: IObservable[]) => 83 | new CombineObservable(selector, sources) 84 | ) as { 85 | (selector: TSelector, sources: TSource): TResult 86 | (selector: TSelector): {(sources: TSource): TResult} 87 | } 88 | -------------------------------------------------------------------------------- /src/operators/Concat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 05/09/17. 3 | */ 4 | import {ErrorMixin, NextMixin, Virgin} from '../internal/Mixins' 5 | import {IObservable} from '../internal/Observable' 6 | import {IObserver} from '../internal/Observer' 7 | import {CompositeSubscription, ISubscription} from '../internal/Subscription' 8 | import {curry} from '../internal/Utils' 9 | import {IScheduler} from '../schedulers/Scheduler' 10 | 11 | class ConcatObserver extends ErrorMixin(NextMixin(Virgin)) 12 | implements IObserver { 13 | constructor( 14 | private src: IObservable, 15 | readonly sink: IObserver, 16 | private sh: IScheduler, 17 | private cSub: CompositeSubscription 18 | ) { 19 | super() 20 | } 21 | 22 | complete(): void { 23 | this.cSub.add(this.src.subscribe(this.sink, this.sh)) 24 | } 25 | } 26 | 27 | class ConcatObservable implements IObservable { 28 | constructor(private src0: IObservable, private src1: IObservable) {} 29 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 30 | const cSub = new CompositeSubscription() 31 | cSub.add( 32 | this.src0.subscribe( 33 | new ConcatObserver(this.src1, observer, scheduler, cSub), 34 | scheduler 35 | ) 36 | ) 37 | return cSub 38 | } 39 | } 40 | 41 | export type ConcatType = { 42 | (s0: IObservable, s1: IObservable): IObservable 43 | (s0: IObservable): {(s1: IObservable): IObservable} 44 | } 45 | 46 | export const concat = curry( 47 | (s0: IObservable, s1: IObservable) => new ConcatObservable(s0, s1) 48 | ) as ConcatType 49 | -------------------------------------------------------------------------------- /src/operators/Debounce.ts: -------------------------------------------------------------------------------- 1 | import {LinkedListNode} from 'dbl-linked-list-ds' 2 | import {ErrorMixin} from '../internal/Mixins' 3 | import {IObservable} from '../internal/Observable' 4 | import {IObserver} from '../internal/Observer' 5 | import {safeObserver} from '../internal/SafeObserver' 6 | import {CompositeSubscription, ISubscription} from '../internal/Subscription' 7 | import {curry} from '../internal/Utils' 8 | import {IScheduler} from '../schedulers/Scheduler' 9 | import {IOperator} from './Operator' 10 | 11 | class DebounceOperator extends ErrorMixin(CompositeSubscription) 12 | implements IOperator { 13 | private node: LinkedListNode 14 | 15 | constructor( 16 | private timeout: number, 17 | src: IObservable, 18 | public sink: IObserver, 19 | private sh: IScheduler 20 | ) { 21 | super() 22 | this.add(src.subscribe(this, sh)) 23 | } 24 | 25 | onEvent(value: T) { 26 | this.sink.next(value) 27 | } 28 | 29 | next(val: T): void { 30 | this.remove(this.node) 31 | this.node = this.add( 32 | this.sh.delay(this.onEvent.bind(this, val), this.timeout) 33 | ) 34 | } 35 | 36 | complete(): void { 37 | this.remove(this.node) 38 | this.sink.complete() 39 | } 40 | } 41 | class Debounce implements IObservable { 42 | constructor(private timeout: number, private source: IObservable) {} 43 | 44 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 45 | return new DebounceOperator( 46 | this.timeout, 47 | this.source, 48 | safeObserver(observer), 49 | scheduler 50 | ) 51 | } 52 | } 53 | 54 | export const debounce = curry( 55 | (timeout: number, source: IObservable) => new Debounce(timeout, source) 56 | ) as { 57 | (timeout: number, source: IObservable): IObservable 58 | (timeout: number): { 59 | (source: IObservable): IObservable 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/operators/Delay.ts: -------------------------------------------------------------------------------- 1 | import {ErrorMixin, Virgin} from '../internal/Mixins' 2 | /** 3 | * Created by tushar on 29/01/17. 4 | */ 5 | import {IObservable} from '../internal/Observable' 6 | import {IObserver} from '../internal/Observer' 7 | import {safeObserver} from '../internal/SafeObserver' 8 | import {CompositeSubscription, ISubscription} from '../internal/Subscription' 9 | import {curry} from '../internal/Utils' 10 | import {IScheduler} from '../schedulers/Scheduler' 11 | 12 | class DelayObserver extends ErrorMixin(Virgin) implements IObserver { 13 | constructor( 14 | private timeout: number, 15 | public sink: IObserver, 16 | private scheduler: IScheduler, 17 | private cSub: CompositeSubscription 18 | ) { 19 | super() 20 | } 21 | 22 | next(val: T): void { 23 | const node = this.cSub.add( 24 | this.scheduler.delay(() => { 25 | this.sink.next(val) 26 | this.cSub.remove(node) 27 | }, this.timeout) 28 | ) 29 | } 30 | 31 | complete(): void { 32 | this.cSub.add( 33 | this.scheduler.delay(this.sink.complete.bind(this.sink), this.timeout) 34 | ) 35 | } 36 | } 37 | 38 | class DelayObservable implements IObservable { 39 | constructor(private timeout: number, private source: IObservable) {} 40 | 41 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 42 | const cSub = new CompositeSubscription() 43 | const delayObserver = new DelayObserver( 44 | this.timeout, 45 | safeObserver(observer), 46 | scheduler, 47 | cSub 48 | ) 49 | cSub.add(this.source.subscribe(delayObserver, scheduler)) 50 | return cSub 51 | } 52 | } 53 | 54 | export const delay = curry( 55 | (timeout: number, source: IObservable): IObservable => 56 | new DelayObservable(timeout, source) 57 | ) as { 58 | (timeout: number, source: IObservable): IObservable 59 | (timeout: number): {(source: IObservable): IObservable} 60 | } 61 | -------------------------------------------------------------------------------- /src/operators/Filter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 27/09/16. 3 | */ 4 | 5 | import {CompleteMixin, ErrorMixin, Virgin} from '../internal/Mixins' 6 | import {IObservable} from '../internal/Observable' 7 | import {IObserver} from '../internal/Observer' 8 | import {ISubscription} from '../internal/Subscription' 9 | import {curry} from '../internal/Utils' 10 | import {IScheduler} from '../schedulers/Scheduler' 11 | 12 | export type TPredicate = {(value: T): boolean} 13 | export type TSource = IObservable 14 | export type TResult = IObservable 15 | 16 | class FilterObserver extends ErrorMixin(CompleteMixin(Virgin)) 17 | implements IObserver { 18 | constructor(private predicate: {(t: T): boolean}, public sink: IObserver) { 19 | super() 20 | } 21 | 22 | next(val: T) { 23 | if (this.predicate(val)) this.sink.next(val) 24 | } 25 | } 26 | 27 | class FilterObservable implements TResult { 28 | constructor( 29 | private predicate: {(t: T): boolean}, 30 | private source: IObservable 31 | ) {} 32 | 33 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 34 | return this.source.subscribe( 35 | new FilterObserver(this.predicate, observer), 36 | scheduler 37 | ) 38 | } 39 | } 40 | 41 | export const filter = curry(function( 42 | predicate: TPredicate, 43 | source: TSource 44 | ) { 45 | return new FilterObservable(predicate, source) 46 | }) as { 47 | (predicate: TPredicate, source: TSource): TResult 48 | (predicate: TPredicate): {(source: TSource): TResult} 49 | } 50 | -------------------------------------------------------------------------------- /src/operators/Map.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 27/09/16. 3 | */ 4 | 5 | import {CompleteMixin, ErrorMixin, Virgin} from '../internal/Mixins' 6 | import {IObservable} from '../internal/Observable' 7 | import {IObserver} from '../internal/Observer' 8 | import {ISubscription} from '../internal/Subscription' 9 | import {curry} from '../internal/Utils' 10 | import {IScheduler} from '../schedulers/Scheduler' 11 | 12 | export type TMapper = (value: T) => R 13 | export type TSource = IObservable 14 | export type TResult = IObservable 15 | 16 | class MapObserver extends ErrorMixin(CompleteMixin(Virgin)) 17 | implements IObserver { 18 | constructor(private mapper: TMapper, public sink: IObserver) { 19 | super() 20 | } 21 | 22 | next(val: T): void { 23 | this.sink.next(this.mapper(val)) 24 | } 25 | } 26 | 27 | class MapObservable implements TResult { 28 | constructor(private mapper: TMapper, private source: TSource) {} 29 | 30 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 31 | return this.source.subscribe( 32 | new MapObserver(this.mapper, observer), 33 | scheduler 34 | ) 35 | } 36 | } 37 | 38 | export const map = curry(function( 39 | mapFunction: (a: T) => R, 40 | source: IObservable 41 | ) { 42 | return new MapObservable(mapFunction, source) 43 | }) as { 44 | (mapper: TMapper, source: TSource): TResult 45 | (mapper: TMapper): {(source: TSource): TResult} 46 | } 47 | 48 | export const tap = curry(function( 49 | mapFunction: (a: T) => void, 50 | source: IObservable 51 | ) { 52 | return new MapObservable((a: T) => { 53 | mapFunction(a) 54 | return a 55 | }, source) 56 | }) as { 57 | (mapper: TMapper, source: TSource): TResult 58 | (mapper: TMapper): {(source: TSource): TResult} 59 | } 60 | 61 | export const mapTo = curry(function( 62 | mapFunction: T, 63 | source: IObservable 64 | ) { 65 | return new MapObservable(() => mapFunction, source) 66 | }) as { 67 | (value: T, source: IObservable): IObservable 68 | (value: T): { 69 | (source: IObservable): IObservable 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/operators/Merge.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 17/10/16. 3 | */ 4 | import {ErrorMixin, NextMixin, Virgin} from '../internal/Mixins' 5 | import {IObservable} from '../internal/Observable' 6 | import {IObserver} from '../internal/Observer' 7 | import {CompositeSubscription, ISubscription} from '../internal/Subscription' 8 | import {IScheduler} from '../schedulers/Scheduler' 9 | 10 | class MergeObserver extends ErrorMixin(NextMixin(Virgin)) 11 | implements IObserver { 12 | private count = 0 13 | 14 | constructor(private total: number, public sink: IObserver) { 15 | super() 16 | } 17 | 18 | complete(): void { 19 | this.count++ 20 | if (this.count === this.total) { 21 | this.sink.complete() 22 | } 23 | } 24 | } 25 | 26 | class MergeObservable implements IObservable { 27 | constructor(private sources: Array>) {} 28 | 29 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 30 | const cSub = new CompositeSubscription() 31 | const mergeObserver = new MergeObserver(this.sources.length, observer) 32 | for (var i = 0; i < this.sources.length; ++i) { 33 | cSub.add(this.sources[i].subscribe(mergeObserver, scheduler)) 34 | } 35 | return cSub 36 | } 37 | } 38 | 39 | export function merge(...sources: Array>): IObservable { 40 | return new MergeObservable(sources) 41 | } 42 | -------------------------------------------------------------------------------- /src/operators/MergeMap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 31/08/17. 3 | */ 4 | import {LinkedListNode} from 'dbl-linked-list-ds' 5 | import {ErrorMixin, Virgin} from '../internal/Mixins' 6 | import {IObservable} from '../internal/Observable' 7 | import {IObserver} from '../internal/Observer' 8 | import {CompositeSubscription, ISubscription} from '../internal/Subscription' 9 | import {curry} from '../internal/Utils' 10 | import {IScheduler} from '../schedulers/Scheduler' 11 | import {just} from '../sources/Create' 12 | 13 | export type Project = (t: T) => IObservable 14 | 15 | export type mergeMapFunctionWithConcurrency = { 16 | ( 17 | concurrency: IObservable, 18 | project: Project, 19 | source: IObservable 20 | ): IObservable 21 | (concurrency: IObservable): { 22 | (project: Project, source: IObservable): IObservable 23 | } 24 | (concurrency: IObservable): { 25 | (project: Project): { 26 | (source: IObservable): IObservable 27 | } 28 | } 29 | } 30 | export type mergeMapFunction = { 31 | (project: Project, source: IObservable): IObservable 32 | (project: Project, source: IObservable): IObservable 33 | (project: Project): { 34 | (source: IObservable): IObservable 35 | } 36 | } 37 | export type joinFunction = ( 38 | source: IObservable> 39 | ) => IObservable 40 | 41 | class ConcurrencyObserver extends ErrorMixin(Virgin) 42 | implements IObserver { 43 | constructor( 44 | private outer: MergeMapOuterObserver, 45 | readonly sink: IObserver 46 | ) { 47 | super() 48 | } 49 | 50 | next(val: number): void { 51 | this.outer.setConc(val) 52 | } 53 | 54 | complete(): void {} 55 | } 56 | 57 | class MergeMapInnerObserver implements IObserver { 58 | node: LinkedListNode 59 | 60 | constructor(private parent: MergeMapOuterObserver) {} 61 | 62 | next(val: S): void { 63 | this.parent.sink.next(val) 64 | } 65 | 66 | error(err: Error): void { 67 | this.parent.sink.error(err) 68 | } 69 | 70 | setup(node: LinkedListNode) { 71 | this.node = node 72 | } 73 | 74 | complete(): void { 75 | this.parent.cSub.remove(this.node) 76 | this.parent.checkComplete() 77 | } 78 | } 79 | 80 | class MergeMapOuterObserver extends ErrorMixin(Virgin) 81 | implements IObserver { 82 | private __completed: boolean = false 83 | private __buffer: T[] = [] 84 | private __conc = 0 85 | readonly cSub = new CompositeSubscription() 86 | 87 | constructor( 88 | conc: IObservable, 89 | readonly proj: Project, 90 | readonly sink: IObserver, 91 | readonly sh: IScheduler 92 | ) { 93 | super() 94 | conc.subscribe(new ConcurrencyObserver(this, sink), sh) 95 | } 96 | 97 | private getSpace() { 98 | return this.__conc - this.cSub.length() 99 | } 100 | 101 | private subscribeNew() { 102 | while (this.__buffer.length > 0 && this.getSpace() > 0) { 103 | this.next(this.__buffer.shift() as T) 104 | } 105 | } 106 | 107 | private unsubscribeOld() { 108 | while (this.getSpace() < 0) { 109 | this.cSub.remove(this.cSub.head()) 110 | } 111 | } 112 | 113 | next(val: T): void { 114 | if (this.getSpace() > 0) { 115 | const innerObserver = new MergeMapInnerObserver(this) 116 | const node = this.cSub.add( 117 | this.proj(val).subscribe(innerObserver, this.sh) 118 | ) 119 | innerObserver.setup(node) 120 | } else this.__buffer.push(val) 121 | } 122 | 123 | complete(): void { 124 | this.__completed = true 125 | this.checkComplete() 126 | } 127 | 128 | checkComplete() { 129 | if ( 130 | this.__completed && 131 | this.cSub.length() === 0 && 132 | this.__buffer.length === 0 133 | ) 134 | this.sink.complete() 135 | else this.subscribeNew() 136 | } 137 | 138 | setConc(value: number) { 139 | this.__conc = value 140 | const space = this.getSpace() 141 | if (space > 0) this.subscribeNew() 142 | else if (space < 0) this.unsubscribeOld() 143 | } 144 | } 145 | 146 | class MergeMap implements IObservable { 147 | constructor( 148 | private conc: IObservable, 149 | private proj: Project, 150 | private src: IObservable 151 | ) {} 152 | 153 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 154 | const cSub = new CompositeSubscription() 155 | 156 | // prettier-ignore 157 | const outerObserver = new MergeMapOuterObserver(this.conc, this.proj, observer, scheduler) 158 | cSub.add(outerObserver.cSub) 159 | cSub.add(this.src.subscribe(outerObserver, scheduler)) 160 | return cSub 161 | } 162 | } 163 | 164 | export const mergeMap: mergeMapFunctionWithConcurrency = curry( 165 | ( 166 | concurrency: IObservable, 167 | project: Project, 168 | source: IObservable 169 | ): IObservable => new MergeMap(concurrency, project, source) 170 | ) 171 | export const flatMap = mergeMap( 172 | just(Number.POSITIVE_INFINITY) 173 | ) as mergeMapFunction 174 | export const concatMap: mergeMapFunction = mergeMap(just(1)) as mergeMapFunction 175 | export const join = flatMap(i => i) as joinFunction 176 | -------------------------------------------------------------------------------- /src/operators/Multicast.ts: -------------------------------------------------------------------------------- 1 | import {LinkedListNode} from 'dbl-linked-list-ds' 2 | import {IObservable} from '../internal/Observable' 3 | import {CompositeObserver, IObserver} from '../internal/Observer' 4 | import {ISubscription} from '../internal/Subscription' 5 | import {IScheduler} from '../schedulers/Scheduler' 6 | 7 | class MulticastSubscription implements ISubscription { 8 | closed = false 9 | private node = this.sharedObserver.addObserver(this.observer, this.scheduler) 10 | 11 | constructor( 12 | private observer: IObserver, 13 | private scheduler: IScheduler, 14 | private sharedObserver: MulticastObserver 15 | ) {} 16 | 17 | unsubscribe(): void { 18 | this.closed = true 19 | this.sharedObserver.removeObserver(this.node) 20 | } 21 | } 22 | 23 | class MulticastObserver extends CompositeObserver { 24 | private subscription: ISubscription 25 | 26 | constructor(private source: IObservable) { 27 | super() 28 | } 29 | 30 | addObserver(observer: IObserver, scheduler: IScheduler) { 31 | const node = this.add(observer) 32 | if (this.length === 1) { 33 | this.subscription = this.source.subscribe(this, scheduler) 34 | } 35 | return node 36 | } 37 | 38 | removeObserver(node: LinkedListNode>) { 39 | this.remove(node) 40 | if (this.length === 0) { 41 | this.subscription.unsubscribe() 42 | } 43 | } 44 | } 45 | 46 | class Multicast implements IObservable { 47 | private sharedObserver = new MulticastObserver(this.source) 48 | 49 | constructor(private source: IObservable) {} 50 | 51 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 52 | return new MulticastSubscription(observer, scheduler, this.sharedObserver) 53 | } 54 | } 55 | 56 | export function multicast(source: IObservable): IObservable { 57 | return new Multicast(source) 58 | } 59 | -------------------------------------------------------------------------------- /src/operators/Operator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 16/02/17. 3 | */ 4 | import {IObserver} from '../internal/Observer' 5 | import {ISubscription} from '../internal/Subscription' 6 | 7 | export interface IOperator extends ISubscription, IObserver {} 8 | -------------------------------------------------------------------------------- /src/operators/Reduce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 27/09/16. 3 | */ 4 | 5 | import {ErrorMixin, Virgin} from '../internal/Mixins' 6 | import {IObservable} from '../internal/Observable' 7 | import {IObserver} from '../internal/Observer' 8 | import {ISubscription} from '../internal/Subscription' 9 | import {curry} from '../internal/Utils' 10 | import {IScheduler} from '../schedulers/Scheduler' 11 | 12 | export type TReducer = {(memory: R, current: T): R} 13 | export type TSeed = R 14 | export type TSource = IObservable 15 | export type TResult = IObservable 16 | 17 | class ReduceObserver extends ErrorMixin(Virgin) implements IObserver { 18 | constructor( 19 | private reducer: TReducer, 20 | private value: TSeed, 21 | public sink: IObserver 22 | ) { 23 | super() 24 | } 25 | 26 | next(val: T): void { 27 | this.value = this.reducer(this.value, val) 28 | } 29 | 30 | complete(): void { 31 | this.sink.next(this.value) 32 | this.sink.complete() 33 | } 34 | } 35 | 36 | class ReduceObservable implements TResult { 37 | constructor( 38 | private reducer: TReducer, 39 | private seed: TSeed, 40 | private source: TSource 41 | ) {} 42 | 43 | subscribe(obr: IObserver, scheduler: IScheduler): ISubscription { 44 | return this.source.subscribe( 45 | new ReduceObserver(this.reducer, this.seed, obr), 46 | scheduler 47 | ) 48 | } 49 | } 50 | 51 | export const reduce = curry(function( 52 | t0: TReducer, 53 | t1: R, 54 | t2: IObservable 55 | ) { 56 | return new ReduceObservable(t0, t1, t2) 57 | }) as { 58 | (reducer: TReducer, seed: TSeed, source: TSource): TResult< 59 | R 60 | > 61 | 62 | (reducer: TReducer): { 63 | (seed: TSeed, source: TSource): TResult 64 | } 65 | 66 | (reducer: TReducer, seed: TSeed): { 67 | (source: TSource): TResult 68 | } 69 | 70 | (reducer: TReducer): { 71 | (seed: TSeed): {(source: TSource): TResult} 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/operators/Sample.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 18/10/16. 3 | */ 4 | import {container} from '../internal/Container' 5 | import {ErrorMixin, Virgin} from '../internal/Mixins' 6 | import {IObservable} from '../internal/Observable' 7 | import {IObserver} from '../internal/Observer' 8 | import {CompositeSubscription, ISubscription} from '../internal/Subscription' 9 | import {curry} from '../internal/Utils' 10 | import {IScheduler} from '../schedulers/Scheduler' 11 | 12 | export type TSelector = {(...e: Array): T} 13 | export type TSampler = IObservable 14 | export type TSources = Array> 15 | export type TResult = IObservable 16 | 17 | class SampleValueObserver implements IObserver { 18 | constructor(private id: number, private sampleObserver: SampleObserver) {} 19 | 20 | next(val: T): void { 21 | this.sampleObserver.onNext(val, this.id) 22 | } 23 | 24 | error(err: Error): void { 25 | this.sampleObserver.error(err) 26 | } 27 | 28 | complete(): void { 29 | this.sampleObserver.onComplete(this.id) 30 | } 31 | } 32 | class SampleObserver extends ErrorMixin(Virgin) implements IObserver { 33 | private container = container(this.total) 34 | private completed = false 35 | 36 | constructor( 37 | private total: number, 38 | public sink: IObserver, 39 | private func: TSelector 40 | ) { 41 | super() 42 | } 43 | 44 | onNext(value: T, id: number) { 45 | this.container.next(value, id) 46 | } 47 | 48 | onComplete(id: number) { 49 | const hasCompleted = this.container.complete(id) 50 | if (this.completed && hasCompleted) { 51 | this.sink.complete() 52 | } 53 | } 54 | 55 | private actuallyCompleted() { 56 | if (this.completed && this.container.isDone()) { 57 | this.sink.complete() 58 | } 59 | } 60 | 61 | next(val: T): void { 62 | if (this.container.isOn()) { 63 | this.sink.next(this.func.apply(null, this.container.values)) 64 | } 65 | } 66 | 67 | complete(): void { 68 | this.completed = true 69 | this.actuallyCompleted() 70 | } 71 | } 72 | 73 | class SampleObservable implements TResult { 74 | constructor( 75 | private func: TSelector, 76 | private sampler: TSampler, 77 | private sources: TSources 78 | ) {} 79 | 80 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 81 | const cSub = new CompositeSubscription() 82 | const sampleObserver = new SampleObserver( 83 | this.sources.length, 84 | observer, 85 | this.func 86 | ) 87 | for (var i = 0; i < this.sources.length; ++i) { 88 | const sampleValueObserver = new SampleValueObserver(i, sampleObserver) 89 | cSub.add(this.sources[i].subscribe(sampleValueObserver, scheduler)) 90 | } 91 | cSub.add(this.sampler.subscribe(sampleObserver, scheduler)) 92 | return cSub 93 | } 94 | } 95 | 96 | export const sample = curry(function( 97 | f: TSelector, 98 | sampler: TSampler, 99 | sources: TSources 100 | ) { 101 | return new SampleObservable(f, sampler, sources) 102 | }) as { 103 | (selector: TSelector, sampler: TSampler, source: TSources): TResult 104 | 105 | (selector: TSelector): { 106 | (sampler: TSampler, source: TSources): TResult 107 | } 108 | 109 | (selector: TSelector, sampler: TSampler): { 110 | (source: TSources): TResult 111 | } 112 | 113 | (selector: TSelector): { 114 | (sampler: TSampler): {(source: TSources): TResult} 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/operators/Scan.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 09/10/16. 3 | */ 4 | import {CompleteMixin, ErrorMixin, Virgin} from '../internal/Mixins' 5 | import {IObservable} from '../internal/Observable' 6 | import {IObserver} from '../internal/Observer' 7 | import {ISubscription} from '../internal/Subscription' 8 | import {curry} from '../internal/Utils' 9 | import {IScheduler} from '../schedulers/Scheduler' 10 | 11 | export type TReducer = (memory: R, current: T) => R 12 | export type TSeed = R 13 | export type TSource = IObservable 14 | export type TResult = IObservable 15 | 16 | class ScanObserver extends ErrorMixin(CompleteMixin(Virgin)) 17 | implements IObserver { 18 | constructor( 19 | private reducer: TReducer, 20 | private value: V, 21 | public sink: IObserver 22 | ) { 23 | super() 24 | } 25 | 26 | next(val: T): void { 27 | this.value = this.reducer(this.value, val) 28 | this.sink.next(this.value) 29 | } 30 | } 31 | 32 | class ScanObservable implements TResult { 33 | constructor( 34 | private reducer: TReducer, 35 | private seed: TSeed, 36 | private source: TSource 37 | ) {} 38 | 39 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 40 | return this.source.subscribe( 41 | new ScanObserver(this.reducer, this.seed, observer), 42 | scheduler 43 | ) 44 | } 45 | } 46 | 47 | export const scan = curry(function( 48 | reducer: TReducer, 49 | value: V, 50 | source: IObservable 51 | ) { 52 | return new ScanObservable(reducer, value, source) 53 | }) as { 54 | (reducer: TReducer, seed: TSeed, source: TSource): TResult< 55 | R 56 | > 57 | 58 | (reducer: TReducer): { 59 | (seed: TSeed, source: TSource): TResult 60 | } 61 | 62 | (reducer: TReducer, seed: TSeed): { 63 | (source: TSource): TResult 64 | } 65 | 66 | (reducer: TReducer): { 67 | (seed: TSeed): {(source: TSource): TResult} 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/operators/SkipRepeats.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by niranjan on 12/10/16. 3 | */ 4 | import {CompleteMixin, ErrorMixin, Virgin} from '../internal/Mixins' 5 | import {IObservable} from '../internal/Observable' 6 | import {IObserver} from '../internal/Observer' 7 | import {ISubscription} from '../internal/Subscription' 8 | import {curry} from '../internal/Utils' 9 | import {IScheduler} from '../schedulers/Scheduler' 10 | 11 | export type TComparator = (a: T, b: T) => boolean 12 | export type TSource = IObservable 13 | export type TResult = IObservable 14 | 15 | class SkipRepeatsObserver extends ErrorMixin(CompleteMixin(Virgin)) 16 | implements IObserver { 17 | private previous: T | void = undefined 18 | private init = true 19 | 20 | constructor(private cmp: {(a: T, b: T): boolean}, public sink: IObserver) { 21 | super() 22 | } 23 | 24 | next(val: T) { 25 | if (this.init) { 26 | this.init = false 27 | this.sink.next(val) 28 | this.previous = val 29 | } else if (this.cmp(this.previous as T, val) === false) { 30 | this.sink.next(val) 31 | this.previous = val 32 | } 33 | } 34 | } 35 | 36 | class SkipRepeatsObservable implements TResult { 37 | constructor(private cmp: TComparator, private source: TSource) {} 38 | 39 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 40 | return this.source.subscribe( 41 | new SkipRepeatsObserver(this.cmp, observer), 42 | scheduler 43 | ) 44 | } 45 | } 46 | 47 | export const skipRepeats = curry(function( 48 | hashFunction: TComparator, 49 | source: IObservable 50 | ) { 51 | return new SkipRepeatsObservable(hashFunction, source) 52 | }) as { 53 | (cmp: TComparator, source: TSource): TResult 54 | (cmp: TComparator): {(source: TSource): TResult} 55 | } 56 | -------------------------------------------------------------------------------- /src/operators/Slice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 27/09/16. 3 | */ 4 | 5 | import {IObservable} from '../internal/Observable' 6 | import {IObserver} from '../internal/Observer' 7 | import {ISubscription} from '../internal/Subscription' 8 | import {curry} from '../internal/Utils' 9 | import {IScheduler} from '../schedulers/Scheduler' 10 | 11 | class SliceObserver implements IObserver { 12 | closed: boolean 13 | private index: number 14 | private subscription: ISubscription 15 | 16 | constructor( 17 | private from: number, 18 | private total: number, 19 | private sink: IObserver 20 | ) { 21 | this.closed = false 22 | this.index = 0 23 | } 24 | 25 | start(subscription: ISubscription) { 26 | this.subscription = subscription 27 | } 28 | 29 | next(value: T): void { 30 | const diff = this.index++ - this.from 31 | if (diff >= 0 && diff < this.total) { 32 | this.sink.next(value) 33 | } 34 | if (diff + 1 === this.total) this.end() 35 | } 36 | 37 | private end() { 38 | this.subscription.unsubscribe() 39 | this.complete() 40 | } 41 | 42 | complete(): void { 43 | if (this.closed) return 44 | this.sink.complete() 45 | this.closed = true 46 | } 47 | 48 | error(error: Error): void { 49 | if (this.closed) return 50 | this.sink.error(error) 51 | } 52 | } 53 | 54 | class SliceObservable implements IObservable { 55 | constructor( 56 | private start: number, 57 | private total: number, 58 | private source: IObservable 59 | ) {} 60 | 61 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 62 | const sliceObserver = new SliceObserver(this.start, this.total, observer) 63 | const subscription = this.source.subscribe(sliceObserver, scheduler) 64 | sliceObserver.start(subscription) 65 | return subscription 66 | } 67 | } 68 | 69 | export const slice = curry(function( 70 | start: number, 71 | count: number, 72 | source: IObservable 73 | ) { 74 | return new SliceObservable(start, count, source) 75 | }) as { 76 | (start: number, count: number, source: IObservable): IObservable 77 | 78 | (start: number): {(count: number, source: IObservable): IObservable} 79 | 80 | (start: number, count: number): { 81 | (source: IObservable): IObservable 82 | } 83 | 84 | (start: number): { 85 | (count: number): {(source: IObservable): IObservable} 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/operators/Switch.ts: -------------------------------------------------------------------------------- 1 | import {LinkedListNode} from 'dbl-linked-list-ds' 2 | import {ErrorMixin, NextMixin, Virgin} from '../internal/Mixins' 3 | import {IObservable} from '../internal/Observable' 4 | import {IObserver} from '../internal/Observer' 5 | import {CompositeSubscription, ISubscription} from '../internal/Subscription' 6 | import {curry} from '../internal/Utils' 7 | import {IScheduler} from '../schedulers/Scheduler' 8 | import {map} from './Map' 9 | import {IOperator} from './Operator' 10 | 11 | class SwitchValueObserver extends ErrorMixin(NextMixin(Virgin)) 12 | implements IObserver { 13 | constructor(public sink: IObserver) { 14 | super() 15 | } 16 | 17 | complete(): void {} 18 | } 19 | 20 | class SwitchOperator extends ErrorMixin(CompositeSubscription) 21 | implements IOperator> { 22 | public sink = new SwitchValueObserver(this.mainSink) 23 | private srcSub: LinkedListNode 24 | 25 | constructor( 26 | private source: IObservable>, 27 | private mainSink: IObserver, 28 | private scheduler: IScheduler 29 | ) { 30 | super() 31 | this.add(this.source.subscribe(this, scheduler)) 32 | } 33 | 34 | next(val: IObservable): void { 35 | this.remove(this.srcSub) 36 | this.srcSub = this.add(val.subscribe(this.sink, this.scheduler)) 37 | } 38 | 39 | complete(): void { 40 | this.remove(this.srcSub) 41 | this.mainSink.complete() 42 | } 43 | } 44 | 45 | class SwitchLatest implements IObservable { 46 | constructor(private source: IObservable>) {} 47 | 48 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 49 | return new SwitchOperator(this.source, observer, scheduler) 50 | } 51 | } 52 | 53 | export function switchLatest( 54 | source: IObservable> 55 | ): IObservable { 56 | return new SwitchLatest(source) 57 | } 58 | export const switchMap = curry( 59 | (fn: (t: K) => IObservable, source: IObservable) => { 60 | return switchLatest(map(fn, source)) 61 | } 62 | ) as { 63 | (mapper: (t: K) => IObservable, source: IObservable): IObservable< 64 | T 65 | > 66 | 67 | (mapper: (t: K) => IObservable): { 68 | (source: IObservable): IObservable 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/operators/Unique.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 14/09/17. 3 | */ 4 | 5 | import {CompleteMixin, ErrorMixin, Virgin} from '../internal/Mixins' 6 | import {IObservable} from '../internal/Observable' 7 | import {IObserver} from '../internal/Observer' 8 | import {ISubscription} from '../internal/Subscription' 9 | import {curry} from '../internal/Utils' 10 | import {IScheduler} from '../schedulers/Scheduler' 11 | 12 | class UniqueObserver extends ErrorMixin(CompleteMixin(Virgin)) 13 | implements IObserver { 14 | constructor(private set: Set, readonly sink: IObserver) { 15 | super() 16 | } 17 | next(val: T): void { 18 | if (this.set.has(val) === false) { 19 | this.set.add(val) 20 | this.sink.next(val) 21 | } 22 | } 23 | } 24 | 25 | class UniqueObservable implements IObservable { 26 | constructor(private set: Set, private src: IObservable) {} 27 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 28 | return this.src.subscribe(new UniqueObserver(this.set, observer), scheduler) 29 | } 30 | } 31 | 32 | export type UniqueWithType = { 33 | (set: Set, source: IObservable): IObservable 34 | (set: Set): {(source: IObservable): IObservable} 35 | } 36 | 37 | export const uniqueWith = curry( 38 | (set: Set, source: IObservable) => new UniqueObservable(set, source) 39 | ) as UniqueWithType 40 | 41 | export const unique = (source: IObservable): IObservable => 42 | new UniqueObservable(new Set(), source) 43 | -------------------------------------------------------------------------------- /src/schedulers/Scheduler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 03/10/16. 3 | */ 4 | import {ISubscription} from '../internal/Subscription' 5 | 6 | export interface IScheduler { 7 | delay(task: () => void, relativeTime: number): ISubscription 8 | 9 | periodic(task: () => void, interval: number): ISubscription 10 | 11 | frame(task: () => void): ISubscription 12 | 13 | frames(task: () => void): ISubscription 14 | 15 | asap(task: () => void): ISubscription 16 | 17 | now(): number 18 | } 19 | 20 | function getGlobal(): any { 21 | return typeof window === 'object' ? window : global 22 | } 23 | 24 | class Periodic implements ISubscription { 25 | closed = false 26 | private id: number 27 | 28 | constructor(private task: () => void, private interval: number) { 29 | this.id = (setInterval(this.onEvent, this.interval) as any) as number 30 | } 31 | 32 | private onEvent = () => { 33 | if (!this.closed) this.task() 34 | } 35 | 36 | unsubscribe(): void { 37 | if (this.closed) return 38 | this.closed = true 39 | clearInterval(this.id) 40 | } 41 | } 42 | 43 | class Delay implements ISubscription { 44 | closed = false 45 | 46 | constructor(private task: () => void, private timeout: number) { 47 | setTimeout(this.onEvent.bind(this), this.timeout) 48 | } 49 | 50 | private onEvent() { 51 | if (this.closed) return 52 | this.task() 53 | this.closed = true 54 | } 55 | 56 | unsubscribe(): void { 57 | if (this.closed === false) { 58 | this.closed = true 59 | } 60 | } 61 | } 62 | 63 | class ASAP implements ISubscription { 64 | closed = false 65 | 66 | constructor(private task: () => void) { 67 | const global = getGlobal() 68 | if (global.process) global.process.nextTick(this.onEvent) 69 | else Promise.resolve().then(this.onEvent) 70 | } 71 | 72 | private onEvent = () => { 73 | if (!this.closed) this.task() 74 | } 75 | 76 | unsubscribe(): void { 77 | if (this.closed === false) this.closed = true 78 | } 79 | } 80 | 81 | class Frame implements ISubscription { 82 | closed = false 83 | private frame: number 84 | 85 | constructor(private task: () => void) { 86 | this.frame = requestAnimationFrame(this.onEvent) 87 | } 88 | 89 | private onEvent = () => { 90 | if (this.closed) return 91 | this.task() 92 | this.frame = requestAnimationFrame(this.onEvent) 93 | } 94 | 95 | unsubscribe(): void { 96 | if (this.closed) return 97 | this.closed = true 98 | cancelAnimationFrame(this.frame) 99 | } 100 | } 101 | 102 | class Frames implements ISubscription { 103 | closed = false 104 | private frame: number 105 | 106 | constructor(private task: () => void) { 107 | this.onEvent = this.onEvent.bind(this) 108 | this.frame = requestAnimationFrame(this.onEvent) 109 | } 110 | 111 | onEvent() { 112 | this.task() 113 | this.frame = requestAnimationFrame(this.onEvent) 114 | } 115 | 116 | unsubscribe(): void { 117 | if (this.closed) return 118 | this.closed = true 119 | cancelAnimationFrame(this.frame) 120 | } 121 | } 122 | 123 | class Scheduler implements IScheduler { 124 | frame(task: () => void): ISubscription { 125 | return new Frame(task) 126 | } 127 | 128 | frames(task: () => void): ISubscription { 129 | return new Frames(task) 130 | } 131 | 132 | asap(task: () => void): ISubscription { 133 | return new ASAP(task) 134 | } 135 | 136 | periodic(task: () => void, interval: number): ISubscription { 137 | return new Periodic(task, interval) 138 | } 139 | 140 | delay(task: () => void, relativeTime: number): ISubscription { 141 | return new Delay(task, relativeTime) 142 | } 143 | 144 | now(): number { 145 | return Date.now() 146 | } 147 | } 148 | 149 | export const createScheduler = (): IScheduler => new Scheduler() 150 | -------------------------------------------------------------------------------- /src/schedulers/TestScheduler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 02/10/16. 3 | */ 4 | import {LinkedList, LinkedListNode} from 'dbl-linked-list-ds' 5 | import {ColdTestObservable} from '../internal/ColdTestObservable' 6 | import {IObservableEvent} from '../internal/Events' 7 | import {HotTestObservable} from '../internal/HotTestObservable' 8 | import {fromMarble} from '../internal/Marble' 9 | import {IObservable} from '../internal/Observable' 10 | import {ISubscription} from '../internal/Subscription' 11 | import {TestObservable} from '../internal/TestObservable' 12 | import {TestObserver} from '../internal/TestObserver' 13 | import {DEFAULT_OPTIONS} from '../internal/TestOptions' 14 | import {IScheduler} from './Scheduler' 15 | 16 | // TODO: convert to interface 17 | class TaskSchedule { 18 | constructor(public task: () => void, public time: number) {} 19 | } 20 | 21 | class TaskSubscription implements ISubscription { 22 | closed: boolean 23 | 24 | constructor( 25 | private queue: LinkedList, 26 | private taskNode: LinkedListNode 27 | ) {} 28 | 29 | unsubscribe(): void { 30 | this.queue.remove(this.taskNode) 31 | } 32 | } 33 | 34 | export type EventSource = Array 35 | 36 | function normalizeEvents(...ev: EventSource): EventSource 37 | function normalizeEvents(ev: EventSource): EventSource 38 | function normalizeEvents(events: string): EventSource 39 | function normalizeEvents(events: any) { 40 | if (typeof events === 'string') return fromMarble(events) 41 | if (arguments.length > 1) return Array.from(arguments) 42 | if (arguments.length === 1) return Array.isArray(events) ? events : [events] 43 | return events 44 | } 45 | 46 | export class TestScheduler implements IScheduler { 47 | asap(task: () => void): ISubscription { 48 | return this.delay(task, 1) 49 | } 50 | 51 | private clock = 0 52 | private queue = new LinkedList() 53 | 54 | constructor(private rafTimeout: number) {} 55 | 56 | get length() { 57 | return this.queue.length 58 | } 59 | 60 | tick() { 61 | this.clock++ 62 | this.run() 63 | } 64 | 65 | advanceBy(time: number): void { 66 | while (time-- > 0) this.tick() 67 | } 68 | 69 | advanceTo(time: number): void { 70 | this.advanceBy(time - this.now()) 71 | } 72 | 73 | now() { 74 | return this.clock 75 | } 76 | 77 | delay( 78 | task: () => void, 79 | time: number, 80 | now: number = this.now() 81 | ): ISubscription { 82 | return new TaskSubscription( 83 | this.queue, 84 | this.queue.add(new TaskSchedule(task, time + now)) 85 | ) 86 | } 87 | 88 | frame(task: () => void): ISubscription { 89 | return this.delay(task, this.now() + this.rafTimeout, 0) 90 | } 91 | 92 | frames(task: () => void): ISubscription { 93 | return this.periodic(task, this.rafTimeout) 94 | } 95 | 96 | periodic(task: () => void, interval: number): ISubscription { 97 | var closed = false 98 | const repeatedTask = () => { 99 | if (closed) return 100 | task() 101 | this.delay(repeatedTask, interval) 102 | } 103 | this.delay(repeatedTask, interval) 104 | return { 105 | get closed() { 106 | return closed 107 | }, 108 | unsubscribe() { 109 | closed = true 110 | } 111 | } 112 | } 113 | 114 | private run() { 115 | this.queue.forEach(node => { 116 | const qItem = node.value 117 | if (qItem.time === this.clock) { 118 | qItem.task() 119 | this.queue.remove(node) 120 | } 121 | }) 122 | } 123 | 124 | subscribeTo( 125 | f: () => IObservable, 126 | start: number = DEFAULT_OPTIONS.subscriptionStart, 127 | stop: number = DEFAULT_OPTIONS.subscriptionStop 128 | ) { 129 | let subscription: ISubscription 130 | const observer = this.Observer() 131 | this.delay(() => (subscription = f().subscribe(observer, this)), start, 0) 132 | this.delay( 133 | () => !subscription.closed && subscription.unsubscribe(), 134 | stop, 135 | 0 136 | ) 137 | return observer 138 | } 139 | 140 | start( 141 | f: () => IObservable, 142 | start = DEFAULT_OPTIONS.subscriptionStart, 143 | stop = DEFAULT_OPTIONS.subscriptionStop 144 | ) { 145 | const resultsObserver = this.subscribeTo(f, start, stop) 146 | this.advanceBy(stop) 147 | return resultsObserver 148 | } 149 | 150 | startSubscription( 151 | f: () => ISubscription, 152 | start = DEFAULT_OPTIONS.subscriptionStart, 153 | stop = DEFAULT_OPTIONS.subscriptionStop 154 | ) { 155 | let subscription: ISubscription 156 | this.delay(() => (subscription = f()), start) 157 | this.delay(() => subscription.unsubscribe(), stop) 158 | this.advanceBy(stop) 159 | } 160 | 161 | Cold(events: string): TestObservable 162 | Cold(events: EventSource): TestObservable 163 | Cold(...events: EventSource): TestObservable 164 | Cold(...t: any[]) { 165 | return ColdTestObservable(this, normalizeEvents(...t)) as TestObservable 166 | } 167 | 168 | Hot(events: string): TestObservable 169 | Hot(...events: EventSource): TestObservable 170 | Hot(events: EventSource): TestObservable 171 | Hot(...t: any[]) { 172 | return HotTestObservable(this, normalizeEvents(...t)) as TestObservable 173 | } 174 | 175 | Observer() { 176 | return new TestObserver(this) 177 | } 178 | 179 | timeline(tasks: Array<[number, () => void]>) { 180 | tasks.forEach(([time, task]) => this.delay(task, time)) 181 | } 182 | } 183 | 184 | export const createTestScheduler = ( 185 | rafTimeout: number = DEFAULT_OPTIONS.rafTimeout 186 | ) => new TestScheduler(rafTimeout) 187 | -------------------------------------------------------------------------------- /src/sinks/ForEach.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 01/11/16. 3 | */ 4 | import {IObservable} from '../internal/Observable' 5 | import {IObserver} from '../internal/Observer' 6 | import {ISubscription} from '../internal/Subscription' 7 | import {curry} from '../internal/Utils' 8 | import {createScheduler, IScheduler} from '../schedulers/Scheduler' 9 | 10 | export type TOnNext = {(value: T): void} | IObserver 11 | export type TSource = IObservable 12 | export type TResult = ISubscription 13 | 14 | const noop = () => void 0 15 | 16 | export type ForEachType = { 17 | (onNext: TOnNext, source: TSource, scheduler?: IScheduler): TResult 18 | (onNext: TOnNext): { 19 | (source: TSource, scheduler?: IScheduler): TResult 20 | } 21 | } 22 | 23 | export const forEach = curry(function( 24 | next: TOnNext, 25 | observable: TSource, 26 | scheduler: IScheduler = createScheduler() 27 | ) { 28 | const observer: IObserver = 29 | typeof next === 'function' 30 | ? { 31 | next, 32 | error: err => { 33 | iSubscription.unsubscribe() 34 | throw err 35 | }, 36 | complete: noop 37 | } 38 | : next 39 | 40 | const iSubscription = observable.subscribe(observer, scheduler) 41 | return iSubscription 42 | }) as ForEachType 43 | -------------------------------------------------------------------------------- /src/sinks/ToNodeStream.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 29/10/17. 3 | */ 4 | import {Transform, TransformCallback, Writable} from 'stream' 5 | import {IObservable} from '../internal/Observable' 6 | import {IObserver} from '../internal/Observer' 7 | import {ISubscription} from '../internal/Subscription' 8 | import {createScheduler} from '../schedulers/Scheduler' 9 | 10 | class Writer implements IObserver { 11 | constructor(private dup: Writable) {} 12 | 13 | next(val: any): void { 14 | this.dup.write(val) 15 | } 16 | 17 | error(err: Error): void { 18 | this.dup.emit('error', err) 19 | } 20 | 21 | complete(): void { 22 | this.dup.end() 23 | } 24 | } 25 | 26 | class ToNodeStream extends Transform { 27 | private sub: ISubscription 28 | constructor(src: IObservable) { 29 | super({ 30 | readableObjectMode: true, 31 | writableObjectMode: true 32 | }) 33 | this.sub = src.subscribe(new Writer(this), createScheduler()) 34 | } 35 | _destroy() { 36 | this.sub.unsubscribe() 37 | } 38 | 39 | _transform(chunk: any, encoding: string, callback: TransformCallback) { 40 | callback(undefined, chunk) 41 | } 42 | } 43 | 44 | export const toNodeStream = (source: IObservable): Transform => 45 | new ToNodeStream(source) 46 | -------------------------------------------------------------------------------- /src/sinks/ToPromise.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 29/10/17. 3 | */ 4 | import {IObservable} from '../internal/Observable' 5 | import {createScheduler} from '../schedulers/Scheduler' 6 | 7 | export const toPromise = (src: IObservable): Promise => { 8 | return new Promise((resolve, reject) => { 9 | let value: T 10 | src.subscribe( 11 | { 12 | next: _value => (value = _value), 13 | error: reject, 14 | complete: () => resolve(value) 15 | }, 16 | createScheduler() 17 | ) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/sources/Create.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 16/10/16. 3 | */ 4 | import {IObservable} from '../internal/Observable' 5 | import {IObserver} from '../internal/Observer' 6 | import {ISubscription} from '../internal/Subscription' 7 | import {IScheduler} from '../schedulers/Scheduler' 8 | 9 | class JustObservable implements IObservable { 10 | constructor(private val: T) {} 11 | 12 | run(observer: IObserver) { 13 | observer.next(this.val) 14 | observer.complete() 15 | } 16 | 17 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 18 | return scheduler.asap(this.run.bind(this, observer)) 19 | } 20 | } 21 | 22 | const MockSubscription: ISubscription = { 23 | unsubscribe() {}, 24 | closed: false 25 | } 26 | 27 | class Never implements IObservable { 28 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 29 | return MockSubscription 30 | } 31 | } 32 | 33 | class Empty implements IObservable { 34 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 35 | return scheduler.asap(observer.complete.bind(observer)) 36 | } 37 | } 38 | 39 | export const empty = (): IObservable => new Empty() 40 | export const just = (value: T): IObservable => new JustObservable(value) 41 | export const never = (): IObservable => new Never() 42 | -------------------------------------------------------------------------------- /src/sources/Frames.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 28/01/17. 3 | */ 4 | import {IObservable} from '../internal/Observable' 5 | import {IObserver} from '../internal/Observer' 6 | import {Periodic} from '../internal/Periodic' 7 | import {ISafeObserver, safeObserver} from '../internal/SafeObserver' 8 | import {ISubscription} from '../internal/Subscription' 9 | import {IScheduler} from '../schedulers/Scheduler' 10 | 11 | class RAFSubscription extends Periodic { 12 | constructor(readonly sink: ISafeObserver, scheduler: IScheduler) { 13 | super() 14 | this.sub = scheduler.frames(this.onEvent.bind(this)) 15 | } 16 | } 17 | 18 | class FrameObservable implements IObservable { 19 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 20 | return new RAFSubscription(safeObserver(observer), scheduler) 21 | } 22 | } 23 | 24 | export function frames(): IObservable { 25 | return new FrameObservable() 26 | } 27 | -------------------------------------------------------------------------------- /src/sources/FromArray.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 28/09/16. 3 | */ 4 | import {IObservable} from '../internal/Observable' 5 | import {IObserver} from '../internal/Observer' 6 | import {ISubscription} from '../internal/Subscription' 7 | import {tryCatch} from '../internal/Utils' 8 | import {IScheduler} from '../schedulers/Scheduler' 9 | 10 | class FromArraySubscription implements ISubscription { 11 | private subscription: ISubscription 12 | closed = false 13 | 14 | constructor( 15 | private array: Array, 16 | private sink: IObserver, 17 | scheduler: IScheduler 18 | ) { 19 | this.subscription = scheduler.asap(this.executeSafely.bind(this)) 20 | } 21 | 22 | // TODO: use mixins 23 | private executeSafely() { 24 | const r = tryCatch(this.execute).call(this) 25 | if (r.isError) this.sink.error(r.getError()) 26 | } 27 | 28 | execute() { 29 | const l = this.array.length 30 | const sink = this.sink 31 | for (var i = 0; i < l && !this.closed; ++i) { 32 | sink.next(this.array[i]) 33 | } 34 | sink.complete() 35 | } 36 | 37 | unsubscribe(): void { 38 | this.subscription.unsubscribe() 39 | this.closed = true 40 | } 41 | } 42 | 43 | class FromObservable implements IObservable { 44 | constructor(private array: Array) {} 45 | 46 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 47 | return new FromArraySubscription(this.array, observer, scheduler) 48 | } 49 | } 50 | 51 | export function fromArray(list: Array): IObservable { 52 | return new FromObservable(list) 53 | } 54 | 55 | export const of = (...t: Array): IObservable => new FromObservable(t) 56 | -------------------------------------------------------------------------------- /src/sources/FromDOM.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 14/10/16. 3 | */ 4 | import {IObservable} from '../internal/Observable' 5 | import {IObserver} from '../internal/Observer' 6 | import {ISubscription} from '../internal/Subscription' 7 | import {curry} from '../internal/Utils' 8 | 9 | export type TResult = IObservable 10 | export type IListener = { 11 | (e: Event): void 12 | } 13 | 14 | class DOMSubscription implements ISubscription { 15 | closed: boolean = false 16 | 17 | constructor( 18 | private element: EventTarget, 19 | private listener: IListener, 20 | private name: string 21 | ) {} 22 | 23 | unsubscribe(): void { 24 | this.element.removeEventListener(this.name, this.listener) 25 | } 26 | } 27 | 28 | class DOMObservable implements TResult { 29 | constructor(private name: string, private element: EventTarget) {} 30 | 31 | subscribe(observer: IObserver): ISubscription { 32 | const listener = observer.next.bind(observer) 33 | this.element.addEventListener(this.name, listener) 34 | return new DOMSubscription(this.element, listener, this.name) 35 | } 36 | } 37 | 38 | export const fromDOM = curry(function(element: EventTarget, name: string) { 39 | return new DOMObservable(name, element) 40 | }) as { 41 | (element: EventTarget, name: string): TResult 42 | (element: EventTarget): {(name: string): TResult} 43 | } 44 | -------------------------------------------------------------------------------- /src/sources/FromNodeStream.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 29/10/17. 3 | */ 4 | 5 | import {Readable} from 'stream' 6 | import {IObservable} from '../internal/Observable' 7 | import {IObserver} from '../internal/Observer' 8 | import {ISubscription, Subscription} from '../internal/Subscription' 9 | import {IScheduler} from '../schedulers/Scheduler' 10 | 11 | class FromNodeStream implements IObservable { 12 | constructor(private src: Readable) {} 13 | 14 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 15 | this.src.on('data', observer.next.bind(observer)) 16 | this.src.on('error', observer.error.bind(observer)) 17 | this.src.on('end', observer.complete.bind(observer)) 18 | return new Subscription(this.src.destroy.bind(this.src)) 19 | } 20 | } 21 | export const fromNodeStream = (source: Readable): IObservable => 22 | new FromNodeStream(source) 23 | -------------------------------------------------------------------------------- /src/sources/FromPromise.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 16/10/16. 3 | */ 4 | import {IObservable, Observable} from '../internal/Observable' 5 | import {IObserver} from '../internal/Observer' 6 | 7 | export function onResult(observer: IObserver, result: T) { 8 | observer.next(result) 9 | observer.complete() 10 | } 11 | 12 | export function onError(observer: IObserver, error: Error) { 13 | observer.error(error) 14 | observer.complete() 15 | } 16 | 17 | export function subscriberFunction( 18 | f: () => Promise, 19 | observer: IObserver 20 | ) { 21 | f() 22 | .then(result => onResult(observer, result)) 23 | .catch(err => onError(observer, err)) 24 | } 25 | 26 | export function fromPromise(f: () => Promise): IObservable { 27 | return new Observable(observer => subscriberFunction(f, observer)) 28 | } 29 | -------------------------------------------------------------------------------- /src/sources/Interval.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 27/09/16. 3 | */ 4 | import {IObservable} from '../internal/Observable' 5 | import {IObserver} from '../internal/Observer' 6 | import {Periodic} from '../internal/Periodic' 7 | import {ISafeObserver, safeObserver} from '../internal/SafeObserver' 8 | import {ISubscription} from '../internal/Subscription' 9 | import {IScheduler} from '../schedulers/Scheduler' 10 | 11 | class TimerSubscription extends Periodic { 12 | constructor( 13 | readonly sink: ISafeObserver, 14 | scheduler: IScheduler, 15 | interval: number 16 | ) { 17 | super() 18 | this.sub = scheduler.periodic(this.onEvent.bind(this), interval) 19 | } 20 | } 21 | 22 | class IntervalObservable implements IObservable { 23 | constructor(private interval: number) {} 24 | 25 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 26 | return new TimerSubscription( 27 | safeObserver(observer), 28 | scheduler, 29 | this.interval 30 | ) 31 | } 32 | } 33 | 34 | export function interval(interval: number): IObservable { 35 | return new IntervalObservable(interval) 36 | } 37 | -------------------------------------------------------------------------------- /src/sources/Subject.ts: -------------------------------------------------------------------------------- 1 | import {LinkedListNode} from 'dbl-linked-list-ds' 2 | import {IObservable} from '../internal/Observable' 3 | import {CompositeObserver, IObserver} from '../internal/Observer' 4 | import {ISubscription} from '../internal/Subscription' 5 | import {IScheduler} from '../schedulers/Scheduler' 6 | 7 | class SubjectSubscription implements ISubscription { 8 | constructor( 9 | private cObserver: CompositeObserver, 10 | private node: LinkedListNode> 11 | ) {} 12 | closed: boolean 13 | 14 | unsubscribe(): void { 15 | this.cObserver.remove(this.node) 16 | } 17 | } 18 | 19 | export class Subject extends CompositeObserver implements IObservable { 20 | subscribe(observer: IObserver, scheduler: IScheduler): ISubscription { 21 | return new SubjectSubscription(this, this.add(observer)) 22 | } 23 | } 24 | export const subject = () => new Subject() 25 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 13/04/17. 3 | */ 4 | 5 | export {EVENT} from './src/internal/Events' 6 | export {createTestScheduler} from './src/schedulers/TestScheduler' 7 | export {fromMarble, toMarble} from './src/internal/Marble' 8 | -------------------------------------------------------------------------------- /test/test.Combine.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 26/02/17. 3 | */ 4 | 5 | import * as assert from 'assert' 6 | import {EVENT} from '../src/internal/Events' 7 | import {fromMarble} from '../src/internal/Marble' 8 | import {combine} from '../src/operators/Combine' 9 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 10 | 11 | describe('combine()', () => { 12 | it('should combine events from multiple sources', () => { 13 | const SH = createTestScheduler() 14 | const {results} = SH.start(() => { 15 | return combine((a, b, c) => a + b + c, [ 16 | SH.Hot(fromMarble('a-b-c-d|')), 17 | SH.Hot(fromMarble('-p-q-r-s|')), 18 | SH.Hot(fromMarble('---x-y-z--|')) 19 | ]) 20 | }) 21 | assert.deepEqual(results, [ 22 | EVENT.next(203, 'bqx'), 23 | EVENT.next(204, 'cqx'), 24 | EVENT.next(205, 'crx'), 25 | EVENT.next(205, 'cry'), 26 | EVENT.next(206, 'dry'), 27 | EVENT.next(207, 'dsy'), 28 | EVENT.next(207, 'dsz'), 29 | EVENT.complete(210) 30 | ]) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/test.Concat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 05/09/17. 3 | */ 4 | import * as t from 'assert' 5 | import {toMarble} from '../src/internal/Marble' 6 | import {concat} from '../src/operators/Concat' 7 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 8 | 9 | describe('concat()', () => { 10 | it('should concat two stream', () => { 11 | const sh = createTestScheduler() 12 | const s0 = sh.Hot('--A--B--C--|') 13 | const s1 = sh.Hot('---1---2---3---4---|') 14 | const actual = toMarble(sh.start(() => concat(s0, s1)).results) 15 | const expected = '--A--B--C--3---4---|' 16 | t.strictEqual(actual, expected) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/test.Debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 12/02/17. 3 | */ 4 | import * as t from 'assert' 5 | import {fromMarble, toMarble} from '../src/internal/Marble' 6 | import {debounce} from '../src/operators/Debounce' 7 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 8 | 9 | describe('debounce()', () => { 10 | it('should not fire until the source pauses for atleast the give unit of time', () => { 11 | const sh = createTestScheduler() 12 | const {results} = sh.start(() => 13 | debounce(1, sh.Hot(fromMarble('012-345-678|'))) 14 | ) 15 | t.strictEqual(toMarble(results), '---2---5---|') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/test.Delay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 29/01/17. 3 | */ 4 | import * as t from 'assert' 5 | import {EVENT} from '../src/internal/Events' 6 | import {fromMarble, toMarble} from '../src/internal/Marble' 7 | import {ERROR_MESSAGE, thrower} from '../src/internal/Thrower' 8 | import {delay} from '../src/operators/Delay' 9 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 10 | 11 | describe('delay()', () => { 12 | it('should delay the source events', () => { 13 | const sh = createTestScheduler() 14 | const {results} = sh.start(() => delay(2, sh.Hot(fromMarble('12345|')))) 15 | t.strictEqual(toMarble(results), '--12345|') 16 | }) 17 | 18 | it('should forward error', () => { 19 | const sh = createTestScheduler() 20 | const {results} = sh.start(() => delay(2, sh.Hot(fromMarble('--#|')))) 21 | t.strictEqual(toMarble(results), '--#--|') 22 | }) 23 | 24 | it('should catch internal exception', () => { 25 | const sh = createTestScheduler() 26 | const {results} = sh.start(() => 27 | thrower(delay(20, sh.Hot(fromMarble('0')))) 28 | ) 29 | t.deepEqual(results, [EVENT.error(220, Error(ERROR_MESSAGE))]) 30 | }) 31 | 32 | it('should unsubscribe', () => { 33 | const sh = createTestScheduler() 34 | sh.start(() => delay(50, sh.Hot(fromMarble('0'))), 200, 230) 35 | t.strictEqual(sh.length, 0) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/test.ForEach.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 09/09/17. 3 | */ 4 | 5 | import * as assert from 'assert' 6 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 7 | import {forEach} from '../src/sinks/ForEach' 8 | 9 | describe('forEach()', () => { 10 | context('when a function is passed', () => { 11 | it('should forward values', () => { 12 | const sh = createTestScheduler() 13 | const $ = sh.Cold('-1234') 14 | 15 | const expected = ['1', '2', '3', '4'] 16 | const actual: number[] = [] 17 | 18 | sh.startSubscription(() => forEach((i: number) => actual.push(i), $)) 19 | assert.deepEqual(actual, expected) 20 | }) 21 | it('should unsubscribe from the source on error', () => { 22 | const sh = createTestScheduler() 23 | const testObservable = sh.Hot('-123#') 24 | const expected = ' ^---!' 25 | assert.throws( 26 | () => 27 | sh.startSubscription(() => forEach(() => void 0, testObservable, sh)), 28 | (_: Error) => _.message === '#' 29 | ) 30 | 31 | const actual = testObservable.toString() 32 | assert.strictEqual(actual, expected.trim()) 33 | }) 34 | }) 35 | 36 | context('when an observer is passed', () => { 37 | it('should forward values', () => { 38 | const sh = createTestScheduler() 39 | const $ = sh.Hot('-1234|') 40 | 41 | const testObserver = sh.Observer() 42 | 43 | sh.startSubscription(() => forEach(testObserver, $)) 44 | 45 | const actual = testObserver.toString() 46 | const expected = '-1234|' 47 | 48 | assert.strictEqual(actual, expected) 49 | }) 50 | it('should forward errors', () => { 51 | const sh = createTestScheduler() 52 | const expected = '-123#' 53 | 54 | const testObservable = sh.Hot(expected) 55 | const testObserver = sh.Observer() 56 | 57 | sh.startSubscription(() => forEach(testObserver, testObservable, sh)) 58 | 59 | const actual = testObserver.toString() 60 | 61 | assert.strictEqual(actual, expected) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /test/test.Frames.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 28/01/17. 3 | */ 4 | import * as t from 'assert' 5 | import {toMarble} from '../src/internal/Marble' 6 | import {throwError} from '../src/internal/Thrower' 7 | import {scan} from '../src/operators/Scan' 8 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 9 | import {frames} from '../src/sources/Frames' 10 | 11 | describe('frames()', () => { 12 | it('should emit requestAnimationFrame events', () => { 13 | const sh = createTestScheduler(1) 14 | const {results} = sh.start(() => scan(i => i + 1, -1, frames()), 200, 205) 15 | t.strictEqual(toMarble(results), '-0123') 16 | }) 17 | 18 | it('should capture internal errors', () => { 19 | const sh = createTestScheduler(2) 20 | const reduce = (i: number) => { 21 | if (i === 5) throwError('Yo Air') 22 | return i + 1 23 | } 24 | const {results} = sh.start(() => scan(reduce, -1, frames()), 200, 500) 25 | t.strictEqual(toMarble(results), '--0-1-2-3-4-5-#') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/test.FromArray.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 04/10/16. 3 | */ 4 | 5 | import * as t from 'assert' 6 | import {EVENT} from '../src/internal/Events' 7 | import {ERROR_MESSAGE, throwError} from '../src/internal/Thrower' 8 | import {map} from '../src/operators/Map' 9 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 10 | import {fromArray} from '../src/sources/FromArray' 11 | 12 | const {next, error} = EVENT 13 | 14 | describe('fromArray()', () => { 15 | const throwMessage = (message: string) => { 16 | throw message 17 | } 18 | it('should emit array values as events', () => { 19 | const sh = createTestScheduler() 20 | const testFunction = (x: any) => 21 | x === 2 ? throwError(ERROR_MESSAGE) : x * 100 22 | const {results} = sh.start(() => map(testFunction, fromArray([1, 2, 3]))) 23 | t.deepEqual(results, [next(201, 100), error(201, new Error(ERROR_MESSAGE))]) 24 | }) 25 | 26 | it('should handle thrown non Error exceptions', () => { 27 | const sh = createTestScheduler() 28 | const testFunction = (x: any) => 29 | x === 2 ? throwMessage(ERROR_MESSAGE) : x * 100 30 | const {results} = sh.start(() => map(testFunction, fromArray([1, 2, 3]))) 31 | t.deepEqual(results, [next(201, 100), error(201, ERROR_MESSAGE as any)]) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/test.FromPromise.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 17/10/16. 3 | */ 4 | import * as t from 'assert' 5 | import {createScheduler} from '../src/schedulers/Scheduler' 6 | import {fromPromise} from '../src/sources/FromPromise' 7 | 8 | describe('fromPromise()', () => { 9 | it('should emit the value of the promise', cb => { 10 | const results: number[] = [] 11 | const s$ = fromPromise( 12 | () => 13 | new Promise(function(resolve) { 14 | resolve(100) 15 | }) 16 | ) 17 | s$.subscribe( 18 | { 19 | next: (x: number) => results.push(x), 20 | complete: () => { 21 | t.deepEqual(results, [100]) 22 | cb() 23 | }, 24 | error: (err: Error) => { 25 | throw err 26 | } 27 | }, 28 | createScheduler() 29 | ) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/test.Interval.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 27/09/16. 3 | */ 4 | 5 | import * as t from 'assert' 6 | import {EVENT, EventError} from '../src/internal/Events' 7 | import {toMarble} from '../src/internal/Marble' 8 | import {ERROR_MESSAGE, thrower, throwError} from '../src/internal/Thrower' 9 | import {scan} from '../src/operators/Scan' 10 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 11 | import {interval} from '../src/sources/Interval' 12 | const {error} = EVENT 13 | 14 | describe('interval()', () => { 15 | it('should emit values every t ms', () => { 16 | const sh = createTestScheduler() 17 | const {results} = sh.start( 18 | () => scan(i => i + 1, -1, interval(1)), 19 | 200, 20 | 205 21 | ) 22 | t.strictEqual(toMarble(results), '-0123') 23 | }) 24 | it('should catch exceptions', () => { 25 | const sh = createTestScheduler() 26 | const observer = sh.Observer() 27 | thrower(interval(100)).subscribe(observer, sh) 28 | sh.advanceBy(100) 29 | t.deepEqual(observer.results, [error(100, Error(ERROR_MESSAGE))]) 30 | t.strictEqual( 31 | (observer.results[0] as EventError).value.message, 32 | ERROR_MESSAGE 33 | ) 34 | }) 35 | 36 | it('should stop after error', () => { 37 | const sh = createTestScheduler() 38 | const {results} = sh.start(() => 39 | scan(i => (i === 5 ? throwError('Yay!') : i + 1), 0, interval(2)) 40 | ) 41 | const expected = '--1-2-3-4-5-#' 42 | t.strictEqual(toMarble(results), expected) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/test.Map.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 27/09/16. 3 | */ 4 | 5 | import * as t from 'assert' 6 | import {EVENT} from '../src/internal/Events' 7 | import {map} from '../src/operators/Map' 8 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 9 | 10 | const {next, complete} = EVENT 11 | 12 | describe('map()', () => { 13 | it('should should be subscribe-able', () => { 14 | const sh = createTestScheduler() 15 | const $ = sh.Cold([ 16 | next(210, 0), 17 | next(220, 10), 18 | next(230, 20), 19 | complete(250) 20 | ]) 21 | const {results} = sh.start(() => map((x: number) => x + 1, $)) 22 | t.deepEqual(results, [ 23 | next(410, 1), 24 | next(420, 11), 25 | next(430, 21), 26 | complete(450) 27 | ]) 28 | }) 29 | it('should be able to subscribe to hot stream', () => { 30 | const sh = createTestScheduler() 31 | const $ = sh.Hot([ 32 | next(100, -10), 33 | next(210, 0), 34 | next(220, 10), 35 | next(230, 20), 36 | complete(250) 37 | ]) 38 | const {results} = sh.start(() => map((x: number) => x + 1, $)) 39 | t.deepEqual(results, [ 40 | next(210, 1), 41 | next(220, 11), 42 | next(230, 21), 43 | complete(250) 44 | ]) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/test.Marble.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 02/11/16. 3 | */ 4 | import * as assert from 'assert' 5 | import {EVENT} from '../src/internal/Events' 6 | import {fromMarble, toMarble} from '../src/internal/Marble' 7 | 8 | describe('marble()', () => { 9 | it('should work for ABC|', () => { 10 | const expected = 'ABC|' 11 | const actual = toMarble(fromMarble(expected)) 12 | assert.strictEqual(actual, expected) 13 | }) 14 | 15 | it('should work for -ABC|', () => { 16 | const expected = '-ABC|' 17 | const actual = toMarble(fromMarble(expected)) 18 | assert.strictEqual(actual, expected) 19 | }) 20 | 21 | it('should work for -A-B-C-|', () => { 22 | const expected = '-A-B-C-|' 23 | const actual = toMarble(fromMarble(expected)) 24 | assert.strictEqual(actual, expected) 25 | }) 26 | 27 | it('should work for -ABC#', () => { 28 | const expected = '-ABC#' 29 | const actual = toMarble(fromMarble(expected)) 30 | assert.strictEqual(actual, expected) 31 | }) 32 | 33 | it('should ignore whitespaces', () => { 34 | const message = ' -A B -- C | ' 35 | const actual = toMarble(fromMarble(message)) 36 | const expected = '-AB--C|' 37 | assert.strictEqual(actual, expected) 38 | }) 39 | 40 | it('should parse multi-letter messages', () => { 41 | const message = ' -(ABC)-(PQR)-|' 42 | const actual = fromMarble(message) 43 | const expected = [ 44 | EVENT.next(201, 'ABC'), 45 | EVENT.next(203, 'PQR'), 46 | EVENT.complete(205) 47 | ] 48 | assert.deepEqual(actual, expected) 49 | }) 50 | 51 | it('should create multi-letter messages', () => { 52 | const actual = toMarble([ 53 | EVENT.next(201, 'ABC'), 54 | EVENT.next(203, 'PQR'), 55 | EVENT.complete(205) 56 | ]) 57 | const expected = '-(ABC)-(PQR)-|' 58 | assert.deepEqual(actual, expected) 59 | }) 60 | 61 | it('should interop with multi-letter messages', () => { 62 | const test = '--a--(bc)--p(qr)--|' 63 | const actual = toMarble(fromMarble(test)) 64 | const expected = test 65 | assert.strictEqual(actual, expected) 66 | }) 67 | 68 | it('should auto convert values to numbers if possible', () => { 69 | const message = '--1-2-3-4' 70 | const actual = fromMarble(message) 71 | const expected = [ 72 | EVENT.next(202, 1), 73 | EVENT.next(204, 2), 74 | EVENT.next(206, 3), 75 | EVENT.next(208, 4) 76 | ] 77 | 78 | assert.deepEqual(actual, expected) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /test/test.Merge.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 17/10/16. 3 | */ 4 | 5 | import * as t from 'assert' 6 | import {EVENT} from '../src/internal/Events' 7 | import {merge} from '../src/operators/Merge' 8 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 9 | 10 | describe('merge', () => { 11 | it('should merge multiple source streams', () => { 12 | const sh = createTestScheduler() 13 | const a$ = sh.Hot([ 14 | EVENT.next(210, 'A0'), 15 | EVENT.next(220, 'A1'), 16 | EVENT.complete(230) 17 | ]) 18 | const b$ = sh.Hot([ 19 | EVENT.next(212, 'B0'), 20 | EVENT.next(222, 'B1'), 21 | EVENT.complete(232) 22 | ]) 23 | const c$ = sh.Hot([ 24 | EVENT.next(215, 'C0'), 25 | EVENT.next(225, 'C1'), 26 | EVENT.complete(235) 27 | ]) 28 | const {results} = sh.start(() => merge(a$, b$, c$)) 29 | t.deepEqual(results, [ 30 | EVENT.next(210, 'A0'), 31 | EVENT.next(212, 'B0'), 32 | EVENT.next(215, 'C0'), 33 | EVENT.next(220, 'A1'), 34 | EVENT.next(222, 'B1'), 35 | EVENT.next(225, 'C1'), 36 | EVENT.complete(235) 37 | ]) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/test.MergeMap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 31/08/17. 3 | */ 4 | import * as assert from 'assert' 5 | import {EVENT} from '../src/internal/Events' 6 | import {mergeMap} from '../src/operators/MergeMap' 7 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 8 | 9 | const {next, complete} = EVENT 10 | describe('mergeMap()', () => { 11 | context('when concurrency is Infinity', () => { 12 | it('should work like flatMap()', () => { 13 | const sh = createTestScheduler() 14 | const conc = ' -3| ' 15 | const a = ' --A---A| ' 16 | const b = ' ---B---B---B---B| ' 17 | const c = ' ----C---C---C---C---C---|' 18 | const expected = '--ABC-ABC--BC--BC---C---|' 19 | 20 | const observer = sh.start(() => { 21 | const conc$ = sh.Hot(conc) 22 | const a$ = sh.Hot(a) 23 | const b$ = sh.Hot(b) 24 | const c$ = sh.Hot(c) 25 | const source$$ = sh.Hot( 26 | next(200, a$), 27 | next(201, b$), 28 | next(202, c$), 29 | complete(203) 30 | ) 31 | return mergeMap(conc$, (i: any) => i, source$$) 32 | }) 33 | assert.deepEqual(observer.toString(), expected) 34 | }) 35 | }) 36 | context('when concurrency is 1', () => { 37 | it('should work like concatMap()', () => { 38 | const sh = createTestScheduler() 39 | 40 | const conc = ' -1| ' 41 | const a = ' --A---A| ' 42 | const b = ' ---B---B---B---B| ' 43 | const c = ' ----C---C---C---C---C---|' 44 | const expected = '--A---AB---B---BC---C---|' 45 | 46 | const observer = sh.start(() => { 47 | const conc$ = sh.Hot(conc) 48 | const a$ = sh.Hot(a) 49 | const b$ = sh.Hot(b) 50 | const c$ = sh.Hot(c) 51 | const source$$ = sh.Hot( 52 | next(200, a$), 53 | next(201, b$), 54 | next(202, c$), 55 | complete(203) 56 | ) 57 | return mergeMap(conc$, (i: any) => i, source$$) 58 | }) 59 | assert.deepEqual(observer.toString(), expected) 60 | }) 61 | }) 62 | 63 | context('when concurrency increases', () => { 64 | it('should automatically subscribe from buffer', () => { 65 | const sh = createTestScheduler() 66 | const conc = ' -1-----------3----------|' 67 | const a = ' --A---A---A---A---A---A-|' 68 | const b = ' ---B---B---B---B---B---B|' 69 | const c = ' ----C---C---C---C---C---|' 70 | const expected = '--A---A---A---ABC-ABC-AB|' 71 | 72 | const observer = sh.start(() => { 73 | const concurr$ = sh.Hot(conc) 74 | const a$ = sh.Hot(a) 75 | const b$ = sh.Hot(b) 76 | const c$ = sh.Hot(c) 77 | const source$$ = sh.Hot( 78 | next(200, a$), 79 | next(201, b$), 80 | next(202, c$), 81 | complete(203) 82 | ) 83 | return mergeMap(concurr$, (i: any) => i, source$$) 84 | }) 85 | 86 | assert.strictEqual(observer.toString(), expected) 87 | }) 88 | }) 89 | 90 | context('when concurrency decreases', () => { 91 | it('should automatically unsubscribe the oldest', () => { 92 | const sh = createTestScheduler() 93 | const conc = ' -3-----------1----------|' 94 | const a = ' --A---A---A---A---A---A-|' 95 | const b = ' ---B---B---B---B---B---B|' 96 | const c = ' ----C---C---C---C---C---|' 97 | const expected = '--ABC-ABC-ABC---C---C---|' 98 | 99 | const observer = sh.start(() => { 100 | const concurr$ = sh.Hot(conc) 101 | const a$ = sh.Hot(a) 102 | const b$ = sh.Hot(b) 103 | const c$ = sh.Hot(c) 104 | const source$$ = sh.Hot( 105 | next(200, a$), 106 | next(201, b$), 107 | next(202, c$), 108 | complete(203) 109 | ) 110 | return mergeMap(concurr$, (i: any) => i, source$$) 111 | }) 112 | 113 | assert.strictEqual(observer.toString(), expected) 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /test/test.Multicast.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 24/10/16. 3 | */ 4 | 5 | import * as t from 'assert' 6 | import {EVENT} from '../src/internal/Events' 7 | import {TestObserver} from '../src/internal/TestObserver' 8 | import {map} from '../src/operators/Map' 9 | import {multicast} from '../src/operators/Multicast' 10 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 11 | 12 | describe('multicast()', () => { 13 | it('should subscribe only once', () => { 14 | let i = 0 15 | const sh = createTestScheduler() 16 | const ob0 = new TestObserver(sh) 17 | const ob1 = new TestObserver(sh) 18 | const t$ = multicast( 19 | map( 20 | (x: {(): number}) => x(), 21 | sh.Hot([ 22 | EVENT.next(10, () => ++i), 23 | EVENT.next(20, () => ++i), 24 | EVENT.next(30, () => ++i), 25 | EVENT.next(40, () => ++i), 26 | EVENT.next(50, () => ++i) 27 | ]) 28 | ) 29 | ) 30 | t$.subscribe(ob0, sh) 31 | t$.subscribe(ob1, sh) 32 | sh.advanceBy(50) 33 | t.deepEqual(ob0, ob1) 34 | t.deepEqual(ob0.results, [ 35 | EVENT.next(10, 1), 36 | EVENT.next(20, 2), 37 | EVENT.next(30, 3), 38 | EVENT.next(40, 4), 39 | EVENT.next(50, 5) 40 | ]) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/test.NodeStream.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 29/10/17. 3 | */ 4 | 5 | import * as assert from 'assert' 6 | import {scan} from '../src/operators/Scan' 7 | import {slice} from '../src/operators/Slice' 8 | import {toNodeStream} from '../src/sinks/ToNodeStream' 9 | import {toPromise} from '../src/sinks/ToPromise' 10 | import {fromNodeStream} from '../src/sources/FromNodeStream' 11 | import {interval} from '../src/sources/Interval' 12 | 13 | describe('NodeStream()', () => { 14 | it('it should inter-convert', async function() { 15 | const src = scan(a => a + 1, 0, interval(10)) 16 | const result = await toPromise( 17 | slice(0, 5, fromNodeStream(toNodeStream(src))) 18 | ) 19 | assert.equal(result, 5) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/test.Observable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 08/12/16. 3 | */ 4 | import * as t from 'assert' 5 | import {EVENT} from '../src/internal/Events' 6 | import {Observable} from '../src/internal/Observable' 7 | import {slice} from '../src/main' 8 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 9 | 10 | describe('new Observable()', () => { 11 | it('should emit values via next()', () => { 12 | const sh = createTestScheduler() 13 | const {results} = sh.start(() => new Observable(ob => ob.next('A'))) 14 | t.deepEqual(results, [EVENT.next(201, 'A')]) 15 | }) 16 | 17 | it('should be subscribe-able', () => { 18 | const sh = createTestScheduler() 19 | const {results} = sh.start( 20 | () => 21 | new Observable((ob, sh) => { 22 | sh.delay(() => ob.next('A'), 15) 23 | }) 24 | ) 25 | t.deepEqual(results, [EVENT.next(216, 'A')]) 26 | }) 27 | 28 | it('should unsubscribe', () => { 29 | const sh = createTestScheduler() 30 | const actual = sh.start(() => 31 | slice( 32 | 0, 33 | 3, 34 | new Observable(ob => { 35 | let done = false 36 | for (let i = 0; i < 10 && !done; i++) ob.next(i) 37 | ob.complete() 38 | return () => (done = true) 39 | }) 40 | ) 41 | ).results 42 | 43 | const expected = [ 44 | EVENT.next(201, 0), 45 | EVENT.next(201, 1), 46 | EVENT.next(201, 2), 47 | EVENT.complete(201) 48 | ] 49 | 50 | t.deepEqual(actual, expected) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/test.Sample.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 19/10/16. 3 | */ 4 | import * as t from 'assert' 5 | import {EVENT} from '../src/internal/Events' 6 | import {fromMarble} from '../src/internal/Marble' 7 | import {sample} from '../src/operators/Sample' 8 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 9 | 10 | function toArray(...t: Array) { 11 | return t.join(',') 12 | } 13 | 14 | describe('sample()', () => { 15 | it('should sample multiple sources', () => { 16 | const sh = createTestScheduler() 17 | const a$ = sh.Hot([ 18 | EVENT.next(201, 'A0'), 19 | EVENT.next(203, 'A1'), 20 | EVENT.next(205, 'A2'), 21 | EVENT.complete(205) 22 | ]) 23 | const b$ = sh.Hot([ 24 | EVENT.next(201, 'B0'), 25 | EVENT.next(202, 'B1'), 26 | EVENT.next(203, 'B2'), 27 | EVENT.next(204, 'B3'), 28 | EVENT.complete(204) 29 | ]) 30 | const S$ = sh.Hot([ 31 | EVENT.next(201, '#'), 32 | EVENT.next(202, '#'), 33 | EVENT.next(203, '#'), 34 | EVENT.next(204, '#'), 35 | EVENT.next(205, '#'), 36 | EVENT.complete(205) 37 | ]) 38 | const {results} = sh.start(() => sample(toArray, S$, [a$, b$])) 39 | t.deepEqual(results, [ 40 | EVENT.next(201, 'A0,B0'), 41 | EVENT.next(202, 'A0,B1'), 42 | EVENT.next(203, 'A1,B2'), 43 | EVENT.next(204, 'A1,B3'), 44 | EVENT.next(205, 'A2,B3'), 45 | EVENT.complete(205) 46 | ]) 47 | }) 48 | 49 | it('should sample()', () => { 50 | const sh = createTestScheduler() 51 | const a$ = sh.Hot([ 52 | EVENT.next(201, 0), 53 | EVENT.next(203, 1), 54 | EVENT.next(205, 2), 55 | EVENT.complete(205) 56 | ]) 57 | const b$ = sh.Hot([ 58 | EVENT.next(201, 0), 59 | EVENT.next(202, 1000), 60 | EVENT.next(203, 2000), 61 | EVENT.next(204, 3000), 62 | EVENT.complete(204) 63 | ]) 64 | const S$ = sh.Hot([ 65 | EVENT.next(201, '#'), 66 | EVENT.next(202, '#'), 67 | EVENT.next(203, '#'), 68 | EVENT.next(204, '#'), 69 | EVENT.next(205, '#'), 70 | EVENT.complete(205) 71 | ]) 72 | const {results} = sh.start(() => sample((a, b) => a + b, S$, [a$, b$])) 73 | t.deepEqual(results, [ 74 | EVENT.next(201, 0 + 0), 75 | EVENT.next(202, 0 + 1000), 76 | EVENT.next(203, 1 + 2000), 77 | EVENT.next(204, 1 + 3000), 78 | EVENT.next(205, 2 + 3000), 79 | EVENT.complete(205) 80 | ]) 81 | }) 82 | 83 | it('should sample()', () => { 84 | const sh = createTestScheduler() 85 | const t1$ = sh.Hot(fromMarble('-A-B-C-D')) 86 | const t2$ = sh.Hot(fromMarble('--a-b-c-d')) 87 | const {results} = sh.start(() => sample((a, b) => a + b, t2$, [t1$, t2$])) 88 | t.deepEqual(results, [ 89 | EVENT.next(202, 'Aa'), 90 | EVENT.next(204, 'Bb'), 91 | EVENT.next(206, 'Cc'), 92 | EVENT.next(208, 'Dd') 93 | ]) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /test/test.Scan.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 09/10/16. 3 | */ 4 | 5 | import * as t from 'assert' 6 | import {EVENT} from '../src/internal/Events' 7 | import {scan} from '../src/operators/Scan' 8 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 9 | 10 | const {next, complete} = EVENT 11 | 12 | describe('scan()', () => { 13 | it('should work like R.scan', () => { 14 | const sh = createTestScheduler() 15 | const $ = sh.Cold([ 16 | next(210, 0), 17 | next(220, 1), 18 | next(230, 2), 19 | next(240, 3), 20 | next(250, 4), 21 | complete(250) 22 | ]) 23 | const {results} = sh.start(() => scan((a, b) => a + b, 0, $)) 24 | t.deepEqual(results, [ 25 | next(410, 0), 26 | next(420, 1), 27 | next(430, 3), 28 | next(440, 6), 29 | next(450, 10), 30 | complete(450) 31 | ]) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/test.SkipRepeats.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by niranjan on 12/10/16. 3 | */ 4 | 5 | import * as t from 'assert' 6 | import {EVENT} from '../src/internal/Events' 7 | import {skipRepeats} from '../src/operators/SkipRepeats' 8 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 9 | 10 | const {next, complete} = EVENT 11 | 12 | describe('skipRepeats()', () => { 13 | it('should skip duplicates', () => { 14 | const sh = createTestScheduler() 15 | const $ = sh.Cold([ 16 | next(210, 0), 17 | next(215, 0), 18 | next(220, 10), 19 | next(230, 20), 20 | next(235, 20), 21 | complete(250) 22 | ]) 23 | const {results} = sh.start(() => skipRepeats((a, b) => a === b, $)) 24 | t.deepEqual(results, [ 25 | next(410, 0), 26 | next(420, 10), 27 | next(430, 20), 28 | complete(450) 29 | ]) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/test.Slice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 27/09/16. 3 | */ 4 | 5 | import * as t from 'assert' 6 | import {EVENT} from '../src/internal/Events' 7 | import {slice} from '../src/operators/Slice' 8 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 9 | 10 | const {next, complete} = EVENT 11 | describe('slice()', () => { 12 | it('should take 0 to 3', () => { 13 | const sh = createTestScheduler() 14 | const ob$ = sh.Cold([ 15 | next(0, 1), 16 | next(10, 2), 17 | next(20, 3), 18 | next(30, 4), 19 | next(40, 5), 20 | complete(50) 21 | ]) 22 | const {results} = sh.start(() => slice(0, 3, ob$)) 23 | t.deepEqual(results, [ 24 | next(200, 1), 25 | next(210, 2), 26 | next(220, 3), 27 | complete(220) 28 | ]) 29 | }) 30 | 31 | it('should take 0 to Infinity', () => { 32 | const sh = createTestScheduler() 33 | const ob$ = sh.Cold([ 34 | next(0, 1), 35 | next(10, 2), 36 | next(20, 3), 37 | next(30, 4), 38 | next(40, 5), 39 | complete(50) 40 | ]) 41 | const {results} = sh.start(() => slice(0, Infinity, ob$)) 42 | t.deepEqual(results, [ 43 | next(200, 1), 44 | next(210, 2), 45 | next(220, 3), 46 | next(230, 4), 47 | next(240, 5), 48 | complete(250) 49 | ]) 50 | }) 51 | 52 | it('should take 1 to 3', () => { 53 | const sh = createTestScheduler() 54 | const ob$ = sh.Cold([ 55 | next(0, 1), 56 | next(10, 2), 57 | next(20, 3), 58 | next(30, 4), 59 | next(40, 5), 60 | complete(50) 61 | ]) 62 | const {results} = sh.start(() => slice(1, 3, ob$)) 63 | t.deepEqual(results, [ 64 | next(210, 2), 65 | next(220, 3), 66 | next(230, 4), 67 | complete(230) 68 | ]) 69 | }) 70 | 71 | it('should take 1 to 3 ', () => { 72 | const sh = createTestScheduler() 73 | const ob$ = sh.Hot([ 74 | next(201, 1), 75 | next(210, 2), 76 | next(220, 3), 77 | next(230, 4), 78 | next(240, 5), 79 | complete(250) 80 | ]) 81 | const {results} = sh.start(() => slice(1, 3, ob$)) 82 | t.deepEqual(results, [ 83 | next(210, 2), 84 | next(220, 3), 85 | next(230, 4), 86 | complete(230) 87 | ]) 88 | t.deepEqual(ob$.subscriptions.map(t => t.time), [200, 230]) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /test/test.Subject.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 11/01/18. 3 | */ 4 | import * as assert from 'assert' 5 | import {EVENT} from '../src/internal/Events' 6 | import {ISubscription} from '../src/internal/Subscription' 7 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 8 | import {subject} from '../src/sources/Subject' 9 | 10 | describe('subject()', () => { 11 | it('should be an observable+observer', () => { 12 | const sh = createTestScheduler() 13 | const ob = sh.Observer() 14 | const $ = subject() 15 | let sub: ISubscription 16 | 17 | // Timeline 18 | sh.timeline([ 19 | [210, () => $.next('A')], 20 | [220, () => (sub = $.subscribe(ob, sh))], 21 | [230, () => $.next('B')], 22 | [240, () => $.next('C')], 23 | [250, () => sub.unsubscribe()], 24 | [260, () => $.next('D')], 25 | [270, () => $.complete()] 26 | ]) 27 | 28 | sh.advanceTo(211) 29 | assert.deepEqual(ob.results, []) // nothing gets captured 30 | 31 | sh.advanceTo(241) 32 | assert.deepEqual(ob.results, [EVENT.next(230, 'B'), EVENT.next(240, 'C')]) 33 | 34 | sh.advanceTo(261) 35 | assert.deepEqual(ob.results, [EVENT.next(230, 'B'), EVENT.next(240, 'C')]) 36 | 37 | sh.advanceTo(271) 38 | assert.deepEqual(ob.results, [EVENT.next(230, 'B'), EVENT.next(240, 'C')]) 39 | }) 40 | 41 | it('should be an observable+observer', () => { 42 | const sh = createTestScheduler() 43 | const ob0 = sh.Observer() 44 | const ob1 = sh.Observer() 45 | const $ = subject() 46 | let sb1: ISubscription 47 | let sb0: ISubscription 48 | 49 | // Timeline 50 | sh.timeline([ 51 | [210, () => $.next('A')], 52 | // -- 211 53 | [220, () => (sb0 = $.subscribe(ob0, sh))], 54 | [230, () => $.next('B')], 55 | // -- 231 56 | [240, () => (sb1 = $.subscribe(ob1, sh))], 57 | [250, () => $.next('C')], 58 | // -- 251 59 | [260, () => sb1.unsubscribe()], 60 | [270, () => $.next('D')], 61 | // -- 271 62 | [280, () => sb0.unsubscribe()], 63 | [290, () => $.next('E')] 64 | // -- 291 65 | ]) 66 | 67 | sh.advanceTo(211) 68 | assert.deepEqual(ob0.results, []) 69 | assert.deepEqual(ob1.results, []) 70 | 71 | sh.advanceTo(231) 72 | assert.deepEqual(ob0.results, [EVENT.next(230, 'B')]) 73 | assert.deepEqual(ob1.results, []) 74 | 75 | sh.advanceTo(251) 76 | assert.deepEqual(ob0.results, [EVENT.next(230, 'B'), EVENT.next(250, 'C')]) 77 | assert.deepEqual(ob1.results, [EVENT.next(250, 'C')]) 78 | 79 | sh.advanceTo(271) 80 | assert.deepEqual(ob0.results, [ 81 | EVENT.next(230, 'B'), 82 | EVENT.next(250, 'C'), 83 | EVENT.next(270, 'D') 84 | ]) 85 | assert.deepEqual(ob1.results, [EVENT.next(250, 'C')]) 86 | 87 | sh.advanceTo(291) 88 | assert.deepEqual(ob0.results, [ 89 | EVENT.next(230, 'B'), 90 | EVENT.next(250, 'C'), 91 | EVENT.next(270, 'D') 92 | ]) 93 | assert.deepEqual(ob1.results, [EVENT.next(250, 'C')]) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /test/test.Switch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 16/10/16. 3 | */ 4 | import * as t from 'assert' 5 | import {EVENT} from '../src/internal/Events' 6 | import {switchLatest} from '../src/operators/Switch' 7 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 8 | 9 | describe('switch()', () => { 10 | it('should auto cancel', () => { 11 | const sh = createTestScheduler() 12 | const a$$ = sh.Hot([ 13 | EVENT.next(210, 'A0'), 14 | EVENT.next(220, 'A1'), 15 | EVENT.next(230, 'A2'), 16 | EVENT.next(240, 'A3'), 17 | EVENT.complete(250) 18 | ]) 19 | 20 | const b$$ = sh.Hot([ 21 | EVENT.next(230, 'B0'), 22 | EVENT.next(240, 'B1'), 23 | EVENT.complete(250) 24 | ]) 25 | 26 | const source$ = sh.Hot([ 27 | EVENT.next(205, a$$), 28 | EVENT.next(225, b$$), 29 | EVENT.complete(300) 30 | ]) 31 | const {results} = sh.start(() => switchLatest(source$)) 32 | t.deepEqual(results, [ 33 | EVENT.next(210, 'A0'), 34 | EVENT.next(220, 'A1'), 35 | EVENT.next(230, 'B0'), 36 | EVENT.next(240, 'B1'), 37 | EVENT.complete(300) 38 | ]) 39 | t.deepEqual(a$$.subscriptions.map(r => r.time), [205, 225]) 40 | t.deepEqual(b$$.subscriptions.map(r => r.time), [225, 300]) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/test.TestScheduler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 28/01/17. 3 | */ 4 | import * as assert from 'assert' 5 | import {EVENT} from '../src/internal/Events' 6 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 7 | 8 | describe('new TestScheduler()', () => { 9 | describe('now()', () => { 10 | it('should return current time', () => { 11 | const sh = createTestScheduler() 12 | 13 | assert.strictEqual(sh.now(), 0) 14 | 15 | sh.advanceBy(10) 16 | assert.strictEqual(sh.now(), 10) 17 | 18 | sh.advanceBy(2) 19 | assert.strictEqual(sh.now(), 12) 20 | 21 | sh.advanceTo(20) 22 | assert.strictEqual(sh.now(), 20) 23 | }) 24 | }) 25 | 26 | describe('start()', () => { 27 | it('should return TestObserver', () => { 28 | const sh = createTestScheduler() 29 | const {results} = sh.start(() => 30 | sh.Hot([ 31 | EVENT.next(210, '0'), 32 | EVENT.next(220, '1'), 33 | EVENT.next(230, '2'), 34 | EVENT.complete(240) 35 | ]) 36 | ) 37 | 38 | assert.deepEqual(results, [ 39 | EVENT.next(210, '0'), 40 | EVENT.next(220, '1'), 41 | EVENT.next(230, '2'), 42 | EVENT.complete(240) 43 | ]) 44 | }) 45 | }) 46 | 47 | describe('Hot()', () => { 48 | it('should create events ', () => { 49 | const sh = createTestScheduler() 50 | const {results} = sh.start(() => sh.Hot(EVENT.next(210, '0'))) 51 | assert.deepEqual(results, [EVENT.next(210, '0')]) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/test.Unique.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar on 14/09/17. 3 | */ 4 | import * as assert from 'assert' 5 | import {unique} from '../src/operators/Unique' 6 | import {createTestScheduler} from '../src/schedulers/TestScheduler' 7 | 8 | describe('unique', () => { 9 | it('should return emit unique value', () => { 10 | const sh = createTestScheduler() 11 | const source = sh.Hot('-a-b-c-a--b-|') 12 | const observer = sh.start(() => unique(source)) 13 | assert.strictEqual(observer.toString(), '-a-b-c------|') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/test.Utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tushar.mathur on 15/10/16. 3 | */ 4 | import * as t from 'assert' 5 | import {curry} from '../src/internal/Utils' 6 | 7 | const func = curry((a: number, b: number, c: number) => [a, b, c]) 8 | 9 | describe('utils', () => { 10 | describe('curry', () => { 11 | it('should fn(1, 2, 3)', () => { 12 | t.deepEqual(func(1, 2, 3), [1, 2, 3]) 13 | }) 14 | 15 | it('should fn(1, 2)(3)', () => { 16 | const func2 = func(1, 2) 17 | t.deepEqual(func2(3), [1, 2, 3]) 18 | }) 19 | 20 | it('should fn(1)(2)(3)', () => { 21 | t.deepEqual(func(1)(2)(3), [1, 2, 3]) 22 | }) 23 | 24 | it('should fn(1, 2, 3, 4)', () => { 25 | t.deepEqual(func(1, 2, 3, 4), [1, 2, 3]) 26 | }) 27 | 28 | it('should fn()', () => { 29 | const f = curry(() => 'HELLO') 30 | t.strictEqual(f(), 'HELLO') 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "sourceMap": true, 7 | "strictNullChecks": true, 8 | "removeComments": true, 9 | "noUnusedLocals": true, 10 | "declaration": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsRules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-trailing-whitespace": true, 15 | "no-unsafe-finally": true, 16 | "one-line": [ 17 | true, 18 | "check-open-brace", 19 | "check-whitespace" 20 | ], 21 | "quotemark": [ 22 | true, 23 | "single" 24 | ], 25 | "semicolon": [ 26 | true, 27 | "never" 28 | ], 29 | "triple-equals": [ 30 | true, 31 | "allow-null-check" 32 | ], 33 | "variable-name": [ 34 | true, 35 | "ban-keywords" 36 | ], 37 | "whitespace": [ 38 | true, 39 | "check-branch", 40 | "check-decl", 41 | "check-operator", 42 | "check-separator", 43 | "check-type" 44 | ] 45 | }, 46 | "rules": { 47 | "class-name": true, 48 | "comment-format": [ 49 | true, 50 | "check-space" 51 | ], 52 | "indent": [ 53 | true, 54 | "spaces" 55 | ], 56 | "no-eval": true, 57 | "no-internal-module": true, 58 | "no-trailing-whitespace": true, 59 | "no-unsafe-finally": true, 60 | "no-var-keyword": false, 61 | "one-line": [ 62 | true, 63 | "check-open-brace", 64 | "check-whitespace" 65 | ], 66 | "quotemark": [ 67 | true, 68 | "single" 69 | ], 70 | "semicolon": [ 71 | true, 72 | "never" 73 | ], 74 | "triple-equals": [ 75 | true, 76 | "allow-null-check" 77 | ], 78 | "typedef-whitespace": [ 79 | true, 80 | { 81 | "call-signature": "nospace", 82 | "index-signature": "nospace", 83 | "parameter": "nospace", 84 | "property-declaration": "nospace", 85 | "variable-declaration": "nospace" 86 | } 87 | ], 88 | "variable-name": [ 89 | true, 90 | "ban-keywords" 91 | ], 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | "jsdoc-format": true, 101 | "ordered-imports": true 102 | } 103 | } 104 | --------------------------------------------------------------------------------