My adventure in building a complicated application on top of yjs subdocuments

Hi all! Over the past two months or so I have been prototyping a new project in which a core feature is real time collaborative editing. Yjs has been really awesome to work with.

I recently wrote up some of the challenges and solutions I decided on when I came up on the edges of what the ecosystem supports, namely sub documents and robust server side persistence (in your application database).

I’d love some feedback from folks who might have been working on other approaches. I had a super interesting conversation with someone who found and referenced my y-websocket fork about how they approached some of these issues. I think it’s definitely worth a read, as they went a totally separate direction: Multiple text types per doc + multiple editors in the UI · Discussion #310 · BitPhinix/slate-yjs · GitHub

My y-websocket fork with the changes to support multiplexing subdocuments over one connection can be found here: GitHub - DAlperin/y-websocket: Websocket Connector for Yjs I hope to find some time to release an example compatible server implementation as well.

My y-prosemirror changes can be found in this open PR here Introduce sub document support by DAlperin · Pull Request #88 · yjs/y-prosemirror · GitHub those changes are pretty uninteresting, just fixing up a lesser used feature which is key to subdocuments as well as adding some compatibility options to make slowly migrating to subdocuments easier (in theory)

Let me know what you think and if you have any questions!

7 Likes

Really awesome work @DAlperin!

I appreciate your contributions and especially your blog post. Thank you for sharing your experience!

I’m sorry that I haven’t had time to go through your PRs. The y-prosemirror changes seem reasonable (I will write about my intentions for the current approach in the PR). The y-websocket changes make a lot of sense. But I probably have to reject it because it breaks the current authentication approach. Most users of Yjs implement authentication by authenticating each WebSocket connection using URL parameters. The subdocument protocol addition will likely break that and allow users who have access to one document to have access to all other documents. However, your implementation is fine and I’m happy that you made that work in a separate provider.

Personally, I also think that subdocuments are one of the coolest new feature. Thanks @braden for funding that! So far we are missing a scalable backend that can sync subdocuments. You’re right that I’m working on a really cool new protocol that allows syncing and authenticating subdocuments. But that’s all secret :shushing_face:

Thanks for making yjs! It’s made what would otherwise be one of the most complicated part of an already complicated system relatively effortless. Subdocuments are definitely a ridiculously cool feature (thank you @braden). I figured you’d have something cool in the works for a more right way to sync subdocuments. My implementation is pretty crude but it works, I also didn’t have to worry about breaking compatibility since my closed source server implementation is pretty clean room and does some weird stuff.

I wish I could help sponsor development but alas I’m a student. If there is ever any way for me to help with the official subdocument protocol let me know.

(Ps. I’m sorry about my open y-prosemirror PR, it’s a little disorganized)

3 Likes

@dmonad Hi Kevin! Sorry to ping in this old thread. I was wondering how the new protocol that allows syncing subdocuments is going? My fork of y-websocket technically “works” but I’m eager for something better and more supported. Let me know if there is anything I can do!

Thanks
Dov

Hi @DAlperin,

I put my work on hold for a bit, but I will continue work this summer.

I’m working on Ydb, a scalable backend for Yjs which will support syncing many documents at the same time and have integrated support for authentication.

The Ydb implementation is quite opinionated and will share no basis with y-websocket. Hence the easier y-websocket server will definitely stay around in the future as it covers more use-cases.

If you have something working, you should definitely share it if you want feedback :wink:

2 Likes

is it still a secret?

1 Like

By retrieving a list of all updates for a given document and applying them to each other, you end up with a complete view of the document.

If you send each generated update to the server, what if one doesn’t go through do to some network issue. You would forever have a “hole” in your list of updates right?

That issue led me to think I need to either always send the entire document as an update, or use the state vector techniques to send a diff.

Am I missing something here? I don’t want to overcomplicate my system.

The most efficient way to perform an initial sync between two clients is to exchange state vectors.

“Holes” don’t really happen in TCP. If you use a communication protocol where “holes” can occur, you should probably switch to a TCP-like protocol (e.g. Websocket). Otherwise, you really have to send the whole state anytime anything changes.

I’m sure its unlikely, but it could happen whether due to network, or server issues. The risk is low, but the result is severe (future updates having no effect, at least in the case of arrays). Exchanging state vectors of course fixes it, but if there isn’t another client with all the data you are quite stuck, right?

Hmm… I heard this argument several times and it really confuses me. TCP guarantees in-order delivery. That is why people use TCP over UDP. Why would you fix a problem on a higher level when it has already been fixed by the underlying protocol? Maybe there is something I don’t understand.

I can think of reasons there would be a missing update.

  • Someone could go into the database and delete a row by mistake.
  • A bug in server-side code that fails to record it a row, or a bug deletes it a row.

If we are creative, I’m sure we can think of many situations where this could conceivably happen. Now, of course these are rare, unlikely situations, but the result is severe, more severe than with a traditional database model.

Yeah, if you delete a row, you can have dataloss…

However, as long as you sync via state-vectors, the clients can sync-up the missing state to the backend. If no one has the data (not even your backup database), then you will lose the data forever as expected.

In any case, it is not required to send the whole document to the backend. A normal sync via state-vectors will suffice.

If there are now holes in your document, you won’t see the missing state or the changes that depended on the holes. Everyone will still end up with the same document and everyone can still apply changes. You just really don’t want to get into this scenario.

If you lose updates from client X, you won’t be able to apply future updates from client X. However, once that client reloads the window you can again apply changes.

Yjs & CRDTs are very powerful and provide additional safety guarantees over alternative solutions. However, you should not expect that they fix all problems. CRDTs generally still require guaranteed deliveries. Protocols like TCP add delivery guarantees to your messages (messages are delivered in-order without holes). You can build on that. It is your responsibility to make sure that messages are also stored without holes in the database. There is no way that I can do that for you on my end. It seems that there is a problematic design flaw on your end.

Right, my concern is:

That’s a significant difference from other non-CRDT systems. Which is fine, it’s different. I just want to make sure I understand the tradeoffs.

I didn’t mean my questions as a criticism, I apologize if it came off that way. Now I’m a little hesitant to further clarify my understanding. I don’t want to come off as critical :frowning:

Maybe? I have only built prototypes and experiments so far, my hope was that I could avoid design flaws by clarifying my understanding.

Maybe it has to do with realtime collaboration compared to offline-first apps. In the realtime collaboration world, these problems are less of a concern, but in an offline-first app with no collaboration features, YJS is being used for long-term storage without lots of other clients to merge their data together, so the robustness of the data in the long-term database might be more of a concern.

In other non-CRDT systems you still have critical dataloss, right?

Yjs works exactly like other non-CRDT systems. But it handles dataloss more gracefully as it still allows clients to sync and converge.

In other systems, you wouldn’t be able to have clients and servers converge (everyone would end up with different content).

No system can recover from this kind of dataloss. So you should really not have holes in your document.

I don’t see your comment as criticism. I just want to make clear that Yjs (nor any other similar system that allows syncing list-like data) will ever be able to recover from “holes” in the list of updates. It is just not possible. The same is true for GIT, OT, Firebase, XMPP, matrix, …

My expectation from users of Yjs is to ensure this doesn’t happen. It is definitely possible to build an offline-capable, collaborative app using Yjs without producing holes in the list of updates.

Store your updates in sequential order in a database. It is possible to ensure that updates are written in the order how you received them. E.g. by waiting for completion before you write the next set of updates.

1 Like