I might be misunderstanding internals here, but we have a product bug and it's looking like it might be an underlying library issue.
It looks as though the reproduction path for us was:
- User runs a query Q1 with operation hash
H1 against store-or-network. Records are persisted; root entry for H1 is persisted with a valid fetchTime.
- In a later release we remove a fragment from that query in a clean-up. This changes the cache key used (call it
H2)
- User then opens the app and this query never runs again. So data is now stuck from before they upgraded to this version.
Looking at the internals, after the cache key is changed and the user opens the page again:
_get_or_create_root_entry finds no existing entry under H2 → creates a new one with fetchTime: null.
- Records reachable from Q1 are still in the in-memory
RecordSource because they are also referenced by other retained operations (or were rehydrated alongside the persisted set). DataChecker.check returns status: 'available'.
Store.prototype.check calls getAvailabilityStatus with operationFetchTime = null and the inner status 'available'.
getAvailabilityStatus falls through every guard and returns { status: 'available', fetchTime: null }.
store-or-network short-circuits, no network request is issued, the user is served whatever records were already in memory.
- Because no network response ever lands,
Store.prototype.notify never fires for H2 → fetchTime stays null → step 5 repeats forever.
This is on v7.0.0 of the library - haven't upgrade to v8 yet. But the code looks the same so I assume the issue is the same.
Possible root cause
getAvailabilityStatus (lib/Store.js, current 8.0.0):
function getAvailabilityStatus(operationAvailability, operationLastWrittenAt, operationFetchTime, queryCacheExpirationTime, staleForExpiration) {
// …invalidation check…
if (status === 'missing') return { status: 'missing' };
if (operationFetchTime != null && queryCacheExpirationTime != null) {
// TTL stale check — only entered when fetchTime is non-null
}
return { status: 'available', fetchTime: operationFetchTime ?? null };
}
When operationFetchTime == null, the TTL-based stale check is skipped and the function falls through to 'available'. There is no path that treats "records present but freshness metadata missing" as stale.
Possible fix
We tested this patch on our bug and it fixed it, but not 100% if I'm introducing a side effect here
if (operationFetchTime != null && queryCacheExpirationTime != null) {
var isStale = operationFetchTime <= Date.now() - queryCacheExpirationTime;
if (isStale && staleForExpiration) {
return { status: 'stale' };
}
}
+ if (operationFetchTime == null) {
+ return { status: 'stale' };
+ }
return { status: 'available', fetchTime: operationFetchTime ?? null };
Does this look like a possible bug, or could there be something wrong on our side?
I might be misunderstanding internals here, but we have a product bug and it's looking like it might be an underlying library issue.
It looks as though the reproduction path for us was:
H1againststore-or-network. Records are persisted; root entry forH1is persisted with a validfetchTime.H2)Looking at the internals, after the cache key is changed and the user opens the page again:
_get_or_create_root_entryfinds no existing entry underH2→ creates a new one withfetchTime: null.RecordSourcebecause they are also referenced by other retained operations (or were rehydrated alongside the persisted set).DataChecker.checkreturnsstatus: 'available'.Store.prototype.checkcallsgetAvailabilityStatuswithoperationFetchTime = nulland the inner status'available'.getAvailabilityStatusfalls through every guard and returns{ status: 'available', fetchTime: null }.store-or-networkshort-circuits, no network request is issued, the user is served whatever records were already in memory.Store.prototype.notifynever fires forH2→fetchTimestaysnull→ step 5 repeats forever.This is on v7.0.0 of the library - haven't upgrade to v8 yet. But the code looks the same so I assume the issue is the same.
Possible root cause
getAvailabilityStatus(lib/Store.js, current8.0.0):When
operationFetchTime == null, the TTL-based stale check is skipped and the function falls through to'available'. There is no path that treats "records present but freshness metadata missing" as stale.Possible fix
We tested this patch on our bug and it fixed it, but not 100% if I'm introducing a side effect here
if (operationFetchTime != null && queryCacheExpirationTime != null) { var isStale = operationFetchTime <= Date.now() - queryCacheExpirationTime; if (isStale && staleForExpiration) { return { status: 'stale' }; } } + if (operationFetchTime == null) { + return { status: 'stale' }; + } return { status: 'available', fetchTime: operationFetchTime ?? null };Does this look like a possible bug, or could there be something wrong on our side?