Skip to content

feat: Implement BaseDataService#8039

Open
FrederikBolding wants to merge 39 commits intomainfrom
fb/data-service-base
Open

feat: Implement BaseDataService#8039
FrederikBolding wants to merge 39 commits intomainfrom
fb/data-service-base

Conversation

@FrederikBolding
Copy link
Member

@FrederikBolding FrederikBolding commented Feb 25, 2026

Explanation

This PR implements BaseDataService and a function to wrap QueryClient to proxy requests accordingly.

The BaseDataService, similarly to the BaseController provides the framework for building a service that can be registered and accessed via the messenger system, but also provides guarantees about per-request deduping, retries, caching, invalidation, state-while-revalidate etc via @tanstack/query-core.

The BaseDataService provides two utilities for this: fetchQuery and fetchInfiniteQuery, which is similar but one is separated for special pagination behaviour. Each service has its own cache for the APIs that it exposes that must also be synchronized with the UI processes. To facilitate this synchronization, the BaseDataService also automatically provides a cacheUpdate event.

The overall goal of the PR is to provide a base layer that can keep as much compatibility as possible with native TanStack Query while also simultaneously allowing us to have one source of truth per data service.

The synchronization is achieved via a special QueryClient created by createUIQueryClient, which wraps functionality such as cache invalidation, provides the default proxied fetch behaviour and subscribes to cache updates from data services that it is observing (e.g. has active queries for).

References

https://consensyssoftware.atlassian.net/browse/WPC-445

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Introduces a new caching/messaging abstraction built on TanStack Query that affects query invalidation, pagination, and cross-process cache synchronization; issues here could cause stale/incorrect UI data or missed invalidations.

Overview
Adds initial implementation of @metamask/base-data-service, introducing BaseDataService (TanStack Query-backed query/infinite-query helpers plus a messenger :invalidateQueries action and :cacheUpdated/granular cache update events).

Adds createUIQueryClient to proxy UI QueryClient fetches through messenger actions, auto-subscribe/unsubscribe to per-query cache updates (hydrating local cache), and override invalidateQueries to also invalidate the backing data services.

Exports the new APIs from src/index.ts, adds typed wrapper hooks (useQuery, useInfiniteQuery), comprehensive Jest/Nock tests with an ExampleDataService, and updates package deps/TS project refs/changelog (including a Yarn constraints exception for @tanstack/query-core).

Written by Cursor Bugbot for commit 7f1fe7d. This will update automatically on new commits. Configure here.

@socket-security
Copy link

socket-security bot commented Feb 25, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​tanstack/​react-query@​4.43.01001008996100

View full report

Base automatically changed from fb/init-base-data-service to main February 25, 2026 15:39
@FrederikBolding FrederikBolding force-pushed the fb/data-service-base branch 3 times, most recently from 1b629b1 to c19b3d7 Compare February 26, 2026 18:36
@FrederikBolding
Copy link
Member Author

@metamaskbot publish-previews

