aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Database/Core/FWriteTree.m
blob: c5b08ea5c17dc8df6cec0309b3c70c5e1d0b47c4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
/*
 * 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 "FWriteTree.h"
#import "FImmutableTree.h"
#import "FPath.h"
#import "FNode.h"
#import "FWriteTreeRef.h"
#import "FChildrenNode.h"
#import "FNamedNode.h"
#import "FWriteRecord.h"
#import "FEmptyNode.h"
#import "FIndex.h"
#import "FCompoundWrite.h"
#import "FCacheNode.h"

@interface FWriteTree ()
/**
* A tree tracking the results of applying all visible writes. This does not include transactions with
* applyLocally=false or writes that are completely shadowed by other writes.
* Contains id<FNode> as values.
*/
@property (nonatomic, strong) FCompoundWrite *visibleWrites;
/**
* A list of pending writes, regardless of visibility and shadowed-ness. Used to calcuate arbitrary
* sets of the changed data, such as hidden writes (from transactions) or changes with certain writes excluded (also
* used by transactions).
* Contains FWriteRecords.
*/
@property (nonatomic, strong) NSMutableArray *allWrites;
@property (nonatomic) NSInteger lastWriteId;
@end

/**
* FWriteTree tracks all pending user-initiated writes and has methods to calcuate the result of merging them with
* underlying server data (to create "event cache" data). Pending writes are added with addOverwriteAtPath: and
* addMergeAtPath: and removed with removeWriteId:.
*/
@implementation FWriteTree

@synthesize allWrites;
@synthesize lastWriteId;

- (id) init {
    self = [super init];
    if (self) {
        self.visibleWrites = [FCompoundWrite emptyWrite];
        self.allWrites = [[NSMutableArray alloc] init];
        self.lastWriteId = -1;
    }
    return self;
}

/**
* Create a new WriteTreeRef for the given path. For use with a new sync point at the given path.
*/
- (FWriteTreeRef *) childWritesForPath:(FPath *)path {
    return [[FWriteTreeRef alloc] initWithPath:path writeTree:self];
}

/**
* Record a new overwrite from user code.
* @param visible Is set to false by some transactions. It should be excluded from event caches.
*/
- (void) addOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible {
    NSAssert(writeId > self.lastWriteId, @"Stacking an older write on top of a newer one");
    FWriteRecord *record = [[FWriteRecord alloc] initWithPath:path overwrite:newData writeId:writeId visible:visible];
    [self.allWrites addObject:record];

    if (visible) {
        self.visibleWrites = [self.visibleWrites addWrite:newData atPath:path];
    }

    self.lastWriteId = writeId;
}

/**
* Record a new merge from user code.
* @param changedChildren maps NSString -> id<FNode>
*/
- (void) addMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId {
    NSAssert(writeId > self.lastWriteId, @"Stacking an older merge on top of newer one");
    FWriteRecord *record = [[FWriteRecord alloc] initWithPath:path merge:changedChildren writeId:writeId];
    [self.allWrites addObject:record];

    self.visibleWrites = [self.visibleWrites addCompoundWrite:changedChildren atPath:path];
    self.lastWriteId = writeId;
}

- (FWriteRecord *)writeForId:(NSInteger)writeId {
    NSUInteger index = [self.allWrites indexOfObjectPassingTest:^BOOL(FWriteRecord *write, NSUInteger idx, BOOL *stop) {
        return write.writeId == writeId;
    }];
    return (index == NSNotFound) ? nil : self.allWrites[index];
}

/**
* Remove a write (either an overwrite or merge) that has been successfully acknowledged by the server. Recalculates the
* tree if necessary. We return the path of the write and whether it may have been visible, meaning views need to
* reevaluate.
*
* @return YES if the write may have been visible (meaning we'll need to reevaluate / raise events as a result).
*/
- (BOOL) removeWriteId:(NSInteger)writeId {
    NSUInteger index = [self.allWrites indexOfObjectPassingTest:^BOOL(FWriteRecord *record, NSUInteger idx, BOOL *stop) {
        if (record.writeId == writeId) {
            return YES;
        } else {
            return NO;
        }
    }];
    NSAssert(index != NSNotFound, @"[FWriteTree removeWriteId:] called with nonexistent writeId.");
    FWriteRecord *writeToRemove = self.allWrites[index];
    [self.allWrites removeObjectAtIndex:index];

    BOOL removedWriteWasVisible = writeToRemove.visible;
    BOOL removedWriteOverlapsWithOtherWrites = NO;
    NSInteger i = [self.allWrites count] - 1;

    while (removedWriteWasVisible && i >= 0) {
        FWriteRecord *currentWrite = [self.allWrites objectAtIndex:i];
        if (currentWrite.visible) {
            if (i >= index && [self record:currentWrite containsPath:writeToRemove.path]) {
                // The removed write was completely shadowed by a subsequent write.
                removedWriteWasVisible = NO;
            } else if ([writeToRemove.path contains:currentWrite.path]) {
                // Either we're covering some writes or they're covering part of us (depending on which came first).
                removedWriteOverlapsWithOtherWrites = YES;
            }
        }
        i--;
    }

    if (!removedWriteWasVisible) {
        return NO;
    } else if (removedWriteOverlapsWithOtherWrites) {
        // There's some shadowing going on. Just rebuild the visible writes from scratch.
        [self resetTree];
        return YES;
    } else {
        // There's no shadowing.  We can safely just remove the write(s) from visibleWrites.
        if ([writeToRemove isOverwrite]) {
            self.visibleWrites = [self.visibleWrites removeWriteAtPath:writeToRemove.path];
        } else {
            FCompoundWrite *merge = writeToRemove.merge;
            [merge enumerateWrites:^(FPath *path, id<FNode> node, BOOL *stop) {
                self.visibleWrites = [self.visibleWrites removeWriteAtPath:[writeToRemove.path child:path]];
            }];
        }
        return YES;
    }
}

- (NSArray *) removeAllWrites {
    NSArray *writes = self.allWrites;
    self.visibleWrites = [FCompoundWrite emptyWrite];
    self.allWrites = [NSMutableArray array];
    return writes;
}

/**
* @return A complete snapshot for the given path if there's visible write data at that path, else nil.
* No server data is considered.
*/
- (id <FNode>) completeWriteDataAtPath:(FPath *)path {
    return [self.visibleWrites completeNodeAtPath:path];
}

/**
* Given optional, underlying server data, and an optional set of constraints (exclude some sets, include hidden
* writes), attempt to calculate a complete snapshot for the given path
* @param includeHiddenWrites Defaults to false, whether or not to layer on writes with visible set to false
*/
- (id <FNode>) calculateCompleteEventCacheAtPath:(FPath *)treePath completeServerCache:(id <FNode>)completeServerCache
                                 excludeWriteIds:(NSArray *)writeIdsToExclude includeHiddenWrites:(BOOL)includeHiddenWrites {
    if (writeIdsToExclude == nil && !includeHiddenWrites) {
        id<FNode> shadowingNode = [self.visibleWrites completeNodeAtPath:treePath];
        if (shadowingNode != nil) {
            return shadowingNode;
        } else {
            // No cache here. Can't claim complete knowledge.
            FCompoundWrite *subMerge = [self.visibleWrites childCompoundWriteAtPath:treePath];
            if (subMerge.isEmpty) {
                return completeServerCache;
            } else if (completeServerCache == nil && ![subMerge hasCompleteWriteAtPath:[FPath empty]]) {
                // We wouldn't have a complete snapshot since there's no underlying data and no complete shadow
                return nil;
            } else {
                id<FNode> layeredCache = completeServerCache != nil ? completeServerCache : [FEmptyNode emptyNode];
                return [subMerge applyToNode:layeredCache];
            }
        }
    } else {
        FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
        if (!includeHiddenWrites && merge.isEmpty) {
            return completeServerCache;
        } else {
            // If the server cache is null and we don't have a complete cache, we need to return nil
            if (!includeHiddenWrites && completeServerCache == nil && ![merge hasCompleteWriteAtPath:[FPath empty]]) {
                return nil;
            } else {
                BOOL (^filter) (FWriteRecord *) = ^(FWriteRecord *record) {
                    return (BOOL) ((record.visible || includeHiddenWrites) &&
                            (writeIdsToExclude == nil || ![writeIdsToExclude containsObject:[NSNumber numberWithInteger:record.writeId]]) &&
                            ([record.path contains:treePath] || [treePath contains:record.path]));
                };
                FCompoundWrite *mergeAtPath = [FWriteTree layerTreeFromWrites:self.allWrites filter:filter treeRoot:treePath];
                id<FNode> layeredCache = completeServerCache ? completeServerCache : [FEmptyNode emptyNode];
                return [mergeAtPath applyToNode:layeredCache];
            }
        }
    }
}

