Initial Commit
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Deterministic in-memory PeerJS substitute used by the test suites. Models a
|
||||
* single shared broker — every `FakePeer` constructed against the same
|
||||
* `FakeBroker` can `connect` to every other peer. No mocks: this is a real
|
||||
* implementation of the structural contract.
|
||||
*/
|
||||
import type {
|
||||
DataConnectionLike,
|
||||
PeerFactory,
|
||||
PeerJSMessage,
|
||||
PeerJSPeerOptions,
|
||||
PeerLike,
|
||||
} from "../../src/types/types.js";
|
||||
|
||||
type Handler = (arg?: unknown) => void;
|
||||
|
||||
/** Shared broker state — wires `FakePeer`s together by id. */
|
||||
export class FakeBroker {
|
||||
public readonly peers = new Map<string, FakePeer>();
|
||||
public nextId = 0;
|
||||
/** When `true`, peer-open is rejected with `Error('broker down')`. */
|
||||
public broken = false;
|
||||
|
||||
public register(peer: FakePeer): string {
|
||||
this.nextId += 1;
|
||||
const id = peer.configuredPeerId || `fake-peer-${this.nextId}`;
|
||||
this.peers.set(id, peer);
|
||||
return id;
|
||||
}
|
||||
|
||||
public unregister(id: string): void {
|
||||
this.peers.delete(id);
|
||||
}
|
||||
|
||||
public find(id: string): FakePeer | undefined {
|
||||
return this.peers.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
/** Implementation of `DataConnectionLike` shared by both ends of a channel. */
|
||||
export class FakeDataConnection implements DataConnectionLike {
|
||||
public open = false;
|
||||
private readonly handlers = new Map<string, Set<Handler>>();
|
||||
public partner: FakeDataConnection | undefined;
|
||||
|
||||
public constructor(public readonly peer: string) {}
|
||||
|
||||
public send(message: PeerJSMessage): void {
|
||||
if (!this.open) throw new Error("connection closed");
|
||||
const partner = this.partner;
|
||||
if (partner === undefined) return;
|
||||
queueMicrotask(() => {
|
||||
partner.emit("data", message);
|
||||
});
|
||||
}
|
||||
|
||||
public on(event: "open", h: () => void): void;
|
||||
public on(event: "data", h: (data: unknown) => void): void;
|
||||
public on(event: "error", h: (error: Error) => void): void;
|
||||
public on(event: "close", h: () => void): void;
|
||||
public on(event: string, h: Handler): void {
|
||||
let set = this.handlers.get(event);
|
||||
if (set === undefined) {
|
||||
set = new Set();
|
||||
this.handlers.set(event, set);
|
||||
}
|
||||
set.add(h);
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
if (!this.open && this.handlers.size === 0) return;
|
||||
this.open = false;
|
||||
this.emit("close");
|
||||
this.partner?.fireClose();
|
||||
}
|
||||
|
||||
public fireOpen(): void {
|
||||
this.open = true;
|
||||
this.emit("open");
|
||||
}
|
||||
|
||||
public fireError(error: Error): void {
|
||||
this.emit("error", error);
|
||||
}
|
||||
|
||||
private fireClose(): void {
|
||||
if (!this.open) return;
|
||||
this.open = false;
|
||||
this.emit("close");
|
||||
}
|
||||
|
||||
public emit(event: string, arg?: unknown): void {
|
||||
const set = this.handlers.get(event);
|
||||
if (set === undefined) return;
|
||||
for (const handler of set) {
|
||||
handler(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Implementation of `PeerLike`. */
|
||||
export class FakePeer implements PeerLike {
|
||||
public readonly handlers = new Map<string, Set<Handler>>();
|
||||
public id: string | undefined;
|
||||
public destroyed = false;
|
||||
public outboundFailure: Error | undefined;
|
||||
|
||||
public constructor(
|
||||
public readonly broker: FakeBroker,
|
||||
public readonly configuredPeerId: string,
|
||||
public readonly options: PeerJSPeerOptions
|
||||
) {
|
||||
queueMicrotask(() => {
|
||||
if (this.destroyed) return;
|
||||
if (broker.broken) {
|
||||
this.emit("error", new Error("broker down"));
|
||||
return;
|
||||
}
|
||||
this.id = broker.register(this);
|
||||
this.emit("open", this.id);
|
||||
});
|
||||
}
|
||||
|
||||
public connect(peerId: string): DataConnectionLike {
|
||||
if (this.outboundFailure !== undefined) {
|
||||
throw this.outboundFailure;
|
||||
}
|
||||
const remote = this.broker.find(peerId);
|
||||
const local = new FakeDataConnection(peerId);
|
||||
const partner = new FakeDataConnection(this.id ?? "");
|
||||
local.partner = partner;
|
||||
partner.partner = local;
|
||||
if (remote !== undefined) {
|
||||
queueMicrotask(() => {
|
||||
remote.emit("connection", partner);
|
||||
partner.fireOpen();
|
||||
local.fireOpen();
|
||||
});
|
||||
} else {
|
||||
queueMicrotask(() => {
|
||||
local.fireError(new Error(`peer ${peerId} not found`));
|
||||
});
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
public on(event: "open", h: (id: string) => void): void;
|
||||
public on(event: "connection", h: (c: DataConnectionLike) => void): void;
|
||||
public on(event: "error", h: (e: Error) => void): void;
|
||||
public on(event: "disconnected", h: () => void): void;
|
||||
public on(event: "close", h: () => void): void;
|
||||
public on(event: string, h: Handler): void {
|
||||
let set = this.handlers.get(event);
|
||||
if (set === undefined) {
|
||||
set = new Set();
|
||||
this.handlers.set(event, set);
|
||||
}
|
||||
set.add(h);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.destroyed) return;
|
||||
this.destroyed = true;
|
||||
if (this.id !== undefined) this.broker.unregister(this.id);
|
||||
this.emit("close");
|
||||
}
|
||||
|
||||
public emit(event: string, arg?: unknown): void {
|
||||
const set = this.handlers.get(event);
|
||||
if (set === undefined) return;
|
||||
for (const handler of set) {
|
||||
handler(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Build a `PeerFactory` bound to a shared broker. */
|
||||
export function fakePeerFactory(broker: FakeBroker): PeerFactory {
|
||||
return (peerId, options) => new FakePeer(broker, peerId, options);
|
||||
}
|
||||
|
||||
/** Yield control until pending microtasks have drained. */
|
||||
export async function flushMicrotasks(): Promise<void> {
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Ensure `globalThis.crypto` is available in bare-Node vitest runs.
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
type WritableGlobal = { crypto?: Crypto };
|
||||
const g = globalThis as WritableGlobal;
|
||||
if (g.crypto === undefined) {
|
||||
g.crypto = webcrypto as unknown as Crypto;
|
||||
}
|
||||
Reference in New Issue
Block a user