From 974353893e6a6a586b6901c6c74686593537a4f7 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 30 Jun 2020 22:27:41 -0700 Subject: [PATCH 1/3] Remove Mutation overhead from Lite SDK --- .../firestore/lite/test/dependencies.json | 244 +--- packages/firestore/src/api/field_value.ts | 2 +- .../src/local/local_documents_view.ts | 5 +- packages/firestore/src/local/local_store.ts | 10 +- packages/firestore/src/model/mutation.ts | 1010 +++++++++-------- .../firestore/src/model/mutation_batch.ts | 28 +- .../src/model/transform_operation.ts | 322 +++--- packages/firestore/src/remote/serializer.ts | 2 +- .../test/unit/model/mutation.test.ts | 95 +- .../test/unit/remote/serializer.helper.ts | 5 +- 10 files changed, 820 insertions(+), 903 deletions(-) diff --git a/packages/firestore/lite/test/dependencies.json b/packages/firestore/lite/test/dependencies.json index df88ad5b687..41462a2b7c1 100644 --- a/packages/firestore/lite/test/dependencies.json +++ b/packages/firestore/lite/test/dependencies.json @@ -199,7 +199,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -219,7 +218,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -236,19 +234,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 99162 + "sizeInBytes": 91030 }, "DocumentReference": { "dependencies": { @@ -698,7 +694,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -718,7 +713,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -734,18 +728,16 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 85476 + "sizeInBytes": 77344 }, "QueryDocumentSnapshot": { "dependencies": { @@ -1053,19 +1045,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "Transaction$1", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 69588 + "sizeInBytes": 62673 }, "WriteBatch": { "dependencies": { @@ -1077,7 +1067,6 @@ "binaryStringFromUint8Array", "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "debugAssert", @@ -1095,18 +1084,12 @@ "fromDotSeparatedString", "fullyQualifiedPrefixPath", "geoPointEquals", - "getEncodedDatabaseId", "getLocalWriteTime", "hardAssert", "invalidClassError", - "invokeCommitRpc", - "isArray", - "isDouble", "isEmpty", - "isInteger", "isMapValue", "isNegativeZero", - "isNumber", "isPlainObject", "isSafeInteger", "isServerTimestamp", @@ -1136,23 +1119,15 @@ "parseSentinelFieldValue", "primitiveComparator", "registerFirestore", - "serverTimestamp", "terminate", "terminateDatastore", "timestampEquals", "toBytes", - "toDocumentMask", "toDouble", - "toFieldTransform", "toInteger", - "toMutation", - "toMutationDocument", - "toName", "toNumber", - "toPrecondition", "toResourceName", "toTimestamp", - "toVersion", "tryGetCustomObjectType", "typeOrder", "uint8ArrayFromBinaryString", @@ -1166,8 +1141,6 @@ "valueEquals" ], "classes": [ - "ArrayRemoveTransformOperation", - "ArrayUnionTransformOperation", "BaseFieldPath", "BasePath", "Blob", @@ -1179,7 +1152,6 @@ "Deferred", "DeleteFieldValueImpl", "DeleteMutation", - "Document", "DocumentKeyReference", "DocumentReference", "FieldMask", @@ -1192,10 +1164,7 @@ "GeoPoint", "GrpcConnection", "JsonProtoSerializer", - "MaybeDocument", "Mutation", - "NoDocument", - "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", "ObjectValueBuilder", @@ -1206,21 +1175,17 @@ "Precondition", "ResourcePath", "SerializableFieldValue", - "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", - "VerifyMutation", "WriteBatch" ], "variables": [] }, - "sizeInBytes": 72512 + "sizeInBytes": 56525 }, "addDoc": { "dependencies": { @@ -1247,7 +1212,6 @@ "canonifyTimestamp", "canonifyValue", "cast", - "coercedFieldValuesArray", "compareArrays", "compareBlobs", "compareDocs", @@ -1329,7 +1293,6 @@ "randomBytes", "refValue", "registerFirestore", - "serverTimestamp", "sortsBeforeDocument", "stringifyFilter", "stringifyOrderBy", @@ -1385,7 +1348,6 @@ "Deferred", "DeleteFieldValueImpl", "DeleteMutation", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -1405,9 +1367,7 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", - "NoDocument", "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", @@ -1425,12 +1385,11 @@ "SerializableFieldValue", "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "UserDataReader", "UserDataWriter", @@ -1438,19 +1397,16 @@ ], "variables": [] }, - "sizeInBytes": 109513 + "sizeInBytes": 97301 }, "arrayRemove": { "dependencies": { "functions": [ "argToString", - "arrayEquals", "arrayRemove", "assertUint8ArrayAvailable", "binaryStringFromUint8Array", - "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "createSentinelChildContext", @@ -1463,16 +1419,12 @@ "formatJSON", "formatPlural", "fullyQualifiedPrefixPath", - "geoPointEquals", - "getLocalWriteTime", "hardAssert", "invalidClassError", - "isArray", "isEmpty", "isNegativeZero", "isPlainObject", "isSafeInteger", - "isServerTimestamp", "isWrite", "loadProtos", "logDebug", @@ -1484,12 +1436,6 @@ "newDatastore", "newSerializer", "nodePromise", - "normalizeByteString", - "normalizeNumber", - "normalizeTimestamp", - "numberEquals", - "objectEquals", - "objectSize", "ordinal", "parseArray", "parseData", @@ -1500,7 +1446,6 @@ "registerFirestore", "terminate", "terminateDatastore", - "timestampEquals", "toBytes", "toDouble", "toInteger", @@ -1508,15 +1453,13 @@ "toResourceName", "toTimestamp", "tryGetCustomObjectType", - "typeOrder", "uint8ArrayFromBinaryString", "validateArgType", "validateAtLeastNumberOfArgs", "validateExactNumberOfArgs", "validatePlainObject", "validateType", - "valueDescription", - "valueEquals" + "valueDescription" ], "classes": [ "ArrayRemoveFieldValueImpl", @@ -1545,23 +1488,21 @@ "SerializableFieldValue", "StreamBridge", "Timestamp", + "TransformOperation", "User" ], "variables": [] }, - "sizeInBytes": 42383 + "sizeInBytes": 36971 }, "arrayUnion": { "dependencies": { "functions": [ "argToString", - "arrayEquals", "arrayUnion", "assertUint8ArrayAvailable", "binaryStringFromUint8Array", - "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "createSentinelChildContext", @@ -1574,16 +1515,12 @@ "formatJSON", "formatPlural", "fullyQualifiedPrefixPath", - "geoPointEquals", - "getLocalWriteTime", "hardAssert", "invalidClassError", - "isArray", "isEmpty", "isNegativeZero", "isPlainObject", "isSafeInteger", - "isServerTimestamp", "isWrite", "loadProtos", "logDebug", @@ -1595,12 +1532,6 @@ "newDatastore", "newSerializer", "nodePromise", - "normalizeByteString", - "normalizeNumber", - "normalizeTimestamp", - "numberEquals", - "objectEquals", - "objectSize", "ordinal", "parseArray", "parseData", @@ -1611,7 +1542,6 @@ "registerFirestore", "terminate", "terminateDatastore", - "timestampEquals", "toBytes", "toDouble", "toInteger", @@ -1619,15 +1549,13 @@ "toResourceName", "toTimestamp", "tryGetCustomObjectType", - "typeOrder", "uint8ArrayFromBinaryString", "validateArgType", "validateAtLeastNumberOfArgs", "validateExactNumberOfArgs", "validatePlainObject", "validateType", - "valueDescription", - "valueEquals" + "valueDescription" ], "classes": [ "ArrayUnionFieldValueImpl", @@ -1656,11 +1584,12 @@ "SerializableFieldValue", "StreamBridge", "Timestamp", + "TransformOperation", "User" ], "variables": [] }, - "sizeInBytes": 42391 + "sizeInBytes": 36963 }, "collection": { "dependencies": { @@ -1804,7 +1733,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -1824,7 +1752,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -1841,19 +1768,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 99790 + "sizeInBytes": 91658 }, "collectionGroup": { "dependencies": { @@ -1995,7 +1920,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -2015,7 +1939,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -2032,50 +1955,36 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 99222 + "sizeInBytes": 91090 }, "deleteDoc": { "dependencies": { "functions": [ "argToString", - "arrayEquals", - "binaryStringFromUint8Array", - "blobEquals", "cast", - "coercedFieldValuesArray", "createMetadata", "debugAssert", "debugCast", - "decodeBase64", "deleteDoc", - "encodeBase64", "fail", "formatJSON", "fullyQualifiedPrefixPath", - "geoPointEquals", "getEncodedDatabaseId", - "getLocalWriteTime", "hardAssert", "invokeCommitRpc", - "isArray", "isDouble", "isInteger", - "isMapValue", - "isNegativeZero", "isNumber", - "isServerTimestamp", "loadProtos", "logDebug", "logError", @@ -2085,76 +1994,54 @@ "newDatastore", "newSerializer", "nodePromise", - "normalizeByteString", - "normalizeNumber", - "normalizeTimestamp", - "numberEquals", - "objectEquals", - "objectSize", "primitiveComparator", "registerFirestore", - "serverTimestamp", "terminate", "terminateDatastore", - "timestampEquals", "toDocumentMask", - "toDouble", "toFieldTransform", - "toInteger", "toMutation", "toMutationDocument", "toName", "toPrecondition", "toResourceName", "toTimestamp", - "toVersion", - "typeOrder", - "uint8ArrayFromBinaryString", - "valueEquals" + "toVersion" ], "classes": [ "ArrayRemoveTransformOperation", "ArrayUnionTransformOperation", "BasePath", - "ByteString", "DatabaseId", "DatabaseInfo", "Datastore", "DatastoreImpl", "Deferred", "DeleteMutation", - "Document", "DocumentKeyReference", "DocumentReference", - "FieldPath", "FirebaseCredentialsProvider", "Firestore", "FirestoreError", "GrpcConnection", "JsonProtoSerializer", - "MaybeDocument", "Mutation", - "NoDocument", "NumericIncrementTransformOperation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "PatchMutation", "Precondition", "ResourcePath", "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", - "Timestamp", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "VerifyMutation" ], "variables": [] }, - "sizeInBytes": 50238 + "sizeInBytes": 26210 }, "deleteField": { "dependencies": { @@ -2349,7 +2236,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -2369,7 +2255,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -2386,19 +2271,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 100611 + "sizeInBytes": 92479 }, "documentId": { "dependencies": { @@ -2734,6 +2617,7 @@ "uint8ArrayFromBinaryString", "validateArgType", "validateExactNumberOfArgs", + "validateHasExplicitOrderByForLimitToLast", "validateNamedArrayAtLeastNumberOfElements", "validatePlainObject", "validatePositiveNumber", @@ -2798,23 +2682,20 @@ "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 91235 + "sizeInBytes": 84898 }, "increment": { "dependencies": { "functions": [ "argToString", - "arrayEquals", "assertUint8ArrayAvailable", "binaryStringFromUint8Array", - "blobEquals", "cast", "createError", "createMetadata", @@ -2828,8 +2709,6 @@ "formatJSON", "formatPlural", "fullyQualifiedPrefixPath", - "geoPointEquals", - "getLocalWriteTime", "hardAssert", "increment", "invalidClassError", @@ -2840,7 +2719,6 @@ "isNumber", "isPlainObject", "isSafeInteger", - "isServerTimestamp", "isWrite", "loadProtos", "logDebug", @@ -2852,12 +2730,6 @@ "newDatastore", "newSerializer", "nodePromise", - "normalizeByteString", - "normalizeNumber", - "normalizeTimestamp", - "numberEquals", - "objectEquals", - "objectSize", "ordinal", "parseArray", "parseData", @@ -2868,7 +2740,6 @@ "registerFirestore", "terminate", "terminateDatastore", - "timestampEquals", "toBytes", "toDouble", "toInteger", @@ -2876,14 +2747,12 @@ "toResourceName", "toTimestamp", "tryGetCustomObjectType", - "typeOrder", "uint8ArrayFromBinaryString", "validateArgType", "validateExactNumberOfArgs", "validatePlainObject", "validateType", - "valueDescription", - "valueEquals" + "valueDescription" ], "classes": [ "BasePath", @@ -2912,11 +2781,12 @@ "SerializableFieldValue", "StreamBridge", "Timestamp", + "TransformOperation", "User" ], "variables": [] }, - "sizeInBytes": 42341 + "sizeInBytes": 36914 }, "initializeFirestore": { "dependencies": { @@ -3104,7 +2974,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -3124,7 +2993,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -3141,19 +3009,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 99505 + "sizeInBytes": 91373 }, "queryEqual": { "dependencies": { @@ -3270,7 +3136,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -3290,7 +3155,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -3306,18 +3170,16 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 85680 + "sizeInBytes": 77548 }, "refEqual": { "dependencies": { @@ -3460,7 +3322,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -3480,7 +3341,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -3497,19 +3357,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 99447 + "sizeInBytes": 91315 }, "runTransaction": { "dependencies": { @@ -3522,7 +3380,6 @@ "binaryStringFromUint8Array", "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "debugAssert", @@ -3558,7 +3415,6 @@ "invalidClassError", "invokeBatchGetDocumentsRpc", "invokeCommitRpc", - "isArray", "isDouble", "isEmpty", "isIndexedDbTransactionError", @@ -3599,7 +3455,6 @@ "primitiveComparator", "registerFirestore", "runTransaction", - "serverTimestamp", "terminate", "terminateDatastore", "timestampEquals", @@ -3684,7 +3539,7 @@ "Transaction$1", "TransactionRunner", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "UserDataReader", "UserDataWriter", @@ -3692,7 +3547,7 @@ ], "variables": [] }, - "sizeInBytes": 91963 + "sizeInBytes": 81867 }, "serverTimestamp": { "dependencies": { @@ -3717,7 +3572,6 @@ "primitiveComparator", "registerFirestore", "serverTimestamp", - "serverTimestamp$1", "terminate", "terminateDatastore" ], @@ -3740,11 +3594,12 @@ "ServerTimestampFieldValueImpl", "ServerTimestampTransform", "StreamBridge", + "TransformOperation", "User" ], "variables": [] }, - "sizeInBytes": 19024 + "sizeInBytes": 18118 }, "setDoc": { "dependencies": { @@ -3756,7 +3611,6 @@ "binaryStringFromUint8Array", "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "debugAssert", @@ -3779,7 +3633,6 @@ "hardAssert", "invalidClassError", "invokeCommitRpc", - "isArray", "isDouble", "isEmpty", "isInteger", @@ -3815,7 +3668,6 @@ "parseSentinelFieldValue", "primitiveComparator", "registerFirestore", - "serverTimestamp", "setDoc", "terminate", "terminateDatastore", @@ -3858,7 +3710,6 @@ "Deferred", "DeleteFieldValueImpl", "DeleteMutation", - "Document", "DocumentKeyReference", "DocumentReference", "FieldMask", @@ -3870,9 +3721,7 @@ "GeoPoint", "GrpcConnection", "JsonProtoSerializer", - "MaybeDocument", "Mutation", - "NoDocument", "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", @@ -3886,18 +3735,17 @@ "SerializableFieldValue", "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "UserDataReader", "VerifyMutation" ], "variables": [] }, - "sizeInBytes": 70801 + "sizeInBytes": 58711 }, "setLogLevel": { "dependencies": { @@ -4060,7 +3908,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -4080,7 +3927,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -4097,18 +3943,16 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 86413 + "sizeInBytes": 78281 }, "terminate": { "dependencies": { @@ -4163,7 +4007,6 @@ "binaryStringFromUint8Array", "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "debugAssert", @@ -4186,7 +4029,6 @@ "hardAssert", "invalidClassError", "invokeCommitRpc", - "isArray", "isDouble", "isEmpty", "isInteger", @@ -4222,7 +4064,6 @@ "parseSentinelFieldValue", "primitiveComparator", "registerFirestore", - "serverTimestamp", "terminate", "terminateDatastore", "timestampEquals", @@ -4265,7 +4106,6 @@ "Deferred", "DeleteFieldValueImpl", "DeleteMutation", - "Document", "DocumentKeyReference", "DocumentReference", "FieldMask", @@ -4278,9 +4118,7 @@ "GeoPoint", "GrpcConnection", "JsonProtoSerializer", - "MaybeDocument", "Mutation", - "NoDocument", "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", @@ -4294,18 +4132,17 @@ "SerializableFieldValue", "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "UserDataReader", "VerifyMutation" ], "variables": [] }, - "sizeInBytes": 70838 + "sizeInBytes": 58748 }, "writeBatch": { "dependencies": { @@ -4317,7 +4154,6 @@ "binaryStringFromUint8Array", "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "debugAssert", @@ -4340,7 +4176,6 @@ "hardAssert", "invalidClassError", "invokeCommitRpc", - "isArray", "isDouble", "isEmpty", "isInteger", @@ -4376,7 +4211,6 @@ "parseSentinelFieldValue", "primitiveComparator", "registerFirestore", - "serverTimestamp", "terminate", "terminateDatastore", "timestampEquals", @@ -4420,7 +4254,6 @@ "Deferred", "DeleteFieldValueImpl", "DeleteMutation", - "Document", "DocumentKeyReference", "DocumentReference", "FieldMask", @@ -4433,9 +4266,7 @@ "GeoPoint", "GrpcConnection", "JsonProtoSerializer", - "MaybeDocument", "Mutation", - "NoDocument", "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", @@ -4449,11 +4280,10 @@ "SerializableFieldValue", "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "UserDataReader", "VerifyMutation", @@ -4461,6 +4291,6 @@ ], "variables": [] }, - "sizeInBytes": 72592 + "sizeInBytes": 60620 } } \ No newline at end of file diff --git a/packages/firestore/src/api/field_value.ts b/packages/firestore/src/api/field_value.ts index d1dd98db81a..731445539b0 100644 --- a/packages/firestore/src/api/field_value.ts +++ b/packages/firestore/src/api/field_value.ts @@ -123,7 +123,7 @@ export class ServerTimestampFieldValueImpl extends SerializableFieldValue { } _toFieldTransform(context: ParseContext): FieldTransform { - return new FieldTransform(context.path!, ServerTimestampTransform.instance); + return new FieldTransform(context.path!, new ServerTimestampTransform()); } isEqual(other: FieldValue): boolean { diff --git a/packages/firestore/src/local/local_documents_view.ts b/packages/firestore/src/local/local_documents_view.ts index 75c43f8dafc..16694311d9e 100644 --- a/packages/firestore/src/local/local_documents_view.ts +++ b/packages/firestore/src/local/local_documents_view.ts @@ -35,7 +35,7 @@ import { ResourcePath } from '../model/path'; import { debugAssert } from '../util/assert'; import { IndexManager } from './index_manager'; import { MutationQueue } from './mutation_queue'; -import { PatchMutation } from '../model/mutation'; +import { applyMutationToLocalView, PatchMutation } from '../model/mutation'; import { PersistenceTransaction } from './persistence'; import { PersistencePromise } from './persistence_promise'; import { RemoteDocumentCache } from './remote_document_cache'; @@ -258,7 +258,8 @@ export class LocalDocumentsView { for (const mutation of batch.mutations) { const key = mutation.key; const baseDoc = results.get(key); - const mutatedDoc = mutation.applyToLocalView( + const mutatedDoc = applyMutationToLocalView( + mutation, baseDoc, baseDoc, batch.localWriteTime diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index c996fe532c1..151afa3335b 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -30,7 +30,12 @@ import { } from '../model/collections'; import { MaybeDocument, NoDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; -import { Mutation, PatchMutation, Precondition } from '../model/mutation'; +import { + Mutation, + PatchMutation, + Precondition, + extractMutationBaseValue +} from '../model/mutation'; import { BATCHID_UNKNOWN, MutationBatch, @@ -449,7 +454,8 @@ class LocalStoreImpl implements LocalStore { const baseMutations: Mutation[] = []; for (const mutation of mutations) { - const baseValue = mutation.extractBaseValue( + const baseValue = extractMutationBaseValue( + mutation, existingDocs.get(mutation.key) ); if (baseValue != null) { diff --git a/packages/firestore/src/model/mutation.ts b/packages/firestore/src/model/mutation.ts index 9777ba25229..bef77cfd819 100644 --- a/packages/firestore/src/model/mutation.ts +++ b/packages/firestore/src/model/mutation.ts @@ -19,7 +19,7 @@ import * as api from '../protos/firestore_proto_api'; import { Timestamp } from '../api/timestamp'; import { SnapshotVersion } from '../core/snapshot_version'; -import { debugAssert, fail, hardAssert } from '../util/assert'; +import { debugAssert, hardAssert } from '../util/assert'; import { Document, @@ -30,7 +30,13 @@ import { import { DocumentKey } from './document_key'; import { ObjectValue, ObjectValueBuilder } from './object_value'; import { FieldPath } from './path'; -import { TransformOperation } from './transform_operation'; +import { + applyTransformOperationToLocalView, + applyTransformOperationToRemoteDocument, + computeTransformOperationBaseValue, + TransformOperation, + transformOperationEquals +} from './transform_operation'; import { arrayEquals } from '../util/misc'; /** @@ -81,12 +87,16 @@ export class FieldTransform { readonly field: FieldPath, readonly transform: TransformOperation ) {} +} - isEqual(other: FieldTransform): boolean { - return ( - this.field.isEqual(other.field) && this.transform.isEqual(other.transform) - ); - } +export function fieldTransformEquals( + l: FieldTransform, + r: FieldTransform +): boolean { + return ( + l.field.isEqual(r.field) && + transformOperationEquals(l.transform, r.transform) + ); } /** The result of successfully applying a mutation to the backend. */ @@ -158,24 +168,6 @@ export class Precondition { return this.updateTime === undefined && this.exists === undefined; } - /** - * Returns true if the preconditions is valid for the given document - * (or null if no document is available). - */ - isValidFor(maybeDoc: MaybeDocument | null): boolean { - if (this.updateTime !== undefined) { - return ( - maybeDoc instanceof Document && - maybeDoc.version.isEqual(this.updateTime) - ); - } else if (this.exists !== undefined) { - return this.exists === maybeDoc instanceof Document; - } else { - debugAssert(this.isNone, 'Precondition should be empty'); - return true; - } - } - isEqual(other: Precondition): boolean { return ( this.exists === other.exists && @@ -186,6 +178,27 @@ export class Precondition { } } +/** + * Returns true if the preconditions is valid for the given document + * (or null if no document is available). + */ +export function preconditionIsValidForDocument( + precondition: Precondition, + maybeDoc: MaybeDocument | null +): boolean { + if (precondition.updateTime !== undefined) { + return ( + maybeDoc instanceof Document && + maybeDoc.version.isEqual(precondition.updateTime) + ); + } else if (precondition.exists !== undefined) { + return precondition.exists === maybeDoc instanceof Document; + } else { + debugAssert(precondition.isNone, 'Precondition should be empty'); + return true; + } +} + /** * A mutation describes a self-contained change to a document. Mutations can * create, replace, delete, and update subsets of documents. @@ -239,89 +252,187 @@ export abstract class Mutation { abstract readonly type: MutationType; abstract readonly key: DocumentKey; abstract readonly precondition: Precondition; +} - /** - * Applies this mutation to the given MaybeDocument or null for the purposes - * of computing a new remote document. If the input document doesn't match the - * expected state (e.g. it is null or outdated), an `UnknownDocument` can be - * returned. - * - * @param maybeDoc The document to mutate. The input document can be null if - * the client has no knowledge of the pre-mutation state of the document. - * @param mutationResult The result of applying the mutation from the backend. - * @return The mutated document. The returned document may be an - * UnknownDocument if the mutation could not be applied to the locally - * cached base document. - */ - abstract applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument; +/** + * Applies this mutation to the given MaybeDocument or null for the purposes + * of computing a new remote document. If the input document doesn't match the + * expected state (e.g. it is null or outdated), an `UnknownDocument` can be + * returned. + * + * @param mutation The mutation to apply. + * @param maybeDoc The document to mutate. The input document can be null if + * the client has no knowledge of the pre-mutation state of the document. + * @param mutationResult The result of applying the mutation from the backend. + * @return The mutated document. The returned document may be an + * UnknownDocument if the mutation could not be applied to the locally + * cached base document. + */ +export function applyMutationToRemoteDocument( + mutation: Mutation, + maybeDoc: MaybeDocument | null, + mutationResult: MutationResult +): MaybeDocument { + verifyMutationKeyMatches(mutation, maybeDoc); + if (mutation instanceof SetMutation) { + return applySetMutationToRemoteDocument(mutation, maybeDoc, mutationResult); + } else if (mutation instanceof PatchMutation) { + return applyPatchMutationToRemoteDocument( + mutation, + maybeDoc, + mutationResult + ); + } else if (mutation instanceof TransformMutation) { + return applyTransformMutationToRemoteDocument( + mutation, + maybeDoc, + mutationResult + ); + } else { + debugAssert( + mutation instanceof DeleteMutation, + 'Unexpected mutation type: ' + mutation + ); + return applyDeleteMutationToRemoteDocument( + mutation, + maybeDoc, + mutationResult + ); + } +} - /** - * Applies this mutation to the given MaybeDocument or null for the purposes - * of computing the new local view of a document. Both the input and returned - * documents can be null. - * - * @param maybeDoc The document to mutate. The input document can be null if - * the client has no knowledge of the pre-mutation state of the document. - * @param baseDoc The state of the document prior to this mutation batch. The - * input document can be null if the client has no knowledge of the - * pre-mutation state of the document. - * @param localWriteTime A timestamp indicating the local write time of the - * batch this mutation is a part of. - * @return The mutated document. The returned document may be null, but only - * if maybeDoc was null and the mutation would not create a new document. - */ - abstract applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null; +/** + * Applies this mutation to the given MaybeDocument or null for the purposes + * of computing the new local view of a document. Both the input and returned + * documents can be null. + * + * @param mutation The mutation to apply. + * @param maybeDoc The document to mutate. The input document can be null if + * the client has no knowledge of the pre-mutation state of the document. + * @param baseDoc The state of the document prior to this mutation batch. The + * input document can be null if the client has no knowledge of the + * pre-mutation state of the document. + * @param localWriteTime A timestamp indicating the local write time of the + * batch this mutation is a part of. + * @return The mutated document. The returned document may be null, but only + * if maybeDoc was null and the mutation would not create a new document. + */ +export function applyMutationToLocalView( + mutation: Mutation, + maybeDoc: MaybeDocument | null, + baseDoc: MaybeDocument | null, + localWriteTime: Timestamp +): MaybeDocument | null { + verifyMutationKeyMatches(mutation, maybeDoc); + + if (mutation instanceof SetMutation) { + return applySetMutationToLocalView(mutation, maybeDoc); + } else if (mutation instanceof PatchMutation) { + return applyPatchMutationToLocalView(mutation, maybeDoc); + } else if (mutation instanceof TransformMutation) { + return applyTransformMutationToLocalView( + mutation, + maybeDoc, + localWriteTime, + baseDoc + ); + } else { + debugAssert( + mutation instanceof DeleteMutation, + 'Unexpected mutation type: ' + mutation + ); + return applyDeleteMutationToLocalView(mutation, maybeDoc); + } +} - /** - * If this mutation is not idempotent, returns the base value to persist with - * this mutation. If a base value is returned, the mutation is always applied - * to this base value, even if document has already been updated. - * - * The base value is a sparse object that consists of only the document - * fields for which this mutation contains a non-idempotent transformation - * (e.g. a numeric increment). The provided value guarantees consistent - * behavior for non-idempotent transforms and allow us to return the same - * latency-compensated value even if the backend has already applied the - * mutation. The base value is null for idempotent mutations, as they can be - * re-played even if the backend has already applied them. - * - * @return a base value to store along with the mutation, or null for - * idempotent mutations. - */ - abstract extractBaseValue(maybeDoc: MaybeDocument | null): ObjectValue | null; +/** + * If this mutation is not idempotent, returns the base value to persist with + * this mutation. If a base value is returned, the mutation is always applied + * to this base value, even if document has already been updated. + * + * The base value is a sparse object that consists of only the document + * fields for which this mutation contains a non-idempotent transformation + * (e.g. a numeric increment). The provided value guarantees consistent + * behavior for non-idempotent transforms and allow us to return the same + * latency-compensated value even if the backend has already applied the + * mutation. The base value is null for idempotent mutations, as they can be + * re-played even if the backend has already applied them. + * + * @return a base value to store along with the mutation, or null for + * idempotent mutations. + */ +export function extractMutationBaseValue( + mutation: Mutation, + maybeDoc: MaybeDocument | null +): ObjectValue | null { + if (mutation instanceof TransformMutation) { + return extractTransformMutationBaseValue(mutation, maybeDoc); + } + return null; +} - abstract isEqual(other: Mutation): boolean; +export function mutationEquals(left: Mutation, right: Mutation): boolean { + if (left.type !== right.type) { + return false; + } - protected verifyKeyMatches(maybeDoc: MaybeDocument | null): void { - if (maybeDoc != null) { - debugAssert( - maybeDoc.key.isEqual(this.key), - 'Can only apply a mutation to a document with the same key' - ); - } + if (!left.key.isEqual(right.key)) { + return false; } - /** - * Returns the version from the given document for use as the result of a - * mutation. Mutations are defined to return the version of the base document - * only if it is an existing document. Deleted and unknown documents have a - * post-mutation version of SnapshotVersion.min(). - */ - protected static getPostMutationVersion( - maybeDoc: MaybeDocument | null - ): SnapshotVersion { - if (maybeDoc instanceof Document) { - return maybeDoc.version; - } else { - return SnapshotVersion.min(); - } + if (!left.precondition.isEqual(right.precondition)) { + return false; + } + + if (left.type === MutationType.Set) { + return (left as SetMutation).value.isEqual((right as SetMutation).value); + } + + if (left.type === MutationType.Patch) { + return ( + (left as PatchMutation).data.isEqual((right as PatchMutation).data) && + (left as PatchMutation).fieldMask.isEqual( + (right as PatchMutation).fieldMask + ) + ); + } + + if (left.type === MutationType.Transform) { + return arrayEquals( + (left as TransformMutation).fieldTransforms, + (left as TransformMutation).fieldTransforms, + (l, r) => fieldTransformEquals(l, r) + ); + } + + return true; +} + +function verifyMutationKeyMatches( + mutation: Mutation, + maybeDoc: MaybeDocument | null +): void { + if (maybeDoc != null) { + debugAssert( + maybeDoc.key.isEqual(mutation.key), + 'Can only apply a mutation to a document with the same key' + ); + } +} + +/** + * Returns the version from the given document for use as the result of a + * mutation. Mutations are defined to return the version of the base document + * only if it is an existing document. Deleted and unknown documents have a + * post-mutation version of SnapshotVersion.min(). + */ +function getPostMutationVersion( + maybeDoc: MaybeDocument | null +): SnapshotVersion { + if (maybeDoc instanceof Document) { + return maybeDoc.version; + } else { + return SnapshotVersion.min(); } } @@ -339,57 +450,38 @@ export class SetMutation extends Mutation { } readonly type: MutationType = MutationType.Set; +} - applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument { - this.verifyKeyMatches(maybeDoc); - - debugAssert( - mutationResult.transformResults == null, - 'Transform results received by SetMutation.' - ); - - // Unlike applyToLocalView, if we're applying a mutation to a remote - // document the server has accepted the mutation so the precondition must - // have held. - - const version = mutationResult.version; - return new Document(this.key, version, this.value, { - hasCommittedMutations: true - }); - } - - applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null { - this.verifyKeyMatches(maybeDoc); - - if (!this.precondition.isValidFor(maybeDoc)) { - return maybeDoc; - } - - const version = Mutation.getPostMutationVersion(maybeDoc); - return new Document(this.key, version, this.value, { - hasLocalMutations: true - }); - } +function applySetMutationToRemoteDocument( + mutation: SetMutation, + maybeDoc: MaybeDocument | null, + mutationResult: MutationResult +): Document { + debugAssert( + mutationResult.transformResults == null, + 'Transform results received by SetMutation.' + ); + + // Unlike applySetMutationToLocalView, if we're applying a mutation to a + // remote document the server has accepted the mutation so the precondition + // must have held. + return new Document(mutation.key, mutationResult.version, mutation.value, { + hasCommittedMutations: true + }); +} - extractBaseValue(maybeDoc: MaybeDocument | null): null { - return null; +function applySetMutationToLocalView( + mutation: SetMutation, + maybeDoc: MaybeDocument | null +): MaybeDocument | null { + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + return maybeDoc; } - isEqual(other: Mutation): boolean { - return ( - other instanceof SetMutation && - this.key.isEqual(other.key) && - this.value.isEqual(other.value) && - this.precondition.isEqual(other.precondition) - ); - } + const version = getPostMutationVersion(maybeDoc); + return new Document(mutation.key, version, mutation.value, { + hasLocalMutations: true + }); } /** @@ -416,92 +508,78 @@ export class PatchMutation extends Mutation { } readonly type: MutationType = MutationType.Patch; +} - applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument { - this.verifyKeyMatches(maybeDoc); - - debugAssert( - mutationResult.transformResults == null, - 'Transform results received by PatchMutation.' - ); - - if (!this.precondition.isValidFor(maybeDoc)) { - // Since the mutation was not rejected, we know that the precondition - // matched on the backend. We therefore must not have the expected version - // of the document in our cache and return an UnknownDocument with the - // known updateTime. - return new UnknownDocument(this.key, mutationResult.version); - } - - const newData = this.patchDocument(maybeDoc); - return new Document(this.key, mutationResult.version, newData, { - hasCommittedMutations: true - }); - } - - applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null { - this.verifyKeyMatches(maybeDoc); - - if (!this.precondition.isValidFor(maybeDoc)) { - return maybeDoc; - } +function applyPatchMutationToRemoteDocument( + mutation: PatchMutation, + maybeDoc: MaybeDocument | null, + mutationResult: MutationResult +): MaybeDocument { + debugAssert( + mutationResult.transformResults == null, + 'Transform results received by PatchMutation.' + ); + + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + // Since the mutation was not rejected, we know that the precondition + // matched on the backend. We therefore must not have the expected version + // of the document in our cache and return an UnknownDocument with the + // known updateTime. + return new UnknownDocument(mutation.key, mutationResult.version); + } + + const newData = patchDocument(mutation, maybeDoc); + return new Document(mutation.key, mutationResult.version, newData, { + hasCommittedMutations: true + }); +} - const version = Mutation.getPostMutationVersion(maybeDoc); - const newData = this.patchDocument(maybeDoc); - return new Document(this.key, version, newData, { - hasLocalMutations: true - }); +function applyPatchMutationToLocalView( + mutation: PatchMutation, + maybeDoc: MaybeDocument | null +): MaybeDocument | null { + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + return maybeDoc; } - extractBaseValue(maybeDoc: MaybeDocument | null): null { - return null; - } + const version = getPostMutationVersion(maybeDoc); + const newData = patchDocument(mutation, maybeDoc); + return new Document(mutation.key, version, newData, { + hasLocalMutations: true + }); +} - isEqual(other: Mutation): boolean { - return ( - other instanceof PatchMutation && - this.key.isEqual(other.key) && - this.fieldMask.isEqual(other.fieldMask) && - this.precondition.isEqual(other.precondition) - ); - } +/** + * Patches the data of document if available or creates a new document. Note + * that this does not check whether or not the precondition of this patch + * holds. + */ +function patchDocument( + mutation: PatchMutation, + maybeDoc: MaybeDocument | null +): ObjectValue { + let data: ObjectValue; + if (maybeDoc instanceof Document) { + data = maybeDoc.data(); + } else { + data = ObjectValue.empty(); + } + return patchObject(mutation, data); +} - /** - * Patches the data of document if available or creates a new document. Note - * that this does not check whether or not the precondition of this patch - * holds. - */ - private patchDocument(maybeDoc: MaybeDocument | null): ObjectValue { - let data: ObjectValue; - if (maybeDoc instanceof Document) { - data = maybeDoc.data(); - } else { - data = ObjectValue.empty(); - } - return this.patchObject(data); - } - - private patchObject(data: ObjectValue): ObjectValue { - const builder = new ObjectValueBuilder(data); - this.fieldMask.fields.forEach(fieldPath => { - if (!fieldPath.isEmpty()) { - const newValue = this.data.field(fieldPath); - if (newValue !== null) { - builder.set(fieldPath, newValue); - } else { - builder.delete(fieldPath); - } +function patchObject(mutation: PatchMutation, data: ObjectValue): ObjectValue { + const builder = new ObjectValueBuilder(data); + mutation.fieldMask.fields.forEach(fieldPath => { + if (!fieldPath.isEmpty()) { + const newValue = mutation.data.field(fieldPath); + if (newValue !== null) { + builder.set(fieldPath, newValue); + } else { + builder.delete(fieldPath); } - }); - return builder.build(); - } + } + }); + return builder.build(); } /** @@ -527,211 +605,216 @@ export class TransformMutation extends Mutation { ) { super(); } +} - applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument { - this.verifyKeyMatches(maybeDoc); - - hardAssert( - mutationResult.transformResults != null, - 'Transform results missing for TransformMutation.' - ); - - if (!this.precondition.isValidFor(maybeDoc)) { - // Since the mutation was not rejected, we know that the precondition - // matched on the backend. We therefore must not have the expected version - // of the document in our cache and return an UnknownDocument with the - // known updateTime. - return new UnknownDocument(this.key, mutationResult.version); - } - - const doc = this.requireDocument(maybeDoc); - const transformResults = this.serverTransformResults( - maybeDoc, - mutationResult.transformResults! - ); +function applyTransformMutationToRemoteDocument( + mutation: TransformMutation, + maybeDoc: MaybeDocument | null, + mutationResult: MutationResult +): Document | UnknownDocument { + hardAssert( + mutationResult.transformResults != null, + 'Transform results missing for TransformMutation.' + ); + + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + // Since the mutation was not rejected, we know that the precondition + // matched on the backend. We therefore must not have the expected version + // of the document in our cache and return an UnknownDocument with the + // known updateTime. + return new UnknownDocument(mutation.key, mutationResult.version); + } + + const doc = requireDocument(mutation, maybeDoc); + const transformResults = serverTransformResults( + mutation.fieldTransforms, + maybeDoc, + mutationResult.transformResults! + ); + + const version = mutationResult.version; + const newData = transformObject(mutation, doc.data(), transformResults); + return new Document(mutation.key, version, newData, { + hasCommittedMutations: true + }); +} - const version = mutationResult.version; - const newData = this.transformObject(doc.data(), transformResults); - return new Document(this.key, version, newData, { - hasCommittedMutations: true - }); +function applyTransformMutationToLocalView( + mutation: TransformMutation, + maybeDoc: MaybeDocument | null, + localWriteTime: Timestamp, + baseDoc: MaybeDocument | null +): MaybeDocument | null { + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + return maybeDoc; } - applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null { - this.verifyKeyMatches(maybeDoc); - - if (!this.precondition.isValidFor(maybeDoc)) { - return maybeDoc; - } + const doc = requireDocument(mutation, maybeDoc); + const transformResults = localTransformResults( + mutation.fieldTransforms, + localWriteTime, + maybeDoc, + baseDoc + ); + const newData = transformObject(mutation, doc.data(), transformResults); + return new Document(mutation.key, doc.version, newData, { + hasLocalMutations: true + }); +} - const doc = this.requireDocument(maybeDoc); - const transformResults = this.localTransformResults( - localWriteTime, - maybeDoc, - baseDoc +function extractTransformMutationBaseValue( + mutation: TransformMutation, + maybeDoc: MaybeDocument | null | Document +): ObjectValue | null { + let baseObject: ObjectValueBuilder | null = null; + for (const fieldTransform of mutation.fieldTransforms) { + const existingValue = + maybeDoc instanceof Document + ? maybeDoc.field(fieldTransform.field) + : undefined; + const coercedValue = computeTransformOperationBaseValue( + fieldTransform.transform, + existingValue || null ); - const newData = this.transformObject(doc.data(), transformResults); - return new Document(this.key, doc.version, newData, { - hasLocalMutations: true - }); - } - - extractBaseValue(maybeDoc: MaybeDocument | null): ObjectValue | null { - let baseObject: ObjectValueBuilder | null = null; - for (const fieldTransform of this.fieldTransforms) { - const existingValue = - maybeDoc instanceof Document - ? maybeDoc.field(fieldTransform.field) - : undefined; - const coercedValue = fieldTransform.transform.computeBaseValue( - existingValue || null - ); - - if (coercedValue != null) { - if (baseObject == null) { - baseObject = new ObjectValueBuilder().set( - fieldTransform.field, - coercedValue - ); - } else { - baseObject = baseObject.set(fieldTransform.field, coercedValue); - } + + if (coercedValue != null) { + if (baseObject == null) { + baseObject = new ObjectValueBuilder().set( + fieldTransform.field, + coercedValue + ); + } else { + baseObject = baseObject.set(fieldTransform.field, coercedValue); } } - return baseObject ? baseObject.build() : null; } + return baseObject ? baseObject.build() : null; +} - isEqual(other: Mutation): boolean { - return ( - other instanceof TransformMutation && - this.key.isEqual(other.key) && - arrayEquals(this.fieldTransforms, other.fieldTransforms, (l, r) => - l.isEqual(r) - ) && - this.precondition.isEqual(other.precondition) - ); - } +/** + * Asserts that the given MaybeDocument is actually a Document and verifies + * that it matches the key for this mutation. Since we only support + * transformations with precondition exists this method is guaranteed to be + * safe. + */ +function requireDocument( + mutation: Mutation, + maybeDoc: MaybeDocument | null +): Document { + debugAssert( + maybeDoc instanceof Document, + 'Unknown MaybeDocument type ' + maybeDoc + ); + debugAssert( + maybeDoc.key.isEqual(mutation.key), + 'Can only transform a document with the same key' + ); + return maybeDoc; +} - /** - * Asserts that the given MaybeDocument is actually a Document and verifies - * that it matches the key for this mutation. Since we only support - * transformations with precondition exists this method is guaranteed to be - * safe. - */ - private requireDocument(maybeDoc: MaybeDocument | null): Document { - debugAssert( - maybeDoc instanceof Document, - 'Unknown MaybeDocument type ' + maybeDoc - ); - debugAssert( - maybeDoc.key.isEqual(this.key), - 'Can only transform a document with the same key' +/** + * Creates a list of "transform results" (a transform result is a field value + * representing the result of applying a transform) for use after a + * TransformMutation has been acknowledged by the server. + * + * @param fieldTransforms The field transforms to apply the result to. + * @param baseDoc The document prior to applying this mutation batch. + * @param serverTransformResults The transform results received by the server. + * @return The transform results list. + */ +function serverTransformResults( + fieldTransforms: FieldTransform[], + baseDoc: MaybeDocument | null, + serverTransformResults: Array +): api.Value[] { + const transformResults: api.Value[] = []; + hardAssert( + fieldTransforms.length === serverTransformResults.length, + `server transform result count (${serverTransformResults.length}) ` + + `should match field transform count (${fieldTransforms.length})` + ); + + for (let i = 0; i < serverTransformResults.length; i++) { + const fieldTransform = fieldTransforms[i]; + const transform = fieldTransform.transform; + let previousValue: api.Value | null = null; + if (baseDoc instanceof Document) { + previousValue = baseDoc.field(fieldTransform.field); + } + transformResults.push( + applyTransformOperationToRemoteDocument( + transform, + previousValue, + serverTransformResults[i] + ) ); - return maybeDoc; } + return transformResults; +} - /** - * Creates a list of "transform results" (a transform result is a field value - * representing the result of applying a transform) for use after a - * TransformMutation has been acknowledged by the server. - * - * @param baseDoc The document prior to applying this mutation batch. - * @param serverTransformResults The transform results received by the server. - * @return The transform results list. - */ - private serverTransformResults( - baseDoc: MaybeDocument | null, - serverTransformResults: Array - ): api.Value[] { - const transformResults: api.Value[] = []; - hardAssert( - this.fieldTransforms.length === serverTransformResults.length, - `server transform result count (${serverTransformResults.length}) ` + - `should match field transform count (${this.fieldTransforms.length})` - ); - - for (let i = 0; i < serverTransformResults.length; i++) { - const fieldTransform = this.fieldTransforms[i]; - const transform = fieldTransform.transform; - let previousValue: api.Value | null = null; - if (baseDoc instanceof Document) { - previousValue = baseDoc.field(fieldTransform.field); - } - transformResults.push( - transform.applyToRemoteDocument( - previousValue, - serverTransformResults[i] - ) - ); +/** + * Creates a list of "transform results" (a transform result is a field value + * representing the result of applying a transform) for use when applying a + * TransformMutation locally. + * + * @param fieldTransforms The field transforms to apply the result to. + * @param localWriteTime The local time of the transform mutation (used to + * generate ServerTimestampValues). + * @param maybeDoc The current state of the document after applying all + * previous mutations. + * @param baseDoc The document prior to applying this mutation batch. + * @return The transform results list. + */ +function localTransformResults( + fieldTransforms: FieldTransform[], + localWriteTime: Timestamp, + maybeDoc: MaybeDocument | null, + baseDoc: MaybeDocument | null +): api.Value[] { + const transformResults: api.Value[] = []; + for (const fieldTransform of fieldTransforms) { + const transform = fieldTransform.transform; + + let previousValue: api.Value | null = null; + if (maybeDoc instanceof Document) { + previousValue = maybeDoc.field(fieldTransform.field); } - return transformResults; - } - /** - * Creates a list of "transform results" (a transform result is a field value - * representing the result of applying a transform) for use when applying a - * TransformMutation locally. - * - * @param localWriteTime The local time of the transform mutation (used to - * generate ServerTimestampValues). - * @param maybeDoc The current state of the document after applying all - * previous mutations. - * @param baseDoc The document prior to applying this mutation batch. - * @return The transform results list. - */ - private localTransformResults( - localWriteTime: Timestamp, - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null - ): api.Value[] { - const transformResults: api.Value[] = []; - for (const fieldTransform of this.fieldTransforms) { - const transform = fieldTransform.transform; - - let previousValue: api.Value | null = null; - if (maybeDoc instanceof Document) { - previousValue = maybeDoc.field(fieldTransform.field); - } - - if (previousValue === null && baseDoc instanceof Document) { - // If the current document does not contain a value for the mutated - // field, use the value that existed before applying this mutation - // batch. This solves an edge case where a PatchMutation clears the - // values in a nested map before the TransformMutation is applied. - previousValue = baseDoc.field(fieldTransform.field); - } - - transformResults.push( - transform.applyToLocalView(previousValue, localWriteTime) - ); + if (previousValue === null && baseDoc instanceof Document) { + // If the current document does not contain a value for the mutated + // field, use the value that existed before applying this mutation + // batch. This solves an edge case where a PatchMutation clears the + // values in a nested map before the TransformMutation is applied. + previousValue = baseDoc.field(fieldTransform.field); } - return transformResults; - } - private transformObject( - data: ObjectValue, - transformResults: api.Value[] - ): ObjectValue { - debugAssert( - transformResults.length === this.fieldTransforms.length, - 'TransformResults length mismatch.' + transformResults.push( + applyTransformOperationToLocalView( + transform, + previousValue, + localWriteTime + ) ); - - const builder = new ObjectValueBuilder(data); - for (let i = 0; i < this.fieldTransforms.length; i++) { - const fieldTransform = this.fieldTransforms[i]; - const fieldPath = fieldTransform.field; - builder.set(fieldPath, transformResults[i]); - } - return builder.build(); } + return transformResults; +} + +function transformObject( + mutation: TransformMutation, + data: ObjectValue, + transformResults: api.Value[] +): ObjectValue { + debugAssert( + transformResults.length === mutation.fieldTransforms.length, + 'TransformResults length mismatch.' + ); + + const builder = new ObjectValueBuilder(data); + for (let i = 0; i < mutation.fieldTransforms.length; i++) { + const fieldTransform = mutation.fieldTransforms[i]; + builder.set(fieldTransform.field, transformResults[i]); + } + return builder.build(); } /** A mutation that deletes the document at the given key. */ @@ -741,58 +824,42 @@ export class DeleteMutation extends Mutation { } readonly type: MutationType = MutationType.Delete; +} - applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument { - this.verifyKeyMatches(maybeDoc); - - debugAssert( - mutationResult.transformResults == null, - 'Transform results received by DeleteMutation.' - ); - - // Unlike applyToLocalView, if we're applying a mutation to a remote - // document the server has accepted the mutation so the precondition must - // have held. - - return new NoDocument(this.key, mutationResult.version, { - hasCommittedMutations: true - }); - } - - applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null { - this.verifyKeyMatches(maybeDoc); - - if (!this.precondition.isValidFor(maybeDoc)) { - return maybeDoc; - } - - if (maybeDoc) { - debugAssert( - maybeDoc.key.isEqual(this.key), - 'Can only apply mutation to document with same key' - ); - } - return new NoDocument(this.key, SnapshotVersion.min()); - } +function applyDeleteMutationToRemoteDocument( + mutation: DeleteMutation, + maybeDoc: MaybeDocument | null, + mutationResult: MutationResult +): NoDocument { + debugAssert( + mutationResult.transformResults == null, + 'Transform results received by DeleteMutation.' + ); + + // Unlike applyToLocalView, if we're applying a mutation to a remote + // document the server has accepted the mutation so the precondition must + // have held. + + return new NoDocument(mutation.key, mutationResult.version, { + hasCommittedMutations: true + }); +} - extractBaseValue(maybeDoc: MaybeDocument | null): null { - return null; +function applyDeleteMutationToLocalView( + mutation: DeleteMutation, + maybeDoc: MaybeDocument | null +): MaybeDocument | null { + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + return maybeDoc; } - isEqual(other: Mutation): boolean { - return ( - other instanceof DeleteMutation && - this.key.isEqual(other.key) && - this.precondition.isEqual(other.precondition) + if (maybeDoc) { + debugAssert( + maybeDoc.key.isEqual(mutation.key), + 'Can only apply mutation to document with same key' ); } + return new NoDocument(mutation.key, SnapshotVersion.min()); } /** @@ -808,31 +875,4 @@ export class VerifyMutation extends Mutation { } readonly type: MutationType = MutationType.Verify; - - applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument { - fail('VerifyMutation should only be used in Transactions.'); - } - - applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null { - fail('VerifyMutation should only be used in Transactions.'); - } - - extractBaseValue(maybeDoc: MaybeDocument | null): null { - fail('VerifyMutation should only be used in Transactions.'); - } - - isEqual(other: Mutation): boolean { - return ( - other instanceof VerifyMutation && - this.key.isEqual(other.key) && - this.precondition.isEqual(other.precondition) - ); - } } diff --git a/packages/firestore/src/model/mutation_batch.ts b/packages/firestore/src/model/mutation_batch.ts index 16044342263..31fcae1583a 100644 --- a/packages/firestore/src/model/mutation_batch.ts +++ b/packages/firestore/src/model/mutation_batch.ts @@ -18,7 +18,7 @@ import { Timestamp } from '../api/timestamp'; import { SnapshotVersion } from '../core/snapshot_version'; import { BatchId } from '../core/types'; -import { hardAssert, debugAssert } from '../util/assert'; +import { debugAssert, hardAssert } from '../util/assert'; import { arrayEquals } from '../util/misc'; import { documentKeySet, @@ -29,7 +29,13 @@ import { } from './collections'; import { MaybeDocument } from './document'; import { DocumentKey } from './document_key'; -import { Mutation, MutationResult } from './mutation'; +import { + applyMutationToLocalView, + applyMutationToRemoteDocument, + Mutation, + mutationEquals, + MutationResult +} from './mutation'; export const BATCHID_UNKNOWN = -1; @@ -91,7 +97,11 @@ export class MutationBatch { const mutation = this.mutations[i]; if (mutation.key.isEqual(docKey)) { const mutationResult = mutationResults[i]; - maybeDoc = mutation.applyToRemoteDocument(maybeDoc, mutationResult); + maybeDoc = applyMutationToRemoteDocument( + mutation, + maybeDoc, + mutationResult + ); } } return maybeDoc; @@ -120,7 +130,8 @@ export class MutationBatch { // transform against a consistent set of values. for (const mutation of this.baseMutations) { if (mutation.key.isEqual(docKey)) { - maybeDoc = mutation.applyToLocalView( + maybeDoc = applyMutationToLocalView( + mutation, maybeDoc, maybeDoc, this.localWriteTime @@ -133,7 +144,8 @@ export class MutationBatch { // Second, apply all user-provided mutations. for (const mutation of this.mutations) { if (mutation.key.isEqual(docKey)) { - maybeDoc = mutation.applyToLocalView( + maybeDoc = applyMutationToLocalView( + mutation, maybeDoc, baseDoc, this.localWriteTime @@ -174,9 +186,11 @@ export class MutationBatch { isEqual(other: MutationBatch): boolean { return ( this.batchId === other.batchId && - arrayEquals(this.mutations, other.mutations, (l, r) => l.isEqual(r)) && + arrayEquals(this.mutations, other.mutations, (l, r) => + mutationEquals(l, r) + ) && arrayEquals(this.baseMutations, other.baseMutations, (l, r) => - l.isEqual(r) + mutationEquals(l, r) ) ); } diff --git a/packages/firestore/src/model/transform_operation.ts b/packages/firestore/src/model/transform_operation.ts index 846c54fd6ad..5c6fc64ee0b 100644 --- a/packages/firestore/src/model/transform_operation.ts +++ b/packages/firestore/src/model/transform_operation.ts @@ -31,155 +31,155 @@ import { serverTimestamp } from './server_timestamps'; import { arrayEquals } from '../util/misc'; /** Represents a transform within a TransformMutation. */ -export interface TransformOperation { - /** - * Computes the local transform result against the provided `previousValue`, - * optionally using the provided localWriteTime. - */ - applyToLocalView( - previousValue: api.Value | null, - localWriteTime: Timestamp - ): api.Value; - - /** - * Computes a final transform result after the transform has been acknowledged - * by the server, potentially using the server-provided transformResult. - */ - applyToRemoteDocument( - previousValue: api.Value | null, - transformResult: api.Value | null - ): api.Value; - - /** - * If this transform operation is not idempotent, returns the base value to - * persist for this transform. If a base value is returned, the transform - * operation is always applied to this base value, even if document has - * already been updated. - * - * Base values provide consistent behavior for non-idempotent transforms and - * allow us to return the same latency-compensated value even if the backend - * has already applied the transform operation. The base value is null for - * idempotent transforms, as they can be re-played even if the backend has - * already applied them. - * - * @return a base value to store along with the mutation, or null for - * idempotent transforms. - */ - computeBaseValue(previousValue: api.Value | null): api.Value | null; - - isEqual(other: TransformOperation): boolean; +export class TransformOperation { + // Make sure that the structural type of `TransformOperation` is unique. + // See https://github.com/microsoft/TypeScript/issues/5451 + private _ = undefined; } -/** Transforms a value into a server-generated timestamp. */ -export class ServerTimestampTransform implements TransformOperation { - private constructor() {} - static instance = new ServerTimestampTransform(); - - applyToLocalView( - previousValue: api.Value | null, - localWriteTime: Timestamp - ): api.Value { +/** + * Computes the local transform result against the provided `previousValue`, + * optionally using the provided localWriteTime. + */ +export function applyTransformOperationToLocalView( + transform: TransformOperation, + previousValue: api.Value | null, + localWriteTime: Timestamp +): api.Value { + if (transform instanceof ServerTimestampTransform) { return serverTimestamp(localWriteTime!, previousValue); - } - - applyToRemoteDocument( - previousValue: api.Value | null, - transformResult: api.Value | null - ): api.Value { - return transformResult!; - } - - computeBaseValue(previousValue: api.Value | null): api.Value | null { - return null; // Server timestamps are idempotent and don't require a base value. - } - - isEqual(other: TransformOperation): boolean { - return other instanceof ServerTimestampTransform; + } else if (transform instanceof ArrayUnionTransformOperation) { + return applyArrayUnionTransformOperation(transform, previousValue); + } else if (transform instanceof ArrayRemoveTransformOperation) { + return applyArrayRemoveTransformOperation(transform, previousValue); + } else { + debugAssert( + transform instanceof NumericIncrementTransformOperation, + 'Expected NumericIncrementTransformOperation but was: ' + transform + ); + return applyNumericIncrementTransformOperationToLocalView( + transform, + previousValue + ); } } -/** Transforms an array value via a union operation. */ -export class ArrayUnionTransformOperation implements TransformOperation { - constructor(readonly elements: api.Value[]) {} - - applyToLocalView( - previousValue: api.Value | null, - localWriteTime: Timestamp - ): api.Value { - return this.apply(previousValue); - } - - applyToRemoteDocument( - previousValue: api.Value | null, - transformResult: api.Value | null - ): api.Value { - // The server just sends null as the transform result for array operations, - // so we have to calculate a result the same as we do for local - // applications. - return this.apply(previousValue); - } +/** + * Computes a final transform result after the transform has been acknowledged + * by the server, potentially using the server-provided transformResult. + */ +export function applyTransformOperationToRemoteDocument( + transform: TransformOperation, + previousValue: api.Value | null, + transformResult: api.Value | null +): api.Value { + // The server just sends null as the transform result for array operations, + // so we have to calculate a result the same as we do for local + // applications. + if (transform instanceof ArrayUnionTransformOperation) { + return applyArrayUnionTransformOperation(transform, previousValue); + } else if (transform instanceof ArrayRemoveTransformOperation) { + return applyArrayRemoveTransformOperation(transform, previousValue); + } + + debugAssert( + transformResult !== null, + "Didn't receive transformResult for non-array transform" + ); + return transformResult; +} - private apply(previousValue: api.Value | null): api.Value { - const values = coercedFieldValuesArray(previousValue); - for (const toUnion of this.elements) { - if (!values.some(element => valueEquals(element, toUnion))) { - values.push(toUnion); - } - } - return { arrayValue: { values } }; +/** + * If this transform operation is not idempotent, returns the base value to + * persist for this transform. If a base value is returned, the transform + * operation is always applied to this base value, even if document has + * already been updated. + * + * Base values provide consistent behavior for non-idempotent transforms and + * allow us to return the same latency-compensated value even if the backend + * has already applied the transform operation. The base value is null for + * idempotent transforms, as they can be re-played even if the backend has + * already applied them. + * + * @return a base value to store along with the mutation, or null for + * idempotent transforms. + */ +export function computeTransformOperationBaseValue( + transform: TransformOperation, + previousValue: api.Value | null +): api.Value | null { + if (transform instanceof NumericIncrementTransformOperation) { + return isNumber(previousValue) ? previousValue! : { integerValue: 0 }; } + return null; +} - computeBaseValue(previousValue: api.Value | null): api.Value | null { - return null; // Array transforms are idempotent and don't require a base value. +export function transformOperationEquals( + left: TransformOperation, + right: TransformOperation +): boolean { + if ( + left instanceof ArrayUnionTransformOperation && + right instanceof ArrayUnionTransformOperation + ) { + return arrayEquals(left.elements, right.elements, valueEquals); + } else if ( + left instanceof ArrayRemoveTransformOperation && + right instanceof ArrayRemoveTransformOperation + ) { + return arrayEquals(left.elements, right.elements, valueEquals); + } else if ( + left instanceof NumericIncrementTransformOperation && + right instanceof NumericIncrementTransformOperation + ) { + return valueEquals(left.operand, right.operand); } - isEqual(other: TransformOperation): boolean { - return ( - other instanceof ArrayUnionTransformOperation && - arrayEquals(this.elements, other.elements, valueEquals) - ); - } + return ( + left instanceof ServerTimestampTransform && + right instanceof ServerTimestampTransform + ); } -/** Transforms an array value via a remove operation. */ -export class ArrayRemoveTransformOperation implements TransformOperation { - constructor(readonly elements: api.Value[]) {} - - applyToLocalView( - previousValue: api.Value | null, - localWriteTime: Timestamp - ): api.Value { - return this.apply(previousValue); - } +/** Transforms a value into a server-generated timestamp. */ +export class ServerTimestampTransform extends TransformOperation {} - applyToRemoteDocument( - previousValue: api.Value | null, - transformResult: api.Value | null - ): api.Value { - // The server just sends null as the transform result for array operations, - // so we have to calculate a result the same as we do for local - // applications. - return this.apply(previousValue); +/** Transforms an array value via a union operation. */ +export class ArrayUnionTransformOperation extends TransformOperation { + constructor(readonly elements: api.Value[]) { + super(); } +} - private apply(previousValue: api.Value | null): api.Value { - let values = coercedFieldValuesArray(previousValue); - for (const toRemove of this.elements) { - values = values.filter(element => !valueEquals(element, toRemove)); +function applyArrayUnionTransformOperation( + transform: ArrayUnionTransformOperation, + previousValue: api.Value | null +): api.Value { + const values = coercedFieldValuesArray(previousValue); + for (const toUnion of transform.elements) { + if (!values.some(element => valueEquals(element, toUnion))) { + values.push(toUnion); } - return { arrayValue: { values } }; } + return { arrayValue: { values } }; +} - computeBaseValue(previousValue: api.Value | null): api.Value | null { - return null; // Array transforms are idempotent and don't require a base value. +/** Transforms an array value via a remove operation. */ +export class ArrayRemoveTransformOperation extends TransformOperation { + constructor(readonly elements: api.Value[]) { + super(); } +} - isEqual(other: TransformOperation): boolean { - return ( - other instanceof ArrayRemoveTransformOperation && - arrayEquals(this.elements, other.elements, valueEquals) - ); +function applyArrayRemoveTransformOperation( + transform: ArrayRemoveTransformOperation, + previousValue: api.Value | null +): api.Value { + let values = coercedFieldValuesArray(previousValue); + for (const toRemove of transform.elements) { + values = values.filter(element => !valueEquals(element, toRemove)); } + return { arrayValue: { values } }; } /** @@ -188,62 +188,40 @@ export class ArrayRemoveTransformOperation implements TransformOperation { * backend does not cap integer values at 2^63. Instead, JavaScript number * arithmetic is used and precision loss can occur for values greater than 2^53. */ -export class NumericIncrementTransformOperation implements TransformOperation { +export class NumericIncrementTransformOperation extends TransformOperation { constructor( - private readonly serializer: JsonProtoSerializer, + readonly serializer: JsonProtoSerializer, readonly operand: api.Value ) { + super(); debugAssert( isNumber(operand), 'NumericIncrementTransform transform requires a NumberValue' ); } +} - applyToLocalView( - previousValue: api.Value | null, - localWriteTime: Timestamp - ): api.Value { - // PORTING NOTE: Since JavaScript's integer arithmetic is limited to 53 bit - // precision and resolves overflows by reducing precision, we do not - // manually cap overflows at 2^63. - const baseValue = this.computeBaseValue(previousValue); - const sum = this.asNumber(baseValue) + this.asNumber(this.operand); - if (isInteger(baseValue) && isInteger(this.operand)) { - return toInteger(sum); - } else { - return toDouble(this.serializer, sum); - } - } - - applyToRemoteDocument( - previousValue: api.Value | null, - transformResult: api.Value | null - ): api.Value { - debugAssert( - transformResult !== null, - "Didn't receive transformResult for NUMERIC_ADD transform" - ); - return transformResult; - } - - /** - * Inspects the provided value, returning the provided value if it is already - * a NumberValue, otherwise returning a coerced value of 0. - */ - computeBaseValue(previousValue: api.Value | null): api.Value { - return isNumber(previousValue) ? previousValue! : { integerValue: 0 }; - } - - isEqual(other: TransformOperation): boolean { - return ( - other instanceof NumericIncrementTransformOperation && - valueEquals(this.operand, other.operand) - ); +export function applyNumericIncrementTransformOperationToLocalView( + transform: NumericIncrementTransformOperation, + previousValue: api.Value | null +): api.Value { + // PORTING NOTE: Since JavaScript's integer arithmetic is limited to 53 bit + // precision and resolves overflows by reducing precision, we do not + // manually cap overflows at 2^63. + const baseValue = computeTransformOperationBaseValue( + transform, + previousValue + )!; + const sum = asNumber(baseValue) + asNumber(transform.operand); + if (isInteger(baseValue) && isInteger(transform.operand)) { + return toInteger(sum); + } else { + return toDouble(transform.serializer, sum); } +} - private asNumber(value: api.Value): number { - return normalizeNumber(value.integerValue || value.doubleValue); - } +function asNumber(value: api.Value): number { + return normalizeNumber(value.integerValue || value.doubleValue); } function coercedFieldValuesArray(value: api.Value | null): api.Value[] { diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 3e0dd5e7182..f5a9117b656 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -774,7 +774,7 @@ function fromFieldTransform( proto.setToServerValue === 'REQUEST_TIME', 'Unknown server value transform proto: ' + JSON.stringify(proto) ); - transform = ServerTimestampTransform.instance; + transform = new ServerTimestampTransform(); } else if ('appendMissingElements' in proto) { const values = proto.appendMissingElements!.values || []; transform = new ArrayUnionTransformOperation(values); diff --git a/packages/firestore/test/unit/model/mutation.test.ts b/packages/firestore/test/unit/model/mutation.test.ts index 10320234529..8c53e9e45de 100644 --- a/packages/firestore/test/unit/model/mutation.test.ts +++ b/packages/firestore/test/unit/model/mutation.test.ts @@ -21,6 +21,9 @@ import { Timestamp } from '../../../src/api/timestamp'; import { Document, MaybeDocument } from '../../../src/model/document'; import { serverTimestamp } from '../../../src/model/server_timestamps'; import { + applyMutationToLocalView, + applyMutationToRemoteDocument, + extractMutationBaseValue, Mutation, MutationResult, Precondition @@ -58,7 +61,7 @@ describe('Mutation', () => { const baseDoc = doc('collection/key', 0, docData); const set = setMutation('collection/key', { bar: 'bar-value' }); - const setDoc = set.applyToLocalView(baseDoc, baseDoc, timestamp); + const setDoc = applyMutationToLocalView(set, baseDoc, baseDoc, timestamp); expect(setDoc).to.deep.equal( doc( 'collection/key', @@ -77,7 +80,12 @@ describe('Mutation', () => { 'foo.bar': 'new-bar-value' }); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -98,7 +106,12 @@ describe('Mutation', () => { Precondition.none() ); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -119,7 +132,12 @@ describe('Mutation', () => { Precondition.none() ); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -138,7 +156,12 @@ describe('Mutation', () => { 'foo.bar': FieldValue.delete() }); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -158,7 +181,12 @@ describe('Mutation', () => { 'foo.bar': 'new-bar-value' }); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -172,7 +200,12 @@ describe('Mutation', () => { it('patching a NoDocument yields a NoDocument', () => { const baseDoc = deletedDoc('collection/key', 0); const patch = patchMutation('collection/key', { foo: 'bar' }); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal(baseDoc); }); @@ -184,7 +217,8 @@ describe('Mutation', () => { 'foo.bar': FieldValue.serverTimestamp() }); - const transformedDoc = transform.applyToLocalView( + const transformedDoc = applyMutationToLocalView( + transform, baseDoc, baseDoc, timestamp @@ -360,7 +394,8 @@ describe('Mutation', () => { for (const transformData of transforms) { const transform = transformMutation('collection/key', transformData); - transformedDoc = transform.applyToLocalView( + transformedDoc = applyMutationToLocalView( + transform, transformedDoc, transformedDoc, timestamp @@ -389,7 +424,8 @@ describe('Mutation', () => { } } ]); - const transformedDoc = transform.applyToRemoteDocument( + const transformedDoc = applyMutationToRemoteDocument( + transform, baseDoc, mutationResult ); @@ -414,7 +450,8 @@ describe('Mutation', () => { // Server just sends null transform results for array operations. const mutationResult = new MutationResult(version(1), [null, null]); - const transformedDoc = transform.applyToRemoteDocument( + const transformedDoc = applyMutationToRemoteDocument( + transform, baseDoc, mutationResult ); @@ -500,7 +537,8 @@ describe('Mutation', () => { const mutationResult = new MutationResult(version(1), [ { integerValue: 3 } ]); - const transformedDoc = transform.applyToRemoteDocument( + const transformedDoc = applyMutationToRemoteDocument( + transform, baseDoc, mutationResult ); @@ -514,7 +552,12 @@ describe('Mutation', () => { const baseDoc = doc('collection/key', 0, { foo: 'bar' }); const mutation = deleteMutation('collection/key'); - const result = mutation.applyToLocalView(baseDoc, baseDoc, Timestamp.now()); + const result = applyMutationToLocalView( + mutation, + baseDoc, + baseDoc, + Timestamp.now() + ); expect(result).to.deep.equal(deletedDoc('collection/key', 0)); }); @@ -523,7 +566,7 @@ describe('Mutation', () => { const docSet = setMutation('collection/key', { foo: 'new-bar' }); const setResult = mutationResult(4); - const setDoc = docSet.applyToRemoteDocument(baseDoc, setResult); + const setDoc = applyMutationToRemoteDocument(docSet, baseDoc, setResult); expect(setDoc).to.deep.equal( doc( 'collection/key', @@ -539,7 +582,7 @@ describe('Mutation', () => { const mutation = patchMutation('collection/key', { foo: 'new-bar' }); const result = mutationResult(5); - const patchedDoc = mutation.applyToRemoteDocument(baseDoc, result); + const patchedDoc = applyMutationToRemoteDocument(mutation, baseDoc, result); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -558,7 +601,11 @@ describe('Mutation', () => { mutationResult: MutationResult, expected: MaybeDocument | null ): void { - const actual = mutation.applyToRemoteDocument(base, mutationResult); + const actual = applyMutationToRemoteDocument( + mutation, + base, + mutationResult + ); expect(actual).to.deep.equal(expected); } @@ -614,13 +661,13 @@ describe('Mutation', () => { const baseDoc = doc('collection/key', 0, data); const set = setMutation('collection/key', { foo: 'bar' }); - expect(set.extractBaseValue(baseDoc)).to.be.null; + expect(extractMutationBaseValue(set, baseDoc)).to.be.null; const patch = patchMutation('collection/key', { foo: 'bar' }); - expect(patch.extractBaseValue(baseDoc)).to.be.null; + expect(extractMutationBaseValue(patch, baseDoc)).to.be.null; const deleter = deleteMutation('collection/key'); - expect(deleter.extractBaseValue(baseDoc)).to.be.null; + expect(extractMutationBaseValue(deleter, baseDoc)).to.be.null; }); it('extracts null base value for ServerTimestamp', () => { @@ -634,7 +681,7 @@ describe('Mutation', () => { // Server timestamps are idempotent and don't have base values. const transform = transformMutation('collection/key', allTransforms); - expect(transform.extractBaseValue(baseDoc)).to.be.null; + expect(extractMutationBaseValue(transform, baseDoc)).to.be.null; }); it('extracts base value for increment', () => { @@ -672,7 +719,7 @@ describe('Mutation', () => { missing: 0, nested: { double: 42.0, long: 42, text: 0, map: 0, missing: 0 } }); - const actualBaseValue = transform.extractBaseValue(baseDoc); + const actualBaseValue = extractMutationBaseValue(transform, baseDoc); expect(expectedBaseValue.isEqual(actualBaseValue!)).to.be.true; }); @@ -683,12 +730,14 @@ describe('Mutation', () => { const increment = { sum: FieldValue.increment(1) }; const transform = transformMutation('collection/key', increment); - let mutatedDoc = transform.applyToLocalView( + let mutatedDoc = applyMutationToLocalView( + transform, baseDoc, baseDoc, Timestamp.now() ); - mutatedDoc = transform.applyToLocalView( + mutatedDoc = applyMutationToLocalView( + transform, mutatedDoc, baseDoc, Timestamp.now() diff --git a/packages/firestore/test/unit/remote/serializer.helper.ts b/packages/firestore/test/unit/remote/serializer.helper.ts index 859e08c8819..376f7ab0174 100644 --- a/packages/firestore/test/unit/remote/serializer.helper.ts +++ b/packages/firestore/test/unit/remote/serializer.helper.ts @@ -42,6 +42,7 @@ import { DeleteMutation, FieldMask, Mutation, + mutationEquals, Precondition, SetMutation, VerifyMutation @@ -601,12 +602,10 @@ export function serializerTest( }); describe('toMutation / fromMutation', () => { - addEqualityMatcher(); - function verifyMutation(mutation: Mutation, proto: unknown): void { const serialized = toMutation(s, mutation); expect(serialized).to.deep.equal(proto); - expect(fromMutation(s, serialized)).to.deep.equal(mutation); + expect(mutationEquals(fromMutation(s, serialized), mutation)); } it('SetMutation', () => { From 544fe754837af9b053530982907c206d82404887 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Wed, 1 Jul 2020 09:29:31 -0700 Subject: [PATCH 2/3] Create rotten-hats-fry.md --- .changeset/rotten-hats-fry.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/rotten-hats-fry.md diff --git a/.changeset/rotten-hats-fry.md b/.changeset/rotten-hats-fry.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/rotten-hats-fry.md @@ -0,0 +1,2 @@ +--- +--- From 3a82fcf5fa6abd3dacf247f58a7673efb2b11887 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Wed, 1 Jul 2020 09:58:07 -0700 Subject: [PATCH 3/3] Cleanup --- packages/firestore/src/model/mutation.ts | 8 ++++---- packages/firestore/src/model/transform_operation.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/firestore/src/model/mutation.ts b/packages/firestore/src/model/mutation.ts index bef77cfd819..7c585225d93 100644 --- a/packages/firestore/src/model/mutation.ts +++ b/packages/firestore/src/model/mutation.ts @@ -90,12 +90,12 @@ export class FieldTransform { } export function fieldTransformEquals( - l: FieldTransform, - r: FieldTransform + left: FieldTransform, + right: FieldTransform ): boolean { return ( - l.field.isEqual(r.field) && - transformOperationEquals(l.transform, r.transform) + left.field.isEqual(right.field) && + transformOperationEquals(left.transform, right.transform) ); } diff --git a/packages/firestore/src/model/transform_operation.ts b/packages/firestore/src/model/transform_operation.ts index 5c6fc64ee0b..86c69c7dde9 100644 --- a/packages/firestore/src/model/transform_operation.ts +++ b/packages/firestore/src/model/transform_operation.ts @@ -47,7 +47,7 @@ export function applyTransformOperationToLocalView( localWriteTime: Timestamp ): api.Value { if (transform instanceof ServerTimestampTransform) { - return serverTimestamp(localWriteTime!, previousValue); + return serverTimestamp(localWriteTime, previousValue); } else if (transform instanceof ArrayUnionTransformOperation) { return applyArrayUnionTransformOperation(transform, previousValue); } else if (transform instanceof ArrayRemoveTransformOperation) {