1 | l8 0.1.43
|
2 | =========
|
3 |
|
4 | [![Build Status](https://travis-ci.org/JeanHuguesRobert/l8.png)](https://travis-ci.org/JeanHuguesRobert/l8)
|
5 |
|
6 | l8 is a modern multi-tasker for javascript. It schedules javascript tasks using
|
7 | promises and distributed actors.
|
8 |
|
9 | This is a work in progress that is not ready for production.
|
10 | See [![Build Status](https://c9.io/site/wp-content/themes/cloud9/img/logo_cloud9_small.png)](https://c9.io/jhr/l8)
|
11 |
|
12 | A task is any activity that a "normal" non-blocking javascript function cannot
|
13 | do because... javascript functions cannot block! Where functions provide
|
14 | results, tasks provide promises instead. To become tasks that can block,
|
15 | functions are broken into steps that the l8 scheduler executes.
|
16 |
|
17 | ```
|
18 | l8.task ->
|
19 | @repeat ->
|
20 | round = random = 0
|
21 | @step -> input "Enter a decent number to start a new game"
|
22 | @step (r) ->
|
23 | @continue if (r = parseInt( r)) < 10
|
24 | random = Math.floor Math.random() * r
|
25 | round = 0
|
26 | @repeat ->
|
27 | @step -> input "Guess a number"
|
28 | @step (r) ->
|
29 | round++
|
30 | r = parseInt( r)
|
31 | if r > random then printnl "#{r} is too big"
|
32 | if r < random then printnl "#{r} is too small"
|
33 | if r is random
|
34 | cls()
|
35 | printnl "Win in #{round} rounds! Try again"
|
36 | @break
|
37 | # extracted from test/input.coffee
|
38 | ```
|
39 |
|
40 | What is it?
|
41 | ===========
|
42 |
|
43 | This is a library to help those who want to embrace the Promise/A style of
|
44 | asynchronous programming but feel that the classic thread/blocking-function
|
45 | model is also very readable.
|
46 |
|
47 | l8 schedules the execution of multiple "tasks". A task is made of "steps", much
|
48 | like a function is made of statements. Steps are walked on multiple "paths".
|
49 | Such tasks and paths (sub-tasks) can nest, like blocks of statements.
|
50 |
|
51 | Execution goes from "step" to "step", steps are closures. If one cannot walk a
|
52 | step immediately, one does block, waiting for something before resuming.
|
53 |
|
54 | l8 tasks are kind of user level non preemptive threads. They are neither
|
55 | native threads, nor worker threads, nor fibers nor the result of some CPS
|
56 | transformation. Just a bunch of cooperating closures. However, if you are
|
57 | familiar with threads, l8 tasks should seem natural to you.
|
58 |
|
59 | l8 tasks are also "promises". Once a task is completed, it's promise is either
|
60 | fullfilled or rejected depending on the task success or failure.
|
61 |
|
62 | The main flow control structures are the sequential execution of steps, the
|
63 | execution and join of forked steps on parallel paths, steps that loop until
|
64 | they exit, steps that wait for something, error propagation similar to
|
65 | exception handling and synchronisation using the usual suspects (semaphores,
|
66 | mutexes, reentrant locks, message queues, ports, signals, generators...).
|
67 |
|
68 | When compared to callbacks, steps add some overhead due to the fact that
|
69 | what to do next is computed (based on pre-queued steps) instead of being
|
70 | specified by the callback itself. The overhead is small (see test_11 in the
|
71 | test suite) considering the extra features provided (ie. nesting, cancelation,
|
72 | tasks hierarchie, etc). When that overhead is useless, one can revert to the
|
73 | classic callback style, ie. blocking and callback modes intermix well.
|
74 |
|
75 | Steps vs Statements
|
76 | ===================
|
77 |
|
78 | Javascript code is made of statements (and expressions). One key characteristic
|
79 | of the language is the fact that all these statements are "non blocking". This
|
80 | means that a statement cannot "block". It is executed with no delay, it cannot
|
81 | "wait" for something to happen.
|
82 |
|
83 | As a result there is only one "thread" of execution and any activity that
|
84 | cannot complete immediately needs to register code to execute later when
|
85 | some "event" occurs. This single thread runs a tight loop that consumes events
|
86 | and run code registered to handle them. This is "the event loop".
|
87 |
|
88 | ```
|
89 | while( true ){
|
90 | event = get_next_event();
|
91 | dispatch( event);
|
92 | }
|
93 | ```
|
94 |
|
95 | Code that is executed when an event happens is often named "callback". This is
|
96 | because it is the "event loop" (though "dispatch()") that "calls back" that
|
97 | code.
|
98 |
|
99 | ```
|
100 | function process_mouse_over(){
|
101 | obj.onmouseover = call_me_back;
|
102 | function call_me_back(){
|
103 | // called when mouse runs over that obj
|
104 | }
|
105 | }
|
106 | ```
|
107 |
|
108 | That event_loop/callback style is simple and efficient. However, it has some
|
109 | notorious drawbacks. Things can get fairly complex to handle when some activity
|
110 | involves many sub-activities that must be run in some specific order.
|
111 |
|
112 | Multiple solutions exist to care with such cases. The most basic one is to
|
113 | start a new activity from within the callback that gets called when the
|
114 | previous activity is completed.
|
115 |
|
116 | ```
|
117 | ajax_get_user( name, function user_found( user ){
|
118 | ajax_check_credential( user, function credential_checked( is_ok ){
|
119 | if( is_ok ){
|
120 | ajax_do_action( user, "delete", function delete_result( err ){
|
121 | if( err ) signal( err)
|
122 | }
|
123 | }
|
124 | }
|
125 | }
|
126 | ```
|
127 | This code is not very readable because of the "nesting" of the different parts
|
128 | that obscures it.
|
129 |
|
130 | ```
|
131 | ajax_get_user( name, user_found);
|
132 | function user_found( user ){
|
133 | ajax_check_credential( user, credential_checked);
|
134 | }
|
135 | function credential_checked( is_ok ){
|
136 | if( !is_ok )return
|
137 | ajax_do_action( user, "delete", delete_result)
|
138 | }
|
139 | function delete_result( err ){
|
140 | if( err ) signal( err)
|
141 | }
|
142 | ```
|
143 | This slightly different style is barely more readable. What would be readable
|
144 | is something like this:
|
145 |
|
146 | ```
|
147 | var user = ajax_get_user( name);
|
148 | if( !ajax_check_credential( user) ) return;
|
149 | if( err = ajax_do_action( user, "delete") ) signal( err);
|
150 | ```
|
151 |
|
152 | However, this cannot exist in javascript because no function can "block". The
|
153 | function "ajax_get_user()" cannot "block" until it receives an answer.
|
154 |
|
155 | This is where l8 helps.
|
156 |
|
157 | Steps
|
158 | -----
|
159 |
|
160 | Steps are to Tasks what statements are to functions: a way to describe what
|
161 | they do.
|
162 |
|
163 | ```
|
164 | var user;
|
165 | l8
|
166 | .step( function( ){ ajax_get_user( name) })
|
167 | .step( function( result ){ ajax_check_credential( user = result) })
|
168 | .step( function( result ){ if( !result ) l8.return();
|
169 | ajax_do_action( user, "delete") })
|
170 | .step( function( result ){ if( result ) signal( result) })
|
171 | ```
|
172 |
|
173 | This is less verbose with CoffeeScript:
|
174 |
|
175 | ```
|
176 | user = null
|
177 | @step -> ajax_get_user name
|
178 | @step (r) -> ajax_check_credential (user = r)
|
179 | @step (r) -> if !r then @return() else
|
180 | ajax_do_action user, "delete"
|
181 | @step (r) -> if err = r then signal err
|
182 | ```
|
183 |
|
184 | By "breaking" a function into multiple "steps", code become almost as readable
|
185 | as it would be if statements in javascript could block, minus the "step" noise.
|
186 |
|
187 | This example is a fairly simple one. Execution goes from step to step in a
|
188 | sequential way. Sometimes the flow of control is much more sophisticated.
|
189 | There can be multiple "threads" of control, with actions initiated concurrently
|
190 | and various styles of collaboration between these actions.
|
191 |
|
192 | Please note that the ajax_xxx() functions of the example are not regular
|
193 | functions, they are "task constructors". When you invoke such a function, a new
|
194 | task is created. See below.
|
195 |
|
196 | If they were usual ajax_xxx( p1, p2, cb) style of functions, one would need to
|
197 | use .walk or .proceed() as the callback in order to ask l8 to move to the next
|
198 | step:
|
199 |
|
200 | ```
|
201 | var user = null, err
|
202 | this.step( function( ){ ajax_get_user( name, this.walk ) }
|
203 | this.step( function( r ){ ajax_check_credential( (user = r), this.walk ) }
|
204 | this.step( function( r ){ if( !r ) this->return()
|
205 | ajax_do_action( user, "delete", this.walk ) }
|
206 | this.step( function( r ){ if( err = r ) signal( err)
|
207 | ```
|
208 |
|
209 | Tasks
|
210 | -----
|
211 |
|
212 | About the notion of "task". A Task is a l8 object that consolidates the result
|
213 | of multiple threads of control (aka sub-tasks) that all participate in the
|
214 | completion of a task.
|
215 |
|
216 | Tasks are to steps what functions are to statements: a way to group them.
|
217 |
|
218 | To perform a task, the simplest way is to invoke a "task constructor". It will
|
219 | schedule the new task and return a Task object. Such an object is also a
|
220 | "Promise". This means that it is fairly easy to get notified of the task's
|
221 | completion, either it's success or it's failure.
|
222 |
|
223 | ```
|
224 | var new_task = do_something_task()
|
225 | new_task.then( on_success, on_failure)
|
226 | function on_success( result ){ ... }
|
227 | function on_failure( reason ){ ... }
|
228 | ```
|
229 |
|
230 | A "task constructor" is to a "task" what a "function" is to a "function call":
|
231 | both define (statically) what happens when they are invoked (dynamically).
|
232 |
|
233 | Tasks queue steps that the l8 scheduler will execute much like functions queue
|
234 | statements that the Javascript interpretor executes. With functions, statements
|
235 | queueing is implicit. With tasks, it becomes explicit. As a result, defining
|
236 | what a task does is of course sligthly less syntaxically easy.
|
237 |
|
238 | ```
|
239 | do_something_task = l8.Task( do_something_as_task );
|
240 | function do_something_as_task(){
|
241 | this
|
242 | .step( function(){ this.sleep( 1000) })
|
243 | .fork( function(){ do_some_other_task() })
|
244 | .fork( function(){ do_another_task() })
|
245 | .step( function(){ ... })
|
246 | }
|
247 | ```
|
248 |
|
249 | This is the "procedural" style. A "declarative" style is also available where
|
250 | what is usually a function can be a list of steps:
|
251 |
|
252 | ```
|
253 | do_something_task = l8.Task(
|
254 | function(){ this.sleep( 1000) },
|
255 | {fork: function(){ do_some_other_task() }},
|
256 | {fork: function(){ do_another_task() }},
|
257 | [
|
258 | {step: function(){...}},
|
259 | {failure: function(){...}}
|
260 | ],
|
261 | {repeat:[
|
262 | function(){ do_something },
|
263 | function(r){ if( !r ) this.break }
|
264 | {failure: function(){ ... }}
|
265 | ]}
|
266 | {success: function(){ ... }},
|
267 | {final: function(){ .... }}
|
268 | )
|
269 | ```
|
270 |
|
271 | There is also a trans-compiler option that takes a funny looking function and
|
272 | turns it into a task constructor. It's compact but you lose the ability to set
|
273 | break-points in a debugger.
|
274 |
|
275 | ```
|
276 | do_something_task = l8.compile( do_something_as_task );
|
277 | function do_something_as_task(){
|
278 | step; this.sleep( 1000);
|
279 | fork; do_some_other_task_xx();
|
280 | fork; another_task_xx();
|
281 | step( a, b ); use_xx( a); use_xx( b);
|
282 | begin
|
283 | ...
|
284 | step; ...
|
285 | failure; ...
|
286 | end
|
287 | repeat; begin
|
288 | ...
|
289 | step; act_xx()
|
290 | step( r ); if( !r ) this.break
|
291 | end
|
292 | success( r ); done_xx( r);
|
293 | failure( e ); problem_xx( e);
|
294 | final( r, e); always_xx();
|
295 | }
|
296 | ```
|
297 |
|
298 | Note that when do_something_task() is called, it does not do the actual work,
|
299 | it only registers future steps. These steps, and steps later added to the task,
|
300 | are executed later, in the appropriate order, until the task is fully done.
|
301 | It is then, and only then, that the on_success/on_failure callbacks of the
|
302 | task's promise will be called.
|
303 |
|
304 | In a function, statements are executed in a purely sequentiel order. That
|
305 | restriction does not apply with steps in a task. While the sequential order
|
306 | is still the "normal", steps that run in parallel paths can also exist. Such
|
307 | steps can be the result of "forks". When all forks are done, the forks "join"
|
308 | and execution continues with the next normal step. When using a generator, the
|
309 | steps of the producer and those of the consumer are executed alternatively when
|
310 | .yield() and .next() are called to handle a new generated results.
|
311 |
|
312 | Promises
|
313 | --------
|
314 |
|
315 | Promises are to tasks what result/exception are to a function: a way to provide
|
316 | information about the outcome.
|
317 |
|
318 | The "de facto" current standard for promises is part of the CommonJS effort:
|
319 | http://wiki.commonjs.org/wiki/Promises/A
|
320 |
|
321 | Such a "promise" is any object that provides a "then" method. That method does
|
322 | two things: it registers callbacks to call when the promise is either, later or
|
323 | now, fullfilled or rejected and it also returns a new promise that will be
|
324 | fullfilled or rejected depending on the result of these callbacks; this makes
|
325 | chaining easy.
|
326 |
|
327 | Please note that "promises" in the Javascript world is not a mature feature.
|
328 | The "de facto" CommonJS standard is challenged by another "de facto" strong
|
329 | strong contender: jQuery. Their implementation of then() differs significantly
|
330 | regarding chaining and exception handling. l8.wait() does not use these
|
331 | features and consequently l8.wait() should work with most implementations,
|
332 | including jQuery's one.
|
333 |
|
334 | One can invoke .then() multiple times on the same promise. When that promise is
|
335 | either fullfilled or rejected, all the registered callbacks are processed.
|
336 |
|
337 | Some features of l8 that involve promises require a promise factory. l8 can use
|
338 | the factories of Q.js, When.js, Angular.js, etc. The factory must return a new
|
339 | object that supports .resolve(), .reject() and a .promise() that returns an
|
340 | object that supports a Promise/A compliant .then().
|
341 |
|
342 | Generators
|
343 | ----------
|
344 |
|
345 | Generators are subtasks that provide a result in multiple pieces instead of in
|
346 | just one piece as regular tasks do. Such a task is a "producer" of results,
|
347 | some other task, often the one that spawn the generator, is the "consumer" of
|
348 | these results.
|
349 |
|
350 | Consumers usually consume the next result that some subtask yields until the
|
351 | generator reaches an end and is closed, either by the producer or the consumer.
|
352 |
|
353 | l8.Generator( block) builds a "Generator Constructor" much like l8.Task( block)
|
354 | does with "Task Constructor". When the constructor is invoked, a generator task
|
355 | is spawn. That task uses .yield() to produce results. On the consumer side, the
|
356 | task uses .next([opt]) to get that result and optionaly provide a hint about
|
357 | future results.
|
358 |
|
359 | ```
|
360 | var fibonacci = L4.Generator( function(){
|
361 | var i = 0, j = 1;
|
362 | this.repeat( function(){
|
363 | this.yield( i);
|
364 | var tmp = i;
|
365 | i = j;
|
366 | j += tmp;
|
367 | })
|
368 | })
|
369 |
|
370 | var gen = fibonacci()
|
371 | var count_down = 10
|
372 | this.repeat( function(){
|
373 | this.step( function(){
|
374 | if( !count_down-- ) this.break
|
375 | gen.next()
|
376 | }).step( function( r ){
|
377 | trace( count_down, "fibo", r)
|
378 | })
|
379 | })
|
380 | ```
|
381 |
|
382 |
|
383 | API
|
384 | ===
|
385 |
|
386 | ```
|
387 | l8
|
388 | -- step/task creation. "body" can create additional steps/subtasks
|
389 | .step( body ) -- queue a step on the path to task's completion
|
390 | .task( body ) -- queue a step that waits on a blocking subtask
|
391 | .fork( body ) -- queue a step that starts a forked task, forks "join"
|
392 | .repeat( body ) -- queue a step that repeats a blocking subtask
|
393 | .spawn( body ) -- like fork() but next step does not wait for subtask
|
394 | .generator( body ) -- queue a step that spwans a task that yields results
|
395 |
|
396 | -- step walking
|
397 | .proceed( block ) -- walk a step on its path, at most once per step
|
398 | .walk -- idem but params of block become results of step
|
399 | .flow -- idem but first param is filtered out unless thrown
|
400 | .continue -- stop executing current task, reschedule it instead
|
401 | .break -- "break" for "repeat" steps
|
402 | .return( [val] ) -- like "return" in normal flow, skip all queued steps
|
403 | .raise( error ) -- raise an exception in task, skip all queued steps
|
404 |
|
405 | -- task completion monitoring, for task users
|
406 | .then( ... ) -- Promise/A protocol, tasks are promises
|
407 | .callback( cb ) - - Node.js style callback. Also .callback( promise, cb)
|
408 | .join() -- pause task until all subtasks are done
|
409 |
|
410 | -- task completion handling, for task implementers
|
411 | .defer( body ) -- push a block to execute when task is almost done
|
412 | .progress( block ) -- block to run when a subtask is done or step walked
|
413 | .success( block ) -- block to run when task is done without error
|
414 | .failure( block ) -- block to run when task is done but with error
|
415 | .final( block ) -- block to run when task is all done (after .defer())
|
416 |
|
417 | -- task "local" variables, subtasks inherit them, a binding store them
|
418 | .var( name, val ) -- define a new variable in current task's binding
|
419 | .get( name ) -- get value of task local variable
|
420 | .set( name, val ) -- set value of task local variable
|
421 | .binding( name ) -- return binding where task local variable is stored
|
422 |
|
423 | -- task state related
|
424 | .state -- return state of task, I->[Run|Pause]*->Success/Fail
|
425 | .pause -- block task at step, waiting until task is resumed
|
426 | .paused -- return true if task was paused
|
427 | .resume -- resume execution of task paused at some step
|
428 | .running -- true if task not done nor paused
|
429 | .cancel -- cancel task & its sub tasks, brutal
|
430 | .canceled -- true if task failed because it was canceled
|
431 | .stop -- gentle cancel
|
432 | .stopping -- true after a gentle cancel, until task is done
|
433 | .stopped -- true if done task was gently canceled (gracefull)
|
434 | .done -- true if task done, else either running or paused
|
435 | .succeed -- true if task done without error
|
436 | .fail -- true if task done but with an error
|
437 | .error -- last raised error (ie last exception)
|
438 | .result -- result of last successful step
|
439 | .timeout( milli ) -- cancel task if it is not done on time
|
440 | .sleep( milli ) -- block on step for a while, then move to next step
|
441 | .wait( promise ) -- block task until some lock opens, promise agnostic
|
442 |
|
443 | -- misc, hierarchy
|
444 | .l8 -- return global L8 object, also root task
|
445 | .current -- return current task
|
446 | .parent -- return parent task
|
447 | .tasks -- return immediate pending sub tasks
|
448 | .top -- return top task of sub task (child of l8 root task)
|
449 |
|
450 | -- scoping (value of "this" related)
|
451 | .begin -- create a new task
|
452 | .end -- start that new task
|
453 | .Task( function ) -- the .begin/.end guarded version of a function
|
454 |
|
455 | All these methods, if invoked against the global l8 object, will usually get
|
456 | forwarded to the "current task", the task that is currently executing. That
|
457 | task is often the returned value of such methods, when it makes sense.
|
458 |
|
459 | To synchronize the access to resources, l8 provide a few well known basic
|
460 | solutions implemented using promises and invoked using task.wait( resource):
|
461 |
|
462 | .semaphore( [n] ) -- create a new semaphore, also a promise provider
|
463 | .mutex( [entered] ) -- ... a new mutex, also a ...
|
464 | .lock( [nentered] ) -- ... lock (reentrant mutex), ...
|
465 | .queue( [bound] ) -- message queue, ...
|
466 | .port() -- like a message queue but without any buffering
|
467 | .signal() -- signal, ..., like a promise that fires many times
|
468 | .timeout( delay ) -- a promise fulfilled after a delay
|
469 | .generator() -- a next()/yield() consumer/producer resource
|
470 | .Generator( block ) -- build a Generator Constructor.
|
471 |
|
472 | Semaphores, mutexes and locks provide:
|
473 |
|
474 | .promise -- provide a promise fullfilled when rsrc is acquired
|
475 | .release() -- make resource available
|
476 | .close() -- reject pending promises
|
477 | .task -- resource owner task, when applicable (mutex & lock)
|
478 |
|
479 | Message queues are useful to synchronize a consumer and a producer:
|
480 |
|
481 | .in -- a "can get()" promise, alias for .promise
|
482 | .out -- a "can put()" promise
|
483 | .get() -- pause current task until queue is not empty, get msg
|
484 | .tryGet() -- get msg when one is available, don't block
|
485 | .put( msg ) -- pause current task until queue is not full, put msg
|
486 | .tryPut( msg ) -- put msg in queue unless queue is full
|
487 | .capacity -- total capacity (bound)
|
488 | .length -- used capacity
|
489 | .full -- when capacity is totally used
|
490 | .empty -- when length is 0
|
491 |
|
492 | Signals are usefull to send a signal to multiple tasks when some condition is
|
493 | met:
|
494 |
|
495 | .promise -- a promise fullfilled when signal is next signaled
|
496 | .signal( value ) -- signal signal, resolve all pending promises
|
497 |
|
498 | Generators let a producer and a consumer collaborate in a next()/yield() way:
|
499 |
|
500 | .get -- a "can next()" promise, alias for .promise
|
501 | .put -- a "can yield()" promise
|
502 | .next( [msg] ) -- pause task until producer yields, get/send a msg
|
503 | .yield( msg ) -- pause task until consumer calls .next(), get/send
|
504 | .tryNext( [msg] ) -- if .get promise is ready, get yield's msg
|
505 | .tryYield( msg ) -- if .put promise is ready, get next's msg
|
506 | .close() -- break paused tasks (using .break())
|
507 | .closed -- true once generator is closed
|
508 |
|
509 | When a producer task is created using a Generator Constructor, that task can
|
510 | use l8.yield() while the parent task can use l8.next() ; the associated
|
511 | generator will automatically get closed when either the producer or the
|
512 | consumer task terminates.
|
513 |
|
514 | Many actions are possible when you have a hand of promises, l8 provides some
|
515 | of them:
|
516 |
|
517 | .selector( promises ) -- fires when any promise does
|
518 | .any( promises ) -- alias for .selector()
|
519 | .or( promises ) -- fires when a promise with a non falsy result fires
|
520 | .aggregator( promises) -- collect results, fires when all promises did
|
521 | .all( promises ) -- alias for .aggregator()
|
522 | .and( promises ) -- fires with "false" early or with collected results
|
523 | Note: in addition to promises, the array can contain immediate values and
|
524 | functions returning either an immediate value, a function to evaluate or a
|
525 | promise. The result of a promise can be a Function that will be evaluated and
|
526 | will replace the initial promise.
|
527 |
|
528 | Additional librairies provides other usefull services. See Q.js, When.js,
|
529 | Promise.io, etc
|
530 |
|
531 | .
|
532 |
|
533 | Misc:
|
534 | .debug( [on]) -- get/set debug mode
|
535 | .trace( p1, ... ) -- output trace
|
536 | .de -- my de&&bug darling
|
537 |
|
538 | ```
|
539 |
|
540 | Simple example, explained
|
541 | =========================
|
542 |
|
543 | Two steps. Hypothetical synchronous version if functions could block:
|
544 |
|
545 | ```
|
546 | function fetch( a ){
|
547 | meth1( a)
|
548 | return meth2( a)
|
549 | }
|
550 | ```
|
551 |
|
552 | Idem but actual javascript using callback style:
|
553 |
|
554 | ```
|
555 | function fetch( a, cb ){
|
556 | meth1( a, function( error, result ){
|
557 | if( error ) return cb( error);
|
558 | meth2( a, function( error, result ){
|
559 | cb( error, result);
|
560 | }
|
561 | }
|
562 | }
|
563 | ```
|
564 |
|
565 | Idem but using l8, extra long version:
|
566 |
|
567 | ```
|
568 | function fetch_this_and_that( a, callback ){
|
569 | return l8.begin
|
570 | .step( function(){
|
571 | meth1( a, this.next ) })
|
572 | .step( function( err, result ){
|
573 | if( err ) throw err else meth2( a, this.next }) })
|
574 | .step( function( err, result ){
|
575 | if( err ) throw err else return result })
|
576 | .final( function( err, result ){ callback( err, result) })
|
577 | .end}
|
578 | ```
|
579 |
|
580 | CoffeeScript, much shorter, also thanks to Task() functor:
|
581 |
|
582 | ```
|
583 | fetch = l8.Task (a,cb) ->
|
584 | @step -> meth1 a, @walk
|
585 | @step (e,r) -> if e then throw e else meth2 a, @walk
|
586 | @step (e,r) -> if e then throw e else r
|
587 | @final (e,r) -> cb e, r
|
588 | ```
|
589 |
|
590 | Idem but returning a promise instead of using a callback:
|
591 |
|
592 | ```
|
593 | fetch = l8.Task (a) ->
|
594 | @step -> meth1 a, @walk
|
595 | @step (e,r) -> if e then throw e else meth2 a, @walk
|
596 | @step (e,r) -> if e then throw e else r
|
597 | ```
|
598 |
|
599 | Idem but assuming meth1 and meth2 make tasks returning promises too:
|
600 |
|
601 | ```
|
602 | fetch = l8.Task (a) ->
|
603 | @step -> meth1 a
|
604 | @step -> meth2 a
|
605 | ```
|
606 |
|
607 | Back to Javascript:
|
608 |
|
609 | ```
|
610 | fetch = l8.Task( function( a ){
|
611 | this.step( function(){ meth1( a) })
|
612 | this.step( function(){ meth2( a) })
|
613 | })
|
614 | ```
|
615 |
|
616 | Using the "trans-compiler":
|
617 |
|
618 | ```
|
619 | fetch = l8.compile( function( a ){
|
620 | step; meth1( a);
|
621 | step; meth2( a);
|
622 | })
|
623 | ```
|
624 |
|
625 | The conclusion is that using tasks, steps and promises, the code's structure is
|
626 | similar to the hypothetical javascript blocking function.
|
627 |
|
628 | Other examples
|
629 | --------------
|
630 |
|
631 | Multiple steps, run sequentially:
|
632 |
|
633 | ```
|
634 | fetch_all_seq = l8.Task (urls) ->
|
635 | results = []
|
636 | for url in urls then do (url) ->
|
637 | @step -> scrap url, @proceed -> result.push {url, err, content}
|
638 | @success -> results
|
639 | ```
|
640 |
|
641 | Multiple steps, each run in parallel:
|
642 |
|
643 | ```
|
644 | fetch_all = l8.Task (urls) ->
|
645 | results = []
|
646 | for url in urls then do (url) ->
|
647 | @fork ->
|
648 | scrap url, @proceed (err, content) -> results.push {url, err, content}
|
649 | @success -> results
|
650 | ```
|
651 |
|
652 | Repeated steps, externally terminated, gently:
|
653 |
|
654 | ```
|
655 | spider = l8.Task (urls, queue) ->
|
656 | @repeat ->
|
657 | url = null
|
658 | @step -> url = queue.shift
|
659 | @step -> @delay 10000 if @parent.tasks.length > 10
|
660 | @step ->
|
661 | @break if @stopping
|
662 | scrap url, @walk
|
663 | @step (err,urls) ->
|
664 | return if err or @stopping
|
665 | for url in urls
|
666 | queue.unshift url unless url in queue
|
667 |
|
668 | spider_task = l8.spawn -> spider( "http://xxx.com")
|
669 | ...
|
670 | stop_spider = -> spider_task.stop
|
671 | ```
|
672 |
|
673 | Small loop, on one step, using "continue":
|
674 |
|
675 | ```
|
676 | fire_all = l8.Task (targets) ->
|
677 | ii = 0
|
678 | @step ->
|
679 | return if ii > targets.length
|
680 | targets[ii++].fire()
|
681 | @continue
|
682 | ```
|
683 |
|
684 | StratifiedJs example, see http://onilabs.com/stratifiedjs
|
685 |
|
686 | ```
|
687 | var news;
|
688 | waitfor {
|
689 | news = http.get("http://news.bbc.co.uk");
|
690 | }
|
691 | or {
|
692 | hold(1000);
|
693 | news = http.get("http://news.cnn.com");
|
694 | }
|
695 | or {
|
696 | hold(1000*60);
|
697 | throw "sorry, no news. timeout";
|
698 | }
|
699 | show(news);
|
700 | ```
|
701 |
|
702 | The equivalent code with l8 is:
|
703 |
|
704 | ```
|
705 |
|
706 | // JavaScript
|
707 | var show_news = l8.Task( function(){
|
708 | var news = this
|
709 | .fork( function(){ http.get( "http://news.bbc.co.uk",
|
710 | this.proceed( function( item ){ news.return( item) }) )
|
711 | })
|
712 | .fork( function(){
|
713 | this.step( function(){ this.sleep( 1000) });
|
714 | this.step( function(){ http.get( "http://news.cnn.com",
|
715 | this.proceed( function( item ){ news.return( item) }) )
|
716 | })
|
717 | })
|
718 | .fork( function(){
|
719 | this.step( function(){ this.sleep( 1000 * 60) });
|
720 | this.step( function(){ throw "sorry, no news. timeout" })
|
721 | })
|
722 | .success( function( news ){ show( news) });
|
723 | })
|
724 |
|
725 | // CoffeeScript
|
726 | show_news = l8.Task ->
|
727 | news = @current
|
728 | @fork ->
|
729 | @step -> http.get "http://news.bbc.co.uk"
|
730 | @step -> @news.return()
|
731 | @fork ->
|
732 | @step -> @sleep 1000
|
733 | @step -> http.get "http://news.cnn.com"
|
734 | @step -> @news.return()
|
735 | @fork ->
|
736 | @step -> @sleep 1000 * 60
|
737 | @step -> throw "sorry, no news. timeout"
|
738 | @success( news ) -> show news
|
739 |
|
740 | // l8 trans-compiler
|
741 | var show_new = l8.compile( function(){
|
742 | var news = this
|
743 | fork; begin
|
744 | step; http.get( "http://news.bbc.co.uk");
|
745 | step; news.return();
|
746 | end
|
747 | fork; begin
|
748 | step; this.sleep( 1000);
|
749 | step; http.get( "http://news.cnn.com");
|
750 | step; news.return();
|
751 | end
|
752 | fork; begin
|
753 | step; this.sleep( 1000 * 60);
|
754 | step; throw "sorry, no news. timeout";
|
755 | end
|
756 | success( news ); show( news);
|
757 | })
|
758 |
|
759 | ```
|
760 |
|
761 | Node.js google group "pipe" example, see
|
762 | https://groups.google.com/forum/?fromgroups=#!topic/nodejs/5hv6uIBpDl8
|
763 |
|
764 | ```
|
765 | function pipe( inStream, outStream, callback ){
|
766 | var loop = function( err ){
|
767 | if (err) callback( err);
|
768 | else inStream.read( function( err, data ){
|
769 | if (err) callback(err);
|
770 | else data != null ? outStream.write( data, loop) : callback();
|
771 | });
|
772 | }
|
773 | loop();
|
774 | }
|
775 |
|
776 | pipe = l8.Task( function ( inStream, outStream ){ this
|
777 | .repeat( function(){ this
|
778 | .step( function(){
|
779 | inStream.read() })
|
780 | .step( function( data ){
|
781 | if( !data) this.break;
|
782 | outStream.write( data);
|
783 | })
|
784 | })
|
785 | })
|
786 |
|
787 | pipe = l8.Task (in,out) ->
|
788 | @repeat ->
|
789 | @step -> in.read()
|
790 | @step (data) ->
|
791 | @break if !data
|
792 | out.write data
|
793 |
|
794 | pipe = l8.compile( function( in, out ){
|
795 | repeat; begin
|
796 | step; in.read();
|
797 | step( data ); if( !data ) this.break;
|
798 | out.write( data);
|
799 | end
|
800 | })
|
801 | ```
|
802 |
|
803 | Note: for this example to work, node.js streams need to be "taskified". This
|
804 | is left as an exercize.
|
805 |
|
806 | The "recursive dir walk" nodejs challenge:
|
807 |
|
808 | ```
|
809 | Var fs = require('fs');
|
810 | var path = require('path');
|
811 |
|
812 | var recurseDir = function(dir) {
|
813 | fs.readdirSync(dir).forEach(function(child) {
|
814 | if (child[0] != '.') {
|
815 | var childPath = path.join(dir, child);
|
816 | if (fs.statSync(childPath).isDirectory()) {
|
817 | recurseDir(childPath);
|
818 | } else {
|
819 | console.log(childPath);
|
820 | }
|
821 | }
|
822 | });
|
823 | };
|
824 | recurseDir(process.argv[2]);
|
825 |
|
826 | // Async version:
|
827 | var recurseDir = l8.Task( function( dir ){
|
828 | l8.step( function( ){ fs.readdir( dir, this.flow) })
|
829 | l8.step( function( l ){ l.forEach( function( child ){
|
830 | if( child[0] != "." ){
|
831 | var childPath = path.join( dir, child);
|
832 | l8.step( function( ){ fs.stat( childPath, this.flow) })
|
833 | l8.step( function( r ){
|
834 | if( r.isDirectory() ){
|
835 | recurseDir( childPath)
|
836 | }else{
|
837 | console.log(dchildPath)
|
838 | }
|
839 | })
|
840 | }
|
841 | }) })
|
842 | })
|
843 | ```
|
844 |
|
845 | Cooperating tasks examples
|
846 | ==========================
|
847 |
|
848 | Use promises:
|
849 |
|
850 | A step can block waiting for a promise using L8.wait( promise). If waiting for
|
851 | a promise is the only action of a step, then L8.step( promise) can be used as
|
852 | a shortcut for L8.step( function(){ L8.wait( promise) }). Note however that the
|
853 | two forms are not semantically identical because L8.step( promise) uses the
|
854 | promise available when the new step is created/scheduled whereas in the second
|
855 | form L8.wait() uses the promise available when the step is actually executed,
|
856 | not when it is scheduled.
|
857 |
|
858 | Access to a critical resource:
|
859 |
|
860 | ```
|
861 | var mutex = L8.mutex()
|
862 | ...
|
863 | .step( mutex) // or .step( function(){ this.wait( mutex) })
|
864 | .step( function(){
|
865 | l8.defer( function(){ mutex.release() })
|
866 | xxx
|
867 | })
|
868 | ...
|
869 | ```
|
870 |
|
871 | Producer/consumer:
|
872 |
|
873 | ```
|
874 | TBD
|
875 | ```
|
876 |
|
877 |
|
878 | Mixing statements and steps
|
879 | ---------------------------
|
880 |
|
881 | Because "steps" and "statements" are not on the same level (steps for tasks,
|
882 | statements for functions), the classical javascript control structures have
|
883 | equivalent structures at the step level.
|
884 |
|
885 | ```
|
886 | function xx(){
|
887 | ..1..
|
888 | try{
|
889 | ..2..
|
890 | catch( e ){
|
891 | ..3..
|
892 | finally {
|
893 | ..4..
|
894 | }
|
895 | ..5..
|
896 | }
|
897 | ```
|
898 | becomes:
|
899 |
|
900 | ```
|
901 | xx_task = l8.Task( function(){
|
902 | this.step( function(){
|
903 | ..1..
|
904 | }).step( function(){
|
905 | this.begin.step( function(){
|
906 | ..2..
|
907 | }).failure( function(e){
|
908 | ..3..
|
909 | }).final( function(){
|
910 | ..4..
|
911 | }).end
|
912 | }).step( function(){
|
913 | ..5..
|
914 | })
|
915 | })
|
916 |
|
917 | or
|
918 |
|
919 | xx_task = l8.compile( function(){
|
920 | step; ..1..
|
921 | step; begin
|
922 | ..2...
|
923 | failure;
|
924 | ..3..
|
925 | final;
|
926 | ..4..
|
927 | end
|
928 | step; ..5..
|
929 | })
|
930 | ```
|
931 |
|
932 | ```
|
933 | while( condition ){
|
934 | ...
|
935 | if( extra )break
|
936 | ...
|
937 | if( other_extra )continue
|
938 | ...
|
939 | }
|
940 | ```
|
941 | becomes:
|
942 |
|
943 | ```
|
944 | l8.repeat( function(){
|
945 | ...
|
946 | if( condition ) this.break
|
947 | ...
|
948 | if( extra ) this.break
|
949 | ...
|
950 | if( other_extra ) this.continue
|
951 | ...
|
952 | }
|
953 |
|
954 | or
|
955 |
|
956 | xx = l8.compile( function(){
|
957 | repeat; begin
|
958 | ...
|
959 | if( condition ) this.break
|
960 | ...
|
961 | if( extra ) this.break
|
962 | ...
|
963 | if( other_extra ) this.continue
|
964 | ...
|
965 | end
|
966 | })
|
967 | ```
|
968 |
|
969 | ```
|
970 | for( init ; condition ; next ){
|
971 | ...
|
972 | }
|
973 | ```
|
974 |
|
975 | becomes:
|
976 |
|
977 | ```
|
978 | init
|
979 | this.repeat( function(){
|
980 | if( condition ) this.break
|
981 | ...
|
982 | next
|
983 | })
|
984 | ```
|
985 |
|
986 | ```
|
987 | for( init ; condition ; next ){
|
988 | ...
|
989 | if( extra ) continue
|
990 | ...
|
991 | })
|
992 | ```
|
993 | becomes:
|
994 |
|
995 | ```
|
996 | init
|
997 | this.repeat( function(){
|
998 | if( condition ) this.break
|
999 | this.task( function(){
|
1000 | ...
|
1001 | this.step( function(){ if( extra ) this.return })
|
1002 | ...
|
1003 | this.step( function(){ next })
|
1004 | })
|
1005 | })
|
1006 |
|
1007 | or
|
1008 |
|
1009 | xx = l8.compile( function(){
|
1010 | init; repeat; begin ; if( condition ) this.break ; begin
|
1011 | ...
|
1012 | if( extra ) this.return
|
1013 | ...
|
1014 | end; next; end
|
1015 | })
|
1016 | ```
|
1017 |
|
1018 | ```
|
1019 | for( var key in object ){
|
1020 | ...
|
1021 | }
|
1022 | ```
|
1023 | becomes:
|
1024 |
|
1025 | ```
|
1026 | var keys = object.keys(), key
|
1027 | this.repeat( function(){
|
1028 | if( !(key = keys.shift()) ) this.break
|
1029 | ...
|
1030 | })
|
1031 |
|
1032 | or
|
1033 |
|
1034 | xx = l8.compile( function(){
|
1035 | var keys = object.keys(), key
|
1036 | repeat; begin;
|
1037 | if( !(key = keys.shift()) ) this.break
|
1038 | ...
|
1039 | end
|
1040 | })
|
1041 | ```
|
1042 |
|
1043 | .defer() versus .final()
|
1044 | ========================
|
1045 |
|
1046 | .final( block) provides a solution that mimics the finally clause of the "try"
|
1047 | javascript construct. The final block typically performs "clean up" work
|
1048 | associated with the task it is attached too.
|
1049 |
|
1050 | ```
|
1051 | var file
|
1052 | l8
|
1053 | .step( function(){ file = file_open( file_name) })
|
1054 | .step( function(){ xxxx work with that file })
|
1055 | .final( function(){ file_close( file) })
|
1056 | ```
|
1057 |
|
1058 | There is only one final clause per task. That clause is attached to the task
|
1059 | when the .final() method is executed. When multiple clauses are needed, one
|
1060 | needs to create nested tasks. The final block is executed once the task is
|
1061 | done. As a result additional steps are attached to the "parent" task, not to
|
1062 | the current task (this may change in a future version).
|
1063 |
|
1064 | ```
|
1065 | var file1
|
1066 | var file2
|
1067 | l8
|
1068 | .step( function(){ file2 = file_open( file1_name) })
|
1069 | .step( function(){ xxxx work with file1 xxx })
|
1070 | .step( function(){
|
1071 | if( some_thing ){ l8.begin
|
1072 | .step( function(){ file = file_open( file2_name) })
|
1073 | .step( function(){ xxx work with file2 xxx })
|
1074 | .final( function(){ file_close( file2) })
|
1075 | .end })
|
1076 | .final( function(){ file_close( file1) })
|
1077 | ```
|
1078 |
|
1079 | .defer( block) is inspired by the Go language "defer" keyword. It is itself
|
1080 | a variation around the C++ notion of "destructors". There can be multiple
|
1081 | deferred blocks for a task. Because deferred steps are executed just before the
|
1082 | task reach its end, they can register additional steps to handle async
|
1083 | activities. As a result, the task is not fully done until all the defered work
|
1084 | is done too. Deferred blocks are executed in a LIFO order, ie the last deferred
|
1085 | step is run first.
|
1086 |
|
1087 | ```
|
1088 | var resourceA
|
1089 | var resourceB
|
1090 | l8
|
1091 | .step( function(){ acquireResource( xxx) })
|
1092 | .step( function( r ){
|
1093 | ressourceA = r
|
1094 | l8.defer( function(){ releaseResource( resourceA) })
|
1095 | })
|
1096 | .step( function(){ xxx work with resourceA xxx })
|
1097 | .step( function(){ acquireResource( yyy) })
|
1098 | .step( function( r ){
|
1099 | resourceB = r
|
1100 | l8.defer( function(){ releaseResource( resourceB) })
|
1101 | })
|
1102 | .step( function(){ xxx work with resourceB xxx })
|
1103 | ```
|
1104 |
|
1105 | Because multiple deferred blocks are possible, .defer() is more modular. For
|
1106 | example, it makes it possible to implement the "Resource Acquisition is
|
1107 | Initialization" pattern.
|
1108 | See http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization
|
1109 |
|
1110 | ```
|
1111 | var with_file = l8.Task( function( file_name ){
|
1112 | var file
|
1113 | l8
|
1114 | .step(){ function(){ file_open( file_name) })
|
1115 | .step( r ){
|
1116 | file = r
|
1117 | l8.parent.defer( function(){ file_close( file) })
|
1118 | }
|
1119 | })
|
1120 |
|
1121 | Usage:
|
1122 |
|
1123 | var file
|
1124 | l8
|
1125 | .step( function(){ with_file( file_name) })
|
1126 | .step( function( r ){ xxx work with file r xxx })
|
1127 | xxx don't worry about closing that file xxx
|
1128 | ```
|
1129 |
|
1130 | The general "rule of thumb" is to use .final() for quick & simple stuff and
|
1131 | use .defer() for more elaborated async stuff.
|
1132 |
|
1133 |
|
1134 | Task "local" variables
|
1135 | ======================
|
1136 |
|
1137 | Tasks can define variables much like functions can. There are some differences.
|
1138 | Contary to function local variables, task local variables are "fluid", as per
|
1139 | Scheme jargon, ie they are dynamically scoped (whereas javascript variables use
|
1140 | lexical scoping). See also http://en.wikipedia.org/wiki/Thread_local_storage
|
1141 |
|
1142 | A nice property of task local variables is the fact that a variable defined by
|
1143 | a parent task is accessible from a child subtask. As a result, task local
|
1144 | variables are "global" to a subset of all tasks, based on the task hierarchy.
|
1145 |
|
1146 | When a subtask needs to override an inherited variables, it uses ".var()" to
|
1147 | set a new value that it's own subtasks will share. When a subtask, on the
|
1148 | contrary, wants to share an inherited variables, it uses ".set()" to set a new
|
1149 | value that it's parent task can query using ".get()".
|
1150 |
|
1151 | Please note that tasks can also use regular lexically scoped variables, as long
|
1152 | as such a variable is part of a function's closure. This is the most convenient
|
1153 | and fast use case. When more global variables are required, l8 fluid variables
|
1154 | are handy.
|
1155 |
|
1156 | ```
|
1157 | var trace = function(){
|
1158 | l8.trace( l8.get( "message") + " from " + l8.binding( "message").task)
|
1159 | }
|
1160 | var subtask = function(){
|
1161 | l8.label = "sub"
|
1162 | l8.step( function(){ trace() })
|
1163 | l8.step( function(){ l8.var( "message", "deeper") })
|
1164 | l8.step( function(){ l8.delay( 10) })
|
1165 | l8.step( function(){ trace() })
|
1166 | }
|
1167 |
|
1168 | l8.task( function(){
|
1169 | l8.label = "main"
|
1170 | l8.var( "message", "top")
|
1171 | l8.spawn( subtask )
|
1172 | l8.step( function(){ l8.var( "message", "deeper") })
|
1173 | l8.step( function(){ trace() })
|
1174 | })
|
1175 |
|
1176 | displays: top from Task/x[main], top from Task/x[main], deeper from Task/x[sub]
|
1177 | ```
|
1178 |
|
1179 | Actors and proxies
|
1180 | ==================
|
1181 |
|
1182 | Actors are objects that communicate the one with the others using messages.
|
1183 |
|
1184 | When idle, an actor simply waits for incoming messages. When a message is
|
1185 | received, the actor can either decide to process it or decide to process
|
1186 | it later, after some other messages. As a result, each actor has a message
|
1187 | queue, called a "mailbox" in the actor jargon.
|
1188 |
|
1189 | Some messages don't require an answer. They are "send type" messages. Some
|
1190 | messages do require an answer. They are "call type" messages.
|
1191 |
|
1192 | When it processes a "call type" message, the actor can decide to provide
|
1193 | either a direct response or a promise to be fullfilled (or rejected) later
|
1194 | on. Until that decision is taken, additional "call type" messages are
|
1195 | queued. This makes it easy to "serialize" the processing of calls when that
|
1196 | makes sense.
|
1197 |
|
1198 | Each actor has a unique name, provided by the actor creator. All actors are
|
1199 | remembered in a global registry where one can lookup for them. Names are
|
1200 | in the form xx.xx.xx where the last xx is generated by the actor itself
|
1201 | when only xx.xx. is provided at creation time.
|
1202 |
|
1203 | Actors can be usefull to build distributed systems. In these configurations,
|
1204 | each javascript process hosts actors and communicate with actors hosted in some
|
1205 | other processes, via proxies.
|
1206 |
|
1207 | API:
|
1208 | ```
|
1209 | .Actor( name, pattern ) -- make an Actor task constructor
|
1210 | .send( ... ) -- send a message to the actor
|
1211 | .receive( pattern ) -- redefine reaction to received messages
|
1212 | .Actor.lookup( name ) -- look for an existing actor
|
1213 | .Actor.all -- an object with one propertie per existing actor
|
1214 | .actor -- actor the current task is running
|
1215 | .actor.stage -- stage the actor received current message from
|
1216 | .Role -- base class for Role objects, can be extented
|
1217 | .role( delegate ) -- alternative mechanism to define actor's patterns
|
1218 | .stage( name, [url] ) -- a place with actors in it
|
1219 | .proxy( stage, name ) -- access to remote actors
|
1220 | ```
|
1221 |
|
1222 | L8 Design
|
1223 | ---------
|
1224 |
|
1225 | The key idea is to break a javascript function into "steps" and then walk thru
|
1226 | these steps much like the javascript interpreter runs thru the statements
|
1227 | of a function. This is quite verbose however. But not so much when using
|
1228 | CoffeeScript. This is why, after considering the idea years ago, I waited
|
1229 | until now to implement it. That my cousin Jean Vincent would consider breaking
|
1230 | a function into steps as something close enough to threading was another strong
|
1231 | motivator.
|
1232 |
|
1233 | To break functions into steps, I use a DSL (domain specific language) API.
|
1234 | Once the AST (abstact syntax tree) is built, I interpret it.
|
1235 |
|
1236 | Executable nodes in the AST are called "steps". They are the smallest non
|
1237 | interruptible executable entities. Each Step belongs to a task. Task can
|
1238 | involve sub tasks that cooperate across multiple paths.
|
1239 |
|
1240 | This becomes really interesting when the AST gets dynamically modified! This
|
1241 | happens when a step decides that it requires additional sub steps to complete.
|
1242 |
|
1243 | On a path, execution goes from step to step. When a step involves sub-steps,
|
1244 | the step is blocked until the sub-steps complete, unless sub-steps are created
|
1245 | in a forked parallel path.
|
1246 |
|
1247 | Example:
|
1248 |
|
1249 | ```
|
1250 | MainTask
|
1251 | Task.1 - a task with a single path with a loop subpath
|
1252 | MainPath
|
1253 | Step
|
1254 | Step
|
1255 | RepeatPath
|
1256 | Step
|
1257 | Step
|
1258 | Step
|
1259 | Task.2 - a task with two paths (two forked subtasks)
|
1260 | MainPath
|
1261 | ForkedPath
|
1262 | Step
|
1263 | Step
|
1264 | ForkedPath
|
1265 | Step
|
1266 | Step
|
1267 | ```
|
1268 |
|
1269 | Adding steps
|
1270 | ------------
|
1271 |
|
1272 | Execution goes "step by step" until task completion. Steps to execute are
|
1273 | queued. To queue a new step to execute after the currently executing step, use
|
1274 | .step(). Such steps are run once the current step is completed, FIFO order.
|
1275 |
|
1276 | To insert a new step on a new parallel task/path, use .fork(). Such steps block
|
1277 | the current step until they are completed. When multiple such forked steps are
|
1278 | inserted, the next non forked step will execute when all the forked steps are
|
1279 | done. The result of such multiple steps is accumulated into an array that can be
|
1280 | the parameter of the next non forked step. This is a "join". When only one
|
1281 | forked step is inserted, this is similar to calling a function, ie the next
|
1282 | step receives the result of the task that ran the forked step. There is a
|
1283 | shortcut for that special frequent case, please use .task().
|
1284 |
|
1285 | To insert steps that won't block the current step, use spawn() instead. Such
|
1286 | steps are also run in a new task but the current step is not blocked until
|
1287 | the new task is complete. If the parent task terminates before the spawn tasks
|
1288 | are completed, the spawn tasks are re-attached to the parent task of the task
|
1289 | that created them, ie. spawn tasks are "inherited" by the parent of their
|
1290 | creator (Unix processes are similar).
|
1291 |
|
1292 | Note that is it possible to cancel tasks and/or their subtasks. That cancel
|
1293 | action can be either "gentle" (using .stop() & .stopping) or "brutal" using
|
1294 | .cancel(). a_task.return( x) also cancel a task (and it's subtasks) but
|
1295 | provides the result of that task (whereas .cancel() makes the task fail).
|
1296 |
|
1297 | Blocking
|
1298 | --------
|
1299 |
|
1300 | Steps are useful to describe flows that depends on other flows. As a result
|
1301 | a step often describes sub-steps and/or sub pathes/tasks. These steps then
|
1302 | "block" waiting for the sub items to complete.
|
1303 |
|
1304 | For simple steps, that only depend on the completion of a simple asynchronous
|
1305 | function, .walk or .proceed() provides the callback to register with that
|
1306 | function. When the callback is called, flow walks from the current step to the
|
1307 | next one.
|
1308 |
|
1309 | Note: in the frequent case where the callback only needs to store the result
|
1310 | of the asychronous operation and move forward to the next step, please use
|
1311 | "walk" instead of proceed( function( x ){ this.result = x }).
|
1312 |
|
1313 | However, if the action's result dictates that some new "nested" steps are
|
1314 | required, one adds new steps from within the callback itself. Often, this style
|
1315 | of programming is not adviced because it basically resolves back to the
|
1316 | infamous "callback hell" that l8 attemps to avoid. A better solution is to
|
1317 | let the next step handle that.
|
1318 |
|
1319 | Do:
|
1320 | ```
|
1321 | @step -> fetch @walk
|
1322 | @step( result ) -> if result then more_fetch @walk
|
1323 | @step( result ) -> if result then fetch_again @walk
|
1324 | @step( result ) -> if result then use result @walk
|
1325 | @step -> done()
|
1326 | ```
|
1327 |
|
1328 | Don't:
|
1329 | ```
|
1330 | @step -> fetch @proceed (result) ->
|
1331 | if result
|
1332 | more_fetch @proceed (result) ->
|
1333 | if result
|
1334 | fetch_again @proceed (result) ->
|
1335 | if result then use result @walk
|
1336 | @step -> done()
|
1337 | ```
|
1338 |
|
1339 | Or, even better, use task constructors instead of functions with callbacks:
|
1340 | ```
|
1341 | @step -> fetch()
|
1342 | @step( r ) -> more_fetch() if r
|
1343 | @step( r ) -> fetch_again() if r
|
1344 | @step( r ) -> use r if r
|
1345 | ```
|
1346 |
|
1347 | Extensions
|
1348 | ----------
|
1349 |
|
1350 | The l8 API defines a concept of Task/Path/Step entities that works nicely in
|
1351 | the async/callback dominated world of Javascript and yet manage to provide some
|
1352 | useful tools (hopefully) to address the infamous "callback hell" issue.
|
1353 |
|
1354 | However these tools are very basic.
|
1355 |
|
1356 | One way to improve on that situation is to use one of the multiple existing
|
1357 | "promise" handling libraries, such as Q, when and rsvp. See also
|
1358 | http://howtonode.org/promises and https://gist.github.com/3889970
|
1359 |
|
1360 | What is also needed are more sophisticated yet simpler to use solutions. There
|
1361 | are many styles of coding regarding the orchestration of complex interactions
|
1362 | between fragments on code. We are no longer restricted to the signal/kill
|
1363 | mechanism from the 70s in Unix!
|
1364 |
|
1365 | In addition to the classics (semaphores, mutexes, message queues, conditional
|
1366 | variables, signals, monitors, events...) newer mechanisms are experimented or
|
1367 | rediscovered. Things like channels in Go, actors in Erlang, reactive system in
|
1368 | Angular, these things are interesting to explore.
|
1369 |
|
1370 | I believe l8 may help do that. For sure the granularity is not the same as in
|
1371 | native implementations. Instead of dealing with statements or even machine
|
1372 | code level instructions, l8 deals with much bigger "steps". However, regarding
|
1373 | synchronisation, this difference of scale does not imply a difference of
|
1374 | nature. As a consequence, solutions that work at the machine level may prove
|
1375 | also productive at the "step" higher level.
|
1376 |
|
1377 | Hence, proposals for extensions are welcome.
|
1378 |
|
1379 | Enjoy.
|
1380 |
|
1381 | Jean Hugues Robert, aka @jhr, october/november 2012.
|
1382 |
|
1383 | PS: all this stuff is to relieve me from my "node anxiety".
|
1384 | See http://news.ycombinator.com/item?id=2371152
|
1385 |
|