Repository navigation
Add whereType subtype narrowing (alternative to #41) - #43
Closed
danReynolds wants to merge 2 commits into
Closed
danReynolds wants to merge 2 commits into
danReynolds wants to merge 2 commits into
Conversation
`collection.whereType<S>()` and `query.whereType<S>()` return ordinary Collection, Document and Query handles that see only the documents whose data is an S. A narrowed handle keeps a private source handle. Reads go through the source and keep the snapshots whose data is an S; writes delegate to it, so serialization, persistence and dependencies always use the source's codec. Observers report a document that changes type as added or removed, and narrowed writes only affect documents of the narrowed type. Stream builders also compare their handle's type, since handles compare by path, and transactions roll back narrowed writes through the source. An alternative to #41 for comparison. Its test suite is ported to this API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The narrowed collection, document and observer are now private subclasses in where_type.dart, so Document is unchanged and Collection only gains the whereType entry point. ObservableDocument gets two small hooks, the store read and the reported change event, which the narrowed observer overrides. A narrowed document creates its source handle on first use, so narrowed reads, filters and sorts create no source handles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An alternative to #41 for the same goal: reading a
Collection<A>that holds subtypesBandCas one subtype, by ID and through filters. Opened to compare implementations; #41's test suite is ported to this API.whereTypemirrorsIterable.whereTypeand returns the existingCollection,DocumentandQuerytypes, so snapshots,FilterFns, the stream builders andTransactionWritertake narrowed handles unchanged. There are no new public types and no breaking changes, andS extends Tis checked at compile time.How it works
The narrowing lives in
lib/src/where_type.dartas private subclasses:_NarrowedCollection,_NarrowedDocumentand_NarrowedObservableDocument. Each holds the source handle it was narrowed from, which owns the stored snapshots and their serialization.Documentis unchanged, andCollectionandQueryonly gain thewhereTypeentry points.S, returning aDocumentSnapshot<S>whosedocis a narrowed handle. Hydrated documents are always parsed with the source'sfromJson. A narrowed document creates its source handle on first use, so narrowed reads, filters and sorts create no source handles.toJsonuse its codec, and stored snapshots keep the source's type.ObservableDocumentgains two hooks that the narrowed observer overrides: the store read, and the reported change event. The narrowed observer reports changes relative to the type: a document that becomes anSisadded, one that stops being anSisremoved, and changes to other types emit nothing.ObservableQuerytreats a document that isn't anSlike one that fails the filter, and resolves a narrowed collection's documents through the collection so result handles keep the source's codec.The source handle is what makes this possible. The store uses the reader's type to decide whether a snapshot is parsed:
getSnapshot<T>re-parses anything that isn't aDocumentSnapshot<T>with the reader's codec. So a second typed handle on the same path throws a type-mismatch exception instead of narrowing.Semantics to decide
update,modifyanddeleteon aDocument<Dog>treat a cat as a missing dog.createnever overwrites an existing ID of any type. A narrowed collection'sdeleteandreplaceaffect only its documents, andreplacechecks every ID before writing. Changing a document's type is a write through the source collection. The alternative is source semantics, where a narrowedupdateturns a cat into a dog.runtimeType; without that, switching a builder fromdogs.doc(id)tocats.doc(id)kept the old observer. The alternative is type-aware==.fromJson, since they parse through their source.Compared with #41
DocumentSnapshotView;Queryable.toQuery()returnsQueryViewcreate/update/modify/deletebool Function(DocumentSnapshot<Dog>)filterswhereCollectionView<String>(animals)compiles and is emptyDocumentuntouchedwhere_type.dart, 219 linesMedian timings, JIT, 2,000 dogs and 2,000 cats, two alternating rounds, measured on the first commit:
where+sortByget()#41 projects snapshots inside filter and sort callbacks through
Expandocaches. Here each snapshot is projected once where it is read, and the engine filters and sorts the projected snapshots directly.The second commit moves the same code into
where_type.dart. WithDocument's debug-only path asserts stripped from both layouts, the two measured the same within noise. Underflutter test, the first version of that layout validated two handles per projected snapshot; lazy source handles leave one. Later interleaved runs against #41 kept the same ordering (#41 2–5× slower onget()and re-reads), but the machine was too loaded (load average ~43 on 10 cores) to quote exact figures.Other differences found while reviewing #41:
dispose()overrides dispose observers that the caller passed in. Unmounting aQueryStreamBuildergivencollection.observe(multicast: true)closes that observer for its other listeners. Three probes for this fail at Add live read-only subtype collection views #41's head and pass here, where the builders keep main's lifecycle.CollectionViewcompares by identity, so a view built inline inbuildmakesQueryStreamBuilderre-observe and rescan the collection on every rebuild (11 scans in 11 builds). A new narrowedCollectioncompares equal by path and type and is observed once; a test covers this.isDirtyturns true after a sibling write, andreplacewith the same dog emitsremovedthenadded. A narrowedObservableDocumentmatches a plain one:isDirtystays false, and it emits a singleadded.Validation
test/core/where_type_test.dartports all 18 tests of Add live read-only subtype collection views #41'scollection_view_test.dartand adds 5: typed writes, writable builder snapshots, codec-safe observed results, transaction rollback, and single observation across inline rebuilds.==, which is whatStreamBuildercompares, instead ofsame. Read-only type assertions become ordinary type assertions, and the check that views of different types are unequal is dropped because handles compare by path. Add live read-only subtype collection views #41's unmodifiable-cache and single-handle-after-write assertions are dropped: observed queries return the engine's list as on main, and re-project on a synchronous re-read.flutter analyzereports no issues.clearAlland subcollection parents, and checks synchronous reads, streams and folded change streams. Under two of the mutations it fails 60/60 and 37/60 seeds. The probe is kept locally, not committed.🤖 Generated with Claude Code