const PACKER_STEP_OP_ADD = 0; const PACKER_STEP_OP_REPLACE = 1; const PACKER_STEP_OP_REMOVE = 2; const PACKER_STEP_VALUE_BYTE = 0; const PACKER_STEP_VALUE_UBYTE = 1; const PACKER_STEP_VALUE_SHORT = 2; const PACKER_STEP_VALUE_USHORT = 3; const PACKER_STEP_VALUE_INT = 4; const PACKER_STEP_VALUE_UINT = 5; const PACKER_STEP_VALUE_FLOAT = 6; const PACKER_STEP_VALUE_STRING = 7; const PACKER_STEP_VALUE_JSON = 8; 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. return length + 1 + 4 + 1 + this.constructor.packedValueLength( step.value ); }, 0); } computeNewKeys(steps) { const newKeys = [[], []]; steps.forEach(({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 = new ArrayBuffer(messageLength); const view = new DataView(packedSteps); let caret = 0; // Number of steps. view.setUint32(caret, preparedSteps.length); caret += 4; preparedSteps.forEach(({op, path, value}) => { // Op. view.setUint8(caret, op); caret += 1; // Path. view.setUint32(caret, path); caret += 4; if (PACKER_STEP_OP_REMOVE === op) { return; } // Value. view.setUint8(caret, value.type); caret += 1; switch (value.type) { case PACKER_STEP_VALUE_BYTE: view.setInt8(caret, value.data); caret += 1; break; case PACKER_STEP_VALUE_UBYTE: view.setUint8(caret, value.data); caret += 1; break; case PACKER_STEP_VALUE_SHORT: view.setInt16(caret, value.data); caret += 2; break; case PACKER_STEP_VALUE_USHORT: view.setUint16(caret, value.data); caret += 2; break; case PACKER_STEP_VALUE_INT: view.setInt32(caret, value.data); caret += 4; break; case PACKER_STEP_VALUE_UINT: view.setUint32(caret, value.data); caret += 4; break; case PACKER_STEP_VALUE_FLOAT: view.setFloat32(caret, value.data); caret += 4; break; case PACKER_STEP_VALUE_STRING: case PACKER_STEP_VALUE_JSON: const stringLength = value.data.length; view.setUint32(caret, stringLength); caret += 4; for (let i = 0; i < stringLength; ++i) { view.setUint8(caret, value.data.charCodeAt(i)); 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(({op, path, value}) => { return { op: this.constructor.prepareOpType(op), path: this._keys[path], value: this.constructor.prepareValue(value), }; }); } static prepareValue(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: encodeURIComponent(value), }; } return { type: PACKER_STEP_VALUE_JSON, data: encodeURIComponent(JSON.stringify(value)), }; } static packedValueLength({type, data}) { switch (type) { 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) { const view = new DataView(packedSteps); let caret = 0; const numberOfSteps = view.getUint32(caret); caret += 4; const steps = new Array(numberOfSteps); for (let i = 0; i < numberOfSteps; ++i) { const packedOp = view.getUint8(caret); caret += 1; const packedPath = view.getUint32(caret); caret += 4; steps[i] = { op: this.constructor.unpackOp(packedOp), path: this._keys[packedPath], }; if (PACKER_STEP_OP_REMOVE === packedOp) { continue; } const valueType = view.getUint8(caret); caret += 1; let value; switch (valueType) { case PACKER_STEP_VALUE_BYTE: value = view.getInt8(caret); caret += 1; break; case PACKER_STEP_VALUE_UBYTE: value = view.getUint8(caret); caret += 1; break; case PACKER_STEP_VALUE_SHORT: value = view.getInt16(caret); caret += 2; break; case PACKER_STEP_VALUE_USHORT: value = view.getUint16(caret); caret += 2; break; case PACKER_STEP_VALUE_INT: value = view.getInt32(caret); caret += 4; break; case PACKER_STEP_VALUE_UINT: value = view.getUint32(caret); caret += 4; break; case PACKER_STEP_VALUE_FLOAT: value = view.getFloat32(caret); caret += 4; break; case PACKER_STEP_VALUE_STRING: case PACKER_STEP_VALUE_JSON: const stringLength = view.getUint32(caret); caret += 4; value = ''; for (let j = 0; j < stringLength; ++j) { value += String.fromCharCode(view.getUint8(caret)); caret += 1; } value = decodeURIComponent(value); break; } if (PACKER_STEP_VALUE_JSON === valueType) { value = JSON.parse(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'; } } }