fix(query): refetch on remount after mutation while unmounted [#2986]#2988
Merged
viniciusdacal merged 1 commit intomainfrom Apr 23, 2026
Merged
fix(query): refetch on remount after mutation while unmounted [#2986]#2988viniciusdacal merged 1 commit intomainfrom
viniciusdacal merged 1 commit intomainfrom
Conversation
Closes #2986. When a user navigated list → form → back via `router.navigate()` after creating an entity, the list kept showing the pre-mutation data until a full reload. The list query had unsubscribed from `MutationEventBus` on navigate-away, so the `emit()` fired by the form's mutation reached no live listener — yet the cached entry and its query indices remained and were served on remount. Fix: - `MutationEventBus` tracks a monotonic per-entity-type version that increments on every `emit()`; `getVersion(entityType)` exposes it. - `MemoryCache.set(key, value, version?)` records the stamp; `CacheStore<T>` gained an optional `getVersion(key)` accessor. - `query()` stamps each cache write with the current bus version for its entity type and skips cache hits whose stamp is older than the bus version — falling through to a fresh fetch. SSR-hydrated entries are stamped `0` so a client emit that lands before hydration completes correctly marks the SSR payload stale on remount. - Nav-prefetch cache-hit paths (init-time and effect-time) also honor the staleness check, using the descriptor's `_entity` when the query's `entityMeta` hasn't been assigned yet. Unstamped entries are treated as version 0 so custom `CacheStore` implementations without `getVersion` regress to the previous behavior rather than breaking the build. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
Closes #2986.
Summary
MutationEventBusnow tracks a monotonic per-entity-type version that increments on everyemit();getVersion(entityType)exposes it.MemoryCache.set(key, value, version?)records the stamp;CacheStore<T>gained an optionalgetVersion(key).query()stamps every cache write with the bus version for its entity type and skips cache hits whose stamp is older than the current bus version. SSR-hydrated entries are explicitly stamped0so a client emit that lands before hydration completes correctly marks the SSR payload stale on remount. Nav-prefetch cache-hit paths (init-time and effect-time) honor the same check, using the descriptor's_entitywhen the query'sentityMetahasn't been assigned yet.Why
Navigating list → form → back via
router.navigate()left the list showing pre-mutation data until a full page reload. The list query had unsubscribed from the bus on navigate-away, so theemit()fired by the form's mutation reached no live listener — yet the cached entry and its query indices remained and were served on remount.emit()only notified live subscribers; it didn't touch the cache.Public API Changes
MutationEventBus.getVersion(entityType: string): number— new.CacheStore<T>.set(key, value, version?)— added optionalversionparameter (backward compatible).CacheStore<T>.getVersion(key): number | undefined— new optional method. Custom cache implementations without it keep the previous behavior.No breaking changes. Entity-backed queries automatically participate. Non-entity queries are unaffected.
Test plan
vtz test— 2481 tests pass in@vertz/uivtz test— 1285 tests pass in@vertz/ui-servervtz run typecheck— cleanvtzx oxlint— no new warnings (16 pre-existing, 16 after)vtzx oxfmt --check— cleanpackages/ui/src/query/__tests__/query.test.ts:packages/ui/src/query/__tests__/cache.test.ts(version stamp: set/get/delete/clear/LRU/v0)packages/ui/src/store/__tests__/mutation-event-bus.test.ts(getVersion: initial state, increments per type, reset on clear)Pre-existing out-of-scope note
The effect calls
raw._fetch()eagerly on every run before the cache-hit check is evaluated. On a cache hit the resulting promise is suppressed via.catch, but the fetch call itself has already started. Worth revisiting separately — unrelated to this fix.