2020-12-05 20:00:53 +05:30
|
|
|
import {
|
|
|
|
encryptAESGEM,
|
|
|
|
SocketUpdateData,
|
|
|
|
SocketUpdateDataSource,
|
|
|
|
} from "../data";
|
|
|
|
|
|
|
|
import CollabWrapper from "./CollabWrapper";
|
2020-04-12 16:24:52 +05:30
|
|
|
|
2020-10-23 03:17:34 +05:30
|
|
|
import {
|
|
|
|
getElementMap,
|
|
|
|
getSyncableElements,
|
2020-12-05 20:00:53 +05:30
|
|
|
} from "../../packages/excalidraw/index";
|
|
|
|
import { ExcalidrawElement } from "../../element/types";
|
|
|
|
import { BROADCAST, SCENE } from "../app_constants";
|
2020-04-12 16:24:52 +05:30
|
|
|
|
|
|
|
class Portal {
|
2020-12-05 20:00:53 +05:30
|
|
|
app: CollabWrapper;
|
2020-04-12 16:24:52 +05:30
|
|
|
socket: SocketIOClient.Socket | null = null;
|
|
|
|
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
|
2020-11-29 18:32:51 +02:00
|
|
|
roomId: string | null = null;
|
2020-04-12 16:24:52 +05:30
|
|
|
roomKey: string | null = null;
|
2020-10-21 19:11:20 +05:30
|
|
|
broadcastedElementVersions: Map<string, number> = new Map();
|
2020-04-12 16:24:52 +05:30
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
constructor(app: CollabWrapper) {
|
2020-04-27 10:56:08 -07:00
|
|
|
this.app = app;
|
|
|
|
}
|
|
|
|
|
2020-04-12 16:24:52 +05:30
|
|
|
open(socket: SocketIOClient.Socket, id: string, key: string) {
|
|
|
|
this.socket = socket;
|
2020-11-29 18:32:51 +02:00
|
|
|
this.roomId = id;
|
2020-04-12 16:24:52 +05:30
|
|
|
this.roomKey = key;
|
2020-04-27 10:56:08 -07:00
|
|
|
|
|
|
|
// Initialize socket listeners (moving from App)
|
2020-04-28 09:49:00 -07:00
|
|
|
this.socket.on("init-room", () => {
|
2020-04-27 10:56:08 -07:00
|
|
|
if (this.socket) {
|
2020-11-29 18:32:51 +02:00
|
|
|
this.socket.emit("join-room", this.roomId);
|
2020-04-27 10:56:08 -07:00
|
|
|
}
|
|
|
|
});
|
2020-08-14 20:14:22 +02:00
|
|
|
this.socket.on("new-user", async (_socketId: string) => {
|
2020-12-05 20:00:53 +05:30
|
|
|
this.broadcastScene(
|
|
|
|
SCENE.INIT,
|
|
|
|
getSyncableElements(this.app.getSceneElementsIncludingDeleted()),
|
|
|
|
/* syncAll */ true,
|
|
|
|
);
|
2020-04-27 10:56:08 -07:00
|
|
|
});
|
2020-04-28 09:49:00 -07:00
|
|
|
this.socket.on("room-user-change", (clients: string[]) => {
|
|
|
|
this.app.setCollaborators(clients);
|
|
|
|
});
|
2020-04-12 16:24:52 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
if (!this.socket) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.socket.close();
|
|
|
|
this.socket = null;
|
2020-11-29 18:32:51 +02:00
|
|
|
this.roomId = null;
|
2020-04-12 16:24:52 +05:30
|
|
|
this.roomKey = null;
|
2020-11-09 15:34:26 +01:00
|
|
|
this.socketInitialized = false;
|
|
|
|
this.broadcastedElementVersions = new Map();
|
2020-04-12 16:24:52 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
isOpen() {
|
|
|
|
return !!(
|
|
|
|
this.socketInitialized &&
|
|
|
|
this.socket &&
|
2020-11-29 18:32:51 +02:00
|
|
|
this.roomId &&
|
2020-04-12 16:24:52 +05:30
|
|
|
this.roomKey
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async _broadcastSocketData(
|
|
|
|
data: SocketUpdateData,
|
|
|
|
volatile: boolean = false,
|
|
|
|
) {
|
|
|
|
if (this.isOpen()) {
|
|
|
|
const json = JSON.stringify(data);
|
|
|
|
const encoded = new TextEncoder().encode(json);
|
|
|
|
const encrypted = await encryptAESGEM(encoded, this.roomKey!);
|
|
|
|
this.socket!.emit(
|
|
|
|
volatile ? BROADCAST.SERVER_VOLATILE : BROADCAST.SERVER,
|
2020-11-29 18:32:51 +02:00
|
|
|
this.roomId,
|
2020-04-12 16:24:52 +05:30
|
|
|
encrypted.data,
|
|
|
|
encrypted.iv,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2020-10-21 19:11:20 +05:30
|
|
|
|
|
|
|
broadcastScene = async (
|
|
|
|
sceneType: SCENE.INIT | SCENE.UPDATE,
|
2020-12-05 20:00:53 +05:30
|
|
|
syncableElements: ExcalidrawElement[],
|
2020-10-21 19:11:20 +05:30
|
|
|
syncAll: boolean,
|
|
|
|
) => {
|
|
|
|
if (sceneType === SCENE.INIT && !syncAll) {
|
|
|
|
throw new Error("syncAll must be true when sending SCENE.INIT");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!syncAll) {
|
|
|
|
// sync out only the elements we think we need to to save bandwidth.
|
|
|
|
// periodically we'll resync the whole thing to make sure no one diverges
|
|
|
|
// due to a dropped message (server goes down etc).
|
|
|
|
syncableElements = syncableElements.filter(
|
|
|
|
(syncableElement) =>
|
|
|
|
!this.broadcastedElementVersions.has(syncableElement.id) ||
|
|
|
|
syncableElement.version >
|
|
|
|
this.broadcastedElementVersions.get(syncableElement.id)!,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const data: SocketUpdateDataSource[typeof sceneType] = {
|
|
|
|
type: sceneType,
|
|
|
|
payload: {
|
|
|
|
elements: syncableElements,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
for (const syncableElement of syncableElements) {
|
|
|
|
this.broadcastedElementVersions.set(
|
|
|
|
syncableElement.id,
|
|
|
|
syncableElement.version,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const broadcastPromise = this._broadcastSocketData(
|
|
|
|
data as SocketUpdateData,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (syncAll && this.app.state.isCollaborating) {
|
|
|
|
await Promise.all([
|
|
|
|
broadcastPromise,
|
|
|
|
this.app.saveCollabRoomToFirebase(syncableElements),
|
|
|
|
]);
|
|
|
|
} else {
|
|
|
|
await broadcastPromise;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
broadcastMouseLocation = (payload: {
|
|
|
|
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
|
|
|
|
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
|
|
|
|
}) => {
|
|
|
|
if (this.socket?.id) {
|
|
|
|
const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
|
|
|
|
type: "MOUSE_LOCATION",
|
|
|
|
payload: {
|
|
|
|
socketId: this.socket.id,
|
|
|
|
pointer: payload.pointer,
|
|
|
|
button: payload.button || "up",
|
2020-12-05 20:00:53 +05:30
|
|
|
selectedElementIds:
|
|
|
|
this.app.excalidrawAppState?.selectedElementIds || {},
|
2020-10-21 19:11:20 +05:30
|
|
|
username: this.app.state.username,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
return this._broadcastSocketData(
|
|
|
|
data as SocketUpdateData,
|
|
|
|
true, // volatile
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
2020-10-23 03:17:34 +05:30
|
|
|
|
2020-12-05 20:00:53 +05:30
|
|
|
reconcileElements = (
|
|
|
|
sceneElements: readonly ExcalidrawElement[],
|
|
|
|
): readonly ExcalidrawElement[] => {
|
2020-10-23 03:17:34 +05:30
|
|
|
const currentElements = this.app.getSceneElementsIncludingDeleted();
|
|
|
|
// create a map of ids so we don't have to iterate
|
|
|
|
// over the array more than once.
|
|
|
|
const localElementMap = getElementMap(currentElements);
|
|
|
|
|
|
|
|
// Reconcile
|
2020-12-05 20:00:53 +05:30
|
|
|
return (
|
|
|
|
sceneElements
|
|
|
|
.reduce((elements, element) => {
|
|
|
|
// if the remote element references one that's currently
|
|
|
|
// edited on local, skip it (it'll be added in the next step)
|
|
|
|
if (
|
|
|
|
element.id === this.app.excalidrawAppState?.editingElement?.id ||
|
|
|
|
element.id === this.app.excalidrawAppState?.resizingElement?.id ||
|
|
|
|
element.id === this.app.excalidrawAppState?.draggingElement?.id
|
|
|
|
) {
|
|
|
|
return elements;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
localElementMap.hasOwnProperty(element.id) &&
|
|
|
|
localElementMap[element.id].version > element.version
|
|
|
|
) {
|
2020-10-23 03:17:34 +05:30
|
|
|
elements.push(localElementMap[element.id]);
|
2020-12-05 20:00:53 +05:30
|
|
|
delete localElementMap[element.id];
|
|
|
|
} else if (
|
|
|
|
localElementMap.hasOwnProperty(element.id) &&
|
|
|
|
localElementMap[element.id].version === element.version &&
|
|
|
|
localElementMap[element.id].versionNonce !== element.versionNonce
|
|
|
|
) {
|
|
|
|
// resolve conflicting edits deterministically by taking the one with the lowest versionNonce
|
|
|
|
if (
|
|
|
|
localElementMap[element.id].versionNonce < element.versionNonce
|
|
|
|
) {
|
|
|
|
elements.push(localElementMap[element.id]);
|
|
|
|
} else {
|
|
|
|
// it should be highly unlikely that the two versionNonces are the same. if we are
|
|
|
|
// really worried about this, we can replace the versionNonce with the socket id.
|
|
|
|
elements.push(element);
|
|
|
|
}
|
|
|
|
delete localElementMap[element.id];
|
2020-10-23 03:17:34 +05:30
|
|
|
} else {
|
|
|
|
elements.push(element);
|
2020-12-05 20:00:53 +05:30
|
|
|
delete localElementMap[element.id];
|
2020-10-23 03:17:34 +05:30
|
|
|
}
|
2020-12-05 20:00:53 +05:30
|
|
|
|
|
|
|
return elements;
|
|
|
|
}, [] as Mutable<typeof sceneElements>)
|
|
|
|
// add local elements that weren't deleted or on remote
|
|
|
|
.concat(...Object.values(localElementMap))
|
|
|
|
);
|
2020-10-23 03:17:34 +05:30
|
|
|
};
|
2020-04-12 16:24:52 +05:30
|
|
|
}
|
2020-10-21 19:11:20 +05:30
|
|
|
|
2020-04-12 16:24:52 +05:30
|
|
|
export default Portal;
|