87 lines
3.0 KiB
TypeScript
87 lines
3.0 KiB
TypeScript
import { HEADER_SIZE, ID_SIZE } from './constants.js';
|
|
import { readU32, readU64, toBase64Url, type Bytes } from './bytes.js';
|
|
import { IntegrityError } from './errors.js';
|
|
|
|
/** 墓碑原因(protocol §8)。 */
|
|
export type RemovedReason = 'deleted' | 'expired' | 'evicted' | 'download_limit' | 'superseded';
|
|
|
|
const REASONS: readonly RemovedReason[] = ['deleted', 'expired', 'evicted', 'download_limit', 'superseded'];
|
|
|
|
export type LogRecord =
|
|
| { kind: 'frame'; seq: number; envelope: Bytes }
|
|
| { kind: 'tombstone'; seq: number; itemId: string; reason: RemovedReason }
|
|
| { kind: 'announce'; seq: number; itemId: string; flags: number; createdAt: number };
|
|
|
|
const KIND_FRAME = 1;
|
|
const KIND_TOMBSTONE = 2;
|
|
const KIND_ANNOUNCE = 3;
|
|
|
|
/** body 的字节数,不够返回 null。 */
|
|
function bodyLength(kind: number, buf: Uint8Array, bodyStart: number): number | null {
|
|
switch (kind) {
|
|
case KIND_FRAME: {
|
|
// 封装自带 payload_len:body 长度 = 26 + payload_len
|
|
if (buf.length < bodyStart + HEADER_SIZE) return null;
|
|
return HEADER_SIZE + readU32(buf, bodyStart + 22);
|
|
}
|
|
case KIND_TOMBSTONE:
|
|
return ID_SIZE + 1;
|
|
case KIND_ANNOUNCE:
|
|
return ID_SIZE + 1 + 8;
|
|
default:
|
|
throw new IntegrityError('bad_envelope');
|
|
}
|
|
}
|
|
|
|
function parseOne(buf: Uint8Array, offset: number): { record: LogRecord; next: number } {
|
|
if (buf.length < offset + 9) throw new IntegrityError('bad_envelope');
|
|
const seq = readU64(buf, offset);
|
|
const kind = buf[offset + 8] as number;
|
|
const bodyStart = offset + 9;
|
|
|
|
const len = bodyLength(kind, buf, bodyStart);
|
|
if (len === null || buf.length < bodyStart + len) throw new IntegrityError('bad_envelope');
|
|
const body = buf.subarray(bodyStart, bodyStart + len);
|
|
const next = bodyStart + len;
|
|
|
|
switch (kind) {
|
|
case KIND_FRAME:
|
|
return { record: { kind: 'frame', seq, envelope: body.slice() }, next };
|
|
case KIND_TOMBSTONE: {
|
|
const reason = REASONS[body[ID_SIZE] as number];
|
|
if (!reason) throw new IntegrityError('bad_envelope');
|
|
return { record: { kind: 'tombstone', seq, itemId: toBase64Url(body.subarray(0, ID_SIZE)), reason }, next };
|
|
}
|
|
default:
|
|
return {
|
|
record: {
|
|
kind: 'announce',
|
|
seq,
|
|
itemId: toBase64Url(body.subarray(0, ID_SIZE)),
|
|
flags: body[ID_SIZE] as number,
|
|
createdAt: readU64(body, ID_SIZE + 1),
|
|
},
|
|
next,
|
|
};
|
|
}
|
|
}
|
|
|
|
/** 解析 log 接口返回的记录拼接(长轮询的响应体)。 */
|
|
export function parseLogRecords(data: Uint8Array): LogRecord[] {
|
|
const out: LogRecord[] = [];
|
|
let offset = 0;
|
|
while (offset < data.length) {
|
|
const { record, next } = parseOne(data, offset);
|
|
out.push(record);
|
|
offset = next;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 解析恰好一条记录(WebSocket 的一条二进制消息)。多余或缺少字节都视为非法。 */
|
|
export function parseLogRecord(data: Uint8Array): LogRecord {
|
|
const { record, next } = parseOne(data, 0);
|
|
if (next !== data.length) throw new IntegrityError('bad_envelope');
|
|
return record;
|
|
}
|