feat: F5.4 presence: who else is in the project

Over Yjs awareness each editor tells who it is (F names the person in
the access message), the device it has chosen and the windows it has
open, and nothing else: no pointer, nothing that changes without a
cause, sent only when it changed. Others' states are read with care.
A frame in their colour round a device they chose, a dot on a window
they have open too, a circle with initials each beside the mode
buttons; the bridge tells the dashboard who is there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 16:09:34 +00:00
co-authored by Claude Opus 5.5
parent f52852493a
commit 47beda6b85
22 changed files with 600 additions and 33 deletions
@@ -10,6 +10,7 @@ import { randomUUID } from 'node:crypto';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import WebSocket from 'ws';
import { addDevice } from '@mdg-filius/doc';
import { peersOf } from '../../editor/src/remote/presence.js';
import { connectSharedDoc, type SharedDocState } from '../../editor/src/remote/sharedDoc.js';
import { PostgresSessions, PostgresStore } from '../src/index.js';
import { testDatabase, type TestDatabase } from './database.js';
@@ -68,6 +69,29 @@ describe("the editor's shared document", () => {
await server.close();
});
it('knows who it is, from F, and sees the others through the awareness', async () => {
const server = await startTestServer(store, sessions);
const project = await newProject(store);
const [miaId, benId] = [randomUUID(), randomUUID()];
const mia = connectEditor(server, project, await signIn(server, project, { user: miaId, name: 'Mia' }));
const ben = connectEditor(server, project, await signIn(server, project, { user: benId, name: 'Ben', level: 'read' }));
await mia.ready();
await ben.ready();
const miaReady = mia.last() as Extract<SharedDocState, { kind: 'ready' }>;
const benReady = ben.last() as Extract<SharedDocState, { kind: 'ready' }>;
expect(miaReady.you).toEqual({ id: miaId, name: 'Mia', color: '#3366cc' });
expect(benReady.you).toMatchObject({ id: benId, name: 'Ben' });
// A reader is seen too: the awareness is not the document.
benReady.awareness.setLocalState({ v: 1, user: benReady.you, selection: 'router', windows: [] });
await until(() => peersOf(miaReady.awareness.getStates(), miaReady.awareness.clientID).length === 1);
expect(peersOf(miaReady.awareness.getStates(), miaReady.awareness.clientID)[0]!.state).toEqual({ v: 1, user: benReady.you, selection: 'router', windows: [] });
// Gone, he is gone for her too.
ben.close();
await until(() => peersOf(miaReady.awareness.getStates(), miaReady.awareness.clientID).length === 0);
mia.close();
await server.close();
});
it('reads while the project is frozen and gets the document anew when it is thawed', async () => {
const server = await startTestServer(store, sessions);
const project = await newProject(store);