Files
WebDrop/packages/core/src/log.ts
T
2026-10-02 00:59:22 +08:00

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;
}