@github-actions
Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "4.1.1-preview-ee1fa5d01",
  "@metamask-previews/accounts-controller": "36.0.1-preview-ee1fa5d01",
  "@metamask-previews/address-book-controller": "7.0.1-preview-ee1fa5d01",
  "@metamask-previews/ai-controllers": "0.1.0-preview-ee1fa5d01",
  "@metamask-previews/analytics-controller": "1.0.0-preview-ee1fa5d01",
  "@metamask-previews/analytics-data-regulation-controller": "0.0.0-preview-ee1fa5d01",
  "@metamask-previews/announcement-controller": "8.0.0-preview-ee1fa5d01",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-ee1fa5d01",
  "@metamask-previews/approval-controller": "8.0.0-preview-ee1fa5d01",
  "@metamask-previews/assets-controller": "2.1.0-preview-ee1fa5d01",
  "@metamask-previews/assets-controllers": "100.0.3-preview-ee1fa5d01",
  "@metamask-previews/base-controller": "9.0.0-preview-ee1fa5d01",
  "@metamask-previews/base-data-service": "0.0.0-preview-ee1fa5d01",
  "@metamask-previews/bridge-controller": "67.3.0-preview-ee1fa5d01",
  "@metamask-previews/bridge-status-controller": "67.0.1-preview-ee1fa5d01",
  "@metamask-previews/build-utils": "3.0.4-preview-ee1fa5d01",
  "@metamask-previews/chain-agnostic-permission": "1.4.0-preview-ee1fa5d01",
  "@metamask-previews/claims-controller": "0.4.2-preview-ee1fa5d01",
  "@metamask-previews/client-controller": "1.0.0-preview-ee1fa5d01",
  "@metamask-previews/compliance-controller": "1.0.1-preview-ee1fa5d01",
  "@metamask-previews/composable-controller": "12.0.0-preview-ee1fa5d01",
  "@metamask-previews/connectivity-controller": "0.1.0-preview-ee1fa5d01",
  "@metamask-previews/controller-utils": "11.19.0-preview-ee1fa5d01",
  "@metamask-previews/core-backend": "6.0.0-preview-ee1fa5d01",
  "@metamask-previews/delegation-controller": "2.0.1-preview-ee1fa5d01",
  "@metamask-previews/earn-controller": "11.1.1-preview-ee1fa5d01",
  "@metamask-previews/eip-5792-middleware": "2.1.0-preview-ee1fa5d01",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-ee1fa5d01",
  "@metamask-previews/eip1193-permission-middleware": "1.0.3-preview-ee1fa5d01",
  "@metamask-previews/ens-controller": "19.0.3-preview-ee1fa5d01",
  "@metamask-previews/error-reporting-service": "3.0.1-preview-ee1fa5d01",
  "@metamask-previews/eth-block-tracker": "15.0.1-preview-ee1fa5d01",
  "@metamask-previews/eth-json-rpc-middleware": "23.1.0-preview-ee1fa5d01",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-ee1fa5d01",
  "@metamask-previews/foundryup": "1.0.1-preview-ee1fa5d01",
  "@metamask-previews/gas-fee-controller": "26.0.3-preview-ee1fa5d01",
  "@metamask-previews/gator-permissions-controller": "2.0.0-preview-ee1fa5d01",
  "@metamask-previews/json-rpc-engine": "10.2.2-preview-ee1fa5d01",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-ee1fa5d01",
  "@metamask-previews/keyring-controller": "25.1.0-preview-ee1fa5d01",
  "@metamask-previews/logging-controller": "7.0.1-preview-ee1fa5d01",
  "@metamask-previews/message-manager": "14.1.0-preview-ee1fa5d01",
  "@metamask-previews/messenger": "0.3.0-preview-ee1fa5d01",
  "@metamask-previews/multichain-account-service": "7.0.0-preview-ee1fa5d01",
  "@metamask-previews/multichain-api-middleware": "1.2.7-preview-ee1fa5d01",
  "@metamask-previews/multichain-network-controller": "3.0.4-preview-ee1fa5d01",
  "@metamask-previews/multichain-transactions-controller": "7.0.1-preview-ee1fa5d01",
  "@metamask-previews/name-controller": "9.0.0-preview-ee1fa5d01",
  "@metamask-previews/network-controller": "30.0.0-preview-ee1fa5d01",
  "@metamask-previews/network-enablement-controller": "4.1.2-preview-ee1fa5d01",
  "@metamask-previews/notification-services-controller": "22.0.0-preview-ee1fa5d01",
  "@metamask-previews/permission-controller": "12.2.0-preview-ee1fa5d01",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-ee1fa5d01",
  "@metamask-previews/perps-controller": "0.0.0-preview-ee1fa5d01",
  "@metamask-previews/phishing-controller": "16.3.0-preview-ee1fa5d01",
  "@metamask-previews/polling-controller": "16.0.3-preview-ee1fa5d01",
  "@metamask-previews/preferences-controller": "22.1.0-preview-ee1fa5d01",
  "@metamask-previews/profile-metrics-controller": "3.0.1-preview-ee1fa5d01",
  "@metamask-previews/profile-sync-controller": "27.1.0-preview-ee1fa5d01",
  "@metamask-previews/ramps-controller": "10.0.0-preview-ee1fa5d01",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-ee1fa5d01",
  "@metamask-previews/remote-feature-flag-controller": "4.1.0-preview-ee1fa5d01",
  "@metamask-previews/sample-controllers": "4.0.3-preview-ee1fa5d01",
  "@metamask-previews/seedless-onboarding-controller": "8.1.0-preview-ee1fa5d01",
  "@metamask-previews/selected-network-controller": "26.0.3-preview-ee1fa5d01",
  "@metamask-previews/shield-controller": "5.0.1-preview-ee1fa5d01",
  "@metamask-previews/signature-controller": "39.0.4-preview-ee1fa5d01",
  "@metamask-previews/storage-service": "1.0.0-preview-ee1fa5d01",
  "@metamask-previews/subscription-controller": "6.0.0-preview-ee1fa5d01",
  "@metamask-previews/transaction-controller": "62.19.0-preview-ee1fa5d01",
  "@metamask-previews/transaction-pay-controller": "16.1.0-preview-ee1fa5d01",
  "@metamask-previews/user-operation-controller": "41.0.3-preview-ee1fa5d01"
}

