UNPKG

rhea

Version:

reactive AMQP 1.0 library

392 lines (356 loc) 12.1 kB
/* * Copyright 2015 Red Hat Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; var frames = require('./frames.js'); var log = require('./log.js'); var message = require('./message.js'); var terminus = require('./terminus.js'); var EndpointState = require('./endpoint.js'); var FlowController = function (window) { this.window = window; }; FlowController.prototype.update = function (context) { var delta = this.window - context.receiver.credit; if (delta >= (this.window/4)) { context.receiver.flow(delta); } }; function auto_settle(context) { context.delivery.settled = true; } function auto_accept(context) { context.delivery.update(undefined, message.accepted().described()); } function LinkError(message, condition, link) { Error.call(this); Error.captureStackTrace(this, this.constructor); this.message = message; this.condition = condition; this.description = message; Object.defineProperty(this, 'link', { value: link }); } require('util').inherits(LinkError, Error); var EventEmitter = require('events').EventEmitter; var link = Object.create(EventEmitter.prototype); link.dispatch = function(name) { log.events('[%s] Link got event: %s', this.connection.options.id, name); EventEmitter.prototype.emit.apply(this.observers, arguments); if (this.listeners(name).length) { EventEmitter.prototype.emit.apply(this, arguments); return true; } else { return this.session.dispatch.apply(this.session, arguments); } }; link.set_source = function (fields) { this.local.attach.source = terminus.source(fields).described(); }; link.set_target = function (fields) { this.local.attach.target = terminus.target(fields).described(); }; link.attach = function () { if (this.state.open()) { this.connection._register(); } }; link.open = link.attach; link.detach = function () { this.local.detach.closed = false; if (this.state.close()) { this.connection._register(); } }; link.close = function(error) { if (error) this.local.detach.error = error; this.local.detach.closed = true; if (this.state.close()) { this.connection._register(); } }; /** * This forcibly removes the link from the parent session. It should * not be called for a link on an active session/connection, where * close() should be used instead. */ link.remove = function() { this.session.remove_link(this); }; link.is_open = function () { return this.session.is_open() && this.state.is_open(); }; link.is_remote_open = function () { return this.session.is_remote_open() && this.state.remote_open; }; link.is_itself_closed = function() { return this.state.is_closed(); }; link.is_closed = function () { return this.session.is_closed() || this.is_itself_closed(); }; link._process = function () { do { if (this.state.need_open()) { this.session.output(this.local.attach); } if (this.issue_flow && this.state.local_open) { this.session._write_flow(this); this.issue_flow = false; } if (this.state.need_close()) { this.session.output(this.local.detach); } } while (!this.state.has_settled()); }; link.on_attach = function (frame) { if (this.state.remote_opened()) { if (!this.remote.handle) { this.remote.handle = frame.handle; } frame.performative.source = terminus.unwrap(frame.performative.source); frame.performative.target = terminus.unwrap(frame.performative.target); this.remote.attach = frame.performative; this.open(); this.dispatch(this.is_receiver() ? 'receiver_open' : 'sender_open', this._context()); } else { throw Error('Attach already received'); } }; link.prefix_event = function (event) { return (this.local.attach.role ? 'receiver_' : 'sender_') + event; }; link.on_detach = function (frame) { if (this.state.remote_closed()) { if (this._incomplete) { this._incomplete.settled = true; } this.remote.detach = frame.performative; var error = this.remote.detach.error; if (error) { var handled = this.dispatch(this.prefix_event('error'), this._context()); handled = this.dispatch(this.prefix_event('close'), this._context()) || handled; if (!handled) { EventEmitter.prototype.emit.call(this.connection.container, 'error', new LinkError(error.description, error.condition, this)); } } else { this.dispatch(this.prefix_event('close'), this._context()); } var self = this; var token = this.state.mark(); process.nextTick(function () { if (self.state.marker === token) { self.close(); process.nextTick(function () { self.remove(); }); } }); } else { throw Error('Detach already received'); } }; function is_internal(name) { switch (name) { case 'name': case 'handle': case 'role': case 'initial_delivery_count': return true; default: return false; } } var aliases = [ 'snd_settle_mode', 'rcv_settle_mode', 'source', 'target', 'max_message_size', 'offered_capabilities', 'desired_capabilities', 'properties' ]; function remote_property_shortcut(name) { return function() { return this.remote.attach ? this.remote.attach[name] : undefined; }; } link.init = function (session, name, local_handle, opts, is_receiver) { this.session = session; this.connection = session.connection; this.name = name; this.options = opts === undefined ? {} : opts; this.state = new EndpointState(); this.issue_flow = false; this.local = {'handle': local_handle}; this.local.attach = frames.attach({'handle':local_handle,'name':name, role:is_receiver}); for (var field in this.local.attach) { if (!is_internal(field) && this.options[field] !== undefined) { this.local.attach[field] = this.options[field]; } } this.local.detach = frames.detach({'handle':local_handle, 'closed':true}); this.remote = {'handle':undefined}; this.delivery_count = 0; this.credit = 0; this.observers = new EventEmitter(); var self = this; aliases.forEach(function (alias) { Object.defineProperty(self, alias, { get: remote_property_shortcut(alias) }); }); Object.defineProperty(this, 'error', { get: function() { return this.remote.detach ? this.remote.detach.error : undefined; }}); }; link._disconnect = function() { this.state.disconnected(); if (!this.state.was_open) { this.remove(); } }; link._reconnect = function() { this.state.reconnect(); this.remote = {'handle':undefined}; this.delivery_count = 0; this.credit = 0; }; link.has_credit = function () { return this.credit > 0; }; link.is_receiver = function () { return this.local.attach.role; }; link.is_sender = function () { return !this.is_receiver(); }; link._context = function (c) { var context = c ? c : {}; if (this.is_receiver()) { context.receiver = this; } else { context.sender = this; } return this.session._context(context); }; link.get_option = function (name, default_value) { if (this.options[name] !== undefined) return this.options[name]; else return this.session.get_option(name, default_value); }; var Sender = function (session, name, local_handle, opts) { this.init(session, name, local_handle, opts, false); this._draining = false; this._drained = false; this.local.attach.initial_delivery_count = 0; this.tag = 0; if (this.get_option('autosettle', true)) { this.observers.on('settled', auto_settle); } var sender = this; if (this.get_option('treat_modified_as_released', true)) { this.observers.on('modified', function (context) { sender.dispatch('released', context); }); } }; Sender.prototype = Object.create(link); Sender.prototype.constructor = Sender; Sender.prototype._get_drain = function () { if (this._draining && this._drained && this.credit) { while (this.credit) { ++this.delivery_count; --this.credit; } return true; } else { return false; } }; Sender.prototype.set_drained = function (drained) { this._drained = drained; if (this._draining && this._drained) { this.issue_flow = true; } }; Sender.prototype.next_tag = function () { return Buffer.from(new String(this.tag++)); }; Sender.prototype.sendable = function () { return Boolean(this.credit && this.session.outgoing.available()); }; Sender.prototype.on_flow = function (frame) { var flow = frame.performative; this.credit = flow.delivery_count + flow.link_credit - this.delivery_count; this._draining = flow.drain; this._drained = this.credit > 0; if (this.is_open()) { this.dispatch('sender_flow', this._context()); if (this._draining) { this.dispatch('sender_draining', this._context()); } if (this.sendable()) { this.dispatch('sendable', this._context()); } } }; Sender.prototype.on_transfer = function () { throw Error('got transfer on sending link'); }; Sender.prototype.send = function (msg, tag, format) { var payload = format === undefined ? message.encode(msg) : msg; var delivery = this.session.send(this, tag ? tag : this.next_tag(), payload, format); if (this.local.attach.snd_settle_mode === 1) { delivery.settled = true; } return delivery; }; var Receiver = function (session, name, local_handle, opts) { this.init(session, name, local_handle, opts, true); this.drain = false; this.set_credit_window(this.get_option('credit_window', 1000)); if (this.get_option('autoaccept', true)) { this.observers.on('message', auto_accept); } if (this.local.attach.rcv_settle_mode === 1 && this.get_option('autosettle', true)) { this.observers.on('settled', auto_settle); } }; Receiver.prototype = Object.create(link); Receiver.prototype.constructor = Receiver; Receiver.prototype.on_flow = function (frame) { this.dispatch('receiver_flow', this._context()); if (frame.performative.drain) { this.credit = frame.performative.link_credit; this.delivery_count = frame.performative.delivery_count; if (frame.performative.link_credit > 0) console.error('ERROR: received flow with drain set, but non zero credit'); else this.dispatch('receiver_drained', this._context()); } }; Receiver.prototype.flow = function(credit) { if (credit > 0) { this.credit += credit; this.issue_flow = true; this.connection._register(); } }; Receiver.prototype.drain_credit = function() { this.drain = true; this.issue_flow = true; this.connection._register(); }; Receiver.prototype.add_credit = Receiver.prototype.flow;//alias Receiver.prototype._get_drain = function () { return this.drain; }; Receiver.prototype.set_credit_window = function(credit_window) { if (credit_window > 0) { var flow_controller = new FlowController(credit_window); var listener = flow_controller.update.bind(flow_controller); this.observers.on('message', listener); this.observers.on('receiver_open', listener); } }; module.exports = {'Sender': Sender, 'Receiver':Receiver};