You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

265 lines
9.3 KiB

/*
* Copyright 2017 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FIRMessagingPendingTopicsList.h"
#import "FIRMessaging_Private.h"
#import "FIRMessagingLogger.h"
#import "FIRMessagingPubSub.h"
#import "FIRMessagingDefines.h"
NSString *const kPendingTopicBatchActionKey = @"action";
NSString *const kPendingTopicBatchTopicsKey = @"topics";
NSString *const kPendingBatchesEncodingKey = @"batches";
NSString *const kPendingTopicsTimestampEncodingKey = @"ts";
#pragma mark - FIRMessagingTopicBatch
@interface FIRMessagingTopicBatch ()
@property(nonatomic, strong, nonnull) NSMutableDictionary
<NSString *, NSMutableArray <FIRMessagingTopicOperationCompletion> *> *topicHandlers;
@end
@implementation FIRMessagingTopicBatch
- (instancetype)initWithAction:(FIRMessagingTopicAction)action {
if (self = [super init]) {
_action = action;
_topics = [NSMutableSet set];
_topicHandlers = [NSMutableDictionary dictionary];
}
return self;
}
#pragma mark NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeInteger:self.action forKey:kPendingTopicBatchActionKey];
[aCoder encodeObject:self.topics forKey:kPendingTopicBatchTopicsKey];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
// Ensure that our integer -> enum casting is safe
NSInteger actionRawValue = [aDecoder decodeIntegerForKey:kPendingTopicBatchActionKey];
FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe;
if (actionRawValue == FIRMessagingTopicActionUnsubscribe) {
action = FIRMessagingTopicActionUnsubscribe;
}
if (self = [self initWithAction:action]) {
NSSet *topics = [aDecoder decodeObjectForKey:kPendingTopicBatchTopicsKey];
if ([topics isKindOfClass:[NSSet class]]) {
_topics = [topics mutableCopy];
}
_topicHandlers = [NSMutableDictionary dictionary];
}
return self;
}
@end
#pragma mark - FIRMessagingPendingTopicsList
@interface FIRMessagingPendingTopicsList ()
@property(nonatomic, readwrite, strong) NSDate *archiveDate;
@property(nonatomic, strong) NSMutableArray <FIRMessagingTopicBatch *> *topicBatches;
@property(nonatomic, strong) FIRMessagingTopicBatch *currentBatch;
@property(nonatomic, strong) NSMutableSet <NSString *> *topicsInFlight;
@end
@implementation FIRMessagingPendingTopicsList
- (instancetype)init {
if (self = [super init]) {
_topicBatches = [NSMutableArray array];
_topicsInFlight = [NSMutableSet set];
}
return self;
}
+ (void)pruneTopicBatches:(NSMutableArray <FIRMessagingTopicBatch *> *)topicBatches {
// For now, just remove empty batches. In the future we can use this to make the subscriptions
// more efficient, by actually pruning topic actions that cancel each other out, for example.
for (NSInteger i = topicBatches.count-1; i >= 0; i--) {
FIRMessagingTopicBatch *batch = topicBatches[i];
if (batch.topics.count == 0) {
[topicBatches removeObjectAtIndex:i];
}
}
}
#pragma mark NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:[NSDate date] forKey:kPendingTopicsTimestampEncodingKey];
[aCoder encodeObject:self.topicBatches forKey:kPendingBatchesEncodingKey];
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [self init]) {
_archiveDate = [aDecoder decodeObjectForKey:kPendingTopicsTimestampEncodingKey];
NSArray *archivedBatches = [aDecoder decodeObjectForKey:kPendingBatchesEncodingKey];
if (archivedBatches) {
_topicBatches = [archivedBatches mutableCopy];
[FIRMessagingPendingTopicsList pruneTopicBatches:_topicBatches];
}
_topicsInFlight = [NSMutableSet set];
}
return self;
}
#pragma mark Getters
- (NSUInteger)numberOfBatches {
return self.topicBatches.count;
}
#pragma mark Adding/Removing topics
- (void)addOperationForTopic:(NSString *)topic
withAction:(FIRMessagingTopicAction)action
completion:(nullable FIRMessagingTopicOperationCompletion)completion {
FIRMessagingTopicBatch *lastBatch = nil;
@synchronized (self) {
lastBatch = self.topicBatches.lastObject;
if (!lastBatch || lastBatch.action != action) {
// There either was no last batch, or our last batch's action was not the same, so we have to
// create a new batch
lastBatch = [[FIRMessagingTopicBatch alloc] initWithAction:action];
[self.topicBatches addObject:lastBatch];
}
BOOL topicExistedBefore = ([lastBatch.topics member:topic] != nil);
if (!topicExistedBefore) {
[lastBatch.topics addObject:topic];
[self.delegate pendingTopicsListDidUpdate:self];
}
// Add the completion handler to the batch
if (completion) {
NSMutableArray *handlers = lastBatch.topicHandlers[topic];
if (!handlers) {
handlers = [[NSMutableArray alloc] init];
}
[handlers addObject:completion];
lastBatch.topicHandlers[topic] = handlers;
}
if (!self.currentBatch) {
self.currentBatch = lastBatch;
}
// This may have been the first topic added, or was added to an ongoing batch
if (self.currentBatch == lastBatch && !topicExistedBefore) {
// Add this topic to our ongoing operations
FIRMessaging_WEAKIFY(self);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
FIRMessaging_STRONGIFY(self);
[self resumeOperationsIfNeeded];
});
}
}
}
- (void)resumeOperationsIfNeeded {
@synchronized (self) {
// If current batch is not set, set it now
if (!self.currentBatch) {
self.currentBatch = self.topicBatches.firstObject;
}
if (self.currentBatch.topics.count == 0) {
return;
}
if (!self.delegate) {
FIRMessagingLoggerError(kFIRMessagingMessageCodePendingTopicsList000,
@"Attempted to update pending topics without a delegate");
return;
}
if (![self.delegate pendingTopicsListCanRequestTopicUpdates:self]) {
return;
}
for (NSString *topic in self.currentBatch.topics) {
if ([self.topicsInFlight member:topic]) {
// This topic is already active, so skip
continue;
}
[self beginUpdateForCurrentBatchTopic:topic];
}
}
}
- (BOOL)subscriptionErrorIsRecoverable:(NSError *)error {
return [error.domain isEqualToString:NSURLErrorDomain];
}
- (void)beginUpdateForCurrentBatchTopic:(NSString *)topic {
@synchronized (self) {
[self.topicsInFlight addObject:topic];
}
FIRMessaging_WEAKIFY(self);
[self.delegate
pendingTopicsList:self
requestedUpdateForTopic:topic
action:self.currentBatch.action
completion:^(NSError *error) {
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
FIRMessaging_STRONGIFY(self);
@synchronized(self) {
[self.topicsInFlight removeObject:topic];
BOOL recoverableError = [self subscriptionErrorIsRecoverable:error];
if (!error || !recoverableError) {
// Notify our handlers and remove the topic from our batch
NSMutableArray *handlers = self.currentBatch.topicHandlers[topic];
if (handlers.count) {
dispatch_async(dispatch_get_main_queue(), ^{
for (FIRMessagingTopicOperationCompletion handler in handlers) {
handler(error);
}
[handlers removeAllObjects];
});
}
[self.currentBatch.topics removeObject:topic];
[self.currentBatch.topicHandlers removeObjectForKey:topic];
if (self.currentBatch.topics.count == 0) {
// All topic updates successfully finished in this batch, move on
// to the next batch
[self.topicBatches removeObject:self.currentBatch];
self.currentBatch = nil;
}
[self.delegate pendingTopicsListDidUpdate:self];
FIRMessaging_WEAKIFY(self);
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),
^{
FIRMessaging_STRONGIFY(self);
[self resumeOperationsIfNeeded];
});
}
}
});
}];
}
@end