311 lines
7.7 KiB
JavaScript
311 lines
7.7 KiB
JavaScript
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] = newKeys[path] = this._keyCaret++;
|
|
}
|
|
});
|
|
return newKeys;
|
|
}
|
|
|
|
pack(steps) {
|
|
const newKeys = this.computeNewKeys(steps);
|
|
const packedSteps = this.packSteps(steps);
|
|
const payload = {
|
|
s: packedSteps,
|
|
};
|
|
if (0 !== Object.keys(newKeys).length) {
|
|
payload.k = newKeys;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
// 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.setInt32(caret, preparedSteps.length);
|
|
caret += 4;
|
|
preparedSteps.forEach(({op, path, value}) => {
|
|
// Op.
|
|
view.setInt8(caret, op);
|
|
caret += 1;
|
|
// Path.
|
|
view.setInt32(caret, path);
|
|
caret += 4;
|
|
if ('remove' === op) {
|
|
return;
|
|
}
|
|
// Value.
|
|
view.setInt8(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 = {};
|
|
}
|
|
|
|
unpack(payload) {
|
|
const {k: newKeys, s: packedSteps} = payload;
|
|
// Track path keys.
|
|
for (const key in newKeys) {
|
|
this._keys[newKeys[key]] = key;
|
|
}
|
|
const view = new DataView(packedSteps);
|
|
let caret = 0;
|
|
const numberOfSteps = view.getInt32(caret);
|
|
caret += 4;
|
|
const steps = new Array(numberOfSteps);
|
|
for (let i = 0; i < numberOfSteps; ++i) {
|
|
const packedOp = view.getInt8(caret);
|
|
caret += 1;
|
|
const packedPath = view.getInt32(caret);
|
|
caret += 4;
|
|
steps[i] = {
|
|
op: this.constructor.unpackOp(packedOp),
|
|
path: this._keys[packedPath],
|
|
};
|
|
if (PACKER_STEP_OP_REMOVE === packedOp) {
|
|
continue;
|
|
}
|
|
const valueType = view.getInt8(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';
|
|
}
|
|
}
|
|
|
|
}
|