Skip to content

Swift observeSingleEvent closure issue. #274

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

Closed
ryanwilson opened this issue Sep 15, 2017 · 30 comments
Closed

Swift observeSingleEvent closure issue. #274

ryanwilson opened this issue Sep 15, 2017 · 30 comments
Assignees

Comments

@ryanwilson
Copy link
Member

ryanwilson commented Sep 15, 2017

Describe your environment

  • Xcode version: N/A
  • Firebase SDK version: 4+
  • Library version: 4.0.2
  • Firebase Product: database

Describe the problem

It's difficult to observe a single event without getting a cached value in Swift. The value is cached, leaving the opportunity to retrieve stale data. This can usually be mitigated by using the removeObserver(withHandle:) call, but that doesn't work well in Swift due to the way closures work.

Steps to reproduce:

Original issue.
  1. Set Database's persistence enabled flag to true (Database.database().isPersistenceEnabled = true)
  2. Call observeSingleEvent on a reference from Database.
  3. Change the value on the server.
  4. Call observeSingleEvent again, retrieve old stale value.
Attempted solution.
  1. Call observeEvent on a reference, assign the return value to a var.
  2. Attempt to call removeObserver(withHandle:) in the closure.
  3. Compiler error. "Variable used within it's own initial value."

Relevant Code:

let ref = Database.database().reference()
var handle = ref.observe(.value) { (snapshot) in
  ref.removeObserver(withHandle: handle) // Compiler error on 'handle' usage.
}

Workaround

If the ref only has a single observer, ref.removeAllObservers() will work in place of the removeObserve(withHandle:) call. Unfortunately, this doesn't work if multiple observers are added to the ref.

Adding a property declaration should work, but isn't ideal as it pollutes the class or struct being used, leaves some uncertainty whether or not the removeObserver call will actually happen.

// Property declaration
var handle: DatabaseHandle?

func readFromDatabase() {
  let ref = Database.database().reference()
  handle = ref.observe(.value) { [weak self] (snapshot) in
    guard let handle = self?.handle else { return }

    ref.removeObserver(withHandle: handle)
  }
}

Potential Solution(s)

Providing a parameter on the observeSingleEvent method that bypasses the cache and will only fetch from the server is a possibility. Some naming thoughts: bypassCache forceFetch, fromServer.

@morganchen12
Copy link
Contributor

morganchen12 commented Sep 15, 2017

Can you not avoid the compiler error by changing the handle declaration from a let to a var?

let ref = Database.database().reference()
var handle = ref.observe(.value) { (snapshot) in
  ref.removeObserver(withHandle: handle)
}

@ryanwilson
Copy link
Member Author

@morganchen12 nope, same issue. Swift tries to capture the value before it's initialized. I can change the sample code to a var so it's a bit more clear!

@BigSauce
Copy link

BigSauce commented Sep 18, 2017

Thanks for creating this issue, @ryanwilson.

Important Note:**
observeSingleEvent will retrieve cached values only when persistenceEnabled = true.

I would modify the Steps to Reproduce from:

Steps to reproduce:

Original issue.

  1. Call observeSingleEvent on a reference from Database.
  2. Change the value on the server.
  3. Call observeSingleEvent again, retrieve old stale value.

to

  1. Set Database's persistence enabled flag to true (Database.database().isPersistenceEnabled = true)
  2. Call observeSingleEvent on a reference from Database.
  3. Change the value on the server.
  4. Call observeSingleEvent again, retrieve old stale value.

In cases where persistence is enabled, Swift consumers of the Firebase SDK have no recourse for retrieving a fresh Firebase node value other than storing a node handle (UInt returned by observeEvent) as a property, which is very impractical. Another workaround is to create a trampoline interface in Objective-C for Swift consumers to use, but this is also impractical.

My suggestion would be to add a Bool parameter to observeSingleEvent (maybe called bypassingCache) that defaults to false. When true, observeSingleEvent would always perform a retrieval of the node's value. After retrieval of the node's value, the method would remove associated observers from the DatabaseReference.

The final method signature might look something like this in Objective-C:

- (void)observeSingleEventOfType:(FIRDataEventType)eventType bypassingCache:(BOOL)bypassCache withBlock:(void (^)(FIRDataSnapshot *snapshot))block;

I might recommend adding this as an overloaded method so it doesn't break existing code. If we want to support Swift default values, then bypassingCache bypassCache: Bool = false would need to be the last parameter in the function, instead of where I listed it in the method declaration.

@BigSauce
Copy link

Can you not avoid the compiler error by changing the handle declaration from a let to a var?

@morganchen12 I tried that approach, to no avail. There must be something I'm not understanding about how that type is captured, because the following similar pattern works in NotificationCenter:

