Store icon indicating copy to clipboard operation
Store copied to clipboard

Two stores backed by the same source of truth doesn't work

Open OrhanTozan opened this issue 5 years ago • 10 comments

As @digitalbuddha recommends (https://github.com/dropbox/Store/issues/193#issuecomment-671104053), I'm backing my two stores with the same source of truth:

Given the following two stores:

  • UsersStore: Store<Unit, List<User>>
  • UserStore: Store<Int, User>

and both using the same db table as Source of Truth, and the server containing the following 4 users: A, B, C and D:

  1. userStore.get("A") => returns User A, and also adds to the user table: User A
  2. Right now the users table only has the User A saved.
  3. When I call usersStore.get(Unit), I'm now only getting User A (while if the fetcher would have been called, User A, B, C and D would have been returned).

Probably cause: when I'm asking the usersStore to get all the users, it first tries to read the Source of truth to see if anything is in there (which there is: User A, but User A is obviously not all users).

This is a really simple Store usecase, so I guess I must be doing something wrong or missing something.

Code:

// UsersStore.kt
sourceOfTruth = SourceOfTruth.of(
                reader = {
                    db.userRowDao().all()
                        .map { userRows ->
                            userRows.map { userRow ->
                                userRow.toModel()
                            }
                        }
                },
                writer = { _, users->
                    val rows = users.map { it.toRow() }
                    db.userRowDao().insert(*rows.toTypedArray())
                },
                delete = {
                    db.userRowDao().deleteAll()
                },
                deleteAll = {
                    db.userRowDao().deleteAll()
                }
            )
// UserStore.kt
sourceOfTruth = SourceOfTruth.of(
                reader = { userId->
                    db.userRowDao().byId(userId)
                        .map { userRow ->
                            userRow ?.toModel()
                        }
                },
                writer = { _, user ->
                    db.userRowDao().insert(user .toRow())
                },
                delete = { userId ->
                    db.userRowDao().deleteById(userId )
                },
                deleteAll = {
                    db.userRowDao().deleteAll()
                }
            )

OrhanTozan avatar Jan 07 '21 14:01 OrhanTozan

I think it would help to write out user stories for what you are expecting. Think of what you want without store and then we can make it with store.

What it seems like you want is the following: When store.get(id) is called it will get a single user from the source of truth. When no user exists in source of truth you want to go to api and get all users to make sure that calls to store.get(unit) will already have all users in disk or no users on disk.

That to me sounds like both stores should have the same fetcher and return all users no matter the key.

Now if that's not what you want I have a question. How can store know if all users are in your south of truth? Similar how would store now when E exists on server? Think about what your definition of all users is.

Is there a way for you to know in the Unit persister whether you have already fetched your definition of all?

digitalbuddha avatar Jan 07 '21 14:01 digitalbuddha

@digitalbuddha First of all, I appreciate the blazing fast reply!

My usecase is basically the same as the BlogPost example you described in the linked comment (except I have Users instead of Posts, just a naming difference):

Let's say I have a screen that shows blog posts, I want to be able to show all posts or just 1. I would then make a PostStore which takes a key of a postId and return a single item AllPostStore which takes Unit as a key (no key more or less) and returns all items

PostStore is the typical get record by ID store, it has a table backing it within a source of truth and a fetcher that retrieves individual items for a key AllPostStore would be configured with a no memory cache and a source of truth that returns select * from your local storage. It can have a no-op fetcher or a fetcher that gets all items from a network source (still updates same table as PostStore).

The nice part about backing both stores with the same source of truth is that updates from one fetcher (or local writes) will update any streams from either store. Hope that helps!

My code above was an attempt to implement the above usecase, but my "AllPostsStore" (UsersStore for my case) doesn't return all items when I made a PostStore.get(key) before. (Instead of returning all the posts, it just returns that single post)

So I think my question becomes: given the above BlogPost example you gave, how would you implement that?

OrhanTozan avatar Jan 07 '21 14:01 OrhanTozan

It depends on the following answers How can store know if all users are in your source of truth? Similar how would store now when more users exists on server?

digitalbuddha avatar Jan 08 '21 17:01 digitalbuddha

@digitalbuddha let's say the server has these simple endpoints: /posts/ -> gets all posts /posts/{id}/ -> gets post by id

Do you suggest that the AllPostsStore needs to fetch from /posts/ and compare the source of truth post list with the fetched response?

OrhanTozan avatar Jan 08 '21 18:01 OrhanTozan

You need to change your List<User> store to know whether it fetched before or not. Just checking whether the list in db or not is not sufficient because you have multiple inserts to the SoT (which is totally fine).

ALso, you may want to use the stream API instead of this, because with stream API you can ask it to refresh the data as well.

usersStore.stream(StoreRequest.cached(key = Unit, refresh=true))

This will give first A and trigger the fetch for A,B,C,D. Of course, you don't want cached data, you can also use StoreRequest.fresh

yigit avatar Jan 11 '21 01:01 yigit

Adding some comment here: I ran into a similar problem where you fetch a list of items and than show the details of one. This is indeed a most common use-case and i found it puzzling, that it is not described in the README. Delving through closed GitHub issues is not the best experience.

As discussed above, the suggested solution to use a store per query with a shared SoT become pretty verbose once you start to have multiple queries. eg allUsers, userByName, userByAge, userBy... etc. Having one store with different keys could help this somehow makes the store configuration pretty bloated.

Btw, i also found it confusing to have the reader by key and a delete by key and than the deleteAll where the stores initial concept seams to be based in the use-case of getting one entry.

Hope to see some better support for such common use-cases since all the other features of store are really compelling.

mpost avatar Mar 02 '21 09:03 mpost

For future readers:

In my case, I solved this with having a seperate table in my local database that keeps track of whether a "fetch all" has already been made.

This way, my store can know if that the data is not fetched yet vs. the data truly being empty.

OrhanTozan avatar Dec 07 '21 15:12 OrhanTozan

Hey all - Thanks for raising. We are actively thinking about how to better share data across collection, single, paging, and socket reads. Let us know if you have suggestions or samples. Very much open to proposals and contributions

matt-ramotar avatar Feb 25 '23 17:02 matt-ramotar

Hey all - First stab at making it easier to work with lists: https://github.com/MobileNativeFoundation/Store/pull/548. Please consider taking a look and leaving feedback

matt-ramotar avatar Apr 20 '23 13:04 matt-ramotar

Hey all - #548 just merged. Please share any feedback 🙏

matt-ramotar avatar May 19 '23 22:05 matt-ramotar