YousefED / SyncedStore

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

Is there a way to replace a store with a known good value? #33

Closed armincerf closed 2 years ago

armincerf commented 2 years ago

Hi, apologies if this question is answered elsewhere, I looked through all the docs and other issues and couldn't see anything though.

I have a simple chat app, and I would like to use this library to get instant updates. However, I also want each message to be persisted in my database, and when data is fetched from that db I want to replace the syncedstore with that new data.

So really I just want to do this:

// when adding a comment
const handleAddComment = () => {
   // update store to trigger real time updates for others
    state.comments.push({
      comment,
    })
   // persist to db, might take a second or so and won't trigger any updates
    addCommentHook.mutate({
      comment
    })
  }

// data is from the server, I don't know exactly when it will change, 
// but if it does I want to fully replace the contents of my store with it
  useEffect(() => {
    state.comments = data.comments
  }, [data])

However, this doesn't work because state.comments is read only. Is there a way to do this? Or am I going down the wrong path entirely?

YousefED commented 2 years ago

Hi @armincerf!

While you can't replace an array / object on the root of the store, you can take one of the following approaches:

A) Replace array elements using splice

state.comments.splice(0, state.comments.length, data.comments);

B) Store array in a nested object

// first add an object "container" to the store shape, then:
state.container.comments = data.comments;

However, this (the answers above) is probably not what you want. Example of an issue you'd run into:

The recommended way would be to persist the syncedstore / underlying yjs document to the database. You can do this using one of the https://syncedstore.org/docs/sync-providers, in particular y-websocket or hocuspocus would be relevant for your scenario (y-redis might also be interesting).

I agree that this is not yet trivial in the yjs ecosystem - most people write custom solutions now, but there's quite some development in this area at the moment as well. What kind of backend storage are you using / interested in?

armincerf commented 2 years ago

Ok thank you that is good advice. I did try using splice but encountered a strange situation where my chat messages were duplicated. I was doing exactly what you suggested state.comments.splice(0, state.comments.length, data.comments);

Can try and put together a reproducible repo if you think that might be a bug.

I will look into using y-websocket but data from the server must be treated as a source of truth and I'm a bit worried about things not being in sync.. though I guess that's just part of the difficulty with this stuff. I don't think I can persist the yjs store directly because there are other clients connected to the backend API which need to read and modify the comments, but perhaps I can have my frontend only connect to the yjs backend, and have the yjs server sync with the main DB..

Anyway thanks again, feel free to close this

YousefED commented 2 years ago

Alright, I'll close this for now. Splice should work, feel free to open a separate issue (ideally with codesandbox link) if you still run into issues.

If you need some more general pointers, feel free to comment in this thread. I'd need to know a little bit more of the system you envision (e.g.: what do you want to use SyncedStore for vs what scenarios should your backend support).

I don't think I can persist the yjs store directly because there are other clients connected to the backend API which need to read and modify the comments

Note that these two don't exclude each other. Your API could read the document and apply a change (modify / add / remove comments) to it.

armincerf commented 2 years ago

ok here's a codesandbox that can reproduce some of my issues https://codesandbox.io/s/adoring-smoke-qzop9?file=/src/App.tsx

if you try opening it in two tabs and refreshing the first one, you'll see that 'foo' is duplicated. Also, I couldn't get the server data change test to work at all since I get Not supported: reassigning object that already occurs in the tree....

Not sure what's going on there since I thought you could reassign values with syncedstore, but I'm probably just not understanding how it works.. definitely feel out of my depth with this stuff haha

YousefED commented 2 years ago

Thanks for providing the sandbox.

I've played around with it, and using a root array indeed has the issue that you don't know whether all data has been "loaded" when you delete the items using splice. So when you load the app, and call splice the array might be empty, because it hasn't been loaded yet. splice then doesn't delete all items (as length will be 0). As there's no eventually consistent operation to clear an array (e.g.: array.clear() or .reset()) operation this approach doesn't work.

The solution with a wrapper works better for your scenario, as you can just use a completely new array: https://codesandbox.io/s/elegant-faraday-l1jkk?file=/src/App.tsx