Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

onSnapshot weird behaviour when using a different "orderby" .. #2094

Closed
sr1dh4r opened this issue Aug 18, 2019 · 3 comments
Closed

onSnapshot weird behaviour when using a different "orderby" .. #2094

sr1dh4r opened this issue Aug 18, 2019 · 3 comments

Comments

@sr1dh4r
Copy link

sr1dh4r commented Aug 18, 2019

  • Operating System version: mac os x 10.13.6
  • Browser version: Google chrome 75.0.3770.90
  • Firebase SDK version: 6.4.0
  • Firebase Product: firestore

I'm getting different results for the same query, after running a query with a different orderby field on same collection.

Follwing is the output of a test functions I'm running from js console .. app.test2() behaviour is different during each invocation (the second invocation is the correct result). Originally (on a different collection), the issue was that test3 was showing incorrect results and on second invocation, shows the correct result.

>app.test1()
undefined
hmm.html:81 {comments: 3, likes: 1, msg: "message3", timestamp: io}
hmm.html:81 {comments: 2, likes: 0, msg: "message4", timestamp: io}
hmm.html:81 {comments: 1, likes: 5, msg: "message5", timestamp: io}
hmm.html:81 {comments: 0, likes: 4, msg: "message6", timestamp: io}
>app.test2()
undefined
hmm.html:87 {comments: 2, likes: 0, msg: "message4", timestamp: io}
hmm.html:87 {comments: 3, likes: 1, msg: "message3", timestamp: io}
hmm.html:87 {comments: 0, likes: 4, msg: "message6", timestamp: io}
hmm.html:87 {comments: 1, likes: 5, msg: "message5", timestamp: io}
hmm.html:87 {comments: 4, likes: 2, msg: "message2", timestamp: io}
hmm.html:87 {comments: 5, likes: 3, msg: "message1", timestamp: io}
hmm.html:87 {comments: 2, likes: 0, msg: "message4", timestamp: io}
hmm.html:87 {comments: 3, likes: 1, msg: "message3", timestamp: io}
>app.test2()
undefined
hmm.html:87 {comments: 4, likes: 2, msg: "message2", timestamp: io}
hmm.html:87 {comments: 5, likes: 3, msg: "message1", timestamp: io}
hmm.html:87 {comments: 0, likes: 4, msg: "message6", timestamp: io}
hmm.html:87 {comments: 1, likes: 5, msg: "message5", timestamp: io}

Steps to reproduce:

I have prepared a test html file with the script and dependencies @ https://www.dropbox.com/s/pt3f5pmugivtpxw/hmm.html?dl=0

In the js console, you need to run app.prepare() to create a "testing" collection.

executing app.test1() will get the list with orderby timestamp
executing app.test2() will get the list with orderby likes
executing app.test3() will get the list with orderby comments

The weird part is, if I run app.test(), which will run all the three functions above in a single shot, the results are as expected. It's only if I invoke them manually from the shell (basically some delay between each call) I see the issue.

@mikelehen
Copy link
Contributor

@sr1dh4r I think there are two things going on here:

First, you are not logging change.type, which makes it harder to see what is happening. With that added, you'll see something like:

added {comments: 2, likes: 0, msg: "message4", timestamp: io}
added {comments: 3, likes: 1, msg: "message3", timestamp: io}
added {comments: 0, likes: 4, msg: "message6", timestamp: io}
added {comments: 1, likes: 5, msg: "message5", timestamp: io}
added {comments: 4, likes: 2, msg: "message2", timestamp: io}
added {comments: 5, likes: 3, msg: "message1", timestamp: io}
removed {comments: 2, likes: 0, msg: "message4", timestamp: io}
removed {comments: 3, likes: 1, msg: "message3", timestamp: io}

