UNPKG

flownote

Version:

FlowNote lets developers create, organize, and reason about event-oriented applications with a simple flow-based language.

769 lines (671 loc) 20.9 kB
import Flow from './flow' import Event from './event' import Compiler from '../compiler/index' import Action from './action' const querystring = require('qs') const executeCode = require('./utils/vm') const IdGenerator = require('./utils/idGenerator') const EventQueue = require('./eventQueue') // const Delegate = require('./delegate') const Request = require('./request') const CommonClass = require('./utils/commonClass') const Log = require('./utils/log') const stringify = require('fast-safe-stringify') const NotFoundError = require('./errors/notFoundError') const idGenerator = IdGenerator() const noop = () => {} class Application extends CommonClass { /** * [constructor description] * @param {[type]} id [description] * @param {[type]} name [description] * @param {[type]} config [description] * @param {[type]} flows [description] * @return {[type]} [description] */ constructor (id, name, config, publicFlow, flows, actionGenerators, inputPipe, outputPipe, errorPipe, eventQueue) { super() this.inputPipe = inputPipe || process.stdin this.outputPipe = outputPipe || process.stdout this.errorPipe = errorPipe || process.stderr this.fromJSON({ id: id || idGenerator(), name: name || 'Unnamed', config: Object.assign({ logLevel: 2, silent: true }, config || {}), // publicFlow: publicFlow || undefined, flows: flows || [], actionGenerators: actionGenerators || [], eventQueue: eventQueue || { queue: { type: 'memory', pendingEvents: [] } } }) this.log.info('FlowNote running...') } /** * [onHttpRequest description] * @param {[type]} req [description] * @param {[type]} res [description] * @return {[type]} [description] */ httpRequestHandler () { const application = this return async function (req, res) { const parts = req.url.split('?') let result // Assume GET or DELETE is the HTTP method, so extract the params from the URL let params = parts[1] if (req.method === 'POST' || req.method === 'PUT') { // Extract the params from the body of POST/PUT params = JSON.parse(await new Promise(resolve => { if (req.method === 'POST') { let body = '' req.on('data', chunk => { body += chunk.toString() // convert Buffer to string }) req.on('end', () => { resolve(body) }) } })) } try { result = await application.request(req.method, parts[0], params) res.writeHead(200, { 'Content-Type': 'text/plain' }) } catch (e) { result = { error: e.message } if (e instanceof NotFoundError) { res.writeHead(404, { 'Content-Type': 'text/plain' }) } else { res.writeHead(500, { 'Content-Type': 'text/plain' }) } } res.end(JSON.stringify(result)) } } /** * [listen description] * @return {[type]} [description] */ listen () { this.log.debug('Listening') this.log.debug(this.listening) // Prepare Readable stream handling for the inputPipe if (!this.listening) { // Master this.inputPipe.setEncoding('utf8') this.inputPipe.on('readable', () => { // Gather a chunk of input let chunk while ((chunk = this.inputPipe.read()) !== null) { this.onInput(chunk) } }) // Register the StdIn end callback this.inputPipe.on('end', this.onShutdown) this.listening = true /* if (cluster.isMaster) { this.log.debug(`Starting workers...`) const delegatePromise = new Promise(resolve => { let completed = 0 cluster.on('online', (worker) => { this.log.debug(`Worker ${worker.id} ready`) completed += 1 if (completed === numCPUs) { resolve() } }) this.delegate.startMaster(function masterHandler (message) { switch (message._event) {} }) }) delegatePromise.then(() => { this.listening = true this.log.debug(`Application ready`) }) return delegatePromise } else { // Worker this.outputPipe.write('Worker listen') this.delegate.startWorker(async message => { switch (message._event) { case 'executeAction': this.application.debug('Got message', message) return this.executeAction(message.method, message.path, message.params) } }) this.listening = true } */ } } /** * [unlisten description] * @return {[type]} [description] */ unlisten () { if (this.listening) { this.log.debug(`Application no longer listening...`) this.inputPipe.removeAllListeners() // this.log.debug(`Stopping application delegators...`) // this.delegate.stopMaster() this.listening = false } } /** * [onEvent description] * @param {Function} callback [description] * @return {[type]} [description] */ setOnEvent (callback) { this.onEvent = callback || noop } /** * [setOnInput description] * @param {Function} callback [description] */ setOnInput (callback) { this.onInput = callback || noop } /** * [setOnInput description] * @param {Function} callback [description] */ setOnShutdown (callback) { this.onShutdown = callback || noop } /** * [getConfig description] * @param {[type]} key [description] * @return {[type]} [description] */ getConfig (key) { return this.config[key] } /** * [setConfig description] * @param {[type]} key [description] * @param {[type]} value [description] */ setConfig (key, value) { this.config[key] = value return this.config[key] } /** * [toJSON description] * @return {[type]} [description] */ toJSON () { const actionGenerators = [] for (var i = 0, len = this.actionGenerators.length; i < len; i++) { actionGenerators.push(this.actionGenerators[i].toString()) } return { id: this.id, name: this.name, config: this.config, // publicFlow: this.publicFlow.asFlattened(), flows: this.flows, actionGenerators, eventQueue: this.eventQueue } } /** * [registerQueueType description] * @param {[type]} name [description] * @param {[type]} queueType [description] * @return {[type]} [description] */ registerQueueType (name, queueType) { this.EventQueue.registerQueueType(name, queueType) } /** * [fromJSON description] * @param {[type]} flattened [description] * @return {[type]} [description] */ fromJSON (flattened) { let result var i, len if (typeof flattened === 'string') { result = this.loadFlattened(flattened) } else if (flattened instanceof Object && !(flattened instanceof Array)) { result = flattened } else { throw new Error(`Expected Application JSON to be a string or an object, but got a ${typeof json} instead`) } /* if (result.flows.length === 0 && result.publicFlow) { result.flows.push(result.publicFlow) } */ this.id = result.id this.name = result.name this.config = result.config this.flows = [] this.actions = new Map() this.activeRequests = new Map() this.listeners = [] this.onEvent = noop this.onInput = noop this.onShutdown = noop this.listening = false this.nodeAliases = new Map() this.actionGenerators = [] this.pendingSteps = [] // this.delegate = new Delegate(this) /* if (result.publicFlow instanceof Flow) { this.setPublicFlow(this.publicFlow) } else if (result.publicFlow instanceof Object || typeof result.publicFlow === 'string') { this.setPublicFlow(new Flow(this).fromJSON(result.publicFlow)) } */ for (i = 0, len = result.flows.length; i < len; i++) { /* if (result.flows[i].id === result.publicFlow.id) { continue } */ if (result.flows[i] instanceof Flow) { result.flows[i].application = this this.registerFlow(result.flows[i]) } else if (result.flows[i] instanceof Object) { this.registerFlow(new Flow(this).fromJSON(result.flows[i])) } } // Register Action Generators and their Actions for (i = 0, len = result.actionGenerators.length; i < len; i++) { this.registerActionGenerator(result.actionGenerators[i]) } if (result.eventQueue instanceof EventQueue) { this.eventQueue = result.eventQueue this.eventQueue.application = this } else if (result.eventQueue instanceof Object) { this.eventQueue = new EventQueue(this).fromJSON(result.eventQueue) } this.log = new Log(this.id, 'Application', this.name, this.config.logLevel, this.outputPipe, this.errorPipe) return this } /** * [setPublicFlow description] * @param {[type]} flow [description] */ setPublicFlow (flow) { /* this.log.debug(`Connecting ${flow.name}:${flow.id} flow to ${this.name} application`) this.publicFlow = flow if (this.flows.indexOf(flow) === -1) { // Register the flow if it is the first flow this.registerFlow(flow) } */ } /** * [registerActionGenerator description] * @param {[type]} generator [description] * @return {[type]} [description] */ registerActionGenerator (generator) { const actionMethod = executeCode(generator) this.actionGenerators.push(actionMethod) let actions = [] actions = actionMethod.call(this, require) actions.forEach(action => { this.registerAction(action.name, action) }) } /** * [setPendingStep description] * @param {[type]} stepId [description] */ setPendingStep (stepId) { // @TODO Detect pending steps that overlap and schedule them for connection once everything is done at the app level if (this.pendingSteps.indexOf(stepId) === -1) { this.pendingSteps.push(stepId) } } /** * [hasPendingStep description] * @param {[type]} stepId [description] * @return {Boolean} [description] */ isPendingStep (stepId) { return this.pendingSteps.indexOf(stepId) > -1 } /** * [removePendingStep description] * @param {[type]} stepId [description] * @return {Boolean} [description] */ removePendingStep (stepId) { const index = this.pendingSteps.indexOf(stepId) if (index > -1) { this.pendingSteps.splice(index, 1) } } /** * [dispatch description] * @param {[type]} source [description] * @param {[type]} eventName [description] * @param {[type]} event [description] * @param {[type]} destination [description] * @return {[type]} [description] */ dispatch (type, request, flow, from, retries = 0, error) { this.log.debug(`Dispatch starts from ${from.name} with type ${type}`) if (type === 'RetryChannel') { // Dealing with a retry, so rollback the request state request.rollbackChanges(from.to.id) } if (from.to) { if (from.to instanceof Array) { // Dealing with a Node or Milestone let dispatched = false for (var channel = 0, len = from.to.length; channel < len; channel++) { if (from.to[channel].accepts.indexOf(type) > -1) { this.log.debug(`... and node leads to ${from.to[channel].name}`) this.log.debug(`Dispatching ${type} to ${from.to[channel].name}`) const event = new Event(this, undefined, type, request, from.to[channel], flow, retries) this.eventQueue.push(event) dispatched = true } } if (!dispatched) { if (error) { // Throw an error if an Error channel was not found this.emit('Flow.end', flow, request, error) } else { this.emit('Flow.end', flow, request) } } } else { if (error) { // Throw an error if an Error channel was not found this.emit('Flow.end', flow, request, error) } else { // Dealing with a Channel this.log.debug(`... and channel leads to ${from.to.name}`) const event = new Event(this, undefined, type, request, from.to, flow, retries) this.eventQueue.push(event) } } } else { if (error) { // Throw an error if an Error channel was not found this.emit('Flow.end', flow, request, error) } else { // Only end up here if a node without a channel dispatches this.log.debug(`... and step leads to ${from.to.name}`) const event = new Event(this, undefined, type, request, from.to, flow, retries) this.eventQueue.push(event) } } } /** * [dispatch description] * @param {[type]} source [description] * @param {[type]} eventName [description] * @param {[type]} event [description] * @param {[type]} destination [description] * @return {[type]} [description] */ processEvents () { this.eventQueue.process() } /** * [registerAction description] * @param {[type]} name [description] * @param {[type]} method [description] * @return {[type]} [description] */ registerAction (name, action) { action.application = this this.actions.set(name, action) return action } /** * [getAction description] * @param {[type]} name [description] * @return {[type]} [description] */ getAction (name) { return this.actions.get(name) } /** * [requireAction description] * @param {[type]} actionName [description] * @param {[type]} method [description] * @return {[type]} [description] */ requireAction (actionName, method) { let action = this.getAction(actionName) if (!action) { action = this.registerAction(actionName, new Action(actionName, method, this)) } return action } /** * [getFlow description] * @param {[type]} nameOrId [description] * @return {[type]} [description] */ getFlow (nameOrId) { for (var i = 0, len = this.flows.length; i < len; i++) { if (this.flows[i].id === nameOrId || this.flows[i].name === nameOrId) { return this.flows[i] } } return false } getFlowByHttp (method, path) { for (var i = 0, len = this.flows.length; i < len; i++) { if (this.flows[i].endpointMethod === method && this.flows[i].endpointRoute === path) { return this.flows[i] } } return false } /** * [getUniqueId description] * @return {[type]} [description] */ getUniqueId () { return idGenerator() } /** * [registerFlow description] * @param {[type]} flow [description] * @return {[type]} [description] */ registerFlow (flow) { flow.application = this for (var index = 0, len = this.flows.length; index < len; index++) { // Overwrite any flow with matching unique data if (this.flows[index].id === flow.id || (this.flows[index].endpointRoute === flow.endpointRoute && this.flows[index].endpointMethod === flow.endpointMethod)) { this.flows[flow] = flow return } } this.flows.push(flow) } /** * [setNodeAlias description] * @param {[type]} name [description] * @param {[type]} node [description] */ setNodeAlias (name, node) { this.nodeAliases.set(name, node) } /** * [getNodeAlias description] * @param {[type]} name [description] * @return {[type]} [description] */ getNodeAlias (name) { return this.nodeAliases(name) } /** * [connect description] * @param {[type]} node [description] * @return {[type]} [description] */ connect (node, flow) { if (flow === undefined) { flow = this.flows[0] if (flow === undefined) { throw new RangeError('No flow specified for Application to connect to') } } this.log.debug(`Connecting ${node.name}:${node.id} to ${this.name} applicaiton`) flow.connect(node) } /** * [emit description] * @param {[type]} eventType [description] * @param {[type]} request [description] * @return {[type]} [description] */ async emit (type, source, request, error, silent = false) { for (const listener of this.listeners) { if (listener.eventType === type) { await listener.method(source, request, error) } } const data = source.id !== undefined && source.name !== undefined ? { id: source.id, name: source.name } : source const state = request.getState ? request.getState() : request const message = { type, data, state, error } this.onEvent(message) if (!(type === 'Flow.end' && request.waiting)) { if (this.config.silent === false && silent === false) { if (error) { this.errorPipe.write(stringify(message)) } else { this.outputPipe.write(stringify(message)) } } } } /** * [on description] * @param {[type]} eventType [description] * @param {[type]} method [description] * @return {[type]} [description] */ on (eventType, method) { const listener = { eventType, method } this.listeners.push(listener) return listener } /** * [off description] * @param {[type]} eventType [description] * @return {[type]} [description] */ off (listener) { const index = this.listeners.indexOf(listener) if (index > -1) { this.listeners.splice(index, 1) } } /** * [request description] * @param {[type]} method [description] * @param {[type]} path [description] * @param {[type]} params [description] * @return {[type]} [description] */ async request (method, path, params = {}) { this.log.info(`Requesting ${method} ${path} with ${params}...`) const flow = this.getFlowByHttp(method, path) if (!flow) { throw new NotFoundError(`${method} ${path} is not a valid endpoint.`) } if (typeof params === 'string') { params = querystring.parse(params) } else if (!(params instanceof Object)) { throw new TypeError(`Unknown flow request of type ${typeof params}`) } this.emit('Request.start', this, params) const request = new Request(this, params, flow, flow.to, undefined) const result = await flow.request(params, request) this.emit('Request.end', this, request) return result /* const promise = this.delegate.send('executeAction', { method, path, params }) if (promise) { return promise } else { return this.executeAction(method, path, params) } */ } /** * [execute description] * @param {[type]} method [description] * @param {[type]} path [description] * @param {[type]} params [description] * @return {[type]} [description] */ /* async executeAction (method, path, params) { this.log.debug('Starting Request...') const flow = this.getFlowByHttp(method, path) if (typeof params === 'string') { params = querystring.parse(params) } else if (!(params instanceof Object)) { throw new TypeError(`Unknown flow request of type ${typeof params}`) } this.emit('Request.start', this, params) const request = new Request(this, params, flow, flow.to) const result = await flow.request(params, request) this.emit('Request.end', this, request) return result } */ } /** * [compile description] * @param {[type]} name [description] * @param {[type]} flowFilePath [description] * @param {[type]} config [description] * @param {[type]} actions [description] * @param {[type]} eventQueue [description] * @param {[type]} inputPipe [description] * @param {[type]} outputPipe [description] * @param {[type]} errorPipe [description] * @return {[type]} [description] */ Application.compile = async function compile (name, flowFilePath, config, actions, eventQueue, inputPipe, outputPipe, errorPipe) { const compiler = new Compiler(undefined, undefined, undefined, config, actions, name) await compiler.loadSemantics() return compiler.compileFromFile(flowFilePath) } export { Application as default }