Core
Transform-Signal-Executor framework for Reactive Streams
Install / Use
/learn @tsers-js/CoreREADME
TSERS
Transform-Signal-Executor framework for Reactive Streams (RxJS only at the moment... :disappointed:).
"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
- Signals flowing through the application
- Signal Transform functions transforming
inputsignals intooutputsignals - Executors performing effects based on the
outputsignals
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:
- Input signals and/or signal transforms
- 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:
- You need to use Observable's
.subscribe- you should never need to use that inside themain - You need to communicate with the external world somehow
- 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
