diff options
Diffstat (limited to 'Firebase/Messaging/FIRMessagingPendingTopicsList.m')
-rw-r--r-- | Firebase/Messaging/FIRMessagingPendingTopicsList.m | 261 |
1 files changed, 261 insertions, 0 deletions
diff --git a/Firebase/Messaging/FIRMessagingPendingTopicsList.m b/Firebase/Messaging/FIRMessagingPendingTopicsList.m new file mode 100644 index 0000000..792090e --- /dev/null +++ b/Firebase/Messaging/FIRMessagingPendingTopicsList.m @@ -0,0 +1,261 @@ +/* + * 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 arrayWithCapacity:1]; + } + [handlers addObject:completion]; + } + 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:^(FIRMessagingTopicOperationResult result, 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 (result == FIRMessagingTopicOperationResultSucceeded || + result == FIRMessagingTopicOperationResultCancelled || + !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(result, 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 |