YousefED / SyncedStore

SyncedStore CRDT is an easy-to-use library for building live, collaborative applications that sync automatically.
https://syncedstore.org
MIT License
1.69k stars 51 forks source link

Question: Why syncedStore wrap values from YDoc automatically? #118

Closed frontend-sensei closed 8 months ago

frontend-sensei commented 9 months ago

Hey, @YousefED! This library looks cool. Im trying figure out it deeper.

What I am going to do

Get initial data using REST and fill it to YDoc which I got from getYjsDoc(store). When I check store values, I found that added values wrapped as a Boxed values.

Example of my code:

const store = syncedStore({ todos: []});

const doc = getYjsDoc(store)
doc.on('update', (update, origin) => {
    if(origin === "REST") {
        console.log('update from rest', store.todos.map(el => el))
    } else {
        // other updates
    }
})
doc.transact(() => {
    doc.getArray("todos").push([{ id: 1, completed: false, title: 'one' }])
}, "REST")

What I see in the console:

Снимок экрана 2023-10-01 в 14 06 16
nvanhoeck-dpgmedia commented 8 months ago

We are encountering the same issue.

We have a React application which used SyncedStore with 2 properties in it: article, en metadata. Article is an object which is constructed of primitive values and 1 units property, which is an array of objects.

The syncedStore gets its data through YJS, where the logic of handling the YJS docs is built upon a centralised server.

The current situation is that, when a backend call tells this server to fetch the most recent 'article' and replace the existing one, the updated value of units is a boxed array. How can we prevent this from happening?

The Code

this.logger.info("[${this.name}] Reloading article content")
        const garbageCollection = true
        const articleMap = this.getMap(ARTICLE_MAP_NAME)

        logger.info("[${this.name}] Reloading WSSharedDoc from our ACL endpoint")
        const updatedSharedDoc = await getWSSharedDoc(this.name, garbageCollection)

        const updatedArticleContent = updatedSharedDoc.getMap(ARTICLE_MAP_NAME).toJSON()
        const updatedArticleContentFields = Object.keys(updatedArticleContent)
        updatedArticleContentFields.forEach((updatedArticleContentField) => {
                articleMap.set(updatedArticleContentField, updatedArticleContent[updatedArticleContentField])
        })
YousefED commented 8 months ago

Hi! When you use yjs methods, such as doc.getArray("todos").push(...) (@frontend-sensei) or Y.Map.set() (@nvanhoeck-dpgmedia ), then you'll directly add the object your pushing / setting as "one value". In syncedstore we call this "boxed".

A "boxed" value means that the entire object is the granularity of syncing. If you use SyncedStore style methods (e.g.: store.todos.push({...}) ), the values won't be boxed, and you syncing happens all the way through the property level.

To understand what's going on beneath. When you use Y.Array push, this entire object ({ id: 1, completed: false, title: 'one' }) in the first example will be seen as one "opaque" object.

When you use SyncedStore todos.push(...), it internally get's translated to something like (pseudo code): Y.Array push(new Y.Map({id: 1, completed: false, title: 'one' })

TLDR: make sure you either use the syncedstore methods that simplify this, or create a Y.Map to represent your object

nvanhoeck-dpgmedia commented 8 months ago

@YousefED thanks for the clarification. So if I get this correct, if we want to push changes from a YJS server to all clients, we need to map our data to a Y.Map structure and push the changes in this Y.Array. If we talk about a nested data structure, we'll have to do this for every array in the nested data structure?