├── 10 ├── index.ts └── readme.md ├── 11 ├── index.ts └── readme.md ├── 12 ├── index.ts ├── readme.md └── redux.ts ├── 13 ├── index.ts └── readme.md ├── 14 ├── index.ts └── readme.md ├── 15 ├── index.ts └── readme.md ├── 16 ├── index.ts └── readme.md ├── 17 ├── index.ts └── readme.md ├── 18 ├── index.ts └── readme.md ├── 19 ├── index.ts └── readme.md ├── 20 ├── index.ts └── readme.md ├── 21 ├── index.ts └── readme.md ├── 22 ├── index.ts └── readme.md ├── .editorconfig ├── .gitignore ├── 01 ├── index.ts └── readme.md ├── 02 ├── index.ts ├── raf.d.ts └── readme.md ├── 03 ├── index.ts └── readme.md ├── 04 ├── index.ts └── readme.md ├── 05 ├── index.ts └── readme.md ├── 06 ├── index.ts └── readme.md ├── 07 ├── index.ts └── readme.md ├── 08 ├── index.ts └── readme.md ├── 09 ├── index.ts └── readme.md ├── LICENSE ├── package-lock.json ├── package.json ├── readme.md └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | indent_style = space 7 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /01/index.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | 3 | let n = 0; 4 | let id: NodeJS.Timeout = null; 5 | 6 | // Create an Observable from scratch 7 | const number$ = new Observable(observer => { 8 | id = setInterval(() => { 9 | // Counter to run 5 times 10 | if (n++ < 5) { 11 | observer.next(n); 12 | } else { 13 | // When finished, clear the interval and complete the stream 14 | observer.complete(); 15 | } 16 | }, 1000); 17 | 18 | return () => clearInterval(id); 19 | }); 20 | 21 | // Handle only the next and completion handkers 22 | const observer = { 23 | next: (x: number) => { 24 | console.log(`Next: ${x}`); 25 | }, 26 | complete: () => { 27 | console.log(`Complete!`); 28 | } 29 | }; 30 | 31 | // Subscribe to the Observable with our obsercer 32 | const subscription = number$.subscribe(observer); 33 | 34 | // Terminate the sequence early if you want to 35 | // setTimeout(() => subscription.unsubscribe(), 2500); 36 | -------------------------------------------------------------------------------- /01/readme.md: -------------------------------------------------------------------------------- 1 | # Day 1 - Creating an Observable 2 | 3 | The first entry for our calendar is creating an Observable. 4 | 5 | ## What is this RxJS stuff? 6 | 7 | What is an Observable? Simply put, it is a push-based event emitter which can push 0 to infinite amounts of data. 8 | 9 | Next, we will create an Observer, which is a sink that an Observable push data into when you want to listen for the data in the cases of more data via `next`, and error occurred via `error`, or the stream has completed via `complete`. 10 | 11 | Finally, we can hook it all together with subscribing the observer to the observable. This subscription process by calling `subscribe` on the Observable, passing in our Observer. This returns a Subscription which contains the teardown logic required to clean up any resources created by the Observable stream. 12 | 13 | ## Walking through the code 14 | 15 | Let's walk through the [first example](index.ts), creating an Observable. First, we need to import RxJS, only getting the Observable that is required. 16 | 17 | ```typescript 18 | import { Observable } from 'rxjs'; 19 | ``` 20 | 21 | Creating an Observable gives you a sense of the Observable lifecycle. When you create a new Observable, you pass in an Observer which you can emit data to at any point, 0 to infinite number of times `next` can be called, followed by an optional `error` or `complete` call. Finally, when we create our Observable, we also include any teardown logic such as clearing a timeout or interval, or cancelling an incomplete network request. 22 | 23 | ```typescript 24 | const source$ = new Observable(observer => { 25 | observer.next(42); 26 | observer.complete(); 27 | 28 | return function teardown () { console.log('Teardown'); }; 29 | }); 30 | ``` 31 | 32 | Let's go more advanced with a timer based approach using `setInterval` to emit values every second, and then the teardown will cancel via `clearInterval`. This example will run 5 times and then complete the stream. We return a teardown function which calls `clearInterval` on our given `id` so that we can stop the intervals. 33 | 34 | ```typescript 35 | let n = 0; 36 | let id = 0; 37 | 38 | // Create an Observable from scratch 39 | const number$ = new Observable(observer => { 40 | id = setInterval(() => { 41 | // Counter to run 5 times 42 | if (n++ < 5) { 43 | observer.next(n); 44 | } else { 45 | // When finished, clear the interval and complete the stream 46 | observer.complete(); 47 | } 48 | }, 1000); 49 | 50 | return () => clearInterval(id); 51 | }); 52 | ``` 53 | 54 | Next, we can create an observer or data sink to emit data values into at any point. Remember from above, we have three choices, 0 to infinite data via `next`, and handling errors via `error` and handling completion via `complete`. In this case, we are not worried about errors, so we will only implement `next` and `complete`. 55 | 56 | ```typescript 57 | // Handle only the next and completion handlers 58 | const observer = { 59 | next: (x: number) => { 60 | console.log(`Next: ${x}`); 61 | }, 62 | complete: () => { 63 | console.log(`Complete!`); 64 | } 65 | }; 66 | ``` 67 | 68 | Finally, we can tie it all together, calling `subscribe` on the Observable with our Observer instance. This will return our subscription that we can unsubscribe from at any time. Note that since the inner observer calls `complete`, the teardown logic will happen automatically and the subscription automatically unsubscribes. 69 | 70 | ```typescript 71 | // Subscribe to the Observable with our observer 72 | const subscription = number$.subscribe(observer); 73 | ``` 74 | 75 | This then emits the following data: 76 | ```bash 77 | $ npx ts-node 01/index.ts 78 | Next: 1 79 | Next: 2 80 | Next: 3 81 | Next: 4 82 | Next: 5 83 | Complete! 84 | ``` 85 | 86 | We can terminate this sequence early, however, since it will take 5 seconds to complete, we could cancel after 2.5 seconds. 87 | 88 | ```typescript 89 | // Subscribe to the Observable with our observer 90 | const subscription = number$.subscribe(observer); 91 | 92 | // Terminate the sequence after 2.5 seconds 93 | setTimeout(() => subscription.unsubscribe(), 2500); 94 | ``` 95 | 96 | This then changes the output to the following: 97 | ```bash 98 | $ npx ts-node 01/index.ts 99 | Next: 1 100 | Next: 2 101 | ``` 102 | 103 | I hope you learned the basics as we start our journey through the advent of RxJS! 104 | -------------------------------------------------------------------------------- /02/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { 4 | Observable, 5 | SchedulerLike, 6 | animationFrameScheduler 7 | } from 'rxjs'; 8 | 9 | // Polyfill requestAnimationFrame 10 | import * as raf from 'raf'; 11 | raf.polyfill(); 12 | 13 | function once(value: T, scheduler: SchedulerLike) { 14 | return new Observable(observer => { 15 | return scheduler.schedule(() => { 16 | observer.next(value); 17 | observer.complete(); 18 | }); 19 | }); 20 | } 21 | 22 | const subscription = once(42, animationFrameScheduler) 23 | .subscribe({ 24 | next:x => console.log(`Next: ${x}`), 25 | complete: () => console.log(`Complete!`) 26 | }); 27 | 28 | // If we really don't want it to happen 29 | //subscription.unsubscribe(); 30 | -------------------------------------------------------------------------------- /02/raf.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'raf'; 2 | -------------------------------------------------------------------------------- /02/readme.md: -------------------------------------------------------------------------------- 1 | # Day 2 - Sequences over time 2 | 3 | In the [previous day](../01/readme.md), we looked at the basics of creating Observables and the parts required such as Observers and Subscriptions. There is another aspect that is missing from the initial equation which is that Observables are push based sequences over time, noting that last part, time. RxJS was designed with the notion of virtual time and a virtual clock which means you can say when a particular action happens. This concept is introduced with the idea of Schedulers, which not only controls when a particular action happens, but also the context of an action as well. 4 | 5 | Let's make this a little bit more concrete. Let's say that we want to use `requestAnimationFrame` to schedule our next actions. We could usually do something like the following where we schedule an action using `requestAnimationFrame`, and as part of the teardown, we have `cancelAnimationFrame` to clean up any resources or if we want to immediately cancel the item. 6 | 7 | ```typescript 8 | import { Observable } from 'rxjs'; 9 | 10 | const number$ = new Observable(observer => { 11 | let id = requestAnimationFrame(() => { 12 | observer.next(42); 13 | observer.complete(); 14 | }); 15 | 16 | return () => cancelAnimationFrame(id); 17 | }); 18 | ``` 19 | 20 | This, of course, is a nice to have, but it's not super testable in a way we could use an abstraction like a Scheduler which would allow us to swap out `requestAnimationFrame` for a virtual time stamp for testing or historical data processing. Instead, let's look at how we could do it better. 21 | 22 | In RxJS, we have two interfaces that are required for any emitting of data, a `Scheduler`, and a `SchedulerAction`. The scheduler schedules the unit of work called the `SchedulerAction` and returns a `Subscription` which like for our Observable, allows us to tear down the work at any point. 23 | 24 | ```typescript 25 | export interface SchedulerLike { 26 | now(): number; 27 | schedule(work: (this: SchedulerAction, state?: T) => void, delay?: number, state?: T): Subscription; 28 | } 29 | 30 | export interface SchedulerAction extends Subscription { 31 | schedule(state?: T, delay?: number): Subscription; 32 | } 33 | ``` 34 | 35 | Since our work is rather easy, we can skip a lot of the overhead and implement our new item like the following just using a function and not caring about any unit of work `SchedulerAction`. RxJS has a number of built-in schedulers so we don't need to implement our own for most work we need. 36 | 37 | The following schedulers are available to you from RxJS: 38 | - `animationFrameScheduler` - Schedule an async action using `requestAnimationFrame` 39 | - `asapScheduler` - Schedule an async action as soon as possible such as using `setImmediate` 40 | - `asyncScheduler` - Schedule an async action using `setInterval`/`clearInterval` 41 | - `VirtualTimeScheduler` - define your own time semantics in virtual time 42 | 43 | ```typescript 44 | import { 45 | Observable, 46 | SchedulerLike, 47 | animationFrameScheduler 48 | } from 'rxjs'; 49 | 50 | // Polyfill requestAnimationFrame 51 | import * as raf from 'raf'; 52 | raf.polyfill(); 53 | 54 | function once(value: T, scheduler: SchedulerLike) { 55 | return new Observable(observer => { 56 | return scheduler.schedule(() => { 57 | observer.next(value); 58 | observer.complete(); 59 | }); 60 | }); 61 | } 62 | 63 | const subscription = once(42, animationFrameScheduler) 64 | .subscribe({ 65 | next:x => console.log(`Next: ${x}`), 66 | complete: () => console.log(`Complete!`) 67 | }); 68 | ``` 69 | 70 | Running this, we get the following: 71 | ```bash 72 | $ npx ts-node 02/index.ts 73 | Next: 42 74 | Complete! 75 | ``` 76 | 77 | Since this action is async by nature, we can preempt this call by immediately calling `subscription.unsubscribe()` if you so desire. Join us next time as we get into easier ways of creating observables rather than by hand! 78 | -------------------------------------------------------------------------------- /03/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | asapScheduler, 3 | of, 4 | range, 5 | generate 6 | } from 'rxjs'; 7 | 8 | // Using no scheduler 9 | const source1$ = of(1, 2, 3); 10 | 11 | // Using the ASAP scheduler 12 | const source2$ = of(1, 2, 3, asapScheduler); 13 | 14 | const observer = (id: number) => { 15 | return { 16 | next: (x: number) => console.log(`ID ${id} Next: ${x}`), 17 | complete: () => console.log(`ID ${id} Complete!`) 18 | }; 19 | } 20 | 21 | // const subscription1 = source1$.subscribe(observer(1)); 22 | // const subscription2 = source2$.subscribe(observer(2)); 23 | 24 | // Using no scheduler 25 | const range1$ = range(0, 3); 26 | 27 | // Using the ASAP scheduler 28 | const range2$ = range(3, 3); 29 | 30 | // const subscription3 = range1$.subscribe(observer(1)); 31 | // const subscription4 = range2$.subscribe(observer(2)); 32 | 33 | // Create generated sequence 34 | const number$ = generate( 35 | 43, 36 | i => i < 46, 37 | i => i + 1, 38 | i => String.fromCharCode(i) 39 | ); 40 | 41 | const subscription = number$.subscribe({ 42 | next: x => console.log(`Next: ${x}`), 43 | complete: () => console.log(`Complete!`) 44 | }); -------------------------------------------------------------------------------- /03/readme.md: -------------------------------------------------------------------------------- 1 | # Day 3 - Creating Observables the easy way! 2 | 3 | In the [previous entry](../02/readme.md), we covered a bit more about Observable creation, but once again we were in the weeds showing how the magic is made. Now that you understand the basics, let's make it easy for you going forward. Built into RxJS are a number of predefined ways of creating Observables such as: 4 | 5 | Conversions: 6 | - `bindCallback` - binds a callback to an Observable 7 | - `bindNodeCallback` - binds a Node.js style error callback to an Observable 8 | - `from` - converts a number of structures to an Observable such as Arrays, Promises, Observables, Iterables, etc. 9 | - `fromEvent`/`fromEventPattern` - converts DOM events and `EventEmitter` 10 | 11 | Creation: 12 | - `generate` - creates an Observable based upon a for loop behavior 13 | - `empty` - creates an Observable that emits only a `complete` 14 | - `interval` - creates an Observable that emits on the given time sequence 15 | - `never` - creates an Observable that never emits a value 16 | - `of` - creates an Observable from a list of arguments 17 | - `range` - creates an Observable from a range of numbers 18 | - `throwError` - creates an error Observable which throws the given error 19 | - `timer` - creates an Observable that emits at the first time, and repeats on the period time if given 20 | 21 | Today, we're going to talk a little bit about the creation operations since they are the easiest way to get started. 22 | 23 | ### The of operator 24 | 25 | The most common used way of creating Observables is via the `of` factory. This takes a number of arguments, followed by an optional Scheduler of your choosing which determines how the values are emitted. 26 | 27 | ```typescript 28 | import { 29 | asapScheduler, 30 | of 31 | } from 'rxjs'; 32 | 33 | // Using the default scheduler 34 | const source1$ = of(1, 2, 3); 35 | 36 | // Using the ASAP scheduler 37 | const source2$ = of(4, 5, 6, asapScheduler); 38 | ``` 39 | 40 | We can then listen for the results with a nice tracer for which sequence is which: 41 | ```typescript 42 | const observer = (id: number) => { 43 | return { 44 | next: (x: number) => console.log(`ID ${id} Next: ${x}`), 45 | complete: () => console.log(`ID ${id} Complete!`) 46 | }; 47 | } 48 | 49 | const subscription1 = source1$.subscribe(observer(1)); 50 | const subscription2 = source2$.subscribe(observer(2)); 51 | ``` 52 | 53 | Running that via `ts-node` gives us the following: 54 | ```bash 55 | $ npx ts-node 03/index.ts 56 | ID 1 Next: 1 57 | ID 1 Next: 2 58 | ID 1 Next: 3 59 | ID 1 Complete! 60 | ID 2 Next: 4 61 | ID 2 Next: 5 62 | ID 2 Next: 6 63 | ID 2 Complete! 64 | ``` 65 | 66 | # The range operator 67 | 68 | Another way of creating Observables is via the `range` operator which creates a sequence of numbers with a start value, a number of elements to produce, and an optional scheduler once again to control the concurrency. You'll notice a common theme here that all creation operations take in an optional scheduler. This is for testing purposes but also allows you to control the concurrency and where it runs. 69 | 70 | ```typescript 71 | import { 72 | asapScheduler, 73 | range 74 | } from 'rxjs'; 75 | 76 | // Without a scheduler 77 | const number1$ = range(0, 3); 78 | 79 | // With the asap scheduler 80 | const number2$ = range(3, 3, asapScheduler); 81 | ``` 82 | 83 | Using the above subscription techniques to track which one we're listening to, let's run through the values. 84 | 85 | ```bash 86 | $ npx ts-node 03/index.ts 87 | ID 1 Next: 0 88 | ID 1 Next: 1 89 | ID 1 Next: 2 90 | ID 1 Complete! 91 | ID 2 Next: 3 92 | ID 2 Next: 4 93 | ID 2 Next: 5 94 | ID 2 Complete! 95 | ``` 96 | 97 | # The generate operator 98 | 99 | Finally, let's cover one last creation operation, the `generate` function. This acts as your standard for loop like this: 100 | ```typescript 101 | for (let i = 0; /* initial value */ 102 | i < 10; /* condition */ 103 | i++ /* increment */) { 104 | // Do something with the i value 105 | } 106 | ``` 107 | 108 | In the same way, our `generate` function acts the same way with the following arguments: 109 | ```typescript 110 | generate( 111 | initialValue: Source, 112 | condition: (value: Source) => bool 113 | iterate: (value: Source) => Source, 114 | resultSelector: (value: Source) => Result 115 | ); 116 | ``` 117 | 118 | Using this knowledge, we can easily emit three values such as: 119 | ```typescript 120 | import { 121 | generate 122 | } from 'rxjs'; 123 | 124 | const number$ = generate( 125 | 43, 126 | i => i < 46, 127 | i => i + 1, 128 | i => String.fromCharCode(i) 129 | ); 130 | 131 | const subscription = number$.subscribe({ 132 | next: x => console.log(`Next: ${x}`), 133 | complete: () => console.log(`Complete!`) 134 | }); 135 | ``` 136 | 137 | Running our solution now will look like this where we transformed our numbers into characters based upon their character code: 138 | ```bash 139 | $ npx ts-node 03/index.ts 140 | Next: + 141 | Next: , 142 | Next: - 143 | Complete! 144 | ``` 145 | 146 | Stay tuned to tomorrow's session where we'll cover more about creation, such as values over time and conversions! 147 | -------------------------------------------------------------------------------- /04/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import { 3 | timer 4 | } from 'rxjs'; 5 | 6 | // Schedule something once after half a second 7 | const delay = 500; 8 | const poll$ = timer(delay); 9 | 10 | poll$.subscribe({ 11 | next: (x: number) => console.log(`Delayed by ${delay}ms item: ${x}`), 12 | complete: () => console.log('Delayed complete!') 13 | }); 14 | */ 15 | 16 | /* 17 | import { 18 | timer 19 | } from 'rxjs'; 20 | 21 | // Schedule something once after half a second 22 | const delay = 500; 23 | const period = 1000; 24 | const poll$ = timer(delay, period); 25 | 26 | const subscription = poll$.subscribe({ 27 | next: (x: number) => console.log(`Delayed by ${delay}ms item: ${x}`), 28 | complete: () => console.log('Delayed complete!') 29 | }); 30 | 31 | // Unsubscribe after 4 seconds 32 | setTimeout(() => subscription.unsubscribe(), 4000); 33 | */ 34 | 35 | /* 36 | import { timer } from 'rxjs'; 37 | import { take } from 'rxjs/operators'; 38 | 39 | // Schedule something once after half a second 40 | const delay = 500; 41 | const period = 1000; 42 | const poll$ = timer(delay, period); 43 | 44 | poll$ 45 | .pipe(take(4)) 46 | .subscribe({ 47 | next: (x: number) => console.log(`Delayed by ${delay}ms item: ${x}`), 48 | complete: () => console.log('Delayed complete!') 49 | }); 50 | */ 51 | 52 | import { interval } from 'rxjs'; 53 | import { take } from 'rxjs/operators'; 54 | 55 | // Schedule something once after half a second 56 | const period = 1000; 57 | const poll$ = interval(period); 58 | 59 | poll$ 60 | .pipe(take(4)) 61 | .subscribe({ 62 | next: (x: number) => console.log(`Delayed by ${period}ms item: ${x}`), 63 | complete: () => console.log('Delayed complete!') 64 | }); -------------------------------------------------------------------------------- /04/readme.md: -------------------------------------------------------------------------------- 1 | # Day 4 - Creating delayed and polling operations 2 | 3 | In the [previous entry](../03/readme.md), we covered some of the creation and conversion operators. We will get to the conversion ones quickly, but today I wanted to cover two quick creation operations for creating a polling mechanism in RxJS, with both `timer` and `interval`. These are good operations for example long polling, or scheduling work items in the future. If you want, for example, a game play loop, `interval` or `timer` can be used in this scenario. 4 | 5 | # The timer operation 6 | 7 | The first operation we'll look at is the `timer` operation. This has two functions really, a way of scheduling something in the future, or a way of scheduling something in the future with a recurring period. Note that as always with creation operations, it allows for your own scheduler implementation. 8 | 9 | ```typescript 10 | timer(initial: number, scheduler?: SchedulerLike): Observable; 11 | timer(initial: number, period: number, scheduler?: SchedulerLike): Observable; 12 | ``` 13 | 14 | In this first example, we will schedule an item to occur in half a second from now. 15 | 16 | ```typescript 17 | import { 18 | timer 19 | } from 'rxjs'; 20 | 21 | // Schedule something once after half a second 22 | const delay = 500 /* ms */; 23 | const poll$ = timer(delay); 24 | 25 | poll$.subscribe({ 26 | next: (x: number) => console.log(`Delayed by ${delay}ms item: ${x}`), 27 | complete: () => console.log('Delayed complete!') 28 | }); 29 | ``` 30 | 31 | Running this via `ts-node` gives us the following where it shows the first value is 0, and then it completes the stream. 32 | 33 | ```bash 34 | $ npx ts-node 04/index.ts 35 | Delayed by 500ms item: 0 36 | Delayed complete! 37 | ``` 38 | 39 | The next example, we will schedule an item a half second from now, and then to run at one second intervals. Note that this operation will run infinitely, so we can truncate by unsubscribing at any point, or a simple operator we'll talk about later called `take`. 40 | 41 | ```typescript 42 | import { 43 | timer 44 | } from 'rxjs'; 45 | 46 | // Schedule something once after half a second 47 | const delay = 500 /* ms */; 48 | const period = 1000 /* ms */; 49 | const poll$ = timer(delay, period); 50 | 51 | const subscription = poll$.subscribe({ 52 | next: (x: number) => console.log(`Delayed by ${delay}ms item: ${x}`), 53 | complete: () => console.log('Delayed complete!') 54 | }); 55 | 56 | // Unsubscribe after 4 seconds 57 | setTimeout(() => subscription.unsubscribe(), 4000); 58 | ``` 59 | 60 | Running this gives us the following result which truncates after 4 seconds. 61 | ```bash 62 | $ npx ts-node 04/index.ts 63 | Delayed by 500ms item: 0 64 | Delayed by 500ms item: 1 65 | Delayed by 500ms item: 2 66 | Delayed by 500ms item: 3 67 | ``` 68 | 69 | Instead of this magic unsubscription, we can simply use the `take` operator that we will cover later in our limiting Observables part and composed via the `pipe` operator. 70 | 71 | ```typescript 72 | import { timer } from 'rxjs'; 73 | import { take } from 'rxjs/operators'; 74 | 75 | // Schedule something once after half a second 76 | const delay = 500; 77 | const period = 1000; 78 | const poll$ = timer(delay, period); 79 | 80 | poll$ 81 | .pipe(take(4)) 82 | .subscribe({ 83 | next: (x: number) => console.log(`Delayed by ${delay}ms item: ${x}`), 84 | complete: () => console.log('Delayed complete!') 85 | }); 86 | ``` 87 | 88 | Running this now gives us the following, and note that this sequence does terminate with a `complete` call unlike the `unsubscribe()` call we did above. 89 | ```bash 90 | $ npx ts-node 04/index.ts 91 | Delayed by 500ms item: 0 92 | Delayed by 500ms item: 1 93 | Delayed by 500ms item: 2 94 | Delayed by 500ms item: 3 95 | Delayed complete! 96 | ``` 97 | 98 | # The interval operation 99 | 100 | The `interval` creation operation is quite straightforward to create an interval at a specified period of time. 101 | 102 | ```typescript 103 | interval(period: number, scheduler?: SchedulerLike): Observable; 104 | ``` 105 | 106 | In fact, you could easily write `interval` just by using `timer` for example `timer(1000, 1000)` is the same as `interval(1000)`. We can run through the same exercise as above but instead of starting at half a second, this starts at a one second delay. 107 | 108 | ```typescript 109 | import { interval } from 'rxjs'; 110 | import { take } from 'rxjs/operators'; 111 | 112 | // Schedule something once after half a second 113 | const period = 1000; 114 | const poll$ = interval(period); 115 | 116 | poll$ 117 | .pipe(take(4)) 118 | .subscribe({ 119 | next: (x: number) => console.log(`Delayed by ${period}ms item: ${x}`), 120 | complete: () => console.log('Delayed complete!') 121 | }); 122 | ``` 123 | 124 | Running our example once again, we can take 4 items and then truncate the observable at that point. 125 | 126 | ```bash 127 | $ npx ts-node 04/index.ts 128 | Delayed by 1000ms item: 0 129 | Delayed by 1000ms item: 1 130 | Delayed by 1000ms item: 2 131 | Delayed by 1000ms item: 3 132 | Delayed complete! 133 | ``` 134 | 135 | This should start to give you some ideas on the power of RxJS and the tools at your disposal to create really interesting games and other scenarios. Stay tuned! 136 | -------------------------------------------------------------------------------- /05/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | from, 3 | asapScheduler, 4 | Observable 5 | } from 'rxjs'; 6 | import { map } from 'rxjs/operators'; 7 | 8 | /* 9 | const set = new Set([1, 2, 3]); 10 | const set$ = from(set); 11 | 12 | const subscription = set$.subscribe({ 13 | next: x => console.log(`Next: ${x}`), 14 | complete: () => console.log(`Complete!`) 15 | }); 16 | */ 17 | 18 | /* 19 | const arrayLike = { length: 3 }; 20 | const array$ = from(arrayLike) 21 | .pipe(map((_, i) => i)); 22 | 23 | const subscription = array$.subscribe({ 24 | next: x => console.log(`Next: ${x}`), 25 | complete: () => console.log(`Complete!`) 26 | }); 27 | 28 | */ 29 | 30 | /* 31 | const array = [1, 2, 3]; 32 | const array$ = from (array, asapScheduler); 33 | array$.subscribe({ 34 | next: x => console.log(`Next: ${x}`), 35 | complete: () => console.log('Complete') 36 | }); 37 | */ 38 | 39 | const iterable = function* () { 40 | yield 1; 41 | yield 2; 42 | yield 3; 43 | }; 44 | 45 | const iterable$ = from(iterable(), asapScheduler); 46 | iterable$.subscribe({ 47 | next: x => console.log(`Next: ${x}`), 48 | complete: () => console.log('Complete') 49 | }); 50 | 51 | /* 52 | const promise = Promise.resolve(42); 53 | const promise$ = from(promise); 54 | promise$.subscribe({ 55 | next: x => console.log(`Next: ${x}`), 56 | complete: () => console.log('Complete') 57 | }); 58 | */ 59 | 60 | /* 61 | const obs$ = new Observable(observer => { 62 | observer.next(42); 63 | observer.complete(); 64 | }); 65 | obs$.subscribe({ 66 | next: x => console.log(`Next: ${x}`), 67 | complete: () => console.log('Complete') 68 | }); 69 | */ -------------------------------------------------------------------------------- /05/readme.md: -------------------------------------------------------------------------------- 1 | # Day 5 - Converting to Observables 2 | 3 | In the [previous entry](../04/readme.md), we covered creating polling and timed operations via `timer` and `interval`. Today, we're still covering Observable creation, this time, by converting existing data structures to Observables via the `from` operation. This `from` operation matches the [`Array.from`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) method which was new as of ES2015. 4 | 5 | ## Using the from operation 6 | 7 | Pretty much everything can be turned into an Observable with RxJS using the `from` creation operation. It can convert an `Array`, an `Iterable` such as a `Generator`, `Set`, or a `Map`, a `Promise`, and even another `Observable`. Like all other creation operations, this also takes a `SchedulerLike` object used for scheduling the operation. By default, if not provided, the `from` operation will perform things synchronously. 8 | 9 | Internally, RxJS uses a number of operations to convert our incoming data such as: 10 | - `fromArray` - converts an array 11 | - `fromIterable` - Converts a sequence which exposes the `Symbol.iterator` property 12 | - `fromObservable` - Converts an Observable which exposes the `Symbol.observable` property 13 | - `fromPromise` - converts a `Promise` 14 | 15 | ## Converting an Array to an Observable 16 | 17 | The first conversion we can do is from an `Array` or array-like structure, meaning it has a length property. Let's first convert using just an array. 18 | 19 | ```typescript 20 | import { from } from 'rxjs'; 21 | 22 | const array = [1, 2, 3]; 23 | const array$ = from(array); 24 | 25 | const subscription = array$.subscribe({ 26 | next: x => console.log(`Next: ${x}`), 27 | complete: () => console.log(`Complete!`) 28 | }); 29 | ``` 30 | 31 | Running this gives us the following results: 32 | ```bash 33 | $ npx ts-node 05/index.ts 34 | Next: 1 35 | Next: 2 36 | Next: 3 37 | Complete 38 | ``` 39 | 40 | We can also run it with an array-like structure as well with a length property of 3. 41 | 42 | ```typescript 43 | const arrayLike = { length: 3 }; 44 | const array$ = from(arrayLike); 45 | 46 | array$.subscribe({ 47 | next: x => console.log(`Next: ${x}`), 48 | complete: () => console.log('Complete') 49 | }); 50 | ``` 51 | 52 | Running this gives particular version gives us the following results with `undefined` everywhere. 53 | 54 | ```bash 55 | $ npx ts-node 05/index.ts 56 | Next: undefined 57 | Next: undefined 58 | Next: undefined 59 | Complete 60 | ``` 61 | 62 | Unfortunately, in later versions, the result selector function was removed from the `from` operation, but we can easily get this back via using the `map` operator which we will cover later. The `map` operator simply takes the current value and allows you to project a new value in the stream in its place. In this instance, we're more concerned with the index argument from the selector which will give us our unique place. 63 | 64 | ```typescript 65 | import { from } from 'rxjs'; 66 | import { map } from 'rxjs/operators'; 67 | 68 | const array = [1, 2, 3]; 69 | const array$ = from(array) 70 | .pipe(map((_, i) => i)); 71 | 72 | const subscription = array$.subscribe({ 73 | next: x => console.log(`Next: ${x}`), 74 | complete: () => console.log(`Complete!`) 75 | }); 76 | ``` 77 | 78 | Now, since we're projecting the index, we now have some data to display. 79 | ```bash 80 | $ npx ts-node 05/index.ts 81 | Next: 0 82 | Next: 1 83 | Next: 2 84 | Complete! 85 | ``` 86 | 87 | ## Converting an Iterable to an Observable 88 | 89 | With ES2015 came the advent of [Iterables, Iterators, and Generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators). This gave us a way to iterate over existing sequences such as arrays, but also new structures such as `Map` and `Set`. Each of these objects now implement the `Symbol.iterator` property which returns an `Iterator` object. This `Iterator` object has three methods which correspond directly to our `Observer` with `next()`, `throw()` and `return()` and its Observer equivalents with `next()`, `error()`, and `complete()`. When `next()` is called on an iterator, it returns a structure with both a property of the current value, if any, and a done flag indicating whether the sequence has been iterated. Generators also implement this `Symbol.iterator` property which allows us to create infinite sequences if we so choose. 90 | 91 | We can for example, create an Observable from a `Set` with a few values. 92 | ```typescript 93 | const set = new Set([1, 2, 3]); 94 | const set$ = from(set); 95 | 96 | const subscription = set$.subscribe({ 97 | next: x => console.log(`Next: ${x}`), 98 | complete: () => console.log(`Complete!`) 99 | }); 100 | ``` 101 | 102 | And when we run the example, we get the following output from the sequence. 103 | ```bash 104 | $ npx ts-node 05/index.ts 105 | Next: 1 106 | Next: 2 107 | Next: 3 108 | Complete! 109 | ``` 110 | 111 | We can also create a `Generator` function which yields values as well via the `yield` keyword. 112 | 113 | ```typescript 114 | const generatorFunction = function* () { 115 | yield 1; 116 | yield 2; 117 | yield 3; 118 | }; 119 | 120 | const iterable$ = from(generatorFunction(), asapScheduler); 121 | iterable$.subscribe({ 122 | next: x => console.log(`Next: ${x}`), 123 | complete: () => console.log('Complete') 124 | }); 125 | ``` 126 | 127 | And just as above, we get the results of 1, 2, 3. 128 | ```bash 129 | $ npx ts-node 05/index.ts 130 | Next: 1 131 | Next: 2 132 | Next: 3 133 | Complete! 134 | ``` 135 | 136 | ## Converting an Observable to an Observable 137 | 138 | Another thing we can convert is an Observable to yet another Observable. There are a number of libraries that implement the Observable contract which has the `subscribe` method, just as the `Promise` exposes the `then` method. For example, we could take something from Redux and convert it to an RxJS Observable. In this example, we'll just use another RxJS Observable, but it could be anything that implements the `Observable` contract. 139 | 140 | ```typescript 141 | import { from, Observable } from 'rxjs'; 142 | 143 | const obs$ = new Observable(observer => { 144 | observer.next(42); 145 | observer.complete(); 146 | }); 147 | 148 | obs$.subscribe({ 149 | next: x => console.log(`Next: ${x}`), 150 | complete: () => console.log('Complete') 151 | }); 152 | ``` 153 | 154 | Running this sample, we get the following output: 155 | ```bash 156 | $ npx ts-node 05/index.ts 157 | Next: 42 158 | Complete! 159 | ``` 160 | 161 | ## Converting a Promise to an Observable 162 | 163 | One of the more common scenarios using `from` is converting a `Promise` to an Observable. Many libraries now implement Promises as part of their asynchronous APIs, including Node.js, so it's only natural we give an easy way to convert that a `Promise` to an `Observable`. 164 | 165 | ```typescript 166 | const promise = Promise.resolve(42); 167 | const promise$ = from(promise); 168 | 169 | promise$.subscribe({ 170 | next: x => console.log(`Next: ${x}`), 171 | complete: () => console.log('Complete') 172 | }); 173 | ``` 174 | 175 | Like above with our Observable example, we get the same output of 42 and then a completion. 176 | ```bash 177 | $ npx ts-node 05/index.ts 178 | Next: 42 179 | Complete! 180 | ``` 181 | 182 | That's enough for now, but stay tuned as we cover converting events to Observables! 183 | -------------------------------------------------------------------------------- /06/index.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { 3 | empty, 4 | fromEvent, 5 | fromEventPattern, 6 | merge, 7 | never, 8 | throwError 9 | } from 'rxjs'; 10 | import { 11 | mergeMap 12 | } from 'rxjs/operators'; 13 | 14 | const emitter1 = new EventEmitter(); 15 | 16 | const emitter1$ = fromEvent(emitter1, 'data'); 17 | 18 | emitter1.emit('data', 'hello world'); 19 | 20 | /* 21 | const subscription1 = emitter1$.subscribe({ 22 | next: x => console.log(`Data: ${x}`) 23 | }); 24 | 25 | for (let i of ['foo', 'bar', 'baz', 'quux']) { 26 | emitter1.emit('data', i); 27 | } 28 | 29 | subscription1.unsubscribe(); 30 | */ 31 | 32 | const emitter = new EventEmitter(); 33 | const data$ = fromEvent(emitter, 'data'); 34 | 35 | const error$ = fromEvent(emitter, 'error') 36 | .pipe(mergeMap(err => throwError(err))); 37 | 38 | const merged$ = merge(data$, error$); 39 | 40 | merged$.subscribe({ 41 | next: x => console.log(`Data: ${x}`), 42 | error: x => console.log(`Error: ${x}`) 43 | }); 44 | 45 | /* 46 | for (let i of ['foo', 'bar', 'baz', 'quux']) { 47 | emitter.emit('data', i); 48 | } 49 | emitter.emit('error', new Error('woops')); 50 | */ 51 | emitter.emit('data', 'foo'); 52 | emitter.emit('error', new Error('woops')); 53 | emitter.emit('data', 'bar'); 54 | 55 | /* 56 | const emitter3 = new EventEmitter(); 57 | const emitter3$ = fromEventPattern( 58 | h => { console.log('Added handler'); emitter3.addListener('data', h as (...args: any[]) => void); }, 59 | h => { console.log('removed handler'); emitter3.removeListener('data', h as (...args: any[]) => void); } 60 | ); 61 | 62 | const subscription3 = emitter3$.subscribe({ 63 | next: x => console.log(`Data: ${x}`) 64 | }); 65 | 66 | for (let i of ['foo', 'bar', 'baz', 'quux']) { 67 | emitter3.emit('data', i); 68 | } 69 | 70 | subscription3.unsubscribe(); 71 | 72 | */ -------------------------------------------------------------------------------- /06/readme.md: -------------------------------------------------------------------------------- 1 | # Day 6 - Converting Events to Observables 2 | 3 | In the [previous entry](../05/readme.md), we covered converting various data structures to Observables via the `from` operation. Today, we'll continue on the conversion operations, this time with events, using `fromEvent` and `fromEventPattern`. 4 | 5 | ## Converting events to Observable with fromEvent 6 | 7 | The first operation we're going to look at here is `fromEvent` which allows us to take DOM Nodes, a DOM Node List, a Node.js `EventListener`, and even jQuery objects, and transform their events into Observables. This will attach the event during the Observable creation, and during the teardown, it will remove the event listener. 8 | 9 | Let's make this a bit more concrete with some code. Let's take for example a DOM Node, capturing click events from a button. 10 | ```typescript 11 | import { fromEvent } from 'rxjs'; 12 | 13 | const element = document.querySelector('#submitButton'); 14 | const click$ = fromEvent(element, 'click'); 15 | 16 | click$.subscribe({ 17 | next: () => console.log('Clicked'); 18 | }); 19 | ``` 20 | 21 | We can also capture a DOM Node list and `fromEvent` will iterate over the items and subscribe to the `click` event for each one. 22 | ```typescript 23 | import { fromEvent } from 'rxjs'; 24 | 25 | const elements = document.querySelectorAll('.selectButtons'); 26 | const click$ = fromEvent(element, 'click'); 27 | 28 | click$.subscribe({ 29 | next: () => console.log('Clicked'); 30 | }); 31 | ``` 32 | 33 | We also bridge to the Node.js `EventEmitter` so capturing data is pretty quick and easy! 34 | 35 | ```typescript 36 | import { EventEmitter } from 'events'; 37 | import { fromEvent } from 'rxjs'; 38 | 39 | const emitter = new EventEmitter(); 40 | const event$ = fromEvent(emitter, 'data'); 41 | 42 | emitter.emit('data', 'foobarbaz'); 43 | 44 | event$.subscribe({ 45 | next: x => console.log(`Data: ${x}`) 46 | }); 47 | 48 | for (let i of ['foo', 'bar', 'baz', 'quux']) { 49 | emitter.emit('data', i); 50 | } 51 | ``` 52 | 53 | What you'll notice in this example, we will completely miss the first event with the data `'foobarbaz'` because we had not yet subscribed to our Observable. This is often the cause of a lot of confusion. One workaround for this is to add `shareReplay` which will record all data and replay to every subscriber. We will cover the whole "share" aspect later on in this series. Running the above sample will give us the expected output of 'foo', 'bar', 'baz' and 'quux'. 54 | ```bash 55 | $ npx ts-node 06/index.ts 56 | Data: foo 57 | Data: bar 58 | Data: baz 59 | Data: quux 60 | ``` 61 | 62 | Let's get a little bit more advanced where we want to completely convert an `EventEmitter` to an Observable, where we can capture not only the 'data' event, but also react to the 'error' and 'close' events as well. We would need a way to throw the error if an error happens in the 'error' event. We can merge these two streams together using the `merge` operation which creates a single observable which encompasses all two events. 63 | 64 | ```typescript 65 | import { EventEmitter } from 'events'; 66 | import { 67 | fromEvent, 68 | throwError 69 | } from 'rxjs'; 70 | import { mergeMap } from 'rxjs/operators'; 71 | 72 | const emitter = new EventEmitter(); 73 | const data$ = fromEvent(emitter, 'data'); 74 | 75 | const error$ = fromEvent(emitter, 'error') 76 | .pipe(mergeMap(err => throwError(err))); 77 | 78 | const merged$ = merge(data$, error$); 79 | ``` 80 | 81 | Now let's run this through with some data to see what happens! 82 | ```typescript 83 | merged$.subscribe({ 84 | next: x => console.log(`Data: ${x}`), 85 | error: x => console.log(`Error: ${x}`) 86 | }); 87 | 88 | for (let i of ['foo', 'bar', 'baz', 'quux']) { 89 | emitter.emit('data', i); 90 | } 91 | emitter.emit('error', new Error('woops')); 92 | ``` 93 | 94 | And running it at the console gives the results where we capture not only the data, but an error as well. 95 | ```bash 96 | $ npx ts-node 06/index.ts 97 | Data: foo 98 | Data: bar 99 | Data: baz 100 | Data: quux 101 | Error: Error: woops 102 | ``` 103 | 104 | ## Converting events to Observables with fromEventPattern 105 | 106 | Sometimes, our event-based contract is not quite as straightforward as the DOM, Node.js `EventEmitter` or jQuery. Using `fromEventPattern` you can encompass your own way of attaching and detaching handlers from your event emitter, whatever it may be. For example, you could model if your API uses `attachEvent` and `detachEvent` as the subscription pair. 107 | 108 | ```typescript 109 | import { fromEventPattern } from 'rxjs'; 110 | 111 | const event$ = fromEventPattern( 112 | h => obj.attachEvent('data', h), 113 | h => obj.detachEvent('data', h) 114 | ); 115 | ``` 116 | 117 | We can also encompass an API that returns a token for example for unsubscription for example an `AbortController`. By returning it from the add handler, we can then use it in our remove handler by passing it in as the second argument. 118 | 119 | ```typescript 120 | import { fromEventPattern } from 'rxjs'; 121 | 122 | const event$ = fromEventPattern( 123 | h => { return obj.registerListener('data', h); }, 124 | (h, controller) => { controller.abort(); } 125 | ); 126 | ``` 127 | 128 | That's enough for today! Join me tomorrow for our next adventure where we're getting into the `pipe` finally! 129 | -------------------------------------------------------------------------------- /07/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | from, 3 | of, 4 | merge 5 | } from 'rxjs'; 6 | import { 7 | filter, 8 | map 9 | } from 'rxjs/operators'; 10 | 11 | const obs$ = of('foo', 'foobar', 'foobarbaz'); 12 | const piped$ = obs$.pipe( 13 | map(x => x.length), 14 | filter(x => { throw new Error(); }), 15 | ); 16 | 17 | piped$ 18 | .subscribe({ 19 | next: x => console.log(`Next: ${x}`), 20 | error: err => console.log(`Next: ${err}`) 21 | }); -------------------------------------------------------------------------------- /07/readme.md: -------------------------------------------------------------------------------- 1 | # Day 7 - Pipe Dreams 2 | 3 | In the [previous entry](../06/readme.md), we talked about converting existing events to RxJS Observables. Today, we're going to take our adventure a little bit further with chaining operations together. 4 | 5 | ## Adding operators the old fashioned way 6 | 7 | In previous releases, RxJS would use dot-chaining for operators, meaning we would add methods such as `map`, `filter`, `scan` and others directly to the prototype so we could achieve a nice fluent API. This had some advantages of a complete toolbox where you could easily consume new operators by adding it to the prototype. 8 | 9 | ```typescript 10 | var observable = Rx.Observable.range(0, 10) 11 | .map(x => x * x) 12 | .filter(x => x % 3 === 0); 13 | ``` 14 | 15 | This had many advantages at the time with a batteries included approach where you had all the operators you usually needed directly out of the box. Unfortunately, the number of operators used in RxJS grew over time where we had to split out each operator by functionality, either by time, by join patterns, by grouping, or core. That was a little bit of a hassle because you would get many operators you would never even use. 16 | 17 | ## Lettable Operators 18 | 19 | Instead, we focused on an operator we already had with `let` which would allow you to compose operators in a bit of a nicer way, for example we could export `map` as a function and then compose as we wished. 20 | 21 | ```typescript 22 | function map(source: Observable, selector: (value: T, index: number) => R) { 23 | return new Observable(observer => { 24 | let index = 0; 25 | return source.subscribe({ 26 | next: x => { 27 | let value: any; 28 | try { 29 | value = selector(x, index++); 30 | } catch (err) { 31 | observer.error(err); 32 | return; 33 | } 34 | 35 | observer.next(value); 36 | }, 37 | error: err => observer.error(err), 38 | complete: () => observer.complete() 39 | }) 40 | }); 41 | } 42 | 43 | const obs$ = Observable.range(0, 100) 44 | .let(o => map(o, x => x * x)); 45 | ``` 46 | 47 | This was a step in the right direction, but having to project the source over yourself again and again was a bit of a pain. What if we could abstract away the source and have that applied later? We could then write our map function as a partially applied function. 48 | 49 | ```typescript 50 | function map(selector: (value: T, index: number) => R) { 51 | return function mapOperator(source: Observable) : Observable { 52 | return new Observable(observer => { 53 | let index = 0; 54 | return source.subscribe({ 55 | next: x => { 56 | let value: any; 57 | try { 58 | value = selector(x, index++); 59 | } catch (err) { 60 | observer.error(err); 61 | return; 62 | } 63 | 64 | observer.next(value); 65 | } 66 | }, 67 | error: err => observer.error(err), 68 | complete: () => observer.complete() 69 | }); 70 | }; 71 | } 72 | 73 | const num$ = Observable.range(0, 100) 74 | .let(map(x => x * x)); 75 | ``` 76 | 77 | ## Piping data all the way through 78 | 79 | That looks a bit better and towards a solution we'd want for importing only the operators we want. This style was called lettable operators. This was changed later on to `pipe` because of the confusing name around it, like what the heck does "let" even mean? Not only did this change a bit from allowing only a single operator, to allowing any number of operators, where we could write `pipe` easily enough over an arguments array of them. 80 | 81 | ```typescript 82 | function pipe(...operators: Operator) { 83 | const ops = Array.from(operators); 84 | 85 | return function piped(input: any) { 86 | return ops.reduce((prev, fn) => fn(prev), input); 87 | } 88 | } 89 | ``` 90 | 91 | Then we could write something like the following where we could take map and filter together and then give it an initial value. 92 | 93 | ```typescript 94 | const pipes = pipe( 95 | map(x => x * x), 96 | filter(x => x % 3 === 0) 97 | ); 98 | 99 | const pipe$ = pipes(range(, 100)).subscribe({ 100 | next: x => console.log(`Next: ${x}`) 101 | }); 102 | ``` 103 | 104 | Luckily this is all done for you with there being a `Observable.prototype.pipe` which provides this method for you. And all the operators have been rewritten in such a way to support this style by importing from `rxjs/operators`. So now we could rewrite our above sample as easily as this with imports. 105 | 106 | ```typescript 107 | import { range } from 'rxjs'; 108 | import { 109 | filter, 110 | map 111 | } from 'rxjs/operators'; 112 | 113 | const num$ = range(0, 100).pipe( 114 | map(x => x * x), 115 | filter(x => x % 3 === 0) 116 | ); 117 | 118 | num$.subscribe({ 119 | next: x => console.log(`Next: ${x}`) 120 | }); 121 | ``` 122 | 123 | That's enough for now, but now gets us to a nicer spot where we can tree shake, taking only the pieces of RxJS we need, such as the `map` and `filter` operator, the `range` creation method, and that's it! Next, we'll go into some basic operators, so stay tuned! 124 | -------------------------------------------------------------------------------- /08/index.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'rxjs'; 2 | import { filter, map } from 'rxjs/operators'; 3 | 4 | function mapArr(source: Array, selector: (value: T, index: number) => R, thisArg?: any): Array { 5 | const length = source.length; 6 | const results = new Array(length); 7 | for (let i = 0; i < length; i++) { 8 | results[i] = selector.call(thisArg, source[i], i) 9 | } 10 | 11 | return results; 12 | } 13 | 14 | mapArr([1, 2, 3], x => x * x).forEach(x => console.log(`Next: ${x}`)); 15 | 16 | function* mapIter(source: Iterable, selector: (value: T, index: number) => R, thisArg?: any): Iterable { 17 | let i = 0; 18 | for (let item of source) { 19 | yield selector.call(thisArg, item, i++); 20 | } 21 | } 22 | 23 | const mapped = mapIter(new Set([1, 2, 3]), x => x * x); 24 | for (let item of mapped) { 25 | console.log(`Next: ${item}`); 26 | } 27 | 28 | const obs$ = of(1, 2, 3).pipe( 29 | map(x => x * x) 30 | ); 31 | 32 | obs$.subscribe({ 33 | next: x => console.log(`Next: ${x}`) 34 | }); 35 | 36 | // Filter 37 | function filterArr(source: Array, predicate: (value: T, index: number) => boolean, thisArg?: any) { 38 | let results = []; 39 | for (let i = 0; i < source.length; i++) { 40 | if (predicate.call(thisArg, source[i], i)) { 41 | results.push(source[i]); 42 | } 43 | } 44 | 45 | return results; 46 | } 47 | 48 | filterArr([1, 2, 3], x => x % 2 === 0).forEach(x => console.log(`Next: ${x}`)); 49 | 50 | function* filterIter(source: Iterable, predicate: (value: T, index: number) => boolean, thisArg?: any) { 51 | let i = 0; 52 | for (let item of source) { 53 | if (predicate.call(thisArg, item, i++)) { 54 | yield item; 55 | } 56 | } 57 | } 58 | 59 | const filtered = filterIter(new Set([1, 2, 3]), x => x % 2 === 0); 60 | for (let item of filtered) { 61 | console.log(`Next: ${item}`); 62 | } 63 | 64 | const obs2$ = of(1, 2, 3).pipe( 65 | filter(x => x % 2 === 0), 66 | map(x => x * x) 67 | ); 68 | 69 | obs2$.subscribe({ 70 | next: x => console.log(`Next: ${x}`) 71 | }); -------------------------------------------------------------------------------- /08/readme.md: -------------------------------------------------------------------------------- 1 | # Day 8 - Mapping, Plucking, Tapping, and Filtering 2 | 3 | In the [previous entry](../07/readme.md), we covered how RxJS has evolved from using dot chaining to "lettable" operators, and finally how we ended up with `pipe` to chain our operators together. Today, we're going to cover probably the two most used operators, `map` and `filter`. One of the early goals of RxJS was to make it as easy as possible to learn, so if you knew the [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) methods such as [`Array.prototype.map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map), [`Array.prototype.filter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter), [`Array.prototype.reduce`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce), they operate pretty much the same way as Array does, except for being a pull collection, it becomes a push collection with values over time. 4 | 5 | ## Mapping one value to another 6 | 7 | One of the most common operations over data structure is [`map`](https://en.wikipedia.org/wiki/Map_%28higher-order_function%29) which applies to each element of a [functor](https://en.wikipedia.org/wiki/Functor), such as arrays, and returns a new instance with results in the same order. In simple terms, a functor is just anything that supports that mapping operation such as an array, a Promise, and yes even an Observable. In JavaScript, we have [`Array.prototype.map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) which creates a new array with the projected values. In RxJS, we give you the operator `map` in the `rxjs/operators` imports. 8 | 9 | As we showed in the previous post, implementing `map` in JavaScript is pretty simple, for example we could have implemented `map` ourselves on an Array. This creates a new array, filling the array with a function call on each element from the source array, and returning the new array. 10 | 11 | ```typescript 12 | function map(source: Array, selector: (value: T, index: number) => R, thisArg?: any): Array { 13 | const length = source.length; 14 | const results = new Array(length); 15 | for (let i = 0; i < length; i++) { 16 | results[i] = selector.call(thisArg, source[i], i) 17 | } 18 | 19 | return results; 20 | } 21 | 22 | map([1, 2, 3], x => x * x); 23 | // [1, 4, 9] 24 | ``` 25 | 26 | With the advent of Iterables in ES2015, we could generalize this a bit more to apply for both `Set` and `Array` and have it lazy as well. We can iterate over the existing structure using the [`for .. of`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of) and then yielding the transformed item. 27 | 28 | ```typescript 29 | function* map(source: Iterable, selector: (value: T, index: number) => R, thisArg?: any): Iterable { 30 | let i = 0; 31 | for (let item of source) { 32 | yield selector.call(thisArg, item, i++); 33 | } 34 | } 35 | 36 | const mapped = map(new Set([1, 2, 3]), x => x * x); 37 | for (let item of mapped) { 38 | console.log(`Next: ${item}`); 39 | } 40 | // [1, 4, 9] 41 | ``` 42 | 43 | Implementing this in Observables is almost as straightforward, except that we have to take care of the error case should our selector function throw an error, and forwarding the error and completion channels on through. 44 | 45 | ```typescript 46 | function map(selector: (value: T, index: number, thisArg?: any) => R) { 47 | return function mapOperator(source: Observable) : Observable { 48 | return new Observable(observer => { 49 | let index = 0; 50 | return source.subscribe({ 51 | next: x => { 52 | let value: any; 53 | try { 54 | value = selector.call(thisArg, x, index++); 55 | } catch (err) { 56 | observer.error(err); 57 | return; 58 | } 59 | 60 | observer.next(value); 61 | } 62 | }, 63 | error: err => observer.error(err), 64 | complete: () => observer.complete() 65 | }); 66 | }; 67 | } 68 | ``` 69 | 70 | Luckily we have it nice and optimized in RxJS, so you can use it by simply importing it from `rxjs/operators`. 71 | 72 | ```typescript 73 | include { of } from 'rxjs'; 74 | include { map } from 'rxjs/operators'; 75 | 76 | const obs$ = of(1, 2, 3).pipe( 77 | map(x => x * x) 78 | ); 79 | 80 | obs$.subscribe({ 81 | next: x => console.log(`Next: ${x}`) 82 | }); 83 | 84 | // Next: 1 85 | // Next: 4 86 | // Next: 9 87 | ``` 88 | 89 | ## Plucking Data 90 | 91 | With the `map` operator, we can easily project values to a new sequence. But, what if we wanted to just pull out values from the sequence itself? To do that, we have the `pluck` operator, which allows us to specify which properties to pull out. As you will notice, we can specify multiple values which recursively walks the object to pluck that desired value. 92 | 93 | ```typescript 94 | import { from } from 'rxjs'; 95 | import { pluck } from 'rxjs/operators'; 96 | 97 | const people = [ 98 | { name: 'Kim' }, 99 | { name: 'Bob' }, 100 | { name: 'Joe' } 101 | ]; 102 | 103 | const person$ = from(people).pipe(pluck('name')); 104 | 105 | const props = [ 106 | { prop1: { prop2: 'Kim' } }, 107 | { prop1: { prop2: 'Bob' } }, 108 | { prop1: { prop2: 'Joe' } } 109 | ]; 110 | 111 | const data$ = from(data).pipe(pluck('prop1', 'prop2')); 112 | ``` 113 | 114 | ## Tapping Data 115 | 116 | While `map` allows us to project a value to a new sequence. But, what if we want to cause a side effect for each item, while project the current value to a new sequence? That's what the `tap` operator is for, which allows us to intercept not only `next` calls, but also `error` and `complete` calls as well. This is good for when during a sequence, some side effect needs to happen, for example a progress status to be updated, while not affecting the stream itself. 117 | 118 | ```typescript 119 | import { of } from 'rxjs'; 120 | import { tap } from 'rxjs/operators'; 121 | 122 | const val$ = of(1, 2, 3).pipe( 123 | tap({ 124 | next: item => console.log(`Tapped next: ${item}`), 125 | complete: () => console.log('Tapped complete') 126 | }) 127 | ); 128 | 129 | const subscription = val$.subscribe({ 130 | next: item => console.log(`Next: ${item}`), 131 | complete: () => console.log('Done') 132 | }); 133 | // Tapped next: 1 134 | // Next: 1 135 | // Tapped next: 2 136 | // Next: 2 137 | // Tapped next: 3 138 | // Next: 3 139 | // Tapped complete 140 | // Done 141 | ``` 142 | 143 | ## Filtering Data 144 | 145 | Another higher-order function that's often used is [`filter`](https://en.wikipedia.org/wiki/Filter_%28higher-order_function%29), which iterates over a given data structure, and creates a new data structure where the predicate returns true. No magical functional programming jargon like functor required for this operator! In JavaScript, we have it implemented for us on the Array with [`Array.prototype.filter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter). 146 | 147 | We could easily implement this ourselves much like we did for `map` above, iterating over the array and only including values where the predicate evaluates to true. 148 | 149 | ```typescript 150 | function filter(source: Array, predicate: (value: T, index: number) => boolean, thisArg?: any) { 151 | let results = []; 152 | for (let i = 0; i < source.length; i++) { 153 | if (predicate.call(thisArg, source[i], i)) { 154 | results.push(source[i]); 155 | } 156 | } 157 | 158 | return results; 159 | } 160 | 161 | filter([1, 2, 3], x => x % 2 === 0); 162 | // [2] 163 | ``` 164 | 165 | Similarly, as with the above, we can implement `filter` on things that implement `[Symbol.iterator]` such as Array, Map, Set, and even generator functions. 166 | 167 | ```typescript 168 | let i = 0; 169 | for (let item of source) { 170 | if (predicate.call(thisArg, item, i++)) { 171 | yield item; 172 | } 173 | } 174 | } 175 | 176 | const filtered = filter(new Set([1, 2, 3]), x => x % 2 === 0); 177 | for (let item of filtered) { 178 | console.log(`Next: ${item}`); 179 | } 180 | // [2] 181 | ``` 182 | 183 | Lastly, implementing this for Observables is pretty much as straightforward, sending values to `observer.next` only if the predicate returns true. 184 | 185 | ```typescript 186 | function filter(predicate: predicate: (value: T, index: number) => boolean, thisArg?: any) { 187 | return function filterOperator(source: Observable): Observable { 188 | return new Observable(observer => { 189 | let i = 0; 190 | return source.subscribe({ 191 | next: x => { 192 | function* filter(source: Iterable, predicate: (value: T, index: number) => bool, thisArg?: any) { 193 | let shouldYield = false; 194 | try { 195 | shouldYield = predicate.call(thisArg, x, i++); 196 | } catch (err) { 197 | observer.error(err); 198 | return; 199 | } 200 | 201 | if (shouldYield) { 202 | observer.next(x); 203 | } 204 | }, 205 | error: err => observer.error(err), 206 | complete: () => observer.complete() 207 | }); 208 | }); 209 | } 210 | } 211 | ``` 212 | 213 | Luckily, just as before, we don't need to implement this ourselves, and instead it is provided by the `filter` operator in `rxjs/operators`. 214 | 215 | ```typescript 216 | import { of } from 'rxjs'; 217 | import { filter } from 'rxjs/operators'; 218 | 219 | const obs$ = of(1, 2, 3).pipe( 220 | filter(x => x % 2 === 0), 221 | map(x => x * x) 222 | ); 223 | 224 | obs$.subscribe({ 225 | next: x => console.log(`Next: ${x}`) 226 | }); 227 | 228 | // Next: 4 229 | ``` 230 | 231 | That's enough for now as we've learned how to create operators from scratch with the two most basic ones, `map` and `filter`. I hope this also gives you the confidence that you too could write something like RxJS. In the next posts, we'll cover more of the everyday operators you'll be using as you get more comfortable with RxJS! 232 | -------------------------------------------------------------------------------- /09/index.ts: -------------------------------------------------------------------------------- 1 | // TODO: Implement redux style reducer -------------------------------------------------------------------------------- /09/readme.md: -------------------------------------------------------------------------------- 1 | # Aggregating via reduce and scan 2 | 3 | In the [previous entry](../08/readme.md), we covered the basic operators that we would find on `Array` like `map` and `filter` are also there in for Observables and act pretty much the same way! There's another common operator, `reduce` that takes a value and aggregates it by folding over the data until it gets to a single value. This `reduce` method is two forms, one that provides an initial seed value for the accumulator, and another that doesn't. We could implement this for Iterables like Arrays pretty easily such as the following code, accounting for both a seed value and no seed value. If there is no seed value and the iterable is empty, then we throw an error because we cannot determine what the end value might be. 4 | 5 | ```typescript 6 | function reduce(source: Iterable, accumulator: (acc: R, value: T, index: number) => R), seed?: R): R { 7 | const hasSeed = seed.length === 1; 8 | let i = 0, 9 | hasValue = false, 10 | acc = seed[0] as T | R; 11 | for (let item of source) { 12 | if (hasValue || (hasValue = hasSeed)) { 13 | acc = accumulator(acc, item, i++); 14 | } else { 15 | acc = item; 16 | hasValue = true; 17 | i++; 18 | } 19 | } 20 | 21 | if (!(hasSeed || hasValue)) { 22 | throw new Error('Sequence contains no elements'); 23 | } 24 | 25 | return acc as R; 26 | } 27 | 28 | const sum = reduce([1, 2, 3], (acc, current) => acc + current, 0); 29 | // 6 30 | ``` 31 | 32 | With RxJS, for composition purposes, we do not reduce to a single value such as a `Promise` but instead reduce it to an Observable with a single value. Optionally, we could do away with the whole throwing an error on empty observables and instead return an empty Observable. 33 | 34 | ```typescript 35 | import { Observable } from 'rxjs'; 36 | 37 | function reduce(accumulator: (acc: R, value: T, index: number) => R), seed?: R): Observable { 38 | const hasSeed = arguments.length === 2; 39 | return function reduceOperator(source: Observable): Observable { 40 | return new Observable(observer => { 41 | let hasValue = false, 42 | acc = seed as T | R; 43 | 44 | return source.subscribe({ 45 | next: item => { 46 | if (hasValue || (hasValue = hasSeed)) { 47 | acc = accumulator(acc, item); 48 | } else { 49 | acc = item; 50 | hasValue = true; 51 | } 52 | }, 53 | error: err => observer.error(err), 54 | complete: () => { 55 | if (!(hasSeed || hasValue)) { 56 | observer.complete(); 57 | return; 58 | } 59 | 60 | observer.next(acc as R); 61 | observer.complete(); 62 | } 63 | }) 64 | }); 65 | }; 66 | } 67 | ``` 68 | 69 | Much as before, we have `reduce` already implemented for us in `rxjs/operators` so none of this is required, but it's good to know how the sausage is made sometimes. 70 | 71 | ```typescript 72 | import { of } from 'rxjs'; 73 | import { reduce } from 'rxjs/operators'; 74 | 75 | const num$ = of(1, 2, 3).pipe( 76 | reduce((acc, x) => acc + x, 0); 77 | ); 78 | 79 | num$.subscribe({ 80 | next: x => console.log(`Next: ${x}`) 81 | }); 82 | // Next: 6 83 | ``` 84 | 85 | Aggregations such as `reduce` and others aren't super useful as the sequence must have some sort of termination in order to work. Many Observables such as mouse movements don't necessarily end as they can be infinite streams, so what good is having `reduce`? Well, what if we told you there was such a thing as `scan` which does incremental emitting of data as it accumulates data? 86 | 87 | ## Incremental accumulators via scan 88 | 89 | As I said before, aggregate operations aren't necessarily useful especially when dealing with infinite streams of data. Instead, what if could emit intermediate values as part of the accumulation? A useful example of this would be counting mouse clicks, where our `scan` operator is basically a state machine. We could implement this for Iterables just as we did for reduce above. 90 | 91 | ```typescript 92 | function scan(source: Iterable, accumulator: (acc: R, current, T) => R, seed?: R) { 93 | let hasValue = false, 94 | acc = this._seed; 95 | for (let item of source) { 96 | if (hasValue || (hasValue = this._hasSeed)) { 97 | acc = accumulator(acc, item); 98 | yield acc; 99 | } else { 100 | acc = item; 101 | hasValue = true; 102 | } 103 | } 104 | } 105 | 106 | scan([1, 2, 3], (acc, x) => acc + x, 0); 107 | // 1 108 | // 3 109 | // 6 110 | ``` 111 | 112 | Implementing this using Observables is pretty much as straight forward as the iterables approach, and not that much different than what we did for `reduce` except that we're not caring about the completion as much in order to emit values. 113 | 114 | ```typescript 115 | function scan(accumulator: (acc: R, curr: T) => R, seed?: R) { 116 | return function scanOperator(source: Observable): Observable { 117 | const hasSeed = arguments.length === 2; 118 | return new Observable(observer => { 119 | let hasValue = false, 120 | acc = seed as T | R; 121 | 122 | return source.subscribe({ 123 | next: item => { 124 | let result: any; 125 | if (hasValue || (hasValue = this._hasSeed)) { 126 | try { 127 | result = accumulator(seed, item); 128 | } catch (err) { 129 | observer.error(err); 130 | return; 131 | } 132 | 133 | seed = result; 134 | observer.next(result); 135 | } else { 136 | seed = item; 137 | observer.next(item); 138 | } 139 | }, 140 | error: err => observer.error(err), 141 | complete: () => observer.complete() 142 | }); 143 | }); 144 | }; 145 | } 146 | ``` 147 | 148 | Now let's look at an example where this might be useful instead of just simple addition on lists, what about counting mouse button clicks? 149 | 150 | ```typescript 151 | import { fromEvent } from 'rxjs'; 152 | import { scan } from 'rxjs/operators'; 153 | 154 | const click$ = fromEvent(document, 'click').pipe( 155 | scan((acc, x) => acc + x, 0) 156 | ); 157 | 158 | click$.subscribe({ 159 | next: console.log(`Number of clicks so far: ${x}`) 160 | }); 161 | ``` 162 | 163 | This idea of the incremental state machine is a driving factor behind such libraries as [Redux](https://redux.js.org/) with its concept of reducers. In fact, you could probably implement the basic concepts of Redux reducers in a single line of RxJS as seen in [this post](http://rudiyardley.com/redux-single-line-of-code-rxjs/) doing nothing more than this: 164 | ```typescript 165 | action$ 166 | .pipe(scan(reducer, initialState)) 167 | .subscribe(renderer); 168 | ``` 169 | 170 | Once again, this shows how RxJS gives you many building blocks to create rich experiences, whether it's clicking mouse buttons and tracking state, to Redux style reducers, RxJS has you covered. In the next post, we'll go deeper in other operators you'll need to know! -------------------------------------------------------------------------------- /10/index.ts: -------------------------------------------------------------------------------- 1 | import { empty, of } from 'rxjs'; 2 | import { startWith, endWith, defaultIfEmpty } from 'rxjs/operators'; 3 | /* 4 | of(1, 2, 3).pipe( 5 | startWith(-1, 0), 6 | endWith(4, 5, 6) 7 | ) 8 | .subscribe(x => console.log(x)); 9 | */ 10 | 11 | empty().pipe( 12 | defaultIfEmpty(42), 13 | startWith(0, 1), 14 | endWith(4, 5, 6) 15 | ) 16 | .subscribe(x => console.log(x)); 17 | 18 | -------------------------------------------------------------------------------- /10/readme.md: -------------------------------------------------------------------------------- 1 | # Day 10 - Starting with data, ending with data, and defaulting if empty 2 | 3 | In the [previous entry](../09/readme.md), we covered aggregation operations via `reduce` and `scan`. Today, we're going to dive deeper into some operators where you can add that initial state to your observable, as well as add anything to the end. And in the cases where your data stream is empty, we can accommodate for that too with yet another operator. 4 | 5 | ## Starting off with startWith 6 | 7 | The first operator we're going to look at is `startWith`, which allows us to prepend data to the beginning of an Observable sequence. As with other operators that create things, this also takes a scheduler which allows you to control the concurrency. 8 | 9 | ```typescript 10 | startWith(...args: (Array | SchedulerLike)): Observable; 11 | ``` 12 | 13 | We could easily write this using this for Iterables which would show you basically how `startWith` works. 14 | 15 | ```typescript 16 | function* startWith(source: Iterable, ...args: Array) { 17 | for (let i = 0; i < args.length; i++) { 18 | yield args[i]; 19 | } 20 | 21 | for (let item of source) { 22 | yield item; 23 | } 24 | } 25 | 26 | startWith([2, 3, 4], 0, 1); 27 | // [0, 1, 2, 3, 4] 28 | ``` 29 | 30 | So, we can take an existing sequence and prepend any number of values to it. Once again like all operators, we can implement them in terms of another, for example using `concat` and `of`. That's the beauty of RxJS, is that it's easy to make your own custom operator just as if they were Lego blocks. 31 | 32 | ```typescript 33 | function startWith(source: Observable, ...args: Array) { 34 | return concat(of(...args), source); 35 | } 36 | ``` 37 | 38 | Let's add some values to the beginning of this sequence! 39 | 40 | ```typescript 41 | import { of } from 'rxjs'; 42 | import { startWith } from 'rxjs/operators'; 43 | 44 | const num$ = of(2, 3, 4).pipe( 45 | startWith(0, 1) 46 | ); 47 | 48 | num$.subscribe({ 49 | next: x => console.log(`Next: ${x}`) 50 | }); 51 | // Next 0 52 | // Next 1 53 | // Next 2 54 | // Next 3 55 | // Next 4 56 | ``` 57 | 58 | This is useful of course if you want to seed your stream with default data, such as an initial state for mouse movements with an x and y coordinate. 59 | 60 | ## Ending our sequence with endWith 61 | 62 | Just as we have `startWith` which allows us to prepend values to the front of the Observable, we also have `endWith` which appends values to the end of the Observable sequence. For a while, I had resisted such an operator, given how sequences must have some sort of termination in order to add values to the end. Would it make sense to end a stream of mouse moves with more values? After a while, I relented, and we added `endWith` which allows us once again like `startWith` to add arbitrary values with a Scheduler. 63 | 64 | ```typescript 65 | endWith(...args: (Array | SchedulerLike)): Observable; 66 | ``` 67 | 68 | We could implement this for Iterables once again to show you how it works at least for pull sequences, iterating first over our source sequence, then appending all the arguments given in the function. 69 | 70 | ```typescript 71 | function* endWith(source: Iterable, ...args: Array) { 72 | for (let item of source) { 73 | yield item; 74 | } 75 | 76 | for (let i = 0; i < args.length; i++) { 77 | yield args[i]; 78 | } 79 | } 80 | ``` 81 | 82 | Like above, we could implement this in RxJS instead of building from scratch, simply implement via the following using `concat` and `of`. 83 | 84 | ```typescript 85 | import { concat, of } from 'rxjs'; 86 | 87 | function endWith(source: Observable, ...args: Array) { 88 | return concat(source, of(...args))); 89 | } 90 | ``` 91 | 92 | An example of adding to the end could be like the following where we add 3 and 4 to an Observable of 1 and 2. 93 | 94 | ```typescript 95 | import { of } from 'rxjs'; 96 | import { endWith } from 'rxjs/operators'; 97 | 98 | const num$ = of(1, 2).pipe( 99 | endWith(3, 4) 100 | ); 101 | 102 | num$.subscribe({ 103 | next: x => console.log(`Next: ${x}`) 104 | }); 105 | // Next 1 106 | // Next 2 107 | // Next 3 108 | // Next 4 109 | ``` 110 | 111 | ## What to do when you are empty 112 | 113 | In some cases, our Observable stream may be empty, whether it is due to connectivity, an empty cache or so on. In RxJS, we have a way of getting around this by allowing you to substitute a value if the sequence is empty. This is helpful for example if you don't have any data, and instead want to return some cached data or some skeleton data. 114 | 115 | We could implement this using Iterables just to give you an idea how this might work with Observables where we have an internal state flag which says whether it has yielded any data, and if it hasn't, then yield the default value. 116 | 117 | ```typescript 118 | function* defaultIfEmpty(source: Iterable, defaultValue: T) { 119 | let state = 1; 120 | for (let item of source) { 121 | state = 2; 122 | yield item; 123 | } 124 | if (state === 1) { 125 | yield defaultValue; 126 | } 127 | } 128 | ``` 129 | 130 | Bringing this all together, we could take an empty sequence with a default, prepend some data to it, and then add some data to it at the end. 131 | 132 | ```typescript 133 | empty().pipe( 134 | defaultIfEmpty(42), 135 | startWith(0, 1), 136 | endWith(4, 5) 137 | ) 138 | .subscribe(x => console.log(x)); 139 | // 0 140 | // 1 141 | // 42 142 | // 4 143 | // 5 144 | ``` 145 | 146 | That's enough for now, but I hope this gives you some idea of what you can do to build operators on your own using the building blocks we have! Next up, we'll implement our own Redux and then spend the next days spending explaining how we did it! Stay tuned! 147 | -------------------------------------------------------------------------------- /11/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AsyncSubject, 3 | BehaviorSubject, 4 | ReplaySubject, 5 | Subject, 6 | of, 7 | fromEvent 8 | } from 'rxjs'; 9 | 10 | const event$ = new BehaviorSubject(12); 11 | 12 | console.log(`Current value: ${event$.getValue()}`); 13 | 14 | event$.subscribe({ 15 | next: val => console.log(`Received ${val}`) 16 | }); 17 | 18 | event$.next(42); 19 | 20 | console.log(`Current value: ${event$.getValue()}`); 21 | 22 | event$.next(56); 23 | 24 | console.log(`Current value: ${event$.getValue()}`); 25 | 26 | event$.next(78); 27 | 28 | console.log(`Current value: ${event$.getValue()}`); 29 | 30 | 31 | 32 | /* 33 | const event$ = new Subject(); 34 | 35 | event$.next('hello'); 36 | 37 | const subscription = event$.subscribe({ 38 | next: val => console.log(`Received ${val}`) 39 | }); 40 | 41 | event$.next('world'); 42 | */ 43 | 44 | /* 45 | const event$ = new ReplaySubject(); 46 | 47 | event$.next('hello'); 48 | 49 | const subscription = event$.subscribe({ 50 | next: val => console.log(`Received ${val}`) 51 | }); 52 | 53 | event$.next('world'); 54 | */ 55 | 56 | /* 57 | const result$ = new AsyncSubject(); 58 | 59 | result$.subscribe({ 60 | next: val => console.log(`First subscriber: ${val}`) 61 | }); 62 | 63 | result$.next('hello world'); 64 | result$.next('goodbye world'); 65 | result$.complete(); 66 | 67 | result$.subscribe({ 68 | next: val => console.log(`Second subscriber: ${val}`) 69 | }); 70 | */ 71 | 72 | /* 73 | const event$ = new BehaviorSubject('initial value'); 74 | 75 | console.log(`Current value: ${event$.getValue()}`); 76 | 77 | event$.subscribe({ 78 | next: val => console.log(`Data: ${val}`) 79 | }); 80 | 81 | event$.next('hello world'); 82 | 83 | console.log(`Current value: ${event$.getValue()}`); 84 | 85 | event$.next('goodbye world'); 86 | 87 | console.log(`Current value: ${event$.getValue()}`); 88 | 89 | */ 90 | -------------------------------------------------------------------------------- /11/readme.md: -------------------------------------------------------------------------------- 1 | # Day 11 - On the Subject of Subjects 2 | 3 | In the [previous entry](../10/readme.md), we covered dealing with prepending, appending and substituting data if the stream is empty. Before we get started with our Redux clone, I wanted to talk briefly on the subject of Subjects. As we've talked about earlier, we have a number of concepts: 4 | 5 | - `Observable`: The producer of data that can emit data zero to infinite times. This has a contract of `subscribe` which takes an `Observer` to emit data to. 6 | - `Observer`: The data sink in which to emit the `Observable` data. This can be emitted to the `next` method zero to infinite times, optionally followed by an error via `error` or completion via `complete`. Calling the `subscribe` method on the `Observable` with the `Observer` gives us a `Subscription`. 7 | - `Subscription`: Contains the teardown logic which can be composed together, for example, multiple streams can be added together, giving a subscription, that can be torn down via `unsubscribe`. 8 | 9 | ## Broaching the Subject 10 | 11 | We also have another concept that we haven't covered yet, and that is Subjects, which is both an `Observable` and `Observer`. This is useful in many ways for example having a custom event emitter where you can expose the `subscribe` to consumers while you push data via the `next` method calls. There are a number of subjects that you can use, depending on the behavior that you want. We'll first look at the plain `Subject`. 12 | 13 | ```typescript 14 | import { Subject } from 'rxjs'; 15 | 16 | const event$ = new Subject(); 17 | 18 | event$.next('hello'); 19 | 20 | const subscription = event$.subscribe({ 21 | next: val => console.log(`Received ${val}`) 22 | }); 23 | 24 | event$.next('world'); 25 | // Received: world 26 | ``` 27 | 28 | ## Going Hot and Cold 29 | 30 | As you'll notice from the above code, we're missing the 'hello' value that we emitted because we had not yet subscribed to the `Subject`. This style of `Observable` or rather `Subject` is what we call a "Hot Observable", meaning it emits data whether we are listening to it or not, such as events like mouse moves, and in this case `Subject`. In contrast, a "Cold Observable" is one that will always emit the same data no matter when you subscribe, such as operators like `from`, `of`, and others. 31 | 32 | ## Replaying your data with ReplaySubject 33 | 34 | We have ways around our problematic `Subject` with data loss due to subscribing late. One way is using the `ReplaySubject`. This does exactly what you think it might, it replays data, given the criteria of a given time window or number of items. For example, I can specify that I only want to replay items from the past five minutes, or 500 items in the queue. By default, the `ReplaySubject` will retain data indefinitely, so that any subscriber can get the same data no matter when they subscribe. 35 | 36 | ```typescript 37 | import { ReplaySubject } from 'rxjs'; 38 | 39 | // Replays all data 40 | const replayAll = new ReplaySubject(); 41 | 42 | // Replays only the past 10 items 43 | const replayCount = new ReplaySubject(10); 44 | 45 | // Replays on the past 5 minutes 46 | const replayTime = new ReplaySubject(Number.POSITIVE_INFINITY, 5 * 1000 * 60); 47 | 48 | // Replays either on count or time 49 | const replayTime = new ReplaySubject(50, 5 * 1000 * 60); 50 | ``` 51 | 52 | So, now that we have this in place, we could easily capture the event we missed and replay it back, so no more chances of lost data! 53 | 54 | ```typescript 55 | import { ReplaySubject } from 'rxjs'; 56 | 57 | const event$ = new ReplaySubject(); 58 | 59 | event$.next('hello'); 60 | 61 | const subscription = event$.subscribe({ 62 | next: val => console.log(`Received ${val}`) 63 | }); 64 | 65 | event$.next('world'); 66 | // Received: hello 67 | // Received: world 68 | ``` 69 | 70 | Much better results and we're not missing any data. What we have to be careful of here, however, is the size of our `ReplaySubject` buffer since we're keeping all those values by default. 71 | 72 | ## Caching Data with AsyncSubject 73 | 74 | Another style of subject is where we'll receive the result of some async computation, such as a `Promise`. The `AsyncSubject` is a mirror for the JavaScript `Promise` as it will only resolve the data once, and then give the same value to all incoming subscribers. You'll note that in order to finish the async computation, the `complete` must be called explicitly, and then the data is cached. 75 | 76 | ```typescript 77 | import { AsyncSubject } from 'rxjs'; 78 | 79 | const result$ = new AsyncSubject(); 80 | 81 | result$.subscribe({ 82 | next: val => console.log(`First subscriber: ${val}`) 83 | }); 84 | 85 | result$.next('hello world'); 86 | result$.complete(); 87 | 88 | result$.subscribe({ 89 | next: val => console.log(`Second subscriber: ${val}`) 90 | }); 91 | // First subscriber: hello world 92 | // Second subscriber: hello world 93 | ``` 94 | 95 | You could hypothetically keep emitting values and then finally call `complete` and only the last value will be kept. 96 | 97 | ```typescript 98 | import { AsyncSubject } from 'rxjs'; 99 | 100 | const result$ = new AsyncSubject(); 101 | 102 | result$.subscribe({ 103 | next: val => console.log(`First subscriber: ${val}`) 104 | }); 105 | 106 | result$.next('hello world'); 107 | result$.next('goodbye world'); 108 | result$.complete(); 109 | 110 | result$.subscribe({ 111 | next: val => console.log(`Second subscriber: ${val}`) 112 | }); 113 | // First subscriber: goodbye world 114 | // Second subscriber: goodbye world 115 | ``` 116 | 117 | There are many uses for the `AsyncSubject` where it caches the data. There are downsides of this one of course as you can't retry a failed `AsyncSubject` as you can with a normal `Observable`. That's for another discussion on error handling. 118 | 119 | ## State Machines with BehaviorSubject 120 | 121 | The last subject is one of the most used types of subjects, called the `BehaviorSubject`. This subject takes an initial value, such as the initial state of a state machine, such as a shopping cart or whatever you're trying to model. 122 | 123 | ```typescript 124 | import { BehaviorSubject } from 'rxjs'; 125 | 126 | const event$ = new BehaviorSubject('initial value'); 127 | 128 | event$.subscribe({ 129 | next: val => console.log(`Data: ${val}`) 130 | }); 131 | 132 | event$.next('hello world'); 133 | event$.next('goodbye world'); 134 | 135 | // Data: initial value 136 | // Data: hello world 137 | // Data: goodbye world 138 | ``` 139 | 140 | What makes the `BehaviorSubject` even more interesting is that you can peek at its current value via the `getValue()` method call. 141 | 142 | ```typescript 143 | import { BehaviorSubject } from 'rxjs'; 144 | 145 | const event$ = new BehaviorSubject('initial value'); 146 | 147 | console.log(`Current value: ${event$.getValue()}`); 148 | 149 | event$.subscribe({ 150 | next: val => console.log(`Data: ${val}`) 151 | }); 152 | 153 | event$.next('hello world'); 154 | 155 | console.log(`Current value: ${event$.getValue()}`); 156 | 157 | event$.next('goodbye world'); 158 | 159 | console.log(`Current value: ${event$.getValue()}`); 160 | // Current value: initial value 161 | // Data: initial value 162 | // Data: hello world 163 | // Current value: hello world 164 | // Data: goodbye world 165 | // Current value: goodbye world 166 | ``` 167 | 168 | I hope this gives you a clear understanding of Subjects and why you might use each. This will serve as a basis for creating a Redux clone going forward, stay tuned! -------------------------------------------------------------------------------- /12/index.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | import { scan, startWith } from 'rxjs/operators'; 3 | 4 | enum ActionType { 5 | Increment, 6 | Decrement, 7 | Default 8 | } 9 | 10 | interface ReduxAction { 11 | type: ActionType; 12 | } 13 | 14 | interface ReduxState { 15 | value: number; 16 | } 17 | const INITIAL_STATE = { value: 0 }; 18 | 19 | function counter(state: ReduxState = INITIAL_STATE, action: ReduxAction = { type: ActionType.Default }) { 20 | switch (action.type) { 21 | case ActionType.Increment: 22 | return Object.freeze({ value: state.value + 1 }); 23 | case ActionType.Decrement: 24 | return Object.freeze({ value: state.value - 1 }); 25 | default: 26 | return state; 27 | } 28 | } 29 | 30 | const store$ = new Subject(); 31 | const action$ = store$.pipe( 32 | scan(counter, INITIAL_STATE), 33 | startWith(INITIAL_STATE) 34 | ); 35 | 36 | const unsubscribe = action$.subscribe({ next: render }); 37 | 38 | function render(state: ReduxState = INITIAL_STATE) { 39 | console.log(`Rendering changes with ${state.value}`); 40 | } 41 | 42 | // Increment 43 | store$.next({ type: ActionType.Increment }); 44 | 45 | // Decrement 46 | store$.next({ type: ActionType.Decrement }); -------------------------------------------------------------------------------- /12/readme.md: -------------------------------------------------------------------------------- 1 | # Day 12 - Implementing Redux with RxJS 2 | 3 | In the [previous entry](../11/readme.md), we covered subjects, and their uses, whether it is `Subject`, `ReplaySubject`, `AsyncSubject`, and lastly the `BehaviorSubject`. This all leads up to understanding the [Redux](https://redux.js.org/) library and understanding state management and its patterns. From there, we will understand how we could implement those features from Redux using RxJS and the knowledge we've covered so far in the series. 4 | 5 | ## Intro to Redux 6 | 7 | At its heart, Redux is a state container for JavaScript apps. It became particularly popular in the React ecosystem. The library was based on the Flux architecture which introduced the concepts of stores, actions and dispatchers. Redux diverged from Flux a little bit renaming stores to reducers, no concept of a dispatcher, and relies on pure functions. 8 | 9 | Let's implement a simple counter using the concepts of Redux, with our "store" and our reducer, and how we can send messages to it. Let's add some boilerplate so that we get nicely typed experience instead of magic strings, using enums. 10 | 11 | ```typescript 12 | import { createStore } from 'redux'; 13 | 14 | enum ActionType { 15 | Increment, 16 | Decrement 17 | } 18 | 19 | interface ReduxAction { 20 | type: ActionType; 21 | } 22 | 23 | interface ReduxState { 24 | value: number; 25 | } 26 | const INITIAL_STATE = { value: 0 }; 27 | ``` 28 | 29 | Now, let's get to the heart of the matter, which is to implement the reducer function. This will react to actions as they come in, such as `Increment` or `Decrement`, and returning new state based upon the action and previous state. 30 | 31 | ```typescript 32 | function counter(state: ReduxState = INITIAL_STATE, action: ReduxAction) { 33 | switch (action.type) { 34 | case ActionType.Increment: 35 | return Object.freeze({ value: state.value + 1 }); 36 | case ActionType.Decrement: 37 | return Object.freeze({ value: state.value - 1 }); 38 | default: 39 | return state; 40 | } 41 | } 42 | ``` 43 | 44 | We can then create the store which is bound to our reducer function. This store allows us to get the current state, dispatch an action to the reducer, and subscribe to any state changes. 45 | 46 | ```typescript 47 | const store = createStore(counter); 48 | ``` 49 | 50 | We will keep track of all state changes in our `render` function, where we subscribe to changes via, surprise, the `subscribe` method. This method takes no arguments and returns no value. We can then initially render the data. 51 | 52 | ```typescript 53 | function render() { 54 | console.log(`Rendering changes with ${store.getState().value}`); 55 | } 56 | 57 | render(); 58 | const unsubscribe = store.subscribe(render); 59 | ``` 60 | 61 | Now we can increment or decrement the state via dispatching an action via the `dispatch` method. Doing so call our reducer, and then trigger our `render` function. 62 | 63 | ```typescript 64 | // Increment 65 | store.dispatch({ type: ActionType.Increment }); 66 | // Rendering changes with 1 67 | 68 | // Decrement 69 | store.dispatch({ type: ActionType.Decrement }); 70 | // Rendering changes with 0 71 | ``` 72 | 73 | You can see some similarities here to what we've already learned so far. In fact, there are some learnings that Redux got from RxJS in the [Prior Art](https://redux.js.org/introduction/prior-art#rxjs) section of the Redux site. For a full source for this example, see [`redux.ts`](redux.ts) 74 | 75 | ## Implementing Redux in RxJS 76 | 77 | Using Subjects, `startWith` and `scan`, we can implement the core concepts of Redux using RxJS directly. In fact, we could have easily implemented the main part of Redux as shown above as the following code. 78 | 79 | ```typescript 80 | import { Subject } from 'rxjs'; 81 | import { scan, startWith } from 'rxjs/operators'; 82 | 83 | const store$ = new Subject(); 84 | const subscription = store$ 85 | .pipe( 86 | startWith(initialState) 87 | scan(reducer), 88 | ) 89 | .subscribe({ 90 | next: render 91 | }); 92 | ``` 93 | 94 | So, let's look at the changes you need to make to fit it into an RxJS solution. We would need to change the `render` function since we do not have access to the current value, instead we get the new state passed into it. The other change is using `next` to dispatch our actions to our reducer instead of the `dispatch` method. 95 | 96 | ```typescript 97 | const store$ = new Subject(); 98 | const action$ = store$.pipe( 99 | scan(counter, INITIAL_STATE), 100 | startWith(INITIAL_STATE) 101 | ); 102 | 103 | const unsubscribe = action$.subscribe({ next: render }); 104 | 105 | function render(state: ReduxState = INITIAL_STATE) { 106 | console.log(`Rendering changes with ${state.value}`); 107 | } 108 | 109 | // Increment 110 | store$.next({ type: ActionType.Increment }); 111 | // Rendering changes with 1 112 | 113 | // Decrement 114 | store$.next({ type: ActionType.Decrement }); 115 | // Rendering changes with 0 116 | ``` 117 | 118 | That's it for the basic implementation of Redux using RxJS, but we will dive further into Redux and async actions soon enough! Stay tuned! 119 | -------------------------------------------------------------------------------- /12/redux.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | 3 | enum ActionType { 4 | Increment, 5 | Decrement 6 | } 7 | 8 | interface ReduxAction { 9 | type: ActionType; 10 | } 11 | 12 | interface ReduxState { 13 | value: number; 14 | } 15 | const INITIAL_STATE = { value: 0 }; 16 | 17 | function counter(state: ReduxState = INITIAL_STATE, action: ReduxAction) { 18 | switch (action.type) { 19 | case ActionType.Increment: 20 | return Object.freeze({ value: state.value + 1 }); 21 | case ActionType.Decrement: 22 | return Object.freeze({ value: state.value - 1 }); 23 | default: 24 | return state; 25 | } 26 | } 27 | 28 | const store = createStore(counter); 29 | 30 | function render() { 31 | console.log(`Rendering changes with ${JSON.stringify(store.getState())}`); 32 | } 33 | 34 | render(); 35 | const unsubscribe = store.subscribe(render); 36 | 37 | // Increment 38 | store.dispatch({ type: ActionType.Increment }); 39 | 40 | // Decrement 41 | store.dispatch({ type: ActionType.Decrement }); -------------------------------------------------------------------------------- /13/index.ts: -------------------------------------------------------------------------------- 1 | import { combineLatest, merge, interval, of, zip } from 'rxjs'; 2 | import { map, take, withLatestFrom } from 'rxjs/operators'; 3 | 4 | /* 5 | const num1$ = of(1, 2, 3); 6 | const num2$ = of('foo', 'bar', 'baz'); 7 | 8 | const num$ = zip(num1$, num2$); 9 | 10 | num$.subscribe({ next: x => console.log(`Data: ${x}`) }); 11 | */ 12 | /* 13 | const num1$ = interval(1000).pipe(map(x => `First: ${x}`), take(3)); 14 | const num2$ = interval(1500).pipe(map(x => `Second: ${x}`), take(3)); 15 | const num3$ = interval(500).pipe(map(x => `Third: ${x}`), take(3)); 16 | 17 | const num$ = combineLatest(num1$, num2$, num3$); 18 | num$.subscribe({ next: x => console.log(`${x}`) }); 19 | */ 20 | 21 | const num1$ = interval(1000).pipe(map(x => `First: ${x}`), take(3)); 22 | const num2$ = interval(1500).pipe(map(x => `Second: ${x}`), take(3)); 23 | const num3$ = interval(500).pipe(map(x => `Third: ${x}`), take(3)); 24 | 25 | const num$ = num1$.pipe(withLatestFrom(num2$, num3$)); 26 | num$.subscribe({ next: x => console.log(`${x}`) }); -------------------------------------------------------------------------------- /13/readme.md: -------------------------------------------------------------------------------- 1 | # Day 13 - Combining Sequences Part 1 2 | 3 | In the [previous entry](../12/readme.md), we covered Redux and the Flux architecture, and how you could implement it using nothing but RxJS! We will at some point revisit this with, especially with async actions, but for now, let's move on to a new subject, combining sequences. In this post, we will cover how we could combine sequences using operators like `zip`, and `combineLatest`. 4 | 5 | ## Zipping together sequences with zip 6 | 7 | The first operator we will look at is `zip`. This operator takes any number of sequences, and zips them together into a new Observable containing the elements from both Observables at the same position. We could write this easily for Iterables such as the following code where it emits values only when both sides have values, and if one side is shorter than the other, the longer side is truncated. 8 | 9 | ```typescript 10 | function* zip(source1: Iterable, source2: Iterable): Iterable> { 11 | let it1 = source[Symbol.iterator](), it2 = source[Symbol.iterator](); 12 | while (true) { 13 | let next1 = it1.next(), next2 = it2.next(); 14 | if (next1.done || next2.done) { 15 | break; 16 | } 17 | 18 | yield [next1.value, next2.value]; 19 | } 20 | } 21 | 22 | const results = zip([1, 2, 3], [4, 5, 6]); 23 | for (let item of results) { 24 | console.log(item); 25 | } 26 | // [1, 4] 27 | // [2, 5] 28 | // [3, 6] 29 | ``` 30 | 31 | With Observables, we get the same semantics but with push collections instead of pull. 32 | 33 | ```typescript 34 | import { of, zip } from 'rxjs'; 35 | 36 | const num1$ = of(1, 2, 3); 37 | const num2$ = of(4, 5, 6); 38 | 39 | const result$ = zip(num1$, num2$); 40 | const subscription = result$.subscribe({ 41 | next: item => console.log(item) 42 | }); 43 | // [1, 4] 44 | // [2, 5] 45 | // [3, 6] 46 | ``` 47 | 48 | ## Zipping together with combineLatest 49 | 50 | But dealing with concurrent collections, it's not super reasonable to ensure that both collections emit values at the same time at the same index. Instead, we have `combineLatest` which emits when both sides have a value, and as the value on any of the sequences change. Let's walk through an example using Observables using `interval` at different times, taking 3 of each. 51 | 52 | ```typescript 53 | import { combineLatest, interval } from 'rxjs'; 54 | import { map, take } from 'rxjs/operators'; 55 | 56 | const num1$ = interval(1000).pipe(map(item => `First ${item}`), take(3)); 57 | const num2$ = interval(1500).pipe(map(item => `Second ${item}`), take(3)); 58 | const num3$ = interval(500).pipe(map(item => `Third ${item}`), take(3)); 59 | 60 | const num$ = combineLatest(num1$, num2$, num3$); 61 | num$.subscribe({ next: item => console.log(item) }); 62 | // [First: 0, Second: 0, Third: 1] 63 | // [First: 0, Second: 0, Third: 2] 64 | // [First: 1, Second: 0, Third: 2] 65 | // [First: 1, Second: 1, Third: 2] 66 | // [First: 2, Second: 1, Third: 2] 67 | // [First: 2, Second: 2, Third: 2] 68 | ``` 69 | 70 | What's interesting about this approach is that by the time the first two Observables are ready to omit, the third Observable has already yielded its second value. 71 | 72 | ## Zipping with withLatestFrom 73 | 74 | The third way of combining Observables is `withLatestFrom`, which combines the source Observable with other Observable to create an Observable whose values are calculated from the latest values of each, only when the source emits. Let's look at how this differs from our `combineLatest` from above. 75 | 76 | ```typescript 77 | import { interval } from 'rxjs'; 78 | import { map, take, withLatestFrom } from 'rxjs/operators'; 79 | 80 | const num1$ = interval(1000).pipe(map(item => `First ${item}`), take(3)); 81 | const num2$ = interval(1500).pipe(map(item => `Second ${item}`), take(3)); 82 | const num3$ = interval(500).pipe(map(item => `Third ${item}`), take(3)); 83 | 84 | const num$ = num1$.pipe(withLatestFrom(num2$, num3$)); 85 | num$.subscribe({ next: item => console.log(item) }); 86 | // First: 1, Second: 0, Third: 2 87 | // First: 2, Second: 1, Third: 2 88 | ``` 89 | 90 | To explain the output here, by the time the second Observable emits its first, the source itself has moved on to second item, and the third Observable has already finished. That's enough for today, but check back tomorrow when we work on `merge` versus `concat` and combining sequences. -------------------------------------------------------------------------------- /14/index.ts: -------------------------------------------------------------------------------- 1 | import { concat, interval, merge, of } from 'rxjs'; 2 | import { map, take } from 'rxjs/operators'; 3 | 4 | /* 5 | const num1$ = of(1, 2); 6 | const num2$ = of(3, 4); 7 | const num3$ = of(5, 6); 8 | 9 | const num$ = concat(num1$, num2$, num3$); 10 | const subscription = num$.subscribe({ 11 | next: item => console.log(item) 12 | }); 13 | */ 14 | 15 | const num1$ = interval(1000).pipe(map(x => `First: ${x}`), take(3)); 16 | const num2$ = interval(1500).pipe(map(x => `Second: ${x}`), take(3)); 17 | const num3$ = interval(500).pipe(map(x => `Third: ${x}`), take(3)); 18 | 19 | const num$ = merge(num1$, num2$, num3$, 1); 20 | const subscription = num$.subscribe({ 21 | next: item => console.log(item) 22 | }); -------------------------------------------------------------------------------- /14/readme.md: -------------------------------------------------------------------------------- 1 | # Day 14 - Combining Sequences Part Deux 2 | 3 | In the [previous entry](../13/readme.md), we covered combining Observable sequences together using `zip`, `combineLatest` and `withLatestFrom`. Today, we'll be looking at two other ways of combining sequences, with `concat` and `merge`. 4 | 5 | ## Combining sequences with concat 6 | 7 | The first operator we will look at today is `concat`, which is to combine the sequences, by waiting until the end before starting the next sequence. This keeps all values in order from the sequences no matter when they were emitted. Let's walk through an example of `concat`. 8 | 9 | ```typescript 10 | import { concat, of } from 'rxjs'; 11 | 12 | const num1$ = of(1, 2); 13 | const num2$ = of(3, 4); 14 | const num3$ = of(5, 6); 15 | 16 | const num$ = concat(num1$, num2$, num3$); 17 | const subscription = num$.subscribe({ 18 | next: item => console.log(item) 19 | }); 20 | // 1 21 | // 2 22 | // 3 23 | // 4 24 | // 5 25 | // 6 26 | ``` 27 | 28 | As you will notice, the next sequence is triggered by the previous sequence finishing, thus putting the sequences in order. But, we're dealing with collections over time, so when we're dealing with events such as move movements, that's not super useful, so let's do a concurrent combining of sequences. 29 | 30 | ## Combining sequences with merge 31 | 32 | In order to do concurrent combining of sequences into a single sequence, we have the `merge` operator. This allows us to merge any number of Observables, but also control the concurrency via adding a Scheduler, or a maximum number of concurrent sequences. 33 | 34 | ```typescript 35 | merge(...args: Observable[]): Observable 36 | merge(...args: Observable[], maxConcurrent: number): Observable; 37 | merge(...args: Observable[], scheduler: SchedulerLike): Observable; 38 | ``` 39 | 40 | To make this more concrete, let's take the `interval` calls from the previous lesson, and merge them together. Note how the sequences are combined with the third sequence firing first, followed by the first, and then the second. 41 | 42 | ```typescript 43 | import { interval, merge } from 'rxjs'; 44 | import { map, take } from 'rxjs/operators'; 45 | 46 | const num1$ = interval(1000).pipe(map(x => `First: ${x}`), take(3)); 47 | const num2$ = interval(1500).pipe(map(x => `Second: ${x}`), take(3)); 48 | const num3$ = interval(500).pipe(map(x => `Third: ${x}`), take(3)); 49 | 50 | const num$ = merge(num1$, num2$, num3$); 51 | const subscription = num$.subscribe({ 52 | next: item => console.log(item) 53 | }); 54 | // Third: 0 55 | // First: 0 56 | // Third: 1 57 | // Second: 0 58 | // Third: 2 59 | // First: 1 60 | // Second: 1 61 | // First: 2 62 | // Second: 2 63 | ``` 64 | 65 | We could control the amount of concurrency with `merge` via the last parameter. In fact, we could turn `merge` into `concat` by setting the max concurrency at 1. 66 | 67 | ```typescript 68 | import { interval, merge } from 'rxjs'; 69 | import { map, take } from 'rxjs/operators'; 70 | 71 | const num1$ = interval(1000).pipe(map(x => `First: ${x}`), take(3)); 72 | const num2$ = interval(1500).pipe(map(x => `Second: ${x}`), take(3)); 73 | const num3$ = interval(500).pipe(map(x => `Third: ${x}`), take(3)); 74 | 75 | const num$ = merge(num1$, num2$, num3$, 1); 76 | const subscription = num$.subscribe({ 77 | next: item => console.log(item) 78 | }); 79 | // First: 0 80 | // First: 1 81 | // First: 2 82 | // Second: 0 83 | // Second: 1 84 | // Second: 2 85 | // Third: 0 86 | // Third: 1 87 | // Third: 2 88 | ``` 89 | 90 | Despite the sequences themselves being asynchronous, we didn't subscribe to the next one until the first sequence was finished. Now that we have some of the basics down of combining sequences, we will get into the whole `mergeAll`, `concatAll`, and of course all the *Map operators like `concatMap`, `mergeMap`, `switchMap` and others. Stay tuned! -------------------------------------------------------------------------------- /15/index.ts: -------------------------------------------------------------------------------- 1 | import { interval, of } from 'rxjs'; 2 | import { concatAll, map, mergeAll, take } from 'rxjs/operators'; 3 | 4 | /* 5 | const num$ = of( 6 | of(1, 2, 3), 7 | of(4, 5, 6), 8 | of(7, 8, 9) 9 | ); 10 | */ 11 | 12 | const num1$ = interval(1000).pipe(map(x => `First: ${x}`), take(3)); 13 | const num2$ = interval(1500).pipe(map(x => `Second: ${x}`), take(3)); 14 | const num3$ = interval(500).pipe(map(x => `Third: ${x}`), take(3)); 15 | const num$ = of(num1$, num2$, num3$); 16 | 17 | const result$ = num$.pipe(mergeAll(1)); 18 | 19 | result$.subscribe({ next: item => console.log(item) }); 20 | 21 | -------------------------------------------------------------------------------- /15/readme.md: -------------------------------------------------------------------------------- 1 | # Day 15 - Combining Sequences of Sequences 2 | 3 | In the [previous entry](../14/readme.md), we covered combining sequences with `concat` and `merge`. Today we're going to dive a bit deeper into `merge` and `concat` with how we combine sequence of sequences. 4 | 5 | ## Smooshing a sequence of sequences 6 | 7 | In the previous section, we demonstrated `concat` which took a number of sequences and smooshed them together in order. But, what if we had a sequence of sequences, for example `Observable>` that we want to smoosh into a single sequence? We have several ways of doing that, such as we noted, whether we want concurrent or single instance at a time. In the case of `concatAll`, it only allows one sequence at a time. 8 | 9 | ```typescript 10 | import { of } from 'rxjs'; 11 | import { concatAll } from 'rxjs/operators'; 12 | 13 | const num1$ = of(1, 2); 14 | const num2$ = of(3, 4); 15 | const num3$ = of(5, 6); 16 | 17 | const num$ = of(num1$, num2$, num3$).pipe(concatAll()); 18 | 19 | const subscription = num$.subscribe({ 20 | next: item => console.log(item); 21 | }); 22 | // 1 23 | // 2 24 | // 3 25 | // 4 26 | // 5 27 | // 6 28 | ``` 29 | 30 | ## Concurrent smooshes with mergeAll 31 | 32 | As I've said before, when dealing with infinite sequences, chances are that `concatAll` isn't going to help us because it expects an end to the sequence. For this scenario, we have `mergeAll`, where we can give it as many inner sequences as we wish. 33 | 34 | ```typescript 35 | import { interval, of } from 'rxjs'; 36 | import { map, mergeAll, take } from 'rxjs/operators'; 37 | 38 | const num1$ = interval(1000).pipe(map(x => `First: ${x}`), take(3)); 39 | const num2$ = interval(1500).pipe(map(x => `Second: ${x}`), take(3)); 40 | const num3$ = interval(500).pipe(map(x => `Third: ${x}`), take(3)); 41 | const num$ = of(num1$, num2$, num3$); 42 | 43 | const result$ = num$.pipe(mergeAll()); 44 | 45 | result$.subscribe({ next: item => console.log(item) }); 46 | // Third: 0 47 | // First: 0 48 | // Third: 1 49 | // Second: 0 50 | // Third: 2 51 | // First: 1 52 | // Second: 1 53 | // First: 2 54 | // Second: 2 55 | ``` 56 | 57 | In addition, we can specify the limit of concurrency for the inner sequence subscriptions. For example, we could limit it to 1, which would be the same as `concatAll`. 58 | 59 | ```typescript 60 | const result$ = num$.pipe(mergeAll(1)); 61 | 62 | result$.subscribe({ next: item => console.log(item) }); 63 | // First: 0 64 | // First: 1 65 | // First: 2 66 | // Second: 0 67 | // Second: 1 68 | // Second: 2 69 | // Third: 0 70 | // Third: 1 71 | // Third: 2 72 | ``` 73 | 74 | So, now we have a basic handle on smooshing and combining sequences together, so we'll take the next step to combine sequences via `mergeMap` and `concatMap`. -------------------------------------------------------------------------------- /16/index.ts: -------------------------------------------------------------------------------- 1 | import { from, fromEvent, of } from 'rxjs'; 2 | import { concatMap, delay, map, mergeMap } from 'rxjs/operators'; 3 | import axios from 'axios'; 4 | import { EventEmitter } from 'events'; 5 | 6 | /* 7 | const num$ = of(1, 2, 3).pipe( 8 | concatMap(item => of(item).pipe( 9 | delay(item * 1000), 10 | map(item => `Item ${item} delayed by ${item * 1000} ms`) 11 | )) 12 | ); 13 | 14 | const subscription = num$.subscribe({ 15 | next: item => console.log(item) 16 | }); 17 | */ 18 | 19 | async function searchWikipedia(term: string): Promise> { 20 | const response = await axios.get('http://en.wikipedia.org/w/api.php', { 21 | params: { 22 | action: 'opensearch', 23 | format: 'json', 24 | search: term 25 | } 26 | }); 27 | return response.data[1]; 28 | } 29 | 30 | const eventEmitter = new EventEmitter(); 31 | const value$ = fromEvent(eventEmitter, 'data'); 32 | 33 | const result$ = value$.pipe( 34 | mergeMap(searchWikipedia) 35 | ); 36 | 37 | result$.subscribe({ 38 | next: item => console.log(`First match ${item[0]}`) 39 | }); 40 | 41 | eventEmitter.emit('data', 'react'); 42 | eventEmitter.emit('data', 'reacti'); 43 | eventEmitter.emit('data', 'reactive'); -------------------------------------------------------------------------------- /16/readme.md: -------------------------------------------------------------------------------- 1 | # Day 16 - Projecting to new sequences 2 | 3 | In the [previous entry](../15/readme.md), we covered how we can smoosh a sequence of sequences with both `concatAll` and `mergeAll`. In this entry, we'll explore some of the most common operators you'll encounter with projecting a value to a new sequence via `mergeMap`, and `concatMap`. These operators, as you might think, are simply a combination of the `map` operator, which produces the Observable of Observables, and then it's up to us how we combine those sequences into a single sequence, whether concurrent or not. 4 | 5 | The reason why these operators are important is because we need ways to compose a value from our current Observable, for example a stream of text values as you type, and then you want to take that value and query a service which is also an Observable or Promise that we'd want to flatten into a single answer. These operators are all aptly named *Map operators, where the style of combining is the first part of the name whether it is `mergeMap`, `concatMap`, or the ever important `switchMap` which we will get into in the next post. 6 | 7 | ## Projecting a new value with concatMap 8 | 9 | The first one we will look at is `concatMap`, which is a combination of the previous operators we've covered, `map`, and `concatAll`. In this first example, we'll take a value from our list, and delay the value by a second times its value by using the `delay` operator and then mapping it so we have a nice value to show how long each was delayed by. We're diving into the deeper end of composition, but I want to give the idea of the art of the possible. 10 | 11 | ```typescript 12 | import { of } from 'rxjs'; 13 | import { concatMap, delay, map } from 'rxjs/operators'; 14 | 15 | const num$ = of(1, 2, 3).pipe( 16 | concatMap(item => of(item).pipe( 17 | delay(item * 1000), 18 | map(item => `Item ${item} delayed by ${item * 1000} ms`) 19 | )) 20 | ); 21 | 22 | const subscription = num$.subscribe({ 23 | next: item => console.log(item) 24 | }); 25 | // Item 1 delayed by 1000ms 26 | // Item 2 delayed by 2000ms 27 | // Item 3 delayed by 3000ms 28 | ``` 29 | 30 | ## Projecting a new value with mergeMap 31 | 32 | Now that we have a basic understanding of `concatMap`, let's talk about a more concurrent approach where we can take the data as it comes in and project it to the new sequence. For example, we could query Wikipedia with Axios using Node.js with events from an `EventEmitter`. Remember, a `Promise` that is returned from this will be converted automatically into an Observable. 33 | 34 | ```typescript 35 | import { fromEvent } from 'rxjs'; 36 | import { mergeMap } from 'rxjs/operators'; 37 | import axios from 'axios'; 38 | import { EventEmitter } from 'events'; 39 | 40 | async function searchWikipedia(term: string): Promise> { 41 | const response = await axios.get('http://en.wikipedia.org/w/api.php', { 42 | params: { 43 | action: 'opensearch', 44 | format: 'json', 45 | search: term 46 | } 47 | }); 48 | return response.data[1]; 49 | } 50 | 51 | const eventEmitter = new EventEmitter(); 52 | const value$ = fromEvent(eventEmitter, 'data'); 53 | 54 | const result$ = value$.pipe( 55 | mergeMap(searchWikipedia) 56 | ); 57 | 58 | result$.subscribe({ 59 | next: item => console.log(`First match ${item[0]}`) 60 | }); 61 | 62 | eventEmitter.emit('data', 'react'); 63 | eventEmitter.emit('data', 'reacti'); 64 | eventEmitter.emit('data', 'reactive'); 65 | // First match Reactive 66 | // First match React 67 | // First match Reactive oxygen species 68 | ``` 69 | 70 | We can then emit three queries at once and see the results come in. Note of course, since this is a merge operation, the results can come in any order, unlike with `concatMap`. So, the question is, how would we fix it so that we only get the latest and greatest query and cancel the rest? That's for the next lesson! Stay tuned! -------------------------------------------------------------------------------- /17/index.ts: -------------------------------------------------------------------------------- 1 | import { fromEvent } from 'rxjs'; 2 | import { mergeMap, switchMap } from 'rxjs/operators'; 3 | import axios from 'axios'; 4 | import { EventEmitter } from 'events'; 5 | 6 | async function searchWikipedia(term: string) {//: Promise> { 7 | const response = await axios.get('http://en.wikipedia.org/w/api.php', { 8 | params: { 9 | action: 'opensearch', 10 | format: 'json', 11 | search: term 12 | } 13 | }); 14 | return response.data[1]; 15 | } 16 | 17 | const eventEmitter = new EventEmitter(); 18 | const value$ = fromEvent(eventEmitter, 'data'); 19 | 20 | const result$ = value$.pipe( 21 | switchMap(searchWikipedia) 22 | ); 23 | 24 | result$.subscribe({ 25 | next: item => console.log(`First match ${item[0]}`) 26 | }); 27 | 28 | eventEmitter.emit('data', 'reacti'); 29 | eventEmitter.emit('data', 'reactive'); 30 | eventEmitter.emit('data', 'react'); 31 | eventEmitter.emit('data', 'foo'); 32 | eventEmitter.emit('data', 'bar'); -------------------------------------------------------------------------------- /17/readme.md: -------------------------------------------------------------------------------- 1 | # Day 17 - Getting only the latest data 2 | 3 | In the [previous entry](../16/readme.md), we covered both `concatMap` and `mergeMap`, how we compose together operations such as querying Wikipedia with queries coming from events. This is the start of an autosuggest scenario where we want to give the user some suggestions when they are typing, giving them an idea of what results they can expect. 4 | 5 | Making sure the user has the right data is key in the autosuggest scenario. As we've discovered, `mergeMap` allows us to query an external source, but unlike `concatMap`, the order of the results isn't guaranteed. For example, I could query for "react", and then "reactive", there is no guarantee that "reactive" comes back last, thus confusing the user. To get around this issue, what if we could cancel the previous request and ensure that we do indeed have the latest value? That's where the `switchMap` operator comes into play. 6 | 7 | Let's review of where we were with our autocomplete scenario from before with using `mergeMap`, searching Wikipedia with our terms from our `EventEmitter` and its `data` event. 8 | 9 | ```typescript 10 | import { fromEvent } from 'rxjs'; 11 | import { mergeMap } from 'rxjs/operators'; 12 | import axios from 'axios'; 13 | import { EventEmitter } from 'events'; 14 | 15 | async function searchWikipedia(term: string): Promise> { 16 | const response = await axios.get('http://en.wikipedia.org/w/api.php', { 17 | params: { 18 | action: 'opensearch', 19 | format: 'json', 20 | search: term 21 | } 22 | }); 23 | return response.data[1]; 24 | } 25 | 26 | const eventEmitter = new EventEmitter(); 27 | const value$ = fromEvent(eventEmitter, 'data'); 28 | 29 | const result$ = value$.pipe( 30 | mergeMap(searchWikipedia) 31 | ); 32 | 33 | result$.subscribe({ 34 | next: item => console.log(`First match ${item[0]}`) 35 | }); 36 | 37 | eventEmitter.emit('data', 'reacti'); 38 | eventEmitter.emit('data', 'reactive'); 39 | eventEmitter.emit('data', 'react'); 40 | // First match Reactive oxygen species 41 | // First match Reactive 42 | // First match React 43 | ``` 44 | 45 | As you may notice, we have our results out of order. In an autosuggest scenario, the last thing you'd want to do is update the user with old data, so we need to cancel the previous request. We can do that with the `switchMap`, which unsubscribes from the previous Observable and tears down the logic, for example, cancelling a `fetch` API call. 46 | 47 | ```typescript 48 | import { fromEvent } from 'rxjs'; 49 | import { switchMap } from 'rxjs/operators'; 50 | import axios from 'axios'; 51 | import { EventEmitter } from 'events'; 52 | 53 | async function searchWikipedia(term: string): Promise> { 54 | const response = await axios.get('http://en.wikipedia.org/w/api.php', { 55 | params: { 56 | action: 'opensearch', 57 | format: 'json', 58 | search: term 59 | } 60 | }); 61 | return response.data[1]; 62 | } 63 | 64 | const eventEmitter = new EventEmitter(); 65 | const value$ = fromEvent(eventEmitter, 'data'); 66 | 67 | const result$ = value$.pipe( 68 | switchMap(searchWikipedia) 69 | ); 70 | 71 | result$.subscribe({ 72 | next: item => console.log(`First match ${item[0]}`) 73 | }); 74 | 75 | eventEmitter.emit('data', 'reacti'); 76 | eventEmitter.emit('data', 'reactive'); 77 | eventEmitter.emit('data', 'react'); 78 | // First match React 79 | ``` 80 | 81 | Now, what you'll notice is that we're not getting three responses back, and instead only one, that with our query of `react` returned `React`, which is exaclty what we need. We can further enhance this scenario with the events to make them less noisy such as debouncing the user input, and ensuring we're only querying when the data has in fact changed. We'll talk about that in the next post! Stay tuned! -------------------------------------------------------------------------------- /18/index.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | import { distinct, distinctUntilChanged, distinctUntilKeyChanged } from 'rxjs/operators'; 3 | /* 4 | const num$ = new Subject(); 5 | 6 | num$ 7 | .pipe(distinctUntilChanged()) 8 | .subscribe({ next: item => console.log(`Distinct Item: ${item}`) }); 9 | 10 | num$.next(1); 11 | num$.next(1); 12 | num$.next(2); 13 | num$.next(1); 14 | num$.next(2); 15 | num$.next(2); 16 | 17 | */ 18 | 19 | interface Person { name: string } 20 | 21 | const person$ = new Subject(); 22 | 23 | person$ 24 | .pipe(distinctUntilKeyChanged('name')) 25 | .subscribe({ next: item => console.log(`Distinct Item: ${item.name}`) }); 26 | 27 | person$.next({ name: 'Bob' }); 28 | // Distinct Item: Bob 29 | person$.next({ name: 'Bob' }); 30 | person$.next({ name: 'Mary' }); 31 | // Distinct Item: Mary 32 | person$.next({ name: 'Mary' }); 33 | person$.next({ name: 'Frank' }); 34 | // Distinct Item: Frank 35 | -------------------------------------------------------------------------------- /18/readme.md: -------------------------------------------------------------------------------- 1 | # Day 18 - Getting distinct data 2 | 3 | In the [previous entry](../17/readme.md), we explored how we can create an autosuggest scenario based upon the user input, such as coming from an event stream. Today, we're going to look at how you can get unique data, as well as unique data until the data changes. 4 | 5 | ## Only distinct data 6 | 7 | The first operator we're going to look at is getting distinct data via the `distinct` operator. This allows us to get data that when compared to others is distinct. For example, we can get distinct numbers. 8 | 9 | ```typescript 10 | import { Subject } from 'rxjs'; 11 | import { distinct } from 'rxjs/operators'; 12 | 13 | const num$ = new Subject(); 14 | 15 | num$ 16 | .pipe(distinct()) 17 | .subscribe({ next: item => console.log(`Distinct Item: ${item}`) }); 18 | 19 | num$.next(1); 20 | // Distinct Item 1 21 | num$.next(2); 22 | // Distinct Item 2 23 | num$.next(1); 24 | num$.next(2); 25 | num$.next(3); 26 | // Distinct Item 3 27 | ``` 28 | 29 | As you'll notice, it's emitting only distinct values as they come along in our stream, getting rid of duplicates. Now, it's not super realistic obviously to have just number data, but what if instead we allowed you to pick a key to use as the distinct selector? 30 | 31 | ```typescript 32 | interface Person { name: string } 33 | 34 | const person$ = new Subject(); 35 | 36 | person$ 37 | .pipe(distinct(item => item.name)) 38 | .subscribe({ next: item => console.log(`Distinct Item: ${item.name}`) }); 39 | 40 | person$.next({ name: 'Bob' }); 41 | // Distinct Item: Bob 42 | person$.next({ name: 'Mary' }); 43 | // Distinct Item: Mary 44 | person$.next({ name: 'Bob' }); 45 | person$.next({ name: 'Frank' }); 46 | // Distinct Item: Frank 47 | ``` 48 | 49 | Note that since this is collecting distinct values in an internal hash set, this could grow infinitely large in size over time. There is an optional overload which takes an Observable, which when fires, clears the internal cache. For example, we could specify an Observable that fires every five seconds, which clears the cache, so we have a rolling window of distinct values. 50 | 51 | ```typescript 52 | import { Subject, interval } from 'rxjs'; 53 | import { distinct } from 'rxjs/operators'; 54 | 55 | const num$ = new Subject(); 56 | 57 | num$ 58 | .pipe(distinct(undefined, interval(5000))) 59 | .subscribe({ next: item => console.log(`Distinct Item: ${item}`) }); 60 | ``` 61 | 62 | ## Streaming distinct 63 | 64 | Having unique values is definitely very useful in reactive programming, but what about firing only when the data changes from the previous value? An example of this would be capturing keyboard input from a DOM element and projecting the text from the input. The problem of course is that you could hit the arrow keys, and although the 'keyup' event is firing, our data has not in fact changed. 65 | 66 | ```typescript 67 | import { fromEvent } from 'rxjs'; 68 | import { distinctUntilChanged, map } from 'rxjs/operators'; 69 | 70 | const input$ = fromEvent(document.querySelector('#input', 'keyup')) 71 | .pipe( 72 | map(ev => ev.target.value), 73 | distinctUntilChanged() 74 | ); 75 | ``` 76 | 77 | This will then filter out the unwanted noise from the keys and other items that don't actually change the value at all. We can demonstrate that here as well with a `Subject` emitting data. 78 | 79 | ```typescript 80 | import { Subject } from 'rxjs'; 81 | import { distinctUntilChanged } from 'rxjs/operators'; 82 | 83 | const num$ = new Subject(); 84 | 85 | num$ 86 | .pipe(distinctUntilChanged()) 87 | .subscribe({ next: item => console.log(`Distinct Item: ${item}`) }); 88 | 89 | num$.next(1); 90 | // Distinct Item: 1 91 | num$.next(1); 92 | num$.next(2); 93 | // Distinct Item: 2 94 | num$.next(1); 95 | // Distinct Item: 1 96 | num$.next(2); 97 | // Distinct Item: 2 98 | num$.next(2); 99 | num$.next(2); 100 | ``` 101 | 102 | You'll note from this example that we only emit when our new data is different from our previous, and not overall distinct. We can also give a comparator function which allows us to compare the previous and the current item to see if they are equal. 103 | 104 | ```typescript 105 | interface Person { name: string } 106 | 107 | const person$ = new Subject(); 108 | 109 | person$ 110 | .pipe(distinctUntilChanged((prev, curr) => prev.name === curr.name)) 111 | .subscribe({ next: item => console.log(`Distinct Item: ${item.name}`) }); 112 | 113 | person$.next({ name: 'Bob' }); 114 | // Distinct Item: Bob 115 | person$.next({ name: 'Bob' }); 116 | person$.next({ name: 'Mary' }); 117 | // Distinct Item: Mary 118 | person$.next({ name: 'Mary' }); 119 | person$.next({ name: 'Frank' }); 120 | // Distinct Item: Frank 121 | ``` 122 | 123 | Finally, there is another way of comparing with a given key, for example, we could extract the 'name' from our data and use that as a comparator. 124 | 125 | ```typescript 126 | interface Person { name: string } 127 | 128 | const person$ = new Subject(); 129 | 130 | person$ 131 | .pipe(distinctUntilKeyChanged('name')) 132 | .subscribe({ next: item => console.log(`Distinct Item: ${item.name}`) }); 133 | 134 | person$.next({ name: 'Bob' }); 135 | // Distinct Item: Bob 136 | person$.next({ name: 'Bob' }); 137 | person$.next({ name: 'Mary' }); 138 | // Distinct Item: Mary 139 | person$.next({ name: 'Mary' }); 140 | person$.next({ name: 'Frank' }); 141 | // Distinct Item: Frank 142 | ``` 143 | 144 | So, this is one technique for taming user input for our autosuggest scenario. We'll cover time based ones tomorrow, so stay tuned! 145 | -------------------------------------------------------------------------------- /19/index.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { fromEvent } from 'rxjs'; 3 | import { throttleTime, debounceTime } from 'rxjs/operators'; 4 | 5 | const emitter = new EventEmitter(); 6 | 7 | function throttle(delay: number, fn: Function) { 8 | let lastCall = 0; 9 | return function (...args: any[]) { 10 | const now = +new Date(); 11 | if (now - lastCall < delay) { 12 | return; 13 | } 14 | lastCall = now; 15 | return fn(...args); 16 | } 17 | } 18 | 19 | function debounce(delay: number, fn: Function) { 20 | let timerId: NodeJS.Timeout; 21 | return function debounceFunction(...args: any[]) { 22 | if (timerId) { 23 | clearTimeout(timerId); 24 | } 25 | timerId = setTimeout(() => { 26 | fn(...args); 27 | timerId = null; 28 | }, delay); 29 | } 30 | } 31 | 32 | const event$ = fromEvent(emitter, 'data') 33 | .pipe(debounceTime(500)); 34 | 35 | const subscription = event$.subscribe({ 36 | next: item => console.log(`Next: ${item}`) 37 | }); 38 | 39 | emitter.emit('data', 'foo'); 40 | emitter.emit('data', 'bar'); 41 | emitter.emit('data', 'baz'); -------------------------------------------------------------------------------- /19/readme.md: -------------------------------------------------------------------------------- 1 | # Day 19 - Throttling and Debouncing Observables 2 | 3 | In the [previous entry](../18/readme.md), we talked about taming user input via the `distinct` and `distinctUntilChanged` operators. Today, we're going to look at another aspect of taming user input, this time with `throttle` and `debounce`. 4 | 5 | ## What's the Difference 6 | 7 | There's a fundamental difference between `throttle` and `debounce`, although the words are similar in nature. 8 | 9 | Throttle 10 | > The throttle operation enforces a maximum number of times a function can be called over time. For example, an HTTP request can be made at most once every 500 milliseconds. 11 | 12 | Debounce 13 | > The debounce operation enforces that a function is not called again until a certain amount of time has passed without it being called. For example, make an HTTP request if only 500 milliseconds have passed without it being called. 14 | 15 | ## Throttle 16 | 17 | Now that we have the basic understanding of what `throttle` is versus `debounce`, let's look at a sample implementation of it in JavaScript. First we'll need to determine when the function was called. Then we'll return the throttled function which takes the current time and checks if it's within the time constraint. If it is still within the window, the function exits without invoking the function. Otherwise, the last time the function is invoked is updated and finally the function is invoked. 18 | 19 | ```typescript 20 | function throttle(delay: number, fn: Function) { 21 | let lastCall = 0; 22 | return function throttledFunction(...args: any[]) { 23 | const now = +new Date(); 24 | if (now - lastCall < delay) { 25 | return; 26 | } 27 | lastCall = now; 28 | return fn(...args); 29 | } 30 | } 31 | 32 | import { EventEmitter } from 'events'; 33 | 34 | const emitter = new EventEmitter(); 35 | 36 | const dataHandler = (arg: string) => console.log(`Next: ${arg}`); 37 | const throttledHandler = throttle(500, dataHandler); 38 | 39 | emitter.addListener('data', throttledHandler); 40 | ``` 41 | 42 | In RxJS, we have two ways of dealing with throttling behavior, either by throttling by time in `throttleTime`, or by throttling by another Observable via `throttle`. We can model our example above using RxJS the very same way. 43 | 44 | ```typescript 45 | import { fromEvent } from 'rxjs'; 46 | import { throttleTime } from 'rxjs/operators'; 47 | import { EventEmitter } from 'events'; 48 | 49 | const emitter = new EventEmitter(); 50 | 51 | const event$ = fromEvent(emitter, 'data') 52 | .pipe(throttleTime(500)); 53 | 54 | const subscription = event$.subscribe({ 55 | next: item => console.log(`Next: ${item}`) 56 | }); 57 | 58 | emitter.emit('data', 'foo'); 59 | // Next: foo 60 | emitter.emit('data', 'bar'); 61 | emitter.emit('data', 'baz'); 62 | ``` 63 | 64 | We also have the opportunity to put in any Observable we wish as the throttling agent, for example, it could use the `interval` operator, or even our own custom `Subject` by using the `throttle` operator. 65 | 66 | ```typescript 67 | import { fromEvent, Subject } from 'rxjs'; 68 | import { throttle } from 'rxjs/operators'; 69 | import { EventEmitter } from 'events'; 70 | 71 | const emitter = new EventEmitter(); 72 | 73 | const throttle$ = new Subject(); 74 | 75 | const event$ = fromEvent(emitter, 'data') 76 | .pipe(throttle(() => throttle$)); 77 | 78 | const subscription = event$.subscribe({ 79 | next: item => console.log(`Next: ${item}`) 80 | }); 81 | 82 | // At some point fire a next for the throttler 83 | throttle$.next(); 84 | ``` 85 | 86 | Now that we understand the basics of throttling, let's dive into debouncing data. 87 | 88 | ## Debounce 89 | 90 | In order to fully understand debounce, let's look at what a sample implementation might look like in JavaScript for `debounce`. In essence, we are turning any given function into an asynchronous call via `setTimeout` to ensure we call the function later if no other input has come in. So, we can take the `EventEmitter` example we have above and debounce it in a way that it will emit if no other events come within half a second. 91 | 92 | ```typescript 93 | function debounce(delay: number, fn: Function) { 94 | let timerId: NodeJS.Timeout; 95 | return function debounceFunction(...args: any[]) { 96 | if (timerId) { 97 | clearTimeout(timerId); 98 | } 99 | timerId = setTimeout(() => { 100 | fn(...args); 101 | timerId = null; 102 | }, delay); 103 | } 104 | } 105 | 106 | import { EventEmitter } from 'events'; 107 | 108 | const emitter = new EventEmitter(); 109 | 110 | const dataHandler = (arg: string) => console.log(`Next: ${arg}`); 111 | const debounceHandler = debounce(500, dataHandler); 112 | 113 | emitter.addListener('data', debounceHandler); 114 | 115 | emitter.emit('data', 'foobarbaz'); 116 | ``` 117 | 118 | Just like with `throttle`, we have two ways of dealing with debouncing behavior, `debounceTime`, which allows you to set a relative time for debouncing, and `debounce` which allows you to put in any selector which returns an Observable. 119 | 120 | ```typescript 121 | import { fromEvent } from 'rxjs'; 122 | import { debounceTime } from 'rxjs/operators'; 123 | import { EventEmitter } from 'events'; 124 | 125 | const emitter = new EventEmitter(); 126 | 127 | const event$ = fromEvent(emitter, 'data') 128 | .pipe(debounceTime(500)); 129 | 130 | const subscription = event$.subscribe({ 131 | next: item => console.log(`Next: ${item}`) 132 | }); 133 | 134 | emitter.emit('data', 'foo'); 135 | emitter.emit('data', 'bar'); 136 | emitter.emit('data', 'baz'); 137 | // Next: baz 138 | ``` 139 | 140 | We also have the opportunity to put in any Observable we wish as the debouncing agent, for example, it could use the `interval` operator, or even our own custom `Subject` by using the `debounce` operator. 141 | 142 | ```typescript 143 | import { fromEvent, interval } from 'rxjs'; 144 | import { throttle } from 'rxjs/operators'; 145 | import { EventEmitter } from 'events'; 146 | 147 | const emitter = new EventEmitter(); 148 | 149 | const event$ = fromEvent(emitter, 'data') 150 | .pipe(throttle(() => interval(500))); 151 | 152 | const subscription = event$.subscribe({ 153 | next: item => console.log(`Next: ${item}`) 154 | }); 155 | ``` 156 | 157 | ## Putting it all together 158 | 159 | Now that we have an understanding of `distinctUtilChanged` and `debounceTime` to tame our user input and `switchMap` to ensure we get our proper order of results, we could write our autosuggest example like this. 160 | 161 | ```typescript 162 | import { fromEvent } from 'rxjs'; 163 | import { 164 | debounceTime, 165 | distinctUntilChanged, 166 | map, 167 | switchMap 168 | } from 'rxjs/operators'; 169 | 170 | const value$ = fromEvent(document.querySelector('#input'), 'keyup').pipe( 171 | map(e => e.target.value), 172 | debounceTime(500), 173 | distinctUntilChanged() 174 | ); 175 | 176 | const result$ = value$.pipe( 177 | switchMap(queryWikipedia) 178 | ); 179 | 180 | const subscription = result$.subscribe({ 181 | next: items => console.log(`Results: ${items}`) 182 | }); 183 | ``` 184 | 185 | Now we have a basic understanding of taming user input, combining sequences, and more! Stay tuned for much more in our journey of discovering RxJS! -------------------------------------------------------------------------------- /20/index.ts: -------------------------------------------------------------------------------- 1 | import { interval, range, zip } from 'rxjs'; 2 | import { skip, skipLast, skipWhile, take, takeLast, takeWhile } from 'rxjs/operators'; 3 | 4 | /* 5 | const num$ = interval(500).pipe( 6 | take(3) 7 | ); 8 | */ 9 | /* 10 | const num$ = interval(500).pipe( 11 | takeWhile((item, index) => index < 4) 12 | ); 13 | */ 14 | 15 | const num$ = range(0, 5); 16 | const pair$ = zip(num$, num$.pipe(skip(1))); 17 | 18 | /* 19 | const num$ = range(0, 10).pipe(skipLast(8)); 20 | */ 21 | 22 | /* 23 | const num$ = range(0, 10).pipe( 24 | skipWhile((item, index) => index < 7) 25 | ); 26 | */ 27 | 28 | const subscription = pair$.subscribe({ 29 | next: item => console.log(`Next: ${item}`), 30 | complete: () => console.log('Done') 31 | }); -------------------------------------------------------------------------------- /20/readme.md: -------------------------------------------------------------------------------- 1 | # Day 20 - Skip and Take Observables 2 | 3 | In the [previous entry](../19/readme.md), we talked about throttling and debouncing our Observables. Today, we're going to go back to more fundamentals with skipping and taking values from Observables. This is important as we think of Observables over time, the need to skip values is important, just as it is to truncate our Observables via taking. 4 | 5 | ## Taking a number of values with take 6 | 7 | The first section we will look at is taking a number of values from our Obesrvable with the `take` operator. This is extra useful if you have an infinite stream and only want a certain number of values. We have shown this operator plenty in previous posts, but now it deserves a spot of its own. This operator simply takes a number of values you want from the Observable stream, and then completes the it. 8 | 9 | ```typescript 10 | import { interval } from 'rxjs'; 11 | import { take } from 'rxjs/operators'; 12 | 13 | const num$ = interval(500).pipe( 14 | take(3) 15 | ); 16 | 17 | const subscription = num$.subscribe({ 18 | next: item => console.log(`Next: ${item}`), 19 | complete: () => console.log('Done') 20 | }); 21 | // Next: 0 22 | // Next: 1 23 | // Next: 2 24 | // Done 25 | ``` 26 | 27 | As you can see, we get three values from our stream, followed by the completion, thus terminating what would have been an infinite stream. 28 | 29 | ## Taking the last values with takeLast 30 | 31 | What if we want to only take the last number of values from a stream, such as the last five values? To do that, we would use the `takeLast` operator. Unlike the previous operator, `take`, this operator requires our Observable sequence to be finite in size, for example using a `range` to create data and then pluck the last values from it. 32 | 33 | ```typescript 34 | import { range } from 'rxjs'; 35 | import { takeLast } from 'rxjs/operators'; 36 | 37 | const num$ = range(0, 10).pipe( 38 | takeLast(3) 39 | ); 40 | 41 | const subscription = num$.subscribe({ 42 | next: item => console.log(`Next: ${item}`), 43 | complete: () => console.log('Done') 44 | }); 45 | // Next: 7 46 | // Next: 8 47 | // Next: 9 48 | // Done 49 | ``` 50 | 51 | ## Taking values while a predicate holds true with takeWhile 52 | 53 | Another variation on take is while a predicate holds true via the `takeWhile` operator. For example, we could take values less than a certain value. This differs from `filter` as the sequence completes once the `predicate` is no longer true. The predicate takes in both the current value as well as the index. 54 | 55 | ```typescript 56 | import { range } from 'rxjs'; 57 | import { takeLast } from 'rxjs/operators'; 58 | 59 | const num$ = range(0, 10).pipe( 60 | takeWhile((item, index) => index < 4) 61 | ); 62 | 63 | const subscription = num$.subscribe({ 64 | next: item => console.log(`Next: ${item}`), 65 | complete: () => console.log('Done') 66 | }); 67 | // Next: 0 68 | // Next: 1 69 | // Next: 2 70 | // Next: 3 71 | // Done 72 | ``` 73 | 74 | ## Taking values until another Observable with takeUntil 75 | 76 | One of the more interesting operators, especially dealing with infinite streams, is to truncate our current stream based upon another stream. This is via the `takeUntil` method which takes in another Observable and truncates when the other fires its first `next` value. One common scenario you see for this is drag and drop which could be described as starting with mouse down and mouse move, calculate the delta between the start and current position, taking until the mouse up happens. 77 | 78 | ```typescript 79 | import { fromEvent } from 'rxjs'; 80 | import { map, mergeMap, takeUntil } from 'rxjs/operators'; 81 | 82 | const el = document.querySelector('#drag'); 83 | const mouseup$ = fromEvent(el, 'mouseup'); 84 | const mousedown$ = fromEvent(el, 'mousedown'); 85 | const mousemove$ = fromEvent(document, 'mousemove'); 86 | 87 | const mousedrag$ = mousedown$.pipe( 88 | mergeMap(down => { 89 | let startX = down.offsetX, startY = down.offsetY; 90 | 91 | return mousemove$.pipe( 92 | map(move => { 93 | move.preventDefault(); 94 | 95 | return { 96 | left: move.clientX - startX, 97 | top: move.clientY - startY 98 | }; 99 | }), 100 | takeUntil(mouseup$) 101 | ); 102 | }) 103 | ); 104 | ``` 105 | 106 | This is a pretty useful operator outside of this particular scenario, but it demonstrates the capability of combining two streams together until another stream interrupts it. 107 | 108 | ## Skipping a number values with skip 109 | 110 | Just as we have a way of taking a number of values, we can also skip a number of values using the `skip` operator. What makes this more fun is combining it with `zip` to combine a stream to get the previous and current values. 111 | 112 | ```typescript 113 | import { range, zip } from 'rxjs'; 114 | import { skip } from 'rxjs/operators'; 115 | 116 | const num$ = range(0, 5); 117 | const pair$ = zip(num$, num$.pipe(skip(1))); 118 | 119 | const subscription = pair$.subscribe({ 120 | next: item => console.log(`Next: ${item}`), 121 | complete: () => console.log('Done') 122 | }); 123 | // Next: 0,1 124 | // Next: 1,2 125 | // Next: 2,3 126 | // Next: 3,4 127 | // Done 128 | ``` 129 | 130 | ## Skipping the last number of values with skipLast 131 | 132 | Just as we can take a number of items from the last of the stream with `takeLast`, we can also skip those last values using `skipLast`. This is useful for trimming off data we don't want at the end. 133 | 134 | ```typescript 135 | import { range } from 'rxjs'; 136 | import { skipLast } from 'rxjs/operators'; 137 | 138 | const num$ = range(0, 10).pipe(skipLast(8)); 139 | 140 | const subscription = num$.subscribe({ 141 | next: item => console.log(`Next: ${item}`), 142 | complete: () => console.log('Done') 143 | }); 144 | // Next: 0 145 | // Next: 1 146 | // Done 147 | ``` 148 | 149 | ## Skipping data while the predicate is true with skipWhile 150 | 151 | As you'll notice a trend, if we have a take method, chances are we will have a corresponding skip method as well. This also holds true for `takeWhile` and `skipWhile` where we will skip values until the predicate no longer holds true. Much like before, the predicate takes the current value as well as the index and you return a boolean value whether the predicate holds or not. 152 | 153 | ```typescript 154 | import { range } from 'rxjs'; 155 | import { skipWhile } from 'rxjs/operators'; 156 | 157 | const num$ = range(0, 10).pipe( 158 | skipWhile((item, index) => index < 7) 159 | ); 160 | 161 | const subscription = num$.subscribe({ 162 | next: item => console.log(`Next: ${item}`), 163 | complete: () => console.log('Done') 164 | }); 165 | // Next: 7 166 | // Next: 8 167 | // Next: 9 168 | // Done 169 | ``` 170 | 171 | ## Skipping until another Observable fires with skipUntil 172 | 173 | Just like `takeUntil`, `skipUntil` allows us to skip values in our source Observable until the other Observable fires its first `next` value. This could be useful in scenarios when you only want to listen to data when a 'open' event happens and then listen to the data on the 'data' event for example. 174 | 175 | ```typescript 176 | import { EventEmitter } from 'events'; 177 | import { fromEvent } from 'rxjs'; 178 | import { skipUntil, takeUntil } from 'rxjs/operators'; 179 | 180 | const emitter = new EventEmitter(); 181 | 182 | const open$ = fromEvent(emitter, 'open'); 183 | const close$ = fromEvent(emitter, 'close'); 184 | const data$ = fromEvent(emitter, 'data'); 185 | 186 | const value$ = data$.pipe( 187 | skipUntil(open$), 188 | takeUntil(close$) 189 | ); 190 | ``` 191 | 192 | This gives you a brief overview of what is the art of the possible with skipping and taking data from Observable streams. Stay tuned for more! -------------------------------------------------------------------------------- /21/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Observable, 3 | of, 4 | range, 5 | throwError, 6 | timer, 7 | zip 8 | } from 'rxjs'; 9 | 10 | import { 11 | catchError, 12 | delayWhen, 13 | finalize, 14 | map, 15 | mergeMap, 16 | onErrorResumeNext, 17 | retry, 18 | retryWhen, 19 | tap 20 | } from 'rxjs/operators'; 21 | 22 | let retryCount = 0; 23 | const obs$ = new Observable(observer => { 24 | if (++retryCount == 3) { 25 | observer.next(42); 26 | observer.complete(); 27 | } else { 28 | console.log('failed call'); 29 | observer.error(new Error('woops')); 30 | } 31 | }); 32 | 33 | const num$ = obs$.pipe( 34 | retryWhen(errors => { 35 | return zip(errors, range(1, 3)).pipe( 36 | map(([_, index]) => index), 37 | tap(item => console.log(`Retrying after ${item} seconds`)), 38 | delayWhen(item => timer(item * 1000)) 39 | ); 40 | }) 41 | ); 42 | 43 | num$.subscribe({ 44 | next: item => console.log(`Next: ${item}`), 45 | error: err => console.log(`Error: ${err.message}`) 46 | }); 47 | 48 | /* 49 | const num$ = of(1, 2, 3).pipe( 50 | map(item => { 51 | if (item > 2) { 52 | throw new Error('woops'); 53 | } else { 54 | return item; 55 | } 56 | }) 57 | ); 58 | */ 59 | 60 | /* 61 | const num$ = throwError(new Error('woops')); 62 | */ 63 | 64 | /* 65 | const num$ = of(1, 2, 3).pipe( 66 | mergeMap(item => { 67 | if (item > 2) { 68 | return throwError(new Error('woops')); 69 | } 70 | 71 | return of(item * item); 72 | }), 73 | catchError(err => { 74 | console.log(`Error caught: ${err.message}`); 75 | return of(42); 76 | }) 77 | ); 78 | */ 79 | 80 | /* 81 | const num1$ = throwError(new Error('first')); 82 | const num2$ = throwError(new Error('second')); 83 | const num3$ = of(42); 84 | const num4$ = of(56); 85 | 86 | const num$ = num1$.pipe( 87 | onErrorResumeNext(num2$, num3$, num4$) 88 | ); 89 | 90 | num$.subscribe({ 91 | next: item => console.log(`Next: ${item}`), 92 | error: err => console.log(`Error: ${err.message}`) 93 | }); 94 | */ 95 | 96 | /* 97 | of(42).pipe( 98 | finalize(() => console.log('Finally called')) 99 | ).subscribe({ 100 | next: item => console.log(`Next: ${item}`), 101 | error: err => console.log(`Error: ${err.message}`), 102 | complete: () => console.log('Done') 103 | }); 104 | 105 | throwError(new Error('woops')).pipe( 106 | finalize(() => console.log('Finally called')) 107 | ).subscribe({ 108 | next: item => console.log(`Next: ${item}`), 109 | error: err => console.log(`Error: ${err.message}`), 110 | complete: () => console.log('Done') 111 | }); 112 | */ 113 | /* 114 | let retryCount = 0; 115 | const obs$ = new Observable(observer => { 116 | if (++retryCount == 3) { 117 | observer.next(42); 118 | observer.complete(); 119 | } else { 120 | observer.error(new Error('woops')); 121 | } 122 | }); 123 | 124 | /* 125 | const num$ = obs$.pipe(retry(3)); 126 | */ 127 | 128 | /* 129 | const num$ = obs$.pipe( 130 | retryWhen(errors => { 131 | return zip(errors, range(1, 3)).pipe( 132 | map(([_, index]) => index), 133 | tap(item => console.log(`Retrying after ${item} seconds`)), 134 | delayWhen(item => timer(item * 1000)) 135 | ); 136 | }) 137 | ); 138 | 139 | 140 | num$.subscribe({ 141 | next: item => console.log(`Next: ${item}`), 142 | error: err => console.log(`Error: ${err.message}`) 143 | }); 144 | 145 | */ -------------------------------------------------------------------------------- /21/readme.md: -------------------------------------------------------------------------------- 1 | # Day 21 - Error Handling with Observables 2 | 3 | In the [previous entry](../20/readme.md), we covered some basic operators for skipping and taking values based upon certain criteria. Today, we're going to go back to a fundamental concept of error handling. 4 | 5 | ## Sometimes errors happen 6 | 7 | One of the key concepts of the Reactive Extensions is its completion semantics. What we're trying to accomplish is a push based version of the basic iteration loop with try/catch. This matches perfectly with our `Observer` which has a `next`, with either an `complete` or an `error` terminating the sequence. Here, I've mapped these methods to `handle` methods so you understand where each fits in the world. 8 | 9 | ```typescript 10 | try { 11 | for (let item of source) { 12 | handleNext(item); 13 | } 14 | 15 | handleComplete(); 16 | } catch (err) { 17 | handleError(err); 18 | } 19 | ``` 20 | 21 | When we subscribe to a given Observable, we have the option of supplying an `error` handler to our `Observer` in case an error happens. If we do not supply an error handler, a general error will be thrown and script halted. This is different from Promises where they will report unhandled rejections via the 'unhandledRejection' on the host. 22 | 23 | ```typescript 24 | import { of } from 'rxjs'; 25 | import { map } from 'rxjs/operators'; 26 | 27 | const num$ = of(1, 2, 3).pipe( 28 | map(() => { throw new Error('woops'); }) 29 | ); 30 | 31 | num$.subscribe({ 32 | next: item => console.log(`Next: ${item}`) 33 | }); 34 | ``` 35 | 36 | If we run this, our process will crash such as the following which will tell us where the error occurred as well as the stack trace. 37 | 38 | ```bash 39 | $ npx ts-node 21/index.ts 40 | rxjs-advent-2018/21/index.ts:5 41 | map(() => { throw new Error('woops'); }) 42 | ^ 43 | Error: woops 44 | ``` 45 | 46 | We can get around this by adding an error handler with the subscription where we will display the error's message. 47 | 48 | ```typescript 49 | import { of } from 'rxjs'; 50 | import { map } from 'rxjs/operators'; 51 | 52 | const num$ = of(1, 2, 3).pipe( 53 | map(() => { throw new Error('woops'); }) 54 | ); 55 | 56 | num$.subscribe({ 57 | next: item => console.log(`Next: ${item}`), 58 | error: err => console.log(`Error: ${err.message}`) 59 | }); 60 | // Error: woops 61 | ``` 62 | 63 | Keep in mind, that errors could happen at any time, and not just the first element which we're doing right now. 64 | 65 | ```typescript 66 | import { of } from 'rxjs'; 67 | import { map } from 'rxjs/operators'; 68 | 69 | const num$ = of(1, 2, 3).pipe( 70 | map(item => { 71 | if (item > 2) { 72 | throw new Error('woops'); 73 | } else { 74 | return item; 75 | } 76 | }) 77 | ); 78 | 79 | num$.subscribe({ 80 | next: item => console.log(`Next: ${item}`), 81 | error: err => console.log(`Error: ${err.message}`) 82 | }); 83 | // Next: 1 84 | // Next: 2 85 | // Error: woops 86 | ``` 87 | 88 | ## Throwing errors 89 | 90 | Just as we randomly threw errors in the previous examples, we could also create errors via the `throwError` factory operation. This is useful for modeling errors when an Observable is required. Not only that, but we can control it via a Scheduler if need be. 91 | 92 | ```typescript 93 | import { throwError } from 'rxjs'; 94 | 95 | const num$ = throwError(new Error('woops')); 96 | 97 | num$.subscribe({ 98 | next: item => console.log(`Next: ${item}`), 99 | error: err => console.log(`Error: ${err.message}`) 100 | }); 101 | // Error: woops 102 | ``` 103 | 104 | ## Catching errors 105 | 106 | Just as we have the ability to throw errors via the `throwError` or just throwing errors, we can also recover from our errors via the `catchError` operator. This allows us to inspect the current error, and return an Observable which continues the sequence. In this case, we'll throw an error once we get past our second item and then merge in an error. I will also include a console logging mechanism to show that an error has been thrown and caught. 107 | 108 | ```typescript 109 | import { of, throwError } from 'rxjs'; 110 | import { catchError, mergeMap } from 'rxjs'; 111 | 112 | const num$ = of(1, 2, 3).pipe( 113 | mergeMap(item => { 114 | if (item > 2) { 115 | return throwError(new Error('woops')); 116 | } 117 | 118 | return of(item * item); 119 | }), 120 | catchError(err => { 121 | console.log(`Error caught: ${err.message}`); 122 | return of(42); 123 | }) 124 | ); 125 | 126 | num$.subscribe({ 127 | next: item => console.log(`Next: ${item}`), 128 | error: err => console.log(`Error: ${err.message}`) 129 | }); 130 | // Next: 1 131 | // Next: 4 132 | // Error caught: woops 133 | // Next: 42 134 | ``` 135 | 136 | As you can see, our handler caught the error and then we returned a sequence of a single item of `42`. 137 | 138 | ## Resuming after an error VB Style 139 | 140 | Just as we can catch an error via the `catchError` operator, we can also go Visual Basic Style with `onErrorResumeNext`, which is an ode to the Visual Basic [`On Error Resume Next`](https://docs.microsoft.com/en-us/dotnet/visual-basic/language-reference/statements/on-error-statement#on-error-resume-next), of which the Reactive Extensions creator, Erik Meijer, is a fan. This allows us to continue after an error with a new Observable or set of Observables. This will keep on going even if the last operation succeeds. 141 | 142 | ```typescript 143 | import { of, throwError } from 'rxjs'; 144 | import { onErrorResumeNext } from 'rxjs/operators'; 145 | 146 | const num1$ = throwError(new Error('first')); 147 | const num2$ = throwError(new Error('second')); 148 | const num3$ = of(42); 149 | const num4$ = of(56); 150 | 151 | const num$ = num1$.pipe( 152 | onErrorResumeNext(num2$, num3$) 153 | ); 154 | 155 | num$.subscribe({ 156 | next: item => console.log(`Next: ${item}`), 157 | error: err => console.log(`Error: ${err.message}`) 158 | }); 159 | // Next: 42 160 | // Next: 56 161 | ``` 162 | 163 | ## Finalizing an operation 164 | 165 | So far we've covered the `try/catch` aspect of Observables, but not the `finally` part just yet. We have the ability to, at the end of the sequence, we can invoke some action. This is useful if we have some resources open and we want to ensure that they are closed, regardless of whether an error has occurred or not. 166 | 167 | ```typescript 168 | import { of, throwError } from 'rxjs'; 169 | import { finalize } from 'rxjs/operators'; 170 | 171 | of(42).pipe( 172 | finalize(() => console.log('Finally called')) 173 | ).subscribe({ 174 | next: item => console.log(`Next: ${item}`), 175 | error: err => console.log(`Error: ${err.message}`), 176 | complete: () => console.log('Done') 177 | }); 178 | // Next: 42 179 | // Done 180 | // Finally called 181 | 182 | throwError(new Error('woops')).pipe( 183 | finalize(() => console.log('Finally called')) 184 | ).subscribe({ 185 | next: item => console.log(`Next: ${item}`), 186 | error: err => console.log(`Error: ${err.message}`), 187 | complete: () => console.log('Done') 188 | }); 189 | // Error: woops 190 | // Finally called 191 | ``` 192 | 193 | As you'll notice, the `finalize` operation happens after the `complete` handler is called. 194 | 195 | ## Retrying an operation 196 | 197 | Operations can fail such as with an HTTP request or some other network connection issue, so we need a way to retry operations, such as try three times and then give up if it doesn't work. We accomodate this feature via the `retry` operator which specifies the number of retries before the operation gives up and thus letting the error through. Note if you do not specify a retry count, then it will retry indefinitely. 198 | 199 | ```typescript 200 | import { Observable } from 'rxjs'; 201 | import { retry } from 'rxjs/operators'; 202 | 203 | let retryCount = 0; 204 | const obs$ = new Observable(observer => { 205 | if (++retryCount == 2) { 206 | observer.next(42); 207 | observer.complete(); 208 | } else { 209 | observer.error(new Error('woops')); 210 | } 211 | }); 212 | 213 | const nums$ = obs$.pipe(retry(3)); 214 | 215 | num$.subscribe({ 216 | next: item => console.log(`Next: ${item}`), 217 | error: err => console.log(`Error: ${err.message}`) 218 | }); 219 | // Next: 42 220 | ``` 221 | 222 | ## Conditionally retrying an operation 223 | 224 | The `retry` operator is good for many things like immediately retrying an operation. There may be instances, however, when you need finer grained control over when and how a retry happens. For that, we have the `retryWhen` operator which allows us to create a backoff strategy, for example, using `delayWhen` to specify the delayed time for that next try. 225 | 226 | ```typescript 227 | import { Observable, range, timer, zip } from 'rxjs'; 228 | import { delayWhen, map, retryWhen, tap } from 'rxjs/operators'; 229 | 230 | let retryCount = 0; 231 | const obs$ = new Observable(observer => { 232 | if (++retryCount == 3) { 233 | observer.next(42); 234 | observer.complete(); 235 | } else { 236 | observer.error(new Error('woops')); 237 | } 238 | }); 239 | 240 | const num$ = obs$.pipe( 241 | retryWhen(errors => { 242 | return zip(errors, range(1, 3)).pipe( 243 | map(([_, index]) => index), 244 | tap(item => console.log(`Retrying after ${item} seconds`)), 245 | delayWhen(item => timer(item * 1000)) 246 | ); 247 | }) 248 | ); 249 | 250 | num$.subscribe({ 251 | next: item => console.log(`Next: ${item}`), 252 | error: err => console.log(`Error: ${err.message}`) 253 | }); 254 | // Retrying after 1 seconds 255 | // Retrying after 2 seconds 256 | // Next: 42 257 | ``` 258 | 259 | There are more advanced scenarios for error handling, but this should give you a flavor of what the art of the possible is when it comes to handling errors in your application! -------------------------------------------------------------------------------- /22/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import { Observable } from 'rxjs'; 3 | 4 | const cold$ = new Observable(observer => { 5 | for (let item of [42, 56]) { 6 | console.log(`Observable next: ${item}`); 7 | observer.next(item); 8 | } 9 | }); 10 | 11 | const subcription1 = cold$.subscribe({ 12 | next: item => console.log(`Subscription 1: ${item}`) 13 | }); 14 | 15 | const subcription2 = cold$.subscribe({ 16 | next: item => console.log(`Subscription 2: ${item}`) 17 | }); 18 | */ 19 | 20 | /* 21 | import { Observable, ConnectableObservable } from 'rxjs'; 22 | import { publish } from 'rxjs/operators'; 23 | 24 | const cold$ = new Observable(observer => { 25 | for (let item of [42, 56]) { 26 | console.log(`Observable next: ${item}`); 27 | observer.next(item); 28 | } 29 | observer.complete(); 30 | }); 31 | 32 | const hot$ = (cold$.pipe(publish()) as ConnectableObservable).refCount(); 33 | 34 | const subcription1 = hot$.subscribe({ 35 | next: item => console.log(`Subscription 1: ${item}`) 36 | }); 37 | 38 | const subcription2 = hot$.subscribe({ 39 | next: item => console.log(`Subscription 2: ${item}`) 40 | }); 41 | */ 42 | 43 | /* 44 | import { interval, merge, range, zip } from 'rxjs'; 45 | import { map, publish } from 'rxjs/operators'; 46 | 47 | const hot$ = 48 | zip( 49 | interval(1000), 50 | range(0, 2) 51 | ) 52 | .pipe(publish(multicasted$ => { 53 | return merge( 54 | multicasted$.pipe(map(x => `stream1: ${x}`)), 55 | multicasted$.pipe(map(x => `stream2: ${x}`)), 56 | multicasted$.pipe(map(x => `stream3: ${x}`)) 57 | ); 58 | })); 59 | 60 | const subcription1 = hot$.subscribe({ 61 | next: item => console.log(`Subscription 1: ${item}`) 62 | }); 63 | 64 | const subcription2 = hot$.subscribe({ 65 | next: item => console.log(`Subscription 2: ${item}`) 66 | }); 67 | */ 68 | 69 | /* 70 | import { Observable, ConnectableObservable } from 'rxjs'; 71 | import { publishBehavior } from 'rxjs/operators'; 72 | 73 | const cold$ = new Observable(observer => { 74 | for (let item of [42, 56]) { 75 | console.log(`Observable next: ${item}`); 76 | observer.next(item); 77 | } 78 | }); 79 | 80 | const hot$ = cold$.pipe(publishBehavior(-1)) as ConnectableObservable; 81 | 82 | const subcription1 = hot$.subscribe({ 83 | next: item => console.log(`Subscription 1: ${item}`) 84 | }); 85 | 86 | const subcription2 = hot$.subscribe({ 87 | next: item => console.log(`Subscription 2: ${item}`) 88 | }); 89 | 90 | hot$.connect(); 91 | 92 | const subcription3 = hot$.subscribe({ 93 | next: item => console.log(`Subscription 3: ${item}`) 94 | }); 95 | */ 96 | 97 | /* 98 | import { Observable, ConnectableObservable } from 'rxjs'; 99 | import { publishReplay } from 'rxjs/operators'; 100 | 101 | const cold$ = new Observable(observer => { 102 | for (let item of [42, 56]) { 103 | console.log(`Observable next: ${item}`); 104 | observer.next(item); 105 | } 106 | }); 107 | 108 | const hot$ = cold$.pipe(publishReplay(2)) as ConnectableObservable; 109 | 110 | const subcription1 = hot$.subscribe({ 111 | next: item => console.log(`Subscription 1: ${item}`) 112 | }); 113 | 114 | const subcription2 = hot$.subscribe({ 115 | next: item => console.log(`Subscription 2: ${item}`) 116 | }); 117 | 118 | hot$.connect(); 119 | 120 | const subcription3 = hot$.subscribe({ 121 | next: item => console.log(`Subscription 3: ${item}`) 122 | }); 123 | */ 124 | 125 | import { Observable, ConnectableObservable } from 'rxjs'; 126 | import { publishLast } from 'rxjs/operators'; 127 | 128 | const cold$ = new Observable(observer => { 129 | for (let item of [42, 56]) { 130 | console.log(`Observable next: ${item}`); 131 | observer.next(item); 132 | } 133 | 134 | observer.complete(); 135 | }); 136 | 137 | const hot$ = cold$.pipe(publishLast()) as ConnectableObservable; 138 | 139 | const subcription1 = hot$.subscribe({ 140 | next: item => console.log(`Subscription 1: ${item}`) 141 | }); 142 | 143 | const subcription2 = hot$.subscribe({ 144 | next: item => console.log(`Subscription 2: ${item}`) 145 | }); 146 | 147 | hot$.connect(); 148 | 149 | const subcription3 = hot$.subscribe({ 150 | next: item => console.log(`Subscription 3: ${item}`) 151 | }); -------------------------------------------------------------------------------- /22/readme.md: -------------------------------------------------------------------------------- 1 | # Day 22 - You're Hot and You're Cold 2 | 3 | In the [previous entry](../21/readme.md), we covered pretty much an extensive look into error handling. Today, we're going to go off on a different tangent and revisit [Day 11](../11/readme.md) which talked about Subjects, but this time coming at it from the perspective of hot and cold Observables. Let's go through some definitions here in this post. 4 | 5 | ## Cold Observables 6 | 7 | To be a cold Observable, it must have the following two properties: 8 | 9 | 1. The Observable does not emit values until a subscriber subscribes 10 | 2. If we have more than one subscriber, then the Observable will emit all values to all subscribers one by one. 11 | 12 | In other words, cold observables are unicast observables, one producer to one consumer. To illustrate this concept in action, let's look at some code. 13 | 14 | ```typescript 15 | import { Observable } from 'rxjs'; 16 | 17 | const cold$ = new Observable(observer => { 18 | for (let item of [42, 56]) { 19 | console.log(`Observable next: ${item}`); 20 | observer.next(item); 21 | } 22 | }); 23 | 24 | const subcription1 = cold$.subscribe({ 25 | next: item => console.log(`Subscription 1: ${item}`) 26 | }); 27 | 28 | const subcription2 = cold$.subscribe({ 29 | next: item => console.log(`Subscription 2: ${item}`) 30 | }); 31 | ``` 32 | 33 | Running this through, we get the following which shows that subscription 1, gets its own values and then finished, followed by subscription 2 with its values. You'll notice that each time a new subscriber comes in, they get a fresh set of data. 34 | 35 | ```bash 36 | $ npx ts-node 22/index.ts 37 | Observable next: 42 38 | Subscription 1: 42 39 | Observable next: 56 40 | Subscription 1: 56 41 | Observable next: 42 42 | Subscription 2: 42 43 | Observable next: 56 44 | Subscription 2: 56 45 | ``` 46 | 47 | What if we wanted to share the data instead of having to produce it again and again? In that case, we have a mechanism or rather several to turn it on its head to make it a "Hot Observable" 48 | 49 | ## Hot Observables 50 | 51 | Let's contrast this with Hot Observables, or call it multicasted Observables, where it has the following properties: 52 | 53 | 1. Observables do not wait for any subscription, as the data starts emitting when it was created. 54 | 2. They don't emit the sequence of items again for any new subscriber. 55 | 3. When a new item is emitted by a hot observable, all subscribed observers will get the emitted item all at once. 56 | 57 | We can show what a hot observable is by either creating a `Subject`, or another example would be using a `ConnectableObservable`. This `ConnectableObservable` allows us to take the multicasted observable and have our subscribers subscribe, and once they are done, we can then call `.connect()`. In this example, we will use `publish()`, which behind the scenes, is a variant of the `multicast` operator using a `Subject`, which we will be talking about later. 58 | 59 | ```typescript 60 | import { Observable, ConnectableObservable } from 'rxjs'; 61 | import { publish } from 'rxjs/operators'; 62 | 63 | const cold$ = new Observable(observer => { 64 | for (let item of [42, 56]) { 65 | console.log(`Observable next: ${item}`); 66 | observer.next(item); 67 | } 68 | }); 69 | 70 | const hot$ = cold$.pipe(publish()) as ConnectableObservable; 71 | 72 | const subcription1 = hot$.subscribe({ 73 | next: item => console.log(`Subscription 1: ${item}`) 74 | }); 75 | 76 | const subcription2 = hot$.subscribe({ 77 | next: item => console.log(`Subscription 2: ${item}`) 78 | }); 79 | 80 | hot$.connect(); 81 | ``` 82 | 83 | If you leave off the `.connect()`, you won't see any data flowing through, and it's only when you call the method does the data start flowing to all recipients. 84 | 85 | ```bash 86 | $ npx ts-node 22/index.ts 87 | Observable next: 42 88 | Subscription 1: 42 89 | Subscription 2: 42 90 | Observable next: 56 91 | Subscription 1: 56 92 | Subscription 2: 56 93 | ``` 94 | 95 | At the time of `.connect()`, even if you don't have any subscribers attached, the data will still flow as seen below with this example. 96 | 97 | ```typescript 98 | import { Observable, ConnectableObservable } from 'rxjs'; 99 | import { publish } from 'rxjs/operators'; 100 | 101 | const cold$ = new Observable(observer => { 102 | for (let item of [42, 56]) { 103 | console.log(`Observable next: ${item}`); 104 | observer.next(item); 105 | } 106 | }); 107 | 108 | const hot$ = cold$.pipe(publish()) as ConnectableObservable; 109 | hot$.connect(); 110 | ``` 111 | 112 | Running this through will show that the data still flows despite nobody listening to it. 113 | 114 | ```bash 115 | $ npx ts-node 22/index.ts 116 | Observable next: 42 117 | Observable next: 56 118 | ``` 119 | 120 | Our `ConnectableObservable` has another way of determining when subscriptions are made outside of the manual `connect()` method, amd that's via the `refCount` method. This method takes care of determining when subscriptions are made, maintaining a reference count of them, and cools the observable so it doesn't flow. When a subscription is made, the reference count is incremented. If the prior reference count was zero, then the underlying Subject is subscribed and data starts flowing, thus making the Observable hot again. And when the subscription is disposed, and the reference count drops to zero, the underlying Subject is unsubscribed from the source. This is the most used operator when it comes to hot observables, and is the default for such operators as `fromEvent`, `fromEventPattern` and others. 121 | 122 | So, why would we use hot observables? There are a number of reasons why you might want to use hot observables including: 123 | 124 | 1. When we want to run a job for its side effects and don't need a subscription 125 | 2. We want to broadcast our value to all subscribers at once. 126 | 3. When we don't want to trigger the source of data again for each new subscriber. 127 | 128 | The last one is probably the biggest point, for example, with events, not triggering adding yet another handler to the node for listening to the same event. 129 | 130 | ## Ways of Creating Hot Observables 131 | 132 | In RxJS, we provide two ways of creating hot observables. 133 | 134 | 1. Using the `ConnectableObservable` as described above. 135 | 2. Using a `Subject`, which we covered in [Day 11](../11/readme.md) 136 | 137 | I won't go over the whole idea of subjects here, instead, we'll talk more about `ConnectableObservable` and how you use it through the various `multicast` overloads. 138 | 139 | ### Looking at the publish operator 140 | 141 | The first way we'll talk about is using a `Subject` to backing store for our multicasted values using the `multicast` operator. This is done with the `publish()` operator which is just a convenience method for `multicast(() => new Subject())`. We've already covered `publish` above so I won't go into much more detail here. 142 | 143 | There is an overload to the `publish` method that is worth exploring. This turns the existing Observable hot, and then you have access to it inside the selector. You don't need to worry about ref counting or connecting, as it is all contained during that scope. Let's go through a quick example of that. 144 | 145 | ```typescript 146 | import { interval, merge, range, zip } from 'rxjs'; 147 | import { map, publish } from 'rxjs/operators'; 148 | 149 | const hot$ = 150 | zip( 151 | interval(1000), 152 | range(0, 2) 153 | ) 154 | .pipe(publish(multicasted$ => { 155 | return merge( 156 | multicasted$.pipe(map(x => `stream1: ${x}`)), 157 | multicasted$.pipe(map(x => `stream2: ${x}`)), 158 | multicasted$.pipe(map(x => `stream3: ${x}`)) 159 | ); 160 | })); 161 | 162 | const subcription1 = hot$.subscribe({ 163 | next: item => console.log(`Subscription 1: ${item}`) 164 | }); 165 | 166 | const subcription2 = hot$.subscribe({ 167 | next: item => console.log(`Subscription 2: ${item}`) 168 | }); 169 | ``` 170 | 171 | Running this latest code, we get the following results where we merged the inner results without having to do any `connect` or `refCount`. 172 | 173 | ```bash 174 | $ npx ts-node 22/index.ts 175 | Subscription 1: stream1: 0,0 176 | Subscription 1: stream2: 0,0 177 | Subscription 1: stream3: 0,0 178 | Subscription 2: stream1: 0,0 179 | Subscription 2: stream2: 0,0 180 | Subscription 2: stream3: 0,0 181 | Subscription 1: stream1: 1,1 182 | Subscription 1: stream2: 1,1 183 | Subscription 1: stream3: 1,1 184 | Subscription 2: stream1: 1,1 185 | Subscription 2: stream2: 1,1 186 | Subscription 2: stream3: 1,1 187 | ``` 188 | 189 | ### Looking at the publishBehavior operator 190 | 191 | Another overload of the `multicast` operator is using a `BehaviorSubject` as the underlying subject is called oddly enough, `publishBehavior`. This contrasts with the earllier `publish` which uses the `Subject`. How this differs is that we supply an initial value to the Observable which is then emitted to the subscribers before any `next` call to the source Observable. 192 | 193 | Let's take a look at an example, where we supply two subscribers and then call `connect`, and then we add a third subscription which should then miss the initial default value. 194 | 195 | ```typescript 196 | import { Observable, ConnectableObservable } from 'rxjs'; 197 | import { publishBehavior } from 'rxjs/operators'; 198 | 199 | const cold$ = new Observable(observer => { 200 | for (let item of [42, 56]) { 201 | console.log(`Observable next: ${item}`); 202 | observer.next(item); 203 | } 204 | }); 205 | 206 | const hot$ = cold$.pipe(publishBehavior(-1)) as ConnectableObservable; 207 | 208 | const subcription1 = hot$.subscribe({ 209 | next: item => console.log(`Subscription 1: ${item}`) 210 | }); 211 | 212 | const subcription2 = hot$.subscribe({ 213 | next: item => console.log(`Subscription 2: ${item}`) 214 | }); 215 | 216 | hot$.connect(); 217 | 218 | const subcription3 = hot$.subscribe({ 219 | next: item => console.log(`Subscription 3: ${item}`) 220 | }); 221 | ``` 222 | 223 | Running this example, we will indeed see the third subscription will miss the initial value of `-1` and will continue to receive the other values. 224 | 225 | ```bash 226 | $ npx ts-node 22/index.ts 227 | Subscription 1: -1 228 | Subscription 2: -1 229 | Observable next: 42 230 | Subscription 1: 42 231 | Subscription 2: 42 232 | Observable next: 56 233 | Subscription 1: 56 234 | Subscription 2: 56 235 | Subscription 3: 56 236 | ``` 237 | 238 | ### Looking at the publishReplay operator 239 | 240 | Another version of the publish operator uses the `ReplaySubject` where we can replay values either by the count, or by the timeframe with the `publishReplay` operator. We'll demonstrate this operator by modifying the above sample, but replaying only the first value. In this case once again, since the third subscription happens after the `connect()` call, therefore it misses the first value. 241 | 242 | ```typescript 243 | import { Observable, ConnectableObservable } from 'rxjs'; 244 | import { publishReplay } from 'rxjs/operators'; 245 | 246 | const cold$ = new Observable(observer => { 247 | for (let item of [42, 56]) { 248 | console.log(`Observable next: ${item}`); 249 | observer.next(item); 250 | } 251 | }); 252 | 253 | const hot$ = cold$.pipe(publishReplay(1)) as ConnectableObservable; 254 | 255 | const subcription1 = hot$.subscribe({ 256 | next: item => console.log(`Subscription 1: ${item}`) 257 | }); 258 | 259 | const subcription2 = hot$.subscribe({ 260 | next: item => console.log(`Subscription 2: ${item}`) 261 | }); 262 | 263 | hot$.connect(); 264 | 265 | const subcription3 = hot$.subscribe({ 266 | next: item => console.log(`Subscription 3: ${item}`) 267 | }); 268 | ``` 269 | 270 | Running this sample, we see the following output which shows that the third subscription misses the first value. 271 | 272 | ```bash 273 | $ npx ts-node 22/index.ts 274 | Observable next: 42 275 | Subscription 1: 42 276 | Subscription 2: 42 277 | Observable next: 56 278 | Subscription 1: 56 279 | Subscription 2: 56 280 | Subscription 3: 56 281 | ``` 282 | 283 | Changing the above call to `publishReplay(2)`, we'll then see that the first value was preserved for the third subscription, however, it is different in that the third subscription will be catching up with getting the first value followed immediately by the second value. 284 | 285 | ```bash 286 | $ npx ts-node 22/index.ts 287 | Observable next: 42 288 | Subscription 1: 42 289 | Subscription 2: 42 290 | Observable next: 56 291 | Subscription 1: 56 292 | Subscription 2: 56 293 | Subscription 3: 42 294 | Subscription 3: 56 295 | ``` 296 | 297 | ### Looking at the publishLast operator 298 | 299 | Finally, let's look at the last variation of the publish operator, this time wrapping the `AsyncSubject`. As we know from before, we don't actually get any values from our Observable until the `complete` has been fired. 300 | 301 | ```typescript 302 | import { Observable, ConnectableObservable } from 'rxjs'; 303 | import { publishLast } from 'rxjs/operators'; 304 | 305 | const cold$ = new Observable(observer => { 306 | for (let item of [42, 56]) { 307 | console.log(`Observable next: ${item}`); 308 | observer.next(item); 309 | } 310 | 311 | observer.complete(); 312 | }); 313 | 314 | const hot$ = cold$.pipe(publishLast()) as ConnectableObservable; 315 | 316 | const subcription1 = hot$.subscribe({ 317 | next: item => console.log(`Subscription 1: ${item}`) 318 | }); 319 | 320 | const subcription2 = hot$.subscribe({ 321 | next: item => console.log(`Subscription 2: ${item}`) 322 | }); 323 | 324 | hot$.connect(); 325 | 326 | const subcription3 = hot$.subscribe({ 327 | next: item => console.log(`Subscription 3: ${item}`) 328 | }); 329 | ``` 330 | 331 | Running this, we can see, this only fires the last value, and doesn't matter when you connect, you should all still get the same value. 332 | 333 | ```bash 334 | Observable next: 42 335 | Observable next: 56 336 | Subscription 1: 56 337 | Subscription 2: 56 338 | Subscription 3: 56 339 | ``` 340 | 341 | ### Sharing is caring 342 | 343 | As I stated above, it's very common to use `publish` variants and `refCount` all at the same time. To make this easier, we have provided a number of `share*` methods which is shorthand for `publish*().refCount()` where the `*` represents the kind of publish we are doing as we did in the previous section. 344 | 345 | Stay tuned for yet more RxJS goodness as we wrap up this series! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Matthew Podwysocki 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 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxjs-advent-2018", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "10.12.12", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.12.tgz", 10 | "integrity": "sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A==", 11 | "dev": true 12 | }, 13 | "arrify": { 14 | "version": "1.0.1", 15 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 16 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 17 | "dev": true 18 | }, 19 | "axios": { 20 | "version": "0.18.0", 21 | "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", 22 | "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", 23 | "requires": { 24 | "follow-redirects": "^1.3.0", 25 | "is-buffer": "^1.1.5" 26 | } 27 | }, 28 | "buffer-from": { 29 | "version": "1.1.1", 30 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 31 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 32 | "dev": true 33 | }, 34 | "debug": { 35 | "version": "3.1.0", 36 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 37 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 38 | "requires": { 39 | "ms": "2.0.0" 40 | } 41 | }, 42 | "diff": { 43 | "version": "3.5.0", 44 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 45 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 46 | "dev": true 47 | }, 48 | "follow-redirects": { 49 | "version": "1.5.10", 50 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 51 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 52 | "requires": { 53 | "debug": "=3.1.0" 54 | } 55 | }, 56 | "is-buffer": { 57 | "version": "1.1.6", 58 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 59 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 60 | }, 61 | "js-tokens": { 62 | "version": "4.0.0", 63 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 64 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 65 | }, 66 | "loose-envify": { 67 | "version": "1.4.0", 68 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 69 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 70 | "requires": { 71 | "js-tokens": "^3.0.0 || ^4.0.0" 72 | } 73 | }, 74 | "make-error": { 75 | "version": "1.3.5", 76 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", 77 | "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", 78 | "dev": true 79 | }, 80 | "minimist": { 81 | "version": "1.2.0", 82 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 83 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 84 | "dev": true 85 | }, 86 | "mkdirp": { 87 | "version": "0.5.1", 88 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 89 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 90 | "dev": true, 91 | "requires": { 92 | "minimist": "0.0.8" 93 | }, 94 | "dependencies": { 95 | "minimist": { 96 | "version": "0.0.8", 97 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 98 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 99 | "dev": true 100 | } 101 | } 102 | }, 103 | "ms": { 104 | "version": "2.0.0", 105 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 106 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 107 | }, 108 | "performance-now": { 109 | "version": "2.1.0", 110 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 111 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 112 | }, 113 | "raf": { 114 | "version": "3.4.1", 115 | "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", 116 | "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", 117 | "requires": { 118 | "performance-now": "^2.1.0" 119 | } 120 | }, 121 | "redux": { 122 | "version": "4.0.1", 123 | "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", 124 | "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", 125 | "requires": { 126 | "loose-envify": "^1.4.0", 127 | "symbol-observable": "^1.2.0" 128 | } 129 | }, 130 | "rxjs": { 131 | "version": "6.3.3", 132 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", 133 | "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", 134 | "requires": { 135 | "tslib": "^1.9.0" 136 | } 137 | }, 138 | "source-map": { 139 | "version": "0.6.1", 140 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 141 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 142 | "dev": true 143 | }, 144 | "source-map-support": { 145 | "version": "0.5.9", 146 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", 147 | "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", 148 | "dev": true, 149 | "requires": { 150 | "buffer-from": "^1.0.0", 151 | "source-map": "^0.6.0" 152 | } 153 | }, 154 | "symbol-observable": { 155 | "version": "1.2.0", 156 | "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", 157 | "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" 158 | }, 159 | "ts-node": { 160 | "version": "7.0.1", 161 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", 162 | "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", 163 | "dev": true, 164 | "requires": { 165 | "arrify": "^1.0.0", 166 | "buffer-from": "^1.1.0", 167 | "diff": "^3.1.0", 168 | "make-error": "^1.1.1", 169 | "minimist": "^1.2.0", 170 | "mkdirp": "^0.5.1", 171 | "source-map-support": "^0.5.6", 172 | "yn": "^2.0.0" 173 | } 174 | }, 175 | "tslib": { 176 | "version": "1.9.3", 177 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 178 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" 179 | }, 180 | "typescript": { 181 | "version": "3.2.1", 182 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.1.tgz", 183 | "integrity": "sha512-jw7P2z/h6aPT4AENXDGjcfHTu5CSqzsbZc6YlUIebTyBAq8XaKp78x7VcSh30xwSCcsu5irZkYZUSFP1MrAMbg==", 184 | "dev": true 185 | }, 186 | "yn": { 187 | "version": "2.0.0", 188 | "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", 189 | "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", 190 | "dev": true 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxjs-advent-2018", 3 | "version": "1.0.0", 4 | "description": "RxJS Advent Calendar for 2018", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mattpodwysocki/rxjs-advent-2018.git" 12 | }, 13 | "keywords": [ 14 | "RxJS", 15 | "Advent", 16 | "Reactive", 17 | "Reactive-Programming" 18 | ], 19 | "author": "Matthew Podwysocki", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/mattpodwysocki/rxjs-advent-2018/issues" 23 | }, 24 | "homepage": "https://github.com/mattpodwysocki/rxjs-advent-2018#readme", 25 | "devDependencies": { 26 | "@types/node": "^10.12.12", 27 | "ts-node": "^7.0.1", 28 | "typescript": "^3.2.1" 29 | }, 30 | "dependencies": { 31 | "axios": "^0.18.0", 32 | "raf": "^3.4.1", 33 | "redux": "^4.0.1", 34 | "rxjs": "^6.3.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 2018 RxJS Advent Calendar 2 | 3 | This is the first annual RxJS Advent Calendar with 25 lessons on using [RxJS](https://github.com/reactivex/rxjs). All advent entries are written in TypeScript and can be run via in Node.js using `npx` and [ts-node](https://www.npmjs.com/package/ts-node). Follow along online at my Twitch Channel, [BluerThanBlueFalcon](https://www.twitch.tv/bluerthanbluefalcon/)! 4 | 5 | ## Daily Entries 6 | 7 | To run each entry, simply using ts-node such as the following: 8 | 9 | ```bash 10 | npx ts-node 01/index.ts 11 | ``` 12 | 13 | 1. [Creating An Observable](01/readme.md) 14 | 2. [Sequences over time](02/readme.md) 15 | 3. [Creating Observables the easy way!](03/readme.md) 16 | 4. [Creating delayed and polling operations](04/readme.md) 17 | 5. [Converting to Observables](05/readme.md) 18 | 6. [Converting Events to Observables](06/readme.md) 19 | 7. [Pipe Dreams](07/readme.md) 20 | 8. [Mapping, Plucking, Tapping, and Filtering](08/readme.md) 21 | 9. [Reducing and Scanning](09/readme.md) 22 | 10. [Starting with data, ending with data, and defaulting if empty](10/readme.md) 23 | 11. [On the Subject of Subjects](11/readme.md) 24 | 12. [Implementing Redux with RxJS](12/readme.md) 25 | 13. [Combining Sequences Part 1](13/readme.md) 26 | 14. [Combining Sequences Part Deux](14/readme.md) 27 | 15. [Combining Sequences of Sequences](15/readme.md) 28 | 16. [Projecting to new sequences](16/readme.md) 29 | 17. [Getting only the latest data](17/readme.md) 30 | 18. [Getting distinct data](18/readme.md) 31 | 19. [Throttling and Debouncing Observables](19/readme.md) 32 | 20. [Skip and Take Observables](20/readme.md) 33 | 21. [Error Handling with Observables](21/readme.md) 34 | 22. [Publishing and Sharing](22/readme.md) 35 | 23. [Buffers and Windows](23/readme.md) 36 | 24. [Parallelizing Work with ForkJoin](24/readme.md) 37 | 25. [Grouping work with groupBy](25/readme.md) 38 | 39 | ## License 40 | 41 | MIT License 42 | 43 | Copyright (c) 2018 Matthew Podwysocki 44 | 45 | Permission is hereby granted, free of charge, to any person obtaining a copy 46 | of this software and associated documentation files (the "Software"), to deal 47 | in the Software without restriction, including without limitation the rights 48 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 49 | copies of the Software, and to permit persons to whom the Software is 50 | furnished to do so, subject to the following conditions: 51 | 52 | The above copyright notice and this permission notice shall be included in all 53 | copies or substantial portions of the Software. 54 | 55 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 56 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 57 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 58 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 59 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 60 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 61 | SOFTWARE. 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "downlevelIteration": true, 4 | "removeComments": true, 5 | "preserveConstEnums": true, 6 | "sourceMap": true, 7 | "strictFunctionTypes": true, 8 | "noImplicitAny": true, 9 | "noImplicitReturns": true, 10 | "noImplicitThis": true, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "moduleResolution": "node", 13 | "stripInternal": false, 14 | "target": "es5", 15 | "outDir": "./.out", 16 | "lib": [ 17 | "es5", 18 | "es2015.iterable", 19 | "es2015.collection", 20 | "es2015.promise", 21 | "es2015.symbol", 22 | "es2015.symbol.wellknown", 23 | "dom", 24 | ] 25 | }, 26 | "formatCodeOptions": { 27 | "indentSize": 2, 28 | "tabSize": 2 29 | }, 30 | "types": ["node"] 31 | } --------------------------------------------------------------------------------