Bypassing the offline persistence cache (iOS)

213 views
Skip to first unread message

Bartholomew Furrow

unread,
Aug 3, 2017, 9:06:29 PM8/3/17
to Firebase Google Group
Firebase Magi,

I'm facing a problem where, with offline persistence enabled, I need to know what the current value of something is in the database, without the cache getting in the way.

I'll give a specific example shortly, but it's characterized by:
- A decision being made by the client (e.g. "Do I enter the sign-up flow, or is the user already signed up?"), and
- No opportunity or no desire to call keepSynced significantly before making the decision (e.g. it has to happen immediately after the user signs in).

For example: 
1. I create an account for my app using device A.
2. The app on device A checks whether my account has a user profile. It doesn't, so the app enters the signup flow. I get distracted and put the app down before finishing.
3. I sign in to the same account on device B, and complete the signup flow. Now a user profile exists in the database.
4. Later, I reopen the app on device A. As soon as the app opens, it sees the user is signed in and asks Firebase "What is in the user's profile?" it immediately hits the offline cache, which says "Nothing. This user doesn't have a profile; I checked last time you were online." So the app enters the signup flow.

I've done some rudimentary Googling and found a couple of proposed hacks (fetching the data in transaction, which I gather only works if the user has write access to the data; doing a write and then a read, which I couldn't get to work), but nothing that looks as though it's intended to solve this problem.

I've heard it said that naming and cache invalidation are the two hardest problems in computer science, so I don't expect Firebase to resolve either of them automatically; I'm just hoping for the tools to let me handle some part of cache invalidation manually.

Thanks,
Bartholomew

Ian Barber

unread,
Aug 3, 2017, 9:52:40 PM8/3/17
to Firebase Google Group
Are you making the call with a once()? If you use a listen, and are online, you should get a read from the remote database before the cache. 

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/firebase-talk/CAHaiWHPe-5AuTEtML1QwBr79DLpqGBFLpAO9qrFt%2BPR%2BdwQUJA%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Bartholomew Furrow

unread,
Aug 3, 2017, 11:07:45 PM8/3/17
to Firebase Google Group
Ian, I appreciate you responding. I don't think your information is correct, however. I was using a once (observeSingleEvent on iOS), but using observe doesn't do any better.

Here's how I checked. I hope the opaque library code isn't too distracting; I hope it's fairly clear that what I'm doing is reading a field from the server and then printing it out, and that what I print out is first the cached version.

1. Create a "user profile" in the database at /user-data/profile/abunchofcharactersinmyuserid
2. Sign in.
3. Code like this is called when I sign in:

    AuthUtil.DB.getPathForUserProfile(firUser.uid).observe(.value, with: { (snapshot) -> Void in

      if let user = AuthUser(snapshot: snapshot) {

        NSLog("Profile present. Name: \(user.displayName)")

        self.signInComplete(user)

      } else {

        NSLog("Profile NOT present.")

        self.gatherUserData()

      }

    }) { (error) in

      NSLog(error.localizedDescription)

    }


4. The code prints "Profile present. Name: Bartholomew Furrow"
5. Sign out.
6. Go to https://console.firebase.google.com/project/<myproject>/database/data and change the displayName field to "Fire Base".
7. Sign back in.
8. The code prints "Profile present. Name: Bartholomew Furrow" then, 97ms later, "Profile present. Name: Fire Base".

So it appears that both observeSingleEvent and observe hit the cache first.

Thanks,
Bartholomew

Kato Richardson

unread,
Aug 4, 2017, 3:10:09 PM8/4/17
to Firebase Google Group
Hi Bartholomew, that's correct. They will hit the cache first. And you should generally try to treat RTDB data as an event stream instead of a CRUD op, particularly when working with offline persistence.

This makes a bit more sense when you consider the complexities here. During initial setup, we don't know if the app is still in the process of connecting, or if it's permanently offline. There's no way to know until we get a connection when it might become available. So the app just delivers the local content. You can detect online status via .info/connected to see when that connection is established, which helps in some edge cases.

You can always just use a real CRUD op if that's what you want, and do a GET via the REST API to fetch the remote value. But again, you're going to need to deal with the offline/online status yourself and bypass the work Firebase does here to deliver local content until the connection is available (the whole point of offline persistence in my mind).

☼, Kato

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

Bartholomew Furrow

unread,
Aug 5, 2017, 12:57:38 AM8/5/17
to Firebase Google Group
Kato,

Thank you for the detailed response! I really appreciate that you took the time to explain the philosophical and technical considerations here. In particular, it didn't occur to me that the app might still be in the initial process of connecting, which makes things tougher.

This is a bit of an essay, I'm afraid -- sometimes it's better to have these conversations in person, but I don't work out of MTV anymore -- so I've added headings.

Concern
I agree that the point of offline persistence is to take advantage of the work Firebase does to deliver local content until the connection is available... but right now if I have offline persistence enabled and the cache matches the server, then I can never know if my information is fresh to any standard. Without turning off offline persistence for every request in the whole app, that is.

