Skip to content

Core Data: Discard entity list responses superseded by a newer request - #81886

Draft
adamsilverstein wants to merge 3 commits into
fix/image-resolution-stale-attachment-recordfrom
fix/entity-records-stale-list-response
Draft

adamsilverstein wants to merge 3 commits into
fix/image-resolution-stale-attachment-recordfrom
fix/entity-records-stale-list-response

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Aug 20, 2026 •

Copy link
Copy Markdown
Member

Test this PR in WordPress Playground

Claude took on the follow-up it flagged in #81846, fix and tests below:

Fixes #81885

Note

Stacked on #81846, which must land first - only the last two commits here are new.

What?

getEntityRecords now discards a list response once a newer response for the same query has already been received - the list counterpart of the record guard added in #81846.

Why?

invalidateResolution starts a second request for the same list while the first may still be in flight, and the store keeps whichever response is delivered last. A list read from the database before an update can therefore replace the refetched one that already reflects it - and nothing invalidates it afterwards.

Batch uploads are where this bites. Uploading several files at once invalidates the attachment list once per file, so several list refetches are in flight together, and the odds of one arriving out of order grow with every extra file. A stale one delivered late makes an already-uploaded image vanish from the inserter's Media tab.

The damage also reaches past the list itself. The resolver writes every record it received through receiveEntityRecords and ends by calling finishResolutions( 'getEntityRecord', ... ) for each of them, so a stale list response can overwrite fresher individual records and mark their resolutions finished. That write never goes through the getEntityRecord resolver, so the per-record guard from #81846 cannot catch it.

As with #81846: Playground serializes PHP requests, so responses cannot arrive out of order there - this only reproduces on real installations.

How?

The same mechanism as #81846 - reserve a sequence number before the request goes out, and drop the response once a newer one for the same query has been received. getEntityRecords needed three decisions on top of that:

  • One guard covers all three fetch branches. The paginated, progressive, and plain branches all funnel into a single check placed before the store writes, so the trailing finishResolutions and receiveUserPermissions calls are covered along with the list write.
  • A superseded progressive run stops mid-loop. Progressive loading dispatches receiveEntityRecords per page, so each page checks the guard before delivering. Pages a superseded run already delivered were overwritten by the newer response's deliveries, so stopping is enough.
  • The store lock is still released on every discarded path.

The guard keeps the conservative properties from #81846: only responses to an identical query are compared, so a different _fields set or query never discards another one's data, and a newer request that fails discards nothing.

Testing Instructions

All tests were verified to fail without the change and pass with it.

Unit - the ordering guard across the fetch branches:

npm run test:unit -- packages/core-data/src/test/resolvers.js -t "same query"

E2e - the race reproduced deterministically against a real editor. It holds the inserter Media tab's attachment list request at the network layer (reading its stale body immediately), uploads a new image while it is held, lets the post-upload refetch deliver a fresher list, and only then delivers the stale one. The fresher list must survive in the store, and the Media tab must keep showing the new upload:

npm run test:e2e -- test/e2e/specs/editor/various/entity-records-response-order.spec.js

Manual reproduction needs an unlucky response ordering on a busy installation - see the reproduction notes in the issue.

AI Use

Claude Code did the typing here, I did the asking. I will review and test.

getEntityRecords kept whichever list response was delivered last, even
when its body was read from the database before a newer one. A batch
upload invalidates the attachment list once per file, so several list
refetches are in flight together and a stale one delivered late could
replace the fresher list, overwrite the records it contains, and mark
their individual resolutions finished. Reserve a sequence number before
each request goes out - as getEntityRecord already does - and drop a
response once a newer one for the same query has been received,
including the remaining pages of a superseded progressive run.
Hold the inserter Media tab's list request at the network layer, upload
an image while its stale body is in flight, let the post-upload refetch
deliver a fresher list, and only then deliver the stale one. The fresher
list must survive, so the Media tab keeps showing the new upload.
@github-actions github-actions Bot added the [Package] Core data /packages/core-data label Aug 20, 2026
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@adamsilverstein adamsilverstein added the [Type] Bug An existing feature does not function as intended label Aug 20, 2026
@github-actions

Copy link
Copy Markdown

Size Change: +4 B (0%)

Total Size: 7.75 MB

📦 View Changed
Filename Size Change
build/scripts/core-data/index.min.js 37.4 kB +4 B (+0.01%)

compressed-size-action

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Core data /packages/core-data [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant