aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Database/Api/FIRDatabaseQuery.m
blob: de18a7c0e9c7b25c8a5d7479c79a5be191262969 (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
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
/*
 * 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 "FIRDatabaseQuery.h"
#import "FIRDatabaseQuery_Private.h"
#import "FValidation.h"
#import "FQueryParams.h"
#import "FQuerySpec.h"
#import "FValueEventRegistration.h"
#import "FChildEventRegistration.h"
#import "FPath.h"
#import "FKeyIndex.h"
#import "FPathIndex.h"
#import "FPriorityIndex.h"
#import "FValueIndex.h"
#import "FLeafNode.h"
#import "FSnapshotUtilities.h"
#import "FConstants.h"

@implementation FIRDatabaseQuery

@synthesize repo;
@synthesize path;
@synthesize queryParams;

#define INVALID_QUERY_PARAM_ERROR @"InvalidQueryParameter"


+ (dispatch_queue_t)sharedQueue
{
    // We use this shared queue across all of the FQueries so things happen FIFO (as opposed to dispatch_get_global_queue(0, 0) which is concurrent)
    static dispatch_once_t pred;
    static dispatch_queue_t sharedDispatchQueue;

    dispatch_once(&pred, ^{
        sharedDispatchQueue = dispatch_queue_create("FirebaseWorker", NULL);
    });

    return sharedDispatchQueue;
}

- (id) initWithRepo:(FRepo *)theRepo path:(FPath *)thePath {
    return [self initWithRepo:theRepo path:thePath params:nil orderByCalled:NO priorityMethodCalled:NO];
}

- (id) initWithRepo:(FRepo *)theRepo
               path:(FPath *)thePath
             params:(FQueryParams *)theParams
      orderByCalled:(BOOL)orderByCalled
priorityMethodCalled:(BOOL)priorityMethodCalled {
    self = [super init];
    if (self) {
        self.repo = theRepo;
        self.path = thePath;
        if (!theParams) {
            theParams = [FQueryParams defaultInstance];
        }
        if (![theParams isValid]) {
            @throw [[NSException alloc] initWithName:@"InvalidArgumentError" reason:@"Queries are limited to two constraints" userInfo:nil];
        }
        self.queryParams = theParams;
        self.orderByCalled = orderByCalled;
        self.priorityMethodCalled = priorityMethodCalled;
    }
    return self;
}

- (FQuerySpec *)querySpec {
    return [[FQuerySpec alloc] initWithPath:self.path params:self.queryParams];
}

- (void)validateQueryEndpointsForParams:(FQueryParams *)params {
    if ([params.index isEqual:[FKeyIndex keyIndex]]) {
        if ([params hasStart]) {
            if (params.indexStartKey != [FUtilities minName]) {
                [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryStartingAtValue:childKey: or queryEqualTo:andChildKey: in combination with queryOrderedByKey"];
            }
            if (![params.indexStartValue.val isKindOfClass:[NSString class]]) {
                [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryStartingAtValue: with other types than string in combination with queryOrderedByKey"];
            }
        }
        if ([params hasEnd]) {
            if (params.indexEndKey != [FUtilities maxName]) {
                [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryEndingAtValue:childKey: or queryEqualToValue:childKey: in combination with queryOrderedByKey"];
            }
            if (![params.indexEndValue.val isKindOfClass:[NSString class]]) {
                [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryEndingAtValue: with other types than string in combination with queryOrderedByKey"];
            }
        }
    } else if ([params.index isEqual:[FPriorityIndex priorityIndex]]) {
        if (([params hasStart] && ![FValidation validatePriorityValue:params.indexStartValue.val]) ||
            ([params hasEnd] && ![FValidation validatePriorityValue:params.indexEndValue.val])) {
            [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"When using queryOrderedByPriority, values provided to queryStartingAtValue:, queryEndingAtValue:, or queryEqualToValue: must be valid priorities."];
        }
    }
}

- (void)validateEqualToCall {
    if ([self.queryParams hasStart]) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot combine queryEqualToValue: and queryStartingAtValue:"];
    }
    if ([self.queryParams hasEnd]) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot combine queryEqualToValue: and queryEndingAtValue:"];
    }
}

- (void)validateNoPreviousOrderByCalled {
    if (self.orderByCalled) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot use multiple queryOrderedBy calls!"];
    }
}

- (void)validateIndexValueType:(id)type fromMethod:(NSString *)method {
    if (type != nil &&
        ![type isKindOfClass:[NSNumber class]] &&
        ![type isKindOfClass:[NSString class]] &&
        ![type isKindOfClass:[NSNull class]]) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"You can only pass nil, NSString or NSNumber to %@", method];
    }
}

- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue {
    return [self queryStartingAtInternal:startValue childKey:nil from:@"queryStartingAtValue:" priorityMethod:NO];
}

- (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue childKey:(NSString *)childKey {
    if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
        @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
                                          reason:@"You must use queryStartingAtValue: instead of queryStartingAtValue:childKey: when using queryOrderedByKey:"
                                        userInfo:nil];
    }
    return [self queryStartingAtInternal:startValue
                                childKey:childKey
                                    from:@"queryStartingAtValue:childKey:"
                          priorityMethod:NO];
}

- (FIRDatabaseQuery *)queryStartingAtInternal:(id<FNode>)startValue
                                     childKey:(NSString *)childKey
                                         from:(NSString *)methodName
                               priorityMethod:(BOOL)priorityMethod {
    [self validateIndexValueType:startValue fromMethod:methodName];
    if (childKey != nil) {
         [FValidation validateFrom:methodName validKey:childKey];
    }
    if ([self.queryParams hasStart]) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR
                    format:@"Can't call %@ after queryStartingAtValue or queryEqualToValue was previously called", methodName];
    }
    id<FNode> startNode = [FSnapshotUtilities nodeFrom:startValue];
    FQueryParams* params = [self.queryParams startAt:startNode childKey:childKey];
    [self validateQueryEndpointsForParams:params];
    return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
                                             path:self.path
                                           params:params
                                    orderByCalled:self.orderByCalled
                             priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
}

- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue {
    return [self queryEndingAtInternal:endValue
                              childKey:nil
                                  from:@"queryEndingAtValue:"
                        priorityMethod:NO];
}

- (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue childKey:(NSString *)childKey {
    if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
        @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
                                          reason:@"You must use queryEndingAtValue: instead of queryEndingAtValue:childKey: when using queryOrderedByKey:"
                                        userInfo:nil];
    }

    return [self queryEndingAtInternal:endValue
                           childKey:childKey
                                  from:@"queryEndingAtValue:childKey:"
                        priorityMethod:NO];
}

- (FIRDatabaseQuery *)queryEndingAtInternal:(id)endValue
                                   childKey:(NSString *)childKey
                                       from:(NSString *)methodName
                             priorityMethod:(BOOL)priorityMethod {
    [self validateIndexValueType:endValue fromMethod:methodName];
    if (childKey != nil) {
        [FValidation validateFrom:methodName validKey:childKey];
    }
    if ([self.queryParams hasEnd]) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR
                    format:@"Can't call %@ after queryEndingAtValue or queryEqualToValue was previously called", methodName];
    }
    id<FNode> endNode = [FSnapshotUtilities nodeFrom:endValue];
    FQueryParams* params = [self.queryParams endAt:endNode childKey:childKey];
    [self validateQueryEndpointsForParams:params];
    return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
                                             path:self.path
                                           params:params
                                    orderByCalled:self.orderByCalled
                             priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
}

- (FIRDatabaseQuery *)queryEqualToValue:(id)value {
    return [self queryEqualToInternal:value childKey:nil from:@"queryEqualToValue:" priorityMethod:NO];
}

- (FIRDatabaseQuery *)queryEqualToValue:(id)value childKey:(NSString *)childKey {
    if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
        @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
                                          reason:@"You must use queryEqualToValue: instead of queryEqualTo:childKey: when using queryOrderedByKey:"
                                        userInfo:nil];
    }
    return [self queryEqualToInternal:value childKey:childKey from:@"queryEqualToValue:childKey:" priorityMethod:NO];
}

- (FIRDatabaseQuery *)queryEqualToInternal:(id)value
                                  childKey:(NSString *)childKey
                                      from:(NSString *)methodName
                            priorityMethod:(BOOL)priorityMethod {
    [self validateIndexValueType:value fromMethod:methodName];
    if (childKey != nil) {
        [FValidation validateFrom:methodName validKey:childKey];
    }
    if ([self.queryParams hasEnd] || [self.queryParams hasStart]) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR
                    format:@"Can't call %@ after queryStartingAtValue, queryEndingAtValue or queryEqualToValue was previously called", methodName];
    }
    id<FNode> node = [FSnapshotUtilities nodeFrom:value];
    FQueryParams* params = [[self.queryParams startAt:node childKey:childKey] endAt:node childKey:childKey];
    [self validateQueryEndpointsForParams:params];
    return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
                                             path:self.path
                                           params:params
                                    orderByCalled:self.orderByCalled
                             priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
}

- (void)validateLimitRange:(NSUInteger)limit
{
    // No need to check for negative ranges, since limit is unsigned
    if (limit == 0) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Limit can't be zero"];
    }
    if (limit >= 1ul<<31) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Limit must be less than 2,147,483,648"];
    }
}

- (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit {
    if (self.queryParams.limitSet) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't call queryLimitedToFirst: if a limit was previously set"];
    }
    [self validateLimitRange:limit];
    FQueryParams* params = [self.queryParams limitToFirst:limit];
    return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
                                             path:self.path
                                           params:params
                                    orderByCalled:self.orderByCalled
                             priorityMethodCalled:self.priorityMethodCalled];
}

- (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit {
    if (self.queryParams.limitSet) {
        [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't call queryLimitedToLast: if a limit was previously set"];
    }
    [self validateLimitRange:limit];
    FQueryParams* params = [self.queryParams limitToLast:limit];
    return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
                                             path:self.path
                                           params:params
                                    orderByCalled:self.orderByCalled
                             priorityMethodCalled:self.priorityMethodCalled];
}

- (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)indexPathString {
    if ([indexPathString isEqualToString:@"$key"] || [indexPathString isEqualToString:@".key"]) {
        @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
                                          reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid.  Use queryOrderedByKey: instead.", indexPathString]
                                        userInfo:nil];
    } else if ([indexPathString isEqualToString:@"$priority"] || [indexPathString isEqualToString:@".priority"]) {
        @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
                                          reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid.  Use queryOrderedByPriority: instead.", indexPathString]
                                        userInfo:nil];
    } else if ([indexPathString isEqualToString:@"$value"] || [indexPathString isEqualToString:@".value"]) {
        @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
                                          reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid.  Use queryOrderedByValue: instead.", indexPathString]
                                        userInfo:nil];
    }
    [self validateNoPreviousOrderByCalled];

    [FValidation validateFrom:@"queryOrderedByChild:" validPathString:indexPathString];
    FPath *indexPath = [FPath pathWithString:indexPathString];
    if (indexPath.isEmpty) {
        @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
                                          reason:[NSString stringWithFormat:@"(queryOrderedByChild:) with an empty path is invalid.  Use queryOrderedByValue: instead."]
                                        userInfo:nil];
    }
    id<FIndex> index = [[FPathIndex alloc] initWithPath:indexPath];

    FQueryParams *params = [self.queryParams orderBy:index];
    [self validateQueryEndpointsForParams:params];
    return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
                                             path:self.path
                                           params:params
                                    orderByCalled:YES
                             priorityMethodCalled:self.priorityMethodCalled];
}

- (FIRDatabaseQuery *) queryOrderedByKey {
    [self validateNoPreviousOrderByCalled];
    FQueryParams *params = [self.queryParams orderBy:[FKeyIndex keyIndex]];
    [self validateQueryEndpointsForParams:params];
    return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
                                             path:self.path
                                           params:params
                                    orderByCalled:YES
                             priorityMethodCalled:self.priorityMethodCalled];
}

- (FIRDatabaseQuery *) queryOrderedByValue {
    [self validateNoPreviousOrderByCalled];
    FQueryParams *params = [self.queryParams orderBy:[FValueIndex valueIndex]];
    return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
                                             path:self.path
                                           params:params
                                    orderByCalled:YES
                             priorityMethodCalled:self.priorityMethodCalled];
}

- (FIRDatabaseQuery *) queryOrderedByPriority {
    [self validateNoPreviousOrderByCalled];
    FQueryParams *params = [self.queryParams orderBy:[FPriorityIndex priorityIndex]];
    return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
                                             path:self.path
                                           params:params
                                    orderByCalled:YES
                             priorityMethodCalled:self.priorityMethodCalled];
}

- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *))block {
    [FValidation validateFrom:@"observeEventType:withBlock:" knownEventType:eventType];
    return [self observeEventType:eventType withBlock:block withCancelBlock:nil];
}


- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
    [FValidation validateFrom:@"observeEventType:andPreviousSiblingKeyWithBlock:" knownEventType:eventType];
    return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
}


- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
    [FValidation validateFrom:@"observeEventType:withBlock:withCancelBlock:" knownEventType:eventType];

    if (eventType == FIRDataEventTypeValue) {
        // Handle FIRDataEventTypeValue specially because they shouldn't have prevName callbacks
        NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
        [self observeValueEventWithHandle:handle withBlock:block cancelCallback:cancelBlock];
        return handle;
    } else {
        // Wrap up the userCallback so we can treat everything as a callback that has a prevName
        fbt_void_datasnapshot userCallback = [block copy];
        return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
            if (userCallback != nil) {
                userCallback(snapshot);
            }
        } withCancelBlock:cancelBlock];
    }
}

- (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
    [FValidation validateFrom:@"observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:" knownEventType:eventType];


    if (eventType == FIRDataEventTypeValue) {
        // TODO: This gets hit by observeSingleEventOfType.  Need to fix.
        /*
        @throw [[NSException alloc] initWithName:@"InvalidEventTypeForObserver"
                                          reason:@"(observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:) Cannot use observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock: with FIRDataEventTypeValue. Use observeEventType:withBlock:withCancelBlock: instead."
                                        userInfo:nil];
        */
    }

    NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
    NSDictionary *callbacks = @{[NSNumber numberWithInteger:eventType]: [block copy]};
    [self observeChildEventWithHandle:handle withCallbacks:callbacks cancelCallback:cancelBlock];

    return handle;
}

