feat: F5.3b the editor on a project of server F

?project=<uuid> opens the shared document over /sync and the room over
/sim (RemoteSimHost: messages wait for the socket, a lost socket comes
back with what it watched, F's refusal or parting ends it); instead of
the editor, a word why when F refuses or lets go. F tells each
connection its access at once. Who only reads sees everything and
changes nothing: no palette or undo, a locked surface, the sheet's
fields disabled (tabs stay), no starting or stopping; thawed, the
document comes anew. The toolbar names who started the simulation.
Refusals carry no message, so Hocuspocus writes nothing outside F's
JSON log. A browser test runs three people against a real F.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 15:00:18 +00:00
co-authored by Claude Opus 5.5
parent 8cfc518f0c
commit f52852493a
28 changed files with 1142 additions and 58 deletions
@@ -0,0 +1,51 @@
// The editor on a project of server F (F5.3b): the shared document over
// /sync and the room over /sim, both signed in by the cookie F set at
// /__auth. Until the document is there, a word that it is coming; when F
// refuses or lets go — of the document or the room — a word why, instead of
// the editor. Who only reads gets the editor locked (Editor's readOnly).
import { useEffect, useMemo, useState } from 'react';
import { Editor } from '../Editor.js';
import { CATALOGS, useLanguage, useTranslate } from '../i18n/i18n.js';
import type { SimHostFactory } from '../simulation/useSimulation.js';
import { RemoteSimHost, simUrl } from './remoteSimHost.js';
import { connectSharedDoc, type SharedDocState } from './sharedDoc.js';
export interface SharedProjectProps {
/** F's address. */
readonly server: string;
readonly project: string;
}
export function SharedProject({ server, project }: SharedProjectProps) {
const t = useTranslate();
const language = useLanguage();
const [state, setState] = useState<SharedDocState>({ kind: 'connecting' });
/** F ended the room for this page: the editor goes as when the document is let go. */
const [roomEnd, setRoomEnd] = useState<string | null>(null);
useEffect(() => connectSharedDoc({ server, project, onState: setState }), [server, project]);
const createSimHost = useMemo<SimHostFactory>(
() => () => {
const host = new RemoteSimHost(simUrl(server, project));
host.onEnd((end) => setRoomEnd(end.reason));
return host;
},
[server, project],
);
const ended = roomEnd ?? (state.kind === 'refused' || state.kind === 'closed' ? state.reason : null);
if (ended !== null) {
const key = `remote.${ended}`;
return (
<p role="alert" className="remote-message">
{key in CATALOGS[language] ? t(key) : t('remote.refused', { reason: ended })}
</p>
);
}
if (state.kind !== 'ready')
return (
<p role="status" className="remote-message">
{t('remote.connecting')}
</p>
);
return <Editor key={state.doc.guid} doc={state.doc} createSimHost={createSimHost} readOnly={state.readOnly} frozen={state.frozen} />;
}