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,17 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
describe("network-transport-hyperswarm dep-graph", () => {
it("must not have circular dependencies", { timeout: 5000 }, async () => {
const mod = await import("../src/index.js");
expect(mod).toBeDefined();
expect(mod.HyperswarmPlugin).toBeDefined();
expect(mod.HyperswarmTransport).toBeDefined();
expect(mod.NET_HYPERSWARM_CODES).toBeDefined();
expect(mod.FrameDecoder).toBeDefined();
expect(mod.ReputationStore).toBeDefined();
expect(mod.ConnectionRegistry).toBeDefined();
expect(mod.validatePeer).toBeDefined();
expect(mod.joinTopic).toBeDefined();
expect(mod.prepareTopic).toBeDefined();
});
});

View File

@@ -0,0 +1,126 @@
import type { MessageEnvelope, PeerId } from "@suaveplan/network";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { HyperswarmTransport } from "../src/transport/transport.js";
import { FakeSwarmHub, makeFakeFactory } from "./setup/fake-hyperswarm.js";
const TOPIC = new Uint8Array(32).fill(0x77);
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 = 2): Promise<void> {
for (let i = 0; i < n; i++) {
await new Promise((r) => setTimeout(r, 0));
}
}
describe("3-peer in-process integration", () => {
it("all-to-all envelope exchange via shared DHT topic", {
timeout: 8000,
}, async () => {
const hub = new FakeSwarmHub();
const { factory: fA } = makeFakeFactory(hub);
const { factory: fB } = makeFakeFactory(hub);
const { factory: fC } = makeFakeFactory(hub);
const a = new HyperswarmTransport({
topic: TOPIC,
hyperswarmFactory: fA,
});
const b = new HyperswarmTransport({
topic: TOPIC,
hyperswarmFactory: fB,
});
const c = new HyperswarmTransport({
topic: TOPIC,
hyperswarmFactory: fC,
});
await a.start();
await b.start();
await c.start();
await tick(4);
// Each node sees the other two
expect(a.getConnectedPeers().length).toBe(2);
expect(b.getConnectedPeers().length).toBe(2);
expect(c.getConnectedPeers().length).toBe(2);
const inboxA: Array<[PeerId, string]> = [];
const inboxB: Array<[PeerId, string]> = [];
const inboxC: Array<[PeerId, string]> = [];
a.onMessage((pid, env) => inboxA.push([pid, String(env.id)]));
b.onMessage((pid, env) => inboxB.push([pid, String(env.id)]));
c.onMessage((pid, env) => inboxC.push([pid, String(env.id)]));
// a -> all peers
for (const peerId of a.getConnectedPeers()) {
await a.send(peerId, envelope("from-a", "a"));
}
// b -> all peers
for (const peerId of b.getConnectedPeers()) {
await b.send(peerId, envelope("from-b", "b"));
}
// c -> all peers
for (const peerId of c.getConnectedPeers()) {
await c.send(peerId, envelope("from-c", "c"));
}
await tick(4);
// Each inbox should contain two messages — one from each remote.
expect(inboxA.length).toBe(2);
expect(inboxB.length).toBe(2);
expect(inboxC.length).toBe(2);
const messagesA = inboxA.map(([, id]) => id).sort();
expect(messagesA).toEqual(["from-b", "from-c"]);
const messagesB = inboxB.map(([, id]) => id).sort();
expect(messagesB).toEqual(["from-a", "from-c"]);
const messagesC = inboxC.map(([, id]) => id).sort();
expect(messagesC).toEqual(["from-a", "from-b"]);
await a.close();
await b.close();
await c.close();
});
it("peer disconnect propagates to remote node", {
timeout: 8000,
}, async () => {
const hub = new FakeSwarmHub();
const { factory: fA } = makeFakeFactory(hub);
const { factory: fB } = makeFakeFactory(hub);
const a = new HyperswarmTransport({
topic: TOPIC,
hyperswarmFactory: fA,
});
const b = new HyperswarmTransport({
topic: TOPIC,
hyperswarmFactory: fB,
});
await a.start();
await b.start();
await tick(2);
const events: string[] = [];
b.onPeerEvent((_id, e) => events.push(e));
const peerOnA = a.getConnectedPeers()[0];
if (!peerOnA) throw new Error("no peer");
await a.disconnect(peerOnA);
await tick(2);
expect(events).toContain("disconnected");
expect(b.getConnectedPeers()).toHaveLength(0);
await a.close();
await b.close();
});
});

