♻️(frontend) create useProviderStore

We created useProviderStore, a store dedicated
to managing the provider of the document.
We created as well a new hook useCollaboration,
it will be use to interact with the provider store.
This refacto is a first step to implement
the long polling.
This commit is contained in:
Anthony LC
2024-12-23 11:07:55 +01:00
committed by Anthony LC
parent 6cb2702e6b
commit 02a4740c66
10 changed files with 127 additions and 74 deletions

View File

@@ -16,6 +16,7 @@ and this project adheres to
## Changed ## Changed
- 🏗️(yjs-server) organize yjs server #528 - 🏗️(yjs-server) organize yjs server #528
- ♻️(frontend) better separation collaboration process #528
## [1.10.0] - 2024-12-17 ## [1.10.0] - 2024-12-17

View File

@@ -41,7 +41,7 @@ export const createDoc = async (
.click(); .click();
await page.getByRole('heading', { name: 'Untitled document' }).click(); await page.getByRole('heading', { name: 'Untitled document' }).click();
await page.keyboard.type(randomDocs[i]); await page.keyboard.type(randomDocs[i], { delay: 100 });
await page.getByText('Created at ').click(); await page.getByText('Created at ').click();
} }

View File

@@ -10,7 +10,7 @@ import { DocHeader } from '@/features/docs/doc-header';
import { import {
Doc, Doc,
base64ToBlocknoteXmlFragment, base64ToBlocknoteXmlFragment,
useDocStore, useProviderStore,
} from '@/features/docs/doc-management'; } from '@/features/docs/doc-management';
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/'; import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
import { useResponsiveStore } from '@/stores'; import { useResponsiveStore } from '@/stores';
@@ -33,8 +33,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const { providers } = useDocStore(); const { provider } = useProviderStore();
const provider = providers?.[doc.id];
if (!provider) { if (!provider) {
return null; return null;

View File

@@ -1 +1,2 @@
export * from './useCollaboration';
export * from './useTrans'; export * from './useTrans';

View File

@@ -0,0 +1,35 @@
import { useEffect } from 'react';
import { useCollaborationUrl } from '@/core/config';
import { useBroadcastStore } from '@/stores';
import { useProviderStore } from '../stores/useProviderStore';
import { Base64 } from '../types';
export const useCollaboration = (room?: string, initialContent?: Base64) => {
const collaborationUrl = useCollaborationUrl(room);
const { setBroadcastProvider } = useBroadcastStore();
const { provider, createProvider, destroyProvider } = useProviderStore();
useEffect(() => {
if (!room || !collaborationUrl || provider) {
return;
}
const newProvider = createProvider(collaborationUrl, room, initialContent);
setBroadcastProvider(newProvider);
}, [
provider,
collaborationUrl,
room,
initialContent,
createProvider,
setBroadcastProvider,
]);
useEffect(() => {
return () => {
destroyProvider();
};
}, [destroyProvider]);
};

View File

@@ -1 +1,2 @@
export * from './useDocStore'; export * from './useDocStore';
export * from './useProviderStore';

View File

@@ -1,53 +1,14 @@
import { HocuspocusProvider } from '@hocuspocus/provider';
import * as Y from 'yjs';
import { create } from 'zustand'; import { create } from 'zustand';
import { Base64, Doc } from '@/features/docs/doc-management'; import { Doc } from '@/features/docs/doc-management';
export interface UseDocStore { export interface UseDocStore {
currentDoc?: Doc; currentDoc?: Doc;
providers: {
[storeId: string]: HocuspocusProvider;
};
createProvider: (
providerUrl: string,
storeId: string,
initialDoc: Base64,
) => HocuspocusProvider;
setProviders: (storeId: string, providers: HocuspocusProvider) => void;
setCurrentDoc: (doc: Doc | undefined) => void; setCurrentDoc: (doc: Doc | undefined) => void;
} }
export const useDocStore = create<UseDocStore>((set, get) => ({ export const useDocStore = create<UseDocStore>((set) => ({
currentDoc: undefined, currentDoc: undefined,
providers: {},
createProvider: (providerUrl, storeId, initialDoc) => {
const doc = new Y.Doc({
guid: storeId,
});
if (initialDoc) {
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
}
const provider = new HocuspocusProvider({
url: providerUrl,
name: storeId,
document: doc,
});
get().setProviders(storeId, provider);
return provider;
},
setProviders: (storeId, provider) => {
set(({ providers }) => ({
providers: {
...providers,
[storeId]: provider,
},
}));
},
setCurrentDoc: (doc) => { setCurrentDoc: (doc) => {
set({ currentDoc: doc }); set({ currentDoc: doc });
}, },

View File

@@ -0,0 +1,52 @@
import { HocuspocusProvider } from '@hocuspocus/provider';
import * as Y from 'yjs';
import { create } from 'zustand';
import { Base64 } from '@/features/docs/doc-management';
export interface UseCollaborationStore {
createProvider: (
providerUrl: string,
storeId: string,
initialDoc?: Base64,
) => HocuspocusProvider;
destroyProvider: () => void;
provider: HocuspocusProvider | undefined;
}
const defaultValues = {
provider: undefined,
};
export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
...defaultValues,
createProvider: (wsUrl, storeId, initialDoc) => {
const doc = new Y.Doc({
guid: storeId,
});
if (initialDoc) {
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
}
const provider = new HocuspocusProvider({
url: wsUrl,
name: storeId,
document: doc,
});
set({
provider,
});
return provider;
},
destroyProvider: () => {
const provider = get().provider;
if (provider) {
provider.destroy();
}
set(defaultValues);
},
}));

View File

@@ -13,9 +13,9 @@ import { Box, Text } from '@/components';
import { import {
Doc, Doc,
base64ToYDoc, base64ToYDoc,
useDocStore, useProviderStore,
useUpdateDoc, useUpdateDoc,
} from '@/features/docs/doc-management'; } from '@/features/docs/doc-management/';
import { useDocVersion } from '../api'; import { useDocVersion } from '../api';
import { KEY_LIST_DOC_VERSIONS } from '../api/useDocVersions'; import { KEY_LIST_DOC_VERSIONS } from '../api/useDocVersions';
@@ -40,7 +40,7 @@ export const ModalVersion = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToastProvider(); const { toast } = useToastProvider();
const { push } = useRouter(); const { push } = useRouter();
const { providers } = useDocStore(); const { provider } = useProviderStore();
const { mutate: updateDoc } = useUpdateDoc({ const { mutate: updateDoc } = useUpdateDoc({
listInvalideQueries: [KEY_LIST_DOC_VERSIONS], listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
onSuccess: () => { onSuccess: () => {
@@ -49,14 +49,14 @@ export const ModalVersion = ({
void push(`/docs/${docId}`); void push(`/docs/${docId}`);
}; };
if (!providers?.[docId] || !version?.content) { if (!provider || !version?.content) {
onDisplaySuccess(); onDisplaySuccess();
return; return;
} }
revertUpdate( revertUpdate(
providers[docId].document, provider.document,
providers[docId].document, provider.document,
base64ToYDoc(version.content), base64ToYDoc(version.content),
); );

View File

@@ -6,10 +6,15 @@ import { useEffect, useState } from 'react';
import { Box, Text } from '@/components'; import { Box, Text } from '@/components';
import { TextErrors } from '@/components/TextErrors'; import { TextErrors } from '@/components/TextErrors';
import { useCollaborationUrl } from '@/core';
import { useAuthStore } from '@/core/auth'; import { useAuthStore } from '@/core/auth';
import { DocEditor } from '@/features/docs/doc-editor'; import { DocEditor } from '@/features/docs/doc-editor';
import { KEY_DOC, useDoc, useDocStore } from '@/features/docs/doc-management'; import {
Doc,
KEY_DOC,
useCollaboration,
useDoc,
useDocStore,
} from '@/features/docs/doc-management/';
import { MainLayout } from '@/layouts'; import { MainLayout } from '@/layouts';
import { useBroadcastStore } from '@/stores'; import { useBroadcastStore } from '@/stores';
import { NextPageWithLayout } from '@/types/next'; import { NextPageWithLayout } from '@/types/next';
@@ -41,14 +46,25 @@ interface DocProps {
const DocPage = ({ id }: DocProps) => { const DocPage = ({ id }: DocProps) => {
const { login } = useAuthStore(); const { login } = useAuthStore();
const { data: docQuery, isError, error } = useDoc({ id }); const {
const [doc, setDoc] = useState(docQuery); data: docQuery,
const { setCurrentDoc, createProvider, providers } = useDocStore(); isError,
const { setBroadcastProvider, addTask } = useBroadcastStore(); isFetching,
error,
} = useDoc(
{ id },
{
staleTime: 0,
queryKey: [KEY_DOC, { id }],
},
);
const [doc, setDoc] = useState<Doc>();
const { setCurrentDoc } = useDocStore();
const { addTask } = useBroadcastStore();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { replace } = useRouter(); const { replace } = useRouter();
const provider = providers?.[id]; useCollaboration(doc?.id, doc?.content);
const collaborationUrl = useCollaborationUrl(doc?.id);
useEffect(() => { useEffect(() => {
if (doc?.title) { if (doc?.title) {
@@ -59,26 +75,13 @@ const DocPage = ({ id }: DocProps) => {
}, [doc?.title]); }, [doc?.title]);
useEffect(() => { useEffect(() => {
if (!docQuery) { if (!docQuery || isFetching) {
return; return;
} }
setDoc(docQuery); setDoc(docQuery);
setCurrentDoc(docQuery); setCurrentDoc(docQuery);
}, [docQuery, setCurrentDoc]); }, [docQuery, setCurrentDoc, isFetching]);
useEffect(() => {
if (!doc?.id || !collaborationUrl) {
return;
}
let newProvider = provider;
if (!provider || provider.document.guid !== doc.id) {
newProvider = createProvider(collaborationUrl, doc.id, doc.content);
}
setBroadcastProvider(newProvider);
}, [createProvider, doc, provider, setBroadcastProvider, collaborationUrl]);
/** /**
* We add a broadcast task to reset the query cache * We add a broadcast task to reset the query cache