You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TanStack DB needs an official internal framework-binding platform, not more framework-by-framework ports of React hooks.
The current issue cluster spans React, Vue, Svelte, Solid, Angular, infinite queries, and SSR. The repeated root problem is that each adapter owns too much of the same semantic state machine: input normalization, collection ownership, sync activation, status/readiness/error tracking, result cardinality, initial snapshots, subscription timing, ordered materialization, pagination windows, SSR identity, and stale async protection.
Share DB/query semantics and lifecycle; keep renderer state management framework-native.
React should not be the reference implementation. Solid should not be the reference implementation. Vue should not be asked to match React or Solid. All official adapters should match the same internal observable contract while using their own native reactivity models.
Context
This RFC comes from a cluster of issues and PRs that initially look framework-specific but point to one shared architectural problem: framework adapters are independently rebuilding the live-query lifecycle and result-materialization state machine. The same semantic gap then appears with different symptoms depending on the renderer.
In React, the failure mode is mostly around the external-store boundary. useLiveQuery can miss rows written between render and subscription, notify React before a component is committed, or trigger extra renders when a collection is already ready. Representative items include #439, #690, #1178, #1516, #1587, and #1594. These are not just timing bugs in one hook. They show that React currently observes a hook-local version counter and mutable collection state rather than one canonical live-query snapshot with well-defined subscription semantics.
In non-React adapters, the symptoms are different but the shape is familiar. Svelte has unsafe mutation and SSR-initialization problems when state is established through effect-time subscriptions (#985, #1196). Vue has adapter modernization work focused on shallow reactivity, lifecycle cleanup, disabled-query handling, and avoiding React-shaped patterns (#1302). Solid has proxy, keyed reconciliation, remount, and full-store-reset performance problems (#1598, #1608, #1609). Angular has seen cardinality drift where findOne() semantics are not consistently represented by the shared query result contract (#1272).
The infinite-query work shows the same duplication pressure. React's useLiveInfiniteQuery contains a substantial pagination/window state machine, and Vue/Svelte PRs are naturally tempted to port it (#1105, #1447, #1513). Bugs such as #1553 show that the active { offset, limit } window needs a single semantic owner; it should not be split between query-builder limits and hook-local setWindow() effects.
Core result publication also leaks into adapter behavior. #1601 shows that a live query can reorder without emitting a visible row-value change when the projected row is unchanged. Adapters cannot reliably render a change the core does not express. The required fix is a canonical observer snapshot whose layoutRevision, keys, and data change when the visible order changes, even if row values are unchanged.
SSR and identity are the other half of the same problem. #1564 moves toward DbClient, DbProvider, structured query IR identity, and row-based dehydration/hydration. That direction is compatible with this RFC, but the observer and adapter contracts may affect final SSR payload choices. Hydration cannot be solved independently in each framework adapter; it needs client-owned identity and snapshots that the adapters can consume consistently.
The current codebase already has separate framework adapters:
packages/react-db/src/useLiveQuery.ts
packages/vue-db/src/useLiveQuery.ts
packages/svelte-db/src/useLiveQuery.svelte.ts
packages/solid-db/src/useLiveQuery.ts
packages/angular-db
These adapters duplicate substantial behavior.
Examples observed in the current code:
React useLiveQuery detects collections through method duck typing, starts sync during render, maintains a hook-local version counter, synthesizes a ready notification in subscribe, and currently calls useSyncExternalStore without a server snapshot.
Vue and Svelte recreate similar input normalization, disabled-query behavior, eager sync startup, status tracking, and state/data materialization.
Vue uses exception-driven disabled-query handling in one path.
Svelte initializes data in $effect, which cannot be the full SSR story and interacts poorly with unsafe mutation windows.
Solid uses createStore/reconcile over full data snapshots, which is correct but can be expensive and has proxy/remount edge cases.
React infinite query owns a pagination state machine inside the hook, including page counts, setWindow, stale promise protection, and serialized dependency comparison.
The conclusion is that the project needs a shared live-query protocol and conformance suite. Framework packages should not decide independently what counts as a disabled query, when a subscription may replay, whether findOne() returns one row or an array, how order-only moves are represented, or which lifecycle owns SSR identity.
PR #1564 is the current direction for DbClient, query identity, and SSR. This RFC builds on that direction but does not freeze every SSR detail. The observer and adapter contracts may influence the final SSR design, and Tanner/maintainers should be involved in that decision.
Goals
Define an official internal live-query observer contract for first-party framework adapters.
Move shared live-query semantics out of individual adapters.
Preserve framework-native reactivity and scheduling.
Make DbClient the owner of query identity, observer registry, scope, hydration, and shared lifecycle policy.
Normalize disabled queries, cardinality, status, errors, readiness, and cleanup across adapters.
Support ordered result snapshots and layout revisions, including order-only moves.
Move infinite/windowed query semantics into a shared internal controller.
Make SSR/hydration client- and scope-first rather than React-hook-first.
Establish a shared adapter conformance suite for React, Vue, Svelte, Solid, and Angular.
Preserve existing public adapter APIs during migration where practical.
Non-goals
Expose a public third-party adapter API.
Make a universal renderer data structure that every framework wraps.
Replace framework-native scheduling with a generic scheduler.
Stabilize every SSR payload detail in this RFC.
Implement all phases in one PR.
Force every adapter to expose identical ergonomic APIs beyond shared semantics.
The observer contract is for official adapters. It can be exported where package boundaries require it, but it should be marked internal or unstable and not documented as an extension point for third-party adapters.
Proposed architecture
1. DbClient and scope layer
DbClient should own:
live-query identity
observer registry
request/client scope
dehydration and hydration
shared GC policy
structured query IR identity where possible
explicit user-provided identity for opaque functional queries
Framework adapters should not create, start, and dispose independent live-query collections directly during render or effect setup. They should resolve user input into a normalized spec, ask the client for an observer, and materialize that observer through native framework primitives.
2. Internal live-query observer
Introduce an internal observer contract along these lines:
Provide one shared collection brand/type guard. Avoid instanceof CollectionImpl and per-adapter duck typing.
Call query factories in one well-defined place. Avoid probe-then-call-again behavior.
Normalize legacy public overloads into one internal representation.
Define disabled-query semantics once.
Carry cardinality in the normalized spec/snapshot.
Dependency tracking remains framework-native, but the resolved spec passed to the observer is consistent.
Renderer materialization
The shared layer should not expose a universal mutable Map plus mutable array. That would become a lowest-common-denominator renderer.
Instead, the observer should expose stable snapshots as the required contract. Adapters can render correctly from snapshots alone as long as every user-visible result change publishes a new revision and every membership, order, or window change publishes a new layoutRevision with updated keys and data.
Ordered result snapshots
Adapters need to know about the materialized result layout, not just row-value changes. A visible result can change because of:
When order changes, layoutRevision, keys, and data change even if projected row values are unchanged. This is sufficient for correctness across all adapters.
An ordered patch stream may be added later as an internal optimization for large windows, row-level subscriptions, or renderer-specific performance work, but it is not required for the first binding-platform milestone.
React materializer
React should be a thin useSyncExternalStore adapter:
getSnapshot, subscribe, and getServerSnapshot come from the observer/client.
Returned result object is memoized by snapshot revision.
Lazy state/map view is allowed for compatibility.
Future fine-grained APIs should use selectors, key subscriptions, and row subscriptions rather than mutable lazy arrays.
Render should be inert: no sync startup, listener attachment, GC scheduling, or ownership mutation during render.
Vue materializer
Vue should use Vue-native reactivity:
shallow refs for ordered data and keyed state
effect-scope cleanup with onScopeDispose
no deep proxying of immutable row snapshots
atomic application of observer snapshots
shared normalization/branding instead of exception-based disabled-query handling or instanceof CollectionImpl
Svelte materializer
Svelte should be rune-safe and SSR-aware:
initialize synchronously from getSnapshot() before $effect
do not rely on $effect for SSR initial data
depend on non-reentrant subscribe() behavior
apply updates outside unsafe mutation windows
preserve Svelte 5 ergonomics while deriving semantics from the observer
Solid materializer
Solid should preserve fine-grained identity:
keyed reconciliation with $key
stable proxies for unaffected rows
no full-store reset for one-row updates
lazy keyed state materialization where compatibility requires it
layout/row revisions to choose between preserving state and resetting state
Angular materializer
Angular should be first-class:
signal-backed result state
injector-aware lifecycle and cleanup
shared cardinality so findOne() returns the correct runtime shape
participation in the same conformance matrix as React, Vue, Svelte, and Solid
Infinite and windowed queries
useLiveInfiniteQuery should not be a React hook state machine copied into Vue and Svelte. Pagination/window semantics should live in a shared internal controller layered on top of the observer/client.
reset behavior when the underlying query generation changes
validation that the query is ordered/window-capable
serializable pagination state for hydration, if supported
Required API decisions:
fetchNextPage() returns Promise<void>.
Pagination errors are exposed in the snapshot, not only logged.
getNextPageParam is either implemented or deprecated/removed before the API stabilizes.
The pagination controller does not use JSON.stringify(deps).
For infinite queries, the controller is the single authority for { offset, limit }; builder .limit().offset() must not compete with later setWindow() calls.
Migration stance:
Narrow correctness fixes can land when needed.
Long-term Vue/Svelte infinite query support should not copy React's hook-local state machine.
Existing React implementation and historical fixes should become requirements/tests, not the canonical implementation.
SSR, identity, and hydration
The RFC assumes DbClient is the current direction for identity and SSR, but the final API should be decided with Tanner and maintainers after reviewing how the observer contract affects hydration.
Target model
SSR should be scope/client-first, not React-hook-first.
Framework adapters then read first snapshots from hydrated observers.
Requirements
getServerSnapshot() exists for adapters that need it.
Server snapshot and first client snapshot agree when hydration data is present.
Client-only mode has a stable server/first-client empty or loading snapshot.
Hydration payloads are versioned.
Request-scoped clients are isolated by default.
Process-wide reuse is explicit.
Hydration carries enough revision/layout identity to avoid false immediate rerenders.
Derived live-query rows may be omitted when source collection snapshots are included and the client can rebuild them deterministically.
Sync resume metadata can be included where collection implementations support it.
RSC/server components use one-shot scoped evaluation or prefetch; they do not instantiate enduring client-style subscriptions.
Maintainer decision: payload policy
The RFC should not force one answer before maintainer review. The key decision is whether DbClient dehydrates:
live-query result snapshots,
source collection rows,
or both with pruning.
Trade-offs:
Result snapshots are smaller and match hook hydration directly.
Source rows are more canonical and let the client rebuild derived query/dataflow state.
A hybrid payload can prune derived rows when source rows are already present.
The recommended direction is source-first or hybrid, but the final policy should be decided with Tanner and maintainers.
Framework provider surfaces
Each adapter should expose a framework-native way to provide the client:
React: DbProvider
Vue: provide/inject
Svelte: context
Solid: context
Angular: DI provider
The SSR data model belongs in core/client. Framework packages should not invent independent hydration formats.
Conformance suite
The conformance suite is part of the architecture, not a follow-up nicety. The project needs a shared behavioral contract, not a reference framework.
Each official adapter should provide a harness with operations such as:
mount/use a live query
read current result
update source collection
flush framework work
render server-side, where applicable
hydrate, where applicable
unmount/dispose
The same scenarios should run against React, Vue, Svelte, Solid, and Angular.
Required coverage:
query callback, config object, direct collection, and nullable/disabled query
disabled → enabled → disabled transitions
collection replacement and stale callbacks
idle/loading/ready/error/cleaned-up statuses
already-ready collection at mount
no synchronous replay during subscribe
no missed render-to-subscribe updates
no updates after unmount/dispose
StrictMode or equivalent lifecycle stress where applicable
findOne() runtime shape and type inference
insert, update, delete, order-only move, and window membership changes
SSR server snapshot and hydration equality
row identity preservation for unaffected rows
infinite query window expansion and stale setWindow() promises
error propagation and suspense/resource behavior where applicable
Executable invariants should include:
same revision => Object.is(previousSnapshot, nextSnapshot)
observable change => exactly one later revision
no observable change => no revision
unsubscribe => no future callback
render => no sync or ownership side effects
retain/release => balanced after every mount sequence
SSR snapshot => equals first hydration snapshot when hydrated
Migration plan
Phase 1: write contract tests
Encode desired behavior before moving adapter code.
Phase 2: introduce internal observer and normalized input contract
Add:
shared collection branding/type guard
normalized live query spec
disabled query representation
cardinality in snapshot
status/error/readiness in snapshot
non-reentrant subscription behavior
retain/release lifecycle
stale generation protection
Phase 3: migrate all five adapters
Migrate React, Vue, Svelte, Solid, and Angular to consume the observer while preserving public APIs where practical.
Remove duplicated lifecycle/state-machine code from adapters. Keep renderer-native materializers.
Phase 4: add ordered snapshot/layout contract
Support order-only moves and layout revisions directly. Ensure revision, layoutRevision, keys, and data update for all visible membership, ordering, and window changes. This is a correctness milestone, not a patch-stream optimization milestone.
Use this to improve:
React external-store consistency
Vue shallow/atomic snapshot updates
Svelte safe snapshot updates
Solid keyed reconciliation correctness
Angular signal updates
Phase 5: extract infinite/window controller
Move pagination/window ownership out of React and into a shared internal controller.
Finish Vue and Svelte infinite-query support on top of shared semantics.
Phase 6: finalize SSR/client hydration
Integrate with the #1564 direction or with a revised maintainer decision.
Add provider/context/DI wrappers across adapters and SSR e2e coverage.
Phase 7: docs and feature matrix
Document shared semantics once. Framework docs should focus on native usage examples.
Add a feature matrix covering:
useLiveQuery
infinite/windowed queries
SSR/hydration
disabled queries
suspense/resource behavior
single-result behavior
fine-grained subscriptions/selectors
Treatment of current open work
Vue modernization work should inform the Vue materializer, especially shallow reactivity, effect-scope cleanup, stale collection guards, and removal of deep proxying.
Solid renderer rework should inform the snapshot/layout contract and Solid materializer, especially $key, proxy preservation, lazy state, and avoiding unnecessary full resets.
React eager-notification and render-to-subscribe fixes should become conformance tests. Microtask deferral can be a tactical fix but should not be the final semantic model.
Order-only move fixes should become an explicit layout-revision requirement, not a hidden forced-update path.
Vue/Svelte infinite query PRs should be rebased onto the shared infinite/window controller rather than copying React's state machine.
A patch stream is not required for correctness in the first milestone. UI-facing live queries usually have small active windows, and adapters already render from snapshots or snapshot-like materializations today. A canonical snapshot with revision, layoutRevision, keys, and data is enough to fix the semantic bugs, including order-only moves, as long as every visible layout change publishes a new snapshot.
The remaining requirement is that core detects visible layout changes. For example, when an orderBy field changes but the projected row value is unchanged, the observer must still publish a new snapshot with an incremented layoutRevision and updated order. It does not need to expose a { type: "move" } patch initially.
An ordered patch stream can be revisited later if benchmarks show snapshot replacement is a bottleneck, or if fine-grained APIs need efficient row/key-level change descriptions. That later design can build on the same revision and layout-revision contract without blocking the observer, adapter migration, infinite-query controller, or SSR work.
Risks and mitigations
Risk: the RFC is too broad
Mitigation: treat this as one platform RFC with staged implementation. The first implementation milestone is the observer contract plus conformance tests.
Risk: internal observer leaks as public API
Mitigation: mark it internal/unstable and document that it is for official adapters only. Avoid public adapter-author documentation.
Consideration: centralized snapshots should not preserve today's avoidable costs
Adapters already generate snapshots or snapshot-like materializations today. React memoizes a hook-local external-store snapshot and captures Array.from(collection.entries()) for each returned result revision. Vue, Svelte, and Solid also maintain framework-local ordered data and keyed state derived from collection contents. Query snapshot cost is not expected to be a primary risk because UI-facing live queries usually have small active windows.
The design should still avoid carrying forward unnecessary duplication. The central observer snapshot should be cheaper and more canonical than today's per-adapter snapshots by using stable identity for unchanged revisions, structural sharing for data and keys, layout revisions, row revisions, and lazy state/map compatibility views.
Risk: SSR decisions block adapter fixes
Mitigation: separate the observer/conformance milestone from final SSR payload policy. Keep DbClient as the assumed owner while leaving hydration details open for maintainer review.
Risk: adapters lose framework idioms
Mitigation: share semantics, not renderer state. Each adapter keeps native scheduling/materialization.
Risk: migration breaks public APIs
Mitigation: preserve public overloads initially and normalize internally. Deprecations can come later after conformance is in place.
Alternatives considered
Observer contract only
A narrower RFC could define only InternalLiveQueryObserver and migrate adapters to it.
This is the best first implementation milestone, but too narrow as an RFC because observer decisions affect ordered changes, infinite queries, SSR snapshots, and conformance.
DbClient/SSR first
A narrower RFC could center on DbClient, identity, and hydration.
This aligns with current momentum but does not fix the duplicated adapter state machines or cross-framework behavior drift.
React as reference implementation
React could continue as the de facto source of truth, with other adapters porting its logic.
This has already produced drift and framework-shaped bugs. React's external-store model is not the right model for every renderer.
Universal renderer state
Core could expose one mutable Map and one ordered array for all adapters.
This would simplify code superficially but would fight framework-native reactivity and likely reproduce Solid/Vue/Svelte performance and lifecycle issues in a different form.
Recommendation
Adopt the full binding platform design as the architectural direction.
Implement it in phases:
contract tests,
internal observer and input normalization,
adapter migration,
ordered snapshots and layout revisions,
infinite/window controller,
SSR/hydration finalization,
docs and feature matrix.
The decisive shift is from “port hooks from React” to “implement one internal live-query protocol and several native renderers.” Pagination, SSR, query cardinality, disabled semantics, stale async behavior, and lifecycle correctness can then be fixed once while keeping each framework adapter idiomatic.
TanStack DB needs an official internal framework-binding platform, not more framework-by-framework ports of React hooks.
The current issue cluster spans React, Vue, Svelte, Solid, Angular, infinite queries, and SSR. The repeated root problem is that each adapter owns too much of the same semantic state machine: input normalization, collection ownership, sync activation, status/readiness/error tracking, result cardinality, initial snapshots, subscription timing, ordered materialization, pagination windows, SSR identity, and stale async protection.
This RFC proposes a full binding platform:
The key principle is:
React should not be the reference implementation. Solid should not be the reference implementation. Vue should not be asked to match React or Solid. All official adapters should match the same internal observable contract while using their own native reactivity models.
Context
This RFC comes from a cluster of issues and PRs that initially look framework-specific but point to one shared architectural problem: framework adapters are independently rebuilding the live-query lifecycle and result-materialization state machine. The same semantic gap then appears with different symptoms depending on the renderer.
In React, the failure mode is mostly around the external-store boundary.
useLiveQuerycan miss rows written between render and subscription, notify React before a component is committed, or trigger extra renders when a collection is already ready. Representative items include #439, #690, #1178, #1516, #1587, and #1594. These are not just timing bugs in one hook. They show that React currently observes a hook-local version counter and mutable collection state rather than one canonical live-query snapshot with well-defined subscription semantics.In non-React adapters, the symptoms are different but the shape is familiar. Svelte has unsafe mutation and SSR-initialization problems when state is established through effect-time subscriptions (#985, #1196). Vue has adapter modernization work focused on shallow reactivity, lifecycle cleanup, disabled-query handling, and avoiding React-shaped patterns (#1302). Solid has proxy, keyed reconciliation, remount, and full-store-reset performance problems (#1598, #1608, #1609). Angular has seen cardinality drift where
findOne()semantics are not consistently represented by the shared query result contract (#1272).The infinite-query work shows the same duplication pressure. React's
useLiveInfiniteQuerycontains a substantial pagination/window state machine, and Vue/Svelte PRs are naturally tempted to port it (#1105, #1447, #1513). Bugs such as #1553 show that the active{ offset, limit }window needs a single semantic owner; it should not be split between query-builder limits and hook-localsetWindow()effects.Core result publication also leaks into adapter behavior. #1601 shows that a live query can reorder without emitting a visible row-value change when the projected row is unchanged. Adapters cannot reliably render a change the core does not express. The required fix is a canonical observer snapshot whose
layoutRevision,keys, anddatachange when the visible order changes, even if row values are unchanged.SSR and identity are the other half of the same problem. #1564 moves toward
DbClient,DbProvider, structured query IR identity, and row-based dehydration/hydration. That direction is compatible with this RFC, but the observer and adapter contracts may affect final SSR payload choices. Hydration cannot be solved independently in each framework adapter; it needs client-owned identity and snapshots that the adapters can consume consistently.The current codebase already has separate framework adapters:
packages/react-db/src/useLiveQuery.tspackages/vue-db/src/useLiveQuery.tspackages/svelte-db/src/useLiveQuery.svelte.tspackages/solid-db/src/useLiveQuery.tspackages/angular-dbThese adapters duplicate substantial behavior.
Examples observed in the current code:
useLiveQuerydetects collections through method duck typing, starts sync during render, maintains a hook-local version counter, synthesizes a ready notification insubscribe, and currently callsuseSyncExternalStorewithout a server snapshot.$effect, which cannot be the full SSR story and interacts poorly with unsafe mutation windows.createStore/reconcileover full data snapshots, which is correct but can be expensive and has proxy/remount edge cases.setWindow, stale promise protection, and serialized dependency comparison.The conclusion is that the project needs a shared live-query protocol and conformance suite. Framework packages should not decide independently what counts as a disabled query, when a subscription may replay, whether
findOne()returns one row or an array, how order-only moves are represented, or which lifecycle owns SSR identity.PR #1564 is the current direction for
DbClient, query identity, and SSR. This RFC builds on that direction but does not freeze every SSR detail. The observer and adapter contracts may influence the final SSR design, and Tanner/maintainers should be involved in that decision.Goals
DbClientthe owner of query identity, observer registry, scope, hydration, and shared lifecycle policy.Non-goals
The observer contract is for official adapters. It can be exported where package boundaries require it, but it should be marked internal or unstable and not documented as an extension point for third-party adapters.
Proposed architecture
1. DbClient and scope layer
DbClientshould own:Framework adapters should not create, start, and dispose independent live-query collections directly during render or effect setup. They should resolve user input into a normalized spec, ask the client for an observer, and materialize that observer through native framework primitives.
2. Internal live-query observer
Introduce an internal observer contract along these lines:
The exact method list can evolve. The required behavior is the contract:
getSnapshot()is synchronous.subscribe()does not synchronously replay initial state just because a collection is already ready.unknown, not reduced toisError: true.nullconventions.3. Snapshot model
The snapshot is the single source of truth for adapter-visible state.
Illustrative shape:
The exact fields may change. The required concepts are:
findOne()is not reimplemented differently in every adapter4. Normalized input contract
Adapters should not independently decide whether user input is a collection, config object, query callback, disabled query, or live-query collection.
The shared layer should normalize to a representation like:
Specific requirements:
instanceof CollectionImpland per-adapter duck typing.Renderer materialization
The shared layer should not expose a universal mutable
Mapplus mutable array. That would become a lowest-common-denominator renderer.Instead, the observer should expose stable snapshots as the required contract. Adapters can render correctly from snapshots alone as long as every user-visible result change publishes a new revision and every membership, order, or window change publishes a new
layoutRevisionwith updatedkeysanddata.Ordered result snapshots
Adapters need to know about the materialized result layout, not just row-value changes. A visible result can change because of:
The required internal contract is snapshot-based:
When order changes,
layoutRevision,keys, anddatachange even if projected row values are unchanged. This is sufficient for correctness across all adapters.An ordered patch stream may be added later as an internal optimization for large windows, row-level subscriptions, or renderer-specific performance work, but it is not required for the first binding-platform milestone.
React materializer
React should be a thin
useSyncExternalStoreadapter:getSnapshot,subscribe, andgetServerSnapshotcome from the observer/client.state/map view is allowed for compatibility.Vue materializer
Vue should use Vue-native reactivity:
onScopeDisposeinstanceof CollectionImplSvelte materializer
Svelte should be rune-safe and SSR-aware:
getSnapshot()before$effect$effectfor SSR initial datasubscribe()behaviorSolid materializer
Solid should preserve fine-grained identity:
$keystatematerialization where compatibility requires itAngular materializer
Angular should be first-class:
findOne()returns the correct runtime shapeInfinite and windowed queries
useLiveInfiniteQueryshould not be a React hook state machine copied into Vue and Svelte. Pagination/window semantics should live in a shared internal controller layered on top of the observer/client.Illustrative contract:
The controller should own:
hasNextPageisFetchingNextPage{ offset, limit }windowsetWindow()promise protectionRequired API decisions:
fetchNextPage()returnsPromise<void>.getNextPageParamis either implemented or deprecated/removed before the API stabilizes.JSON.stringify(deps).{ offset, limit }; builder.limit().offset()must not compete with latersetWindow()calls.Migration stance:
SSR, identity, and hydration
The RFC assumes
DbClientis the current direction for identity and SSR, but the final API should be decided with Tanner and maintainers after reviewing how the observer contract affects hydration.Target model
SSR should be scope/client-first, not React-hook-first.
Server:
Client:
Framework adapters then read first snapshots from hydrated observers.
Requirements
getServerSnapshot()exists for adapters that need it.Maintainer decision: payload policy
The RFC should not force one answer before maintainer review. The key decision is whether
DbClientdehydrates:Trade-offs:
The recommended direction is source-first or hybrid, but the final policy should be decided with Tanner and maintainers.
Framework provider surfaces
Each adapter should expose a framework-native way to provide the client:
DbProviderThe SSR data model belongs in core/client. Framework packages should not invent independent hydration formats.
Conformance suite
The conformance suite is part of the architecture, not a follow-up nicety. The project needs a shared behavioral contract, not a reference framework.
Each official adapter should provide a harness with operations such as:
The same scenarios should run against React, Vue, Svelte, Solid, and Angular.
Required coverage:
findOne()runtime shape and type inferencesetWindow()promisesExecutable invariants should include:
Migration plan
Phase 1: write contract tests
Encode desired behavior before moving adapter code.
Include known regressions from the issue cluster:
findOne()cardinalitysetWindow()promisesPhase 2: introduce internal observer and normalized input contract
Add:
Phase 3: migrate all five adapters
Migrate React, Vue, Svelte, Solid, and Angular to consume the observer while preserving public APIs where practical.
Remove duplicated lifecycle/state-machine code from adapters. Keep renderer-native materializers.
Phase 4: add ordered snapshot/layout contract
Support order-only moves and layout revisions directly. Ensure
revision,layoutRevision,keys, anddataupdate for all visible membership, ordering, and window changes. This is a correctness milestone, not a patch-stream optimization milestone.Use this to improve:
Phase 5: extract infinite/window controller
Move pagination/window ownership out of React and into a shared internal controller.
Finish Vue and Svelte infinite-query support on top of shared semantics.
Phase 6: finalize SSR/client hydration
Integrate with the #1564 direction or with a revised maintainer decision.
Add provider/context/DI wrappers across adapters and SSR e2e coverage.
Phase 7: docs and feature matrix
Document shared semantics once. Framework docs should focus on native usage examples.
Add a feature matrix covering:
useLiveQueryTreatment of current open work
$key, proxy preservation, lazystate, and avoiding unnecessary full resets.Patch model deferral
A patch stream is not required for correctness in the first milestone. UI-facing live queries usually have small active windows, and adapters already render from snapshots or snapshot-like materializations today. A canonical snapshot with
revision,layoutRevision,keys, anddatais enough to fix the semantic bugs, including order-only moves, as long as every visible layout change publishes a new snapshot.The remaining requirement is that core detects visible layout changes. For example, when an
orderByfield changes but the projected row value is unchanged, the observer must still publish a new snapshot with an incrementedlayoutRevisionand updated order. It does not need to expose a{ type: "move" }patch initially.An ordered patch stream can be revisited later if benchmarks show snapshot replacement is a bottleneck, or if fine-grained APIs need efficient row/key-level change descriptions. That later design can build on the same revision and layout-revision contract without blocking the observer, adapter migration, infinite-query controller, or SSR work.
Risks and mitigations
Risk: the RFC is too broad
Mitigation: treat this as one platform RFC with staged implementation. The first implementation milestone is the observer contract plus conformance tests.
Risk: internal observer leaks as public API
Mitigation: mark it internal/unstable and document that it is for official adapters only. Avoid public adapter-author documentation.
Consideration: centralized snapshots should not preserve today's avoidable costs
Adapters already generate snapshots or snapshot-like materializations today. React memoizes a hook-local external-store snapshot and captures
Array.from(collection.entries())for each returned result revision. Vue, Svelte, and Solid also maintain framework-local ordered data and keyed state derived from collection contents. Query snapshot cost is not expected to be a primary risk because UI-facing live queries usually have small active windows.The design should still avoid carrying forward unnecessary duplication. The central observer snapshot should be cheaper and more canonical than today's per-adapter snapshots by using stable identity for unchanged revisions, structural sharing for
dataandkeys, layout revisions, row revisions, and lazystate/map compatibility views.Risk: SSR decisions block adapter fixes
Mitigation: separate the observer/conformance milestone from final SSR payload policy. Keep
DbClientas the assumed owner while leaving hydration details open for maintainer review.Risk: adapters lose framework idioms
Mitigation: share semantics, not renderer state. Each adapter keeps native scheduling/materialization.
Risk: migration breaks public APIs
Mitigation: preserve public overloads initially and normalize internally. Deprecations can come later after conformance is in place.
Alternatives considered
Observer contract only
A narrower RFC could define only
InternalLiveQueryObserverand migrate adapters to it.This is the best first implementation milestone, but too narrow as an RFC because observer decisions affect ordered changes, infinite queries, SSR snapshots, and conformance.
DbClient/SSR first
A narrower RFC could center on
DbClient, identity, and hydration.This aligns with current momentum but does not fix the duplicated adapter state machines or cross-framework behavior drift.
React as reference implementation
React could continue as the de facto source of truth, with other adapters porting its logic.
This has already produced drift and framework-shaped bugs. React's external-store model is not the right model for every renderer.
Universal renderer state
Core could expose one mutable
Mapand one ordered array for all adapters.This would simplify code superficially but would fight framework-native reactivity and likely reproduce Solid/Vue/Svelte performance and lifecycle issues in a different form.
Recommendation
Adopt the full binding platform design as the architectural direction.
Implement it in phases:
The decisive shift is from “port hooks from React” to “implement one internal live-query protocol and several native renderers.” Pagination, SSR, query cardinality, disabled semantics, stale async behavior, and lifecycle correctness can then be fixed once while keeping each framework adapter idiomatic.