fix: Only update views in collaborative server on data change (#5804)

This commit is contained in:
Tom Moor
2023-09-09 22:16:02 -04:00
committed by GitHub
parent 5c839998c1
commit 6159973df9

View File

@@ -1,9 +1,9 @@
import { import {
Extension, Extension,
onAwarenessUpdatePayload,
onDisconnectPayload, onDisconnectPayload,
onChangePayload,
} from "@hocuspocus/server"; } from "@hocuspocus/server";
import { Second } from "@shared/utils/time"; import { Minute } from "@shared/utils/time";
import Logger from "@server/logging/Logger"; import Logger from "@server/logging/Logger";
import { trace } from "@server/logging/tracing"; import { trace } from "@server/logging/tracing";
import { View } from "@server/models"; import { View } from "@server/models";
@@ -11,63 +11,44 @@ import { View } from "@server/models";
@trace() @trace()
export class ViewsExtension implements Extension { export class ViewsExtension implements Extension {
/** /**
* Map of socketId -> intervals * Map of last view recorded by socket
*/ */
intervalsBySocket: Map<string, NodeJS.Timer> = new Map(); lastViewBySocket: Map<string, Date> = new Map();
/** /**
* onAwarenessUpdate hook * onChange hook. When a user changes a document, we update their "viewedAt"
* @param data The awareness payload * timestamp if it's been more than a minute since their last change.
*
* @param data The change payload
*/ */
async onAwarenessUpdate({ async onChange({ documentName, context, socketId }: onChangePayload) {
documentName, const lastUpdate = this.lastViewBySocket.get(socketId);
// @ts-expect-error Hocuspocus types are wrong
connection,
context,
socketId,
}: onAwarenessUpdatePayload) {
if (this.intervalsBySocket.get(socketId)) {
return;
}
const [, documentId] = documentName.split("."); const [, documentId] = documentName.split(".");
const updateView = async () => { if (!lastUpdate || Date.now() - lastUpdate.getTime() > Minute) {
this.lastViewBySocket.set(socketId, new Date());
Logger.debug( Logger.debug(
"multiplayer", "multiplayer",
`Updating last viewed at for "${documentName}"` `User ${context.user.id} viewed "${documentName}"`
); );
try { await Promise.all([
await View.touch(documentId, context.user.id, !connection.readOnly); View.touch(documentId, context.user.id, true),
} catch (err) { context.user.update({ lastViewedAt: new Date() }),
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);
} }
/** /**
* onDisconnect hook * onDisconnect hook. When a user disconnects, we remove their socket from
* the lastViewBySocket map to cleanup memory.
*
* @param data The disconnect payload * @param data The disconnect payload
*/ */
async onDisconnect({ socketId }: onDisconnectPayload) { async onDisconnect({ socketId }: onDisconnectPayload) {
const interval = this.intervalsBySocket.get(socketId); const interval = this.lastViewBySocket.get(socketId);
if (interval) { if (interval) {
clearInterval(interval); this.lastViewBySocket.delete(socketId);
this.intervalsBySocket.delete(socketId);
} }
} }
@@ -76,9 +57,6 @@ export class ViewsExtension implements Extension {
* @param data The destroy payload * @param data The destroy payload
*/ */
async onDestroy() { async onDestroy() {
this.intervalsBySocket.forEach((interval, socketId) => { this.lastViewBySocket = new Map();
clearInterval(interval);
this.intervalsBySocket.delete(socketId);
});
} }
} }