import { Extension, onLoadDocumentPayload, onStoreDocumentPayload, } from '@hocuspocus/server'; import * as Y from 'yjs'; import { Injectable, Logger } from '@nestjs/common'; import { TiptapTransformer } from '@hocuspocus/transformer'; import { getPageId, jsonToText, tiptapExtensions } from '../collaboration.util'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB } from '@docmost/db/types/kysely.types'; import { executeTx } from '@docmost/db/utils'; import { EventEmitter2 } from '@nestjs/event-emitter'; @Injectable() export class PersistenceExtension implements Extension { private readonly logger = new Logger(PersistenceExtension.name); constructor( private readonly pageRepo: PageRepo, @InjectKysely() private readonly db: KyselyDB, private eventEmitter: EventEmitter2, ) {} async onLoadDocument(data: onLoadDocumentPayload) { const { documentName, document } = data; const pageId = getPageId(documentName); if (!document.isEmpty('default')) { return; } const page = await this.pageRepo.findById(pageId, { includeContent: true, includeYdoc: true, }); if (!page) { this.logger.warn('page not found'); return; } if (page.ydoc) { this.logger.debug(`ydoc loaded from db: ${pageId}`); const doc = new Y.Doc(); const dbState = new Uint8Array(page.ydoc); Y.applyUpdate(doc, dbState); return doc; } // if no ydoc state in db convert json in page.content to Ydoc. if (page.content) { this.logger.debug(`converting json to ydoc: ${pageId}`); const ydoc = TiptapTransformer.toYdoc( page.content, 'default', tiptapExtensions, ); Y.encodeStateAsUpdate(ydoc); return ydoc; } this.logger.debug(`creating fresh ydoc: ${pageId}`); return new Y.Doc(); } async onStoreDocument(data: onStoreDocumentPayload) { const { documentName, document, context } = data; const pageId = getPageId(documentName); const tiptapJson = TiptapTransformer.fromYdoc(document, 'default'); const ydocState = Buffer.from(Y.encodeStateAsUpdate(document)); const textContent = jsonToText(tiptapJson); try { let page = null; await executeTx(this.db, async (trx) => { page = await this.pageRepo.findById(pageId, { withLock: true, trx, }); if (!page) { this.logger.error(`Page with id ${pageId} not found`); return; } await this.pageRepo.updatePage( { content: tiptapJson, textContent: textContent, ydoc: ydocState, lastUpdatedById: context.user.id, }, pageId, trx, ); }); this.eventEmitter.emit('collab.page.updated', { page: { ...page, lastUpdatedById: context.user.id, content: tiptapJson, textContent: textContent, }, }); } catch (err) { this.logger.error(`Failed to update page ${pageId}`, err); } } }