aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Messaging/FIRMessagingPendingTopicsList.m
diff options
context:
space:
mode:
Diffstat (limited to 'Firebase/Messaging/FIRMessagingPendingTopicsList.m')
-rw-r--r--Firebase/Messaging/FIRMessagingPendingTopicsList.m261
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