View File

@@ -0,0 +1,302 @@
import { randomBytes } from "node:crypto";
import type {
HyperswarmDiscoveryLike,
HyperswarmFactory,
HyperswarmInstanceLike,
HyperswarmPeerInfo,
HyperswarmSocketLike,
} from "../../src/types/types.js";
/**
* Deterministic real implementation of `HyperswarmSocketLike` for tests.
* NOT a mock — every method does real work over an in-memory pair so
* the test exercises the same code paths as production.
*/
export class FakeSocket implements HyperswarmSocketLike {
public destroyed = false;
#peer: FakeSocket | undefined;
#dataHandlers = new Set<(data: Uint8Array) => void>();
#errorHandlers = new Set<(err: Error) => void>();
#closeHandlers = new Set<() => void>();
#timeoutHandlers = new Set<() => void>();
#timeoutMs = 0;
public pair(other: FakeSocket): void {
this.#peer = other;
}
public write(data: Uint8Array, callback?: (err?: Error) => void): boolean {
if (this.destroyed) {
callback?.(new Error("fake-socket.destroyed"));
return false;
}
const peer = this.#peer;
if (!peer) {
callback?.(new Error("fake-socket.unpaired"));
return false;
}
queueMicrotask(() => {
if (peer.destroyed) {
callback?.(new Error("fake-socket.peer-destroyed"));
return;
}
for (const handler of peer.#dataHandlers) {
handler(data);
}
callback?.();
});
return true;
}
public on(event: string, listener: (...args: never[]) => void): this {
if (event === "data") {
this.#dataHandlers.add(listener as (data: Uint8Array) => void);
} else if (event === "error") {
this.#errorHandlers.add(listener as (err: Error) => void);
} else if (event === "close") {
this.#closeHandlers.add(listener as () => void);
} else if (event === "timeout") {
this.#timeoutHandlers.add(listener as () => void);
}
return this;
}
public setTimeout(ms: number): void {
this.#timeoutMs = ms;
}
public get timeoutMs(): number {
return this.#timeoutMs;
}
public destroy(_error?: Error): void {
if (this.destroyed) {
return;
}
this.destroyed = true;
for (const handler of this.#closeHandlers) {
handler();
}
if (this.#peer && !this.#peer.destroyed) {
this.#peer.destroy();
}
}
/** Simulate a remote error event for tests. */
public emitError(err: Error): void {
for (const handler of this.#errorHandlers) {
handler(err);
}
}
/** Simulate an idle-timeout for tests. */
public emitTimeout(): void {
for (const handler of this.#timeoutHandlers) {
handler();
}
}
/** Simulate a remote close without destroying for tests. */
public emitClose(): void {
for (const handler of this.#closeHandlers) {
handler();
}
}
}
/**
* In-process swarm registry shared by every `FakeHyperswarm` instance
* created within a single test. Models the DHT as a multi-map from
* topic-hex to swarm-set; whenever two swarms join the same topic, a
* `FakeSocket` pair is materialised and delivered to both.
*/
export class FakeSwarmHub {
readonly #byTopic = new Map<string, Set<FakeHyperswarm>>();
public join(swarm: FakeHyperswarm, topic: Uint8Array): void {
const hex = bufHex(topic);
let set = this.#byTopic.get(hex);
if (!set) {
set = new Set();
this.#byTopic.set(hex, set);
}
for (const other of set) {
if (other === swarm) {
continue;
}
this.#wirePair(swarm, other);
}
set.add(swarm);
}
public leave(swarm: FakeHyperswarm, topic: Uint8Array): void {
const hex = bufHex(topic);
const set = this.#byTopic.get(hex);
if (!set) {
return;
}
set.delete(swarm);
if (set.size === 0) {
this.#byTopic.delete(hex);
}
}
#wirePair(a: FakeHyperswarm, b: FakeHyperswarm): void {
const sockA = new FakeSocket();
const sockB = new FakeSocket();
sockA.pair(sockB);
sockB.pair(sockA);
// Emit on next microtask so listeners attached after join still see it.
queueMicrotask(() => {
a.deliverInbound(sockA, { publicKey: b.publicKey });
b.deliverInbound(sockB, { publicKey: a.publicKey });
});
}
}
/**
* Deterministic real implementation of `HyperswarmInstanceLike`.
* NOT a mock — backed by `FakeSwarmHub` so writes traverse a real
* in-memory duplex pair.
*/
export class FakeHyperswarm implements HyperswarmInstanceLike {
public readonly publicKey: Uint8Array;
public connecting = 0;
public readonly connections = new Set<HyperswarmSocketLike>();
#connectionListeners = new Set<
(socket: HyperswarmSocketLike, info: HyperswarmPeerInfo) => void
>();
#hub: FakeSwarmHub;
#joinedTopics = new Set<string>();
#destroyed = false;
#flushSucceeds: boolean;
#joinThrows: boolean;
public constructor(
hub: FakeSwarmHub,
opts: {
publicKey?: Uint8Array;
flushSucceeds?: boolean;
joinThrows?: boolean;
} = {}
) {
this.publicKey = opts.publicKey ?? randomBytes(32);
this.#hub = hub;
this.#flushSucceeds = opts.flushSucceeds ?? true;
this.#joinThrows = opts.joinThrows ?? false;
}
public join(
topic: Uint8Array,
_opts?: { server?: boolean; client?: boolean }
): HyperswarmDiscoveryLike {
if (this.#joinThrows) {
throw new Error("fake-hyperswarm.join-threw");
}
const hex = bufHex(topic);
this.#joinedTopics.add(hex);
this.#hub.join(this, topic);
const flushSucceeds = this.#flushSucceeds;
return {
flushed: async () => {
if (!flushSucceeds) {
throw new Error("fake-hyperswarm.flush-failed");
}
},
destroy: async () => {
this.#hub.leave(this, topic);
this.#joinedTopics.delete(hex);
},
refresh: async () => {
/* no-op in fake */
},
};
}
public async flush(): Promise<void> {
/* no-op */
}
public async destroy(): Promise<void> {
if (this.#destroyed) {
return;
}
this.#destroyed = true;
for (const sock of Array.from(this.connections)) {
sock.destroy();
}
this.connections.clear();
this.#connectionListeners.clear();
this.#joinedTopics.clear();
}
public on(event: string, listener: (...args: never[]) => void): this {
if (event === "connection") {
this.#connectionListeners.add(
listener as (
socket: HyperswarmSocketLike,
info: HyperswarmPeerInfo
) => void
);
}
return this;
}
public off(event: string, listener: (...args: never[]) => void): this {
if (event === "connection") {
this.#connectionListeners.delete(
listener as (
socket: HyperswarmSocketLike,
info: HyperswarmPeerInfo
) => void
);
}
return this;
}
/** Internal hook used by the hub to fire `connection`. */
public deliverInbound(
socket: HyperswarmSocketLike,
peerInfo: HyperswarmPeerInfo
): void {
if (this.#destroyed) {
return;
}
this.connections.add(socket);
for (const handler of this.#connectionListeners) {
handler(socket, peerInfo);
}
}
}
/**
* Build an `HyperswarmFactory` bound to a `FakeSwarmHub`. Successive
* `factory()` calls return fresh swarms, all sharing the hub.
*/
export function makeFakeFactory(
hub: FakeSwarmHub,
opts?: {
publicKey?: Uint8Array;
flushSucceeds?: boolean;
joinThrows?: boolean;
}
): {
factory: HyperswarmFactory;
instances: FakeHyperswarm[];
} {
const instances: FakeHyperswarm[] = [];
const factory: HyperswarmFactory = () => {
const inst = new FakeHyperswarm(hub, opts);
instances.push(inst);
return inst;
};
return { factory, instances };
}
function bufHex(buf: Uint8Array): string {
let out = "";
for (let i = 0; i < buf.length; i++) {
out += (buf[i] ?? 0).toString(16).padStart(2, "0");
}
return out;
}

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