feat(server): the relay joins modems of different projects by a code (K3)

A modem that offers a connection to another project is found by six
characters derived from its project and itself (modemCode): a copy of the
project has another code, so no two modems offer the same one. The room
names its modems of other projects to F's relay at a start and none at a
stop; the relay pairs each offer with the first modem that joins its code,
one to one as Java's server modem takes one client, and passes the frames
between the rooms' threads. Every client hears how each modem stands
(`modems`: connected, waiting, taken, unpaired, ambiguous, detached, or
unavailable without F). The document keeps the connection and the code.

Measured on F with two threads: a ping from one project's notebook to the
other's goes through; a third project that wants the same code reads
taken, and takes it when the second stops; a deleted project's modem
leaves with its room.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:24:19 +00:00
co-authored by Claude Opus 5.5
parent c85b0e45da
commit 7b943d3d96
15 changed files with 726 additions and 25 deletions
+82 -6
View File
@@ -9,11 +9,11 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, describe, expect, it, vi } from 'vitest';
import * as Y from 'yjs';
import { NetworkRuntimeEngine, type Id } from '@mdg-filius/core';
import { docSnapshot, readProject, writeProject } from '@mdg-filius/doc';
import { NetworkRuntimeEngine, type EthernetFrame, type Id } from '@mdg-filius/core';
import { addDevice, connect, docSnapshot, emptyProject, readProject, setHostAddress, setModemConnection, writeProject } from '@mdg-filius/doc';
import { openProjectArchive } from '@mdg-filius/fls';
import type { HostMessage, TraceFrame } from '@mdg-filius/protocol';
import { MAXIMUM_CATCH_UP_MILLISECONDS, SimulationRoom, TICK_MILLISECONDS, traceFramesOf } from '../src/index.js';
import { MAXIMUM_CATCH_UP_MILLISECONDS, SimulationRoom, TICK_MILLISECONDS, traceFramesOf, type RelayModem, type RoomRelay } from '../src/index.js';
const corpus = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
const SEED = 14;
@@ -26,7 +26,6 @@ function exampleDoc(file = 'java-examples/internetwork_komplett.fls'): Y.Doc {
return doc;
}
/** A room on a clock the test moves, and a client that keeps what it gets. */
/**
* A core that fails at the first frame `node` sends — as it once did at a
* modem, before the core had modems. Every test lets it go again.
@@ -43,9 +42,10 @@ afterEach(() => {
vi.restoreAllMocks();
});
function setUp(doc = exampleDoc()) {
/** A room on a clock the test moves, and a client that keeps what it gets; with `relay`, F's relay to other projects. */
function setUp(doc = exampleDoc(), relay?: RoomRelay) {
let now = 1_000_000;
const room = new SimulationRoom(doc, { now: () => now, seed: () => SEED, epoch: () => 1_700_000_000_000 + now });
const room = new SimulationRoom(doc, { now: () => now, seed: () => SEED, epoch: () => 1_700_000_000_000 + now, ...(relay === undefined ? {} : { relay }) });
const got: HostMessage[] = [];
const client = room.connect((message) => got.push(message));
const project = readProject(doc);
@@ -336,6 +336,82 @@ describe('a simulation room', () => {
expect(text).toContain('4 Paket(e) gesendet, 4 Paket(e) empfangen');
});
it("tells every client how the modems stand: FILIUS' pair connected while it runs, nothing in the design", () => {
const { room, got, send, node } = setUp(exampleDoc('java-examples/modem_beispiel.fls'));
const modems = readProject(exampleDoc('java-examples/modem_beispiel.fls')).graph.nodes.filter((each) => each.kind === 'remoteLink');
expect(modems).toHaveLength(2);
send({ kind: 'start' });
expect(ofKind(got, 'modems')).toEqual([{ v: 1, kind: 'modems', modems: Object.fromEntries(modems.map((each) => [node(each.displayName).id, 'connected'])) }]);
// One who comes later hears it at once.
const later: HostMessage[] = [];
room.connect((message) => later.push(message));
expect(ofKind(later, 'modems')).toEqual(ofKind(got, 'modems'));
send({ kind: 'stop' });
expect(ofKind(got, 'modems').at(-1)).toEqual({ v: 1, kind: 'modems', modems: {} });
});
it('names its modems of other projects to the relay at a start and none at a stop, passes their frames on and takes what comes back', () => {
const doc = new Y.Doc();
writeProject(doc, emptyProject());
let counter = 0;
const newId = () => `00000000-0000-0000-0000-${(++counter).toString(16).padStart(12, 'a')}`;
const [computer, modem] = [newId(), newId()];
addDevice(doc, { kind: 'notebook', id: computer, name: 'Notebook', x: 0, y: 0, newId }, 'setup');
addDevice(doc, { kind: 'remoteLink', id: modem, name: 'Modem', x: 200, y: 0, newId }, 'setup');
connect(doc, { node: computer }, { node: modem }, newId(), 'setup');
setHostAddress(doc, computer, 'ipAddress', '10.0.0.1', 'setup');
setModemConnection(doc, modem, 'offer', 'setup');
const named: (readonly RelayModem[])[] = [];
const passed: EthernetFrame[] = [];
const { room, got, send, run } = setUp(doc, { modems: (modems) => named.push(modems), frame: (_node, frame) => passed.push(frame) });
send({ kind: 'start' });
expect(named).toEqual([[{ node: modem, relay: { role: 'offer' } }]]);
expect(ofKind(got, 'modems').at(-1)!.modems).toEqual({ [modem]: 'waiting' });
room.relayStatesChanged({ [modem]: 'connected' });
room.relayStatesChanged({ [modem]: 'connected' });
// Said once: what did not change is not sent again.
expect(ofKind(got, 'modems').map((each) => each.modems)).toEqual([{ [modem]: 'waiting' }, { [modem]: 'connected' }]);
// An ARP request from the other project: the notebook answers through the modem.
room.relayed(modem, {
identity: 1,
sourceMacAddress: '02:00:00:00:99:01',
destinationMacAddress: 'FF:FF:FF:FF:FF:FF',
payload: { kind: 'arp', packet: { operation: 'request', senderMacAddress: '02:00:00:00:99:01', senderIPAddress: '10.0.0.2', targetMacAddress: 'FF:FF:FF:FF:FF:FF', targetIPAddress: '10.0.0.1' } },
});
run(1_000);
expect(passed.map((frame) => frame.payload.kind === 'arp' && frame.payload.packet.operation)).toEqual(['reply']);
expect(passed[0]!.destinationMacAddress).toBe('02:00:00:00:99:01');
send({ kind: 'stop' });
expect(named.at(-1)).toEqual([]);
// Stopped, it takes nothing.
room.relayed(modem, passed[0]!);
expect(ofKind(got, 'error')).toEqual([]);
});
it('names no modem that is switched off', () => {
const doc = new Y.Doc();
writeProject(doc, emptyProject());
addDevice(doc, { kind: 'remoteLink', id: 'modem', name: 'Modem', x: 0, y: 0, newId: () => 'port' }, 'setup');
setModemConnection(doc, 'modem', 'offer', 'setup');
((doc.getMap<Y.Map<unknown>>('nodes').get('modem')!.get('settings') as Y.Map<Y.Map<unknown>>).get('remoteLink')!).set('isEnabled', false);
const named: (readonly RelayModem[])[] = [];
const { send } = setUp(doc, { modems: (modems) => named.push(modems), frame: () => {} });
send({ kind: 'start' });
expect(named).toEqual([[]]);
});
it('says a modem of another project is unavailable without a relay (a room of one\'s own page)', () => {
const doc = new Y.Doc();
writeProject(doc, emptyProject());
addDevice(doc, { kind: 'remoteLink', id: 'modem', name: 'Modem', x: 0, y: 0, newId: () => 'port' }, 'setup');
addDevice(doc, { kind: 'pc', id: 'pc', name: 'Rechner', x: 0, y: 0, newId: () => 'pc-port' }, 'setup');
connect(doc, { node: 'pc' }, { node: 'modem' }, 'cable', 'setup');
setModemConnection(doc, 'modem', 'join', 'setup');
const { got, send } = setUp(doc);
send({ kind: 'start' });
expect(ofKind(got, 'modems').at(-1)!.modems).toEqual({ modem: 'unavailable' });
});
it('sends a card anew to a client that watches it again', () => {
const { got, send, node, run } = setUp();
const notebook = node('Notebook 1.10');