Здравствуйте!
При переподключению к вебсокету дублируються данные в редактор.
Видео скрин
Код сервера:
import cors from '@fastify/cors'
import websocketPlugin from '@fastify/websocket'
import fastify from 'fastify'
import type { RawData } from 'ws'
import { checkAccountFromGraphQL, getProjectDocumentPageFromGraphQL, saveProjectDocumentPageToGraphQL } from './graphql.js';
import { Hocuspocus } from "@hocuspocus/server";
import { yTextToSlateElement, slateNodesToInsertDelta } from '@slate-yjs/core';
import * as Y from 'yjs';
import { Redis } from '@hocuspocus/extension-redis';
const HOST = process.env.HOST || '0.0.0.0';
const PORT = parseInt(process.env.SERVER_PORT || '3000', 10);
const app = fastify()
const hocuspocus = new Hocuspocus({
extensions: [
new Redis({
host: "redis",
port: 6379,
})
],
async onAuthenticate({ token, requestHeaders, requestParameters, documentName }) {
try {
const endpoint = requestHeaders.origin + '/graphql';
const projectId = requestParameters.get('projectId') as string;
const documentId = requestParameters.get('documentId') as string;
if (!projectId) {
throw new Error("Project ID is required!");
}
const hasAccess = await checkAccountFromGraphQL(endpoint, token, projectId);
if (!hasAccess) {
throw new Error("Not authorized!");
}
return { token, projectId, documentId, endpoint }
} catch (error) {
console.error('Authentication error:', error);
throw error;
}
},
async onLoadDocument({ documentName, context, document }) {
const { endpoint, token, projectId, documentId } = context;
const sharedRoot = document.get('content', Y.XmlText) as Y.XmlText;
if (sharedRoot.length > 0)
return document
const data = await getProjectDocumentPageFromGraphQL(endpoint, token, documentName, projectId, documentId);
const insertDelta = slateNodesToInsertDelta(data);
sharedRoot.applyDelta(insertDelta);
return document;
},
onStoreDocument: async ({ document, documentName, context }) => {
const sharedContent = document.get('content', Y.XmlText) as Y.XmlText;
const data = yTextToSlateElement(sharedContent);
const { token, projectId, documentId, endpoint } = context;
saveProjectDocumentPageToGraphQL(endpoint, token, documentName, projectId, documentId, data.children)
},
});
app.register(websocketPlugin)
app.register(cors, { origin: '*' })
app.register(async (app) => {
app.get("/ws/collaboration", { websocket: true }, async (socket, req) => {
hocuspocus.handleConnection(socket, req.raw, {});
});
app.addContentTypeParser('*', (_, __, done) => done(null))
})
app.listen({ port: PORT, host: HOST }, (err, address) => {
if (err) {
console.error(err)
process.exit(1)
}
console.log(`Server started on ${HOST}:${PORT}`)
})
Код клиента:
export default ({ className, title, changeTitle, icon, pageId, changeIcon, readOnly, children, defaultValue, changeValue }: any) => {
const { user } = useApp();;
const { projectId, documentId } = useParams();
const [isSyncing, setIsSyncing] = useState(false);
const yjsPluginOptions: any = useMemo(() => ({
render: {
afterEditable: RemoteCursorOverlay,
},
options: {
onSyncChange: ({ type, isSynced }) => setIsSyncing(isSynced),
cursors: {
data: {
name: user.fullName,
color: userAvatarColor(user.fullName),
previewUrl: user.previewUrl,
id: user.id
},
},
providers: [
{
type: 'hocuspocus',
options: {
name: pageId,
url: `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.hostname}:${_sharedData.websocketPort}/ws/collaboration?projectId=${projectId}&documentId=${documentId}`,
token: localStorage.getItem(LocalStorage.token),
},
},
],
}
}), [user.fullName, pageId, projectId, documentId]);
const plugins = useMemo(() => [YjsPlugin.configure(yjsPluginOptions)], [yjsPluginOptions]);
const editor: any = useCreateEditor([], plugins);
const theme: any = useTheme();
const classes = useStyles();
const titleRef = useRef<any>(null);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (readOnly && editor && defaultValue && !isEqual(editor?.children, defaultValue)) {
editor.children = defaultValue;
editor.onChange();
}
}, [defaultValue]);
useEffect(() => {
if (!mounted) return;
const api = editor.getApi(YjsPlugin);
if (!api) return;
api.yjs.init({
id: pageId,
// value: defaultValue,
}).then(() => {
if (
editor.children.length === 1 &&
editor.children[0].type === 'p' &&
editor.children[0].children?.[0]?.text === ''
) {
editor.children = [];
editor.onChange();
}
});
return () => {
editor.getApi(YjsPlugin).yjs.destroy();
};
}, [editor, mounted, pageId]);
return (
<Box className={cn(classes.root, className ?? '')}>
<div data-registry="plate" className={`${theme.palette.mode} flex flex-col flex-1`} >
<Plate
readOnly={readOnly}
editor={editor}
>
<EditorWrapper
title={title}
changeTitle={changeTitle}
icon={icon}
changeIcon={changeIcon}
editor={editor}
titleRef={titleRef}
readOnly={readOnly}
>
{children}
</EditorWrapper>
</Plate>
</div>
</Box>
);
}