@ckb-lumos/codec
Version:
Make your own molecule binding in JavaScript(TypeScript)
340 lines (324 loc) • 12.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.array = array;
exports.dynvec = dynvec;
exports.fixvec = fixvec;
exports.option = option;
exports.struct = struct;
exports.table = table;
exports.union = union;
exports.vector = vector;
var _base = require("../base");
var _number = require("../number");
var _bytes = require("../bytes");
var _error = require("../error");
var _highOrder = require("../high-order");
/**
* | Type | Header | Body |
* |--------+--------------------------------------------------+-----------------------------------|
* | array | | item-0 | item-1 | ... | item-N |
* | struct | | field-0 | field-1 | ... | field-N |
* | fixvec | items-count | item-0 | item-1 | ... | item-N |
* | dynvec | full-size | offset-0 | offset-1 | ... | offset-N | item-0 | item-1 | ... | item-N |
* | table | full-size | offset-0 | offset-1 | ... | offset-N | filed-0 | field-1 | ... | field-N |
* | option | | item or none (zero bytes) |
* | union | item-type-id | item |
*/
// prettier-ignore
/**
* A codec for struct and table of Molecule
*/
/**
* A codec for option of Molecule
*/
/**
* A code for array and vector of Molecule
*/
/**
* A molecule codec for `
*/
/**
* The array is a fixed-size type: it has a fixed-size inner type and a fixed length.
* The size of an array is the size of inner type times the length.
* @param itemCodec the fixed-size array item codec
* @param itemCount
*/
function array(itemCodec, itemCount) {
const enhancedArrayCodec = (0, _highOrder.createArrayCodec)(itemCodec);
return (0, _base.createFixedBytesCodec)({
byteLength: itemCodec.byteLength * itemCount,
pack(items) {
const itemsBuf = enhancedArrayCodec.pack(items);
return (0, _bytes.concat)(...itemsBuf);
},
unpack(buf) {
const result = [];
const itemLength = itemCodec.byteLength;
for (let offset = 0; offset < buf.byteLength; offset += itemLength) {
result.push(itemCodec.unpack(buf.slice(offset, offset + itemLength)));
}
return result;
}
});
}
function diff(x1, x2) {
return x1.filter(x => !x2.includes(x));
}
function checkShape(shape, fields) {
const shapeKeys = Object.keys(shape);
const missingFields = diff(shapeKeys, fields);
const missingShape = diff(fields, shapeKeys);
if (missingFields.length > 0 || missingShape.length > 0) {
throw new Error(`Invalid shape: missing fields ${missingFields.join(", ")} or shape ${missingShape.join(", ")}`);
}
}
/**
* Struct is a fixed-size type: all fields in struct are fixed-size and it has a fixed quantity of fields.
* The size of a struct is the sum of all fields' size.
* @param shape a object contains all fields' codec
* @param fields the shape's keys. It provide an order for serialization/deserialization.
*/
function struct(shape, fields) {
checkShape(shape, fields);
const objectCodec = (0, _highOrder.createObjectCodec)(shape);
return (0, _base.createFixedBytesCodec)({
byteLength: fields.reduce((sum, field) => sum + shape[field].byteLength, 0),
pack(obj) {
const packed = objectCodec.pack(obj);
return fields.reduce((result, field) => {
return (0, _bytes.concat)(result, packed[field]);
}, Uint8Array.from([]));
},
unpack(buf) {
const result = {};
let offset = 0;
fields.forEach(field => {
const itemCodec = shape[field];
const itemBuf = buf.slice(offset, offset + itemCodec.byteLength);
Object.assign(result, {
[field]: itemCodec.unpack(itemBuf)
});
offset = offset + itemCodec.byteLength;
});
return result;
}
});
}
/**
* Vector with fixed size item codec
* @param itemCodec fixed-size vector item codec
*/
function fixvec(itemCodec) {
return (0, _base.createBytesCodec)({
pack(items) {
const arrayCodec = (0, _highOrder.createArrayCodec)(itemCodec);
return (0, _bytes.concat)(_number.Uint32LE.pack(items.length), arrayCodec.pack(items).reduce((buf, item) => (0, _bytes.concat)(buf, item), new ArrayBuffer(0)));
},
unpack(buf) {
if (buf.byteLength < 4) {
throw new Error(`fixvec: buffer is too short, expected at least 4 bytes, got ${buf.byteLength}`);
}
const itemCount = _number.Uint32LE.unpack(buf.slice(0, 4));
return array(itemCodec, itemCount).unpack(buf.slice(4));
}
});
}
/**
* Vector with dynamic size item codec
* @param itemCodec the vector item codec. It can be fixed-size or dynamic-size.
* For example, you can create a recursive vector with this.
*/
function dynvec(itemCodec) {
return (0, _base.createBytesCodec)({
pack(obj) {
const arrayCodec = (0, _highOrder.createArrayCodec)(itemCodec);
const packed = arrayCodec.pack(obj).reduce((result, item) => {
const packedHeader = _number.Uint32LE.pack(result.offset);
return {
header: (0, _bytes.concat)(result.header, packedHeader),
body: (0, _bytes.concat)(result.body, item),
offset: result.offset + item.byteLength
};
}, {
header: new ArrayBuffer(0),
body: new ArrayBuffer(0),
offset: 4 + obj.length * 4
});
const packedTotalSize = _number.Uint32LE.pack(packed.header.byteLength + packed.body.byteLength + 4);
return (0, _bytes.concat)(packedTotalSize, packed.header, packed.body);
},
unpack(buf) {
const totalSize = _number.Uint32LE.unpack(buf.slice(0, 4));
if (totalSize !== buf.byteLength) {
throw new Error(`Invalid buffer size, read from header: ${totalSize}, actual: ${buf.byteLength}`);
}
const result = [];
if (totalSize <= 4) {
return result;
} else {
const offset0 = _number.Uint32LE.unpack(buf.slice(4, 8));
const itemCount = (offset0 - 4) / 4;
const offsets = new Array(itemCount).fill(1).map((_, index) => _number.Uint32LE.unpack(buf.slice(4 + index * 4, 8 + index * 4)));
offsets.push(totalSize);
const result = [];
for (let index = 0; index < offsets.length - 1; index++) {
const start = offsets[index];
const end = offsets[index + 1];
const itemBuf = buf.slice(start, end);
result.push(itemCodec.unpack(itemBuf));
}
return result;
}
}
});
}
/**
* General vector codec, if `itemCodec` is fixed size type, it will create a fixvec codec, otherwise a dynvec codec will be created.
* @param itemCodec
*/
function vector(itemCodec) {
if ((0, _base.isFixedCodec)(itemCodec)) {
return fixvec(itemCodec);
}
return dynvec(itemCodec);
}
/**
* Table is a dynamic-size type. It can be considered as a dynvec but the length is fixed.
* @param shape The table shape, item codec can be dynamic size
* @param fields the shape's keys. Also provide an order for pack/unpack.
*/
function table(shape, fields) {
checkShape(shape, fields);
return (0, _base.createBytesCodec)({
pack(obj) {
const headerLength = 4 + fields.length * 4;
const objectCodec = (0, _highOrder.createObjectCodec)(shape);
const packedObj = objectCodec.pack(obj);
const packed = fields.reduce((result, field) => {
const packedItem = packedObj[field];
const packedOffset = _number.Uint32LE.pack(result.offset);
return {
header: (0, _bytes.concat)(result.header, packedOffset),
body: (0, _bytes.concat)(result.body, packedItem),
offset: result.offset + packedItem.byteLength
};
}, {
header: new ArrayBuffer(0),
body: new ArrayBuffer(0),
offset: headerLength
});
const packedTotalSize = _number.Uint32LE.pack(packed.header.byteLength + packed.body.byteLength + 4);
return (0, _bytes.concat)(packedTotalSize, packed.header, packed.body);
},
unpack(buf) {
const totalSize = _number.Uint32LE.unpack(buf.slice(0, 4));
if (totalSize !== buf.byteLength) {
throw new Error(`Invalid buffer size, read from header: ${totalSize}, actual: ${buf.byteLength}`);
}
if (totalSize <= 4 || fields.length === 0) {
return {};
} else {
const offsets = fields.map((_, index) => _number.Uint32LE.unpack(buf.slice(4 + index * 4, 8 + index * 4)));
offsets.push(totalSize);
const obj = {};
for (let index = 0; index < offsets.length - 1; index++) {
const start = offsets[index];
const end = offsets[index + 1];
const field = fields[index];
const itemCodec = shape[field];
const itemBuf = buf.slice(start, end);
Object.assign(obj, {
[field]: itemCodec.unpack(itemBuf)
});
}
return obj;
}
}
});
}
/**
* Union is a dynamic-size type.
* Serializing a union has two steps:
* - Serialize an item type id in bytes as a 32 bit unsigned integer in little-endian. The item type id is the index of the inner items, and it's starting at 0.
* - Serialize the inner item.
* @param itemCodec the union item record
* @param fields the union item keys, can be an array or an object with custom id
* @example
* // without custom id
* union({ cafe: Uint8, bee: Uint8 }, ['cafe', 'bee'])
* // with custom id
* union({ cafe: Uint8, bee: Uint8 }, { cafe: 0xcafe, bee: 0xbee })
*/
function union(itemCodec, fields) {
checkShape(itemCodec, Array.isArray(fields) ? fields : Object.keys(fields));
// check duplicated id
if (!Array.isArray(fields)) {
const ids = Object.values(fields);
if (ids.length !== new Set(ids).size) {
throw new Error(`Duplicated id in union: ${ids.join(", ")}`);
}
}
return (0, _base.createBytesCodec)({
pack(obj) {
const availableFields = Object.keys(itemCodec);
const type = obj.type;
const typeName = `Union(${availableFields.join(" | ")})`;
/* c8 ignore next */
if (typeof type !== "string") {
throw new _error.CodecBaseParseError(`Invalid type in union, type must be a string`, typeName);
}
const fieldId = Array.isArray(fields) ? fields.indexOf(type) : fields[type];
if (fieldId < 0) {
throw new _error.CodecBaseParseError(`Unknown union type: ${String(obj.type)}`, typeName);
}
const packedFieldIndex = _number.Uint32LE.pack(fieldId);
const packedBody = itemCodec[type].pack(obj.value);
return (0, _bytes.concat)(packedFieldIndex, packedBody);
},
unpack(buf) {
const fieldId = _number.Uint32LE.unpack(buf.slice(0, 4));
const type = (() => {
if (Array.isArray(fields)) {
return fields[fieldId];
}
const entry = Object.entries(fields).find(([, id]) => id === fieldId);
return entry === null || entry === void 0 ? void 0 : entry[0];
})();
if (!type) {
throw new Error(`Unknown union field id: ${fieldId}, only ${fields} are allowed`);
}
return {
type,
value: itemCodec[type].unpack(buf.slice(4))
};
}
});
}
/**
* Option is a dynamic-size type.
* Serializing an option depends on whether it is empty or not:
* - if it's empty, there is zero bytes (the size is 0).
* - if it's not empty, just serialize the inner item (the size is same as the inner item's size).
* @param itemCodec
*/
function option(itemCodec) {
return (0, _base.createBytesCodec)({
pack(obj) {
const nullableCodec = (0, _highOrder.createNullableCodec)(itemCodec);
if (obj !== undefined && obj !== null) {
return nullableCodec.pack(obj);
} else {
return Uint8Array.from([]);
}
},
unpack(buf) {
if (buf.byteLength === 0) {
return undefined;
}
return itemCodec.unpack(buf);
}
});
}
//# sourceMappingURL=layout.js.map