feat(editor): the simulation's properties in a sheet of their own, with a restart for everyone
Who started the simulation, "Pakete verwerfen" and the dropped packets move from the toolbar into a properties sheet beside the surface. New in it: restart the simulation, after asking. The room learns a restart message: stopped as by stop, every capture, dropped frame and program window emptied, started anew and named after who restarted; refused for readers and frozen projects, logged, and waiting for asked blobs like a start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ export type AppKind = (typeof APP_KINDS)[number];
|
||||
*/
|
||||
export type Topic = 'activity' | 'dropped' | `trace:${string}` | `app:${string}/${AppKind}` | `node:${string}`;
|
||||
|
||||
export const CLIENT_MESSAGE_KINDS = ['sub', 'unsub', 'start', 'stop', 'speed', 'drop', 'clearCapture', 'input'] as const;
|
||||
export const CLIENT_MESSAGE_KINDS = ['sub', 'unsub', 'start', 'stop', 'restart', 'speed', 'drop', 'clearCapture', 'input'] as const;
|
||||
export type ClientMessageKind = (typeof CLIENT_MESSAGE_KINDS)[number];
|
||||
|
||||
export type ClientMessage =
|
||||
@@ -35,6 +35,15 @@ export type ClientMessage =
|
||||
*/
|
||||
| { readonly v: 1; readonly kind: 'start'; readonly language?: PageLanguage }
|
||||
| { readonly v: 1; readonly kind: 'stop' }
|
||||
/**
|
||||
* Starts the simulation anew (not Java's: there one stops and starts, and
|
||||
* the data exchange tables and the desktops' windows outlive it): a running
|
||||
* one is stopped as by `stop` — what the devices wrote stays in the
|
||||
* document — then every card's capture, the dropped frames and every
|
||||
* program window are emptied, and it starts as by `start`. From the design
|
||||
* it only starts, emptied as well.
|
||||
*/
|
||||
| { readonly v: 1; readonly kind: 'restart'; readonly language?: PageLanguage }
|
||||
/** 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. */
|
||||
@@ -80,7 +89,7 @@ export interface DroppedUnit {
|
||||
export const ERROR_CODES = ['badMessage', 'badVersion', 'unknownKind', 'badTopic', 'notRunning', 'unknownNode', 'unknownApp', 'unknownAction', 'badArgs', 'engineFailed', 'readOnly', 'frozen'] as const;
|
||||
|
||||
/** What a client sends that changes the room: only with a write grant (plan §6.4); a reader subscribes. */
|
||||
export const WRITING_KINDS: readonly ClientMessageKind[] = ['start', 'stop', 'speed', 'drop', 'clearCapture', 'input'];
|
||||
export const WRITING_KINDS: readonly ClientMessageKind[] = ['start', 'stop', 'restart', 'speed', 'drop', 'clearCapture', 'input'];
|
||||
export type ErrorCode = (typeof ERROR_CODES)[number];
|
||||
|
||||
export type HostMessage =
|
||||
@@ -160,9 +169,12 @@ export function parseClientMessage(value: unknown): Parsed<ClientMessage> {
|
||||
return { message: { v: 1, kind: kind as 'sub' | 'unsub', topics: topics as Topic[] } };
|
||||
}
|
||||
case 'start':
|
||||
if (value.language === undefined) return { message: { v: 1, kind: 'start' } };
|
||||
case 'restart': {
|
||||
const starting = kind as 'start' | 'restart';
|
||||
if (value.language === undefined) return { message: { v: 1, kind: starting } };
|
||||
if (value.language !== 'de' && value.language !== 'en') return bad('badMessage', `language ${String(value.language)}`);
|
||||
return { message: { v: 1, kind: 'start', language: value.language } };
|
||||
return { message: { v: 1, kind: starting, language: value.language } };
|
||||
}
|
||||
case 'stop':
|
||||
return { message: { v: 1, kind: 'stop' } };
|
||||
case 'clearCapture':
|
||||
|
||||
@@ -11,6 +11,7 @@ describe("a client's messages", () => {
|
||||
{ v: 1, kind: 'unsub', topics: ['activity'] },
|
||||
{ v: 1, kind: 'start' },
|
||||
{ v: 1, kind: 'stop' },
|
||||
{ v: 1, kind: 'restart' },
|
||||
{ v: 1, kind: 'speed', value: 50 },
|
||||
{ v: 1, kind: 'drop', on: true },
|
||||
{ v: 1, kind: 'clearCapture' },
|
||||
@@ -19,7 +20,7 @@ describe("a client's messages", () => {
|
||||
expect(messages.map((each) => each.kind)).toEqual([...CLIENT_MESSAGE_KINDS]);
|
||||
for (const each of messages) expect(parseClientMessage(each)).toEqual({ message: each });
|
||||
// A start in a language, and the language dropped where it is none.
|
||||
for (const language of ['de', 'en']) expect(parseClientMessage({ v: 1, kind: 'start', language })).toEqual({ message: { v: 1, kind: 'start', language } });
|
||||
for (const kind of ['start', 'restart']) for (const language of ['de', 'en']) expect(parseClientMessage({ v: 1, kind, language })).toEqual({ message: { v: 1, kind, language } });
|
||||
expect(parseClientMessage({ v: 1, kind: 'stop', language: 'en' })).toEqual({ message: { v: 1, kind: 'stop' } });
|
||||
expect(parseClientMessage({ v: 1, kind: 'clearCapture', port: 'p' })).toEqual({ message: { v: 1, kind: 'clearCapture' } });
|
||||
});
|
||||
@@ -34,6 +35,7 @@ describe("a client's messages", () => {
|
||||
expect(code({ v: 1, kind: 'sub', topics: ['activity', 'trace:'] })).toBe('badTopic');
|
||||
expect(code({ v: 1, kind: 'unsub', topics: ['app:node/paint'] })).toBe('badTopic');
|
||||
expect(code({ v: 1, kind: 'start', language: 'fr' })).toBe('badMessage');
|
||||
expect(code({ v: 1, kind: 'restart', language: 'fr' })).toBe('badMessage');
|
||||
expect(code({ v: 1, kind: 'drop', on: 'yes' })).toBe('badMessage');
|
||||
expect(code({ v: 1, kind: 'sub', topics: ['dropped', 'app:node/sat', 'app:node/nat'] })).toBe('ok');
|
||||
expect(code({ v: 1, kind: 'start', language: null })).toBe('badMessage');
|
||||
|
||||
Reference in New Issue
Block a user