Skip to content

Add live read-only subtype collection views - #41

Closed
danReynolds wants to merge 8 commits into
mainfrom
feature/collectionView
Closed

danReynolds wants to merge 8 commits into
mainfrom
feature/collectionView

Conversation

@danReynolds

@danReynolds danReynolds commented Sep 4, 2026 •

Copy link
Copy Markdown
Owner

A mixed Collection<A> can expose its B documents through a live, read-only view. Other subtypes are excluded rather than returned as snapshots with null data.

final bs = CollectionView<B>(collection);
final sameBs = collection.view<B>();
bs.get();
bs.doc('id').get();
bs.doc('id').stream();
bs.where((snap) => snap.data.bProperty > 0).observe();

CollectionView<B>, DocumentView<B>, and their snapshots preserve the existing read API and subtype inference. Snapshot document handles are also read-only. The parent collection owns serialization and writes; subtype changes appear as removals/additions relative to each view. view<S>() enforces a subtype bound at compile time; the constructor filters by runtime data type and returns no matches for unrelated types.

Views reuse the existing query engine for filters, sorting, hydration, dependency invalidation, and change events. Document observation uses direct document and event lookup. Weak-key projection caches share typed snapshots across filters, sorting, and reads without a separate invalidation protocol. Observed result lists are unmodifiable so a caller cannot corrupt the shared cached result. Projected streams retain their identity across widget rebuilds.

Review findings addressed:

  • A 2,000-result sorting probe created 59,202 document handles; projection reuse reduces that to 2,000.
  • Twenty cached reads created 40,000 handles; they now create none and reuse the result list.
  • exists() avoids projecting all matching documents.
  • Single-ID observers no longer scan unrelated collection events.
  • Constructor/helper equivalence, cached-result integrity, and randomized mixed batched operations have regression coverage. These allocation counts are not throughput measurements.

Compatibility: builder callbacks receive DocumentSnapshotView<T>, and Queryable.toQuery() returns QueryView<T>. Direct collection/document methods retain their writable return types. The README, changelog, and example describe using the original collection or document for writes from builders.

Validation:

  • flutter analyze --no-pub: clean.
  • Core and native suites: 270 passed.
  • Subtype-view and IndexedDB suites in Chrome: 60 passed.
  • Static API probe: both construction APIs reject mutation through views, and view<S>() rejects unrelated subtype arguments.

Copilot AI lite review requested due to automatic review settings September 4, 2026 02:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

It introduces compile-time issues in lib/loon.dart and lib/collection_view.dart (invalid library directive and invalid generic parameter ordering) that must be fixed before it can be safely merged.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds a new “view” layer for collections/documents intended to enable type-safe narrowing of snapshot data, wires it into the main loon.dart library, and adjusts broadcast cache invalidation behavior.

Changes:

  • Add CollectionView/DocumentView/DocumentSnapshotView types to support subtype “views” over existing Collection/Document APIs.
  • Register the new part file in lib/loon.dart.
  • Always invalidate observer cache entries when writing document broadcast events.
File summaries
File Description
lib/loon.dart Registers the new collection_view.dart part (but currently introduces an invalid library directive).
lib/collection_view.dart Adds the new view types for type-safe narrowing of document snapshot data.
lib/broadcast_manager.dart Changes cache invalidation behavior on writeDocument and updates nearby logic/comments.
example/pubspec.lock Updates resolved dependency versions and SDK constraint in the example lockfile.
Review details

Suppressed comments (2)

lib/collection_view.dart:28

  • This class has the same invalid type-parameter ordering issue (S extends T references T before it is declared). Swap the type parameter order and update the return type / constructor call accordingly.
class DocumentView<S extends T, T> {
  final Document<T> doc;

  DocumentView(this.doc);

  DocumentSnapshotView<S, T>? get() {
    final snap = doc.get();
    if (snap == null) {
      return null;
    }
    return DocumentSnapshotView(snap);
  }

lib/collection_view.dart:39

  • This class also uses the invalid type-parameter ordering (S extends T, T). Swap the type parameter order and update the doc() return type / constructor call to match the updated DocumentView declaration.
class CollectionView<S extends T, T> {
  final Collection<T> _collection;

  CollectionView(this._collection);

  DocumentView<S, T> doc(String id) {
    return DocumentView(_collection.doc(id));
  }
  • Files reviewed: 3/4 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/collection_view.dart Outdated
@@ -0,0 +1,40 @@
part of 'loon.dart';

class DocumentSnapshotView<S extends T, T> {
Comment thread lib/loon.dart
@@ -1,4 +1,4 @@
library loon;
library;
Comment thread lib/broadcast_manager.dart Outdated
Comment on lines +106 to +107
// All cached observer values for the document and its collection
// are invalidated after the document is mutated.
Comment thread lib/collection_view.dart Outdated
Comment on lines +31 to +39
/// A view of a collection that allows for type-safe narrowing to a specific subtype of the collection's document type.
class CollectionView<S extends T, T> {
final Collection<T> _collection;

CollectionView(this._collection);

DocumentView<S, T> doc(String id) {
return DocumentView(_collection.doc(id));
}
danReynolds and others added 3 commits September 3, 2026 23:22
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
main replaced the asyncEvent helper with flushBroadcasts, so the stream tests
now use the same fakeAsync pattern as the rest of the suite.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@danReynolds danReynolds changed the title Collection view Add live read-only subtype collection views Sep 29, 2026
* Dispose only the observers stream builders create

`observe()` on an observed document or query returns the same observer, so
a builder given one was disposing the caller's observer when it was removed
or switched sources, closing it for every other listener. The builders now
dispose an observer only if they created it.

Removal is the case this branch introduced; disposing on a source switch
predates it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Construct collection views only through view()

The public `CollectionView<S>(collection)` constructor accepted any type, so
`CollectionView<String>(animals)` compiled and silently matched nothing.
`view<S>()` requires S to extend the collection's type at compile time, and
is now the only way to create a view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Compare collection views by collection and subtype

A collection view compared by identity, so a view created inline in `build`
made `QueryStreamBuilder` observe and scan the collection again on every
rebuild. Views of the same type over the same collection select the same
documents, so they now compare equal, as collections compare by path and
document views by path and type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Give views their own stream builders

Accepting views in DocumentStreamBuilder and QueryStreamBuilder changed their
callbacks to read-only snapshots for every caller, so existing builders could
no longer write through `snap.doc`, and `Queryable.toQuery()` changed type.
Those builders keep their signatures from main. DocumentViewStreamBuilder and
QueryViewStreamBuilder build from views, and also accept ordinary documents
and queries through the read interfaces they implement.

With this, the view layer only adds API: no existing signature changes, and
the example app is back to writing through `userSnap.doc`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Sort a view's projected snapshots instead of projecting in the comparator

A view's sort ran in the underlying query, with a comparator that looked up
both snapshots' projections on every comparison. The view now keeps its sort
and applies it to its projected snapshots, so each snapshot's projection is
looked up once per read. In interleaved local runs over 2,000 matching
documents, `get()`, reads after a write, and stream emissions were faster in
every round, by roughly 2-2.7x at the median.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants