Luna
Luna-Saga, Write Business Logic As Generators for Reactive Redux StoreLuna-saga is a saga runner built for Luna, a reactive redux implementation based on the Reactive-Extension (Rxjs@v5.0-beta) and build with Typescript. It enhances Rxjs's existing handling operators for Es6 generators by adding support for store Actions
and a collection of flow management helpers.
In English: luna-saga allows you to write all of your business logic synchronously using generators. It is awesome, easier than reactive-thunks, and cleaner than stateful thunk.
get started: run npm install luna-saga --save
after you install luna
with npm install luna --save
Throttling Example
Here is a simple throttle helper that is implemented using saga, and it's test:
first the throttle function:
/** throttle process: Takes in a task function, a trigger object <RegExp, string, TSym>, input interval, and flag for triggering on falling edge. */
To use this throttle function:
;; ;p.run
For Angular2 Developers Out There~
luna-saga
is written in TypeScript with Angular2 in mind. You can directly import it to your ng2 project. To use angular's dependency injection, you can write a simple provider. Personally I prefer to keep things uncoupled from the framework as much as possible, this is why luna-saga
is written with Angular2 in mind but does not depend on it.
For React Developers~
TLDR; I work with react everyday using luna
and luna-saga
(when I'm not training deep reinforcement learning agents and writing papers.)
After spending two month with Angular 2, I decided to work with React. The functional pattern is much easier to work with compared with the DOM-parser pattern of Angular 2. And the ability to use ES6 with flow-type instead of Typescript made things a lot more portable. In particular it is easier to debug code with node.js, since the code does not need to be transpiled with the -r babel-register
flag. Even es6 module is supported out of the box.
The ability to debug without have to read call stack in a transpiled code base is a must for good developer experience.
Wanna use ES6 Generators to build Sagas in your project, but also want to use rxjs?
Luna-saga is the reactive operator for Luna.
The most innovative part of redux-saga
is the coherent use of es6 generator patterns for thread management, and a powerful suite of flow management utility helper functions. To a certain extent, if you are using Luna with rxjs, you can already use the Rxjs.spawn
(not available in Rxjs@5.0-beta, see here) or Rxjs.from
operator to interface with your saga (generators). Below is an example:
Note that from
returns all of the yielded results but does not handle Thunk and Promises well.
Meanwhile spawn
returns the returned value at the end but it handles yield promise and thunk expressions properly.
; const thunk = { return { ; };}; const spawned = Observable; spawned; // => next 212// => completed
references:
Luna-Saga is an enhanced Reactive Process Manager for Generators (to 1st order)
You can do:
; ;;saga.log$.subscribeconsole.log"log: ", _, null,;saga.subscribe;saga.action$.subscribeconsole.log"action: ", _;saga.thunk$.subscribeconsole.log"thunk: ", _;saga.run; // 'log: ', 0// 'log: ', undefined// 'log: ', Object{type: 'INC'}// 'action: ', Object{type: 'INC'}// 'log: ', Object{type: 'INC', __isNotAction: true}// 'log: ', () => { ... }// 'thunk: ', () => { ... }// 'log: ', Promise{}// 'log: ', 0// 'log: ', 1// 'log: ', 2// 'log: ', 3// 'log: ', 'returned value is logged but not evaluated.'// 'saga execution took 0.005 seconds'
So this is awesome, what's next?
The next thing to do is to build a set of process helpers, side-effects and flow-control helpers. I'm implementing this following redux-saga
fantastic API documentation here.
Installing Luna-Saga
You can install from npm: npm install luna-saga
, or from github
npm install luna-saga@git+https://git@github.com/escherpad/luna-saga.git
saga$
to your luna store$
, just do:
To hook up // processGenerator is your generator// Don't forget to `run` it!let saga = ; // store$ is your luna rxjs storestore$update$ sagaaction$;sagathunk$;
The Instance
Calling new Saga(processGenerator())
returns a saga
instance. Luna saga
instance inherits from Rxjs.Subject
.
The saga.log$
is a (Publish) Subject for all of the yielded expressions from the generator.
~~The saga.error$
is a (Publish) Subject for errors. The reason why we do not use rxjs
observer's built-in error handling is because there is not a unified hook for handling both error out and completion in a single call-back. As a result, we never error out saga.log$
, and complete the process when an error is propagated through saga.error$
. ~~
We are using the standard rxjs
Subject.subscribe({error: CallBack})
for subscribing to errors. A single uncought error leads the Saga to stop running.
- if yielded expression is a simple object, the object is logged.
- if yielded expression is an action, the object is logged and pushed to
.action$
. - if yielded expression is an action but with
__isNotAction
flag, it is logged but not pushed to.action$
- if yielded expression is a Promise, it is logged
- The return value of the generator is logged but not evaluated.
The saga.update$
is a ReplaySubject with a buffer size of 1. This means that you can always get the current value of the store$
state by quering saga.update$.getValue()
. Subscribing to saga.update$
results in a {state, action}
data on subscription.
The saga.action$
is a (Publish) Subject for out-going actions that you are gonna dispatch back to the store. It does not have the getValue()
method.
The saga.thunk$
is a (Publish) Subject for out-going thunks that you are gonna dispatch back to the store. It also does not have the getValue()
method.
Errors
To log Saga internal errors, subscribe to
Todo:
-
spawn
effect, the main difference is that error in this type of fork does not terminate parent process. - Waht is the problem? The problem with handling errors through
rxjs
is that the default error handling convention kind os sucks. So we now use a dedicatederror$
stream for errors. This way we don't have to jump through the hoops to avoid random subscription to <Saga> erroring out unexpectedly. - properly handle process completion.
- figure out how to terminate a process from the outside.
- need to return processId and implement
cancel
effect. (How to allow cancel across generators? (b/c forks are children of parent process.)) - When parent process is cancelled, the complete event propagates to child processes (by id).
-
takeEvery
helper -
takeLast
helper - ... many other effects
-
race
flow controller -
parallel
flow controller
Done
- generator spawning and test =>
new Saga(procGenerator())
- ==NEW!== double yield syntax for standard "error-first" callback function.
-
take
effect -
dispatch
(replace nameput
) effect -
call
effect -
apply
effect (call
alias) -
select
effect -
delay
helper function - finish on
fork
, becausecall
does not allow async start of process - refactor call (and apply code). Difference is that call (and apply) do not return
processId
. because they finish right away synchronously. On the other hand,fork
returns a processId that could be used to cancel the task. - All new processes are attached to a parent's
childProcesses
. - properly handle child process completion. Use
finally
operator to catch both error and completion, remove child process from parent without double complete the process.
Saga Helpers, Effects, and Flow helpers (Updated Below!)
These special functions are implemented following the API documentasion of redux-saga
. You can look at the details here. Below is the documentation for luna-saga
.
callback
, take
, put
, call
, apply
, cps
, fork
, join
, cancel
, select
Saga Effects: Effect is short for side-effect. luna-saga
's effect collection allows you to write things synchronously while keeping the generators pure. These side-effect functions merely produce a side-effect object, which is then processed by saga
internally to do the correct thing.
Below is the test for luna-saga
. You can look at what is happening here to understand how they work.
NEW!! NodeJs "Error-first" Callback Function with Yield-Yield syntax!
Have you ever wanted to use the node.js
kind of "error first" callback syntax to write your code? With luna-saga
, you can. Below is what happens when you run a simple generator in node and pass yield "something"
in place of the actual callback function:
let { ; return "==> asyncFn return <==";}; { result = ; // this is where you ask for a callback function. ;} // now let's run this!!!let it = ; let result = itnext; // yield the first yield inside the async function; let callbackResult;result = itnext{callbackResult = res;};// now pass in the callback functionresult = itnextcallbackResult; // now yield the second yield, and this is where we return the callback result;
To use this syntax, just import callback
token from luna-saga
. It will generate a @@luna-saga/CALLBACK
type object, and get intercepted by the luna-saga
effect executer.
; { try user = mongo; catch error console;
isn't this fantastic?
take(ACTION_TYPE) yield {state, action}
say in your generator you want to pause the process untill the process recieves a certain action. You can write:
let update = ;// update = {state: <your current state>, action: <the action for that state>}
dispatch(action) yield {state, action}
(put
in redux-saga)
use this to dispatch an action. This function does not support thunk at the moment.
let update = ;// update = {state: <your state>, action: <action>}
call(fn[, args]) yield <return from fn>
, call([context, fn][, args])
and apply(context, fn[, args])
I didn't really see a huge need for these but I implemented them anyways. You can yield functions directly without using side effect. However these are useful when you want to run object methods.
update: now the delay function is implemented with this call effect!
Example usage for delay:
; { ; // <= this lets the saga wait for 500 milliseconds)}
select([selector:string]) return <selected part of state>
returns a selected part of the store state. If selector is undefined, select returns the entire store state.
// simple examplelet data = // <entire store>let data = // 1
fork(generator, [args, [context]])
;saga.log$.subscribenull, console.error;saga.run;
Important Note: the store$.update$
stream is a Publish Subject instead of a Behavior Subject.
This means that the subscriber does not receive current state on subscription.
this is implemented on purpose to prevent the saga from always getting a stale {state, action}
bundle on connection.
To solve this problem, we implemented in the sagaConnect
helper function, to synthetically emit
a action with action type: SAGA_CONNECT_ACTION
. This action is not passed to the store, but can
be used by the SAGA process to recognize the bundle as a connection event.
In addition, it allows effects such as select
and dispatch
to obtain an update-to-date copy
of the store state upon connection. Without this connection update, the update$.take(1)
stream
would not be populated, and the Saga would not run.
var store$ = reducer number: 0;{ let n = ; console; var state = ; console; state = ; console;}var saga = ; // this would not run:store$update$;saga; // generator hangs forever // this would:store$update$;saga; // generator hangs forever saganext state: store$ action: type: SAGA_CONNECT_ACTION;// number is: 0// connection event triggers all subsequent select effects:// 2rd select// 3rd select
Developing Luna-Saga
- you need to have
karma-cli
installed globally. (donpm install -g karma-cli
) - to build, run
npm run clean-build
. This callsclean
andbuild:tsc
, the latter callstsc
in project root. - to test, you can use
karma start
. I use webStorm's karma integration to run the tests.
Work In Progress Below This Line
takeLast
and takeEvery
Saga Helpers: These are the high-level helpers to spawn sagas processes from within a saga. takeEvery
and takeLast
are similar to the rxjs.takeEvery
and rxjs.takeLast
race(effects)
and parallel([...effects])
Flow helpers: Allows one to pick the result of a winner, or run a few effects in parallel.
Acknowledgement
This library is inspired by @yelouafi's work on redux-saga.
Luna-saga is part of my effort on re-writting escherpad, a beautiful real-time collaborative notebook supporting real-time LaTeX, collaborative Jupyter notebook, and a WYSIWYG rich-text editor.
About Ge
I'm a graduate student studying quantum information and quantum computer at University of Chicago. When I'm not tolling away in a cleanroom or working on experiments, I write (java|type)script
to relax. You can find my publications here: google scholar
LICENSING
MIT.