Сhannel - Universal Reactive Abstraction
Tags: webdev, javascript, architecture, reactivity
Published: 2024-02-10 21:14:41

It is good to describe reactive invariants in the semantics of *pull*, so as not to calculate them again. But to process actions, we need *push* semantics, so as not to miss a single event, but to deliver it to the right place in the right form, from where further pull will begin.

In native JS there is a special syntax for this - getters and setters:

```ts
_title = ''
get title() { return this._title } // pull
set title( text: string ) { this._title = text } // push
```

But it have a lot of limitations:

- We cannot pass any additional parameters.
- It require an owner object.
- It cannot be asynchronous.
- Quite a lot of code, especially when delegating.

In some frameworks, hooks like this are common:

```ts
//               push         pull
const [ title, setTitle ] = useState( '' )
```

But it isn't efficient and it isn't convenient to use, since it don't separate the initialization phase (setting the default, receiving a push callback) from the phase of working with values (pulling and pushing the value). In addition, for two-way binding you have to write a lot of code like:

```jsx
<Input value={ title } onChange={ setTitle } />
```

We will introduce the abstraction **"channel" - a function that, depending on the number of arguments, can act as both a getter and a getter-setter**. You can both *pull* the current value out of it and *push* a new one into it, obtaining the current result.

Let's create a simple channel to understand its essence:

```ts
let _title = ''
const title = ( text = _title )=> _title = text

title() // ''
title( 'Buy some milk' ) // 'Buy some milk'
title() // 'Buy some milk'
```

Experience with various abstractions in JS has shown that channels are the most practical of them. It abstract the consumer who wants to read and/or write some value from how that value is actually obtained and stored.

For example, we can easily ignore writing a value, and when reading, generate it on the fly, thus obtaining a *read-only* channel:

```ts
let id = ( dur?: number )=> Math.floor( Math.random() * 100 )

id() // 34
id( 18 ) // 83
id() // 13
```

We can make it *write-only* events, using it as an event handler:

```ts
let _completed = false
complete( event: Event ) {
  _completed = true
}
```

Or we can completely delegate one channel to another:

```ts
let details = ( text?: string )=> title( text )

details() // ''
details( 'Buy some milk' ) // 'Buy some milk'
details() // 'Buy some milk'
```

Thus, channels can be arranged in chains, transforming data as it intersects layers of abstractions:

```ts
let details_html = ( html?: string )=>
html_encode(
  details(
    html && html_decode( html )
  )
)

details() // ''
details_html( 'Buy milk &amp; bread' ) // 'Buy milk &amp; bread'
details() // 'Buy milk & bread'
details_html() // 'Buy milk &amp; bread'
```

**Channels can be**:

- **stateless** - don't storing any data, but only access other channels.
- **stateful** - storing initial data or calculation caches.

Note that singleton channels are not very practical, because they do not allow us to decompose the application. That's why we need OOP...

# Object Reactive Programing

Objects allow us to group channels that are related in meaning into a single capsule. Moreover, they allow us to have many such capsules built from the same code (class instances). Simple example:

```ts
class Task extends Object {

  // stateful channel
  _title = ''
  title( title = this._title ) {
    return this._title = title
  }

  //stateless channel
  details( details?: string ) {
    return this.title( details )
  }

}
```

Note that if everything is quite simple with delegates, then with channels that store state, everything is not so rosy: the channel name has to be repeated 4 times. This means that when copy-pasting there will inevitably be problems, because you will have to make the same edits 4 times. And auto-refactoring will not help us here.

Well, notice that we always return the value that we store, and we always store what we want to return. This means that we can write a decorator that will take care of all this:

```ts
class Task extends Object {

  @mem title( title = '' ) {
    return title
  }

  details( details?: string ) {
    return this.title( details )
  }

}
```

Well, or like this, if we are unlucky with the programming language:

```ts
class Task extends Object {

  title( title = '' ) {
    return title
  }

  details( details ) {
    return this.title( details )
  }

}
mem( Task.prototype, 'title' )
```

All we need to know now is that **the $mol_wire_solo decorator memoizes the value returned from the method, regardless of whether we passed it an argument or not**. However, if the argument was not passed, and there is already something in the cache, then the method call is skipped and the value from the cache is immediately returned. And this will continue until the cache is cleared. And if the cache is empty, the method is still called to get the default value. Well, if the argument is passed, then the method will be called in any case to update the cache.

In addition to code brevity, memoization gives us a couple more important properties: computational savings and idempotency. Let's illustrate this with the following example:

