From f008e536a2ec4dfd9836592ea9ad4d10a663dd4b Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sat, 18 Jul 2026 00:57:11 +0100 Subject: [PATCH] WIP 1 --- .../extensions/persistence.extension.ts | 14 +- .../redis-sync/collab-proxy-socket.ts | 45 ++---- .../redis-sync/redis-sync.extension.ts | 144 ++++++++++-------- .../extensions/redis-sync/redis-sync.types.ts | 61 +++++--- .../redis-sync/ws-socket-wrapper.ts | 17 +-- package.json | 6 +- pnpm-lock.yaml | 66 ++++---- 7 files changed, 180 insertions(+), 173 deletions(-) diff --git a/apps/server/src/collaboration/extensions/persistence.extension.ts b/apps/server/src/collaboration/extensions/persistence.extension.ts index 3a4df24a7..85efb5ec4 100644 --- a/apps/server/src/collaboration/extensions/persistence.extension.ts +++ b/apps/server/src/collaboration/extensions/persistence.extension.ts @@ -96,7 +96,7 @@ export class PersistenceExtension implements Extension { } async onStoreDocument(data: onStoreDocumentPayload) { - const { documentName, document, context } = data; + const { documentName, document, lastContext } = data; const pageId = getPageId(documentName); @@ -151,7 +151,7 @@ export class PersistenceExtension implements Extension { content: tiptapJson, textContent: textContent, ydoc: ydocState, - lastUpdatedById: context.user.id, + lastUpdatedById: lastContext.user.id, contributorIds: contributorIds, }, pageId, @@ -169,12 +169,12 @@ export class PersistenceExtension implements Extension { JSON.stringify({ type: 'page.updated', updatedAt: new Date().toISOString(), - lastUpdatedById: context?.user?.id, - lastUpdatedBy: context?.user + lastUpdatedById: lastContext?.user?.id, + lastUpdatedBy: lastContext?.user ? { - id: context.user?.id, - name: context.user?.name, - avatarUrl: context.user?.avatarUrl, + id: lastContext.user?.id, + name: lastContext.user?.name, + avatarUrl: lastContext.user?.avatarUrl, } : undefined, }), diff --git a/apps/server/src/collaboration/extensions/redis-sync/collab-proxy-socket.ts b/apps/server/src/collaboration/extensions/redis-sync/collab-proxy-socket.ts index 8ecfa00af..3ac39c810 100644 --- a/apps/server/src/collaboration/extensions/redis-sync/collab-proxy-socket.ts +++ b/apps/server/src/collaboration/extensions/redis-sync/collab-proxy-socket.ts @@ -1,44 +1,35 @@ import type RedisClient from 'ioredis'; -import { EventEmitter } from 'tseep'; -import type { - Pack, - RSAMessageClose, - RSAMessagePing, - RSAMessageSend, -} from './redis-sync.types'; +import type { WebSocketLike } from '@hocuspocus/server'; +import type { Pack, RSAMessageClose, RSAMessageSend } from './redis-sync.types'; -export class CollabProxySocket extends EventEmitter { +// Stands in for the client WebSocket on the server that owns the document. +// Outgoing traffic is relayed over redis to the origin server, which holds the real socket. +export class CollabProxySocket implements WebSocketLike { private readonly replyTo: string; - private readonly serverChannel: string; private readonly socketId: string; private pub: RedisClient; private readonly pack: Pack; readyState = 1; - constructor( - pub: RedisClient, - pack: Pack, - replyTo: string, - serverChannel: string, - socketId: string, - ) { - super(); + constructor(pub: RedisClient, pack: Pack, replyTo: string, socketId: string) { this.replyTo = replyTo; this.socketId = socketId; - this.serverChannel = serverChannel; this.pub = pub; this.pack = pack; - this.once('close', () => { - this.readyState = 3; - }); } - private publish(msg: RSAMessageClose | RSAMessagePing | RSAMessageSend) { + private publish(msg: RSAMessageClose | RSAMessageSend) { this.pub.publish(this.replyTo, this.pack(msg)); } + // The origin server already closed the real socket; stop relaying without echoing a close back + markClosed() { + this.readyState = 3; + } + close(code?: number, reason?: string) { if (this.readyState !== 1) return; + this.readyState = 3; const msg: RSAMessageClose = { type: 'close', code, @@ -48,16 +39,6 @@ export class CollabProxySocket extends EventEmitter { this.publish(msg); } - ping() { - if (this.readyState !== 1) return; - const msg: RSAMessagePing = { - type: 'ping', - socketId: this.socketId, - replyTo: this.serverChannel, - }; - this.publish(msg); - } - send(message: Uint8Array) { if (this.readyState !== 1) return; const msg: RSAMessageSend = { diff --git a/apps/server/src/collaboration/extensions/redis-sync/redis-sync.extension.ts b/apps/server/src/collaboration/extensions/redis-sync/redis-sync.extension.ts index 38747465e..da30a302d 100644 --- a/apps/server/src/collaboration/extensions/redis-sync/redis-sync.extension.ts +++ b/apps/server/src/collaboration/extensions/redis-sync/redis-sync.extension.ts @@ -3,15 +3,14 @@ import { Extension, Hocuspocus, IncomingMessage, - afterUnloadDocumentPayload, onConfigurePayload, onLoadDocumentPayload, + afterUnloadDocumentPayload, + WebSocketLike, } from '@hocuspocus/server'; import RedisClient from 'ioredis'; -import { readVarString } from 'lib0/decoding.js'; import { CollabProxySocket } from './collab-proxy-socket'; import { - BaseWebSocket, Configuration, CustomEvents, Pack, @@ -19,11 +18,13 @@ import { RSAMessageCloseProxy, RSAMessageCustomEventComplete, RSAMessageCustomEventStart, - RSAMessagePong, RSAMessageProxy, RSAMessageUnload, SerializedHTTPRequest, Unpack, + OriginConnection, + ProxyConnection, + toWebRequest, } from './redis-sync.types'; export type { Pack, SerializedHTTPRequest } from './redis-sync.types'; @@ -38,10 +39,10 @@ export class RedisSyncExtension implements Extension { private sub: RedisClient; private readonly pack: Pack; private readonly unpack: Unpack; - private originSockets: Record = {}; + private originConnections: Record = {}; private locks: Record = {}; private lockPromises: Record> = {}; - private proxySockets: Record = {}; + private proxyConnections: Record = {}; private readonly prefix: string; private readonly lockPrefix: string; private readonly msgChannel: string; @@ -54,6 +55,9 @@ export class RedisSyncExtension implements Extension { // @ts-ignore private pendingReplies: Record['resolve']> = {}; + private deriveContext: ( + serializedHTTPRequest: SerializedHTTPRequest, + ) => Record; constructor(configuration: Configuration) { const { @@ -65,6 +69,7 @@ export class RedisSyncExtension implements Extension { prefix, customEvents, customEventTTL, + deriveContext, } = configuration; this.pub = redis.duplicate(); this.sub = redis.duplicate(); @@ -77,6 +82,7 @@ export class RedisSyncExtension implements Extension { this.lockPrefix = `${this.prefix}Lock`; this.msgChannel = `${this.prefix}Msg`; this.customEvents = (customEvents as any) ?? ({} as any as CustomEvents); + this.deriveContext = deriveContext ?? (() => ({})); this.sub.subscribe(this.msgChannel, `${this.msgChannel}:${this.serverId}`); this.sub.on('messageBuffer', this.handleRedisMessage); this.pub.on('error', () => {}); @@ -87,44 +93,46 @@ export class RedisSyncExtension implements Extension { } private closeProxy(socketId: string) { - const proxySocket = this.proxySockets[socketId]; - if (proxySocket) { - proxySocket.emit( - 'close', - 1000, - Buffer.from('provider_initiated', 'utf-8'), - ); - delete this.proxySockets[socketId]; + const entry = this.proxyConnections[socketId]; + if (entry) { + delete this.proxyConnections[socketId]; + const { socket, clientConnection } = entry; + // The origin socket is already gone; don't echo a close message back + socket.markClosed(); + clientConnection.handleClose({ + code: 1000, + reason: 'provider_initiated', + }); } } - private pongProxy(socketId: string) { - this.proxySockets[socketId]?.emit('pong'); - } - private handleProxyMessage( msg: Pick, ) { const { replyTo, message, serializedHTTPRequest } = msg; const { headers } = serializedHTTPRequest; - const socketId = headers['sec-websocket-key']!; - let socket = this.proxySockets[socketId]; - if (!socket) { - socket = new CollabProxySocket( + const socketId = headers['sec-websocket-key']; + let entry = this.proxyConnections[socketId]; + if (!entry) { + const socket = new CollabProxySocket( this.pub, this.pack, replyTo, - `${this.msgChannel}:${this.serverId}`, socketId, ); - this.proxySockets[socketId] = socket; - this.instance.handleConnection( - socket as any, - serializedHTTPRequest as any, - {}, + const clientConnection = this.instance.handleConnection( + socket, + toWebRequest(serializedHTTPRequest), + this.deriveContext(serializedHTTPRequest), ); + entry = { clientConnection, socket }; + this.proxyConnections[socketId] = entry; } - socket.emit('message', message); + entry.clientConnection.handleMessage(message); + } + + private getLock(documentName: string) { + return this.pub.get(this.getKey(documentName)); } private getOrClaimLock(documentName: string) { @@ -166,10 +174,6 @@ export class RedisSyncExtension implements Extension { this.closeProxy(msg.socketId); return; } - if (type === 'pong') { - this.pongProxy(msg.socketId); - return; - } if (type === 'unload') { delete this.lockPromises[msg.documentName]; return; @@ -198,22 +202,14 @@ export class RedisSyncExtension implements Extension { return; } const { socketId } = msg; - const socket = this.originSockets[socketId]; - if (!socket) { + const entry = this.originConnections[socketId]; + if (!entry) { // origin socket already cleaned up return; } + const { socket } = entry; if (type === 'close') { socket.close(msg.code, msg.reason); - } else if (type === 'ping') { - // Reply instantly to the proxy socket, without forwarding to client - // The origin socket handles heartbeat for itself - const { replyTo, socketId } = msg; - const reply: RSAMessagePong = { - type: 'pong', - socketId, - }; - this.pub.publish(`${replyTo}`, this.pack(reply)); } else if (type === 'send') { socket.send(msg.message); } @@ -251,6 +247,8 @@ export class RedisSyncExtension implements Extension { eventName: TName, documentName: string, payload: any, + // if true, don't claim the lock. Useful for targeting pages that are currently open + onlyIfOpen = false, ) { const isDocLoadedOnInstance = this.instance.documents.has(documentName); @@ -258,7 +256,14 @@ export class RedisSyncExtension implements Extension { return this.handleEventLocally(eventName, documentName, payload); } - const proxyTo = await this.getOrClaimLockThrottled(documentName); + const proxyTo = await (onlyIfOpen + ? this.getLock(documentName) + : this.getOrClaimLockThrottled(documentName)); + + if (!proxyTo && onlyIfOpen) { + return; + } + if (proxyTo && proxyTo !== this.serverId) { ++this.replyIdCounter; // bug in biome thinks this.replyIdCounter is not used if written on the line below const replyId = this.replyIdCounter; @@ -277,7 +282,8 @@ export class RedisSyncExtension implements Extension { const { promise, resolve, reject } = Promise.withResolvers(); this.pendingReplies[replyId] = resolve; setTimeout(() => { - reject('TIMEOUT'); + delete this.pendingReplies[replyId]; + reject(new Error('TIMEOUT')); }, this.customEventTTL); return promise as Promise>; } @@ -296,31 +302,39 @@ export class RedisSyncExtension implements Extension { /* WebSocket Server Hooks */ onSocketOpen( - ws: BaseWebSocket, + ws: WebSocketLike, serializedHTTPRequest: SerializedHTTPRequest, - context = {}, ) { - const socketId = serializedHTTPRequest.headers['sec-websocket-key']!; - this.originSockets[socketId] = ws; - this.instance.handleConnection( - ws as any, - serializedHTTPRequest as any, - context, + const socketId = serializedHTTPRequest.headers['sec-websocket-key']; + const clientConnection = this.instance.handleConnection( + ws, + toWebRequest(serializedHTTPRequest), + this.deriveContext(serializedHTTPRequest), ); + this.originConnections[socketId] = { clientConnection, socket: ws }; } async onSocketMessage( - ws: BaseWebSocket, serializedHTTPRequest: SerializedHTTPRequest, detachableMsg: ArrayBuffer, ) { const message = new Uint8Array(detachableMsg.slice()); const tmpMsg = new IncomingMessage(detachableMsg); - const documentName = readVarString(tmpMsg.decoder); + const documentNameAndSessionId = tmpMsg.readVarString(); + // session-aware providers suffix the documentName with \0sessionId + const sepIdx = documentNameAndSessionId.indexOf('\0'); + const documentName = + sepIdx === -1 + ? documentNameAndSessionId + : documentNameAndSessionId.slice(0, sepIdx); const isDocLoadedOnInstance = this.instance.documents.has(documentName); + const socketId = serializedHTTPRequest.headers['sec-websocket-key']; + const entry = this.originConnections[socketId]; + if (!entry) return; + const { clientConnection } = entry; if (isDocLoadedOnInstance) { - ws.emit('message', message); + clientConnection.handleMessage(message); return; } @@ -338,16 +352,17 @@ export class RedisSyncExtension implements Extension { return; } // This server owns the document, but hocuspocus hasn't loaded it yet - ws.emit('message', message); + clientConnection.handleMessage(message); } onSocketClose(socketId: string, code?: number, reason?: ArrayBuffer) { - const socket = this.originSockets[socketId]; - if (!socket) return; - // at this point the socket is considered GC'd and we cannot call close - // The origin socket did not set up any connections for the proxy, so none of the hooks will work if we just emit - socket?.emit('close', code, reason); - delete this.originSockets[socketId]; + const entry = this.originConnections[socketId]; + if (!entry) return; + delete this.originConnections[socketId]; + entry.clientConnection.handleClose({ + code: code ?? 1000, + reason: reason ? Buffer.from(reason).toString() : '', + }); const msg: RSAMessageCloseProxy = { type: 'closeProxy', socketId }; this.pub.publish(this.msgChannel, this.pack(msg)).catch(() => {}); } @@ -372,6 +387,7 @@ export class RedisSyncExtension implements Extension { } async onDestroy() { + this.pendingReplies = {}; this.pub.disconnect(false); this.sub.disconnect(false); } diff --git a/apps/server/src/collaboration/extensions/redis-sync/redis-sync.types.ts b/apps/server/src/collaboration/extensions/redis-sync/redis-sync.types.ts index 1bbab80a2..9df49b0f4 100644 --- a/apps/server/src/collaboration/extensions/redis-sync/redis-sync.types.ts +++ b/apps/server/src/collaboration/extensions/redis-sync/redis-sync.types.ts @@ -1,12 +1,13 @@ -import EventEmitter from 'node:events'; import { IncomingHttpHeaders } from 'node:http2'; import RedisClient from 'ioredis'; +import { CollabProxySocket } from './collab-proxy-socket'; +import { type Hocuspocus, type WebSocketLike } from '@hocuspocus/server'; export type SecondParam = T extends ( - arg1: unknown, + arg1: any, arg2: infer A, - ...args: unknown[] -) => unknown + ...args: any[] +) => any ? A : never; @@ -41,12 +42,6 @@ export type RSAMessageClose = { socketId: string; }; -export type RSAMessagePing = { - type: 'ping'; - socketId: string; - replyTo: string; -}; - export type RSAMessagePong = { type: 'pong'; socketId: string; @@ -59,7 +54,7 @@ export type RSAMessageSend = { socketId: string; }; -export type RSAMessageCustomEventStart = { +export type RSAMessageCustomEventStart = { type: 'customEventStart'; documentName: string; eventName: TName; @@ -71,7 +66,7 @@ export type RSAMessageCustomEventStart = { export type RSAMessageCustomEventComplete = { type: 'customEventComplete'; replyId: number; - payload: unknown; + payload: any; }; export type RSAMessage = @@ -79,7 +74,6 @@ export type RSAMessage = | RSAMessageCloseProxy | RSAMessageUnload | RSAMessageClose - | RSAMessagePing | RSAMessagePong | RSAMessageSend | RSAMessageCustomEventStart @@ -99,9 +93,20 @@ type CustomEventName = string; export type CustomEvents = Record< CustomEventName, - (documentName: string, payload: unknown) => Promise + (documentName: string, payload: any) => Promise >; +// Not exported by @hocuspocus/server +export type ClientConnection = ReturnType; +export type OriginConnection = { + clientConnection: ClientConnection; + socket: WebSocketLike; +}; +export type ProxyConnection = { + clientConnection: ClientConnection; + socket: CollabProxySocket; +}; + export interface Configuration { redis: RedisClient; pack: Pack; @@ -111,11 +116,29 @@ export interface Configuration { customEventTTL?: number; prefix?: string; customEvents?: TCE; + // Derive the hocuspocus context once per socket instead of re-deriving it in a + // per-document hook like onConnect/onAuthenticate. Runs on the origin server when + // the socket opens and on the doc owner when the first proxied message arrives. + deriveContext?: ( + serializedHTTPRequest: SerializedHTTPRequest, + ) => Record; } -export type BaseWebSocket = EventEmitter & { - readyState: number; - close(code?: number, reason?: string): void; - ping(): void; - send(message: Uint8Array): void; +// Hocuspocus expects a web-standard Request, so rehydrate one from what crossed the wire +export const toWebRequest = (serializedHTTPRequest: SerializedHTTPRequest) => { + const { method, url, headers } = serializedHTTPRequest; + const webHeaders = new Headers(); + Object.entries(headers).forEach(([name, value]) => { + if (Array.isArray(value)) { + value.forEach((v) => { + webHeaders.append(name, v); + }); + } else if (value !== undefined) { + webHeaders.set(name, value); + } + }); + return new Request(new URL(url, 'http://localhost'), { + method, + headers: webHeaders, + }); }; diff --git a/apps/server/src/collaboration/extensions/redis-sync/ws-socket-wrapper.ts b/apps/server/src/collaboration/extensions/redis-sync/ws-socket-wrapper.ts index 258e6e12f..714111108 100644 --- a/apps/server/src/collaboration/extensions/redis-sync/ws-socket-wrapper.ts +++ b/apps/server/src/collaboration/extensions/redis-sync/ws-socket-wrapper.ts @@ -1,20 +1,16 @@ -import { EventEmitter } from 'events'; import type WebSocket from 'ws'; +import type { WebSocketLike } from '@hocuspocus/server'; /** * Wrapper around ws WebSocket that only receives events via emit(). * This prevents double-handling when used with RedisSyncExtension. */ -export class WsSocketWrapper extends EventEmitter { +export class WsSocketWrapper implements WebSocketLike { private ws: WebSocket; readyState = 1; constructor(ws: WebSocket) { - super(); this.ws = ws; - this.once('close', () => { - this.readyState = 3; - }); } close(code?: number, reason?: string) { @@ -27,15 +23,6 @@ export class WsSocketWrapper extends EventEmitter { } } - ping() { - if (this.readyState !== 1) return; - try { - this.ws.ping(); - } catch (e) { - // Socket already closed - } - } - send(message: Uint8Array) { if (this.readyState !== 1) return; try { diff --git a/package.json b/package.json index 90c24981c..5e69774c1 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "@casl/ability": "6.8.0", "@docmost/editor-ext": "workspace:*", "@floating-ui/dom": "1.7.3", - "@hocuspocus/provider": "3.4.4", - "@hocuspocus/server": "3.4.4", - "@hocuspocus/transformer": "3.4.4", + "@hocuspocus/provider": "4.4.0", + "@hocuspocus/server": "4.4.0", + "@hocuspocus/transformer": "4.4.0", "@joplin/turndown": "4.0.82", "@joplin/turndown-plugin-gfm": "1.0.64", "@sindresorhus/slugify": "3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5495d9503..afc19394b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,14 +65,14 @@ importers: specifier: 1.7.3 version: 1.7.3 '@hocuspocus/provider': - specifier: 3.4.4 - version: 3.4.4(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30) + specifier: 4.4.0 + version: 4.4.0(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30) '@hocuspocus/server': - specifier: 3.4.4 - version: 3.4.4(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30) + specifier: 4.4.0 + version: 4.4.0(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30) '@hocuspocus/transformer': - specifier: 3.4.4 - version: 3.4.4(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.41.9)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30) + specifier: 4.4.0 + version: 4.4.0(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.41.9)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30) '@joplin/turndown': specifier: 4.0.82 version: 4.0.82 @@ -2433,23 +2433,24 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@hocuspocus/common@3.4.4': - resolution: {integrity: sha512-RykIJ0tsHHMP4Xk+4UCbc7SO5LgGxGUSTdbh6anJEsaALAyqinf1Nn5HYuMjLPolAmsar1v++m9zufR09NLpXA==} + '@hocuspocus/common@4.4.0': + resolution: {integrity: sha512-cPSlPu/ws2JzBHW6OzctyaQMyDCq/YUS4nhXSbyMdkQwJzhjCkvfmmRnVtcmgLR3TBLaNwToNDwR/mNaJFOYsg==} - '@hocuspocus/provider@3.4.4': - resolution: {integrity: sha512-KbsMAfdYcIJD8eMU/5QnpXcSOvIWAcCNI33FSRSaKCIpYBFtAwkYIwWnZJmPZ8a1BMAtqQc+uvy9+UQf7GHnGQ==} + '@hocuspocus/provider@4.4.0': + resolution: {integrity: sha512-A83ROMFqU2bgjTTQ1YMteY9v2cIhDHj4S6XgzjPaqYEgOFr2XvBYGnK+NXzZ6DgldKTnlN7qqvWvZplPvltgyw==} peerDependencies: y-protocols: ^1.0.6 yjs: ^13.6.8 - '@hocuspocus/server@3.4.4': - resolution: {integrity: sha512-UV+oaONAejOzeYgUygNcgsc8RdZvSokVvAxluZJIisLACpRO/VsseQ5lWKDRwLd7Fn6+rHWDH3hGuQ1fdX1Ycg==} + '@hocuspocus/server@4.4.0': + resolution: {integrity: sha512-eVSnx+76CN81vaRol+OlT8FyyGFOBF9Z+kdJQhjVXRpj3kIAI1TzuRHKiZN1vkUogdHfmOAN5Bhs5v7V/qhMkQ==} + engines: {node: '>=22'} peerDependencies: y-protocols: ^1.0.6 yjs: ^13.6.8 - '@hocuspocus/transformer@3.4.4': - resolution: {integrity: sha512-X0EJ863LV97YbL5m8WTt4NDSC6uHi6ZCq/teIH5aholdjdhdTmFrzMemdhha/ZqPUZyaKhOoAmZzwR55HZLPpQ==} + '@hocuspocus/transformer@4.4.0': + resolution: {integrity: sha512-wvHgbiWfU1QERIMd37aImtb4xbHRGwek+66VPgFaSx3POgxCKmkZH5iVY25j/T0PBmloofPoV1XYbwLwJZMtJw==} peerDependencies: '@tiptap/core': ^3.0.1 '@tiptap/pm': ^3.0.1 @@ -5434,9 +5435,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - async-lock@1.4.1: - resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} - async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} @@ -5953,6 +5951,14 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.4.10: + resolution: {integrity: sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA==} + peerDependencies: + srvx: '>=0.11.5' + peerDependenciesMeta: + srvx: + optional: true + css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} @@ -12285,37 +12291,31 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@hocuspocus/common@3.4.4': + '@hocuspocus/common@4.4.0': dependencies: lib0: 0.2.117 - '@hocuspocus/provider@3.4.4(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)': + '@hocuspocus/provider@4.4.0(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)': dependencies: - '@hocuspocus/common': 3.4.4 + '@hocuspocus/common': 4.4.0 '@lifeomic/attempt': 3.0.3 lib0: 0.2.117 - ws: 8.21.0 y-protocols: 1.0.6(yjs@13.6.30) yjs: 13.6.30 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@hocuspocus/server@3.4.4(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)': + '@hocuspocus/server@4.4.0(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)': dependencies: - '@hocuspocus/common': 3.4.4 - async-lock: 1.4.1 + '@hocuspocus/common': 4.4.0 async-mutex: 0.5.0 + crossws: 0.4.10 kleur: 4.1.5 lib0: 0.2.117 - ws: 8.21.0 y-protocols: 1.0.6(yjs@13.6.30) yjs: 13.6.30 transitivePeerDependencies: - - bufferutil - - utf-8-validate + - srvx - '@hocuspocus/transformer@3.4.4(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.41.9)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)': + '@hocuspocus/transformer@4.4.0(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.9)(prosemirror-state@1.4.4)(prosemirror-view@1.41.9)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30))(yjs@13.6.30)': dependencies: '@tiptap/core': 3.27.1(@tiptap/pm@3.27.1) '@tiptap/pm': 3.27.1 @@ -15603,8 +15603,6 @@ snapshots: assertion-error@2.0.1: {} - async-lock@1.4.1: {} - async-mutex@0.5.0: dependencies: tslib: 2.8.1 @@ -16181,6 +16179,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crossws@0.4.10: {} + css-select@5.1.0: dependencies: boolbase: 1.0.0