save room to firebase on unload or portal close (#2207)

* save on unload or portal close

* align naming
This commit is contained in:
David Luzar 2020-10-06 04:34:40 +02:00 committed by GitHub
parent ae1ab1ab37
commit d18a72c879
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 76 additions and 24 deletions

View File

@ -176,7 +176,11 @@ import {
import { MaybeTransformHandleType } from "../element/transformHandles"; import { MaybeTransformHandleType } from "../element/transformHandles";
import { renderSpreadsheet } from "../charts"; import { renderSpreadsheet } from "../charts";
import { isValidLibrary } from "../data/json"; import { isValidLibrary } from "../data/json";
import { loadFromFirebase, saveToFirebase } from "../data/firebase"; import {
loadFromFirebase,
saveToFirebase,
isSavedToFirebase,
} from "../data/firebase";
/** /**
* @param func handler taking at most single parameter (event). * @param func handler taking at most single parameter (event).
@ -469,7 +473,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return false; return false;
} }
const roomId = roomMatch[1]; const roomID = roomMatch[1];
let collabForceLoadFlag; let collabForceLoadFlag;
try { try {
@ -488,7 +492,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
); );
// if loading same room as the one previously unloaded within 15sec // if loading same room as the one previously unloaded within 15sec
// force reload without prompting // force reload without prompting
if (previousRoom === roomId && Date.now() - timestamp < 15000) { if (previousRoom === roomID && Date.now() - timestamp < 15000) {
return true; return true;
} }
} catch {} } catch {}
@ -784,7 +788,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
); );
} catch {} } catch {}
} }
if (this.state.isCollaborating && this.scene.getElements().length > 0) { const syncableElements = getSyncableElements(
this.scene.getElementsIncludingDeleted(),
);
if (
this.state.isCollaborating &&
!isSavedToFirebase(this.portal, syncableElements)
) {
// this won't run in time if user decides to leave the site, but
// the purpose is to run in immediately after user decides to stay
this.saveCollabRoomToFirebase(syncableElements);
event.preventDefault(); event.preventDefault();
// NOTE: modern browsers no longer allow showing a custom message here // NOTE: modern browsers no longer allow showing a custom message here
event.returnValue = ""; event.returnValue = "";
@ -1182,6 +1196,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}; };
closePortal = () => { closePortal = () => {
this.saveCollabRoomToFirebase();
window.history.pushState({}, "Excalidraw", window.location.origin); window.history.pushState({}, "Excalidraw", window.location.origin);
this.destroySocketClient(); this.destroySocketClient();
}; };
@ -1227,8 +1242,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
const roomMatch = getCollaborationLinkData(window.location.href); const roomMatch = getCollaborationLinkData(window.location.href);
if (roomMatch) { if (roomMatch) {
const roomId = roomMatch[1]; const roomID = roomMatch[1];
const roomSecret = roomMatch[2]; const roomKey = roomMatch[2];
const initialize = () => { const initialize = () => {
this.portal.socketInitialized = true; this.portal.socketInitialized = true;
@ -1358,7 +1373,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
/* webpackChunkName: "socketIoClient" */ "socket.io-client" /* webpackChunkName: "socketIoClient" */ "socket.io-client"
); );
this.portal.open(socketIOClient(SOCKET_SERVER), roomId, roomSecret); this.portal.open(socketIOClient(SOCKET_SERVER), roomID, roomKey);
// All socket listeners are moving to Portal // All socket listeners are moving to Portal
this.portal.socket!.on( this.portal.socket!.on(
@ -1430,7 +1445,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}); });
try { try {
const elements = await loadFromFirebase(roomId, roomSecret); const elements = await loadFromFirebase(roomID, roomKey);
if (elements) { if (elements) {
updateScene( updateScene(
{ type: "SCENE_UPDATE", payload: { elements } }, { type: "SCENE_UPDATE", payload: { elements } },
@ -1484,6 +1499,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
}; };
saveCollabRoomToFirebase = async (
syncableElements: ExcalidrawElement[] = getSyncableElements(
this.scene.getElementsIncludingDeleted(),
),
) => {
try {
await saveToFirebase(this.portal, syncableElements);
} catch (error) {
console.error(error);
}
};
// maybe should move to Portal // maybe should move to Portal
broadcastScene = async ( broadcastScene = async (
sceneType: SCENE.INIT | SCENE.UPDATE, sceneType: SCENE.INIT | SCENE.UPDATE,
@ -1530,16 +1557,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
data as SocketUpdateData, data as SocketUpdateData,
); );
if (syncAll && this.portal.roomID && this.portal.roomKey) { if (syncAll && this.state.isCollaborating) {
await Promise.all([ await Promise.all([
broadcastPromise, broadcastPromise,
saveToFirebase( this.saveCollabRoomToFirebase(syncableElements),
this.portal.roomID,
this.portal.roomKey,
syncableElements,
).catch((e) => {
console.error(e);
}),
]); ]);
} else { } else {
await broadcastPromise; await broadcastPromise;

View File

@ -1,6 +1,7 @@
import { createIV, getImportedKey } from "./index"; import { createIV, getImportedKey } from "./index";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { getSceneVersion } from "../element"; import { getSceneVersion } from "../element";
import Portal from "../components/Portal";
let firebasePromise: Promise<typeof import("firebase/app")> | null = null; let firebasePromise: Promise<typeof import("firebase/app")> | null = null;
@ -69,14 +70,40 @@ async function decryptElements(
return JSON.parse(decodedData); return JSON.parse(decodedData);
} }
const firebaseSceneVersionCache = new WeakMap<SocketIOClient.Socket, number>();
export const isSavedToFirebase = (
portal: Portal,
elements: readonly ExcalidrawElement[],
): boolean => {
if (portal.socket && portal.roomID && portal.roomKey) {
const sceneVersion = getSceneVersion(elements);
return firebaseSceneVersionCache.get(portal.socket) === sceneVersion;
}
// if no room exists, consider the room saved so that we don't unnecessarily
// prevent unload (there's nothing we could do at that point anyway)
return true;
};
export async function saveToFirebase( export async function saveToFirebase(
roomId: string, portal: Portal,
roomSecret: string,
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
) { ) {
const { roomID, roomKey, socket } = portal;
if (
// if no room exists, consider the room saved because there's nothing we can
// do at this point
!roomID ||
!roomKey ||
!socket ||
isSavedToFirebase(portal, elements)
) {
return true;
}
const firebase = await getFirebase(); const firebase = await getFirebase();
const sceneVersion = getSceneVersion(elements); const sceneVersion = getSceneVersion(elements);
const { ciphertext, iv } = await encryptElements(roomSecret, elements); const { ciphertext, iv } = await encryptElements(roomKey, elements);
const nextDocData = { const nextDocData = {
sceneVersion, sceneVersion,
@ -87,7 +114,7 @@ export async function saveToFirebase(
} as FirebaseStoredScene; } as FirebaseStoredScene;
const db = firebase.firestore(); const db = firebase.firestore();
const docRef = db.collection("scenes").doc(roomId); const docRef = db.collection("scenes").doc(roomID);
const didUpdate = await db.runTransaction(async (transaction) => { const didUpdate = await db.runTransaction(async (transaction) => {
const doc = await transaction.get(docRef); const doc = await transaction.get(docRef);
if (!doc.exists) { if (!doc.exists) {
@ -104,17 +131,21 @@ export async function saveToFirebase(
return true; return true;
}); });
if (didUpdate) {
firebaseSceneVersionCache.set(socket, sceneVersion);
}
return didUpdate; return didUpdate;
} }
export async function loadFromFirebase( export async function loadFromFirebase(
roomId: string, roomID: string,
roomSecret: string, roomKey: string,
): Promise<readonly ExcalidrawElement[] | null> { ): Promise<readonly ExcalidrawElement[] | null> {
const firebase = await getFirebase(); const firebase = await getFirebase();
const db = firebase.firestore(); const db = firebase.firestore();
const docRef = db.collection("scenes").doc(roomId); const docRef = db.collection("scenes").doc(roomID);
const doc = await docRef.get(); const doc = await docRef.get();
if (!doc.exists) { if (!doc.exists) {
return null; return null;
@ -122,6 +153,6 @@ export async function loadFromFirebase(
const storedScene = doc.data() as FirebaseStoredScene; const storedScene = doc.data() as FirebaseStoredScene;
const ciphertext = storedScene.ciphertext.toUint8Array(); const ciphertext = storedScene.ciphertext.toUint8Array();
const iv = storedScene.iv.toUint8Array(); const iv = storedScene.iv.toUint8Array();
const plaintext = await decryptElements(roomSecret, iv, ciphertext); const plaintext = await decryptElements(roomKey, iv, ciphertext);
return plaintext; return plaintext;
} }