/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Time } from "#time/Time.js"; import { Bytes } from "#util/Bytes.js"; import type { Transport } from "../Transport.js"; import type { TcpConnection } from "../tcp/TcpConnection.js"; /** * Mock TCP connection for testing. Two connections are created as a connected pair; * data written to one is delivered to the other via async iteration. */ export class MockTcpConnection implements TcpConnection { readonly remoteAddress: string; readonly remotePort: number; readonly localPort: number; #peer?: MockTcpConnection; readonly #closeListeners = new Set<() => void>(); readonly #errorListeners = new Set<(error: Error) => void>(); #closed = false; /** Queue for async iteration — chunks waiting to be consumed. */ #chunks = new Array(); /** Resolver for a pending next() call waiting for data. */ #waiter?: (value: IteratorResult) => void; private constructor(localPort: number, remoteAddress: string, remotePort: number) { this.localPort = localPort; this.remoteAddress = remoteAddress; this.remotePort = remotePort; } /** * Create a connected pair of mock TCP connections. */ static createPair( clientAddress: string, clientPort: number, serverAddress: string, serverPort: number, ): [client: MockTcpConnection, server: MockTcpConnection] { const client = new MockTcpConnection(clientPort, serverAddress, serverPort); const server = new MockTcpConnection(serverPort, clientAddress, clientPort); client.#peer = server; server.#peer = client; return [client, server]; } async send(data: Bytes): Promise { if (this.#closed) { throw new Error("Connection is closed"); } const peer = this.#peer; if (!peer || peer.#closed) { throw new Error("Peer connection is closed"); } // Deliver asynchronously await Time.macrotask; // Re-check after yield — peer may have closed during the macrotask if (peer.#closed) return; // Push to peer's iterator queue if (peer.#waiter) { const resolve = peer.#waiter; peer.#waiter = undefined; resolve({ value: data, done: false }); } else { peer.#chunks.push(data); } } [Symbol.asyncIterator](): AsyncIterator { return { next: () => { if (this.#chunks.length > 0) { return Promise.resolve({ value: this.#chunks.shift()!, done: false }); } if (this.#closed) { return Promise.resolve({ value: undefined as unknown as Bytes, done: true }); } return new Promise>(resolve => { this.#waiter = resolve; }); }, }; } onClose(listener: () => void): Transport.Listener { this.#closeListeners.add(listener); return { close: async () => { this.#closeListeners.delete(listener); }, }; } onError(listener: (error: Error) => void): Transport.Listener { this.#errorListeners.add(listener); return { close: async () => { this.#errorListeners.delete(listener); }, }; } async close(): Promise { if (this.#closed) { return; } this.#closed = true; // Terminate iterator this.#waiter?.({ value: undefined as unknown as Bytes, done: true }); this.#waiter = undefined; // Notify local close listeners for (const listener of this.#closeListeners) { listener(); } // Notify peer const peer = this.#peer; if (peer && !peer.#closed) { peer.#closed = true; peer.#waiter?.({ value: undefined as unknown as Bytes, done: true }); peer.#waiter = undefined; for (const listener of peer.#closeListeners) { listener(); } } } }