import { APIError, AuthReason, Transport, TransportConfig, buildCleanConnectFlags, marshalJSON, } from "./types.js"; export interface FakeConnect { cleanStart: boolean; sessionExpiry: number; password: string; } /** 单测用假 MQTT。 */ export class FakeTransport implements Transport { private cfg?: TransportConfig; private cred = ""; private up: string[] = []; private connects: FakeConnect[] = []; autoHello = true; helloToken = "nst_test_token"; helloServerTimeMs = 1_750_000_000_000; maxBodyBytes = 262144; maxMetaBytes = 4096; maxFrameBytes = 786432; setCredential(passwordOrToken: string): void { this.cred = passwordOrToken; } async start(cfg: TransportConfig): Promise { this.cfg = cfg; await this.simulateConnectOK(); } async publishUp(payload: Uint8Array | string): Promise { const s = typeof payload === "string" ? payload : new TextDecoder().decode(payload); this.up.push(s); if (this.autoHello) { try { const head = JSON.parse(s) as { type?: string; rid?: string }; if (head.type === "hello" && head.rid) this.replyHello(head.rid); } catch { /* ignore */ } } } async stop(): Promise { this.cfg?.onOffline(); } private replyHello(rid: string): void { const resp = marshalJSON({ v: 1, type: "resp", rid, ok: true, data: { server_time_ms: this.helloServerTimeMs, server_version: "0.1.0", max_body_bytes: this.maxBodyBytes, max_meta_bytes: this.maxMetaBytes, max_frame_bytes: this.maxFrameBytes, max_ttl_seconds: 2592000, max_schedule_seconds: 31536000, ack_timeout_seconds: 300, session_token: this.helloToken, }, }); this.injectDown(resp); } async simulateConnectOK(): Promise { const { cleanStart, sessionExpiry } = buildCleanConnectFlags(); this.connects.push({ cleanStart, sessionExpiry, password: this.cred }); this.cfg?.onConnectPacket?.(cleanStart, sessionExpiry); this.cfg?.backoff.markOnline(); await this.cfg?.mqttReady(); } async simulateReconnect(): Promise { this.cfg?.backoff.markOffline(); this.cfg?.onOffline(); await this.simulateConnectOK(); } simulateAuthFail(reason: AuthReason): void { this.cfg?.onAuthFailed(reason); } simulateKick(): void { this.cfg?.onKicked(); } injectDown(payload: string | Uint8Array): void { const bytes = typeof payload === "string" ? new TextEncoder().encode(payload) : payload; this.cfg?.onDown(bytes); } replyOK(rid: string, data: unknown = {}): void { this.injectDown(marshalJSON({ v: 1, type: "resp", rid, ok: true, data })); } replyErr(rid: string, code: string, message: string): void { this.injectDown( marshalJSON({ v: 1, type: "resp", rid, ok: false, error: { code, message } }), ); } upFrames(): string[] { return [...this.up]; } getConnects(): FakeConnect[] { return [...this.connects]; } findUp(type: string): Array> { const out: Array> = []; for (const s of this.up) { try { const m = JSON.parse(s) as Record; if (m.type === type) out.push(m); } catch { /* ignore */ } } return out; } } export function assertNever(_x: never): void { throw new APIError("bad_request", "unreachable"); }