|  | @@ -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<any, AppState> {
 | 
	
		
			
				|  |  |    canvas: HTMLCanvasElement | null = null;
 | 
	
		
			
				|  |  |    rc: RoughCanvas | null = null;
 | 
	
		
			
				|  |  |    portal: Portal = new Portal(this);
 | 
	
		
			
				|  |  |    lastBroadcastedOrReceivedSceneVersion: number = -1;
 | 
	
		
			
				|  |  | +  broadcastedElementVersions: Map<string, number> = new Map();
 | 
	
		
			
				|  |  |    removeSceneCallback: SceneStateCallbackRemover | null = null;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    actionManager: ActionManager;
 | 
	
	
		
			
				|  | @@ -474,6 +479,10 @@ class App extends React.Component<any, AppState> {
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +  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<any, AppState> {
 | 
	
		
			
				|  |  |        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<any, AppState> {
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    // 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);
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 |