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

Subscribing to two signals but one only executes once #1176

Closed
trevorsheridan opened this issue Mar 16, 2014 · 11 comments
Closed

Subscribing to two signals but one only executes once #1176

trevorsheridan opened this issue Mar 16, 2014 · 11 comments
Labels

Comments

@trevorsheridan
Copy link
Contributor

I'm trying to understand the best way to implement the following scenario:

Let's say I have two signals, one that returns cached data and one that returns data from the network. What I'm looking to do is subscribe to a signal that 'next's cached data immediately followed by 'next'ing data from the network request. Then any subsequent subscriptions on the signal are only sent data from the network since the cached data is no longer relevant.

I've taken a stab at this but I'd love feedback on whether there's a more declarative approach to knowing that the cached data signal has been executed once.

This illustrates the scenario:

RACSignal *cachedSignal, *networkSignal;
...
RACSignal *signal = [RACSignal doSignal:cachedSignal count:1 followedBy:networkSignal];
[signal subscribeNext:...];
[signal subscribeNext:...];
[signal subscribeNext:...];

// outputs
// data from cache
// data from network
// data from network
// data from network

And this is the implementation of doSignal:count:followedBy:

+ (RACSignal *)doSignal:(RACSignal *)signal count:(NSUInteger)count followedBy:(RACSignal *)followedBy
{
    __block NSUInteger used = 0;

    return [[RACSignal if:[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@(used < count)];
        used++;

        return nil;
    }] then:[signal concat:followedBy] else:followedBy] setNameWithFormat:@"+doSignal:count:followedBy:"];
}
@KyleLeneau
Copy link
Contributor

I have thought of this same problem before but my approach to thinking of it was a little different. I thought it would be cleaner to subscribe to the cache only and then let the cache do a network request if needed. The cache would then subscribe to the network call and update itself. That way new data from the network would end up in the cache first. That way the UI, ViewController, or ViewModel would only have to know how to communicate with a cache.

@Coneko
Copy link
Member

Coneko commented Mar 16, 2014

-takeUntilReplacement: might be what you're looking for. Check it out and let me know!

@trevorsheridan
Copy link
Contributor Author

Thanks for your feedback @KyleLeneau. The approach I'm taking is the way it is because I'm using MMRecord to perform the network request and update the cache for me (it updates CoreData behind the scenes), which is all wrapped in it's own signal. The "illustrated scenario" above doesn't really give the full picture. What I really have going is a method in a data manager that returns a signal that my ViewModel subscribes to. That method instantiates the cache and network signals, combines them into a single signal (by using doSignal:count:followedBy:) and returns that signal back to the caller. So really that method is acting like the "cache" in your situation but it's just breaking apart the actual act of retrieving from cache and performing a network request/saving to cache.

The goal behind this method is for my ViewModel to subscribe to it, get an initial set of data to populate a table, then immediately fire off a network request to get the latest data. Later after a certain action occurs (refresh the table view) the signal will react by grabbing new data and pushing it onto the signal.

@Coneko I will take a look now to see if I can get that to work!

@trevorsheridan
Copy link
Contributor Author

@Coneko -takeUntilReplacement: was exactly what I was looking for! Thanks 🍦

@Coneko
Copy link
Member

Coneko commented Mar 16, 2014

Great! I'll go ahead and close this but feel free to open it again if something else related to this comes up. Of course if something unrelated to this comes up you can always open a new issue.

@Coneko Coneko closed this as completed Mar 16, 2014
@trevorsheridan
Copy link
Contributor Author

Actually I just tested it and it doesn't work, and it's probably because I have a fundamental misunderstanding of RAC. Maybe you could help. I've expanded the example below:

Here we setup the signal using takeUntilReplacement:

RACSignal *cachedSignal, *networkSignal;
...
RACSignal *signal = [cachedSignal takeUntilReplacement:networkSignal];

Then immediately following I setup a binding on self.messages. It's triggered by self.refresh's value changing. When it does I map the signal.

RAC(self, messages) = [[RACObserve(self, refresh) map:^id(id value) {
    return signal;
}] switchToLatest];

[RACObserve(self, messages) subscribeNext:^(NSArray *messages) {
    NSLog(@"Update the interface with new messages: %@", messages);
}];

At this point the signal is subscribed to and it's triggered right away. The console produces:

Update the interface with new messages: (null)
Update the interface with new messages: [ @"array from cache" ]
...a few seconds later when the network request finishes...
Update the interface with new messages: [ @updated array" ]

To demonstrate the issue I invoked the signal again by setting the refresh value to YES

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    self.refresh = YES;
});

Which didn't produce the results I expected. What I got was an initial return from cache then the network request.

