Files
mdg-filius/packages/editor/browser/serverF.ts
T
julian.kiedaischandClaude Opus 5.5 44b3f3ccb2 feat: F5.6d a drag is one change of the document; the others see it through the presence
Moving a device, moving or resizing a documentation item went into the
shared document at every pointer move: 59 updates a second, each leaving
about 18 bytes of history (M4 on test-filius). Now the editor keeps the drag
while it lasts, shows it itself and tells the others through the awareness
(moving, at most every 50 ms, the end at once), and writes it once when it
is let go. A drag the browser cancels or a second finger turns into a pinch
is written where it is; WebKit sends a second pointerdown for the first
finger when another comes, which had lost the drag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:17:38 +00:00

116 lines
5.5 KiB
TypeScript

// A real server F for the editor's browser tests: bundled as the build
// bundles it (dist/main.mjs and its room thread), started as a process with
// a throwaway Postgres (packages/server/test/database.ts), on the host the
// editor's page comes from, so that F's cookie goes with the page's
// requests. A project is made in F's store straight from a FILIUS file; a
// person signs in as the dashboard would let them, with a ticket at /__auth.
// Given a built editor, F hands it out itself, as in the image: then the
// editor's page comes from F's own origin.
import { spawn, type ChildProcess } from 'node:child_process';
import { mkdtempSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'node:crypto';
import type { BrowserContext } from '@playwright/test';
import { build } from 'esbuild';
import pg from 'pg';
import * as Y from 'yjs';
import { writeProject } from '@mdg-filius/doc';
import { openProjectArchive } from '@mdg-filius/fls';
import { testDatabase, type TestDatabase } from '../../server/test/database.js';
import { signTicket } from '../../server/test/tickets.js';
const TICKET_SECRET = 'browser-ticket-secret';
export const SERVICE_KEY = 'browser-service-key';
export interface ServerF {
readonly base: string;
/** A project made from a FILIUS file of the corpus; its devices' ids by name. */
project(file: string): Promise<{ readonly id: string; readonly node: (name: string) => string }>;
/** Signs a person in, in this browser context. */
signIn(context: BrowserContext, project: string, person: { readonly name: string; readonly level?: 'read' | 'write'; readonly user?: string }): Promise<void>;
service(method: string, path: string, body?: unknown): Promise<Response>;
/** The size of a blob in F's store, none without it. */
blobSize(sha: string): Promise<number | undefined>;
/** How many updates of a project F has stored since it last compacted it. */
updateCount(project: string): Promise<number>;
close(): Promise<void>;
}
/** Server F on the editor page's host (`editorBase`), for pages from there, or from itself with `editorFolder`. */
export async function startServerF(editorBase: string, editorFolder?: string): Promise<ServerF> {
const editor = new URL(editorBase);
const database: TestDatabase = await testDatabase();
const out = mkdtempSync(join(tmpdir(), 'filius-server-f-'));
const source = new URL('../../server/src/', import.meta.url).pathname;
await build({
entryPoints: [join(source, 'main.ts'), join(source, 'simWorker.ts')],
bundle: true,
platform: 'node',
format: 'esm',
target: 'node20',
outdir: out,
outExtension: { '.js': '.mjs' },
external: ['pg-native'],
banner: { js: "import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);" },
logLevel: 'error',
});
const port = 20_000 + Math.floor(Math.random() * 20_000);
const base = `${editor.protocol}//${editor.hostname}:${port}`;
const child: ChildProcess = spawn(process.execPath, [join(out, 'main.mjs')], {
env: {
...process.env,
DATABASE_URL: database.url,
FILIUS_TICKET_SECRET: TICKET_SECRET,
FILIUS_SERVICE_KEY: SERVICE_KEY,
FILIUS_ORIGINS: editorFolder === undefined ? editor.origin : base,
...(editorFolder === undefined ? {} : { FILIUS_EDITOR_DIR: editorFolder }),
FILIUS_PORT: String(port),
FILIUS_INSECURE_COOKIE: '1',
},
stdio: ['ignore', 'pipe', 'inherit'],
});
await new Promise<void>((resolve, reject) => {
child.stdout!.on('data', (data: Buffer) => {
if (data.toString().includes('"server.listening"')) resolve();
});
child.once('exit', (code) => reject(new Error(`server F ended (${code})`)));
});
const sql = new pg.Pool({ connectionString: database.url });
return {
base,
async project(file) {
const id = randomUUID();
const imported = openProjectArchive(new Uint8Array(readFileSync(new URL(`../../../corpus/${file}`, import.meta.url))), () => randomUUID());
const doc = new Y.Doc();
writeProject(doc, imported);
await sql.query('INSERT INTO filius_doc (project_uuid, state) VALUES ($1, $2)', [id, Buffer.from(Y.encodeStateAsUpdate(doc))]);
return { id, node: (name) => imported.graph.nodes.find((each) => each.displayName === name)!.id };
},
async signIn(context, project, { name, level = 'write', user = randomUUID() }) {
const ticket = signTicket({ u: user, n: name.toLowerCase(), d: name, p: project, lvl: level }, TICKET_SECRET);
const page = await context.newPage();
const response = await page.goto(`${base}/__auth?ticket=${encodeURIComponent(ticket)}&next=/healthz`);
if (response?.status() !== 200) throw new Error(`not signed in: ${response?.status()}`);
await page.close();
},
async blobSize(sha) {
const result = await sql.query<{ size: number }>('SELECT size FROM filius_blob WHERE sha = $1', [sha]);
return result.rows[0]?.size;
},
async updateCount(project) {
const result = await sql.query<{ count: string }>('SELECT count(*) FROM filius_doc_update WHERE project_uuid = $1', [project]);
return Number(result.rows[0]!.count);
},
service: (method, path, body) => fetch(`${base}${path}`, { method, headers: { 'x-filius-service-key': SERVICE_KEY }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }),
async close() {
const ended = new Promise((resolve) => child.once('exit', resolve));
child.kill('SIGTERM');
await ended;
await sql.end();
await database.stop();
},
};
}