nflow
Version:
event/data/control flow
256 lines (241 loc) • 8.13 kB
JavaScript
import { ERRORS
, STATUS
, DIRECTION
, DIRECTION_BITMASK
, UNSET } from '../consts'
import { merge
, detach
, assert
, isFlow
, getLocalName
, dispatchInternalEvent } from '../utils'
import routes from '../routes'
export default (flow) => {
/**
* return the current status of the node
* @readonly
* @return {STATUS} The current status of the node
*/
flow.status = (...args) => {
assert(args.length
, ERRORS.invalidStatus)
if (flow.cancel.value) return STATUS.CANCELLED
if (flow.dispose.value) return STATUS.DISPOSED
return flow.status.value
}
/**
* @internal
* @param {STATUS} The new status of the node
*/
flow.status.set = (status) => {
if (status === flow.status.value) return
!flow.name.isInternal && dispatchInternalEvent(flow, 'status', status, flow.status.value, true)
flow.status.value = status
}
flow.status.value = STATUS.IDLE
merge(STATUS, flow.status)
/**
* Set the traversal direction of the node.
* The direction defines how the node traverses the event tree when it's emitted.
* @param {DIRECTION} [direction] The traversal direction
* @return {flow} The current flow node
*/
flow.direction = (direction = UNSET) => {
if (direction === UNSET) return flow.direction.value
let oldDirection = flow.direction.value
flow.direction.value = direction
dispatchInternalEvent(flow, 'direction', direction, oldDirection)
return flow
}
flow.direction.value = flow.create.defaults.direction
merge(DIRECTION, flow.direction)
/**
* Emit a node to traverse the flow tree.
*
* In nflow `nodes` and `events` are the same type of objects.
* An event is a node that gets detached from the parent, traverses the tree (see {@tutorial propagation}) and gets delivered to all listeners (see {@tutorial namespacing}).
* > The `.emit` API has **3 distinct behaviours**:
* ```js
* foo.emit() // turns foo into an event and emits it
* foo.emit('bar') // creates bar and emits it on foo
* foo.emit(anotherNode) // reparents anotherNode to foo and emits it
* ```
* Essentially, the following two operations are the same:
* ```js
* foo.emit('bar')
* foo.create('bar').emit()
* ```
*
* #### Listener Context
* Listeners are always invoked in the context of the emitted event:
* ```js
* .on('price-update', function(d){
* this // refers to the emitted event
* this.data() // === d
* this.name() // === 'price-update'
* })
* ```
*
* Since **events are also flow objects**, you can dispatch further events on them! ({@tutorial event-chain})
* @param {String} [name] The name of the event
* @param {...object} [data] optional data stored on the event
* @returns {flow} the emitted event
* @tutorial event-chain
* @tutorial propagation
* @tutorial namespacing
* @emits 'flow.emit'
* @emits 'flow.parent.emit'
* @emits 'flow.children.emit'
* @emits 'flow.emitted'
* @emits 'flow.parent.emitted'
* @emits 'flow.children.emitted'
*/
flow.emit = (name = UNSET, ...args) => {
return emit(name, args)
}
flow.emit.recipients = []
flow.emit.recipientsMap = {}
createEmitAPI(flow)
/**
*
* Dispatched when a node is about to be emitted.
* @event 'flow.emit'
* @property {flow} parent - The emitter, ie. the parent of the emitted node.
* @property {flow} flow - the emitted node.
* @see flow.parent
*/
/**
*
* Dispatched when one of the node's parents is about to be emitted.
* @event 'flow.parent.emit'
* @property {flow} parent - The emitter, ie. the parent of the emitted node.
* @property {flow} flow - the emitted node.
* @see flow.emit
*/
/**
*
* Dispatched when ove of the node's children(recursive) is about to be emitted.
* @event 'flow.children.emit'
* @property {flow} parent - The emitter, ie. the parent of the emitted node.
* @property {flow} flow - the emitted node.
* @see flow.emit
*/
/**
*
* Dispatched after a node has been emitted.
* @event 'flow.emitted'
* @property {flow} parent - The emitter, ie. the parent of the emitted node.
* @property {flow} flow - the emitted node.
* @see flow.parent
*/
/**
*
* Dispatched after one of the node's parents has been emitted.
* @event 'flow.parent.emitted'
* @property {flow} parent - The emitter, ie. the parent of the emitted node.
* @property {flow} flow - the emitted node.
* @see flow.emit
*/
/**
*
* Dispatched after one of the node's children(recursive) has been emitted.
* @event 'flow.children.emitted'
* @property {flow} parent - The emitter, ie. the parent of the emitted node.
* @property {flow} flow - the emitted node.
* @see flow.emit
*/
function emit (name = UNSET, args, direction) {
if (name === UNSET) {
// emit current flow object
detach(flow)
let p = flow.parent() || flow
direction && flow.direction(direction)
dispatchInternalEvent(p, 'emit', flow, null, true)
p.emit.route(flow)
dispatchInternalEvent(p, 'emitted', flow, null, true)
return flow
}
if (isFlow(name)) {
// 1. reparent the passed in flow object where it's emitted from
name.parent(flow)
// 2. emit the passed in flow object
detach(name)
direction && name.direction(direction)
args.length && name.data(...args)
dispatchInternalEvent(flow, 'emit', name, null, true)
flow.emit.route(name)
dispatchInternalEvent(flow, 'emitted', name, null, true)
return flow
}
// create and emit a new event
assert(typeof (name) !== 'string'
, ERRORS.invalidEventName)
var event = flow.create(name, ...args)
detach(event)
if (direction) event.direction.value = direction
dispatchInternalEvent(flow, 'emit', event, null, true)
flow.emit.route(event)
dispatchInternalEvent(flow, 'emitted', event, null, true)
return event
}
/**
* @internal
*/
flow.emit.route = (event) => {
event.stopPropagation.value = false
event.status.set(STATUS.FLOWING)
let localName = getLocalName(event.name())
let matcher = f => f.on.listenerCache[localName]
event.emit.targets = flow.emit.route[event.direction()](event, matcher)
.concat(event.emit.targets || [])
while (event.emit.targets.length) {
var destination = event.emit.targets.shift()
if (event.isCancelled()) break
if (event.propagationStopped()) break
if (destination.flow.isCancelled()) continue
notify(event, destination)
}
if (!event.isCancelled() && !event.propagationStopped()) {
event.status.set(STATUS.COMPLETED)
}
}
Object.keys(routes).forEach(route => {
flow.emit.route[route] =
flow.emit.route[route.toUpperCase()] =
routes[route]
})
function notify (flow, currentNode) {
// only deliver once per node
if (flow.emit.recipientsMap[currentNode.flow.guid.value]) return
if (isUnreachable(flow, currentNode)) return
flow.emit.recipientsMap[currentNode.flow.guid.value] = flow.direction.value
let result = currentNode.flow.on.notifyListeners(flow)
if (result) {
result.route = currentNode.route
result.direction = flow.direction.value
flow.emit.recipients.push(result)
}
}
function isUnreachable (flow, destination) {
let modifiers = Object.keys(flow.stopPropagation.modifiers)
if (!modifiers.length) return false
let routeMap = destination.route.reduce((i, route) => {
i[route.flow.guid()] |= DIRECTION_BITMASK[route.direction]
return i
}, {})
return modifiers.some(guid => {
return routeMap[guid] === flow.stopPropagation.modifiers[guid]
})
}
/*!
* create directional (eg. `flow.emit.dowsntream(...)`) API
*/
function createEmitAPI (flow) {
Object.keys(DIRECTION)
.forEach(direction => {
flow.emit[direction] =
flow.emit[direction.toLowerCase()] =
(name, ...args) => emit(name, args, direction)
})
}
}