UNPKG

integration

Version:

Enterprise integration patterns for JavaScript

889 lines (812 loc) 28 kB
/* * Copyright (c) 2012 VMware, Inc. All Rights Reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ (function (define) { 'use strict'; var undef; /** * Global integration bus including all core components and facilities. * Domain specific functionality is located in other modules that augment * this object. * * Creating child buses for specific tasks or modules of an application is * highly recommended. Each child bus is able to resolve channels and * handlers from its parent. Children may export components to a parent * bus to expose endpoints for a sub-flow. * * Advanced functionality may be added to all buses within the system by * adding properties to this objects 'prototype'. * * @author Scott Andrews */ define(function (require) { var broadcastDispatcher, directDispatcher, unicastDispatcher, when, busCounter; broadcastDispatcher = require('./channels/dispatchers/broadcast'); directDispatcher = require('./channels/dispatchers/direct'); unicastDispatcher = require('./channels/dispatchers/unicast'); when = require('when'); busCounter = counter(); /** * Create a new message * * @param {Object} payload content of the message * @param {Object} [headers] meta data for the message */ function Message(payload, headers) { this.payload = payload; this.headers = freeze(headers || {}); freeze(this); } Message.prototype = { /** * Create a new message from this message overriding certain * headers with the provided values. The current message is not * modified. * * @param {Object} [payload] payload for the new message, defaults * to the current message payload * @param {Object} declaredHeaders headers that overwrite the * current message's headers * @return {Message} a new message with the same payload and new * headers */ mixin: function mixin(payload, declaredHeaders) { var headers; if (arguments.length < 2) { declaredHeaders = payload; payload = this.payload; } declaredHeaders = declaredHeaders || {}; headers = {}; Object.keys(this.headers).forEach(function (header) { headers[header] = this.headers[header]; }, this); Object.keys(declaredHeaders).forEach(function (header) { headers[header] = declaredHeaders[header]; }, this); return new Message(payload, headers); } }; /** * Holds a reference to a channel or handler that can be resolved * later. Useful for sharing components outside of their home bus. */ function Ref(resolver) { this.resolve = resolver; } /** * @returns true if a Ref */ function isRef(ref) { return ref instanceof Ref; } /** * Create a new message bus * * @param {MessageBus} [parent] a parent message bus to extend from */ function MessageBus(parent) { var components = {}, children = [], busId = busCounter(), messageCounter = counter(); /** * @param {Function} [config] configuration helper invoked in the * context of the bus. * * @returns a new message bus who's parent is the current bus */ this.bus = function bus(config) { var messageBus = new MessageBus(this); children.push(messageBus); if (config) { config.call(messageBus, messageBus); } return messageBus; }; /** * Create a new message * * @param {Object|Message} payload the message payload * @param {Object} [declaredHeaders] the message headers * @returns the new message */ this._message = function _message(payload, declaredHeaders) { var headers; headers = {}; declaredHeaders = declaredHeaders || {}; Object.keys(declaredHeaders).forEach(function (header) { headers[header] = declaredHeaders[header]; }, this); headers.id = busId + '-' + messageCounter(); return this.isMessage(payload) ? payload.mixin(headers) : new Message(payload, headers); }; /** * Find a handler by name. If the handler is not found in the local * message bus, the parent message bus is queried. * * @param {string|Handler} name the handler name to find * @returns the found handler, undefined when not found */ this.resolveHandler = function resolveHandler(name) { var handler; if (this.isHandler(name)) { return name; } if (name in components) { handler = components[name]; if (isRef(handler)) { handler = handler.resolve(); } return this.resolveHandler(handler); } if (parent) { return parent.resolveHandler(name); } }; /** * Find a channel by name. If the channel is not found in the local * message bus, the parent message bus is queried. * * @param {string|Channel} name the channel name to find * @returns the found channel, undefined when not found */ this.resolveChannel = function resolveChannel(name) { var channel; if (this.isChannel(name)) { return name; } if (name in components) { channel = components[name]; if (isRef(channel)) { channel = channel.resolve(); } return this.resolveChannel(channel); } if (parent) { return parent.resolveChannel(name); } }; /** * Create an alias for a handler or channel * * @param {string} name the alias * @param {string|Channel|Handler} component the item to register */ this.alias = function alias(name, component) { if (!(this.resolveChannel(component) || this.resolveHandler(component) || isRef(component))) { throw new Error('Unable to alias: handler or channel is required'); } if (!name) { throw new Error('Unable to alias: name is required'); } if (name in components) { throw new Error('Unable to alias: the name \'' + name + '\' is in use'); } components[name] = component; }; /** * Dead letter channel that handles messages that were sent, but * have no handlers. */ this.deadLetterChannel = this._channel('deadLetterChannel', broadcastDispatcher()); /** * Invalid message channel that handles messages when an error was * encountered sending the message. */ this.invalidMessageChannel = this._channel('invalidMessageChannel', broadcastDispatcher()); if (parent) { // share messages with parent's channels this.deadLetterChannel.subscribe(this.bridge(parent.deadLetterChannel)); this.invalidMessageChannel.subscribe(this.bridge(parent.invalidMessageChannel)); /** * Make a channel available to the parent bus. Useful for * defining contained sub flows that provide entry and exit * points. * * @param {string} [name] the name to export as * @param {string|Channel} channel the channel to export */ this.exportChannel = function exportChannel(name, channel) { if (arguments.length === 1) { channel = name; } parent.alias(name, new Ref(function () { return this.resolveChannel(channel); }.bind(this))); }; /** * Deconstructor that cleans up any lingering state that would * not be automatically garbage collected */ this.destroy = function destroy() { children.forEach(function (bus) { bus.destroy(); }); Object.keys(components).forEach(function (name) { var component = components[name]; if (component.destroy) { component.destroy(); } delete components[name]; }, this); this.deadLetterChannel.destroy(); this.invalidMessageChannel.destroy(); }; } } MessageBus.prototype = { /** * @returns true if the object is a message */ isMessage: function isMessage(message) { return message instanceof Message; }, /** * @returns true if the object can handle messages */ isHandler: function isHandler(handler) { return handler && typeof handler.handle === 'function'; }, /** * @returns true if the object can send messages */ isChannel: function isChannel(channel) { return channel && typeof channel.send === 'function'; }, /** * @returns true is the object is a message bus */ isBus: function isBus(bus) { return bus instanceof MessageBus; }, /** * Create a new channel to pass messages * * @param {string} [name] the name to register this channel under * @param {Dispatcher} dispatcher dispatching strategy for this * channel * @returns {Channel} a new channel */ _channel: function _channel(name, dispatcher) { var taps, channel; channel = { send: function send(message) { if (taps) { try { taps.dispatch(message, this.resolveHandler.bind(this)); } catch (e) { // squelch, wiretaps must never interfere with normal operation } } try { if (!dispatcher.dispatch(message, this.resolveHandler.bind(this))) { if (channel !== this.deadLetterChannel) { this.send(this.deadLetterChannel, message); } } } catch (e) { if (channel !== this.invalidMessageChannel) { this.send(this.invalidMessageChannel, message, { error: e }); } } }.bind(this), tap: function tap(handler) { if (!taps) { taps = broadcastDispatcher(); } taps.subscribe(handler); }, untap: function untap(handler) { if (!taps) { return; } taps.unsubscribe(handler); } }; Object.keys(dispatcher.channelMixins || {}).forEach(function (prop) { channel[prop] = dispatcher.channelMixins[prop]; }); channel.destroy = function destroy() { if (taps) { taps.destroy(); } if (dispatcher.destroy) { dispatcher.destroy(); } }; if (name) { this.alias(name, channel); } return channel; }, /** * Create a new handler * * @param {string} [name] the name to register this handler under * @param {Function} transform function to transform the message * @param {string|Channel} [outputChannel] where to forward the * handled message * @param {string|Channel} [inputChannel] channel to receive * messages from * @param {string|Channel} [errorChannel] where to forward the * message when an error occurs * @returns a new handler */ _handler: function _handler(name, transform, outputChannel, inputChannel, errorChannel) { var handler = { handle: function handle(message, outputChannelOverride) { var payload, nextOutput, nextError; try { nextOutput = outputChannelOverride || outputChannel || message.headers.replyChannel; nextError = errorChannel || message.headers.errorChannel; payload = transform.call(this, message, nextOutput, nextError); if (payload && nextOutput) { this.send(nextOutput, payload, message.headers); } } catch (e) { if (nextError) { this.send(nextError, message, { error: e }); } else { throw e; } } }.bind(this) }; if (name) { this.alias(name, handler); } if (inputChannel && this.subscribe) { this.subscribe(inputChannel, handler); } return handler; }, /** * Create a unicast channel. Messages are load balanced between * each subscriber. Only one handler receives a copy of each * message sent to this channel. * * @param {string} [name] the name to register this channel under * @param {Function} [loadBalancer] load balancer * @returns the channel */ channel: optionalName(function channel(name, loadBalancer) { return this._channel(name, unicastDispatcher(loadBalancer)); }), /** * Subscribe a handler to a channel. The channel must be * subscribable * * @param {string|Channel} from the publishing channel * @param {string|Handler} to the consuming handler */ subscribe: function subscribe(from, to) { this.resolveChannel(from).subscribe(to); }, /** * Unsubscribe a handler from a channel. The channel must be * subscribable * * @param {string|Channel} from the publishing channel * @param {string|Handler} to the consuming handler */ unsubscribe: function unsubscribe(from, to) { this.resolveChannel(from).unsubscribe(to); }, /** * Wire tap a channel. The channel must be tappable * * @param {string|Channel} channel the channel to tap * @param {string|Handler} handler the receiver of tapped messages */ tap: function tap(channel, handler) { this.resolveChannel(channel).tap(handler); }, /** * Remove a wire tap from a channel. The channel must be tappable * * @param {string|Channel} channel the channel to untap * @param {string|Handler} handler the receiver of tapped messages */ untap: function tap(channel, handler) { this.resolveChannel(channel).untap(handler); }, /** * Create and send a message to a channel * * @param {string|Channel} channel the channel to sent the message to * @param {Object|Message} payload the message to send * @param {Object} [headers] headers for the message */ send: function send(channel, payload, headers) { this.resolveChannel(channel).send(this._message(payload, headers)); }, /** * Handler that sends messages directly to the target channel * * @param {string} [name] the name to register the forward as * @param {string|Channel} target the channel to forward to */ bridge: function bridge(name, target) { // optionalName won't work since target may be a string if (arguments.length < 2) { target = name; name = ''; } return this._handler(name, this.utils.noop, target); }, /** * Forwards messages from one channel directly to another * * @param {string|Channel} from source channel * @param {string|Channel} to recipient channel */ forward: function forward(from, to) { return this._handler(undef, this.utils.noop, to, from); }, /** * Treat an array of handlers as if they are a single handler. Each * handler is executed in order with the message from the previous * handler in the pipeline. * * @param {string} [name] the name to register the pipeline as * @param {Array[Handler]} handlers array of handlers * @param {string|Channel} [opts.output] the channel to forward * messages to * @param {string|Channel} [opts.input] the channel to receive * message from * @param {string|Channel} [opts.error] channel to receive errors * @returns the pipeline */ chain: optionalName(function chain(name, handlers, opts) { opts = opts || {}; return this._handler(name, function (message) { handlers.map(this.resolveHandler, this).forEach(function (handler) { if (!message) { return; } var m = message; // unset 'message' forcing it to be handled in order to continue in the chain message = undef; handler.handle(m, { send: function send(m) { message = m; return true; } }); }, this); return message; }, opts.output, opts.input, opts.error); }), /** * Transform messages sent to this channel * * @param {string} [name] the name to register the transform as * @param {Function} translator transform function, invoked with * message payload and message headers as args, a new payload * must be returned. * @param {string|Channel} [opts.output] the channel to forward * transformed messages to * @param {string|Channel} [opts.input] the channel to receive * message from * @param {string|Channel} [opts.error] channel to receive errors * @returns the transform */ transform: optionalName(function transform(name, translator, opts) { opts = opts || {}; return this._handler(name, function (message) { return message.mixin(translator.call(undef, message.payload, message.headers), {}); }, opts.output, opts.input, opts.error); }), /** * Filter messages based on some criteria. Abandoned messages may * be forward to a discard channel if defined. * * @param {string} [name] the name to register the filter as * @param {Function} rule filter function, invoked with message * payload and message headers as args. If true is returned, the * message is forwarded, otherwise it is discarded. * @param {string|Channel} [opts.output] the channel to forward * messages to * @param {string|Channel} [opts.discard] channel to handle * discarded messages * @param {string|Channel} [opts.input] the channel to receive * message from * @param {string|Channel} [opts.error] channel to receive errors * @returns the filter */ filter: optionalName(function filter(name, rule, opts) { opts = opts || {}; return this._handler(name, function (message) { if (rule.call(this, message.payload, message.headers)) { return message; } else if (opts.discard) { this.send(opts.discard, message, { discardedBy: name }); } }, opts.output, opts.input, opts.error); }), /** * Route messages to handlers defined by the rule. The rule may * return 0..n recipient channels. * @param {string} [name] the name to register the router as * @param {Function} rule function that accepts the message and * defined routes returning channels to route the message to * @param {Object|Array} [opts.routes] channel aliases for the * router * @param {string|Channel} [opts.input] the channel to receive * message from * @param {string|Channel} [opts.error] channel to receive errors * @returns the router */ router: optionalName(function router(name, rule, opts) { opts = opts || {}; return this._handler(name, function (message) { var recipients = rule.call(this, message, opts.routes); if (!(recipients instanceof Array)) { recipients = [recipients]; } opts.routes = opts.routes || {}; recipients.forEach(function (recipient) { this.send(recipient in opts.routes ? opts.routes[recipient] : recipient, message); }, this); }, this.noopChannel, opts.input, opts.error); }), /** * Split one message into many * * @param {string} [name] the name to register the splitter as * @param {Function} rule function that accepts a message and * returns an array of messages * @param {string|Channel} [opts.output] the channel to forward * split messages to * @param {string|Channel} [opts.input] the channel to receive * message from * @param {string|Channel} [opts.error] channel to receive errors * @returns the splitter */ splitter: optionalName(function splitter(name, rule, opts) { opts = opts || {}; return this._handler(name, function (message) { rule.call(this, message).forEach(function (splitMessage, index, splitMessages) { this.send(opts.output, splitMessage, { sequenceNumber: index, sequenceSize: splitMessages.length, correlationId: message.headers.id }); }, this); }, this.noopChannel, opts.input, opts.error); }), /** * Aggregate multiple messages into a single message * * @param {string} [name] the name to register the aggregator as * @param {Function} strategy function that accepts a message and * a callback function. When the strategy determines a new * message is ready, it must invoke the callback function with * that message. * @param {string|Channel} [opts.output] the channel to forward * aggregated messages to * @param {string|Channel} [opts.input] the channel to receive * message from * @param {string|Channel} [opts.error] channel to receive errors * @returns the aggregator */ aggregator: optionalName(function aggregator(name, correlator, opts) { opts = opts || {}; var release = function (payload, headers) { this.send(opts.output, payload, headers); }.bind(this); return this._handler(name, function (message) { correlator.call(this, message, release); }, this.noopChannel, opts.input, opts.error); }), /** * Log messages at the desired level * * @param {string} [name] the name to register the logger as * @param {Console} [opts.console=console] the console to log with * @param {string} [opts.level='log'] the console level to log at, * defaults to 'log' * @param {Object|string} [opts.prefix] value included with the * logged message * @param {string|Channel} [opts.tap] the channel to log messages * from * @returns the logger */ logger: optionalName(function logger(name, opts) { opts = opts || {}; opts.console = opts.console || console; opts.level = opts.level || 'log'; var handler, channel; handler = this._handler(name, function (message) { var output = 'prefix' in opts ? [opts.prefix, message] : [message]; opts.console[opts.level].apply(opts.console, output); }, this.noopChannel); channel = this.resolveChannel(opts.tap); if (channel && channel.tap) { channel.tap(handler); } return handler; }), /** * Post messages to a channel that can be invoked as a JS function. * The first argument of the returned function becomes the message * payload. * * @param {string|Channel} output the channel to post messages to * @param {Function} [adapter] function to adapt the arguments into * a message payload. The function must return a message payload. * @returns a common function that sends messages */ inboundAdapter: function inboundAdapter(output, adapter) { var counter = this.utils.counter(); adapter = adapter || this.utils.noop; return function () { var payload = adapter.apply(arguments[0], arguments); if (payload !== undef) { this.send(output, payload, { sequenceNumber: counter() }); } }.bind(this); }, /** * Bridge a handler to a common function. The function is invoked * as messages are handled with the message payload provided as an * argument. * * @param {string} [name] the name to register the adapter as * @param {Function} func common JS function to invoke * @param {string|Channel} opts.input the channel to output * messages for * @param {string|Channel} [opts.error] channel to receive errors * @retuns {Handler} the handler for this adapter */ outboundAdapter: optionalName(function outboundAdapter(name, func, opts) { opts = opts || {}; return this._handler(name, function (message) { func.call(undef, message.payload); }, this.noopChannel, opts.input, opts.error); }), /** * Gateway between application code that expects a reply and the * message bus. Similar to an inbound adapter, however, the * returned function itself returns a promise representing the * outcome of the message. * * @param {string|Channel} output the channel to post messages to * @returns {Function} a function that when invoked places a * message on the bus that returns a promise representing the * outcome of the message */ inboundGateway: function inboundGateway(output) { return function (payload) { var defer; defer = when.defer(); this.send(output, payload, { replyChannel: this._channel(undef, directDispatcher(this.outboundAdapter(defer.resolve))), errorChannel: this._channel(undef, directDispatcher(this.outboundAdapter(defer.reject))) }); return defer.promise; }.bind(this); }, /** * Gateway out of the messaging system to a traditional service * within the application. The service may return an object, which * becomes the reply message payload, or a promise to defer a reply. * * @param {string} [name] the name to register the activator as * @param {Function} service the service to activate. Invoked with * the message payload and headers as arguments. * @param {string|Channel} [opts.output] the channel to receive * replies from the service * @param {string|Channel} [opts.input] the channel to receive * message from * @param {string|Channel} [opts.error] channel to receive errors * @returns the service activator handler */ outboundGateway: optionalName(function outboundGateway(name, service, opts) { opts = opts || {}; return this._handler(name, function (message, reply, error) { when(service.call(this, message.payload, message.headers), function (result) { this.send(reply, result, message.headers); }.bind(this), function (result) { this.send(error, result, message.headers); }.bind(this) ); }, opts.output, opts.input, opts.error); }), /** * Channel that does nothing */ noopChannel: freeze({ send: function () { return true; } }), /** * Handler that does nothing */ noopHandler: freeze({ handle: function () {} }), /** * Common helpers that are useful to other modules but not worthy * of their own module */ utils: { counter: counter, noop: function noop() { return arguments[0]; }, optionalName: optionalName } }; // make it easy for custom extensions to the MessageBus prototype MessageBus.prototype.prototype = MessageBus.prototype; return new MessageBus(); }); /** * Incrementing counter */ function counter() { /*jshint plusplus:false */ var count = 0; return function increment() { return count++; }; } /** * Prevent modification to an object if supported on the platform * * @param obj the object to freeze * @returns the frozen object */ function freeze(obj) { return Object.freeze ? Object.freeze(obj) : obj; } /** * Detect if the first parameter is a name. If the param is omitted, * arguments are normalized and passed to the wrapped function. * Behavior is undesirable if the second argument can be a string. * * @param {Function} func function who's first parameter is a name that * may be omitted. */ function optionalName(func) { return function (name) { var args = Array.prototype.slice.call(arguments); if (typeof name !== 'string') { // use empty string instead of undef so that this optionalName helpers can be stacked args.unshift(''); } return func.apply(this, args); }; } }( typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); } // Boilerplate for AMD and Node ));