feat: F5.3a the simulation rooms on server F

One room per project for everyone in it, held by a few worker_threads,
each room with a replica of the document that Hocuspocus' direct
connection keeps open: changes go in, what the room writes comes back.
/sim?project=... signs in as /sync does and says refusals and partings
as the socket's closing; only a write grant changes the room. The state
names who started the simulation. After the last person the room stays
60 s, then stops, writes back and ends. Freezing stops a running
simulation and refuses to start one, deleting ends the room without
writing back, revoking lets the person go from the room too; a failed
thread ends its rooms, stopping the service stops them all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 14:29:11 +00:00
co-authored by Claude Opus 5.5
parent 5ded84cb33
commit 8cfc518f0c
17 changed files with 1033 additions and 43 deletions
+33
View File
@@ -79,6 +79,39 @@ describe('a simulation room', () => {
}
});
it('names who started the simulation while it runs, and no one in a room of one page', () => {
const { room, got, send, run } = setUp();
const mia: HostMessage[] = [];
const miaId = room.connect((message) => mia.push(message), 'Mia');
room.receive(miaId, { v: 1, kind: 'start' });
run(400);
const late: HostMessage[] = [];
room.connect((message) => late.push(message), 'Ben');
expect(ofKind(late, 'state').map((state) => state.startedBy)).toEqual(['Mia']);
send({ kind: 'speed', value: 50 });
send({ kind: 'stop' });
expect(ofKind(mia, 'state').map(({ mode, startedBy }) => [mode, startedBy])).toEqual([
['design', undefined],
['simulation', 'Mia'],
['simulation', 'Mia'],
['design', undefined],
]);
// Started by the client without a name, the room names no one.
send({ kind: 'start' });
expect(ofKind(got, 'state').at(-1)).not.toHaveProperty('startedBy');
});
it('forgets who started it when the core fails', () => {
const { room, run, node } = setUp(exampleDoc('java-examples/modem_beispiel.fls'));
const mia: HostMessage[] = [];
const miaId = room.connect((message) => mia.push(message), 'Mia');
room.receive(miaId, { v: 1, kind: 'start' });
room.receive(miaId, { v: 1, kind: 'input', node: node('Notebook 0.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.11' }, reqId: 'r' });
run(10_000);
expect(ofKind(mia, 'state').at(-1)).toMatchObject({ mode: 'design' });
expect(ofKind(mia, 'state').at(-1)).not.toHaveProperty('startedBy');
});
it("sends a card's frames and a terminal's lines as the engine has them", () => {
const { got, send, node, run } = setUp();
const notebook = node('Notebook 1.10');