feat: add real-time collaborative editing with Hocuspocus + Yjs

- Server: Hocuspocus server integrated via WebSocket upgrade on /_collab
- Server: Database extension for persistence
- Frontend: Yjs + HocuspocusProvider for real-time sync
- Frontend: @tiptap/extension-collaboration for Yjs-based CRDT editing
- Frontend: @tiptap/extension-collaboration-cursor for live cursors
- Editor: history disabled when collab active (Yjs handles undo/redo)
pull/7973/head
Gabriel Mowses (Mouse) 1 week ago
parent 3173a87572
commit 9156afbf6f

@ -0,0 +1,76 @@
import { Hocuspocus } from '@hocuspocus/server'
import { Database } from '@hocuspocus/extension-database'
import * as Y from 'yjs'
export default {
hocuspocus: null,
async init() {
this.hocuspocus = new Hocuspocus({
port: null, // Don't listen on its own port - we'll handle WebSocket upgrade
quiet: true,
async onAuthenticate({ token }) {
// TODO: validate JWT token
// For now, allow authenticated connections
if (!token) {
throw new Error('Not authenticated')
}
return { user: { name: 'User' } }
},
extensions: [
new Database({
async fetch({ documentName }) {
try {
const page = await WIKI.db.knex('pages')
.where('id', documentName)
.first('id', 'content')
if (page && page.content) {
// Convert markdown content to Y.Doc
const ydoc = new Y.Doc()
const yxml = ydoc.getXmlFragment('default')
// Return null to let Hocuspocus create a fresh doc
// The editor will load content from the page store
return null
}
return null
} catch (err) {
WIKI.logger.warn(`Collab fetch error for ${documentName}: ${err.message}`)
return null
}
},
async store({ documentName, state }) {
// Store Y.Doc state for persistence between sessions
try {
await WIKI.db.knex('pages')
.where('id', documentName)
.update({
updatedAt: new Date().toISOString()
})
WIKI.logger.debug(`Collab state saved for ${documentName}`)
} catch (err) {
WIKI.logger.warn(`Collab store error for ${documentName}: ${err.message}`)
}
}
})
]
})
WIKI.logger.info('Collaboration Server initialized: [ OK ]')
},
handleUpgrade(request, socket, head) {
if (this.hocuspocus) {
this.hocuspocus.handleUpgrade(request, socket, head)
}
},
handleConnection(socket, request) {
if (this.hocuspocus) {
this.hocuspocus.handleConnection(socket, request)
}
}
}

