packages/lexical-website/docs/collaboration/faq.md
It's recommended to treat the Yjs model as the source of truth. You can store the document to a database for indexing. But, if possible, you should never forget the Yjs model, as this is the only way clients without internet access can reliably join and sync with the server.
You can also treat the database as the source of truth. This is how it could be achieved:
sessionId when they connect to the serversessionId, get the content from the database and create a sessionIdsessionId on the server after some timeout (e.g. 1 hour)sessionId from the clientsessionId reconnect, one of the clients should forget the room content. In this case the client will lose content - although it is very unlikely if you set the forget timeout (see point 2) very high.Or, there is an ever simpler approach:
When the database is the source of truth, and if you want to be able to forget the Yjs model, you will always run into cases where clients are not able to commit changes. That's not too bad in most projects. It somehow limits you, because you can't cache the document on the client using y-indexeddb. On the other hand, it is much easier to maintain, and do Yjs upgrades. Furthermore, most people would say that SQL is a bit more reliable than Yjs.
* Based on the advice of the Yjs author - Kevin Jahns
EditorState from Yjs DocumentIt's achievable by leveraging headless Lexical and no-op provider for Yjs:
<details> <summary>createHeadlessCollaborativeEditor.ts</summary>import type {Binding, Provider} from '@lexical/yjs';
import type {
Klass,
LexicalEditor,
LexicalNode,
LexicalNodeReplacement,
SerializedEditorState,
SerializedLexicalNode,
} from 'lexical';
import {createHeadlessEditor} from '@lexical/headless';
import {
createBinding,
syncLexicalUpdateToYjs,
syncYjsChangesToLexical,
} from '@lexical/yjs';
import {type YEvent, applyUpdate, Doc, Transaction} from 'yjs';
export default function headlessConvertYDocStateToLexicalJSON(
nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,
yDocState: Uint8Array,
): SerializedEditorState<SerializedLexicalNode> {
return withHeadlessCollaborationEditor(nodes, (editor, binding) => {
applyUpdate(binding.doc, yDocState, {isUpdateRemote: true});
editor.update(() => {}, {discrete: true});
return editor.getEditorState().toJSON();
});
}
/**
* Creates headless collaboration editor with no-op provider (since it won't
* connect to message distribution infra) and binding. It also sets up
* bi-directional synchronization between yDoc and editor
*/
function withHeadlessCollaborationEditor<T>(
nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>,
callback: (editor: LexicalEditor, binding: Binding, provider: Provider) => T,
): T {
const editor = createHeadlessEditor({
nodes,
});
const id = 'main';
const doc = new Doc();
const docMap = new Map([[id, doc]]);
const provider = createNoOpProvider();
const binding = createBinding(editor, provider, id, doc, docMap);
const unsubscribe = registerCollaborationListeners(editor, provider, binding);
const res = callback(editor, binding, provider);
unsubscribe();
return res;
}
function registerCollaborationListeners(
editor: LexicalEditor,
provider: Provider,
binding: Binding,
): () => void {
const unsubscribeUpdateListener = editor.registerUpdateListener(
({
dirtyElements,
dirtyLeaves,
editorState,
normalizedNodes,
prevEditorState,
tags,
}) => {
if (tags.has('skip-collab') === false) {
syncLexicalUpdateToYjs(
binding,
provider,
prevEditorState,
editorState,
dirtyElements,
dirtyLeaves,
normalizedNodes,
tags,
);
}
},
);
const observer = (events: Array<YEvent<any>>, transaction: Transaction) => {
if (transaction.origin !== binding) {
syncYjsChangesToLexical(binding, provider, events, false);
}
};
binding.root.getSharedType().observeDeep(observer);
return () => {
unsubscribeUpdateListener();
binding.root.getSharedType().unobserveDeep(observer);
};
}
function createNoOpProvider(): Provider {
const emptyFunction = () => {};
return {
awareness: {
getLocalState: () => null,
getStates: () => new Map(),
off: emptyFunction,
on: emptyFunction,
setLocalState: emptyFunction,
},
connect: emptyFunction,
disconnect: emptyFunction,
off: emptyFunction,
on: emptyFunction,
};
}