cloudworker-proxy
Version:
An api gateway for cloudflare workers
283 lines (240 loc) • 6.57 kB
JavaScript
;
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];
};