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:
@@ -3,7 +3,11 @@
|
||||
// with the fewest rooms takes a new one). A room keeps its project's
|
||||
// document open on F (a direct connection: what the simulation leaves goes
|
||||
// into the document even when no one has it open) and gets every change of
|
||||
// it; what the room writes comes back as the room's own change.
|
||||
// it; what the room writes comes back as the room's own change. The blobs
|
||||
// its document names come from F's store when the thread asks; a blob the
|
||||
// simulation makes is kept before the change that names it is applied (the
|
||||
// room's messages are taken in order), so that no one is told of a file
|
||||
// whose bytes are not there.
|
||||
//
|
||||
// Who may do what: everyone in the project watches; only with a write grant
|
||||
// does one start, stop, set the speed, hold "Pakete verwerfen", clear the
|
||||
@@ -23,6 +27,7 @@ import type { DirectConnection, Hocuspocus } from '@hocuspocus/server';
|
||||
import * as Y from 'yjs';
|
||||
import { SIM_CLOSED_CODE, SIM_ROOM_FAILED_CODE, WRITING_KINDS, type ClientMessageKind, type HostMessage, type SyncClosing } from '@mdg-filius/protocol';
|
||||
import type WebSocket from 'ws';
|
||||
import type { BlobStore } from './blobs.js';
|
||||
import { errorText, type Log } from './log.js';
|
||||
import type { FromRoomThread, ToRoomThread } from './simChannel.js';
|
||||
|
||||
@@ -39,6 +44,7 @@ export interface SimPerson {
|
||||
|
||||
export interface SimRoomsOptions {
|
||||
readonly hocuspocus: Hocuspocus;
|
||||
readonly blobs: BlobStore;
|
||||
readonly log: Log;
|
||||
/** The bundled room thread (dist/simWorker.mjs). */
|
||||
readonly workerUrl: URL;
|
||||
@@ -64,6 +70,8 @@ interface Room {
|
||||
frozen: boolean;
|
||||
linger: ReturnType<typeof setTimeout> | undefined;
|
||||
ended: boolean;
|
||||
/** The thread's changes and blobs, taken in order. */
|
||||
taken: Promise<void>;
|
||||
}
|
||||
|
||||
type RawData = Buffer | ArrayBuffer | Buffer[];
|
||||
@@ -124,9 +132,30 @@ export class SimRooms {
|
||||
private fromThread(thread: Thread, message: FromRoomThread): void {
|
||||
const room = [...thread.rooms].find((each) => each.project === message.room);
|
||||
if (room === undefined) return;
|
||||
const inOrder = (work: () => Promise<void> | void) => {
|
||||
room.taken = room.taken.then(work).catch((error: unknown) => this.options.log.error('sim.blobFailed', { project: room.project, error: errorText(error) }));
|
||||
};
|
||||
switch (message.type) {
|
||||
case 'doc':
|
||||
Y.applyUpdate(room.doc, message.update, room);
|
||||
inOrder(() => Y.applyUpdate(room.doc, message.update, room));
|
||||
return;
|
||||
case 'blob':
|
||||
inOrder(async () => void (await this.options.blobs.put(message.bytes)));
|
||||
return;
|
||||
case 'needBlobs':
|
||||
void this.options.blobs.get(message.shas).then(
|
||||
(found) =>
|
||||
this.post(room, {
|
||||
type: 'blobs',
|
||||
room: room.project,
|
||||
blobs: [...found].map(([sha, bytes]) => ({ sha, bytes })),
|
||||
missing: message.shas.filter((sha) => !found.has(sha)),
|
||||
}),
|
||||
(error: unknown) => {
|
||||
this.options.log.error('sim.blobFailed', { project: room.project, error: errorText(error) });
|
||||
this.post(room, { type: 'blobs', room: room.project, blobs: [], missing: message.shas });
|
||||
},
|
||||
);
|
||||
return;
|
||||
case 'host':
|
||||
room.clients.get(message.client)?.socket.send(JSON.stringify(message.message));
|
||||
@@ -168,6 +197,7 @@ export class SimRooms {
|
||||
frozen,
|
||||
linger: undefined,
|
||||
ended: false,
|
||||
taken: Promise.resolve(),
|
||||
};
|
||||
thread.rooms.add(room);
|
||||
this.post(room, { type: 'open', room: project, state: Y.encodeStateAsUpdate(doc) });
|
||||
@@ -267,6 +297,8 @@ export class SimRooms {
|
||||
const done = (async () => {
|
||||
this.rooms.delete(room.project);
|
||||
await this.ask(room, { type: 'close', room: room.project, writeBack });
|
||||
// What it wrote back is in the document before the document is let go.
|
||||
await room.taken;
|
||||
this.forget(room);
|
||||
await room.direct.disconnect();
|
||||
this.options.log.info('sim.roomClosed', { project: room.project, writeBack });
|
||||
|
||||
Reference in New Issue
Block a user