161 lines
4.0 KiB
TypeScript
161 lines
4.0 KiB
TypeScript
import {
|
|
APIError,
|
|
AuthReason,
|
|
Transport,
|
|
TransportConfig,
|
|
buildCleanConnectFlags,
|
|
marshalJSON,
|
|
} from "./types.js";
|
|
|
|
export interface FakeConnect {
|
|
cleanStart: boolean;
|
|
sessionExpiry: number;
|
|
password: string;
|
|
receiveMaximumSet: boolean;
|
|
keepalive: number;
|
|
}
|
|
|
|
/** 单测用假 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;
|
|
publishUpImpl?: (payload: string) => Promise<void>;
|
|
|
|
setCredential(passwordOrToken: string): void {
|
|
this.cred = passwordOrToken;
|
|
}
|
|
|
|
async start(cfg: TransportConfig): Promise<void> {
|
|
this.cfg = cfg;
|
|
queueMicrotask(() => {
|
|
void this.simulateConnectOK().catch(() => {});
|
|
});
|
|
}
|
|
|
|
async publishUp(payload: Uint8Array | string): Promise<void> {
|
|
const s = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
|
|
if (this.publishUpImpl) {
|
|
await this.publishUpImpl(s);
|
|
}
|
|
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<void> {
|
|
/* 不触发 onOffline,避免 close 后误重连 */
|
|
}
|
|
|
|
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<void> {
|
|
const { cleanStart, sessionExpiry } = buildCleanConnectFlags();
|
|
this.connects.push({
|
|
cleanStart,
|
|
sessionExpiry,
|
|
password: this.cred,
|
|
receiveMaximumSet: false,
|
|
keepalive: 30,
|
|
});
|
|
this.cfg?.onConnectPacket?.(cleanStart, sessionExpiry);
|
|
this.cfg?.backoff.markOnline();
|
|
await this.cfg?.mqttReady();
|
|
}
|
|
|
|
async simulateReconnect(): Promise<void> {
|
|
this.cfg?.backoff.markOffline();
|
|
this.cfg?.onOffline();
|
|
await this.simulateConnectOK();
|
|
}
|
|
|
|
simulateAuthFail(reason: AuthReason): void {
|
|
this.cfg?.onAuthFailed(reason);
|
|
}
|
|
|
|
simulateKick(): void {
|
|
this.cfg?.onKicked();
|
|
}
|
|
|
|
simulateServerDisconnect(code: number): void {
|
|
if (code === 0x8e) {
|
|
this.cfg?.onKicked();
|
|
return;
|
|
}
|
|
this.cfg?.backoff.markOffline();
|
|
this.cfg?.onOffline();
|
|
}
|
|
|
|
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<Record<string, unknown>> {
|
|
const out: Array<Record<string, unknown>> = [];
|
|
for (const s of this.up) {
|
|
try {
|
|
const m = JSON.parse(s) as Record<string, unknown>;
|
|
if (m.type === type) out.push(m);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
}
|
|
|
|
export function assertNever(_x: never): void {
|
|
throw new APIError("bad_request", "unreachable");
|
|
}
|