tiptap icon indicating copy to clipboard operation
tiptap copied to clipboard

Custom Document & TrailingNode extensions don't play well with Collaboration/Y.js

Open holdenmatt opened this issue 2 years ago • 5 comments

What’s the bug you are facing?

I have an editor that uses both CustomDocument and TrailingNode extensions, which works great (thanks!).

I recently added the Collaboration extension with an IndexeddbPersistence provider to support offline access, as described here: https://docs.yjs.dev/getting-started/allowing-offline-editing

However, the combination of these causes a bug where extraneous nodes are appended to the editor every time the page is refreshed. I've narrowed the issue down to the interaction between these 3 extensions.

Which browser was this experienced in? Are any special extensions installed?

Chrome Version 109.0.5414.119

How can we reproduce the bug on our side?

Here's a minimal repro: https://codesandbox.io/s/tiptap-indexeddb-bug-rzv4cg?file=/src/App.js

I followed the Tiptap docs to enable these extensions:

  • https://tiptap.dev/examples/custom-document
  • https://tiptap.dev/api/extensions/collaboration
  • https://tiptap.dev/experiments/trailing-node

Then added a Y.js provider for offline access:

  • https://docs.yjs.dev/getting-started/allowing-offline-editing

On initial load, everything works. But every time the page is refreshed, extraneous nodes are appended to the end of the doc.

Interestingly, if you disable any of these 3 extensions, everything works, but the bug occurs reliably if all 3 are enabled.

I've tried changing order/priority, and nothing seems to fix it :-(

Can you provide a CodeSandbox?

No response

What did you expect to happen?

These extensions can be used together, without extraneous nodes being added.

Anything to add? (optional)

Happy to spend time debugging if you have any guess what might be causing this.

Did you update your dependencies?

  • [X] Yes, I’ve updated my dependencies to use the latest version of all packages.

Are you sponsoring us?

  • [ ] Yes, I’m a sponsor. 💖

holdenmatt avatar Feb 08 '23 21:02 holdenmatt

Same here -ish. Custom node breaks my hocuspocus/yjs server implementation. Getting "Yjs update RangeError: Position X out of range".

andrem0 avatar Mar 23 '23 11:03 andrem0

This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 7 days

github-actions[bot] avatar Jun 22 '23 00:06 github-actions[bot]

After TipTap Editor is rendered with an empty const ydoc = new Y.Doc(), The Trailing extension immediately inserts a new paragraph, and the moment after yDoc receives changes from a persister, it merges them with that new paragraph. So there are probably 2 solutions:

  1. Do not render Editor before you receive changes from a persister.
  2. Register trailing plugin (should be prosemirror plugin, not TipTap extension) after you receive changes from a persister
import { trailingNode } from 'prosemirror-trailing-node'
// ...
editor.registerPlugin(trailingNode())

yan-yanishevsky avatar Jul 14 '23 21:07 yan-yanishevsky

@holdenmatt I have been working through a similar issue and deduced this down to a side-effect of using any customization that modifies the document content on the initial load, for example:

  • A CustomDocument with a non-standard content schema (in your example, forcing the first-line heading) will initialize the doc to match that schema...so it's not really empty.
  • A TrailingNode extension will add an empty paragraph on initialization.

When you pass in an empty Y.Doc, the ProseMirror editor initializes it as empty and then inserts content to match the schema (for example, an empty heading and an empty block node is inserted or an extension adds an empty paragraph at the end). Then, when the doc synchronizes via a provider, the changes that are loaded from the provider are merged with the "empty" doc, replicating the "empty" heading and first block and causing "duplicate" content.

After researching this a bit, I stumbled upon an old ProseMirror post: https://discuss.yjs.dev/t/initial-offline-value-of-a-shared-document/465/13 describing how to set the initial value of a shared doc. I followed these suggestions, and instead of initializing an empty Y.Doc, I initialize it with a templated Y.Doc with the initial content the editor would normally insert (a heading + an empty paragraph or with a trailing node already added). Any client synchronizing via the provider will then have the same "base" document to work from, so no merges of that duplicate initial content will happen.

Feel free to shoot me a note if you need more details. Cheers!

jsherer avatar Jan 04 '24 02:01 jsherer

@holdenmatt did you figure out the issue? I tried both @yan-yanishevsky and @jsherer solutions. But it doesn't seem to work.

bigyankarki avatar Feb 13 '24 00:02 bigyankarki