feat(core): modems run — the pair in the project, and a modem of another project hands its frames on (K0–K2)

Measured first: tools/java-oracle's ModemRun joins two networks by a pair
of Java modems over TCP on localhost. The modem has no card and adds no
delay of its own — a frame takes the cables on both sides, nothing more.

The core pairs the two enabled modems on a port (Swift's inProject, with
its conditions and cycle suppression) on Java's timing: no 20 ms. The
modem example of FILIUS now pings through instead of failing the core.
A modem of another project (relay) gives its frames to whoever runs the
engine and puts on its cable what comes back, with this engine's frame
and packet numbers; a frame dies after MAXIMUM_RELAY_HOPS trips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:12:27 +00:00
co-authored by Claude Opus 5.5
parent 6f583616f6
commit c85b0e45da
19 changed files with 1743 additions and 44 deletions
+266
View File
@@ -0,0 +1,266 @@
// A modem of another project (server F's relay): the engine hands what
// reaches such a modem to `relay` and puts on the modem's cable what comes
// back through receiveRelayedFrame. Here two engines stand for two projects,
// joined by a queue that delivers between their steps, as F's threads do.
import { describe, expect, it } from 'vitest';
import {
deviceConfiguration,
MAXIMUM_RELAY_HOPS,
NetworkRuntimeEngine,
portSnapshot,
topologySnapshot,
type EthernetFrame,
type Id,
type NodeKind,
type RemoteLinkRelay,
type TopologySnapshot,
} from '../src/index.js';
const id = (number: number) => `00000000-0000-0000-0000-${String(number).padStart(12, '0')}`;
interface Device {
readonly id: number;
readonly kind: NodeKind;
readonly ports: number;
readonly ip?: string;
readonly relay?: RemoteLinkRelay;
}
/** A project: its devices (port n of device d is d * 10 + n) and cables between ports. */
function project(devices: readonly Device[], cables: readonly [number, number][]): TopologySnapshot {
const owner = new Map(devices.flatMap((device) => Array.from({ length: device.ports }, (_, index) => [device.id * 10 + index, device.id] as const)));
return topologySnapshot({
nodes: devices.map((device) => ({
id: id(device.id),
kind: device.kind,
ports: Array.from({ length: device.ports }, (_, index) => portSnapshot(id(device.id * 10 + index), `p${index}`)),
})),
links: cables.map(([source, target], index) => ({
id: id(900_000 + source * 1000 + target + index),
sourceNodeId: id(owner.get(source)!),
sourcePortId: id(source),
targetNodeId: id(owner.get(target)!),
targetPortId: id(target),
})),
deviceConfigurations: new Map(devices.filter((device) => device.ip !== undefined).map((device) => [id(device.id), deviceConfiguration(device.ip!, '255.255.255.0', '', '')])),
remoteLinkConfigurationsByNodeId: new Map(
devices
.filter((device) => device.kind === 'remoteLink')
.map((device) => [id(device.id), { pairIdentifier: 'java-modem-port-12345', latencyMilliseconds: 20, isEnabled: true, ...(device.relay === undefined ? {} : { relay: device.relay }) }]),
),
});
}
/**
* Two engines whose relay modems are paired as `pairs` says (A's modem →
* B's); what one relays reaches the other at its next step. `relayed`
* counts what went across.
*/
function joined(a: TopologySnapshot, b: TopologySnapshot, pairs: readonly [number, number][]) {
const queue: { to: 'a' | 'b'; node: Id; frame: EthernetFrame }[] = [];
const toB = new Map(pairs.map(([from, to]) => [id(from), id(to)]));
const toA = new Map(pairs.map(([from, to]) => [id(to), id(from)]));
let relayed = 0;
const engineA = new NetworkRuntimeEngine(7, undefined, {
relay: (node, frame) => {
relayed += 1;
queue.push({ to: 'b', node: toB.get(node)!, frame });
},
});
const engineB = new NetworkRuntimeEngine(8, undefined, {
relay: (node, frame) => {
relayed += 1;
queue.push({ to: 'a', node: toA.get(node)!, frame });
},
});
engineA.handle({ type: 'start', snapshot: a, seed: 7, initialTimeMilliseconds: 0 });
engineB.handle({ type: 'start', snapshot: b, seed: 8, initialTimeMilliseconds: 0 });
/** Both engines a millisecond further, then what is queued delivered. */
const step = () => {
for (const engine of [engineA, engineB]) engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 1 });
for (const { to, node, frame } of queue.splice(0)) (to === 'a' ? engineA : engineB).receiveRelayedFrame(node, frame);
};
const run = (milliseconds: number) => {
for (let index = 0; index < milliseconds; index += 1) step();
};
return { engineA, engineB, run, relayed: () => relayed };
}
/** A UDP datagram from `from` to `target` (a send returns at once, as in Java; ARP runs while the engines step). */
function sendUdp(engine: NetworkRuntimeEngine, from: number, target: string): void {
const socket = engine.bindUdpSocket(id(from), { remoteIPAddress: target, remotePort: 9 })!;
engine.sendUdp(socket, new TextEncoder().encode('hallo'));
}
/** Whether a card captured a frame of `kind` from `sender`'s address. */
function captured(engine: NetworkRuntimeEngine, port: number, kind: 'arp reply' | 'udp', sender: string): boolean {
return engine.capture
.frames(id(port))
.some(({ frame }) =>
kind === 'arp reply'
? frame.payload.kind === 'arp' && frame.payload.packet.operation === 'reply' && frame.payload.packet.senderIPAddress === sender
: frame.payload.kind === 'ipv4' && frame.payload.packet.protocol === 'udp' && frame.payload.packet.senderIPAddress === sender,
);
}
const A = project(
[
{ id: 1, kind: 'pc', ports: 1, ip: '10.0.0.1' },
{ id: 2, kind: 'remoteLink', ports: 1, relay: { role: 'offer' } },
],
[[10, 20]],
);
const B = project(
[
{ id: 3, kind: 'remoteLink', ports: 1, relay: { role: 'join', code: 'K7F3QX' } },
{ id: 4, kind: 'networkSwitch', ports: 3 },
{ id: 5, kind: 'pc', ports: 1, ip: '10.0.0.2' },
],
[
[30, 40],
[41, 50],
],
);
describe('a modem of another project', () => {
it('stands as relay on its cable, detached without', () => {
const { engineA } = joined(A, B, [[2, 3]]);
expect(engineA.remoteLinkCondition(id(2))).toEqual({ kind: 'relay' });
const alone = new NetworkRuntimeEngine(7);
alone.handle({ type: 'start', snapshot: project([{ id: 2, kind: 'remoteLink', ports: 1, relay: { role: 'offer' } }], []), seed: 7, initialTimeMilliseconds: 0 });
expect(alone.remoteLinkCondition(id(2))).toEqual({ kind: 'detached' });
});
it('joins two projects: ARP goes across and back, then the datagram', () => {
const { engineA, engineB, run } = joined(A, B, [[2, 3]]);
sendUdp(engineA, 1, '10.0.0.2');
run(3_000);
expect(captured(engineA, 10, 'arp reply', '10.0.0.2')).toBe(true);
expect(captured(engineB, 50, 'udp', '10.0.0.1')).toBe(true);
// A modem has no card; each side captures only its own cards.
expect(engineA.capture.frames(id(20))).toEqual([]);
});
it('numbers a frame from the other project anew, so that a switch does not take it for one it saw', () => {
// B with a second computer on the switch: their datagrams go between them, unicast, not across.
const busyB = project(
[
{ id: 3, kind: 'remoteLink', ports: 1, relay: { role: 'join', code: 'K7F3QX' } },
{ id: 4, kind: 'networkSwitch', ports: 3 },
{ id: 5, kind: 'pc', ports: 1, ip: '10.0.0.2' },
{ id: 9, kind: 'pc', ports: 1, ip: '10.0.0.3' },
],
[
[30, 40],
[41, 50],
[42, 90],
],
);
const { engineA, engineB, run } = joined(A, busyB, [[2, 3]]);
for (let index = 0; index < 5; index += 1) {
sendUdp(engineB, 5, '10.0.0.3');
run(500);
}
// B's switch has seen the number A's next frame will carry.
expect(engineB.state.switchSeenFrameIdentitiesByNodeId.get(id(4))!.has(engineA.state.nextFrameIdentity)).toBe(true);
sendUdp(engineA, 1, '10.0.0.2');
run(3_000);
expect(captured(engineB, 50, 'udp', '10.0.0.1')).toBe(true);
expect(engineB.state.packetTraces.some((event) => event.detail === 'switch cycle suppression')).toBe(false);
});
it(`drops a frame after ${MAXIMUM_RELAY_HOPS} trips between projects: two pairs between the same networks make no endless circle`, () => {
const a = project(
[
{ id: 1, kind: 'pc', ports: 1, ip: '10.0.0.1' },
{ id: 6, kind: 'networkSwitch', ports: 3 },
{ id: 2, kind: 'remoteLink', ports: 1, relay: { role: 'offer' } },
{ id: 7, kind: 'remoteLink', ports: 1, relay: { role: 'offer' } },
],
[
[10, 60],
[61, 20],
[62, 70],
],
);
const b = project(
[
{ id: 3, kind: 'remoteLink', ports: 1, relay: { role: 'join', code: 'AAAAAA' } },
{ id: 8, kind: 'remoteLink', ports: 1, relay: { role: 'join', code: 'BBBBBB' } },
{ id: 4, kind: 'networkSwitch', ports: 2 },
],
[
[30, 40],
[80, 41],
],
);
const { engineA, engineB, run, relayed } = joined(a, b, [
[2, 3],
[7, 8],
]);
sendUdp(engineA, 1, '10.0.0.9');
run(20_000);
const dropped = [engineA, engineB].flatMap((engine) => engine.state.packetTraces).filter((event) => event.detail === 'remote link relay hop limit');
expect(dropped.length).toBeGreaterThan(0);
// Two ARP requests (Java asks twice), each going round until its trips are used up.
expect(relayed()).toBeLessThanOrEqual(2 * 2 * MAXIMUM_RELAY_HOPS);
});
it('is no partner for a modem of this project on the same port', () => {
const engine = new NetworkRuntimeEngine(7);
const mixed = project(
[
{ id: 1, kind: 'pc', ports: 1, ip: '10.0.0.1' },
{ id: 2, kind: 'remoteLink', ports: 1 },
{ id: 3, kind: 'remoteLink', ports: 1, relay: { role: 'offer' } },
{ id: 5, kind: 'pc', ports: 1, ip: '10.0.0.2' },
],
[
[10, 20],
[30, 50],
],
);
engine.handle({ type: 'start', snapshot: mixed, seed: 7, initialTimeMilliseconds: 0 });
expect(engine.remoteLinkCondition(id(2))).toEqual({ kind: 'unpaired' });
});
it('loses what reaches it without a relay, as an unconnected modem does', () => {
const engine = new NetworkRuntimeEngine(7);
engine.handle({ type: 'start', snapshot: A, seed: 7, initialTimeMilliseconds: 0 });
sendUdp(engine, 1, '10.0.0.2');
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
expect(engine.state.packetTraces.some((event) => event.nodeId === id(2) && event.detail === 'remote link relay unavailable')).toBe(true);
});
it('takes nothing from the relay while stopped, nor at a modem of this project', () => {
const { engineA, engineB } = joined(A, B, [[2, 3]]);
const frame: EthernetFrame = {
identity: 1,
sourceMacAddress: '02:00:00:00:00:01',
destinationMacAddress: 'FF:FF:FF:FF:FF:FF',
payload: {
kind: 'arp',
packet: { operation: 'request', senderMacAddress: '02:00:00:00:00:01', senderIPAddress: '10.0.0.1', targetMacAddress: '00:00:00:00:00:00', targetIPAddress: '10.0.0.2' },
},
};
expect(engineB.receiveRelayedFrame(id(5), frame)).toBe(false);
expect(engineB.receiveRelayedFrame(id(3), frame)).toBe(true);
engineA.handle({ type: 'stop' });
expect(engineA.receiveRelayedFrame(id(2), frame)).toBe(false);
const inProject = new NetworkRuntimeEngine(7);
inProject.handle({
type: 'start',
snapshot: project(
[
{ id: 2, kind: 'remoteLink', ports: 1 },
{ id: 1, kind: 'pc', ports: 1, ip: '10.0.0.1' },
],
[[10, 20]],
),
seed: 7,
initialTimeMilliseconds: 0,
});
expect(inProject.receiveRelayedFrame(id(2), frame)).toBe(false);
});
});