mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Server snapshot WIP
This commit is contained in:
parent
49925038fd
commit
7b72406824
10 changed files with 97 additions and 45 deletions
|
@ -9,7 +9,7 @@ import { Network } from "../sync/utils";
|
|||
// CFDO II: add senderId, possibly roomId as well
|
||||
export class DurableDeltasRepository implements DeltasRepository {
|
||||
// there is a 2MB row limit, hence working with max payload size of 1.5 MB
|
||||
// and leaving a buffer for other row metadata
|
||||
// and leaving a ~500kB buffer for other row metadata
|
||||
private static readonly MAX_PAYLOAD_SIZE = 1_500_000;
|
||||
|
||||
constructor(private storage: DurableObjectStorage) {
|
||||
|
|
|
@ -2,8 +2,6 @@ import { DurableObject } from "cloudflare:workers";
|
|||
import { DurableDeltasRepository } from "./repository";
|
||||
import { ExcalidrawSyncServer } from "../sync/server";
|
||||
|
||||
import type { ExcalidrawElement } from "../element/types";
|
||||
|
||||
/**
|
||||
* Durable Object impl. of Excalidraw room.
|
||||
*/
|
||||
|
@ -11,26 +9,10 @@ export class DurableRoom extends DurableObject {
|
|||
private roomId: string | null = null;
|
||||
private sync: ExcalidrawSyncServer;
|
||||
|
||||
private snapshot!: {
|
||||
appState: Record<string, any>;
|
||||
elements: Map<string, ExcalidrawElement>;
|
||||
version: number;
|
||||
};
|
||||
|
||||
constructor(ctx: DurableObjectState, env: Env) {
|
||||
super(ctx, env);
|
||||
|
||||
this.ctx.blockConcurrencyWhile(async () => {
|
||||
// CFDO I: snapshot should likely be a transient store
|
||||
// CFDO II: loaded the latest state from the db
|
||||
this.snapshot = {
|
||||
// CFDO: start persisting acknowledged version (not a scene version!)
|
||||
// CFDO: we don't persist appState, should we?
|
||||
appState: {},
|
||||
elements: new Map(),
|
||||
version: 0,
|
||||
};
|
||||
|
||||
this.roomId = (await this.ctx.storage.get("roomId")) || null;
|
||||
});
|
||||
|
||||
|
|
|
@ -5367,7 +5367,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
: -1;
|
||||
|
||||
if (midPoint && midPoint > -1) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
this.store.scheduleCapture();
|
||||
LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
|
||||
|
||||
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||
|
|
|
@ -32,7 +32,7 @@ import type {
|
|||
} from "./element/types";
|
||||
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
||||
import { getNonDeletedGroupIds } from "./groups";
|
||||
import { getObservedAppState } from "./store";
|
||||
import { getObservedAppState, StoreSnapshot } from "./store";
|
||||
import type {
|
||||
AppState,
|
||||
ObservedAppState,
|
||||
|
@ -1036,7 +1036,10 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||
* @param modifierOptions defines which of the delta (`deleted` or `inserted`) will be updated
|
||||
* @returns new instance with modified delta/s
|
||||
*/
|
||||
public applyLatestChanges(elements: SceneElementsMap): ElementsDelta {
|
||||
public applyLatestChanges(
|
||||
elements: SceneElementsMap,
|
||||
modifierOptions: "deleted" | "inserted",
|
||||
): ElementsDelta {
|
||||
const modifier =
|
||||
(element: OrderedExcalidrawElement) => (partial: ElementPartial) => {
|
||||
const latestPartial: { [key: string]: unknown } = {};
|
||||
|
@ -1069,7 +1072,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||
delta.deleted,
|
||||
delta.inserted,
|
||||
modifier(existingElement),
|
||||
"inserted",
|
||||
modifierOptions,
|
||||
);
|
||||
|
||||
modifiedDeltas[id] = modifiedDelta;
|
||||
|
@ -1092,7 +1095,10 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||
|
||||
public applyTo(
|
||||
elements: SceneElementsMap,
|
||||
snapshot: Map<string, OrderedExcalidrawElement>,
|
||||
elementsSnapshot: Map<
|
||||
string,
|
||||
OrderedExcalidrawElement
|
||||
> = StoreSnapshot.empty().elements,
|
||||
): [SceneElementsMap, boolean] {
|
||||
let nextElements = toBrandedType<SceneElementsMap>(new Map(elements));
|
||||
let changedElements: Map<string, OrderedExcalidrawElement>;
|
||||
|
@ -1106,7 +1112,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||
try {
|
||||
const applyDeltas = ElementsDelta.createApplier(
|
||||
nextElements,
|
||||
snapshot,
|
||||
elementsSnapshot,
|
||||
flags,
|
||||
);
|
||||
|
||||
|
|
|
@ -159,7 +159,13 @@ export class History {
|
|||
entry: HistoryEntry,
|
||||
prevElements: SceneElementsMap,
|
||||
) {
|
||||
const updatedEntry = HistoryEntry.applyLatestChanges(entry, prevElements);
|
||||
const inversedEntry = HistoryEntry.inverse(entry);
|
||||
const updatedEntry = HistoryEntry.applyLatestChanges(
|
||||
inversedEntry,
|
||||
prevElements,
|
||||
"inserted",
|
||||
);
|
||||
|
||||
return stack.push(updatedEntry);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -474,14 +474,13 @@ export class StoreDelta {
|
|||
public static applyLatestChanges(
|
||||
delta: StoreDelta,
|
||||
elements: SceneElementsMap,
|
||||
modifierOptions: "deleted" | "inserted",
|
||||
): StoreDelta {
|
||||
const inversedDelta = this.inverse(delta);
|
||||
|
||||
return this.create(
|
||||
inversedDelta.elements.applyLatestChanges(elements),
|
||||
inversedDelta.appState,
|
||||
delta.elements.applyLatestChanges(elements, modifierOptions),
|
||||
delta.appState,
|
||||
{
|
||||
id: inversedDelta.id,
|
||||
id: delta.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { DTO } from "../utility-types";
|
|||
export type CLIENT_DELTA = DTO<StoreDelta>;
|
||||
export type CLIENT_CHANGE = DTO<StoreChange>;
|
||||
|
||||
export type RESTORE_PAYLOAD = {};
|
||||
export type RELAY_PAYLOAD = CLIENT_CHANGE;
|
||||
export type PUSH_PAYLOAD = CLIENT_DELTA;
|
||||
export type PULL_PAYLOAD = { lastAcknowledgedVersion: number };
|
||||
|
@ -15,6 +16,7 @@ export type CHUNK_INFO = {
|
|||
};
|
||||
|
||||
export type CLIENT_MESSAGE = (
|
||||
| { type: "restore"; payload: RESTORE_PAYLOAD }
|
||||
| { type: "relay"; payload: RELAY_PAYLOAD }
|
||||
| { type: "pull"; payload: PULL_PAYLOAD }
|
||||
| { type: "push"; payload: PUSH_PAYLOAD }
|
||||
|
@ -48,7 +50,8 @@ export type SERVER_MESSAGE =
|
|||
| {
|
||||
type: "rejected";
|
||||
payload: { deltas: Array<CLIENT_DELTA>; message: string };
|
||||
};
|
||||
}
|
||||
| { type: "restored"; payload: { elements: Array<ExcalidrawElement> } };
|
||||
|
||||
export interface DeltasRepository {
|
||||
save(delta: CLIENT_DELTA): SERVER_DELTA | null;
|
||||
|
|
|
@ -3,7 +3,6 @@ import { Network, Utils } from "./utils";
|
|||
|
||||
import type {
|
||||
DeltasRepository,
|
||||
CLIENT_MESSAGE,
|
||||
PULL_PAYLOAD,
|
||||
PUSH_PAYLOAD,
|
||||
SERVER_MESSAGE,
|
||||
|
@ -11,7 +10,10 @@ import type {
|
|||
CHUNK_INFO,
|
||||
RELAY_PAYLOAD,
|
||||
CLIENT_MESSAGE_BINARY,
|
||||
CLIENT_MESSAGE,
|
||||
ExcalidrawElement,
|
||||
} from "./protocol";
|
||||
import { StoreDelta } from "../store";
|
||||
|
||||
/**
|
||||
* Core excalidraw sync logic.
|
||||
|
@ -24,7 +26,22 @@ export class ExcalidrawSyncServer {
|
|||
Map<CHUNK_INFO["position"], CLIENT_MESSAGE_BINARY["payload"]>
|
||||
>();
|
||||
|
||||
constructor(private readonly repository: DeltasRepository) {}
|
||||
// CFDO II: load from the db
|
||||
private elements = new Map<string, ExcalidrawElement>();
|
||||
|
||||
constructor(private readonly repository: DeltasRepository) {
|
||||
// CFDO II: load from the db
|
||||
const deltas = this.repository.getAllSinceVersion(0);
|
||||
|
||||
for (const delta of deltas) {
|
||||
const storeDelta = StoreDelta.load(delta.payload);
|
||||
|
||||
// CFDO II: fix types (everywhere)
|
||||
const [nextElements] = storeDelta.elements.applyTo(this.elements as any);
|
||||
|
||||
this.elements = nextElements;
|
||||
}
|
||||
}
|
||||
|
||||
// CFDO: optimize, should send a message about collaborators (no collaborators => no need to send ephemerals)
|
||||
public onConnect(client: WebSocket) {
|
||||
|
@ -48,11 +65,12 @@ export class ExcalidrawSyncServer {
|
|||
return;
|
||||
}
|
||||
|
||||
const { type, payload, chunkInfo } = rawMessage;
|
||||
|
||||
// if there is chunkInfo, there are more than 1 chunks => process them first
|
||||
if (chunkInfo) {
|
||||
return this.processChunks(client, { type, payload, chunkInfo });
|
||||
if (rawMessage.chunkInfo) {
|
||||
return this.processChunks(client, {
|
||||
...rawMessage,
|
||||
chunkInfo: rawMessage.chunkInfo,
|
||||
});
|
||||
}
|
||||
|
||||
return this.processMessage(client, rawMessage);
|
||||
|
@ -132,6 +150,8 @@ export class ExcalidrawSyncServer {
|
|||
}
|
||||
|
||||
switch (type) {
|
||||
case "restore":
|
||||
return this.restore(client);
|
||||
case "relay":
|
||||
return this.relay(client, parsedPayload as RELAY_PAYLOAD);
|
||||
case "pull":
|
||||
|
@ -147,6 +167,15 @@ export class ExcalidrawSyncServer {
|
|||
}
|
||||
}
|
||||
|
||||
private restore(client: WebSocket) {
|
||||
return this.send(client, {
|
||||
type: "restored",
|
||||
payload: {
|
||||
elements: Array.from(this.elements.values()),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private relay(client: WebSocket, payload: RELAY_PAYLOAD) {
|
||||
// CFDO I: we should likely apply these to the snapshot
|
||||
return this.broadcast(
|
||||
|
@ -191,10 +220,38 @@ export class ExcalidrawSyncServer {
|
|||
}
|
||||
|
||||
private push(client: WebSocket, delta: PUSH_PAYLOAD) {
|
||||
// CFDO I: apply latest changes to delt & apply the deltas to the snapshot
|
||||
const [acknowledged, savingError] = Utils.try(() =>
|
||||
this.repository.save(delta),
|
||||
);
|
||||
const [storeDelta, applyingError] = Utils.try(() => {
|
||||
// update the "deleted" delta according to the latest changes (in case of concurrent changes)
|
||||
const storeDelta = StoreDelta.applyLatestChanges(
|
||||
StoreDelta.load(delta),
|
||||
this.elements as any,
|
||||
"deleted",
|
||||
);
|
||||
|
||||
// apply the delta to the elements snapshot
|
||||
const [nextElements] = storeDelta.elements.applyTo(this.elements as any);
|
||||
|
||||
this.elements = nextElements;
|
||||
|
||||
return storeDelta;
|
||||
});
|
||||
|
||||
if (applyingError) {
|
||||
// CFDO: everything should be automatically rolled-back in the db -> double-check
|
||||
return this.send(client, {
|
||||
type: "rejected",
|
||||
payload: {
|
||||
message: applyingError
|
||||
? applyingError.message
|
||||
: "Couldn't apply the delta.",
|
||||
deltas: [delta],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [acknowledged, savingError] = Utils.try(() => {
|
||||
return this.repository.save(storeDelta);
|
||||
});
|
||||
|
||||
if (savingError || !acknowledged) {
|
||||
// CFDO: everything should be automatically rolled-back in the db -> double-check
|
||||
|
@ -204,7 +261,7 @@ export class ExcalidrawSyncServer {
|
|||
message: savingError
|
||||
? savingError.message
|
||||
: "Coudn't persist the delta.",
|
||||
deltas: [delta],
|
||||
deltas: [storeDelta],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue