Skip to content

Commit 111a4d0

Browse files
authored
Realtime Database increment operator
* Adds a prerelease copy of the increment operator (`_increment`) * Updated local operations to handle increment as well as passing the server operation * ServerValues now takes a copy of existing data, but uses JIT access to avoid performance regressions * Lots of new tests
1 parent 353f5ca commit 111a4d0

File tree

8 files changed

+333
-45
lines changed

8 files changed

+333
-45
lines changed

.vscode/launch.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"type": "node",
99
"request": "launch",
1010
"name": "RTDB Unit Tests (Node)",
11-
"program": "${workspaceRoot}/packages/firestore/node_modules/.bin/_mocha",
11+
"program": "${workspaceRoot}/packages/firebase/node_modules/.bin/_mocha",
1212
"cwd": "${workspaceRoot}/packages/database",
1313
"args": [
1414
"test/{,!(browser)/**/}*.test.ts",

packages/database/src/api/Database.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2017 Google Inc.
3+
* Copyright 2017 Google LLC
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -39,6 +39,13 @@ export class Database implements FirebaseService {
3939
static readonly ServerValue = {
4040
TIMESTAMP: {
4141
'.sv': 'timestamp'
42+
},
43+
_increment: (x: number) => {
44+
return {
45+
'.sv': {
46+
'increment': x
47+
}
48+
};
4249
}
4350
};
4451

packages/database/src/core/Repo.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2017 Google Inc.
3+
* Copyright 2017 Google LLC
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -313,8 +313,10 @@ export class Repo {
313313
// (b) store unresolved paths on JSON parse
314314
const serverValues = this.generateServerValues();
315315
const newNodeUnresolved = nodeFromJSON(newVal, newPriority);
316+
const existing = this.serverSyncTree_.calcCompleteEventCache(path);
316317
const newNode = resolveDeferredValueSnapshot(
317318
newNodeUnresolved,
319+
existing,
318320
serverValues
319321
);
320322

@@ -362,9 +364,10 @@ export class Repo {
362364
const changedChildren: { [k: string]: Node } = {};
363365
each(childrenToMerge, (changedKey: string, changedValue: unknown) => {
364366
empty = false;
365-
const newNodeUnresolved = nodeFromJSON(changedValue);
366-
changedChildren[changedKey] = resolveDeferredValueSnapshot(
367-
newNodeUnresolved,
367+
changedChildren[changedKey] = resolveDeferredValueTree(
368+
path.child(changedKey),
369+
nodeFromJSON(changedValue),
370+
this.serverSyncTree_,
368371
serverValues
369372
);
370373
});
@@ -417,10 +420,16 @@ export class Repo {
417420
this.log_('onDisconnectEvents');
418421

419422
const serverValues = this.generateServerValues();
420-
const resolvedOnDisconnectTree = resolveDeferredValueTree(
421-
this.onDisconnect_,
422-
serverValues
423-
);
423+
const resolvedOnDisconnectTree = new SparseSnapshotTree();
424+
this.onDisconnect_.forEachTree(Path.Empty, (path, node) => {
425+
const resolved = resolveDeferredValueTree(
426+
path,
427+
node,
428+
this.serverSyncTree_,
429+
serverValues
430+
);
431+
resolvedOnDisconnectTree.remember(path, resolved);
432+
});
424433
let events: Event[] = [];
425434

426435
resolvedOnDisconnectTree.forEachTree(Path.Empty, (path, snap) => {

packages/database/src/core/Repo_transaction.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2017 Google Inc.
3+
* Copyright 2017 Google LLC
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -248,6 +248,7 @@ Repo.prototype.startTransaction = function(
248248
const newNodeUnresolved = nodeFromJSON(newVal, priorityForNode);
249249
const newNode = resolveDeferredValueSnapshot(
250250
newNodeUnresolved,
251+
currentState,
251252
serverValues
252253
);
253254
transaction.currentOutputSnapshotRaw = newNodeUnresolved;
@@ -533,6 +534,7 @@ Repo.prototype.startTransaction = function(
533534
const serverValues = this.generateServerValues();
534535
const newNodeResolved = resolveDeferredValueSnapshot(
535536
newDataNode,
537+
currentNode,
536538
serverValues
537539
);
538540

packages/database/src/core/SyncTree.ts

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2017 Google Inc.
3+
* Copyright 2017 Google LLC
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -474,18 +474,17 @@ export class SyncTree {
474474
}
475475

476476
/**
477-
* Returns a complete cache, if we have one, of the data at a particular path. The location must have a listener above
478-
* it, but as this is only used by transaction code, that should always be the case anyways.
477+
* Returns a complete cache, if we have one, of the data at a particular path. If the location does not have a
478+
* listener above it, we will get a false "null". This shouldn't be a problem because transactions will always
479+
* have a listener above, and atomic operations would correctly show a jitter of <increment value> ->
480+
* <incremented total> as the write is applied locally and then acknowledged at the server.
479481
*
480482
* Note: this method will *include* hidden writes from transaction with applyLocally set to false.
481483
*
482484
* @param path The path to the data we want
483485
* @param writeIdsToExclude A specific set to be excluded
484486
*/
485-
calcCompleteEventCache(
486-
path: Path,
487-
writeIdsToExclude?: number[]
488-
): Node | null {
487+
calcCompleteEventCache(path: Path, writeIdsToExclude?: number[]): Node {
489488
const includeHiddenSets = true;
490489
const writeTree = this.pendingWriteTree_;
491490
const serverCache = this.syncPointTree_.findOnPath(

packages/database/src/core/util/ServerValues.ts

+145-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2017 Google Inc.
3+
* Copyright 2017 Google LLC
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -23,8 +23,53 @@ import { nodeFromJSON } from '../snap/nodeFromJSON';
2323
import { PRIORITY_INDEX } from '../snap/indexes/PriorityIndex';
2424
import { Node } from '../snap/Node';
2525
import { ChildrenNode } from '../snap/ChildrenNode';
26+
import { SyncTree } from '../SyncTree';
2627
import { Indexable } from './misc';
2728

29+
/* It's critical for performance that we do not calculate actual values from a SyncTree
30+
* unless and until the value is needed. Because we expose both a SyncTree and Node
31+
* version of deferred value resolution, we ned a wrapper class that will let us share
32+
* code.
33+
*
34+
* @see https://github.com/firebase/firebase-js-sdk/issues/2487
35+
*/
36+
interface ValueProvider {
37+
getImmediateChild(childName: string): ValueProvider;
38+
node(): Node;
39+
}
40+
41+
class ExistingValueProvider implements ValueProvider {
42+
constructor(readonly node_: Node) {}
43+
44+
getImmediateChild(childName: string): ValueProvider {
45+
const child = this.node_.getImmediateChild(childName);
46+
return new ExistingValueProvider(child);
47+
}
48+
49+
node(): Node {
50+
return this.node_;
51+
}
52+
}
53+
54+
class DeferredValueProvider implements ValueProvider {
55+
private syncTree_: SyncTree;
56+
private path_: Path;
57+
58+
constructor(syncTree: SyncTree, path: Path) {
59+
this.syncTree_ = syncTree;
60+
this.path_ = path;
61+
}
62+
63+
getImmediateChild(childName: string): ValueProvider {
64+
const childPath = this.path_.child(childName);
65+
return new DeferredValueProvider(this.syncTree_, childPath);
66+
}
67+
68+
node(): Node {
69+
return this.syncTree_.calcCompleteEventCache(this.path_);
70+
}
71+
}
72+
2873
/**
2974
* Generate placeholders for deferred values.
3075
* @param {?Object} values
@@ -47,39 +92,92 @@ export const generateWithValues = function(
4792
* @param {!Object} serverValues
4893
* @return {!(string|number|boolean)}
4994
*/
50-
export const resolveDeferredValue = function(
51-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
52-
value: { [k: string]: any } | string | number | boolean,
53-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
54-
serverValues: { [k: string]: any }
95+
export const resolveDeferredLeafValue = function(
96+
value: { [k: string]: unknown } | string | number | boolean,
97+
existingVal: ValueProvider,
98+
serverValues: { [k: string]: unknown }
5599
): string | number | boolean {
56100
if (!value || typeof value !== 'object') {
57101
return value as string | number | boolean;
102+
}
103+
assert('.sv' in value, 'Unexpected leaf node or priority contents');
104+
105+
if (typeof value['.sv'] === 'string') {
106+
return resolveScalarDeferredValue(value['.sv'], existingVal, serverValues);
107+
} else if (typeof value['.sv'] === 'object') {
108+
return resolveComplexDeferredValue(value['.sv'], existingVal, serverValues);
58109
} else {
59-
assert('.sv' in value, 'Unexpected leaf node or priority contents');
60-
return serverValues[value['.sv']];
110+
assert(false, 'Unexpected server value: ' + JSON.stringify(value, null, 2));
111+
}
112+
};
113+
114+
const resolveScalarDeferredValue = function(
115+
op: string,
116+
existing: ValueProvider,
117+
serverValues: { [k: string]: unknown }
118+
): string | number | boolean {
119+
switch (op) {
120+
case 'timestamp':
121+
return serverValues['timestamp'] as string | number | boolean;
122+
default:
123+
assert(false, 'Unexpected server value: ' + op);
61124
}
62125
};
63126

127+
const resolveComplexDeferredValue = function(
128+
op: object,
129+
existing: ValueProvider,
130+
unused: { [k: string]: unknown }
131+
): string | number | boolean {
132+
if (!op.hasOwnProperty('increment')) {
133+
assert(false, 'Unexpected server value: ' + JSON.stringify(op, null, 2));
134+
}
135+
const delta = op['increment'];
136+
if (typeof delta !== 'number') {
137+
assert(false, 'Unexpected increment value: ' + delta);
138+
}
139+
140+
const existingNode = existing.node();
141+
assert(
142+
existingNode !== null && typeof existingNode !== 'undefined',
143+
'Expected ChildrenNode.EMPTY_NODE for nulls'
144+
);
145+
146+
// Incrementing a non-number sets the value to the incremented amount
147+
if (!existingNode.isLeafNode()) {
148+
return delta;
149+
}
150+
151+
const leaf = existingNode as LeafNode;
152+
const existingVal = leaf.getValue();
153+
if (typeof existingVal !== 'number') {
154+
return delta;
155+
}
156+
157+
// No need to do over/underflow arithmetic here because JS only handles floats under the covers
158+
return existingVal + delta;
159+
};
160+
64161
/**
65162
* Recursively replace all deferred values and priorities in the tree with the
66163
* specified generated replacement values.
67-
* @param {!SparseSnapshotTree} tree
164+
* @param {!Path} path path to which write is relative
165+
* @param {!Node} node new data written at path
166+
* @param {!SyncTree} syncTree current data
68167
* @param {!Object} serverValues
69168
* @return {!SparseSnapshotTree}
70169
*/
71170
export const resolveDeferredValueTree = function(
72-
tree: SparseSnapshotTree,
73-
serverValues: object
74-
): SparseSnapshotTree {
75-
const resolvedTree = new SparseSnapshotTree();
76-
tree.forEachTree(new Path(''), (path, node) => {
77-
resolvedTree.remember(
78-
path,
79-
resolveDeferredValueSnapshot(node, serverValues)
80-
);
81-
});
82-
return resolvedTree;
171+
path: Path,
172+
node: Node,
173+
syncTree: SyncTree,
174+
serverValues: Indexable
175+
): Node {
176+
return resolveDeferredValue(
177+
node,
178+
new DeferredValueProvider(syncTree, path),
179+
serverValues
180+
);
83181
};
84182

85183
/**
@@ -92,20 +190,41 @@ export const resolveDeferredValueTree = function(
92190
*/
93191
export const resolveDeferredValueSnapshot = function(
94192
node: Node,
95-
serverValues: object
193+
existing: Node,
194+
serverValues: Indexable
195+
): Node {
196+
return resolveDeferredValue(
197+
node,
198+
new ExistingValueProvider(existing),
199+
serverValues
200+
);
201+
};
202+
203+
function resolveDeferredValue(
204+
node: Node,
205+
existingVal: ValueProvider,
206+
serverValues: Indexable
96207
): Node {
97208
const rawPri = node.getPriority().val() as
98209
| Indexable
99210
| boolean
100211
| null
101212
| number
102213
| string;
103-
const priority = resolveDeferredValue(rawPri, serverValues);
214+
const priority = resolveDeferredLeafValue(
215+
rawPri,
216+
existingVal.getImmediateChild('.priority'),
217+
serverValues
218+
);
104219
let newNode: Node;
105220

106221
if (node.isLeafNode()) {
107222
const leafNode = node as LeafNode;
108-
const value = resolveDeferredValue(leafNode.getValue(), serverValues);
223+
const value = resolveDeferredLeafValue(
224+
leafNode.getValue(),
225+
existingVal,
226+
serverValues
227+
);
109228
if (
110229
value !== leafNode.getValue() ||
111230
priority !== leafNode.getPriority().val()
@@ -121,8 +240,9 @@ export const resolveDeferredValueSnapshot = function(
121240
newNode = newNode.updatePriority(new LeafNode(priority));
122241
}
123242
childrenNode.forEachChild(PRIORITY_INDEX, (childName, childNode) => {
124-
const newChildNode = resolveDeferredValueSnapshot(
243+
const newChildNode = resolveDeferredValue(
125244
childNode,
245+
existingVal.getImmediateChild(childName),
126246
serverValues
127247
);
128248
if (newChildNode !== childNode) {
@@ -131,4 +251,4 @@ export const resolveDeferredValueSnapshot = function(
131251
});
132252
return newNode;
133253
}
134-
};
254+
}

0 commit comments

Comments
 (0)