import * as I from 'immutable'; import msgpack from 'msgpack-lite'; const PACKER_STEP_OP_ADD = 0; const PACKER_STEP_OP_REPLACE = 1; const PACKER_STEP_OP_REMOVE = 2; const PACKER_STEP_VALUE_BOOLEAN = 0; const PACKER_STEP_VALUE_BYTE = 1; const PACKER_STEP_VALUE_UBYTE = 2; const PACKER_STEP_VALUE_SHORT = 3; const PACKER_STEP_VALUE_USHORT = 4; const PACKER_STEP_VALUE_INT = 5; const PACKER_STEP_VALUE_UINT = 6; const PACKER_STEP_VALUE_FLOAT = 7; const PACKER_STEP_VALUE_STRING = 8; const PACKER_STEP_VALUE_JSON = 9; export class Packer { constructor() { this._keyCaret = 0; this._keys = {}; } computeMessageLength(preparedSteps) { // num of steps + packed. return 4 + preparedSteps.reduce((length, step) => { // op + path + value type + value. let valueLength; if (PACKER_STEP_OP_REMOVE === step.get('op')) { valueLength = 0; } else { valueLength = this.constructor.packedValueLength( step.get('value') ); } return length + 1 + 4 + 1 + valueLength; }, 0); } computeNewKeys(steps) { const newKeys = [[], []]; steps.forEach((step) => { const path = step.get('path'); if (!this._keys[path]) { this._keys[path] = this._keyCaret++; newKeys[0].push(path); newKeys[1].push(this._keys[path]); } }); return newKeys; } pack(steps) { return this.packSteps(steps); } // Binary packed steps. packSteps(steps) { const preparedSteps = this.prepareSteps(steps); const messageLength = this.computeMessageLength(preparedSteps); const packedSteps = Buffer.allocUnsafe(messageLength); let caret = 0; // Number of steps. packedSteps.writeUInt32LE(preparedSteps.size, caret) caret += 4; preparedSteps.forEach((step) => { // Op. const op = step.get('op'); packedSteps.writeUInt8(op, caret) caret += 1; // Path. const path = step.get('path'); packedSteps.writeUInt32LE(path, caret) caret += 4; if (PACKER_STEP_OP_REMOVE === op) { return; } // Value. const value = step.get('value'); packedSteps.writeUInt8(value.type, caret) caret += 1; switch (value.type) { case PACKER_STEP_VALUE_BOOLEAN: packedSteps.writeInt8(value.data ? 1 : 0, caret) caret += 1; break; case PACKER_STEP_VALUE_BYTE: packedSteps.writeInt8(value.data, caret) caret += 1; break; case PACKER_STEP_VALUE_UBYTE: packedSteps.writeUInt8(value.data, caret) caret += 1; break; case PACKER_STEP_VALUE_SHORT: packedSteps.writeInt16LE(value.data, caret) caret += 2; break; case PACKER_STEP_VALUE_USHORT: packedSteps.writeUInt16LE(value.data, caret) caret += 2; break; case PACKER_STEP_VALUE_INT: packedSteps.writeInt32LE(value.data, caret) caret += 4; break; case PACKER_STEP_VALUE_UINT: packedSteps.writeUInt32LE(value.data, caret) caret += 4; break; case PACKER_STEP_VALUE_FLOAT: packedSteps.writeFloatLE(value.data, caret); caret += 4; break; case PACKER_STEP_VALUE_STRING: case PACKER_STEP_VALUE_JSON: const bufferLength = value.data.length; packedSteps.writeUInt32LE(bufferLength, caret) caret += 4; for (let i = 0; i < bufferLength; ++i) { packedSteps.writeUInt8(value.data.readUInt8(i), caret) caret += 1; } break; } }); return packedSteps; } static prepareOpType(op) { switch (op) { case 'add': return PACKER_STEP_OP_ADD; case 'replace': return PACKER_STEP_OP_REPLACE; case 'remove': return PACKER_STEP_OP_REMOVE; } } prepareSteps(steps) { return steps.map((step) => { const op = step.get('op'); const rawStep = { op: this.constructor.prepareOpType(op), path: this._keys[step.get('path')], }; if ('remove' !== op) { rawStep.value = this.constructor.prepareValue(step.get('value')); } return I.Map(rawStep); }); } static prepareValue(value) { if ('boolean' === typeof value) { return { type: PACKER_STEP_VALUE_BOOLEAN, data: value, }; } if (Number.isInteger(value)) { if (value < 0) { if (value >= -128 && value < 128) { return { type: PACKER_STEP_VALUE_BYTE, data: value, }; } if (value >= -32768 && value < 32768) { return { type: PACKER_STEP_VALUE_SHORT, data: value, }; } return { type: PACKER_STEP_VALUE_INT, data: value, }; } if (value < 256) { return { type: PACKER_STEP_VALUE_UBYTE, data: value, }; } if (value < 65536) { return { type: PACKER_STEP_VALUE_USHORT, data: value, }; } return { type: PACKER_STEP_VALUE_UINT, data: value, }; } if (Number.isFinite(value)) { return { type: PACKER_STEP_VALUE_FLOAT, data: value, }; } if ('string' === typeof value) { return { type: PACKER_STEP_VALUE_STRING, data: Buffer.from(value), }; } if (!value.toJS) { console.log(value); } return { type: PACKER_STEP_VALUE_JSON, data: msgpack.encode(value.toJS()), }; } static packedValueLength({type, data}) { switch (type) { case PACKER_STEP_VALUE_BOOLEAN: case PACKER_STEP_VALUE_BYTE: case PACKER_STEP_VALUE_UBYTE: return 1; case PACKER_STEP_VALUE_SHORT: case PACKER_STEP_VALUE_USHORT: return 2; case PACKER_STEP_VALUE_INT: case PACKER_STEP_VALUE_UINT: return 4; case PACKER_STEP_VALUE_FLOAT: return 4; case PACKER_STEP_VALUE_STRING: case PACKER_STEP_VALUE_JSON: return 4 + data.length; } } } export class Unpacker { constructor() { this._keys = {}; } registerKeys(keys) { for (let i = 0; i < keys[0].length; ++i) { this._keys[keys[1][i]] = keys[0][i]; } } unpack(packedSteps) { let caret = 0; const numberOfSteps = packedSteps.readUInt32LE(caret); caret += 4; const steps = new Array(numberOfSteps); for (let i = 0; i < numberOfSteps; ++i) { const packedOp = packedSteps.readUInt8(caret); caret += 1; const packedPath = packedSteps.readUInt32LE(caret); caret += 4; steps[i] = { op: this.constructor.unpackOp(packedOp), path: this._keys[packedPath], }; if (PACKER_STEP_OP_REMOVE === packedOp) { continue; } const valueType = packedSteps.readUInt8(caret); caret += 1; let value; switch (valueType) { case PACKER_STEP_VALUE_BOOLEAN: value = !!packedSteps.readInt8(caret); caret += 1; break; case PACKER_STEP_VALUE_BYTE: value = packedSteps.readInt8(caret); caret += 1; break; case PACKER_STEP_VALUE_UBYTE: value = packedSteps.readUInt8(caret); caret += 1; break; case PACKER_STEP_VALUE_SHORT: value = packedSteps.readInt16LE(caret); caret += 2; break; case PACKER_STEP_VALUE_USHORT: value = packedSteps.readUInt16LE(caret); caret += 2; break; case PACKER_STEP_VALUE_INT: value = packedSteps.readInt32LE(caret); caret += 4; break; case PACKER_STEP_VALUE_UINT: value = packedSteps.readUInt32LE(caret); caret += 4; break; case PACKER_STEP_VALUE_FLOAT: value = packedSteps.readFloatLE(caret); caret += 4; break; case PACKER_STEP_VALUE_STRING: case PACKER_STEP_VALUE_JSON: const bufferLength = packedSteps.readUInt32LE(caret); caret += 4; value = Buffer.allocUnsafe(bufferLength); for (let j = 0; j < bufferLength; ++j) { value.writeUInt8(packedSteps.readUInt8(caret), j); caret += 1; } break; } if (PACKER_STEP_VALUE_STRING === valueType) { value = value.toString(); } if (PACKER_STEP_VALUE_JSON === valueType) { value = msgpack.decode(value); } steps[i].value = value; } return steps; } static unpackOp(packedOp) { switch (packedOp) { case PACKER_STEP_OP_ADD: return 'add'; case PACKER_STEP_OP_REPLACE: return 'replace'; case PACKER_STEP_OP_REMOVE: return 'remove'; } } }