@FrederikBolding
Copy link
Member Author

@metamaskbot publish-previews

@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "4.1.1-preview-5111712",
  "@metamask-previews/accounts-controller": "36.0.1-preview-5111712",
  "@metamask-previews/address-book-controller": "7.0.1-preview-5111712",
  "@metamask-previews/ai-controllers": "0.1.0-preview-5111712",
  "@metamask-previews/analytics-controller": "1.0.0-preview-5111712",
  "@metamask-previews/analytics-data-regulation-controller": "0.0.0-preview-5111712",
  "@metamask-previews/announcement-controller": "8.0.0-preview-5111712",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-5111712",
  "@metamask-previews/approval-controller": "8.0.0-preview-5111712",
  "@metamask-previews/assets-controller": "2.2.0-preview-5111712",
  "@metamask-previews/assets-controllers": "100.0.3-preview-5111712",
  "@metamask-previews/base-controller": "9.0.0-preview-5111712",
  "@metamask-previews/base-data-service": "0.0.0-preview-5111712",
  "@metamask-previews/bridge-controller": "67.4.0-preview-5111712",
  "@metamask-previews/bridge-status-controller": "67.0.1-preview-5111712",
  "@metamask-previews/build-utils": "3.0.4-preview-5111712",
  "@metamask-previews/chain-agnostic-permission": "1.4.0-preview-5111712",
  "@metamask-previews/claims-controller": "0.4.2-preview-5111712",
  "@metamask-previews/client-controller": "1.0.0-preview-5111712",
  "@metamask-previews/compliance-controller": "1.0.1-preview-5111712",
  "@metamask-previews/composable-controller": "12.0.0-preview-5111712",
  "@metamask-previews/connectivity-controller": "0.1.0-preview-5111712",
  "@metamask-previews/controller-utils": "11.19.0-preview-5111712",
  "@metamask-previews/core-backend": "6.0.0-preview-5111712",
  "@metamask-previews/delegation-controller": "2.0.1-preview-5111712",
  "@metamask-previews/earn-controller": "11.1.1-preview-5111712",
  "@metamask-previews/eip-5792-middleware": "3.0.0-preview-5111712",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-5111712",
  "@metamask-previews/eip1193-permission-middleware": "1.0.3-preview-5111712",
  "@metamask-previews/ens-controller": "19.0.3-preview-5111712",
  "@metamask-previews/error-reporting-service": "3.0.1-preview-5111712",
  "@metamask-previews/eth-block-tracker": "15.0.1-preview-5111712",
  "@metamask-previews/eth-json-rpc-middleware": "23.1.0-preview-5111712",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-5111712",
  "@metamask-previews/foundryup": "1.0.1-preview-5111712",
  "@metamask-previews/gas-fee-controller": "26.0.3-preview-5111712",
  "@metamask-previews/gator-permissions-controller": "2.0.0-preview-5111712",
  "@metamask-previews/geolocation-controller": "0.0.0-preview-5111712",
  "@metamask-previews/json-rpc-engine": "10.2.3-preview-5111712",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-5111712",
  "@metamask-previews/keyring-controller": "25.1.0-preview-5111712",
  "@metamask-previews/logging-controller": "7.0.1-preview-5111712",
  "@metamask-previews/message-manager": "14.1.0-preview-5111712",
  "@metamask-previews/messenger": "0.3.0-preview-5111712",
  "@metamask-previews/multichain-account-service": "7.0.0-preview-5111712",
  "@metamask-previews/multichain-api-middleware": "1.2.7-preview-5111712",
  "@metamask-previews/multichain-network-controller": "3.0.4-preview-5111712",
  "@metamask-previews/multichain-transactions-controller": "7.0.1-preview-5111712",
  "@metamask-previews/name-controller": "9.0.0-preview-5111712",
  "@metamask-previews/network-controller": "30.0.0-preview-5111712",
  "@metamask-previews/network-enablement-controller": "4.1.2-preview-5111712",
  "@metamask-previews/notification-services-controller": "22.0.0-preview-5111712",
  "@metamask-previews/permission-controller": "12.2.0-preview-5111712",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-5111712",
  "@metamask-previews/perps-controller": "0.0.0-preview-5111712",
  "@metamask-previews/phishing-controller": "16.3.0-preview-5111712",
  "@metamask-previews/polling-controller": "16.0.3-preview-5111712",
  "@metamask-previews/preferences-controller": "22.1.0-preview-5111712",
  "@metamask-previews/profile-metrics-controller": "3.0.1-preview-5111712",
  "@metamask-previews/profile-sync-controller": "27.1.0-preview-5111712",
  "@metamask-previews/ramps-controller": "10.0.0-preview-5111712",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-5111712",
  "@metamask-previews/remote-feature-flag-controller": "4.1.0-preview-5111712",
  "@metamask-previews/sample-controllers": "4.0.3-preview-5111712",
  "@metamask-previews/seedless-onboarding-controller": "8.1.0-preview-5111712",
  "@metamask-previews/selected-network-controller": "26.0.3-preview-5111712",
  "@metamask-previews/shield-controller": "5.0.1-preview-5111712",
  "@metamask-previews/signature-controller": "39.0.4-preview-5111712",
  "@metamask-previews/storage-service": "1.0.0-preview-5111712",
  "@metamask-previews/subscription-controller": "6.0.0-preview-5111712",
  "@metamask-previews/transaction-controller": "62.19.0-preview-5111712",
  "@metamask-previews/transaction-pay-controller": "16.1.2-preview-5111712",
  "@metamask-previews/user-operation-controller": "41.0.3-preview-5111712"
}

