FIRDatabaseQuery.m 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. /*
  2. * Copyright 2017 Google
  3. *
  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
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  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.
  15. */
  16. #import "FIRDatabaseQuery.h"
  17. #import "FIRDatabaseQuery_Private.h"
  18. #import "FValidation.h"
  19. #import "FQueryParams.h"
  20. #import "FQuerySpec.h"
  21. #import "FValueEventRegistration.h"
  22. #import "FChildEventRegistration.h"
  23. #import "FPath.h"
  24. #import "FKeyIndex.h"
  25. #import "FPathIndex.h"
  26. #import "FPriorityIndex.h"
  27. #import "FValueIndex.h"
  28. #import "FLeafNode.h"
  29. #import "FSnapshotUtilities.h"
  30. #import "FConstants.h"
  31. @implementation FIRDatabaseQuery
  32. @synthesize repo;
  33. @synthesize path;
  34. @synthesize queryParams;
  35. #define INVALID_QUERY_PARAM_ERROR @"InvalidQueryParameter"
  36. + (dispatch_queue_t)sharedQueue
  37. {
  38. // 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)
  39. static dispatch_once_t pred;
  40. static dispatch_queue_t sharedDispatchQueue;
  41. dispatch_once(&pred, ^{
  42. sharedDispatchQueue = dispatch_queue_create("FirebaseWorker", NULL);
  43. });
  44. return sharedDispatchQueue;
  45. }
  46. - (id) initWithRepo:(FRepo *)theRepo path:(FPath *)thePath {
  47. return [self initWithRepo:theRepo path:thePath params:nil orderByCalled:NO priorityMethodCalled:NO];
  48. }
  49. - (id) initWithRepo:(FRepo *)theRepo
  50. path:(FPath *)thePath
  51. params:(FQueryParams *)theParams
  52. orderByCalled:(BOOL)orderByCalled
  53. priorityMethodCalled:(BOOL)priorityMethodCalled {
  54. self = [super init];
  55. if (self) {
  56. self.repo = theRepo;
  57. self.path = thePath;
  58. if (!theParams) {
  59. theParams = [FQueryParams defaultInstance];
  60. }
  61. if (![theParams isValid]) {
  62. @throw [[NSException alloc] initWithName:@"InvalidArgumentError" reason:@"Queries are limited to two constraints" userInfo:nil];
  63. }
  64. self.queryParams = theParams;
  65. self.orderByCalled = orderByCalled;
  66. self.priorityMethodCalled = priorityMethodCalled;
  67. }
  68. return self;
  69. }
  70. - (FQuerySpec *)querySpec {
  71. return [[FQuerySpec alloc] initWithPath:self.path params:self.queryParams];
  72. }
  73. - (void)validateQueryEndpointsForParams:(FQueryParams *)params {
  74. if ([params.index isEqual:[FKeyIndex keyIndex]]) {
  75. if ([params hasStart]) {
  76. if (params.indexStartKey != [FUtilities minName]) {
  77. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryStartingAtValue:childKey: or queryEqualTo:andChildKey: in combination with queryOrderedByKey"];
  78. }
  79. if (![params.indexStartValue.val isKindOfClass:[NSString class]]) {
  80. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryStartingAtValue: with other types than string in combination with queryOrderedByKey"];
  81. }
  82. }
  83. if ([params hasEnd]) {
  84. if (params.indexEndKey != [FUtilities maxName]) {
  85. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryEndingAtValue:childKey: or queryEqualToValue:childKey: in combination with queryOrderedByKey"];
  86. }
  87. if (![params.indexEndValue.val isKindOfClass:[NSString class]]) {
  88. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't use queryEndingAtValue: with other types than string in combination with queryOrderedByKey"];
  89. }
  90. }
  91. } else if ([params.index isEqual:[FPriorityIndex priorityIndex]]) {
  92. if (([params hasStart] && ![FValidation validatePriorityValue:params.indexStartValue.val]) ||
  93. ([params hasEnd] && ![FValidation validatePriorityValue:params.indexEndValue.val])) {
  94. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"When using queryOrderedByPriority, values provided to queryStartingAtValue:, queryEndingAtValue:, or queryEqualToValue: must be valid priorities."];
  95. }
  96. }
  97. }
  98. - (void)validateEqualToCall {
  99. if ([self.queryParams hasStart]) {
  100. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot combine queryEqualToValue: and queryStartingAtValue:"];
  101. }
  102. if ([self.queryParams hasEnd]) {
  103. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot combine queryEqualToValue: and queryEndingAtValue:"];
  104. }
  105. }
  106. - (void)validateNoPreviousOrderByCalled {
  107. if (self.orderByCalled) {
  108. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Cannot use multiple queryOrderedBy calls!"];
  109. }
  110. }
  111. - (void)validateIndexValueType:(id)type fromMethod:(NSString *)method {
  112. if (type != nil &&
  113. ![type isKindOfClass:[NSNumber class]] &&
  114. ![type isKindOfClass:[NSString class]] &&
  115. ![type isKindOfClass:[NSNull class]]) {
  116. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"You can only pass nil, NSString or NSNumber to %@", method];
  117. }
  118. }
  119. - (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue {
  120. return [self queryStartingAtInternal:startValue childKey:nil from:@"queryStartingAtValue:" priorityMethod:NO];
  121. }
  122. - (FIRDatabaseQuery *)queryStartingAtValue:(id)startValue childKey:(NSString *)childKey {
  123. if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
  124. @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
  125. reason:@"You must use queryStartingAtValue: instead of queryStartingAtValue:childKey: when using queryOrderedByKey:"
  126. userInfo:nil];
  127. }
  128. return [self queryStartingAtInternal:startValue
  129. childKey:childKey
  130. from:@"queryStartingAtValue:childKey:"
  131. priorityMethod:NO];
  132. }
  133. - (FIRDatabaseQuery *)queryStartingAtInternal:(id<FNode>)startValue
  134. childKey:(NSString *)childKey
  135. from:(NSString *)methodName
  136. priorityMethod:(BOOL)priorityMethod {
  137. [self validateIndexValueType:startValue fromMethod:methodName];
  138. if (childKey != nil) {
  139. [FValidation validateFrom:methodName validKey:childKey];
  140. }
  141. if ([self.queryParams hasStart]) {
  142. [NSException raise:INVALID_QUERY_PARAM_ERROR
  143. format:@"Can't call %@ after queryStartingAtValue or queryEqualToValue was previously called", methodName];
  144. }
  145. id<FNode> startNode = [FSnapshotUtilities nodeFrom:startValue];
  146. FQueryParams* params = [self.queryParams startAt:startNode childKey:childKey];
  147. [self validateQueryEndpointsForParams:params];
  148. return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
  149. path:self.path
  150. params:params
  151. orderByCalled:self.orderByCalled
  152. priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
  153. }
  154. - (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue {
  155. return [self queryEndingAtInternal:endValue
  156. childKey:nil
  157. from:@"queryEndingAtValue:"
  158. priorityMethod:NO];
  159. }
  160. - (FIRDatabaseQuery *)queryEndingAtValue:(id)endValue childKey:(NSString *)childKey {
  161. if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
  162. @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
  163. reason:@"You must use queryEndingAtValue: instead of queryEndingAtValue:childKey: when using queryOrderedByKey:"
  164. userInfo:nil];
  165. }
  166. return [self queryEndingAtInternal:endValue
  167. childKey:childKey
  168. from:@"queryEndingAtValue:childKey:"
  169. priorityMethod:NO];
  170. }
  171. - (FIRDatabaseQuery *)queryEndingAtInternal:(id)endValue
  172. childKey:(NSString *)childKey
  173. from:(NSString *)methodName
  174. priorityMethod:(BOOL)priorityMethod {
  175. [self validateIndexValueType:endValue fromMethod:methodName];
  176. if (childKey != nil) {
  177. [FValidation validateFrom:methodName validKey:childKey];
  178. }
  179. if ([self.queryParams hasEnd]) {
  180. [NSException raise:INVALID_QUERY_PARAM_ERROR
  181. format:@"Can't call %@ after queryEndingAtValue or queryEqualToValue was previously called", methodName];
  182. }
  183. id<FNode> endNode = [FSnapshotUtilities nodeFrom:endValue];
  184. FQueryParams* params = [self.queryParams endAt:endNode childKey:childKey];
  185. [self validateQueryEndpointsForParams:params];
  186. return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
  187. path:self.path
  188. params:params
  189. orderByCalled:self.orderByCalled
  190. priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
  191. }
  192. - (FIRDatabaseQuery *)queryEqualToValue:(id)value {
  193. return [self queryEqualToInternal:value childKey:nil from:@"queryEqualToValue:" priorityMethod:NO];
  194. }
  195. - (FIRDatabaseQuery *)queryEqualToValue:(id)value childKey:(NSString *)childKey {
  196. if ([self.queryParams.index isEqual:[FKeyIndex keyIndex]]) {
  197. @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
  198. reason:@"You must use queryEqualToValue: instead of queryEqualTo:childKey: when using queryOrderedByKey:"
  199. userInfo:nil];
  200. }
  201. return [self queryEqualToInternal:value childKey:childKey from:@"queryEqualToValue:childKey:" priorityMethod:NO];
  202. }
  203. - (FIRDatabaseQuery *)queryEqualToInternal:(id)value
  204. childKey:(NSString *)childKey
  205. from:(NSString *)methodName
  206. priorityMethod:(BOOL)priorityMethod {
  207. [self validateIndexValueType:value fromMethod:methodName];
  208. if (childKey != nil) {
  209. [FValidation validateFrom:methodName validKey:childKey];
  210. }
  211. if ([self.queryParams hasEnd] || [self.queryParams hasStart]) {
  212. [NSException raise:INVALID_QUERY_PARAM_ERROR
  213. format:@"Can't call %@ after queryStartingAtValue, queryEndingAtValue or queryEqualToValue was previously called", methodName];
  214. }
  215. id<FNode> node = [FSnapshotUtilities nodeFrom:value];
  216. FQueryParams* params = [[self.queryParams startAt:node childKey:childKey] endAt:node childKey:childKey];
  217. [self validateQueryEndpointsForParams:params];
  218. return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
  219. path:self.path
  220. params:params
  221. orderByCalled:self.orderByCalled
  222. priorityMethodCalled:priorityMethod || self.priorityMethodCalled];
  223. }
  224. - (void)validateLimitRange:(NSUInteger)limit
  225. {
  226. // No need to check for negative ranges, since limit is unsigned
  227. if (limit == 0) {
  228. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Limit can't be zero"];
  229. }
  230. if (limit >= 1l<<31) {
  231. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Limit must be less than 2,147,483,648"];
  232. }
  233. }
  234. - (FIRDatabaseQuery *)queryLimitedToFirst:(NSUInteger)limit {
  235. if (self.queryParams.limitSet) {
  236. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't call queryLimitedToFirst: if a limit was previously set"];
  237. }
  238. [self validateLimitRange:limit];
  239. FQueryParams* params = [self.queryParams limitToFirst:limit];
  240. return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
  241. path:self.path
  242. params:params
  243. orderByCalled:self.orderByCalled
  244. priorityMethodCalled:self.priorityMethodCalled];
  245. }
  246. - (FIRDatabaseQuery *)queryLimitedToLast:(NSUInteger)limit {
  247. if (self.queryParams.limitSet) {
  248. [NSException raise:INVALID_QUERY_PARAM_ERROR format:@"Can't call queryLimitedToLast: if a limit was previously set"];
  249. }
  250. [self validateLimitRange:limit];
  251. FQueryParams* params = [self.queryParams limitToLast:limit];
  252. return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
  253. path:self.path
  254. params:params
  255. orderByCalled:self.orderByCalled
  256. priorityMethodCalled:self.priorityMethodCalled];
  257. }
  258. - (FIRDatabaseQuery *)queryOrderedByChild:(NSString *)indexPathString {
  259. if ([indexPathString isEqualToString:@"$key"] || [indexPathString isEqualToString:@".key"]) {
  260. @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
  261. reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByKey: instead.", indexPathString]
  262. userInfo:nil];
  263. } else if ([indexPathString isEqualToString:@"$priority"] || [indexPathString isEqualToString:@".priority"]) {
  264. @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
  265. reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByPriority: instead.", indexPathString]
  266. userInfo:nil];
  267. } else if ([indexPathString isEqualToString:@"$value"] || [indexPathString isEqualToString:@".value"]) {
  268. @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
  269. reason:[NSString stringWithFormat:@"(queryOrderedByChild:) %@ is invalid. Use queryOrderedByValue: instead.", indexPathString]
  270. userInfo:nil];
  271. }
  272. [self validateNoPreviousOrderByCalled];
  273. [FValidation validateFrom:@"queryOrderedByChild:" validPathString:indexPathString];
  274. FPath *indexPath = [FPath pathWithString:indexPathString];
  275. if (indexPath.isEmpty) {
  276. @throw [[NSException alloc] initWithName:INVALID_QUERY_PARAM_ERROR
  277. reason:[NSString stringWithFormat:@"(queryOrderedByChild:) with an empty path is invalid. Use queryOrderedByValue: instead."]
  278. userInfo:nil];
  279. }
  280. id<FIndex> index = [[FPathIndex alloc] initWithPath:indexPath];
  281. FQueryParams *params = [self.queryParams orderBy:index];
  282. [self validateQueryEndpointsForParams:params];
  283. return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
  284. path:self.path
  285. params:params
  286. orderByCalled:YES
  287. priorityMethodCalled:self.priorityMethodCalled];
  288. }
  289. - (FIRDatabaseQuery *) queryOrderedByKey {
  290. [self validateNoPreviousOrderByCalled];
  291. FQueryParams *params = [self.queryParams orderBy:[FKeyIndex keyIndex]];
  292. [self validateQueryEndpointsForParams:params];
  293. return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
  294. path:self.path
  295. params:params
  296. orderByCalled:YES
  297. priorityMethodCalled:self.priorityMethodCalled];
  298. }
  299. - (FIRDatabaseQuery *) queryOrderedByValue {
  300. [self validateNoPreviousOrderByCalled];
  301. FQueryParams *params = [self.queryParams orderBy:[FValueIndex valueIndex]];
  302. return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
  303. path:self.path
  304. params:params
  305. orderByCalled:YES
  306. priorityMethodCalled:self.priorityMethodCalled];
  307. }
  308. - (FIRDatabaseQuery *) queryOrderedByPriority {
  309. [self validateNoPreviousOrderByCalled];
  310. FQueryParams *params = [self.queryParams orderBy:[FPriorityIndex priorityIndex]];
  311. return [[FIRDatabaseQuery alloc] initWithRepo:self.repo
  312. path:self.path
  313. params:params
  314. orderByCalled:YES
  315. priorityMethodCalled:self.priorityMethodCalled];
  316. }
  317. - (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(void (^)(FIRDataSnapshot *))block {
  318. [FValidation validateFrom:@"observeEventType:withBlock:" knownEventType:eventType];
  319. return [self observeEventType:eventType withBlock:block withCancelBlock:nil];
  320. }
  321. - (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
  322. [FValidation validateFrom:@"observeEventType:andPreviousSiblingKeyWithBlock:" knownEventType:eventType];
  323. return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
  324. }
  325. - (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
  326. [FValidation validateFrom:@"observeEventType:withBlock:withCancelBlock:" knownEventType:eventType];
  327. if (eventType == FIRDataEventTypeValue) {
  328. // Handle FIRDataEventTypeValue specially because they shouldn't have prevName callbacks
  329. NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
  330. [self observeValueEventWithHandle:handle withBlock:block cancelCallback:cancelBlock];
  331. return handle;
  332. } else {
  333. // Wrap up the userCallback so we can treat everything as a callback that has a prevName
  334. fbt_void_datasnapshot userCallback = [block copy];
  335. return [self observeEventType:eventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
  336. if (userCallback != nil) {
  337. userCallback(snapshot);
  338. }
  339. } withCancelBlock:cancelBlock];
  340. }
  341. }
  342. - (FIRDatabaseHandle)observeEventType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
  343. [FValidation validateFrom:@"observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:" knownEventType:eventType];
  344. if (eventType == FIRDataEventTypeValue) {
  345. // TODO: This gets hit by observeSingleEventOfType. Need to fix.
  346. /*
  347. @throw [[NSException alloc] initWithName:@"InvalidEventTypeForObserver"
  348. reason:@"(observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock:) Cannot use observeEventType:andPreviousSiblingKeyWithBlock:withCancelBlock: with FIRDataEventTypeValue. Use observeEventType:withBlock:withCancelBlock: instead."
  349. userInfo:nil];
  350. */
  351. }
  352. NSUInteger handle = [[FUtilities LUIDGenerator] integerValue];
  353. NSDictionary *callbacks = @{[NSNumber numberWithInteger:eventType]: [block copy]};
  354. [self observeChildEventWithHandle:handle withCallbacks:callbacks cancelCallback:cancelBlock];
  355. return handle;
  356. }
  357. // If we want to distinguish between value event listeners and child event listeners, like in the Java client, we can
  358. // consider exporting this. If we do, add argument validation. Otherwise, arguments are validated in the public-facing
  359. // portions of the API. Also, move the FIRDatabaseHandle logic.
  360. - (void)observeValueEventWithHandle:(FIRDatabaseHandle)handle withBlock:(fbt_void_datasnapshot)block cancelCallback:(fbt_void_nserror)cancelBlock {
  361. // Note that we don't need to copy the callbacks here, FEventRegistration callback properties set to copy
  362. FValueEventRegistration *registration = [[FValueEventRegistration alloc] initWithRepo:self.repo
  363. handle:handle
  364. callback:block
  365. cancelCallback:cancelBlock];
  366. dispatch_async([FIRDatabaseQuery sharedQueue], ^{
  367. [self.repo addEventRegistration:registration forQuery:self.querySpec];
  368. });
  369. }
  370. // Note: as with the above method, we may wish to expose this at some point.
  371. - (void)observeChildEventWithHandle:(FIRDatabaseHandle)handle withCallbacks:(NSDictionary *)callbacks cancelCallback:(fbt_void_nserror)cancelBlock {
  372. // Note that we don't need to copy the callbacks here, FEventRegistration callback properties set to copy
  373. FChildEventRegistration *registration = [[FChildEventRegistration alloc] initWithRepo:self.repo
  374. handle:handle
  375. callbacks:callbacks
  376. cancelCallback:cancelBlock];
  377. dispatch_async([FIRDatabaseQuery sharedQueue], ^{
  378. [self.repo addEventRegistration:registration forQuery:self.querySpec];
  379. });
  380. }
  381. - (void) removeObserverWithHandle:(FIRDatabaseHandle)handle {
  382. FValueEventRegistration *event = [[FValueEventRegistration alloc] initWithRepo:self.repo
  383. handle:handle
  384. callback:nil
  385. cancelCallback:nil];
  386. dispatch_async([FIRDatabaseQuery sharedQueue], ^{
  387. [self.repo removeEventRegistration:event forQuery:self.querySpec];
  388. });
  389. }
  390. - (void) removeAllObservers {
  391. [self removeObserverWithHandle:NSNotFound];
  392. }
  393. - (void)keepSynced:(BOOL)keepSynced {
  394. if ([self.path.getFront isEqualToString:kDotInfoPrefix]) {
  395. [NSException raise:NSInvalidArgumentException format:@"Can't keep query on .info tree synced (this already is the case)."];
  396. }
  397. dispatch_async([FIRDatabaseQuery sharedQueue], ^{
  398. [self.repo keepQuery:self.querySpec synced:keepSynced];
  399. });
  400. }
  401. - (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block {
  402. [self observeSingleEventOfType:eventType withBlock:block withCancelBlock:nil];
  403. }
  404. - (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block {
  405. [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:block withCancelBlock:nil];
  406. }
  407. - (void)observeSingleEventOfType:(FIRDataEventType)eventType withBlock:(fbt_void_datasnapshot)block withCancelBlock:(fbt_void_nserror)cancelBlock {
  408. // XXX: user reported memory leak in method
  409. // "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."
  410. // http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1
  411. // So... we don't need to do this since inside the on: we copy this block off the stack to the heap.
  412. // __block fbt_void_datasnapshot userCallback = [callback copy];
  413. [self observeSingleEventOfType:eventType andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
  414. if (block != nil) {
  415. block(snapshot);
  416. }
  417. } withCancelBlock:cancelBlock];
  418. }
  419. /**
  420. * Attaches a listener, waits for the first event, and then removes the listener
  421. */
  422. - (void)observeSingleEventOfType:(FIRDataEventType)eventType andPreviousSiblingKeyWithBlock:(fbt_void_datasnapshot_nsstring)block withCancelBlock:(fbt_void_nserror)cancelBlock {
  423. // XXX: user reported memory leak in method
  424. // "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."
  425. // http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1
  426. // So... we don't need to do this since inside the on: we copy this block off the stack to the heap.
  427. // __block fbt_void_datasnapshot userCallback = [callback copy];
  428. __block FIRDatabaseHandle handle;
  429. __block BOOL firstCall = YES;
  430. fbt_void_datasnapshot_nsstring callback = [block copy];
  431. fbt_void_datasnapshot_nsstring wrappedCallback = ^(FIRDataSnapshot *snap, NSString* prevName) {
  432. if (firstCall) {
  433. firstCall = NO;
  434. [self removeObserverWithHandle:handle];
  435. callback(snap, prevName);
  436. }
  437. };
  438. fbt_void_nserror cancelCallback = [cancelBlock copy];
  439. handle = [self observeEventType:eventType andPreviousSiblingKeyWithBlock:wrappedCallback withCancelBlock:^(NSError* error){
  440. [self removeObserverWithHandle:handle];
  441. if (cancelCallback) {
  442. cancelCallback(error);
  443. }
  444. }];
  445. }
  446. - (NSString *) description {
  447. return [NSString stringWithFormat:@"(%@ %@)", self.path, self.queryParams.description];
  448. }
  449. - (FIRDatabaseReference *) ref {
  450. return [[FIRDatabaseReference alloc] initWithRepo:self.repo path:self.path];
  451. }
  452. @end