@firebase/firestore 3.4.8 → 3.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/firestore/lite/index.d.ts +1 -0
  3. package/dist/firestore/src/api/transaction.d.ts +2 -1
  4. package/dist/firestore/src/api/transaction_options.d.ts +17 -0
  5. package/dist/firestore/src/api.d.ts +1 -0
  6. package/dist/firestore/src/core/firestore_client.d.ts +2 -1
  7. package/dist/firestore/src/core/query.d.ts +2 -4
  8. package/dist/firestore/src/core/target.d.ts +6 -4
  9. package/dist/firestore/src/core/transaction_options.d.ts +25 -0
  10. package/dist/firestore/src/core/transaction_runner.d.ts +3 -2
  11. package/dist/firestore/src/lite-api/transaction.d.ts +2 -1
  12. package/dist/firestore/src/lite-api/transaction_options.d.ts +23 -0
  13. package/dist/firestore/src/local/index_manager.d.ts +24 -3
  14. package/dist/firestore/src/local/indexeddb_index_manager.d.ts +4 -7
  15. package/dist/firestore/src/local/indexeddb_schema.d.ts +1 -0
  16. package/dist/firestore/src/local/local_documents_view.d.ts +1 -1
  17. package/dist/firestore/src/local/memory_index_manager.d.ts +3 -2
  18. package/dist/firestore/src/local/query_engine.d.ts +44 -5
  19. package/dist/firestore/src/model/collections.d.ts +1 -1
  20. package/dist/firestore/src/model/values.d.ts +14 -2
  21. package/dist/firestore/src/util/misc.d.ts +3 -0
  22. package/dist/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
  23. package/dist/firestore/test/unit/local/test_index_manager.d.ts +2 -2
  24. package/dist/index.d.ts +24 -1
  25. package/dist/index.esm2017.js +4108 -3992
  26. package/dist/index.esm2017.js.map +1 -1
  27. package/dist/index.esm5.js +4198 -4087
  28. package/dist/index.esm5.js.map +1 -1
  29. package/dist/index.node.cjs.js +1711 -1565
  30. package/dist/index.node.cjs.js.map +1 -1
  31. package/dist/index.node.mjs +1711 -1565
  32. package/dist/index.node.mjs.map +1 -1
  33. package/dist/index.rn.js +4100 -3984
  34. package/dist/index.rn.js.map +1 -1
  35. package/dist/internal.d.ts +50 -4
  36. package/dist/lite/firestore/lite/index.d.ts +1 -0
  37. package/dist/lite/firestore/src/api/transaction.d.ts +2 -1
  38. package/dist/lite/firestore/src/api/transaction_options.d.ts +17 -0
  39. package/dist/lite/firestore/src/api.d.ts +1 -0
  40. package/dist/lite/firestore/src/core/firestore_client.d.ts +2 -1
  41. package/dist/lite/firestore/src/core/query.d.ts +2 -4
  42. package/dist/lite/firestore/src/core/target.d.ts +6 -4
  43. package/dist/lite/firestore/src/core/transaction_options.d.ts +25 -0
  44. package/dist/lite/firestore/src/core/transaction_runner.d.ts +3 -2
  45. package/dist/lite/firestore/src/lite-api/transaction.d.ts +2 -1
  46. package/dist/lite/firestore/src/lite-api/transaction_options.d.ts +23 -0
  47. package/dist/lite/firestore/src/local/index_manager.d.ts +24 -3
  48. package/dist/lite/firestore/src/local/indexeddb_index_manager.d.ts +4 -7
  49. package/dist/lite/firestore/src/local/indexeddb_schema.d.ts +1 -0
  50. package/dist/lite/firestore/src/local/local_documents_view.d.ts +1 -1
  51. package/dist/lite/firestore/src/local/memory_index_manager.d.ts +3 -2
  52. package/dist/lite/firestore/src/local/query_engine.d.ts +44 -5
  53. package/dist/lite/firestore/src/model/collections.d.ts +1 -1
  54. package/dist/lite/firestore/src/model/values.d.ts +14 -2
  55. package/dist/lite/firestore/src/util/misc.d.ts +3 -0
  56. package/dist/lite/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
  57. package/dist/lite/firestore/test/unit/local/test_index_manager.d.ts +2 -2
  58. package/dist/lite/index.browser.esm2017.js +341 -312
  59. package/dist/lite/index.browser.esm2017.js.map +1 -1
  60. package/dist/lite/index.browser.esm5.js +299 -291
  61. package/dist/lite/index.browser.esm5.js.map +1 -1
  62. package/dist/lite/index.d.ts +24 -1
  63. package/dist/lite/index.node.cjs.js +56 -15
  64. package/dist/lite/index.node.cjs.js.map +1 -1
  65. package/dist/lite/index.node.mjs +56 -15
  66. package/dist/lite/index.node.mjs.map +1 -1
  67. package/dist/lite/index.rn.esm2017.js +1227 -1198
  68. package/dist/lite/index.rn.esm2017.js.map +1 -1
  69. package/dist/lite/internal.d.ts +25 -1
  70. package/dist/lite/packages/firestore/dist/lite/index.browser.esm2017.d.ts +28 -28
  71. package/dist/lite/packages/firestore/lite/index.d.ts +1 -0
  72. package/dist/lite/packages/firestore/src/api/transaction.d.ts +2 -1
  73. package/dist/lite/packages/firestore/src/api/transaction_options.d.ts +17 -0
  74. package/dist/lite/packages/firestore/src/api.d.ts +1 -0
  75. package/dist/lite/packages/firestore/src/core/firestore_client.d.ts +2 -1
  76. package/dist/lite/packages/firestore/src/core/query.d.ts +2 -4
  77. package/dist/lite/packages/firestore/src/core/target.d.ts +6 -4
  78. package/dist/lite/packages/firestore/src/core/transaction_options.d.ts +25 -0
  79. package/dist/lite/packages/firestore/src/core/transaction_runner.d.ts +3 -2
  80. package/dist/lite/packages/firestore/src/lite-api/transaction.d.ts +2 -1
  81. package/dist/lite/packages/firestore/src/lite-api/transaction_options.d.ts +23 -0
  82. package/dist/lite/packages/firestore/src/local/index_manager.d.ts +24 -3
  83. package/dist/lite/packages/firestore/src/local/indexeddb_index_manager.d.ts +4 -7
  84. package/dist/lite/packages/firestore/src/local/indexeddb_schema.d.ts +1 -0
  85. package/dist/lite/packages/firestore/src/local/local_documents_view.d.ts +1 -1
  86. package/dist/lite/packages/firestore/src/local/memory_index_manager.d.ts +3 -2
  87. package/dist/lite/packages/firestore/src/local/query_engine.d.ts +44 -5
  88. package/dist/lite/packages/firestore/src/model/collections.d.ts +1 -1
  89. package/dist/lite/packages/firestore/src/model/values.d.ts +14 -2
  90. package/dist/lite/packages/firestore/src/util/misc.d.ts +3 -0
  91. package/dist/lite/packages/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
  92. package/dist/lite/packages/firestore/test/unit/local/test_index_manager.d.ts +2 -2
  93. package/dist/lite/private.d.ts +25 -1
  94. package/dist/packages/firestore/dist/index.esm2017.d.ts +155 -154
  95. package/dist/packages/firestore/lite/index.d.ts +1 -0
  96. package/dist/packages/firestore/src/api/transaction.d.ts +2 -1
  97. package/dist/packages/firestore/src/api/transaction_options.d.ts +17 -0
  98. package/dist/packages/firestore/src/api.d.ts +1 -0
  99. package/dist/packages/firestore/src/core/firestore_client.d.ts +2 -1
  100. package/dist/packages/firestore/src/core/query.d.ts +2 -4
  101. package/dist/packages/firestore/src/core/target.d.ts +6 -4
  102. package/dist/packages/firestore/src/core/transaction_options.d.ts +25 -0
  103. package/dist/packages/firestore/src/core/transaction_runner.d.ts +3 -2
  104. package/dist/packages/firestore/src/lite-api/transaction.d.ts +2 -1
  105. package/dist/packages/firestore/src/lite-api/transaction_options.d.ts +23 -0
  106. package/dist/packages/firestore/src/local/index_manager.d.ts +24 -3
  107. package/dist/packages/firestore/src/local/indexeddb_index_manager.d.ts +4 -7
  108. package/dist/packages/firestore/src/local/indexeddb_schema.d.ts +1 -0
  109. package/dist/packages/firestore/src/local/local_documents_view.d.ts +1 -1
  110. package/dist/packages/firestore/src/local/memory_index_manager.d.ts +3 -2
  111. package/dist/packages/firestore/src/local/query_engine.d.ts +44 -5
  112. package/dist/packages/firestore/src/model/collections.d.ts +1 -1
  113. package/dist/packages/firestore/src/model/values.d.ts +14 -2
  114. package/dist/packages/firestore/src/util/misc.d.ts +3 -0
  115. package/dist/packages/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
  116. package/dist/packages/firestore/test/unit/local/test_index_manager.d.ts +2 -2
  117. package/dist/private.d.ts +50 -4
  118. package/package.json +7 -7
@@ -33,7 +33,7 @@ var grpc__namespace = /*#__PURE__*/_interopNamespace(grpc);
33
33
  var protoLoader__namespace = /*#__PURE__*/_interopNamespace(protoLoader);
34
34
 
35
35
  const name = "@firebase/firestore";
36
- const version$1 = "3.4.8";
36
+ const version$1 = "3.4.9";
37
37
 
38
38
  /**
39
39
  * @license
@@ -86,7 +86,7 @@ User.GOOGLE_CREDENTIALS = new User('google-credentials-uid');
86
86
  User.FIRST_PARTY = new User('first-party-uid');
87
87
  User.MOCK_USER = new User('mock-user');
88
88
 
89
- const version = "9.6.11";
89
+ const version = "9.8.0";
90
90
 
91
91
  /**
92
92
  * @license
@@ -3346,7 +3346,7 @@ function typeOrder(value) {
3346
3346
  return 4 /* ServerTimestampValue */;
3347
3347
  }
3348
3348
  else if (isMaxValue(value)) {
3349
- return 9 /* ArrayValue */;
3349
+ return 9007199254740991 /* MaxValue */;
3350
3350
  }
3351
3351
  return 10 /* ObjectValue */;
3352
3352
  }
@@ -3558,6 +3558,15 @@ function compareArrays(left, right) {
3558
3558
  return primitiveComparator(leftArray.length, rightArray.length);
3559
3559
  }
3560
3560
  function compareMaps(left, right) {
3561
+ if (left === MAX_VALUE.mapValue && right === MAX_VALUE.mapValue) {
3562
+ return 0;
3563
+ }
3564
+ else if (left === MAX_VALUE.mapValue) {
3565
+ return 1;
3566
+ }
3567
+ else if (right === MAX_VALUE.mapValue) {
3568
+ return -1;
3569
+ }
3561
3570
  const leftMap = left.fields || {};
3562
3571
  const leftKeys = Object.keys(leftMap);
3563
3572
  const rightMap = right.fields || {};
@@ -3805,27 +3814,31 @@ function valuesGetUpperBound(value) {
3805
3814
  return fail();
3806
3815
  }
3807
3816
  }