@FrederikBolding
Copy link
Member Author

@metamaskbot publish-previews

@FrederikBolding
Copy link
Member Author

@metamaskbot publish-previews

@github-actions
Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "5.0.0-preview-00245ea",
  "@metamask-previews/accounts-controller": "37.0.0-preview-00245ea",
  "@metamask-previews/address-book-controller": "7.0.1-preview-00245ea",
  "@metamask-previews/ai-controllers": "0.2.0-preview-00245ea",
  "@metamask-previews/analytics-controller": "1.0.0-preview-00245ea",
  "@metamask-previews/analytics-data-regulation-controller": "0.0.0-preview-00245ea",
  "@metamask-previews/announcement-controller": "8.0.0-preview-00245ea",
  "@metamask-previews/app-metadata-controller": "2.0.0-preview-00245ea",
  "@metamask-previews/approval-controller": "8.0.0-preview-00245ea",
  "@metamask-previews/assets-controller": "2.3.0-preview-00245ea",
  "@metamask-previews/assets-controllers": "100.2.0-preview-00245ea",
  "@metamask-previews/base-controller": "9.0.0-preview-00245ea",
  "@metamask-previews/base-data-service": "0.0.0-preview-00245ea",
  "@metamask-previews/bridge-controller": "69.0.0-preview-00245ea",
  "@metamask-previews/bridge-status-controller": "68.0.1-preview-00245ea",
  "@metamask-previews/build-utils": "3.0.4-preview-00245ea",
  "@metamask-previews/chain-agnostic-permission": "1.4.0-preview-00245ea",
  "@metamask-previews/claims-controller": "0.4.2-preview-00245ea",
  "@metamask-previews/client-controller": "1.0.0-preview-00245ea",
  "@metamask-previews/compliance-controller": "1.0.1-preview-00245ea",
  "@metamask-previews/composable-controller": "12.0.0-preview-00245ea",
  "@metamask-previews/config-registry-controller": "0.1.0-preview-00245ea",
  "@metamask-previews/connectivity-controller": "0.1.0-preview-00245ea",
  "@metamask-previews/controller-utils": "11.19.0-preview-00245ea",
  "@metamask-previews/core-backend": "6.1.0-preview-00245ea",
  "@metamask-previews/delegation-controller": "2.0.2-preview-00245ea",
  "@metamask-previews/earn-controller": "11.1.2-preview-00245ea",
  "@metamask-previews/eip-5792-middleware": "3.0.0-preview-00245ea",
  "@metamask-previews/eip-7702-internal-rpc-middleware": "0.1.0-preview-00245ea",
  "@metamask-previews/eip1193-permission-middleware": "1.0.3-preview-00245ea",
  "@metamask-previews/ens-controller": "19.0.3-preview-00245ea",
  "@metamask-previews/error-reporting-service": "3.0.1-preview-00245ea",
  "@metamask-previews/eth-block-tracker": "15.0.1-preview-00245ea",
  "@metamask-previews/eth-json-rpc-middleware": "23.1.0-preview-00245ea",
  "@metamask-previews/eth-json-rpc-provider": "6.0.0-preview-00245ea",
  "@metamask-previews/foundryup": "1.0.1-preview-00245ea",
  "@metamask-previews/gas-fee-controller": "26.0.3-preview-00245ea",
  "@metamask-previews/gator-permissions-controller": "2.1.0-preview-00245ea",
  "@metamask-previews/geolocation-controller": "0.1.1-preview-00245ea",
  "@metamask-previews/json-rpc-engine": "10.2.3-preview-00245ea",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.8-preview-00245ea",
  "@metamask-previews/keyring-controller": "25.1.0-preview-00245ea",
  "@metamask-previews/logging-controller": "7.0.1-preview-00245ea",
  "@metamask-previews/message-manager": "14.1.0-preview-00245ea",
  "@metamask-previews/messenger": "0.3.0-preview-00245ea",
  "@metamask-previews/multichain-account-service": "7.1.0-preview-00245ea",
  "@metamask-previews/multichain-api-middleware": "1.2.7-preview-00245ea",
  "@metamask-previews/multichain-network-controller": "3.0.5-preview-00245ea",
  "@metamask-previews/multichain-transactions-controller": "7.0.2-preview-00245ea",
  "@metamask-previews/name-controller": "9.0.0-preview-00245ea",
  "@metamask-previews/network-controller": "30.0.0-preview-00245ea",
  "@metamask-previews/network-enablement-controller": "4.2.0-preview-00245ea",
  "@metamask-previews/notification-services-controller": "22.0.0-preview-00245ea",
  "@metamask-previews/permission-controller": "12.2.0-preview-00245ea",
  "@metamask-previews/permission-log-controller": "5.0.0-preview-00245ea",
  "@metamask-previews/perps-controller": "1.0.0-preview-00245ea",
  "@metamask-previews/phishing-controller": "16.3.0-preview-00245ea",
  "@metamask-previews/polling-controller": "16.0.3-preview-00245ea",
  "@metamask-previews/preferences-controller": "23.0.0-preview-00245ea",
  "@metamask-previews/profile-metrics-controller": "3.0.2-preview-00245ea",
  "@metamask-previews/profile-sync-controller": "27.1.0-preview-00245ea",
  "@metamask-previews/ramps-controller": "10.2.0-preview-00245ea",
  "@metamask-previews/rate-limit-controller": "7.0.0-preview-00245ea",
  "@metamask-previews/remote-feature-flag-controller": "4.1.0-preview-00245ea",
  "@metamask-previews/sample-controllers": "4.0.3-preview-00245ea",
  "@metamask-previews/seedless-onboarding-controller": "8.1.0-preview-00245ea",
  "@metamask-previews/selected-network-controller": "26.0.3-preview-00245ea",
  "@metamask-previews/shield-controller": "5.0.1-preview-00245ea",
  "@metamask-previews/signature-controller": "39.0.5-preview-00245ea",
  "@metamask-previews/storage-service": "1.0.0-preview-00245ea",
  "@metamask-previews/subscription-controller": "6.0.0-preview-00245ea",
  "@metamask-previews/transaction-controller": "62.21.0-preview-00245ea",
  "@metamask-previews/transaction-pay-controller": "16.4.1-preview-00245ea",
  "@metamask-previews/user-operation-controller": "41.0.3-preview-00245ea"
}