Now we can start to see the second thing that's going on... You'll notice that the documents from the first 4 results (messages 3, 4, 5, 6) are the same results as from test1() but then you get 2 adds and 2 removes, which add the two missing docs (1 and 2) and removes the two extra docs (3 and 4) so that the results now represent the correct results for query2. The reason for this is that Firestore first gives you query results from running the query locally against any documents in cache, before it runs it against the backend. And because you have an existing listener on query1, docs 3, 4, 5, 6 were in cache and so they show up in that first set of query results. If we adjust your listen() function to include:

    ....  
    return query.onSnapshot(function(querySnapshot) {
        console.log('Got snapshot. fromCache=', querySnapshot.metadata.fromCache);
        console.log('Matching docs:', querySnapshot.docs.map(d => d.id));
        console.log('Changes:');
        ...

Then the new output is:

Got snapshot. fromCache= true
Matching docs: ["5", "6", "3", "4"]
Changes:
added {comments: 2, likes: 0, msg: "message4", timestamp: io}
added {comments: 3, likes: 1, msg: "message3", timestamp: io}
added {comments: 0, likes: 4, msg: "message6", timestamp: io}
added {comments: 1, likes: 5, msg: "message5", timestamp: io}
Got snapshot. fromCache= true
Matching docs: ["5", "6", "1", "2"]
Changes:
added {comments: 4, likes: 2, msg: "message2", timestamp: io}
added {comments: 5, likes: 3, msg: "message1", timestamp: io}
removed {comments: 2, likes: 0, msg: "message4", timestamp: io}
removed {comments: 3, likes: 1, msg: "message3", timestamp: io}

You'll notice that we get 2 snapshots. The first one is from cache and contains docs 5,6,3,4. The second one is not from cache and contains the correct docs: 5, 6, 1, 2.

Depending on what you're trying to accomplish, this behavior may be 100% fine. As long as you respect the change.type, your UI should update accordingly. If you want to avoid seeing the cache results, you could have your code explicitly check for snapshot.metadata.fromCache and suppress any snapshots from cache.

Another thing that would make a difference is if you enabled offline persistence. See https://firebase.google.com/docs/firestore/manage-data/enable-offline. With offline persistence, Firestore caches any document that you read or write, which means that it would cache the results from all 6 documents when you write them in app.prepare() and so they would all be in the cache, and so test2() would find the correct documents in cache right away.

Hope this helps. I'm closing the issue, since I think this behavior is expected. But feel free to holler if anything's unclear. Thanks!

@sr1dh4r
Copy link
Author

sr1dh4r commented Aug 21, 2019

@mikelehen Thanks a lot for looking into this. Didn't realize that there were two events when app.test1() was invoked. I think I understand the behaviour now. But I have some more concerns ..

While its true that the effective results are correct, the ordering seems to be lost ..

When I query with orderby 'likes' (app.test1()), I guess the documents are in the correct order in the first and second execution of the same query .. documents are sorted correctly with like-count 5,4,3,2

When I query with order by comments (app.test2()), I get the documents with comment count [3,2,1,0] and [1,0,5,4], so effectively 3,2,5,4 while the re-execution of the query gives me 5,4,3,2 as shown below.

While the query is returning the correct set, the ordering is lost and this becomes a problem when presenting the data to the user. It also creates problems identifying new content that needs to be shown above the existing content. I guess I can manage this when presenting the data but wanted to see if there is something that can be done in the query phase itself.

I did try to ignore snapshot.metadata.fromCache=true case but turns out I get empty results when I do that for the first execution and get the correct ordered set on the second execution of a query. So I guess that's not a solution ?

Also, offline persistance probably won't help in case the data is written by another user to the same collection so not sure if I can rely on that.

app.test3()
snapshot ..
{comments: 3, likes: 1, msg: "message3", timestamp: io} "added"
{comments: 2, likes: 0, msg: "message4", timestamp: io} "added"
{comments: 1, likes: 5, msg: "message5", timestamp: io} "added"
{comments: 0, likes: 4, msg: "message6", timestamp: io} "added"
snapshot ..
{comments: 1, likes: 5, msg: "message5", timestamp: io} "removed"
{comments: 0, likes: 4, msg: "message6", timestamp: io} "removed"
{comments: 5, likes: 3, msg: "message1", timestamp: io} "added"
{comments: 4, likes: 2, msg: "message2", timestamp: io} "added"

app.test3()
snapshot ..
{comments: 5, likes: 3, msg: "message1", timestamp: io} "added"
{comments: 4, likes: 2, msg: "message2", timestamp: io} "added"
{comments: 3, likes: 1, msg: "message3", timestamp: io} "added"
{comments: 2, likes: 0, msg: "message4", timestamp: io} "added"

fyi, I removed the changes.reverse() in the listen function to avoid any confusion (in the test script shared earlier)

@mikelehen
Copy link
Contributor

@sr1dh4r To recover the ordering, you'll need to use change.newIndex in order to know where to insert the added documents. And more generally you should use both change.oldIndex and change.newIndex together in order to handle all change types (For modified docs you'll need to move it from oldIndex to newIndex in your result set).

The other option which is much simpler but possibly a little less efficient is to just use querySnapshot.docs (which always contains the entire result set in the correct order) instead of querySnapshot.docChanges()). But with either approach you should be able to preserve the correct ordering. If you're still stuck, let me know.

@firebase firebase locked and limited conversation to collaborators Oct 8, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants