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:
@@ -2,9 +2,15 @@
|
||||
// test secrets, a project as the service makes it, a person signed in with
|
||||
// a ticket as the dashboard signs one, and that person in a project with
|
||||
// Hocuspocus' own provider over a real WebSocket (bringing the session's
|
||||
// cookie, as the browser does).
|
||||
// cookie, as the browser does). The rooms' thread is the real one, bundled
|
||||
// by esbuild as the build bundles it.
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join as joinPath } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { build } from 'esbuild';
|
||||
import { HocuspocusProvider, HocuspocusProviderWebsocket } from '@hocuspocus/provider';
|
||||
import { expect } from 'vitest';
|
||||
import WebSocket from 'ws';
|
||||
@@ -32,6 +38,18 @@ export function memoryLog(): MemoryLog {
|
||||
|
||||
export type TestServer = FiliusServer & { readonly log: MemoryLog; readonly base: string };
|
||||
|
||||
let roomThread: Promise<URL> | undefined;
|
||||
|
||||
/** src/simWorker.ts bundled into a directory of its own, once a test file. */
|
||||
export function bundledRoomThread(): Promise<URL> {
|
||||
roomThread ??= (async () => {
|
||||
const outfile = joinPath(mkdtempSync(joinPath(tmpdir(), 'filius-room-thread-')), 'simWorker.mjs');
|
||||
await build({ entryPoints: [new URL('../src/simWorker.ts', import.meta.url).pathname], bundle: true, platform: 'node', format: 'esm', target: 'node20', outfile, logLevel: 'error' });
|
||||
return pathToFileURL(outfile);
|
||||
})();
|
||||
return roomThread;
|
||||
}
|
||||
|
||||
export async function startTestServer(store: DocStore, sessions: SessionStore, options: Partial<ServerOptions> = {}): Promise<TestServer> {
|
||||
const log = memoryLog();
|
||||
const server = await startServer({
|
||||
@@ -44,6 +62,8 @@ export async function startTestServer(store: DocStore, sessions: SessionStore, o
|
||||
serviceKey: SERVICE_KEY,
|
||||
origins: [EDITOR_ORIGIN],
|
||||
secureCookie: false,
|
||||
workerUrl: await bundledRoomThread(),
|
||||
simThreads: 1,
|
||||
...options,
|
||||
});
|
||||
return Object.assign(server, { log, base: `http://127.0.0.1:${server.port}` });
|
||||
@@ -61,14 +81,16 @@ export async function newProject(store: DocStore): Promise<string> {
|
||||
export interface Person {
|
||||
readonly user?: string;
|
||||
readonly login?: string;
|
||||
/** The name the dashboard shows. */
|
||||
readonly name?: string;
|
||||
readonly level?: 'read' | 'write';
|
||||
/** The cookie the browser brings to /__auth. */
|
||||
readonly cookie?: string;
|
||||
}
|
||||
|
||||
/** The answer of /__auth to a fresh ticket. */
|
||||
export function exchange(server: TestServer, project: string, { user = randomUUID(), login = 'ab12', level = 'write', cookie }: Person = {}, next = '/'): Promise<Response> {
|
||||
const ticket = signTicket({ u: user, n: login, d: 'Ada Beispiel', c: '#3366cc', lang: 'de', p: project, lvl: level }, TICKET_SECRET);
|
||||
export function exchange(server: TestServer, project: string, { user = randomUUID(), login = 'ab12', name = 'Ada Beispiel', level = 'write', cookie }: Person = {}, next = '/'): Promise<Response> {
|
||||
const ticket = signTicket({ u: user, n: login, d: name, c: '#3366cc', lang: 'de', p: project, lvl: level }, TICKET_SECRET);
|
||||
return fetch(`${server.base}/__auth?ticket=${encodeURIComponent(ticket)}&next=${encodeURIComponent(next)}`, {
|
||||
redirect: 'manual',
|
||||
...(cookie === undefined ? {} : { headers: { cookie } }),
|
||||
|
||||
Reference in New Issue
Block a user