Files
NixMsg/sdk/js/src/fake.ts
T

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