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'; @Injectable() export class PersistenceExtension implements Extension { private readonly logger = new Logger(PersistenceExtension.name); constructor(private readonly pageRepo: PageRepo) {} 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 { await this.pageRepo.updatePage( { content: tiptapJson, textContent: textContent, ydoc: ydocState, lastUpdatedById: context.user.id, updatedAt: new Date(), }, pageId, ); } catch (err) { this.logger.error(`Failed to update page ${pageId}`, err); } } }