/**
* With optional, underlying server data, attempt to return a children node of children that we have complete data for.
* Used when creating new views, to pre-fill their complete event children snapshot.
*/
- (FChildrenNode *) calculateCompleteEventChildrenAtPath:(FPath *)treePath
                                  completeServerChildren:(id<FNode>)completeServerChildren {
    __block id<FNode> completeChildren = [FEmptyNode emptyNode];
    id<FNode> topLevelSet = [self.visibleWrites completeNodeAtPath:treePath];
    if (topLevelSet != nil) {
        if (![topLevelSet isLeafNode]) {
            // We're shadowing everything. Return the children.
            FChildrenNode *topChildrenNode = topLevelSet;
            [topChildrenNode enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
                completeChildren = [completeChildren updateImmediateChild:key withNewChild:node];
            }];
        }
        return completeChildren;
    } else {
        // Layer any children we have on top of this
        // We know we don't have a top-level set, so just enumerate existing children, and apply any updates
        FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
        [completeServerChildren enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
            FCompoundWrite *childMerge = [merge childCompoundWriteAtPath:[[FPath alloc] initWith:key]];
            id<FNode> newChildNode = [childMerge applyToNode:node];
            completeChildren = [completeChildren updateImmediateChild:key withNewChild:newChildNode];
        }];
        // Add any complete children we have from the set.
        for (FNamedNode *node in merge.completeChildren) {
            completeChildren = [completeChildren updateImmediateChild:node.name withNewChild:node.node];
        }
        return completeChildren;
    }
}

/**
* Given that the underlying server data has updated, determine what, if anything, needs to be applied to the event cache.
*
* Possibilities
*
* 1. No write are shadowing. Events should be raised, the snap to be applied comes from the server data.
*
* 2. Some write is completely shadowing. No events to be raised.
*
* 3. Is partially shadowed. Events ..
*
* Either existingEventSnap or existingServerSnap must exist.
*/
- (id <FNode>) calculateEventCacheAfterServerOverwriteAtPath:(FPath *)treePath childPath:(FPath *)childPath existingEventSnap:(id <FNode>)existingEventSnap existingServerSnap:(id <FNode>)existingServerSnap {
    NSAssert(existingEventSnap != nil || existingServerSnap != nil,
            @"Either existingEventSnap or existingServerSanp must exist.");

    FPath *path = [treePath child:childPath];
    if ([self.visibleWrites hasCompleteWriteAtPath:path]) {
        // At this point we can probably guarantee that we're in case 2, meaning no events
        // May need to check visibility while doing the findRootMostValueAndPath call
        return nil;
    } else {
        // This could be more efficient if the serverNode + updates doesn't change the eventSnap
        // However this is tricky to find out, since user updates don't necessary change the server
        // snap, e.g. priority updates on empty nodes, or deep deletes. Another special case is if the server
        // adds nodes, but doesn't change any existing writes. It is therefore not enough to
        // only check if the updates change the serverNode.
        // Maybe check if the merge tree contains these special cases and only do a full overwrite in that case?
        FCompoundWrite *childMerge = [self.visibleWrites childCompoundWriteAtPath:path];
        if (childMerge.isEmpty) {
            // We're not shadowing at all. Case 1
            return [existingServerSnap getChild:childPath];
        } else {
            return [childMerge applyToNode:[existingServerSnap getChild:childPath]];
        }
    }
}

/**
* Returns a complete child for a given server snap after applying all user writes or nil if there is no complete child
* for this child key.
*/
- (id<FNode>) calculateCompleteChildAtPath:(FPath *)treePath childKey:(NSString *)childKey cache:(FCacheNode *)existingServerCache {
    FPath *path = [treePath childFromString:childKey];
    id<FNode> shadowingNode = [self.visibleWrites completeNodeAtPath:path];
    if (shadowingNode != nil) {
        return shadowingNode;
    } else {
        if ([existingServerCache isCompleteForChild:childKey]) {
            FCompoundWrite *childMerge = [self.visibleWrites childCompoundWriteAtPath:path];
            return [childMerge applyToNode:[existingServerCache.node getImmediateChild:childKey]];
        } else {
            return nil;
        }
    }
}

/**
* Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at
* a higher path, this will return the child of that write relative to the write and this path.
* Returns null if there is no write at this path.
*/
- (id<FNode>) shadowingWriteAtPath:(FPath *)path {
    return [self.visibleWrites completeNodeAtPath:path];
}

