template/packages-user/data-common/src/replay/array.ts

903 lines
30 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 长度的字符串
*
* 该类型码表属于录像参数编解码格式的一部分,遵循只增不改的约定:新语义必须分配
* 新的类型码,禁止复用既有类型码。历史上 5/6/7/8 与短字符串基址(长度 + 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;
}
interface IParamRange {
/** 命令参数在参数缓冲区中的起始字节 */
readonly start: number;
/** 命令参数在参数缓冲区中的结束字节,不包含该字节 */
readonly end: 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 参数值
* @returns 标准化结果,当参数无法用现有编码表示时返回 `null`
*/
private normalizeParam(param: ReplayParamValue): INormalizedParam | null {
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,均以幅值按字节写入
// 幅值长度只有一个字节,故幅值必须小于 2 ^ 2040(即 255 字节),否则长度字节会溢出
const magnitude = param < 0n ? -param : param;
if (magnitude >= 2n ** 2040n) {
logger.warn(152);
return null;
}
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);
}
// 丢弃的项不保留占位,保证命令参数计数与实际写入的参数个数一致
const normalized: INormalizedParam[] = [];
arr.forEach(v => {
const param = this.normalizeParam(v);
if (param) normalized.push(param);
});
return normalized;
}
/**
* 计算参数所需的字节数
* @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;
});
}
/**
* 获取指定命令在参数缓冲区中的字节区间
* @param index 命令索引
*/
private getParamRange(index: number): IParamRange {
const start = this.indexArray[index];
const end =
index + 1 < this.length
? this.indexArray[index + 1]
: this.paramUsed;
return { start, end };
}
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, normalized.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;
if (index < 0 || index > this.length) {
logger.warn(
179,
'insert',
index.toString(),
this.length.toString()
);
return;
}
const normalized = this.normalizeParamList(params);
const length = this.calculateParamsLength(normalized);
this.checkBufferExpand(length);
const commandSize = this.getCommandSize();
const commandStart = index * commandSize;
// 插入到末尾等同于追加,此时无需任何位移
if (index === this.length) {
this.setCommandArray(commandStart, normalized.length, command);
this.setParamArray(this.paramUsed, normalized);
this.indexArray[this.length] = this.paramUsed;
this.length++;
this.paramUsed += length;
this.expireStreams();
return;
}
// 先进行位移
const paramStart = this.indexArray[index];
this.commandArray.copyWithin(commandStart + commandSize, commandStart);
this.paramArray.copyWithin(paramStart + length, paramStart);
this.indexArray.copyWithin(index + 1, index);
// 然后进行赋值操作,索引数组因为这一个指令的起始索引其实没变,所以不需要赋值
this.setCommandArray(commandStart, normalized.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;
if (index < 0 || index >= this.length) {
logger.warn(
179,
'delete',
index.toString(),
this.length.toString()
);
return;
}
const commandSize = this.getCommandSize();
const commandStart = index * commandSize;
const range = this.getParamRange(index);
const paramStart = range.start;
const nextParam = range.end;
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 = index; i < this.length; i++) {
this.indexArray[i] -= paramLength;
}
this.expireStreams();
}
set(index: number, command: number, params: ReplayParamValue[]): void {
if (this.disabled > 0) return;
if (index < 0 || index >= this.length) {
logger.warn(179, 'set', index.toString(), this.length.toString());
return;
}
const normalized = this.normalizeParamList(params);
const length = this.calculateParamsLength(normalized);
// 末步的参数终点取 paramUsed,与 delete 同口径,避免读到未初始化的 indexArray[index + 1]
const range = this.getParamRange(index);
const paramStart = range.start;
const nextParam = range.end;
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, normalized.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 = index + 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.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();
// 缓冲区被整体替换后,此前创建的读流偏移已失效,需要与其它写路径一样标记过期
this.expireStreams();
}
//#endregion
getCommandArray(): ArrayBuffer {
return this.commandBuffer;
}
getParamArray(): ArrayBuffer {
return this.paramBuffer;
}
disable(): void {
this.disabled++;
}
revert(): void {
if (this.disabled > 0) {
this.disabled--;
}
}
clearDisableFlag(): void {
this.disabled = 0;
}
}