Files
mdg-filius/tools/live/lib.ts
T
julian.kiedaischandClaude Opus 5.5 416d5fdea3 feat: F5.6c measured and accepted on test-filius; tickets up to 5 s ahead of F's clock
test-filius refused every ticket as from the future: its clock was 886 ms
behind the dashboard's machine, with NTP still 8 ms (one ticket in 125, as a
ticket carries whole seconds). F now takes a ticket signed up to 5 s ahead
(TICKET_AHEAD_SECONDS); itsdangerous refuses one second, and the oracle
keeps that on record.

tools/live measures and accepts on a live F, with fresh projects each run:
m3 (200 rooms of the largest example with pings: probe p95 37 ms), m4 (63 KB
document; dragging makes an update per pointer move, 59/s), m5 (an hour
through the proxy), acceptance (two browsers, 14 checks, passed three times)
and clock (F's clock against ours).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:22:22 +00:00

172 lines
6.9 KiB
TypeScript

// Measuring on test-filius: projects through the service, people through
// tickets, the shared document through /sync, rooms through /sim. The
// secrets come from /root/.filius-test.env and are never printed.
import { readFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
import * as Y from 'yjs';
import { openProjectArchive } from '@mdg-filius/fls';
import { writeProject } from '@mdg-filius/doc';
import { signTicket } from '../../packages/server/test/tickets.js';
import { HocuspocusProvider, HocuspocusProviderWebsocket } from '@hocuspocus/provider';
import WebSocket from 'ws';
const env = Object.fromEntries(
readFileSync('/root/.filius-test.env', 'utf8')
.split('\n')
.filter((l) => l.includes('='))
.map((l) => [l.slice(0, l.indexOf('=')).trim(), l.slice(l.indexOf('=') + 1).trim()]),
);
export const HOST = process.env.FILIUS_HOST ?? 'test-filius.hub.mdg-hamburg.de';
export const BASE = `https://${HOST}`;
const SECRET = env.FILIUS_TICKET_SECRET!;
const KEY = env.FILIUS_SERVICE_KEY!;
export const made: string[] = [];
/**
* How many seconds before now the tickets are signed. F before
* TICKET_AHEAD_SECONDS refused a ticket from its future, and test-filius'
* clock was behind this machine's (886 ms, then 8 ms with NTP).
*/
const SKEW_SECONDS = Number(process.env.SKEW_SECONDS ?? 2);
export async function svc(method: string, path: string, body?: unknown): Promise<{ status: number; json: any }> {
const r = await fetch(`${BASE}${path}`, { method, headers: { 'x-filius-service-key': KEY, 'content-type': 'application/json' }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
return { status: r.status, json: await r.json().catch(() => null) };
}
export async function newProject(): Promise<string> {
const id = randomUUID();
const r = await svc('POST', '/svc/projects', { project: id });
if (r.status !== 201) throw new Error(`project not made: ${r.status}`);
made.push(id);
return id;
}
export async function cleanUp(): Promise<void> {
for (const id of made.splice(0)) await svc('DELETE', `/svc/projects/${id}`);
}
/** The address a person's browser opens: a fresh ticket, then the editor on the project. */
export function signInAddress(project: string, { user = randomUUID(), name = 'Messung', level = 'write' }: { user?: string; name?: string; level?: string } = {}): string {
const ticket = signTicket({ u: user, n: name.toLowerCase().replace(/\W/g, ''), d: name, c: '#3366cc', lang: 'de', p: project, lvl: level }, SECRET, Math.floor(Date.now() / 1000) - SKEW_SECONDS);
return `${BASE}/__auth?ticket=${encodeURIComponent(ticket)}&next=${encodeURIComponent(`/?project=${project}`)}`;
}
export async function signIn(project: string, { user = randomUUID(), name = 'Messung', level = 'write' }: { user?: string; name?: string; level?: string } = {}): Promise<string> {
const ticket = signTicket({ u: user, n: name.toLowerCase().replace(/\W/g, ''), d: name, c: '#3366cc', lang: 'de', p: project, lvl: level }, SECRET, Math.floor(Date.now() / 1000) - SKEW_SECONDS);
const r = await fetch(`${BASE}/__auth?ticket=${encodeURIComponent(ticket)}&next=/`, { redirect: 'manual' });
if (r.status !== 302) throw new Error(`not signed in: ${r.status}`);
return r.headers.get('set-cookie')!.split(';')[0]!;
}
export function readExample(file: string) {
return openProjectArchive(new Uint8Array(readFileSync(new URL(`../../corpus/${file}`, import.meta.url))), () => randomUUID());
}
export interface Shared {
doc: Y.Doc;
provider: any;
close(): void;
}
export function joinDoc(project: string, cookie: string): Promise<Shared> {
const doc = new Y.Doc();
class Browser extends WebSocket {
constructor(url: string, protocols?: string | string[]) {
super(url, protocols, { headers: { cookie, origin: BASE } });
this.on('error', () => {});
}
}
const socket = new HocuspocusProviderWebsocket({ url: `wss://${HOST}/sync`, WebSocketPolyfill: Browser });
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('not synced in 15 s')), 15_000);
const provider = new HocuspocusProvider({
websocketProvider: socket,
name: project,
document: doc,
token: 'cookie',
onSynced: () => {
clearTimeout(timer);
resolve({
doc,
provider,
close: () => {
provider.destroy();
socket.destroy();
},
});
},
onAuthenticationFailed: ({ reason }: any) => reject(new Error(`refused ${reason}`)),
});
provider.attach();
});
}
export async function whenStored(shared: Shared, ms = 15_000): Promise<void> {
for (let waited = 0; shared.provider.unsyncedChanges > 0; waited += 20) {
if (waited > ms) throw new Error('not stored');
await new Promise((r) => setTimeout(r, 20));
}
}
/** A project of F with an example written into it, as one person's editor would. */
export async function seededProject(file: string) {
const project = await newProject();
const example = readExample(file);
const shared = await joinDoc(project, await signIn(project));
writeProject(shared.doc, example);
await whenStored(shared);
shared.close();
return { project, example };
}
export interface Room {
send(m: object): void;
messages: any[];
waitFor(test: (m: any) => boolean, ms?: number): Promise<any>;
onMessage: ((m: any) => void) | undefined;
close(): void;
closed: Promise<string>;
}
export function openRoom(project: string, cookie: string): Promise<Room> {
return new Promise((resolve, reject) => {
const socket = new WebSocket(`wss://${HOST}/sim?project=${project}`, { headers: { cookie, origin: BASE } });
const messages: any[] = [];
const waiters: { test: (m: any) => boolean; resolve: (m: any) => void }[] = [];
let closedWith: (r: string) => void = () => {};
const room: Room = {
send: (m) => socket.send(JSON.stringify({ v: 1, ...m })),
messages,
onMessage: undefined,
waitFor: (test, ms = 10_000) =>
new Promise((resolve, reject) => {
const found = messages.find(test);
if (found) return resolve(found);
const timer = setTimeout(() => reject(new Error('waited in vain')), ms);
waiters.push({
test,
resolve: (m) => {
clearTimeout(timer);
resolve(m);
},
});
}),
close: () => socket.close(),
closed: new Promise((r) => (closedWith = r)),
};
socket.on('message', (data: Buffer) => {
const m = JSON.parse(data.toString());
room.onMessage?.(m);
if (m.kind !== 'activity') messages.push(m);
for (const w of [...waiters])
if (w.test(m)) {
waiters.splice(waiters.indexOf(w), 1);
w.resolve(m);
}
});
socket.on('open', () => resolve(room));
socket.on('error', (e: Error) => reject(e));
socket.on('close', (code: number, reason: Buffer) => closedWith(`${code} ${reason}`));
});
}