var token: NSObjectProtocol?
token = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: kNotificationOnlineStatusUpdated), object: nil, queue: OperationQueue.main) { notification in
            
     // this works
            
     NotificationCenter.default.removeObserver(token!)
}

I speculate that it must have something to do with the way NSObjectProtocol works or the way optionals are captured.

@morganchen12
Copy link
Contributor

morganchen12 commented Sep 18, 2017

It's because we return a value type (FIRDatabaseHandle) and not a reference type, so the value is copied when captured in the block rather than referenced like in the NotificationCenter example. Using an ivar/property gets around this because the object's members/the variable captured by reference can be modified and then referenced from inside the closure.

Agreed, this is pretty annoying and not immediately clear.

@quanvo87
Copy link

quanvo87 commented Jan 5, 2018

@ryanwilson even in your workaround, the callback will fire twice, first returning stale data, and second returning fresh data. I tried the same thing and this had negative downstream implications in my app. I strongly support a new feature to force observeSingleEvent to get fresh data. As of now, observeSingleEvent and persistence are essentially mutually exclusive features.

@morganchen12
Copy link
Contributor

For those struggling with this issue, the cleanest workaround is probably to use a Box reference type. If you don't want to take on another dependency, you can implement your own box pretty easily.

@BigSauce
Copy link

Thanks for the advice, @morganchen12. I'll give that route a try.

@ryanwilson, has this issue gained any traction with the dev team?

@morganchen12
Copy link
Contributor

@BigSauce it's a very straightforward problem with an equally straightforward (and easily implemented) solution. The only thing that's prevented us from making this change is it's a breaking API change requiring a major version bump, as the return type of our function must be changed.

@BigSauce
Copy link

Gotcha. So you're saying the best time for this to be implemented by the Firebase iOS SDK team is for a major release as opposed to ASAP (due to breaking changes)? If so, that makes sense.

For now I'll try the boxed type workaround and see if it works on my end. In the mean time, I'll reply on this thread if I run into any issues with that, so perhaps others can benefit from my work.

@quanvo87
Copy link

One workaround I considered was:

let ref = Database.database().reference()
var handle = UInt.max
handle = ref.observe(.value) { (snapshot) in
  ref.removeObserver(withHandle: handle)
}

One problem I had was:

  • we usually aren't retrieving a value from the root of our database
  • we build a path with ref.child("etc")
  • if we build a path with no value at the end, the observe callback is never fired
  • so how do we know when something doesn't exist at the path?

@morganchen12 morganchen12 self-assigned this Jan 29, 2018
@morganchen12
Copy link
Contributor

@ryanwilson can you tackle this for our next major release?

@ryanwilson
Copy link
Member Author

I'll do my best to get this in for the next major release. I'll have to check if there's enough time to deprecate and remove the old one by the next major release.

@ghost
Copy link

ghost commented Mar 7, 2018

Guess there wasn't enough time... This whole persistenceEnabled thing is really genius, but only being able to use it with the permanent observers makes it really hard to work with...

@ryanwilson
Copy link
Member Author

There's still time to get this in and I'm still giving it a shot. Apologies for the delay in response! Contributions are welcome for the implementation side of this, too 😃

@damirstuhec
Copy link

Any updates on this?

@Kimnyhuus
Copy link

Kimnyhuus commented May 14, 2018

I just ran into this issue because I'd experienced the exact same problem.
With persistence enabled, we cannot check if a child node exists because it only checks locally.

We're currently building a chat for our app.
Each chat corresponds to something called a "challenge".
So if a user creates a challenge which is send to multiple other users, then a chat is created in that same instance.

Because we're rolling out this chat after the app it self was launched, a lot of old challenges won't have a chat. Therefore we need to check if a chat exists or not. This is where "observeSingleEvent" became an issue.

Because we've enabled persistence. And after doing that, it was suddenly only the creator of the challenge on the exact phone it was created from, who could find the chat. When logging in to the same user on another phone, no chat is found and therefore we don't show a chat button, even though we know the chat exists.

My solution to this is:

var tempHandle: DatabaseHandle?
tempHandle = chatRef.child("foo").child("bar").observe(.value) { [weak self] (snapshot) in
    guard let handle = tempHandle, let `self` = self else { return }

    if snapshot.hasChild("someChatKey") {
        self.showChatButton()
        print("Has chat")
    }
    else {
        print("No chat")
    }

    chatRef.removeObserver(withHandle: handle)
}

After testing this, I can see in the console that says "No chat" first, and then "Has chat".
So it works, and we'll go with this for now. (The "else" gets deleted though. Only necessary for testing.)

@Kimnyhuus
Copy link

Any update on this?

