Skip to content

Commit

Permalink
feat: added ServerValue.increment server value
Browse files Browse the repository at this point in the history
fixes #58
  • Loading branch information
rbellens committed Jan 1, 2025
1 parent 4237e9d commit f63f56a
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'events/value.dart';
import 'event.dart';
import 'backend_connection/rules.dart';
import 'events/cancel.dart';
import 'tree.dart';

part 'backend_connection/connection.dart';
part 'backend_connection/transport.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ class SyncTreeBackend extends Backend {
throw FirebaseDatabaseException.dataStale();
}
}
var existing = syncTree.valueForPathAndFilter(p, const QueryFilter());
syncTree.applyServerOperation(
TreeOperation.overwrite(
p,
ServerValueX.resolve(
TreeStructuredData.fromJson(value), serverValues)),
TreeStructuredData.fromJson(value), existing, serverValues)),
null);
}

Expand All @@ -51,7 +52,12 @@ class SyncTreeBackend extends Backend {
children.map((k, v) => MapEntry(
Name.parsePath(k),
ServerValueX.resolve(
TreeStructuredData.fromJson(v), serverValues)))),
TreeStructuredData.fromJson(v),
syncTree.valueForPathAndFilter(
Path.from(
[...Name.parsePath(path), ...Name.parsePath(k)]),
const QueryFilter()),
serverValues)))),
null);
}
}
21 changes: 16 additions & 5 deletions packages/firebase_dart/lib/src/database/impl/repo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,12 @@ class Repo {

var newValue = TreeStructuredData.fromJson(value, priority);
var writeId = _nextWriteId++;
_syncTree.applyUserOverwrite(path,
ServerValueX.resolve(newValue, _connection.serverValues), writeId);

var existing = _syncTree.valueForPathAndFilter(path, const QueryFilter());
_syncTree.applyUserOverwrite(
path,
ServerValueX.resolve(newValue, existing, _connection.serverValues),
writeId);
_transactions.abort(path, FirebaseDatabaseException.overriddenBySet());
try {
await _connection.put(path.asString(), newValue.toJson(true));
Expand All @@ -226,8 +230,12 @@ class Repo {
var serverValues = _connection.serverValues;
var changedChildren = Map<Path<Name>, TreeStructuredData>.fromIterables(
value.keys.map<Path<Name>>((c) => Name.parsePath(c)),
value.values.map<TreeStructuredData>((v) => ServerValueX.resolve(
TreeStructuredData.fromJson(v, null), serverValues)));
value.keys.map<TreeStructuredData>((k) => ServerValueX.resolve(
TreeStructuredData.fromJson(value[k], null),
_syncTree.valueForPathAndFilter(
Path.from([...path, ...Name.parsePath(k)]),
const QueryFilter()),
serverValues)));
var writeId = _nextWriteId++;
_syncTree.applyUserMerge(path, changedChildren, writeId);
try {
Expand Down Expand Up @@ -345,8 +353,11 @@ class Repo {
var sv = _connection.serverValues;
_onDisconnect.forEachNode((path, snap) {
if (snap == null) return;
var existing = _syncTree.valueForPathAndFilter(path, const QueryFilter());
_syncTree.applyServerOperation(
TreeOperation.overwrite(path, ServerValueX.resolve(snap, sv)), null);
TreeOperation.overwrite(
path, ServerValueX.resolve(snap, existing, sv)),
null);
_transactions.abort(path, FirebaseDatabaseException.overriddenBySet());
});
_onDisconnect.children.clear();
Expand Down
7 changes: 7 additions & 0 deletions packages/firebase_dart/lib/src/database/impl/synctree.dart
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,13 @@ class SyncTree {
return _handleInvalidPointsFuture!;
}

TreeStructuredData valueForPathAndFilter(
Path<Name> path, QueryFilter filter) {
var node = root.subtree(path, _createNode);
var point = node.value;
return point.valueForFilter(filter);
}

/// Adds an event listener for events of [type] and for data at [path] and
/// filtered by [filter].
Future<void> addEventListener(String type, Path<Name> path,
Expand Down
8 changes: 5 additions & 3 deletions packages/firebase_dart/lib/src/database/impl/transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ class Transaction implements Comparable<Transaction> {
var newNode =
TreeStructuredData.fromJson(data.value, currentState.priority);
currentOutputSnapshotRaw = newNode;
currentOutputSnapshotResolved =
ServerValueX.resolve(newNode, repo._connection.serverValues);
currentOutputSnapshotResolved = ServerValueX.resolve(
newNode, currentState, repo._connection.serverValues);

if (applyLocally) {
repo._syncTree.applyUserOverwrite(
Expand Down Expand Up @@ -373,7 +373,9 @@ class TransactionsNode extends ModifiableTreeNode<Name, List<Transaction>> {
.put(path.join('/'), output!.toJson(true), hash: latestHash);
complete();

if (out == ServerValueX.resolve(out, repo._connection.serverValues)) {
if (out ==
ServerValueX.resolve(
out, TreeStructuredData(), repo._connection.serverValues)) {
// the confirmed value did not contain any server values, so we can reset the input to the confirmed value
input = out;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,38 @@
part of '../treestructureddata.dart';

extension ServerValueX on ServerValue {
static const Map<String, ServerValue> values = {
'timestamp': ServerValue.timestamp
static final Map<String, ServerValue Function(dynamic)> factories = {
'timestamp': (v) => ServerValue.timestamp,
'increment': (v) => ServerValue.increment(v),
};

static TreeStructuredData resolve(
TreeStructuredData value, Map<ServerValue, Value> serverValues) {
static TreeStructuredData resolve(TreeStructuredData value,
TreeStructuredData existing, Map<ServerValue, Value> serverValues) {
if (value.isLeaf) {
return value.value!.value is ServerValue
? TreeStructuredData.leaf(serverValues[value.value!.value]!)
: value;
var s = value.value!.value;
if (s is ServerValue) {
if (s['.sv'] is String) {
return TreeStructuredData.leaf(serverValues[s]!);
}
var op = s['.sv'] as Map;
if (op.keys.single == 'increment') {
var delta = op.values.single;
if (existing.isLeaf) {
var existingValue = existing.value!.value;
if (existingValue is num) {
return TreeStructuredData.leaf(Value.num(existingValue + delta));
}
}
return TreeStructuredData.leaf(Value.num(delta));
}
throw StateError('Invalid server value $s');
}
return value;
}

for (var k in value.children.keys.toList()) {
var newChild = resolve(value.children[k]!, serverValues);
var newChild = resolve(value.children[k]!,
existing.children[k] ?? TreeStructuredData._nill, serverValues);
if (newChild != value.children[k]) {
value = value.withChild(k, newChild);
}
Expand Down Expand Up @@ -48,7 +66,10 @@ class Value implements Comparable<Value> {

const Value.string(String value) : this._(value);

Value.server(String? type) : this._(ServerValueX.values[type!]);
Value.server(dynamic value)
: this._(ServerValueX.factories[
value is String ? value : (value as Map).keys.single]!(
value is String ? null : (value as Map).values.single));

bool get isBool => value is bool;

Expand Down
10 changes: 8 additions & 2 deletions packages/firebase_dart/lib/src/database/server_value.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@

part of firebase_dart;

class ServerValue extends MapView<String, String> {
class ServerValue extends MapView<String, dynamic> {
static const ServerValue timestamp = ServerValue._({'.sv': 'timestamp'});

const ServerValue._(Map<String, String> map) : super(map);
/// Returns a placeholder value that can be used to atomically increment the
/// current database value by the provided delta.
static ServerValue increment(num delta) => ServerValue._({
'.sv': {'increment': delta}
});

const ServerValue._(Map<String, dynamic> map) : super(map);
}
52 changes: 52 additions & 0 deletions packages/firebase_dart/test/database/database_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,58 @@ void testsWith(Map<String, dynamic> secrets, {required bool isolated}) {
});

group('ServerValue', () {
test('increment', () async {
var ref = db1.reference().child('test/server-values/increment');
await ref.set(1);

await ref.set(ServerValue.increment(1.5));

expect(await ref.get(), 2.5);

await ref.set(null);

await ref.set(ServerValue.increment(1.5));

expect(await ref.get(), 1.5);

await ref.set({'counter': ServerValue.increment(1)});

expect(await ref.get(), {'counter': 1});

await ref.set({
'counter': {
'.sv': {'increment': 1}
}
});

expect(await ref.get(), {'counter': 2});

await ref.runTransaction(
(v) => v..value['counter'] = ServerValue.increment(1));
expect(await ref.get(), {'counter': 3});

await ref.onDisconnect().set({'counter': ServerValue.increment(1)});

expect(await ref.get(), {'counter': 3});

await ref.database.goOffline();

expect(await ref.get(), {'counter': 4});

await ref.database.goOnline();

expect(await ref.get(), {'counter': 4});

await ref.update({
'counter/first': {
'.sv': {'increment': 1}
},
});
expect(await ref.get(), {
'counter': {'first': 1}
});
});

test('timestamp', () async {
var ref = db1.reference().child('test/server-values/timestamp');
await ref.set(null);
Expand Down

0 comments on commit f63f56a

Please sign in to comment.