Does wanting to know this violate the principle that the RTDB should be treated as an event stream? Yes. But I have to choose what to display to the user at some point, and I can't always update what I'm displaying when new information comes in.


Challenges
Here are two examples of problems I would like to be able to solve, where things are made more complicated because if I want any offline persistence, it has to be on all the time:

1. See whether the user has created a profile and choose what View to display to her based on that.
2. Display a list of events the user's invited to, and show the user a "loading" icon until I know the information in front of him is somewhat fresh.


Suggestions
What I'd like is to have some idea of whether the data I'm using is somewhat fresh, and be updated if that changes. This could be accomplished in a couple of ways:

1. Include that information directly as a field in DataSnapshot. You'd have to add a followup callback when the server comes back and the only thing that's changed is the freshness of the data. I think this is generically useful information to have, but dislike the second callback.

2. Add a version of observeSingleValueEvent that pretends offline persistence is disabled. With that I can build fallback-to-cache-on-timeout, I can call it alongside regular calls to observe() to remove "loading" indicators, etc.


Downsides
I recognize that this is philosophically murky. There's purity to saying "RTDB data is an event stream" and requiring users to handle that; but I don't know how to handle that when I need to make decisions based on the best data I can get in a reasonable response time. With offline persistence enabled, I can't even tell when I have the best data I can get in that response time, unless I wait until the end of it*.

*Except when there's a difference between the client and DB, because the second callback from observe is always fresh.


Alternatives

1. I appreciate you pointing out that I could use the REST API to perform CRUD operations, though I can't find any information about how to use my user's app credentials in the REST API. Is that possible?

2. I could use an https Firebase Function to do my work. I also don't know how to authenticate those without live access to the RTDB, but I can solve my problem #1 without authentication -- "does user $userId have a profile" seems reasonable to expose to anyone who asks.

3. I could put a timeout of X milliseconds on whatever I need to fetch, always make the user wait that long if the cached data matches the database, and presume that the server would have gotten back to the client by that time if it was ever going to.


If you've gotten this far, thank you! I hope I've made good use of your time.
Bartholomew

Kato Richardson

unread,
Aug 7, 2017, 2:15:33 PM8/7/17
to Firebase Google Group
This is all familiar ground and use cases/questions I've seen before, so you're in the minority here. These are all reasonable requests.

Determining if something was delivered is definitely on our radar. I'll find the feature request for that and add the discussion notes from here, as well as a vote on your behalf.

As for using GET with Auth, that's a particularly painful point to raise, since I've been sitting on a change to the docs to get that added for a while. Sorry for this, I'll bump that back up in my priorities and get it added asap.

The short answer is that in admin tools, you can use your service account to create a usable token for REST. On clients, you can do this using either a Google OAuth token or a Firebase ID token (received during client auth).

I don't want to post any extensive documentation here as it would quickly become outdated and distract from the real docs. But here's the gist of what that looks like:
curl "https://<DATABASE_NAME>.firebaseio.com/users/ada/name.json?access_token=<ACCESS_TOKEN>"
Or from Node:

// Read data from the Realtime Database, using the access token to
// authenticate the request.
request
({
  url
: "https://<DATABASE_NAME>.firebaseio.com/users/ada/name/.json",
  method
: "GET",
  headers
: {
   
Authorization: "Bearer " + <ACCESS_TOKEN>
 
}
}, function(error, response) { ... });

Here's a few useful threads for more context:


☼, Kato

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Bartholomew Furrow

unread,
Aug 7, 2017, 11:34:51 PM8/7/17
to fireba...@googlegroups.com
Great, that's really helpful! Thanks for passing on the discussion, and explaining what you're working on.

I put together a blog post with some crappy illustrative code for handling communication with a Firebase Function from an iOS app in Swift, which works like a charm.

Unfortunately I can't seem to make it communicate with myapp.firebaseio.com; I get a 401 status code with both the approaches I've tried. Specifically, neither the header "authorization": "Bearer: <mytoken>" nor the CGI param ?access_token=<mytoken> gets me anything but a 401, though I note that my token is in excess of 1024 characters.

Thanks for your help -- even if nothing else ever comes of this thread, I can now authenticate with Firebase Functions over https, which is very nice.

Thanks,
Bartholomew

To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/_1u3YlMgGjs/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-tal...@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

Kato Richardson

unread,
Aug 8, 2017, 10:15:25 AM8/8/17
to Firebase Google Group
Want to put together a smallish repro of what you're doing? I'd be happy to test it and see if I can spot anything.

On Mon, Aug 7, 2017 at 7:34 PM, Bartholomew Furrow <fur...@gmail.com> wrote:
Great, that's really helpful! Thanks for passing on the discussion, and explaining what you're working on.

I put together a blog post with some crappy illustrative code for handling communication with a Firebase Function from an iOS app in Swift, which works like a charm.

Unfortunately I can't seem to make it communicate with myapp.firebaseio.com; I get a 401 status code with both the approaches I've tried. Specifically, neither the header "authorization": "Bearer: <mytoken>" nor the CGI param ?access_token=<mytoken> gets me anything but a 401, though I note that my token is in excess of 1024 characters.