After updating the Firebase pods on our project, the workaround described in this thread (and in my own comment above this) doesn't seem to work anymore.

Before, the ".observe" returned first the local data and then the online if there were any changes.
Now it only returns the local just as "observeSingleEvent" did when this issue were created.

To solve this issue I had to add a boolean flag, for get both the local and online data:

var tempHandle: DatabaseHandle?
var onlineReturned = false
tempHandle = chatRef.child("foo").child("bar").observe(.value) { [weak self] (snapshot) in
    guard let handle = tempHandle, let `self` = self else { return }

    if snapshot.hasChild("someChatKey") {
        self.showChatButton()
        print("Has chat")
    }
    else {
        print("No chat")
    }
    
    if onlineReturned {
        chatRef.removeObserver(withHandle: handle)
    }

    onlineReturned = true
}

This however, creates a minor issue because the observer isn't removed unless I receive data twice (offline & online). I'm getting around this by removing the observer on viewDidDisappear.

These are the versions of Firebase we run:
Using Firebase (5.0.1)
Using FirebaseABTesting (2.0.0)
Using FirebaseAnalytics (5.0.0)
Using FirebaseAuth (5.0.0)
Using FirebaseCore (5.0.1)
Using FirebaseDatabase (5.0.0)
Using FirebaseInstanceID (3.1.1)
Using FirebaseMessaging (3.0.0)
Using FirebaseRemoteConfig (3.0.0)

christibbs added a commit that referenced this issue Jan 25, 2019
* compile and run

* unit tests pass and swift target compiles

* dump build version in activity log