@FrederikBolding FrederikBolding marked this pull request as ready for review March 10, 2026 15:30
@FrederikBolding FrederikBolding requested a review from a team as a code owner March 10, 2026 15:30
*/
const ALLOWED_INCONSISTENT_DEPENDENCIES = {
// '@metamask/json-rpc-engine': ['^9.0.3'],
'@tanstack/query-core': ['^4.43.0'],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core-backend will need to downgrade to use BaseDataService or wait until we are able to bump everywhere (blocked by extension not supporting React 18).

export default function greeter(name: string): string {
return `Hello, ${name}!`;
}
export * from './BaseDataService';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on not using export * and naming each export explicitly? We are considering banning these entirely, see here for more: MetaMask/eslint-config#331

"@metamask/messenger": "^0.3.0",
"@metamask/utils": "^11.9.0",
"@tanstack/query-core": "^4.43.0",
"@tanstack/react-query": "^4.43.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like react-query has a peer dependency on React 18.x or 19.x: https://github.com/TanStack/query/blob/67cf8b60d923ad158fdf89c80f86decea073f472/packages/react-query/package.json#L84. Do we want to declare this peer dependency here as well?

// We provide re-exports of the underlying TanStack Query hooks with narrower types,
// removing `staleTime` and `queryFn` which aren't useful when using data services.

export function useQuery<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these hooks go in this package? Should we split this and createUIQueryClient off into their own UI-specific packages?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on the previous commit:

The advantage I can see for keeping UI-related code in this package is that all of the data service foundational stuff is in one place (although in that case perhaps we should rename the package to data-services to account for the scope?). The disadvantage is that the React dependency may not be completely obvious (and traditionally we have tried to keep stack-agnostic isolated, especially in core).

If we created a new package, here are some naming ideas:

  • @metamask/data-service-query-react
  • @metamask/data-query-react
  • @metamask/react-query-wrappers

@mcmire
Copy link
Contributor

mcmire commented Mar 10, 2026

I know that TanStack Query is the motivation for this PR, but before we merge this I want to make sure that we've (at least briefly) considered how this new package would overlap with the other "data service layer" features which are already implemented and which we've discussed adding in the future. Namely:

  • Do we still want to encourage teams to use a Cockatiel policy when making HTTP requests? If so, would it be within the domain of this new package to make it easier for teams to do this? (For instance, perhaps the constructor could use createServicePolicy to initialize a policy which could be used when making HTTP requests inside of queryFn's, and perhaps the BaseDataService class could include callbacks for onBreak, onDegraded, etc. Also it might be worth moving createServicePolicy out of controller-utils into this package at some point.) Or, are we essentially deprecating Cockatiel policies in favor of whatever TanStack Query provides?
  • Where do existing patterns around fetching data — polling and websockets — come into play? If we were to provide official solutions for these patterns would they also eventually go into this package or somewhere?

I guess the theme of these questions is that I want to understand what the intended domain of this package is. Should it be only restricted to TanStack Query integration in the future — meaning that we may create other packages to solve other problems later — or should it be designed to encompass other things that are data-service-related in the future?

@Gudahtt
Copy link
Member

Gudahtt commented Mar 10, 2026

I was expecting that we'd continue to use Cockatiel. TanStack does have basic built-in retry functionality, but nothing remotely similar to what our current retry/circuit break policies do.

@Gudahtt
Copy link
Member

Gudahtt commented Mar 10, 2026

Related question: Are there any data services where this query-related functionality would not be useful? i.e. where extending this base class would be unwanted.

If so, we could rename this to BaseQueryService or something.

I think the answer here is "no" though. When wouldn't we want request deduplication, and easier-to-use caching options?

Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still working through this PR, but made some more suggestions.

// We provide re-exports of the underlying TanStack Query hooks with narrower types,
// removing `staleTime` and `queryFn` which aren't useful when using data services.

export function useQuery<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on the previous commit:

The advantage I can see for keeping UI-related code in this package is that all of the data service foundational stuff is in one place (although in that case perhaps we should rename the package to data-services to account for the scope?). The disadvantage is that the React dependency may not be completely obvious (and traditionally we have tried to keep stack-agnostic isolated, especially in core).

If we created a new package, here are some naming ideas:

  • @metamask/data-service-query-react
  • @metamask/data-query-react
  • @metamask/react-query-wrappers

}) {
this.name = name;

this.#messenger = messenger as unknown as Messenger<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a comment explaining why we are doing this? (It's essentially the same reason we are doing it in BaseController, I think.) Unless there is a way to avoid this?

#setupCacheListener(): void {
this.#client.getQueryCache().subscribe((event) => {
if (['added', 'updated', 'removed'].includes(event.type)) {
this.#broadcastCacheUpdate(event.query.queryKey);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that we are publishing the same :cacheUpdate event even if a cache key is added or removed. Do we want to have separate events? Should we at least not publish :cacheUpdate if a key is removed? Or maybe I am misunderstanding how we intend this event to be used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cacheUpdate contains the full cache for a specific query key (not the diff). The assumption is that publishing that is enough to add, update and clear out any cache entries on the UI-side. Thinking more about it, I should verify that removed works as expected though.

We could have separate events and/or provide diffs, which may be a bit more efficient, but may also introduce complexity.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BugBot just pointed out the same thing that I may have just realized above: #8039 (comment) 😄

const { pages } = query.state.data as InfiniteData<TQueryFnData>;
const previous = options.getPreviousPageParam?.(pages[0], pages);

const direction = deepEqual(pageParam, previous) ? 'backward' : 'forward';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If pageParam and previous are both undefined, then direction will be "backward". Is that a concern?

});
}