```ts
class Task_extended extends Task {

  @mem title_for_mirror() {
    const segmenter = new Intl.Segmenter()
    const segments = [ ... segmenter.segment( this.title() ) ]
    return segments.map( s => s.segment ).reverse().join('')
  }

  @mem Duration() {
    return new $mol_time_duration({ hour: this.duration() })
  }

}
```

The first method is quite heavy, but thanks to memoization it will be executed only once, and lazily, at the time of the first call, and then the value will be returned from the cache until the value of `title` changes, of course.

The second method is not so much heavy as it returns a new object each time, which is often unacceptable. Here memoization makes it possible to guarantee that no matter how many times we call a method, the result of its call will be the same. This property is called idempotency.

Experience suggests that when designing an application architecture, it is extremely important that it be as idempotent as possible, because **any non-idempotency is a time bomb**. We will not substantiate this in detail now. However, idempotency will come in handy later.

For now, let’s remember the famous [statement](https://www.karlton.org/2017/12/naming-things-hard/\\Phil Karlton):

> There are only two hard things in Computer Science: cache invalidation and naming things.

Two common problems in any field of programming are mentioned here. So, in this series we will solve the first completely, and the second... not completely, but we will make it less problematic.

# Recomposition

A channel can produce not only simple types of data, but also complex ones, including those collected from other channels. For example, let's collect all the object data in the form of one [DTO](https://en.wikipedia.org/wiki/DTO):

```ts
class Task extends Object {

  @mem title( title = '' ) {
    return title
  }

  @mem duration( dur = 0 ) {
    return dur
  }

  @mem data(
    data?: Readonly< Partial< {
      title: string
      dur: number
    } > >
  ) {
    return {
      title: this.title( data?.title ),
      dur: this.duration( data?.dur ),
    } as const
  }
	
}
```

Please note that through a compound channel we can update several simple channels at once:

```ts
const task = new Task
task.data() // { title: '', dur: 0 }

const data = task.data({
  title: 'Buy milk',
  dur: 2,
})
task.title() // 'Buy milk'
task.duration() // 2
```

Conversely, channels can be **lenses that allow you to work with part of a large immutable structure, as with an independent mutable entity**:

```ts
class Task extends Object {

  @mem data( data = {
    title: '',
    dur: 0,
  } ) {
    return data
  }

  @mem title( title?: string ) {
    return this.data(
      //title === undefined ? undefined : { ... this.data(), title }
      title?.valueOf && { ... this.data(), title }
    ).title
  }

  @mem duration( dur?: number ) {
    return this.data(
      dur?.valueOf && { ... this.data(), dur }
    ).dur
  }

}
```

Please note that these two diametrically opposed approaches lead to the same object interface. This is **the main advantage of channels - they allow you to change internal implementation within a fairly wide range without affecting consumers**. Moreover, it allow you to customize the internal operation of objects from outside without breaking their operation. But more on that later.

# Multiplexing

So far we have named each channel individually. But sometimes we need to write one code for an unlimited set of channels. Here **multiplexed channels come to our aid, where the first parameter is the key identifying the channel**. For example, let's get rid of the copy-paste of relatively complex logic from the previous section, moving it into a common super class:

```ts
class Entity extends Object {

  constructor(
    readonly id: number
  ) { super() }

  destructor() {}
	
  @mem data( data = {} ) {
    return data
  }

  @mem value<
    Field extends keyof ReturnType< this['data'] >
  >(
    field: Field,
    value?: ReturnType< this['data'] >[ Field ],
  ): ReturnType< this['data'] >[ Field ] {
		
    return this.data( value === undefined
      ? undefined
      : {
        ... this.data(),
        [ field ]: value,
      }
    )[ field as never ]
		
  }

}
```

Now, we can work with the `data` channel through the multiplexed `value` channel, without having to do manual (de)structuring:

```ts
class Task extends Entity {

  @mem data( data = { title: '', dur: 0 } ) {
    return data
  }

  title( title?: string ) {
    return this.value( 'title', title )
  }
	
  duration( dur?: number ) {
    return this.value( 'dur', dur )
  }

}
```

Now let's redirect state storage from memory to local storage:

```ts
// At first tab
const task0 = new Task
task0.data = data => $mol_state_local.value( `task=0`, data )
  ?? { title: '', cost: 0, dur: 0 }
task0.title( 'Buy some milk' ) // 'Buy some milk'

// At second tab
const task0 = new Task
task0.data = data => $mol_state_local.value( `task=0`, data )
  ?? { title: '', cost: 0, dur: 0 }
task0.title()                  // 'Buy some milk'
```

And since our channels are reactive, then [applications in different tabs receive instant synchronization automatically](https://www.youtube.com/watch?v=frddLjdkKpA):

{% embed https://www.youtube.com/embed/frddLjdkKpA %}