mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Switch from sqlite payload strings to buffers, utils refactor, dev logging
This commit is contained in:
parent
05ba0339fe
commit
49925038fd
11 changed files with 192 additions and 182 deletions
|
@ -1,11 +1,9 @@
|
|||
/* eslint-disable no-console */
|
||||
import throttle from "lodash.throttle";
|
||||
import msgpack from "msgpack-lite";
|
||||
import ReconnectingWebSocket, {
|
||||
type Event,
|
||||
type CloseEvent,
|
||||
} from "reconnecting-websocket";
|
||||
import { Utils } from "./utils";
|
||||
import { Network, Utils } from "./utils";
|
||||
import {
|
||||
LocalDeltasQueue,
|
||||
type MetadataRepository,
|
||||
|
@ -16,16 +14,17 @@ import type { StoreChange } from "../store";
|
|||
import type { ExcalidrawImperativeAPI } from "../types";
|
||||
import type { ExcalidrawElement, SceneElementsMap } from "../element/types";
|
||||
import type {
|
||||
CLIENT_MESSAGE_RAW,
|
||||
SERVER_DELTA,
|
||||
CHANGE,
|
||||
CLIENT_CHANGE,
|
||||
SERVER_MESSAGE,
|
||||
CLIENT_MESSAGE_BINARY,
|
||||
} from "./protocol";
|
||||
import { debounce } from "../utils";
|
||||
import { randomId } from "../random";
|
||||
import { orderByFractionalIndex } from "../fractionalIndex";
|
||||
import { ENV } from "../constants";
|
||||
|
||||
class SocketMessage implements CLIENT_MESSAGE_RAW {
|
||||
class SocketMessage implements CLIENT_MESSAGE_BINARY {
|
||||
constructor(
|
||||
public readonly type: "relay" | "pull" | "push",
|
||||
public readonly payload: Uint8Array,
|
||||
|
@ -77,6 +76,7 @@ class SocketClient {
|
|||
window.addEventListener("online", this.onOnline);
|
||||
window.addEventListener("offline", this.onOffline);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Connecting to the room "${this.roomId}"...`);
|
||||
this.socket = new ReconnectingWebSocket(
|
||||
`${this.host}/connect?roomId=${this.roomId}`,
|
||||
|
@ -103,7 +103,6 @@ class SocketClient {
|
|||
{ leading: true, trailing: false },
|
||||
);
|
||||
|
||||
// CFDO: the connections seem to keep hanging for some reason
|
||||
public disconnect() {
|
||||
if (this.isDisconnected) {
|
||||
return;
|
||||
|
@ -119,6 +118,7 @@ class SocketClient {
|
|||
this.socket?.removeEventListener("error", this.onError);
|
||||
this.socket?.close();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Disconnected from the room "${this.roomId}".`);
|
||||
} finally {
|
||||
this.socket = null;
|
||||
|
@ -135,7 +135,6 @@ class SocketClient {
|
|||
return;
|
||||
}
|
||||
|
||||
// CFDO: could be closed / closing / connecting
|
||||
if (this.isDisconnected) {
|
||||
this.connect();
|
||||
return;
|
||||
|
@ -143,10 +142,14 @@ class SocketClient {
|
|||
|
||||
const { type, payload } = message;
|
||||
|
||||
// CFDO II: could be slowish for large payloads, thing about a better solution (i.e. msgpack 10x faster, 2x smaller)
|
||||
const payloadBuffer = msgpack.encode(payload) as Uint8Array;
|
||||
const payloadBuffer = Network.toBinary(payload);
|
||||
const payloadSize = payloadBuffer.byteLength;
|
||||
|
||||
if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug("send", message, payloadSize);
|
||||
}
|
||||
|
||||
if (payloadSize < SocketClient.MAX_MESSAGE_SIZE) {
|
||||
const message = new SocketMessage(type, payloadBuffer);
|
||||
return this.sendMessage(message);
|
||||
|
@ -176,86 +179,55 @@ class SocketClient {
|
|||
return;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug("onMessage", message);
|
||||
}
|
||||
|
||||
this.handlers.onMessage(message);
|
||||
});
|
||||
};
|
||||
|
||||
private onOpen = (event: Event) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Connection to the room "${this.roomId}" opened.`);
|
||||
this.isOffline = false;
|
||||
this.handlers.onOpen(event);
|
||||
};
|
||||
|
||||
private onClose = (event: CloseEvent) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`Connection to the room "${this.roomId}" closed.`, event);
|
||||
};
|
||||
|
||||
private onError = (event: Event) => {
|
||||
console.debug(
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Connection to the room "${this.roomId}" returned an error.`,
|
||||
event,
|
||||
);
|
||||
};
|
||||
|
||||
private sendMessage = ({ payload, ...metadata }: CLIENT_MESSAGE_RAW) => {
|
||||
const metadataBuffer = msgpack.encode(metadata) as Uint8Array;
|
||||
|
||||
// contains the length of the rest of the message, so that we could decode it server side
|
||||
const headerBuffer = new ArrayBuffer(4);
|
||||
new DataView(headerBuffer).setUint32(0, metadataBuffer.byteLength);
|
||||
|
||||
// concatenate into [header(4 bytes)][metadata][payload]
|
||||
const message = Uint8Array.from([
|
||||
...new Uint8Array(headerBuffer),
|
||||
...metadataBuffer,
|
||||
...payload,
|
||||
]);
|
||||
|
||||
// CFDO: add dev-level logging
|
||||
{
|
||||
const headerLength = 4;
|
||||
const header = new Uint8Array(message.buffer, 0, headerLength);
|
||||
const metadataLength = new DataView(
|
||||
header.buffer,
|
||||
header.byteOffset,
|
||||
).getUint32(0);
|
||||
|
||||
const metadata = new Uint8Array(
|
||||
message.buffer,
|
||||
headerLength,
|
||||
headerLength + metadataLength,
|
||||
);
|
||||
|
||||
const payload = new Uint8Array(
|
||||
message.buffer,
|
||||
headerLength + metadataLength,
|
||||
);
|
||||
|
||||
console.log({
|
||||
...msgpack.decode(metadata),
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
this.socket?.send(message);
|
||||
private sendMessage = (message: CLIENT_MESSAGE_BINARY) => {
|
||||
this.socket?.send(Network.encodeClientMessage(message));
|
||||
};
|
||||
|
||||
// CFDO: should be (runtime) type-safe
|
||||
private async receiveMessage(
|
||||
message: Blob,
|
||||
): Promise<SERVER_MESSAGE | undefined> {
|
||||
const arrayBuffer = await message.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
const [decodedMessage, decodeError] = Utils.try<SERVER_MESSAGE>(() =>
|
||||
msgpack.decode(uint8Array),
|
||||
const [decodedMessage, decodingError] = Utils.try<SERVER_MESSAGE>(() =>
|
||||
Network.fromBinary(uint8Array),
|
||||
);
|
||||
|
||||
if (decodeError) {
|
||||
if (decodingError) {
|
||||
console.error("Failed to decode message:", message);
|
||||
return;
|
||||
}
|
||||
|
||||
// CFDO: should be type-safe
|
||||
return decodedMessage;
|
||||
}
|
||||
}
|
||||
|
@ -285,7 +257,7 @@ export class SyncClient {
|
|||
>();
|
||||
|
||||
// #region ACKNOWLEDGED DELTAS & METADATA
|
||||
// CFDO: shouldn't be stateful, only request / response
|
||||
// CFDO II: shouldn't be stateful, only request / response
|
||||
private readonly acknowledgedDeltasMap: Map<string, AcknowledgedDelta> =
|
||||
new Map();
|
||||
|
||||
|
@ -336,7 +308,7 @@ export class SyncClient {
|
|||
return new SyncClient(api, repository, queue, {
|
||||
host: SyncClient.HOST_URL,
|
||||
roomId: roomId ?? SyncClient.ROOM_ID,
|
||||
// CFDO: temporary, so that all deltas are loaded and applied on init
|
||||
// CFDO II: temporary, so that all deltas are loaded and applied on init
|
||||
lastAcknowledgedVersion: 0,
|
||||
});
|
||||
}
|
||||
|
@ -377,7 +349,7 @@ export class SyncClient {
|
|||
}
|
||||
}
|
||||
|
||||
// CFDO: should be throttled! 60 fps for live scenes, 10s or so for single player
|
||||
// CFDO: should be throttled! 16ms (60 fps) for live scenes, not needed at all for single player
|
||||
public relay(change: StoreChange): void {
|
||||
if (this.client.isDisconnected) {
|
||||
// don't reconnect if we're explicitly disconnected
|
||||
|
@ -414,7 +386,7 @@ export class SyncClient {
|
|||
|
||||
// #region PRIVATE SOCKET MESSAGE HANDLERS
|
||||
private onOpen = (event: Event) => {
|
||||
// CFDO: hack to pull everything for on init
|
||||
// CFDO II: hack to pull everything for on init
|
||||
this.pull(0);
|
||||
this.push();
|
||||
};
|
||||
|
@ -425,9 +397,8 @@ export class SyncClient {
|
|||
this.push();
|
||||
};
|
||||
|
||||
private onMessage = ({ type, payload }: SERVER_MESSAGE) => {
|
||||
// CFDO: add dev-level logging
|
||||
console.log({ type, payload });
|
||||
private onMessage = (serverMessage: SERVER_MESSAGE) => {
|
||||
const { type, payload } = serverMessage;
|
||||
|
||||
switch (type) {
|
||||
case "relayed":
|
||||
|
@ -441,8 +412,8 @@ export class SyncClient {
|
|||
}
|
||||
};
|
||||
|
||||
private handleRelayed = (payload: CHANGE) => {
|
||||
// CFDO: retrieve the map already
|
||||
private handleRelayed = (payload: CLIENT_CHANGE) => {
|
||||
// CFDO I: retrieve the map already
|
||||
const nextElements = new Map(
|
||||
this.api.getSceneElementsIncludingDeleted().map((el) => [el.id, el]),
|
||||
) as SceneElementsMap;
|
||||
|
@ -457,7 +428,6 @@ export class SyncClient {
|
|||
!existingElement || // new element
|
||||
existingElement.version < relayedElement.version // updated element
|
||||
) {
|
||||
// CFDO: in theory could make the yet unsynced element (due to a bug) to move to the top
|
||||
nextElements.set(id, relayedElement);
|
||||
this.relayedElementsVersionsCache.set(id, relayedElement.version);
|
||||
}
|
||||
|
@ -492,7 +462,7 @@ export class SyncClient {
|
|||
) as SceneElementsMap;
|
||||
|
||||
for (const { id, version, payload } of remoteDeltas) {
|
||||
// CFDO: temporary to load all deltas on init
|
||||
// CFDO II: temporary to load all deltas on init
|
||||
this.acknowledgedDeltasMap.set(id, {
|
||||
delta: StoreDelta.load(payload),
|
||||
version,
|
||||
|
@ -503,7 +473,7 @@ export class SyncClient {
|
|||
continue;
|
||||
}
|
||||
|
||||
// CFDO:strictly checking for out of order deltas; might be relaxed if it becomes a problem
|
||||
// CFDO: strictly checking for out of order deltas; might be relaxed if it becomes a problem
|
||||
if (version !== nextAcknowledgedVersion + 1) {
|
||||
throw new Error(
|
||||
`Received out of order delta, expected "${
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue