Initial Commit
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Deterministic in-memory Trystero substitute used by the test suites. Models
|
||||
* a single shared broker — every `FakeTrysteroRoom` constructed against the
|
||||
* same `FakeTrysteroBroker` and the same `(appId, transport)` pair shares
|
||||
* peer-join/leave + message broadcast.
|
||||
*
|
||||
* No mocks: these are real implementations of the `TrysteroRoomLike`
|
||||
* structural contract. Tests that exercise the transport-specific adapters
|
||||
* select one of the four `fake<X>RoomFactory` factories below — but all four
|
||||
* factories share the same broker, the same `FakeTrysteroRoom` class, and
|
||||
* are differentiated only by the `kind` they tag the room with for
|
||||
* observability assertions.
|
||||
*/
|
||||
import type {
|
||||
IpfsRoomFactory,
|
||||
MqttRoomFactory,
|
||||
NostrRoomFactory,
|
||||
TorrentRoomFactory,
|
||||
TrysteroMessage,
|
||||
TrysteroRoomLike,
|
||||
TrysteroTransport,
|
||||
} from "../../src/types/types.js";
|
||||
|
||||
type MessageHandler = (peerId: string, data: TrysteroMessage) => void;
|
||||
type PeerHandler = (peerId: string) => void;
|
||||
|
||||
let nextPeerSeq = 0;
|
||||
|
||||
interface RoomKey {
|
||||
readonly appId: string;
|
||||
readonly transport: TrysteroTransport;
|
||||
}
|
||||
|
||||
function keyString(k: RoomKey): string {
|
||||
return `${k.transport}:${k.appId}`;
|
||||
}
|
||||
|
||||
export class FakeTrysteroBroker {
|
||||
public broken = false;
|
||||
public readonly rooms = new Map<string, Set<FakeTrysteroRoom>>();
|
||||
|
||||
public register(room: FakeTrysteroRoom): void {
|
||||
const k = keyString({ appId: room.appId, transport: room.transport });
|
||||
let set = this.rooms.get(k);
|
||||
if (set === undefined) {
|
||||
set = new Set();
|
||||
this.rooms.set(k, set);
|
||||
}
|
||||
for (const other of set) {
|
||||
other.notePeerJoined(room.myPeerId);
|
||||
room.notePeerJoined(other.myPeerId);
|
||||
}
|
||||
set.add(room);
|
||||
}
|
||||
|
||||
public unregister(room: FakeTrysteroRoom): void {
|
||||
const k = keyString({ appId: room.appId, transport: room.transport });
|
||||
const set = this.rooms.get(k);
|
||||
if (set === undefined) return;
|
||||
if (!set.delete(room)) return;
|
||||
for (const other of set) {
|
||||
other.notePeerLeft(room.myPeerId);
|
||||
}
|
||||
}
|
||||
|
||||
public broadcast(
|
||||
room: FakeTrysteroRoom,
|
||||
message: TrysteroMessage,
|
||||
target?: string | readonly string[]
|
||||
): void {
|
||||
const k = keyString({ appId: room.appId, transport: room.transport });
|
||||
const set = this.rooms.get(k);
|
||||
if (set === undefined) return;
|
||||
const targets =
|
||||
typeof target === "string"
|
||||
? new Set<string>([target])
|
||||
: Array.isArray(target)
|
||||
? new Set<string>(target as readonly string[])
|
||||
: undefined;
|
||||
for (const other of set) {
|
||||
if (other === room) continue;
|
||||
if (targets !== undefined && !targets.has(other.myPeerId)) continue;
|
||||
other.deliver(room.myPeerId, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FakeTrysteroRoom implements TrysteroRoomLike {
|
||||
public readonly appId: string;
|
||||
public readonly transport: TrysteroTransport;
|
||||
public readonly myPeerId: string;
|
||||
private readonly broker: FakeTrysteroBroker;
|
||||
private peerJoinHandlers = new Set<PeerHandler>();
|
||||
private peerLeaveHandlers = new Set<PeerHandler>();
|
||||
private messageHandlers = new Set<MessageHandler>();
|
||||
private destroyed = false;
|
||||
|
||||
public constructor(
|
||||
appId: string,
|
||||
transport: TrysteroTransport,
|
||||
broker: FakeTrysteroBroker
|
||||
) {
|
||||
this.appId = appId;
|
||||
this.transport = transport;
|
||||
this.broker = broker;
|
||||
nextPeerSeq += 1;
|
||||
this.myPeerId = `peer-${transport}-${nextPeerSeq}`;
|
||||
if (broker.broken) {
|
||||
throw new Error(`broker down [${transport}]`);
|
||||
}
|
||||
queueMicrotask(() => {
|
||||
if (!this.destroyed) broker.register(this);
|
||||
});
|
||||
}
|
||||
|
||||
public getPeerId(): string {
|
||||
return this.myPeerId;
|
||||
}
|
||||
|
||||
public makeAction<T = TrysteroMessage>(
|
||||
_namespace: string
|
||||
): readonly [
|
||||
send: (data: T, target?: string | readonly string[]) => Promise<void>,
|
||||
get: (peerId: string, data: T) => void,
|
||||
onReceive: (handler: (peerId: string, data: T) => void) => void,
|
||||
] {
|
||||
return [
|
||||
async (data, target) =>
|
||||
this.broker.broadcast(this, data as TrysteroMessage, target),
|
||||
(_peerId, _data) => {
|
||||
/* parity with Trystero's [send, get, on] triple */
|
||||
},
|
||||
(handler) =>
|
||||
this.messageHandlers.add(handler as unknown as MessageHandler),
|
||||
] as const;
|
||||
}
|
||||
|
||||
public onPeerJoin(handler: (peerId: string) => void): () => void {
|
||||
this.peerJoinHandlers.add(handler);
|
||||
return () => this.peerJoinHandlers.delete(handler);
|
||||
}
|
||||
|
||||
public onPeerLeave(handler: (peerId: string) => void): () => void {
|
||||
this.peerLeaveHandlers.add(handler);
|
||||
return () => this.peerLeaveHandlers.delete(handler);
|
||||
}
|
||||
|
||||
public leave(): void {
|
||||
if (this.destroyed) return;
|
||||
this.destroyed = true;
|
||||
this.broker.unregister(this);
|
||||
this.peerJoinHandlers.clear();
|
||||
this.peerLeaveHandlers.clear();
|
||||
this.messageHandlers.clear();
|
||||
}
|
||||
|
||||
public async ping(_peerId: string): Promise<number> {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public notePeerJoined(peerId: string): void {
|
||||
for (const h of this.peerJoinHandlers) h(peerId);
|
||||
}
|
||||
|
||||
public notePeerLeft(peerId: string): void {
|
||||
for (const h of this.peerLeaveHandlers) h(peerId);
|
||||
}
|
||||
|
||||
public deliver(from: string, message: TrysteroMessage): void {
|
||||
for (const h of this.messageHandlers) h(from, message);
|
||||
}
|
||||
}
|
||||
|
||||
export function fakeTorrentRoomFactory(
|
||||
broker: FakeTrysteroBroker
|
||||
): TorrentRoomFactory {
|
||||
return async (appId) => new FakeTrysteroRoom(appId, "torrent", broker);
|
||||
}
|
||||
|
||||
export function fakeNostrRoomFactory(
|
||||
broker: FakeTrysteroBroker
|
||||
): NostrRoomFactory {
|
||||
return async (appId) => new FakeTrysteroRoom(appId, "nostr", broker);
|
||||
}
|
||||
|
||||
export function fakeMqttRoomFactory(
|
||||
broker: FakeTrysteroBroker
|
||||
): MqttRoomFactory {
|
||||
return async (appId) => new FakeTrysteroRoom(appId, "mqtt", broker);
|
||||
}
|
||||
|
||||
export function fakeIpfsRoomFactory(
|
||||
broker: FakeTrysteroBroker
|
||||
): IpfsRoomFactory {
|
||||
return async (appId) => new FakeTrysteroRoom(appId, "ipfs", broker);
|
||||
}
|
||||
|
||||
/** Yield control until pending microtasks have drained. */
|
||||
export async function flushMicrotasks(): Promise<void> {
|
||||
for (let i = 0; i < 8; 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