FSTLevelDBMutationQueue.mm 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  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 "Firestore/Source/Local/FSTLevelDBMutationQueue.h"
  17. #include <memory>
  18. #include <set>
  19. #include <string>
  20. #include <utility>
  21. #include <vector>
  22. #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h"
  23. #import "Firestore/Source/Core/FSTQuery.h"
  24. #import "Firestore/Source/Local/FSTLevelDB.h"
  25. #import "Firestore/Source/Local/FSTLocalSerializer.h"
  26. #import "Firestore/Source/Model/FSTMutation.h"
  27. #import "Firestore/Source/Model/FSTMutationBatch.h"
  28. #include "Firestore/core/src/firebase/firestore/auth/user.h"
  29. #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h"
  30. #include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h"
  31. #include "Firestore/core/src/firebase/firestore/local/leveldb_util.h"
  32. #include "Firestore/core/src/firebase/firestore/model/mutation_batch.h"
  33. #include "Firestore/core/src/firebase/firestore/model/resource_path.h"
  34. #include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
  35. #include "Firestore/core/src/firebase/firestore/util/string_apple.h"
  36. #include "Firestore/core/src/firebase/firestore/util/string_util.h"
  37. #include "absl/strings/match.h"
  38. #include "leveldb/db.h"
  39. #include "leveldb/write_batch.h"
  40. NS_ASSUME_NONNULL_BEGIN
  41. namespace util = firebase::firestore::util;
  42. using firebase::firestore::auth::User;
  43. using firebase::firestore::local::DescribeKey;
  44. using firebase::firestore::local::LevelDbDocumentMutationKey;
  45. using firebase::firestore::local::LevelDbMutationKey;
  46. using firebase::firestore::local::LevelDbMutationQueueKey;
  47. using firebase::firestore::local::LevelDbTransaction;
  48. using firebase::firestore::local::MakeStringView;
  49. using firebase::firestore::model::BatchId;
  50. using firebase::firestore::model::kBatchIdUnknown;
  51. using firebase::firestore::model::DocumentKey;
  52. using firebase::firestore::model::DocumentKeySet;
  53. using firebase::firestore::model::ResourcePath;
  54. using leveldb::DB;
  55. using leveldb::Iterator;
  56. using leveldb::ReadOptions;
  57. using leveldb::Slice;
  58. using leveldb::Status;
  59. using leveldb::WriteBatch;
  60. using leveldb::WriteOptions;
  61. @interface FSTLevelDBMutationQueue ()
  62. - (instancetype)initWithUserID:(std::string)userID
  63. db:(FSTLevelDB *)db
  64. serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER;
  65. /**
  66. * Next value to use when assigning sequential IDs to each mutation batch.
  67. *
  68. * NOTE: There can only be one FSTLevelDBMutationQueue for a given db at a time, hence it is safe
  69. * to track nextBatchID as an instance-level property. Should we ever relax this constraint we'll
  70. * need to revisit this.
  71. */
  72. @property(nonatomic, assign) BatchId nextBatchID;
  73. /** A write-through cache copy of the metadata describing the current queue. */
  74. @property(nonatomic, strong, nullable) FSTPBMutationQueue *metadata;
  75. @property(nonatomic, strong, readonly) FSTLocalSerializer *serializer;
  76. @end
  77. @implementation FSTLevelDBMutationQueue {
  78. // This instance is owned by FSTLevelDB; avoid a retain cycle.
  79. __weak FSTLevelDB *_db;
  80. /** The normalized userID (e.g. nil UID => @"" userID) used in our LevelDB keys. */
  81. std::string _userID;
  82. }
  83. + (instancetype)mutationQueueWithUser:(const User &)user
  84. db:(FSTLevelDB *)db
  85. serializer:(FSTLocalSerializer *)serializer {
  86. std::string userID = user.is_authenticated() ? user.uid() : "";
  87. return [[FSTLevelDBMutationQueue alloc] initWithUserID:std::move(userID)
  88. db:db
  89. serializer:serializer];
  90. }
  91. - (instancetype)initWithUserID:(std::string)userID
  92. db:(FSTLevelDB *)db
  93. serializer:(FSTLocalSerializer *)serializer {
  94. if (self = [super init]) {
  95. _userID = std::move(userID);
  96. _db = db;
  97. _serializer = serializer;
  98. }
  99. return self;
  100. }
  101. - (void)start {
  102. self.nextBatchID = [FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr];
  103. std::string key = [self keyForCurrentMutationQueue];
  104. FSTPBMutationQueue *metadata = [self metadataForKey:key];
  105. if (!metadata) {
  106. metadata = [FSTPBMutationQueue message];
  107. }
  108. self.metadata = metadata;
  109. }
  110. + (BatchId)loadNextBatchIDFromDB:(DB *)db {
  111. // TODO(gsoltis): implement Prev() and SeekToLast() on LevelDbTransaction::Iterator, then port
  112. // this to a transaction.
  113. std::unique_ptr<Iterator> it(db->NewIterator(LevelDbTransaction::DefaultReadOptions()));
  114. auto tableKey = LevelDbMutationKey::KeyPrefix();
  115. LevelDbMutationKey rowKey;
  116. BatchId maxBatchID = kBatchIdUnknown;
  117. BOOL moreUserIDs = NO;
  118. std::string nextUserID;
  119. it->Seek(tableKey);
  120. if (it->Valid() && rowKey.Decode(MakeStringView(it->key()))) {
  121. moreUserIDs = YES;
  122. nextUserID = rowKey.user_id();
  123. }
  124. // This loop assumes that nextUserId contains the next username at the start of the iteration.
  125. while (moreUserIDs) {
  126. // Compute the first key after the last mutation for nextUserID.
  127. auto userEnd = LevelDbMutationKey::KeyPrefix(nextUserID);
  128. userEnd = util::PrefixSuccessor(userEnd);
  129. // Seek to that key with the intent of finding the boundary between nextUserID's mutations
  130. // and the one after that (if any).
  131. it->Seek(userEnd);
  132. // At this point there are three possible cases to handle differently. Each case must prepare
  133. // the next iteration (by assigning to nextUserID or setting moreUserIDs = NO) and seek the
  134. // iterator to the last row in the current user's mutation sequence.
  135. if (!it->Valid()) {
  136. // The iterator is past the last row altogether (there are no additional userIDs and now
  137. // rows in any table after mutations). The last row will have the highest batchID.
  138. moreUserIDs = NO;
  139. it->SeekToLast();
  140. } else if (rowKey.Decode(MakeStringView(it->key()))) {
  141. // The iterator is valid and the key decoded successfully so the next user was just decoded.
  142. nextUserID = rowKey.user_id();
  143. it->Prev();
  144. } else {
  145. // The iterator is past the end of the mutations table but there are other rows.
  146. moreUserIDs = NO;
  147. it->Prev();
  148. }
  149. // In all the cases above there was at least one row for the current user and each case has
  150. // set things up such that iterator points to it.
  151. if (!rowKey.Decode(MakeStringView(it->key()))) {
  152. HARD_FAIL("There should have been a key previous to %s", userEnd);
  153. }
  154. if (rowKey.batch_id() > maxBatchID) {
  155. maxBatchID = rowKey.batch_id();
  156. }
  157. }
  158. return maxBatchID + 1;
  159. }
  160. - (BOOL)isEmpty {
  161. std::string userKey = LevelDbMutationKey::KeyPrefix(_userID);
  162. auto it = _db.currentTransaction->NewIterator();
  163. it->Seek(userKey);
  164. BOOL empty = YES;
  165. if (it->Valid() && absl::StartsWith(it->key(), userKey)) {
  166. empty = NO;
  167. }
  168. return empty;
  169. }
  170. - (void)acknowledgeBatch:(FSTMutationBatch *)batch streamToken:(nullable NSData *)streamToken {
  171. FSTPBMutationQueue *metadata = self.metadata;
  172. metadata.lastStreamToken = streamToken;
  173. _db.currentTransaction->Put([self keyForCurrentMutationQueue], metadata);
  174. }
  175. - (nullable NSData *)lastStreamToken {
  176. return self.metadata.lastStreamToken;
  177. }
  178. - (void)setLastStreamToken:(nullable NSData *)streamToken {
  179. FSTPBMutationQueue *metadata = self.metadata;
  180. metadata.lastStreamToken = streamToken;
  181. _db.currentTransaction->Put([self keyForCurrentMutationQueue], metadata);
  182. }
  183. - (std::string)keyForCurrentMutationQueue {
  184. return LevelDbMutationQueueKey::Key(_userID);
  185. }
  186. - (nullable FSTPBMutationQueue *)metadataForKey:(const std::string &)key {
  187. std::string value;
  188. Status status = _db.currentTransaction->Get(key, &value);
  189. if (status.ok()) {
  190. return [self parsedMetadata:value];
  191. } else if (status.IsNotFound()) {
  192. return nil;
  193. } else {
  194. HARD_FAIL("metadataForKey: failed loading key %s with status: %s", key, status.ToString());
  195. }
  196. }
  197. - (FSTMutationBatch *)addMutationBatchWithWriteTime:(FIRTimestamp *)localWriteTime
  198. mutations:(NSArray<FSTMutation *> *)mutations {
  199. BatchId batchID = self.nextBatchID;
  200. self.nextBatchID += 1;
  201. FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID
  202. localWriteTime:localWriteTime
  203. mutations:mutations];
  204. std::string key = [self mutationKeyForBatch:batch];
  205. _db.currentTransaction->Put(key, [self.serializer encodedMutationBatch:batch]);
  206. // Store an empty value in the index which is equivalent to serializing a GPBEmpty message. In the
  207. // future if we wanted to store some other kind of value here, we can parse these empty values as
  208. // with some other protocol buffer (and the parser will see all default values).
  209. std::string emptyBuffer;
  210. for (FSTMutation *mutation in mutations) {
  211. key = LevelDbDocumentMutationKey::Key(_userID, mutation.key, batchID);
  212. _db.currentTransaction->Put(key, emptyBuffer);
  213. }
  214. return batch;
  215. }
  216. - (nullable FSTMutationBatch *)lookupMutationBatch:(BatchId)batchID {
  217. std::string key = [self mutationKeyForBatchID:batchID];
  218. std::string value;
  219. Status status = _db.currentTransaction->Get(key, &value);
  220. if (!status.ok()) {
  221. if (status.IsNotFound()) {
  222. return nil;
  223. }
  224. HARD_FAIL("Lookup mutation batch (%s, %s) failed with status: %s", _userID, batchID,
  225. status.ToString());
  226. }
  227. return [self decodedMutationBatch:value];
  228. }
  229. - (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(BatchId)batchID {
  230. BatchId nextBatchID = batchID + 1;
  231. std::string key = [self mutationKeyForBatchID:nextBatchID];
  232. auto it = _db.currentTransaction->NewIterator();
  233. it->Seek(key);
  234. LevelDbMutationKey rowKey;
  235. if (!it->Valid() || !rowKey.Decode(it->key())) {
  236. // Past the last row in the DB or out of the mutations table
  237. return nil;
  238. }
  239. if (rowKey.user_id() != _userID) {
  240. // Jumped past the last mutation for this user
  241. return nil;
  242. }
  243. HARD_ASSERT(rowKey.batch_id() >= nextBatchID, "Should have found mutation after %s", nextBatchID);
  244. return [self decodedMutationBatch:it->value()];
  245. }
  246. - (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingDocumentKey:
  247. (const DocumentKey &)documentKey {
  248. // Scan the document-mutation index starting with a prefix starting with the given documentKey.
  249. std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID, documentKey.path());
  250. auto indexIterator = _db.currentTransaction->NewIterator();
  251. indexIterator->Seek(indexPrefix);
  252. // Simultaneously scan the mutation queue. This works because each (key, batchID) pair is unique
  253. // and ordered, so when scanning a table prefixed by exactly key, all the batchIDs encountered
  254. // will be unique and in order.
  255. std::string mutationsPrefix = LevelDbMutationKey::KeyPrefix(_userID);
  256. auto mutationIterator = _db.currentTransaction->NewIterator();
  257. NSMutableArray *result = [NSMutableArray array];
  258. LevelDbDocumentMutationKey rowKey;
  259. for (; indexIterator->Valid(); indexIterator->Next()) {
  260. // Only consider rows matching exactly the specific key of interest. Index rows have this
  261. // form (with markers in brackets):
  262. //
  263. // <User>user <Path>collection <Path>doc <BatchId>2 <Terminator>
  264. // <User>user <Path>collection <Path>doc <BatchId>3 <Terminator>
  265. // <User>user <Path>collection <Path>doc <Path>sub <Path>doc <BatchId>3 <Terminator>
  266. //
  267. // Note that Path markers sort after BatchId markers so this means that when searching for
  268. // collection/doc, all the entries for it will be contiguous in the table, allowing a break
  269. // after any mismatch.
  270. if (!absl::StartsWith(indexIterator->key(), indexPrefix) ||
  271. !rowKey.Decode(indexIterator->key()) || rowKey.document_key() != documentKey) {
  272. break;
  273. }
  274. // Each row is a unique combination of key and batchID, so this foreign key reference can
  275. // only occur once.
  276. std::string mutationKey = LevelDbMutationKey::Key(_userID, rowKey.batch_id());
  277. mutationIterator->Seek(mutationKey);
  278. if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) {
  279. HARD_FAIL("Dangling document-mutation reference found: "
  280. "%s points to %s; seeking there found %s",
  281. DescribeKey(indexIterator), DescribeKey(mutationKey),
  282. DescribeKey(mutationIterator));
  283. }
  284. [result addObject:[self decodedMutationBatch:mutationIterator->value()]];
  285. }
  286. return result;
  287. }
  288. - (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingDocumentKeys:
  289. (const DocumentKeySet &)documentKeys {
  290. // Take a pass through the document keys and collect the set of unique mutation batchIDs that
  291. // affect them all. Some batches can affect more than one key.
  292. std::set<BatchId> batchIDs;
  293. auto indexIterator = _db.currentTransaction->NewIterator();
  294. LevelDbDocumentMutationKey rowKey;
  295. for (const DocumentKey &documentKey : documentKeys) {
  296. std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID, documentKey.path());
  297. for (indexIterator->Seek(indexPrefix); indexIterator->Valid(); indexIterator->Next()) {
  298. // Only consider rows matching exactly the specific key of interest. Index rows have this
  299. // form (with markers in brackets):
  300. //
  301. // <User>user <Path>collection <Path>doc <BatchId>2 <Terminator>
  302. // <User>user <Path>collection <Path>doc <BatchId>3 <Terminator>
  303. // <User>user <Path>collection <Path>doc <Path>sub <Path>doc <BatchId>3 <Terminator>
  304. //
  305. // Note that Path markers sort after BatchId markers so this means that when searching for
  306. // collection/doc, all the entries for it will be contiguous in the table, allowing a break
  307. // after any mismatch.
  308. if (!absl::StartsWith(indexIterator->key(), indexPrefix) ||
  309. !rowKey.Decode(indexIterator->key()) || rowKey.document_key() != documentKey) {
  310. break;
  311. }
  312. batchIDs.insert(rowKey.batch_id());
  313. }
  314. }
  315. return [self allMutationBatchesWithBatchIDs:batchIDs];
  316. }
  317. - (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingQuery:(FSTQuery *)query {
  318. HARD_ASSERT(![query isDocumentQuery], "Document queries shouldn't go down this path");
  319. const ResourcePath &queryPath = query.path;
  320. size_t immediateChildrenPathLength = queryPath.size() + 1;
  321. // TODO(mcg): Actually implement a single-collection query
  322. //
  323. // This is actually executing an ancestor query, traversing the whole subtree below the
  324. // collection which can be horrifically inefficient for some structures. The right way to
  325. // solve this is to implement the full value index, but that's not in the cards in the near
  326. // future so this is the best we can do for the moment.
  327. //
  328. // Since we don't yet index the actual properties in the mutations, our current approach is to
  329. // just return all mutation batches that affect documents in the collection being queried.
  330. //
  331. // Unlike allMutationBatchesAffectingDocumentKey, this iteration will scan the document-mutation
  332. // index for more than a single document so the associated batchIDs will be neither necessarily
  333. // unique nor in order. This means an efficient simultaneous scan isn't possible.
  334. std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID, queryPath);
  335. auto indexIterator = _db.currentTransaction->NewIterator();
  336. indexIterator->Seek(indexPrefix);
  337. LevelDbDocumentMutationKey rowKey;
  338. // Collect up unique batchIDs encountered during a scan of the index. Use a set<BatchId> to
  339. // accumulate batch IDs so they can be traversed in order in a scan of the main table.
  340. //
  341. // This method is faster than performing lookups of the keys with _db->Get and keeping a hash of
  342. // batchIDs that have already been looked up. The performance difference is minor for small
  343. // numbers of keys but > 30% faster for larger numbers of keys.
  344. std::set<BatchId> uniqueBatchIDs;
  345. for (; indexIterator->Valid(); indexIterator->Next()) {
  346. if (!absl::StartsWith(indexIterator->key(), indexPrefix) ||
  347. !rowKey.Decode(indexIterator->key())) {
  348. break;
  349. }
  350. // Rows with document keys more than one segment longer than the query path can't be matches.
  351. // For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx.
  352. // TODO(mcg): we'll need a different scanner when we implement ancestor queries.
  353. if (rowKey.document_key().path().size() != immediateChildrenPathLength) {
  354. continue;
  355. }
  356. uniqueBatchIDs.insert(rowKey.batch_id());
  357. }
  358. return [self allMutationBatchesWithBatchIDs:uniqueBatchIDs];
  359. }
  360. /**
  361. * Constructs an array of matching batches, sorted by batchID to ensure that multiple mutations
  362. * affecting the same document key are applied in order.
  363. */
  364. - (NSArray<FSTMutationBatch *> *)allMutationBatchesWithBatchIDs:
  365. (const std::set<BatchId> &)batchIDs {
  366. NSMutableArray *result = [NSMutableArray array];
  367. // Given an ordered set of unique batchIDs perform a skipping scan over the main table to find
  368. // the mutation batches.
  369. auto mutationIterator = _db.currentTransaction->NewIterator();
  370. for (BatchId batchID : batchIDs) {
  371. std::string mutationKey = LevelDbMutationKey::Key(_userID, batchID);
  372. mutationIterator->Seek(mutationKey);
  373. if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) {
  374. HARD_FAIL("Dangling document-mutation reference found: "
  375. "Missing batch %s; seeking there found %s",
  376. DescribeKey(mutationKey), DescribeKey(mutationIterator));
  377. }
  378. [result addObject:[self decodedMutationBatch:mutationIterator->value()]];
  379. }
  380. return result;
  381. }
  382. - (NSArray<FSTMutationBatch *> *)allMutationBatches {
  383. std::string userKey = LevelDbMutationKey::KeyPrefix(_userID);
  384. auto it = _db.currentTransaction->NewIterator();
  385. it->Seek(userKey);
  386. NSMutableArray *result = [NSMutableArray array];
  387. for (; it->Valid() && absl::StartsWith(it->key(), userKey); it->Next()) {
  388. [result addObject:[self decodedMutationBatch:it->value()]];
  389. }
  390. return result;
  391. }
  392. - (void)removeMutationBatch:(FSTMutationBatch *)batch {
  393. auto checkIterator = _db.currentTransaction->NewIterator();
  394. BatchId batchID = batch.batchID;
  395. std::string key = LevelDbMutationKey::Key(_userID, batchID);
  396. // As a sanity check, verify that the mutation batch exists before deleting it.
  397. checkIterator->Seek(key);
  398. HARD_ASSERT(checkIterator->Valid(), "Mutation batch %s did not exist", DescribeKey(key));
  399. HARD_ASSERT(key == checkIterator->key(), "Mutation batch %s not found; found %s",
  400. DescribeKey(key), DescribeKey(checkIterator));
  401. _db.currentTransaction->Delete(key);
  402. for (FSTMutation *mutation in batch.mutations) {
  403. key = LevelDbDocumentMutationKey::Key(_userID, mutation.key, batchID);
  404. _db.currentTransaction->Delete(key);
  405. [_db.referenceDelegate removeMutationReference:mutation.key];
  406. }
  407. }
  408. - (void)performConsistencyCheck {
  409. if (![self isEmpty]) {
  410. return;
  411. }
  412. // Verify that there are no entries in the document-mutation index if the queue is empty.
  413. std::string indexPrefix = LevelDbDocumentMutationKey::KeyPrefix(_userID);
  414. auto indexIterator = _db.currentTransaction->NewIterator();
  415. indexIterator->Seek(indexPrefix);
  416. std::vector<std::string> danglingMutationReferences;
  417. for (; indexIterator->Valid(); indexIterator->Next()) {
  418. // Only consider rows matching this index prefix for the current user.
  419. if (!absl::StartsWith(indexIterator->key(), indexPrefix)) {
  420. break;
  421. }
  422. danglingMutationReferences.push_back(DescribeKey(indexIterator));
  423. }
  424. HARD_ASSERT(danglingMutationReferences.empty(),
  425. "Document leak -- detected dangling mutation references when queue "
  426. "is empty. Dangling keys: %s",
  427. util::ToString(danglingMutationReferences));
  428. }
  429. - (std::string)mutationKeyForBatch:(FSTMutationBatch *)batch {
  430. return LevelDbMutationKey::Key(_userID, batch.batchID);
  431. }
  432. - (std::string)mutationKeyForBatchID:(BatchId)batchID {
  433. return LevelDbMutationKey::Key(_userID, batchID);
  434. }
  435. /** Parses the MutationQueue metadata from the given LevelDB row contents. */
  436. - (FSTPBMutationQueue *)parsedMetadata:(Slice)slice {
  437. NSData *data = [[NSData alloc] initWithBytesNoCopy:(void *)slice.data()
  438. length:slice.size()
  439. freeWhenDone:NO];
  440. NSError *error;
  441. FSTPBMutationQueue *proto = [FSTPBMutationQueue parseFromData:data error:&error];
  442. if (!proto) {
  443. HARD_FAIL("FSTPBMutationQueue failed to parse: %s", error);
  444. }
  445. return proto;
  446. }
  447. - (FSTMutationBatch *)decodedMutationBatch:(absl::string_view)encoded {
  448. NSData *data = [[NSData alloc] initWithBytesNoCopy:(void *)encoded.data()
  449. length:encoded.size()
  450. freeWhenDone:NO];
  451. NSError *error;
  452. FSTPBWriteBatch *proto = [FSTPBWriteBatch parseFromData:data error:&error];
  453. if (!proto) {
  454. HARD_FAIL("FSTPBMutationBatch failed to parse: %s", error);
  455. }
  456. return [self.serializer decodedMutationBatch:proto];
  457. }
  458. @end
  459. NS_ASSUME_NONNULL_END