feat(server): the relay joins modems of different projects by a code (K3)
A modem that offers a connection to another project is found by six characters derived from its project and itself (modemCode): a copy of the project has another code, so no two modems offer the same one. The room names its modems of other projects to F's relay at a start and none at a stop; the relay pairs each offer with the first modem that joins its code, one to one as Java's server modem takes one client, and passes the frames between the rooms' threads. Every client hears how each modem stands (`modems`: connected, waiting, taken, unpaired, ambiguous, detached, or unavailable without F). The document keeps the connection and the code. Measured on F with two threads: a ping from one project's notebook to the other's goes through; a third project that wants the same code reads taken, and takes it when the second stops; a deleted project's modem leaves with its room. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,14 @@
|
||||
// installed or removed in the running engine at once, as Java starts or ends
|
||||
// it; any other change comes in at the next start.
|
||||
//
|
||||
// Modems (FILIUS_MODEM_PLAN): a pair in the project is the engine's alone. A
|
||||
// modem of another project needs server F's relay (`relay`): the room names
|
||||
// its relay modems when the simulation starts and none when it stops, hands
|
||||
// on what they pass, and takes what comes back (relayed); F tells it how they
|
||||
// stand (relayStates). Every client hears how each modem stands (`modems`)
|
||||
// when that changes — without a relay (a room of one's own page) a modem of
|
||||
// another project is `unavailable`.
|
||||
//
|
||||
// What a run leaves that Java's project keeps (files, which servers run,
|
||||
// accounts, mails, a DHCP client's address …) goes back into the document,
|
||||
// as `origin: 'sim'`: at most once a second while it runs, after each action
|
||||
@@ -59,6 +67,8 @@ import {
|
||||
stableGeneratedMacAddress,
|
||||
type CapturedFrame,
|
||||
type CaptureRow,
|
||||
type EthernetFrame,
|
||||
type RemoteLinkRelay,
|
||||
type Id,
|
||||
type InstallableProgram,
|
||||
type LogLine,
|
||||
@@ -73,6 +83,7 @@ import {
|
||||
type ClientMessage,
|
||||
type ErrorCode,
|
||||
type HostMessage,
|
||||
type ModemStatus,
|
||||
type NodeSettings,
|
||||
type SimMode,
|
||||
type Topic,
|
||||
@@ -105,6 +116,25 @@ export interface RoomOptions {
|
||||
readonly writeBackMilliseconds?: number;
|
||||
/** The time of day, in milliseconds since 1970, for the viewers' times (Date.now by default). */
|
||||
readonly epoch?: () => number;
|
||||
/** Server F's relay to the modems of other projects; none in a room of one's own page. */
|
||||
readonly relay?: RoomRelay;
|
||||
}
|
||||
|
||||
/** A modem of another project, as the simulation started. */
|
||||
export interface RelayModem {
|
||||
readonly node: Id;
|
||||
readonly relay: RemoteLinkRelay;
|
||||
}
|
||||
|
||||
/** How a modem of another project stands, as the relay knows it. */
|
||||
export type RelayState = 'connected' | 'waiting' | 'taken';
|
||||
|
||||
/** What a room tells server F's relay. */
|
||||
export interface RoomRelay {
|
||||
/** The room's modems of other projects, when the simulation starts; none when it stops. */
|
||||
modems(modems: readonly RelayModem[]): void;
|
||||
/** A frame one of them passes on. */
|
||||
frame(node: Id, frame: EthernetFrame): void;
|
||||
}
|
||||
|
||||
interface Client {
|
||||
@@ -159,12 +189,19 @@ export class SimulationRoom {
|
||||
private lastWriteBack = 0;
|
||||
/** Each computer's programs as the running engine has them. */
|
||||
private software = new Map<Id, readonly InstallableProgram[]>();
|
||||
/** How the relay says the modems of other projects stand. */
|
||||
private relayStates = new Map<Id, RelayState>();
|
||||
/** The modems' standing last sent to the clients, as text. */
|
||||
private modemsSent = '{}';
|
||||
|
||||
constructor(
|
||||
private readonly doc: Y.Doc,
|
||||
private readonly options: RoomOptions,
|
||||
) {
|
||||
this.engine = new NetworkRuntimeEngine(options.seed(), undefined, { wallClock: options.now });
|
||||
this.engine = new NetworkRuntimeEngine(options.seed(), undefined, {
|
||||
wallClock: options.now,
|
||||
...(options.relay === undefined ? {} : { relay: (node: Id, frame: EthernetFrame) => options.relay!.frame(node, frame) }),
|
||||
});
|
||||
doc.on('update', () => {
|
||||
if (this.mode === 'simulation') this.guarded(() => this.syncSoftware());
|
||||
});
|
||||
@@ -338,6 +375,7 @@ export class SimulationRoom {
|
||||
const id = this.nextClient++;
|
||||
this.clients.set(id, { send, name, topics: new Set(), traceSent: new Map(), droppedSent: undefined, appSent: new Map(), nodeSent: new Map(), viewSent: new Map() });
|
||||
send(this.state());
|
||||
if (this.modemsSent !== '{}') send(message({ kind: 'modems', modems: JSON.parse(this.modemsSent) as Record<Id, ModemStatus> }));
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -461,7 +499,14 @@ export class SimulationRoom {
|
||||
this.startedBy = startedBy;
|
||||
this.lastWall = this.options.now();
|
||||
this.lastWriteBack = this.lastWall;
|
||||
this.relayStates.clear();
|
||||
this.options.relay?.modems(
|
||||
[...snapshot.remoteLinkConfigurationsByNodeId].flatMap(([node, configuration]) =>
|
||||
configuration.relay !== undefined && configuration.isEnabled ? [{ node, relay: configuration.relay }] : [],
|
||||
),
|
||||
);
|
||||
this.broadcast(this.state());
|
||||
this.sendModems();
|
||||
this.flush([...this.clients.values()], false);
|
||||
}
|
||||
|
||||
@@ -474,10 +519,58 @@ export class SimulationRoom {
|
||||
this.mode = 'design';
|
||||
this.startedBy = undefined;
|
||||
this.lastWall = undefined;
|
||||
this.options.relay?.modems([]);
|
||||
this.broadcast(this.state());
|
||||
this.sendModems();
|
||||
this.flush([...this.clients.values()], false);
|
||||
}
|
||||
|
||||
/** A frame from the partner of one of the room's modems of another project (server F's relay); a stopped engine takes none. */
|
||||
relayed(node: Id, frame: EthernetFrame): void {
|
||||
this.guarded(() => this.engine.receiveRelayedFrame(node, frame));
|
||||
}
|
||||
|
||||
/** How the relay says the room's modems of other projects stand now (those it names). */
|
||||
relayStatesChanged(states: Readonly<Record<Id, RelayState>>): void {
|
||||
for (const [node, state] of Object.entries(states)) this.relayStates.set(node, state);
|
||||
this.sendModems();
|
||||
}
|
||||
|
||||
/** How each modem stands while the simulation runs; none in the design. */
|
||||
private modemStatuses(): Record<Id, ModemStatus> {
|
||||
const statuses: Record<Id, ModemStatus> = {};
|
||||
if (this.mode !== 'simulation') return statuses;
|
||||
for (const node of this.engine.state.topologySnapshot.nodes) {
|
||||
const condition = this.engine.remoteLinkCondition(node.id);
|
||||
if (condition === undefined) continue;
|
||||
switch (condition.kind) {
|
||||
case 'active':
|
||||
statuses[node.id] = 'connected';
|
||||
break;
|
||||
case 'relay':
|
||||
statuses[node.id] = this.options.relay === undefined ? 'unavailable' : (this.relayStates.get(node.id) ?? 'waiting');
|
||||
break;
|
||||
case 'unpaired':
|
||||
case 'ambiguous':
|
||||
case 'detached':
|
||||
statuses[node.id] = condition.kind;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return statuses;
|
||||
}
|
||||
|
||||
/** The modems' standing to every client, when it changed. */
|
||||
private sendModems(): void {
|
||||
const modems = this.modemStatuses();
|
||||
const text = JSON.stringify(modems);
|
||||
if (text === this.modemsSent) return;
|
||||
this.modemsSent = text;
|
||||
this.broadcast(message({ kind: 'modems', modems }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the simulation anew, as though nothing had run before: stopped as
|
||||
* by stop (what the devices wrote stays in the document), every card's
|
||||
@@ -529,7 +622,9 @@ export class SimulationRoom {
|
||||
this.mode = 'design';
|
||||
this.startedBy = undefined;
|
||||
this.lastWall = undefined;
|
||||
this.options.relay?.modems([]);
|
||||
this.broadcast(this.state());
|
||||
this.sendModems();
|
||||
this.flush([...this.clients.values()], false);
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user