Riew is a reactive library that uses CSP concepts for managing data and application flow.
๐ Learning materials
- Reactive view - the concept
- Part 1: We need channels! A gentle introduction to CSP
- Part 2: Riew - reactive view basics
- Part 3: Riew - reactive view in patterns
๐ฎ Playgrounds
- Online demo.
- Examples
Table of contents
- Concepts
- API
- Inspiration
Imagine that you need to transfer messages between two functions in your system. They don't know about each other. With Riew you can use a channel to connect and synchronize them. We can put and take messages from that channel. Consider the following example:
const ch = chan();
go(function * A() {
const name = yield take(ch);
yield put(ch, `Hey ${name}, how are you?`);
});
go(function * B() {
yield put(ch, "Steve");
console.log(yield take(ch));
});
We have two generator functions (routines) A
and B
. They start synchronously one after each other. However, A
is paused (blocked) at the yield take
statement because it wants to read from the channel ch
but there is nothing inside. Then routine B
puts Steve
and routine A
resumes back. Now B
is blocked because it tries to read from the same channel. Steve
is already consumed by the other routine so we are again at the same blocking situation. B
waits till A
puts Hello Steve, how are you?
. At the end the log happens and we see the message into the console.
This is the basic idea behind CSP. We have channels that are used for communication and synchronization. By default the channel operations are blocking. Putting can't happen until there is someone to take and the opposite - taking can't happen until there is someone to put. This is the behavior of the standard non-buffered channel or the so called fixed buffer. There are couple of other types and we will learn about them below.
As for the routines, we may yield
all sort of things. We may put
, take
, sleep
but we may also yield
a promise. Then Riew will wait till the promise is resolved and will resume the generator. We may even use the call
and fork
helpers to run other routines. ๐ฎ
The riew is a combination between view function and routine functions. It's materialized into an object that has mount
, update
and unmount
methods. The routines get started when we mount the riew. They receive a render
method so we can send data to the view function.
const ch = chan();
const view = function(props) {
console.log(props);
};
function* A() {
const name = yield take(ch);
yield put(ch, `Hey ${name}, how are you?`);
}
function* B({ render }) {
yield put(ch, 'Steve');
render({ message: yield take(ch) });
}
const r = riew(view, A, B);
r.mount();
This example prints out an object { message: "Hey Steve, how are you?" }
. As we know from the previous section, B
routine waits till it receives the message formatted by routine A
. It sends it to the view
function by using the render
function. This code sample illustrates one of the core concept behind this library - keep the view pure and distribute the business logic across routines.
We may directly send a channel to the render
function and whatever we put
inside will reach the view. For example:
const view = function (props) {
console.log(props);
}
function * A({ render }) {
const ch = chan();
render({ name: ch })
yield put(ch, 'Martin');
}
const r = riew(view, A);
r.mount();
The result here is { name: 'Martin' }
.
(There is a React extension bundled within the library so if you use React you'll probably never call mount
, update
and unmount
manually. This is done by the library.)
In the original CSP paper there is no concept of a state. At least not in the same way as we use it in JavaScript today. For us state is a value that persist across time. It can be accessed, changed and it is always available. The channels can keep values but they are consumed at some point. Or in other words taken and don't exists as such in the channels anymore.
Riew brings the idea of a state by defining a value that is outside the channels. It can be however accessed and modified by using channels. Imagine the state as a black box with a value and channels for writing and reading that value. Riew follows this idea by allowing the definition of multiple read channels called selectors and multiple write channels called mutators. Let's see an example:
// A state which value is an empty array.
const users = state([]);
// Channel for updating the state value.
const add = users.mutate(function reducer(currentUsers, newUser) {
return [...currentUsers, newUser];
});
// Channel for reading the state value.
const getUsers = users.select(function mapping(users) {
return users.map(({ name }) => name).join(', ');
});
go(function* A() {
yield put(add, { name: 'Steve', age: 24 });
yield put(add, { name: 'Ana', age: 25 });
yield put(add, { name: 'Peter', age: 22 });
console.log(yield take(getUsers)); // Steve, Ana, Peter
});
The mutate
method of the state accepts a reducer
function and returns a channel. Let's say that we put "foo" in the channel. The reducer will be called with the current state value followed by "foo" string.
The select
method returns a channel which we can read from and the value is whatever the mapping
function returns. That function receives the current state value and returns it. Or maybe just a portion of it. Or perhaps a different representation of the data. Like in our case we return a comma separated list of user names.
A helpful pattern is to use a routine as a mutator. And because the routine may be asynchronous you may block until the mutation is done. Consider the trivial case where we have to get data from remote endpoint and show it to the user.
const cat = state(null);
const getKitty = cat.mutate(function * () {
const { file } = yield fetch('https://aws.random.cat/meow').then(res => res.json());
return file;
});
go(function * A() {
console.log('I want a kitty.');
yield put(getKitty);
console.log(`Here we go ${ yield take(cat) }`);
});
The routine A
is blocked on the put to put(getKitty)
. Our mutator is picked up and makes a request to https://aws.random.cat/meow
. Once it finishes it mutates the state and replaces null
with a URL. Then our routine is resumed and we can print that URL.
> I want a kitty.
> Here we go https://purr.objects-us-east-1.dream.io/i/W6jh8.jpg
Further more we can handle the request error inside the mutator and put something else in the cat
state.
The PubSub pattern is widely used in JavaScript. There are tons of libraries that are using it and it's a de-facto standard these days. I (the author of this library) however think that this pattern doesn't play well with the CSP concepts. I've made couple of tests and tried implementing it with pure CSP concepts but it simply doesn't work as expected. That's because in the PubSub pattern we have a broadcasting system. A system in which the dispatcher of the message doesn't care what happens with the dispatched message. The act of message sending is not a blocking operation. In CSP is quite opposite. When we put something into the channel we are blocked until someone takes it. Also in PubSub we have one-to-many relationship and all the subscribers receive the same message. While in CSP if we hook multiple takers to a channel they'll all receive different messages because once the message is consumed it disappears from the channel and the next taker will read the next message. CSP and PubSub are kind of similar concepts. They both follow the push model and could be used to write code in a reactive way. However, they act differently.
Riew offers PubSub pattern capabilities. They are however added next to the core CSP processes and the developer needs to make a clear separation between the two. Consider the following example:
go(function* A() {
console.log(`Value: ${yield read(ch)}`);
});
go(function* B() {
yield put(ch, 'Foo');
console.log('Bar');
});
The result of this snippet is only "Value: undefined"
. The read function only reads the value but doesn't consume it from the channel. It also resoloves with whatever the channel contains at this exact moment. Above we are getting undefined
because there is nothing in the channel yet. Also routine B
stays blocked because there is no taker for the ch
channel.
By default read
(and its standalone version sread
) is one-shot operation. This means that we aread and move forward. However, we may use the listen
method and effectively subscribe for values passed to the channel.
const ch = chan();
listen(ch, value => {
console.log(`Value: ${value}`);
});
sput(ch, 'foo'); // Value: foo
sput(ch, 'bar'); // Value: bar
sput(ch, 'moo'); // Value: moo
So, there is a way to use the PubSub pattern. It's just the difference between reading and taking.
chan(id, buff)
orchan(buff)
Creates a new channel with ID equal to id
and buffer equal to buff
.
id
(String
, optional) - must be unique for your system. If not specified it will be auto-generated by the library.buff
(Buffer
, optional) - buffer created by using thebuffer
helpers. If not specifiedbuffer.fixed()
is used.
The function returns a channel object with the following methods/fields:
channel.id
- the ID of the channel.channel.isActive()
- returnstrue
if the channel is in anOPEN
state.channel.state()
- returns one of the following:OPEN
,CLOSED
orENDED
. When thechannel
isOPEN
we can put and take from it. When it isCLOSED
every put gets resolved withCLOSED
. Thetake
on aCLOSED
channel consumes the values left in the channel. If no values thetake
will be resolved withCLOSED
. When a channel isENDED
bothput
andtake
are resolved withENDED
.exportAs(key)
- this is useful when we want to share this channel with the rest of the application without passing the instance around. Check this section for more information.
Example:
const ch = chan('FOOBAR');
console.log(ch.id); // FOOBAR
console.log(ch.state()); // Symbol(OPEN)
console.log(ch.isActive()); // true
It creates a fixed buffer with size 0. The put
to the channel is blocked until a take
happens. A take
is blocked until a put
happens.
Example:
const ch = chan('MYCHANNEL', buffer.fixed());
go(function * A() {
yield put(ch, 'foo');
});
go(function * B() {
yield sleep(1000);
const value = yield take(ch);
console.log(value); // foo
});
- Routine A starts and stops at the
yield put
. - Routine B starts and stops at
yield sleep
for one second. - Routine B is resumed and
take
s 'foo' from the channel. - Routine A is resumed and it ends.
- Routine B ends with
value
equal tofoo
.
It creates a fixed buffer with size n
. n
number of puts are non-blocking and the buffer holds the values till they are consumed. The n + 1
put is blocking.
Example:
const ch = chan('MYCHANNEL', buffer.fixed(2));
go(function* A() {
yield put(ch, 'foo');
console.log('a');
yield put(ch, 'bar');
console.log('b');
yield put(ch, 'moo');
console.log('c');
});
go(function* B() {
yield sleep(2000);
console.log(yield take(ch));
console.log(yield take(ch));
console.log(yield take(ch));
});
- Routine A starts and we see
"a"
and"b"
in the console. It's because we have a buffer with size 2. - Routine B starts and stops at
yield sleep(2000)
for two seconds. - Routine B continues and consumes all the values from the channel including
"moo"
.
Similar to the fixed buffer except that the put
s are never blocking. If the buffer is full a new item is added at the end but one item is removed from the beginning.
Example:
const ch = chan('MYCHANNEL', buffer.sliding(2));
go(function * A() {
yield put(ch, 'foo');
yield put(ch, 'bar');
yield put(ch, 'moo');
yield put(ch, 'zoo');
});
go(function * B() {
yield sleep(2000);
console.log(yield take(ch));
console.log(yield take(ch));
});
- Routine A starts and all the puts happen with no stopping of the generator.
- Routine B starts and in the channel we have
["moo", "zoo"]
. - Routine B ends and in the console we see
"moo"
followed by"zoo"
.
Similar to the fixed buffer except that every put
outside of the buffer range is not blocking the routing. It resolves with true
if there is space in the buffer and false
if not. If there is no space for new values it's simply ignored.
Example:
const ch = chan('MYCHANNEL', buffer.dropping(2));
go(function * A() {
yield put(ch, 'foo');
yield put(ch, 'bar');
yield put(ch, 'moo');
yield put(ch, 'zoo');
});
go(function * B() {
yield sleep(2000);
console.log(yield take(ch));
console.log(yield take(ch));
});
- Routine A starts and all the puts happen with no stopping of the generator. It's just the last two do nothing.
- Routine B starts and in the channel we have
["foo", "bar"]
. - Routine B ends and in the console we see
"foo"
followed by"bar"
.
Alias to chan(<id>, buffer.fixed(n))
. The id
is auto-generated. See here.
Alias to chan(<id>, buffer.sliding(n))
. The id
is auto-generated. See here.
Alias to chan(<id>, buffer.dropping(n))
. The id
is auto-generated. See here.
go(routine, done, routineArgs)
Runs a routine.
routine
(Generator
, required) - a generator functiondone
(Function
, optional) - a callback function which is fired when the routine ends.routineArgs
(Array<Any>
, optional) - any optional arguments that come as arguments to our generator.
Example:
const ch = chan();
go(
function * A(greeting) {
const name = yield take(ch);
return `${ greeting }, ${ name }`;
},
(v) => {
console.log(v);
},
['Hey']
);
go(function * B() {
yield put(ch, 'Pablo')
});
go
runs a routineA
with one argument"Hey"
. The routine stops atyield take(ch)
.- Routine
B
starts and we put"Pablo"
to the channelch
. - Routine
A
is resumed and forms its result which isHey, Pablo
. - The routine done callback is called and we see the result in the console.
The go
function returns an object that has a stop
method. Once you call it the routine will be terminated.
const routine = go(function * () {
yield sleep(1000);
// This line will never be executed because
// the routine is terminated before it gets resumed.
console.log('Never called!');
});
routine.stop(); // <-- this terminates the routine
Another way to stop the routine is to yield stop()
. This is not the same as writing return
because return
will fire the done
callback of the routine. stop()
is about terminating it. In the following example we'll see only "done B"
.
go(function * A() {
yield stop();
console.log('Never called!');
}, () => console.log('done A'));
go(function * B() {
return;
console.log('Never called!');
}, () => console.log('done B'));
The routine is automatically terminated if it's part of a riew and the riew is unmounted.
const routine = function * () {
yield sleep(1000);
// This line will never be executed because
// the routine is terminated before it gets resumed.
console.log('Never called!');
};
const r = riew(() => {}, routine);
r.mount();
r.unmount(); // <-- this terminates the routine
Re-running a routine means terminating the current processes and running the generator again.
The go
function returns an object that has a rerun
method. Once you call it the routine will be restarted.
const routine = go(function * () {
console.log('Hello!');
yield sleep(1000);
console.log('Bye!');
});
routine.rerun();
We'll see "Hello" twice before seeing "Bye", because the routing will be restarted before it gets resumed from the sleep
.
Another way to restart the routine is to return
the go
function. For example:
go(function * () {
console.log('Hello!');
yield sleep(1000);
console.log('Bye!');
return go;
});
This routine will print "Hello!"
, will wait a second and will print "Bye!"
. And will do that in a endless loop because the generator is restarted every time.
- Promise - riew will wait till the promise is resolved and will resume the generator.
const routine = go(function * () {
const res = yield fetch('https://aws.random.cat/meow');
const { file } = yield res.json();
console.log(file);
});
You may want to pass something to your routines which is defined far a way from the place where the routine starts. In such cases you can use the go.with
method:
// somewhere in your code's entry point
register('config', { theme: 'dark' });
// somewhere deep
const A = function*({ config, foo }) {
console.log(config.theme, foo);
};
go.with('config', { foo: 'bar' })(A); // "dark", "bar"
put(channel, anything)
Meant to be used only inside a routine. It puts items into a channel. Could be blocking. It depends on the channel's buffer.
channel
(One or array ofString
s or a channel object, required) - the channel which we want to put items in.anything
(Any
, required) - the item that we want to put into the channel. If we pass an array of channels as first argument then this should be also an array.
The generator is resumed with true
if the put
is successful. false
in the case of a channel with a dropping buffer. Or it may return channel statuses CLOSED
or ENDED
.
Example:
const ch = chan();
go(function * () {
console.log(yield take(ch));
})
go(function * () {
yield put(ch, 'foo');
});
Putting to multiple channels:
const channelA = chan();
const channelB = chan();
go(function*() {
console.log(yield take(channelA)); // "foo"
console.log(yield take(channelB)); // "foo"
});
go(function*() {
yield put([channelA, channelB], 'foo');
});
Have in mind that the put
in this case is released only when we take from both channels.
sput(channel, anything, callback)
Same as put but it can be used outside of a routine. This function is super handy to make the bridge between Riew and non-Riew code. (The s
comes from standalone
.)
channel
(String
or a channel object, required) - the channel which we want to put items in.anything
(Any
, required) - the item that we want to put into the channel.callback
(Function
, optional) - it will be fired when theput
operation ends.
The callback may be fired with true
if the put
is successful. false
in the case of a channel with a dropping buffer. Or with channel statuses CLOSED
or ENDED
.
Example:
const ch = chan();
go(function * () {
console.log(yield take(ch));
});
sput(ch, 'foo', res => console.log(`Put successful ${ res }`));
take(channels, options)
Meant to be used only inside a routine. It takes an item from a channel. Could be blocking. It depends on the channel's buffer.
channels
(One or array ofString
s or channel objects, required) - the channel which we want to take items from.options
(Object
, optional) - additional options for the take operationstrategy
(ALL_REQUIRED
orONE_OFF
,ALL_REQUIRED
by default) - if we take from multiple channels this option describes our strategy. Either we wait for all the channels to have values (ALL_REQUIRED
) or we expect at least one of them to be fulfilled (ONE_OF
).
The generator is resumed with the item taken from the channel.
Example:
const ch = chan();
go(function * () {
console.log(yield take(ch));
})
go(function * () {
yield put(ch, 'foo');
});
In case of taking from multiple channels we get an array as a result. If the strategy is ALL_REQUIRED
(by default) we'll get an array of ordered channel values. For example:
const ch1 = chan();
const ch2 = chan();
go(function*() {
console.log(yield take([ch2, ch1])); // ['bar', 'foo']
});
sput(ch1, 'foo');
sput(ch2, 'bar');
Notice that we are getting ["bar", "foo"]
and not ["foo", "bar"]
.
If we use ONE_OF
strategy the second element of the array is the index of the matched channel:
const ch1 = chan();
const ch2 = chan();
go(function*() {
console.log(yield take([ch2, ch1], { strategy: ONE_OF })); // ['foo', 1]
});
sput(ch1, 'foo');
The second element of the array is 1
, not 0, because ch1
is at the second position in the array.
stake(channels, callback, options)
Same as take but it can be called outside of a routine. This function is super handy to make the bridge between Riew and non-Riew code. The s
comes from standalone
.
channels
(One or array ofString
s or channel objects, required) - the channel which we want to take items from.callback
(Function
, optional) - it will be fired with the item taken from the channel.options
(Object
, optional) - additional options for the take operationstrategy
(ALL_REQUIRED
orONE_OFF
,ALL_REQUIRED
by default) - if we take from multiple channels this option describes our strategy. Either we wait for all the channels to have values (ALL_REQUIRED
) or we expect at least one of them to be fulfilled (ONE_OF
).
The callback is fired with the item put to the channel. If we take from more then one channel we'll receive an array of values. If we use ONE_OF
strategy the second argument of the callback is the index of the channel received a value.
Example:
const ch = chan();
go(function * () {
yield put(ch, 'foo');
yield put(ch, 'bar');
});
stake(ch, item => console.log(item));
Notice that this is one-time call. It's not like a subscription to a channel. In the example here we'll see only "foo"
but not "bar"
.
close(channel)
This function closes a channel and it is meant to be used inside a generator. Which means that the channel's state is set to CLOSE
or maybe ENDED
. It depends on the buffer strategy.
channel
(String
or a channel object, required) - the channel that we want to close.
Example:
const ch = chan();
go(function * () {
console.log(yield take(ch));
console.log(yield take(ch));
console.log(yield take(ch));
});
go(function * () {
yield put(ch, 'foo');
yield close(ch);
});
In the console we'll see "foo"
followed by two Symbol(ENDED)
. The routine is paused at the first take. put
resumes it with the value of "foo"
and the routine gets paused at the second take. The close
call closes the channel and releases all the pending takes. Each of the next takes will result with either CLOSE
ro ENDED
depending of the value of the channel's buffer. Every put to a CLOSED
or ENDED
channel is resolved with a channel status immediately.
sclose(channel)
This function is the same as close but it's meant to be used outside of a routine.
channel
(String
or a channel object, required) - the channel that we want to close.
Example:
const ch = chan();
go(function * () {
console.log(yield take(ch));
console.log(yield take(ch));
console.log(yield take(ch));
});
sput(ch, 'foo');
sclose(ch);
In the console we'll see "foo"
followed by two Symbol(ENDED)
. The routine is paused at the first take. sput
resumes it with the value of "foo"
and the routine gets paused at the second take. The close
call closes the channel and releases all the pending takes. Each of the next takes will result with either CLOSE
ro ENDED
depending of the value of the channel's buffer. Every put to a CLOSED
or ENDED
channel is resolved with a channel status immediately.
read(sourceChannels, options)
It's meant to be used only inside a routine and it reads from a channel/s. yield read(channel)
is not blocking the put on the other side of the channel and it's not consuming the value. It is always resolved no mather if the channel has value or not.
sourceChannels
(array of channels or a single channel, required) - the source channel which we will be subscribed to. In case of multiple channels the default behavior is that the subscriber is notified only when all the source channels have values. Check out thestrategy
option below.options
(Object
, optional) - additional options for the readingstrategy
(ONE_OF
orALL_REQUIRED
, default toALL_REQUIRED
) - base on that the library decided wether to wait for all the source channels or not. If set toONE_OF
it will resolve the read as soon as some of the sources receives a value.
Example:
const ch = chan();
go(function * A() {
console.log(yield read(ch)); // undefined
console.log('done'); // done
});
go(function * B() {
yield put(ch, 'foo'); // <-- never resolved
yield put(ch, 'bar'); // <-- never called
});
If we run this code we'll see "undefined"
followed by "done"
. The second put in routine B
never happens because the first put is blocked. We need a take to release it. The read
in routine A
doesn't consume the value of the channel, just reads it so can't unblock B
. And because routine A
starts first we still have nothing (undefined
) in the channel.
sread(sourceChannels, to, options)
Same as read but it's meant to be used outside of a routine.
sourceChannels
(array of channels or a single channel, required) - the source channel which we will be subscribed to. In case of multiple channels the default behavior is that the subscriber is notified only when all the source channels have values. Check out thestrategy
option below.to
(Channel
, or a function, required) - the values from thesourceChannels
are piped to the passed channel. If a function, that function is called with the values.options
(Object
, optional) - additional options for the readingstrategy
(ONE_OF
orALL_REQUIRED
, default toALL_REQUIRED
) - base on that the library decided wether to wait for all the source channels or not. If set toONE_OF
it will resolve the read as soon as some of the sources receives a value.
Example:
const ch = sliding();
sput(ch, 'foo');
sread(ch, value => {
console.log(value); // foo
});
stake(ch, value => {
console.log(value);
});
stake
is successfully resolved because sread
is not consuming the value of the ch
channel.
listen(sourceChannels, to, options)
Meant to be used outside of a routine. It subscribes for sourceChannels
and runs the to
function (or pushes to to
channel) every time when there is an input.
sourceChannels
(array of channels or a single channel, required) - the source channel which we will be subscribed to. In case of multiple channels the default behavior is that the subscriber is notified only when all the source channels have values. Check out thestrategy
option below.to
(Channel
, or a function, required) - the values from thesourceChannels
are piped to the passed channel. If a function, that function is called with the values.options
(Object
, optional) - additional options for the readingstrategy
(ONE_OF
orALL_REQUIRED
, default toALL_REQUIRED
) - base on that the library decided wether to wait for all the source channels or not. If set toONE_OF
it will resolve the read as soon as some of the sources receives a value.initialCall
(Boolean
, optional,false
by default) - by default the subscription doesn't apply until there is a new value to thesourceChannels
. If we set this totrue
we will get one initial call.
The most basic example is when we want to get values pushed to a channel into a regular JavaScript function.
const ch = chan();
listen(ch, value => {
console.log(value);
});
sput(ch, 'foo');
sput(ch, 'bar');
The result is foo
followed by bar
.
We can listen to more then one channel. By default listen
waits by default to all of the channels to be filled with values. Then it calls the callback.
const ch1 = chan();
const ch2 = chan();
listen([ch1, ch2], function subscriber(value) {
console.log(value);
});
sput(ch1, 'foo');
sput(ch2, 'bar');
sput(ch2, 'moo');
sput(ch1, 'zoo');
The first sput
doesn't trigger the subscriber
. After the second one we get ["foo", "bar"]
as a value. Then we get ["foo", "moo"]
and at the end ["zoo", "moo"]
. The third and the fourth sput
trigger the subscriber because then both channels have received values. Also notice that if we have more then one channel the value in the subscriber comes as an array.
Another interesting use case for listen
is when we use a ONE_OF
strategy and receive the input from each of the source channels:
const chA = chan();
const chB = chan();
listen([chA, chB], (v, idx) => {
console.log(v, idx);
}, { strategy: ONE_OF });
sput(chA, 'foo');
sput(chB, 'bar');
sput(chA, 'moo');
The result of this script will be "foo 0"
followed by "bar 1"
followed by "moo 0"
. It's because the library is not waiting for all the channels to have values but triggers the callback immediately when it receives a value.
If we have used listen
to subscribe to channel values we may want to unsubscribe. In this case we may use the function returned by listen
. For example:
Example:
const source = chan();
const subscriber = chan();
const unsubscribe = listen(source, subscriber);
go(function * () {
console.log(yield take(subscriber));
unsubscribe();
console.log(yield take(subscriber));
});
sput(source, 'foo');
sput(source, 'bar');
We see only "foo"
because after the first take
we remove the subscription. Then there is no one to put
into the subscriber
channel.
unsubAll(sourceChannel)
Removes all the subscriptions made to the sourceChannel
. If we use listen we effectively create a subscription. We may want to stop it.
sourceChannel
(Channel
, required) - the source channel that we subscribed to.
Example:
const source = chan();
const subscriber = chan();
listen(source, subscriber);
go(function * () {
console.log(yield take(subscriber));
unsubAll(source);
console.log(yield take(subscriber));
});
sput(source, 'foo');
sput(source, 'bar');
We see only "foo"
because after the first take
we remove all the subscription.
call(routine, ...routineArgs)
It runs another routine and it's meant to be used only inside a routine.
routine
(Generator
, required) - a generator functionroutineArgs
(Any
, optional) - any optional arguments that come as arguments to our generator.
Example:
function * A(name) {
return `Hey, ${ name }!`;
}
go(function * B() {
console.log(yield call(A, 'Ana')); // Hey, Ana!
});
Notice that the routine B
is paused until routine A
finishes.
fork(routine, ...routineArgs)
Like call but it's not pausing the main routine. Meant to be used only inside a routine.
routine
(Generator
, required) - a generator functionroutineArgs
(Any
, optional) - any optional arguments that come as arguments to our generator.
Example:
const ch = chan();
function * fillName(name) {
yield sleep(1000);
yield put(ch, name);
}
go(function * printName() {
yield fork(fillName, 'Ana');
console.log('Hey,'); // <- we see "Hey," immediately because `fork` is not-blocking operation
console.log(`How are you ${ yield take(ch) }!`); // How are you Ana!
});
Notice that the routine printName
is not paused when we yield fork
.
sleep(interval)
It's meant to be used only inside a routine. It pauses the routine for a given time.
interval
(Number
, required) - milliseconds
Example:
go(function * () {
console.log('A');
yield sleep(2000); // <-- two seconds delay
console.log('A');
});
stop()
It's meant to be used only inside a routine. It terminates the routine.
Example:
const answer = 42;
go(function * () {
console.log('A');
if (answer === 42) {
yield stop();
}
console.log('Never called!');
});
timeout(interval)
It creates a channel that will close after a given interval of time.
interval
(Number
, required) - milliseconds
Example:
const ch = timeout(1000);
go(function * () {
yield put(ch, 'foo');
yield put(ch, 'bar');
yield put(ch, 'moo');
})
go(function * () {
console.log(yield take(ch));
yield sleep(2000);
console.log(yield take(ch));
console.log(yield take(ch));
});
We'll see "foo"
and then two seconds later two Symbol(ENDED)
. That's because the channel is closed after a second and the two takes after the sleep
are operating with an ENDED
channel.
merge(...sourceChannels)
Returns a channel that contains the values from all the source channels. Have in mind that internally this function registers a taker.
sourceChannels
(Channels
, required) - one or many channels
Example:
const chA = chan();
const chB = chan();
const ch = merge(chA, chB);
go(function * () {
console.log(yield take(ch)); // foo
console.log(yield take(ch)); // bar
console.log(yield take(ch)); // moo
console.log(yield take(ch)); // zoo
});
go(function * () {
yield put(chA, 'foo');
yield put(chB, 'bar');
yield put(chB, 'moo');
yield put(chA, 'zoo');
});
The behavior of this function could be replicated by using listen. The difference between these two variants is that merge
consumes values from channels while listen
is just reading from them.
state(initialValue)
As we mentioned in the Application state section, the CSP pattern doesn't really offer state capabilities. The channels can keep value over time, but once someone takes it the channel is empty and the next taker will receive another value. Riew builds on top of the CSP idea by saying that the state lives outside of the pattern and we use channels to get and set the value.
initialValue
(Any
, optional) - if not specified the initial value isundefined
.
Returns a state object with the following fields:
DEFAULT
- a sliding channel. When we read from it, we will receive the value of the state. When we put to it will update the state value.select(selector, onError)
- creates a sliding channel for reading.selector
is a function or a routine that receives the current value and must return another value.onError
is here to handle errors coming from theselector
function.mutate(reducer, onError)
- creates a sliding channel for writing.reducer
is a function or a routine that receives the current state value and an item that is put to the channel. It must return a new version of the state.onError
is here to handle errors produced by thereducer
.destroy
- closes all the created channelsset(value)
- a synchronous way to set the value of the state. It puts the new value to all sliding channels created from the state.get()
- a synchronous way to get the value of the state without involving channels.
Example:
const counter = state(0);
go(function * () {
console.log(yield take(counter.READ)) // 0
yield put(counter.WRITE, 42);
console.log(yield take(counter.READ)) // 42
});
0
in this example is the initial value of the state. Then we have two built-in channels - counter.READ
and counter.WRITE
. The putting and taking from this channel usually happens synchronously. If we are about to use those channels we may skip writing .READ
and .WRITE
and simply use the state object directly. Like so:
const counter = state(0);
go(function*() {
console.log(yield read(counter.DEFAULT)); // 0
yield put(counter.DEFAULT, 42);
console.log(yield read(counter.DEFAULT)); // 42
});
Notice that we are using read
and not take
to get the value out of the default channel. If we use take
the value will be consumed and every next take
will be blocking. This though doesn't mean that the value disappears. It is still in the counter
object and we can get it via counter.get()
. Also notice that all the channels created out of a state are using a sliding buffer. This is to have non-blocking puts.
state.select(selector, onError)
The read channels are called selectors and are defined by using the select
method of the state.
const name = state('Samantha');
const up = name.select(str => str.toUpperCase());
const lower = name.select(str => str.toLowerCase());
const message = name.select(str => `My name is ${str}`);
go(function*() {
console.log(yield read(up)); // SAMANTHA
console.log(yield read(lower)); // samantha
console.log(yield read(message)); // My name is Samantha
});
state.mutate(reducer, onError)
The write channels are called mutators and are defined by using the mutate
method of the state.
const counter = state(0);
const add = counter.mutate((current, n) => current + n);
const double = counter.mutate(current => current * 2);
go(function*() {
yield put(add, 10);
yield put(double);
console.log(yield read(counter)); // 20
});
Have in mind that the reducer
can be a routine (generator function) and as such may be asynchronous.
riew(view, ...routines)
Creates a riew based on the given view
function and routines
generator functions.
view
(Function
, required) - a view function that will receive the data.routines
(one or many routines, optional) - routines that will start when the riew is mounted and terminate when the riew is unmounted.
Returns an object with the following methods:
mount(props)
-props
is an object that will reach our view.unmount()
- unmounts the riew. Performs clean up operations.update(props)
-props
is an object that will reach our view.with(...externals)
- a list of externals to be injected into the view and the routines. Check the Externals section for more details.
Example
function view(props) {
console.log(props);
}
function * A({ render }) {
render({ foo: 'bar' });
}
function * B({ render }) {
render({ moo: 'zoo' });
}
const r = riew(view, A, B);
r.mount();
The view
functions gets called once with { foo: "bar", moo: "zoo" }
.
The Riew routines that are passed to the riew
function receive an object with the following properties:
render
- must be called with an object. That object reaches theview
. Have in mind that multiplerender
calls are batched into a singleview
call via micro task. Thisrender
function is the main tool for updating the view.state
- alias to state. All the states created with this alias will be destroyed when the riew is unmounted. It's recommended if you have local for the riew state to created it via this function so it gets cleaned up when the riew is unmounted.fixed
,sliding
,dropping
- alias to channel methods. All the channels created with this aliases will be destroyed when the riew is unmounted. It's recommended if you have local for the riew channel to created it via this functions so it gets cleaned up when the riew is unmounted.props
- a props channel of the riew. It gives you access to the object that is passed tomount
andupdated
methods.- the Riew routines also receive the externals injected into the riew (if any)
We have to clarify what we mean by "object that reaches the view
". It's a JavaScript object literal that contains keys and values. This may be raw data but may be also a Riew channel or a state. In this cases the view gets whatever is the data in the channel or whatever is the value of the state. Keep reading, there're more examples in the Channels and state section.
Very often we'll need services and configuration to reach our riews. The library provides a mechanism for dealing with such dependencies. We'll better understand it by checking the following example:
const answerService = {
getAnswer() {
return new Promise(done => setTimeout(() => done(42), 1000));
}
}
const ch = chan();
const view = function (data) {
console.log(data);
}
const routine = function * ({ answer, service }) {
const number = yield service.getAnswer();
yield put(answer, number);
}
const r = riew(view, routine).with({
answer: ch,
service: answerService
});
r.mount();
answerService
is a silly example of a service with a single method that does some async job. We then have a channel and our view
followed by our routine
. Notice the usage of the with
method when defining the riew. We are saying that we want to initiate our riew with two props - answer
and service
. The view
function gets called twice with the following props:
{ service: {...} }
{ service: {...}, answer: 42 }
The answerService
reaches the view
and the routine as it is because Riew have no idea what it is. However the answer
is treated differently. The library sees that this is a channel and subscribes the view
to items coming from that channel. The routine receives the channel as it is and we can put
to it. At the end of the snippet the riew is mounted, the routine starts, we get the async call happening and we push the number to the channel.
The with
method is one of the ways to subscribe our views to channels. Every channel has an exportAs
method which saves a reference to the channel in a global registry. We may then use the with
method to access that same channel:
// At the top level of your application.
const ch = chan().exportAs('my-channel');
// Somewhere in the UI layer.
const view = function(data) {
console.log(data);
};
const r = riew(view).with('my-channel');
r.mount();
// At some point we put data to the channel.
sput(ch, 'Simon');
The result of this snippet is { name: "Simon" }
in the console. It works like that because our riew gets subscribed for new values coming to the channel.
The with
helper could be also used to inject stuff that are pushed to a global registry directly.
register('config', { theme: 'dark' });
const r = riew(function (props) {
console.log(`Selected theme: ${ props.config.theme }`); // Selected theme: dark
}).with('config');
r.mount();
Same as in exportAs
instead of an object we just have to pass the key of the dependency.
The channels and state are integral part of the riew concept. We use them to push data to the view, synchronize processes and communicate events. Our riews internally check what is behind the data before passing it to the view.
- If we have a channel the riew creates a subscription using listen and re-renders the view with the value send to the channel.
- If we have a state the riew again creates a subscription but to the default read channel of the state.
const view = function(data) {
console.log(data);
};
const routine = function*({ render }) {
const counter = state(0);
const inc = counter.mutate(n => n + 1);
const sel = counter.select(n => `value is ${n}`);
setInterval(() => sput(inc, 1), 1000);
render({ counter, message: sel });
};
const r = riew(view, routine);
r.mount();
In this example we are creating a state that keeps a number. We also have two channels one of which is created by the state's mutate
method - inc
. What we send to the view is the state counter
and a message (check the state section to see what select
and mutate
mean in details). The result of this code is that our view
is called every second with {counter: <number>, value: "value is <number>"}
.
Our riews are bound to channels and we control what the view renders by putting data to those channels.
react.riew(reactComponent, ...routines)
Same as riew but for the view instead of a regular function takes a React component.
reactComponent
(React component
, required) - React component (class or a function)routines
(one or many routines, optional) - routines that will start when the riew is mounted and terminates when the riew is unmounted.
react.riew
returns another React component.
Example:
function Greeting(props) {
return <p>Hey, { props.name }.</p>
}
function * routine({ render }) {
render({ name: 'Ana' });
}
const Component = react.riew(Greeting, routine);
ReactDOM.render(<Component />, document.querySelector('#output'));
The Component
constant here is indeed a React component but it is also a riew. So it has a with method. The Greeting
component is the same as the view
function in all the other examples above.
register(key, dependency)
Together with use and with this function implements a mechanism for dealing with dependencies. register
defines an entry in a globally available registry and use is accessing that entry. Once we define something with register
we may inject it into our riews with the with method.
key
(String
, required) - a string the uniquely identifies the dependencydependency
(Any
, required) - this could be a primitive like string or a number but could be also instance of a service or a state object.
Example:
register('config', { theme: 'dark' });
const r = riew(function (props) {
console.log(`Selected theme: ${ props.config.theme }`); // Selected theme: dark
}).with('config');
r.mount();
use(key)
It gives you access to the dependency registered using the register function. This function is useful if we want to use something in our routines.
key
(String
, required) - the key identifier of the dependency.
register('config', { theme: 'dark' });
const view = function (props) {
console.log(`Selected theme: ${ props.theme }`); // Selected theme: dark
};
const routine = function * ({ render }) {
const config = use('config');
render({ theme: config.theme });
}
const r = riew(view, routine).with('config');
r.mount();
reset()
It resets all the internal structures of the library. This function is useful while writing unit tests for your riews or routines. Riew for example keeps track of all the channels that we are creating. And in the unit tests we don't want to have conflicts of channel IDs. So we call the reset
function before each test.
We have three constants which we can use together with the state method of the channel to identify what's the current state.
OPEN
- channel is openCLOSED
- channel is closed but we it still has items in it.ENDED
- channel is closed and empty
The two options for subscription strategies:
ALL_REQUIRED
- notify the subscriber only if all the source channels have value.ONE_OF
- notify the subscriber if at least one of the source channels have value.