feat: F4.6g1 the echo server and the generic client windows

Oracle EchoWindowRun uses Java's echo server and client windows as a
person does; SaveAfterRunRun echo saves what Java keeps after the run.
The room takes setActive with a port, connect/disconnect/send, sends
both views and writes the server's port and the client's target back.
The editor's windows show Java's buttons, fields and lines; a port that
is no port is marked and not handed over (Java's button throws).
Core fix: an empty message (one empty segment with the push, as Java's
erstelleSegmente makes) counted as a repeat and never arrived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 20:04:33 +00:00
co-authored by Claude Opus 5.5
parent 3dc2713f81
commit e31da62ae0
24 changed files with 1481 additions and 18 deletions
+71 -3
View File
@@ -20,8 +20,9 @@
//
// A program's window may show more than its lines (plan §7.3): the browser's
// page, the web server's button and table, the e-mail program's account and
// folders, the e-mail server's domain and accounts go to the clients as a
// view, whole, when they change.
// folders, the e-mail server's domain and accounts, the echo server's button
// and port, the generic client's connection go to the clients as a view,
// whole, when they change.
//
// While the simulation runs, the document's programs are watched: a program
// installed or removed on the desktop (Java's software installation) is
@@ -70,6 +71,8 @@ import {
type Topic,
type EmailClientView,
type EmailServerView,
type EchoServerView,
type SimpleClientView,
type TraceFrame,
type WebServerView,
} from '@mdg-filius/protocol';
@@ -237,7 +240,12 @@ export class SimulationRoom {
settings.emailServer = emailServerAfterRun(configured as never, mailServerSettings);
}
const echo = this.engine.echoServer(id);
if (echo !== undefined) active.echoServer = echo.isActive;
if (echo !== undefined) {
active.echoServer = echo.isActive;
settings.echoServer = { port: echo.port };
}
const client = this.engine.simpleClient(id);
if (client !== undefined) settings.simpleClient = { targetAddress: client.targetAddress, targetPort: client.targetPort };
settings.emailClient = snapshot.emailClientsByNodeId.get(id);
settings.firewall = snapshot.firewallConfigurationsByNodeId.get(id);
if (snapshot.dhcpClientConfigurationsByNodeId.get(id)?.isEnabled === true) settings.device = snapshot.deviceConfigurations.get(id);
@@ -442,6 +450,49 @@ export class SimulationRoom {
return this.actInEmailClient(input);
case 'emailServer':
return this.actInEmailServer(input);
case 'echoServer':
return this.actInEchoServer(input);
case 'simpleClient':
return this.actInSimpleClient(input);
default:
return 'unknownAction';
}
}
/** The start button with the port typed (Java's window sets the port before and after it starts the server). */
private actInEchoServer(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
const server = this.engine.echoServer(input.node);
if (server === undefined) return 'unknownApp';
const { args } = input;
if (input.action !== 'setActive') return 'unknownAction';
if (typeof args.active !== 'boolean') return 'badArgs';
if (!args.active) {
server.setActive(false);
return undefined;
}
if (!isPort(args.port)) return 'badArgs';
server.setPort(args.port);
server.setActive(true);
return undefined;
}
/** "Verbinden" with the address and port typed, "Trennen", "Senden". */
private actInSimpleClient(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
const client = this.engine.simpleClient(input.node);
if (client === undefined) return 'unknownApp';
const { args } = input;
switch (input.action) {
case 'connect':
if (typeof args.address !== 'string' || !isPort(args.port)) return 'badArgs';
client.connect(args.address, args.port);
return undefined;
case 'disconnect':
client.disconnect();
return undefined;
case 'send':
if (typeof args.text !== 'string') return 'badArgs';
client.send(args.text);
return undefined;
default:
return 'unknownAction';
}
@@ -583,6 +634,18 @@ export class SimulationRoom {
const view: EmailClientView = { account: client.account, inbox: client.inbox.map((entry) => entry.mail), sent: client.sent.map((entry) => entry.mail) };
return view;
}
case 'echoServer': {
const server = this.engine.echoServer(node);
if (server === undefined) return null;
const view: EchoServerView = { active: server.isActive, port: server.port };
return view;
}
case 'simpleClient': {
const client = this.engine.simpleClient(node);
if (client === undefined) return null;
const view: SimpleClientView = { connected: client.isConnected, address: client.targetAddress, port: client.targetPort };
return view;
}
case 'emailServer': {
const server = this.engine.emailServer(node);
if (server === undefined) return null;
@@ -732,6 +795,11 @@ export class SimulationRoom {
}
}
/** A port a socket can have. */
function isPort(value: unknown): value is number {
return Number.isInteger(value) && (value as number) >= 0 && (value as number) <= 65_535;
}
/** A card's frames as the capture window shows them, both views. */
export function traceFramesOf(frames: readonly CapturedFrame[]): TraceFrame[] {
const byNumber = (rows: readonly CaptureRow[]) => {