feat(core): a cable carries one frame after the other, as in Java

Java's SimplexVerbindung sleeps a cable's delay with the port's buffer
locked: frames on one cable leave one after the other, and a frame shows
in the packet log when it leaves. The engine now queues a frame in the
output buffer until the one before has arrived (ethernetLinkDeparture).
Swift sends a cable's frames at once; the option swiftCables keeps that for
the Swift comparison, which goes on checking everything else trace by
trace.

With the queue, the Java comparisons match within 120 ms (was 200); a
state of the TCP stories within 250 ms, where the engine's calls block
one after the other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 16:37:45 +00:00
co-authored by Claude Opus 5.5
parent cec0a92f95
commit efc7c440f6
7 changed files with 153 additions and 15 deletions
+78
View File
@@ -248,6 +248,28 @@ describe('event queue and virtual clock', () => {
]).toEqual([0, 0, 0, 0, 0, 0, 0]);
});
it('forgets which cables are busy on stop', () => {
const [clientId, clientPortId, serverId, serverPortId] = [0x25, 0x26, 0x27, 0x28].map(uuid) as [string, string, string, string];
const input = {
nodes: [pc(clientId, clientPortId), pc(serverId, serverPortId)],
links: [link(clientId, clientPortId, serverId, serverPortId)],
deviceConfigurations: new Map([
[clientId, device('10.5.0.1')],
[serverId, device('10.5.0.2')],
]),
};
const engine = startedEngine(input);
engine.handle({ type: 'advance', toMilliseconds: 10_000 });
engine.openTcpServerSocket(serverId, 80);
expect(engine.connectTcp(engine.openTcpClientSocket(clientId, '10.5.0.2', 80)!)).toBe(true);
engine.handle({ type: 'stop' });
engine.handle({ type: 'start', snapshot: topologySnapshot(input), seed: 14, initialTimeMilliseconds: 0 });
engine.resolveMacAddress(clientId, '10.5.0.2');
expect(engine.state.packetTraces.find((trace) => trace.layer === 'physical' && trace.operation === 'sent')?.timeMilliseconds).toBe(0);
});
it('forgets TCP sessions and accepted sockets on stop', () => {
const [clientId, clientPortId, serverId, serverPortId] = [0x21, 0x22, 0x23, 0x24].map(uuid) as [string, string, string, string];
const engine = startedEngine({
@@ -925,6 +947,62 @@ describe('TCP sockets', () => {
expect(engine.state.socketsById.get(listenerId)?.tcpState).toBe('LISTEN');
});
it('carries one frame after the other on a cable, as Java (Swift: all at once)', () => {
const departures = (swiftCables: boolean) => {
const [clientId, clientPortId, serverId, serverPortId] = [0x17a1, 0x17b1, 0x17a2, 0x17b2].map(uuid) as [string, string, string, string];
const engine = new NetworkRuntimeEngine(14, undefined, { swiftCables });
engine.handle({
type: 'start',
snapshot: topologySnapshot({
nodes: [pc(clientId, clientPortId), pc(serverId, serverPortId)],
links: [link(clientId, clientPortId, serverId, serverPortId)],
deviceConfigurations: new Map([
[clientId, device('10.23.0.1')],
[serverId, device('10.23.0.2')],
]),
}),
seed: 14,
initialTimeMilliseconds: 0,
});
engine.openTcpServerSocket(serverId, 80);
const client = engine.openTcpClientSocket(clientId, '10.23.0.2', 80)!;
expect(engine.connectTcp(client)).toBe(true);
const closedAt = engine.state.currentTimeMilliseconds;
engine.closeTcpSocket(client);
// The server answers the FIN with its ACK and, closing by itself, its FIN at once.
const physical = (nodeId: string, operation: string) =>
engine.state.packetTraces
.filter((trace) => trace.nodeId === nodeId && trace.layer === 'physical' && trace.operation === operation && trace.timeMilliseconds > closedAt)
.map((trace) => trace.timeMilliseconds - closedAt);
return { sent: physical(serverId, 'sent'), received: physical(clientId, 'received') };
};
expect(departures(false)).toEqual({ sent: [55, 110], received: [110, 165] });
expect(departures(true)).toEqual({ sent: [55, 55], received: [110, 110] });
});
it('queues a third frame behind the second on a busy cable', () => {
// Four DHCP clients start at once and broadcast their DISCOVER; the switch
// floods three of them onto each client's cable.
const switchId = uuid(0x17c0);
const hosts = [0, 1, 2, 3].map((index) => ({ id: uuid(0x17c1 + 2 * index), portId: uuid(0x17c2 + 2 * index), switchPortId: uuid(0x17d0 + index) }));
const engine = startedEngine({
nodes: [
{ id: switchId, kind: 'networkSwitch', ports: hosts.map((host, index) => portSnapshot(host.switchPortId, `sw${index + 1}`)) },
...hosts.map((host) => pc(host.id, host.portId)),
],
links: hosts.map((host) => link(host.id, host.portId, switchId, host.switchPortId)),
deviceConfigurations: new Map(hosts.map((host) => [host.id, device('0.0.0.0')])),
dhcpClientConfigurationsByNodeId: new Map(hosts.map((host) => [host.id, { isEnabled: true }])),
});
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
const received = engine.state.packetTraces
.filter((trace) => trace.nodeId === hosts[0]!.id && trace.layer === 'physical' && trace.operation === 'received')
.map((trace) => trace.timeMilliseconds);
expect(received).toEqual([110, 165, 220]);
});
it('ends TIME_WAIT with the next segment that comes', () => {
const { engine, clientId, serverId } = pair(0x1771, 0x1772, '10.21.0');
engine.openTcpServerSocket(serverId, 9090);