fix(fetchium): resolve TopicQuery adapter via subclass-aware lookup - #13
Merged
Conversation
TopicQuery declared `static adapter` as a type-only annotation with no
runtime value, so subclasses had to set it explicitly. That breaks two
patterns:
1. Generated TopicQuery classes that don't know which concrete adapter
a consumer will use — they can only target the abstract base.
2. Hand-authored subclasses had to write
`static adapter = MyAdapter as unknown as typeof TopicQueryAdapter`
to satisfy the override.
This change makes TopicQuery assign `static adapter = TopicQueryAdapter`
(mirroring the RESTQuery pattern) and teaches `QueryClient.getAdapter()`
to fall back to a subclass `instanceof` scan before auto-instantiating.
Why both changes are needed: RESTQueryAdapter is concrete, so the
registered instance's `.constructor` equals the static-adapter key on
the query and exact-match lookup hits. TopicQueryAdapter is abstract, so
consumers must register a subclass — the registered constructor is the
subclass, not the base, and exact-match misses. The new instanceof fallback
also makes the REST path correct in the (previously undefined) case where
a consumer registers a subclass of RESTQueryAdapter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s adapter The new test in `topic-query.test.ts` relies on the outer `beforeEach` (~1800 lines above) registering a `MockTopicQueryAdapter` instance on the QueryClient. Without that context the test reads as magic. Replace the comment with one that names the inheritance + instanceof-scan path explicitly and points at the outer registration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…overrides After the subclass-aware adapter resolution change, individual TopicQuery subclasses no longer need to declare `static override adapter` for the common case of one registered streaming adapter. Update the streaming page so: - Inline examples extend `TopicQuery` directly instead of an intermediate `MyTopicQuery` base. - The "Registering the adapter" section explains that the resolution happens via inheritance + the QueryClient's instanceof lookup, with an opt-in note for disambiguating between multiple registered subclasses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… note Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In practice, apps register one streaming adapter per QueryClient — the multi-adapter override pattern was over-engineering. Replace with a one-liner pointing at "use a separate QueryClient" if you ever hit the case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The only justification for "use a separate QueryClient" is the adapter resolution ambiguity case we already trimmed as hypothetical. Without that reasoning, the sentence dangles. Remove it; the common-case explanation stands on its own. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`QueryClient.getAdapter()`'s subclass-aware lookup picks the first `instanceof` match in registration order. If a consumer registers two adapters that both satisfy the same lookup base (e.g. two `TopicQueryAdapter` subclasses on one client), the resolution is silent and brittle. In dev builds, scan all registered adapters and throw on more than one match, naming the conflicting classes. In production, keep the original fast path: first match wins, exit early. The dev-only branch is gated behind `if (IS_DEV)` so it tree-shakes out of production bundles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `getAdapter()`: collapse the four-case branch in the instanceof scan to a single dev-mode ambiguity check plus `match ??= registered` for first-match-wins. Also drop a now-redundant inline comment on the auto-instantiate fallback (the JSDoc enumerates step 3 and the catch's error message covers the failure mode). - `TopicQuery.ts`: trim the five-line comment on `static override adapter` to a single-line note about why the explicit type annotation is load-bearing. No behavior change. All topic-query tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the "one TopicQueryAdapter subclass per QueryClient" design intent and pre-warns about the dev-mode ambiguity error so users structure their setup correctly the first time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pzuraq
approved these changes
Apr 28, 2026
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.
Summary
TopicQuerynow assignsstatic adapter = TopicQueryAdapterso subclasses inherit a runtime value (mirroringRESTQuery/RESTQueryAdapter). The type stays widened toQueryAdapterClass<TopicQueryAdapter>so subclasses can override with concrete adapters whose constructors take arguments.QueryClient.getAdapter()now falls back to aninstanceofscan over registered adapters before auto-instantiating, so an abstract base on the query resolves to a consumer-registered concrete subclass.Why this is needed
Before this change,
TopicQuery.adapterwas a type-only annotation with no value. That forced everyTopicQuerysubclass to setstatic adapterexplicitly, which:TopicQuerysubclasses without knowing which concrete adapter the consumer will register. The generated classes can only target the abstract base.as unknown as typeof TopicQueryAdapter) when their adapter has constructor args, because the override has to match the abstract base shape.Why subclass-aware lookup is needed for topics but not REST
RESTQueryAdapteris concrete, so consumers registernew RESTQueryAdapter(...)directly — the registered instance's.constructorequals the static-adapter key on the query, and exact-match lookup hits.TopicQueryAdapteris abstract (subscribe/unsubscribe are abstract). Consumers must register a subclass instance (e.g. a WebSocket-backed adapter), so the registered.constructoris the subclass, not the base. Without the subclass-aware fallback,getAdapter(TopicQueryAdapter)misses, falls through to auto-instantiation, and throws because you can'tnewan abstract class.The instanceof fallback also makes the REST path correct in the (previously undefined) case where a consumer registers a subclass of
RESTQueryAdapter— the old code would silently auto-instantiate a freshRESTQueryAdapterand ignore the registered instance.Test plan
npm run check-typespasses (existingadapter-types.test-d.tsstill validates the override surface).react/__tests__/basic.test.tsx:658"Loading vs 0" timing assertion unrelated to this change —getAdapter's fast-path is unchanged when an exact-match adapter is registered).topic-query.test.tscovers aTopicQuerysubclass that does not setstatic adapter, asserting (a) the inherited static value isTopicQueryAdapterand (b)fetchQuery()resolves via the registered subclass instance.🤖 Generated with Claude Code