Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/firebase/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1977,6 +1977,25 @@ declare namespace firebase.firestore {
*/
static arrayRemove(...elements: any[]): FieldValue;

/**
* Returns a special value that can be used with set() or update() that tells
* the server to add the given value to the field's current value.
*
* If either the operand or the current field value uses floating point
* precision, all arithmetic will follow IEEE 754 semantics. If both values
* are integers, values outside of JavaScript's safe number range
* (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`) are also subject
* to precision loss. Furthermore, once processed by the Firestore backend,
* all integer operations are capped between -2^63 and 2^63-1.
*
* If the current field value is not of type 'number', or if the field does
* not yet exist, the transformation will set the field to the given value.
*
* @param n The value to add.
* @return The FieldValue sentinel for use in a call to set() or update().
*/
static numericAdd(n: number): FieldValue;

/**
* Returns true if this `FieldValue` is equal to the provided one.
*
Expand Down
19 changes: 19 additions & 0 deletions packages/firestore-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,25 @@ export class FieldValue {
*/
static arrayRemove(...elements: any[]): FieldValue;

/**
* Returns a special value that can be used with set() or update() that tells
* the server to add the given value to the field's current value.
*
* If either the operand or the current field value uses floating point
* precision, all arithmetic will follow IEEE 754 semantics. If both values
* are integers, values outside of JavaScript's safe number range
* (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`) are also subject
* to precision loss. Furthermore, once processed by the Firestore backend,
* all integer operations are capped between -2^63 and 2^63-1.
*
* If the current field value is not of type 'number', or if the field does
* not yet exist, the transformation will set the field to the given value.
*
* @param n The value to add.
* @return The FieldValue sentinel for use in a call to set() or update().
*/
static numericAdd(n: number): FieldValue;

/**
* Returns true if this `FieldValue` is equal to the provided one.
*
Expand Down
21 changes: 20 additions & 1 deletion packages/firestore/src/api/field_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
import * as firestore from '@firebase/firestore-types';

import { makeConstructorPrivate } from '../util/api';
import { validateAtLeastNumberOfArgs } from '../util/input_validation';
import {
validateArgType,
validateAtLeastNumberOfArgs,
validateExactNumberOfArgs,
validateNoArgs
} from '../util/input_validation';
import { AnyJs } from '../util/misc';

/**
Expand All @@ -29,10 +34,12 @@ export abstract class FieldValueImpl implements firestore.FieldValue {
protected constructor(readonly _methodName: string) {}

static delete(): FieldValueImpl {
validateNoArgs('FieldValue.delete', arguments);
return DeleteFieldValueImpl.instance;
}

static serverTimestamp(): FieldValueImpl {
validateNoArgs('FieldValue.serverTimestamp', arguments);
return ServerTimestampFieldValueImpl.instance;
}

Expand All @@ -50,6 +57,12 @@ export abstract class FieldValueImpl implements firestore.FieldValue {
return new ArrayRemoveFieldValueImpl(elements);
}

static numericAdd(n: number): FieldValueImpl {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably use validateExactNumberOfArgs() to verify we got 1 arg. delete() and serverTimestamp() should probably be verifying no args.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I added a validateNoArgs and used it in delete() and serverTimestamp().

validateArgType('FieldValue.numericAdd', 'number', 1, n);
validateExactNumberOfArgs('FieldValue.numericAdd', arguments, 1);
return new NumericAddFieldValueImpl(n);
}

isEqual(other: FieldValueImpl): boolean {
return this === other;
}
Expand Down Expand Up @@ -83,6 +96,12 @@ export class ArrayRemoveFieldValueImpl extends FieldValueImpl {
}
}

export class NumericAddFieldValueImpl extends FieldValueImpl {
constructor(readonly _operand: number) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would do public readonly to make the intent clear.

super('FieldValue.numericAdd');
}
}

// Public instance that disallows construction at runtime. This constructor is
// used when exporting FieldValueImpl on firebase.firestore.FieldValue and will
// be called FieldValue publicly. Internally we still use FieldValueImpl which
Expand Down
13 changes: 12 additions & 1 deletion packages/firestore/src/api/user_data_converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import * as firestore from '@firebase/firestore-types';
import { Timestamp } from '../api/timestamp';
import { DatabaseId } from '../core/database_info';
import { DocumentKey } from '../model/document_key';
import { FieldValue, ObjectValue } from '../model/field_value';
import { FieldValue, NumberValue, ObjectValue } from '../model/field_value';
import {
ArrayValue,
BlobValue,
Expand Down Expand Up @@ -54,6 +54,7 @@ import * as typeUtils from '../util/types';
import {
ArrayRemoveTransformOperation,
ArrayUnionTransformOperation,
NumericAddTransformOperation,
ServerTimestampTransform
} from '../model/transform_operation';
import { Blob } from './blob';
Expand All @@ -66,6 +67,7 @@ import {
ArrayUnionFieldValueImpl,
DeleteFieldValueImpl,
FieldValueImpl,
NumericAddFieldValueImpl,
ServerTimestampFieldValueImpl
} from './field_value';
import { GeoPoint } from './geo_point';
Expand Down Expand Up @@ -644,6 +646,15 @@ export class UserDataConverter {
context.fieldTransforms.push(
new FieldTransform(context.path, arrayRemove)
);
} else if (value instanceof NumericAddFieldValueImpl) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative if you are not matching super class.

switch( value.constructor) {
  case NumericAddFieldValueImpl: 
....
}

const operand = this.parseQueryValue(
'FieldValue.numericAdd',
value._operand
) as NumberValue;
const numericAdd = new NumericAddTransformOperation(operand);
context.fieldTransforms.push(
new FieldTransform(context.path, numericAdd)
);
} else {
fail('Unknown FieldValue type: ' + value);
}
Expand Down
8 changes: 7 additions & 1 deletion packages/firestore/src/local/indexeddb_mutation_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export class IndexedDbMutationQueue implements MutationQueue {
addMutationBatch(
transaction: PersistenceTransaction,
localWriteTime: Timestamp,
baseMutations: Mutation[],
mutations: Mutation[]
): PersistencePromise<MutationBatch> {
const documentStore = documentMutationsStore(transaction);
Expand All @@ -165,7 +166,12 @@ export class IndexedDbMutationQueue implements MutationQueue {
return mutationStore.add({} as any).next(batchId => {
assert(typeof batchId === 'number', 'Auto-generated key is not a number');

const batch = new MutationBatch(batchId, localWriteTime, mutations);
const batch = new MutationBatch(
batchId,
localWriteTime,
baseMutations,
mutations
);
const dbBatch = this.serializer.toDbMutationBatch(this.userId, batch);

this.documentKeysByBatchId[batchId] = batch.keys();
Expand Down
13 changes: 13 additions & 0 deletions packages/firestore/src/local/indexeddb_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,19 @@ export class DbMutationBatch {
* epoch.
*/
public localWriteTimeMs: number,
/**
* A list of "mutations" that represent a partial base state from when this
* write batch was initially created. During local application of the write
* batch, these baseMutations are applied prior to the real writes in order
* to override certain document fields from the remote document cache. This
* is necessary in the case of non-idempotent writes (e.g. `numericAdd()`
* transforms) to make sure that the local view of the modified documents
* doesn't flicker if the remote document cache receives the result of the
* non-idempotent write before the write is removed from the queue.
*
* These mutations are never sent to the backend.
*/
public baseMutations: api.Write[] | undefined,
/**
* A list of mutations to apply. All mutations will be applied atomically.
*
Expand Down
14 changes: 13 additions & 1 deletion packages/firestore/src/local/local_serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,24 +118,36 @@ export class LocalSerializer {

/** Encodes a batch of mutations into a DbMutationBatch for local storage. */
toDbMutationBatch(userId: string, batch: MutationBatch): DbMutationBatch {
const serializedBaseMutations = batch.baseMutations.map(m =>
this.remoteSerializer.toMutation(m)
);
const serializedMutations = batch.mutations.map(m =>
this.remoteSerializer.toMutation(m)
);
return new DbMutationBatch(
userId,
batch.batchId,
batch.localWriteTime.toMillis(),
serializedBaseMutations,
serializedMutations
);
}

/** Decodes a DbMutationBatch into a MutationBatch */
fromDbMutationBatch(dbBatch: DbMutationBatch): MutationBatch {
const baseMutations = (dbBatch.baseMutations || []).map(m =>
this.remoteSerializer.fromMutation(m)
);
const mutations = dbBatch.mutations.map(m =>
this.remoteSerializer.fromMutation(m)
);
const timestamp = Timestamp.fromMillis(dbBatch.localWriteTimeMs);
return new MutationBatch(dbBatch.batchId, timestamp, mutations);
return new MutationBatch(
dbBatch.batchId,
timestamp,
baseMutations,
mutations
);
}

/*
Expand Down
74 changes: 58 additions & 16 deletions packages/firestore/src/local/local_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import {
DocumentMap,
MaybeDocumentMap
} from '../model/collections';
import { MaybeDocument } from '../model/document';
import { Document, MaybeDocument } from '../model/document';
import { DocumentKey } from '../model/document_key';
import { Mutation } from '../model/mutation';
import { Mutation, PatchMutation, Precondition } from '../model/mutation';
import {
BATCHID_UNKNOWN,
MutationBatch,
Expand All @@ -38,6 +38,7 @@ import { assert } from '../util/assert';
import * as log from '../util/log';
import * as objUtils from '../util/obj';

import { ObjectValue } from '../model/field_value';
import { LocalDocumentsView } from './local_documents_view';
import { LocalViewChanges } from './local_view_changes';
import { MutationQueue } from './mutation_queue';
Expand Down Expand Up @@ -242,24 +243,65 @@ export class LocalStore {
}
/* Accept locally generated Mutations and commit them to storage. */
localWrite(mutations: Mutation[]): Promise<LocalWriteResult> {
const localWriteTime = Timestamp.now();
const keys = mutations.reduce(
(keys, m) => keys.add(m.key),
documentKeySet()
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Nice/Oh... Nice/g


return this.persistence.runTransaction(
'Locally write mutations',
'readwrite',
txn => {
let batch: MutationBatch;
const localWriteTime = Timestamp.now();
return this.mutationQueue
.addMutationBatch(txn, localWriteTime, mutations)
.next(promisedBatch => {
batch = promisedBatch;
// TODO(koss): This is doing an N^2 update by replaying ALL the
// mutations on each document (instead of just the ones added) in
// this batch.
const keys = batch.keys();
return this.localDocuments.getDocuments(txn, keys);
})
.next((changedDocuments: MaybeDocumentMap) => {
return { batchId: batch.batchId, changes: changedDocuments };
// Load and apply all existing mutations. This lets us compute the
// current base state for all non-idempotent transforms before applying
// any additional user-provided writes.
return this.localDocuments
.getDocuments(txn, keys)
.next(existingDocs => {
// For non-idempotent mutations (such as `FieldValue.numericAdd()`),
// we record the base state in a separate patch mutation. This is
// later used to guarantee consistent values and prevents flicker
// even if the backend sends us an update that already includes our
// transform.
const baseMutations: Mutation[] = [];

for (const mutation of mutations) {
const maybeDoc = existingDocs.get(mutation.key);
if (!mutation.isIdempotent) {
// Theoretically, we should only include non-idempotent fields
// in this field mask as this mask is used to populate the base
// state for all DocumentTransforms. By including all fields,
// we incorrectly prevent rebasing of idempotent transforms
// (such as `arrayUnion()`) when any non-idempotent transforms
// are present.
const fieldMask = mutation.fieldMask;
if (fieldMask) {
const baseValues =
maybeDoc instanceof Document
? fieldMask.applyTo(maybeDoc.data)
: ObjectValue.EMPTY;
// NOTE: The base state should only be applied if there's some
// existing document to override, so use a Precondition of
// exists=true
baseMutations.push(
new PatchMutation(
mutation.key,
baseValues,
fieldMask,
Precondition.exists(true)
)
);
}
}
}

return this.mutationQueue
.addMutationBatch(txn, localWriteTime, baseMutations, mutations)
.next(batch => {
const changes = batch.applyToLocalDocumentSet(existingDocs);
return { batchId: batch.batchId, changes };
});
});
}
);
Expand Down
8 changes: 7 additions & 1 deletion packages/firestore/src/local/memory_mutation_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export class MemoryMutationQueue implements MutationQueue {
addMutationBatch(
transaction: PersistenceTransaction,
localWriteTime: Timestamp,
baseMutations: Mutation[],
mutations: Mutation[]
): PersistencePromise<MutationBatch> {
assert(mutations.length !== 0, 'Mutation batches should not be empty');
Expand All @@ -123,7 +124,12 @@ export class MemoryMutationQueue implements MutationQueue {
);
}

const batch = new MutationBatch(batchId, localWriteTime, mutations);
const batch = new MutationBatch(
batchId,
localWriteTime,
baseMutations,
mutations
);
this.mutationQueue.push(batch);

// Track references by document key.
Expand Down
1 change: 1 addition & 0 deletions packages/firestore/src/local/memory_persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export class MemoryEagerDelegate implements ReferenceDelegate {
txn: PersistenceTransaction
): PersistencePromise<void> {
const cache = this.persistence.getRemoteDocumentCache();
const size = this.orphanedDocuments.size;
return PersistencePromise.forEach(this.orphanedDocuments, key => {
return this.isReferenced(txn, key).next(isReferenced => {
if (!isReferenced) {
Expand Down
8 changes: 8 additions & 0 deletions packages/firestore/src/local/mutation_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,18 @@ export interface MutationQueue {

/**
* Creates a new mutation batch and adds it to this mutation queue.
*
* @param transaction The transaction this operation is scoped to.
* @param localWriteTime The original write time of this mutation.
* @param baseMutations Mutations that are used to populate the base values
* when this mutation is applied locally. These mutations are used to locally
* overwrite values that are persisted in the remote document cache.
* @param mutations The user-provided mutations in this mutation batch.
*/
addMutationBatch(
transaction: PersistenceTransaction,
localWriteTime: Timestamp,
baseMutations: Mutation[],
mutations: Mutation[]
): PersistencePromise<MutationBatch>;

Expand Down
2 changes: 2 additions & 0 deletions packages/firestore/src/model/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export abstract class MaybeDocument {
abstract get hasPendingWrites(): boolean;

abstract isEqual(other: MaybeDocument | null | undefined): boolean;

abstract toString(): string;
}

/**
Expand Down
Loading