Understanding ngrx/component-store debounceSync()
Tags: angular, ngrx, rxjs, opensource
Published: 2020-10-12 20:27:07

In the previous post we looked at the `debounce` option for `@ngrx/component-store` selectors and how it changes their behaviour. In this post we look at the code that makes this possible and figure out how it works.

The code is located [here in NgRx](https://github.com/ngrx/platform/blob/master/modules/component-store/src/debounce-sync.ts) and can also be found in [rxjs-etc](https://github.com/cartant/rxjs-etc) where it was originally created by [Nicholas Jamieson](https://twitter.com/ncjamieson). As it is published with the MIT license and all conditions are fulfilled by NgRx they are able to avoid having an extra dependency here. 

## Selector Debouncing

Inside the `select` method of `component-store` is the following line of code. This takes our `debounce` config value and either adds the `debounceSync()` operator or returns the original source with no change.

```ts
this.stateSubject$.pipe(
     config.debounce ? debounceSync() : (source$) => source$,
     map(projector)
);
```
What is this `debounceSync()` operator doing?

## The `debounceSync()` code

Below is the code for `debounceSync()`. When I first looked at this I had no idea what was going on! So I reached for our trusty friend `console.log` and started experimenting in Stackblitz. You can try this too [here](https://stackblitz.com/edit/debouncesync?devtoolsheight=33&file=index.ts).

```ts
export function debounceSync<T>(): MonoTypeOperatorFunction<T> {
  return source =>
    new Observable<T>(observer => {
      let actionSubscription: Subscription | undefined;
      let actionValue: T | undefined;
      const rootSubscription = new Subscription();
      rootSubscription.add(
        source.subscribe({
          complete: () => {
            console.log("COMPLETE", { actionSubscription, actionValue });
            if (actionSubscription) {
              observer.next(actionValue);
            }
            observer.complete();
          },
          error: error => {
            console.log("ERROR", { actionSubscription });
            observer.error(error);
          },
          next: value => {
            console.log("NEXT", { actionSubscription, value });
            actionValue = value;
            if (!actionSubscription) {
              actionSubscription = asapScheduler.schedule(() => {
                console.log("ASAP", { actionSubscription, actionValue });
                observer.next(actionValue);
                actionSubscription = undefined;
              });
              rootSubscription.add(actionSubscription);
            }
          }
        })
      );
      return rootSubscription;
    });
}
```
To run the code I setup two streams. One uses `interval` to be asynchronous and the other uses `from` to run synchronously. 

```ts
console.warn("Before interval");

interval(1).pipe(
  debounceSync(),
  take(3)
).subscribe(val => console.log("interval", val));

console.warn("Before from");

from([10, 20, 30]).pipe(
  debounceSync()
).subscribe(val => console.log("fromArray", val));

console.warn("After From");
```

This along with the logging I added into the `debounceSync()` function gave me the following output.

![Console Output](https://dev-to-uploads.s3.amazonaws.com/i/aolhgbx0llwuby9v5f3l.png)

The first thing to notice is that the `interval` code does not appear between the `Before interval` and `Before from`. This is because this interval code is running asynchronously. What's more interesting is that it appears right at the end, even after our `fromArray` code! This demonstrates how synchronous code is executed before asynchronous and that Observables can run synchronously. 

This behaviour comes down to the Event Loop. If you are not familiar with the Event loop it would be worth reading this fantastic [article](https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif) to learn more before carrying on. (It even has animations to demonstrate it!)

## The Differences

If we compare the two outputs we get an idea of what the code is doing. On the left we have the synchronously `from` stream and on the right the asynchronous `interval` stream.

![Log output, asynchronous code logs ASAP](https://dev-to-uploads.s3.amazonaws.com/i/rr5sema9fkdzie8t0dmg.png)

In both cases the first value comes in and as the `actionSubscription` is undefined it creates one. This `actionSubscription` uses the [`asapScheduler`](https://rxjs.dev/api/index/const/asapScheduler) to register its call-back method.

> asapScheduler: Perform task as fast as it can be performed asynchronously

In the call-back it passes the value to the next observer in the chain and also clears the current `actionSubscription`.

```ts
next: value => {
   console.log("NEXT", { actionSubscription, value });
   actionValue = value;
   if (!actionSubscription) {
       actionSubscription = asapScheduler.schedule(() => {
           console.log("ASAP", { actionSubscription, actionValue });
           observer.next(actionValue);
           actionSubscription = undefined;
       });
       rootSubscription.add(actionSubscription);
   }
}
```

### Synchronous

With a fully synchronous event stream the `actionSubscription` never gets to fire. This is because all the synchronous events are on the call stack and these must be executed before the `asapScheduler` can run. So any new values just update the internal `actionValue` without being passed to the next observer.

As we are using the `from` operator it completes synchronously after the 3 values are emitted and before the `asapScheduler` has the chance to run. This is why the `complete` method checks for the existence of an outstanding `actionSubscription` and if it is defined it then emits the current `actionValue` to the observer before completing. Without this check no values would be emitted.

```ts
complete: () => {
    if (actionSubscription) {
        observer.next(actionValue);
    }
    observer.complete();
},
```

Note with the synchronous code only the last value actually gets emitted. This is why this function is called `debounceSync` because it debounces synchronous events. 

### Asynchronous

With the asynchronous events we see that every value gets emitted via the `actionSubscription`. This is because the `asapScheduler` will get the opportunity to run between the asynchronous `interval` events. Each time the `asapScheuler` fires the current `actionSubscription` is cleared so the the next value will also setup a new `actionSubscription` to enable that to be emitted via the call-back too.

# Summary

The `debounceSync()` operator works by recording each new event in a local variable `actionValue` which will only be emitted when the observable completes or after the call stack has been cleared (all synchronous events have completed). The asynchronous call-back uses the `asapScheduler` to keep any introduced delay to a minimum. 

After writing this article the following section in the [docs](https://ngrx.io/guide/component-store/read#debounce-selectors) makes a lot more sense to me.

> Sometimes the preferred behaviour would be to wait (or debounce) until the state "settles" (meaning all the changes within the current microtask occur) and only then emit the final value.

## Multiple Selectors

At this point it is worth noting that as long as all the events are synchronous you may find that debouncing enables feedback loops in selectors to stabalize before emitting. Our example has not covered this use case but it is the main advantage of debouncing selectors as I see it.

### Alternative Implementation

This function is equivalent to using the `debounceTime` operator with a delay value of 0 setup with the `asapScheduler`.

```ts
debounceTime(0, asapScheduler)
```

[Alex Okrushko](https://twitter.com/AlexOkrushko), from the NgRx core team, said that this was the first thought for synchronous debouncing however this has the side effect of creating extra timers that are not required for this specific behaviour.  [Nicholas Jamieson](https://twitter.com/ncjamieson)  suggested to use this custom operator, `debounceSync` which he created to ensure the best possible performance for the NgRx Component Store.

## Further Reading

I learnt a lot about this feature from the [NgRx Discord](https://discord.com/invite/gCEj4zC) channel which you should definitely join if you have not already!