Skip to content

Commit 9061f73

Browse files
committed
Always take the latest network result when polling
1 parent 48f7386 commit 9061f73

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

src/core/ObservableQuery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,6 +1473,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`,
14731473
!isNetworkRequestInFlight(this.networkStatus) &&
14741474
!this.options.skipPollAttempt?.()
14751475
) {
1476+
this._lastWrite = undefined;
14761477
this._reobserve(
14771478
{
14781479
// Most fetchPolicy options don't make sense to use in a polling context, as

src/core/__tests__/ApolloClient/general.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3118,6 +3118,158 @@ describe("ApolloClient", () => {
31183118
await expect(stream).not.toEmitAnything();
31193119
});
31203120

3121+
it("keeps the clobbered cache value when a polled result matches the last written result", async () => {
3122+
const query = gql`
3123+
query {
3124+
hero {
3125+
id
3126+
name
3127+
}
3128+
}
3129+
`;
3130+
3131+
const client = new ApolloClient({
3132+
cache: new InMemoryCache(),
3133+
link: new MockLink([
3134+
{
3135+
request: { query },
3136+
result: {
3137+
data: { hero: { __typename: "Hero", id: "1", name: "Luke" } },
3138+
},
3139+
delay: 20,
3140+
maxUsageCount: Number.POSITIVE_INFINITY,
3141+
},
3142+
]),
3143+
});
3144+
3145+
const observable = client.watchQuery({ query, pollInterval: 10 });
3146+
const stream = new ObservableStream(observable);
3147+
3148+
await expect(stream).toEmitTypedValue({
3149+
data: undefined,
3150+
dataState: "empty",
3151+
loading: true,
3152+
networkStatus: NetworkStatus.loading,
3153+
partial: true,
3154+
});
3155+
3156+
await expect(stream).toEmitTypedValue({
3157+
data: { hero: { __typename: "Hero", id: "1", name: "Luke" } },
3158+
dataState: "complete",
3159+
loading: false,
3160+
networkStatus: NetworkStatus.ready,
3161+
partial: false,
3162+
});
3163+
3164+
// Another writer clobbers the field this query wrote.
3165+
client.writeQuery({
3166+
query,
3167+
data: { hero: { __typename: "Hero", id: "1", name: "Clobbered" } },
3168+
});
3169+
3170+
await expect(stream).toEmitTypedValue({
3171+
data: { hero: { __typename: "Hero", id: "1", name: "Clobbered" } },
3172+
dataState: "complete",
3173+
loading: false,
3174+
networkStatus: NetworkStatus.ready,
3175+
partial: false,
3176+
});
3177+
3178+
await expect(stream).toEmitTypedValue({
3179+
data: { hero: { __typename: "Hero", id: "1", name: "Clobbered" } },
3180+
dataState: "complete",
3181+
loading: true,
3182+
networkStatus: NetworkStatus.poll,
3183+
partial: false,
3184+
});
3185+
3186+
await expect(stream).toEmitTypedValue({
3187+
data: { hero: { __typename: "Hero", id: "1", name: "Luke" } },
3188+
dataState: "complete",
3189+
loading: false,
3190+
networkStatus: NetworkStatus.ready,
3191+
partial: false,
3192+
});
3193+
3194+
observable.stopPolling();
3195+
3196+
await expect(stream).not.toEmitAnything();
3197+
});
3198+
3199+
it("applies read functions when a polled result matches the last written result", async () => {
3200+
const query = gql`
3201+
query {
3202+
greeting
3203+
}
3204+
`;
3205+
3206+
const client = new ApolloClient({
3207+
cache: new InMemoryCache({
3208+
typePolicies: {
3209+
Query: {
3210+
fields: {
3211+
greeting: {
3212+
read: (existing: string | undefined) =>
3213+
existing && existing.toUpperCase(),
3214+
},
3215+
},
3216+
},
3217+
},
3218+
}),
3219+
link: new MockLink([
3220+
{
3221+
request: { query },
3222+
result: { data: { greeting: "hello" } },
3223+
delay: 20,
3224+
maxUsageCount: Number.POSITIVE_INFINITY,
3225+
},
3226+
]),
3227+
});
3228+
3229+
const observable = client.watchQuery({ query, pollInterval: 10 });
3230+
const stream = new ObservableStream(observable);
3231+
3232+
await expect(stream).toEmitTypedValue({
3233+
data: undefined,
3234+
dataState: "empty",
3235+
loading: true,
3236+
networkStatus: NetworkStatus.loading,
3237+
partial: true,
3238+
});
3239+
3240+
await expect(stream).toEmitTypedValue({
3241+
data: { greeting: "HELLO" },
3242+
dataState: "complete",
3243+
loading: false,
3244+
networkStatus: NetworkStatus.ready,
3245+
partial: false,
3246+
});
3247+
3248+
await expect(stream).toEmitTypedValue({
3249+
data: { greeting: "HELLO" },
3250+
dataState: "complete",
3251+
loading: true,
3252+
networkStatus: NetworkStatus.poll,
3253+
partial: false,
3254+
});
3255+
3256+
// The polled result is `hello` again, identical to what this query last
3257+
// wrote, so the cache write is skipped. The emitted result still comes from
3258+
// the cache, so the `read` function is applied instead of delivering the
3259+
// raw network value.
3260+
await expect(stream).toEmitTypedValue({
3261+
data: { greeting: "HELLO" },
3262+
dataState: "complete",
3263+
loading: false,
3264+
networkStatus: NetworkStatus.ready,
3265+
partial: false,
3266+
});
3267+
3268+
observable.stopPolling();
3269+
3270+
await expect(stream).not.toEmitAnything();
3271+
});
3272+
31213273
it("should not error when replacing unidentified data with a normalized ID", async () => {
31223274
const queryWithoutId = gql`
31233275
query {

0 commit comments

Comments
 (0)