@cloudpss/ubjson
Version:
Opinionated UBJSON encoder/decoder for CloudPSS.
195 lines (187 loc) • 7.89 kB
text/typescript
import { constants } from '../helper/constants.js';
import {
type EncodeCursor,
I8_MASK,
type TypedArrayType,
writeNumber,
writeTypedArray,
writeTypedArrayData,
writeTypedArrayHeader,
} from '../helper/encode.js';
import { unsupportedType } from '../helper/errors.js';
import { stringByteLength, encodeInto } from '../helper/string-encoder.js';
const LARGE_DATA_LENGTH = 65536;
const { isArray } = Array;
// eslint-disable-next-line @typescript-eslint/unbound-method
const { isView } = ArrayBuffer;
const { keys: objectKeys } = Object;
/** 编码至 ubjson */
export abstract class EncoderBase {
/** 序列化对象时排序属性 */
sortObjectKeys = false;
/** 当前写指针位置 */
protected length = 0;
/** 数据 */
protected data!: Uint8Array;
/** buffer 的 DataView */
protected view!: DataView;
/**
* 确保 buffer 还有 capacity 的空闲空间
*/
protected abstract ensureCapacity(capacity: number): void;
/** 编码至 ubjson,对于 `undefined` 写入 NOOP */
protected writeValue(value: unknown): void {
if (value === undefined) {
this.ensureCapacity(1);
this.data[this.length++] = constants.NO_OP;
return;
}
this.write(value);
}
/** 写入一个对象 */
private write(value: unknown): void {
switch (typeof value) {
case 'string':
if (value.length === 1 && value.charCodeAt(0) < 0x80) {
// 1 byte ascii char
this.ensureCapacity(2);
this.data[this.length++] = constants.CHAR;
this.data[this.length++] = value.charCodeAt(0);
} else {
this.ensureCapacity(2);
this.data[this.length++] = constants.STRING;
this.writeStringData(value);
}
return;
case 'number':
return writeNumber(this as this & EncodeCursor, value);
case 'object': {
if (value === null) {
this.ensureCapacity(1);
this.data[this.length++] = constants.NULL;
return;
}
if (isArray(value)) {
this.ensureCapacity(1);
this.data[this.length++] = constants.ARRAY;
const size = value.length;
for (let index = 0; index < size; index++) {
const element = value[index] as unknown;
// 在数组中 undefined 和 function 也被视作 null 进行序列化
if (element == null || typeof element == 'function') {
this.ensureCapacity(1);
this.data[this.length++] = constants.NULL;
} else {
this.write(element);
}
}
this.ensureCapacity(1);
this.data[this.length++] = constants.ARRAY_END;
return;
}
if (isView(value)) {
if (value.byteLength <= LARGE_DATA_LENGTH) {
writeTypedArray(this as this & EncodeCursor, value);
return;
}
const type = writeTypedArrayHeader(this as this & EncodeCursor, value);
this.writeLargeTypedArrayData(type, value);
return;
}
const { toJSON } = value as Record<string, unknown>;
if (typeof toJSON == 'function') {
this.write(toJSON.call(value));
return;
}
// 生成稳定的结果以便 hash 计算
const keys = objectKeys(value);
const size = keys.length;
if (this.sortObjectKeys && size > 1) {
keys.sort();
}
this.ensureCapacity(2 + size);
this.data[this.length++] = constants.OBJECT;
for (let index = 0; index < size; index++) {
const key = keys[index]!;
const element = (value as Record<string, unknown>)[key];
if (element === undefined || typeof element == 'function') continue;
this.writeStringData(key);
this.write(element);
}
this.ensureCapacity(1);
this.data[this.length++] = constants.OBJECT_END;
return;
}
case 'boolean':
this.ensureCapacity(1);
this.data[this.length++] = value ? constants.TRUE : constants.FALSE;
return;
case 'bigint':
// int32 range
if (value >= -2_147_483_648n && value <= 2_147_483_647n) {
this.write(Number(value));
}
// int64 range
else if (value >= -9_223_372_036_854_775_808n && value <= 9_223_372_036_854_775_807n) {
this.ensureCapacity(9);
this.data[this.length++] = constants.INT64;
this.view.setBigInt64(this.length, value);
this.length += 8;
} else {
throw new RangeError(`BigInt value out of range: ${value}`);
}
return;
default:
unsupportedType(value);
}
}
/** writeStringData */
private writeStringData(value: string): void {
const strLength = value.length;
if (strLength > LARGE_DATA_LENGTH) {
return this.writeLargeStringData(value);
}
// 对于短字符串,直接计算最大使用空间
const maxUsage = strLength * 3;
// 一次性分配 setLength 和 encodeInto 的空间,避免无法回溯
// 额外分配 3 字节,避免 encodeInto 无法写入最后一个字符
this.ensureCapacity(maxUsage + 5 + 3);
// 预估头部大小
const headerSize = strLength < 0x80 ? 2 : strLength < 0x8000 ? 3 : 5;
const { length: headerPos, data, view } = this;
const bufLength = encodeInto(value, data, headerPos + headerSize);
if (bufLength < 0x80) {
view.setInt16(headerPos, I8_MASK | bufLength);
this.length = headerPos + 2 + bufLength;
} else if (bufLength < 0x8000) {
if (headerSize < 3) {
data.copyWithin(headerPos + 3, headerPos + headerSize, headerPos + headerSize + bufLength);
}
data[headerPos] = constants.INT16;
view.setInt16(headerPos + 1, bufLength);
this.length = headerPos + 3 + bufLength;
} else {
if (headerSize < 5) {
data.copyWithin(headerPos + 5, headerPos + headerSize, headerPos + headerSize + bufLength);
}
data[headerPos] = constants.INT32;
view.setInt32(headerPos + 1, bufLength);
this.length = headerPos + 5 + bufLength;
}
}
/** 写入大字符串 */
protected writeLargeStringData(value: string): void {
const binLen = stringByteLength(value);
this.ensureCapacity(5);
this.data[this.length++] = constants.INT32;
this.view.setInt32(this.length, binLen);
this.length += 4;
this.ensureCapacity(binLen);
encodeInto(value, this.data, this.length);
this.length += binLen;
}
/** 写入数组 */
protected writeLargeTypedArrayData(type: TypedArrayType, value: ArrayBufferView): void {
writeTypedArrayData(this as this & EncodeCursor, type, value);
}
}