feat: F5.5 large binary files as blobs on server F
From 16 KB a picture's or binary file's entry names its bytes by their SHA-256 instead of holding them; the bytes are on F, content-addressed (GET/POST /blobs with the session's grant, from the editor's origin, at most 16 MiB). A document's blob side says where they are: the editor's fetches and uploads, the room thread's asks F's main thread, and a start waits for the ones asked for; without a side everything stays in the document. Text stays Y.Text. A blob whose bytes are not there is never written back empty. A blob F has not got yet is asked for again a few times, and a running room reads late bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,12 @@
|
||||
// told, thawed he writes again; his grant ended, he is told and the editor
|
||||
// goes.
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdtempSync, readFileSync, statSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { expect, test, type Browser, type Page } from '@playwright/test';
|
||||
import { BLOB_THRESHOLD_BYTES } from '@mdg-filius/doc';
|
||||
import { drag, onDevice, serveEditor } from './editorPage.js';
|
||||
import { colorOf } from '../src/remote/presence.js';
|
||||
import { startServerF, type ServerF } from './serverF.js';
|
||||
@@ -158,6 +163,51 @@ test('three people in one project of F: moving, reading only, one simulation for
|
||||
await expect(canvas(mia)).toBeVisible();
|
||||
});
|
||||
|
||||
test('takes a large picture one person imports to F as a blob, and shows it to another', async ({ browser }) => {
|
||||
test.setTimeout(120_000);
|
||||
const project = await f.project('java-examples/internetwork_komplett.fls');
|
||||
const notebook = project.node('Notebook 1.10');
|
||||
const mia = await person(browser, project.id, 'Mia');
|
||||
const ben = await person(browser, project.id, 'Ben');
|
||||
// A picture far over 16 KB: a screenshot of the page.
|
||||
const file = join(mkdtempSync(join(tmpdir(), 'filius-blob-')), 'bild.png');
|
||||
await mia.screenshot({ path: file });
|
||||
const size = statSync(file).size;
|
||||
expect(size).toBeGreaterThan(BLOB_THRESHOLD_BYTES);
|
||||
await mia.locator('button[name="simulationMode"]').click();
|
||||
for (const page of [mia, ben]) await expect(canvas(page)).toHaveAttribute('data-mode', 'simulation');
|
||||
const open = async (page: Page) => {
|
||||
const at = await onDevice(page, notebook);
|
||||
await page.mouse.click(at.x, at.y);
|
||||
return desktop(page, notebook);
|
||||
};
|
||||
const miaDesktop = await open(mia);
|
||||
await miaDesktop.locator('[data-program="installation"]').click();
|
||||
const installation = miaDesktop.locator('[data-program-window="installation"]');
|
||||
for (const program of ['Datei-Explorer', 'Bildbetrachter']) await installation.locator('[data-list="available"] [role="option"]').filter({ hasText: program }).click();
|
||||
await installation.getByRole('button', { name: 'Installieren' }).click();
|
||||
await installation.getByRole('button', { name: 'Änderungen annehmen' }).click();
|
||||
await miaDesktop.locator('[data-program="fileExplorer"]').click();
|
||||
const explorer = miaDesktop.locator('[data-program-window="fileExplorer"]');
|
||||
await explorer.getByRole('toolbar', { name: 'Aktionen' }).getByRole('button', { name: 'Importieren' }).click();
|
||||
const importing = explorer.getByRole('dialog', { name: 'Datei importieren' });
|
||||
await importing.locator('input[type="file"]').setInputFiles(file);
|
||||
await importing.getByRole('button', { name: 'Datei importieren' }).click();
|
||||
await expect(importing.getByRole('status')).toHaveText('Datei erfolgreich importiert!');
|
||||
// Ben opens it in his viewer: the picture, as large as the page was.
|
||||
const benDesktop = await open(ben);
|
||||
await benDesktop.locator('[data-program="imageViewer"]').click();
|
||||
const viewer = benDesktop.locator('[data-program-window="imageViewer"]');
|
||||
await viewer.getByRole('button', { name: 'Öffnen' }).click();
|
||||
const dialog = viewer.getByRole('dialog', { name: 'Datei Öffnen' });
|
||||
await dialog.locator('.file-dialog-entries button', { hasText: 'bild.png' }).click();
|
||||
await dialog.getByRole('button', { name: 'Öffnen' }).click();
|
||||
await expect.poll(() => viewer.getByRole('img', { name: 'Bild' }).evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(mia.viewportSize()!.width);
|
||||
// In F's store under its SHA-256.
|
||||
const sha = createHash('sha256').update(readFileSync(file)).digest('hex');
|
||||
expect(await f.blobSize(sha)).toBe(size);
|
||||
});
|
||||
|
||||
test('tells the dashboard over the bridge who else is in the project, each time it changes', async ({ browser }) => {
|
||||
const project = await f.project('java-examples/zwei_rechnernetze_komplett.fls');
|
||||
const context = await browser.newContext();
|
||||
|
||||
Reference in New Issue
Block a user