/**
* This method is used when processing child remove events on a query. If we can, we pull in children that were outside
* the window, but may now be in the window.
*/
- (FNamedNode *)calculateNextNodeAfterPost:(FNamedNode *)post
                                    atPath:(FPath *)treePath
                        completeServerData:(id<FNode>)completeServerData
                                   reverse:(BOOL)reverse
                                     index:(id<FIndex>)index
{
    __block id<FNode> toIterate;
    FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
    id<FNode> shadowingNode = [merge completeNodeAtPath:[FPath empty]];
    if (shadowingNode != nil) {
        toIterate = shadowingNode;
    } else if (completeServerData != nil) {
        toIterate = [merge applyToNode:completeServerData];
    } else {
        return nil;
    }

    __block NSString *currentNextKey = nil;
    __block id<FNode> currentNextNode = nil;
    [toIterate enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
        if ([index compareKey:key andNode:node toOtherKey:post.name andNode:post.node reverse:reverse] > NSOrderedSame &&
            (!currentNextKey || [index compareKey:key andNode:node toOtherKey:currentNextKey andNode:currentNextNode reverse:reverse] < NSOrderedSame)) {
            currentNextKey = key;
            currentNextNode = node;
        }
    }];

    if (currentNextKey != nil) {
        return [FNamedNode nodeWithName:currentNextKey node:currentNextNode];
    } else {
        return nil;
    }
}

#pragma mark -
#pragma mark Private Methods

- (BOOL) record:(FWriteRecord *)record containsPath:(FPath *)path {
    if ([record isOverwrite]) {
        return [record.path contains:path];
    } else {
        __block BOOL contains = NO;
        [record.merge enumerateWrites:^(FPath *childPath, id<FNode> node, BOOL *stop) {
            contains = [[record.path child:childPath] contains:path];
            *stop = contains;
        }];
        return contains;
    }
}

/**
* Re-layer the writes and merges into a tree so we can efficiently calculate event snapshots
*/
- (void) resetTree {
    self.visibleWrites = [FWriteTree layerTreeFromWrites:self.allWrites filter:[FWriteTree defaultFilter] treeRoot:[FPath empty]];
    if ([self.allWrites count] > 0) {
        FWriteRecord *lastRecord = self.allWrites[[self.allWrites count] - 1];
        self.lastWriteId = lastRecord.writeId;
    } else {
        self.lastWriteId = -1;
    }
}

/**
* The default filter used when constructing the tree. Keep everything that's visible.
*/
+ (BOOL (^)(FWriteRecord *record)) defaultFilter {
    static BOOL (^filter)(FWriteRecord *);
    static dispatch_once_t filterToken;
    dispatch_once(&filterToken, ^{
        filter = ^(FWriteRecord *record) {
            return YES;
        };
    });
    return filter;
}

/**
* Static method. Given an array of WriteRecords, a filter for which ones to include, and a path, construct a merge
* at that path
* @return An FImmutableTree of id<FNode>s.
*/
+ (FCompoundWrite *) layerTreeFromWrites:(NSArray *)writes filter:(BOOL (^)(FWriteRecord *record))filter treeRoot:(FPath *)treeRoot {
    __block FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
    [writes enumerateObjectsUsingBlock:^(FWriteRecord *record, NSUInteger idx, BOOL *stop) {
        // Theory, a later set will either:
        // a) abort a relevant transaction, so no need to worry about excluding it from calculating that transaction
        // b) not be relevant to a transaction (separate branch), so again will not affect the data for that transaction
        if (filter(record)) {
            FPath *writePath = record.path;
            if ([record isOverwrite]) {
                if ([treeRoot contains:writePath]) {
                    FPath *relativePath = [FPath relativePathFrom:treeRoot to:writePath];
                    compoundWrite = [compoundWrite addWrite:record.overwrite atPath:relativePath];
                } else if ([writePath contains:treeRoot]) {
                    id<FNode> child = [record.overwrite getChild:[FPath relativePathFrom:writePath to:treeRoot]];
                    compoundWrite = [compoundWrite addWrite:child atPath:[FPath empty]];
                } else {
                    // There is no overlap between root path and write path, ignore write
                }
            } else {
                if ([treeRoot contains:writePath]) {
                    FPath *relativePath = [FPath relativePathFrom:treeRoot to:writePath];
                    compoundWrite = [compoundWrite addCompoundWrite:record.merge atPath:relativePath];
                } else if ([writePath contains:treeRoot]) {
                    FPath *relativePath = [FPath relativePathFrom:writePath to:treeRoot];
                    if (relativePath.isEmpty) {
                        compoundWrite = [compoundWrite addCompoundWrite:record.merge atPath:[FPath empty]];
                    } else {
                        id<FNode> child = [record.merge completeNodeAtPath:relativePath];
                        if (child != nil) {
                            // There exists a child in this node that matches the root path
                            id<FNode> deepNode = [child getChild:[relativePath popFront]];
                            compoundWrite = [compoundWrite addWrite:deepNode atPath:[FPath empty]];
                        }
                    }
                } else {
                    // There is no overlap between root path and write path, ignore write
                }
            }
        }
    }];
    return compoundWrite;
}

@end