Thanks for your help -- even if nothing else ever comes of this thread, I can now authenticate with Firebase Functions over https, which is very nice.

Thanks,
Bartholomew

To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/_1u3YlMgGjs/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-talk+unsubscribe@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Bartholomew Furrow

unread,
Aug 8, 2017, 7:29:44 PM8/8/17
to Firebase Google Group
Is there any such thing as a smallish repro when we're talking about authentication? :-)

Actually there's not really much to put together -- I'm trying to issue a GET request that's basically the same as the one you described above:
curl "https://<DATABASE_NAME>.firebaseio.com/users/ada/name.json?access_token=<ACCESS_TOKEN>"
...but I'm getting a 401 error, which I assume means the server doesn't think I've even tried to authenticate. I can send you the specific URL I'm using privately if that would help.

The access token I'm using is one I get from Auth.auth().currentUser!.getIDToken(...), but since it's over 1024 characters I'm pretty sure that isn't what I'm supposed to use. I've also tried putting in a header with "Bearer <token>", which works for Firebase Functions, but it doesn't do the trick here.

On Tue, Aug 8, 2017 at 8:15 AM 'Kato Richardson' via Firebase Google Group <fireba...@googlegroups.com> wrote:
Want to put together a smallish repro of what you're doing? I'd be happy to test it and see if I can spot anything.

On Mon, Aug 7, 2017 at 7:34 PM, Bartholomew Furrow <fur...@gmail.com> wrote:
Great, that's really helpful! Thanks for passing on the discussion, and explaining what you're working on.

I put together a blog post with some crappy illustrative code for handling communication with a Firebase Function from an iOS app in Swift, which works like a charm.

Unfortunately I can't seem to make it communicate with myapp.firebaseio.com; I get a 401 status code with both the approaches I've tried. Specifically, neither the header "authorization": "Bearer: <mytoken>" nor the CGI param ?access_token=<mytoken> gets me anything but a 401, though I note that my token is in excess of 1024 characters.

Thanks for your help -- even if nothing else ever comes of this thread, I can now authenticate with Firebase Functions over https, which is very nice.

Thanks,
Bartholomew

To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/_1u3YlMgGjs/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-tal...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-tal...@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/_1u3YlMgGjs/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-tal...@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

Kato Richardson

unread,
Aug 8, 2017, 11:55:36 PM8/8/17
to Firebase Google Group
 
Is there any such thing as a smallish repro when we're talking about authentication? :-)
Lulz. Uh... yes!
 
...but I'm getting a 401 error, which I assume means the server doesn't think I've even tried to authenticate. I can send you the specific URL I'm using privately if that would help.
Hrm, yeah, send me that URL directly and we'll try some ideas offline. Then I'll get those docs done : )
 
The access token I'm using is one I get from Auth.auth().currentUser!.getIDToken(...), but since it's over 1024 characters I'm pretty sure that isn't what I'm supposed to use. I've also tried putting in a header with "Bearer <token>", which works for Firebase Functions, but it doesn't do the trick here.
Yeah, this doesn't seem right.
 
On Tue, Aug 8, 2017 at 8:15 AM 'Kato Richardson' via Firebase Google Group <firebase-talk@googlegroups.com> wrote:
Want to put together a smallish repro of what you're doing? I'd be happy to test it and see if I can spot anything.

On Mon, Aug 7, 2017 at 7:34 PM, Bartholomew Furrow <fur...@gmail.com> wrote:
Great, that's really helpful! Thanks for passing on the discussion, and explaining what you're working on.

I put together a blog post with some crappy illustrative code for handling communication with a Firebase Function from an iOS app in Swift, which works like a charm.

Unfortunately I can't seem to make it communicate with myapp.firebaseio.com; I get a 401 status code with both the approaches I've tried. Specifically, neither the header "authorization": "Bearer: <mytoken>" nor the CGI param ?access_token=<mytoken> gets me anything but a 401, though I note that my token is in excess of 1024 characters.

Thanks for your help -- even if nothing else ever comes of this thread, I can now authenticate with Firebase Functions over https, which is very nice.

Thanks,
Bartholomew

To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/_1u3YlMgGjs/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-talk+unsubscribe@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--

Kato Richardson | Developer Programs Eng | kato...@google.com | 775-235-8398

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/_1u3YlMgGjs/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-talk+unsubscribe@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Firebase Google Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to firebase-talk+unsubscribe@googlegroups.com.

To post to this group, send email to fireba...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Bartholomew Furrow

unread,
Aug 9, 2017, 12:12:11 AM8/9/17
to fireba...@googlegroups.com
OK, problem solved! Thanks for the help off-thread. The issue is that using foo.json?access_token=... works, presumably, for an oauth2 access token. For the kind of token I'm providing, the arg should be: foo.json?auth=...

Here's a kludgy Swift example, for anyone following along:

Auth.auth().currentUser!.getIDToken(completion: { (token, error) in
  if let token = token {
    // Now do whatever you want with url!
  }
}

Thanks again for your help, Kato. I think we've covered everything I wanted to talk about, and I've come away with a couple of great new tricks.

Bartholomew
Reply all
Reply to author
Forward
0 new messages