Reactive Interactions with External Systems
Tags: webdev, javascript, architecture, reactivity
Published: 2024-02-08 08:49:00

Sometimes an invariant requires asynchronous communication. For example, for heavy calculations in a separate worker. Most reactive libraries do not support asynchronous invariants, but there are some that do. Let's consider both options..

> 🏊 Sync: Synchronous invariants
> 🏇 Async: (A)synchronous invariants

# 🏊 Sync

If only synchronous reactivity is supported, and we need to perform some kind of asynchronous call, then it usually goes somewhere to the side. Let's take a simple example using RxJS...

```ts
const image = source_element.pipe( map( capture ) )
const data = image.pipe( map( recognize ) )
const text = data.pipe( map( data => data.text ) )

text.subscribe( text => {
  output.innerText = text
} )
```

The `capture` and `recognize` functions are asynchronous, since the first one needs to wait for the image to load, and the second one launches neurons on a pool of workers. When we change `source_element`, `output.innerText` will not change at all. That is, the states will no longer be consistent. And they will come to consistency only when all asynchronous operations are completed.

This problem is usually solved by interactively setting some `isLoading` flag at the beginning and interactively resetting it at the end. And when this flag is raised, the waiting indicator is drawn reactively.

Not only is this a routine, but it is also often prone to bugs when several tasks being performed are tied to one indicator. Which, with interactive logic, can cause a so-called *race condition*.

# 🏇 Async

If asynchronous invariants are also supported, then runtime maintains consistency automatically. A typical solution is through a mechanism for dealing with emergency situations. Let's write what the code might look like using, for example, generators..

```ts
@computed
text*() {
  const image = yield capture( this.source_element )
  const data = yield recognize( image )
  return data.text
}
```

Why not asynchronous functions? Yes, because they are made in JS through the ass. So the authors of libraries have to rely on generators that are made through the opposite place, but also not through what they should.

In fact, you can even do without generators. $mol_wire and React support SuspenseAPI, which allows you to write pseudo-synchronous code and not have to worry about `yield` and `await`. Well, it doesn’t matter, the generators for my story will be clearer.

When runtime calls the `text` generator, it is given a promise instead of a string. It understands that the final result will come later, subscribes to the finalization of the promise, and in the meantime marks the state as “pending value”. This wait flag applies to all dependent states. And the rendering system, seeing this, automatically draws a waiting indicator. Cool!

# Function Colors

There are two value access interfaces: **sync** and **async**. Let's look at their features...

Synchronous access assumes that the returned value will always be valid. If there is nowhere to get such a value, then an exception occurs, which can either be skipped or intercepted and processed:

```ts
something(): string {

  try {

    // returns always string
    return do_something()

  } catch( cause: unknown ) {

    if( cause instanceof Error ) {
      // Usual error handling
    }

    if( cause instanceof Promise ) {
      // Suspense API
    }

    // Something wrong
  }

}
```

Please note that not only an error, but also a promise can be thrown as an exception. This is the so-called Suspense API, which allows you to work with asynchronous code as if it were synchronous. And this can be supported in a wide variety of libraries. It works like this:

1. A synchronous idempotent function is called.
2. If you need to wait, then a promise is thrown as an exception.
3. This exception is caught outside that synchronous function and a subscription is made to finalize the promise.
4. Once the promise is finalized, the function is automatically called again.
5. This time, instead of throwing a promise, the result of the promise is immediately returned, or an error is thrown from it.

Asynchronous access, on the contrary, always returns a `Promise` even if there is already a valid value and asynchrony is not needed:

```ts
async something(): Promise< string > {

  try {

   // returns always string
   return await do_something()

  } catch( cause: unknown ) {

    if( cause instanceof Error ) {
      // Usual error handling
    }

    // Something wrong
  }

}
```

With synchronous code, we can easily automatically track dependencies by temporarily placing the subscriber in a global variable:

```ts
function tracked() {
  let backup = current
  current = new Subscriber
  try {
    // sync code with tracking deps
  } finally {
    current = backup
  }
}
```

With asynchronous, this will not work, since at the very first await we will exit the function without rolling back the global variable, which will break everything:

```ts
async function tracked() {
  let backup = current
  current = new Subscriber
  try {
    // sync part with tracking deps
    await something // dirty current
    // other path with wrong current
  } finally {
    // break others current
    current = backup
  }
}
```

You can abandon asynchronous functions and switch to generators, as they did in MobX:

```ts
const tracked = suspendable( function*() {
  let backup = current
  current = new Subscriber
  try {
    // sync part with tracking deps
    yield something // suspendable temp rollbaks current
    // other part with continue tracking deps
  } finally {
    current = backup
  }
} )
```

But generators, like asynchronous functions, do not solve the very exhausting problem of [color functions](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/), but only aggravate it by introducing another color. In addition, they are also much slower than synchronous code, since [they cannot be properly optimized by the JIT compiler](https://perf.js.hyoo.ru/#!bench=l6uytl_2ob3af):

![](https://i.imgur.com/As8PKhp.png)

Let’s summarize why it’s better to leave most of the code synchronous:

- It's easier to understand.
- It can be called from any function.
- It can be passed as a callback to any function.
- It's faster.
- It requires less memory.
- It's easy and reliable to track dependencies.

## Recoloring functions

Many native APIs and third-party libraries are asynchronous and we must be able to transparently integrate with them. That is, we need mechanisms for transparent transformation of a synchronous API into an asynchronous one and vice versa.

To do this, we implement a couple of wrappers:

- [$mol_wire_sync](https://github.com/hyoo-ru/mam_mol/tree/master/wire/sync) makes any API synchronous.
- [$mol_wire_async](https://github.com/hyoo-ru/mam_mol/tree/master/wire/async) makes any API asynchronous.

For example, let's implement the simplest synchronous json loading function:

```ts
function getData( uri: string ): { lucky: number } {
  const request = $mol_wire_sync( fetch )
  const response = $mol_wire_sync( request( uri ) )
  return response.json().data
}
```

And vice versa, let's implement an asynchronous function that is compatible with SuspenseAPI, and does not fall with `Promise` logging to the console:

```ts
async function lucky_update( uri: string ): Promise< undefined > {

  const fetchData = $mol_wire_async( getData )
  const data = await fetchData( uri )
  data.lucky = 777

  await fetch( uri, {
    method: 'put',
    body: JSON.stringify({ data }),
  } )

}
```

Thus, synchronously describing any application logic, be it reactive invariants or interactive actions, we can always pause on an asynchronous task, and the reactive system will respond adequately to this: dependency tracking will not break, and the fiber will automatically restart when necessary.

## Concurrent tasks

Once our tasks start pausing to allow other tasks to run, we have contention problems where the same task runs multiple times and we have to decide what to do about it.

With invariants, everything is trivial: if any state on which a suspended atom depends has changed, then this atom is simply restarted under new conditions, so no competition arises.

But with actions everything is more interesting: they are launched from the outside in response to an event:

```ts
button.onclick = function() {
   // run multiple tasks concurrently
   $mol_wire_async( counter ).sendIncrement()
}
```

Here, each time a new task will be launched that is in no way related to the already running ones. How many times you click - so many tasks will start working simultaneously. But often this is a waste of resources, and even harmful behavior. Therefore, as a rule, it is better to launch actions so that the new launch cancels the previous one:

```ts
// next run abortions previous
button.onclick = $mol_wire_async( function() {
   counter.sendIncrement()
} )
```

Since here a wrapper around the function is not created every time, but the same one is used, no matter how many times we click on the button, only the last launched task will reach the end, and the rest will be cancelled. If they did not manage to be completed by the time the new one was launched, of course.

And if we insert a time delay at the beginning of the task, then due to this behavior we simply get *debounce*:

```ts
button.onclick = $mol_wire_async( function() {
   $mol_wait_timeout( 1000 )
   // no last-second calls if we're here
   counter.sendIncrement()
} )
```

## Cascade Abortions

When the promises were still in their infancy, there were many different implementations. The most advanced of them supported [cancellation of asynchronous tasks](http://bluebirdjs.com/docs/api/cancellation.html). Unfortunately, this functionality was not included in the JavaScript standard, and we are offered to manually drag an instance of [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) through the parameters, which is not very convenient .

However, with us, all fibers form a connected graph, which means the cancellation of any fiber should lead to unsubscribes, followed by automatic cascading cancellation of other fibers that remain without subscribers. And if a fiber owns an object, then when it is destroyed it also calls its destructor, in which we can already implement our logic. For example, let's implement a simple HTTP data loader with the ability to cancel an incomplete request:

```ts
const fetchJSON = $mol_wire_sync( function fetch_abortable(
  input: RequestInfo,
  init: RequestInit = {},
) {

  const controller = new AbortController
  init.signal ||= controller.signal

  const promise = fetch( input, init )
    .then( response => response.json() )

  const destructor = ()=> controller.abort()
  return Object.assign( promise, { destructor } )

} )
```

Now we can simply call functions and not worry about storing controllers and passing signals, but be sure that all connections will be canceled correctly:

```ts
button.onclick = $mol_wire_async( function() {

  const { profile } = fetchJSON( 'https://example.org/input' )

  fetchJSON( 'https://example.org/output', {
    method: 'PUT',
    body: JSON.stringify( profile ),
  } )

} )
```

It is important to note that if you make multiple conversions from a synchronous API to an asynchronous one and vice versa, then the fibers do not form a single graph, since the graph will be broken at points of asynchrony. Accordingly, due to the lack of a cancellation mechanism for native promises, you will have to send the corresponding signals manually. But this is a rather marginal case, since normally the entire application should form a single graph with a synchronous API, and asynchrony should appear only at the interface with external APIs.