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:
@@ -1,7 +1,8 @@
|
||||
// The shared documents (/sync, plan §5): Hocuspocus, a project a document.
|
||||
// Who comes in is decided by the session the browser's cookie names (plan
|
||||
// §8.5): its grant to the project, read or write; a frozen project is read
|
||||
// by everyone; a refusal says why (SYNC_REFUSALS). A project is loaded from
|
||||
// by everyone; a refusal says why (SYNC_REFUSALS); the access a connection
|
||||
// has is told to it at once (and again when it changes). A project is loaded from
|
||||
// the store when its first person comes (projects are made through the
|
||||
// service), every
|
||||
// update stored as it comes, compacted a minute after the last change, when
|
||||
@@ -34,6 +35,8 @@ export const SESSION_COOKIE = 'filius_session';
|
||||
export interface ConnectionContext {
|
||||
readonly person: SessionPerson;
|
||||
readonly level: Level;
|
||||
/** Whether the project was frozen when the connection came. */
|
||||
readonly frozen: boolean;
|
||||
}
|
||||
|
||||
export interface SyncOptions {
|
||||
@@ -51,9 +54,14 @@ export interface Sync {
|
||||
discard(project: string): Promise<void>;
|
||||
}
|
||||
|
||||
/** A refusal of a connection, with the reason the person's editor is told (Hocuspocus sends an error's `reason`). */
|
||||
/**
|
||||
* A refusal of a connection, with the reason the person's editor is told
|
||||
* (Hocuspocus sends an error's `reason`). Without a message: Hocuspocus
|
||||
* writes a failed hook's message onto the console, outside F's JSON log,
|
||||
* which has the refusal already (`sync.refused`, `doc.unknown`).
|
||||
*/
|
||||
export function refusal(reason: SyncRefusal): Error & { readonly reason: SyncRefusal } {
|
||||
return Object.assign(new Error(reason), { reason });
|
||||
return Object.assign(new Error(), { reason });
|
||||
}
|
||||
|
||||
/** A cookie's value from a Cookie header. */
|
||||
@@ -109,7 +117,11 @@ export function createSync({ store, sessions, log, compactAfterIdleMilliseconds
|
||||
}
|
||||
connectionConfig.readOnly = access.level === 'read' || access.frozen;
|
||||
log.info('sync.joined', { project, user: access.person.login, readOnly: connectionConfig.readOnly });
|
||||
return { person: access.person, level: access.level };
|
||||
return { person: access.person, level: access.level, frozen: access.frozen };
|
||||
},
|
||||
// Told at once, so that the editor knows from the start whether it reads, and why.
|
||||
async connected({ connection, context }) {
|
||||
connection.sendStateless(accessMessage(connection.readOnly, (context as ConnectionContext).frozen));
|
||||
},
|
||||
async onLoadDocument({ documentName, document }) {
|
||||
const persistence = new DocumentPersistence(store, documentName, log);
|
||||
|
||||
Reference in New Issue
Block a user