Skip to content

Revert "Revert: Add Database.Servervalue.increment(x)" #2505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 26, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions packages/database/src/api/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export class Database implements FirebaseService {
static readonly ServerValue = {
TIMESTAMP: {
'.sv': 'timestamp'
},
_increment: (x: number) => {
return {
'.sv': {
'increment': x
}
};
}
};

Expand Down
4 changes: 4 additions & 0 deletions packages/database/src/core/Repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,10 @@ export class Repo {
// (b) store unresolved paths on JSON parse
const serverValues = this.generateServerValues();
const newNodeUnresolved = nodeFromJSON(newVal, newPriority);
const existing = this.serverSyncTree_.calcCompleteEventCache(path);
const newNode = resolveDeferredValueSnapshot(
newNodeUnresolved,
existing,
serverValues
);

Expand Down Expand Up @@ -365,6 +367,7 @@ export class Repo {
const newNodeUnresolved = nodeFromJSON(changedValue);
changedChildren[changedKey] = resolveDeferredValueSnapshot(
newNodeUnresolved,
this.serverSyncTree_.calcCompleteEventCache(path),
serverValues
);
});
Expand Down Expand Up @@ -419,6 +422,7 @@ export class Repo {
const serverValues = this.generateServerValues();
const resolvedOnDisconnectTree = resolveDeferredValueTree(
this.onDisconnect_,
this.serverSyncTree_,
serverValues
);
let events: Event[] = [];
Expand Down
2 changes: 2 additions & 0 deletions packages/database/src/core/Repo_transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ Repo.prototype.startTransaction = function(
const newNodeUnresolved = nodeFromJSON(newVal, priorityForNode);
const newNode = resolveDeferredValueSnapshot(
newNodeUnresolved,
currentState,
serverValues
);
transaction.currentOutputSnapshotRaw = newNodeUnresolved;
Expand Down Expand Up @@ -533,6 +534,7 @@ Repo.prototype.startTransaction = function(
const serverValues = this.generateServerValues();
const newNodeResolved = resolveDeferredValueSnapshot(
newDataNode,
currentNode,
serverValues
);

Expand Down
11 changes: 5 additions & 6 deletions packages/database/src/core/SyncTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,18 +474,17 @@ export class SyncTree {
}

/**
* Returns a complete cache, if we have one, of the data at a particular path. The location must have a listener above
* it, but as this is only used by transaction code, that should always be the case anyways.
* Returns a complete cache, if we have one, of the data at a particular path. If the location does not have a
* listener above it, we will get a false "null". This shouldn't be a problem because transactions will always
* have a listener above, and atomic operations would correctly show a jitter of <increment value> ->
* <incremented total> as the write is applied locally and then acknowledged at the server.
*
* Note: this method will *include* hidden writes from transaction with applyLocally set to false.
*
* @param path The path to the data we want
* @param writeIdsToExclude A specific set to be excluded
*/
calcCompleteEventCache(
path: Path,
writeIdsToExclude?: number[]
): Node | null {
calcCompleteEventCache(path: Path, writeIdsToExclude?: number[]): Node {
const includeHiddenSets = true;
const writeTree = this.pendingWriteTree_;
const serverCache = this.syncPointTree_.findOnPath(
Expand Down
85 changes: 74 additions & 11 deletions packages/database/src/core/util/ServerValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { nodeFromJSON } from '../snap/nodeFromJSON';
import { PRIORITY_INDEX } from '../snap/indexes/PriorityIndex';
import { Node } from '../snap/Node';
import { ChildrenNode } from '../snap/ChildrenNode';
import { SyncTree } from '../SyncTree';
import { Indexable } from './misc';

/**
Expand All @@ -48,17 +49,63 @@ export const generateWithValues = function(
* @return {!(string|number|boolean)}
*/
export const resolveDeferredValue = function(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: { [k: string]: any } | string | number | boolean,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
serverValues: { [k: string]: any }
value: { [k: string]: unknown } | string | number | boolean,
existing: Node,
serverValues: { [k: string]: unknown }
): string | number | boolean {
if (!value || typeof value !== 'object') {
return value as string | number | boolean;
}
assert('.sv' in value, 'Unexpected leaf node or priority contents');

if (typeof value['.sv'] === 'string') {
return resolveScalarDeferredValue(value['.sv'], existing, serverValues);
} else if (typeof value['.sv'] === 'object') {
return resolveComplexDeferredValue(value['.sv'], existing, serverValues);
} else {
assert('.sv' in value, 'Unexpected leaf node or priority contents');
return serverValues[value['.sv']];
assert(false, 'Unexpected server value: ' + JSON.stringify(value, null, 2));
}
};

const resolveScalarDeferredValue = function(
op: string,
existing: Node,
serverValues: { [k: string]: unknown }
): string | number | boolean {
switch (op) {
case 'timestamp':
return serverValues['timestamp'] as string | number | boolean;
default:
assert(false, 'Unexpected server value: ' + op);
}
};

const resolveComplexDeferredValue = function(
op: object,
existing: Node,
unused: { [k: string]: unknown }
): string | number | boolean {
if (!op.hasOwnProperty('increment')) {
assert(false, 'Unexpected server value: ' + JSON.stringify(op, null, 2));
}
const delta = op['increment'];
if (typeof delta !== 'number') {
assert(false, 'Unexpected increment value: ' + delta);
}

// Incrementing a non-number sets the value to the incremented amount
if (!existing.isLeafNode()) {
return delta;
}

const leaf = existing as LeafNode;
const existingVal = leaf.getValue();
if (typeof existingVal !== 'number') {
return delta;
}

// No need to do over/underflow arithmetic here because JS only handles floats under the covers
return existingVal + delta;
};

/**
Expand All @@ -70,13 +117,19 @@ export const resolveDeferredValue = function(
*/
export const resolveDeferredValueTree = function(
tree: SparseSnapshotTree,
serverValues: object
syncTree: SyncTree,
serverValues: Indexable
): SparseSnapshotTree {
const resolvedTree = new SparseSnapshotTree();
tree.forEachTree(new Path(''), (path, node) => {
const existing = syncTree.calcCompleteEventCache(path);
assert(
existing !== null && typeof existing !== 'undefined',
'Expected ChildrenNode.EMPTY_NODE for nulls'
);
resolvedTree.remember(
path,
resolveDeferredValueSnapshot(node, serverValues)
resolveDeferredValueSnapshot(node, existing, serverValues)
);
});
return resolvedTree;
Expand All @@ -92,20 +145,29 @@ export const resolveDeferredValueTree = function(
*/
export const resolveDeferredValueSnapshot = function(
node: Node,
serverValues: object
existing: Node,
serverValues: Indexable
): Node {
const rawPri = node.getPriority().val() as
| Indexable
| boolean
| null
| number
| string;
const priority = resolveDeferredValue(rawPri, serverValues);
const priority = resolveDeferredValue(
rawPri,
existing.getPriority(),
serverValues
);
let newNode: Node;

if (node.isLeafNode()) {
const leafNode = node as LeafNode;
const value = resolveDeferredValue(leafNode.getValue(), serverValues);
const value = resolveDeferredValue(
leafNode.getValue(),
existing,
serverValues
);
if (
value !== leafNode.getValue() ||
priority !== leafNode.getPriority().val()
Expand All @@ -123,6 +185,7 @@ export const resolveDeferredValueSnapshot = function(
childrenNode.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
const newChildNode = resolveDeferredValueSnapshot(
childNode,
existing.getImmediateChild(childName),
serverValues
);
if (newChildNode !== childNode) {
Expand Down
96 changes: 96 additions & 0 deletions packages/database/test/servervalues.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @license
* Copyright 2019 Google Inc.
*
* 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 { expect } from 'chai';
import { getFreshRepoFromReference, getRandomNode } from './helpers/util';
import { Database } from '../src/api/Database';
import { Reference } from '../src/api/Reference';
import { nodeFromJSON } from '../src/core/snap/nodeFromJSON';

describe('ServerValue tests', () => {
it('resolves timestamps locally', async () => {
const node = getRandomNode() as Reference;
const start = Date.now();
const values: number[] = [];
node.on('value', snap => {
expect(typeof snap.val()).to.equal('number');
values.push(snap.val() as number);
});
await node.set(Database.ServerValue.TIMESTAMP);
node.off('value');

// By the time the write is acknowledged, we should have a local and
// server version of the timestamp.
expect(values.length).to.equal(2);
values.forEach(serverTime => {
const delta = Math.abs(serverTime - start);
expect(delta).to.be.lessThan(1000);
});
});

it('handles increments without listeners', () => {
// Ensure that increments don't explode when the SyncTree must return a null
// node (i.e. ChildrenNode.EMPTY_NODE) because there is not yet any synced
// data.
// TODO(b/146657568): Remove getFreshRepoFromReference() call and goOffline()
// once we have emulator support. We can also await the set() call.
const node = getFreshRepoFromReference(getRandomNode()) as Reference;
node.database.goOffline();

const addOne = Database.ServerValue._increment(1);

node.set(addOne);
});

it('handles increments locally', async () => {
// TODO(b/146657568): Remove getFreshRepoFromReference() call and goOffline()
// once we have emulator support. We can also await the set() calls.
const node = getFreshRepoFromReference(getRandomNode()) as Reference;
node.database.goOffline();

const addOne = Database.ServerValue._increment(1);

const values: any = [];
const expected: any = [];
node.on('value', snap => values.push(snap.val()));

// null -> increment(x) = x
node.set(addOne);
expected.push(1);

// x -> increment(y) = x + y
node.set(5);
node.set(addOne);
expected.push(5);
expected.push(6);

// str -> increment(x) = x
node.set('hello');
node.set(addOne);
expected.push('hello');
expected.push(1);

// obj -> increment(x) = x
node.set({ 'hello': 'world' });
node.set(addOne);
expected.push({ 'hello': 'world' });
expected.push(1);

node.off('value');
expect(values).to.deep.equal(expected);
});
});