// If we want to distinguish between value event listeners and child event listeners, like in the Java client, we can
// consider exporting this. If we do, add argument validation. Otherwise, arguments are validated in the public-facing
// portions of the API. Also, move the FIRDatabaseHandle logic.
- (void)observeValueEventWithHandle:(FIRDatabaseHandle)handle withBlock:(fbt_void_datasnapshot)block cancelCallback:(fbt_void_nserror)cancelBlock {
    // Note that we don't need to copy the callbacks here, FEventRegistration callback properties set to copy
    FValueEventRegistration *registration = [[FValueEventRegistration alloc] initWithRepo:self.repo
                                                                                   handle:handle
                                                                                 callback:block
                                                                           cancelCallback:cancelBlock];
    dispatch_async([FIRDatabaseQuery sharedQueue], ^{
        [self.repo addEventRegistration:registration forQuery:self.querySpec];
    });
}

// Note: as with the above method, we may wish to expose this at some point.
- (void)observeChildEventWithHandle:(FIRDatabaseHandle)handle withCallbacks:(NSDictionary *)callbacks cancelCallback:(fbt_void_nserror)cancelBlock {
    // Note that we don't need to copy the callbacks here, FEventRegistration callback properties set to copy
    FChildEventRegistration *registration = [[FChildEventRegistration alloc] initWithRepo:self.repo
                                                                                   handle:handle
                                                                                callbacks:callbacks
                                                                           cancelCallback:cancelBlock];
    dispatch_async([FIRDatabaseQuery sharedQueue], ^{
        [self.repo addEventRegistration:registration forQuery:self.querySpec];
    });
}