protected async fetchQuery<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding JSDoc summarizing why we are requiring queryKey and queryFn?

return this.#client.fetchQuery(options);
}

protected async fetchInfiniteQuery<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding JSDoc that not only summarizes why we are requiring queryKey and queryFn but also how this is different from TanStack Query's version of fetchInfiniteQuery? I haven't been following the discussions very well so I admit that I don't have a firm grasp of what we have to do here to accommodate the existing architecture. If it takes more than three paragraphs to explain then maybe here is not the right place to document that knowledge, but maybe there are a few pointers you could provide at least.

deepEqual(param, pageParam),
);

return result.pages[pageIndex];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unguarded findIndex -1 returns undefined page data

Medium Severity

findIndex can return -1 if pageParam isn't found in result.pageParams, causing result.pages[-1] to silently return undefined instead of TData. This is especially likely when the initial fetchInfiniteQuery call stores undefined in pageParams[0] (TanStack's default for the first page) even though the actual fetch used a real pageParam via the wrapper on line 165. A subsequent call with that same pageParam enters the second branch, but findIndex won't match it against the stored undefined, returning -1.

Fix in Cursor Fix in Web

this.#broadcastCacheUpdate(event.query.queryKey);
}
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broadcasting on 'removed' dehydrates empty state, never propagates removal

