diff --git a/packages/firestore/src/api/blob.ts b/packages/firestore/src/api/blob.ts index 4cea49a2a6c..b27215b8b9f 100644 --- a/packages/firestore/src/api/blob.ts +++ b/packages/firestore/src/api/blob.ts @@ -24,6 +24,10 @@ import { validateExactNumberOfArgs } from '../util/input_validation'; import { primitiveComparator } from '../util/misc'; +import { + binaryStringFromUint8Array, + uint8ArrayFromBinaryString +} from '../util/proto_byte_string'; /** Helper function to assert Uint8Array is available at runtime. */ function assertUint8ArrayAvailable(): void { @@ -85,10 +89,7 @@ export class Blob { if (!(array instanceof Uint8Array)) { throw invalidClassError('Blob.fromUint8Array', 'Uint8Array', 1, array); } - let binaryString = ''; - for (let i = 0; i < array.length; ++i) { - binaryString += String.fromCharCode(array[i]); - } + const binaryString = binaryStringFromUint8Array(array); return new Blob(binaryString); } @@ -101,10 +102,7 @@ export class Blob { toUint8Array(): Uint8Array { validateExactNumberOfArgs('Blob.toUint8Array', arguments, 0); assertUint8ArrayAvailable(); - const buffer = new Uint8Array(this._binaryString.length); - for (let i = 0; i < this._binaryString.length; i++) { - buffer[i] = this._binaryString.charCodeAt(i); - } + const buffer = uint8ArrayFromBinaryString(this._binaryString); return buffer; } diff --git a/packages/firestore/src/core/types.ts b/packages/firestore/src/core/types.ts index 98c367af4c4..009700fd016 100644 --- a/packages/firestore/src/core/types.ts +++ b/packages/firestore/src/core/types.ts @@ -29,10 +29,6 @@ export type TargetId = number; export type ListenSequenceNumber = number; -// TODO(b/35918695): In GRPC / node, tokens are Uint8Array. In WebChannel, -// they're strings. We should probably (de-)serialize to a common internal type. -export type ProtoByteString = Uint8Array | string; - /** The different states of a mutation batch. */ export type MutationBatchState = 'pending' | 'acknowledged' | 'rejected'; diff --git a/packages/firestore/src/local/indexeddb_mutation_queue.ts b/packages/firestore/src/local/indexeddb_mutation_queue.ts index f91023b8ed0..66064c1b242 100644 --- a/packages/firestore/src/local/indexeddb_mutation_queue.ts +++ b/packages/firestore/src/local/indexeddb_mutation_queue.ts @@ -18,7 +18,7 @@ import { Timestamp } from '../api/timestamp'; import { User } from '../auth/user'; import { Query } from '../core/query'; -import { BatchId, ProtoByteString } from '../core/types'; +import { BatchId } from '../core/types'; import { DocumentKeySet } from '../model/collections'; import { DocumentKey } from '../model/document_key'; import { Mutation } from '../model/mutation'; @@ -26,6 +26,7 @@ import { BATCHID_UNKNOWN, MutationBatch } from '../model/mutation_batch'; import { ResourcePath } from '../model/path'; import { assert, fail } from '../util/assert'; import { primitiveComparator } from '../util/misc'; +import { ByteString } from '../util/proto_byte_string'; import { SortedMap } from '../util/sorted_map'; import { SortedSet } from '../util/sorted_set'; @@ -47,7 +48,7 @@ import { LocalSerializer } from './local_serializer'; import { MutationQueue } from './mutation_queue'; import { PersistenceTransaction, ReferenceDelegate } from './persistence'; import { PersistencePromise } from './persistence_promise'; -import { SimpleDb, SimpleDbStore, SimpleDbTransaction } from './simple_db'; +import { SimpleDbStore, SimpleDbTransaction } from './simple_db'; /** A mutation queue for a specific user, backed by IndexedDB. */ export class IndexedDbMutationQueue implements MutationQueue { @@ -121,10 +122,12 @@ export class IndexedDbMutationQueue implements MutationQueue { acknowledgeBatch( transaction: PersistenceTransaction, batch: MutationBatch, - streamToken: ProtoByteString + streamToken: ByteString ): PersistencePromise { return this.getMutationQueueMetadata(transaction).next(metadata => { - metadata.lastStreamToken = convertStreamToken(streamToken); + // We can't store the resumeToken as a ByteString in IndexedDB, so we + // convert it to a Base64 string for storage. + metadata.lastStreamToken = streamToken.toBase64(); return mutationQueuesStore(transaction).put(metadata); }); @@ -132,18 +135,20 @@ export class IndexedDbMutationQueue implements MutationQueue { getLastStreamToken( transaction: PersistenceTransaction - ): PersistencePromise { - return this.getMutationQueueMetadata(transaction).next( - metadata => metadata.lastStreamToken + ): PersistencePromise { + return this.getMutationQueueMetadata(transaction).next( + metadata => ByteString.fromBase64String(metadata.lastStreamToken) ); } setLastStreamToken( transaction: PersistenceTransaction, - streamToken: ProtoByteString + streamToken: ByteString ): PersistencePromise { return this.getMutationQueueMetadata(transaction).next(metadata => { - metadata.lastStreamToken = convertStreamToken(streamToken); + // We can't store the resumeToken as a ByteString in IndexedDB, so we + // convert it to a Base64 string for storage. + metadata.lastStreamToken = streamToken.toBase64(); return mutationQueuesStore(transaction).put(metadata); }); } @@ -671,19 +676,6 @@ export function removeMutationBatch( return PersistencePromise.waitFor(promises).next(() => removedDocuments); } -function convertStreamToken(token: ProtoByteString): string { - if (token instanceof Uint8Array) { - // TODO(b/78771403): Convert tokens to strings during deserialization - assert( - SimpleDb.isMockPersistence(), - 'Persisting non-string stream tokens is only supported with mock persistence.' - ); - return token.toString(); - } else { - return token; - } -} - /** * Helper to get a typed SimpleDbStore for the mutations object store. */ diff --git a/packages/firestore/src/local/local_serializer.ts b/packages/firestore/src/local/local_serializer.ts index 82a7eab7a97..1f9e9a6b6c4 100644 --- a/packages/firestore/src/local/local_serializer.ts +++ b/packages/firestore/src/local/local_serializer.ts @@ -28,6 +28,7 @@ import { MutationBatch } from '../model/mutation_batch'; import * as api from '../protos/firestore_proto_api'; import { JsonProtoSerializer } from '../remote/serializer'; import { assert, fail } from '../util/assert'; +import { ByteString } from '../util/proto_byte_string'; import { documentKeySet, DocumentKeySet } from '../model/collections'; import { Target } from '../core/target'; @@ -42,7 +43,6 @@ import { DbTimestampKey, DbUnknownDocument } from './indexeddb_schema'; -import { SimpleDb } from './simple_db'; import { TargetData, TargetPurpose } from './target_data'; /** Serializer for values stored in the LocalStore. */ @@ -207,8 +207,6 @@ export class LocalSerializer { dbTarget.lastLimboFreeSnapshotVersion !== undefined ? this.fromDbTimestamp(dbTarget.lastLimboFreeSnapshotVersion) : SnapshotVersion.MIN; - // TODO(b/140573486): Convert to platform representation - const resumeToken = dbTarget.resumeToken; let target: Target; if (isDocumentQuery(dbTarget.query)) { @@ -223,7 +221,7 @@ export class LocalSerializer { dbTarget.lastListenSequenceNumber, version, lastLimboFreeSnapshotVersion, - resumeToken + ByteString.fromBase64String(dbTarget.resumeToken) ); } @@ -247,18 +245,9 @@ export class LocalSerializer { queryProto = this.remoteSerializer.toQueryTarget(targetData.target); } - let resumeToken: string; - - if (targetData.resumeToken instanceof Uint8Array) { - // TODO(b/78771403): Convert tokens to strings during deserialization - assert( - SimpleDb.isMockPersistence(), - 'Persisting non-string stream tokens is only supported with mock persistence .' - ); - resumeToken = targetData.resumeToken.toString(); - } else { - resumeToken = targetData.resumeToken; - } + // We can't store the resumeToken as a ByteString in IndexedDb, so we + // convert it to a base64 string for storage. + const resumeToken = targetData.resumeToken.toBase64(); // lastListenSequenceNumber is always 0 until we do real GC. return new DbTarget( diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index 1c319b4dea5..4e64b243389 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -20,7 +20,7 @@ import { User } from '../auth/user'; import { Query } from '../core/query'; import { SnapshotVersion } from '../core/snapshot_version'; import { Target } from '../core/target'; -import { BatchId, ProtoByteString, TargetId } from '../core/types'; +import { BatchId, TargetId } from '../core/types'; import { DocumentKeySet, documentKeySet, @@ -62,6 +62,7 @@ import { RemoteDocumentCache } from './remote_document_cache'; import { RemoteDocumentChangeBuffer } from './remote_document_change_buffer'; import { ClientId } from './shared_client_state'; import { TargetData, TargetPurpose } from './target_data'; +import { ByteString } from '../util/proto_byte_string'; const LOG_TAG = 'LocalStore'; @@ -464,7 +465,7 @@ export class LocalStore { } /** Returns the last recorded stream token for the current user. */ - getLastStreamToken(): Promise { + getLastStreamToken(): Promise { return this.persistence.runTransaction( 'Get last stream token', 'readonly-idempotent', @@ -479,7 +480,7 @@ export class LocalStore { * mutation batch. This is usually only useful after a stream handshake or in * response to an error that requires clearing the stream token. */ - setLastStreamToken(streamToken: ProtoByteString): Promise { + setLastStreamToken(streamToken: ByteString): Promise { return this.persistence.runTransaction( 'Set last stream token', 'readwrite-primary-idempotent', @@ -551,7 +552,7 @@ export class LocalStore { const resumeToken = change.resumeToken; // Update the resume token if the change includes one. - if (resumeToken.length > 0) { + if (resumeToken.approximateByteSize() > 0) { const newTargetData = oldTargetData .withResumeToken(resumeToken, remoteVersion) .withSequenceNumber(txn.currentSequenceNumber); @@ -696,12 +697,12 @@ export class LocalStore { change: TargetChange ): boolean { assert( - newTargetData.resumeToken.length > 0, + newTargetData.resumeToken.approximateByteSize() > 0, 'Attempted to persist target data with no resume token' ); // Always persist target data if we don't already have a resume token. - if (oldTargetData.resumeToken.length === 0) { + if (oldTargetData.resumeToken.approximateByteSize() === 0) { return true; } diff --git a/packages/firestore/src/local/memory_mutation_queue.ts b/packages/firestore/src/local/memory_mutation_queue.ts index 1c9c62eb280..5f27bea338b 100644 --- a/packages/firestore/src/local/memory_mutation_queue.ts +++ b/packages/firestore/src/local/memory_mutation_queue.ts @@ -17,14 +17,14 @@ import { Timestamp } from '../api/timestamp'; import { Query } from '../core/query'; -import { BatchId, ProtoByteString } from '../core/types'; +import { BatchId } from '../core/types'; import { DocumentKeySet } from '../model/collections'; import { DocumentKey } from '../model/document_key'; import { Mutation } from '../model/mutation'; import { MutationBatch, BATCHID_UNKNOWN } from '../model/mutation_batch'; -import { emptyByteString } from '../platform/platform'; import { assert } from '../util/assert'; import { primitiveComparator } from '../util/misc'; +import { ByteString } from '../util/proto_byte_string'; import { SortedMap } from '../util/sorted_map'; import { SortedSet } from '../util/sorted_set'; @@ -48,7 +48,7 @@ export class MemoryMutationQueue implements MutationQueue { * responses the client has processed. Stream tokens are opaque checkpoint * markers whose only real value is their inclusion in the next request. */ - private lastStreamToken: ProtoByteString = emptyByteString(); + private lastStreamToken: ByteString = ByteString.EMPTY_BYTE_STRING; /** An ordered mapping between documents and the mutations batch IDs. */ private batchesByDocumentKey = new SortedSet(DocReference.compareByKey); @@ -65,7 +65,7 @@ export class MemoryMutationQueue implements MutationQueue { acknowledgeBatch( transaction: PersistenceTransaction, batch: MutationBatch, - streamToken: ProtoByteString + streamToken: ByteString ): PersistencePromise { const batchId = batch.batchId; const batchIndex = this.indexOfExistingBatchId(batchId, 'acknowledged'); @@ -90,13 +90,13 @@ export class MemoryMutationQueue implements MutationQueue { getLastStreamToken( transaction: PersistenceTransaction - ): PersistencePromise { + ): PersistencePromise { return PersistencePromise.resolve(this.lastStreamToken); } setLastStreamToken( transaction: PersistenceTransaction, - streamToken: ProtoByteString + streamToken: ByteString ): PersistencePromise { this.lastStreamToken = streamToken; return PersistencePromise.resolve(); diff --git a/packages/firestore/src/local/mutation_queue.ts b/packages/firestore/src/local/mutation_queue.ts index a396ded8c3f..253f3baf49d 100644 --- a/packages/firestore/src/local/mutation_queue.ts +++ b/packages/firestore/src/local/mutation_queue.ts @@ -17,11 +17,12 @@ import { Timestamp } from '../api/timestamp'; import { Query } from '../core/query'; -import { BatchId, ProtoByteString } from '../core/types'; +import { BatchId } from '../core/types'; import { DocumentKeySet } from '../model/collections'; import { DocumentKey } from '../model/document_key'; import { Mutation } from '../model/mutation'; import { MutationBatch } from '../model/mutation_batch'; +import { ByteString } from '../util/proto_byte_string'; import { SortedMap } from '../util/sorted_map'; import { PersistenceTransaction } from './persistence'; @@ -38,18 +39,18 @@ export interface MutationQueue { acknowledgeBatch( transaction: PersistenceTransaction, batch: MutationBatch, - streamToken: ProtoByteString + streamToken: ByteString ): PersistencePromise; /** Returns the current stream token for this mutation queue. */ getLastStreamToken( transaction: PersistenceTransaction - ): PersistencePromise; + ): PersistencePromise; /** Sets the stream token for this mutation queue. */ setLastStreamToken( transaction: PersistenceTransaction, - streamToken: ProtoByteString + streamToken: ByteString ): PersistencePromise; /** diff --git a/packages/firestore/src/local/target_data.ts b/packages/firestore/src/local/target_data.ts index 06b168322c4..ad210979641 100644 --- a/packages/firestore/src/local/target_data.ts +++ b/packages/firestore/src/local/target_data.ts @@ -17,8 +17,8 @@ import { SnapshotVersion } from '../core/snapshot_version'; import { Target } from '../core/target'; -import { ListenSequenceNumber, ProtoByteString, TargetId } from '../core/types'; -import { emptyByteString } from '../platform/platform'; +import { ListenSequenceNumber, TargetId } from '../core/types'; +import { ByteString } from '../util/proto_byte_string'; /** An enumeration of the different purposes we have for targets. */ export enum TargetPurpose { @@ -66,7 +66,7 @@ export class TargetData { * matches the target. The resume token essentially identifies a point in * time from which the server should resume sending results. */ - readonly resumeToken: ProtoByteString = emptyByteString() + readonly resumeToken: ByteString = ByteString.EMPTY_BYTE_STRING ) {} /** Creates a new target data instance with an updated sequence number. */ @@ -87,7 +87,7 @@ export class TargetData { * snapshot version. */ withResumeToken( - resumeToken: ProtoByteString, + resumeToken: ByteString, snapshotVersion: SnapshotVersion ): TargetData { return new TargetData( @@ -128,7 +128,7 @@ export class TargetData { this.lastLimboFreeSnapshotVersion.isEqual( other.lastLimboFreeSnapshotVersion ) && - this.resumeToken === other.resumeToken && + this.resumeToken.isEqual(other.resumeToken) && this.target.isEqual(other.target) ); } diff --git a/packages/firestore/src/model/mutation_batch.ts b/packages/firestore/src/model/mutation_batch.ts index 28e3b6ebe4f..73986f7a3ec 100644 --- a/packages/firestore/src/model/mutation_batch.ts +++ b/packages/firestore/src/model/mutation_batch.ts @@ -17,9 +17,10 @@ import { Timestamp } from '../api/timestamp'; import { SnapshotVersion } from '../core/snapshot_version'; -import { BatchId, ProtoByteString } from '../core/types'; +import { BatchId } from '../core/types'; import { assert } from '../util/assert'; import * as misc from '../util/misc'; +import { ByteString } from '../util/proto_byte_string'; import { documentKeySet, DocumentKeySet, @@ -186,7 +187,7 @@ export class MutationBatchResult { readonly batch: MutationBatch, readonly commitVersion: SnapshotVersion, readonly mutationResults: MutationResult[], - readonly streamToken: ProtoByteString, + readonly streamToken: ByteString, /** * A pre-computed mapping from each mutated document to the resulting * version. @@ -203,7 +204,7 @@ export class MutationBatchResult { batch: MutationBatch, commitVersion: SnapshotVersion, results: MutationResult[], - streamToken: ProtoByteString + streamToken: ByteString ): MutationBatchResult { assert( batch.mutations.length === results.length, diff --git a/packages/firestore/src/platform/platform.ts b/packages/firestore/src/platform/platform.ts index bd54878e3cf..2263fed1d6b 100644 --- a/packages/firestore/src/platform/platform.ts +++ b/packages/firestore/src/platform/platform.ts @@ -16,7 +16,6 @@ */ import { DatabaseId, DatabaseInfo } from '../core/database_info'; -import { ProtoByteString } from '../core/types'; import { Connection } from '../remote/connection'; import { JsonProtoSerializer } from '../remote/serializer'; import { fail } from '../util/assert'; @@ -52,8 +51,6 @@ export interface Platform { /** True if and only if the Base64 conversion functions are available. */ readonly base64Available: boolean; - - readonly emptyByteString: ProtoByteString; } /** @@ -77,11 +74,3 @@ export class PlatformSupport { return PlatformSupport.platform; } } - -/** - * Returns the representation of an empty "proto" byte string for the - * platform. - */ -export function emptyByteString(): ProtoByteString { - return PlatformSupport.getPlatform().emptyByteString; -} diff --git a/packages/firestore/src/platform_browser/browser_platform.ts b/packages/firestore/src/platform_browser/browser_platform.ts index 2c6b523d028..c047def3773 100644 --- a/packages/firestore/src/platform_browser/browser_platform.ts +++ b/packages/firestore/src/platform_browser/browser_platform.ts @@ -28,8 +28,6 @@ import { WebChannelConnection } from './webchannel_connection'; export class BrowserPlatform implements Platform { readonly base64Available: boolean; - readonly emptyByteString = ''; - constructor() { this.base64Available = typeof atob !== 'undefined'; } diff --git a/packages/firestore/src/platform_node/node_platform.ts b/packages/firestore/src/platform_node/node_platform.ts index 8e66c72d3ee..8937f3fbe4d 100644 --- a/packages/firestore/src/platform_node/node_platform.ts +++ b/packages/firestore/src/platform_node/node_platform.ts @@ -31,8 +31,6 @@ import { loadProtos } from './load_protos'; export class NodePlatform implements Platform { readonly base64Available = true; - readonly emptyByteString = new Uint8Array(0); - readonly document = null; get window(): Window | null { diff --git a/packages/firestore/src/remote/persistent_stream.ts b/packages/firestore/src/remote/persistent_stream.ts index 6a6435bdba2..9f618e54e90 100644 --- a/packages/firestore/src/remote/persistent_stream.ts +++ b/packages/firestore/src/remote/persistent_stream.ts @@ -17,7 +17,7 @@ import { CredentialsProvider, Token } from '../api/credentials'; import { SnapshotVersion } from '../core/snapshot_version'; -import { ProtoByteString, TargetId } from '../core/types'; +import { TargetId } from '../core/types'; import { TargetData } from '../local/target_data'; import { Mutation, MutationResult } from '../model/mutation'; import * as api from '../protos/firestore_proto_api'; @@ -32,7 +32,7 @@ import { ExponentialBackoff } from './backoff'; import { Connection, Stream } from './connection'; import { JsonProtoSerializer } from './serializer'; import { WatchChange } from './watch_change'; -import { emptyByteString } from '../platform/platform'; +import { ByteString } from '../util/proto_byte_string'; const LOG_TAG = 'PersistentStream'; @@ -661,7 +661,7 @@ export class PersistentWriteStream extends PersistentStream< * PersistentWriteStream manages propagating this value from responses to the * next request. */ - lastStreamToken: ProtoByteString = emptyByteString(); + lastStreamToken: ByteString = ByteString.EMPTY_BYTE_STRING; /** * Tracks whether or not a handshake has been successfully exchanged and @@ -698,7 +698,7 @@ export class PersistentWriteStream extends PersistentStream< !!responseProto.streamToken, 'Got a write response without a stream token' ); - this.lastStreamToken = responseProto.streamToken; + this.lastStreamToken = this.serializer.fromBytes(responseProto.streamToken); if (!this.handshakeComplete_) { // The first response is always the handshake response @@ -748,14 +748,12 @@ export class PersistentWriteStream extends PersistentStream< 'Handshake must be complete before writing mutations' ); assert( - this.lastStreamToken.length > 0, + this.lastStreamToken.approximateByteSize() > 0, 'Trying to write mutation without a token' ); const request: WriteRequest = { - // Protos are typed with string, but we support UInt8Array on Node - // eslint-disable-next-line @typescript-eslint/no-explicit-any - streamToken: this.lastStreamToken as any, + streamToken: this.serializer.toBytes(this.lastStreamToken), writes: mutations.map(mutation => this.serializer.toMutation(mutation)) }; diff --git a/packages/firestore/src/remote/remote_event.ts b/packages/firestore/src/remote/remote_event.ts index ebbd84f539d..0cc38050657 100644 --- a/packages/firestore/src/remote/remote_event.ts +++ b/packages/firestore/src/remote/remote_event.ts @@ -16,7 +16,7 @@ */ import { SnapshotVersion } from '../core/snapshot_version'; -import { ProtoByteString, TargetId } from '../core/types'; +import { TargetId } from '../core/types'; import { documentKeySet, DocumentKeySet, @@ -24,8 +24,8 @@ import { MaybeDocumentMap, targetIdSet } from '../model/collections'; -import { emptyByteString } from '../platform/platform'; import { SortedSet } from '../util/sorted_set'; +import { ByteString } from '../util/proto_byte_string'; /** * An event from the RemoteStore. It is split into targetChanges (changes to the @@ -101,7 +101,7 @@ export class TargetChange { * query. The resume token essentially identifies a point in time from which * the server should resume sending results. */ - readonly resumeToken: ProtoByteString, + readonly resumeToken: ByteString, /** * The "current" (synced) status of this target. Note that "current" * has special meaning in the RPC protocol that implies that a target is @@ -135,7 +135,7 @@ export class TargetChange { current: boolean ): TargetChange { return new TargetChange( - emptyByteString(), + ByteString.EMPTY_BYTE_STRING, current, documentKeySet(), documentKeySet(), diff --git a/packages/firestore/src/remote/remote_store.ts b/packages/firestore/src/remote/remote_store.ts index 73d6b930bcc..e3f5cf544d1 100644 --- a/packages/firestore/src/remote/remote_store.ts +++ b/packages/firestore/src/remote/remote_store.ts @@ -26,7 +26,6 @@ import { MutationBatch, MutationBatchResult } from '../model/mutation_batch'; -import { emptyByteString } from '../platform/platform'; import { assert } from '../util/assert'; import { FirestoreError } from '../util/error'; import * as log from '../util/log'; @@ -52,6 +51,7 @@ import { WatchTargetChange, WatchTargetChangeState } from './watch_change'; +import { ByteString } from '../util/proto_byte_string'; const LOG_TAG = 'RemoteStore'; @@ -431,7 +431,7 @@ export class RemoteStore implements TargetMetadataProvider { // Update in-memory resume tokens. LocalStore will update the // persistent view of these when applying the completed RemoteEvent. objUtils.forEachNumber(remoteEvent.targetChanges, (targetId, change) => { - if (change.resumeToken.length > 0) { + if (change.resumeToken.approximateByteSize() > 0) { const targetData = this.listenTargets[targetId]; // A watched target might have been removed already. if (targetData) { @@ -455,7 +455,7 @@ export class RemoteStore implements TargetMetadataProvider { // Clear the resume token for the target, since we're in a known mismatch // state. this.listenTargets[targetId] = targetData.withResumeToken( - emptyByteString(), + ByteString.EMPTY_BYTE_STRING, targetData.snapshotVersion ); @@ -665,10 +665,10 @@ export class RemoteStore implements TargetMetadataProvider { 'RemoteStore error before completed handshake; resetting stream token: ', this.writeStream.lastStreamToken ); - this.writeStream.lastStreamToken = emptyByteString(); + this.writeStream.lastStreamToken = ByteString.EMPTY_BYTE_STRING; return this.localStore - .setLastStreamToken(emptyByteString()) + .setLastStreamToken(ByteString.EMPTY_BYTE_STRING) .catch(ignoreIfPrimaryLeaseLoss); } else { // Some other error, don't reset stream token. Our stream logic will diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 40e1ddc88c6..9ed0d043059 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -31,7 +31,7 @@ import { } from '../core/query'; import { SnapshotVersion } from '../core/snapshot_version'; import { Target } from '../core/target'; -import { ProtoByteString, TargetId } from '../core/types'; +import { TargetId } from '../core/types'; import { TargetData, TargetPurpose } from '../local/target_data'; import { Document, MaybeDocument, NoDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; @@ -53,6 +53,7 @@ import * as api from '../protos/firestore_proto_api'; import { assert, fail } from '../util/assert'; import { Code, FirestoreError } from '../util/error'; import * as obj from '../util/obj'; +import { ByteString } from '../util/proto_byte_string'; import * as typeUtils from '../util/types'; import { @@ -140,20 +141,6 @@ export class JsonProtoSerializer { private options: SerializerOptions ) {} - private emptyByteString(): ProtoByteString { - if (this.options.useProto3Json) { - return ''; - } else { - return new Uint8Array(0); - } - } - - private unsafeCastProtoByteString(byteString: ProtoByteString): string { - // byteStrings can be either string or UInt8Array, but the typings say - // it's always a string. Cast as string to avoid type check failing - return byteString as string; - } - fromRpcStatus(status: api.Status): FirestoreError { const code = status.code === undefined @@ -278,13 +265,33 @@ export class JsonProtoSerializer { * This method cheats. It's typed as returning "string" because that's what * our generated proto interfaces say bytes must be. But it should return * an Uint8Array in Node. + * + * Visible for testing. */ - private toBytes(bytes: Blob): string { + toBytes(bytes: Blob | ByteString): string { if (this.options.useProto3Json) { return bytes.toBase64(); } else { - // The typings say it's a string, but it needs to be a Uint8Array in Node. - return this.unsafeCastProtoByteString(bytes.toUint8Array()); + return (bytes.toUint8Array() as unknown) as string; + } + } + + /** + * Returns a ByteString based on the proto string value. + */ + fromBytes(value: string | Uint8Array | undefined): ByteString { + if (this.options.useProto3Json) { + assert( + value === undefined || typeof value === 'string', + 'value must be undefined or a string when using proto3 Json' + ); + return ByteString.fromBase64String(value ? value : ''); + } else { + assert( + value === undefined || value instanceof Uint8Array, + 'value must be undefined or Uint8Array' + ); + return ByteString.fromUint8Array(value ? value : new Uint8Array()); } } @@ -700,7 +707,7 @@ export class JsonProtoSerializer { targetChange: { targetChangeType: this.toWatchTargetChangeState(watchChange.state), targetIds: watchChange.targetIds, - resumeToken: this.unsafeCastProtoByteString(watchChange.resumeToken), + resumeToken: this.toBytes(watchChange.resumeToken), cause } }; @@ -718,8 +725,8 @@ export class JsonProtoSerializer { change.targetChange.targetChangeType || 'NO_CHANGE' ); const targetIds: TargetId[] = change.targetChange.targetIds || []; - const resumeToken = - change.targetChange.resumeToken || this.emptyByteString(); + + const resumeToken = this.fromBytes(change.targetChange.resumeToken); const causeProto = change.targetChange!.cause; const cause = causeProto && this.fromRpcStatus(causeProto); watchChange = new WatchTargetChange( @@ -1184,10 +1191,8 @@ export class JsonProtoSerializer { result.targetId = targetData.targetId; - if (targetData.resumeToken.length > 0) { - result.resumeToken = this.unsafeCastProtoByteString( - targetData.resumeToken - ); + if (targetData.resumeToken.approximateByteSize() > 0) { + result.resumeToken = this.toBytes(targetData.resumeToken); } return result; diff --git a/packages/firestore/src/remote/watch_change.ts b/packages/firestore/src/remote/watch_change.ts index d1940c0d13e..ecde3f2b589 100644 --- a/packages/firestore/src/remote/watch_change.ts +++ b/packages/firestore/src/remote/watch_change.ts @@ -16,7 +16,7 @@ */ import { SnapshotVersion } from '../core/snapshot_version'; -import { ProtoByteString, TargetId } from '../core/types'; +import { TargetId } from '../core/types'; import { ChangeType } from '../core/view_snapshot'; import { TargetData, TargetPurpose } from '../local/target_data'; import { @@ -26,7 +26,6 @@ import { } from '../model/collections'; import { Document, MaybeDocument, NoDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; -import { emptyByteString } from '../platform/platform'; import { assert, fail } from '../util/assert'; import { FirestoreError } from '../util/error'; import { debug } from '../util/log'; @@ -36,6 +35,7 @@ import { SortedMap } from '../util/sorted_map'; import { SortedSet } from '../util/sorted_set'; import { ExistenceFilter } from './existence_filter'; import { RemoteEvent, TargetChange } from './remote_event'; +import { ByteString } from '../util/proto_byte_string'; /** * Internal representation of the watcher API protocol buffers. @@ -94,7 +94,7 @@ export class WatchTargetChange { * matches the target. The resume token essentially identifies a point in * time from which the server should resume sending results. */ - public resumeToken: ProtoByteString = emptyByteString(), + public resumeToken: ByteString = ByteString.EMPTY_BYTE_STRING, /** An RPC error indicating why the watch failed. */ public cause: FirestoreError | null = null ) {} @@ -120,7 +120,7 @@ class TargetState { > = snapshotChangesMap(); /** See public getters for explanations of these fields. */ - private _resumeToken: ProtoByteString = emptyByteString(); + private _resumeToken: ByteString = ByteString.EMPTY_BYTE_STRING; private _current = false; /** @@ -143,7 +143,7 @@ class TargetState { } /** The last resume token sent to us for this target. */ - get resumeToken(): ProtoByteString { + get resumeToken(): ByteString { return this._resumeToken; } @@ -161,8 +161,8 @@ class TargetState { * Applies the resume token to the TargetChange, but only when it has a new * value. Empty resumeTokens are discarded. */ - updateResumeToken(resumeToken: ProtoByteString): void { - if (resumeToken.length > 0) { + updateResumeToken(resumeToken: ByteString): void { + if (resumeToken.approximateByteSize() > 0) { this._hasPendingChanges = true; this._resumeToken = resumeToken; } diff --git a/packages/firestore/src/util/proto_byte_string.ts b/packages/firestore/src/util/proto_byte_string.ts new file mode 100644 index 00000000000..b6c0c3b9c00 --- /dev/null +++ b/packages/firestore/src/util/proto_byte_string.ts @@ -0,0 +1,84 @@ +/** + * @license + * 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. + * 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 { PlatformSupport } from '../platform/platform'; + +/** + * Immutable class that represents a "proto" byte string. + * + * Proto byte strings can either be Base64-encoded strings or Uint8Arrays when + * sent on the wire. This class abstracts away this differentiation by holding + * the proto byte string in a common class that must be converted into a string + * before being sent as a proto. + */ +export class ByteString { + static readonly EMPTY_BYTE_STRING = new ByteString(''); + + private readonly _binaryString: string; + + private constructor(binaryString: string) { + this._binaryString = binaryString; + } + + static fromBase64String(base64: string): ByteString { + const binaryString = PlatformSupport.getPlatform().atob(base64); + return new ByteString(binaryString); + } + + static fromUint8Array(array: Uint8Array): ByteString { + const binaryString = binaryStringFromUint8Array(array); + return new ByteString(binaryString); + } + + toBase64(): string { + return PlatformSupport.getPlatform().btoa(this._binaryString); + } + + toUint8Array(): Uint8Array { + return uint8ArrayFromBinaryString(this._binaryString); + } + + approximateByteSize(): number { + return this._binaryString.length * 2; + } + + isEqual(other: ByteString): boolean { + return this._binaryString === other._binaryString; + } +} + +/** + * Helper function to convert an Uint8array to a binary string. + */ +export function binaryStringFromUint8Array(array: Uint8Array): string { + let binaryString = ''; + for (let i = 0; i < array.length; ++i) { + binaryString += String.fromCharCode(array[i]); + } + return binaryString; +} + +/** + * Helper function to convert a binary string to an Uint8Array. + */ +export function uint8ArrayFromBinaryString(binaryString: string): Uint8Array { + const buffer = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + buffer[i] = binaryString.charCodeAt(i); + } + return buffer; +} diff --git a/packages/firestore/test/unit/local/local_store.test.ts b/packages/firestore/test/unit/local/local_store.test.ts index 8e19580dc14..d4ce4913126 100644 --- a/packages/firestore/test/unit/local/local_store.test.ts +++ b/packages/firestore/test/unit/local/local_store.test.ts @@ -45,7 +45,6 @@ import { MutationBatchResult, BATCHID_UNKNOWN } from '../../../src/model/mutation_batch'; -import { emptyByteString } from '../../../src/platform/platform'; import { RemoteEvent } from '../../../src/remote/remote_event'; import { WatchChangeAggregator, @@ -72,12 +71,14 @@ import { TestSnapshotVersion, transformMutation, unknownDoc, - version + version, + byteStringFromString } from '../../util/helpers'; import { FieldValue, IntegerValue } from '../../../src/model/field_value'; import { CountingQueryEngine } from './counting_query_engine'; import * as persistenceHelpers from './persistence_test_helpers'; +import { ByteString } from '../../../src/util/proto_byte_string'; class LocalStoreTester { private promiseChain: Promise = Promise.resolve(); @@ -175,7 +176,7 @@ class LocalStoreTester { batch, ver, mutationResults, - /*streamToken=*/ emptyByteString() + /*streamToken=*/ ByteString.EMPTY_BYTE_STRING ); return this.localStore.acknowledgeBatch(write); @@ -1124,7 +1125,7 @@ function genericLocalStoreTests( const query = Query.atPath(path('foo/bar')); const targetData = await localStore.allocateTarget(query.toTarget()); const targetId = targetData.targetId; - const resumeToken = 'abc'; + const resumeToken = byteStringFromString('abc'); const watchChange = new WatchTargetChange( WatchTargetChangeState.Current, [targetId], @@ -1156,12 +1157,12 @@ function genericLocalStoreTests( const query = Query.atPath(path('foo/bar')); const targetData = await localStore.allocateTarget(query.toTarget()); const targetId = targetData.targetId; - const resumeToken = 'abc'; + const resumeToken = byteStringFromString('abc'); const watchChange1 = new WatchTargetChange( WatchTargetChangeState.Current, [targetId], - resumeToken + byteStringFromString('abc') ); const aggregator1 = new WatchChangeAggregator({ getRemoteKeysForTarget: () => documentKeySet(), @@ -1174,7 +1175,7 @@ function genericLocalStoreTests( const watchChange2 = new WatchTargetChange( WatchTargetChangeState.Current, [targetId], - emptyByteString() + ByteString.EMPTY_BYTE_STRING ); const aggregator2 = new WatchChangeAggregator({ getRemoteKeysForTarget: () => documentKeySet(), @@ -1527,7 +1528,11 @@ function genericLocalStoreTests( ) ) .after( - noChangeEvent(/* targetId= */ 2, /* snapshotVersion= */ 10, 'foo') + noChangeEvent( + /* targetId= */ 2, + /* snapshotVersion= */ 10, + /* resumeToken= */ byteStringFromString('foo') + ) ) .after(localViewChanges(2, /* fromCache= */ false, {})) .afterExecutingQuery(query) @@ -1552,7 +1557,11 @@ function genericLocalStoreTests( // Advance the query snapshot await localStore.applyRemoteEvent( - noChangeEvent(targetData.targetId, 10, 'resumeToken') + noChangeEvent( + /* targetId= */ targetData.targetId, + /* snapshotVersion= */ 10, + /* resumeToken= */ byteStringFromString('foo') + ) ); // At this point, we have not yet confirmed that the query is limbo free. @@ -1665,7 +1674,7 @@ function genericLocalStoreTests( noChangeEvent( /* targetId= */ 2, /* snapshotVersion= */ 10, - 'resumeToken' + /* resumeToken= */ byteStringFromString('foo') ) ) .after(localViewChanges(2, /* fromCache= */ false, {})) @@ -1727,7 +1736,7 @@ function genericLocalStoreTests( noChangeEvent( /* targetId= */ 2, /* snapshotVersion= */ 10, - 'resumeToken' + /* resumeToken= */ byteStringFromString('foo') ) ) .after(localViewChanges(2, /* fromCache= */ false, {})) diff --git a/packages/firestore/test/unit/local/mutation_queue.test.ts b/packages/firestore/test/unit/local/mutation_queue.test.ts index 9f4d3c26f05..eff81159d47 100644 --- a/packages/firestore/test/unit/local/mutation_queue.test.ts +++ b/packages/firestore/test/unit/local/mutation_queue.test.ts @@ -23,18 +23,19 @@ import { Persistence } from '../../../src/local/persistence'; import { ReferenceSet } from '../../../src/local/reference_set'; import { documentKeySet } from '../../../src/model/collections'; import { MutationBatch } from '../../../src/model/mutation_batch'; -import { emptyByteString } from '../../../src/platform/platform'; import { expectEqualArrays, key, patchMutation, path, - setMutation + setMutation, + byteStringFromString } from '../../util/helpers'; import { addEqualityMatcher } from '../../util/equality_matcher'; import * as persistenceHelpers from './persistence_test_helpers'; import { TestMutationQueue } from './test_mutation_queue'; +import { ByteString } from '../../../src/util/proto_byte_string'; let persistence: Persistence; let mutationQueue: TestMutationQueue; @@ -153,7 +154,7 @@ function genericMutationQueueTests(): void { const batch1 = await addMutationBatch(); expect(await mutationQueue.countBatches()).to.equal(1); - await mutationQueue.acknowledgeBatch(batch1, emptyByteString()); + await mutationQueue.acknowledgeBatch(batch1, ByteString.EMPTY_BYTE_STRING); await mutationQueue.removeMutationBatch(batch1); expect(await mutationQueue.countBatches()).to.equal(0); @@ -307,8 +308,8 @@ function genericMutationQueueTests(): void { }); it('can save the last stream token', async () => { - const streamToken1 = 'token1'; - const streamToken2 = 'token2'; + const streamToken1 = byteStringFromString('token1'); + const streamToken2 = byteStringFromString('token2'); await mutationQueue.setLastStreamToken(streamToken1); diff --git a/packages/firestore/test/unit/local/test_mutation_queue.ts b/packages/firestore/test/unit/local/test_mutation_queue.ts index af26b06dc6c..ec9e44ee714 100644 --- a/packages/firestore/test/unit/local/test_mutation_queue.ts +++ b/packages/firestore/test/unit/local/test_mutation_queue.ts @@ -17,7 +17,7 @@ import { Timestamp } from '../../../src/api/timestamp'; import { Query } from '../../../src/core/query'; -import { BatchId, ProtoByteString } from '../../../src/core/types'; +import { BatchId } from '../../../src/core/types'; import { MutationQueue } from '../../../src/local/mutation_queue'; import { Persistence } from '../../../src/local/persistence'; import { DocumentKeySet } from '../../../src/model/collections'; @@ -25,6 +25,7 @@ import { DocumentKey } from '../../../src/model/document_key'; import { Mutation } from '../../../src/model/mutation'; import { MutationBatch } from '../../../src/model/mutation_batch'; import { SortedMap } from '../../../src/util/sorted_map'; +import { ByteString } from '../../../src/util/proto_byte_string'; /** * A wrapper around a MutationQueue that automatically creates a @@ -53,7 +54,7 @@ export class TestMutationQueue { acknowledgeBatch( batch: MutationBatch, - streamToken: ProtoByteString + streamToken: ByteString ): Promise { return this.persistence.runTransaction( 'acknowledgeThroughBatchId', @@ -64,23 +65,17 @@ export class TestMutationQueue { ); } - getLastStreamToken(): Promise { + getLastStreamToken(): Promise { return this.persistence.runTransaction( 'getLastStreamToken', 'readonly-idempotent', txn => { - return this.queue.getLastStreamToken(txn).next(token => { - if (typeof token === 'string') { - return token; - } else { - throw new Error('Test mutation queue cannot handle Uint8Arrays'); - } - }); + return this.queue.getLastStreamToken(txn); } ); } - setLastStreamToken(streamToken: string): Promise { + setLastStreamToken(streamToken: ByteString): Promise { return this.persistence.runTransaction( 'setLastStreamToken', 'readwrite-primary-idempotent', diff --git a/packages/firestore/test/unit/remote/node/serializer.test.ts b/packages/firestore/test/unit/remote/node/serializer.test.ts index ba009a08473..7e0ceb09f74 100644 --- a/packages/firestore/test/unit/remote/node/serializer.test.ts +++ b/packages/firestore/test/unit/remote/node/serializer.test.ts @@ -81,8 +81,10 @@ import { transformMutation, version, wrap, - wrapObject + wrapObject, + byteStringFromString } from '../../../util/helpers'; +import { ByteString } from '../../../../src/util/proto_byte_string'; describe('Serializer', () => { const partition = new DatabaseId('p', 'd'); @@ -1249,7 +1251,7 @@ describe('Serializer', () => { 4, SnapshotVersion.MIN, SnapshotVersion.MIN, - new Uint8Array([1, 2, 3]) + ByteString.fromUint8Array(new Uint8Array([1, 2, 3])) ) ); const expected = { @@ -1347,14 +1349,14 @@ describe('Serializer', () => { const expected = new WatchTargetChange( WatchTargetChangeState.Removed, [1, 4], - 'token', + byteStringFromString('token'), new FirestoreError(Code.CANCELLED, 'message') ); const actual = s.fromWatchChange({ targetChange: { targetChangeType: 'REMOVE', targetIds: [1, 4], - resumeToken: 'token', + resumeToken: s.toBytes(byteStringFromString('token')), cause: { code: 1, message: 'message' } } }); @@ -1392,14 +1394,14 @@ describe('Serializer', () => { const expected = new WatchTargetChange( WatchTargetChangeState.Removed, [1, 4], - 'resume', + byteStringFromString('resume'), new FirestoreError(Code.CANCELLED, 'message') ); const actual = s.fromWatchChange({ targetChange: { targetChangeType: 'REMOVE', targetIds: [1, 4], - resumeToken: 'resume', + resumeToken: s.toBytes(byteStringFromString('resume')), cause: { code: 1, message: 'message' } } }); diff --git a/packages/firestore/test/unit/remote/remote_event.test.ts b/packages/firestore/test/unit/remote/remote_event.test.ts index b9361631f85..e207bdb9f2c 100644 --- a/packages/firestore/test/unit/remote/remote_event.test.ts +++ b/packages/firestore/test/unit/remote/remote_event.test.ts @@ -21,7 +21,6 @@ import { TargetId } from '../../../src/core/types'; import { TargetData, TargetPurpose } from '../../../src/local/target_data'; import { DocumentKeySet, documentKeySet } from '../../../src/model/collections'; import { DocumentKey } from '../../../src/model/document_key'; -import { emptyByteString } from '../../../src/platform/platform'; import { ExistenceFilter } from '../../../src/remote/existence_filter'; import { RemoteEvent, TargetChange } from '../../../src/remote/remote_event'; import { @@ -43,6 +42,7 @@ import { updateMapping, version } from '../../util/helpers'; +import { ByteString } from '../../../src/util/proto_byte_string'; interface TargetMap { [targetId: string]: TargetData; @@ -77,7 +77,7 @@ function expectTargetChangeEquals( expected: TargetChange ): void { expect(actual.current).to.equal(expected.current, 'TargetChange.current'); - expect(actual.resumeToken).to.equal( + expect(actual.resumeToken).to.deep.equal( expected.resumeToken, 'TargetChange.resumeToken' ); @@ -475,7 +475,7 @@ describe('RemoteEvent', () => { const markCurrent = new WatchTargetChange( WatchTargetChangeState.Current, [1], - emptyByteString() + ByteString.EMPTY_BYTE_STRING ); const aggregator = createAggregator({ diff --git a/packages/firestore/test/unit/specs/spec_builder.ts b/packages/firestore/test/unit/specs/spec_builder.ts index d988eaf91ce..cd026849e85 100644 --- a/packages/firestore/test/unit/specs/spec_builder.ts +++ b/packages/firestore/test/unit/specs/spec_builder.ts @@ -54,6 +54,7 @@ import { SpecWriteAck, SpecWriteFailure } from './spec_test_runner'; +import { PlatformSupport } from '../../../src/platform/platform'; // These types are used in a protected API by SpecBuilder and need to be // exported. @@ -951,18 +952,21 @@ export class SpecBuilder { // `query` is not added yet. this.activeTargets[targetId] = { queries: [SpecBuilder.queryToSpec(query), ...activeQueries], - resumeToken: resumeToken || '' + // Convert to base64 string so it can later be parsed into ByteString. + resumeToken: PlatformSupport.getPlatform().btoa(resumeToken || '') }; } else { this.activeTargets[targetId] = { queries: activeQueries, - resumeToken: resumeToken || '' + // Convert to base64 string so it can later be parsed into ByteString. + resumeToken: PlatformSupport.getPlatform().btoa(resumeToken || '') }; } } else { this.activeTargets[targetId] = { queries: [SpecBuilder.queryToSpec(query)], - resumeToken: resumeToken || '' + // Convert to base64 string so it can later be parsed into ByteString. + resumeToken: PlatformSupport.getPlatform().btoa(resumeToken || '') }; } } diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index 87adb5d630b..7c5affad206 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -30,7 +30,6 @@ import { SyncEngine } from '../../../src/core/sync_engine'; import { OnlineState, OnlineStateSource, - ProtoByteString, TargetId } from '../../../src/core/types'; import { @@ -62,10 +61,7 @@ import { DocumentOptions } from '../../../src/model/document'; import { DocumentKey } from '../../../src/model/document_key'; import { JsonObject } from '../../../src/model/field_value'; import { Mutation } from '../../../src/model/mutation'; -import { - emptyByteString, - PlatformSupport -} from '../../../src/platform/platform'; +import { PlatformSupport } from '../../../src/platform/platform'; import * as api from '../../../src/protos/firestore_proto_api'; import { Connection, Stream } from '../../../src/remote/connection'; import { Datastore } from '../../../src/remote/datastore'; @@ -101,7 +97,8 @@ import { path, setMutation, TestSnapshotVersion, - version + version, + byteStringFromString } from '../../util/helpers'; import { SharedFakeWebStorage, TestPlatform } from '../../util/test_platform'; import { @@ -111,6 +108,7 @@ import { TEST_SERIALIZER } from '../local/persistence_test_helpers'; import { MULTI_CLIENT_TAG } from './describe_spec'; +import { ByteString } from '../../../src/util/proto_byte_string'; const ARBITRARY_SEQUENCE_NUMBER = 2; @@ -200,7 +198,10 @@ class MockConnection implements Connection { ackWrite(commitTime?: string, mutationResults?: api.WriteResult[]): void { this.writeStream!.callOnMessage({ - streamToken: 'write-stream-token-' + this.nextWriteStreamToken, + // Convert to base64 string so it can later be parsed into ByteString. + streamToken: PlatformSupport.getPlatform().btoa( + 'write-stream-token-' + this.nextWriteStreamToken + ), commitTime, writeResults: mutationResults }); @@ -723,7 +724,7 @@ abstract class TestRunner { private doWatchCurrent(currentTargets: SpecWatchCurrent): Promise { const targets = currentTargets[0]; - const resumeToken = currentTargets[1] as ProtoByteString; + const resumeToken = byteStringFromString(currentTargets[1]); const change = new WatchTargetChange( WatchTargetChangeState.Current, targets, @@ -750,7 +751,7 @@ abstract class TestRunner { const change = new WatchTargetChange( WatchTargetChangeState.Removed, removed.targetIds, - emptyByteString(), + ByteString.EMPTY_BYTE_STRING, cause || null ); if (cause) { @@ -831,7 +832,8 @@ abstract class TestRunner { const protoJSON: api.ListenResponse = { targetChange: { readTime: this.serializer.toVersion(version(watchSnapshot.version)), - resumeToken: watchSnapshot.resumeToken, + // Convert to base64 string so it can later be parsed into ByteString. + resumeToken: this.platform.btoa(watchSnapshot.resumeToken || ''), targetIds: watchSnapshot.targetIds } }; @@ -1143,13 +1145,17 @@ abstract class TestRunner { ARBITRARY_SEQUENCE_NUMBER, SnapshotVersion.MIN, SnapshotVersion.MIN, - expected.resumeToken + byteStringFromString(expected.resumeToken) ) ); expect(actualTarget.query).to.deep.equal(expectedTarget.query); expect(actualTarget.targetId).to.equal(expectedTarget.targetId); expect(actualTarget.readTime).to.equal(expectedTarget.readTime); - expect(actualTarget.resumeToken).to.equal(expectedTarget.resumeToken); + // actualTarget's resumeToken is a string, but the serialized + // resumeToken will be a base64 string, so we need to convert it back. + expect(actualTarget.resumeToken || '').to.equal( + this.platform.atob(expectedTarget.resumeToken || '') + ); delete actualTargets[targetId]; }); expect(obj.size(actualTargets)).to.equal( diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index 7ac49374986..5475ef49096 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -34,7 +34,7 @@ import { OrderBy } from '../../src/core/query'; import { SnapshotVersion } from '../../src/core/snapshot_version'; -import { ProtoByteString, TargetId } from '../../src/core/types'; +import { TargetId } from '../../src/core/types'; import { AddedLimboDocument, LimboDocumentChange, @@ -75,7 +75,6 @@ import { TransformMutation } from '../../src/model/mutation'; import { FieldPath, ResourcePath } from '../../src/model/path'; -import { emptyByteString } from '../../src/platform/platform'; import { RemoteEvent, TargetChange } from '../../src/remote/remote_event'; import { DocumentWatchChange, @@ -89,6 +88,8 @@ import { Dict, forEach } from '../../src/util/obj'; import { SortedMap } from '../../src/util/sorted_map'; import { SortedSet } from '../../src/util/sorted_set'; import { query } from './api_helpers'; +import { ByteString } from '../../src/util/proto_byte_string'; +import { PlatformSupport } from '../../src/platform/platform'; export type TestSnapshotVersion = number; @@ -288,7 +289,7 @@ export function targetData( export function noChangeEvent( targetId: number, snapshotVersion: number, - resumeToken: ProtoByteString = emptyByteString() + resumeToken: ByteString = ByteString.EMPTY_BYTE_STRING ): RemoteEvent { const aggregator = new WatchChangeAggregator({ getRemoteKeysForTarget: () => documentKeySet(), @@ -481,14 +482,22 @@ export function localViewChanges( return new LocalViewChanges(targetId, fromCache, addedKeys, removedKeys); } +/** + * Returns a ByteString representation for the platform from the given string. + */ +export function byteStringFromString(value: string): ByteString { + const base64 = PlatformSupport.getPlatform().btoa(value); + return ByteString.fromBase64String(base64); +} + /** Creates a resume token to match the given snapshot version. */ export function resumeTokenForSnapshot( snapshotVersion: SnapshotVersion -): ProtoByteString { +): ByteString { if (snapshotVersion.isEqual(SnapshotVersion.MIN)) { - return emptyByteString(); + return ByteString.EMPTY_BYTE_STRING; } else { - return snapshotVersion.toString(); + return byteStringFromString(snapshotVersion.toString()); } } diff --git a/packages/firestore/test/util/test_platform.ts b/packages/firestore/test/util/test_platform.ts index 3d09b59dd46..3ed3526ed89 100644 --- a/packages/firestore/test/util/test_platform.ts +++ b/packages/firestore/test/util/test_platform.ts @@ -16,7 +16,6 @@ */ import { DatabaseId, DatabaseInfo } from '../../src/core/database_info'; -import { ProtoByteString } from '../../src/core/types'; import { Platform } from '../../src/platform/platform'; import { Connection } from '../../src/remote/connection'; import { JsonProtoSerializer } from '../../src/remote/serializer'; @@ -234,10 +233,6 @@ export class TestPlatform implements Platform { return this.basePlatform.base64Available; } - get emptyByteString(): ProtoByteString { - return this.basePlatform.emptyByteString; - } - raiseVisibilityEvent(visibility: VisibilityState): void { if (this.mockDocument) { this.mockDocument.raiseVisibilityEvent(visibility);