* standardize cross sdk imports in firebase iam code (#211)

standardize cross sdk imports & polish with style script

* using new sample app for testing non-development fiam sdk (#213)

* in-app messaging public interface cleanup (#216)

* slim down public header files

* move testing source files to their correct folders

* compiles

* style script fine-tune

* adding root object creation methods to follow the firebse iOS convension

* IAM url following refactor (#218)

* @available not supported by blaze build yet (#220)

* Clearcut integration for in-app messaging (#222)

* Update iam-master to catch up with latest master (#223)

* Support multiple triggers defined for the same iam message (#224)

* FIAM Clearcut Client Implmentation Improvement (#226)

* fixing action url bug and set auto dismiss to be 12 seconds (#227)

* use official public api dns name (#233)

* Logging firebase analytics events for fiam (#232)

* Fine tuning of fiam's code for loading data from caches (#234)

* fiam ui fine tuning before EAP (#235)

* allow fiam SDK functions to be disabled/enabled dynamically (#247)

* analytics event parameter name tuning (#253)

* migrate off @import for fiam code (#261)

* different modes for fiam sdk at runtime (#262)

* realtime clearcut when in running in simulator (#265)

* Update interface based on the Firebase API review feedback  (#267)

* check existence of fiam SDK resource bundle (#268)

* handling api keys with application restrictions (#269)

* fixing typo and refoctor for unit testing (#270)

* fix color method name clash and layout adjust (#271)

* Yongmao/layout issue and class rename (#273)

* fix long text height calc error

* rename class

* fiam modal view layout tuning (#274)

* test on device for ios client (#275)

* dynamic testing mode handling (#277)

* main header file comment update and run style.sh (#281)

* honor global firebase auto data collection flag in fiam (#283)

* honor global firebase auto data collection flag in fiam

* address comments

* tuning

* address review comments

* tuning for comments

* Rename Core global data collection switch (#287)

* Support displaying a messages defined as recurring (#286)

* continue

* Adjust some inapp messaging dependeces due to the firebase core changes from the upcoming release

* bump up version for in-app messaging

* Fix impression log clearing race condition bug. (#289)

* fix impression handling race condition bug

* podspec and interacting interfaces for new firebaseinappmessagingdisplay pod (#290)

* create inapp messaging display podspec

* Default fiam display UI implementation (#292)

* Default fiam display UI implementation. Created the new pod for FirebaseInAppMessagingDisplay

* Make FirebaseInAppMessaging headless (#293)

* import path tuning

* log test message events (#296)

* respect fetch wait time from server (#297)

* add changelog.md for fiam headless sdk (#300)

* port the fetch wait time change back into github (#304)

* remove resource bundle check for headless sdk

* fix a bug in which the test-on-device message does not follow action url

* Add category on NSString to interlace two strings, use it to obfuscate clearcut host name

* Use FIR prefix to avoid collisions

* Change method name for getting server hostname

* Delete Swift example project, its coverage is already duplicated in the FIRInAppMessagingDisplay UITests

* Miscellaneous open sourcing cleanup tasks

* Convert FIAM to use the Analytics connector (#336)

* Convert FIAM to use Analytics connector along with converting to the FIRLibrary component registration model.
* Update interop headers for missing methods
* Convert existing unit tests

* Fix style for fiam sources (#342)

* Clean up non-idiomatic ObjC method names

* Clean up Swift test target from Podfile

* Use dummy GoogleService-Info.plist and don't expose api keys (#343)

* Update plist to include fake API keys

* Remove changes in non fiam code from iam-master branch (#350)

* Fix a few method and parameter naming nits

* Fix comment typos in several files

* Delete remaining Swift project files
@kostassite
Copy link

@ryanwilson I tried to implement a solution that doesn't need a major release as it doesn't change the observe method signature. I implemented

- (FIRDatabaseQuery *)queryWithBypassCache {
    FQueryParams* params = [self.queryParams enableBypassCache];
    return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
                                             path:self.path
                                           params:params
                                    orderByCalled:self.orderByCalled
                             priorityMethodCalled:self.priorityMethodCalled];;
}

a FIRDatabaseQuery method that sets a property in FQueryParams and then when ask FPersistenceManager for a (FCacheNode *)serverCacheForQuery:(FQuerySpec *)query I return an null node

if (query.params.bypassCache) {
        complete = NO;
        node = [FSnapshotUtilities nodeFrom:nil];
    }

so the observer is not called with the empty node and waiting to get called with the new data from the server. If the user is offline, then the observer is not called at all, so dev needs to do a check for .info/connected or have a timeout.

so the call becomes something like this

ref.child("BlaBla").withBypassCache().observeSingleEvent(of: .value, with: { [weak self] (snapshot) in

Do you see any problem and how that maybe break a different part of the project, because persistence is deeply integrated in it?

the commit can be found here kodika@5dc0adb

@ryanwilson
Copy link
Member Author

Thank you for the attempt @kostassite!

I'm not the expert on Database but @schmidt-sebastian should have more context if this is going to be a problem or not.

@schmidt-sebastian
Copy link
Contributor

I think that we can take a lot from the PR you sent over @kostassite. Let me dwell a bit on the API and find suitable APIs for our other platforms as well. If we can avoid a breaking change, then we will be able to release this much faster.

Note that we just implemented this exact feature in Firestore: https://github.com/firebase/firebase-ios-sdk/blob/master/Firestore/Source/Public/FIRDocumentReference.h#L228. This was also done in a minor release.

@schmidt-sebastian schmidt-sebastian self-assigned this Mar 4, 2019
@Rahish
Copy link

Rahish commented May 8, 2019

I think that we can take a lot from the PR you sent over @kostassite. Let me dwell a bit on the API and find suitable APIs for our other platforms as well. If we can avoid a breaking change, then we will be able to release this much faster.

Note that we just implemented this exact feature in Firestore: https://github.com/firebase/firebase-ios-sdk/blob/master/Firestore/Source/Public/FIRDocumentReference.h#L228. This was also done in a minor release.

So when we can expect the Right solution for this issue? As its causing issues in the project and this issue is open from long ago

@n3at18
Copy link

n3at18 commented Sep 22, 2020

any update on this?

potential solution could be adding a flag to reference itself like "keepSynced" but without downloading the whole tree - it's not acceptable in many cases.

Like "reference.skipCache" flag which defaults to "false"

It shouldn't affect existing code as far as I can see

@schmidt-sebastian
Copy link
Contributor

We are currently in the process of designing a solution for this. This is in the very early stages, but some evidence can be found here: firebase/firebase-js-sdk#3812

@JCsplash
Copy link

JCsplash commented May 13, 2021

@kostassite Any updates on this? Is this issue fixed in version 7.5.0 released on Jan 21, 2021 with getData?

@kostassite
Copy link

@JCsplash I have pushed a commit with a potential fix in our repo but it is up to the Firebase team on when they will merge or create a similar solution. We are using although the solution for over a year now and we didn't have any problem.

@JCsplash
Copy link

@kostassite Gotcha, i think observeSingleEvent still has the same behavior but getData works great and always seems to fetch the latest data. Do you know of any downsides / side effects of using getData?

@paulb777
Copy link
Member

@schmidt-sebastian Any update on this one?

@mortenbekditlevsen
Copy link
Contributor

Wow, I didn't know this trick to avoid caching! Glad that it resurfaced! ❤️

@schmidt-sebastian
Copy link
Contributor

While we have not addressed the original language-level issue, the getData method addresses the shortcomings of observeSingleEvent. getData waits for the latest data from the backend when the client is online.

I am going to close this issue as we have not yet come up with a good way to address the original issue without a breaking change. getData solves the problem that led to this issue.

@firebase firebase locked and limited conversation to collaborators Sep 12, 2021
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

14 participants