yau
v0.1.0
Published
yet another actor utility
Downloads
3
Maintainers
Readme
YetAnotherUtility
YAU provides a few methods and a style guide to write asynchronous, possibly distributed code under the actor model with asynchronous events, similar to the Akka framework.
Xemitter
xemitter uses
sindresorhus/emittery
to provide
an event emitter and task delegation facility that simplifies building
asynchronous applications using the Actor pattern.
Events are pairs of channel names and arbitrary data items. Events are emitted by emitter functions.
An arbitrary number of listeners can listen on any given channel. Listeners may be synchronous (returning
anything but a promise) or asynchronous (returning a promise). Each listener produces a value, be it
implicitly (undefined
) or explicitly (by using return x
or resolve x
). The outcomes of all listeners
are collected into an array of values, which may or may not be consumed by emitters.
Delegators are (inherently asynchronous) emitter functions that not only emit events, but that also use the result(s) that the event listener(s), if any, produced.
Because of the inherent unpredictability of the asynchronous mode of operation, no guarantee is made about
the ordering of values in the event result array. Since an important use case for event emitting is task
delegation, there is a way to distinguish a primary result from spurious and secondary results: On the one
hand, up to one listener may bind to a channel using XMT.primary_on
. Whatever values(s) that listener
produces when answering an event will be wrapped into a nonce object. The delegator then uses await
XMT.delegate
or XMT.select await XMT.emit
to retrieve up to one primary item from the event results:
# define a function that delegates some task:
sample_delegator = ->
result = await delegate 'some_task', 42
if is_sad result
... sad path ...
else
... happy path ...
return some_value
# use the delegator:
sample_delegator()
.then ( x ) ->
return error_handler x if is_sad x
# xxx
help 'resolved', jr x
.catch error_handler
# xemitter
#-----------------------------------------------------------------------------------------------------------
@_emitter = new Emittery()
@_has_primary_listeners = {}
#-----------------------------------------------------------------------------------------------------------
@_mark_as_primary = ( x ) -> { '~isa': 'XEMITTER/preferred', value: x, }
#-----------------------------------------------------------------------------------------------------------
@select = ( values ) -> ( values.filter ( x ) -> CND.isa x, 'XEMITTER/preferred' )[ 0 ]?.value ? null
#-----------------------------------------------------------------------------------------------------------
@primary_on = ( channel, listener ) ->
if @_has_primary_listeners[ channel ]
throw new Error "channel #{rpr channel} already has a primary listener"
@_has_primary_listeners[ channel ] = yes
@_emitter.on channel, ( data ) =>
return @_mark_as_primary await listener data
#-----------------------------------------------------------------------------------------------------------
@also_on = ( channel, listener ) ->
@_emitter.on channel, listener
#-----------------------------------------------------------------------------------------------------------
@emit = ( channel, data ) -> @_emitter.emit channel, data
@delegate = ( channel, data ) -> @select await @_emitter.emit channel, data
# debug '22621', Object::toString.call @delegate
############################################################################################################
for name, value of L = @
### TAINT poor man's 'callable' detection ###
continue unless CND.isa_function value.bind
L[ name ] = value.bind L
{ select, emit, delegate, also_on, primary_on, } = require 'xemitter'
#-----------------------------------------------------------------------------------------------------------
error_handler = ( reason ) ->
if is_sad reason
return urge 'this is sad:', jr reason
if is_crash reason
warn CND.reverse 'bad'
warn 'this is a crash'
warn jr reason
process.exit 1
# don't throw new Error( reason );
# throw reason
alert CND.reverse 'evil'
alert reason
return null
#-----------------------------------------------------------------------------------------------------------
also_on 'some_task_A', ( data ) ->
debug 'on some_task_A', jr data
return 'a secondary result'
#-----------------------------------------------------------------------------------------------------------
also_on 'some_task_B', ( data ) ->
debug 'on some_task_B', jr data
return 'a secondary result'
Example A
# use_sample_delegator_A
#-----------------------------------------------------------------------------------------------------------
primary_on 'some_task_A', ( data ) ->
debug 'on some_task_A', jr data
return new Promise ( pass, toss ) ->
if Math.random() > 0.5
pass "a happy primary result"
else
pass new_failure 'code42', "a sad primary result"
return null
#-----------------------------------------------------------------------------------------------------------
sample_delegator_A = ->
result = await delegate 'some_task_A', 42
# result = select await emit 'some_task_A', 42
if is_sad result
urge 'sample_delegator_A sad result: ', jr result
return null
else
help 'sample_delegator_A happy result: ', jr result
return "**#{result}**"
#-----------------------------------------------------------------------------------------------------------
sample_delegator_A()
.then ( x ) ->
return error_handler x if is_sad x
# xxx
help 'resolved', jr x
.catch error_handler
Example B
# use_sample_delegator_B
#===========================================================================================================
### Synchronous contractors without promises and asynchronous contractors with promises show the same
behavior; crucially, **the delegator does not have to be aware of any difference between the two**: ###
if settings.use_promises_in_contractor
info "using contractor with promises"
#-----------------------------------------------------------------------------------------------------------
primary_on 'some_task_B', ( data ) ->
debug 'on some_task_B', jr data
return new Promise ( resolve, reject ) ->
[ a, b, ] = data
return reject new_failure 'divbyzero', "division by zero: #{rpr a} / #{rpr b}", null if b is 0
resolve a / b
else
info "using contractor *without* promises"
#-----------------------------------------------------------------------------------------------------------
primary_on 'some_task_B', ( data ) ->
debug 'on some_task_B', jr data
[ a, b, ] = data
throw new_failure 'divbyzero', "division by zero: #{rpr a} / #{rpr b}", null if b is 0
return a / b
#-----------------------------------------------------------------------------------------------------------
sample_delegator_B = ->
try
#.......................................................................................................
info "computing 4 / 5"
result_1 = await delegate 'some_task_B', [ 4, 5, ]
info "computing 4 / 5: #{result_1}"
#.......................................................................................................
info "computing 3 / 0"
result_2 = await delegate 'some_task_B', [ 3, 0, ]
info "computing 3 / 0: #{result_2}"
#.......................................................................................................
# In the case of a style B contractor, only happy results are resolved; sad and bad results are
# rejected and end up in the catch clause:
return [ result_1, result_2, ]
catch unhappy
warn '28921', unhappy
if is_sad unhappy
# deal with failures: possibly log where and what occurred, return a replacement value (that may in
# itself by happy or sad):
result_2 = happy unhappy
urge "computing 3 / 0: #{result_2}"
urge 'sample_delegator_B sad result: ', jr unhappy
return null
# refuse to deal with anything else:
throw unhappy
#-----------------------------------------------------------------------------------------------------------
sample_delegator_B()
.then ( x ) ->
return error_handler x if is_sad x
# xxx
help 'resolved', jr x
.catch error_handler
use_sample_delegator_A()
use_sample_delegator_B { use_promises_in_contractor: yes, }
use_sample_delegator_B { use_promises_in_contractor: no, }
Synchronous contractors without promises and asynchronous contractors with promises show the same behavior; crucially, the delegator does not have to be aware of any difference between the two
@emit = ( channel, data ) -> await @_emitter.emit channel, data
@entrust = ( channel, data ) -> @_get_primary await @_emitter.emit channel, data
@delegate = ( channel, data ) -> @reject_unhappy @_get_primary await @_emitter.emit channel, data
### TAINT must consider case where no listener is available which leads to defect or a crash
emit
: Send out some (optional) data on a named channel, await all the results.entrust
: Send out some (optional) data on a named channel, await the primary result ornull
if no-one listened; where a contractor returns or resolves to an unhappy result, handling is up to the delegator.delegate
: Send out some (optional) data on a named channel, await the primary result ornull
if no-one listened; where a contractor returns or resolves to an unhappy result, an exception is thrown so that all unhappy outcomes have to be dealt with either in acatch
block (or in the.catch()
method).