diff --git a/server/collaboration/PersistenceExtension.ts b/server/collaboration/PersistenceExtension.ts index 4342778e7..f47e27722 100644 --- a/server/collaboration/PersistenceExtension.ts +++ b/server/collaboration/PersistenceExtension.ts @@ -82,6 +82,7 @@ export default class PersistenceExtension implements Extension { document, context, documentName, + clientsCount, }: onStoreDocumentPayload) { const [, documentId] = documentName.split("."); @@ -105,6 +106,7 @@ export default class PersistenceExtension implements Extension { // TODO: Right now we're attributing all changes to the last editor, // It would be nice in the future to have multiple editors per revision. userId: collaboratorIds.pop(), + isLastConnection: clientsCount === 0, }); } catch (err) { Logger.error("Unable to persist document", err, { diff --git a/server/commands/documentCollaborativeUpdater.ts b/server/commands/documentCollaborativeUpdater.ts index 357640b96..8c1ae8141 100644 --- a/server/commands/documentCollaborativeUpdater.ts +++ b/server/commands/documentCollaborativeUpdater.ts @@ -14,12 +14,15 @@ type Props = { ydoc: Y.Doc; /** The user ID that is performing the update, if known */ userId?: string; + /** Whether the last connection to the document left */ + isLastConnection: boolean; }; export default async function documentCollaborativeUpdater({ documentId, ydoc, userId, + isLastConnection, }: Props) { return sequelize.transaction(async (transaction) => { const document = await Document.unscoped() @@ -79,6 +82,7 @@ export default async function documentCollaborativeUpdater({ data: { multiplayer: true, title: document.title, + done: isLastConnection, }, }); }); diff --git a/server/commands/documentUpdater.ts b/server/commands/documentUpdater.ts index d13ac7e60..d06813ed8 100644 --- a/server/commands/documentUpdater.ts +++ b/server/commands/documentUpdater.ts @@ -11,6 +11,8 @@ type Props = { title?: string; /** The new text content */ text?: string; + /** Whether the editing session is complete */ + done?: boolean; /** The version of the client editor that was used */ editorVersion?: string; /** The ID of the template that was used */ @@ -47,10 +49,12 @@ export default async function documentUpdater({ append, publish, collectionId, + done, transaction, ip, }: Props): Promise { const previousTitle = document.title; + const cId = collectionId || document.collectionId; if (title !== undefined) { document.title = title.trim(); @@ -70,23 +74,28 @@ export default async function documentUpdater({ const changed = document.changed(); - if (publish) { + const event = { + name: "documents.update", + documentId: document.id, + collectionId: cId, + teamId: document.teamId, + actorId: user.id, + data: { + title: document.title, + }, + ip, + }; + + if (publish && cId) { if (!document.collectionId) { - document.collectionId = collectionId as string; + document.collectionId = cId; } - await document.publish(user.id, collectionId!, { transaction }); + await document.publish(user.id, cId, { transaction }); await Event.create( { + ...event, name: "documents.publish", - documentId: document.id, - collectionId: document.collectionId, - teamId: document.teamId, - actorId: user.id, - data: { - title: document.title, - }, - ip, }, { transaction } ); @@ -94,27 +103,16 @@ export default async function documentUpdater({ document.lastModifiedById = user.id; await document.save({ transaction }); - await Event.create( - { - name: "documents.update", - documentId: document.id, - collectionId: document.collectionId, - teamId: document.teamId, - actorId: user.id, - data: { - title: document.title, - }, - ip, - }, - { transaction } - ); + await Event.create(event, { transaction }); + } else if (done) { + await Event.schedule(event); } if (document.title !== previousTitle) { await Event.schedule({ name: "documents.title_change", documentId: document.id, - collectionId: document.collectionId, + collectionId: cId, teamId: document.teamId, actorId: user.id, data: { diff --git a/server/queues/processors/RevisionsProcessor.ts b/server/queues/processors/RevisionsProcessor.ts index 12e379dad..692241b1f 100644 --- a/server/queues/processors/RevisionsProcessor.ts +++ b/server/queues/processors/RevisionsProcessor.ts @@ -7,13 +7,19 @@ import BaseProcessor from "./BaseProcessor"; export default class RevisionsProcessor extends BaseProcessor { static applicableEvents: Event["name"][] = [ "documents.publish", + "documents.update", "documents.update.debounced", ]; async perform(event: DocumentEvent | RevisionEvent) { switch (event.name) { case "documents.publish": - case "documents.update.debounced": { + case "documents.update.debounced": + case "documents.update": { + if (event.name === "documents.update" && !event.data.done) { + return; + } + const document = await Document.findByPk(event.documentId, { paranoid: false, }); diff --git a/server/routes/api/documents/documents.ts b/server/routes/api/documents/documents.ts index 7a29a5da4..3fc58327c 100644 --- a/server/routes/api/documents/documents.ts +++ b/server/routes/api/documents/documents.ts @@ -899,6 +899,7 @@ router.post( collectionId, append, apiVersion, + done, } = ctx.input.body; const editorVersion = ctx.headers["x-editor-version"] as string | undefined; const { user } = ctx.state.auth; @@ -937,6 +938,7 @@ router.post( templateId, editorVersion, transaction, + done, ip: ctx.request.ip, }); diff --git a/server/routes/api/documents/schema.ts b/server/routes/api/documents/schema.ts index 04418fc48..8a1a9d2a8 100644 --- a/server/routes/api/documents/schema.ts +++ b/server/routes/api/documents/schema.ts @@ -203,6 +203,9 @@ export const DocumentsUpdateSchema = BaseSchema.extend({ /** Version of the API to be used */ apiVersion: z.number().optional(), + + /** Whether the editing session is complete */ + done: z.boolean().optional(), }), }).refine((req) => !(req.body.append && !req.body.text), { message: "text is required while appending",