mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Ditching strings and exchanging buffers
This commit is contained in:
parent
cdd7f6158b
commit
05ba0339fe
8 changed files with 258 additions and 74 deletions
|
@ -31,6 +31,7 @@ export class DurableDeltasRepository implements DeltasRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// CFDO: could be also a buffer
|
||||||
const payload = JSON.stringify(delta);
|
const payload = JSON.stringify(delta);
|
||||||
const payloadSize = new TextEncoder().encode(payload).byteLength;
|
const payloadSize = new TextEncoder().encode(payload).byteLength;
|
||||||
const nextVersion = this.getLastVersion() + 1;
|
const nextVersion = this.getLastVersion() + 1;
|
||||||
|
@ -113,6 +114,7 @@ export class DurableDeltasRepository implements DeltasRepository {
|
||||||
return restoredDeltas[0];
|
return restoredDeltas[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CFDO: fix types (should be buffer in the first place)
|
||||||
private restoreServerDeltas(deltas: SERVER_DELTA[]): SERVER_DELTA[] {
|
private restoreServerDeltas(deltas: SERVER_DELTA[]): SERVER_DELTA[] {
|
||||||
return Array.from(
|
return Array.from(
|
||||||
deltas
|
deltas
|
||||||
|
@ -137,6 +139,10 @@ export class DurableDeltasRepository implements DeltasRepository {
|
||||||
return acc;
|
return acc;
|
||||||
}, new Map<number, SERVER_DELTA>())
|
}, new Map<number, SERVER_DELTA>())
|
||||||
.values(),
|
.values(),
|
||||||
);
|
// CFDO: temporary
|
||||||
|
).map((delta) => ({
|
||||||
|
...delta,
|
||||||
|
payload: JSON.parse(delta.payload),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export class DurableRoom extends DurableObject {
|
||||||
public fetch = async (request: Request): Promise<Response> =>
|
public fetch = async (request: Request): Promise<Response> =>
|
||||||
this.connect(request);
|
this.connect(request);
|
||||||
|
|
||||||
public webSocketMessage = (client: WebSocket, message: string) =>
|
public webSocketMessage = (client: WebSocket, message: ArrayBuffer) =>
|
||||||
this.sync.onMessage(client, message);
|
this.sync.onMessage(client, message);
|
||||||
|
|
||||||
public webSocketClose = (ws: WebSocket) => this.sync.onDisconnect(ws);
|
public webSocketClose = (ws: WebSocket) => this.sync.onDisconnect(ws);
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
"jotai-scope": "0.7.2",
|
"jotai-scope": "0.7.2",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"lodash.throttle": "4.1.1",
|
"lodash.throttle": "4.1.1",
|
||||||
|
"msgpack-lite": "0.1.26",
|
||||||
"nanoid": "3.3.3",
|
"nanoid": "3.3.3",
|
||||||
"open-color": "1.9.1",
|
"open-color": "1.9.1",
|
||||||
"pako": "1.0.11",
|
"pako": "1.0.11",
|
||||||
|
@ -106,6 +107,7 @@
|
||||||
"@testing-library/jest-dom": "5.16.2",
|
"@testing-library/jest-dom": "5.16.2",
|
||||||
"@testing-library/react": "16.0.0",
|
"@testing-library/react": "16.0.0",
|
||||||
"@types/async-lock": "^1.4.2",
|
"@types/async-lock": "^1.4.2",
|
||||||
|
"@types/msgpack-lite": "0.1.11",
|
||||||
"@types/pako": "1.0.3",
|
"@types/pako": "1.0.3",
|
||||||
"@types/pica": "5.1.3",
|
"@types/pica": "5.1.3",
|
||||||
"@types/resize-observer-browser": "0.1.7",
|
"@types/resize-observer-browser": "0.1.7",
|
||||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
|
import type { SERVER_DELTA } from "./sync/protocol";
|
||||||
import { arrayToMap, assertNever } from "./utils";
|
import { arrayToMap, assertNever } from "./utils";
|
||||||
import { hashElementsVersion } from "./element";
|
import { hashElementsVersion } from "./element";
|
||||||
import { syncMovedIndices } from "./fractionalIndex";
|
import { syncMovedIndices } from "./fractionalIndex";
|
||||||
|
@ -449,14 +450,11 @@ export class StoreDelta {
|
||||||
/**
|
/**
|
||||||
* Parse and load the delta from the remote payload.
|
* Parse and load the delta from the remote payload.
|
||||||
*/
|
*/
|
||||||
// CFDO: why it would be a string if it can be a DTO?
|
public static load({
|
||||||
public static load(payload: string) {
|
|
||||||
// CFDO: ensure typesafety
|
|
||||||
const {
|
|
||||||
id,
|
id,
|
||||||
elements: { added, removed, updated },
|
elements: { added, removed, updated },
|
||||||
} = JSON.parse(payload);
|
}: SERVER_DELTA["payload"]) {
|
||||||
|
// CFDO: ensure typesafety
|
||||||
const elements = ElementsDelta.create(added, removed, updated, {
|
const elements = ElementsDelta.create(added, removed, updated, {
|
||||||
shouldRedistribute: false,
|
shouldRedistribute: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
|
import msgpack from "msgpack-lite";
|
||||||
import ReconnectingWebSocket, {
|
import ReconnectingWebSocket, {
|
||||||
type Event,
|
type Event,
|
||||||
type CloseEvent,
|
type CloseEvent,
|
||||||
|
@ -14,7 +15,12 @@ import { StoreAction, StoreDelta } from "../store";
|
||||||
import type { StoreChange } from "../store";
|
import type { StoreChange } from "../store";
|
||||||
import type { ExcalidrawImperativeAPI } from "../types";
|
import type { ExcalidrawImperativeAPI } from "../types";
|
||||||
import type { ExcalidrawElement, SceneElementsMap } from "../element/types";
|
import type { ExcalidrawElement, SceneElementsMap } from "../element/types";
|
||||||
import type { CLIENT_MESSAGE_RAW, SERVER_DELTA, CHANGE } from "./protocol";
|
import type {
|
||||||
|
CLIENT_MESSAGE_RAW,
|
||||||
|
SERVER_DELTA,
|
||||||
|
CHANGE,
|
||||||
|
SERVER_MESSAGE,
|
||||||
|
} from "./protocol";
|
||||||
import { debounce } from "../utils";
|
import { debounce } from "../utils";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { orderByFractionalIndex } from "../fractionalIndex";
|
import { orderByFractionalIndex } from "../fractionalIndex";
|
||||||
|
@ -22,7 +28,7 @@ import { orderByFractionalIndex } from "../fractionalIndex";
|
||||||
class SocketMessage implements CLIENT_MESSAGE_RAW {
|
class SocketMessage implements CLIENT_MESSAGE_RAW {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly type: "relay" | "pull" | "push",
|
public readonly type: "relay" | "pull" | "push",
|
||||||
public readonly payload: string,
|
public readonly payload: Uint8Array,
|
||||||
public readonly chunkInfo?: {
|
public readonly chunkInfo?: {
|
||||||
id: string;
|
id: string;
|
||||||
position: number;
|
position: number;
|
||||||
|
@ -49,7 +55,7 @@ class SocketClient {
|
||||||
private readonly handlers: {
|
private readonly handlers: {
|
||||||
onOpen: (event: Event) => void;
|
onOpen: (event: Event) => void;
|
||||||
onOnline: () => void;
|
onOnline: () => void;
|
||||||
onMessage: (event: MessageEvent) => void;
|
onMessage: (message: SERVER_MESSAGE) => void;
|
||||||
},
|
},
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -138,12 +144,12 @@ class SocketClient {
|
||||||
const { type, payload } = message;
|
const { type, payload } = message;
|
||||||
|
|
||||||
// CFDO II: could be slowish for large payloads, thing about a better solution (i.e. msgpack 10x faster, 2x smaller)
|
// CFDO II: could be slowish for large payloads, thing about a better solution (i.e. msgpack 10x faster, 2x smaller)
|
||||||
const stringifiedPayload = JSON.stringify(payload);
|
const payloadBuffer = msgpack.encode(payload) as Uint8Array;
|
||||||
const payloadSize = new TextEncoder().encode(stringifiedPayload).byteLength;
|
const payloadSize = payloadBuffer.byteLength;
|
||||||
|
|
||||||
if (payloadSize < SocketClient.MAX_MESSAGE_SIZE) {
|
if (payloadSize < SocketClient.MAX_MESSAGE_SIZE) {
|
||||||
const message = new SocketMessage(type, stringifiedPayload);
|
const message = new SocketMessage(type, payloadBuffer);
|
||||||
return this.socket?.send(JSON.stringify(message));
|
return this.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunkId = randomId();
|
const chunkId = randomId();
|
||||||
|
@ -153,19 +159,25 @@ class SocketClient {
|
||||||
for (let position = 0; position < chunksCount; position++) {
|
for (let position = 0; position < chunksCount; position++) {
|
||||||
const start = position * chunkSize;
|
const start = position * chunkSize;
|
||||||
const end = start + chunkSize;
|
const end = start + chunkSize;
|
||||||
const chunkedPayload = stringifiedPayload.slice(start, end);
|
const chunkedPayload = payloadBuffer.subarray(start, end);
|
||||||
const message = new SocketMessage(type, chunkedPayload, {
|
const message = new SocketMessage(type, chunkedPayload, {
|
||||||
id: chunkId,
|
id: chunkId,
|
||||||
position,
|
position,
|
||||||
count: chunksCount,
|
count: chunksCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket?.send(JSON.stringify(message));
|
this.sendMessage(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMessage = (event: MessageEvent) => {
|
private onMessage = (event: MessageEvent) => {
|
||||||
this.handlers.onMessage(event);
|
this.receiveMessage(event.data).then((message) => {
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlers.onMessage(message);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onOpen = (event: Event) => {
|
private onOpen = (event: Event) => {
|
||||||
|
@ -184,6 +196,68 @@ class SocketClient {
|
||||||
event,
|
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 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),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (decodeError) {
|
||||||
|
console.error("Failed to decode message:", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CFDO: should be type-safe
|
||||||
|
return decodedMessage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AcknowledgedDelta {
|
interface AcknowledgedDelta {
|
||||||
|
@ -351,23 +425,17 @@ export class SyncClient {
|
||||||
this.push();
|
this.push();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMessage = (event: MessageEvent) => {
|
private onMessage = ({ type, payload }: SERVER_MESSAGE) => {
|
||||||
// CFDO: could be an array buffer
|
// CFDO: add dev-level logging
|
||||||
const [result, error] = Utils.try(() => JSON.parse(event.data as string));
|
console.log({ type, payload });
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Failed to parse message:", event.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { type, payload } = result;
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "relayed":
|
case "relayed":
|
||||||
return this.handleRelayed(payload);
|
return this.handleRelayed(payload);
|
||||||
case "acknowledged":
|
case "acknowledged":
|
||||||
return this.handleAcknowledged(payload);
|
return this.handleAcknowledged(payload);
|
||||||
case "rejected":
|
// case "rejected":
|
||||||
return this.handleRejected(payload);
|
// return this.handleRejected(payload);
|
||||||
default:
|
default:
|
||||||
console.error("Unknown message type:", type);
|
console.error("Unknown message type:", type);
|
||||||
}
|
}
|
||||||
|
@ -499,7 +567,7 @@ export class SyncClient {
|
||||||
|
|
||||||
private handleRejected = (payload: {
|
private handleRejected = (payload: {
|
||||||
ids: Array<string>;
|
ids: Array<string>;
|
||||||
message: string;
|
message: Uint8Array;
|
||||||
}) => {
|
}) => {
|
||||||
// handle rejected deltas
|
// handle rejected deltas
|
||||||
console.error("Rejected message received:", payload);
|
console.error("Rejected message received:", payload);
|
||||||
|
|
|
@ -16,7 +16,7 @@ export type CHUNK_INFO = {
|
||||||
|
|
||||||
export type CLIENT_MESSAGE_RAW = {
|
export type CLIENT_MESSAGE_RAW = {
|
||||||
type: "relay" | "pull" | "push";
|
type: "relay" | "pull" | "push";
|
||||||
payload: string;
|
payload: Uint8Array;
|
||||||
chunkInfo?: CHUNK_INFO;
|
chunkInfo?: CHUNK_INFO;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,7 +26,12 @@ export type CLIENT_MESSAGE = { chunkInfo: CHUNK_INFO } & (
|
||||||
| { type: "push"; payload: PUSH_PAYLOAD }
|
| { type: "push"; payload: PUSH_PAYLOAD }
|
||||||
);
|
);
|
||||||
|
|
||||||
export type SERVER_DELTA = { id: string; version: number; payload: string };
|
export type SERVER_DELTA = {
|
||||||
|
id: string;
|
||||||
|
version: number;
|
||||||
|
// CFDO: should be type-safe
|
||||||
|
payload: Record<string, any>;
|
||||||
|
};
|
||||||
export type SERVER_MESSAGE =
|
export type SERVER_MESSAGE =
|
||||||
| {
|
| {
|
||||||
type: "relayed";
|
type: "relayed";
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import AsyncLock from "async-lock";
|
import AsyncLock from "async-lock";
|
||||||
|
import msgpack from "msgpack-lite";
|
||||||
import { Utils } from "./utils";
|
import { Utils } from "./utils";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -37,10 +38,35 @@ export class ExcalidrawSyncServer {
|
||||||
this.sessions.delete(client);
|
this.sessions.delete(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onMessage(client: WebSocket, message: string): Promise<void> | void {
|
public onMessage(
|
||||||
|
client: WebSocket,
|
||||||
|
message: ArrayBuffer,
|
||||||
|
): Promise<void> | void {
|
||||||
const [parsedMessage, parseMessageError] = Utils.try<CLIENT_MESSAGE_RAW>(
|
const [parsedMessage, parseMessageError] = Utils.try<CLIENT_MESSAGE_RAW>(
|
||||||
() => {
|
() => {
|
||||||
return JSON.parse(message);
|
const headerLength = 4;
|
||||||
|
const header = new Uint8Array(message, 0, headerLength);
|
||||||
|
const metadataLength = new DataView(
|
||||||
|
header.buffer,
|
||||||
|
header.byteOffset,
|
||||||
|
).getUint32(0);
|
||||||
|
|
||||||
|
const metadata = new Uint8Array(
|
||||||
|
message,
|
||||||
|
headerLength,
|
||||||
|
headerLength + metadataLength,
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = new Uint8Array(message, headerLength + metadataLength);
|
||||||
|
const parsed = {
|
||||||
|
...msgpack.decode(metadata),
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// CFDO: add dev-level logging
|
||||||
|
console.log({ parsed });
|
||||||
|
|
||||||
|
return parsed;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -56,29 +82,7 @@ export class ExcalidrawSyncServer {
|
||||||
return this.processChunks(client, { type, payload, chunkInfo });
|
return this.processChunks(client, { type, payload, chunkInfo });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [parsedPayload, parsePayloadError] = Utils.try<
|
return this.processMessage(client, parsedMessage);
|
||||||
CLIENT_MESSAGE["payload"]
|
|
||||||
>(() => JSON.parse(payload));
|
|
||||||
|
|
||||||
if (parsePayloadError) {
|
|
||||||
console.error(parsePayloadError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "relay":
|
|
||||||
return this.relay(client, parsedPayload as RELAY_PAYLOAD);
|
|
||||||
case "pull":
|
|
||||||
return this.pull(client, parsedPayload as PULL_PAYLOAD);
|
|
||||||
case "push":
|
|
||||||
// apply each one-by-one to avoid race conditions
|
|
||||||
// CFDO: in theory we do not need to block ephemeral appState changes
|
|
||||||
return this.lock.acquire("push", () =>
|
|
||||||
this.push(client, parsedPayload as PUSH_PAYLOAD),
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
console.error(`Unknown message type: ${type}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,16 +122,18 @@ export class ExcalidrawSyncServer {
|
||||||
|
|
||||||
// hopefully we can fit into the 128 MiB memory limit
|
// hopefully we can fit into the 128 MiB memory limit
|
||||||
const restoredPayload = Array.from(chunks)
|
const restoredPayload = Array.from(chunks)
|
||||||
.sort((a, b) => (a <= b ? -1 : 1))
|
.sort(([positionA], [positionB]) => (positionA <= positionB ? -1 : 1))
|
||||||
.reduce((acc, [_, payload]) => (acc += payload), "");
|
.reduce(
|
||||||
|
(acc, [_, payload]) => Uint8Array.from([...acc, ...payload]),
|
||||||
|
new Uint8Array(),
|
||||||
|
);
|
||||||
|
|
||||||
const rawMessage = JSON.stringify({
|
const rawMessage = {
|
||||||
type,
|
type,
|
||||||
payload: restoredPayload,
|
payload: restoredPayload,
|
||||||
} as CLIENT_MESSAGE_RAW);
|
};
|
||||||
|
|
||||||
// process the message
|
return this.processMessage(client, rawMessage);
|
||||||
return this.onMessage(client, rawMessage);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error while processing chunk "${id}"`, error);
|
console.error(`Error while processing chunk "${id}"`, error);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -138,6 +144,35 @@ export class ExcalidrawSyncServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private processMessage(
|
||||||
|
client: WebSocket,
|
||||||
|
{ type, payload }: Omit<CLIENT_MESSAGE_RAW, "chunkInfo">,
|
||||||
|
) {
|
||||||
|
const [parsedPayload, parsePayloadError] = Utils.try<
|
||||||
|
CLIENT_MESSAGE["payload"]
|
||||||
|
>(() => msgpack.decode(payload));
|
||||||
|
|
||||||
|
if (parsePayloadError) {
|
||||||
|
console.error(parsePayloadError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "relay":
|
||||||
|
return this.relay(client, parsedPayload as RELAY_PAYLOAD);
|
||||||
|
case "pull":
|
||||||
|
return this.pull(client, parsedPayload as PULL_PAYLOAD);
|
||||||
|
case "push":
|
||||||
|
// apply each one-by-one to avoid race conditions
|
||||||
|
// CFDO: in theory we do not need to block ephemeral appState changes
|
||||||
|
return this.lock.acquire("push", () =>
|
||||||
|
this.push(client, parsedPayload as PUSH_PAYLOAD),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
console.error(`Unknown message type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private relay(client: WebSocket, payload: RELAY_PAYLOAD) {
|
private relay(client: WebSocket, payload: RELAY_PAYLOAD) {
|
||||||
// CFDO: we should likely apply these to the snapshot
|
// CFDO: we should likely apply these to the snapshot
|
||||||
return this.broadcast(
|
return this.broadcast(
|
||||||
|
@ -205,19 +240,34 @@ export class ExcalidrawSyncServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private send(client: WebSocket, message: SERVER_MESSAGE) {
|
private send(client: WebSocket, message: SERVER_MESSAGE) {
|
||||||
const msg = JSON.stringify(message);
|
const [encodedMessage, encodeError] = Utils.try<Uint8Array>(() =>
|
||||||
client.send(msg);
|
msgpack.encode(message),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (encodeError) {
|
||||||
|
console.error(encodeError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.send(encodedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private broadcast(message: SERVER_MESSAGE, exclude?: WebSocket) {
|
private broadcast(message: SERVER_MESSAGE, exclude?: WebSocket) {
|
||||||
const msg = JSON.stringify(message);
|
const [encodedMessage, encodeError] = Utils.try<Uint8Array>(() =>
|
||||||
|
msgpack.encode(message),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (encodeError) {
|
||||||
|
console.error(encodeError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const ws of this.sessions) {
|
for (const ws of this.sessions) {
|
||||||
if (ws === exclude) {
|
if (ws === exclude) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.send(msg);
|
ws.send(encodedMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
67
yarn.lock
67
yarn.lock
|
@ -3412,6 +3412,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
|
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
|
||||||
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
|
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
|
||||||
|
|
||||||
|
"@types/msgpack-lite@0.1.11":
|
||||||
|
version "0.1.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/msgpack-lite/-/msgpack-lite-0.1.11.tgz#f618e1fc469577f65f36c474ff3309407afef174"
|
||||||
|
integrity sha512-cdCZS/gw+jIN22I4SUZUFf1ZZfVv5JM1//Br/MuZcI373sxiy3eSSoiyLu0oz+BPatTbGGGBO5jrcvd0siCdTQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/node-forge@^1.3.0":
|
"@types/node-forge@^1.3.0":
|
||||||
version "1.3.11"
|
version "1.3.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da"
|
resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da"
|
||||||
|
@ -6440,6 +6447,11 @@ esutils@^2.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||||
|
|
||||||
|
event-lite@^0.1.1:
|
||||||
|
version "0.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.3.tgz#3dfe01144e808ac46448f0c19b4ab68e403a901d"
|
||||||
|
integrity sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==
|
||||||
|
|
||||||
eventemitter3@^4.0.0:
|
eventemitter3@^4.0.0:
|
||||||
version "4.0.7"
|
version "4.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||||
|
@ -7158,7 +7170,7 @@ idb@^7.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
|
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
|
||||||
integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==
|
integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==
|
||||||
|
|
||||||
ieee754@^1.1.13:
|
ieee754@^1.1.13, ieee754@^1.1.8:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||||
|
@ -7234,6 +7246,11 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
|
int64-buffer@^0.1.9:
|
||||||
|
version "0.1.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.1.10.tgz#277b228a87d95ad777d07c13832022406a473423"
|
||||||
|
integrity sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==
|
||||||
|
|
||||||
internal-slot@^1.0.7:
|
internal-slot@^1.0.7:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
|
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"
|
||||||
|
@ -7496,6 +7513,11 @@ is-weakset@^2.0.3:
|
||||||
call-bind "^1.0.7"
|
call-bind "^1.0.7"
|
||||||
get-intrinsic "^1.2.4"
|
get-intrinsic "^1.2.4"
|
||||||
|
|
||||||
|
isarray@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||||
|
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
|
||||||
|
|
||||||
isarray@^2.0.5:
|
isarray@^2.0.5:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||||
|
@ -8515,6 +8537,16 @@ ms@^2.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
|
||||||
|
msgpack-lite@0.1.26:
|
||||||
|
version "0.1.26"
|
||||||
|
resolved "https://registry.yarnpkg.com/msgpack-lite/-/msgpack-lite-0.1.26.tgz#dd3c50b26f059f25e7edee3644418358e2a9ad89"
|
||||||
|
integrity sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw==
|
||||||
|
dependencies:
|
||||||
|
event-lite "^0.1.1"
|
||||||
|
ieee754 "^1.1.8"
|
||||||
|
int64-buffer "^0.1.9"
|
||||||
|
isarray "^1.0.0"
|
||||||
|
|
||||||
multimath@^2.0.0:
|
multimath@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/multimath/-/multimath-2.0.0.tgz#0d37acf67c328f30e3d8c6b0d3209e6082710302"
|
resolved "https://registry.yarnpkg.com/multimath/-/multimath-2.0.0.tgz#0d37acf67c328f30e3d8c6b0d3209e6082710302"
|
||||||
|
@ -10089,8 +10121,7 @@ string-natural-compare@^3.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3:
|
"string-width-cjs@npm:string-width@^4.2.0":
|
||||||
name string-width-cjs
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
@ -10108,6 +10139,15 @@ string-width@^4.1.0, string-width@^4.2.0:
|
||||||
is-fullwidth-code-point "^3.0.0"
|
is-fullwidth-code-point "^3.0.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
|
string-width@^4.2.3:
|
||||||
|
version "4.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
|
dependencies:
|
||||||
|
emoji-regex "^8.0.0"
|
||||||
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
strip-ansi "^6.0.1"
|
||||||
|
|
||||||
string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2:
|
string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2:
|
||||||
version "5.1.2"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
||||||
|
@ -10179,7 +10219,14 @@ stringify-object@^3.3.0:
|
||||||
is-obj "^1.0.1"
|
is-obj "^1.0.1"
|
||||||
is-regexp "^1.0.0"
|
is-regexp "^1.0.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||||
|
version "6.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
dependencies:
|
||||||
|
ansi-regex "^5.0.1"
|
||||||
|
|
||||||
|
strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
@ -11481,8 +11528,7 @@ wrangler@^3.60.3:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||||
name wrap-ansi-cjs
|
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
@ -11500,6 +11546,15 @@ wrap-ansi@^6.2.0:
|
||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
|
wrap-ansi@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.0.0"
|
||||||
|
string-width "^4.1.0"
|
||||||
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
wrap-ansi@^8.1.0:
|
wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue