readValue function

Future<Object?> readValue(
  1. String path, {
  2. DbQuery? query,
  3. Duration settle = const Duration(milliseconds: 400),
  4. Duration timeout = const Duration(seconds: 15),
})

Reads path once, through a listener.

There is no server read on desktop. DatabaseReference::GetValue completes on the first event a listener receives, and for a path with nothing cached that event is the empty local state — it answers null for data that is on the server. So this attaches a listener and returns the value once it has stopped changing for settle.

That is a heuristic, and it is worth being plain about which way it errs. The SDK sends local state first and the server's answer second, but only when the two differ, so counting events cannot work: a cached path sends one event and an uncached path sends two. Waiting for quiet is true in both cases. What it costs is settle of latency on every read, and what it risks is returning an intermediate value if a node is being written faster than settle — for which a listener, not a read, is the right tool.

query filters at the server rather than here, so a limit means what it says. A query the ABI cannot apply is an error, as it is for a listener.

Returns null if nothing arrives within timeout, which is also what an empty path returns; the two are not distinguishable here, as they are not in the SDK.

Implementation

Future<Object?> readValue(
  String path, {
  DbQuery? query,
  Duration settle = const Duration(milliseconds: 400),
  Duration timeout = const Duration(seconds: 15),
}) async {
  final completer = Completer<Object?>();
  Object? last;
  var seen = false;
  Timer? quiet;

  // A query reads through the filtered listener, so the server does the
  // filtering. Reading the whole node and narrowing it here would fetch more
  // than was asked for and answer differently for a limit.
  final source = query == null
      ? onValue(path)
      : onQueryValue(path, query).map(
          (s) => DbSnapshot(seq: s.seq, value: s.value, postedNs: s.postedNs),
        );

  final sub = source.listen(
    (s) {
      last = s.value;
      seen = true;
      quiet?.cancel();
      quiet = Timer(settle, () {
        if (!completer.isCompleted) completer.complete(last);
      });
    },
    onError: (Object e) {
      if (!completer.isCompleted) completer.completeError(e);
    },
  );

  try {
    return await completer.future.timeout(
      timeout,
      // A path that was never written still delivers an event, so a timeout
      // here means nothing arrived at all -- no connection, or a rule that
      // denies the read. Null is what the SDK would say too.
      onTimeout: () => seen ? last : null,
    );
  } finally {
    quiet?.cancel();
    await sub.cancel();
  }
}