SkillAgentSearch skills...

Core

Transform-Signal-Executor framework for Reactive Streams

Install / Use

/learn @tsers-js/Core
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

TSERS

Transform-Signal-Executor framework for Reactive Streams (RxJS only at the moment... :disappointed:).

Travis Build Code Coverage NPM version Gitter GitHub issues

"tsers!"

Pronunciation: [tsers] (Also note that the /r/ is an alveolar trill, or "rolled r", not the postalveolar approximant ɹ̠ that is used in English and sometimes written similarly for convenience.)

Motivation

In the era of the JavaScript fatigue, new JS frameworks pop up like mushrooms after the rain, each of them providing some new and revolutionary concepts. So overwhelming! That's why TSERS was created. It doesn't provide anything new. Instead, it combines some old and well-known techniques/concepts and packs them into single compact form suitable for the modern web application development.

Technically the closest relative to TSERS is Cycle.js, but conceptually the closest one is CALM^2. Roughly it could be said that TSERS tries to combine the excellent state consistency maintaining strategies from CALM^2 and explicit input/output gates from Cycle - the best from both worlds.

Hello TSERS!

The mandatory "Hello world" applicatin with TSERS:

import {Observable as O} from "rx"
import TSERS from "@tsers/core"
import ReactDOM from "@tsers/react"
import Model from "@tsers/model"

function main(signals) {
  const {DOM, model$: text$, mux} = signals
  const {h} = DOM

  const vdom$ = DOM.prepare(text$.map(text =>
    h("div", [
      h("h1", text),
      h("button", "Click me!")
    ])))

  const click$ = DOM.events(vdom$, "button", "click")
  const updateMod$ = text$.mod(
    click$.map(() => text => text + "!")
  )

  return mux({
    DOM: vdom$,
    model$: updateMod$
  })
}

TSERS(main, {
  DOM: ReactDOM("#app"),
  model$: Model("Tsers")
})

Core concepts

TSERS applications are built upon the three following concepts

  1. Signals flowing through the application
  2. Signal Transform functions transforming input signals into output signals
  3. Executors performing effects based on the output signals

Signals

Signals are the backbone of TSERS application. They are the only way to transfer inter-app information and information from main to interpreters and vice versa. In TSERS applications, signals are modeled as (RxJS) observables.

  • Observables are immutable so the defined control flow is always explicit and declarative
  • Observables are first-class objects so they can be transformed into other observables easily by using higher-order functions

TSERS relies entirely on (RxJS) observables and reactive programming so if those concepts are not familiar, you should take a look at some online resources or books before exploring TSERS. One good online tutorial to RxJS can be found here.

TODO: muxing and demuxing

Signal transforms

Assuming you are somehow familiar with RxJS (or some other reactive library like Kefir or Bacon.js), you've definitely familiar with signal transform functions.

The signature of signal transform function f is:

f :: (Observable A, ...params) => Observable B

So basically it's just a pure function that transforms an observable into another observable. So all observable's higher order functions like map, filter, scan (just to name a few) are also signal transformers.

Let's take another example:

function titlesWithPrefix(item$, prefix) {
  return item$
    .map(it => it.title)
    .filter(title => title.indexOf(prefix) === 0)
}

titlesWithPrefix is also a signal transform function: it takes an observable of items and the prefix that must match the item title and returns an observable of item titles having the given prefix.

titlesWithPrefix :: (Observable Item, String) => Observable String

And as you can see, titlesWithPrefix used internally two other signal transform functions: map and filter. Because signal transform functions are pure, it's trivial to compose and reuse them in order to create the desired control flow from input signals to output signals.

If the signals are the backbone of TSERS applications, signal transformers are the muscles around it and moving it.

Executors

After flowing through the pure signal transformers, the transformed output signals arrive to the executors. In TSERS, executors are also functions. But not pure. They are functions that do nasty things: cause side-effects and change state. That is, executors' signature looks like this:

executor :: Observable A => Effects

Let's write an executor for our titles:

function alertNewTitles(title$) {
  title$.subscribe(title => {
    alert(`Got new title! ${title}`)
  })
}

And what this makes executors by using the previous analogy... signals flowing through the backbone down and down and finally to the... anus :hankey:. Yeah, unfortunately the reality is that somewhere in the application you must do the crappy part: render DOM to the browser window, modify the global state etc. In TSERS applications, this part falls down to executors.

But the good news is that these crappy things are (usually) not application specific and easily generalizable! That's why TSERS has the interpreter abstraction.

Application structure

As told before, every application inevitably contains good parts and bad parts. And that's why TSERS tries to create an explicit border between those parts: the interpreter abstraction.

The good (pure) parts are inside the signal transform function main, and the bad parts are encoded into interpreters.

Conceptually the full application structure looks like:

function main(input$) {
  // ... app logic ...
  return output$
}
interpreters = makeInterpreters()
output$ = main(interpreters.signals())
interpreters.execute(output$)

main

main function is the place where you should put the application logic in TSERS application. It describes the user interactions and as a result of those interactions, provides an observable of output signals that are passed to the interpreters' executor functions.

That is, main is just another signal transform function that receives some core transform functions (explained later) plus input signals and other transform functions from interpreters. By using those signals and transforms, main is able to produce the output signals that are consumed by the interpreter executors.

Interpreters

Interpreters are not a new concept: they come from the Free Monad Pattern. In common language (and rounding some edges) interpreters are an API that separates the representation from the actual computation. If you are interested in Free Monads, I recommend to read this article.

In TSERS, interpreters consist of two parts:

  1. Input signals and/or signal transforms
  2. Executor function

Input signals and signal transforms are given to the main. They are a way for interpreter to encapsulate the computation from the representation. For example HTTP interpreter provides the request transform. It takes an observable of request params and returns an observable of request observables (request :: Observable params => Observable (Observable respose)).

Now the main can use that transform:

function main({HTTP}) {
  const click$ = ....
  const users$ = HTTP.request(click$.map(() => ({url: "/users", method: "get"})).switch()
  // ...
}

Note that main doesn't need to know the actual details what happens inside request - it might create the request by using vanilla JavaScript, superagent or any other JS library. It may not even make a HTTP request every time when the click happens but returns a cached result instead! It's not main's business to know such things.

Some interactions may produce output signals that are not interesting in main. That's why interpreters have also possibility to define an executor function which receives those output signals and interprets them, (usually) causing some (side-)effects.

Let's take the DOM interpreter as an example. main may produce virtual dom elements as output signals but it's not interested in how (or where) those virtual dom elements are rendered.

function main({DOM}) {
  const {h} = DOM 
  return DOM.prepare(Observable.just(h("h1", "Tsers!")))
}

What to put into main and what into interpreters?

In a rule of thumb, you should use interpreter if you need to produce some effects. Usually this reduces into three main cases:

  1. You need to use Observable's .subscribe - you should never need to use that inside the main
  2. You need to communicate with the external world somehow
  3. You need to change some global state

Encoding side-effects into signal transforms or output signals?

In a rule of thumb, you should encode the side-effects into signal transform functions if the input signal and the side effect result signal have a direct causation, for example request => response.

You should encode the side-effects into output signals and interpret them with the executor when the input there is no input => output causation (o

View on GitHub
GitHub Stars145
CategoryDevelopment
Updated2y ago
Forks4

Languages

JavaScript

Security Score

80/100

Audited on Sep 8, 2023

No findings