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,19 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
describe("network-discovery-crdt dep-graph", () => {
it("must not have circular dependencies", async () => {
const mod = await import("../src/index.js");
expect(mod).toBeDefined();
expect(mod.CRDTDiscovery).toBeDefined();
expect(mod.CRDTPeerManager).toBeDefined();
expect(mod.CRDTIntervals).toBeDefined();
expect(mod.YjsProviderAdapter).toBeDefined();
expect(mod.AutomergeProviderAdapter).toBeDefined();
expect(mod.NET_DISC_CRDT_CODES).toBeDefined();
expect(mod.NetDiscCRDTError).toBeDefined();
expect(mod.CRDTDiscoveryConfigSchema).toBeDefined();
expect(mod.CRDTMessageType).toBeDefined();
expect(mod.withStartSpan).toBeDefined();
expect(mod.recordPeersDiscovered).toBeDefined();
});
});

View File

@@ -0,0 +1,138 @@
/**
* Deterministic in-memory Automerge substitute used by the test suites.
* Models a single shared broker — every `FakeAutomergeRepo` constructed
* against the same `FakeAutomergeBroker` shares awareness updates with every
* other repo in the same room. No mocks: real implementation of the
* `AutomergeRepoLike` structural contract.
*/
import type {
AutomergeRepoFactory,
AutomergeRepoLike,
CRDTPeerMetadata,
} from "../../src/types/types.js";
type Handler = (...args: unknown[]) => void;
let nextId = 0;
export class FakeAutomergeBroker {
public readonly rooms = new Map<string, Set<FakeAutomergeRepo>>();
public broken = false;
public join(room: string, repo: FakeAutomergeRepo): void {
let set = this.rooms.get(room);
if (set === undefined) {
set = new Set();
this.rooms.set(room, set);
}
// Replay every existing peer's snapshot into the joiner before fire.
for (const other of set) {
for (const m of other.listPeers()) repo.receiveAnnounce(m);
}
set.add(repo);
queueMicrotask(() => {
for (const other of set as Set<FakeAutomergeRepo>) {
if (other === repo) continue;
other.notePeer(repo.peerId);
repo.notePeer(other.peerId);
}
});
}
public leave(room: string, repo: FakeAutomergeRepo): void {
const set = this.rooms.get(room);
if (set === undefined) return;
set.delete(repo);
for (const other of set) other.notePeerLost(repo.peerId);
}
public broadcast(
room: string,
sender: FakeAutomergeRepo,
metadata: CRDTPeerMetadata
): void {
const set = this.rooms.get(room);
if (set === undefined) return;
for (const other of set) {
if (other === sender) continue;
other.receiveAnnounce(metadata);
}
}
}
export class FakeAutomergeRepo implements AutomergeRepoLike {
public readonly peerId: string;
private readonly handlers = new Map<string, Set<Handler>>();
private readonly known = new Map<string, CRDTPeerMetadata>();
private destroyed = false;
public constructor(
public readonly room: string,
private readonly broker: FakeAutomergeBroker
) {
nextId += 1;
this.peerId = `fake-am-${nextId}`;
broker.join(room, this);
}
public on(event: string, handler: Handler): void {
let set = this.handlers.get(event);
if (set === undefined) {
set = new Set();
this.handlers.set(event, set);
}
set.add(handler);
}
public off(event: string, handler: Handler): void {
this.handlers.get(event)?.delete(handler);
}
public announce(metadata: CRDTPeerMetadata): void {
if (this.destroyed) return;
// Mirror the announce locally so late joiners can replay it.
this.known.set(metadata.id, metadata);
this.broker.broadcast(this.room, this, metadata);
}
public listPeers(): readonly CRDTPeerMetadata[] {
return Array.from(this.known.values());
}
public shutdown(): void {
if (this.destroyed) return;
this.destroyed = true;
this.broker.leave(this.room, this);
this.handlers.clear();
}
public notePeer(peerId: string): void {
this.fire("peer", { peerId });
}
public notePeerLost(peerId: string): void {
this.known.delete(peerId);
this.fire("peer-disconnected", { peerId });
}
public receiveAnnounce(metadata: CRDTPeerMetadata): void {
this.known.set(metadata.id, metadata);
this.fire("peer", { peerId: metadata.id });
}
private fire(event: string, ...args: unknown[]): void {
for (const h of this.handlers.get(event) ?? []) h(...args);
}
}
/** Build an `AutomergeRepoFactory` bound to a shared broker. */
export function fakeAutomergeRepoFactory(
broker: FakeAutomergeBroker
): AutomergeRepoFactory {
return (room) => {
if (broker.broken) {
throw new Error("broker down");
}
return new FakeAutomergeRepo(room, broker);
};
}

View File

@@ -0,0 +1,314 @@
/**
* Deterministic in-memory Yjs substitute used by the test suites. Models a
* single shared "room" broker — every `FakeYDoc` constructed against the same
* `FakeYjsBroker` shares its `peers` map updates with every other doc. No
* mocks: this is a real implementation of the `YDocLike` / `YMapLike` /
* `WebrtcProviderLike` structural contracts.
*/
import type {
CRDTPeerMetadata,
WebrtcProviderLike,
YDocLike,
YjsDocFactory,
YMapEventLike,
YMapLike,
} from "../../src/types/types.js";
type Handler = (...args: unknown[]) => void;
let nextGuid = 0;
export class FakeYjsBroker {
public readonly rooms = new Map<string, Set<FakeWebrtcProvider>>();
public broken = false;
public join(room: string, provider: FakeWebrtcProvider): void {
let set = this.rooms.get(room);
if (set === undefined) {
set = new Set();
this.rooms.set(room, set);
}
// Replay every existing peer's current map state to the new joiner
// before announcing presence — mirrors `y-webrtc`'s "sync existing
// state on join" semantics.
for (const other of set) {
const snap = other.snapshotPeers();
for (const [key, value] of snap) {
provider.applyRemote(key, value, "add");
}
}
set.add(provider);
// Sync immediately — everyone gets everyone else's current peers.
queueMicrotask(() => {
provider.fireSynced();
for (const other of set as Set<FakeWebrtcProvider>) {
if (other !== provider) {
other.notePeerAdded(provider.doc.guid);
provider.notePeerAdded(other.doc.guid);
}
}
});
}
public leave(room: string, provider: FakeWebrtcProvider): void {
const set = this.rooms.get(room);
if (set === undefined) return;
set.delete(provider);
for (const other of set) {
other.notePeerRemoved(provider.doc.guid);
}
}
public broadcast(
room: string,
sender: FakeWebrtcProvider,
key: string,
value: CRDTPeerMetadata | undefined,
action: "add" | "update" | "delete"
): void {
const set = this.rooms.get(room);
if (set === undefined) return;
for (const other of set) {
if (other === sender) continue;
other.applyRemote(key, value, action);
}
}
}
export class FakeYMap implements YMapLike {
private readonly data = new Map<string, CRDTPeerMetadata>();
private readonly observers = new Set<(e: YMapEventLike) => void>();
public constructor(
private readonly emitRemote?: (
key: string,
value: CRDTPeerMetadata | undefined,
action: "add" | "update" | "delete"
) => void
) {}
public set(key: string, value: CRDTPeerMetadata): void {
const action: "add" | "update" = this.data.has(key) ? "update" : "add";
this.data.set(key, value);
this.fire(key, action);
this.emitRemote?.(key, value, action);
}
public get(key: string): CRDTPeerMetadata | undefined {
return this.data.get(key);
}
public delete(key: string): void {
if (!this.data.delete(key)) return;
this.fire(key, "delete");
this.emitRemote?.(key, undefined, "delete");
}
public has(key: string): boolean {
return this.data.has(key);
}
public observe(handler: (e: YMapEventLike) => void): void {
this.observers.add(handler);
}
public unobserve(handler: (e: YMapEventLike) => void): void {
this.observers.delete(handler);
}
public toJSON(): Record<string, CRDTPeerMetadata> {
return Object.fromEntries(this.data.entries());
}
/** Apply a remote change without echoing it back to the network. */
public applyRemote(
key: string,
value: CRDTPeerMetadata | undefined,
action: "add" | "update" | "delete"
): void {
if (action === "delete") {
if (!this.data.delete(key)) return;
} else {
if (value === undefined) return;
this.data.set(key, value);
}
this.fire(key, action);
}
private fire(key: string, action: "add" | "update" | "delete"): void {
const event: YMapEventLike = {
changes: { keys: new Map([[key, { action }]]) },
};
for (const observer of this.observers) observer(event);
}
}
export class FakeYDoc implements YDocLike {
public readonly guid: string;
private map: FakeYMap | null = null;
private readonly handlers = new Map<string, Set<Handler>>();
private remoteEmitter:
| ((
key: string,
value: CRDTPeerMetadata | undefined,
action: "add" | "update" | "delete"
) => void)
| undefined;
public constructor(guid?: string) {
nextGuid += 1;
this.guid = guid ?? `fake-doc-${nextGuid}`;
}
public attachEmitter(
emitter: (
key: string,
value: CRDTPeerMetadata | undefined,
action: "add" | "update" | "delete"
) => void
): void {
this.remoteEmitter = emitter;
}
public getMap(_name: string): YMapLike {
if (this.map === null) {
this.map = new FakeYMap((k, v, a) => this.remoteEmitter?.(k, v, a));
}
return this.map;
}
public destroy(): void {
this.fire("destroy");
this.handlers.clear();
}
public on(event: string, handler: Handler): void {
let set = this.handlers.get(event);
if (set === undefined) {
set = new Set();
this.handlers.set(event, set);
}
set.add(handler);
}
public off(event: string, handler: Handler): void {
this.handlers.get(event)?.delete(handler);
}
/** Apply a remote map mutation (received via the broker). */
public applyRemote(
key: string,
value: CRDTPeerMetadata | undefined,
action: "add" | "update" | "delete"
): void {
if (this.map === null) {
// Lazily create the map so applyRemote works before observers attach.
this.map = new FakeYMap((k, v, a) => this.remoteEmitter?.(k, v, a));
}
this.map.applyRemote(key, value, action);
}
public snapshotPeers(): ReadonlyArray<[string, CRDTPeerMetadata]> {
if (this.map === null) return [];
return Object.entries(this.map.toJSON());
}
private fire(event: string): void {
for (const h of this.handlers.get(event) ?? []) h();
}
}
export class FakeWebrtcProvider implements WebrtcProviderLike {
public readonly doc: FakeYDoc;
public readonly room: string;
public connected = true;
public synced = false;
private readonly handlers = new Map<string, Set<Handler>>();
private destroyed = false;
public constructor(
room: string,
doc: FakeYDoc,
private readonly broker: FakeYjsBroker
) {
this.doc = doc;
this.room = room;
doc.attachEmitter((k, v, a) => broker.broadcast(room, this, k, v, a));
if (broker.broken) {
// Schedule an immediate error after construction so callers can wire
// handlers before the failure fires.
queueMicrotask(() => {
this.connected = false;
this.fire("connection-error", new Error("broker down"));
});
return;
}
broker.join(room, this);
}
public on(event: string, handler: Handler): void {
let set = this.handlers.get(event);
if (set === undefined) {
set = new Set();
this.handlers.set(event, set);
}
set.add(handler);
}
public off(event: string, handler: Handler): void {
this.handlers.get(event)?.delete(handler);
}
public destroy(): void {
if (this.destroyed) return;
this.destroyed = true;
this.connected = false;
this.broker.leave(this.room, this);
this.handlers.clear();
}
public fireSynced(): void {
this.synced = true;
this.fire("synced");
}
public notePeerAdded(peerId: string): void {
this.fire("peers", { added: [peerId], removed: [] });
}
public notePeerRemoved(peerId: string): void {
this.fire("peers", { added: [], removed: [peerId] });
}
public applyRemote(
key: string,
value: CRDTPeerMetadata | undefined,
action: "add" | "update" | "delete"
): void {
this.doc.applyRemote(key, value, action);
}
public snapshotPeers(): ReadonlyArray<[string, CRDTPeerMetadata]> {
return this.doc.snapshotPeers();
}
private fire(event: string, ...args: unknown[]): void {
for (const h of this.handlers.get(event) ?? []) h(...args);
}
}
/** Build a `YjsDocFactory` bound to a shared broker. */
export function fakeYjsDocFactory(broker: FakeYjsBroker): YjsDocFactory {
return (room) => {
const doc = new FakeYDoc();
const provider = new FakeWebrtcProvider(room, doc, broker);
return { doc, provider };
};
}
/** Yield control until pending microtasks have drained. */
export async function flushMicrotasks(): Promise<void> {
for (let i = 0; i < 4; i += 1) {
await Promise.resolve();
}
}

View File

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