[go: nahoru, domu]

Skip to content

Commit

Permalink
Add Database.Servervalue.increment(x) (#2348)
Browse files Browse the repository at this point in the history
* Added ServerValue._increment() for not-yet-working operator (needs server-side rollout and API approval)
* Changed server value local resolution to include current offline caches
* Add new tests for ServerValues in general + offline increments
  • Loading branch information
inlined committed Nov 18, 2019
1 parent 9ccc3dc commit 86971ea
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 11 deletions.
7 changes: 7 additions & 0 deletions packages/database/src/api/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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 @@ -307,8 +307,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 @@ -359,6 +361,7 @@ export class Repo {
const newNodeUnresolved = nodeFromJSON(changedValue);
changedChildren[changedKey] = resolveDeferredValueSnapshot(
newNodeUnresolved,
this.serverSyncTree_.calcCompleteEventCache(path),
serverValues
);
});
Expand Down Expand Up @@ -413,6 +416,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 @@ -245,6 +245,7 @@ Repo.prototype.startTransaction = function(
const newNodeUnresolved = nodeFromJSON(newVal, priorityForNode);
const newNode = resolveDeferredValueSnapshot(
newNodeUnresolved,
currentState,
serverValues
);
transaction.currentOutputSnapshotRaw = newNodeUnresolved;
Expand Down Expand Up @@ -522,6 +523,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(path, function(
Expand Down
75 changes: 70 additions & 5 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';

/**
* Generate placeholders for deferred values.
Expand All @@ -48,16 +49,64 @@ export const generateWithValues = function(
*/
export const resolveDeferredValue = function(
value: { [k: string]: any } | string | number | boolean,
existing: Node,
serverValues: { [k: string]: any }
): 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]: any }
): string | number | boolean {
switch (op) {
case 'timestamp':
return serverValues['timestamp'];
default:
assert(false, 'Unexpected server value: ' + op);
}
};

const resolveComplexDeferredValue = function(
op: Object,
existing: Node,
unused: { [k: string]: any }
): 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;
};

/**
* Recursively replace all deferred values and priorities in the tree with the
* specified generated replacement values.
Expand All @@ -67,13 +116,19 @@ export const resolveDeferredValue = function(
*/
export const resolveDeferredValueTree = function(
tree: SparseSnapshotTree,
syncTree: SyncTree,
serverValues: Object
): SparseSnapshotTree {
const resolvedTree = new SparseSnapshotTree();
tree.forEachTree(new Path(''), function(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 @@ -89,6 +144,7 @@ export const resolveDeferredValueTree = function(
*/
export const resolveDeferredValueSnapshot = function(
node: Node,
existing: Node,
serverValues: Object
): Node {
const rawPri = node.getPriority().val() as
Expand All @@ -97,12 +153,20 @@ export const resolveDeferredValueSnapshot = function(
| 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 @@ -120,6 +184,7 @@ export const resolveDeferredValueSnapshot = function(
childrenNode.forEachChild(PRIORITY_INDEX, function(childName, childNode) {
const newChildNode = resolveDeferredValueSnapshot(
childNode,
existing.getImmediateChild(childName),
serverValues
);
if (newChildNode !== childNode) {
Expand Down
95 changes: 95 additions & 0 deletions packages/database/test/servervalues.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* @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 { 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: Array<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.
const node = getRandomNode() as Reference;
const addOne = Database.ServerValue._increment(1);

node.set(addOne);
});

it('handles increments locally', async () => {
const node = getRandomNode() as Reference;
const addOne = Database.ServerValue._increment(1);

// Must go offline because the latest emulator may not support this server op
// This also means we can't await node operations, which would block the test.
node.database.goOffline();
try {
const values: Array<any> = [];
const expected: Array<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);
} finally {
node.database.goOnline();
}
});
});

0 comments on commit 86971ea

Please sign in to comment.