Conversation
🦋 Changeset detectedLatest commit: 1187bd5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
`reexecuteOperation` blocks a reexecute of an operation that is still in flight, but the blocking branch also clears that operation's `dispatched` flag. The next reexecute during the same request therefore passes `nextOperation`'s deduplication check and a second network request goes out. The flag only has to be cleared when the reexecute replaced an operation already waiting in the queue, so the queued operation is not deduplicated as the queue drains. The stale-result path in `onPush` still unblocks an operation whose result came back partial or optimistic.
79d462f to
1187bd5
Compare
|
Hey maintainers. As you can see from the description I indeed used Claude for this PR. But happy to discuss without involving Claude too. It's a very small fix for something we've ran into internally, where a burst of web socket events kept invalidating a slow query and we'd end up with several copies of it in flight at once instead of one. |
|
Friendly ping @JoviDeCroock |
|
I did a small research but this reintroduces an old bug. I have little time atm so I can elaborate further next week, you can have claude figure it out by saying 'look at the historical diff of your changes, they relate to graphcache loop bugs' |
|
Hah, thank you! :) That's useful context. No rush on this btw I have a workaround deployed already on our side of things. |
|
I actually couldn't find how this re-introduces the old bug so parking this for a bit. And again, no rush but you did get me curious now... |
|
Had a look, this brings back #3254.
The Repro with a real client + Graphcache: CI doesn't catch it because the Graphcache test from #3363 mocks core:
|
|
Thanks for that write-up and evidence. But it sounds like we'll need to make changes both here and in the graphcache-exchange to solve this then? I admit I'm a little out of my depth here in my urql knowledge and I'd rather not just slop Claude at the problem now that it's not so simple anymore. Wdyt? Worth solving? For me personally I already have a workaround deployed so I'm in no rush. |
Summary
client.reexecuteOperationblocks a reexecute of an operation that is still in flight (#3573, fixing #3565), but the blockingelsebranch also clears that operation'sdispatchedflag. The next reexecute during the same request therefore passesnextOperation's deduplication check, and a second network request goes out. In effect every second reexecute during one flight gets through.We hit this with Graphcache, which reexecutes an active query on every dependency change. Under a stream of subscription events, every second event started another copy of a slow list query, so one page held several concurrent copies of the same request.
Repro against
@urql/corealone — one slow exchange, no cache exchange — counting the operations that reach the exchange while a singlecache-firstquery is in flight. Verified against a build of currentmain:mainnetwork-onlyreexecute during a flightThe last four rows are the behaviours
unblocks stale operationsandblocks reexecuting operations that are in-flightalready cover, and both stay green.Set of changes
packages/core/src/client.ts— the flag only has to be cleared when the reexecute replaced an operation already waiting in the queue, so that the queued operation is not deduplicated as the queue drains. An in-flight operation keeps its flag.packages/core/src/client.test.ts— new testblocks repeated reexecutes of operations that are in-flight, placed afterblocks reexecuting operations that are in-flight: threereexecuteOperationcalls while the first request is pending must not dispatch again. It fails onmainwith 2 dispatches and passes with the change.Plus a patch changeset for
@urql/core. Only@urql/coreis affected;pnpm test,pnpm run checkandpnpm run lintare green across the monorepo.Why the flag was cleared, and why the stale path covers it
The clearing was introduced by #3363 to unblock an operation stalled after an optimistic mutation (#3254). Since then that case is handled by the stale-result path in
onPush—result.stale && !result.hasNextis "an optimistic mutation or a partial result" and clears the key — which is what the existingunblocks stale operationstest exercises. That leaves the queue case as the only reason to clear the flag inreexecuteOperation, and thequeuedcheck keeps it.One question for you
The unconditional clear was also a hedge for an operation that an exchange swallows without emitting any result — for example Graphcache dropping a
cache-firstmiss that is blocked by an optimistic update, or one whose key is in itsreexecutingOperationsguard. With this change such an operation stays blocked until its teardown, whereas before it would have been let through on the next reexecute.I could not construct that shape without the stale emission that
onPushalready handles, but you know the exchanges far better than I do. If it is reachable, this needs a release valve rather than the plainqueuedcheck, and I am happy to rework it.