LegendApp / legend-state

Legend-State is a super fast and powerful state library that enables fine-grained reactivity and easy automatic persistence
https://legendapp.com/open-source/state/
MIT License
2.96k stars 83 forks source link

Supabase realtime sync after back online #362

Open NekodRider opened 1 month ago

NekodRider commented 1 month ago

I'm working on an offline feature for my app using the Supabase plugin with persist and realtime.

When the app goes offline for a period and then reconnects, it can receive realtime broadcasts after back online, but any updates that occurred during the offline period are lost. So there will be inconsistent records until they are updated again and broadcast by realtime.

I think Legend doesn't check network connection so I have to manually refresh data after back online. But I've been unable to find a reliable method to manually sync data from Supabase(similar to an initial load). I've tried using syncState(Contacts$).sync() with isLoaded, but as mentioned in other issues, it doesn't work and I think it's possible related to syncState.

jmeistrich commented 1 month ago

The retry option in syncedSupabase should enable that to happen automatically. And the retrySync option enables persisting the changes and retrying after reload: https://legendapp.com/open-source/state/v3/sync/persist-sync/#synced

It does not currently check network connection though, so it just keeps retrying and failing until it eventually works. I'm planning to change that soon so it stops retrying while offline and starts again when coming back online. But at least it works :)

NekodRider commented 1 month ago

I keep retry to {infinite: true} and retrySync to true all the time, but I think they are intended for retrying to save local changes to remote, not fetching?

In my case, only remote data changes during the app offline. When the app gets back online, the realtime reconnects instantly. However it won't actively check if it missed some updates, it just assumes there is no remote update during the offline period. Supabase realtime also doesn't guarantee that it knows it missed some broadcasts.

I understand that implementing network check for the legend-state takes time, so I'm trying to do a manual refresh myself. Right now I've found syncState(obs).reset() works for the purpose. I think it only resets the state to initial and clears persistence, then others do the sync. So is there a way to trigger a complete refresh without resetting the state? Just fetch all the remote data and replace the local data.

BTW, I'm curious why reset() works well but when I replace it with clearPersist(), it still gives undefined.

jmeistrich commented 1 month ago

What version are you using? I'm testing it and in the latest alpha.41 I'm seeing that when coming back online, realtime reconnects instantly and updates with all the changes. And also, calling syncState(obs).sync() updates as expected.

retrySync is for retrying saving, but retry is for everything so it should retry selects as well.

Maybe an update to the supabase js library might improve things too? If not can you share some of your code, or ideally a sandbox I could test?

NekodRider commented 1 month ago

I just updated from 39 to 42 and syncState(obs).sync() works great now. Thanks!

I think realtime does miss update during offline, I'll create a sandbox repo and come back later.

NekodRider commented 2 weeks ago

I was building a simple repo until I saw your blog on expo. So I just borrowed it for test and the problem did exist.

To reproduce it, first add some todo items, then go offline and delete some items in database. When the app gets back online, the deleted items are still there. If it's soft delete, the app could bring it back to database when we toggle it, since it will also update the deleted field. But for hard delete it will stay there forever unless we do the reset. image

Btw, I tried to implement delete todo for that repo, but soft delete seems to have some problems when all items are deleted? Although I am also using Object.values() with some filter(undefined key) to access the obs, I hope there would be a helper function to get the key array instead.

IMG_0015 image

jdahdah commented 2 weeks ago

I'm getting the same issue with deleting. Lots of empty todos popping up, it's extremely buggy at the moment.

jmeistrich commented 2 weeks ago

Can you share your repo so I can test it?

Are you using changesSince: 'lastSync'? That requires soft deletes. If not then I would think that Supabase's realtime would reconnect and see the deletes. Or is it not?

jdahdah commented 2 weeks ago

@jmeistrich Sure thing. Yes, I'm using soft deletes with changesSince: 'lastSync'. I've created a minimal reproduction based on the Supabase example from your recent blog post.

I've stuck as close as possible to the example. You can see my changes in this commit. Perhaps I'm doing something very stupid but I can't figure it out.

Steps to reproduce:

  1. Create a single todo
  2. Click the delete icon next to it

You'll see a new empty todo pop up like in @NekodRider's screenshot.

If you add a few todos, mark them as done, then click "clear completed todos" at the bottom, it starts to get pretty wacky (also try refreshing, etc).

I've also tried:

export function deleteTodo(id: string) {
  // todos$[id].delete();
  todos$[id].deleted.set(true);
}

But that doesn't cause a re-render and you only see the change after a hard refresh. The other weird behavior remains the same.


Also, if I set my phone to Airplane Mode, add a todo on my Mac, then go back online with my phone, it will often not pick up on the new todo.

jmeistrich commented 2 weeks ago

Thanks, I repro'd it and I'm working on it.

jmeistrich commented 2 weeks ago

This should be fixed in beta.7. There were a couple bugs:

  1. A special case for empty objects that was making it not remove fields with symbolDelete
  2. Sometimes not loading metadata tables (which include the changes that need retrying) early enough in AsyncStorage on web (which uses LocalStorage and isn't really async)

Is it working better for you now?

jdahdah commented 2 weeks ago

@jmeistrich Looks like this resolves the issue with the empty todos, thanks! Soft deleting works smoothly now.

I'm still seeing the issue where mobile (iOS native in Expo Go) doesn't pick up on new todos after being offline (same repo). Is there something I can do here?

jmeistrich commented 2 weeks ago

Hmm ok I'll check that tomorrow. I'd been testing on expo web.

jmeistrich commented 2 weeks ago

I think that may be a Supabase bug. If I add this logging to SupaLegend.ts I sometimes see a status of CHANNEL_ERROR with error undefined.

supabase
    .channel(`LS_TEST`)
    .on(
        'postgres_changes',
        {
            event: '*',
            table: 'todos',
            schema: 'public',
        },
        (payload) => {
            console.log('1payload', payload);
        },
    )
    .subscribe((status, error) => {
        console.log('1status', status);
        console.log('1error', error);
    });

I'm not sure what causes it but I seem to have a pattern that reproduces it:

  1. Quit app if already open
  2. Open app, realtime connects
  3. Go offline and back online
  4. Reload app in dev tools
  5. Realtime errors forever even with more reloads, until app is fully restarted

Can you try adding that in yours and see what happens for you?

jdahdah commented 1 week ago

@jmeistrich Yeah, I'm seeing a lot of this error when I reconnect:

1status CHANNEL_ERROR
1error undefined
1status CHANNEL_ERROR
1error undefined
1status CHANNEL_ERROR
1error undefined
1status CHANNEL_ERROR
1error undefined
etc…