Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
describe("network-transport-libp2p dep-graph", () => {
it("must not have circular dependencies", { timeout: 5000 }, async () => {
const mod = await import("../src/index.js");
expect(mod).toBeDefined();
expect(mod.Libp2pPlugin).toBeDefined();
expect(mod.Libp2pTransport).toBeDefined();
expect(mod.NET_LIBP2P_CODES).toBeDefined();
expect(mod.FrameDecoder).toBeDefined();
expect(mod.encodeFrame).toBeDefined();
expect(mod.encodePeerMessage).toBeDefined();
expect(mod.decodePeerMessage).toBeDefined();
expect(mod.sendEnvelope).toBeDefined();
expect(mod.registerStreamHandler).toBeDefined();
expect(mod.createAndStartNode).toBeDefined();
});
});

View File

@@ -0,0 +1,95 @@
import type { MessageEnvelope, PeerId } from "@suaveplan/network";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { Libp2pTransport } from "../src/transport/transport.js";
import { FakeLibp2pHub, makeFakeFactory } from "./setup/fake-libp2p.js";
function envelope(id: string, sender: string): MessageEnvelope {
return {
id,
type: "test:msg",
payload: { i: id },
senderId: sender,
timestamp: 1,
headers: { version: "1.0.0" },
ttl: 8,
visitedNodes: [],
} as unknown as MessageEnvelope;
}
async function tick(n = 4): Promise<void> {
for (let i = 0; i < n; i++) await new Promise((r) => setTimeout(r, 0));
}
describe("2-peer integration over fake websockets sub-transport", () => {
it("dial → envelope round-trip", { timeout: 8000 }, async () => {
const hub = new FakeLibp2pHub();
const { factory: fA } = makeFakeFactory(hub, { idPrefix: "ws-a" });
const { factory: fB } = makeFakeFactory(hub, { idPrefix: "ws-b" });
const a = new Libp2pTransport({
libp2pFactory: fA,
bootstrap: ["/memory/seed"],
subTransport: "websockets",
});
const b = new Libp2pTransport({
libp2pFactory: fB,
bootstrap: ["/memory/seed"],
subTransport: "websockets",
});
await a.start();
await b.start();
const inboxB: Array<[PeerId, string]> = [];
b.onMessage((peer, env) => inboxB.push([peer, String(env.id)]));
// a dials b
await a.connect("ws-b-1", "/memory/ws-b-1");
await tick(2);
expect(a.getConnectedPeers()).toContain("ws-b-1");
// a sends to b
await a.send("ws-b-1", envelope("from-a", "ws-a-1"));
await tick(2);
expect(inboxB).toHaveLength(1);
expect(inboxB[0]?.[1]).toBe("from-a");
await a.close();
await b.close();
});
});
describe("2-peer integration over fake webrtc sub-transport", () => {
it("dial → envelope round-trip", { timeout: 8000 }, async () => {
const hub = new FakeLibp2pHub();
const { factory: fA } = makeFakeFactory(hub, { idPrefix: "wr-a" });
const { factory: fB } = makeFakeFactory(hub, { idPrefix: "wr-b" });
const a = new Libp2pTransport({
libp2pFactory: fA,
bootstrap: ["/memory/seed"],
subTransport: "webrtc",
});
const b = new Libp2pTransport({
libp2pFactory: fB,
bootstrap: ["/memory/seed"],
subTransport: "webrtc",
});
await a.start();
await b.start();
const inboxB: Array<[PeerId, string]> = [];
b.onMessage((peer, env) => inboxB.push([peer, String(env.id)]));
await a.connect("wr-b-1", "/memory/wr-b-1");
await tick(2);
await a.send("wr-b-1", envelope("rtc-msg", "wr-a-1"));
await tick(2);
expect(inboxB).toHaveLength(1);
expect(inboxB[0]?.[1]).toBe("rtc-msg");
await a.close();
await b.close();
});
});

View File