- (void) removeObserverWithHandle:(FIRDatabaseHandle)handle {
    FValueEventRegistration *event = [[FValueEventRegistration alloc] initWithRepo:self.repo
                                                                            handle:handle
                                                                          callback:nil
                                                                    cancelCallback:nil];
    dispatch_async([FIRDatabaseQuery sharedQueue], ^{
        [self.repo removeEventRegistration:event forQuery:self.querySpec];
    });
}


- (void) removeAllObservers {
    [self removeObserverWithHandle:NSNotFound];
}

- (void)keepSynced:(BOOL)keepSynced {
    if ([self.path.getFront isEqualToString:kDotInfoPrefix]) {
        [NSException raise:NSInvalidArgumentException format:@"Can't keep query on .info tree synced (this already is the case)."];
    }
    dispatch_async([FIRDatabaseQuery sharedQueue], ^{
        [self.repo keepQuery:self.querySpec synced:keepSynced];
    });
}

- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {

    [self observeSingleEventOfType:eventType withBlock:block withCancelBlock:nil];
}


- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {

    [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
}


- (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {

    // XXX: user reported memory leak in method

    // "When you copy a block, any references to other blocks from within that block are copied if necessary—an entire tree may be copied (from the top). If you have block variables and you reference a block from within the block, that block will be copied."
    // http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1
    // So... we don't need to do this since inside the on: we copy this block off the stack to the heap.
    // __block fbt_void_datasnapshot userCallback = [callback copy];

    [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
        if (block != nil) {
            block(snapshot);
        }
    } withCancelBlock:cancelBlock];
}

/**
* Attaches a listener, waits for the first event, and then removes the listener
*/
- (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {

    // XXX: user reported memory leak in method

    // "When you copy a block, any references to other blocks from within that block are copied if necessary—an entire tree may be copied (from the top). If you have block variables and you reference a block from within the block, that block will be copied."
    // http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1
    // So... we don't need to do this since inside the on: we copy this block off the stack to the heap.
    // __block fbt_void_datasnapshot userCallback = [callback copy];

    __block FIRDatabaseHandle handle;
    __block BOOL firstCall = YES;

    fbt_void_datasnapshot_nsstring callback = [block copy];
    fbt_void_datasnapshot_nsstring wrappedCallback = ^(FIRDataSnapshot *snap, NSString* prevName) {
        if (firstCall) {
            firstCall = NO;
            [self removeObserverWithHandle:handle];
            callback(snap, prevName);
        }
    };

    fbt_void_nserror cancelCallback = [cancelBlock copy];
    handle = [self observeEventType:eventType andPreviousSiblingKeyWithBlock:wrappedCallback withCancelBlock:^(NSError* error){

        [self removeObserverWithHandle:handle];

        if (cancelCallback) {
            cancelCallback(error);
        }
    }];
}

- (NSString *) description {
    return [NSString stringWithFormat:@"(%@ %@)", self.path, self.queryParams.description];
}

- (FIRDatabaseReference *) ref {
    return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:self.path];
}

@end