⬆️(y-provider) update hocuspocus to 3.2.5
The last version of Blocknote seems to have a conflict with hocuspocus 2.15.2, it is a good moment to upgrade to hocuspocus 3.2.5.
This commit is contained in:
@@ -24,8 +24,6 @@
|
|||||||
"groupName": "ignored js dependencies",
|
"groupName": "ignored js dependencies",
|
||||||
"matchManagers": ["npm"],
|
"matchManagers": ["npm"],
|
||||||
"matchPackageNames": [
|
"matchPackageNames": [
|
||||||
"@hocuspocus/provider",
|
|
||||||
"@hocuspocus/server",
|
|
||||||
"docx",
|
"docx",
|
||||||
"fetch-mock",
|
"fetch-mock",
|
||||||
"node",
|
"node",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
"@fontsource/material-icons": "5.2.7",
|
"@fontsource/material-icons": "5.2.7",
|
||||||
"@gouvfr-lasuite/integration": "1.0.3",
|
"@gouvfr-lasuite/integration": "1.0.3",
|
||||||
"@gouvfr-lasuite/ui-kit": "0.16.2",
|
"@gouvfr-lasuite/ui-kit": "0.16.2",
|
||||||
"@hocuspocus/provider": "2.15.2",
|
"@hocuspocus/provider": "3.3.0",
|
||||||
"@mantine/core": "8.3.4",
|
"@mantine/core": "8.3.4",
|
||||||
"@mantine/hooks": "8.3.4",
|
"@mantine/hooks": "8.3.4",
|
||||||
"@openfun/cunningham-react": "3.2.3",
|
"@openfun/cunningham-react": "3.2.3",
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { Box, TextErrors } from '@/components';
|
import { Box, TextErrors } from '@/components';
|
||||||
import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management';
|
import {
|
||||||
|
Doc,
|
||||||
|
useIsCollaborativeEditable,
|
||||||
|
useProviderStore,
|
||||||
|
} from '@/docs/doc-management';
|
||||||
import { useAuth } from '@/features/auth';
|
import { useAuth } from '@/features/auth';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -79,9 +83,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { setEditor } = useEditorStore();
|
const { setEditor } = useEditorStore();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isSynced } = useProviderStore();
|
||||||
|
|
||||||
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
||||||
const isConnectedToCollabServer = provider.isSynced;
|
const isConnectedToCollabServer = isSynced;
|
||||||
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
|
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
|
||||||
const isDeletedDoc = !!doc.deleted_at;
|
const isDeletedDoc = !!doc.deleted_at;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { Box, Text, TextErrors } from '@/components';
|
import { Box, Loading, Text, TextErrors } from '@/components';
|
||||||
import { DocHeader, DocVersionHeader } from '@/docs/doc-header/';
|
import { DocHeader, DocVersionHeader } from '@/docs/doc-header/';
|
||||||
import {
|
import {
|
||||||
Doc,
|
Doc,
|
||||||
@@ -27,8 +27,9 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
|||||||
const isVersion = !!versionId && typeof versionId === 'string';
|
const isVersion = !!versionId && typeof versionId === 'string';
|
||||||
const { provider } = useProviderStore();
|
const { provider } = useProviderStore();
|
||||||
|
|
||||||
|
// TODO: Use skeleton instead of loading
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
return null;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CloseEvent } from '@hocuspocus/common';
|
||||||
import { HocuspocusProvider, WebSocketStatus } from '@hocuspocus/provider';
|
import { HocuspocusProvider, WebSocketStatus } from '@hocuspocus/provider';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
@@ -13,6 +14,7 @@ export interface UseCollaborationStore {
|
|||||||
destroyProvider: () => void;
|
destroyProvider: () => void;
|
||||||
provider: HocuspocusProvider | undefined;
|
provider: HocuspocusProvider | undefined;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
isSynced: boolean;
|
||||||
hasLostConnection: boolean;
|
hasLostConnection: boolean;
|
||||||
resetLostConnection: () => void;
|
resetLostConnection: () => void;
|
||||||
}
|
}
|
||||||
@@ -20,9 +22,12 @@ export interface UseCollaborationStore {
|
|||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
provider: undefined,
|
provider: undefined,
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
|
isSynced: false,
|
||||||
hasLostConnection: false,
|
hasLostConnection: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ExtendedCloseEvent = CloseEvent & { wasClean: boolean };
|
||||||
|
|
||||||
export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
|
export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
createProvider: (wsUrl, storeId, initialDoc) => {
|
createProvider: (wsUrl, storeId, initialDoc) => {
|
||||||
@@ -38,6 +43,12 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
|
|||||||
url: wsUrl,
|
url: wsUrl,
|
||||||
name: storeId,
|
name: storeId,
|
||||||
document: doc,
|
document: doc,
|
||||||
|
onDisconnect(data) {
|
||||||
|
// Attempt to reconnect if the disconnection was clean (initiated by the client or server)
|
||||||
|
if ((data.event as ExtendedCloseEvent).wasClean) {
|
||||||
|
provider.connect();
|
||||||
|
}
|
||||||
|
},
|
||||||
onStatus: ({ status }) => {
|
onStatus: ({ status }) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const nextConnected = status === WebSocketStatus.Connected;
|
const nextConnected = status === WebSocketStatus.Connected;
|
||||||
@@ -50,6 +61,21 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onSynced: ({ state }) => {
|
||||||
|
set({ isSynced: state });
|
||||||
|
},
|
||||||
|
onClose(data) {
|
||||||
|
/**
|
||||||
|
* Handle the "Reset Connection" event from the server
|
||||||
|
* This is triggered when the server wants to reset the connection
|
||||||
|
* for clients in the room.
|
||||||
|
* A disconnect is made automatically but it takes time to be triggered,
|
||||||
|
* so we force the disconnection here.
|
||||||
|
*/
|
||||||
|
if (data.event.code === 1000) {
|
||||||
|
provider.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
set({
|
set({
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
|
|
||||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
import { Access, KEY_DOC, KEY_LIST_DOC, Role } from '@/docs/doc-management';
|
import { Access, KEY_DOC, KEY_LIST_DOC, Role } from '@/docs/doc-management';
|
||||||
import { useBroadcastStore } from '@/stores';
|
|
||||||
|
|
||||||
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
||||||
|
|
||||||
@@ -45,7 +44,6 @@ type UseUpdateDocAccessOptions = UseMutationOptions<
|
|||||||
|
|
||||||
export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { broadcast } = useBroadcastStore();
|
|
||||||
|
|
||||||
return useMutation<Access, APIError, UpdateDocAccessProps>({
|
return useMutation<Access, APIError, UpdateDocAccessProps>({
|
||||||
mutationFn: updateDocAccess,
|
mutationFn: updateDocAccess,
|
||||||
@@ -58,12 +56,10 @@ export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
|
|||||||
queryKey: [KEY_DOC],
|
queryKey: [KEY_DOC],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Broadcast to every user connected to the document
|
|
||||||
broadcast(`${KEY_DOC}-${variables.docId}`);
|
|
||||||
|
|
||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: [KEY_LIST_DOC],
|
queryKey: [KEY_LIST_DOC],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options?.onSuccess) {
|
if (options?.onSuccess) {
|
||||||
void options.onSuccess(data, variables, onMutateResult, context);
|
void options.onSuccess(data, variables, onMutateResult, context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ const DocPage = ({ id }: DocProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addTask(`${KEY_DOC}-${doc.id}`, () => {
|
addTask(`${KEY_DOC}-${doc.id}`, () => {
|
||||||
void queryClient.resetQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: [KEY_DOC, { id: doc.id }],
|
queryKey: [KEY_DOC, { id: doc.id }],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('Server Tests', () => {
|
|||||||
|
|
||||||
test('POST /collaboration/api/reset-connections?room=[ROOM_ID] with correct API key should reset connections', async () => {
|
test('POST /collaboration/api/reset-connections?room=[ROOM_ID] with correct API key should reset connections', async () => {
|
||||||
const closeConnectionsMock = vi
|
const closeConnectionsMock = vi
|
||||||
.spyOn(hocuspocusServer, 'closeConnections')
|
.spyOn(hocuspocusServer.hocuspocus, 'closeConnections')
|
||||||
.mockResolvedValue();
|
.mockResolvedValue();
|
||||||
|
|
||||||
const app = initApp();
|
const app = initApp();
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ describe('Server Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('POST /collaboration/api/get-connections?room=[ROOM_ID] returns connection info, session key existing', async () => {
|
test('POST /collaboration/api/get-connections?room=[ROOM_ID] returns connection info, session key existing', async () => {
|
||||||
const document = await hocuspocusServer.createDocument(
|
const document = await hocuspocusServer.hocuspocus.createDocument(
|
||||||
'test-room',
|
'test-room',
|
||||||
{},
|
{},
|
||||||
uuid(),
|
uuid(),
|
||||||
{ isAuthenticated: true, readOnly: false, requiresAuthentication: true },
|
{ isAuthenticated: true, readOnly: false },
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -138,11 +138,11 @@ describe('Server Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('POST /collaboration/api/get-connections?room=[ROOM_ID] returns connection info, session key not existing', async () => {
|
test('POST /collaboration/api/get-connections?room=[ROOM_ID] returns connection info, session key not existing', async () => {
|
||||||
const document = await hocuspocusServer.createDocument(
|
const document = await hocuspocusServer.hocuspocus.createDocument(
|
||||||
'test-room',
|
'test-room',
|
||||||
{},
|
{},
|
||||||
uuid(),
|
uuid(),
|
||||||
{ isAuthenticated: true, readOnly: false, requiresAuthentication: true },
|
{ isAuthenticated: true, readOnly: false },
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -206,11 +206,11 @@ describe('Server Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('POST /collaboration/api/get-connections?room=[ROOM_ID] returns connection info, session key not existing, read only connection', async () => {
|
test('POST /collaboration/api/get-connections?room=[ROOM_ID] returns connection info, session key not existing, read only connection', async () => {
|
||||||
const document = await hocuspocusServer.createDocument(
|
const document = await hocuspocusServer.hocuspocus.createDocument(
|
||||||
'test-room',
|
'test-room',
|
||||||
{},
|
{},
|
||||||
uuid(),
|
uuid(),
|
||||||
{ isAuthenticated: true, readOnly: false, requiresAuthentication: true },
|
{ isAuthenticated: true, readOnly: false },
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ describe('Server Tests', () => {
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
server = initApp().listen(port);
|
server = initApp().listen(port);
|
||||||
await hocuspocusServer.configure({ port: portWS }).listen();
|
await hocuspocusServer.listen(portWS);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -62,14 +62,11 @@ describe('Server Tests', () => {
|
|||||||
test('WebSocket connection with bad origin should be closed', () => {
|
test('WebSocket connection with bad origin should be closed', () => {
|
||||||
const { promise, done } = promiseDone();
|
const { promise, done } = promiseDone();
|
||||||
const room = uuidv4();
|
const room = uuidv4();
|
||||||
const ws = new WebSocket(
|
const ws = new WebSocket(`ws://localhost:${port}/?room=${room}`, {
|
||||||
`ws://localhost:${port}/collaboration/ws/?room=${room}`,
|
headers: {
|
||||||
{
|
Origin: 'http://bad-origin.com',
|
||||||
headers: {
|
|
||||||
Origin: 'http://bad-origin.com',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
expect(ws.readyState).toBe(ws.CLOSED);
|
expect(ws.readyState).toBe(ws.CLOSED);
|
||||||
@@ -82,14 +79,11 @@ describe('Server Tests', () => {
|
|||||||
test('WebSocket connection without cookies header should be closed', () => {
|
test('WebSocket connection without cookies header should be closed', () => {
|
||||||
const { promise, done } = promiseDone();
|
const { promise, done } = promiseDone();
|
||||||
const room = uuidv4();
|
const room = uuidv4();
|
||||||
const ws = new WebSocket(
|
const ws = new WebSocket(`ws://localhost:${port}/?room=${room}`, {
|
||||||
`ws://localhost:${port}/collaboration/ws/?room=${room}`,
|
headers: {
|
||||||
{
|
Origin: origin,
|
||||||
headers: {
|
|
||||||
Origin: origin,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
expect(ws.readyState).toBe(ws.CLOSED);
|
expect(ws.readyState).toBe(ws.CLOSED);
|
||||||
@@ -106,17 +100,13 @@ describe('Server Tests', () => {
|
|||||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||||
WebSocketPolyfill: WebSocket,
|
WebSocketPolyfill: WebSocket,
|
||||||
maxAttempts: 1,
|
maxAttempts: 1,
|
||||||
quiet: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const providerName = uuidv4();
|
const providerName = uuidv4();
|
||||||
const provider = new HocuspocusProvider({
|
const provider = new HocuspocusProvider({
|
||||||
websocketProvider: wsHocus,
|
websocketProvider: wsHocus,
|
||||||
name: providerName,
|
name: providerName,
|
||||||
broadcast: false,
|
onAuthenticationFailed(data) {
|
||||||
quiet: true,
|
|
||||||
preserveConnection: false,
|
|
||||||
onClose: (data) => {
|
|
||||||
expect(console.log).toHaveBeenCalledWith(
|
expect(console.log).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
' --- ',
|
' --- ',
|
||||||
@@ -126,7 +116,7 @@ describe('Server Tests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
wsHocus.stopConnectionAttempt();
|
wsHocus.stopConnectionAttempt();
|
||||||
expect(data.event.reason).toBe('Forbidden');
|
expect(data.reason).toBe('permission-denied');
|
||||||
wsHocus.webSocket?.close();
|
wsHocus.webSocket?.close();
|
||||||
wsHocus.disconnect();
|
wsHocus.disconnect();
|
||||||
provider.destroy();
|
provider.destroy();
|
||||||
@@ -135,6 +125,8 @@ describe('Server Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
provider.attach();
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,16 +137,12 @@ describe('Server Tests', () => {
|
|||||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||||
WebSocketPolyfill: WebSocket,
|
WebSocketPolyfill: WebSocket,
|
||||||
maxAttempts: 1,
|
maxAttempts: 1,
|
||||||
quiet: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const provider = new HocuspocusProvider({
|
const provider = new HocuspocusProvider({
|
||||||
websocketProvider: wsHocus,
|
websocketProvider: wsHocus,
|
||||||
name: room,
|
name: room,
|
||||||
broadcast: false,
|
onAuthenticationFailed: (data) => {
|
||||||
quiet: true,
|
|
||||||
preserveConnection: false,
|
|
||||||
onClose: (data) => {
|
|
||||||
expect(console.log).toHaveBeenLastCalledWith(
|
expect(console.log).toHaveBeenLastCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
' --- ',
|
' --- ',
|
||||||
@@ -163,7 +151,7 @@ describe('Server Tests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
wsHocus.stopConnectionAttempt();
|
wsHocus.stopConnectionAttempt();
|
||||||
expect(data.event.reason).toBe('Forbidden');
|
expect(data.reason).toBe('permission-denied');
|
||||||
wsHocus.webSocket?.close();
|
wsHocus.webSocket?.close();
|
||||||
wsHocus.disconnect();
|
wsHocus.disconnect();
|
||||||
provider.destroy();
|
provider.destroy();
|
||||||
@@ -172,6 +160,8 @@ describe('Server Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
provider.attach();
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,16 +172,12 @@ describe('Server Tests', () => {
|
|||||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||||
WebSocketPolyfill: WebSocket,
|
WebSocketPolyfill: WebSocket,
|
||||||
maxAttempts: 1,
|
maxAttempts: 1,
|
||||||
quiet: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const provider = new HocuspocusProvider({
|
const provider = new HocuspocusProvider({
|
||||||
websocketProvider: wsHocus,
|
websocketProvider: wsHocus,
|
||||||
name: room,
|
name: room,
|
||||||
broadcast: false,
|
onAuthenticationFailed: (data) => {
|
||||||
quiet: true,
|
|
||||||
preserveConnection: false,
|
|
||||||
onClose: (data) => {
|
|
||||||
expect(console.log).toHaveBeenLastCalledWith(
|
expect(console.log).toHaveBeenLastCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
' --- ',
|
' --- ',
|
||||||
@@ -200,7 +186,7 @@ describe('Server Tests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
wsHocus.stopConnectionAttempt();
|
wsHocus.stopConnectionAttempt();
|
||||||
expect(data.event.reason).toBe('Forbidden');
|
expect(data.reason).toBe('permission-denied');
|
||||||
wsHocus.webSocket?.close();
|
wsHocus.webSocket?.close();
|
||||||
wsHocus.disconnect();
|
wsHocus.disconnect();
|
||||||
provider.destroy();
|
provider.destroy();
|
||||||
@@ -209,6 +195,8 @@ describe('Server Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
provider.attach();
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,23 +212,19 @@ describe('Server Tests', () => {
|
|||||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||||
WebSocketPolyfill: WebSocket,
|
WebSocketPolyfill: WebSocket,
|
||||||
maxAttempts: 1,
|
maxAttempts: 1,
|
||||||
quiet: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const provider = new HocuspocusProvider({
|
const provider = new HocuspocusProvider({
|
||||||
websocketProvider: wsHocus,
|
websocketProvider: wsHocus,
|
||||||
name: room,
|
name: room,
|
||||||
broadcast: false,
|
onAuthenticationFailed: (data) => {
|
||||||
quiet: true,
|
|
||||||
preserveConnection: false,
|
|
||||||
onClose: (data) => {
|
|
||||||
expect(console.error).toHaveBeenLastCalledWith(
|
expect(console.error).toHaveBeenLastCalledWith(
|
||||||
'[onConnect]',
|
'[onConnect]',
|
||||||
'Backend error: Unauthorized',
|
'Backend error: Unauthorized',
|
||||||
);
|
);
|
||||||
|
|
||||||
wsHocus.stopConnectionAttempt();
|
wsHocus.stopConnectionAttempt();
|
||||||
expect(data.event.reason).toBe('Forbidden');
|
expect(data.reason).toBe('permission-denied');
|
||||||
expect(fetchDocumentMock).toHaveBeenCalledExactlyOnceWith(
|
expect(fetchDocumentMock).toHaveBeenCalledExactlyOnceWith(
|
||||||
room,
|
room,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@@ -253,6 +237,8 @@ describe('Server Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
provider.attach();
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -269,16 +255,12 @@ describe('Server Tests', () => {
|
|||||||
url: `ws://localhost:${portWS}/?room=${room}`,
|
url: `ws://localhost:${portWS}/?room=${room}`,
|
||||||
WebSocketPolyfill: WebSocket,
|
WebSocketPolyfill: WebSocket,
|
||||||
maxAttempts: 1,
|
maxAttempts: 1,
|
||||||
quiet: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const provider = new HocuspocusProvider({
|
const provider = new HocuspocusProvider({
|
||||||
websocketProvider: wsHocus,
|
websocketProvider: wsHocus,
|
||||||
name: room,
|
name: room,
|
||||||
broadcast: false,
|
onAuthenticationFailed: (data) => {
|
||||||
quiet: true,
|
|
||||||
preserveConnection: false,
|
|
||||||
onClose: (data) => {
|
|
||||||
expect(console.log).toHaveBeenLastCalledWith(
|
expect(console.log).toHaveBeenLastCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
' --- ',
|
' --- ',
|
||||||
@@ -287,7 +269,7 @@ describe('Server Tests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
wsHocus.stopConnectionAttempt();
|
wsHocus.stopConnectionAttempt();
|
||||||
expect(data.event.reason).toBe('Forbidden');
|
expect(data.reason).toBe('permission-denied');
|
||||||
expect(fetchDocumentMock).toHaveBeenCalledExactlyOnceWith(
|
expect(fetchDocumentMock).toHaveBeenCalledExactlyOnceWith(
|
||||||
room,
|
room,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@@ -300,6 +282,8 @@ describe('Server Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
provider.attach();
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -322,10 +306,8 @@ describe('Server Tests', () => {
|
|||||||
const provider = new HocuspocusProvider({
|
const provider = new HocuspocusProvider({
|
||||||
websocketProvider: wsHocus,
|
websocketProvider: wsHocus,
|
||||||
name: room,
|
name: room,
|
||||||
broadcast: false,
|
|
||||||
quiet: true,
|
|
||||||
onConnect: () => {
|
onConnect: () => {
|
||||||
void hocuspocusServer
|
void hocuspocusServer.hocuspocus
|
||||||
.openDirectConnection(room)
|
.openDirectConnection(room)
|
||||||
.then((connection) => {
|
.then((connection) => {
|
||||||
connection.document?.getConnections().forEach((connection) => {
|
connection.document?.getConnections().forEach((connection) => {
|
||||||
@@ -347,6 +329,8 @@ describe('Server Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
provider.attach();
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -373,30 +357,30 @@ describe('Server Tests', () => {
|
|||||||
const provider = new HocuspocusProvider({
|
const provider = new HocuspocusProvider({
|
||||||
websocketProvider: wsHocus,
|
websocketProvider: wsHocus,
|
||||||
name: room,
|
name: room,
|
||||||
broadcast: false,
|
|
||||||
quiet: true,
|
|
||||||
onConnect: () => {
|
onConnect: () => {
|
||||||
void hocuspocusServer.openDirectConnection(room).then((connection) => {
|
const document = hocuspocusServer.hocuspocus.documents.get(room);
|
||||||
connection.document?.getConnections().forEach((connection) => {
|
if (document) {
|
||||||
|
document.getConnections().forEach((connection) => {
|
||||||
expect(connection.context.userId).toBe('test-user-id');
|
expect(connection.context.userId).toBe('test-user-id');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void connection.disconnect();
|
provider.destroy();
|
||||||
provider.destroy();
|
wsHocus.destroy();
|
||||||
wsHocus.destroy();
|
|
||||||
|
|
||||||
expect(fetchDocumentMock).toHaveBeenCalledWith(
|
expect(fetchDocumentMock).toHaveBeenCalledWith(
|
||||||
room,
|
room,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(fetchCurrentUserMock).toHaveBeenCalled();
|
expect(fetchCurrentUserMock).toHaveBeenCalled();
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
provider.attach();
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
"dev": "cross-env COLLABORATION_LOGGING=true && nodemon --config nodemon.json",
|
"dev": "cross-env COLLABORATION_LOGGING=true && nodemon --config nodemon.json",
|
||||||
"start": "node ./dist/start-server.js",
|
"start": "node ./dist/start-server.js",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"test": "vitest --run --disable-console-intercept"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blocknote/server-util": "0.41.1",
|
"@blocknote/server-util": "0.41.1",
|
||||||
"@hocuspocus/server": "2.15.2",
|
"@hocuspocus/server": "3.3.0",
|
||||||
"@sentry/node": "10.17.0",
|
"@sentry/node": "10.17.0",
|
||||||
"@sentry/profiling-node": "10.17.0",
|
"@sentry/profiling-node": "10.17.0",
|
||||||
"@tiptap/extensions": "*",
|
"@tiptap/extensions": "*",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blocknote/core": "0.41.1",
|
"@blocknote/core": "0.41.1",
|
||||||
"@hocuspocus/provider": "2.15.2",
|
"@hocuspocus/provider": "3.3.0",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
"@types/express": "5.0.3",
|
"@types/express": "5.0.3",
|
||||||
"@types/express-ws": "3.0.5",
|
"@types/express-ws": "3.0.5",
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ export const collaborationResetConnectionsHandler = (
|
|||||||
* If no user ID is provided, close all connections in the room
|
* If no user ID is provided, close all connections in the room
|
||||||
*/
|
*/
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
hocuspocusServer.closeConnections(room);
|
hocuspocusServer.hocuspocus.closeConnections(room);
|
||||||
} else {
|
} else {
|
||||||
/**
|
/**
|
||||||
* Close connections for the user in the room
|
* Close connections for the user in the room
|
||||||
*/
|
*/
|
||||||
hocuspocusServer.documents.forEach((doc) => {
|
hocuspocusServer.hocuspocus.documents.forEach((doc) => {
|
||||||
if (doc.name !== room) {
|
if (doc.name !== room) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { hocuspocusServer } from '@/servers/hocuspocusServer';
|
|||||||
|
|
||||||
export const collaborationWSHandler = (ws: ws.WebSocket, req: Request) => {
|
export const collaborationWSHandler = (ws: ws.WebSocket, req: Request) => {
|
||||||
try {
|
try {
|
||||||
hocuspocusServer.handleConnection(ws, req);
|
hocuspocusServer.hocuspocus.handleConnection(ws, req);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to handle WebSocket connection:', error);
|
console.error('Failed to handle WebSocket connection:', error);
|
||||||
ws.close();
|
ws.close();
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const getDocumentConnectionInfoHandler = (
|
|||||||
|
|
||||||
logger('Getting document connection info for room:', room);
|
logger('Getting document connection info for room:', room);
|
||||||
|
|
||||||
const roomInfo = hocuspocusServer.documents.get(room);
|
const roomInfo = hocuspocusServer.hocuspocus.documents.get(room);
|
||||||
|
|
||||||
if (!roomInfo) {
|
if (!roomInfo) {
|
||||||
logger('Room not found:', room);
|
logger('Room not found:', room);
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { validate as uuidValidate, version as uuidVersion } from 'uuid';
|
|||||||
import { fetchCurrentUser, fetchDocument } from '@/api/collaborationBackend';
|
import { fetchCurrentUser, fetchDocument } from '@/api/collaborationBackend';
|
||||||
import { logger } from '@/utils';
|
import { logger } from '@/utils';
|
||||||
|
|
||||||
export const hocuspocusServer = Server.configure({
|
export const hocuspocusServer = new Server({
|
||||||
name: 'docs-collaboration',
|
name: 'docs-collaboration',
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
quiet: true,
|
quiet: true,
|
||||||
async onConnect({
|
async onConnect({
|
||||||
requestHeaders,
|
requestHeaders,
|
||||||
connection,
|
connectionConfig,
|
||||||
documentName,
|
documentName,
|
||||||
requestParameters,
|
requestParameters,
|
||||||
context,
|
context,
|
||||||
@@ -58,7 +58,7 @@ export const hocuspocusServer = Server.configure({
|
|||||||
return Promise.reject(new Error('Backend error: Unauthorized'));
|
return Promise.reject(new Error('Backend error: Unauthorized'));
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.readOnly = !canEdit;
|
connectionConfig.readOnly = !canEdit;
|
||||||
|
|
||||||
const session = requestHeaders['cookie']
|
const session = requestHeaders['cookie']
|
||||||
?.split('; ')
|
?.split('; ')
|
||||||
|
|||||||
@@ -1787,33 +1787,33 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-negated-glob "^1.0.0"
|
is-negated-glob "^1.0.0"
|
||||||
|
|
||||||
"@hocuspocus/common@^2.15.2":
|
"@hocuspocus/common@^3.3.0":
|
||||||
version "2.15.3"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@hocuspocus/common/-/common-2.15.3.tgz#c5e6a326141b2ceaf49d555fd35917211340ceee"
|
resolved "https://registry.yarnpkg.com/@hocuspocus/common/-/common-3.3.0.tgz#73e6489213d4c50c4521f511e2ee3a507035be42"
|
||||||
integrity sha512-Rzh1HF0a2o/tf90A3w2XNdXd9Ym3aQzMDfD3lAUONCX9B9QOdqdyiORrj6M25QEaJrEIbXFy8LtAFcL0wRdWzA==
|
integrity sha512-BzLFs0Wbwt0RV1kBrtIAf7xludxKMcPBpLGAGX/5TZazx8kW1fmJpjRM3QgRmqO7R6QVyfi6h8MQUw/P43tafw==
|
||||||
dependencies:
|
dependencies:
|
||||||
lib0 "^0.2.87"
|
lib0 "^0.2.87"
|
||||||
|
|
||||||
"@hocuspocus/provider@2.15.2":
|
"@hocuspocus/provider@3.3.0":
|
||||||
version "2.15.2"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@hocuspocus/provider/-/provider-2.15.2.tgz#ce534b90e161cd32fd02928d7296bb38f424c319"
|
resolved "https://registry.yarnpkg.com/@hocuspocus/provider/-/provider-3.3.0.tgz#9b1cc15158c74a344fb324c3988b5fd41464c98d"
|
||||||
integrity sha512-mdBurviyaUd7bQx4vMIE39WqRJDTpfFelHOVXr7w/jA8G1E7K7lxQ9/DacSrbg+9o8s+1z1+SerZiUjaToaBJg==
|
integrity sha512-gxwWAZ8E55VxwdbFwhpjYxFBOxg96+fxkvkguqSJ59chkieDtrZca/0G9ZO4ct/BBq+FNxxYIdAxwwSaM/EOFg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@hocuspocus/common" "^2.15.2"
|
"@hocuspocus/common" "^3.3.0"
|
||||||
"@lifeomic/attempt" "^3.0.2"
|
"@lifeomic/attempt" "^3.0.2"
|
||||||
lib0 "^0.2.87"
|
lib0 "^0.2.87"
|
||||||
ws "^8.17.1"
|
ws "^8.17.1"
|
||||||
|
|
||||||
"@hocuspocus/server@2.15.2":
|
"@hocuspocus/server@3.3.0":
|
||||||
version "2.15.2"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@hocuspocus/server/-/server-2.15.2.tgz#77be1e2540843bec313141dad7f39ea65a4221dd"
|
resolved "https://registry.yarnpkg.com/@hocuspocus/server/-/server-3.3.0.tgz#afa0758a96c2d1af759a1166b9e0be05f1656ca9"
|
||||||
integrity sha512-+fLRVswg+bkgfHqJ+wFgywivw3H08WMOtVvJF7dJzWT2ZR/Sc3nDMFh2KqMF6Ygh4z6mt23xr7SKIm3eP1zoLA==
|
integrity sha512-kPsoKfjRD3JvDaViDVz8utK1lI+GvpyubAs9SKNCFOpaTJD7RlTj9bjwUfL8FJ6/tC/p4HuS8etSYOn+WeVwuw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@hocuspocus/common" "^2.15.2"
|
"@hocuspocus/common" "^3.3.0"
|
||||||
async-lock "^1.3.1"
|
async-lock "^1.3.1"
|
||||||
|
async-mutex "^0.5.0"
|
||||||
kleur "^4.1.4"
|
kleur "^4.1.4"
|
||||||
lib0 "^0.2.47"
|
lib0 "^0.2.47"
|
||||||
uuid "^11.0.3"
|
|
||||||
ws "^8.5.0"
|
ws "^8.5.0"
|
||||||
|
|
||||||
"@humanfs/core@^0.19.1":
|
"@humanfs/core@^0.19.1":
|
||||||
@@ -6560,6 +6560,13 @@ async-lock@^1.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.4.1.tgz#56b8718915a9b68b10fce2f2a9a3dddf765ef53f"
|
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.4.1.tgz#56b8718915a9b68b10fce2f2a9a3dddf765ef53f"
|
||||||
integrity sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==
|
integrity sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==
|
||||||
|
|
||||||
|
async-mutex@^0.5.0:
|
||||||
|
version "0.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.5.0.tgz#353c69a0b9e75250971a64ac203b0ebfddd75482"
|
||||||
|
integrity sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.4.0"
|
||||||
|
|
||||||
async@^3.2.6:
|
async@^3.2.6:
|
||||||
version "3.2.6"
|
version "3.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
|
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
|
||||||
@@ -14709,11 +14716,6 @@ uuid@13.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-13.0.0.tgz#263dc341b19b4d755eb8fe36b78d95a6b65707e8"
|
||||||
integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==
|
integrity sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==
|
||||||
|
|
||||||
uuid@^11.0.3:
|
|
||||||
version "11.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912"
|
|
||||||
integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
|
|
||||||
|
|
||||||
uuid@^8.3.2:
|
uuid@^8.3.2:
|
||||||
version "8.3.2"
|
version "8.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
|
|||||||
Reference in New Issue
Block a user