From 6159973df911b49ccd0a75fcb666c48685b7ecbb Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 9 Sep 2023 22:16:02 -0400 Subject: [PATCH] fix: Only update views in collaborative server on data change (#5804) --- server/collaboration/ViewsExtension.ts | 72 +++++++++----------------- 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/server/collaboration/ViewsExtension.ts b/server/collaboration/ViewsExtension.ts index ca8e85327..b2b5c054e 100644 --- a/server/collaboration/ViewsExtension.ts +++ b/server/collaboration/ViewsExtension.ts @@ -1,9 +1,9 @@ import { Extension, - onAwarenessUpdatePayload, onDisconnectPayload, + onChangePayload, } from "@hocuspocus/server"; -import { Second } from "@shared/utils/time"; +import { Minute } from "@shared/utils/time"; import Logger from "@server/logging/Logger"; import { trace } from "@server/logging/tracing"; import { View } from "@server/models"; @@ -11,63 +11,44 @@ import { View } from "@server/models"; @trace() export class ViewsExtension implements Extension { /** - * Map of socketId -> intervals + * Map of last view recorded by socket */ - intervalsBySocket: Map = new Map(); + lastViewBySocket: Map = new Map(); /** - * onAwarenessUpdate hook - * @param data The awareness payload + * onChange hook. When a user changes a document, we update their "viewedAt" + * timestamp if it's been more than a minute since their last change. + * + * @param data The change payload */ - async onAwarenessUpdate({ - documentName, - // @ts-expect-error Hocuspocus types are wrong - connection, - context, - socketId, - }: onAwarenessUpdatePayload) { - if (this.intervalsBySocket.get(socketId)) { - return; - } - + async onChange({ documentName, context, socketId }: onChangePayload) { + const lastUpdate = this.lastViewBySocket.get(socketId); const [, documentId] = documentName.split("."); - const updateView = async () => { + if (!lastUpdate || Date.now() - lastUpdate.getTime() > Minute) { + this.lastViewBySocket.set(socketId, new Date()); + Logger.debug( "multiplayer", - `Updating last viewed at for "${documentName}"` + `User ${context.user.id} viewed "${documentName}"` ); - try { - await View.touch(documentId, context.user.id, !connection.readOnly); - } catch (err) { - Logger.error( - `Failed to update last viewed at for "${documentName}"`, - err, - { - documentId, - userId: context.user.id, - } - ); - } - }; - - // Set up an interval to update the last viewed at timestamp continuously - // while the user is connected. This should only be done once per socket. - // eslint-disable-next-line @typescript-eslint/no-misused-promises - const interval = setInterval(updateView, 30 * Second); - - this.intervalsBySocket.set(socketId, interval); + await Promise.all([ + View.touch(documentId, context.user.id, true), + context.user.update({ lastViewedAt: new Date() }), + ]); + } } /** - * onDisconnect hook + * onDisconnect hook. When a user disconnects, we remove their socket from + * the lastViewBySocket map to cleanup memory. + * * @param data The disconnect payload */ async onDisconnect({ socketId }: onDisconnectPayload) { - const interval = this.intervalsBySocket.get(socketId); + const interval = this.lastViewBySocket.get(socketId); if (interval) { - clearInterval(interval); - this.intervalsBySocket.delete(socketId); + this.lastViewBySocket.delete(socketId); } } @@ -76,9 +57,6 @@ export class ViewsExtension implements Extension { * @param data The destroy payload */ async onDestroy() { - this.intervalsBySocket.forEach((interval, socketId) => { - clearInterval(interval); - this.intervalsBySocket.delete(socketId); - }); + this.lastViewBySocket = new Map(); } }