|
|
|
@@ -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');
|
|
|
|
|