feat: packets!

This commit is contained in:
cha0s 2019-04-11 15:26:13 -05:00
parent 9a4bb4ce08
commit 6da4e21835
7 changed files with 225 additions and 48 deletions

View File

@ -1,36 +1,30 @@
import {EventEmitter} from 'events';
import io from 'socket.io-client';
const exceptions = [
'connect',
];
import {SocketIoParser, allPackets, idFromPacket} from '@avocado/packet';
class SocketClient extends EventEmitter {
constructor(address) {
super();
this.socket = io(address, {
parser: SocketIoParser,
path: '/avocado',
});
this.socket.on('connect', () => {
this.emit('connect');
});
this.socket.on('message', (...args) => {
this.emit('message', ...args);
for (const Packet of allPackets()) {
const id = idFromPacket(Packet);
this.socket.on(id, (packet) => {
this.emit('packet', packet);
});
}
on(name, fn) {
if (-1 === exceptions.indexOf(name)) {
super.on(name, fn);
}
else {
this.socket.on(name, fn);
}
}
send(...args) {
this.socket.send(...args);
send(packet) {
const id = idFromPacket(packet.constructor);
this.socket.emit(id, packet.data);
}
}

24
packages/packet/index.js Normal file
View File

@ -0,0 +1,24 @@
export class Packet {
constructor(data) {
this.data = data;
}
static get schema() {
return {
_id: 'uint8',
nsp: 'string',
};
}
}
export {
allPackets,
idFromPacket,
packetFromId,
registerPacket,
} from './registry';
import * as SocketIoParser from './socket.io-parser';
export {SocketIoParser};

View File

@ -0,0 +1,12 @@
{
"name": "@avocado/packet",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"dependencies": {
"@avocado/core": "1.x",
"@avocado/mixins": "1.x",
"schemapack": "1.4.2"
}
}

View File

@ -0,0 +1,21 @@
let eventId = 0;
const packetToIdMap = new Map();
const idToPacketMap = new Map();
export function allPackets() {
return Array.from(idToPacketMap.values());
}
export function idFromPacket(Packet) {
return packetToIdMap.get(Packet);
}
export function packetFromId(id) {
return idToPacketMap.get(id);
}
export function registerPacket(Packet) {
if (packetToIdMap.has(Packet)) {
return;
}
const id = eventId++;
packetToIdMap.set(Packet, id);
idToPacketMap.set(id, Packet);
return id;
}

View File

@ -0,0 +1,109 @@
import schemapack from 'schemapack';
import {compose} from '@avocado/core';
import {EventEmitter} from '@avocado/mixins';
import {allPackets, idFromPacket, packetFromId} from './registry';
/**
* Packet types (see https://github.com/socketio/socket.io-protocol)
*/
const TYPES = {
EVENT: 2,
ERROR: 4,
BINARY_EVENT: 5,
};
const errorPacket = {
type: TYPES.ERROR,
data: 'parser error',
};
let schemas = undefined;
function schemaFromId(id) {
if (!schemas) {
schemas = {};
for (const Packet of allPackets()) {
const id_ = idFromPacket(Packet);
schemas[id_] = schemapack.build(Packet.schema);
}
}
return schemas[id];
}
class Encoder {
encode(packet, callback) {
switch (packet.type) {
case TYPES.EVENT:
case TYPES.BINARY_EVENT:
return callback([this.pack(packet)]);
default:
return callback([JSON.stringify(packet)]);
}
}
pack(packet) {
const eventId = packet.data[0];
const schema = schemaFromId(eventId);
if (!schema) {
throw new Error('unknown schema with id: ' + eventId);
}
return schema.encode({
_id: eventId,
data: packet.data[1],
nsp: packet.nsp,
});
}
}
const decorateDecoder = compose(
EventEmitter,
);
class Decoder extends decorateDecoder(class {}) {
add(obj) {
if (typeof obj === 'string') {
this.parseJSON(obj);
}
else {
this.parseBinary(obj);
}
}
destroy() {}
parseBinary(obj) {
const view = new Uint8Array(obj);
const packetId = view[0];
try {
const schema = schemaFromId(packetId);
if (!schema) {
throw new Error(`unknown schema with id: ${packetId}`);
}
const {data, nsp} = schema.decode(obj);
const Packet = packetFromId(packetId);
this.emit('decoded', {
type: TYPES.EVENT,
data: [packetId, new Packet(data)],
nsp,
});
} catch (e) {
this.emit('decoded', errorPacket);
}
}
parseJSON(obj) {
try {
this.emit('decoded', JSON.parse(obj));
}
catch (e) {
this.emit('decoded', errorPacket);
}
}
}
export {Decoder, Encoder};

View File

@ -1,21 +1,40 @@
const {EventEmitter} = require('events');
const SocketServer = require('socket.io');
import {SocketIoParser, allPackets, idFromPacket} from '@avocado/packet';
export class Server extends EventEmitter {
constructor(httpServer) {
super();
this.io = new SocketServer(httpServer, {
parser: SocketIoParser,
path: '/avocado',
serveClient: false,
});
this.io.on('connect', (socket) => {
this.emit('connect', socket);
this.emit('connect', new ServerSocket(socket));
});
}
broadcast(message) {
this.io.send(message);
}
class ServerSocket extends EventEmitter {
constructor(socket) {
super();
this.socket = socket;
for (const Packet of allPackets()) {
const id = idFromPacket(Packet);
this.socket.on(id, (packet) => {
this.emit('packet', packet);
});
}
}
send(packet) {
const id = idFromPacket(packet.constructor);
this.socket.emit(id, packet.data);
}
}

View File

@ -49,61 +49,60 @@ export class Packer {
packSteps(steps) {
const preparedSteps = this.prepareSteps(steps);
const messageLength = this.computeMessageLength(preparedSteps);
const packedSteps = new ArrayBuffer(messageLength);
const view = new DataView(packedSteps);
const packedSteps = Buffer.allocUnsafe(messageLength);
let caret = 0;
// Number of steps.
view.setUint32(caret, preparedSteps.length);
packedSteps.writeUInt32LE(preparedSteps.length, caret)
caret += 4;
preparedSteps.forEach(({op, path, value}) => {
// Op.
view.setUint8(caret, op);
packedSteps.writeUInt8(op, caret)
caret += 1;
// Path.
view.setUint32(caret, path);
packedSteps.writeUInt32LE(path, caret)
caret += 4;
if (PACKER_STEP_OP_REMOVE === op) {
return;
}
// Value.
view.setUint8(caret, value.type);
packedSteps.writeUInt8(value.type, caret)
caret += 1;
switch (value.type) {
case PACKER_STEP_VALUE_BYTE:
view.setInt8(caret, value.data);
packedSteps.writeInt8(value.data, caret)
caret += 1;
break;
case PACKER_STEP_VALUE_UBYTE:
view.setUint8(caret, value.data);
packedSteps.writeUInt8(value.data, caret)
caret += 1;
break;
case PACKER_STEP_VALUE_SHORT:
view.setInt16(caret, value.data);
packedSteps.writeInt16LE(value.data, caret)
caret += 2;
break;
case PACKER_STEP_VALUE_USHORT:
view.setUint16(caret, value.data);
packedSteps.writeUInt16LE(value.data, caret)
caret += 2;
break;
case PACKER_STEP_VALUE_INT:
view.setInt32(caret, value.data);
packedSteps.writeInt32LE(value.data, caret)
caret += 4;
break;
case PACKER_STEP_VALUE_UINT:
view.setUint32(caret, value.data);
packedSteps.writeUInt32LE(value.data, caret)
caret += 4;
break;
case PACKER_STEP_VALUE_FLOAT:
view.setFloat32(caret, value.data);
packedSteps.writeFloatLE(value.data, caret);
caret += 4;
break;
case PACKER_STEP_VALUE_STRING:
case PACKER_STEP_VALUE_JSON:
const stringLength = value.data.length;
view.setUint32(caret, stringLength);
packedSteps.writeUInt32LE(stringLength, caret)
caret += 4;
for (let i = 0; i < stringLength; ++i) {
view.setUint8(caret, value.data.charCodeAt(i));
packedSteps.writeUInt8(value.data.charCodeAt(i), caret)
caret += 1;
}
break;
@ -222,15 +221,14 @@ export class Unpacker {
}
unpack(packedSteps) {
const view = new DataView(packedSteps);
let caret = 0;
const numberOfSteps = view.getUint32(caret);
const numberOfSteps = packedSteps.readUInt32LE(caret);
caret += 4;
const steps = new Array(numberOfSteps);
for (let i = 0; i < numberOfSteps; ++i) {
const packedOp = view.getUint8(caret);
const packedOp = packedSteps.readUInt8(caret);
caret += 1;
const packedPath = view.getUint32(caret);
const packedPath = packedSteps.readUInt32LE(caret);
caret += 4;
steps[i] = {
op: this.constructor.unpackOp(packedOp),
@ -239,45 +237,45 @@ export class Unpacker {
if (PACKER_STEP_OP_REMOVE === packedOp) {
continue;
}
const valueType = view.getUint8(caret);
const valueType = packedSteps.readUInt8(caret);
caret += 1;
let value;
switch (valueType) {
case PACKER_STEP_VALUE_BYTE:
value = view.getInt8(caret);
value = packedSteps.readInt8(caret);
caret += 1;
break;
case PACKER_STEP_VALUE_UBYTE:
value = view.getUint8(caret);
value = packedSteps.readUInt8(caret);
caret += 1;
break;
case PACKER_STEP_VALUE_SHORT:
value = view.getInt16(caret);
value = packedSteps.readInt16LE(caret);
caret += 2;
break;
case PACKER_STEP_VALUE_USHORT:
value = view.getUint16(caret);
value = packedSteps.readUInt16LE(caret);
caret += 2;
break;
case PACKER_STEP_VALUE_INT:
value = view.getInt32(caret);
value = packedSteps.readInt32LE(caret);
caret += 4;
break;
case PACKER_STEP_VALUE_UINT:
value = view.getUint32(caret);
value = packedSteps.readUInt32LE(caret);
caret += 4;
break;
case PACKER_STEP_VALUE_FLOAT:
value = view.getFloat32(caret);
value = packedSteps.readFloatLE(caret);
caret += 4;
break;
case PACKER_STEP_VALUE_STRING:
case PACKER_STEP_VALUE_JSON:
const stringLength = view.getUint32(caret);
const stringLength = packedSteps.readUInt32LE(caret);
caret += 4;
value = '';
for (let j = 0; j < stringLength; ++j) {
value += String.fromCharCode(view.getUint8(caret));
value += String.fromCharCode(packedSteps.readUInt8(caret));
caret += 1;
}
value = decodeURIComponent(value);