feat: F4.6g5 the switch and NAT tables and "Pakete verwerfen", measured against Java's

ViewersRun takes the rows of Java's viewers from the firmware: a switch's
table, a home router's NAT table, each after its reset, and the frames
"Pakete verwerfen" drops while it is held. The core writes a frame as Java
does and resets the NAT's dynamic entries; the room sends both tables as
views, drops while any client holds the button, and sends the dropped
frames, begun anew with each start, with the clock their times of day are
written from; the editor opens a table with a tap on a switch or home
router and holds "Pakete verwerfen" in its toolbar, which now wraps on a
phone. This completes F4.6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 22:13:48 +00:00
co-authored by Claude Opus 5.5
parent 5224e1a28b
commit 6809a71300
31 changed files with 1370 additions and 31 deletions
+1
View File
@@ -6,5 +6,6 @@ export * from './dns.js';
export * from './echo.js';
export * from './firewall.js';
export * from './gnutella.js';
export * from './viewers.js';
export * from './mail.js';
export * from './web.js';
+31 -10
View File
@@ -8,19 +8,20 @@ import type { CaptureRow, PageLanguage } from '@mdg-filius/core';
export const SIM_PROTOCOL_VERSION = 1;
/** The programs whose windows a client can watch and act in. */
export const APP_KINDS = ['terminal', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'echoServer', 'simpleClient', 'gnutella', 'dnsServer', 'firewall'] as const;
/** The programs whose windows a client can watch and act in; and a switch's and a home router's viewers (sat, nat). */
export const APP_KINDS = ['terminal', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'echoServer', 'simpleClient', 'gnutella', 'dnsServer', 'firewall', 'sat', 'nat'] as const;
export type AppKind = (typeof APP_KINDS)[number];
/**
* What a client subscribes to: which cables carry frames (`activity`), a
* card's capture (`trace:<portId>`), a program's window
* (`app:<nodeId>/<app>`), a computer's network settings as it runs with them
* (`node:<nodeId>`: its desktop's title and network info).
* (`node:<nodeId>`: its desktop's title and network info), the frames
* "Pakete verwerfen" dropped (`dropped`).
*/
export type Topic = 'activity' | `trace:${string}` | `app:${string}/${AppKind}` | `node:${string}`;
export type Topic = 'activity' | 'dropped' | `trace:${string}` | `app:${string}/${AppKind}` | `node:${string}`;
export const CLIENT_MESSAGE_KINDS = ['sub', 'unsub', 'start', 'stop', 'speed', 'input'] as const;
export const CLIENT_MESSAGE_KINDS = ['sub', 'unsub', 'start', 'stop', 'speed', 'drop', 'input'] as const;
export type ClientMessageKind = (typeof CLIENT_MESSAGE_KINDS)[number];
export type ClientMessage =
@@ -36,6 +37,8 @@ export type ClientMessage =
| { readonly v: 1; readonly kind: 'stop' }
/** Java's speed slider, 1 to 100 per cent: how long a frame takes over a cable. */
| { readonly v: 1; readonly kind: 'speed'; readonly value: number }
/** Java's "Pakete verwerfen" pressed (on) or let go: every cable drops what it carries while any client holds it. */
| { readonly v: 1; readonly kind: 'drop'; readonly on: boolean }
/** An action in a program's window, answered by `ack` or `error` with the same reqId. */
| {
readonly v: 1;
@@ -47,7 +50,7 @@ export type ClientMessage =
readonly reqId: string;
};
export const HOST_MESSAGE_KINDS = ['state', 'activity', 'trace', 'app', 'view', 'node', 'ack', 'error'] as const;
export const HOST_MESSAGE_KINDS = ['state', 'activity', 'trace', 'dropped', 'app', 'view', 'node', 'ack', 'error'] as const;
export type HostMessageKind = (typeof HOST_MESSAGE_KINDS)[number];
export type SimMode = 'design' | 'simulation';
@@ -59,17 +62,31 @@ export interface TraceFrame {
readonly layeredRows: readonly CaptureRow[];
}
/** A frame "Pakete verwerfen" dropped: its number among them, when, and what it was (EthernetFrame.toString). */
export interface DroppedUnit {
readonly number: number;
readonly timeMilliseconds: number;
readonly text: string;
}
/** engineFailed: the core threw (a part it has not got yet, such as remote links); the room is back in design mode. */
export const ERROR_CODES = ['badMessage', 'badVersion', 'unknownKind', 'badTopic', 'notRunning', 'unknownNode', 'unknownApp', 'unknownAction', 'badArgs', 'engineFailed'] as const;
export type ErrorCode = (typeof ERROR_CODES)[number];
export type HostMessage =
/** The room's mode, speed and time; sent to every client on a change and when it connects. */
| { readonly v: 1; readonly kind: 'state'; readonly mode: SimMode; readonly speed: number; readonly t: number }
/**
* The room's mode, speed and time; sent to every client on a change and
* when it connects. `clock`: the time of day, in milliseconds since 1970,
* that the simulation's time 0 stands for (a viewer writes its times of day
* from it, as Java's write the wall clock's).
*/
| { readonly v: 1; readonly kind: 'state'; readonly mode: SimMode; readonly speed: number; readonly t: number; readonly clock: number }
/** The cables that carried a frame since the last tick. */
| { readonly v: 1; readonly kind: 'activity'; readonly links: readonly string[] }
/** A card's new frames; on subscribing, all it holds (`reset`). */
| { readonly v: 1; readonly kind: 'trace'; readonly portId: string; readonly frames: readonly TraceFrame[]; readonly reset: boolean }
/** The frames "Pakete verwerfen" dropped since the start, new ones; on subscribing, all (`reset`): each its number, time and Java's text. */
| { readonly v: 1; readonly kind: 'dropped'; readonly units: readonly DroppedUnit[]; readonly reset: boolean }
/** A program's new window lines from line `from` on; on subscribing, all (from 0). */
| { readonly v: 1; readonly kind: 'app'; readonly node: string; readonly app: AppKind; readonly from: number; readonly lines: readonly unknown[] }
/**
@@ -79,7 +96,8 @@ export type HostMessage =
* web server's (WebServerView), the e-mail program's and server's
* (EmailClientView, EmailServerView), the echo server's and the generic
* client's (EchoServerView, SimpleClientView), the DNS server's
* (DnsServerView).
* (DnsServerView), the firewall's (FirewallView), Gnutella's
* (GnutellaView), a switch's and a home router's tables (SatView, NatView).
*/
| { readonly v: 1; readonly kind: 'view'; readonly node: string; readonly app: AppKind; readonly view: unknown }
/**
@@ -106,7 +124,7 @@ const bad = (code: ErrorCode, detail: string) => ({ error: { code, detail } });
/** Whether a topic is one the room knows. */
export function isTopic(value: unknown): value is Topic {
if (value === 'activity') return true;
if (value === 'activity' || value === 'dropped') return true;
if (typeof value !== 'string') return false;
if (value.startsWith('trace:')) return value.length > 'trace:'.length;
if (value.startsWith('node:')) return /^node:[^/]+$/.test(value);
@@ -138,6 +156,9 @@ export function parseClientMessage(value: unknown): Parsed<ClientMessage> {
case 'speed':
if (typeof value.value !== 'number' || !Number.isFinite(value.value)) return bad('badMessage', 'speed is no number');
return { message: { v: 1, kind: 'speed', value: value.value } };
case 'drop':
if (typeof value.on !== 'boolean') return bad('badMessage', 'on is no boolean');
return { message: { v: 1, kind: 'drop', on: value.on } };
case 'input': {
const { node, app, action, args, reqId } = value;
if (typeof reqId !== 'string') return bad('badMessage', 'reqId is no string');
+15
View File
@@ -0,0 +1,15 @@
// What the viewers of a running simulation show, as the room keeps them and
// every client shows them (plan §7.3), after Java's SatViewer and NatViewer
// (measured with tools/java-oracle's ViewersRun): a switch's source address
// table and a home router's NAT table. A time is the simulation's; the
// viewer writes it as a time of day (the `state` message's `clock`).
/** A switch's learned addresses: the address (in capitals: the switch learns it so, as holeSAT writes it), the port's number (from 1), its last update. */
export interface SatView {
readonly rows: readonly { readonly macAddress: string; readonly port: number; readonly updatedAtMilliseconds: number }[];
}
/** A home router's NAT entries: protocol, WAN port, remote address, LAN address, LAN port in Java's words; the last update, none for the port forwarding's. */
export interface NatView {
readonly rows: readonly { readonly cells: readonly [string, string, string, string, string]; readonly updatedAtMilliseconds?: number }[];
}