sync-ydoc.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import { Origin, YDocStatus } from '@growi/core';
  2. import type { Delta } from '@growi/editor';
  3. import type { WSSharedDoc } from 'y-websocket/bin/utils';
  4. import loggerFactory from '~/utils/logger';
  5. import { Revision } from '../../models/revision';
  6. import { normalizeLatestRevisionIfBroken } from '../revision/normalize-latest-revision-if-broken';
  7. import type { MongodbPersistence } from './extended/mongodb-persistence';
  8. const logger = loggerFactory('growi:service:yjs:sync-ydoc');
  9. type Context = {
  10. ydocStatus: YDocStatus;
  11. };
  12. /**
  13. * Sync the text and the meta data with the latest revision body
  14. * @param mdb
  15. * @param doc
  16. * @param context true to force sync
  17. */
  18. export const syncYDoc = async (
  19. mdb: MongodbPersistence,
  20. doc: WSSharedDoc,
  21. context: true | Context,
  22. ): Promise<void> => {
  23. const pageId = doc.name;
  24. // Normalize the latest revision which was borken by the migration script '20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js' provided by v6.1.0 - v7.0.15
  25. await normalizeLatestRevisionIfBroken(pageId);
  26. const revision = await Revision.findOne(
  27. // filter
  28. { pageId },
  29. // projection
  30. { body: 1, createdAt: 1, origin: 1 },
  31. // options
  32. { sort: { createdAt: -1 } },
  33. ).lean();
  34. if (revision == null) {
  35. logger.warn(
  36. `Synchronization has been canceled since the revision of the page ('${pageId}') could not be found`,
  37. );
  38. return;
  39. }
  40. const shouldSync =
  41. context === true ||
  42. (() => {
  43. switch (context.ydocStatus) {
  44. case YDocStatus.NEW:
  45. return true;
  46. case YDocStatus.OUTDATED:
  47. // should skip when the YDoc is outdated and the latest revision is created by the editor
  48. return revision.origin !== Origin.Editor;
  49. default:
  50. return false;
  51. }
  52. })();
  53. if (shouldSync) {
  54. logger.debug(
  55. `YDoc for the page ('${pageId}') is synced with the latest revision body`,
  56. );
  57. const ytext = doc.getText('codemirror');
  58. const delta: Delta = [];
  59. if (ytext.length > 0) {
  60. delta.push({ delete: ytext.length });
  61. }
  62. if (revision.body != null) {
  63. delta.push({ insert: revision.body });
  64. }
  65. ytext.applyDelta(delta, { sanitize: false });
  66. }
  67. const shouldSyncMeta =
  68. context === true ||
  69. context.ydocStatus === YDocStatus.NEW ||
  70. context.ydocStatus === YDocStatus.OUTDATED;
  71. if (shouldSyncMeta) {
  72. mdb.setMeta(
  73. doc.name,
  74. 'updatedAt',
  75. revision.createdAt.getTime() ?? Date.now(),
  76. );
  77. }
  78. };