@eyhn/msgpack-stream
Version:
MessagePack for ECMA-262/JavaScript/TypeScript
437 lines • 14.1 kB
JavaScript
import { utf8EncodeJs, utf8Count, TEXT_ENCODER_THRESHOLD, utf8EncodeTE } from "./utils/utf8.mjs";
import { ExtensionCodec } from "./ExtensionCodec.mjs";
import { setInt64, setUint64 } from "./utils/int.mjs";
import { ensureUint8Array } from "./utils/typedArrays.mjs";
export const DEFAULT_MAX_DEPTH = 100;
export const DEFAULT_BUFFER_SIZE = 2048;
export class StreamEncoder {
constructor(extensionCodec = ExtensionCodec.defaultCodec, context = undefined, maxDepth = DEFAULT_MAX_DEPTH, bufferSize = DEFAULT_BUFFER_SIZE, sortKeys = false, forceFloat32 = false, ignoreUndefined = false, forceIntegerToFloat = false) {
this.extensionCodec = extensionCodec;
this.context = context;
this.maxDepth = maxDepth;
this.bufferSize = bufferSize;
this.sortKeys = sortKeys;
this.forceFloat32 = forceFloat32;
this.ignoreUndefined = ignoreUndefined;
this.forceIntegerToFloat = forceIntegerToFloat;
this.view = new DataView(new ArrayBuffer(2048));
this.bytes = new Uint8Array(this.view.buffer);
}
*encode(object) {
const buffer = new Uint8Array(this.bufferSize);
let pos = 0;
for (let chunk of this.doEncode(object, 1)) {
if (chunk.length >= buffer.length) {
if (pos >= 0) {
yield buffer.slice(0, pos);
}
pos = 0;
yield chunk.slice();
}
else {
while (chunk.length > 0) {
const readSize = Math.min(buffer.length - pos, chunk.length);
const read = chunk.subarray(0, readSize);
buffer.set(read, pos);
pos += read.length;
if (pos === buffer.length) {
yield buffer.slice();
pos = 0;
}
chunk = chunk.subarray(readSize, chunk.length);
}
}
}
if (pos >= 0) {
yield buffer.subarray(0, pos);
}
}
doEncode(object, depth) {
if (depth > this.maxDepth) {
throw new Error(`Too deep objects in depth ${depth}`);
}
if (object == null) {
return this.encodeNil();
}
else if (typeof object === "boolean") {
return this.encodeBoolean(object);
}
else if (typeof object === "number") {
return this.encodeNumber(object);
}
else if (typeof object === "string") {
return this.encodeString(object);
}
else {
return this.encodeObject(object, depth);
}
}
ensureBufferSizeToWrite(sizeToWrite) {
if (this.view.byteLength < sizeToWrite) {
this.resizeBuffer(sizeToWrite * 2);
}
}
resizeBuffer(newSize) {
const newBuffer = new ArrayBuffer(newSize);
const newBytes = new Uint8Array(newBuffer);
const newView = new DataView(newBuffer);
this.view = newView;
this.bytes = newBytes;
}
encodeNil() {
return this.writeU8(0xc0);
}
encodeBoolean(object) {
if (object === false) {
return this.writeU8(0xc2);
}
else {
return this.writeU8(0xc3);
}
}
encodeNumber(object) {
if (Number.isSafeInteger(object) && !this.forceIntegerToFloat) {
if (object >= 0) {
if (object < 0x80) {
// positive fixint
return this.writeU8(object);
}
else if (object < 0x100) {
// uint 8
return this.writeU8U8(0xcc, object);
}
else if (object < 0x10000) {
// uint 16
return this.writeU8U16(0xcd, object);
}
else if (object < 0x100000000) {
// uint 32
return this.writeU8U32(0xce, object);
}
else {
// uint 64
return this.writeU8U64(0xcf, object);
}
}
else {
if (object >= -0x20) {
// negative fixint
return this.writeU8(0xe0 | (object + 0x20));
}
else if (object >= -0x80) {
// int 8
return this.writeU8I8(0xd0, object);
}
else if (object >= -0x8000) {
// int 16
return this.writeU8I16(0xd1, object);
}
else if (object >= -0x80000000) {
// int 32
return this.writeU8I32(0xd2, object);
}
else {
// int 64
return this.writeU8I64(0xd3, object);
}
}
}
else {
// non-integer numbers
if (this.forceFloat32) {
// float 32
return this.writeU8F32(0xca, object);
}
else {
// float 64
return this.writeU8F64(0xcb, object);
}
}
}
buildStringHeader(byteLength) {
if (byteLength < 32) {
// fixstr
return [0xa0 + byteLength];
}
else if (byteLength < 0x100) {
// str 8
return [0xd9, byteLength];
}
else if (byteLength < 0x10000) {
// str 16
const bytes = new DataView(new ArrayBuffer(3));
bytes.setUint8(0, 0xda);
bytes.setUint16(1, byteLength);
return new Uint8Array(bytes.buffer);
}
else if (byteLength < 0x100000000) {
// str 32
const bytes = new DataView(new ArrayBuffer(5));
bytes.setUint8(0, 0xdb);
bytes.setUint32(1, byteLength);
return new Uint8Array(bytes.buffer);
}
else {
throw new Error(`Too long string: ${byteLength} bytes in UTF-8`);
}
}
encodeString(object) {
const maxHeaderSize = 1 + 4;
const strLength = object.length;
if (strLength > TEXT_ENCODER_THRESHOLD) {
const byteLength = utf8Count(object);
this.ensureBufferSizeToWrite(maxHeaderSize + byteLength);
const header = this.buildStringHeader(byteLength);
this.bytes.set(header, 0);
utf8EncodeTE(object, this.bytes, header.length);
return this.writeBuffer(byteLength + header.length);
}
else {
const byteLength = utf8Count(object);
this.ensureBufferSizeToWrite(maxHeaderSize + byteLength);
const header = this.buildStringHeader(byteLength);
this.bytes.set(header, 0);
utf8EncodeJs(object, this.bytes, header.length);
return this.writeBuffer(byteLength + header.length);
}
}
encodeObject(object, depth) {
// try to encode objects with custom codec first of non-primitives
const ext = this.extensionCodec.tryToEncode(object, this.context);
if (ext != null) {
return this.encodeExtension(ext);
}
else if (Array.isArray(object)) {
return this.encodeArray(object, depth);
}
else if (ArrayBuffer.isView(object)) {
return this.encodeBinary(object);
}
else if (typeof object === "object") {
return this.encodeMap(object, depth);
}
else {
// symbol, function and other special object come here unless extensionCodec handles them.
throw new Error(`Unrecognized object: ${Object.prototype.toString.apply(object)}`);
}
}
*encodeBinary(object) {
const size = object.byteLength;
if (size < 0x100) {
// bin 8
yield* this.writeU8U8(0xc4, size);
}
else if (size < 0x10000) {
// bin 16
yield* this.writeU8U16(0xc5, size);
}
else if (size < 0x100000000) {
// bin 32
yield* this.writeU8U32(0xc6, size);
}
else {
throw new Error(`Too large binary: ${size}`);
}
const bytes = ensureUint8Array(object);
yield* this.writeU8a(bytes);
}
*encodeArray(object, depth) {
const size = object.length;
if (size < 16) {
// fixarray
yield* this.writeU8(0x90 + size);
}
else if (size < 0x10000) {
// array 16
yield* this.writeU8U16(0xdc, size);
}
else if (size < 0x100000000) {
// array 32
yield* this.writeU8U32(0xdd, size);
}
else {
throw new Error(`Too large array: ${size}`);
}
for (const item of object) {
yield* this.doEncode(item, depth + 1);
}
}
countWithoutUndefined(object, keys) {
let count = 0;
for (const key of keys) {
if (object[key] !== undefined) {
count++;
}
}
return count;
}
*encodeMap(object, depth) {
const keys = Object.keys(object);
if (this.sortKeys) {
keys.sort();
}
const size = this.ignoreUndefined ? this.countWithoutUndefined(object, keys) : keys.length;
if (size < 16) {
// fixmap
yield* this.writeU8(0x80 + size);
}
else if (size < 0x10000) {
// map 16
yield* this.writeU8U16(0xde, size);
}
else if (size < 0x100000000) {
// map 32
yield* this.writeU8U32(0xdf, size);
}
else {
throw new Error(`Too large map object: ${size}`);
}
for (const key of keys) {
const value = object[key];
if (!(this.ignoreUndefined && value === undefined)) {
yield* this.encodeString(key);
yield* this.doEncode(value, depth + 1);
}
}
}
*encodeExtension(ext) {
const size = ext.data.length;
if (size === 1) {
// fixext 1
yield* this.writeU8(0xd4);
}
else if (size === 2) {
// fixext 2
yield* this.writeU8(0xd5);
}
else if (size === 4) {
// fixext 4
yield* this.writeU8(0xd6);
}
else if (size === 8) {
// fixext 8
yield* this.writeU8(0xd7);
}
else if (size === 16) {
// fixext 16
yield* this.writeU8(0xd8);
}
else if (size < 0x100) {
// ext 8
yield* this.writeU8U8(0xc7, size);
}
else if (size < 0x10000) {
// ext 16
yield* this.writeU8U16(0xc8, size);
}
else if (size < 0x100000000) {
// ext 32
yield* this.writeU8U32(0xc9, size);
}
else {
throw new Error(`Too large extension object: ${size}`);
}
yield* this.writeI8(ext.type);
yield* this.writeU8a(ext.data);
}
writeU8(value) {
this.view.setUint8(0, value);
return this.writeBuffer(1);
}
writeU8a(values) {
const size = values.length;
this.ensureBufferSizeToWrite(size);
this.bytes.set(values, 0);
return this.writeBuffer(size);
}
writeI8(value) {
this.view.setInt8(0, value);
return this.writeBuffer(1);
}
writeU16(value) {
this.view.setUint16(0, value);
return this.writeBuffer(2);
}
writeI16(value) {
this.view.setInt16(0, value);
return this.writeBuffer(2);
}
writeU32(value) {
this.view.setUint32(0, value);
return this.writeBuffer(4);
}
writeI32(value) {
this.view.setInt32(0, value);
return this.writeBuffer(4);
}
writeF32(value) {
this.view.setFloat32(0, value);
return this.writeBuffer(4);
}
writeF64(value) {
this.view.setFloat64(0, value);
return this.writeBuffer(8);
}
writeU64(value) {
setUint64(this.view, 0, value);
return this.writeBuffer(8);
}
writeI64(value) {
setInt64(this.view, 0, value);
return this.writeBuffer(8);
}
writeU8U8(value, value2) {
this.view.setUint8(0, value);
this.view.setUint8(1, value2);
return this.writeBuffer(2);
}
writeU8U16(u8, u16) {
this.view.setUint8(0, u8);
this.view.setUint16(1, u16);
return this.writeBuffer(3);
}
writeU8U32(u8, u32) {
this.view.setUint8(0, u8);
this.view.setUint32(1, u32);
return this.writeBuffer(5);
}
writeU8U64(u8, u64) {
this.view.setUint8(0, u8);
setUint64(this.view, 1, u64);
return this.writeBuffer(9);
}
writeU8I8(value, value2) {
this.view.setUint8(0, value);
this.view.setInt8(1, value2);
return this.writeBuffer(2);
}
writeU8I16(u8, i16) {
this.view.setUint8(0, u8);
this.view.setInt16(1, i16);
return this.writeBuffer(3);
}
writeU8I32(u8, i32) {
this.view.setUint8(0, u8);
this.view.setInt32(1, i32);
return this.writeBuffer(5);
}
writeU8I64(u8, i64) {
this.view.setUint8(0, u8);
setInt64(this.view, 1, i64);
return this.writeBuffer(9);
}
writeU8F32(u8, f32) {
this.view.setUint8(0, u8);
this.view.setFloat32(1, f32);
return this.writeBuffer(5);
}
writeU8F64(u8, f64) {
this.view.setUint8(0, u8);
this.view.setFloat64(1, f64);
return this.writeBuffer(9);
}
*writeBuffer(length) {
yield this.bytes.subarray(0, length);
}
}
//# sourceMappingURL=StreamEncoder.mjs.map