This commit is contained in:
Philipinho
2026-07-18 00:57:11 +01:00
parent e28df6ad88
commit f008e536a2
7 changed files with 180 additions and 173 deletions
@@ -96,7 +96,7 @@ export class PersistenceExtension implements Extension {
} }
async onStoreDocument(data: onStoreDocumentPayload) { async onStoreDocument(data: onStoreDocumentPayload) {
const { documentName, document, context } = data; const { documentName, document, lastContext } = data;
const pageId = getPageId(documentName); const pageId = getPageId(documentName);
@@ -151,7 +151,7 @@ export class PersistenceExtension implements Extension {
content: tiptapJson, content: tiptapJson,
textContent: textContent, textContent: textContent,
ydoc: ydocState, ydoc: ydocState,
lastUpdatedById: context.user.id, lastUpdatedById: lastContext.user.id,
contributorIds: contributorIds, contributorIds: contributorIds,
}, },
pageId, pageId,
@@ -169,12 +169,12 @@ export class PersistenceExtension implements Extension {
JSON.stringify({ JSON.stringify({
type: 'page.updated', type: 'page.updated',
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
lastUpdatedById: context?.user?.id, lastUpdatedById: lastContext?.user?.id,
lastUpdatedBy: context?.user lastUpdatedBy: lastContext?.user
? { ? {
id: context.user?.id, id: lastContext.user?.id,
name: context.user?.name, name: lastContext.user?.name,
avatarUrl: context.user?.avatarUrl, avatarUrl: lastContext.user?.avatarUrl,
} }
: undefined, : undefined,
}), }),
@@ -1,44 +1,35 @@
import type RedisClient from 'ioredis'; import type RedisClient from 'ioredis';
import { EventEmitter } from 'tseep'; import type { WebSocketLike } from '@hocuspocus/server';
import type { import type { Pack, RSAMessageClose, RSAMessageSend } from './redis-sync.types';
Pack,
RSAMessageClose,
RSAMessagePing,
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 replyTo: string;
private readonly serverChannel: string;
private readonly socketId: string; private readonly socketId: string;
private pub: RedisClient; private pub: RedisClient;
private readonly pack: Pack; private readonly pack: Pack;
readyState = 1; readyState = 1;
constructor( constructor(pub: RedisClient, pack: Pack, replyTo: string, socketId: string) {
pub: RedisClient,
pack: Pack,
replyTo: string,
serverChannel: string,
socketId: string,
) {
super();
this.replyTo = replyTo; this.replyTo = replyTo;
this.socketId = socketId; this.socketId = socketId;
this.serverChannel = serverChannel;
this.pub = pub; this.pub = pub;
this.pack = pack; 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)); 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) { close(code?: number, reason?: string) {
if (this.readyState !== 1) return; if (this.readyState !== 1) return;
this.readyState = 3;
const msg: RSAMessageClose = { const msg: RSAMessageClose = {
type: 'close', type: 'close',
code, code,
@@ -48,16 +39,6 @@ export class CollabProxySocket extends EventEmitter {
this.publish(msg); 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) { send(message: Uint8Array) {
if (this.readyState !== 1) return; if (this.readyState !== 1) return;
const msg: RSAMessageSend = { const msg: RSAMessageSend = {
@@ -3,15 +3,14 @@ import {
Extension, Extension,
Hocuspocus, Hocuspocus,
IncomingMessage, IncomingMessage,
afterUnloadDocumentPayload,
onConfigurePayload, onConfigurePayload,
onLoadDocumentPayload, onLoadDocumentPayload,
afterUnloadDocumentPayload,
WebSocketLike,
} from '@hocuspocus/server'; } from '@hocuspocus/server';
import RedisClient from 'ioredis'; import RedisClient from 'ioredis';
import { readVarString } from 'lib0/decoding.js';
import { CollabProxySocket } from './collab-proxy-socket'; import { CollabProxySocket } from './collab-proxy-socket';
import { import {
BaseWebSocket,
Configuration, Configuration,
CustomEvents, CustomEvents,
Pack, Pack,
@@ -19,11 +18,13 @@ import {
RSAMessageCloseProxy, RSAMessageCloseProxy,
RSAMessageCustomEventComplete, RSAMessageCustomEventComplete,
RSAMessageCustomEventStart, RSAMessageCustomEventStart,
RSAMessagePong,
RSAMessageProxy, RSAMessageProxy,
RSAMessageUnload, RSAMessageUnload,
SerializedHTTPRequest, SerializedHTTPRequest,
Unpack, Unpack,
OriginConnection,
ProxyConnection,
toWebRequest,
} from './redis-sync.types'; } from './redis-sync.types';
export type { Pack, SerializedHTTPRequest } from './redis-sync.types'; export type { Pack, SerializedHTTPRequest } from './redis-sync.types';
@@ -38,10 +39,10 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
private sub: RedisClient; private sub: RedisClient;
private readonly pack: Pack; private readonly pack: Pack;
private readonly unpack: Unpack; private readonly unpack: Unpack;
private originSockets: Record<SocketId, BaseWebSocket> = {}; private originConnections: Record<SocketId, OriginConnection> = {};
private locks: Record<DocumentName, NodeJS.Timeout> = {}; private locks: Record<DocumentName, NodeJS.Timeout> = {};
private lockPromises: Record<DocumentName, Promise<ServerId | null>> = {}; private lockPromises: Record<DocumentName, Promise<ServerId | null>> = {};
private proxySockets: Record<SocketId, CollabProxySocket> = {}; private proxyConnections: Record<SocketId, ProxyConnection> = {};
private readonly prefix: string; private readonly prefix: string;
private readonly lockPrefix: string; private readonly lockPrefix: string;
private readonly msgChannel: string; private readonly msgChannel: string;
@@ -54,6 +55,9 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
// @ts-ignore // @ts-ignore
private pendingReplies: Record<number, PromiseWithResolvers<any>['resolve']> = private pendingReplies: Record<number, PromiseWithResolvers<any>['resolve']> =
{}; {};
private deriveContext: (
serializedHTTPRequest: SerializedHTTPRequest,
) => Record<string, any>;
constructor(configuration: Configuration<TCE>) { constructor(configuration: Configuration<TCE>) {
const { const {
@@ -65,6 +69,7 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
prefix, prefix,
customEvents, customEvents,
customEventTTL, customEventTTL,
deriveContext,
} = configuration; } = configuration;
this.pub = redis.duplicate(); this.pub = redis.duplicate();
this.sub = redis.duplicate(); this.sub = redis.duplicate();
@@ -77,6 +82,7 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
this.lockPrefix = `${this.prefix}Lock`; this.lockPrefix = `${this.prefix}Lock`;
this.msgChannel = `${this.prefix}Msg`; this.msgChannel = `${this.prefix}Msg`;
this.customEvents = (customEvents as any) ?? ({} as any as CustomEvents); this.customEvents = (customEvents as any) ?? ({} as any as CustomEvents);
this.deriveContext = deriveContext ?? (() => ({}));
this.sub.subscribe(this.msgChannel, `${this.msgChannel}:${this.serverId}`); this.sub.subscribe(this.msgChannel, `${this.msgChannel}:${this.serverId}`);
this.sub.on('messageBuffer', this.handleRedisMessage); this.sub.on('messageBuffer', this.handleRedisMessage);
this.pub.on('error', () => {}); this.pub.on('error', () => {});
@@ -87,44 +93,46 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
} }
private closeProxy(socketId: string) { private closeProxy(socketId: string) {
const proxySocket = this.proxySockets[socketId]; const entry = this.proxyConnections[socketId];
if (proxySocket) { if (entry) {
proxySocket.emit( delete this.proxyConnections[socketId];
'close', const { socket, clientConnection } = entry;
1000, // The origin socket is already gone; don't echo a close message back
Buffer.from('provider_initiated', 'utf-8'), socket.markClosed();
); clientConnection.handleClose({
delete this.proxySockets[socketId]; code: 1000,
reason: 'provider_initiated',
});
} }
} }
private pongProxy(socketId: string) {
this.proxySockets[socketId]?.emit('pong');
}
private handleProxyMessage( private handleProxyMessage(
msg: Pick<RSAMessageProxy, 'replyTo' | 'message' | 'serializedHTTPRequest'>, msg: Pick<RSAMessageProxy, 'replyTo' | 'message' | 'serializedHTTPRequest'>,
) { ) {
const { replyTo, message, serializedHTTPRequest } = msg; const { replyTo, message, serializedHTTPRequest } = msg;
const { headers } = serializedHTTPRequest; const { headers } = serializedHTTPRequest;
const socketId = headers['sec-websocket-key']!; const socketId = headers['sec-websocket-key'];
let socket = this.proxySockets[socketId]; let entry = this.proxyConnections[socketId];
if (!socket) { if (!entry) {
socket = new CollabProxySocket( const socket = new CollabProxySocket(
this.pub, this.pub,
this.pack, this.pack,
replyTo, replyTo,
`${this.msgChannel}:${this.serverId}`,
socketId, socketId,
); );
this.proxySockets[socketId] = socket; const clientConnection = this.instance.handleConnection(
this.instance.handleConnection( socket,
socket as any, toWebRequest(serializedHTTPRequest),
serializedHTTPRequest as any, 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) { private getOrClaimLock(documentName: string) {
@@ -166,10 +174,6 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
this.closeProxy(msg.socketId); this.closeProxy(msg.socketId);
return; return;
} }
if (type === 'pong') {
this.pongProxy(msg.socketId);
return;
}
if (type === 'unload') { if (type === 'unload') {
delete this.lockPromises[msg.documentName]; delete this.lockPromises[msg.documentName];
return; return;
@@ -198,22 +202,14 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
return; return;
} }
const { socketId } = msg; const { socketId } = msg;
const socket = this.originSockets[socketId]; const entry = this.originConnections[socketId];
if (!socket) { if (!entry) {
// origin socket already cleaned up // origin socket already cleaned up
return; return;
} }
const { socket } = entry;
if (type === 'close') { if (type === 'close') {
socket.close(msg.code, msg.reason); 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') { } else if (type === 'send') {
socket.send(msg.message); socket.send(msg.message);
} }
@@ -251,6 +247,8 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
eventName: TName, eventName: TName,
documentName: string, documentName: string,
payload: any, 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); const isDocLoadedOnInstance = this.instance.documents.has(documentName);
@@ -258,7 +256,14 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
return this.handleEventLocally(eventName, documentName, payload); 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) { if (proxyTo && proxyTo !== this.serverId) {
++this.replyIdCounter; // bug in biome thinks this.replyIdCounter is not used if written on the line below ++this.replyIdCounter; // bug in biome thinks this.replyIdCounter is not used if written on the line below
const replyId = this.replyIdCounter; const replyId = this.replyIdCounter;
@@ -277,7 +282,8 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
const { promise, resolve, reject } = Promise.withResolvers(); const { promise, resolve, reject } = Promise.withResolvers();
this.pendingReplies[replyId] = resolve; this.pendingReplies[replyId] = resolve;
setTimeout(() => { setTimeout(() => {
reject('TIMEOUT'); delete this.pendingReplies[replyId];
reject(new Error('TIMEOUT'));
}, this.customEventTTL); }, this.customEventTTL);
return promise as Promise<ReturnType<TCE[TName]>>; return promise as Promise<ReturnType<TCE[TName]>>;
} }
@@ -296,31 +302,39 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
/* WebSocket Server Hooks */ /* WebSocket Server Hooks */
onSocketOpen( onSocketOpen(
ws: BaseWebSocket, ws: WebSocketLike,
serializedHTTPRequest: SerializedHTTPRequest, serializedHTTPRequest: SerializedHTTPRequest,
context = {},
) { ) {
const socketId = serializedHTTPRequest.headers['sec-websocket-key']!; const socketId = serializedHTTPRequest.headers['sec-websocket-key'];
this.originSockets[socketId] = ws; const clientConnection = this.instance.handleConnection(
this.instance.handleConnection( ws,
ws as any, toWebRequest(serializedHTTPRequest),
serializedHTTPRequest as any, this.deriveContext(serializedHTTPRequest),
context,
); );
this.originConnections[socketId] = { clientConnection, socket: ws };
} }
async onSocketMessage( async onSocketMessage(
ws: BaseWebSocket,
serializedHTTPRequest: SerializedHTTPRequest, serializedHTTPRequest: SerializedHTTPRequest,
detachableMsg: ArrayBuffer, detachableMsg: ArrayBuffer,
) { ) {
const message = new Uint8Array(detachableMsg.slice()); const message = new Uint8Array(detachableMsg.slice());
const tmpMsg = new IncomingMessage(detachableMsg); 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 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) { if (isDocLoadedOnInstance) {
ws.emit('message', message); clientConnection.handleMessage(message);
return; return;
} }
@@ -338,16 +352,17 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
return; return;
} }
// This server owns the document, but hocuspocus hasn't loaded it yet // 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) { onSocketClose(socketId: string, code?: number, reason?: ArrayBuffer) {
const socket = this.originSockets[socketId]; const entry = this.originConnections[socketId];
if (!socket) return; if (!entry) return;
// at this point the socket is considered GC'd and we cannot call close delete this.originConnections[socketId];
// The origin socket did not set up any connections for the proxy, so none of the hooks will work if we just emit entry.clientConnection.handleClose({
socket?.emit('close', code, reason); code: code ?? 1000,
delete this.originSockets[socketId]; reason: reason ? Buffer.from(reason).toString() : '',
});
const msg: RSAMessageCloseProxy = { type: 'closeProxy', socketId }; const msg: RSAMessageCloseProxy = { type: 'closeProxy', socketId };
this.pub.publish(this.msgChannel, this.pack(msg)).catch(() => {}); this.pub.publish(this.msgChannel, this.pack(msg)).catch(() => {});
} }
@@ -372,6 +387,7 @@ export class RedisSyncExtension<TCE extends CustomEvents> implements Extension {
} }
async onDestroy() { async onDestroy() {
this.pendingReplies = {};
this.pub.disconnect(false); this.pub.disconnect(false);
this.sub.disconnect(false); this.sub.disconnect(false);
} }
@@ -1,12 +1,13 @@
import EventEmitter from 'node:events';
import { IncomingHttpHeaders } from 'node:http2'; import { IncomingHttpHeaders } from 'node:http2';
import RedisClient from 'ioredis'; import RedisClient from 'ioredis';
import { CollabProxySocket } from './collab-proxy-socket';
import { type Hocuspocus, type WebSocketLike } from '@hocuspocus/server';
export type SecondParam<T> = T extends ( export type SecondParam<T> = T extends (
arg1: unknown, arg1: any,
arg2: infer A, arg2: infer A,
...args: unknown[] ...args: any[]
) => unknown ) => any
? A ? A
: never; : never;
@@ -41,12 +42,6 @@ export type RSAMessageClose = {
socketId: string; socketId: string;
}; };
export type RSAMessagePing = {
type: 'ping';
socketId: string;
replyTo: string;
};
export type RSAMessagePong = { export type RSAMessagePong = {
type: 'pong'; type: 'pong';
socketId: string; socketId: string;
@@ -59,7 +54,7 @@ export type RSAMessageSend = {
socketId: string; socketId: string;
}; };
export type RSAMessageCustomEventStart<TName = string, TPayload = unknown> = { export type RSAMessageCustomEventStart<TName = string, TPayload = any> = {
type: 'customEventStart'; type: 'customEventStart';
documentName: string; documentName: string;
eventName: TName; eventName: TName;
@@ -71,7 +66,7 @@ export type RSAMessageCustomEventStart<TName = string, TPayload = unknown> = {
export type RSAMessageCustomEventComplete = { export type RSAMessageCustomEventComplete = {
type: 'customEventComplete'; type: 'customEventComplete';
replyId: number; replyId: number;
payload: unknown; payload: any;
}; };
export type RSAMessage = export type RSAMessage =
@@ -79,7 +74,6 @@ export type RSAMessage =
| RSAMessageCloseProxy | RSAMessageCloseProxy
| RSAMessageUnload | RSAMessageUnload
| RSAMessageClose | RSAMessageClose
| RSAMessagePing
| RSAMessagePong | RSAMessagePong
| RSAMessageSend | RSAMessageSend
| RSAMessageCustomEventStart | RSAMessageCustomEventStart
@@ -99,9 +93,20 @@ type CustomEventName = string;
export type CustomEvents = Record< export type CustomEvents = Record<
CustomEventName, CustomEventName,
(documentName: string, payload: unknown) => Promise<unknown> (documentName: string, payload: any) => Promise<any>
>; >;
// Not exported by @hocuspocus/server
export type ClientConnection = ReturnType<Hocuspocus['handleConnection']>;
export type OriginConnection = {
clientConnection: ClientConnection;
socket: WebSocketLike;
};
export type ProxyConnection = {
clientConnection: ClientConnection;
socket: CollabProxySocket;
};
export interface Configuration<TCE> { export interface Configuration<TCE> {
redis: RedisClient; redis: RedisClient;
pack: Pack; pack: Pack;
@@ -111,11 +116,29 @@ export interface Configuration<TCE> {
customEventTTL?: number; customEventTTL?: number;
prefix?: string; prefix?: string;
customEvents?: TCE; 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<string, any>;
} }
export type BaseWebSocket = EventEmitter & { // Hocuspocus expects a web-standard Request, so rehydrate one from what crossed the wire
readyState: number; export const toWebRequest = (serializedHTTPRequest: SerializedHTTPRequest) => {
close(code?: number, reason?: string): void; const { method, url, headers } = serializedHTTPRequest;
ping(): void; const webHeaders = new Headers();
send(message: Uint8Array): void; 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,
});
}; };
@@ -1,20 +1,16 @@
import { EventEmitter } from 'events';
import type WebSocket from 'ws'; import type WebSocket from 'ws';
import type { WebSocketLike } from '@hocuspocus/server';
/** /**
* Wrapper around ws WebSocket that only receives events via emit(). * Wrapper around ws WebSocket that only receives events via emit().
* This prevents double-handling when used with RedisSyncExtension. * This prevents double-handling when used with RedisSyncExtension.
*/ */
export class WsSocketWrapper extends EventEmitter { export class WsSocketWrapper implements WebSocketLike {
private ws: WebSocket; private ws: WebSocket;
readyState = 1; readyState = 1;
constructor(ws: WebSocket) { constructor(ws: WebSocket) {
super();
this.ws = ws; this.ws = ws;
this.once('close', () => {
this.readyState = 3;
});
} }
close(code?: number, reason?: string) { 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) { send(message: Uint8Array) {
if (this.readyState !== 1) return; if (this.readyState !== 1) return;
try { try {
+3 -3
View File
@@ -23,9 +23,9 @@
"@casl/ability": "6.8.0", "@casl/ability": "6.8.0",
"@docmost/editor-ext": "workspace:*", "@docmost/editor-ext": "workspace:*",
"@floating-ui/dom": "1.7.3", "@floating-ui/dom": "1.7.3",
"@hocuspocus/provider": "3.4.4", "@hocuspocus/provider": "4.4.0",
"@hocuspocus/server": "3.4.4", "@hocuspocus/server": "4.4.0",
"@hocuspocus/transformer": "3.4.4", "@hocuspocus/transformer": "4.4.0",
"@joplin/turndown": "4.0.82", "@joplin/turndown": "4.0.82",
"@joplin/turndown-plugin-gfm": "1.0.64", "@joplin/turndown-plugin-gfm": "1.0.64",
"@sindresorhus/slugify": "3.0.0", "@sindresorhus/slugify": "3.0.0",
+33 -33
View File
@@ -65,14 +65,14 @@ importers:
specifier: 1.7.3 specifier: 1.7.3
version: 1.7.3 version: 1.7.3
'@hocuspocus/provider': '@hocuspocus/provider':
specifier: 3.4.4 specifier: 4.4.0
version: 3.4.4(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30) version: 4.4.0(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)
'@hocuspocus/server': '@hocuspocus/server':
specifier: 3.4.4 specifier: 4.4.0
version: 3.4.4(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30) version: 4.4.0(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30)
'@hocuspocus/transformer': '@hocuspocus/transformer':
specifier: 3.4.4 specifier: 4.4.0
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) 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': '@joplin/turndown':
specifier: 4.0.82 specifier: 4.0.82
version: 4.0.82 version: 4.0.82
@@ -2433,23 +2433,24 @@ packages:
'@floating-ui/utils@0.2.11': '@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@hocuspocus/common@3.4.4': '@hocuspocus/common@4.4.0':
resolution: {integrity: sha512-RykIJ0tsHHMP4Xk+4UCbc7SO5LgGxGUSTdbh6anJEsaALAyqinf1Nn5HYuMjLPolAmsar1v++m9zufR09NLpXA==} resolution: {integrity: sha512-cPSlPu/ws2JzBHW6OzctyaQMyDCq/YUS4nhXSbyMdkQwJzhjCkvfmmRnVtcmgLR3TBLaNwToNDwR/mNaJFOYsg==}
'@hocuspocus/provider@3.4.4': '@hocuspocus/provider@4.4.0':
resolution: {integrity: sha512-KbsMAfdYcIJD8eMU/5QnpXcSOvIWAcCNI33FSRSaKCIpYBFtAwkYIwWnZJmPZ8a1BMAtqQc+uvy9+UQf7GHnGQ==} resolution: {integrity: sha512-A83ROMFqU2bgjTTQ1YMteY9v2cIhDHj4S6XgzjPaqYEgOFr2XvBYGnK+NXzZ6DgldKTnlN7qqvWvZplPvltgyw==}
peerDependencies: peerDependencies:
y-protocols: ^1.0.6 y-protocols: ^1.0.6
yjs: ^13.6.8 yjs: ^13.6.8
'@hocuspocus/server@3.4.4': '@hocuspocus/server@4.4.0':
resolution: {integrity: sha512-UV+oaONAejOzeYgUygNcgsc8RdZvSokVvAxluZJIisLACpRO/VsseQ5lWKDRwLd7Fn6+rHWDH3hGuQ1fdX1Ycg==} resolution: {integrity: sha512-eVSnx+76CN81vaRol+OlT8FyyGFOBF9Z+kdJQhjVXRpj3kIAI1TzuRHKiZN1vkUogdHfmOAN5Bhs5v7V/qhMkQ==}
engines: {node: '>=22'}
peerDependencies: peerDependencies:
y-protocols: ^1.0.6 y-protocols: ^1.0.6
yjs: ^13.6.8 yjs: ^13.6.8
'@hocuspocus/transformer@3.4.4': '@hocuspocus/transformer@4.4.0':
resolution: {integrity: sha512-X0EJ863LV97YbL5m8WTt4NDSC6uHi6ZCq/teIH5aholdjdhdTmFrzMemdhha/ZqPUZyaKhOoAmZzwR55HZLPpQ==} resolution: {integrity: sha512-wvHgbiWfU1QERIMd37aImtb4xbHRGwek+66VPgFaSx3POgxCKmkZH5iVY25j/T0PBmloofPoV1XYbwLwJZMtJw==}
peerDependencies: peerDependencies:
'@tiptap/core': ^3.0.1 '@tiptap/core': ^3.0.1
'@tiptap/pm': ^3.0.1 '@tiptap/pm': ^3.0.1
@@ -5434,9 +5435,6 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'} engines: {node: '>=12'}
async-lock@1.4.1:
resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
async-mutex@0.5.0: async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
@@ -5953,6 +5951,14 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} 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: css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
@@ -12285,37 +12291,31 @@ snapshots:
'@floating-ui/utils@0.2.11': {} '@floating-ui/utils@0.2.11': {}
'@hocuspocus/common@3.4.4': '@hocuspocus/common@4.4.0':
dependencies: dependencies:
lib0: 0.2.117 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: dependencies:
'@hocuspocus/common': 3.4.4 '@hocuspocus/common': 4.4.0
'@lifeomic/attempt': 3.0.3 '@lifeomic/attempt': 3.0.3
lib0: 0.2.117 lib0: 0.2.117
ws: 8.21.0
y-protocols: 1.0.6(yjs@13.6.30) y-protocols: 1.0.6(yjs@13.6.30)
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: dependencies:
'@hocuspocus/common': 3.4.4 '@hocuspocus/common': 4.4.0
async-lock: 1.4.1
async-mutex: 0.5.0 async-mutex: 0.5.0
crossws: 0.4.10
kleur: 4.1.5 kleur: 4.1.5
lib0: 0.2.117 lib0: 0.2.117
ws: 8.21.0
y-protocols: 1.0.6(yjs@13.6.30) y-protocols: 1.0.6(yjs@13.6.30)
yjs: 13.6.30 yjs: 13.6.30
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - srvx
- utf-8-validate
'@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: dependencies:
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1) '@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
'@tiptap/pm': 3.27.1 '@tiptap/pm': 3.27.1
@@ -15603,8 +15603,6 @@ snapshots:
assertion-error@2.0.1: {} assertion-error@2.0.1: {}
async-lock@1.4.1: {}
async-mutex@0.5.0: async-mutex@0.5.0:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -16181,6 +16179,8 @@ snapshots:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
crossws@0.4.10: {}
css-select@5.1.0: css-select@5.1.0:
dependencies: dependencies:
boolbase: 1.0.0 boolbase: 1.0.0