diff --git a/package-lock.json b/package-lock.json index b2424a4d..f7d2eba3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2017,6 +2017,21 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==" }, + "@types/lodash": { + "version": "4.14.150", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz", + "integrity": "sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w==", + "dev": true + }, + "@types/lodash.throttle": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.6.tgz", + "integrity": "sha512-/UIH96i/sIRYGC60NoY72jGkCJtFN5KVPhEMMMTjol65effe1gPn0tycJqV5tlSwMTzX8FqzB5yAj0rfGHTPNg==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -9635,6 +9650,11 @@ "lodash._reinterpolate": "^3.0.0" } }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", diff --git a/package.json b/package.json index b4946530..76fe6dc7 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,18 @@ "dependencies": { "@sentry/browser": "5.15.5", "@sentry/integrations": "5.15.5", + "@testing-library/jest-dom": "5.5.0", + "@testing-library/react": "10.0.4", + "@types/jest": "25.2.1", + "@types/nanoid": "2.1.0", + "@types/react": "16.9.34", + "@types/react-dom": "16.9.7", + "@types/socket.io-client": "1.4.32", "browser-nativefs": "0.7.1", "i18next-browser-languagedetector": "4.1.1", + "lodash.throttle": "4.1.1", "nanoid": "2.1.11", + "node-sass": "4.14.0", "open-color": "1.7.0", "points-on-curve": "0.2.0", "pwacompat": "2.0.11", @@ -32,17 +41,10 @@ "react-scripts": "3.4.1", "roughjs": "4.2.3", "socket.io-client": "2.3.0", - "node-sass": "4.14.0", - "typescript": "3.8.3", - "@types/jest": "25.2.1", - "@types/nanoid": "2.1.0", - "@types/react": "16.9.34", - "@types/react-dom": "16.9.7", - "@types/socket.io-client": "1.4.32", - "@testing-library/jest-dom": "5.5.0", - "@testing-library/react": "10.0.4" + "typescript": "3.8.3" }, "devDependencies": { + "@types/lodash.throttle": "4.1.6", "asar": "3.0.3", "eslint": "6.8.0", "eslint-config-prettier": "6.11.0", diff --git a/src/components/App.tsx b/src/components/App.tsx index 38cfea3e..13bd6e8b 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -133,6 +133,8 @@ import { saveUsernameToLocalStorage, } from "../data/localStorage"; +import throttle from "lodash.throttle"; + /** * @param func handler taking at most single parameter (event). */ @@ -163,11 +165,14 @@ const gesture: Gesture = { initialScale: null, }; +const SYNC_FULL_SCENE_INTERVAL_MS = 20000; + class App extends React.Component { canvas: HTMLCanvasElement | null = null; rc: RoughCanvas | null = null; portal: Portal = new Portal(this); lastBroadcastedOrReceivedSceneVersion: number = -1; + broadcastedElementVersions: Map = new Map(); removeSceneCallback: SceneStateCallbackRemover | null = null; actionManager: ActionManager; @@ -474,6 +479,10 @@ class App extends React.Component { } }); + queueBroadcastAllElements = throttle(() => { + this.broadcastScene(SCENE.UPDATE, /* syncAll */ true); + }, SYNC_FULL_SCENE_INTERVAL_MS); + componentDidUpdate() { if (this.state.isCollaborating && !this.portal.socket) { this.initializeSocketClient({ showLoadingState: true }); @@ -555,7 +564,8 @@ class App extends React.Component { getDrawingVersion(globalSceneState.getElementsIncludingDeleted()) > this.lastBroadcastedOrReceivedSceneVersion ) { - this.broadcastScene(SCENE.UPDATE); + this.broadcastScene(SCENE.UPDATE, /* syncAll */ false); + this.queueBroadcastAllElements(); } history.record(this.state, globalSceneState.getElementsIncludingDeleted()); @@ -1020,19 +1030,43 @@ class App extends React.Component { }; // maybe should move to Portal - broadcastScene = (sceneType: SCENE.INIT | SCENE.UPDATE) => { + broadcastScene = (sceneType: SCENE.INIT | SCENE.UPDATE, syncAll: boolean) => { + if (sceneType === SCENE.INIT && !syncAll) { + throw new Error("syncAll must be true when sending SCENE.INIT"); + } + + let syncableElements = getSyncableElements( + globalSceneState.getElementsIncludingDeleted(), + ); + + 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: getSyncableElements( - globalSceneState.getElementsIncludingDeleted(), - ), + elements: syncableElements, }, }; this.lastBroadcastedOrReceivedSceneVersion = Math.max( this.lastBroadcastedOrReceivedSceneVersion, getDrawingVersion(globalSceneState.getElementsIncludingDeleted()), ); + for (const syncableElement of syncableElements) { + this.broadcastedElementVersions.set( + syncableElement.id, + syncableElement.version, + ); + } return this.portal._broadcastSocketData(data as SocketUpdateData); }; diff --git a/src/components/Portal.tsx b/src/components/Portal.tsx index 95033046..5ff9e04c 100644 --- a/src/components/Portal.tsx +++ b/src/components/Portal.tsx @@ -29,7 +29,7 @@ class Portal { } }); this.socket.on("new-user", async (_socketID: string) => { - this.app.broadcastScene(SCENE.INIT); + this.app.broadcastScene(SCENE.INIT, /* syncAll */ true); }); this.socket.on("room-user-change", (clients: string[]) => { this.app.setCollaborators(clients);