avocado-old/packages/state/packer.js

311 lines
7.7 KiB
JavaScript
Raw Normal View History

2019-04-08 07:31:02 -05:00
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;
2019-04-05 22:40:04 -05:00
export class Packer {
constructor() {
this._keyCaret = 0;
this._keys = {};
}
2019-04-08 07:31:02 -05:00
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) {
2019-04-05 22:40:04 -05:00
const newKeys = {};
2019-04-08 07:31:02 -05:00
steps.forEach(({path}) => {
2019-04-05 22:40:04 -05:00
if (!this._keys[path]) {
this._keys[path] = newKeys[path] = this._keyCaret++;
}
});
2019-04-08 07:31:02 -05:00
return newKeys;
}
pack(steps) {
const newKeys = this.computeNewKeys(steps);
const packedSteps = this.packSteps(steps);
2019-04-05 22:40:04 -05:00
const payload = {
2019-04-08 07:31:02 -05:00
s: packedSteps,
2019-04-05 22:40:04 -05:00
};
if (0 !== Object.keys(newKeys).length) {
2019-04-08 07:31:02 -05:00
payload.k = newKeys;
2019-04-05 22:40:04 -05:00
}
return payload;
}
2019-04-08 07:31:02 -05:00
// 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.
2019-04-08 08:20:29 -05:00
view.setUint32(caret, preparedSteps.length);
2019-04-08 07:31:02 -05:00
caret += 4;
preparedSteps.forEach(({op, path, value}) => {
// Op.
2019-04-08 08:20:29 -05:00
view.setUint8(caret, op);
2019-04-08 07:31:02 -05:00
caret += 1;
// Path.
2019-04-08 08:20:29 -05:00
view.setUint32(caret, path);
2019-04-08 07:31:02 -05:00
caret += 4;
2019-04-08 07:37:39 -05:00
if ('remove' === op) {
return;
}
2019-04-08 07:31:02 -05:00
// Value.
2019-04-08 08:20:29 -05:00
view.setUint8(caret, value.type);
2019-04-08 07:31:02 -05:00
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;
2019-04-08 07:42:24 -05:00
view.setUint32(caret, stringLength);
2019-04-08 07:31:02 -05:00
caret += 4;
for (let i = 0; i < stringLength; ++i) {
2019-04-08 07:37:22 -05:00
view.setUint8(caret, value.data.charCodeAt(i));
2019-04-08 07:31:02 -05:00
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;
}
}
2019-04-05 22:40:04 -05:00
}
export class Unpacker {
constructor() {
this._keys = {};
}
unpack(payload) {
2019-04-08 07:31:02 -05:00
const {k: newKeys, s: packedSteps} = payload;
// Track path keys.
2019-04-05 22:40:04 -05:00
for (const key in newKeys) {
this._keys[newKeys[key]] = key;
}
2019-04-08 07:31:02 -05:00
const view = new DataView(packedSteps);
let caret = 0;
2019-04-08 08:20:29 -05:00
const numberOfSteps = view.getUint32(caret);
2019-04-08 07:31:02 -05:00
caret += 4;
const steps = new Array(numberOfSteps);
for (let i = 0; i < numberOfSteps; ++i) {
2019-04-08 08:20:29 -05:00
const packedOp = view.getUint8(caret);
2019-04-08 07:31:02 -05:00
caret += 1;
2019-04-08 08:20:29 -05:00
const packedPath = view.getUint32(caret);
2019-04-08 07:31:02 -05:00
caret += 4;
2019-04-08 07:37:39 -05:00
steps[i] = {
op: this.constructor.unpackOp(packedOp),
path: this._keys[packedPath],
};
if (PACKER_STEP_OP_REMOVE === packedOp) {
continue;
}
2019-04-08 08:20:29 -05:00
const valueType = view.getUint8(caret);
2019-04-08 07:31:02 -05:00
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:
2019-04-08 07:42:24 -05:00
const stringLength = view.getUint32(caret);
2019-04-08 07:31:02 -05:00
caret += 4;
value = '';
for (let j = 0; j < stringLength; ++j) {
2019-04-08 07:37:22 -05:00
value += String.fromCharCode(view.getUint8(caret));
2019-04-08 07:31:02 -05:00
caret += 1;
}
value = decodeURIComponent(value);
break;
}
if (PACKER_STEP_VALUE_JSON === valueType) {
value = JSON.parse(value);
}
2019-04-08 07:37:39 -05:00
steps[i].value = value;
2019-04-08 07:31:02 -05:00
}
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';
}
2019-04-05 22:40:04 -05:00
}
}