Medium Severity

The cache listener in #setupCacheListener triggers #broadcastCacheUpdate on removed events. But #getDehydratedState calls dehydrate with a filter matching the just-removed query's hash — which no longer exists in the cache. This produces a DehydratedState with an empty queries array. When UI clients receive this via hydrate(client, payload.state), they hydrate an empty state, which is a no-op — so the removal is never actually propagated. UI clients retain stale cached data for queries that no longer exist on the service side.

Additional Locations (1)
Fix in Cursor Fix in Web

InfiniteData<TData>
>({ queryKey: options.queryKey });

if (!query?.state.data || !pageParam) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Falsy pageParam values incorrectly treated as absent

Medium Severity

The guard !pageParam in fetchInfiniteQuery uses a falsy check instead of an explicit undefined check. Since TPageParam extends Json, valid page params like 0, null, "", or false are all falsy but could be legitimate pagination values. These would incorrectly trigger the initial-fetch branch instead of the paginated-fetch branch, breaking pagination for any service using such page param types.

Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

@mcmire
Copy link
Contributor

mcmire commented Mar 12, 2026

Related question: Are there any data services where this query-related functionality would not be useful? i.e. where extending this base class would be unwanted.

If so, we could rename this to BaseQueryService or something.

I think the answer here is "no" though. When wouldn't we want request deduplication, and easier-to-use caching options?

@Gudahtt If we are talking what is essentially an HTTP client, no, I don't think there's any case where we wouldn't want to use BaseDataService, either. If we are talking about a data service that uses WebSockets or polling, then it's harder to say. I don't have a clear idea on how those patterns would work with TanStack Query. I mean, we haven't talked much about that lately, but do you have any thoughts on that?

@Gudahtt
Copy link
Member

Gudahtt commented Mar 12, 2026

A query client would be even more useful with websockets or polling (request deduplication would be useful even more often in those cases, and is a bit trickier to implement by hand as well).

Though I'm also not 100% sure what that would look like to implement, I haven't thought about it much yet.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants