From 14931c2b9287476b154263a5262ff9fee2301f6b Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 24 Feb 2020 20:08:44 -0800 Subject: [PATCH 1/2] Adding Proto-based equality and comparison --- packages/firestore/src/model/field_value.ts | 40 +- packages/firestore/src/model/proto_values.ts | 385 +++++++++++++++++++ packages/firestore/src/remote/serializer.ts | 85 +--- packages/firestore/src/util/misc.ts | 34 ++ packages/firestore/src/util/obj.ts | 8 +- 5 files changed, 440 insertions(+), 112 deletions(-) create mode 100644 packages/firestore/src/model/proto_values.ts diff --git a/packages/firestore/src/model/field_value.ts b/packages/firestore/src/model/field_value.ts index 6e8c863850b..023f56df73d 100644 --- a/packages/firestore/src/model/field_value.ts +++ b/packages/firestore/src/model/field_value.ts @@ -20,7 +20,11 @@ import { GeoPoint } from '../api/geo_point'; import { Timestamp } from '../api/timestamp'; import { DatabaseId } from '../core/database_info'; import { assert, fail } from '../util/assert'; -import { primitiveComparator } from '../util/misc'; +import { + numericComparator, + numericEquals, + primitiveComparator +} from '../util/misc'; import { DocumentKey } from './document_key'; import { FieldMask } from './mutation'; import { FieldPath } from './path'; @@ -244,40 +248,6 @@ export abstract class NumberValue extends FieldValue { } } -/** Utility function to compare doubles (using Firestore semantics for NaN). */ -function numericComparator(left: number, right: number): number { - if (left < right) { - return -1; - } else if (left > right) { - return 1; - } else if (left === right) { - return 0; - } else { - // one or both are NaN. - if (isNaN(left)) { - return isNaN(right) ? 0 : -1; - } else { - return 1; - } - } -} - -/** - * Utility function to check numbers for equality using Firestore semantics - * (NaN === NaN, -0.0 !== 0.0). - */ -function numericEquals(left: number, right: number): boolean { - // Implemented based on Object.is() polyfill from - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is - if (left === right) { - // +0 != -0 - return left !== 0 || 1 / left === 1 / right; - } else { - // NaN == NaN - return left !== left && right !== right; - } -} - export class IntegerValue extends NumberValue { isEqual(other: FieldValue): boolean { // NOTE: DoubleValue and IntegerValue instances may compareTo() the same, diff --git a/packages/firestore/src/model/proto_values.ts b/packages/firestore/src/model/proto_values.ts new file mode 100644 index 00000000000..24e3a749c1f --- /dev/null +++ b/packages/firestore/src/model/proto_values.ts @@ -0,0 +1,385 @@ +/** + * @license + * Copyright 202 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as api from '../protos/firestore_proto_api'; + +import { TypeOrder } from './field_value'; +import { assert, fail } from '../util/assert'; +import { keys, size } from '../util/obj'; +import { ByteString } from '../util/byte_string'; +import { + numericComparator, + numericEquals, + primitiveComparator +} from '../util/misc'; + +// A RegExp matching ISO 8601 UTC timestamps with optional fraction. +const ISO_TIMESTAMP_REG_EXP = new RegExp( + /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.(\d+))?Z$/ +); + +/** Extracts the backend's type order for the provided value. */ +export function typeOrder(value: api.Value): TypeOrder { + if ('nullValue' in value) { + return TypeOrder.NullValue; + } else if ('booleanValue' in value) { + return TypeOrder.BooleanValue; + } else if ('integerValue' in value || 'doubleValue' in value) { + return TypeOrder.NumberValue; + } else if ('timestampValue' in value) { + return TypeOrder.TimestampValue; + } else if ('stringValue' in value) { + return TypeOrder.StringValue; + } else if ('bytesValue' in value) { + return TypeOrder.BlobValue; + } else if ('referenceValue' in value) { + return TypeOrder.RefValue; + } else if ('geoPointValue' in value) { + return TypeOrder.GeoPointValue; + } else if ('arrayValue' in value) { + return TypeOrder.ArrayValue; + } else if ('mapValue' in value) { + return TypeOrder.ObjectValue; + } else { + return fail('Invalid value type: ' + JSON.stringify(value)); + } +} + +/** Returns whether `value` is defined and corresponds to the given type order. */ +export function isType( + value: api.Value | undefined, + expectedTypeOrder: TypeOrder +): boolean { + return value !== undefined && typeOrder(value) === expectedTypeOrder; +} + +/** Tests `left` and `right` for equality based on the backend semantics. */ +export function equals(left: api.Value, right: api.Value): boolean { + const leftType = typeOrder(left); + const rightType = typeOrder(right); + if (leftType !== rightType) { + return false; + } + + switch (leftType) { + case TypeOrder.NullValue: + return true; + case TypeOrder.BooleanValue: + return left.booleanValue === right.booleanValue; + case TypeOrder.TimestampValue: + return timestampEquals(left, right); + case TypeOrder.StringValue: + return left.stringValue === right.stringValue; + case TypeOrder.BlobValue: + return blobEquals(left, right); + case TypeOrder.RefValue: + return left.referenceValue === right.referenceValue; + case TypeOrder.GeoPointValue: + return geoPointEquals(left, right); + case TypeOrder.NumberValue: + return numberEquals(left, right); + case TypeOrder.ArrayValue: + return arrayEquals(left, right); + case TypeOrder.ObjectValue: + return objectEquals(left, right); + default: + return fail('Unexpected value type: ' + JSON.stringify(left)); + } +} + +function timestampEquals(left: api.Value, right: api.Value): boolean { + const leftTimestamp = normalizeTimestamp(left.timestampValue!); + const rightTimestamp = normalizeTimestamp(right.timestampValue!); + return ( + leftTimestamp.seconds === rightTimestamp.seconds && + leftTimestamp.nanos === rightTimestamp.nanos + ); +} + +function geoPointEquals(left: api.Value, right: api.Value): boolean { + return ( + normalizeNumber(left.geoPointValue!.latitude) === + normalizeNumber(right.geoPointValue!.latitude) && + normalizeNumber(left.geoPointValue!.longitude) === + normalizeNumber(right.geoPointValue!.longitude) + ); +} + +function blobEquals(left: api.Value, right: api.Value): boolean { + return normalizeByteString(left.bytesValue!).isEqual( + normalizeByteString(right.bytesValue!) + ); +} + +export function numberEquals(left: api.Value, right: api.Value): boolean { + if ('integerValue' in left && 'integerValue' in right) { + return ( + normalizeNumber(left.integerValue) === normalizeNumber(right.integerValue) + ); + } else if ('doubleValue' in left && 'doubleValue' in right) { + return numericEquals( + normalizeNumber(left.doubleValue), + normalizeNumber(right.doubleValue) + ); + } + + return false; +} + +function arrayEquals(left: api.Value, right: api.Value): boolean { + const leftArray = left.arrayValue!.values || []; + const rightArray = right.arrayValue!.values || []; + + if (leftArray.length !== rightArray.length) { + return false; + } + + for (let i = 0; i < leftArray.length; ++i) { + if (!equals(leftArray[i], rightArray[i])) { + return false; + } + } + return true; +} + +function objectEquals(left: api.Value, right: api.Value): boolean { + const leftMap = left.mapValue!.fields || {}; + const rightMap = right.mapValue!.fields || {}; + + if (size(leftMap) !== size(rightMap)) { + return false; + } + + for (const key in leftMap) { + if (leftMap.hasOwnProperty(key)) { + if (rightMap[key] === undefined || !equals(leftMap[key], rightMap[key])) { + return false; + } + } + } + return true; +} + +function compare(left: api.Value, right: api.Value): number { + const leftType = typeOrder(left); + const rightType = typeOrder(right); + + if (leftType !== rightType) { + return primitiveComparator(leftType, rightType); + } + + switch (leftType) { + case TypeOrder.NullValue: + return 0; + case TypeOrder.BooleanValue: + return compareBooleans(left.booleanValue!, right.booleanValue!); + case TypeOrder.NumberValue: + return compareNumbers(left, right); + case TypeOrder.TimestampValue: + return compareTimestamps(left.timestampValue!, right.timestampValue!); + case TypeOrder.StringValue: + return primitiveComparator(left.stringValue!, right.stringValue!); + case TypeOrder.BlobValue: + return compareBlobs(left.bytesValue!, right.bytesValue!); + case TypeOrder.RefValue: + return compareReferences(left.referenceValue!, right.referenceValue!); + case TypeOrder.GeoPointValue: + return compareGeoPoints(left.geoPointValue!, right.geoPointValue!); + case TypeOrder.ArrayValue: + return compareArrays(left.arrayValue!, right.arrayValue!); + case TypeOrder.ObjectValue: + return compareMaps(left.mapValue!, right.mapValue!); + default: + throw fail('Invalid value type: ' + leftType); + } +} + +function compareBooleans(b1: boolean, b2: boolean): number { + return b1 !== b2 ? (b1 ? 1 : -1) : 0; +} + +function compareNumbers(left: api.Value, right: api.Value): number { + const leftNumber = + 'doubleValue' in left + ? normalizeNumber(left.doubleValue) + : normalizeNumber(left.integerValue); + const rightNumber = + 'doubleValue' in right + ? normalizeNumber(right.doubleValue) + : normalizeNumber(right.integerValue); + return numericComparator(leftNumber, rightNumber); +} + +function compareTimestamps( + left: string | { seconds?: string; nanos?: number }, + right: string | { seconds?: string; nanos?: number } +): number { + const leftTimestamp = normalizeTimestamp(left); + const rightTimestamp = normalizeTimestamp(right); + + const comparison = primitiveComparator( + leftTimestamp.seconds, + rightTimestamp.seconds + ); + if (comparison !== 0) { + return comparison; + } + return primitiveComparator(leftTimestamp.nanos, rightTimestamp.nanos); +} + +function compareReferences(leftPath: string, rightPath: string): number { + const leftSegments = leftPath.split('/'); + const rightSegments = rightPath.split('/'); + for (let i = 0; i < leftSegments.length && i < rightSegments.length; i++) { + const comparison = primitiveComparator(leftSegments[i], rightSegments[i]); + if (comparison !== 0) { + return comparison; + } + } + return primitiveComparator(leftSegments.length, rightSegments.length); +} + +function compareGeoPoints(left: api.LatLng, right: api.LatLng): number { + const comparison = primitiveComparator( + normalizeNumber(left.latitude), + normalizeNumber(right.latitude) + ); + if (comparison !== 0) { + return comparison; + } + return primitiveComparator( + normalizeNumber(left.longitude), + normalizeNumber(right.longitude) + ); +} + +function compareBlobs( + left: string | Uint8Array, + right: string | Uint8Array +): number { + const leftBytes = normalizeByteString(left); + const rightBytes = normalizeByteString(right); + return leftBytes.compareTo(rightBytes); +} + +function compareArrays(left: api.ArrayValue, right: api.ArrayValue): number { + const leftArray = left.values || []; + const rightArray = right.values || []; + + for (let i = 0; i < leftArray.length && i < rightArray.length; ++i) { + const valueCompare = compare(leftArray[i], rightArray[i]); + if (valueCompare) { + return valueCompare; + } + } + return primitiveComparator(leftArray.length, rightArray.length); +} + +function compareMaps(left: api.MapValue, right: api.MapValue): number { + const leftMap = left.fields || {}; + const leftKeys = keys(leftMap); + const rightMap = right.fields || {}; + const rightKeys = keys(leftMap); + + leftKeys.sort(); + rightKeys.sort(); + + for (let i = 0; i < leftKeys.length && i < rightKeys.length; ++i) { + const keyCompare = primitiveComparator(leftKeys[i], rightKeys[i]); + if (keyCompare !== 0) { + return keyCompare; + } + const valueCompare = compare(leftMap[leftKeys[i]], rightMap[rightKeys[i]]); + if (valueCompare !== 0) { + return valueCompare; + } + } + + return primitiveComparator(leftKeys.length, rightKeys.length); +} + +/** + * Converts the possible Proto values for a timestamp value into a "seconds and + * nanos" representation. + */ +export function normalizeTimestamp( + date: string | { seconds?: string; nanos?: number } +): { seconds: number; nanos: number } { + assert(!!date, 'Cannot deserialize null or undefined timestamp.'); + + // The json interface (for the browser) will return an iso timestamp string, + // while the proto js library (for node) will return a + // google.protobuf.Timestamp instance. + if (typeof date === 'string') { + // The date string can have higher precision (nanos) than the Date class + // (millis), so we do some custom parsing here. + + // Parse the nanos right out of the string. + let nanos = 0; + const fraction = ISO_TIMESTAMP_REG_EXP.exec(date); + assert(!!fraction, 'invalid timestamp: ' + date); + if (fraction[1]) { + // Pad the fraction out to 9 digits (nanos). + let nanoStr = fraction[1]; + nanoStr = (nanoStr + '000000000').substr(0, 9); + nanos = Number(nanoStr); + } + + // Parse the date to get the seconds. + const parsedDate = new Date(date); + const seconds = Math.floor(parsedDate.getTime() / 1000); + + return { seconds, nanos }; + } else { + // TODO(b/37282237): Use strings for Proto3 timestamps + // assert(!this.options.useProto3Json, + // 'The timestamp instance format requires Proto JS.'); + const seconds = normalizeNumber(date.seconds); + const nanos = normalizeNumber(date.nanos); + return { seconds, nanos }; + } +} + +/** Converts the possible Proto types for numbers into a JavaScript number. */ +export function normalizeNumber(value: number | string | undefined): number { + // TODO(bjornick): Handle int64 greater than 53 bits. + if (typeof value === 'number') { + return value; + } else if (typeof value === 'string') { + // Proto 3 uses the string values 'NaN' and 'Infinity'. + if (value === 'NaN') { + return Number.NaN; + } else if (value === 'Infinity') { + return Number.POSITIVE_INFINITY; + } else if (value === '-Infinity') { + return Number.NEGATIVE_INFINITY; + } + + return Number(value); + } else { + return 0; + } +} + +/** Converts the possible Proto types for Blobs into a ByteString. */ +export function normalizeByteString(blob: string | Uint8Array): ByteString { + if (typeof blob === 'string') { + return ByteString.fromBase64String(blob); + } else { + return ByteString.fromUint8Array(blob); + } +} diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 7e9acd74109..183788077b4 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -72,6 +72,11 @@ import { WatchTargetChange, WatchTargetChangeState } from './watch_change'; +import { + normalizeByteString, + normalizeNumber, + normalizeTimestamp +} from '../model/proto_values'; const DIRECTIONS = (() => { const dirs: { [dir: string]: api.OrderDirection } = {}; @@ -93,24 +98,10 @@ const OPERATORS = (() => { return ops; })(); -// A RegExp matching ISO 8601 UTC timestamps with optional fraction. -const ISO_REG_EXP = new RegExp(/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.(\d+))?Z$/); - function assertPresent(value: unknown, description: string): asserts value { assert(!typeUtils.isNullOrUndefined(value), description + ' is missing'); } -function parseInt64(value: number | string): number { - // TODO(bjornick): Handle int64 greater than 53 bits. - if (typeof value === 'number') { - return value; - } else if (typeof value === 'string') { - return Number(value); - } else { - return fail("can't parse " + value); - } -} - // This is a supplement to the generated proto interfaces, which fail to account // for the fact that a timestamp may be encoded as either a string OR this. interface TimestampProto { @@ -218,45 +209,8 @@ export class JsonProtoSerializer { } private fromTimestamp(date: string | TimestampProto): Timestamp { - // The json interface (for the browser) will return an iso timestamp string, - // while the proto js library (for node) will return a - // google.protobuf.Timestamp instance. - if (typeof date === 'string') { - // TODO(b/37282237): Use strings for Proto3 timestamps - // assert(this.options.useProto3Json, - // 'The timestamp string format requires Proto3.'); - return this.fromIso8601String(date); - } else { - assert(!!date, 'Cannot deserialize null or undefined timestamp.'); - // TODO(b/37282237): Use strings for Proto3 timestamps - // assert(!this.options.useProto3Json, - // 'The timestamp instance format requires Proto JS.'); - const seconds = parseInt64(date.seconds || '0'); - const nanos = date.nanos || 0; - return new Timestamp(seconds, nanos); - } - } - - private fromIso8601String(utc: string): Timestamp { - // The date string can have higher precision (nanos) than the Date class - // (millis), so we do some custom parsing here. - - // Parse the nanos right out of the string. - let nanos = 0; - const fraction = ISO_REG_EXP.exec(utc); - assert(!!fraction, 'invalid timestamp: ' + utc); - if (fraction[1]) { - // Pad the fraction out to 9 digits (nanos). - let nanoStr = fraction[1]; - nanoStr = (nanoStr + '000000000').substr(0, 9); - nanos = Number(nanoStr); - } - - // Parse the date to get the seconds. - const date = new Date(utc); - const seconds = Math.floor(date.getTime() / 1000); - - return new Timestamp(seconds, nanos); + const timestamp = normalizeTimestamp(date); + return new Timestamp(timestamp.seconds, timestamp.nanos); } /** @@ -295,27 +249,6 @@ export class JsonProtoSerializer { } } - /** - * Parse the blob from the protos into the internal ByteString class. Note - * that the typings assume all blobs are strings, but they are actually - * Uint8Arrays on Node. - */ - private fromBlob(blob: string | Uint8Array): ByteString { - if (typeof blob === 'string') { - assert( - this.options.useProto3Json, - 'Expected bytes to be passed in as Uint8Array, but got a string instead.' - ); - return ByteString.fromBase64String(blob); - } else { - assert( - !this.options.useProto3Json, - 'Expected bytes to be passed in as Uint8Array, but got a string instead.' - ); - return ByteString.fromUint8Array(blob); - } - } - toVersion(version: SnapshotVersion): string { return this.toTimestamp(version.toTimestamp()); } @@ -477,7 +410,7 @@ export class JsonProtoSerializer { } else if ('booleanValue' in obj) { return fieldValue.BooleanValue.of(obj.booleanValue!); } else if ('integerValue' in obj) { - return new fieldValue.IntegerValue(parseInt64(obj.integerValue!)); + return new fieldValue.IntegerValue(normalizeNumber(obj.integerValue!)); } else if ('doubleValue' in obj) { if (this.options.useProto3Json) { // Proto 3 uses the string values 'NaN' and 'Infinity'. @@ -512,7 +445,7 @@ export class JsonProtoSerializer { return new fieldValue.GeoPointValue(new GeoPoint(latitude, longitude)); } else if ('bytesValue' in obj) { assertPresent(obj.bytesValue, 'bytesValue'); - const blob = this.fromBlob(obj.bytesValue); + const blob = normalizeByteString(obj.bytesValue); return new fieldValue.BlobValue(blob); } else if ('referenceValue' in obj) { assertPresent(obj.referenceValue, 'referenceValue'); diff --git a/packages/firestore/src/util/misc.ts b/packages/firestore/src/util/misc.ts index 6e9cf71da78..11760d1574e 100644 --- a/packages/firestore/src/util/misc.ts +++ b/packages/firestore/src/util/misc.ts @@ -46,6 +46,40 @@ export function primitiveComparator(left: T, right: T): number { return 0; } +/** Utility function to compare doubles (using Firestore semantics for NaN). */ +export function numericComparator(left: number, right: number): number { + if (left < right) { + return -1; + } else if (left > right) { + return 1; + } else if (left === right) { + return 0; + } else { + // one or both are NaN. + if (isNaN(left)) { + return isNaN(right) ? 0 : -1; + } else { + return 1; + } + } +} + +/** + * Utility function to check numbers for equality using Firestore semantics + * (NaN === NaN, -0.0 !== 0.0). + */ +export function numericEquals(left: number, right: number): boolean { + // Implemented based on Object.is() polyfill from + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + if (left === right) { + // +0 != -0 + return left !== 0 || 1 / left === 1 / right; + } else { + // NaN == NaN + return left !== left && right !== right; + } +} + /** Duck-typed interface for objects that have an isEqual() method. */ export interface Equatable { isEqual(other: T): boolean; diff --git a/packages/firestore/src/util/obj.ts b/packages/firestore/src/util/obj.ts index 237a8670689..0a3d52ca36e 100644 --- a/packages/firestore/src/util/obj.ts +++ b/packages/firestore/src/util/obj.ts @@ -30,7 +30,7 @@ export function get(obj: Dict, key: string | number): V | null { return Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : null; } -export function size(obj: Dict): number { +export function size(obj: object): number { let count = 0; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { @@ -65,6 +65,12 @@ export function values(obj: Dict): V[] { return vs; } +export function keys(obj: Dict): string[] { + const ks: string[] = []; + forEach(obj, k => ks.push(k)); + return ks; +} + export function forEach( obj: Dict, fn: (key: string, val: V) => void From 114c0de081086f33f6a2475950c0befed5493da8 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 25 Feb 2020 14:05:08 -0800 Subject: [PATCH 2/2] Address feedback --- packages/firestore/src/model/proto_values.ts | 32 ++++++++------------ packages/firestore/src/remote/serializer.ts | 4 +-- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/packages/firestore/src/model/proto_values.ts b/packages/firestore/src/model/proto_values.ts index 24e3a749c1f..86b07fd8c32 100644 --- a/packages/firestore/src/model/proto_values.ts +++ b/packages/firestore/src/model/proto_values.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 202 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,9 @@ const ISO_TIMESTAMP_REG_EXP = new RegExp( /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.(\d+))?Z$/ ); +// Denotes the possible representations for timestamps in the Value type. +type ProtoTimestampValue = string | { seconds?: string; nanos?: number }; + /** Extracts the backend's type order for the provided value. */ export function typeOrder(value: api.Value): TypeOrder { if ('nullValue' in value) { @@ -186,7 +189,7 @@ function compare(left: api.Value, right: api.Value): number { case TypeOrder.NullValue: return 0; case TypeOrder.BooleanValue: - return compareBooleans(left.booleanValue!, right.booleanValue!); + return primitiveComparator(left.booleanValue!, right.booleanValue!); case TypeOrder.NumberValue: return compareNumbers(left, right); case TypeOrder.TimestampValue: @@ -208,10 +211,6 @@ function compare(left: api.Value, right: api.Value): number { } } -function compareBooleans(b1: boolean, b2: boolean): number { - return b1 !== b2 ? (b1 ? 1 : -1) : 0; -} - function compareNumbers(left: api.Value, right: api.Value): number { const leftNumber = 'doubleValue' in left @@ -225,8 +224,8 @@ function compareNumbers(left: api.Value, right: api.Value): number { } function compareTimestamps( - left: string | { seconds?: string; nanos?: number }, - right: string | { seconds?: string; nanos?: number } + left: ProtoTimestampValue, + right: ProtoTimestampValue ): number { const leftTimestamp = normalizeTimestamp(left); const rightTimestamp = normalizeTimestamp(right); @@ -295,6 +294,10 @@ function compareMaps(left: api.MapValue, right: api.MapValue): number { const rightMap = right.fields || {}; const rightKeys = keys(leftMap); + // Even though MapValues are likely sorted correctly based on their insertion + // order (e.g. when received from the backend), local modifications can bring + // elements out of order. We need to re-sort the elements to ensure that + // canonical IDs are independent of insertion order. leftKeys.sort(); rightKeys.sort(); @@ -317,9 +320,9 @@ function compareMaps(left: api.MapValue, right: api.MapValue): number { * nanos" representation. */ export function normalizeTimestamp( - date: string | { seconds?: string; nanos?: number } + date: ProtoTimestampValue ): { seconds: number; nanos: number } { - assert(!!date, 'Cannot deserialize null or undefined timestamp.'); + assert(!!date, 'Cannot normalize null or undefined timestamp.'); // The json interface (for the browser) will return an iso timestamp string, // while the proto js library (for node) will return a @@ -360,15 +363,6 @@ export function normalizeNumber(value: number | string | undefined): number { if (typeof value === 'number') { return value; } else if (typeof value === 'string') { - // Proto 3 uses the string values 'NaN' and 'Infinity'. - if (value === 'NaN') { - return Number.NaN; - } else if (value === 'Infinity') { - return Number.POSITIVE_INFINITY; - } else if (value === '-Infinity') { - return Number.NEGATIVE_INFINITY; - } - return Number(value); } else { return 0; diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 183788077b4..b3a03a6c6c4 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -445,8 +445,8 @@ export class JsonProtoSerializer { return new fieldValue.GeoPointValue(new GeoPoint(latitude, longitude)); } else if ('bytesValue' in obj) { assertPresent(obj.bytesValue, 'bytesValue'); - const blob = normalizeByteString(obj.bytesValue); - return new fieldValue.BlobValue(blob); + const byteString = normalizeByteString(obj.bytesValue); + return new fieldValue.BlobValue(byteString); } else if ('referenceValue' in obj) { assertPresent(obj.referenceValue, 'referenceValue'); const resourceName = this.fromResourceName(obj.referenceValue);