How updates merge works with Nested Shared Types?

What am I doing?

I’m want to use Yjs for updates only. This means that I get first state from REST and render my app. When user start changing any field, I add this entity to YDoc for updates replication.

I chose this way by multiple reasons. I can’t realize paging in Yjs out of the box. (For example: I have 1000 tasks. Need to Ioad 10 tasks starting from 50) I know about subdocs. But this is not ready for use solution. So 100% decided to use REST and keep YDoc as clean as possible for clients and server.

Structure that I have in YDoc

const doc = new Y.Doc();
const taskUpdatesMap = doc.getMap('tasks');

const taskMap = new Y.Map()
taskMap.set('title', 'Example')
taskMap.set('description', new Y.XmlFragment())
...rest fields

taskUpdatesMap.set('1', taskMap)

About my setup

Yjs, Y-websocket, React, Redux, TipTap editor. In Redux I save state from REST and update simple fields from YDoc. By “simple fields” I mean strings, numbers etc. But source of truth for YXmlFragment is YDoc.

Also I listen YDoc updates. For example when field title changed, I update Redux state.
I don’t listen for XmlFragment updates because when it appears, I just bind it to editor.

Initial state for Editor loads from REST in prosemirrorJSON format.

fragment = prosemirrorJSONToYXmlFragment(
        schema,
        JSON.parse(task.description)
);

taskMap = new Y.Map();
taskMap.set('description', fragment);
taskUpdatesMap.set(task.id.toString(), taskMap);

// then in editor component
const fragment = taskUpdatesMap.get(task.id.toString()).get('description')

Note

When I will say something like “set to YDoc” I mean set something by corresponding path in Structure which I mention above.
For example “set XmlFragment to YDoc” - means set XmlFragment to taskMap which exists in taskUpdatesMap.

Things which works as expected

If one user want to start edit description, under the hood, task adds to taskUpdatesMap with fresh XmlFragment, which used for editor. For now task populated in YDoc. And when any of users want to edit this task, they already have XmlFragment instance with updates.

Things which don’t work as expected

Next I tried to figure out offline cases.

  1. Two users fetch data from REST.
  2. One of the users lost connection to WS server (no yjs updates) - let’s name him as offline user.
  3. Offline user start edit description in offline mode. In this case I need create XmlFragment for editor. I add task to YMap with XmlFragment which has updated description.
  4. In this moment second user also start editing and add task to YMap.
  5. Here situation that we have same task edited, but each user update have fresh created XmlFragment with their updates.
  6. Here problem, when offline user become online, YDoc state somehow merges and I see state of offline user only.

I think here problem of creating fresh XmlFragments and trying to merge them in YMap

// something like this happens. Need to merge two fresh instances
//user update 1
YMap
- description: new Y.XmlFragment()

//offline user update 2
YMap
- description: new Y.XmlFragment()

Instead, probably need to have Y.XmlFragment() instance for all users. But this situation almost impossible to handle in my offline case.

Summarizing my thoughts

Here I see 2 ways.

  1. Add this task to YMap as update and hope that this can be merged successfully.
  2. Create local XmlFragment, save changes. When I become online, check if I had updates in YDoc from remote users.
    If yes, somehow apply local changes to this update and replace current editor with existing XmlFragment from YDoc.
    If no updates in YDoc, just set local XmlFragment to YDoc.

What answers ideally I need?

Ideally I need answer from creator @dmonad to understand core functionality.

  1. How merge on shared types works?
    Specially when I update YMap fields (which have updates for XmlFragment) in other YMap.

  2. Can I get updates from XmlFragment and apply them to another XmlFragment?

  3. Also I see everywhere that for editors and any other cases used doc.getXmlFragment()
    This means that fragment exists on root level. And no examples about placing XmlFragment in shared type as deep nested structure.
    Maybe here exists some differences of merging updates?

I haven’t used XMLFragments, so I can’t comment on the majority of your post, but as for Y.Map, when there are two updates at the same key, one will win. The winner is chosen arbitrarily but deterministically.

If you want to preserve two updates intended for the same key, you will need to save the two different keys, e.g. description-a583c and description-8fe3d. Then you can detect multiple descriptions and merge them yourself. YJS doesn’t have merging per se, it just applies updates one at a time.

Thank you for answer! But I understood that I can’t do normal merge for XmlFragment by myself.
Depending on changes fired in fragment I can’t detect where I should place those changes for another fragment.

It would be great if the creator could give at least a little hint from his perspective.