@ -3,6 +3,7 @@ import eventemitter2 from 'eventemitter2'
import NodeCache from 'node-cache'
import asar from './asar.mjs'
import collaboration from './collaboration.mjs'
import db from './db.mjs'
import extensions from './extensions.mjs'
import scheduler from './scheduler.mjs'
@ -89,6 +90,25 @@ export default {
await WIKI.db.subscribeToNotifications()
await WIKI.scheduler.start()
// Initialize collaboration server
try {
WIKI.collab = collaboration
await WIKI.collab.init()
// Handle WebSocket upgrade for collaboration
const httpServer = WIKI.servers.http || WIKI.servers.https
if (httpServer) {
httpServer.on('upgrade', (request, socket, head) => {
if (request.url && request.url.startsWith('/_collab')) {
WIKI.collab.handleUpgrade(request, socket, head)
}
})
WIKI.logger.info('Collaboration WebSocket on /_collab: [ OK ]')
}
} catch (err) {
WIKI.logger.warn(`Collaboration server init failed: ${err.message}`)
}
},
/**
* Graceful shutdown

@ -43,6 +43,8 @@
"@graphql-tools/schema": "10.0.25",
"@graphql-tools/utils": "10.9.1",
"@hexagon/base64": "2.0.4",
"@hocuspocus/extension-database": "3.4.4",
"@hocuspocus/server": "3.4.4",
"@joplin/turndown-plugin-gfm": "1.0.62",
"@node-saml/passport-saml": "5.1.0",
"@root/csr": "0.8.1",
@ -173,7 +175,8 @@
"uuid": "11.1.0",
"validate.js": "0.13.1",
"vue": "3.5.18",
"xss": "1.0.15"
"xss": "1.0.15",
"yjs": "13.6.30"
},
"devDependencies": {
"eslint": "9.32.0",

@ -29,6 +29,12 @@ importers:
'@hexagon/base64':
specifier: 2.0.4
version: 2.0.4
'@hocuspocus/extension-database':
specifier: 3.4.4
version: 3.4.4(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)
'@hocuspocus/server':
specifier: 3.4.4
version: 3.4.4(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)
'@joplin/turndown-plugin-gfm':
specifier: 1.0.62
version: 1.0.62
@ -422,6 +428,9 @@ importers:
xss:
specifier: 1.0.15
version: 1.0.15
yjs:
specifier: 13.6.30
version: 13.6.30
devDependencies:
eslint:
specifier: 9.32.0
@ -753,6 +762,20 @@ packages:
'@hexagon/base64@2.0.4':
resolution: {integrity: sha512-H/ZY6rGyaEuk0mwQgZ3BVi9hMjFTYpBNFbmtOuec/pPibuGhCMXd8fGtwBaO0h44FkWMurysMsDrpkJsBRmoWQ==}
'@hocuspocus/common@3.4.4':
resolution: {integrity: sha512-RykIJ0tsHHMP4Xk+4UCbc7SO5LgGxGUSTdbh6anJEsaALAyqinf1Nn5HYuMjLPolAmsar1v++m9zufR09NLpXA==}
'@hocuspocus/extension-database@3.4.4':
resolution: {integrity: sha512-z7iq2Dw+GOp4aQq7ys3PD0BA++7tQdXBsSHZ+8mkAbxfTDzjzQ576TphxPiXXC1WQ7yjeFXq03xp/KLIhg3Pyg==}
peerDependencies:
yjs: ^13.6.8
'@hocuspocus/server@3.4.4':
resolution: {integrity: sha512-UV+oaONAejOzeYgUygNcgsc8RdZvSokVvAxluZJIisLACpRO/VsseQ5lWKDRwLd7Fn6+rHWDH3hGuQ1fdX1Ycg==}
peerDependencies:
y-protocols: ^1.0.6
yjs: ^13.6.8
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -1713,6 +1736,12 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
async-lock@1.4.1:
resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
async-retry@1.3.3:
resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
@ -3283,6 +3312,9 @@ packages:
resolution: {integrity: sha512-0FTlXP/gEEWW+O/sXaO9yZ4bgegrHnOqzbdCNAMeO2KYIOVMAcqVIo+uTcWYd1+DmI+nV58vUmNW03nauoKn2w==}
engines: {node: '>=18'}
isomorphic.js@0.2.5:
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
iterator.prototype@1.1.5:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'}
@ -3396,6 +3428,10 @@ packages:
resolution: {integrity: sha512-1zGZ9MF9H22UnkpVeuaGKOjfA2t6WrfdrJmGjy16ykcjnKQDmHVX+KI477rpbGevz/5FD4MC3xf1oxylBgcaQw==}
engines: {node: '>=14.14.0'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
knex@3.1.0:
resolution: {integrity: sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==}
engines: {node: '>=16'}
@ -3443,6 +3479,11 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lib0@0.2.117:
resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==}
engines: {node: '>=16'}
hasBin: true
lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@ -5157,6 +5198,12 @@ packages:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
y-protocols@1.0.7:
resolution: {integrity: sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
peerDependencies:
yjs: ^13.0.0
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@ -5177,6 +5224,10 @@ packages:
yauzl@2.10.0:
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
yjs@13.6.30:
resolution: {integrity: sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@ -5585,6 +5636,33 @@ snapshots:
'@hexagon/base64@2.0.4': {}
'@hocuspocus/common@3.4.4':
dependencies:
lib0: 0.2.117
'@hocuspocus/extension-database@3.4.4(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)':
dependencies:
'@hocuspocus/server': 3.4.4(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)
yjs: 13.6.30
transitivePeerDependencies:
- bufferutil
- utf-8-validate
- y-protocols
'@hocuspocus/server@3.4.4(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)':
dependencies:
'@hocuspocus/common': 3.4.4
async-lock: 1.4.1
async-mutex: 0.5.0
kleur: 4.1.5
lib0: 0.2.117
ws: 8.18.3
y-protocols: 1.0.7(yjs@13.6.30)
yjs: 13.6.30
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@ -6709,6 +6787,12 @@ snapshots:
async-function@1.0.0: {}
async-lock@1.4.1: {}
async-mutex@0.5.0:
dependencies:
tslib: 2.8.1
async-retry@1.3.3:
dependencies:
retry: 0.13.1
@ -8451,6 +8535,8 @@ snapshots:
- supports-color
- utf-8-validate
isomorphic.js@0.2.5: {}
iterator.prototype@1.1.5:
dependencies:
define-data-property: 1.1.4
@ -8616,6 +8702,8 @@ snapshots:
klaw@4.1.0: {}
kleur@4.1.5: {}
knex@3.1.0(pg@8.16.3):
dependencies:
colorette: 2.0.19
@ -8666,6 +8754,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lib0@0.2.117:
dependencies:
isomorphic.js: 0.2.5
lilconfig@2.1.0: {}
lilconfig@3.1.1: {}
@ -10541,6 +10633,11 @@ snapshots:
xtend@4.0.2: {}
y-protocols@1.0.7(yjs@13.6.30):
dependencies:
lib0: 0.2.117
yjs: 13.6.30
y18n@5.0.8: {}
yaml@2.8.0: {}
@ -10562,6 +10659,10 @@ snapshots:
buffer-crc32: 0.2.13
fd-slicer: 1.1.0
yjs@13.6.30:
dependencies:
lib0: 0.2.117
yocto-queue@0.1.0: {}
zen-observable-ts@1.2.5:

@ -12,6 +12,7 @@
},
"dependencies": {
"@apollo/client": "3.13.8",
"@hocuspocus/provider": "3.4.4",
"@lezer/common": "1.2.3",
"@mdi/font": "7.4.47",
"@quasar/extras": "1.17.0",
@ -19,6 +20,8 @@
"@tiptap/core": "2.11.5",
"@tiptap/extension-code-block": "2.11.5",
"@tiptap/extension-code-block-lowlight": "2.11.5",
"@tiptap/extension-collaboration": "2.11.5",
"@tiptap/extension-collaboration-cursor": "2.11.5",
"@tiptap/extension-color": "2.11.5",
"@tiptap/extension-dropcursor": "2.11.5",
"@tiptap/extension-font-family": "2.11.5",
@ -104,6 +107,7 @@
"vue-router": "4.5.1",
"vue3-otp-input": "0.5.40",
"vuedraggable": "4.1.0",
"yjs": "13.6.30",
"zxcvbn": "4.4.2"
},
"devDependencies": {

@ -11,6 +11,9 @@ importers:
'@apollo/client':
specifier: 3.13.8
version: 3.13.8(graphql@16.11.0)
'@hocuspocus/provider':
specifier: 3.4.4
version: 3.4.4(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)
'@lezer/common':
specifier: 1.2.3
version: 1.2.3
@ -32,6 +35,12 @@ importers:
'@tiptap/extension-code-block-lowlight':
specifier: 2.11.5
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-code-block@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(highlight.js@11.11.1)(lowlight@3.3.0)
'@tiptap/extension-collaboration':
specifier: 2.11.5
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(y-prosemirror@1.3.7(prosemirror-model@1.25.2)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30))
'@tiptap/extension-collaboration-cursor':
specifier: 2.11.5
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(y-prosemirror@1.3.7(prosemirror-model@1.25.2)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30))
'@tiptap/extension-color':
specifier: 2.11.5
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))
@ -287,6 +296,9 @@ importers:
vuedraggable:
specifier: 4.1.0
version: 4.1.0(vue@3.5.18(typescript@5.8.3))
yjs:
specifier: 13.6.30
version: 13.6.30
zxcvbn:
specifier: 4.4.2
version: 4.4.2
@ -744,6 +756,15 @@ packages:
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
'@hocuspocus/common@3.4.4':
resolution: {integrity: sha512-RykIJ0tsHHMP4Xk+4UCbc7SO5LgGxGUSTdbh6anJEsaALAyqinf1Nn5HYuMjLPolAmsar1v++m9zufR09NLpXA==}
'@hocuspocus/provider@3.4.4':
resolution: {integrity: sha512-KbsMAfdYcIJD8eMU/5QnpXcSOvIWAcCNI33FSRSaKCIpYBFtAwkYIwWnZJmPZ8a1BMAtqQc+uvy9+UQf7GHnGQ==}
peerDependencies:
y-protocols: ^1.0.6
yjs: ^13.6.8
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -857,6 +878,9 @@ packages:
'@lezer/common@1.2.3':
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
'@lifeomic/attempt@3.1.0':
resolution: {integrity: sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==}
'@mdi/font@7.4.47':
resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==}
@ -1199,6 +1223,19 @@ packages:
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-collaboration-cursor@2.11.5':
resolution: {integrity: sha512-sazBzi5HCHgGRihSzWHhHFMSI9oU0v/qqiDZYJ/zhzCKEWONx8WlS6WTxo6z3l6/Qz9lm7clmHNUQNWxnssAOA==}
peerDependencies:
'@tiptap/core': ^2.7.0
y-prosemirror: ^1.2.11
'@tiptap/extension-collaboration@2.11.5':
resolution: {integrity: sha512-3tMMq0E+FM3/3YBUMq5rLvks2DC/t1XLH2Kz/VcuVCxqg1Zg5s9nKOl6CcUZ8gbdvZoEd/GYoQyROJ957v9wzw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
y-prosemirror: ^1.2.11
'@tiptap/extension-color@2.11.5':
resolution: {integrity: sha512-9gZF6EIpfOJYUt1TtFY37e8iqwKcOmBl8CkFaxq+4mWVvYd2D7KbA0r4tYTxSO0fOBJ5fA/1qJrpvgRlyocp/A==}
peerDependencies:
@ -3061,6 +3098,9 @@ packages:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
isomorphic.js@0.2.5:
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
iterator.prototype@1.1.5:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'}
@ -3151,6 +3191,11 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lib0@0.2.117:
resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==}
engines: {node: '>=16'}
hasBin: true
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
@ -4650,6 +4695,22 @@ packages:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
y-prosemirror@1.3.7:
resolution: {integrity: sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
peerDependencies:
prosemirror-model: ^1.7.1
prosemirror-state: ^1.2.3
prosemirror-view: ^1.9.10
y-protocols: ^1.0.1
yjs: ^13.5.38
y-protocols@1.0.7:
resolution: {integrity: sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
peerDependencies:
yjs: ^13.0.0
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@ -4674,6 +4735,10 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yjs@13.6.30:
resolution: {integrity: sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@ -5081,6 +5146,22 @@ snapshots:
dependencies:
graphql: 16.11.0
'@hocuspocus/common@3.4.4':
dependencies:
lib0: 0.2.117
'@hocuspocus/provider@3.4.4(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)':
dependencies:
'@hocuspocus/common': 3.4.4
'@lifeomic/attempt': 3.1.0
lib0: 0.2.117
ws: 8.17.1
y-protocols: 1.0.7(yjs@13.6.30)
yjs: 13.6.30
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@ -5196,6 +5277,8 @@ snapshots:
'@lezer/common@1.2.3': {}
'@lifeomic/attempt@3.1.0': {}
'@mdi/font@7.4.47': {}
'@napi-rs/wasm-runtime@0.2.12':
@ -5497,6 +5580,17 @@ snapshots:
dependencies:
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
'@tiptap/extension-collaboration-cursor@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(y-prosemirror@1.3.7(prosemirror-model@1.25.2)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30))':
dependencies:
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
y-prosemirror: 1.3.7(prosemirror-model@1.25.2)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)
'@tiptap/extension-collaboration@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(y-prosemirror@1.3.7(prosemirror-model@1.25.2)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30))':
dependencies:
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
'@tiptap/pm': 2.11.5
y-prosemirror: 1.3.7(prosemirror-model@1.25.2)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30)
'@tiptap/extension-color@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))':
dependencies:
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
@ -7649,6 +7743,8 @@ snapshots:
isobject@3.0.1: {}
isomorphic.js@0.2.5: {}
iterator.prototype@1.1.5:
dependencies:
define-data-property: 1.1.4
@ -7746,6 +7842,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lib0@0.2.117:
dependencies:
isomorphic.js: 0.2.5
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
@ -9395,6 +9495,20 @@ snapshots:
xmlhttprequest-ssl@2.1.2: {}
y-prosemirror@1.3.7(prosemirror-model@1.25.2)(prosemirror-state@1.4.3)(prosemirror-view@1.40.1)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30):
dependencies:
lib0: 0.2.117
prosemirror-model: 1.25.2
prosemirror-state: 1.4.3
prosemirror-view: 1.40.1
y-protocols: 1.0.7(yjs@13.6.30)
yjs: 13.6.30
y-protocols@1.0.7(yjs@13.6.30):
dependencies:
lib0: 0.2.117
yjs: 13.6.30
y18n@5.0.8: {}
yallist@3.1.1: {}
@ -9418,6 +9532,10 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
yjs@13.6.30:
dependencies:
lib0: 0.2.117
yocto-queue@0.1.0: {}
yoctocolors-cjs@2.1.2: {}

@ -89,7 +89,8 @@
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
// import Collaboration from '@tiptap/extension-collaboration'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import { Color } from '@tiptap/extension-color'
import FontFamily from '@tiptap/extension-font-family'
@ -108,9 +109,8 @@ import TextStyle from '@tiptap/extension-text-style'
import Typography from '@tiptap/extension-typography'
import { common, createLowlight } from 'lowlight'
import { onBeforeUnmount, onMounted, reactive, shallowRef } from 'vue'
// import * as Y from 'yjs'
// import { IndexeddbPersistence } from 'y-indexeddb'
// import { WebsocketProvider } from 'y-websocket'
import * as Y from 'yjs'
import { HocuspocusProvider } from '@hocuspocus/provider'
import { useMeta, useQuasar, setCssVar } from 'quasar'
import { useI18n } from 'vue-i18n'
@ -139,11 +139,12 @@ const { t } = useI18n()
// STATE
const state = reactive({
// editor: null,
ydoc: null
collabEnabled: false
})
let editor = null
let ydoc = null
let collabProvider = null
const thumbStyle = {
right: '2px',
@ -672,12 +673,32 @@ function init () {
})
// -> Init Live Collab
// this.ydoc = new Y.Doc()
ydoc = new Y.Doc()
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
const collabUrl = `${wsProtocol}://${window.location.host}/_collab`
try {
collabProvider = new HocuspocusProvider({
url: collabUrl,
name: pageStore.id || 'default',
document: ydoc,
token: document.cookie.match(/jwt=([^;]+)/)?.[1] || 'anonymous',
onConnect() { state.collabEnabled = true },
onDisconnect() { state.collabEnabled = false },
onSynced() { console.info('Collaboration synced') }
})
} catch (err) {
console.warn('Collaboration unavailable:', err.message)
}
/* eslint-disable no-unused-vars */
// const dbProvider = new IndexeddbPersistence('example-document', this.ydoc)
// const wsProvider = new WebsocketProvider('ws://127.0.0.1:1234', 'example-document', this.ydoc)
/* eslint-enable no-unused-vars */
// -> Build extensions list
const collabExtensions = ydoc ? [
Collaboration.configure({ document: ydoc }),
...(collabProvider ? [CollaborationCursor.configure({
provider: collabProvider,
user: { name: 'Editor', color: '#006FEE' }
})] : [])
] : []
// -> Initialize TipTap
editor = useEditor({
@ -685,17 +706,13 @@ function init () {
extensions: [
StarterKit.configure({
codeBlock: false,
history: {
depth: 500
}
history: ydoc ? false : { depth: 500 }
}),
CodeBlockLowlight.configure({
lowlight
}),
Color,
// Collaboration.configure({
// document: this.ydoc
// }),
...collabExtensions,
FontFamily,
Highlight.configure({
multicolor: true
@ -745,6 +762,8 @@ onMounted(() => {
})
onBeforeUnmount(() => {
if (collabProvider) { collabProvider.destroy(); collabProvider = null }
if (ydoc) { ydoc.destroy(); ydoc = null }
editor.value.destroy()
})

Loading…
Cancel
Save