3808
- function valuesMax(left, right) {
3809
- if (left === undefined) {
3810
- return right;
3817
+ function lowerBoundCompare(left, right) {
3818
+ const cmp = valueCompare(left.value, right.value);
3819
+ if (cmp !== 0) {
3820
+ return cmp;
3811
3821
  }
3812
- else if (right === undefined) {
3813
- return left;
3822
+ if (left.inclusive && !right.inclusive) {
3823
+ return -1;
3814
3824
  }
3815
- else {
3816
- return valueCompare(left, right) > 0 ? left : right;
3825
+ else if (!left.inclusive && right.inclusive) {
3826
+ return 1;
3817
3827
  }
3828
+ return 0;
3818
3829
  }
3819
- function valuesMin(left, right) {
3820
- if (left === undefined) {
3821
- return right;
3830
+ function upperBoundCompare(left, right) {
3831
+ const cmp = valueCompare(left.value, right.value);
3832
+ if (cmp !== 0) {
3833
+ return cmp;
3822
3834
  }
3823
- else if (right === undefined) {
3824
- return left;
3835
+ if (left.inclusive && !right.inclusive) {
3836
+ return 1;
3825
3837
  }
3826
- else {
3827
- return valueCompare(left, right) < 0 ? left : right;
3838
+ else if (!left.inclusive && right.inclusive) {
3839
+ return -1;
3828
3840
  }
3841
+ return 0;
3829
3842
  }
3830
3843
 
3831
3844
  /**
@@ -4335,7 +4348,7 @@ function indexOffsetComparator(left, right) {
4335
4348
 
4336
4349
  /**
4337
4350
  * @license
4338
- * Copyright 2019 Google LLC
4351
+ * Copyright 2017 Google LLC
4339
4352
  *
4340
4353
  * Licensed under the Apache License, Version 2.0 (the "License");
4341
4354
  * you may not use this file except in compliance with the License.
@@ -4349,609 +4362,445 @@ function indexOffsetComparator(left, right) {
4349
4362
  * See the License for the specific language governing permissions and
4350
4363
  * limitations under the License.
4351
4364
  */
4352
- // Visible for testing
4353
- class TargetImpl {
4354
- constructor(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
4355
- this.path = path;
4356
- this.collectionGroup = collectionGroup;
4357
- this.orderBy = orderBy;
4358
- this.filters = filters;
4359
- this.limit = limit;
4360
- this.startAt = startAt;
4361
- this.endAt = endAt;
4362
- this.memoizedCanonicalId = null;
4365
+ // An immutable sorted map implementation, based on a Left-leaning Red-Black
4366
+ // tree.
4367
+ class SortedMap {
4368
+ constructor(comparator, root) {
4369
+ this.comparator = comparator;
4370
+ this.root = root ? root : LLRBNode.EMPTY;
4363
4371
  }
4364
- }
4365
- /**
4366
- * Initializes a Target with a path and optional additional query constraints.
4367
- * Path must currently be empty if this is a collection group query.
4368
- *
4369
- * NOTE: you should always construct `Target` from `Query.toTarget` instead of
4370
- * using this factory method, because `Query` provides an implicit `orderBy`
4371
- * property.
4372
- */
4373
- function newTarget(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
4374
- return new TargetImpl(path, collectionGroup, orderBy, filters, limit, startAt, endAt);
4375
- }
4376
- function canonifyTarget(target) {
4377
- const targetImpl = debugCast(target);
4378
- if (targetImpl.memoizedCanonicalId === null) {
4379
- let str = targetImpl.path.canonicalString();
4380
- if (targetImpl.collectionGroup !== null) {
4381
- str += '|cg:' + targetImpl.collectionGroup;
4382
- }
4383
- str += '|f:';
4384
- str += targetImpl.filters.map(f => canonifyFilter(f)).join(',');
4385
- str += '|ob:';
4386
- str += targetImpl.orderBy.map(o => canonifyOrderBy(o)).join(',');
4387
- if (!isNullOrUndefined(targetImpl.limit)) {
4388
- str += '|l:';
4389
- str += targetImpl.limit;
4390
- }
4391
- if (targetImpl.startAt) {
4392
- str += '|lb:';
4393
- str += targetImpl.startAt.inclusive ? 'b:' : 'a:';
4394
- str += targetImpl.startAt.position.map(p => canonicalId(p)).join(',');
4395
- }
4396
- if (targetImpl.endAt) {
4397
- str += '|ub:';
4398
- str += targetImpl.endAt.inclusive ? 'a:' : 'b:';
4399
- str += targetImpl.endAt.position.map(p => canonicalId(p)).join(',');
4400
- }
4401
- targetImpl.memoizedCanonicalId = str;
4372
+ // Returns a copy of the map, with the specified key/value added or replaced.
4373
+ insert(key, value) {
4374
+ return new SortedMap(this.comparator, this.root
4375
+ .insert(key, value, this.comparator)
4376
+ .copy(null, null, LLRBNode.BLACK, null, null));
4402
4377
  }
4403
- return targetImpl.memoizedCanonicalId;
4404
- }
4405
- function stringifyTarget(target) {
4406
- let str = target.path.canonicalString();
4407
- if (target.collectionGroup !== null) {
4408
- str += ' collectionGroup=' + target.collectionGroup;
4378
+ // Returns a copy of the map, with the specified key removed.
4379
+ remove(key) {
4380
+ return new SortedMap(this.comparator, this.root
4381
+ .remove(key, this.comparator)
4382
+ .copy(null, null, LLRBNode.BLACK, null, null));
4409
4383
  }
4410
- if (target.filters.length > 0) {
4411
- str += `, filters: [${target.filters
4412
- .map(f => stringifyFilter(f))
4413
- .join(', ')}]`;
4384
+ // Returns the value of the node with the given key, or null.
4385
+ get(key) {
4386
+ let node = this.root;
4387
+ while (!node.isEmpty()) {
4388
+ const cmp = this.comparator(key, node.key);
4389
+ if (cmp === 0) {
4390
+ return node.value;
4391
+ }
4392
+ else if (cmp < 0) {
4393
+ node = node.left;
4394
+ }
4395
+ else if (cmp > 0) {
4396
+ node = node.right;
4397
+ }
4398
+ }
4399
+ return null;
4414
4400
  }
4415
- if (!isNullOrUndefined(target.limit)) {
4416
- str += ', limit: ' + target.limit;
4401
+ // Returns the index of the element in this sorted map, or -1 if it doesn't
4402
+ // exist.
4403
+ indexOf(key) {
4404
+ // Number of nodes that were pruned when descending right
4405
+ let prunedNodes = 0;
4406
+ let node = this.root;
4407
+ while (!node.isEmpty()) {
4408
+ const cmp = this.comparator(key, node.key);
4409
+ if (cmp === 0) {
4410
+ return prunedNodes + node.left.size;
4411
+ }
4412
+ else if (cmp < 0) {
4413
+ node = node.left;
4414
+ }
4415
+ else {
4416
+ // Count all nodes left of the node plus the node itself
4417
+ prunedNodes += node.left.size + 1;
4418
+ node = node.right;
4419
+ }
4420
+ }
4421
+ // Node not found
4422
+ return -1;
4417
4423
  }
4418
- if (target.orderBy.length > 0) {
4419
- str += `, orderBy: [${target.orderBy
4420
- .map(o => stringifyOrderBy(o))
4421
- .join(', ')}]`;
4424
+ isEmpty() {
4425
+ return this.root.isEmpty();
4422
4426
  }
4423
- if (target.startAt) {
4424
- str += ', startAt: ';
4425
- str += target.startAt.inclusive ? 'b:' : 'a:';
4426
- str += target.startAt.position.map(p => canonicalId(p)).join(',');
4427
+ // Returns the total number of nodes in the map.
4428
+ get size() {
4429
+ return this.root.size;
4427
4430
  }
4428
- if (target.endAt) {
4429
- str += ', endAt: ';
4430
- str += target.endAt.inclusive ? 'a:' : 'b:';
4431
- str += target.endAt.position.map(p => canonicalId(p)).join(',');
4431
+ // Returns the minimum key in the map.
4432
+ minKey() {
4433
+ return this.root.minKey();
4432
4434
  }
4433
- return `Target(${str})`;
4434
- }
4435
- function targetEquals(left, right) {
4436
- if (left.limit !== right.limit) {
4437
- return false;
4435
+ // Returns the maximum key in the map.
4436
+ maxKey() {
4437
+ return this.root.maxKey();
4438
4438
  }
4439
- if (left.orderBy.length !== right.orderBy.length) {
4440
- return false;
4439
+ // Traverses the map in key order and calls the specified action function
4440
+ // for each key/value pair. If action returns true, traversal is aborted.
4441
+ // Returns the first truthy value returned by action, or the last falsey
4442
+ // value returned by action.
4443
+ inorderTraversal(action) {
4444
+ return this.root.inorderTraversal(action);
4441
4445
  }
4442
- for (let i = 0; i < left.orderBy.length; i++) {
4443
- if (!orderByEquals(left.orderBy[i], right.orderBy[i])) {
4446
+ forEach(fn) {
4447
+ this.inorderTraversal((k, v) => {
4448
+ fn(k, v);
4444
4449
  return false;
4445
- }
4446
- }
4447
- if (left.filters.length !== right.filters.length) {
4448
- return false;
4450
+ });
4449
4451
  }
4450
- for (let i = 0; i < left.filters.length; i++) {
4451
- if (!filterEquals(left.filters[i], right.filters[i])) {
4452
+ toString() {
4453
+ const descriptions = [];
4454
+ this.inorderTraversal((k, v) => {
4455
+ descriptions.push(`${k}:${v}`);
4452
4456
  return false;
4453
- }
4457
+ });
4458
+ return `{${descriptions.join(', ')}}`;
4454
4459
  }
4455
- if (left.collectionGroup !== right.collectionGroup) {
4456
- return false;
4460
+ // Traverses the map in reverse key order and calls the specified action
4461
+ // function for each key/value pair. If action returns true, traversal is
4462
+ // aborted.
4463
+ // Returns the first truthy value returned by action, or the last falsey
4464
+ // value returned by action.
4465
+ reverseTraversal(action) {
4466
+ return this.root.reverseTraversal(action);
4457
4467
  }
4458
- if (!left.path.isEqual(right.path)) {
4459
- return false;
4468
+ // Returns an iterator over the SortedMap.
4469
+ getIterator() {
4470
+ return new SortedMapIterator(this.root, null, this.comparator, false);
4460
4471
  }
4461
- if (!boundEquals(left.startAt, right.startAt)) {
4462
- return false;
4472
+ getIteratorFrom(key) {
4473
+ return new SortedMapIterator(this.root, key, this.comparator, false);
4463
4474
  }
4464
- return boundEquals(left.endAt, right.endAt);
4465
- }
4466
- function targetIsDocumentTarget(target) {
4467
- return (DocumentKey.isDocumentKey(target.path) &&
4468
- target.collectionGroup === null &&
4469
- target.filters.length === 0);
4470
- }
4471
- /** Returns the field filters that target the given field path. */
4472
- function targetGetFieldFiltersForPath(target, path) {
4473
- return target.filters.filter(f => f instanceof FieldFilter && f.field.isEqual(path));
4474
- }
4475
- /**
4476
- * Returns the values that are used in ARRAY_CONTAINS or ARRAY_CONTAINS_ANY
4477
- * filters. Returns `null` if there are no such filters.
4478
- */
4479
- function targetGetArrayValues(target, fieldIndex) {
4480
- const segment = fieldIndexGetArraySegment(fieldIndex);
4481
- if (segment === undefined) {
4482
- return null;
4475
+ getReverseIterator() {
4476
+ return new SortedMapIterator(this.root, null, this.comparator, true);
4483
4477
  }
4484
- for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
4485
- switch (fieldFilter.op) {
4486
- case "array-contains-any" /* ARRAY_CONTAINS_ANY */:
4487
- return fieldFilter.value.arrayValue.values || [];
4488
- case "array-contains" /* ARRAY_CONTAINS */:
4489
- return [fieldFilter.value];
4490
- // Remaining filters are not array filters.
4491
- }
4478
+ getReverseIteratorFrom(key) {
4479
+ return new SortedMapIterator(this.root, key, this.comparator, true);
4492
4480
  }
4493
- return null;
4494
- }
4495
- /**
4496
- * Returns the list of values that are used in != or NOT_IN filters. Returns
4497
- * `null` if there are no such filters.
4498
- */
4499
- function targetGetNotInValues(target, fieldIndex) {
4500
- const values = new Map();
4501
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
4502
- for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
4503
- switch (fieldFilter.op) {
4504
- case "==" /* EQUAL */:
4505
- case "in" /* IN */:
4506
- // Encode equality prefix, which is encoded in the index value before
4507
- // the inequality (e.g. `a == 'a' && b != 'b'` is encoded to
4508
- // `value != 'ab'`).
4509
- values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
4510
- break;
4511
- case "not-in" /* NOT_IN */:
4512
- case "!=" /* NOT_EQUAL */:
4513
- // NotIn/NotEqual is always a suffix. There cannot be any remaining
4514
- // segments and hence we can return early here.
4515
- values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
4516
- return Array.from(values.values());
4517
- // Remaining filters cannot be used as notIn bounds.
4481
+ } // end SortedMap
4482
+ // An iterator over an LLRBNode.
4483
+ class SortedMapIterator {
4484
+ constructor(node, startKey, comparator, isReverse) {
4485
+ this.isReverse = isReverse;
4486
+ this.nodeStack = [];
4487
+ let cmp = 1;
4488
+ while (!node.isEmpty()) {
4489
+ cmp = startKey ? comparator(node.key, startKey) : 1;
4490
+ // flip the comparison if we're going in reverse
4491
+ if (startKey && isReverse) {
4492
+ cmp *= -1;
4493
+ }
4494
+ if (cmp < 0) {
4495
+ // This node is less than our start key. ignore it
4496
+ if (this.isReverse) {
4497
+ node = node.left;
4498
+ }
4499
+ else {
4500
+ node = node.right;
4501
+ }
4502
+ }
4503
+ else if (cmp === 0) {
4504
+ // This node is exactly equal to our start key. Push it on the stack,
4505
+ // but stop iterating;
4506
+ this.nodeStack.push(node);
4507
+ break;
4508
+ }
4509
+ else {
4510
+ // This node is greater than our start key, add it to the stack and move
4511
+ // to the next one
4512
+ this.nodeStack.push(node);
4513
+ if (this.isReverse) {
4514
+ node = node.right;
4515
+ }
4516
+ else {
4517
+ node = node.left;
4518
+ }
4518
4519
  }
4519
4520
  }
4520
4521
  }
4521
- return null;
4522
- }
4523
- /**
4524
- * Returns a lower bound of field values that can be used as a starting point to
4525
- * scan the index defined by `fieldIndex`. Returns `null` if no lower bound
4526
- * exists.
4527
- */
4528
- function targetGetLowerBound(target, fieldIndex) {
4529
- const values = [];
4530
- let inclusive = true;
4531
- // For each segment, retrieve a lower bound if there is a suitable filter or
4532
- // startAt.
4533
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
4534
- const segmentBound = segment.kind === 0 /* ASCENDING */
4535
- ? targetGetLowerBoundForField(target, segment.fieldPath, target.startAt)
4536
- : targetGetUpperBoundForField(target, segment.fieldPath, target.startAt);
4537
- if (!segmentBound.value) {
4538
- // No lower bound exists
4539
- return null;
4522
+ getNext() {
4523
+ let node = this.nodeStack.pop();
4524
+ const result = { key: node.key, value: node.value };
4525
+ if (this.isReverse) {
4526
+ node = node.left;
4527
+ while (!node.isEmpty()) {
4528
+ this.nodeStack.push(node);
4529
+ node = node.right;
4530
+ }
4540
4531
  }
4541
- values.push(segmentBound.value);
4542
- inclusive && (inclusive = segmentBound.inclusive);
4532
+ else {
4533
+ node = node.right;
4534
+ while (!node.isEmpty()) {
4535
+ this.nodeStack.push(node);
4536
+ node = node.left;
4537
+ }
4538
+ }
4539
+ return result;
4543
4540
  }
4544
- return new Bound(values, inclusive);
4545
- }
4546
- /**
4547
- * Returns an upper bound of field values that can be used as an ending point
4548
- * when scanning the index defined by `fieldIndex`. Returns `null` if no
4549
- * upper bound exists.
4550
- */
4551
- function targetGetUpperBound(target, fieldIndex) {
4552
- const values = [];
4553
- let inclusive = true;
4554
- // For each segment, retrieve an upper bound if there is a suitable filter or
4555
- // endAt.
4556
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
4557
- const segmentBound = segment.kind === 0 /* ASCENDING */
4558
- ? targetGetUpperBoundForField(target, segment.fieldPath, target.endAt)
4559
- : targetGetLowerBoundForField(target, segment.fieldPath, target.endAt);
4560
- if (!segmentBound.value) {
4561
- // No upper bound exists
4541
+ hasNext() {
4542
+ return this.nodeStack.length > 0;
4543
+ }
4544
+ peek() {
4545
+ if (this.nodeStack.length === 0) {
4562
4546
  return null;
4563
4547
  }
4564
- values.push(segmentBound.value);
4565
- inclusive && (inclusive = segmentBound.inclusive);
4548
+ const node = this.nodeStack[this.nodeStack.length - 1];
4549
+ return { key: node.key, value: node.value };
4566
4550
  }
4567
- return new Bound(values, inclusive);
4568
- }
4569
- /**
4570
- * Returns the value to use as the lower bound for ascending index segment at
4571
- * the provided `fieldPath` (or the upper bound for an descending segment).
4572
- */
4573
- function targetGetLowerBoundForField(target, fieldPath, bound) {
4574
- let value = undefined;
4575
- let inclusive = true;
4576
- // Process all filters to find a value for the current field segment
4577
- for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
4578
- let filterValue = undefined;
4579
- let filterInclusive = true;
4580
- switch (fieldFilter.op) {
4581
- case "<" /* LESS_THAN */:
4582
- case "<=" /* LESS_THAN_OR_EQUAL */:
4583
- filterValue = valuesGetLowerBound(fieldFilter.value);
4584
- break;
4585
- case "==" /* EQUAL */:
4586
- case "in" /* IN */:
4587
- case ">=" /* GREATER_THAN_OR_EQUAL */:
4588
- filterValue = fieldFilter.value;
4589
- break;
4590
- case ">" /* GREATER_THAN */:
4591
- filterValue = fieldFilter.value;
4592
- filterInclusive = false;
4593
- break;
4594
- case "!=" /* NOT_EQUAL */:
4595
- case "not-in" /* NOT_IN */:
4596
- filterValue = MIN_VALUE;
4597
- break;
4598
- // Remaining filters cannot be used as lower bounds.
4551
+ } // end SortedMapIterator
4552
+ // Represents a node in a Left-leaning Red-Black tree.
4553
+ class LLRBNode {
4554
+ constructor(key, value, color, left, right) {
4555
+ this.key = key;
4556
+ this.value = value;
4557
+ this.color = color != null ? color : LLRBNode.RED;
4558
+ this.left = left != null ? left : LLRBNode.EMPTY;
4559
+ this.right = right != null ? right : LLRBNode.EMPTY;
4560
+ this.size = this.left.size + 1 + this.right.size;
4561
+ }
4562
+ // Returns a copy of the current node, optionally replacing pieces of it.
4563
+ copy(key, value, color, left, right) {
4564
+ return new LLRBNode(key != null ? key : this.key, value != null ? value : this.value, color != null ? color : this.color, left != null ? left : this.left, right != null ? right : this.right);
4565
+ }
4566
+ isEmpty() {
4567
+ return false;
4568
+ }
4569
+ // Traverses the tree in key order and calls the specified action function
4570
+ // for each node. If action returns true, traversal is aborted.
4571
+ // Returns the first truthy value returned by action, or the last falsey
4572
+ // value returned by action.
4573
+ inorderTraversal(action) {
4574
+ return (this.left.inorderTraversal(action) ||
4575
+ action(this.key, this.value) ||
4576
+ this.right.inorderTraversal(action));
4577
+ }
4578
+ // Traverses the tree in reverse key order and calls the specified action
4579
+ // function for each node. If action returns true, traversal is aborted.
4580
+ // Returns the first truthy value returned by action, or the last falsey
4581
+ // value returned by action.
4582
+ reverseTraversal(action) {
4583
+ return (this.right.reverseTraversal(action) ||
4584
+ action(this.key, this.value) ||
4585
+ this.left.reverseTraversal(action));
4586
+ }
4587
+ // Returns the minimum node in the tree.
4588
+ min() {
4589
+ if (this.left.isEmpty()) {
4590
+ return this;
4599
4591
  }
4600
- if (valuesMax(value, filterValue) === filterValue) {
4601
- value = filterValue;
4602
- inclusive = filterInclusive;
4592
+ else {
4593
+ return this.left.min();
4603
4594
  }
4604
4595
  }
4605
- // If there is an additional bound, compare the values against the existing
4606
- // range to see if we can narrow the scope.
4607
- if (bound !== null) {
4608
- for (let i = 0; i < target.orderBy.length; ++i) {
4609
- const orderBy = target.orderBy[i];
4610
- if (orderBy.field.isEqual(fieldPath)) {
4611
- const cursorValue = bound.position[i];
4612
- if (valuesMax(value, cursorValue) === cursorValue) {
4613
- value = cursorValue;
4614
- inclusive = bound.inclusive;
4615
- }
4616
- break;
4617
- }
4618
- }
4596
+ // Returns the maximum key in the tree.
4597
+ minKey() {
4598
+ return this.min().key;
4619
4599
  }
4620
- return { value, inclusive };
4621
- }
4622
- /**
4623
- * Returns the value to use as the upper bound for ascending index segment at
4624
- * the provided `fieldPath` (or the lower bound for an descending segment).
4625
- */
4626
- function targetGetUpperBoundForField(target, fieldPath, bound) {
4627
- let value = undefined;
4628
- let inclusive = true;
4629
- // Process all filters to find a value for the current field segment
4630
- for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
4631
- let filterValue = undefined;
4632
- let filterInclusive = true;
4633
- switch (fieldFilter.op) {
4634
- case ">=" /* GREATER_THAN_OR_EQUAL */:
4635
- case ">" /* GREATER_THAN */:
4636
- filterValue = valuesGetUpperBound(fieldFilter.value);
4637
- filterInclusive = false;
4638
- break;
4639
- case "==" /* EQUAL */:
4640
- case "in" /* IN */:
4641
- case "<=" /* LESS_THAN_OR_EQUAL */:
4642
- filterValue = fieldFilter.value;
4643
- break;
4644
- case "<" /* LESS_THAN */:
4645
- filterValue = fieldFilter.value;
4646
- filterInclusive = false;
4647
- break;
4648
- case "!=" /* NOT_EQUAL */:
4649
- case "not-in" /* NOT_IN */:
4650
- filterValue = MAX_VALUE;
4651
- break;
4652
- // Remaining filters cannot be used as upper bounds.
4600
+ // Returns the maximum key in the tree.
4601
+ maxKey() {
4602
+ if (this.right.isEmpty()) {
4603
+ return this.key;
4653
4604
  }
4654
- if (valuesMin(value, filterValue) === filterValue) {
4655
- value = filterValue;
4656
- inclusive = filterInclusive;
4605
+ else {
4606
+ return this.right.maxKey();
4657
4607
  }
4658
4608
  }
4659
- // If there is an additional bound, compare the values against the existing
4660
- // range to see if we can narrow the scope.
4661
- if (bound !== null) {
4662
- for (let i = 0; i < target.orderBy.length; ++i) {
4663
- const orderBy = target.orderBy[i];
4664
- if (orderBy.field.isEqual(fieldPath)) {
4665
- const cursorValue = bound.position[i];
4666
- if (valuesMin(value, cursorValue) === cursorValue) {
4667
- value = cursorValue;
4668
- inclusive = bound.inclusive;
4669
- }
4670
- break;
4671
- }
4609
+ // Returns new tree, with the key/value added.
4610
+ insert(key, value, comparator) {
4611
+ let n = this;
4612
+ const cmp = comparator(key, n.key);
4613
+ if (cmp < 0) {
4614
+ n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
4615
+ }
4616
+ else if (cmp === 0) {
4617
+ n = n.copy(null, value, null, null, null);
4672
4618
  }
4619
+ else {
4620
+ n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
4621
+ }
4622
+ return n.fixUp();
4673
4623
  }
4674
- return { value, inclusive };
4675
- }
4676
- class Filter {
4677
- }
4678
- class FieldFilter extends Filter {
4679
- constructor(field, op, value) {
4680
- super();
4681
- this.field = field;
4682
- this.op = op;
4683
- this.value = value;
4624
+ removeMin() {
4625
+ if (this.left.isEmpty()) {
4626
+ return LLRBNode.EMPTY;
4627
+ }
4628
+ let n = this;
4629
+ if (!n.left.isRed() && !n.left.left.isRed()) {
4630
+ n = n.moveRedLeft();
4631
+ }
4632
+ n = n.copy(null, null, null, n.left.removeMin(), null);
4633
+ return n.fixUp();
4684
4634
  }
4685
- /**
4686
- * Creates a filter based on the provided arguments.
4687
- */
4688
- static create(field, op, value) {
4689
- if (field.isKeyField()) {
4690
- if (op === "in" /* IN */ || op === "not-in" /* NOT_IN */) {
4691
- return this.createKeyFieldInFilter(field, op, value);
4635
+ // Returns new tree, with the specified item removed.
4636
+ remove(key, comparator) {
4637
+ let smallest;
4638
+ let n = this;
4639
+ if (comparator(key, n.key) < 0) {
4640
+ if (!n.left.isEmpty() && !n.left.isRed() && !n.left.left.isRed()) {
4641
+ n = n.moveRedLeft();
4692
4642
  }
4693
- else {
4694
- return new KeyFieldFilter(field, op, value);
4643
+ n = n.copy(null, null, null, n.left.remove(key, comparator), null);
4644
+ }
4645
+ else {
4646
+ if (n.left.isRed()) {
4647
+ n = n.rotateRight();
4648
+ }
4649
+ if (!n.right.isEmpty() && !n.right.isRed() && !n.right.left.isRed()) {
4650
+ n = n.moveRedRight();
4651
+ }
4652
+ if (comparator(key, n.key) === 0) {
4653
+ if (n.right.isEmpty()) {
4654
+ return LLRBNode.EMPTY;
4655
+ }
4656
+ else {
4657
+ smallest = n.right.min();
4658
+ n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin());
4659
+ }
4695
4660
  }
4661
+ n = n.copy(null, null, null, null, n.right.remove(key, comparator));
4696
4662
  }
4697
- else if (op === "array-contains" /* ARRAY_CONTAINS */) {
4698
- return new ArrayContainsFilter(field, value);
4663
+ return n.fixUp();
4664
+ }
4665
+ isRed() {
4666
+ return this.color;
4667
+ }
4668
+ // Returns new tree after performing any needed rotations.
4669
+ fixUp() {
4670
+ let n = this;
4671
+ if (n.right.isRed() && !n.left.isRed()) {
4672
+ n = n.rotateLeft();
4699
4673
  }
4700
- else if (op === "in" /* IN */) {
4701
- return new InFilter(field, value);
4674
+ if (n.left.isRed() && n.left.left.isRed()) {
4675
+ n = n.rotateRight();
4702
4676
  }
4703
- else if (op === "not-in" /* NOT_IN */) {
4704
- return new NotInFilter(field, value);
4677
+ if (n.left.isRed() && n.right.isRed()) {
4678
+ n = n.colorFlip();
4705
4679
  }
4706
- else if (op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
4707
- return new ArrayContainsAnyFilter(field, value);
4680
+ return n;
4681
+ }
4682
+ moveRedLeft() {
4683
+ let n = this.colorFlip();
4684
+ if (n.right.left.isRed()) {
4685
+ n = n.copy(null, null, null, null, n.right.rotateRight());
4686
+ n = n.rotateLeft();
4687
+ n = n.colorFlip();
4708
4688
  }
4709
- else {
4710
- return new FieldFilter(field, op, value);
4689
+ return n;
4690
+ }
4691
+ moveRedRight() {
4692
+ let n = this.colorFlip();
4693
+ if (n.left.left.isRed()) {
4694
+ n = n.rotateRight();
4695
+ n = n.colorFlip();
4711
4696
  }
4697
+ return n;
4712
4698
  }
4713
- static createKeyFieldInFilter(field, op, value) {
4714
- return op === "in" /* IN */
4715
- ? new KeyFieldInFilter(field, value)
4716
- : new KeyFieldNotInFilter(field, value);
4699
+ rotateLeft() {
4700
+ const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
4701
+ return this.right.copy(null, null, this.color, nl, null);
4717
4702
  }
4718
- matches(doc) {
4719
- const other = doc.data.field(this.field);
4720
- // Types do not have to match in NOT_EQUAL filters.
4721
- if (this.op === "!=" /* NOT_EQUAL */) {
4722
- return (other !== null &&
4723
- this.matchesComparison(valueCompare(other, this.value)));
4703
+ rotateRight() {
4704
+ const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
4705
+ return this.left.copy(null, null, this.color, null, nr);
4706
+ }
4707
+ colorFlip() {
4708
+ const left = this.left.copy(null, null, !this.left.color, null, null);
4709
+ const right = this.right.copy(null, null, !this.right.color, null, null);
4710
+ return this.copy(null, null, !this.color, left, right);
4711
+ }
4712
+ // For testing.
4713
+ checkMaxDepth() {
4714
+ const blackDepth = this.check();
4715
+ if (Math.pow(2.0, blackDepth) <= this.size + 1) {
4716
+ return true;
4717
+ }
4718
+ else {
4719
+ return false;
4724
4720
  }
4725
- // Only compare types with matching backend order (such as double and int).
4726
- return (other !== null &&
4727
- typeOrder(this.value) === typeOrder(other) &&
4728
- this.matchesComparison(valueCompare(other, this.value)));
4729
4721
  }
4730
- matchesComparison(comparison) {
4731
- switch (this.op) {
4732
- case "<" /* LESS_THAN */:
4733
- return comparison < 0;
4734
- case "<=" /* LESS_THAN_OR_EQUAL */:
4735
- return comparison <= 0;
4736
- case "==" /* EQUAL */:
4737
- return comparison === 0;
4738
- case "!=" /* NOT_EQUAL */:
4739
- return comparison !== 0;
4740
- case ">" /* GREATER_THAN */:
4741
- return comparison > 0;
4742
- case ">=" /* GREATER_THAN_OR_EQUAL */:
4743
- return comparison >= 0;
4744
- default:
4745
- return fail();
4722
+ // In a balanced RB tree, the black-depth (number of black nodes) from root to
4723
+ // leaves is equal on both sides. This function verifies that or asserts.
4724
+ check() {
4725
+ if (this.isRed() && this.left.isRed()) {
4726
+ throw fail();
4727
+ }
4728
+ if (this.right.isRed()) {
4729
+ throw fail();
4730
+ }
4731
+ const blackDepth = this.left.check();
4732
+ if (blackDepth !== this.right.check()) {
4733
+ throw fail();
4734
+ }
4735
+ else {
4736
+ return blackDepth + (this.isRed() ? 0 : 1);
4746
4737
  }
4747
4738
  }
4748
- isInequality() {
4749
- return ([
4750
- "<" /* LESS_THAN */,
4751
- "<=" /* LESS_THAN_OR_EQUAL */,
4752
- ">" /* GREATER_THAN */,
4753
- ">=" /* GREATER_THAN_OR_EQUAL */,
4754
- "!=" /* NOT_EQUAL */,
4755
- "not-in" /* NOT_IN */
4756
- ].indexOf(this.op) >= 0);
4739
+ } // end LLRBNode
4740
+ // Empty node is shared between all LLRB trees.
4741
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4742
+ LLRBNode.EMPTY = null;
4743
+ LLRBNode.RED = true;
4744
+ LLRBNode.BLACK = false;
4745
+ // Represents an empty node (a leaf node in the Red-Black Tree).
4746
+ class LLRBEmptyNode {
4747
+ constructor() {
4748
+ this.size = 0;
4757
4749
  }
4758
- }
4759
- function canonifyFilter(filter) {
4760
- // TODO(b/29183165): Technically, this won't be unique if two values have
4761
- // the same description, such as the int 3 and the string "3". So we should
4762
- // add the types in here somehow, too.
4763
- return (filter.field.canonicalString() +
4764
- filter.op.toString() +
4765
- canonicalId(filter.value));
4766
- }
4767
- function filterEquals(f1, f2) {
4768
- return (f1.op === f2.op &&
4769
- f1.field.isEqual(f2.field) &&
4770
- valueEquals(f1.value, f2.value));
4771
- }
4772
- /** Returns a debug description for `filter`. */
4773
- function stringifyFilter(filter) {
4774
- return `${filter.field.canonicalString()} ${filter.op} ${canonicalId(filter.value)}`;
4775
- }
4776
- /** Filter that matches on key fields (i.e. '__name__'). */
4777
- class KeyFieldFilter extends FieldFilter {
4778
- constructor(field, op, value) {
4779
- super(field, op, value);
4780
- this.key = DocumentKey.fromName(value.referenceValue);
4750
+ get key() {
4751
+ throw fail();
4781
4752
  }
4782
- matches(doc) {
4783
- const comparison = DocumentKey.comparator(doc.key, this.key);
4784
- return this.matchesComparison(comparison);
4753
+ get value() {
4754
+ throw fail();
4785
4755
  }
4786
- }
4787
- /** Filter that matches on key fields within an array. */
4788
- class KeyFieldInFilter extends FieldFilter {
4789
- constructor(field, value) {
4790
- super(field, "in" /* IN */, value);
4791
- this.keys = extractDocumentKeysFromArrayValue("in" /* IN */, value);
4756
+ get color() {
4757
+ throw fail();
4792
4758
  }
4793
- matches(doc) {
4794
- return this.keys.some(key => key.isEqual(doc.key));
4759
+ get left() {
4760
+ throw fail();
4795
4761
  }
4796
- }
4797
- /** Filter that matches on key fields not present within an array. */
4798
- class KeyFieldNotInFilter extends FieldFilter {
4799
- constructor(field, value) {
4800
- super(field, "not-in" /* NOT_IN */, value);
4801
- this.keys = extractDocumentKeysFromArrayValue("not-in" /* NOT_IN */, value);
4762
+ get right() {
4763
+ throw fail();
4802
4764
  }
4803
- matches(doc) {
4804
- return !this.keys.some(key => key.isEqual(doc.key));
4765
+ // Returns a copy of the current node.
4766
+ copy(key, value, color, left, right) {
4767
+ return this;
4805
4768
  }
4806
- }
4807
- function extractDocumentKeysFromArrayValue(op, value) {
4808
- var _a;
4809
- return (((_a = value.arrayValue) === null || _a === void 0 ? void 0 : _a.values) || []).map(v => {
4810
- return DocumentKey.fromName(v.referenceValue);
4811
- });
4812
- }
4813
- /** A Filter that implements the array-contains operator. */
4814
- class ArrayContainsFilter extends FieldFilter {
4815
- constructor(field, value) {
4816
- super(field, "array-contains" /* ARRAY_CONTAINS */, value);
4769
+ // Returns a copy of the tree, with the specified key/value added.
4770
+ insert(key, value, comparator) {
4771
+ return new LLRBNode(key, value);
4817
4772
  }
4818
- matches(doc) {
4819
- const other = doc.data.field(this.field);
4820
- return isArray(other) && arrayValueContains(other.arrayValue, this.value);
4773
+ // Returns a copy of the tree, with the specified key removed.
4774
+ remove(key, comparator) {
4775
+ return this;
4821
4776
  }
4822
- }
4823
- /** A Filter that implements the IN operator. */
4824
- class InFilter extends FieldFilter {
4825
- constructor(field, value) {
4826
- super(field, "in" /* IN */, value);
4777
+ isEmpty() {
4778
+ return true;
4827
4779
  }
4828
- matches(doc) {
4829
- const other = doc.data.field(this.field);
4830
- return other !== null && arrayValueContains(this.value.arrayValue, other);
4831
- }
4832
- }
4833
- /** A Filter that implements the not-in operator. */
4834
- class NotInFilter extends FieldFilter {
4835
- constructor(field, value) {
4836
- super(field, "not-in" /* NOT_IN */, value);
4837
- }
4838
- matches(doc) {
4839
- if (arrayValueContains(this.value.arrayValue, { nullValue: 'NULL_VALUE' })) {
4840
- return false;
4841
- }
4842
- const other = doc.data.field(this.field);
4843
- return other !== null && !arrayValueContains(this.value.arrayValue, other);
4844
- }
4845
- }
4846
- /** A Filter that implements the array-contains-any operator. */
4847
- class ArrayContainsAnyFilter extends FieldFilter {
4848
- constructor(field, value) {
4849
- super(field, "array-contains-any" /* ARRAY_CONTAINS_ANY */, value);
4850
- }
4851
- matches(doc) {
4852
- const other = doc.data.field(this.field);
4853
- if (!isArray(other) || !other.arrayValue.values) {
4854
- return false;
4855
- }
4856
- return other.arrayValue.values.some(val => arrayValueContains(this.value.arrayValue, val));
4857
- }
4858
- }
4859
- /**
4860
- * Represents a bound of a query.
4861
- *
4862
- * The bound is specified with the given components representing a position and
4863
- * whether it's just before or just after the position (relative to whatever the
4864
- * query order is).
4865
- *
4866
- * The position represents a logical index position for a query. It's a prefix
4867
- * of values for the (potentially implicit) order by clauses of a query.
4868
- *
4869
- * Bound provides a function to determine whether a document comes before or
4870
- * after a bound. This is influenced by whether the position is just before or
4871
- * just after the provided values.
4872
- */
4873
- class Bound {
4874
- constructor(position, inclusive) {
4875
- this.position = position;
4876
- this.inclusive = inclusive;
4780
+ inorderTraversal(action) {
4781
+ return false;
4877
4782
  }
4878
- }
4879
- /**
4880
- * An ordering on a field, in some Direction. Direction defaults to ASCENDING.
4881
- */
4882
- class OrderBy {
4883
- constructor(field, dir = "asc" /* ASCENDING */) {
4884
- this.field = field;
4885
- this.dir = dir;
4783
+ reverseTraversal(action) {
4784
+ return false;
4886
4785
  }
4887
- }
4888
- function canonifyOrderBy(orderBy) {
4889
- // TODO(b/29183165): Make this collision robust.
4890
- return orderBy.field.canonicalString() + orderBy.dir;
4891
- }
4892
- function stringifyOrderBy(orderBy) {
4893
- return `${orderBy.field.canonicalString()} (${orderBy.dir})`;
4894
- }
4895
- function orderByEquals(left, right) {
4896
- return left.dir === right.dir && left.field.isEqual(right.field);
4897
- }
4898
- function boundCompareToDocument(bound, orderBy, doc) {
4899
- let comparison = 0;
4900
- for (let i = 0; i < bound.position.length; i++) {
4901
- const orderByComponent = orderBy[i];
4902
- const component = bound.position[i];
4903
- if (orderByComponent.field.isKeyField()) {
4904
- comparison = DocumentKey.comparator(DocumentKey.fromName(component.referenceValue), doc.key);
4905
- }
4906
- else {
4907
- const docValue = doc.data.field(orderByComponent.field);
4908
- comparison = valueCompare(component, docValue);
4909
- }
4910
- if (orderByComponent.dir === "desc" /* DESCENDING */) {
4911
- comparison = comparison * -1;
4912
- }
4913
- if (comparison !== 0) {
4914
- break;
4915
- }
4786
+ minKey() {
4787
+ return null;
4916
4788
  }
4917
- return comparison;
4918
- }
4919
- /**
4920
- * Returns true if a document sorts after a bound using the provided sort
4921
- * order.
4922
- */
4923
- function boundSortsAfterDocument(bound, orderBy, doc) {
4924
- const comparison = boundCompareToDocument(bound, orderBy, doc);
4925
- return bound.inclusive ? comparison >= 0 : comparison > 0;
4926
- }
4927
- /**
4928
- * Returns true if a document sorts before a bound using the provided sort
4929
- * order.
4930
- */
4931
- function boundSortsBeforeDocument(bound, orderBy, doc) {
4932
- const comparison = boundCompareToDocument(bound, orderBy, doc);
4933
- return bound.inclusive ? comparison <= 0 : comparison < 0;
4934
- }
4935
- function boundEquals(left, right) {
4936
- if (left === null) {
4937
- return right === null;
4789
+ maxKey() {
4790
+ return null;
4938
4791
  }
4939
- else if (right === null) {
4792
+ isRed() {
4940
4793
  return false;
4941
4794
  }
4942
- if (left.inclusive !== right.inclusive ||
4943
- left.position.length !== right.position.length) {
4944
- return false;
4795
+ // For testing.
4796
+ checkMaxDepth() {
4797
+ return true;
4945
4798
  }
4946
- for (let i = 0; i < left.position.length; i++) {
4947
- const leftPosition = left.position[i];
4948
- const rightPosition = right.position[i];
4949
- if (!valueEquals(leftPosition, rightPosition)) {
4950
- return false;
4951
- }
4799
+ check() {
4800
+ return 0;
4952
4801
  }
4953
- return true;
4954
- }
4802
+ } // end LLRBEmptyNode
4803
+ LLRBNode.EMPTY = new LLRBEmptyNode();
4955
4804
 
4956
4805
  /**
4957
4806
  * @license
@@ -4970,325 +4819,223 @@ function boundEquals(left, right) {
4970
4819
  * limitations under the License.
4971
4820
  */
4972
4821
  /**
4973
- * Query encapsulates all the query attributes we support in the SDK. It can
4974
- * be run against the LocalStore, as well as be converted to a `Target` to
4975
- * query the RemoteStore results.
4822
+ * SortedSet is an immutable (copy-on-write) collection that holds elements
4823
+ * in order specified by the provided comparator.
4976
4824
  *
4977
- * Visible for testing.
4825
+ * NOTE: if provided comparator returns 0 for two elements, we consider them to
4826
+ * be equal!
4978
4827
  */
4979
- class QueryImpl {
4980
- /**
4981
- * Initializes a Query with a path and optional additional query constraints.
4982
- * Path must currently be empty if this is a collection group query.
4983
- */
4984
- constructor(path, collectionGroup = null, explicitOrderBy = [], filters = [], limit = null, limitType = "F" /* First */, startAt = null, endAt = null) {
4985
- this.path = path;
4986
- this.collectionGroup = collectionGroup;
4987
- this.explicitOrderBy = explicitOrderBy;
4988
- this.filters = filters;
4989
- this.limit = limit;
4990
- this.limitType = limitType;
4991
- this.startAt = startAt;
4992
- this.endAt = endAt;
4993
- this.memoizedOrderBy = null;
4994
- // The corresponding `Target` of this `Query` instance.
4995
- this.memoizedTarget = null;
4996
- if (this.startAt) ;
4997
- if (this.endAt) ;
4828
+ class SortedSet {
4829
+ constructor(comparator) {
4830
+ this.comparator = comparator;
4831
+ this.data = new SortedMap(this.comparator);
4998
4832
  }
4999
- }
5000
- /** Creates a new Query instance with the options provided. */
5001
- function newQuery(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt) {
5002
- return new QueryImpl(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt);
5003
- }
5004
- /** Creates a new Query for a query that matches all documents at `path` */
5005
- function newQueryForPath(path) {
5006
- return new QueryImpl(path);
5007
- }
5008
- /**
5009
- * Helper to convert a collection group query into a collection query at a
5010
- * specific path. This is used when executing collection group queries, since
5011
- * we have to split the query into a set of collection queries at multiple
5012
- * paths.
5013
- */
5014
- function asCollectionQueryAtPath(query, path) {
5015
- return new QueryImpl(path,
5016
- /*collectionGroup=*/ null, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
5017
- }
5018
- /**
5019
- * Returns true if this query does not specify any query constraints that
5020
- * could remove results.
5021
- */
5022
- function matchesAllDocuments(query) {
5023
- return (query.filters.length === 0 &&
5024
- query.limit === null &&
5025
- query.startAt == null &&
5026
- query.endAt == null &&
5027
- (query.explicitOrderBy.length === 0 ||
5028
- (query.explicitOrderBy.length === 1 &&
5029
- query.explicitOrderBy[0].field.isKeyField())));
5030
- }
5031
- function hasLimitToFirst(query) {
5032
- return !isNullOrUndefined(query.limit) && query.limitType === "F" /* First */;
5033
- }
5034
- function hasLimitToLast(query) {
5035
- return !isNullOrUndefined(query.limit) && query.limitType === "L" /* Last */;
5036
- }
5037
- function getFirstOrderByField(query) {
5038
- return query.explicitOrderBy.length > 0
5039
- ? query.explicitOrderBy[0].field
5040
- : null;
5041
- }
5042
- function getInequalityFilterField(query) {
5043
- for (const filter of query.filters) {
5044
- if (filter.isInequality()) {
5045
- return filter.field;
5046
- }
4833
+ has(elem) {
4834
+ return this.data.get(elem) !== null;
5047
4835
  }
5048
- return null;
5049
- }
5050
- /**
5051
- * Checks if any of the provided Operators are included in the query and
5052
- * returns the first one that is, or null if none are.
5053
- */
5054
- function findFilterOperator(query, operators) {
5055
- for (const filter of query.filters) {
5056
- if (operators.indexOf(filter.op) >= 0) {
5057
- return filter.op;
4836
+ first() {
4837
+ return this.data.minKey();
4838
+ }
4839
+ last() {
4840
+ return this.data.maxKey();
4841
+ }
4842
+ get size() {
4843
+ return this.data.size;
4844
+ }
4845
+ indexOf(elem) {
4846
+ return this.data.indexOf(elem);
4847
+ }
4848
+ /** Iterates elements in order defined by "comparator" */
4849
+ forEach(cb) {
4850
+ this.data.inorderTraversal((k, v) => {
4851
+ cb(k);
4852
+ return false;
4853
+ });
4854
+ }
4855
+ /** Iterates over `elem`s such that: range[0] &lt;= elem &lt; range[1]. */
4856
+ forEachInRange(range, cb) {
4857
+ const iter = this.data.getIteratorFrom(range[0]);
4858
+ while (iter.hasNext()) {
4859
+ const elem = iter.getNext();
4860
+ if (this.comparator(elem.key, range[1]) >= 0) {
4861
+ return;
4862
+ }
4863
+ cb(elem.key);
5058
4864
  }
5059
4865
  }
5060
- return null;
5061
- }
5062
- /**
5063
- * Creates a new Query for a collection group query that matches all documents
5064
- * within the provided collection group.
5065
- */
5066
- function newQueryForCollectionGroup(collectionId) {
5067
- return new QueryImpl(ResourcePath.emptyPath(), collectionId);
5068
- }
5069
- /**
5070
- * Returns whether the query matches a single document by path (rather than a
5071
- * collection).
5072
- */
5073
- function isDocumentQuery$1(query) {
5074
- return (DocumentKey.isDocumentKey(query.path) &&
5075
- query.collectionGroup === null &&
5076
- query.filters.length === 0);
5077
- }
5078
- /**
5079
- * Returns whether the query matches a collection group rather than a specific
5080
- * collection.
5081
- */
5082
- function isCollectionGroupQuery(query) {
5083
- return query.collectionGroup !== null;
5084
- }
5085
- /**
5086
- * Returns the implicit order by constraint that is used to execute the Query,
5087
- * which can be different from the order by constraints the user provided (e.g.
5088
- * the SDK and backend always orders by `__name__`).
5089
- */
5090
- function queryOrderBy(query) {
5091
- const queryImpl = debugCast(query);
5092
- if (queryImpl.memoizedOrderBy === null) {
5093
- queryImpl.memoizedOrderBy = [];
5094
- const inequalityField = getInequalityFilterField(queryImpl);
5095
- const firstOrderByField = getFirstOrderByField(queryImpl);
5096
- if (inequalityField !== null && firstOrderByField === null) {
5097
- // In order to implicitly add key ordering, we must also add the
5098
- // inequality filter field for it to be a valid query.
5099
- // Note that the default inequality field and key ordering is ascending.
5100
- if (!inequalityField.isKeyField()) {
5101
- queryImpl.memoizedOrderBy.push(new OrderBy(inequalityField));
5102
- }
5103
- queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), "asc" /* ASCENDING */));
4866
+ /**
4867
+ * Iterates over `elem`s such that: start &lt;= elem until false is returned.
4868
+ */
4869
+ forEachWhile(cb, start) {
4870
+ let iter;
4871
+ if (start !== undefined) {
4872
+ iter = this.data.getIteratorFrom(start);
5104
4873
  }
5105
4874
  else {
5106
- let foundKeyOrdering = false;
5107
- for (const orderBy of queryImpl.explicitOrderBy) {
5108
- queryImpl.memoizedOrderBy.push(orderBy);
5109
- if (orderBy.field.isKeyField()) {
5110
- foundKeyOrdering = true;
5111
- }
5112
- }
5113
- if (!foundKeyOrdering) {
5114
- // The order of the implicit key ordering always matches the last
5115
- // explicit order by
5116
- const lastDirection = queryImpl.explicitOrderBy.length > 0
5117
- ? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1]
5118
- .dir
5119
- : "asc" /* ASCENDING */;
5120
- queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), lastDirection));
5121
- }
5122
- }
5123
- }
5124
- return queryImpl.memoizedOrderBy;
5125
- }
5126
- /**
5127
- * Converts this `Query` instance to it's corresponding `Target` representation.
5128
- */
5129
- function queryToTarget(query) {
5130
- const queryImpl = debugCast(query);
5131
- if (!queryImpl.memoizedTarget) {
5132
- if (queryImpl.limitType === "F" /* First */) {
5133
- queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, queryOrderBy(queryImpl), queryImpl.filters, queryImpl.limit, queryImpl.startAt, queryImpl.endAt);
4875
+ iter = this.data.getIterator();
5134
4876
  }
5135
- else {
5136
- // Flip the orderBy directions since we want the last results
5137
- const orderBys = [];
5138
- for (const orderBy of queryOrderBy(queryImpl)) {
5139
- const dir = orderBy.dir === "desc" /* DESCENDING */
5140
- ? "asc" /* ASCENDING */
5141
- : "desc" /* DESCENDING */;
5142
- orderBys.push(new OrderBy(orderBy.field, dir));
4877
+ while (iter.hasNext()) {
4878
+ const elem = iter.getNext();
4879
+ const result = cb(elem.key);
4880
+ if (!result) {
4881
+ return;
5143
4882
  }
5144
- // We need to swap the cursors to match the now-flipped query ordering.
5145
- const startAt = queryImpl.endAt
5146
- ? new Bound(queryImpl.endAt.position, !queryImpl.endAt.inclusive)
5147
- : null;
5148
- const endAt = queryImpl.startAt
5149
- ? new Bound(queryImpl.startAt.position, !queryImpl.startAt.inclusive)
5150
- : null;
5151
- // Now return as a LimitType.First query.
5152
- queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, orderBys, queryImpl.filters, queryImpl.limit, startAt, endAt);
5153
4883
  }
5154
4884
  }
5155
- return queryImpl.memoizedTarget;
5156
- }
5157
- function queryWithAddedFilter(query, filter) {
5158
- const newFilters = query.filters.concat([filter]);
5159
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), newFilters, query.limit, query.limitType, query.startAt, query.endAt);
5160
- }
5161
- function queryWithAddedOrderBy(query, orderBy) {
5162
- // TODO(dimond): validate that orderBy does not list the same key twice.
5163
- const newOrderBy = query.explicitOrderBy.concat([orderBy]);
5164
- return new QueryImpl(query.path, query.collectionGroup, newOrderBy, query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
5165
- }
5166
- function queryWithLimit(query, limit, limitType) {
5167
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), limit, limitType, query.startAt, query.endAt);
5168
- }
5169
- function queryWithStartAt(query, bound) {
5170
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, bound, query.endAt);
5171
- }
5172
- function queryWithEndAt(query, bound) {
5173
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, bound);
5174
- }
5175
- function queryEquals(left, right) {
5176
- return (targetEquals(queryToTarget(left), queryToTarget(right)) &&
5177
- left.limitType === right.limitType);
5178
- }
5179
- // TODO(b/29183165): This is used to get a unique string from a query to, for
5180
- // example, use as a dictionary key, but the implementation is subject to
5181
- // collisions. Make it collision-free.
5182
- function canonifyQuery(query) {
5183
- return `${canonifyTarget(queryToTarget(query))}|lt:${query.limitType}`;
5184
- }
5185
- function stringifyQuery(query) {
5186
- return `Query(target=${stringifyTarget(queryToTarget(query))}; limitType=${query.limitType})`;
5187
- }
5188
- /** Returns whether `doc` matches the constraints of `query`. */
5189
- function queryMatches(query, doc) {
5190
- return (doc.isFoundDocument() &&
5191
- queryMatchesPathAndCollectionGroup(query, doc) &&
5192
- queryMatchesOrderBy(query, doc) &&
5193
- queryMatchesFilters(query, doc) &&
5194
- queryMatchesBounds(query, doc));
5195
- }
5196
- function queryMatchesPathAndCollectionGroup(query, doc) {
5197
- const docPath = doc.key.path;
5198
- if (query.collectionGroup !== null) {
5199
- // NOTE: this.path is currently always empty since we don't expose Collection
5200
- // Group queries rooted at a document path yet.
5201
- return (doc.key.hasCollectionId(query.collectionGroup) &&
5202
- query.path.isPrefixOf(docPath));
4885
+ /** Finds the least element greater than or equal to `elem`. */
4886
+ firstAfterOrEqual(elem) {
4887
+ const iter = this.data.getIteratorFrom(elem);
4888
+ return iter.hasNext() ? iter.getNext().key : null;
5203
4889
  }
5204
- else if (DocumentKey.isDocumentKey(query.path)) {
5205
- // exact match for document queries
5206
- return query.path.isEqual(docPath);
4890
+ getIterator() {
4891
+ return new SortedSetIterator(this.data.getIterator());
5207
4892
  }
5208
- else {
5209
- // shallow ancestor queries by default
5210
- return query.path.isImmediateParentOf(docPath);
4893
+ getIteratorFrom(key) {
4894
+ return new SortedSetIterator(this.data.getIteratorFrom(key));
5211
4895
  }
5212
- }
5213
- /**
5214
- * A document must have a value for every ordering clause in order to show up
5215
- * in the results.
5216
- */
5217
- function queryMatchesOrderBy(query, doc) {
5218
- for (const orderBy of query.explicitOrderBy) {
5219
- // order by key always matches
5220
- if (!orderBy.field.isKeyField() && doc.data.field(orderBy.field) === null) {
5221
- return false;
4896
+ /** Inserts or updates an element */
4897
+ add(elem) {
4898
+ return this.copy(this.data.remove(elem).insert(elem, true));
4899
+ }
4900
+ /** Deletes an element */
4901
+ delete(elem) {
4902
+ if (!this.has(elem)) {
4903
+ return this;
5222
4904
  }
4905
+ return this.copy(this.data.remove(elem));
5223
4906
  }
5224
- return true;
5225
- }
5226
- function queryMatchesFilters(query, doc) {
5227
- for (const filter of query.filters) {
5228
- if (!filter.matches(doc)) {
4907
+ isEmpty() {
4908
+ return this.data.isEmpty();
4909
+ }
4910
+ unionWith(other) {
4911
+ let result = this;
4912
+ // Make sure `result` always refers to the larger one of the two sets.
4913
+ if (result.size < other.size) {
4914
+ result = other;
4915
+ other = this;
4916
+ }
4917
+ other.forEach(elem => {
4918
+ result = result.add(elem);
4919
+ });
4920
+ return result;
4921
+ }
4922
+ isEqual(other) {
4923
+ if (!(other instanceof SortedSet)) {
5229
4924
  return false;
5230
4925
  }
4926
+ if (this.size !== other.size) {
4927
+ return false;
4928
+ }
4929
+ const thisIt = this.data.getIterator();
4930
+ const otherIt = other.data.getIterator();
4931
+ while (thisIt.hasNext()) {
4932
+ const thisElem = thisIt.getNext().key;
4933
+ const otherElem = otherIt.getNext().key;
4934
+ if (this.comparator(thisElem, otherElem) !== 0) {
4935
+ return false;
4936
+ }
4937
+ }
4938
+ return true;
4939
+ }
4940
+ toArray() {
4941
+ const res = [];
4942
+ this.forEach(targetId => {
4943
+ res.push(targetId);
4944
+ });
4945
+ return res;
4946
+ }
4947
+ toString() {
4948
+ const result = [];
4949
+ this.forEach(elem => result.push(elem));
4950
+ return 'SortedSet(' + result.toString() + ')';
4951
+ }
4952
+ copy(data) {
4953
+ const result = new SortedSet(this.comparator);
4954
+ result.data = data;
4955
+ return result;
5231
4956
  }
5232
- return true;
5233
4957
  }
5234
- /** Makes sure a document is within the bounds, if provided. */
5235
- function queryMatchesBounds(query, doc) {
5236
- if (query.startAt &&
5237
- !boundSortsBeforeDocument(query.startAt, queryOrderBy(query), doc)) {
5238
- return false;
4958
+ class SortedSetIterator {
4959
+ constructor(iter) {
4960
+ this.iter = iter;
5239
4961
  }
5240
- if (query.endAt &&
5241
- !boundSortsAfterDocument(query.endAt, queryOrderBy(query), doc)) {
5242
- return false;
4962
+ getNext() {
4963
+ return this.iter.getNext().key;
4964
+ }
4965
+ hasNext() {
4966
+ return this.iter.hasNext();
5243
4967
  }
5244
- return true;
5245
4968
  }
5246
4969
  /**
5247
- * Returns the collection group that this query targets.
4970
+ * Compares two sorted sets for equality using their natural ordering. The
4971
+ * method computes the intersection and invokes `onAdd` for every element that
4972
+ * is in `after` but not `before`. `onRemove` is invoked for every element in
4973
+ * `before` but missing from `after`.
5248
4974
  *
5249
- * PORTING NOTE: This is only used in the Web SDK to facilitate multi-tab
5250
- * synchronization for query results.
5251
- */
5252
- function queryCollectionGroup(query) {
5253
- return (query.collectionGroup ||
5254
- (query.path.length % 2 === 1
5255
- ? query.path.lastSegment()
5256
- : query.path.get(query.path.length - 2)));
5257
- }
5258
- /**
5259
- * Returns a new comparator function that can be used to compare two documents
5260
- * based on the Query's ordering constraint.
4975
+ * The method creates a copy of both `before` and `after` and runs in O(n log
4976
+ * n), where n is the size of the two lists.
4977
+ *
4978
+ * @param before - The elements that exist in the original set.
4979
+ * @param after - The elements to diff against the original set.
4980
+ * @param comparator - The comparator for the elements in before and after.
4981
+ * @param onAdd - A function to invoke for every element that is part of `
4982
+ * after` but not `before`.
4983
+ * @param onRemove - A function to invoke for every element that is part of
4984
+ * `before` but not `after`.
5261
4985
  */
5262
- function newQueryComparator(query) {
5263
- return (d1, d2) => {
5264
- let comparedOnKeyField = false;
5265
- for (const orderBy of queryOrderBy(query)) {
5266
- const comp = compareDocs(orderBy, d1, d2);
5267
- if (comp !== 0) {
5268
- return comp;
4986
+ function diffSortedSets(before, after, comparator, onAdd, onRemove) {
4987
+ const beforeIt = before.getIterator();
4988
+ const afterIt = after.getIterator();
4989
+ let beforeValue = advanceIterator(beforeIt);
4990
+ let afterValue = advanceIterator(afterIt);
4991
+ // Walk through the two sets at the same time, using the ordering defined by
4992
+ // `comparator`.
4993
+ while (beforeValue || afterValue) {
4994
+ let added = false;
4995
+ let removed = false;
4996
+ if (beforeValue && afterValue) {
4997
+ const cmp = comparator(beforeValue, afterValue);
4998
+ if (cmp < 0) {
4999
+ // The element was removed if the next element in our ordered
5000
+ // walkthrough is only in `before`.
5001
+ removed = true;
5002
+ }
5003
+ else if (cmp > 0) {
5004
+ // The element was added if the next element in our ordered walkthrough
5005
+ // is only in `after`.
5006
+ added = true;
5269
5007
  }
5270
- comparedOnKeyField = comparedOnKeyField || orderBy.field.isKeyField();
5271
5008
  }
5272
- return 0;
5273
- };
5274
- }
5275
- function compareDocs(orderBy, d1, d2) {
5276
- const comparison = orderBy.field.isKeyField()
5277
- ? DocumentKey.comparator(d1.key, d2.key)
5278
- : compareDocumentsByField(orderBy.field, d1, d2);
5279
- switch (orderBy.dir) {
5280
- case "asc" /* ASCENDING */:
5281
- return comparison;
5282
- case "desc" /* DESCENDING */:
5283
- return -1 * comparison;
5284
- default:
5285
- return fail();
5009
+ else if (beforeValue != null) {
5010
+ removed = true;
5011
+ }
5012
+ else {
5013
+ added = true;
5014
+ }
5015
+ if (added) {
5016
+ onAdd(afterValue);
5017
+ afterValue = advanceIterator(afterIt);
5018
+ }
5019
+ else if (removed) {
5020
+ onRemove(beforeValue);
5021
+ beforeValue = advanceIterator(beforeIt);
5022
+ }
5023
+ else {
5024
+ beforeValue = advanceIterator(beforeIt);
5025
+ afterValue = advanceIterator(afterIt);
5026
+ }
5286
5027
  }
5028
+ }
5029
+ /**
5030
+ * Returns the next element from the iterator or `undefined` if none available.
5031
+ */
5032
+ function advanceIterator(it) {
5033
+ return it.hasNext() ? it.getNext() : undefined;
5287
5034
  }
5288
5035
 
5289
5036
  /**
5290
5037
  * @license
5291
- * Copyright 2017 Google LLC
5038
+ * Copyright 2019 Google LLC
5292
5039
  *
5293
5040
  * Licensed under the Apache License, Version 2.0 (the "License");
5294
5041
  * you may not use this file except in compliance with the License.
@@ -5302,555 +5049,961 @@ function compareDocs(orderBy, d1, d2) {
5302
5049
  * See the License for the specific language governing permissions and
5303
5050
  * limitations under the License.
5304
5051
  */
5052
+ // Visible for testing
5053
+ class TargetImpl {
5054
+ constructor(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
5055
+ this.path = path;
5056
+ this.collectionGroup = collectionGroup;
5057
+ this.orderBy = orderBy;
5058
+ this.filters = filters;
5059
+ this.limit = limit;
5060
+ this.startAt = startAt;
5061
+ this.endAt = endAt;
5062
+ this.memoizedCanonicalId = null;
5063
+ }
5064
+ }
5305
5065
  /**
5306
- * A map implementation that uses objects as keys. Objects must have an
5307
- * associated equals function and must be immutable. Entries in the map are
5308
- * stored together with the key being produced from the mapKeyFn. This map
5309
- * automatically handles collisions of keys.
5066
+ * Initializes a Target with a path and optional additional query constraints.
5067
+ * Path must currently be empty if this is a collection group query.
5068
+ *
5069
+ * NOTE: you should always construct `Target` from `Query.toTarget` instead of
5070
+ * using this factory method, because `Query` provides an implicit `orderBy`
5071
+ * property.
5310
5072
  */
5311
- class ObjectMap {
5312
- constructor(mapKeyFn, equalsFn) {
5313
- this.mapKeyFn = mapKeyFn;
5314
- this.equalsFn = equalsFn;
5315
- /**
5316
- * The inner map for a key/value pair. Due to the possibility of collisions we
5317
- * keep a list of entries that we do a linear search through to find an actual
5318
- * match. Note that collisions should be rare, so we still expect near
5319
- * constant time lookups in practice.
5320
- */
5321
- this.inner = {};
5322
- /** The number of entries stored in the map */
5323
- this.innerSize = 0;
5324
- }
5325
- /** Get a value for this key, or undefined if it does not exist. */
5326
- get(key) {
5327
- const id = this.mapKeyFn(key);
5328
- const matches = this.inner[id];
5329
- if (matches === undefined) {
5330
- return undefined;
5073
+ function newTarget(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
5074
+ return new TargetImpl(path, collectionGroup, orderBy, filters, limit, startAt, endAt);
5075
+ }
5076
+ function canonifyTarget(target) {
5077
+ const targetImpl = debugCast(target);
5078
+ if (targetImpl.memoizedCanonicalId === null) {
5079
+ let str = targetImpl.path.canonicalString();
5080
+ if (targetImpl.collectionGroup !== null) {
5081
+ str += '|cg:' + targetImpl.collectionGroup;
5331
5082
  }
5332
- for (const [otherKey, value] of matches) {
5333
- if (this.equalsFn(otherKey, key)) {
5334
- return value;
5335
- }
5083
+ str += '|f:';
5084
+ str += targetImpl.filters.map(f => canonifyFilter(f)).join(',');
5085
+ str += '|ob:';
5086
+ str += targetImpl.orderBy.map(o => canonifyOrderBy(o)).join(',');
5087
+ if (!isNullOrUndefined(targetImpl.limit)) {
5088
+ str += '|l:';
5089
+ str += targetImpl.limit;
5336
5090
  }
5337
- return undefined;
5338
- }
5339
- has(key) {
5340
- return this.get(key) !== undefined;
5341
- }
5342
- /** Put this key and value in the map. */
5343
- set(key, value) {
5344
- const id = this.mapKeyFn(key);
5345
- const matches = this.inner[id];
5346
- if (matches === undefined) {
5347
- this.inner[id] = [[key, value]];
5348
- this.innerSize++;
5349
- return;
5091
+ if (targetImpl.startAt) {
5092
+ str += '|lb:';
5093
+ str += targetImpl.startAt.inclusive ? 'b:' : 'a:';
5094
+ str += targetImpl.startAt.position.map(p => canonicalId(p)).join(',');
5350
5095
  }
5351
- for (let i = 0; i < matches.length; i++) {
5352
- if (this.equalsFn(matches[i][0], key)) {
5353
- // This is updating an existing entry and does not increase `innerSize`.
5354
- matches[i] = [key, value];
5355
- return;
5356
- }
5096
+ if (targetImpl.endAt) {
5097
+ str += '|ub:';
5098
+ str += targetImpl.endAt.inclusive ? 'a:' : 'b:';
5099
+ str += targetImpl.endAt.position.map(p => canonicalId(p)).join(',');
5357
5100
  }
5358
- matches.push([key, value]);
5359
- this.innerSize++;
5101
+ targetImpl.memoizedCanonicalId = str;
5360
5102
  }
5361
- /**
5362
- * Remove this key from the map. Returns a boolean if anything was deleted.
5363
- */
5364
- delete(key) {
5365
- const id = this.mapKeyFn(key);
5366
- const matches = this.inner[id];
5367
- if (matches === undefined) {
5103
+ return targetImpl.memoizedCanonicalId;
5104
+ }
5105
+ function stringifyTarget(target) {
5106
+ let str = target.path.canonicalString();
5107
+ if (target.collectionGroup !== null) {
5108
+ str += ' collectionGroup=' + target.collectionGroup;
5109
+ }
5110
+ if (target.filters.length > 0) {
5111
+ str += `, filters: [${target.filters
5112
+ .map(f => stringifyFilter(f))
5113
+ .join(', ')}]`;
5114
+ }
5115
+ if (!isNullOrUndefined(target.limit)) {
5116
+ str += ', limit: ' + target.limit;
5117
+ }
5118
+ if (target.orderBy.length > 0) {
5119
+ str += `, orderBy: [${target.orderBy
5120
+ .map(o => stringifyOrderBy(o))
5121
+ .join(', ')}]`;
5122
+ }
5123
+ if (target.startAt) {
5124
+ str += ', startAt: ';
5125
+ str += target.startAt.inclusive ? 'b:' : 'a:';
5126
+ str += target.startAt.position.map(p => canonicalId(p)).join(',');
5127
+ }
5128
+ if (target.endAt) {
5129
+ str += ', endAt: ';
5130
+ str += target.endAt.inclusive ? 'a:' : 'b:';
5131
+ str += target.endAt.position.map(p => canonicalId(p)).join(',');
5132
+ }
5133
+ return `Target(${str})`;
5134
+ }
5135
+ function targetEquals(left, right) {
5136
+ if (left.limit !== right.limit) {
5137
+ return false;
5138
+ }
5139
+ if (left.orderBy.length !== right.orderBy.length) {
5140
+ return false;
5141
+ }
5142
+ for (let i = 0; i < left.orderBy.length; i++) {
5143
+ if (!orderByEquals(left.orderBy[i], right.orderBy[i])) {
5368
5144
  return false;
5369
5145
  }
5370
- for (let i = 0; i < matches.length; i++) {
5371
- if (this.equalsFn(matches[i][0], key)) {
5372
- if (matches.length === 1) {
5373
- delete this.inner[id];
5374
- }
5375
- else {
5376
- matches.splice(i, 1);
5377
- }
5378
- this.innerSize--;
5379
- return true;
5380
- }
5381
- }
5382
- return false;
5383
5146
  }
5384
- forEach(fn) {
5385
- forEach(this.inner, (_, entries) => {
5386
- for (const [k, v] of entries) {
5387
- fn(k, v);
5388
- }
5389
- });
5147
+ if (left.filters.length !== right.filters.length) {
5148
+ return false;
5390
5149
  }
5391
- isEmpty() {
5392
- return isEmpty(this.inner);
5150
+ for (let i = 0; i < left.filters.length; i++) {
5151
+ if (!filterEquals(left.filters[i], right.filters[i])) {
5152
+ return false;
5153
+ }
5393
5154
  }
5394
- size() {
5395
- return this.innerSize;
5155
+ if (left.collectionGroup !== right.collectionGroup) {
5156
+ return false;
5396
5157
  }
5397
- }
5398
-
5158
+ if (!left.path.isEqual(right.path)) {
5159
+ return false;
5160
+ }
5161
+ if (!boundEquals(left.startAt, right.startAt)) {
5162
+ return false;
5163
+ }
5164
+ return boundEquals(left.endAt, right.endAt);
5165
+ }
5166
+ function targetIsDocumentTarget(target) {
5167
+ return (DocumentKey.isDocumentKey(target.path) &&
5168
+ target.collectionGroup === null &&
5169
+ target.filters.length === 0);
5170
+ }
5171
+ /** Returns the field filters that target the given field path. */
5172
+ function targetGetFieldFiltersForPath(target, path) {
5173
+ return target.filters.filter(f => f instanceof FieldFilter && f.field.isEqual(path));
5174
+ }
5399
5175
  /**
5400
- * @license
5401
- * Copyright 2017 Google LLC
5402
- *
5403
- * Licensed under the Apache License, Version 2.0 (the "License");
5404
- * you may not use this file except in compliance with the License.
5405
- * You may obtain a copy of the License at
5406
- *
5407
- * http://www.apache.org/licenses/LICENSE-2.0
5408
- *
5409
- * Unless required by applicable law or agreed to in writing, software
5410
- * distributed under the License is distributed on an "AS IS" BASIS,
5411
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5412
- * See the License for the specific language governing permissions and
5413
- * limitations under the License.
5176
+ * Returns the values that are used in ARRAY_CONTAINS or ARRAY_CONTAINS_ANY
5177
+ * filters. Returns `null` if there are no such filters.
5414
5178
  */
5415
- // An immutable sorted map implementation, based on a Left-leaning Red-Black
5416
- // tree.
5417
- class SortedMap {
5418
- constructor(comparator, root) {
5419
- this.comparator = comparator;
5420
- this.root = root ? root : LLRBNode.EMPTY;
5421
- }
5422
- // Returns a copy of the map, with the specified key/value added or replaced.
5423
- insert(key, value) {
5424
- return new SortedMap(this.comparator, this.root
5425
- .insert(key, value, this.comparator)
5426
- .copy(null, null, LLRBNode.BLACK, null, null));
5179
+ function targetGetArrayValues(target, fieldIndex) {
5180
+ const segment = fieldIndexGetArraySegment(fieldIndex);
5181
+ if (segment === undefined) {
5182
+ return null;
5427
5183
  }
5428
- // Returns a copy of the map, with the specified key removed.
5429
- remove(key) {
5430
- return new SortedMap(this.comparator, this.root
5431
- .remove(key, this.comparator)
5432
- .copy(null, null, LLRBNode.BLACK, null, null));
5184
+ for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
5185
+ switch (fieldFilter.op) {
5186
+ case "array-contains-any" /* ARRAY_CONTAINS_ANY */:
5187
+ return fieldFilter.value.arrayValue.values || [];
5188
+ case "array-contains" /* ARRAY_CONTAINS */:
5189
+ return [fieldFilter.value];
5190
+ // Remaining filters are not array filters.
5191
+ }
5433
5192
  }
5434
- // Returns the value of the node with the given key, or null.
5435
- get(key) {
5436
- let node = this.root;
5437
- while (!node.isEmpty()) {
5438
- const cmp = this.comparator(key, node.key);
5439
- if (cmp === 0) {
5440
- return node.value;
5441
- }
5442
- else if (cmp < 0) {
5443
- node = node.left;
5193
+ return null;
5194
+ }
5195
+ /**
5196
+ * Returns the list of values that are used in != or NOT_IN filters. Returns
5197
+ * `null` if there are no such filters.
5198
+ */
5199
+ function targetGetNotInValues(target, fieldIndex) {
5200
+ const values = new Map();
5201
+ for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
5202
+ for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
5203
+ switch (fieldFilter.op) {
5204
+ case "==" /* EQUAL */:
5205
+ case "in" /* IN */:
5206
+ // Encode equality prefix, which is encoded in the index value before
5207
+ // the inequality (e.g. `a == 'a' && b != 'b'` is encoded to
5208
+ // `value != 'ab'`).
5209
+ values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
5210
+ break;
5211
+ case "not-in" /* NOT_IN */:
5212
+ case "!=" /* NOT_EQUAL */:
5213
+ // NotIn/NotEqual is always a suffix. There cannot be any remaining
5214
+ // segments and hence we can return early here.
5215
+ values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
5216
+ return Array.from(values.values());
5217
+ // Remaining filters cannot be used as notIn bounds.
5444
5218
  }
5445
- else if (cmp > 0) {
5446
- node = node.right;
5219
+ }
5220
+ }
5221
+ return null;
5222
+ }
5223
+ /**
5224
+ * Returns a lower bound of field values that can be used as a starting point to
5225
+ * scan the index defined by `fieldIndex`. Returns `MIN_VALUE` if no lower bound
5226
+ * exists.
5227
+ */
5228
+ function targetGetLowerBound(target, fieldIndex) {
5229
+ const values = [];
5230
+ let inclusive = true;
5231
+ // For each segment, retrieve a lower bound if there is a suitable filter or
5232
+ // startAt.
5233
+ for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
5234
+ const segmentBound = segment.kind === 0 /* ASCENDING */
5235
+ ? targetGetAscendingBound(target, segment.fieldPath, target.startAt)
5236
+ : targetGetDescendingBound(target, segment.fieldPath, target.startAt);
5237
+ values.push(segmentBound.value);
5238
+ inclusive && (inclusive = segmentBound.inclusive);
5239
+ }
5240
+ return new Bound(values, inclusive);
5241
+ }
5242
+ /**
5243
+ * Returns an upper bound of field values that can be used as an ending point
5244
+ * when scanning the index defined by `fieldIndex`. Returns `MAX_VALUE` if no
5245
+ * upper bound exists.
5246
+ */
5247
+ function targetGetUpperBound(target, fieldIndex) {
5248
+ const values = [];
5249
+ let inclusive = true;
5250
+ // For each segment, retrieve an upper bound if there is a suitable filter or
5251
+ // endAt.
5252
+ for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
5253
+ const segmentBound = segment.kind === 0 /* ASCENDING */
5254
+ ? targetGetDescendingBound(target, segment.fieldPath, target.endAt)
5255
+ : targetGetAscendingBound(target, segment.fieldPath, target.endAt);
5256
+ values.push(segmentBound.value);
5257
+ inclusive && (inclusive = segmentBound.inclusive);
5258
+ }
5259
+ return new Bound(values, inclusive);
5260
+ }
5261
+ /**
5262
+ * Returns the value to use as the lower bound for ascending index segment at
5263
+ * the provided `fieldPath` (or the upper bound for an descending segment).
5264
+ */
5265
+ function targetGetAscendingBound(target, fieldPath, bound) {
5266
+ let value = MIN_VALUE;
5267
+ let inclusive = true;
5268
+ // Process all filters to find a value for the current field segment
5269
+ for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
5270
+ let filterValue = MIN_VALUE;
5271
+ let filterInclusive = true;
5272
+ switch (fieldFilter.op) {
5273
+ case "<" /* LESS_THAN */:
5274
+ case "<=" /* LESS_THAN_OR_EQUAL */:
5275
+ filterValue = valuesGetLowerBound(fieldFilter.value);
5276
+ break;
5277
+ case "==" /* EQUAL */:
5278
+ case "in" /* IN */:
5279
+ case ">=" /* GREATER_THAN_OR_EQUAL */:
5280
+ filterValue = fieldFilter.value;
5281
+ break;
5282
+ case ">" /* GREATER_THAN */:
5283
+ filterValue = fieldFilter.value;
5284
+ filterInclusive = false;
5285
+ break;
5286
+ case "!=" /* NOT_EQUAL */:
5287
+ case "not-in" /* NOT_IN */:
5288
+ filterValue = MIN_VALUE;
5289
+ break;
5290
+ // Remaining filters cannot be used as lower bounds.
5291
+ }
5292
+ if (lowerBoundCompare({ value, inclusive }, { value: filterValue, inclusive: filterInclusive }) < 0) {
5293
+ value = filterValue;
5294
+ inclusive = filterInclusive;
5295
+ }
5296
+ }
5297
+ // If there is an additional bound, compare the values against the existing
5298
+ // range to see if we can narrow the scope.
5299
+ if (bound !== null) {
5300
+ for (let i = 0; i < target.orderBy.length; ++i) {
5301
+ const orderBy = target.orderBy[i];
5302
+ if (orderBy.field.isEqual(fieldPath)) {
5303
+ const cursorValue = bound.position[i];
5304
+ if (lowerBoundCompare({ value, inclusive }, { value: cursorValue, inclusive: bound.inclusive }) < 0) {
5305
+ value = cursorValue;
5306
+ inclusive = bound.inclusive;
5307
+ }
5308
+ break;
5447
5309
  }
5448
5310
  }
5449
- return null;
5450
5311
  }
5451
- // Returns the index of the element in this sorted map, or -1 if it doesn't
5452
- // exist.
5453
- indexOf(key) {
5454
- // Number of nodes that were pruned when descending right
5455
- let prunedNodes = 0;
5456
- let node = this.root;
5457
- while (!node.isEmpty()) {
5458
- const cmp = this.comparator(key, node.key);
5459
- if (cmp === 0) {
5460
- return prunedNodes + node.left.size;
5312
+ return { value, inclusive };
5313
+ }
5314
+ /**
5315
+ * Returns the value to use as the upper bound for ascending index segment at
5316
+ * the provided `fieldPath` (or the lower bound for a descending segment).
5317
+ */
5318
+ function targetGetDescendingBound(target, fieldPath, bound) {
5319
+ let value = MAX_VALUE;
5320
+ let inclusive = true;
5321
+ // Process all filters to find a value for the current field segment
5322
+ for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
5323
+ let filterValue = MAX_VALUE;
5324
+ let filterInclusive = true;
5325
+ switch (fieldFilter.op) {
5326
+ case ">=" /* GREATER_THAN_OR_EQUAL */:
5327
+ case ">" /* GREATER_THAN */:
5328
+ filterValue = valuesGetUpperBound(fieldFilter.value);
5329
+ filterInclusive = false;
5330
+ break;
5331
+ case "==" /* EQUAL */:
5332
+ case "in" /* IN */:
5333
+ case "<=" /* LESS_THAN_OR_EQUAL */:
5334
+ filterValue = fieldFilter.value;
5335
+ break;
5336
+ case "<" /* LESS_THAN */:
5337
+ filterValue = fieldFilter.value;
5338
+ filterInclusive = false;
5339
+ break;
5340
+ case "!=" /* NOT_EQUAL */:
5341
+ case "not-in" /* NOT_IN */:
5342
+ filterValue = MAX_VALUE;
5343
+ break;
5344
+ // Remaining filters cannot be used as upper bounds.
5345
+ }
5346
+ if (upperBoundCompare({ value, inclusive }, { value: filterValue, inclusive: filterInclusive }) > 0) {
5347
+ value = filterValue;
5348
+ inclusive = filterInclusive;
5349
+ }
5350
+ }
5351
+ // If there is an additional bound, compare the values against the existing
5352
+ // range to see if we can narrow the scope.
5353
+ if (bound !== null) {
5354
+ for (let i = 0; i < target.orderBy.length; ++i) {
5355
+ const orderBy = target.orderBy[i];
5356
+ if (orderBy.field.isEqual(fieldPath)) {
5357
+ const cursorValue = bound.position[i];
5358
+ if (upperBoundCompare({ value, inclusive }, { value: cursorValue, inclusive: bound.inclusive }) > 0) {
5359
+ value = cursorValue;
5360
+ inclusive = bound.inclusive;
5361
+ }
5362
+ break;
5461
5363
  }
5462
- else if (cmp < 0) {
5463
- node = node.left;
5364
+ }
5365
+ }
5366
+ return { value, inclusive };
5367
+ }
5368
+ /** Returns the number of segments of a perfect index for this target. */
5369
+ function targetGetSegmentCount(target) {
5370
+ let fields = new SortedSet(FieldPath$1.comparator);
5371
+ let hasArraySegment = false;
5372
+ for (const filter of target.filters) {
5373
+ // TODO(orquery): Use the flattened filters here
5374
+ const fieldFilter = filter;
5375
+ // __name__ is not an explicit segment of any index, so we don't need to
5376
+ // count it.
5377
+ if (fieldFilter.field.isKeyField()) {
5378
+ continue;
5379
+ }
5380
+ // ARRAY_CONTAINS or ARRAY_CONTAINS_ANY filters must be counted separately.
5381
+ // For instance, it is possible to have an index for "a ARRAY a ASC". Even
5382
+ // though these are on the same field, they should be counted as two
5383
+ // separate segments in an index.
5384
+ if (fieldFilter.op === "array-contains" /* ARRAY_CONTAINS */ ||
5385
+ fieldFilter.op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
5386
+ hasArraySegment = true;
5387
+ }
5388
+ else {
5389
+ fields = fields.add(fieldFilter.field);
5390
+ }
5391
+ }
5392
+ for (const orderBy of target.orderBy) {
5393
+ // __name__ is not an explicit segment of any index, so we don't need to
5394
+ // count it.
5395
+ if (!orderBy.field.isKeyField()) {
5396
+ fields = fields.add(orderBy.field);
5397
+ }
5398
+ }
5399
+ return fields.size + (hasArraySegment ? 1 : 0);
5400
+ }
5401
+ class Filter {
5402
+ }
5403
+ class FieldFilter extends Filter {
5404
+ constructor(field, op, value) {
5405
+ super();
5406
+ this.field = field;
5407
+ this.op = op;
5408
+ this.value = value;
5409
+ }
5410
+ /**
5411
+ * Creates a filter based on the provided arguments.
5412
+ */
5413
+ static create(field, op, value) {
5414
+ if (field.isKeyField()) {
5415
+ if (op === "in" /* IN */ || op === "not-in" /* NOT_IN */) {
5416
+ return this.createKeyFieldInFilter(field, op, value);
5464
5417
  }
5465
5418
  else {
5466
- // Count all nodes left of the node plus the node itself
5467
- prunedNodes += node.left.size + 1;
5468
- node = node.right;
5419
+ return new KeyFieldFilter(field, op, value);
5469
5420
  }
5470
5421
  }
5471
- // Node not found
5472
- return -1;
5422
+ else if (op === "array-contains" /* ARRAY_CONTAINS */) {
5423
+ return new ArrayContainsFilter(field, value);
5424
+ }
5425
+ else if (op === "in" /* IN */) {
5426
+ return new InFilter(field, value);
5427
+ }
5428
+ else if (op === "not-in" /* NOT_IN */) {
5429
+ return new NotInFilter(field, value);
5430
+ }
5431
+ else if (op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
5432
+ return new ArrayContainsAnyFilter(field, value);
5433
+ }
5434
+ else {
5435
+ return new FieldFilter(field, op, value);
5436
+ }
5473
5437
  }
5474
- isEmpty() {
5475
- return this.root.isEmpty();
5438
+ static createKeyFieldInFilter(field, op, value) {
5439
+ return op === "in" /* IN */
5440
+ ? new KeyFieldInFilter(field, value)
5441
+ : new KeyFieldNotInFilter(field, value);
5476
5442
  }
5477
- // Returns the total number of nodes in the map.
5478
- get size() {
5479
- return this.root.size;
5443
+ matches(doc) {
5444
+ const other = doc.data.field(this.field);
5445
+ // Types do not have to match in NOT_EQUAL filters.
5446
+ if (this.op === "!=" /* NOT_EQUAL */) {
5447
+ return (other !== null &&
5448
+ this.matchesComparison(valueCompare(other, this.value)));
5449
+ }
5450
+ // Only compare types with matching backend order (such as double and int).
5451
+ return (other !== null &&
5452
+ typeOrder(this.value) === typeOrder(other) &&
5453
+ this.matchesComparison(valueCompare(other, this.value)));
5480
5454
  }
5481
- // Returns the minimum key in the map.
5482
- minKey() {
5483
- return this.root.minKey();
5455
+ matchesComparison(comparison) {
5456
+ switch (this.op) {
5457
+ case "<" /* LESS_THAN */:
5458
+ return comparison < 0;
5459
+ case "<=" /* LESS_THAN_OR_EQUAL */:
5460
+ return comparison <= 0;
5461
+ case "==" /* EQUAL */:
5462
+ return comparison === 0;
5463
+ case "!=" /* NOT_EQUAL */:
5464
+ return comparison !== 0;
5465
+ case ">" /* GREATER_THAN */:
5466
+ return comparison > 0;
5467
+ case ">=" /* GREATER_THAN_OR_EQUAL */:
5468
+ return comparison >= 0;
5469
+ default:
5470
+ return fail();
5471
+ }
5484
5472
  }
5485
- // Returns the maximum key in the map.
5486
- maxKey() {
5487
- return this.root.maxKey();
5473
+ isInequality() {
5474
+ return ([
5475
+ "<" /* LESS_THAN */,
5476
+ "<=" /* LESS_THAN_OR_EQUAL */,
5477
+ ">" /* GREATER_THAN */,
5478
+ ">=" /* GREATER_THAN_OR_EQUAL */,
5479
+ "!=" /* NOT_EQUAL */,
5480
+ "not-in" /* NOT_IN */
5481
+ ].indexOf(this.op) >= 0);
5488
5482
  }
5489
- // Traverses the map in key order and calls the specified action function
5490
- // for each key/value pair. If action returns true, traversal is aborted.
5491
- // Returns the first truthy value returned by action, or the last falsey
5492
- // value returned by action.
5493
- inorderTraversal(action) {
5494
- return this.root.inorderTraversal(action);
5483
+ }
5484
+ function canonifyFilter(filter) {
5485
+ // TODO(b/29183165): Technically, this won't be unique if two values have
5486
+ // the same description, such as the int 3 and the string "3". So we should
5487
+ // add the types in here somehow, too.
5488
+ return (filter.field.canonicalString() +
5489
+ filter.op.toString() +
5490
+ canonicalId(filter.value));
5491
+ }
5492
+ function filterEquals(f1, f2) {
5493
+ return (f1.op === f2.op &&
5494
+ f1.field.isEqual(f2.field) &&
5495
+ valueEquals(f1.value, f2.value));
5496
+ }
5497
+ /** Returns a debug description for `filter`. */
5498
+ function stringifyFilter(filter) {
5499
+ return `${filter.field.canonicalString()} ${filter.op} ${canonicalId(filter.value)}`;
5500
+ }
5501
+ /** Filter that matches on key fields (i.e. '__name__'). */
5502
+ class KeyFieldFilter extends FieldFilter {
5503
+ constructor(field, op, value) {
5504
+ super(field, op, value);
5505
+ this.key = DocumentKey.fromName(value.referenceValue);
5495
5506
  }
5496
- forEach(fn) {
5497
- this.inorderTraversal((k, v) => {
5498
- fn(k, v);
5499
- return false;
5500
- });
5507
+ matches(doc) {
5508
+ const comparison = DocumentKey.comparator(doc.key, this.key);
5509
+ return this.matchesComparison(comparison);
5501
5510
  }
5502
- toString() {
5503
- const descriptions = [];
5504
- this.inorderTraversal((k, v) => {
5505
- descriptions.push(`${k}:${v}`);
5506
- return false;
5507
- });
5508
- return `{${descriptions.join(', ')}}`;
5511
+ }
5512
+ /** Filter that matches on key fields within an array. */
5513
+ class KeyFieldInFilter extends FieldFilter {
5514
+ constructor(field, value) {
5515
+ super(field, "in" /* IN */, value);
5516
+ this.keys = extractDocumentKeysFromArrayValue("in" /* IN */, value);
5509
5517
  }
5510
- // Traverses the map in reverse key order and calls the specified action
5511
- // function for each key/value pair. If action returns true, traversal is
5512
- // aborted.
5513
- // Returns the first truthy value returned by action, or the last falsey
5514
- // value returned by action.
5515
- reverseTraversal(action) {
5516
- return this.root.reverseTraversal(action);
5518
+ matches(doc) {
5519
+ return this.keys.some(key => key.isEqual(doc.key));
5520
+ }
5521
+ }
5522
+ /** Filter that matches on key fields not present within an array. */
5523
+ class KeyFieldNotInFilter extends FieldFilter {
5524
+ constructor(field, value) {
5525
+ super(field, "not-in" /* NOT_IN */, value);
5526
+ this.keys = extractDocumentKeysFromArrayValue("not-in" /* NOT_IN */, value);
5527
+ }
5528
+ matches(doc) {
5529
+ return !this.keys.some(key => key.isEqual(doc.key));
5530
+ }
5531
+ }
5532
+ function extractDocumentKeysFromArrayValue(op, value) {
5533
+ var _a;
5534
+ return (((_a = value.arrayValue) === null || _a === void 0 ? void 0 : _a.values) || []).map(v => {
5535
+ return DocumentKey.fromName(v.referenceValue);
5536
+ });
5537
+ }
5538
+ /** A Filter that implements the array-contains operator. */
5539
+ class ArrayContainsFilter extends FieldFilter {
5540
+ constructor(field, value) {
5541
+ super(field, "array-contains" /* ARRAY_CONTAINS */, value);
5542
+ }
5543
+ matches(doc) {
5544
+ const other = doc.data.field(this.field);
5545
+ return isArray(other) && arrayValueContains(other.arrayValue, this.value);
5546
+ }
5547
+ }
5548
+ /** A Filter that implements the IN operator. */
5549
+ class InFilter extends FieldFilter {
5550
+ constructor(field, value) {
5551
+ super(field, "in" /* IN */, value);
5517
5552
  }
5518
- // Returns an iterator over the SortedMap.
5519
- getIterator() {
5520
- return new SortedMapIterator(this.root, null, this.comparator, false);
5553
+ matches(doc) {
5554
+ const other = doc.data.field(this.field);
5555
+ return other !== null && arrayValueContains(this.value.arrayValue, other);
5521
5556
  }
5522
- getIteratorFrom(key) {
5523
- return new SortedMapIterator(this.root, key, this.comparator, false);
5557
+ }
5558
+ /** A Filter that implements the not-in operator. */
5559
+ class NotInFilter extends FieldFilter {
5560
+ constructor(field, value) {
5561
+ super(field, "not-in" /* NOT_IN */, value);
5524
5562
  }
5525
- getReverseIterator() {
5526
- return new SortedMapIterator(this.root, null, this.comparator, true);
5563
+ matches(doc) {
5564
+ if (arrayValueContains(this.value.arrayValue, { nullValue: 'NULL_VALUE' })) {
5565
+ return false;
5566
+ }
5567
+ const other = doc.data.field(this.field);
5568
+ return other !== null && !arrayValueContains(this.value.arrayValue, other);
5527
5569
  }
5528
- getReverseIteratorFrom(key) {
5529
- return new SortedMapIterator(this.root, key, this.comparator, true);
5570
+ }
5571
+ /** A Filter that implements the array-contains-any operator. */
5572
+ class ArrayContainsAnyFilter extends FieldFilter {
5573
+ constructor(field, value) {
5574
+ super(field, "array-contains-any" /* ARRAY_CONTAINS_ANY */, value);
5530
5575
  }
5531
- } // end SortedMap
5532
- // An iterator over an LLRBNode.
5533
- class SortedMapIterator {
5534
- constructor(node, startKey, comparator, isReverse) {
5535
- this.isReverse = isReverse;
5536
- this.nodeStack = [];
5537
- let cmp = 1;
5538
- while (!node.isEmpty()) {
5539
- cmp = startKey ? comparator(node.key, startKey) : 1;
5540
- // flip the comparison if we're going in reverse
5541
- if (startKey && isReverse) {
5542
- cmp *= -1;
5543
- }
5544
- if (cmp < 0) {
5545
- // This node is less than our start key. ignore it
5546
- if (this.isReverse) {
5547
- node = node.left;
5548
- }
5549
- else {
5550
- node = node.right;
5551
- }
5552
- }
5553
- else if (cmp === 0) {
5554
- // This node is exactly equal to our start key. Push it on the stack,
5555
- // but stop iterating;
5556
- this.nodeStack.push(node);
5557
- break;
5558
- }
5559
- else {
5560
- // This node is greater than our start key, add it to the stack and move
5561
- // to the next one
5562
- this.nodeStack.push(node);
5563
- if (this.isReverse) {
5564
- node = node.right;
5565
- }
5566
- else {
5567
- node = node.left;
5568
- }
5569
- }
5576
+ matches(doc) {
5577
+ const other = doc.data.field(this.field);
5578
+ if (!isArray(other) || !other.arrayValue.values) {
5579
+ return false;
5570
5580
  }
5581
+ return other.arrayValue.values.some(val => arrayValueContains(this.value.arrayValue, val));
5571
5582
  }
5572
- getNext() {
5573
- let node = this.nodeStack.pop();
5574
- const result = { key: node.key, value: node.value };
5575
- if (this.isReverse) {
5576
- node = node.left;
5577
- while (!node.isEmpty()) {
5578
- this.nodeStack.push(node);
5579
- node = node.right;
5580
- }
5583
+ }
5584
+ /**
5585
+ * Represents a bound of a query.
5586
+ *
5587
+ * The bound is specified with the given components representing a position and
5588
+ * whether it's just before or just after the position (relative to whatever the
5589
+ * query order is).
5590
+ *
5591
+ * The position represents a logical index position for a query. It's a prefix
5592
+ * of values for the (potentially implicit) order by clauses of a query.
5593
+ *
5594
+ * Bound provides a function to determine whether a document comes before or
5595
+ * after a bound. This is influenced by whether the position is just before or
5596
+ * just after the provided values.
5597
+ */
5598
+ class Bound {
5599
+ constructor(position, inclusive) {
5600
+ this.position = position;
5601
+ this.inclusive = inclusive;
5602
+ }
5603
+ }
5604
+ /**
5605
+ * An ordering on a field, in some Direction. Direction defaults to ASCENDING.
5606
+ */
5607
+ class OrderBy {
5608
+ constructor(field, dir = "asc" /* ASCENDING */) {
5609
+ this.field = field;
5610
+ this.dir = dir;
5611
+ }
5612
+ }
5613
+ function canonifyOrderBy(orderBy) {
5614
+ // TODO(b/29183165): Make this collision robust.
5615
+ return orderBy.field.canonicalString() + orderBy.dir;
5616
+ }
5617
+ function stringifyOrderBy(orderBy) {
5618
+ return `${orderBy.field.canonicalString()} (${orderBy.dir})`;
5619
+ }
5620
+ function orderByEquals(left, right) {
5621
+ return left.dir === right.dir && left.field.isEqual(right.field);
5622
+ }
5623
+ function boundCompareToDocument(bound, orderBy, doc) {
5624
+ let comparison = 0;
5625
+ for (let i = 0; i < bound.position.length; i++) {
5626
+ const orderByComponent = orderBy[i];
5627
+ const component = bound.position[i];
5628
+ if (orderByComponent.field.isKeyField()) {
5629
+ comparison = DocumentKey.comparator(DocumentKey.fromName(component.referenceValue), doc.key);
5581
5630
  }
5582
5631
  else {
5583
- node = node.right;
5584
- while (!node.isEmpty()) {
5585
- this.nodeStack.push(node);
5586
- node = node.left;
5587
- }
5632
+ const docValue = doc.data.field(orderByComponent.field);
5633
+ comparison = valueCompare(component, docValue);
5588
5634
  }
5589
- return result;
5590
- }
5591
- hasNext() {
5592
- return this.nodeStack.length > 0;
5593
- }
5594
- peek() {
5595
- if (this.nodeStack.length === 0) {
5596
- return null;
5635
+ if (orderByComponent.dir === "desc" /* DESCENDING */) {
5636
+ comparison = comparison * -1;
5637
+ }
5638
+ if (comparison !== 0) {
5639
+ break;
5597
5640
  }
5598
- const node = this.nodeStack[this.nodeStack.length - 1];
5599
- return { key: node.key, value: node.value };
5600
- }
5601
- } // end SortedMapIterator
5602
- // Represents a node in a Left-leaning Red-Black tree.
5603
- class LLRBNode {
5604
- constructor(key, value, color, left, right) {
5605
- this.key = key;
5606
- this.value = value;
5607
- this.color = color != null ? color : LLRBNode.RED;
5608
- this.left = left != null ? left : LLRBNode.EMPTY;
5609
- this.right = right != null ? right : LLRBNode.EMPTY;
5610
- this.size = this.left.size + 1 + this.right.size;
5611
5641
  }
5612
- // Returns a copy of the current node, optionally replacing pieces of it.
5613
- copy(key, value, color, left, right) {
5614
- return new LLRBNode(key != null ? key : this.key, value != null ? value : this.value, color != null ? color : this.color, left != null ? left : this.left, right != null ? right : this.right);
5642
+ return comparison;
5643
+ }
5644
+ /**
5645
+ * Returns true if a document sorts after a bound using the provided sort
5646
+ * order.
5647
+ */
5648
+ function boundSortsAfterDocument(bound, orderBy, doc) {
5649
+ const comparison = boundCompareToDocument(bound, orderBy, doc);
5650
+ return bound.inclusive ? comparison >= 0 : comparison > 0;
5651
+ }
5652
+ /**
5653
+ * Returns true if a document sorts before a bound using the provided sort
5654
+ * order.
5655
+ */
5656
+ function boundSortsBeforeDocument(bound, orderBy, doc) {
5657
+ const comparison = boundCompareToDocument(bound, orderBy, doc);
5658
+ return bound.inclusive ? comparison <= 0 : comparison < 0;
5659
+ }
5660
+ function boundEquals(left, right) {
5661
+ if (left === null) {
5662
+ return right === null;
5615
5663
  }
5616
- isEmpty() {
5664
+ else if (right === null) {
5617
5665
  return false;
5618
5666
  }
5619
- // Traverses the tree in key order and calls the specified action function
5620
- // for each node. If action returns true, traversal is aborted.
5621
- // Returns the first truthy value returned by action, or the last falsey
5622
- // value returned by action.
5623
- inorderTraversal(action) {
5624
- return (this.left.inorderTraversal(action) ||
5625
- action(this.key, this.value) ||
5626
- this.right.inorderTraversal(action));
5627
- }
5628
- // Traverses the tree in reverse key order and calls the specified action
5629
- // function for each node. If action returns true, traversal is aborted.
5630
- // Returns the first truthy value returned by action, or the last falsey
5631
- // value returned by action.
5632
- reverseTraversal(action) {
5633
- return (this.right.reverseTraversal(action) ||
5634
- action(this.key, this.value) ||
5635
- this.left.reverseTraversal(action));
5667
+ if (left.inclusive !== right.inclusive ||
5668
+ left.position.length !== right.position.length) {
5669
+ return false;
5636
5670
  }
5637
- // Returns the minimum node in the tree.
5638
- min() {
5639
- if (this.left.isEmpty()) {
5640
- return this;
5641
- }
5642
- else {
5643
- return this.left.min();
5671
+ for (let i = 0; i < left.position.length; i++) {
5672
+ const leftPosition = left.position[i];
5673
+ const rightPosition = right.position[i];
5674
+ if (!valueEquals(leftPosition, rightPosition)) {
5675
+ return false;
5644
5676
  }
5645
5677
  }
5646
- // Returns the maximum key in the tree.
5647
- minKey() {
5648
- return this.min().key;
5649
- }
5650
- // Returns the maximum key in the tree.
5651
- maxKey() {
5652
- if (this.right.isEmpty()) {
5653
- return this.key;
5654
- }
5655
- else {
5656
- return this.right.maxKey();
5657
- }
5678
+ return true;
5679
+ }
5680
+
5681
+ /**
5682
+ * @license
5683
+ * Copyright 2017 Google LLC
5684
+ *
5685
+ * Licensed under the Apache License, Version 2.0 (the "License");
5686
+ * you may not use this file except in compliance with the License.
5687
+ * You may obtain a copy of the License at
5688
+ *
5689
+ * http://www.apache.org/licenses/LICENSE-2.0
5690
+ *
5691
+ * Unless required by applicable law or agreed to in writing, software
5692
+ * distributed under the License is distributed on an "AS IS" BASIS,
5693
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5694
+ * See the License for the specific language governing permissions and
5695
+ * limitations under the License.
5696
+ */
5697
+ /**
5698
+ * Query encapsulates all the query attributes we support in the SDK. It can
5699
+ * be run against the LocalStore, as well as be converted to a `Target` to
5700
+ * query the RemoteStore results.
5701
+ *
5702
+ * Visible for testing.
5703
+ */
5704
+ class QueryImpl {
5705
+ /**
5706
+ * Initializes a Query with a path and optional additional query constraints.
5707
+ * Path must currently be empty if this is a collection group query.
5708
+ */
5709
+ constructor(path, collectionGroup = null, explicitOrderBy = [], filters = [], limit = null, limitType = "F" /* First */, startAt = null, endAt = null) {
5710
+ this.path = path;
5711
+ this.collectionGroup = collectionGroup;
5712
+ this.explicitOrderBy = explicitOrderBy;
5713
+ this.filters = filters;
5714
+ this.limit = limit;
5715
+ this.limitType = limitType;
5716
+ this.startAt = startAt;
5717
+ this.endAt = endAt;
5718
+ this.memoizedOrderBy = null;
5719
+ // The corresponding `Target` of this `Query` instance.
5720
+ this.memoizedTarget = null;
5721
+ if (this.startAt) ;
5722
+ if (this.endAt) ;
5658
5723
  }
5659
- // Returns new tree, with the key/value added.
5660
- insert(key, value, comparator) {
5661
- let n = this;
5662
- const cmp = comparator(key, n.key);
5663
- if (cmp < 0) {
5664
- n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
5665
- }
5666
- else if (cmp === 0) {
5667
- n = n.copy(null, value, null, null, null);
5668
- }
5669
- else {
5670
- n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
5724
+ }
5725
+ /** Creates a new Query instance with the options provided. */
5726
+ function newQuery(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt) {
5727
+ return new QueryImpl(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt);
5728
+ }
5729
+ /** Creates a new Query for a query that matches all documents at `path` */
5730
+ function newQueryForPath(path) {
5731
+ return new QueryImpl(path);
5732
+ }
5733
+ /**
5734
+ * Helper to convert a collection group query into a collection query at a
5735
+ * specific path. This is used when executing collection group queries, since
5736
+ * we have to split the query into a set of collection queries at multiple
5737
+ * paths.
5738
+ */
5739
+ function asCollectionQueryAtPath(query, path) {
5740
+ return new QueryImpl(path,
5741
+ /*collectionGroup=*/ null, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
5742
+ }
5743
+ /**
5744
+ * Returns true if this query does not specify any query constraints that
5745
+ * could remove results.
5746
+ */
5747
+ function queryMatchesAllDocuments(query) {
5748
+ return (query.filters.length === 0 &&
5749
+ query.limit === null &&
5750
+ query.startAt == null &&
5751
+ query.endAt == null &&
5752
+ (query.explicitOrderBy.length === 0 ||
5753
+ (query.explicitOrderBy.length === 1 &&
5754
+ query.explicitOrderBy[0].field.isKeyField())));
5755
+ }
5756
+ function getFirstOrderByField(query) {
5757
+ return query.explicitOrderBy.length > 0
5758
+ ? query.explicitOrderBy[0].field
5759
+ : null;
5760
+ }
5761
+ function getInequalityFilterField(query) {
5762
+ for (const filter of query.filters) {
5763
+ if (filter.isInequality()) {
5764
+ return filter.field;
5671
5765
  }
5672
- return n.fixUp();
5673
5766
  }
5674
- removeMin() {
5675
- if (this.left.isEmpty()) {
5676
- return LLRBNode.EMPTY;
5677
- }
5678
- let n = this;
5679
- if (!n.left.isRed() && !n.left.left.isRed()) {
5680
- n = n.moveRedLeft();
5767
+ return null;
5768
+ }
5769
+ /**
5770
+ * Checks if any of the provided Operators are included in the query and
5771
+ * returns the first one that is, or null if none are.
5772
+ */
5773
+ function findFilterOperator(query, operators) {
5774
+ for (const filter of query.filters) {
5775
+ if (operators.indexOf(filter.op) >= 0) {
5776
+ return filter.op;
5681
5777
  }
5682
- n = n.copy(null, null, null, n.left.removeMin(), null);
5683
- return n.fixUp();
5684
5778
  }
5685
- // Returns new tree, with the specified item removed.
5686
- remove(key, comparator) {
5687
- let smallest;
5688
- let n = this;
5689
- if (comparator(key, n.key) < 0) {
5690
- if (!n.left.isEmpty() && !n.left.isRed() && !n.left.left.isRed()) {
5691
- n = n.moveRedLeft();
5779
+ return null;
5780
+ }
5781
+ /**
5782
+ * Creates a new Query for a collection group query that matches all documents
5783
+ * within the provided collection group.
5784
+ */
5785
+ function newQueryForCollectionGroup(collectionId) {
5786
+ return new QueryImpl(ResourcePath.emptyPath(), collectionId);
5787
+ }
5788
+ /**
5789
+ * Returns whether the query matches a single document by path (rather than a
5790
+ * collection).
5791
+ */
5792
+ function isDocumentQuery$1(query) {
5793
+ return (DocumentKey.isDocumentKey(query.path) &&
5794
+ query.collectionGroup === null &&
5795
+ query.filters.length === 0);
5796
+ }
5797
+ /**
5798
+ * Returns whether the query matches a collection group rather than a specific
5799
+ * collection.
5800
+ */
5801
+ function isCollectionGroupQuery(query) {
5802
+ return query.collectionGroup !== null;
5803
+ }
5804
+ /**
5805
+ * Returns the implicit order by constraint that is used to execute the Query,
5806
+ * which can be different from the order by constraints the user provided (e.g.
5807
+ * the SDK and backend always orders by `__name__`).
5808
+ */
5809
+ function queryOrderBy(query) {
5810
+ const queryImpl = debugCast(query);
5811
+ if (queryImpl.memoizedOrderBy === null) {
5812
+ queryImpl.memoizedOrderBy = [];
5813
+ const inequalityField = getInequalityFilterField(queryImpl);
5814
+ const firstOrderByField = getFirstOrderByField(queryImpl);
5815
+ if (inequalityField !== null && firstOrderByField === null) {
5816
+ // In order to implicitly add key ordering, we must also add the
5817
+ // inequality filter field for it to be a valid query.
5818
+ // Note that the default inequality field and key ordering is ascending.
5819
+ if (!inequalityField.isKeyField()) {
5820
+ queryImpl.memoizedOrderBy.push(new OrderBy(inequalityField));
5692
5821
  }
5693
- n = n.copy(null, null, null, n.left.remove(key, comparator), null);
5822
+ queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), "asc" /* ASCENDING */));
5694
5823
  }
5695
5824
  else {
5696
- if (n.left.isRed()) {
5697
- n = n.rotateRight();
5698
- }
5699
- if (!n.right.isEmpty() && !n.right.isRed() && !n.right.left.isRed()) {
5700
- n = n.moveRedRight();
5701
- }
5702
- if (comparator(key, n.key) === 0) {
5703
- if (n.right.isEmpty()) {
5704
- return LLRBNode.EMPTY;
5705
- }
5706
- else {
5707
- smallest = n.right.min();
5708
- n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin());
5825
+ let foundKeyOrdering = false;
5826
+ for (const orderBy of queryImpl.explicitOrderBy) {
5827
+ queryImpl.memoizedOrderBy.push(orderBy);
5828
+ if (orderBy.field.isKeyField()) {
5829
+ foundKeyOrdering = true;
5709
5830
  }
5710
5831
  }
5711
- n = n.copy(null, null, null, null, n.right.remove(key, comparator));
5712
- }
5713
- return n.fixUp();
5714
- }
5715
- isRed() {
5716
- return this.color;
5717
- }
5718
- // Returns new tree after performing any needed rotations.
5719
- fixUp() {
5720
- let n = this;
5721
- if (n.right.isRed() && !n.left.isRed()) {
5722
- n = n.rotateLeft();
5723
- }
5724
- if (n.left.isRed() && n.left.left.isRed()) {
5725
- n = n.rotateRight();
5726
- }
5727
- if (n.left.isRed() && n.right.isRed()) {
5728
- n = n.colorFlip();
5832
+ if (!foundKeyOrdering) {
5833
+ // The order of the implicit key ordering always matches the last
5834
+ // explicit order by
5835
+ const lastDirection = queryImpl.explicitOrderBy.length > 0
5836
+ ? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1]
5837
+ .dir
5838
+ : "asc" /* ASCENDING */;
5839
+ queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), lastDirection));
5840
+ }
5729
5841
  }
5730
- return n;
5731
5842
  }
5732
- moveRedLeft() {
5733
- let n = this.colorFlip();
5734
- if (n.right.left.isRed()) {
5735
- n = n.copy(null, null, null, null, n.right.rotateRight());
5736
- n = n.rotateLeft();
5737
- n = n.colorFlip();
5843
+ return queryImpl.memoizedOrderBy;
5844
+ }
5845
+ /**
5846
+ * Converts this `Query` instance to it's corresponding `Target` representation.
5847
+ */
5848
+ function queryToTarget(query) {
5849
+ const queryImpl = debugCast(query);
5850
+ if (!queryImpl.memoizedTarget) {
5851
+ if (queryImpl.limitType === "F" /* First */) {
5852
+ queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, queryOrderBy(queryImpl), queryImpl.filters, queryImpl.limit, queryImpl.startAt, queryImpl.endAt);
5738
5853
  }
5739
- return n;
5740
- }
5741
- moveRedRight() {
5742
- let n = this.colorFlip();
5743
- if (n.left.left.isRed()) {
5744
- n = n.rotateRight();
5745
- n = n.colorFlip();
5854
+ else {
5855
+ // Flip the orderBy directions since we want the last results
5856
+ const orderBys = [];
5857
+ for (const orderBy of queryOrderBy(queryImpl)) {
5858
+ const dir = orderBy.dir === "desc" /* DESCENDING */
5859
+ ? "asc" /* ASCENDING */
5860
+ : "desc" /* DESCENDING */;
5861
+ orderBys.push(new OrderBy(orderBy.field, dir));
5862
+ }
5863
+ // We need to swap the cursors to match the now-flipped query ordering.
5864
+ const startAt = queryImpl.endAt
5865
+ ? new Bound(queryImpl.endAt.position, queryImpl.endAt.inclusive)
5866
+ : null;
5867
+ const endAt = queryImpl.startAt
5868
+ ? new Bound(queryImpl.startAt.position, queryImpl.startAt.inclusive)
5869
+ : null;
5870
+ // Now return as a LimitType.First query.
5871
+ queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, orderBys, queryImpl.filters, queryImpl.limit, startAt, endAt);
5746
5872
  }
5747
- return n;
5748
5873
  }
5749
- rotateLeft() {
5750
- const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
5751
- return this.right.copy(null, null, this.color, nl, null);
5874
+ return queryImpl.memoizedTarget;
5875
+ }
5876
+ function queryWithAddedFilter(query, filter) {
5877
+ const newFilters = query.filters.concat([filter]);
5878
+ return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), newFilters, query.limit, query.limitType, query.startAt, query.endAt);
5879
+ }
5880
+ function queryWithAddedOrderBy(query, orderBy) {
5881
+ // TODO(dimond): validate that orderBy does not list the same key twice.
5882
+ const newOrderBy = query.explicitOrderBy.concat([orderBy]);
5883
+ return new QueryImpl(query.path, query.collectionGroup, newOrderBy, query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
5884
+ }
5885
+ function queryWithLimit(query, limit, limitType) {
5886
+ return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), limit, limitType, query.startAt, query.endAt);
5887
+ }
5888
+ function queryWithStartAt(query, bound) {
5889
+ return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, bound, query.endAt);
5890
+ }
5891
+ function queryWithEndAt(query, bound) {
5892
+ return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, bound);
5893
+ }
5894
+ function queryEquals(left, right) {
5895
+ return (targetEquals(queryToTarget(left), queryToTarget(right)) &&
5896
+ left.limitType === right.limitType);
5897
+ }
5898
+ // TODO(b/29183165): This is used to get a unique string from a query to, for
5899
+ // example, use as a dictionary key, but the implementation is subject to
5900
+ // collisions. Make it collision-free.
5901
+ function canonifyQuery(query) {
5902
+ return `${canonifyTarget(queryToTarget(query))}|lt:${query.limitType}`;
5903
+ }
5904
+ function stringifyQuery(query) {
5905
+ return `Query(target=${stringifyTarget(queryToTarget(query))}; limitType=${query.limitType})`;
5906
+ }
5907
+ /** Returns whether `doc` matches the constraints of `query`. */
5908
+ function queryMatches(query, doc) {
5909
+ return (doc.isFoundDocument() &&
5910
+ queryMatchesPathAndCollectionGroup(query, doc) &&
5911
+ queryMatchesOrderBy(query, doc) &&
5912
+ queryMatchesFilters(query, doc) &&
5913
+ queryMatchesBounds(query, doc));
5914
+ }
5915
+ function queryMatchesPathAndCollectionGroup(query, doc) {
5916
+ const docPath = doc.key.path;
5917
+ if (query.collectionGroup !== null) {
5918
+ // NOTE: this.path is currently always empty since we don't expose Collection
5919
+ // Group queries rooted at a document path yet.
5920
+ return (doc.key.hasCollectionId(query.collectionGroup) &&
5921
+ query.path.isPrefixOf(docPath));
5752
5922
  }
5753
- rotateRight() {
5754
- const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
5755
- return this.left.copy(null, null, this.color, null, nr);
5923
+ else if (DocumentKey.isDocumentKey(query.path)) {
5924
+ // exact match for document queries
5925
+ return query.path.isEqual(docPath);
5756
5926
  }
5757
- colorFlip() {
5758
- const left = this.left.copy(null, null, !this.left.color, null, null);
5759
- const right = this.right.copy(null, null, !this.right.color, null, null);
5760
- return this.copy(null, null, !this.color, left, right);
5927
+ else {
5928
+ // shallow ancestor queries by default
5929
+ return query.path.isImmediateParentOf(docPath);
5761
5930
  }
5762
- // For testing.
5763
- checkMaxDepth() {
5764
- const blackDepth = this.check();
5765
- if (Math.pow(2.0, blackDepth) <= this.size + 1) {
5766
- return true;
5767
- }
5768
- else {
5931
+ }
5932
+ /**
5933
+ * A document must have a value for every ordering clause in order to show up
5934
+ * in the results.
5935
+ */
5936
+ function queryMatchesOrderBy(query, doc) {
5937
+ for (const orderBy of query.explicitOrderBy) {
5938
+ // order by key always matches
5939
+ if (!orderBy.field.isKeyField() && doc.data.field(orderBy.field) === null) {
5769
5940
  return false;
5770
5941
  }
5771
5942
  }
5772
- // In a balanced RB tree, the black-depth (number of black nodes) from root to
5773
- // leaves is equal on both sides. This function verifies that or asserts.
5774
- check() {
5775
- if (this.isRed() && this.left.isRed()) {
5776
- throw fail();
5777
- }
5778
- if (this.right.isRed()) {
5779
- throw fail();
5780
- }
5781
- const blackDepth = this.left.check();
5782
- if (blackDepth !== this.right.check()) {
5783
- throw fail();
5784
- }
5785
- else {
5786
- return blackDepth + (this.isRed() ? 0 : 1);
5943
+ return true;
5944
+ }
5945
+ function queryMatchesFilters(query, doc) {
5946
+ for (const filter of query.filters) {
5947
+ if (!filter.matches(doc)) {
5948
+ return false;
5787
5949
  }
5788
5950
  }
5789
- } // end LLRBNode
5790
- // Empty node is shared between all LLRB trees.
5791
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5792
- LLRBNode.EMPTY = null;
5793
- LLRBNode.RED = true;
5794
- LLRBNode.BLACK = false;
5795
- // Represents an empty node (a leaf node in the Red-Black Tree).
5796
- class LLRBEmptyNode {
5797
- constructor() {
5798
- this.size = 0;
5799
- }
5800
- get key() {
5801
- throw fail();
5802
- }
5803
- get value() {
5804
- throw fail();
5805
- }
5806
- get color() {
5807
- throw fail();
5808
- }
5809
- get left() {
5810
- throw fail();
5811
- }
5812
- get right() {
5813
- throw fail();
5814
- }
5815
- // Returns a copy of the current node.
5816
- copy(key, value, color, left, right) {
5817
- return this;
5818
- }
5819
- // Returns a copy of the tree, with the specified key/value added.
5820
- insert(key, value, comparator) {
5821
- return new LLRBNode(key, value);
5822
- }
5823
- // Returns a copy of the tree, with the specified key removed.
5824
- remove(key, comparator) {
5825
- return this;
5826
- }
5827
- isEmpty() {
5828
- return true;
5829
- }
5830
- inorderTraversal(action) {
5831
- return false;
5832
- }
5833
- reverseTraversal(action) {
5951
+ return true;
5952
+ }
5953
+ /** Makes sure a document is within the bounds, if provided. */
5954
+ function queryMatchesBounds(query, doc) {
5955
+ if (query.startAt &&
5956
+ !boundSortsBeforeDocument(query.startAt, queryOrderBy(query), doc)) {
5834
5957
  return false;
5835
5958
  }
5836
- minKey() {
5837
- return null;
5838
- }
5839
- maxKey() {
5840
- return null;
5841
- }
5842
- isRed() {
5959
+ if (query.endAt &&
5960
+ !boundSortsAfterDocument(query.endAt, queryOrderBy(query), doc)) {
5843
5961
  return false;
5844
5962
  }
5845
- // For testing.
5846
- checkMaxDepth() {
5847
- return true;
5848
- }
5849
- check() {
5963
+ return true;
5964
+ }
5965
+ /**
5966
+ * Returns the collection group that this query targets.
5967
+ *
5968
+ * PORTING NOTE: This is only used in the Web SDK to facilitate multi-tab
5969
+ * synchronization for query results.
5970
+ */
5971
+ function queryCollectionGroup(query) {
5972
+ return (query.collectionGroup ||
5973
+ (query.path.length % 2 === 1
5974
+ ? query.path.lastSegment()
5975
+ : query.path.get(query.path.length - 2)));
5976
+ }
5977
+ /**
5978
+ * Returns a new comparator function that can be used to compare two documents
5979
+ * based on the Query's ordering constraint.
5980
+ */
5981
+ function newQueryComparator(query) {
5982
+ return (d1, d2) => {
5983
+ let comparedOnKeyField = false;
5984
+ for (const orderBy of queryOrderBy(query)) {
5985
+ const comp = compareDocs(orderBy, d1, d2);
5986
+ if (comp !== 0) {
5987
+ return comp;
5988
+ }
5989
+ comparedOnKeyField = comparedOnKeyField || orderBy.field.isKeyField();
5990
+ }
5850
5991
  return 0;
5992
+ };
5993
+ }
5994
+ function compareDocs(orderBy, d1, d2) {
5995
+ const comparison = orderBy.field.isKeyField()
5996
+ ? DocumentKey.comparator(d1.key, d2.key)
5997
+ : compareDocumentsByField(orderBy.field, d1, d2);
5998
+ switch (orderBy.dir) {
5999
+ case "asc" /* ASCENDING */:
6000
+ return comparison;
6001
+ case "desc" /* DESCENDING */:
6002
+ return -1 * comparison;
6003
+ default:
6004
+ return fail();
5851
6005
  }
5852
- } // end LLRBEmptyNode
5853
- LLRBNode.EMPTY = new LLRBEmptyNode();
6006
+ }
5854
6007
 
5855
6008
  /**
5856
6009
  * @license
@@ -5869,218 +6022,97 @@ LLRBNode.EMPTY = new LLRBEmptyNode();
5869
6022
  * limitations under the License.
5870
6023
  */
5871
6024
  /**
5872
- * SortedSet is an immutable (copy-on-write) collection that holds elements
5873
- * in order specified by the provided comparator.
5874
- *
5875
- * NOTE: if provided comparator returns 0 for two elements, we consider them to
5876
- * be equal!
6025
+ * A map implementation that uses objects as keys. Objects must have an
6026
+ * associated equals function and must be immutable. Entries in the map are
6027
+ * stored together with the key being produced from the mapKeyFn. This map
6028
+ * automatically handles collisions of keys.
5877
6029
  */
5878
- class SortedSet {
5879
- constructor(comparator) {
5880
- this.comparator = comparator;
5881
- this.data = new SortedMap(this.comparator);
5882
- }
5883
- has(elem) {
5884
- return this.data.get(elem) !== null;
5885
- }
5886
- first() {
5887
- return this.data.minKey();
5888
- }
5889
- last() {
5890
- return this.data.maxKey();
5891
- }
5892
- get size() {
5893
- return this.data.size;
5894
- }
5895
- indexOf(elem) {
5896
- return this.data.indexOf(elem);
5897
- }
5898
- /** Iterates elements in order defined by "comparator" */
5899
- forEach(cb) {
5900
- this.data.inorderTraversal((k, v) => {
5901
- cb(k);
5902
- return false;
5903
- });
5904
- }
5905
- /** Iterates over `elem`s such that: range[0] &lt;= elem &lt; range[1]. */
5906
- forEachInRange(range, cb) {
5907
- const iter = this.data.getIteratorFrom(range[0]);
5908
- while (iter.hasNext()) {
5909
- const elem = iter.getNext();
5910
- if (this.comparator(elem.key, range[1]) >= 0) {
5911
- return;
5912
- }
5913
- cb(elem.key);
5914
- }
6030
+ class ObjectMap {
6031
+ constructor(mapKeyFn, equalsFn) {
6032
+ this.mapKeyFn = mapKeyFn;
6033
+ this.equalsFn = equalsFn;
6034
+ /**
6035
+ * The inner map for a key/value pair. Due to the possibility of collisions we
6036
+ * keep a list of entries that we do a linear search through to find an actual
6037
+ * match. Note that collisions should be rare, so we still expect near
6038
+ * constant time lookups in practice.
6039
+ */
6040
+ this.inner = {};
6041
+ /** The number of entries stored in the map */
6042
+ this.innerSize = 0;
5915
6043
  }
5916
- /**
5917
- * Iterates over `elem`s such that: start &lt;= elem until false is returned.
5918
- */
5919
- forEachWhile(cb, start) {
5920
- let iter;
5921
- if (start !== undefined) {
5922
- iter = this.data.getIteratorFrom(start);
5923
- }
5924
- else {
5925
- iter = this.data.getIterator();
6044
+ /** Get a value for this key, or undefined if it does not exist. */
6045
+ get(key) {
6046
+ const id = this.mapKeyFn(key);
6047
+ const matches = this.inner[id];
6048
+ if (matches === undefined) {
6049
+ return undefined;
5926
6050
  }
5927
- while (iter.hasNext()) {
5928
- const elem = iter.getNext();
5929
- const result = cb(elem.key);
5930
- if (!result) {
5931
- return;
6051
+ for (const [otherKey, value] of matches) {
6052
+ if (this.equalsFn(otherKey, key)) {
6053
+ return value;
5932
6054
  }
5933
6055
  }
6056
+ return undefined;
5934
6057
  }
5935
- /** Finds the least element greater than or equal to `elem`. */
5936
- firstAfterOrEqual(elem) {
5937
- const iter = this.data.getIteratorFrom(elem);
5938
- return iter.hasNext() ? iter.getNext().key : null;
5939
- }
5940
- getIterator() {
5941
- return new SortedSetIterator(this.data.getIterator());
5942
- }
5943
- getIteratorFrom(key) {
5944
- return new SortedSetIterator(this.data.getIteratorFrom(key));
5945
- }
5946
- /** Inserts or updates an element */
5947
- add(elem) {
5948
- return this.copy(this.data.remove(elem).insert(elem, true));
6058
+ has(key) {
6059
+ return this.get(key) !== undefined;
5949
6060
  }
5950
- /** Deletes an element */
5951
- delete(elem) {
5952
- if (!this.has(elem)) {
5953
- return this;
6061
+ /** Put this key and value in the map. */
6062
+ set(key, value) {
6063
+ const id = this.mapKeyFn(key);
6064
+ const matches = this.inner[id];
6065
+ if (matches === undefined) {
6066
+ this.inner[id] = [[key, value]];
6067
+ this.innerSize++;
6068
+ return;
5954
6069
  }
5955
- return this.copy(this.data.remove(elem));
5956
- }
5957
- isEmpty() {
5958
- return this.data.isEmpty();
5959
- }
5960
- unionWith(other) {
5961
- let result = this;
5962
- // Make sure `result` always refers to the larger one of the two sets.
5963
- if (result.size < other.size) {
5964
- result = other;
5965
- other = this;
6070
+ for (let i = 0; i < matches.length; i++) {
6071
+ if (this.equalsFn(matches[i][0], key)) {
6072
+ // This is updating an existing entry and does not increase `innerSize`.
6073
+ matches[i] = [key, value];
6074
+ return;
6075
+ }
5966
6076
  }
5967
- other.forEach(elem => {
5968
- result = result.add(elem);
5969
- });
5970
- return result;
6077
+ matches.push([key, value]);
6078
+ this.innerSize++;
5971
6079
  }
5972
- isEqual(other) {
5973
- if (!(other instanceof SortedSet)) {
5974
- return false;
5975
- }
5976
- if (this.size !== other.size) {
6080
+ /**
6081
+ * Remove this key from the map. Returns a boolean if anything was deleted.
6082
+ */
6083
+ delete(key) {
6084
+ const id = this.mapKeyFn(key);
6085
+ const matches = this.inner[id];
6086
+ if (matches === undefined) {
5977
6087
  return false;
5978
6088
  }
5979
- const thisIt = this.data.getIterator();
5980
- const otherIt = other.data.getIterator();
5981
- while (thisIt.hasNext()) {
5982
- const thisElem = thisIt.getNext().key;
5983
- const otherElem = otherIt.getNext().key;
5984
- if (this.comparator(thisElem, otherElem) !== 0) {
5985
- return false;
6089
+ for (let i = 0; i < matches.length; i++) {
6090
+ if (this.equalsFn(matches[i][0], key)) {
6091
+ if (matches.length === 1) {
6092
+ delete this.inner[id];
6093
+ }
6094
+ else {
6095
+ matches.splice(i, 1);
6096
+ }
6097
+ this.innerSize--;
6098
+ return true;
5986
6099
  }
5987
6100
  }
5988
- return true;
6101
+ return false;
5989
6102
  }
5990
- toArray() {
5991
- const res = [];
5992
- this.forEach(targetId => {
5993
- res.push(targetId);
6103
+ forEach(fn) {
6104
+ forEach(this.inner, (_, entries) => {
6105
+ for (const [k, v] of entries) {
6106
+ fn(k, v);
6107
+ }
5994
6108
  });
5995
- return res;
5996
- }
5997
- toString() {
5998
- const result = [];
5999
- this.forEach(elem => result.push(elem));
6000
- return 'SortedSet(' + result.toString() + ')';
6001
- }
6002
- copy(data) {
6003
- const result = new SortedSet(this.comparator);
6004
- result.data = data;
6005
- return result;
6006
- }
6007
- }
6008
- class SortedSetIterator {
6009
- constructor(iter) {
6010
- this.iter = iter;
6011
6109
  }
6012
- getNext() {
6013
- return this.iter.getNext().key;
6014
- }
6015
- hasNext() {
6016
- return this.iter.hasNext();
6110
+ isEmpty() {
6111
+ return isEmpty(this.inner);
6017
6112
  }
6018
- }
6019
- /**
6020
- * Compares two sorted sets for equality using their natural ordering. The
6021
- * method computes the intersection and invokes `onAdd` for every element that
6022
- * is in `after` but not `before`. `onRemove` is invoked for every element in
6023
- * `before` but missing from `after`.
6024
- *
6025
- * The method creates a copy of both `before` and `after` and runs in O(n log
6026
- * n), where n is the size of the two lists.
6027
- *
6028
- * @param before - The elements that exist in the original set.
6029
- * @param after - The elements to diff against the original set.
6030
- * @param comparator - The comparator for the elements in before and after.
6031
- * @param onAdd - A function to invoke for every element that is part of `
6032
- * after` but not `before`.
6033
- * @param onRemove - A function to invoke for every element that is part of
6034
- * `before` but not `after`.
6035
- */
6036
- function diffSortedSets(before, after, comparator, onAdd, onRemove) {
6037
- const beforeIt = before.getIterator();
6038
- const afterIt = after.getIterator();
6039
- let beforeValue = advanceIterator(beforeIt);
6040
- let afterValue = advanceIterator(afterIt);
6041
- // Walk through the two sets at the same time, using the ordering defined by
6042
- // `comparator`.
6043
- while (beforeValue || afterValue) {
6044
- let added = false;
6045
- let removed = false;
6046
- if (beforeValue && afterValue) {
6047
- const cmp = comparator(beforeValue, afterValue);
6048
- if (cmp < 0) {
6049
- // The element was removed if the next element in our ordered
6050
- // walkthrough is only in `before`.
6051
- removed = true;
6052
- }
6053
- else if (cmp > 0) {
6054
- // The element was added if the next element in our ordered walkthrough
6055
- // is only in `after`.
6056
- added = true;
6057
- }
6058
- }
6059
- else if (beforeValue != null) {
6060
- removed = true;
6061
- }
6062
- else {
6063
- added = true;
6064
- }
6065
- if (added) {
6066
- onAdd(afterValue);
6067
- afterValue = advanceIterator(afterIt);
6068
- }
6069
- else if (removed) {
6070
- onRemove(beforeValue);
6071
- beforeValue = advanceIterator(beforeIt);
6072
- }
6073
- else {
6074
- beforeValue = advanceIterator(beforeIt);
6075
- afterValue = advanceIterator(afterIt);
6076
- }
6113
+ size() {
6114
+ return this.innerSize;
6077
6115
  }
6078
- }
6079
- /**
6080
- * Returns the next element from the iterator or `undefined` if none available.
6081
- */
6082
- function advanceIterator(it) {
6083
- return it.hasNext() ? it.getNext() : undefined;
6084
6116
  }
6085
6117
 
6086
6118
  /**
@@ -6104,8 +6136,12 @@ function mutableDocumentMap() {
6104
6136
  return EMPTY_MUTABLE_DOCUMENT_MAP;
6105
6137
  }
6106
6138
  const EMPTY_DOCUMENT_MAP = new SortedMap(DocumentKey.comparator);
6107
- function documentMap() {
6108
- return EMPTY_DOCUMENT_MAP;
6139
+ function documentMap(...docs) {
6140
+ let map = EMPTY_DOCUMENT_MAP;
6141
+ for (const doc of docs) {
6142
+ map = map.insert(doc.key, doc);
6143
+ }
6144
+ return map;
6109
6145
  }
6110
6146
  function newOverlayMap() {
6111
6147
  return new ObjectMap(key => key.toString(), (l, r) => l.isEqual(r));
@@ -9998,9 +10034,9 @@ class MemoryIndexManager {
9998
10034
  // Field indices are not supported with memory persistence.
9999
10035
  return PersistencePromise.resolve(null);
10000
10036
  }
10001
- getFieldIndex(transaction, target) {
10037
+ getIndexType(transaction, target) {
10002
10038
  // Field indices are not supported with memory persistence.
10003
- return PersistencePromise.resolve(null);
10039
+ return PersistencePromise.resolve(0 /* NONE */);
10004
10040
  }
10005
10041
  getFieldIndexes(transaction, collectionGroup) {
10006
10042
  // Field indices are not supported with memory persistence.
@@ -10010,6 +10046,9 @@ class MemoryIndexManager {
10010
10046
  // Field indices are not supported with memory persistence.
10011
10047
  return PersistencePromise.resolve(null);
10012
10048
  }
10049
+ getMinOffset(transaction, target) {
10050
+ return PersistencePromise.resolve(IndexOffset.min());
10051
+ }
10013
10052
  updateCollectionGroup(transaction, collectionGroup, offset) {
10014
10053
  // Field indices are not supported with memory persistence.
10015
10054
  return PersistencePromise.resolve();
@@ -10185,7 +10224,7 @@ class IndexedDbIndexManager {
10185
10224
  const lowerBoundEncoded = this.encodeBound(index, subTarget, lowerBound);
10186
10225
  const upperBoundEncoded = this.encodeBound(index, subTarget, upperBound);
10187
10226
  const notInEncoded = this.encodeValues(index, subTarget, notInValues);
10188
- const indexRanges = this.generateIndexRanges(index.indexId, arrayValues, lowerBoundEncoded, !!lowerBound && lowerBound.inclusive, upperBoundEncoded, !!upperBound && upperBound.inclusive, notInEncoded);
10227
+ const indexRanges = this.generateIndexRanges(index.indexId, arrayValues, lowerBoundEncoded, lowerBound.inclusive, upperBoundEncoded, upperBound.inclusive, notInEncoded);
10189
10228
  return PersistencePromise.forEach(indexRanges, (indexRange) => {
10190
10229
  return indexEntries
10191
10230
  .loadFirst(indexRange, target.limit)
@@ -10223,22 +10262,18 @@ class IndexedDbIndexManager {
10223
10262
  // index range per value in an ARRAY_CONTAINS or ARRAY_CONTAINS_ANY filter
10224
10263
  // combined with the values from the query bounds.
10225
10264
  const totalScans = (arrayValues != null ? arrayValues.length : 1) *
10226
- Math.max(lowerBounds != null ? lowerBounds.length : 1, upperBounds != null ? upperBounds.length : 1);
10265
+ Math.max(lowerBounds.length, upperBounds.length);
10227
10266
  const scansPerArrayElement = totalScans / (arrayValues != null ? arrayValues.length : 1);
10228
10267
  const indexRanges = [];
10229
10268
  for (let i = 0; i < totalScans; ++i) {
10230
10269
  const arrayValue = arrayValues
10231
10270
  ? this.encodeSingleElement(arrayValues[i / scansPerArrayElement])
10232
10271
  : EMPTY_VALUE;
10233
- const lowerBound = lowerBounds
10234
- ? this.generateLowerBound(indexId, arrayValue, lowerBounds[i % scansPerArrayElement], lowerBoundInclusive)
10235
- : this.generateEmptyBound(indexId);
10236
- const upperBound = upperBounds
10237
- ? this.generateUpperBound(indexId, arrayValue, upperBounds[i % scansPerArrayElement], upperBoundInclusive)
10238
- : this.generateEmptyBound(indexId + 1);
10239
- indexRanges.push(...this.createRange(lowerBound, upperBound, notInValues.map((notIn // make non-nullable
10240
- ) => this.generateLowerBound(indexId, arrayValue, notIn,
10241
- /* inclusive= */ true))));
10272
+ const lowerBound = this.generateLowerBound(indexId, arrayValue, lowerBounds[i % scansPerArrayElement], lowerBoundInclusive);
10273
+ const upperBound = this.generateUpperBound(indexId, arrayValue, upperBounds[i % scansPerArrayElement], upperBoundInclusive);
10274
+ const notInBound = notInValues.map(notIn => this.generateLowerBound(indexId, arrayValue, notIn,
10275
+ /* inclusive= */ true));
10276
+ indexRanges.push(...this.createRange(lowerBound, upperBound, notInBound));
10242
10277
  }
10243
10278
  return indexRanges;
10244
10279
  }
@@ -10252,25 +10287,38 @@ class IndexedDbIndexManager {
10252
10287
  const entry = new IndexEntry(indexId, DocumentKey.empty(), arrayValue, directionalValue);
10253
10288
  return inclusive ? entry.successor() : entry;
10254
10289
  }
10255
- /**
10256
- * Generates an empty bound that scopes the index scan to the current index
10257
- * and user.
10258
- */
10259
- generateEmptyBound(indexId) {
10260
- return new IndexEntry(indexId, DocumentKey.empty(), EMPTY_VALUE, EMPTY_VALUE);
10261
- }
10262
10290
  getFieldIndex(transaction, target) {
10263
10291
  const targetIndexMatcher = new TargetIndexMatcher(target);
10264
10292
  const collectionGroup = target.collectionGroup != null
10265
10293
  ? target.collectionGroup
10266
10294
  : target.path.lastSegment();
10267
10295
  return this.getFieldIndexes(transaction, collectionGroup).next(indexes => {
10268
- const matchingIndexes = indexes.filter(i => targetIndexMatcher.servedByIndex(i));
10269
- // Return the index that matches the most number of segments.
10270
- matchingIndexes.sort((l, r) => r.fields.length - l.fields.length);
10271
- return matchingIndexes.length > 0 ? matchingIndexes[0] : null;
10296
+ // Return the index with the most number of segments.
10297
+ let index = null;
10298
+ for (const candidate of indexes) {
10299
+ const matches = targetIndexMatcher.servedByIndex(candidate);
10300
+ if (matches &&
10301
+ (!index || candidate.fields.length > index.fields.length)) {
10302
+ index = candidate;
10303
+ }
10304
+ }
10305
+ return index;
10272
10306
  });
10273
10307
  }
10308
+ getIndexType(transaction, target) {
10309
+ let indexType = 2 /* FULL */;
10310
+ return PersistencePromise.forEach(this.getSubTargets(target), (target) => {
10311
+ return this.getFieldIndex(transaction, target).next(index => {
10312
+ if (!index) {
10313
+ indexType = 0 /* NONE */;
10314
+ }
10315
+ else if (indexType !== 0 /* NONE */ &&
10316
+ index.fields.length < targetGetSegmentCount(target)) {
10317
+ indexType = 1 /* PARTIAL */;
10318
+ }
10319
+ });
10320
+ }).next(() => indexType);
10321
+ }
10274
10322
  /**
10275
10323
  * Returns the byte encoded form of the directional values in the field index.
10276
10324
  * Returns `null` if the document does not have all fields specified in the
@@ -10333,9 +10381,6 @@ class IndexedDbIndexManager {
10333
10381
  * queries, a list of possible values is returned.
10334
10382
  */
10335
10383
  encodeBound(fieldIndex, target, bound) {
10336
- if (bound == null) {
10337
- return null;
10338
- }
10339
10384
  return this.encodeValues(fieldIndex, target, bound.position);
10340
10385
  }
10341
10386
  /** Returns the byte representation for the provided encoders. */
@@ -10530,7 +10575,7 @@ class IndexedDbIndexManager {
10530
10575
  * '[foo > 2 && foo != 3]` becomes `[foo > 2 && < 3, foo > 3]`.
10531
10576
  */
10532
10577
  createRange(lower, upper, notInValues) {
10533
- // The notIb values need to be sorted and unique so that we can return a
10578
+ // The notIn values need to be sorted and unique so that we can return a
10534
10579
  // sorted set of non-overlapping ranges.
10535
10580
  notInValues = notInValues
10536
10581
  .sort((l, r) => indexEntryComparator(l, r))
@@ -10576,6 +10621,20 @@ class IndexedDbIndexManager {
10576
10621
  }
10577
10622
  return ranges;
10578
10623
  }
10624
+ getMinOffset(transaction, target) {
10625
+ let offset;
10626
+ return PersistencePromise.forEach(this.getSubTargets(target), (target) => {
10627
+ return this.getFieldIndex(transaction, target).next(index => {
10628
+ if (!index) {
10629
+ offset = IndexOffset.min();
10630
+ }
10631
+ else if (!offset ||
10632
+ indexOffsetComparator(index.indexState.offset, offset) < 0) {
10633
+ offset = index.indexState.offset;
10634
+ }
10635
+ });
10636
+ }).next(() => offset);
10637
+ }
10579
10638
  }
10580
10639
  /**
10581
10640
  * Helper to get a typed SimpleDbStore for the collectionParents
@@ -13746,7 +13805,7 @@ class LocalStoreImpl {
13746
13805
  this.mutationQueue = this.persistence.getMutationQueue(user, this.indexManager);
13747
13806
  this.localDocuments = new LocalDocumentsView(this.remoteDocuments, this.mutationQueue, this.indexManager);
13748
13807
  this.remoteDocuments.setIndexManager(this.indexManager);
13749
- this.queryEngine.setLocalDocumentsView(this.localDocuments);
13808
+ this.queryEngine.initialize(this.localDocuments, this.indexManager);
13750
13809
  }
13751
13810
  collectGarbage(garbageCollector) {
13752
13811
  return this.persistence.runTransaction('Collect garbage', 'readwrite-primary', txn => garbageCollector.collect(txn, this.targetDataByTarget));
@@ -15488,10 +15547,30 @@ class MemoryEagerDelegate {
15488
15547
  * limitations under the License.
15489
15548
  */
15490
15549
  /**
15491
- * A query engine that takes advantage of the target document mapping in the
15492
- * QueryCache. Query execution is optimized by only reading the documents that
15493
- * previously matched a query plus any documents that were edited after the
15494
- * query was last listened to.
15550
+ * The Firestore query engine.
15551
+ *
15552
+ * Firestore queries can be executed in three modes. The Query Engine determines
15553
+ * what mode to use based on what data is persisted. The mode only determines
15554
+ * the runtime complexity of the query - the result set is equivalent across all
15555
+ * implementations.
15556
+ *
15557
+ * The Query engine will use indexed-based execution if a user has configured
15558
+ * any index that can be used to execute query (via `setIndexConfiguration()`).
15559
+ * Otherwise, the engine will try to optimize the query by re-using a previously
15560
+ * persisted query result. If that is not possible, the query will be executed
15561
+ * via a full collection scan.
15562
+ *
15563
+ * Index-based execution is the default when available. The query engine
15564
+ * supports partial indexed execution and merges the result from the index
15565
+ * lookup with documents that have not yet been indexed. The index evaluation
15566
+ * matches the backend's format and as such, the SDK can use indexing for all
15567
+ * queries that the backend supports.
15568
+ *
15569
+ * If no index exists, the query engine tries to take advantage of the target
15570
+ * document mapping in the TargetCache. These mappings exists for all queries
15571
+ * that have been synced with the backend at least once and allow the query
15572
+ * engine to only read documents that previously matched a query plus any
15573
+ * documents that were edited after the query was last listened to.
15495
15574
  *
15496
15575
  * There are some cases when this optimization is not guaranteed to produce
15497
15576
  * the same results as full collection scans. In these cases, query
@@ -15506,16 +15585,41 @@ class MemoryEagerDelegate {
15506
15585
  * - Queries that have never been CURRENT or free of limbo documents.
15507
15586
  */
15508
15587
  class QueryEngine {
15588
+ constructor() {
15589
+ this.initialized = false;
15590
+ }
15509
15591
  /** Sets the document view to query against. */
15510
- setLocalDocumentsView(localDocuments) {
15592
+ initialize(localDocuments, indexManager) {
15511
15593
  this.localDocumentsView = localDocuments;
15594
+ this.indexManager = indexManager;
15595
+ this.initialized = true;
15512
15596
  }
15513
15597
  /** Returns all local documents matching the specified query. */
15514
15598
  getDocumentsMatchingQuery(transaction, query, lastLimboFreeSnapshotVersion, remoteKeys) {
15515
- // Queries that match all documents don't benefit from using
15516
- // key-based lookups. It is more efficient to scan all documents in a
15517
- // collection, rather than to perform individual lookups.
15518
- if (matchesAllDocuments(query)) {
15599
+ return this.performQueryUsingIndex(transaction, query)
15600
+ .next(result => result
15601
+ ? result
15602
+ : this.performQueryUsingRemoteKeys(transaction, query, remoteKeys, lastLimboFreeSnapshotVersion))
15603
+ .next(result => result ? result : this.executeFullCollectionScan(transaction, query));
15604
+ }
15605
+ /**
15606
+ * Performs an indexed query that evaluates the query based on a collection's
15607
+ * persisted index values. Returns `null` if an index is not available.
15608
+ */
15609
+ performQueryUsingIndex(transaction, query) {
15610
+ {
15611
+ return PersistencePromise.resolve(null);
15612
+ }
15613
+ }
15614
+ /**
15615
+ * Performs a query based on the target's persisted query mapping. Returns
15616
+ * `null` if the mapping is not available or cannot be used.
15617
+ */
15618
+ performQueryUsingRemoteKeys(transaction, query, remoteKeys, lastLimboFreeSnapshotVersion) {
15619
+ if (queryMatchesAllDocuments(query)) {
15620
+ // Queries that match all documents don't benefit from using
15621
+ // key-based lookups. It is more efficient to scan all documents in a
15622
+ // collection, rather than to perform individual lookups.
15519
15623
  return this.executeFullCollectionScan(transaction, query);
15520
15624
  }
15521
15625
  // Queries that have never seen a snapshot without limbo free documents
@@ -15525,8 +15629,7 @@ class QueryEngine {
15525
15629
  }
15526
15630
  return this.localDocumentsView.getDocuments(transaction, remoteKeys).next(documents => {
15527
15631
  const previousResults = this.applyQuery(query, documents);
15528
- if ((hasLimitToFirst(query) || hasLimitToLast(query)) &&
15529
- this.needsRefill(query.limitType, previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
15632
+ if (this.needsRefill(query, previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
15530
15633
  return this.executeFullCollectionScan(transaction, query);
15531
15634
  }
15532
15635
  if (getLogLevel() <= logger.LogLevel.DEBUG) {
@@ -15534,15 +15637,7 @@ class QueryEngine {
15534
15637
  }
15535
15638
  // Retrieve all results for documents that were updated since the last
15536
15639
  // limbo-document free remote snapshot.
15537
- return this.localDocumentsView.getDocumentsMatchingQuery(transaction, query, newIndexOffsetSuccessorFromReadTime(lastLimboFreeSnapshotVersion, INITIAL_LARGEST_BATCH_ID)).next(updatedResults => {
15538
- // We merge `previousResults` into `updateResults`, since
15539
- // `updateResults` is already a DocumentMap. If a document is
15540
- // contained in both lists, then its contents are the same.
15541
- previousResults.forEach(doc => {
15542
- updatedResults = updatedResults.insert(doc.key, doc);
15543
- });
15544
- return updatedResults;
15545
- });
15640
+ return this.appendRemainingResults(transaction, previousResults, query, newIndexOffsetSuccessorFromReadTime(lastLimboFreeSnapshotVersion, INITIAL_LARGEST_BATCH_ID));
15546
15641
  });
15547
15642
  }
15548
15643
  /** Applies the query filter and sorting to the provided documents. */
@@ -15561,6 +15656,7 @@ class QueryEngine {
15561
15656
  * Determines if a limit query needs to be refilled from cache, making it
15562
15657
  * ineligible for index-free execution.
15563
15658
  *
15659
+ * @param query The query.
15564
15660
  * @param sortedPreviousResults - The documents that matched the query when it
15565
15661
  * was last synchronized, sorted by the query's comparator.
15566
15662
  * @param remoteKeys - The document keys that matched the query at the last
@@ -15568,10 +15664,14 @@ class QueryEngine {
15568
15664
  * @param limboFreeSnapshotVersion - The version of the snapshot when the
15569
15665
  * query was last synchronized.
15570
15666
  */
15571
- needsRefill(limitType, sortedPreviousResults, remoteKeys, limboFreeSnapshotVersion) {
15572
- // The query needs to be refilled if a previously matching document no
15573
- // longer matches.
15667
+ needsRefill(query, sortedPreviousResults, remoteKeys, limboFreeSnapshotVersion) {
15668
+ if (query.limit === null) {
15669
+ // Queries without limits do not need to be refilled.
15670
+ return false;
15671
+ }
15574
15672
  if (remoteKeys.size !== sortedPreviousResults.size) {
15673
+ // The query needs to be refilled if a previously matching document no
15674
+ // longer matches.
15575
15675
  return true;
15576
15676
  }
15577
15677
  // Limit queries are not eligible for index-free query execution if there is
@@ -15582,7 +15682,7 @@ class QueryEngine {
15582
15682
  // the boundary of the limit itself did not change and documents from cache
15583
15683
  // will continue to be "rejected" by this boundary. Therefore, we can ignore
15584
15684
  // any modifications that don't affect the last document.
15585
- const docAtLimitEdge = limitType === "F" /* First */
15685
+ const docAtLimitEdge = query.limitType === "F" /* First */
15586
15686
  ? sortedPreviousResults.last()
15587
15687
  : sortedPreviousResults.first();
15588
15688
  if (!docAtLimitEdge) {
@@ -15598,6 +15698,22 @@ class QueryEngine {
15598
15698
  }
15599
15699
  return this.localDocumentsView.getDocumentsMatchingQuery(transaction, query, IndexOffset.min());
15600
15700
  }
15701
+ /**
15702
+ * Combines the results from an indexed execution with the remaining documents
15703
+ * that have not yet been indexed.
15704
+ */
15705
+ appendRemainingResults(transaction, indexedResults, query, offset) {
15706
+ // Retrieve all results for documents that were updated since the offset.
15707
+ return this.localDocumentsView
15708
+ .getDocumentsMatchingQuery(transaction, query, offset)
15709
+ .next(remainingResults => {
15710
+ // Merge with existing results
15711
+ indexedResults.forEach(d => {
15712
+ remainingResults = remainingResults.insert(d.key, d);
15713
+ });
15714
+ return remainingResults;
15715
+ });
15716
+ }
15601
15717
  }
15602
15718
 
15603
15719
  /**
@@ -21857,10 +21973,12 @@ class View {
21857
21973
  //
21858
21974
  // Note that this should never get used in a refill (when previousChanges is
21859
21975
  // set), because there will only be adds -- no deletes or updates.
21860
- const lastDocInLimit = hasLimitToFirst(this.query) && oldDocumentSet.size === this.query.limit
21976
+ const lastDocInLimit = this.query.limitType === "F" /* First */ &&
21977
+ oldDocumentSet.size === this.query.limit
21861
21978
  ? oldDocumentSet.last()
21862
21979
  : null;
21863
- const firstDocInLimit = hasLimitToLast(this.query) && oldDocumentSet.size === this.query.limit
21980
+ const firstDocInLimit = this.query.limitType === "L" /* Last */ &&
21981
+ oldDocumentSet.size === this.query.limit
21864
21982
  ? oldDocumentSet.first()
21865
21983
  : null;
21866
21984
  docChanges.inorderTraversal((key, entry) => {
@@ -21933,9 +22051,9 @@ class View {
21933
22051
  }
21934
22052
  });
21935
22053
  // Drop documents out to meet limit/limitToLast requirement.
21936
- if (hasLimitToFirst(this.query) || hasLimitToLast(this.query)) {
22054
+ if (this.query.limit !== null) {
21937
22055
  while (newDocumentSet.size > this.query.limit) {
21938
- const oldDoc = hasLimitToFirst(this.query)
22056
+ const oldDoc = this.query.limitType === "F" /* First */
21939
22057
  ? newDocumentSet.last()
21940
22058
  : newDocumentSet.first();
21941
22059
  newDocumentSet = newDocumentSet.delete(oldDoc.key);
@@ -23936,18 +24054,18 @@ class Transaction$2 {
23936
24054
  * See the License for the specific language governing permissions and
23937
24055
  * limitations under the License.
23938
24056
  */
23939
- const DEFAULT_MAX_ATTEMPTS_COUNT = 5;
23940
24057
  /**
23941
24058
  * TransactionRunner encapsulates the logic needed to run and retry transactions
23942
24059
  * with backoff.
23943
24060
  */
23944
24061
  class TransactionRunner {
23945
- constructor(asyncQueue, datastore, updateFunction, deferred) {
24062
+ constructor(asyncQueue, datastore, options, updateFunction, deferred) {
23946
24063
  this.asyncQueue = asyncQueue;
23947
24064
  this.datastore = datastore;
24065
+ this.options = options;
23948
24066
  this.updateFunction = updateFunction;
23949
24067
  this.deferred = deferred;
23950
- this.attemptsRemaining = DEFAULT_MAX_ATTEMPTS_COUNT;
24068
+ this.attemptsRemaining = options.maxAttempts;
23951
24069
  this.backoff = new ExponentialBackoff(this.asyncQueue, "transaction_retry" /* TransactionRetry */);
23952
24070
  }
23953
24071
  /** Runs the transaction and sets the result on deferred. */
@@ -24305,11 +24423,11 @@ function firestoreClientAddSnapshotsInSyncListener(client, observer) {
24305
24423
  * been committed. For this reason, it is required that all reads are
24306
24424
  * performed before any writes. Transactions must be performed while online.
24307
24425
  */
24308
- function firestoreClientTransaction(client, updateFunction) {
24426
+ function firestoreClientTransaction(client, updateFunction, options) {
24309
24427
  const deferred = new Deferred();
24310
24428
  client.asyncQueue.enqueueAndForget(async () => {
24311
24429
  const datastore = await getDatastore(client);
24312
- new TransactionRunner(client.asyncQueue, datastore, updateFunction, deferred).run();
24430
+ new TransactionRunner(client.asyncQueue, datastore, options, updateFunction, deferred).run();
24313
24431
  });
24314
24432
  return deferred.promise;
24315
24433
  }
@@ -27066,7 +27184,8 @@ function snapshotEqual(left, right) {
27066
27184
  * limitations under the License.
27067
27185
  */
27068
27186
  function validateHasExplicitOrderByForLimitToLast(query) {
27069
- if (hasLimitToLast(query) && query.explicitOrderBy.length === 0) {
27187
+ if (query.limitType === "L" /* Last */ &&
27188
+ query.explicitOrderBy.length === 0) {
27070
27189
  throw new FirestoreError(Code.UNIMPLEMENTED, 'limitToLast() queries require specifying at least one orderBy() clause');
27071
27190
  }
27072
27191
  }
@@ -27508,6 +27627,31 @@ function validateOrderByAndInequalityMatch(baseQuery, inequality, orderBy) {
27508
27627
  }
27509
27628
  }
27510
27629
 
27630
+ /**
27631
+ * @license
27632
+ * Copyright 2022 Google LLC
27633
+ *
27634
+ * Licensed under the Apache License, Version 2.0 (the "License");
27635
+ * you may not use this file except in compliance with the License.
27636
+ * You may obtain a copy of the License at
27637
+ *
27638
+ * http://www.apache.org/licenses/LICENSE-2.0
27639
+ *
27640
+ * Unless required by applicable law or agreed to in writing, software
27641
+ * distributed under the License is distributed on an "AS IS" BASIS,
27642
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27643
+ * See the License for the specific language governing permissions and
27644
+ * limitations under the License.
27645
+ */
27646
+ const DEFAULT_TRANSACTION_OPTIONS = {
27647
+ maxAttempts: 5
27648
+ };
27649
+ function validateTransactionOptions(options) {
27650
+ if (options.maxAttempts < 1) {
27651
+ throw new FirestoreError(Code.INVALID_ARGUMENT, 'Max attempts must be at least 1');
27652
+ }
27653
+ }
27654
+
27511
27655
  /**
27512
27656
  * @license
27513
27657
  * Copyright 2020 Google LLC
@@ -28229,10 +28373,12 @@ class Transaction extends Transaction$1 {
28229
28373
  * `updateFunction `is returned here. Otherwise, if the transaction failed, a
28230
28374
  * rejected promise with the corresponding failure error is returned.
28231
28375
  */
28232
- function runTransaction(firestore, updateFunction) {
28376
+ function runTransaction(firestore, updateFunction, options) {
28233
28377
  firestore = cast(firestore, Firestore);
28378
+ const optionsWithDefaults = Object.assign(Object.assign({}, DEFAULT_TRANSACTION_OPTIONS), options);
28379
+ validateTransactionOptions(optionsWithDefaults);
28234
28380
  const client = ensureFirestoreConfigured(firestore);
28235
- return firestoreClientTransaction(client, internalTransaction => updateFunction(new Transaction(firestore, internalTransaction)));
28381
+ return firestoreClientTransaction(client, internalTransaction => updateFunction(new Transaction(firestore, internalTransaction)), optionsWithDefaults);
28236
28382
  }
28237
28383
 
28238
28384
  /**