collectLogPage function

Future<LogPage> collectLogPage(
  1. LogQuery query, {
  2. required LogCandidateFetch fetch,
  3. required List<LogTextCondition> textConditions,
})

Builds the page of query for a store whose query language selects only candidates and leaves the text conditions (textConditions) to Dart: SQL narrows (an FTS index or LogTextCondition.prefilterTokens), LogTextCondition.matches decides.

fetch reads candidates in query order. Each is first checked on its JSON text (LogTextCondition.mayMatchJson), which costs a fraction of decoding it; only a candidate that passes is decoded and tested with LogTextCondition.matches. Candidates are read until LogQuery.limit + 1 are accepted (the extra one decides LogPage.next without a count) or they end. The first round reads exactly limit + 1 rows, so a query whose candidates are all accepted costs the same as a plain page; when candidates are rejected, the next rounds continue after the last candidate read and grow (doubling up to _maxRound), so a rare match in a large log costs tens of round trips, not a scan into memory. Without a limit, all candidates are read in one round. LogPage.next continues after the last accepted entry, as with any store.

Implementation

Future<LogPage> collectLogPage(
  LogQuery query, {
  required LogCandidateFetch fetch,
  required List<LogTextCondition> textConditions,
}) async {
  StoredLogEntry? accept(LogCandidate candidate) {
    if (!textConditions.every((condition) => condition.mayMatchJson(candidate.entryJson))) return null;
    final entry = logEntryJsonText.decode(candidate.entryJson);
    if (!textConditions.every((condition) => condition.matches(entry))) return null;
    return StoredLogEntry(candidate.id, entry);
  }

  final limit = query.limit;
  if (limit == null) {
    return LogPage([for (final candidate in await fetch(query.startAfter, null)) ?accept(candidate)]);
  }

  final wanted = limit + 1;
  final accepted = <StoredLogEntry>[];
  var after = query.startAfter;
  var round = wanted;
  while (accepted.length < wanted) {
    final candidates = await fetch(after, round);
    for (final candidate in candidates) {
      if (accept(candidate) case final entry?) {
        accepted.add(entry);
        if (accepted.length == wanted) break;
      }
    }
    if (candidates.length < round) break;
    after = candidates.last.cursor;
    round = math.min(math.max(round * 2, wanted - accepted.length), _maxRound);
  }

  final hasNext = accepted.length > limit;
  final entries = hasNext ? accepted.sublist(0, limit) : accepted;
  return LogPage(entries, next: hasNext ? query.startingAfter(entries.last.cursor) : null);
}