dotfiles/vscode/.vscode/extensions/randomfractalsinc.vscode-data-preview-2.3.0/node_modules/avsc/lib/services.js
Errol Sancaktar ff17c17e23 vscode
2024-06-14 09:31:58 -06:00

2421 lines
70 KiB
JavaScript

/* jshint node: true */
// TODO: Add broadcast option to client `_emitMessage`, accessible for one-way
// messages.
// TODO: Add `server.mount` method to allow combining servers. The API is as
// follows: a mounted server's (i.e. the method's argument) handlers have lower
// precedence than the original server (i.e. `this`); the mounted server's
// middlewares are only invoked for its handlers.
// TODO: Change `objectMode` client and server channel option to `encoding`
// (accepting `'netty'`, `'standard'`, and `null` or `undefined`). Perhaps also
// expose encoders (API TBD).
'use strict';
/** This module implements Avro's IPC/RPC logic. */
var types = require('./types'),
utils = require('./utils'),
events = require('events'),
stream = require('stream'),
util = require('util');
// A few convenience imports.
var Tap = utils.Tap;
var Type = types.Type;
var debug = util.debuglog('avsc:services');
var f = util.format;
// Various useful types. We instantiate options once, to share the registry.
var OPTS = {namespace: 'org.apache.avro.ipc'};
var BOOLEAN_TYPE = Type.forSchema('boolean', OPTS);
var MAP_BYTES_TYPE = Type.forSchema({type: 'map', values: 'bytes'}, OPTS);
var STRING_TYPE = Type.forSchema('string', OPTS);
var HANDSHAKE_REQUEST_TYPE = Type.forSchema({
name: 'HandshakeRequest',
type: 'record',
fields: [
{name: 'clientHash', type: {name: 'MD5', type: 'fixed', size: 16}},
{name: 'clientProtocol', type: ['null', 'string'], 'default': null},
{name: 'serverHash', type: 'MD5'},
{name: 'meta', type: ['null', MAP_BYTES_TYPE], 'default': null}
]
}, OPTS);
var HANDSHAKE_RESPONSE_TYPE = Type.forSchema({
name: 'HandshakeResponse',
type: 'record',
fields: [
{
name: 'match',
type: {
name: 'HandshakeMatch',
type: 'enum',
symbols: ['BOTH', 'CLIENT', 'NONE']
}
},
{name: 'serverProtocol', type: ['null', 'string'], 'default': null},
{name: 'serverHash', type: ['null', 'MD5'], 'default': null},
{name: 'meta', type: ['null', MAP_BYTES_TYPE], 'default': null}
]
}, OPTS);
// Prefix used to differentiate between messages when sharing a stream. This
// length should be smaller than 16. The remainder is used for disambiguating
// between concurrent messages (the current value, 16, therefore supports ~64k
// concurrent messages).
var PREFIX_LENGTH = 16;
// Internal message, used to check protocol compatibility.
var PING_MESSAGE = new Message(
'', // Empty name (invalid for other "normal" messages).
Type.forSchema({name: 'PingRequest', type: 'record', fields: []}, OPTS),
Type.forSchema(['string'], OPTS),
Type.forSchema('null', OPTS)
);
/** An Avro message, containing its request, response, etc. */
function Message(name, reqType, errType, resType, oneWay, doc) {
this.name = name;
if (!Type.isType(reqType, 'record')) {
throw new Error('invalid request type');
}
this.requestType = reqType;
if (
!Type.isType(errType, 'union') ||
!Type.isType(errType.getTypes()[0], 'string')
) {
throw new Error('invalid error type');
}
this.errorType = errType;
if (oneWay) {
if (!Type.isType(resType, 'null') || errType.getTypes().length > 1) {
throw new Error('inapplicable one-way parameter');
}
}
this.responseType = resType;
this.oneWay = !!oneWay;
this.doc = doc !== undefined ? '' + doc : undefined;
Object.freeze(this);
}
Message.forSchema = function (name, schema, opts) {
opts = opts || {};
if (!utils.isValidName(name)) {
throw new Error(f('invalid message name: %s', name));
}
// We use a record with a placeholder name here (the user might have set
// `noAnonymousTypes`, so we can't use an anonymous one). We remove it from
// the registry afterwards to avoid exposing it outside.
if (!Array.isArray(schema.request)) {
throw new Error(f('invalid message request: %s', name));
}
var recordName = f('%s.%sRequest', OPTS.namespace, utils.capitalize(name));
var reqType = Type.forSchema({
name: recordName,
type: 'record',
namespace: opts.namespace || '', // Don't leak request namespace.
fields: schema.request
}, opts);
delete opts.registry[recordName];
if (!schema.response) {
throw new Error(f('invalid message response: %s', name));
}
var resType = Type.forSchema(schema.response, opts);
if (schema.errors !== undefined && !Array.isArray(schema.errors)) {
throw new Error(f('invalid message errors: %s', name));
}
var errType = Type.forSchema(['string'].concat(schema.errors || []), opts);
var oneWay = !!schema['one-way'];
return new Message(name, reqType, errType, resType, oneWay, schema.doc);
};
Message.prototype.schema = Type.prototype.getSchema;
Message.prototype._attrs = function (opts) {
var reqSchema = this.requestType._attrs(opts);
var schema = {
request: reqSchema.fields,
response: this.responseType._attrs(opts)
};
var msgDoc = this.doc;
if (msgDoc !== undefined) {
schema.doc = msgDoc;
}
var errSchema = this.errorType._attrs(opts);
if (errSchema.length > 1) {
schema.errors = errSchema.slice(1);
}
if (this.oneWay) {
schema['one-way'] = true;
}
return schema;
};
// Deprecated.
utils.addDeprecatedGetters(
Message,
['name', 'errorType', 'requestType', 'responseType']
);
Message.prototype.isOneWay = util.deprecate(
function () { return this.oneWay; },
'use `.oneWay` directly instead of `.isOneWay()`'
);
/**
* An Avro RPC service.
*
* This constructor shouldn't be called directly, but via the
* `Service.forProtocol` method. This function performs little logic to better
* support efficient copy.
*/
function Service(name, messages, types, ptcl, server) {
if (typeof name != 'string') {
// Let's be helpful in case this class is instantiated directly.
return Service.forProtocol(name, messages);
}
this.name = name;
this._messagesByName = messages || {};
this.messages = Object.freeze(utils.objectValues(this._messagesByName));
this._typesByName = types || {};
this.types = Object.freeze(utils.objectValues(this._typesByName));
this.protocol = ptcl;
// We cache a string rather than a buffer to not retain an entire slab.
this._hashStr = utils.getHash(JSON.stringify(ptcl)).toString('binary');
this.doc = ptcl.doc ? '' + ptcl.doc : undefined;
// We add a server to each protocol for backwards-compatibility (to allow the
// use of `protocol.on`). This covers all cases except the use of the
// `strictErrors` option, which requires moving to the new API.
this._server = server || this.createServer({silent: true});
Object.freeze(this);
}
Service.Client = Client;
Service.Server = Server;
Service.compatible = function (clientSvc, serverSvc) {
try {
createReaders(clientSvc, serverSvc);
} catch (err) {
return false;
}
return true;
};
Service.forProtocol = function (ptcl, opts) {
opts = opts || {};
var name = ptcl.protocol;
if (!name) {
throw new Error('missing protocol name');
}
if (ptcl.namespace !== undefined) {
opts.namespace = ptcl.namespace;
} else {
var match = /^(.*)\.[^.]+$/.exec(name);
if (match) {
opts.namespace = match[1];
}
}
name = utils.qualify(name, opts.namespace);
if (ptcl.types) {
ptcl.types.forEach(function (obj) { Type.forSchema(obj, opts); });
}
var msgs;
if (ptcl.messages) {
msgs = {};
Object.keys(ptcl.messages).forEach(function (key) {
msgs[key] = Message.forSchema(key, ptcl.messages[key], opts);
});
}
return new Service(name, msgs, opts.registry, ptcl);
};
Service.isService = function (any) {
// Not fool-proof but likely sufficient.
return !!any && any.hasOwnProperty('_hashStr');
};
Service.prototype.createClient = function (opts) {
var client = new Client(this, opts);
process.nextTick(function () {
// We delay this processing such that we can attach handlers to the client
// before any channels get created.
if (opts && opts.server) {
// Convenience in-memory client. This can be useful to make requests
// relatively efficiently to an in-process server. Note that it is still
// is less efficient than direct method calls (because of the
// serialization, which does provide "type-safety" though).
var obj = {objectMode: true};
var pts = [new stream.PassThrough(obj), new stream.PassThrough(obj)];
opts.server.createChannel({readable: pts[0], writable: pts[1]}, obj);
client.createChannel({readable: pts[1], writable: pts[0]}, obj);
} else if (opts && opts.transport) {
// Convenience functionality for the common single channel use-case: we
// add a single channel using default options to the client.
client.createChannel(opts.transport);
}
});
return client;
};
Service.prototype.createServer = function (opts) {
return new Server(this, opts);
};
Object.defineProperty(Service.prototype, 'hash', {
enumerable: true,
get: function () { return utils.bufferFrom(this._hashStr, 'binary'); }
});
Service.prototype.message = function (name) {
return this._messagesByName[name];
};
Service.prototype.type = function (name) {
return this._typesByName[name];
};
Service.prototype.inspect = function () {
return f('<Service %j>', this.name);
};
// Deprecated methods.
utils.addDeprecatedGetters(
Service,
['message', 'messages', 'name', 'type', 'types']
);
Service.prototype.createEmitter = util.deprecate(
function (transport, opts) {
opts = opts || {};
var client = this.createClient({
cache: opts.cache,
buffering: false,
strictTypes: opts.strictErrors,
timeout: opts.timeout
});
var channel = client.createChannel(transport, opts);
forwardErrors(client, channel);
return channel;
},
'use `.createClient()` instead of `.createEmitter()`'
);
Service.prototype.createListener = util.deprecate(
function (transport, opts) {
if (opts && opts.strictErrors) {
throw new Error('use `.createServer()` to support strict errors');
}
return this._server.createChannel(transport, opts);
},
'use `.createServer().createChannel()` instead of `.createListener()`'
);
Service.prototype.emit = util.deprecate(
function (name, req, channel, cb) {
if (!channel || !this.equals(channel.client._svc$)) {
throw new Error('invalid emitter');
}
var client = channel.client;
// In case the method is overridden.
Client.prototype.emitMessage.call(client, name, req, cb && cb.bind(this));
return channel.getPending();
},
'create a client via `.createClient()` to emit messages instead of `.emit()`'
);
Service.prototype.equals = util.deprecate(
function (any) {
return (
Service.isService(any) &&
this.getFingerprint().equals(any.getFingerprint())
);
},
'equality testing is deprecated, compare the `.protocol`s instead'
);
Service.prototype.getFingerprint = util.deprecate(
function (algorithm) {
return utils.getHash(JSON.stringify(this.protocol), algorithm);
},
'use `.hash` instead of `.getFingerprint()`'
);
Service.prototype.getSchema = util.deprecate(
Type.prototype.getSchema,
'use `.protocol` instead of `.getSchema()`'
);
Service.prototype.on = util.deprecate(
function (name, handler) {
var self = this; // This protocol.
this._server.onMessage(name, function (req, cb) {
return handler.call(self, req, this.channel, cb);
});
return this;
},
'use `.createServer().onMessage()` instead of `.on()`'
);
Service.prototype.subprotocol = util.deprecate(
function () {
var parent = this._server;
var opts = {strictTypes: parent._strict, cache: parent._cache};
var server = new Server(parent.service, opts);
server._handlers = Object.create(parent._handlers);
return new Service(
this.name,
this._messagesByName,
this._typesByName,
this.protocol,
server
);
},
'`.subprotocol()` will be removed in 5.1'
);
Service.prototype._attrs = function (opts) {
var ptcl = {protocol: this.name};
var types = [];
this.types.forEach(function (t) {
if (t.getName() === undefined) {
// Don't include any unnamed types (e.g. primitives).
return;
}
var typeSchema = t._attrs(opts);
if (typeof typeSchema != 'string') {
// Some of the named types might already have been defined in a
// previous type, in this case we don't include its reference.
types.push(typeSchema);
}
});
if (types.length) {
ptcl.types = types;
}
var msgNames = Object.keys(this._messagesByName);
if (msgNames.length) {
ptcl.messages = {};
msgNames.forEach(function (name) {
ptcl.messages[name] = this._messagesByName[name]._attrs(opts);
}, this);
}
if (opts && opts.exportAttrs && this.doc !== undefined) {
ptcl.doc = this.doc;
}
return ptcl;
};
/** Function to retrieve a remote service's protocol. */
function discoverProtocol(transport, opts, cb) {
if (cb === undefined && typeof opts == 'function') {
cb = opts;
opts = undefined;
}
var svc = new Service({protocol: 'Empty'}, OPTS);
var ptclStr;
svc.createClient({timeout: opts && opts.timeout})
.createChannel(transport, {
scope: opts && opts.scope,
endWritable: typeof transport == 'function' // Stateless transports only.
}).once('handshake', function (hreq, hres) {
ptclStr = hres.serverProtocol;
this.destroy(true);
})
.once('eot', function (pending, err) {
// Stateless transports will throw an interrupted error when the
// channel is destroyed, we ignore it here.
if (err && !/interrupted/.test(err)) {
cb(err); // Likely timeout.
} else {
cb(null, JSON.parse(ptclStr));
}
});
}
/** Load-balanced message sender. */
function Client(svc, opts) {
opts = opts || {};
events.EventEmitter.call(this);
// We have to suffix all client properties to be safe, since the message
// names aren't prefixed with clients (unlike servers).
this._svc$ = svc;
this._channels$ = []; // Active channels.
this._fns$ = []; // Middleware functions.
this._buffering$ = !!opts.buffering;
this._cache$ = opts.cache || {}; // For backwards compatibility.
this._policy$ = opts.channelPolicy;
this._strict$ = !!opts.strictTypes;
this._timeout$ = utils.getOption(opts, 'timeout', 10000);
if (opts.remoteProtocols) {
insertRemoteProtocols(this._cache$, opts.remoteProtocols, svc, true);
}
this._svc$.messages.forEach(function (msg) {
this[msg.name] = this._createMessageHandler$(msg);
}, this);
}
util.inherits(Client, events.EventEmitter);
Client.prototype.activeChannels = function () {
return this._channels$.slice();
};
Client.prototype.createChannel = function (transport, opts) {
var objectMode = opts && opts.objectMode;
var channel;
if (typeof transport == 'function') {
var writableFactory;
if (objectMode) {
writableFactory = transport;
} else {
// We provide a default standard-compliant codec. This should support
// most use-cases (for example when speaking to the official Java and
// Python implementations over HTTP, or when this library is used for
// both the emitting and listening sides).
writableFactory = function (cb) {
var encoder = new FrameEncoder();
var writable = transport(function (err, readable) {
if (err) {
cb(err);
return;
}
// Since the decoder isn't exposed (so can't have an error handler
// attached, we forward any errors to the client). Since errors would
// only get thrown when the decoder flushes (if there is trailing
// data), at which point the source will have ended, there is no need
// to add re-piping logic (destination errors trigger an unpipe).
var decoder = new FrameDecoder()
.once('error', function (err) { channel.destroy(err); });
cb(null, readable.pipe(decoder));
});
if (writable) {
encoder.pipe(writable);
return encoder;
}
};
}
channel = new StatelessClientChannel(this, writableFactory, opts);
} else {
var readable, writable;
if (isStream(transport)) {
readable = writable = transport;
} else {
readable = transport.readable;
writable = transport.writable;
}
if (!objectMode) {
// To ease communication with Java servers, we provide a default codec
// compatible with Java servers' `NettyTransportCodec`'s implementation.
var decoder = new NettyDecoder();
readable = readable.pipe(decoder);
var encoder = new NettyEncoder();
encoder.pipe(writable);
writable = encoder;
}
channel = new StatefulClientChannel(this, readable, writable, opts);
if (!objectMode) {
// Since we never expose the automatically created encoder and decoder,
// we release them ourselves here when the channel ends. (Unlike for
// stateless channels, it is conceivable for the underlying stream to be
// reused afterwards).
channel.once('eot', function () {
readable.unpipe(decoder);
encoder.unpipe(writable);
});
// We also forward any (trailing data) error.
decoder.once('error', function (err) { channel.destroy(err); });
}
}
var channels = this._channels$;
channels.push(channel);
channel.once('_drain', function () {
// Remove the channel from the list of active ones.
channels.splice(channels.indexOf(this), 1);
});
// We restrict buffering to startup, otherwise we risk silently hiding errors
// (especially since channel timeouts don't apply yet).
this._buffering$ = false;
this.emit('channel', channel);
return channel;
};
Client.prototype.destroyChannels = function (opts) {
this._channels$.forEach(function (channel) {
channel.destroy(opts && opts.noWait);
});
};
Client.prototype.emitMessage = function (name, req, opts, cb) {
var msg = getExistingMessage(this._svc$, name);
var wreq = new WrappedRequest(msg, {}, req);
this._emitMessage$(wreq, opts, cb);
};
Client.prototype.remoteProtocols = function () {
return getRemoteProtocols(this._cache$, true);
};
Object.defineProperty(Client.prototype, 'service', {
enumerable: true,
get: function () { return this._svc$; }
});
Client.prototype.use = function (/* fn ... */) {
var i, l, fn;
for (i = 0, l = arguments.length; i < l; i++) {
fn = arguments[i];
this._fns$.push(fn.length < 3 ? fn(this) : fn);
}
return this;
};
Client.prototype._emitMessage$ = function (wreq, opts, cb) {
// Common logic between `client.emitMessage` and the "named" message methods.
if (!cb && typeof opts === 'function') {
cb = opts;
opts = undefined;
}
var self = this;
var channels = this._channels$;
var numChannels = channels.length;
if (!numChannels) {
if (this._buffering$) {
debug('no active client channels, buffering call');
this.once('channel', function () {
this._emitMessage$(wreq, opts, cb);
});
} else {
var err = new Error('no active channels');
process.nextTick(function () {
if (cb) {
cb.call(new CallContext(wreq._msg), err);
} else {
self.emit('error', err);
}
});
}
return;
}
opts = opts || {};
if (opts.timeout === undefined) {
opts.timeout = this._timeout$;
}
var channel;
if (numChannels === 1) {
// Common case, optimized away.
channel = channels[0];
} else if (this._policy$) {
channel = this._policy$(this._channels$.slice());
if (!channel) {
debug('policy returned no channel, skipping call');
return;
}
} else {
// Random selection, cheap and likely good enough for most use-cases.
channel = channels[Math.floor(Math.random() * numChannels)];
}
channel._emit(wreq, opts, function (err, wres) {
var ctx = this; // Call context.
var errType = ctx.message.errorType;
if (err) {
// System error, likely the message wasn't sent (or an error occurred
// while decoding the response).
if (self._strict$) {
err = errType.clone(err.message, {wrapUnions: true});
}
done(err);
return;
}
if (!wres) {
// This is a one way message.
done();
return;
}
// Message transmission succeeded, we transmit the message data; massaging
// any error strings into actual `Error` objects in non-strict mode.
err = wres.error;
if (!self._strict$) {
// Try to coerce an eventual error into more idiomatic JavaScript types:
// `undefined` becomes `null` and a remote string "system" error is
// wrapped inside an actual `Error` object.
if (err === undefined) {
err = null;
} else {
if (Type.isType(errType, 'union:unwrapped')) {
if (typeof err == 'string') {
err = new Error(err);
}
} else if (err && err.string && typeof err.string == 'string') {
err = new Error(err.string);
}
}
}
done(err, wres.response);
function done(err, res) {
if (cb) {
cb.call(ctx, err, res);
} else if (err) {
self.emit('error', err);
}
}
});
};
Client.prototype._createMessageHandler$ = function (msg) {
// jshint -W054
var fields = msg.requestType.getFields();
var names = fields.map(function (f) { return f.getName(); });
var body = 'return function ' + msg.name + '(';
if (names.length) {
body += names.join(', ') + ', ';
}
body += 'opts, cb) {\n';
body += ' var req = {';
body += names.map(function (n) { return n + ': ' + n; }).join(', ');
body += '};\n';
body += ' return this.emitMessage(\'' + msg.name + '\', req, opts, cb);\n';
body += '};';
return (new Function(body))();
};
/** Message receiver. */
function Server(svc, opts) {
opts = opts || {};
events.EventEmitter.call(this);
this.service = svc;
this._handlers = {};
this._fns = []; // Middleware functions.
this._channels = {}; // Active channels.
this._nextChannelId = 1;
this._cache = opts.cache || {}; // Deprecated.
this._defaultHandler = opts.defaultHandler;
this._sysErrFormatter = opts.systemErrorFormatter;
this._silent = !!opts.silent;
this._strict = !!opts.strictTypes;
if (opts.remoteProtocols) {
insertRemoteProtocols(this._cache, opts.remoteProtocols, svc, false);
}
svc.messages.forEach(function (msg) {
var name = msg.name;
if (!opts.noCapitalize) {
name = utils.capitalize(name);
}
this['on' + name] = this._createMessageHandler(msg);
}, this);
}
util.inherits(Server, events.EventEmitter);
Server.prototype.activeChannels = function () {
return utils.objectValues(this._channels);
};
Server.prototype.createChannel = function (transport, opts) {
var objectMode = opts && opts.objectMode;
var channel;
if (typeof transport == 'function') {
var readableFactory;
if (objectMode) {
readableFactory = transport;
} else {
readableFactory = function (cb) {
var decoder = new FrameDecoder()
.once('error', function (err) { channel.destroy(err); });
return transport(function (err, writable) {
if (err) {
cb(err);
return;
}
var encoder = new FrameEncoder();
encoder.pipe(writable);
cb(null, encoder);
}).pipe(decoder);
};
}
channel = new StatelessServerChannel(this, readableFactory, opts);
} else {
var readable, writable;
if (isStream(transport)) {
readable = writable = transport;
} else {
readable = transport.readable;
writable = transport.writable;
}
if (!objectMode) {
var decoder = new NettyDecoder();
readable = readable.pipe(decoder);
var encoder = new NettyEncoder();
encoder.pipe(writable);
writable = encoder;
}
channel = new StatefulServerChannel(this, readable, writable, opts);
if (!objectMode) {
// Similar to client channels, since we never expose the encoder and
// decoder, we must release them ourselves here.
channel.once('eot', function () {
readable.unpipe(decoder);
encoder.unpipe(writable);
});
decoder.once('error', function (err) { channel.destroy(err); });
}
}
if (!this.listeners('error').length) {
this.on('error', this._onError);
}
var channelId = this._nextChannelId++;
var channels = this._channels;
channels[channelId] = channel
.once('eot', function () { delete channels[channelId]; });
this.emit('channel', channel);
return channel;
};
Server.prototype.onMessage = function (name, handler) {
getExistingMessage(this.service, name); // Check message existence.
this._handlers[name] = handler;
return this;
};
Server.prototype.remoteProtocols = function () {
return getRemoteProtocols(this._cache, false);
};
Server.prototype.use = function (/* fn ... */) {
var i, l, fn;
for (i = 0, l = arguments.length; i < l; i++) {
fn = arguments[i];
this._fns.push(fn.length < 3 ? fn(this) : fn);
}
return this;
};
Server.prototype._createMessageHandler = function (msg) {
// jshint -W054
var name = msg.name;
var fields = msg.requestType.fields;
var numArgs = fields.length;
var args = fields.length ?
', ' + fields.map(function (f) { return 'req.' + f.name; }).join(', ') :
'';
// We are careful to not lose the initial handler's number of arguments (or
// more specifically whether it would have access to the callback or not).
// This is useful to implement "smart promisification" logic downstream.
var body = 'return function (handler) {\n';
body += ' if (handler.length > ' + numArgs + ') {\n';
body += ' return this.onMessage(\'' + name + '\', function (req, cb) {\n';
body += ' return handler.call(this' + args + ', cb);\n';
body += ' });\n';
body += ' } else {\n';
body += ' return this.onMessage(\'' + name + '\', function (req) {\n';
body += ' return handler.call(this' + args + ');\n';
body += ' });\n';
body += ' }\n';
body += '};\n';
return (new Function(body))();
};
Server.prototype._onError = function (err) {
/* istanbul ignore if */
if (!this._silent && err.rpcCode !== 'UNKNOWN_PROTOCOL') {
console.error();
if (err.rpcCode) {
console.error(err.rpcCode);
console.error(err.cause);
} else {
console.error('INTERNAL_SERVER_ERROR');
console.error(err);
}
}
};
/** Base message emitter class. See below for the two available variants. */
function ClientChannel(client, opts) {
opts = opts || {};
events.EventEmitter.call(this);
this.client = client;
this.timeout = utils.getOption(opts, 'timeout', client._timeout$);
this._endWritable = !!utils.getOption(opts, 'endWritable', true);
this._prefix = normalizedPrefix(opts.scope);
var cache = client._cache$;
var clientSvc = client._svc$;
var hash = opts.serverHash;
if (!hash) {
hash = clientSvc.hash;
}
var adapter = cache[hash];
if (!adapter) {
// This might happen even if the server hash option was set if the cache
// doesn't contain the corresponding adapter. In this case we fall back to
// the client's protocol (as mandated by the spec).
hash = clientSvc.hash;
adapter = cache[hash] = new Adapter(clientSvc, clientSvc, hash);
}
this._adapter = adapter;
this._registry = new Registry(this, PREFIX_LENGTH);
this.pending = 0;
this.destroyed = false;
this.draining = false;
this.once('_eot', function (pending, err) {
// Since this listener is only run once, we will only forward an error if
// it is present during the initial `destroy` call, which is OK.
debug('client channel EOT');
this.destroyed = true;
this.emit('eot', pending, err);
});
}
util.inherits(ClientChannel, events.EventEmitter);
ClientChannel.prototype.destroy = function (noWait) {
debug('destroying client channel');
if (!this.draining) {
this.draining = true;
this.emit('_drain');
}
var registry = this._registry;
var pending = this.pending;
if (noWait) {
registry.clear();
}
if (noWait || !pending) {
if (isError(noWait)) {
debug('fatal client channel error: %s', noWait);
this.emit('_eot', pending, noWait);
} else {
this.emit('_eot', pending);
}
} else {
debug('client channel entering drain mode (%s pending)', pending);
}
};
ClientChannel.prototype.ping = function (timeout, cb) {
if (!cb && typeof timeout == 'function') {
cb = timeout;
timeout = undefined;
}
var self = this;
var wreq = new WrappedRequest(PING_MESSAGE);
this._emit(wreq, {timeout: timeout}, function (err) {
if (cb) {
cb.call(self, err);
} else if (err) {
self.destroy(err);
}
});
};
ClientChannel.prototype._createHandshakeRequest = function (adapter, noSvc) {
var svc = this.client._svc$;
return {
clientHash: svc.hash,
clientProtocol: noSvc ? null : JSON.stringify(svc.protocol),
serverHash: adapter._hash
};
};
ClientChannel.prototype._emit = function (wreq, opts, cb) {
var msg = wreq._msg;
var wres = msg.oneWay ? undefined : new WrappedResponse(msg, {});
var ctx = new CallContext(msg, this);
var self = this;
this.pending++;
process.nextTick(function () {
if (!msg.name) {
// Ping request, bypass middleware.
onTransition(wreq, wres, onCompletion);
} else {
self.emit('outgoingCall', ctx, opts);
var fns = self.client._fns$;
debug('starting client middleware chain (%s middleware)', fns.length);
chainMiddleware({
fns: fns,
ctx: ctx,
wreq: wreq,
wres: wres,
onTransition: onTransition,
onCompletion: onCompletion,
onError: onError
});
}
});
function onTransition(wreq, wres, prev) {
// Serialize the message.
var err, reqBuf;
if (self.destroyed) {
err = new Error('channel destroyed');
} else {
try {
reqBuf = wreq.toBuffer();
} catch (cause) {
err = serializationError(
f('invalid %j request', msg.name),
wreq,
[
{name: 'headers', type: MAP_BYTES_TYPE},
{name: 'request', type: msg.requestType}
]
);
}
}
if (err) {
prev(err);
return;
}
// Generate the response callback.
var timeout = (opts && opts.timeout !== undefined) ?
opts.timeout :
self.timeout;
var id = self._registry.add(timeout, function (err, resBuf, adapter) {
if (!err && !msg.oneWay) {
try {
adapter._decodeResponse(resBuf, wres, msg);
} catch (cause) {
err = cause;
}
}
prev(err);
});
id |= self._prefix;
debug('sending message %s', id);
self._send(id, reqBuf, !!msg && msg.oneWay);
}
function onCompletion(err) {
self.pending--;
cb.call(ctx, err, wres);
if (self.draining && !self.destroyed && !self.pending) {
self.destroy();
}
}
function onError(err) {
// This will happen if a middleware callback is called multiple times. We
// forward the error to the client rather than emit it on the channel since
// middleware are a client-level abstraction, so better handled there.
self.client.emit('error', err, self);
}
};
ClientChannel.prototype._getAdapter = function (hres) {
var hash = hres.serverHash;
var cache = this.client._cache$;
var adapter = cache[hash];
if (adapter) {
return adapter;
}
var ptcl = JSON.parse(hres.serverProtocol);
var serverSvc = Service.forProtocol(ptcl);
adapter = new Adapter(this.client._svc$, serverSvc, hash, true);
return cache[hash] = adapter;
};
ClientChannel.prototype._matchesPrefix = function (id) {
return matchesPrefix(id, this._prefix);
};
ClientChannel.prototype._send = utils.abstractFunction;
// Deprecated.
utils.addDeprecatedGetters(ClientChannel, ['pending', 'timeout']);
ClientChannel.prototype.getCache = util.deprecate(
function () { return this.client._cache$; },
'use `.remoteProtocols()` instead of `.getCache()`'
);
ClientChannel.prototype.getProtocol = util.deprecate(
function () {
return this.client._svc$;
},
'use `.service` instead or `.getProtocol()`'
);
ClientChannel.prototype.isDestroyed = util.deprecate(
function () { return this.destroyed; },
'use `.destroyed` instead of `.isDestroyed`'
);
/**
* Factory-based client channel.
*
* This channel doesn't keep a persistent connection to the server and requires
* prepending a handshake to each message emitted. Usage examples include
* talking to an HTTP server (where the factory returns an HTTP request).
*
* Since each message will use its own writable/readable stream pair, the
* advantage of this channel is that it is able to keep track of which response
* corresponds to each request without relying on transport ordering. In
* particular, this means these channels are compatible with any server
* implementation.
*/
function StatelessClientChannel(client, writableFactory, opts) {
ClientChannel.call(this, client, opts);
this._writableFactory = writableFactory;
if (!opts || !opts.noPing) {
// Ping the server to check whether the remote protocol is compatible.
// If not, this will throw an error on the channel.
debug('emitting ping request');
this.ping();
}
}
util.inherits(StatelessClientChannel, ClientChannel);
StatelessClientChannel.prototype._send = function (id, reqBuf) {
var cb = this._registry.get(id);
var adapter = this._adapter;
var self = this;
process.nextTick(emit);
return true;
function emit(retry) {
if (self.destroyed) {
// The request's callback will already have been called.
return;
}
var hreq = self._createHandshakeRequest(adapter, !retry);
var writable = self._writableFactory.call(self, function (err, readable) {
if (err) {
cb(err);
return;
}
readable.on('data', function (obj) {
debug('received response %s', obj.id);
// We don't check that the prefix matches since the ID likely hasn't
// been propagated to the response (see default stateless codec).
var buf = Buffer.concat(obj.payload);
try {
var parts = readHead(HANDSHAKE_RESPONSE_TYPE, buf);
var hres = parts.head;
if (hres.serverHash) {
adapter = self._getAdapter(hres);
}
} catch (cause) {
cb(cause);
return;
}
var match = hres.match;
debug('handshake match: %s', match);
self.emit('handshake', hreq, hres);
if (match === 'NONE') {
// Try again, including the full protocol this time.
process.nextTick(function() { emit(true); });
} else {
// Change the default adapter.
self._adapter = adapter;
cb(null, parts.tail, adapter);
}
});
});
if (!writable) {
cb(new Error('invalid writable stream'));
return;
}
writable.write({
id: id,
payload: [HANDSHAKE_REQUEST_TYPE.toBuffer(hreq), reqBuf]
});
if (self._endWritable) {
writable.end();
}
}
};
/**
* Multiplexing client channel.
*
* These channels reuse the same streams (both readable and writable) for all
* messages. This avoids a lot of overhead (e.g. creating new connections,
* re-issuing handshakes) but requires the underlying transport to support
* forwarding message IDs.
*/
function StatefulClientChannel(client, readable, writable, opts) {
ClientChannel.call(this, client, opts);
this._readable = readable;
this._writable = writable;
this._connected = !!(opts && opts.noPing);
this._readable.on('end', onEnd);
this._writable.on('finish', onFinish);
var self = this;
var timer = null;
this.once('eot', function () {
if (timer) {
clearTimeout(timer);
timer = null;
}
if (!self._connected) {
// Clear any buffered calls (they are guaranteed to error out when
// reaching the transition phase).
self.emit('_ready');
}
// Remove references to this channel to avoid potential memory leaks.
this._writable.removeListener('finish', onFinish);
if (this._endWritable) {
debug('ending transport');
this._writable.end();
}
this._readable
.removeListener('data', onPing)
.removeListener('data', onMessage)
.removeListener('end', onEnd);
});
var hreq; // For handshake events.
if (this._connected) {
this._readable.on('data', onMessage);
} else {
this._readable.on('data', onPing);
process.nextTick(ping);
if (self.timeout) {
timer = setTimeout(function () {
self.destroy(new Error('timeout'));
}, self.timeout);
}
}
function ping(retry) {
if (self.destroyed) {
return;
}
hreq = self._createHandshakeRequest(self._adapter, !retry);
var payload = [
HANDSHAKE_REQUEST_TYPE.toBuffer(hreq),
utils.bufferFrom([0, 0]) // No header, no data (empty message name).
];
// We can use a static ID here since we are guaranteed that this message is
// the only one on the channel (for this scope at least).
self._writable.write({id: self._prefix, payload: payload});
}
function onPing(obj) {
if (!self._matchesPrefix(obj.id)) {
debug('discarding unscoped response %s (still connecting)', obj.id);
return;
}
var buf = Buffer.concat(obj.payload);
try {
var hres = readHead(HANDSHAKE_RESPONSE_TYPE, buf).head;
if (hres.serverHash) {
self._adapter = self._getAdapter(hres);
}
} catch (cause) {
// This isn't a recoverable error.
self.destroy(cause);
return;
}
var match = hres.match;
debug('handshake match: %s', match);
self.emit('handshake', hreq, hres);
if (match === 'NONE') {
process.nextTick(function () { ping(true); });
} else {
debug('successfully connected');
if (timer) {
clearTimeout(timer);
timer = null;
}
self._readable.removeListener('data', onPing).on('data', onMessage);
self._connected = true;
self.emit('_ready');
hreq = null; // Release reference.
}
}
// Callback used after a connection has been established.
function onMessage(obj) {
var id = obj.id;
if (!self._matchesPrefix(id)) {
debug('discarding unscoped message %s', id);
return;
}
var cb = self._registry.get(id);
if (cb) {
process.nextTick(function () {
debug('received message %s', id);
// Ensure that the initial callback gets called asynchronously, even
// for completely synchronous transports (otherwise the number of
// pending requests will sometimes be inconsistent between stateful and
// stateless transports).
cb(null, Buffer.concat(obj.payload), self._adapter);
});
}
}
function onEnd() { self.destroy(true); }
function onFinish() { self.destroy(); }
}
util.inherits(StatefulClientChannel, ClientChannel);
StatefulClientChannel.prototype._emit = function () {
// Override this method to allow calling `_emit` even before the channel is
// connected. Note that we don't perform this logic in `_send` since we want
// to guarantee that `'handshake'` events are emitted before any
// `'outgoingCall'` events.
if (this._connected || this.draining) {
ClientChannel.prototype._emit.apply(this, arguments);
} else {
debug('queuing request');
var args = [];
var i, l;
for (i = 0, l = arguments.length; i < l; i++) {
args.push(arguments[i]);
}
this.once('_ready', function () { this._emit.apply(this, args); });
}
};
StatefulClientChannel.prototype._send = function (id, reqBuf, oneWay) {
if (oneWay) {
var self = this;
// Clear the callback, passing in an empty header.
process.nextTick(function () {
self._registry.get(id)(null, utils.bufferFrom([0, 0, 0]), self._adapter);
});
}
return this._writable.write({id: id, payload: [reqBuf]});
};
/** The server-side emitter equivalent. */
function ServerChannel(server, opts) {
opts = opts || {};
events.EventEmitter.call(this);
this.server = server;
this._endWritable = !!utils.getOption(opts, 'endWritable', true);
this._prefix = normalizedPrefix(opts.scope);
var cache = server._cache;
var svc = server.service;
var hash = svc.hash;
if (!cache[hash]) {
// Add the channel's protocol to the cache if it isn't already there. This
// will save a handshake the first time on channels with the same protocol.
cache[hash] = new Adapter(svc, svc, hash);
}
this._adapter = null;
this.destroyed = false;
this.draining = false;
this.pending = 0;
this.once('_eot', function (pending, err) {
debug('server channel EOT');
this.emit('eot', pending, err);
});
}
util.inherits(ServerChannel, events.EventEmitter);
ServerChannel.prototype.destroy = function (noWait) {
if (!this.draining) {
this.draining = true;
this.emit('_drain');
}
if (noWait || !this.pending) {
this.destroyed = true;
if (isError(noWait)) {
debug('fatal server channel error: %s', noWait);
this.emit('_eot', this.pending, noWait);
} else {
this.emit('_eot', this.pending);
}
}
};
ServerChannel.prototype._createHandshakeResponse = function (err, hreq) {
var svc = this.server.service;
var buf = svc.hash;
var serverMatch = hreq && hreq.serverHash.equals(buf);
return {
match: err ? 'NONE' : (serverMatch ? 'BOTH' : 'CLIENT'),
serverProtocol: serverMatch ? null : JSON.stringify(svc.protocol),
serverHash: serverMatch ? null : buf
};
};
ServerChannel.prototype._getAdapter = function (hreq) {
var hash = hreq.clientHash;
var adapter = this.server._cache[hash];
if (adapter) {
return adapter;
}
if (!hreq.clientProtocol) {
throw toRpcError('UNKNOWN_PROTOCOL');
}
var ptcl = JSON.parse(hreq.clientProtocol);
var clientSvc = Service.forProtocol(ptcl);
adapter = new Adapter(clientSvc, this.server.service, hash, true);
return this.server._cache[hash] = adapter;
};
ServerChannel.prototype._matchesPrefix = function (id) {
return matchesPrefix(id, this._prefix);
};
ServerChannel.prototype._receive = function (reqBuf, adapter, cb) {
var self = this;
var wreq;
try {
wreq = adapter._decodeRequest(reqBuf);
} catch (cause) {
cb(self._encodeSystemError(toRpcError('INVALID_REQUEST', cause)));
return;
}
var msg = wreq._msg;
var wres = new WrappedResponse(msg, {});
if (!msg.name) {
// Ping message, we don't invoke middleware logic in this case.
wres.response = null;
cb(wres.toBuffer(), false);
return;
}
var ctx = new CallContext(msg, this);
self.emit('incomingCall', ctx);
var fns = this.server._fns;
debug('starting server middleware chain (%s middleware)', fns.length);
self.pending++;
chainMiddleware({
fns: fns,
ctx: ctx,
wreq: wreq,
wres: wres,
onTransition: onTransition,
onCompletion: onCompletion,
onError: onError
});
function onTransition(wreq, wres, prev) {
var handler = self.server._handlers[msg.name];
if (!handler) {
// The underlying service hasn't implemented a handler.
var defaultHandler = self.server._defaultHandler;
if (defaultHandler) {
// We call the default handler with arguments similar (slightly
// simpler, there are no phases here) to middleware such that it can
// easily access the message name (useful to implement proxies).
defaultHandler.call(ctx, wreq, wres, prev);
} else {
var cause = new Error(f('no handler for %s', msg.name));
prev(toRpcError('NOT_IMPLEMENTED', cause));
}
} else {
var pending = !msg.oneWay;
try {
if (pending) {
handler.call(ctx, wreq.request, function (err, res) {
pending = false;
wres.error = err;
wres.response = res;
prev();
});
} else {
handler.call(ctx, wreq.request);
prev();
}
} catch (err) {
// We catch synchronous failures (same as express) and return the
// failure. Note that the server process can still crash if an error
// is thrown after the handler returns but before the response is
// sent (again, same as express). We are careful to only trigger the
// response callback once, emitting the errors afterwards instead.
if (pending) {
pending = false;
prev(err);
} else {
onError(err);
}
}
}
}
function onCompletion(err) {
self.pending--;
var server = self.server;
var resBuf;
if (!err) {
var resErr = wres.error;
var isStrict = server._strict;
if (!isStrict) {
if (isError(resErr)) {
// If the error type is wrapped, we must wrap the error too.
wres.error = msg.errorType.clone(resErr.message, {wrapUnions: true});
} else if (resErr === null) {
// We also allow `null`'s as error in this mode, converting them to
// the Avro-compatible `undefined`.
resErr = wres.error = undefined;
}
if (
resErr === undefined &&
wres.response === undefined &&
msg.responseType.isValid(null)
) {
// Finally, for messages with `null` as acceptable response type, we
// allow `undefined`; converting them to `null`. This allows users to
// write a more natural `cb()` instead of `cb(null, null)`.
wres.response = null;
}
}
try {
resBuf = wres.toBuffer();
} catch (cause) {
// Note that we don't add an RPC code here such that the client
// receives the default `INTERNAL_SERVER_ERROR` one.
if (wres.error !== undefined) {
err = serializationError(
f('invalid %j error', msg.name), // Sic.
wres,
[
{name: 'headers', type: MAP_BYTES_TYPE},
{name: 'error', type: msg.errorType}
]
);
} else {
err = serializationError(
f('invalid %j response', msg.name),
wres,
[
{name: 'headers', type: MAP_BYTES_TYPE},
{name: 'response', type: msg.responseType}
]
);
}
}
}
if (!resBuf) {
// The headers are only available if the message isn't one-way.
resBuf = self._encodeSystemError(err, wres.headers);
} else if (resErr !== undefined) {
server.emit('error', toRpcError('APPLICATION_ERROR', resErr));
}
cb(resBuf, msg.oneWay);
if (self.draining && !self.pending) {
self.destroy();
}
}
function onError(err) {
// Similar to the client equivalent, we redirect this error to the server
// since middleware are defined at server-level.
self.server.emit('error', err, self);
}
};
// Deprecated.
utils.addDeprecatedGetters(ServerChannel, ['pending']);
ServerChannel.prototype.getCache = util.deprecate(
function () { return this.server._cache; },
'use `.remoteProtocols()` instead of `.getCache()`'
);
ServerChannel.prototype.getProtocol = util.deprecate(
function () {
return this.server.service;
},
'use `.service` instead of `.getProtocol()`'
);
ServerChannel.prototype.isDestroyed = util.deprecate(
function () { return this.destroyed; },
'use `.destroyed` instead of `.isDestroyed`'
);
/**
* Encode an error and optional header into a valid Avro response.
*
* @param err {Error} Error to encode.
* @param header {Object} Optional response header.
*/
ServerChannel.prototype._encodeSystemError = function (err, header) {
var server = this.server;
server.emit('error', err, this);
var errStr;
if (server._sysErrFormatter) {
// Format the error into a string to send over the wire.
errStr = server._sysErrFormatter.call(this, err);
} else if (err.rpcCode) {
// By default, only forward the error's message when the RPC code is set
// (i.e. when this isn't an internal server error).
errStr = err.message;
}
var hdrBuf;
if (header) {
try {
// Propagate the header if possible.
hdrBuf = MAP_BYTES_TYPE.toBuffer(header);
} catch (cause) {
server.emit('error', cause, this);
}
}
return Buffer.concat([
hdrBuf || utils.bufferFrom([0]),
utils.bufferFrom([1, 0]), // Error flag and first union index.
STRING_TYPE.toBuffer(errStr || 'internal server error')
]);
};
/**
* Server channel for stateless transport.
*
* This channel expect a handshake to precede each message.
*/
function StatelessServerChannel(server, readableFactory, opts) {
ServerChannel.call(this, server, opts);
this._writable = undefined;
var self = this;
var readable;
process.nextTick(function () {
// Delay listening to allow handlers to be attached even if the factory is
// purely synchronous.
readable = readableFactory.call(self, function (err, writable) {
process.nextTick(function () {
// We delay once more here in case this call is synchronous, to allow
// the readable to always be populated first.
if (err) {
onFinish(err);
return;
}
self._writable = writable.on('finish', onFinish);
self.emit('_writable');
});
}).on('data', onRequest).on('end', onEnd);
});
function onRequest(obj) {
var id = obj.id;
var buf = Buffer.concat(obj.payload);
var err;
try {
var parts = readHead(HANDSHAKE_REQUEST_TYPE, buf);
var hreq = parts.head;
var adapter = self._getAdapter(hreq);
} catch (cause) {
err = toRpcError('INVALID_HANDSHAKE_REQUEST', cause);
}
var hres = self._createHandshakeResponse(err, hreq);
self.emit('handshake', hreq, hres);
if (err) {
done(self._encodeSystemError(err));
} else {
self._receive(parts.tail, adapter, done);
}
function done(resBuf) {
if (!self.destroyed) {
if (!self._writable) {
self.once('_writable', function () { done(resBuf); });
return;
}
self._writable.write({
id: id,
payload: [HANDSHAKE_RESPONSE_TYPE.toBuffer(hres), resBuf]
});
}
if (self._writable && self._endWritable) {
self._writable.end();
}
}
}
function onEnd() { self.destroy(); }
function onFinish(err) {
readable
.removeListener('data', onRequest)
.removeListener('end', onEnd);
self.destroy(err || true);
}
}
util.inherits(StatelessServerChannel, ServerChannel);
/**
* Stateful transport listener.
*
* A handshake is done when the channel first receives a message, then all
* messages are sent without.
*/
function StatefulServerChannel(server, readable, writable, opts) {
ServerChannel.call(this, server, opts);
this._adapter = undefined;
this._writable = writable.on('finish', onFinish);
this._readable = readable.on('data', onHandshake).on('end', onEnd);
this
.once('_drain', function () {
// Stop listening to incoming events.
this._readable
.removeListener('data', onHandshake)
.removeListener('data', onRequest)
.removeListener('end', onEnd);
})
.once('eot', function () {
// Clean up any references to the channel on the underlying streams.
this._writable.removeListener('finish', onFinish);
if (this._endWritable) {
this._writable.end();
}
});
var self = this;
function onHandshake(obj) {
var id = obj.id;
if (!self._matchesPrefix(id)) {
return;
}
var buf = Buffer.concat(obj.payload);
var err;
try {
var parts = readHead(HANDSHAKE_REQUEST_TYPE, buf);
var hreq = parts.head;
self._adapter = self._getAdapter(hreq);
} catch (cause) {
err = toRpcError('INVALID_HANDSHAKE_REQUEST', cause);
}
var hres = self._createHandshakeResponse(err, hreq);
self.emit('handshake', hreq, hres);
if (err) {
// Either the client's protocol was unknown or it isn't compatible.
done(self._encodeSystemError(err));
} else {
self._readable
.removeListener('data', onHandshake)
.on('data', onRequest);
self._receive(parts.tail, self._adapter, done);
}
function done(resBuf) {
if (self.destroyed) {
return;
}
self._writable.write({
id: id,
payload: [HANDSHAKE_RESPONSE_TYPE.toBuffer(hres), resBuf]
});
}
}
function onRequest(obj) {
// These requests are not prefixed with handshakes.
var id = obj.id;
if (!self._matchesPrefix(id)) {
return;
}
var reqBuf = Buffer.concat(obj.payload);
self._receive(reqBuf, self._adapter, function (resBuf, oneWay) {
if (self.destroyed || oneWay) {
return;
}
self._writable.write({id: id, payload: [resBuf]});
});
}
function onEnd() { self.destroy(); }
function onFinish() { self.destroy(true); }
}
util.inherits(StatefulServerChannel, ServerChannel);
// Helpers.
/** Enhanced request, used inside forward middleware functions. */
function WrappedRequest(msg, hdrs, req) {
this._msg = msg;
this.headers = hdrs || {};
this.request = req || {};
}
WrappedRequest.prototype.toBuffer = function () {
var msg = this._msg;
return Buffer.concat([
MAP_BYTES_TYPE.toBuffer(this.headers),
STRING_TYPE.toBuffer(msg.name),
msg.requestType.toBuffer(this.request)
]);
};
/** Enhanced response, used inside forward middleware functions. */
function WrappedResponse(msg, hdr, err, res) {
this._msg = msg;
this.headers = hdr;
this.error = err;
this.response = res;
}
WrappedResponse.prototype.toBuffer = function () {
var hdr = MAP_BYTES_TYPE.toBuffer(this.headers);
var hasError = this.error !== undefined;
return Buffer.concat([
hdr,
BOOLEAN_TYPE.toBuffer(hasError),
hasError ?
this._msg.errorType.toBuffer(this.error) :
this._msg.responseType.toBuffer(this.response)
]);
};
/**
* Context for all middleware and handlers.
*
* It exposes a `locals` object which can be used to pass information between
* each other during a given call.
*/
function CallContext(msg, channel) {
this.channel = channel;
this.locals = {};
this.message = msg;
Object.freeze(this);
}
/**
* Callback registry.
*
* Callbacks added must accept an error as first argument. This is used by
* client channels to store pending calls. This class isn't exposed by the
* public API.
*/
function Registry(ctx, prefixLength) {
this._ctx = ctx; // Context for all callbacks.
this._mask = ~0 >>> (prefixLength | 0); // 16 bits by default.
this._id = 0; // Unique integer ID for each call.
this._n = 0; // Number of pending calls.
this._cbs = {};
}
Registry.prototype.get = function (id) { return this._cbs[id & this._mask]; };
Registry.prototype.add = function (timeout, fn) {
this._id = (this._id + 1) & this._mask;
var self = this;
var id = this._id;
var timer;
if (timeout > 0) {
timer = setTimeout(function () { cb(new Error('timeout')); }, timeout);
}
this._cbs[id] = cb;
this._n++;
return id;
function cb() {
if (!self._cbs[id]) {
// The callback has already run.
return;
}
delete self._cbs[id];
self._n--;
if (timer) {
clearTimeout(timer);
}
fn.apply(self._ctx, arguments);
}
};
Registry.prototype.clear = function () {
Object.keys(this._cbs).forEach(function (id) {
this._cbs[id](new Error('interrupted'));
}, this);
};
/**
* Service resolution helper.
*
* It is used both by client and server channels, to respectively decode errors
* and responses, or requests.
*/
function Adapter(clientSvc, serverSvc, hash, isRemote) {
this._clientSvc = clientSvc;
this._serverSvc = serverSvc;
this._hash = hash; // Convenience to access it when creating handshakes.
this._isRemote = !!isRemote;
this._readers = createReaders(clientSvc, serverSvc);
}
Adapter.prototype._decodeRequest = function (buf) {
var tap = new Tap(buf);
var hdr = MAP_BYTES_TYPE._read(tap);
var name = STRING_TYPE._read(tap);
var msg, req;
if (name) {
msg = this._serverSvc.message(name);
req = this._readers[name + '?']._read(tap);
} else {
msg = PING_MESSAGE;
}
if (!tap.isValid()) {
throw new Error(f('truncated %s request', name || 'ping$'));
}
return new WrappedRequest(msg, hdr, req);
};
Adapter.prototype._decodeResponse = function (buf, wres, msg) {
var tap = new Tap(buf);
utils.copyOwnProperties(MAP_BYTES_TYPE._read(tap), wres.headers, true);
var isError = BOOLEAN_TYPE._read(tap);
var name = msg.name;
if (name) {
var reader = this._readers[name + (isError ? '*' : '!')];
msg = this._clientSvc.message(name);
if (isError) {
wres.error = reader._read(tap);
} else {
wres.response = reader._read(tap);
}
if (!tap.isValid()) {
throw new Error(f('truncated %s response', name));
}
} else {
msg = PING_MESSAGE;
}
};
/** Standard "un-framing" stream. */
function FrameDecoder() {
stream.Transform.call(this, {readableObjectMode: true});
this._id = undefined;
this._buf = utils.newBuffer(0);
this._bufs = [];
this.on('finish', function () { this.push(null); });
}
util.inherits(FrameDecoder, stream.Transform);
FrameDecoder.prototype._transform = function (buf, encoding, cb) {
buf = Buffer.concat([this._buf, buf]);
var frameLength;
while (
buf.length >= 4 &&
buf.length >= (frameLength = buf.readInt32BE(0)) + 4
) {
if (frameLength) {
this._bufs.push(buf.slice(4, frameLength + 4));
} else {
var bufs = this._bufs;
this._bufs = [];
this.push({id: null, payload: bufs});
}
buf = buf.slice(frameLength + 4);
}
this._buf = buf;
cb();
};
FrameDecoder.prototype._flush = function () {
if (this._buf.length || this._bufs.length) {
var bufs = this._bufs.slice();
bufs.unshift(this._buf);
var err = toRpcError('TRAILING_DATA');
// Attach the data to help debugging (e.g. if the encoded bytes contain a
// human-readable protocol like HTTP).
err.trailingData = Buffer.concat(bufs).toString();
this.emit('error', err);
}
};
/** Standard framing stream. */
function FrameEncoder() {
stream.Transform.call(this, {writableObjectMode: true});
this.on('finish', function () { this.push(null); });
}
util.inherits(FrameEncoder, stream.Transform);
FrameEncoder.prototype._transform = function (obj, encoding, cb) {
var bufs = obj.payload;
var i, l, buf;
for (i = 0, l = bufs.length; i < l; i++) {
buf = bufs[i];
this.push(intBuffer(buf.length));
this.push(buf);
}
this.push(intBuffer(0));
cb();
};
/** Netty-compatible decoding stream. */
function NettyDecoder() {
stream.Transform.call(this, {readableObjectMode: true});
this._id = undefined;
this._frameCount = 0;
this._buf = utils.newBuffer(0);
this._bufs = [];
this.on('finish', function () { this.push(null); });
}
util.inherits(NettyDecoder, stream.Transform);
NettyDecoder.prototype._transform = function (buf, encoding, cb) {
buf = Buffer.concat([this._buf, buf]);
while (true) {
if (this._id === undefined) {
if (buf.length < 8) {
this._buf = buf;
cb();
return;
}
this._id = buf.readInt32BE(0);
this._frameCount = buf.readInt32BE(4);
buf = buf.slice(8);
}
var frameLength;
while (
this._frameCount &&
buf.length >= 4 &&
buf.length >= (frameLength = buf.readInt32BE(0)) + 4
) {
this._frameCount--;
this._bufs.push(buf.slice(4, frameLength + 4));
buf = buf.slice(frameLength + 4);
}
if (this._frameCount) {
this._buf = buf;
cb();
return;
} else {
var obj = {id: this._id, payload: this._bufs};
this._bufs = [];
this._id = undefined;
this.push(obj);
}
}
};
NettyDecoder.prototype._flush = FrameDecoder.prototype._flush;
/** Netty-compatible encoding stream. */
function NettyEncoder() {
stream.Transform.call(this, {writableObjectMode: true});
this.on('finish', function () { this.push(null); });
}
util.inherits(NettyEncoder, stream.Transform);
NettyEncoder.prototype._transform = function (obj, encoding, cb) {
var bufs = obj.payload;
var l = bufs.length;
var buf;
// Header: [ ID, number of frames ]
buf = utils.newBuffer(8);
buf.writeInt32BE(obj.id, 0);
buf.writeInt32BE(l, 4);
this.push(buf);
// Frames, each: [ length, bytes ]
var i;
for (i = 0; i < l; i++) {
buf = bufs[i];
this.push(intBuffer(buf.length));
this.push(buf);
}
cb();
};
/**
* Returns a buffer containing an integer's big-endian representation.
*
* @param n {Number} Integer.
*/
function intBuffer(n) {
var buf = utils.newBuffer(4);
buf.writeInt32BE(n);
return buf;
}
/**
* Decode a type used as prefix inside a buffer.
*
* @param type {Type} The type of the prefix.
* @param buf {Buffer} Encoded bytes.
*
* This function will return an object `{head, tail}` where head contains the
* decoded value and tail the rest of the buffer. An error will be thrown if
* the prefix cannot be decoded.
*/
function readHead(type, buf) {
var tap = new Tap(buf);
var head = type._read(tap);
if (!tap.isValid()) {
throw new Error(f('truncated %j', type.schema()));
}
return {head: head, tail: tap.buf.slice(tap.pos)};
}
/**
* Generate a decoder, optimizing the case where reader and writer are equal.
*
* @param rtype {Type} Reader's type.
* @param wtype {Type} Writer's type.
*/
function createReader(rtype, wtype) {
return rtype.equals(wtype) ? rtype : rtype.createResolver(wtype);
}
/**
* Generate all readers for a given protocol combination.
*
* @param clientSvc {Service} Client service.
* @param serverSvc {Service} Client service.
*/
function createReaders(clientSvc, serverSvc) {
var obj = {};
clientSvc.messages.forEach(function (c) {
var n = c.name;
var s = serverSvc.message(n);
try {
if (!s) {
throw new Error(f('missing server message: %s', n));
}
if (s.oneWay !== c.oneWay) {
throw new Error(f('inconsistent one-way message: %s', n));
}
obj[n + '?'] = createReader(s.requestType, c.requestType);
obj[n + '*'] = createReader(c.errorType, s.errorType);
obj[n + '!'] = createReader(c.responseType, s.responseType);
} catch (cause) {
throw toRpcError('INCOMPATIBLE_PROTOCOL', cause);
}
});
return obj;
}
/**
* Populate a cache from a list of protocols.
*
* @param cache {Object} Cache of adapters.
* @param svc {Service} The local service (either client or server).
* @param ptcls {Array} Array of protocols to insert.
* @param isClient {Boolean} Whether the local service is a client's or
* server's.
*/
function insertRemoteProtocols(cache, ptcls, svc, isClient) {
Object.keys(ptcls).forEach(function (hash) {
var ptcl = ptcls[hash];
var clientSvc, serverSvc;
if (isClient) {
clientSvc = svc;
serverSvc = Service.forProtocol(ptcl);
} else {
clientSvc = Service.forProtocol(ptcl);
serverSvc = svc;
}
cache[hash] = new Adapter(clientSvc, serverSvc, hash, true);
});
}
/**
* Extract remote protocols from a cache
*
* @param cache {Object} Cache of adapters.
* @param isClient {Boolean} Whether the remote protocols extracted should be
* the servers' or clients'.
*/
function getRemoteProtocols(cache, isClient) {
var ptcls = {};
Object.keys(cache).forEach(function (hs) {
var adapter = cache[hs];
if (adapter._isRemote) {
var svc = isClient ? adapter._serverSvc : adapter._clientSvc;
ptcls[hs] = svc.protocol;
}
});
return ptcls;
}
/**
* Check whether something is an `Error`.
*
* @param any {Object} Any object.
*/
function isError(any) {
// Also not ideal, but avoids brittle `instanceof` checks.
return !!any && Object.prototype.toString.call(any) === '[object Error]';
}
/**
* Forward any errors emitted on the source to the destination.
*
* @param src {EventEmitter} The initial source of error events.
* @param dst {EventEmitter} The new target of the source's error events. The
* original source will be provided as second argument (the error being the
* first).
*
* As a convenience, the source will be returned.
*/
function forwardErrors(src, dst) {
return src.on('error', function (err) {
dst.emit('error', err, src);
});
}
/**
* Create an error.
*
* @param msg {String} Error message.
* @param cause {Error} The cause of the error. It is available as `cause`
* field on the outer error.
*/
function toError(msg, cause) {
var err = new Error(msg);
err.cause = cause;
return err;
}
/**
* Mark an error.
*
* @param rpcCode {String} Code representing the failure.
* @param cause {Error} The cause of the error. It is available as `cause`
* field on the outer error.
*
* This is used to keep the argument of channels' `'error'` event errors.
*/
function toRpcError(rpcCode, cause) {
var err = toError(rpcCode.toLowerCase().replace(/_/g, ' '), cause);
err.rpcCode = (cause && cause.rpcCode) ? cause.rpcCode : rpcCode;
return err;
}
/**
* Provide a helpful error to identify why serialization failed.
*
* @param err {Error} The error to decorate.
* @param obj {...} The object containing fields to validated.
* @param fields {Array} Information about the fields to validate.
*/
function serializationError(msg, obj, fields) {
var details = [];
var i, l, field;
for (i = 0, l = fields.length; i < l; i++) {
field = fields[i];
field.type.isValid(obj[field.name], {errorHook: errorHook});
}
var detailsStr = details
.map(function (obj) {
return f('%s = %j but expected %s', obj.path, obj.value, obj.type);
})
.join(', ');
var err = new Error(f('%s (%s)', msg, detailsStr));
err.details = details;
return err;
function errorHook(parts, any, type) {
var strs = [];
var i, l, part;
for (i = 0, l = parts.length; i < l; i++) {
part = parts[i];
if (isNaN(part)) {
strs.push('.' + part);
} else {
strs.push('[' + part + ']');
}
}
details.push({
path: field.name + strs.join(''),
value: any,
type: type
});
}
}
/**
* Compute a prefix of fixed length from a string.
*
* @param scope {String} Namespace to be hashed.
*/
function normalizedPrefix(scope) {
return scope ?
utils.getHash(scope).readInt16BE(0) << (32 - PREFIX_LENGTH) :
0;
}
/**
* Check whether an ID matches the prefix.
*
* @param id {Integer} Number to check.
* @param prefix {Integer} Already shifted prefix.
*/
function matchesPrefix(id, prefix) {
return ((id ^ prefix) >> (32 - PREFIX_LENGTH)) === 0;
}
/**
* Check whether something is a stream.
*
* @param any {Object} Any object.
*/
function isStream(any) {
// This is a hacky way of checking that the transport is a stream-like
// object. We unfortunately can't use `instanceof Stream` checks since
// some libraries (e.g. websocket-stream) return streams which don't
// inherit from it.
return !!(any && any.pipe);
}
/**
* Get a message, asserting that it exists.
*
* @param svc {Service} The protocol to look into.
* @param name {String} The message's name.
*/
function getExistingMessage(svc, name) {
var msg = svc.message(name);
if (!msg) {
throw new Error(f('unknown message: %s', name));
}
return msg;
}
/**
* Middleware logic.
*
* This is used both in clients and servers to intercept call handling (e.g. to
* populate headers, do access control).
*
* @param params {Object} The following parameters:
* + fns {Array} Array of middleware functions.
* + ctx {Object} Context used to call the middleware functions, onTransition,
* and onCompletion.
* + wreq {WrappedRequest}
* + wres {WrappedResponse}
* + onTransition {Function} End of forward phase callback. It accepts an
* eventual error as single argument. This will be used for the backward
* phase. This function is guaranteed to be called at most once.
* + onCompletion {Function} Final handler, it takes an error as unique
* argument. This function is guaranteed to be only at most once.
* + onError {Function} Error handler, called if an intermediate callback is
* called multiple times.
*/
function chainMiddleware(params) {
var args = [params.wreq, params.wres];
var cbs = [];
var cause; // Backpropagated error.
forward(0);
function forward(pos) {
var isDone = false;
if (pos < params.fns.length) {
params.fns[pos].apply(params.ctx, args.concat(function (err, cb) {
if (isDone) {
params.onError(toError('duplicate forward middleware call', err));
return;
}
isDone = true;
if (
err || (
params.wres && ( // Non one-way messages.
params.wres.error !== undefined ||
params.wres.response !== undefined
)
)
) {
// Stop the forward phase, bypass the handler, and start the backward
// phase. Note that we ignore any callback argument in this case.
cause = err;
backward();
return;
}
if (cb) {
cbs.push(cb);
}
forward(++pos);
}));
} else {
// Done with the middleware forward functions, call the handler.
params.onTransition.apply(params.ctx, args.concat(function (err) {
if (isDone) {
params.onError(toError('duplicate handler call', err));
return;
}
isDone = true;
cause = err;
process.nextTick(backward);
}));
}
}
function backward() {
var cb = cbs.pop();
if (cb) {
var isDone = false;
cb.call(params.ctx, cause, function (err) {
if (isDone) {
params.onError(toError('duplicate backward middleware call', err));
return;
}
// Substitute the error.
cause = err;
isDone = true;
backward();
});
} else {
// Done with all middleware calls.
params.onCompletion.call(params.ctx, cause);
}
}
}
module.exports = {
Adapter: Adapter,
HANDSHAKE_REQUEST_TYPE: HANDSHAKE_REQUEST_TYPE,
HANDSHAKE_RESPONSE_TYPE: HANDSHAKE_RESPONSE_TYPE,
Message: Message,
Registry: Registry,
Service: Service,
discoverProtocol: discoverProtocol,
streams: {
FrameDecoder: FrameDecoder,
FrameEncoder: FrameEncoder,
NettyDecoder: NettyDecoder,
NettyEncoder: NettyEncoder
}
};