template/packages-user/data-common/src/replay/array.ts
unanmed 965e002476 fix(07-03): #06-04-2 round-trip multi-byte and negative bigint parameters
- Encode bigint bytes by bit-shift mask instead of the broken accumulator
- Decode the bigint length prefix and payload bytes unsigned
- Encode negative bigint as dedicated type 8 with magnitude payload
- Un-skip the multi-byte and heterogeneous bigint cases, add focused cases
2026-09-15 18:37:21 +08:00

826 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { logger } from '@motajs/common';
import {
IReplayArray,
IReplayArrayConfig,
IReplayReadStream,
IReplayStepHandler,
ReplayCommandWidth,
ReplayParamValue
} from './types';
interface INormalizedParam {
/**
* 参数类型:
*
* - 0: boolean
* - 1: int8
* - 2: int16
* - 3: int32
* - 4: 非负 int64,载荷为幅值
* - 5: 负 int64,载荷为幅值
* - 6: float
* - 7: 非负 bigint,载荷为幅值
* - 8: 负 bigint,载荷为幅值
* - 9: string,带 int32 长度前缀
* - 10 ~ 255: n - 9 长度的字符串
*/
readonly paramType: number;
/** 参数值 */
readonly paramValue: number | Uint8Array;
/** 此参数占据的字节长度,包含类型 token */
readonly byteLength: number;
}
interface IDecodedParam {
/** 参数值 */
readonly paramValue: ReplayParamValue;
/** 参数占据的字节数,包含类型 token */
readonly byteLength: number;
}
interface IDecodedCommand {
/** 指令标识 */
readonly command: number;
/** 指令的参数数量 */
readonly paramCount: number;
}
export class ReplayArray implements IReplayArray {
length: number = 0;
commandWidth: ReplayCommandWidth = ReplayCommandWidth.Uint8;
/** 文本编码器 */
private readonly textEncoder: TextEncoder;
/** 文本解码器 */
private readonly textDecoder: TextDecoder;
/** 指令数组缓冲区 */
private commandBuffer: ArrayBuffer;
/** 参数数组缓冲区 */
private paramBuffer: ArrayBuffer;
/** 参数起始索引缓冲区 */
private indexBuffer: ArrayBuffer;
/** 当前参数缓冲区已经使用了多少字节 */
private paramUsed: number;
/** 指令数组的 DataView */
private commandView: DataView;
/** 参数数组的 DataView */
private paramView: DataView;
/** 指令数组的 TypedArray */
private commandArray: Uint8Array;
/** 参数数组的 TypedArray */
private paramArray: Uint8Array;
/** 参数起始索引 TypedArray */
private indexArray: Uint32Array;
/** 指令数组扩容乘数 */
private readonly commandExpand: number;
/** 参数数组扩容乘数 */
private readonly paramExpand: number;
/** 指令数组最大长度 */
private readonly commandMax: number;
/** 参数数组最大长度 */
private readonly paramMax: number;
/** 所有可用的读取流 */
private readonly readStreams: Set<IReplayReadStream>;
/** 当前是否处于禁用状态 */
private disabled: number = 0;
constructor(config: Readonly<IReplayArrayConfig>) {
this.textEncoder = new TextEncoder();
this.textDecoder = new TextDecoder();
this.readStreams = new Set();
this.paramUsed = 0;
this.commandWidth = config.commandWidth;
this.commandMax = config.commandMaxLength;
this.paramMax = config.paramMaxLength;
if (config.commandExpandMultiplier < 1) {
const str = config.commandExpandMultiplier.toString();
logger.warn(149, 'command', str);
this.commandExpand = 2;
} else {
this.commandExpand = config.commandExpandMultiplier;
}
if (config.paramExpandMultiplier < 1) {
const str = config.paramExpandMultiplier.toString();
logger.warn(149, 'param', str);
this.paramExpand = 2;
} else {
this.paramExpand = config.paramExpandMultiplier;
}
const commandSize = this.getCommandSize();
this.commandBuffer = new ArrayBuffer(
config.initCommandLength * commandSize
);
this.paramBuffer = new ArrayBuffer(config.initParamLength);
this.indexBuffer = new ArrayBuffer(config.initCommandLength * 4);
this.commandView = new DataView(this.commandBuffer);
this.paramView = new DataView(this.paramBuffer);
this.commandArray = new Uint8Array(this.commandBuffer);
this.paramArray = new Uint8Array(this.paramBuffer);
this.indexArray = new Uint32Array(this.indexBuffer);
}
/**
* 获取指令位宽
*/
private getCommandSize() {
if (this.commandWidth === ReplayCommandWidth.Uint8) {
return 2;
} else {
return 3;
}
}
/**
* 检查两个数组缓冲区的容量,不足则扩容
* @param paramLength 将要添加至参数数组的内容的字节数
*/
private checkBufferExpand(paramLength: number) {
const commandSize = this.getCommandSize();
const commandLength = Math.ceil(
this.commandBuffer.byteLength / commandSize
);
let expanded = false;
if (commandLength - this.length < 10) {
if (commandLength >= this.commandMax) {
logger.warn(150, 'command', this.commandMax.toString());
} else {
const nextSize = Math.min(
Math.ceil(commandLength * this.commandExpand),
this.commandMax
);
const origin = this.commandArray;
this.commandBuffer = new ArrayBuffer(nextSize * commandSize);
this.commandView = new DataView(this.commandBuffer);
this.commandArray = new Uint8Array(this.commandBuffer);
this.commandArray.set(origin);
const originIndex = this.indexArray;
this.indexBuffer = new ArrayBuffer(nextSize * 4);
this.indexArray = new Uint32Array(this.indexBuffer);
this.indexArray.set(originIndex);
expanded = true;
}
}
if (this.paramBuffer.byteLength - this.paramUsed < paramLength + 10) {
if (this.paramBuffer.byteLength >= this.paramMax) {
logger.warn(150, 'param', this.paramMax.toString());
} else {
const nextSize = Math.min(
Math.ceil(this.paramBuffer.byteLength * this.paramExpand),
this.paramMax
);
const origin = this.paramArray;
this.paramBuffer = new ArrayBuffer(nextSize);
this.paramView = new DataView(this.paramBuffer);
this.paramArray = new Uint8Array(this.paramBuffer);
this.paramArray.set(origin);
expanded = true;
}
}
if (expanded) {
// 重新检查一遍,防止拓展之后还是不够用
this.checkBufferExpand(paramLength);
}
}
//#region 写入与编码
/**
* 将所有的读取流标记为过期并删除
*/
private expireStreams() {
this.readStreams.forEach(v => {
v.expired = true;
});
this.readStreams.clear();
}
/**
* 将数值标准化为数字或 Uint8Array
* @param param 参数值
*/
private normalizeParam(param: ReplayParamValue): INormalizedParam {
if (typeof param === 'boolean') {
// 0 - boolean
return {
paramType: 0,
paramValue: param ? 1 : 0,
byteLength: 2
};
} else if (typeof param === 'number') {
let type = 1;
let byte = 2;
if (Number.isInteger(param)) {
if (param < 128 && param >= -128) {
// 1 - int8
type = 1;
byte = 2;
} else if (param < 32768 && param >= -32768) {
// 2 - int16
type = 2;
byte = 3;
} else if (param < 2147483648 && param >= -2147483648) {
// 3 - int32
type = 3;
byte = 5;
} else if (param >= 2147483648) {
// 4 - 非负 int64
type = 4;
byte = 9;
} else {
// 5 - 负 int64,载荷为幅值
type = 5;
byte = 9;
}
} else {
// 6 - float
type = 6;
byte = 9;
}
return {
paramType: type,
paramValue: param,
byteLength: byte
};
} else if (typeof param === 'bigint') {
// 7 - 非负 bigint / 8 - 负 bigint,均以幅值按字节写入
const wall = 2n ** 2047n;
if (param > wall - 1n || param < -wall) {
logger.warn(152);
}
const magnitude = param < 0n ? -param : param;
const bit = magnitude.toString(2);
const length = Math.ceil(bit.length / 8);
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = Number((magnitude >> (8n * BigInt(i))) & 0xffn);
}
return {
paramType: param < 0n ? 8 : 7,
paramValue: arr,
byteLength: arr.length + 2
};
} else if (typeof param === 'string') {
const arr = this.textEncoder.encode(param);
if (arr.length > 0 && arr.length <= 246) {
// 10 ~ 255 - string
return {
paramType: arr.length + 9,
paramValue: arr,
byteLength: arr.length + 1
};
} else {
// 9 - string
return {
paramType: 9,
paramValue: arr,
byteLength: arr.length + 5
};
}
}
logger.warn(148, typeof param, String(param));
return {
paramType: 0,
paramValue: 0,
byteLength: 0
};
}
/**
* 将一系列参数值标准化为数字或 Uint8Array
* @param params 参数值列表
*/
private normalizeParamList(params: ReplayParamValue[]): INormalizedParam[] {
let arr = params;
if (params.length > 255) {
logger.warn(153, params.length.toString());
arr = params.slice(0, 255);
}
return arr.map(v => this.normalizeParam(v));
}
/**
* 计算参数所需的字节数
* @param params 标准化后的参数值
*/
private calculateParamsLength(params: INormalizedParam[]): number {
return params.reduce((prev, curr) => prev + curr.byteLength, 0);
}
/**
* 将指令实际写入缓冲区
* @param startIndex 指令的起始索引
* @param paramCount 指令对应的参数数量
* @param command 指令的标识数字
*/
private setCommandArray(
startIndex: number,
paramCount: number,
command: number
) {
this.commandView.setUint8(startIndex, paramCount);
if (this.commandWidth === ReplayCommandWidth.Uint8) {
this.commandView.setUint8(startIndex + 1, command);
} else {
this.commandView.setUint16(startIndex + 1, command);
}
}
/**
* 将参数列表实际写入缓冲区
* @param startIndex 起始索引
* @param params 参数列表
*/
private setParamArray(startIndex: number, params: INormalizedParam[]) {
let index = startIndex;
params.forEach(param => {
this.paramView.setInt8(index, param.paramType);
const num = param.paramValue as number;
const arr = param.paramValue as Uint8Array;
if (param.paramType === 0) {
// 0 - boolean
this.paramView.setInt8(index + 1, num);
} else if (param.paramType === 1) {
// 1 - int8
this.paramView.setInt8(index + 1, num);
} else if (param.paramType === 2) {
// 2 - int16
this.paramView.setInt16(index + 1, num);
} else if (param.paramType === 3) {
// 3 - int32
this.paramView.setInt32(index + 1, num);
} else if (param.paramType === 4 || param.paramType === 5) {
// 4 - 非负 int64 / 5 - 负 int64,均以幅值拆分高低 32 位
const magnitude = num < 0 ? -num : num;
const high = Math.floor(magnitude / 2147483648);
const low = magnitude - high * 2147483648;
this.paramView.setInt32(index + 1, low);
this.paramView.setInt32(index + 5, high);
} else if (param.paramType === 6) {
// 6 - float
this.paramView.setFloat64(index + 1, num);
} else if (param.paramType === 7 || param.paramType === 8) {
// 7 - 非负 bigint / 8 - 负 bigint,写入幅值字节
this.paramArray[index + 1] = arr.length;
this.paramArray.set(arr, index + 2);
} else if (param.paramType === 9) {
// 9 - string
this.paramView.setInt32(index + 1, arr.length);
this.paramArray.set(arr, index + 5);
} else {
// 10 ~ 255 - string
this.paramArray.set(arr, index + 1);
}
index += param.byteLength;
});
}
add(command: number, params: ReplayParamValue[]): void {
if (this.disabled > 0) return;
const normalized = this.normalizeParamList(params);
const length = this.calculateParamsLength(normalized);
this.checkBufferExpand(length);
// 追加指令
const commandSize = this.getCommandSize();
const commandStart = commandSize * this.length;
this.setCommandArray(commandStart, params.length, command);
// 追加参数
this.setParamArray(this.paramUsed, normalized);
this.indexArray[this.length] = this.paramUsed;
this.paramUsed += length;
this.length++;
this.expireStreams();
}
insert(index: number, command: number, params: ReplayParamValue[]): void {
if (this.disabled > 0) return;
const normalized = this.normalizeParamList(params);
const length = this.calculateParamsLength(normalized);
this.checkBufferExpand(length);
const commandSize = this.getCommandSize();
// 先进行位移
const commandStart = index * commandSize;
const paramStart = this.indexArray[index];
this.commandArray.copyWithin(commandStart + commandSize, commandStart);
this.paramArray.copyWithin(paramStart, paramStart + length);
this.indexArray.copyWithin(index, index + 1);
// 然后进行赋值操作,索引数组因为这一个指令的起始索引其实没变,所以不需要赋值
this.setCommandArray(commandStart, params.length, command);
this.setParamArray(paramStart, normalized);
this.length++;
this.paramUsed += length;
// 最后把后面的内容的索引数组增加 length 即可
for (let i = index + 1; i < this.length; i++) {
this.indexArray[i] += length;
}
this.expireStreams();
}
delete(index: number): void {
if (this.disabled > 0) return;
const commandSize = this.getCommandSize();
const commandStart = index * commandSize;
const paramStart = this.indexArray[index];
const nextParam = this.indexArray[index + 1];
const paramLength = nextParam - paramStart;
// 直接进行位移
this.commandArray.copyWithin(commandStart, commandStart + commandSize);
this.paramArray.copyWithin(paramStart, nextParam);
this.indexArray.copyWithin(index, index + 1);
this.length--;
this.paramUsed -= paramLength;
// 然后把后续值赋零
const commandLast = this.length * commandSize;
const paramLast = this.paramUsed;
this.commandArray[commandLast] = 0;
this.commandArray[commandLast + 1] = 0;
this.commandArray[commandLast + 2] = 0;
for (let i = 0; i < paramLength; i++) {
this.paramArray[paramLast + i] = 0;
}
// 最后把后面的索引减少 paramLength
for (let i = paramStart; i < this.length; i++) {
this.indexArray[i] -= paramLength;
}
this.expireStreams();
}
set(index: number, command: number, params: ReplayParamValue[]): void {
if (this.disabled > 0) return;
const normalized = this.normalizeParamList(params);
const length = this.calculateParamsLength(normalized);
const paramStart = this.indexArray[index];
const nextParam = this.indexArray[index + 1];
const paramLength = nextParam - paramStart;
const deltaLength = length - paramLength;
if (deltaLength > 0) {
this.checkBufferExpand(deltaLength);
}
// 先写入指令
const commandSize = this.getCommandSize();
const commandStart = index * commandSize;
this.setCommandArray(commandStart, params.length, command);
// 然后根据差值位移参数数组,如果参数长度减少还需要将最后几项置零
this.paramArray.copyWithin(nextParam + deltaLength, nextParam);
this.paramUsed += deltaLength;
if (deltaLength < 0) {
const zeros = -deltaLength;
const paramLast = this.paramUsed;
for (let i = 0; i < zeros; i++) {
this.paramArray[paramLast + i] = 0;
}
}
// 然后设置参数数组
this.setParamArray(paramStart, normalized);
// 最后调整索引数组
for (let i = paramStart + 1; i < this.length; i++) {
this.indexArray[i] += deltaLength;
}
this.expireStreams();
}
setCommandWidth(width: ReplayCommandWidth): void {
this.disable();
const oldWidth = this.commandWidth;
const oldSize = this.getCommandSize();
this.commandWidth = width;
const newSize = this.getCommandSize();
const commandLength = this.commandBuffer.byteLength / oldSize;
const length = Math.floor(commandLength * newSize);
const newBuffer = new ArrayBuffer(length);
const newView = new DataView(newBuffer);
for (let i = 0; i < this.length; i++) {
const oldStart = i * oldSize;
const newStart = i * newSize;
// 先读取旧指令
const count = this.commandView.getUint8(oldStart);
let oldCommand = 0;
if (oldWidth === ReplayCommandWidth.Uint8) {
oldCommand = this.commandView.getUint8(oldStart + 1);
} else {
oldCommand = this.commandView.getUint16(oldStart + 1);
}
// 再写入新的 DataView
newView.setUint8(newStart, count);
if (width === ReplayCommandWidth.Uint8) {
if (oldCommand > 255) {
logger.warn(154, oldCommand.toString());
}
newView.setUint8(newStart + 1, oldCommand);
} else {
newView.setUint16(newStart + 1, oldCommand);
}
}
this.commandBuffer = newBuffer;
this.commandArray = new Uint8Array(newBuffer);
this.commandView = newView;
this.expireStreams();
this.revert();
}
//#endregion
//#region 读取与解码
/**
* 解码单个录像指令
* @param startIndex 解码起始索引
*/
private decodeCommand(startIndex: number): IDecodedCommand {
const count = this.commandView.getUint8(startIndex);
let command = 0;
if (this.commandWidth === ReplayCommandWidth.Uint8) {
command = this.commandView.getUint8(startIndex + 1);
} else {
command = this.commandView.getUint16(startIndex + 1);
}
return {
command,
paramCount: count
};
}
/**
* 解码单个录像参数
* @param startIndex 解码起始索引
*/
private decodeParam(startIndex: number): IDecodedParam {
const type = this.paramArray[startIndex];
let value: ReplayParamValue = 0;
let byte = 0;
if (type === 0) {
// 0 - boolean
const next = this.paramArray[startIndex + 1];
byte = 2;
if (next === 1) {
value = true;
} else if (next === 0) {
value = false;
} else {
logger.warn(151, next.toString());
value = false;
}
} else if (type === 1) {
// 1 - int8
byte = 2;
value = this.paramView.getInt8(startIndex + 1);
} else if (type === 2) {
// 2 - int16
byte = 3;
value = this.paramView.getInt16(startIndex + 1);
} else if (type === 3) {
// 3 - int32
byte = 5;
value = this.paramView.getInt32(startIndex + 1);
} else if (type === 4) {
// 4 - 非负 int64
const low = this.paramView.getInt32(startIndex + 1);
const high = this.paramView.getInt32(startIndex + 5);
byte = 9;
value = low + high * 2147483648;
} else if (type === 5) {
// 5 - 负 int64,读回幅值后取负
const low = this.paramView.getInt32(startIndex + 1);
const high = this.paramView.getInt32(startIndex + 5);
byte = 9;
value = -(low + high * 2147483648);
} else if (type === 6) {
// 6 - float
byte = 9;
value = this.paramView.getFloat64(startIndex + 1);
} else if (type === 7 || type === 8) {
// 7 - 非负 bigint / 8 - 负 bigint,读回幅值后按类型码还原符号
const length = this.paramView.getUint8(startIndex + 1);
let base = 0n;
for (let i = 0; i < length; i++) {
const num = this.paramView.getUint8(startIndex + 2 + i);
base += BigInt(num) << (8n * BigInt(i));
}
byte = length + 2;
value = type === 8 ? -base : base;
} else if (type === 9) {
// 9 - string
const length = this.paramView.getInt32(startIndex + 1);
const endIndex = startIndex + 5 + length;
const arr = this.paramArray.slice(startIndex + 5, endIndex);
byte = length + 5;
value = this.textDecoder.decode(arr);
} else {
// 10 ~ 255 - string
const length = type - 9;
const endIndex = startIndex + 1 + length;
const arr = this.paramArray.slice(startIndex + 1, endIndex);
byte = length + 1;
value = this.textDecoder.decode(arr);
}
return {
paramValue: value,
byteLength: byte
};
}
/**
* 解码指定数量的录像参数
* @param startIndex 解码起始索引
* @param count 解码参数数量
*/
private decodeParamList(
startIndex: number,
count: number
): IDecodedParam[] {
const arr: IDecodedParam[] = [];
let index = startIndex;
for (let i = 0; i < count; i++) {
const decoded = this.decodeParam(index);
index += decoded.byteLength;
arr.push(decoded);
}
return arr;
}
/**
* 创建录像步信息对象
* @param command 指令标识
* @param params 解码后的参数列表
* @param index 录像索引
*/
private createReplayStepHandler(
command: number,
params: IDecodedParam[],
index: number
): IReplayStepHandler {
return {
command,
params: params.map(v => v.paramValue),
index
};
}
get(index: number): IReplayStepHandler {
const commandSize = this.getCommandSize();
const commandStart = index * commandSize;
const paramStart = this.indexArray[index];
const command = this.decodeCommand(commandStart);
const params = this.decodeParamList(paramStart, command.paramCount);
return this.createReplayStepHandler(command.command, params, index);
}
createReadStream(startIndex: number = 0): Readonly<IReplayReadStream> {
const commandSize = this.getCommandSize();
let currCommand = startIndex * commandSize;
let currParam = this.indexArray[startIndex];
const stream: IReplayReadStream = {
index: startIndex,
length: this.length,
expired: false,
read: () => {
if (stream.expired) {
logger.warn(155);
}
if (stream.index >= this.length) return null;
const { command, paramCount } = this.decodeCommand(currCommand);
const params = this.decodeParamList(currParam, paramCount);
stream.index++;
currCommand += commandSize;
currParam += params.reduce(
(prev, curr) => prev + curr.byteLength,
0
);
const index = stream.index;
return this.createReplayStepHandler(command, params, index);
},
destroy: () => {
this.readStreams.delete(stream);
}
};
this.readStreams.add(stream);
return stream;
}
//#endregion
//#region 重建方法
rebuildIndexArray(): void {
const commandSize = this.getCommandSize();
let currCommand = 0;
let currParam = 0;
// 需要对每个参数进行解码,然后 cumsum
for (let i = 0; i < this.length; i++) {
const { paramCount } = this.decodeCommand(currCommand);
const params = this.decodeParamList(currParam, paramCount);
this.indexArray[i] = currParam;
currCommand += commandSize;
currParam += params.reduce(
(prev, curr) => prev + curr.byteLength,
0
);
}
this.paramUsed = currParam;
}
setReplayArray(
commandWidth: ReplayCommandWidth,
commandBuffer: ArrayBuffer,
paramBuffer: ArrayBuffer,
length: number
): void {
this.disabled = 0;
this.commandWidth = commandWidth;
this.commandBuffer = commandBuffer;
this.commandArray = new Uint8Array(commandBuffer);
this.commandView = new DataView(commandBuffer);
this.paramBuffer = paramBuffer;
this.paramArray = new Uint8Array(paramBuffer);
this.paramView = new DataView(paramBuffer);
const commandSize = this.getCommandSize();
const commandBufferLength = Math.ceil(
commandBuffer.byteLength / commandSize
);
this.indexBuffer = new ArrayBuffer(commandBufferLength * 4);
this.indexArray = new Uint32Array(this.indexBuffer);
this.length = length;
this.rebuildIndexArray();
}
//#endregion
getCommandArray(): ArrayBuffer {
return this.commandBuffer;
}
getParamArray(): ArrayBuffer {
return this.paramBuffer;
}
disable(): void {
this.disabled++;
}
revert(): void {
if (this.disabled > 0) {
this.disabled--;
}
}
}