UNPKG

cloudworker-proxy

Version:
283 lines (240 loc) 6.57 kB
'use strict'; const util = require('util'); const EventEmitter = require('events').EventEmitter; const parser = require('./parser'); const emit = EventEmitter.prototype.emit; const on = EventEmitter.prototype.on; const slice = Array.prototype.slice; exports = module.exports = Socket; /** * Bind `obj` to `fn`. * * @param {Object} obj * @param {Function|String} fn or string * @return {Function} */ function bind(obj, fn, ...args) { if (typeof fn === 'string') fn = obj[fn]; if (typeof fn !== 'function') throw new Error('bind() requires a function'); return function (..._args) { return fn.apply(obj, args.concat(slice.call(_args))); }; } /** * Base event name for messaging. * * @api public */ exports.event = '$stream'; exports.events = ['error', 'newListener', 'removeListener']; util.inherits(Socket, EventEmitter); /** * Bidirectional stream socket which wraps Socket.IO. * * @param {socket.io#Socket} socket.io * @api public */ function Socket(sio, options) { if (!(this instanceof Socket)) { return new Socket(sio, options); } EventEmitter.call(this); options = options || {}; this.sio = sio; this.forceBase64 = !!options.forceBase64; this.streams = {}; this.encoder = new parser.Encoder(); this.decoder = new parser.Decoder(); const eventName = exports.event; sio.on(eventName, bind(this, emit)); sio.on(`${eventName}-read`, bind(this, '_onread')); sio.on(`${eventName}-write`, bind(this, '_onwrite')); sio.on(`${eventName}-end`, bind(this, '_onend')); sio.on(`${eventName}-error`, bind(this, '_onerror')); sio.on('error', bind(this, emit, 'error')); sio.on('disconnect', bind(this, '_ondisconnect')); this.encoder.on('stream', bind(this, '_onencode')); this.decoder.on('stream', bind(this, '_ondecode')); } /** * Original emit function. * * @api private */ Socket.prototype.$emit = emit; /** * Emits streams to this corresponding server/client. * * @return {Socket} self * @api public */ Socket.prototype.emit = function (type) { /* eslint-disable prefer-rest-params,prefer-spread */ if (~exports.events.indexOf(type)) { return emit.apply(this, arguments); } this._stream.apply(this, arguments); return this; }; Socket.prototype.on = function (type, listener) { if (~exports.events.indexOf(type)) { return on.apply(this, arguments); } this._onstream(type, listener); return this; }; /** * Sends a new stream request. * * @param {String} event type * @api private */ Socket.prototype._stream = function (type) { const self = this; let args = slice.call(arguments, 1); const ack = args[args.length - 1]; if (typeof ack === 'function') { args[args.length - 1] = function () { let _args = slice.call(arguments); _args = self.decoder.decode(_args); ack.apply(this, _args); }; } args = this.encoder.encode(args); const sio = this.sio; sio.emit.apply(sio, [exports.event, type].concat(args)); }; /** * Notifies the read event. * * @api private */ Socket.prototype._read = function (id, size) { this.sio.emit(`${exports.event}-read`, id, size); }; /** * Requests to write a chunk. * * @api private */ Socket.prototype._write = function (id, chunk, encoding, callback) { if (Buffer.isBuffer(chunk)) { if (this.forceBase64) { encoding = 'base64'; chunk = chunk.toString(encoding); } else if (!global.Buffer) { // socket.io can't handle Buffer when using browserify. if (chunk.toArrayBuffer) { chunk = chunk.toArrayBuffer(); } else { chunk = chunk.buffer; } } } this.sio.emit(`${exports.event}-write`, id, chunk, encoding, callback); }; Socket.prototype._end = function (id) { this.sio.emit(`${exports.event}-end`, id); }; Socket.prototype._error = function (id, err) { this.sio.emit(`${exports.event}-error`, id, err.message || err); }; /** * Handles a new stream request. * * @param {String} event type * @param {Function} listener * * @api private */ Socket.prototype._onstream = function (type, listener) { if (typeof listener !== 'function') { throw new TypeError('listener must be a function'); } function onstream() { const self = this; let args = slice.call(arguments); const ack = args[args.length - 1]; if (typeof ack === 'function') { args[args.length - 1] = function () { let _args = slice.call(arguments); _args = self.encoder.encode(_args); ack.apply(this, _args); }; } args = this.decoder.decode(args); listener.apply(this, args); } // for removeListener onstream.listener = listener; on.call(this, type, onstream); }; Socket.prototype._onread = function (id, size) { const stream = this.streams[id]; if (stream) { stream._onread(size); } }; Socket.prototype._onwrite = function (id, chunk, encoding, callback) { const stream = this.streams[id]; if (!stream) { callback(`invalid stream id: ${id}`); return; } if (global.ArrayBuffer && chunk instanceof ArrayBuffer) { // make sure that chunk is a buffer for stream chunk = new Buffer(new Uint8Array(chunk)); } stream._onwrite(chunk, encoding, callback); }; Socket.prototype._onend = function (id) { const stream = this.streams[id]; if (!stream) { return; } stream._end(); }; Socket.prototype._onerror = function (id, message) { const stream = this.streams[id]; if (!stream) { return; } const err = new Error(message); err.remote = true; stream.emit('error', err); }; Socket.prototype._ondisconnect = function () { let stream; const keys = Object.keys(this.streams); for (const id of keys) { stream = this.streams[id]; stream.destroy(); // Close streams when the underlaying // socket.io connection is closed (regardless why) stream.emit('close'); stream.emit('error', new Error('Connection aborted')); } }; Socket.prototype._onencode = function (stream) { if (stream.socket || stream.destroyed) { throw new Error('stream has already been sent.'); } const id = stream.id; if (this.streams[id]) { throw new Error(`Encoded stream already exists: ${id}`); } this.streams[id] = stream; stream.socket = this; }; Socket.prototype._ondecode = function (stream) { const id = stream.id; if (this.streams[id]) { this._error(id, new Error(`Decoded stream already exists: ${id}`)); return; } this.streams[id] = stream; stream.socket = this; }; Socket.prototype.cleanup = function (id) { delete this.streams[id]; };