2 * Copyright 2017 Google
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 #import "FIRMessagingPendingTopicsList.h"
19 #import "FIRMessaging_Private.h"
20 #import "FIRMessagingLogger.h"
21 #import "FIRMessagingPubSub.h"
23 #import "FIRMessagingDefines.h"
25 NSString *const kPendingTopicBatchActionKey = @"action";
26 NSString *const kPendingTopicBatchTopicsKey = @"topics";
28 NSString *const kPendingBatchesEncodingKey = @"batches";
29 NSString *const kPendingTopicsTimestampEncodingKey = @"ts";
31 #pragma mark - FIRMessagingTopicBatch
33 @interface FIRMessagingTopicBatch ()
35 @property(nonatomic, strong, nonnull) NSMutableDictionary
36 <NSString *, NSMutableArray <FIRMessagingTopicOperationCompletion> *> *topicHandlers;
40 @implementation FIRMessagingTopicBatch
42 - (instancetype)initWithAction:(FIRMessagingTopicAction)action {
43 if (self = [super init]) {
45 _topics = [NSMutableSet set];
46 _topicHandlers = [NSMutableDictionary dictionary];
53 - (void)encodeWithCoder:(NSCoder *)aCoder {
54 [aCoder encodeInteger:self.action forKey:kPendingTopicBatchActionKey];
55 [aCoder encodeObject:self.topics forKey:kPendingTopicBatchTopicsKey];
58 - (instancetype)initWithCoder:(NSCoder *)aDecoder {
60 // Ensure that our integer -> enum casting is safe
61 NSInteger actionRawValue = [aDecoder decodeIntegerForKey:kPendingTopicBatchActionKey];
62 FIRMessagingTopicAction action = FIRMessagingTopicActionSubscribe;
63 if (actionRawValue == FIRMessagingTopicActionUnsubscribe) {
64 action = FIRMessagingTopicActionUnsubscribe;
67 if (self = [self initWithAction:action]) {
68 NSSet *topics = [aDecoder decodeObjectForKey:kPendingTopicBatchTopicsKey];
69 if ([topics isKindOfClass:[NSSet class]]) {
70 _topics = [topics mutableCopy];
72 _topicHandlers = [NSMutableDictionary dictionary];
79 #pragma mark - FIRMessagingPendingTopicsList
81 @interface FIRMessagingPendingTopicsList ()
83 @property(nonatomic, readwrite, strong) NSDate *archiveDate;
84 @property(nonatomic, strong) NSMutableArray <FIRMessagingTopicBatch *> *topicBatches;
86 @property(nonatomic, strong) FIRMessagingTopicBatch *currentBatch;
87 @property(nonatomic, strong) NSMutableSet <NSString *> *topicsInFlight;
91 @implementation FIRMessagingPendingTopicsList
93 - (instancetype)init {
94 if (self = [super init]) {
95 _topicBatches = [NSMutableArray array];
96 _topicsInFlight = [NSMutableSet set];
101 + (void)pruneTopicBatches:(NSMutableArray <FIRMessagingTopicBatch *> *)topicBatches {
102 // For now, just remove empty batches. In the future we can use this to make the subscriptions
103 // more efficient, by actually pruning topic actions that cancel each other out, for example.
104 for (NSInteger i = topicBatches.count-1; i >= 0; i--) {
105 FIRMessagingTopicBatch *batch = topicBatches[i];
106 if (batch.topics.count == 0) {
107 [topicBatches removeObjectAtIndex:i];
112 #pragma mark NSCoding
114 - (void)encodeWithCoder:(NSCoder *)aCoder {
115 [aCoder encodeObject:[NSDate date] forKey:kPendingTopicsTimestampEncodingKey];
116 [aCoder encodeObject:self.topicBatches forKey:kPendingBatchesEncodingKey];
119 - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
121 if (self = [self init]) {
122 _archiveDate = [aDecoder decodeObjectForKey:kPendingTopicsTimestampEncodingKey];
123 NSArray *archivedBatches = [aDecoder decodeObjectForKey:kPendingBatchesEncodingKey];
124 if (archivedBatches) {
125 _topicBatches = [archivedBatches mutableCopy];
126 [FIRMessagingPendingTopicsList pruneTopicBatches:_topicBatches];
128 _topicsInFlight = [NSMutableSet set];
135 - (NSUInteger)numberOfBatches {
136 return self.topicBatches.count;
139 #pragma mark Adding/Removing topics
141 - (void)addOperationForTopic:(NSString *)topic
142 withAction:(FIRMessagingTopicAction)action
143 completion:(nullable FIRMessagingTopicOperationCompletion)completion {
145 FIRMessagingTopicBatch *lastBatch = nil;
146 @synchronized (self) {
147 lastBatch = self.topicBatches.lastObject;
148 if (!lastBatch || lastBatch.action != action) {
149 // There either was no last batch, or our last batch's action was not the same, so we have to
150 // create a new batch
151 lastBatch = [[FIRMessagingTopicBatch alloc] initWithAction:action];
152 [self.topicBatches addObject:lastBatch];
154 BOOL topicExistedBefore = ([lastBatch.topics member:topic] != nil);
155 if (!topicExistedBefore) {
156 [lastBatch.topics addObject:topic];
157 [self.delegate pendingTopicsListDidUpdate:self];
159 // Add the completion handler to the batch
161 NSMutableArray *handlers = lastBatch.topicHandlers[topic];
163 handlers = [[NSMutableArray alloc] init];
165 [handlers addObject:completion];
166 lastBatch.topicHandlers[topic] = handlers;
168 if (!self.currentBatch) {
169 self.currentBatch = lastBatch;
171 // This may have been the first topic added, or was added to an ongoing batch
172 if (self.currentBatch == lastBatch && !topicExistedBefore) {
173 // Add this topic to our ongoing operations
174 FIRMessaging_WEAKIFY(self);
175 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
176 FIRMessaging_STRONGIFY(self);
177 [self resumeOperationsIfNeeded];
183 - (void)resumeOperationsIfNeeded {
184 @synchronized (self) {
185 // If current batch is not set, set it now
186 if (!self.currentBatch) {
187 self.currentBatch = self.topicBatches.firstObject;
189 if (self.currentBatch.topics.count == 0) {
192 if (!self.delegate) {
193 FIRMessagingLoggerError(kFIRMessagingMessageCodePendingTopicsList000,
194 @"Attempted to update pending topics without a delegate");
197 if (![self.delegate pendingTopicsListCanRequestTopicUpdates:self]) {
200 for (NSString *topic in self.currentBatch.topics) {
201 if ([self.topicsInFlight member:topic]) {
202 // This topic is already active, so skip
205 [self beginUpdateForCurrentBatchTopic:topic];
210 - (BOOL)subscriptionErrorIsRecoverable:(NSError *)error {
211 return [error.domain isEqualToString:NSURLErrorDomain];
214 - (void)beginUpdateForCurrentBatchTopic:(NSString *)topic {
216 @synchronized (self) {
217 [self.topicsInFlight addObject:topic];
219 FIRMessaging_WEAKIFY(self);
221 pendingTopicsList:self
222 requestedUpdateForTopic:topic
223 action:self.currentBatch.action
224 completion:^(NSError *error) {
226 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
227 FIRMessaging_STRONGIFY(self);
228 @synchronized(self) {
229 [self.topicsInFlight removeObject:topic];
231 BOOL recoverableError = [self subscriptionErrorIsRecoverable:error];
232 if (!error || !recoverableError) {
233 // Notify our handlers and remove the topic from our batch
234 NSMutableArray *handlers = self.currentBatch.topicHandlers[topic];
235 if (handlers.count) {
236 dispatch_async(dispatch_get_main_queue(), ^{
237 for (FIRMessagingTopicOperationCompletion handler in handlers) {
240 [handlers removeAllObjects];
243 [self.currentBatch.topics removeObject:topic];
244 [self.currentBatch.topicHandlers removeObjectForKey:topic];
245 if (self.currentBatch.topics.count == 0) {
246 // All topic updates successfully finished in this batch, move on
248 [self.topicBatches removeObject:self.currentBatch];
249 self.currentBatch = nil;
251 [self.delegate pendingTopicsListDidUpdate:self];
252 FIRMessaging_WEAKIFY(self);
254 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),
256 FIRMessaging_STRONGIFY(self);
257 [self resumeOperationsIfNeeded];