@@ -0,0 +1,321 @@
import type {
Libp2pFactory,
Libp2pFactoryConfig,
Libp2pNodeLike,
Libp2pPeerIdLike,
Libp2pStreamHandler,
Libp2pStreamLike,
} from "../../src/types/types.js";
let nextNodeNum = 1;
/** Deterministic peer-id object. */
export class FakePeerId implements Libp2pPeerIdLike {
public constructor(private readonly id: string) {}
public toString(): string {
return this.id;
}
}
/**
* Real in-memory `Libp2pStreamLike` pair. Two `FakeStream`s share an
* outbound queue + inbound queue so `sink` on one side becomes async-
* iterable `source` chunks on the other.
*/
export class FakeStream implements Libp2pStreamLike {
#inbound: Uint8Array[] = [];
#waiters: Array<(v: IteratorResult<{ subarray(): Uint8Array }>) => void> =
[];
#closed = false;
#sourceEnded = false;
#peer: FakeStream | undefined;
public pair(other: FakeStream): void {
this.#peer = other;
}
public get source(): AsyncIterable<{ subarray(): Uint8Array }> {
const self = this;
return {
[Symbol.asyncIterator]() {
return {
async next(): Promise<
IteratorResult<{ subarray(): Uint8Array }>
> {
if (self.#inbound.length > 0) {
const bytes = self.#inbound.shift();
if (!bytes) {
return { value: undefined, done: true };
}
return {
value: { subarray: () => bytes },
done: false,
};
}
if (self.#sourceEnded || self.#closed) {
return { value: undefined, done: true };
}
return new Promise((resolve) => {
self.#waiters.push(resolve);
});
},
};
},
};
}
public async sink(
data: Iterable<Uint8Array> | AsyncIterable<Uint8Array>
): Promise<void> {
const peer = this.#peer;
if (!peer) {
throw new Error("fake-stream.unpaired");
}
for await (const chunk of data) {
peer.#deliver(chunk);
}
}
public async close(): Promise<void> {
if (this.#closed) return;
this.#closed = true;
this.#sourceEnded = true;
for (const w of this.#waiters) {
w({ value: undefined, done: true });
}
this.#waiters = [];
// Signal end-of-stream to the peer's source iterator as well so
// `consumeInboundStream` exits its `for await` when the writer
// half closes its end of the duplex pair.
this.#peer?.signalSourceEnd();
}
/** Internal: peer-half close signals end-of-stream on our source. */
public signalSourceEnd(): void {
if (this.#sourceEnded) return;
this.#sourceEnded = true;
for (const w of this.#waiters) {
w({ value: undefined, done: true });
}
this.#waiters = [];
}
#deliver(bytes: Uint8Array): void {
if (this.#waiters.length > 0) {
const w = this.#waiters.shift();
w?.({ value: { subarray: () => bytes }, done: false });
return;
}
this.#inbound.push(bytes);
}
}
interface NodeListeners {
handlers: Map<string, Libp2pStreamHandler>;
connectListeners: Set<(event: { detail: unknown }) => void>;
disconnectListeners: Set<(event: { detail: unknown }) => void>;
}
/**
* Hub coordinating multiple `FakeLibp2pNode` instances. Nodes register
* by id; dialing a peer wires their `peer:connect` event listeners and
* unlocks `dialProtocol` between them.
*/
export class FakeLibp2pHub {
readonly #nodes = new Map<string, FakeLibp2pNode>();
public register(node: FakeLibp2pNode): void {
this.#nodes.set(node.id, node);
}
public unregister(node: FakeLibp2pNode): void {
this.#nodes.delete(node.id);
for (const other of this.#nodes.values()) {
other.dropPeer(node.id);
}
}
public get(id: string): FakeLibp2pNode | undefined {
return this.#nodes.get(id);
}
public connectPair(a: FakeLibp2pNode, b: FakeLibp2pNode): void {
a.markConnected(b.id);
b.markConnected(a.id);
}
}
/**
* Real in-memory implementation of `Libp2pNodeLike`. Not a mock — every
* method does real work over `FakeStream` pairs and `FakeLibp2pHub`.
*/
export class FakeLibp2pNode implements Libp2pNodeLike {
public readonly id: string;
readonly #hub: FakeLibp2pHub;
readonly #peers = new Set<string>();
readonly #listeners: NodeListeners = {
handlers: new Map(),
connectListeners: new Set(),
disconnectListeners: new Set(),
};
#started = false;
#stopped = false;
public startThrows = false;
public dialThrows = false;
public dialProtocolThrows = false;
public constructor(hub: FakeLibp2pHub, id?: string) {
this.id = id ?? `peer-${nextNodeNum++}`;
this.#hub = hub;
hub.register(this);
}
public async start(): Promise<void> {
if (this.startThrows) {
throw new Error("fake-libp2p.start-failed");
}
this.#started = true;
}
public async stop(): Promise<void> {
if (this.#stopped) return;
this.#stopped = true;
this.#started = false;
for (const peerId of Array.from(this.#peers)) {
this.dropPeer(peerId);
}
this.#hub.unregister(this);
}
public async dial(addr: string): Promise<unknown> {
if (this.dialThrows) {
throw new Error("fake-libp2p.dial-failed");
}
// addr format: "/memory/<peer-id>"
const targetId = addr.replace(/^.*\//, "");
const target = this.#hub.get(targetId);
if (!target) {
throw new Error(`fake-libp2p.unknown-peer:${targetId}`);
}
this.#hub.connectPair(this, target);
return {};
}
public async dialProtocol(
peerId: Libp2pPeerIdLike,
protocolId: string
): Promise<Libp2pStreamLike> {
if (this.dialProtocolThrows) {
throw new Error("fake-libp2p.dial-protocol-failed");
}
const target = this.#hub.get(peerId.toString());
if (!target) {
throw new Error(`fake-libp2p.unknown-peer:${peerId.toString()}`);
}
const handler = target.#listeners.handlers.get(protocolId);
if (!handler) {
throw new Error(`fake-libp2p.protocol-not-supported:${protocolId}`);
}
const a = new FakeStream();
const b = new FakeStream();
a.pair(b);
b.pair(a);
handler({
stream: b,
connection: { remotePeer: new FakePeerId(this.id) },
});
return a;
}
public async handle(
protocolId: string,
handler: Libp2pStreamHandler
): Promise<void> {
this.#listeners.handlers.set(protocolId, handler);
}
public async unhandle(protocolId: string): Promise<void> {
this.#listeners.handlers.delete(protocolId);
}
public getPeers(): Libp2pPeerIdLike[] {
return Array.from(this.#peers).map((id) => new FakePeerId(id));
}
public addEventListener(
type: string,
listener: (event: { detail: unknown }) => void
): void {
if (type === "peer:connect") {
this.#listeners.connectListeners.add(listener);
} else if (type === "peer:disconnect") {
this.#listeners.disconnectListeners.add(listener);
}
}
public removeEventListener(
type: string,
listener: (event: { detail: unknown }) => void
): void {
if (type === "peer:connect") {
this.#listeners.connectListeners.delete(listener);
} else if (type === "peer:disconnect") {
this.#listeners.disconnectListeners.delete(listener);
}
}
/** Internal: hub uses this to fire peer:connect for both ends. */
public markConnected(peerId: string): void {
if (this.#peers.has(peerId)) return;
this.#peers.add(peerId);
for (const listener of this.#listeners.connectListeners) {
listener({ detail: new FakePeerId(peerId) });
}
}
/** Internal: hub uses this to fire peer:disconnect from either end. */
public dropPeer(peerId: string): void {
if (!this.#peers.has(peerId)) return;
this.#peers.delete(peerId);
for (const listener of this.#listeners.disconnectListeners) {
listener({ detail: new FakePeerId(peerId) });
}
}
/** Test-only: force-fire a peer:connect event bypassing dedup. */
public forceEmitConnect(peerId: string): void {
for (const listener of this.#listeners.connectListeners) {
listener({ detail: new FakePeerId(peerId) });
}
}
/** Test-only: force-fire a peer:disconnect event bypassing dedup. */
public forceEmitDisconnect(peerId: string): void {
for (const listener of this.#listeners.disconnectListeners) {
listener({ detail: new FakePeerId(peerId) });
}
}
public get started(): boolean {
return this.#started;
}
}
/** Build a `Libp2pFactory` bound to a hub. */
export function makeFakeFactory(
hub: FakeLibp2pHub,
opts?: { idPrefix?: string; nodeOptions?: Partial<FakeLibp2pNode> }
): { factory: Libp2pFactory; instances: FakeLibp2pNode[] } {
const instances: FakeLibp2pNode[] = [];
let counter = 0;
const factory: Libp2pFactory = async (_config: Libp2pFactoryConfig) => {
const id = opts?.idPrefix ? `${opts.idPrefix}-${++counter}` : undefined;
const node = new FakeLibp2pNode(hub, id);
if (opts?.nodeOptions?.startThrows) {
node.startThrows = opts.nodeOptions.startThrows;
}
instances.push(node);
return node;
};
return { factory, instances };
}

View File

@@ -0,0 +1,7 @@
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;
}