Update the interface with new messages: [ @"array from cache" ]
...a few seconds later when the network request finishes...
Update the interface with new messages: [ @updated array" ]

At this point I only want the network signal to be triggered and only produce:

...a few seconds later when the network request finishes...
Update the interface with new messages: [ @updated array" ]

This works correctly with my -doSignal:count:followedBy: method but it doesn't feel right to me. I'm just trying to understand why -takeUntilReplacement: doesn't remember it's previous state when it's triggered again. By that I mean why is the cachedSignal fired again?

@Coneko
Copy link
Member

Coneko commented Mar 17, 2014

Because signals in RAC are generally cold: it means they don't do anything until you subscribe to them, and they do what they do every time you subscribe to them. They're stateless by design.

You just have to combine them differently, for example:

RAC(self, messages) = [[[RACObserve(self, refresh) mapReplace:cachedSignal]
    takeUntilReplacement:[RACObserve(self, refresh) mapReplace:networkSignal]]
    swithToLatest];

@trevorsheridan
Copy link
Contributor Author

That doesn't seem to work for me. The response I'm getting is:

Update the interface with new messages: (null)
...a few seconds later when the network request finishes...
Update the interface with new messages: [ @updated array" ]

It seems that the cached signal is getting immediately replaced by the network signal. Also any subsequent refreshes doesn't cause anything else to happen, no network or cached request, nothing.

Also this approach seems to couple the caller very tightly to the data access implementation. Is it not necessarily possible (or advised) to have a signal that wraps two signals and knows under what conditions to invoke one, the other, or both?

Consider this is a method in some data manager:

- (RACSignal)messagesSignal
{
    RACSignal *cachedSignal, *networkSignal;
    ...
    // use the cache signal until the network signal sends next. then any subsequent subscriptions SHOULD only use the network signal.
    return [cachedSignal takeUntilReplacement:networkSignal];
}

Then my viewController (for sake of clarity) will invoke this method to obtain a new signal and subscribe to it however it pleases:

- (void)viewDidLoad
{
    RAC(self, messages) = [[RACObserve(self, refresh) 
        mapReplace:[DataManager messagesSignal]] 
        switchToLatest];

    [RACObserve(self, messages) subscribeNext:^(NSArray *messages) {
        NSLog(@"Update the interface with new messages: %@", messages);
    }];
}

Is this type of approach wrong in the RAC world? I just seems much cleaner from the highest level to be able to subscribe to a single signal and leave the implementation details of knowing when to use a caching signal vs. a networking signal to something lower.

@Coneko
Copy link
Member

Coneko commented Mar 17, 2014

That doesn't seem to work for me.

Yes sorry, what I wrote there was wrong, it should have been:

RACSignal *refreshFromCacheSignal = [[RACObserve(self, refresh)
    map:^(id value) {
        return cachedSignal;
    }]
    switchToLatest];

RACSignal *refreshFromNetworkSignal = [[RACObserve(self, refresh)
    map:^(id value) {
        return networkSignal;
    }]
    switchToLatest];

RAC(self, messages) = [refreshFromCacheSignal
    takeUntilReplacement:refreshFromNetworkSignal];

Basically the -takeUntilReplacement: must go at the very end, even after the -switchToLatest.

If you want to display the cached data immediately and have refreshes get the network data bypassing the cache it's much cleaner:

RACSignal *refreshFromNetworkSignal = [[RACObserve(self, refresh)
    map:^(id value) {
        return networkSignal;
    }]
    switchToLatest];

RAC(self, messages) = [cachedSignal
    takeUntilReplacement:refreshFromNetworkSignal];

not because it's necessarily a cleaner way of doing it generally speaking, but because -takeUntilReplacement: was made for this kind of pattern. It also sounds more like what you're describing in your post.

Is this type of approach wrong in the RAC world?

Generally cold signals are best, so we don't like introducing state in our signals. I would prefer @KyleLeneau's approach if you wanted to abstract data access.

@trevorsheridan
Copy link
Contributor Author

I see! This really helped shed light on the underlying concepts of RAC. I've taken the approach of letting my view model deal with observing a property on itself (self.refresh):

RACSignal *refreshedMessages = [[RACObserve(self, refresh)
        mapReplace:[MessagesModel networkSignal]]
        switchToLatest];

RAC(self, messages) = [[MessagesModel cachedSignal]
    takeUntilReplacement:refreshedMessages];

Then in my view controller:

[RACObserve(self.viewModel, messages) subscribeNext:^(NSArray *messages) {
    NSLog(@"Update the interface with new messages: %@", messages);
}];
...
[self.viewModel refreshMessages];

Thanks for your help @Coneko and @KyleLeneau!

@Coneko
Copy link
Member

Coneko commented Mar 17, 2014

No problem, glad I could be of help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants