Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
0303748
Add beginning of BaseDataService
FrederikBolding Feb 10, 2026
79d2f3a
Support pagination
FrederikBolding Feb 11, 2026
4406b48
Improve pagination
FrederikBolding Feb 11, 2026
5f45528
Add invalidateQueries action
FrederikBolding Feb 11, 2026
e24309e
Account for fetch direction
FrederikBolding Feb 13, 2026
ea3f10c
Follow conventions for referencing local packages
FrederikBolding Feb 25, 2026
55c8c59
Improve typing
FrederikBolding Feb 25, 2026
643a866
Bring test over from other branch
FrederikBolding Feb 25, 2026
6c745c3
Add example types
FrederikBolding Feb 25, 2026
e8ceb4c
Add createUIQueryClient + lint
FrederikBolding Feb 26, 2026
46fb4d9
Improve tests
FrederikBolding Feb 26, 2026
4346286
Add export
FrederikBolding Feb 26, 2026
65315d5
Fix pagination test
FrederikBolding Feb 26, 2026
4069266
Fix missing assertion
FrederikBolding Feb 26, 2026
346738d
Revert accidental change
FrederikBolding Feb 26, 2026
c0dc77e
Add test for paginated observers
FrederikBolding Feb 26, 2026
eb7b92c
Fix lint
FrederikBolding Feb 26, 2026
e635305
Fix issues with pagination when query was not already called once
FrederikBolding Feb 27, 2026
864ad52
Improve example
FrederikBolding Feb 27, 2026
60fec5a
Add working test for backwards pagination
FrederikBolding Feb 27, 2026
fba5acc
Fix lint
FrederikBolding Feb 27, 2026
b7c9049
Unsubscribe cache listeners
FrederikBolding Mar 3, 2026
ed24453
Add :cacheUpdate messenger event
FrederikBolding Mar 3, 2026
0aa17b2
Improve typing
FrederikBolding Mar 3, 2026
3367cb1
Improve handling of non data service queries
FrederikBolding Mar 3, 2026
c85ab2e
Simplify
FrederikBolding Mar 3, 2026
e80a458
Fix type issue in test
FrederikBolding Mar 3, 2026
bfc45aa
Use messenger API for subscriptions
FrederikBolding Mar 5, 2026
5f48224
Adjust staleTime and add utility hooks
FrederikBolding Mar 10, 2026
a274f47
Add basic test for hooks
FrederikBolding Mar 10, 2026
4323908
Allow configuring query client
FrederikBolding Mar 10, 2026
a4331f5
Add comment
FrederikBolding Mar 10, 2026
5411578
Add CHANGELOG entry
FrederikBolding Mar 10, 2026
2c83246
Allow inconsistent tanstack dependency for now
FrederikBolding Mar 10, 2026
d0e5a82
Fix bugbot flagged issues
FrederikBolding Mar 10, 2026
2195da1
Address PR comments
FrederikBolding Mar 12, 2026
bdc4c21
Address more PR comments
FrederikBolding Mar 12, 2026
4ca85e5
Add cacheUpdated event test
FrederikBolding Mar 12, 2026
496403a
Fix hook return type
FrederikBolding Mar 12, 2026
b2dfa26
Add logic for destroying the service
FrederikBolding Mar 17, 2026
fbf0898
Emit additional type information for cache updates to facilitate prop…
FrederikBolding Mar 17, 2026
1d47520
Use named exports
FrederikBolding Mar 18, 2026
806c757
Simplify
FrederikBolding Mar 18, 2026
229f8bc
Add service policy support
FrederikBolding Mar 18, 2026
2134475
Run Prettier on tsconfig
FrederikBolding Mar 18, 2026
090e534
Address more PR comments
FrederikBolding Mar 18, 2026
3adfec4
Move QueryKey type
FrederikBolding Mar 18, 2026
0d3c01d
Move UI-specific code to react-data-query
FrederikBolding Mar 19, 2026
76aad78
Update CHANGELOG
FrederikBolding Mar 19, 2026
8a83840
Fix lint
FrederikBolding Mar 19, 2026
c5acf3e
Add missing test
FrederikBolding Mar 19, 2026
d0d303c
Fix formatting
FrederikBolding Mar 19, 2026
1fdda05
Ignore React peer deps
FrederikBolding Mar 19, 2026
e0a7d03
More cleanup
FrederikBolding Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/base-data-service/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Initial release ([#8039](https://github.com/MetaMask/core/pull/8039))

[Unreleased]: https://github.com/MetaMask/core/
8 changes: 8 additions & 0 deletions packages/base-data-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,20 @@
"test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
},
"dependencies": {
"@metamask/controller-utils": "^11.19.0",
"@metamask/messenger": "^0.3.0",
"@metamask/utils": "^11.9.0",
"@tanstack/query-core": "^4.43.0",
"fast-deep-equal": "^3.1.3"
},
"devDependencies": {
"@metamask/auto-changelog": "^3.4.4",
"@ts-bridge/cli": "^0.6.4",
"@types/jest": "^29.5.14",
"deepmerge": "^4.2.2",
"jest": "^29.7.0",
"nock": "^13.3.1",
"ts-jest": "^29.2.5",
"typedoc": "^0.25.13",
"typedoc-plugin-missing-exports": "^2.0.0",
Expand Down
283 changes: 283 additions & 0 deletions packages/base-data-service/src/BaseDataService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
import { BrokenCircuitError } from '@metamask/controller-utils';
import { Messenger } from '@metamask/messenger';
import { hashQueryKey } from '@tanstack/query-core';
import { cleanAll } from 'nock';

import { ExampleDataService, serviceName } from '../tests/ExampleDataService';
import {
mockAssets,
mockTransactionsPage1,
mockTransactionsPage2,
mockTransactionsPage3,
TRANSACTIONS_PAGE_2_CURSOR,
TRANSACTIONS_PAGE_3_CURSOR,
} from '../tests/mocks';

const TEST_ADDRESS = '0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520';

const MOCK_ASSETS = [
'eip155:1/slip44:60',
'bip122:000000000019d6689c085ae165831e93/slip44:0',
'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f',
];

describe('BaseDataService', () => {
beforeEach(() => {
mockAssets();
mockTransactionsPage1();
mockTransactionsPage2();
mockTransactionsPage3();
});

it('handles basic queries', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);

expect(await service.getAssets(MOCK_ASSETS)).toStrictEqual([
{
assetId: 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f',
decimals: 18,
name: 'Dai Stablecoin',
symbol: 'DAI',
},
{
assetId: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
decimals: 8,
name: 'Bitcoin',
symbol: 'BTC',
},
{
assetId: 'eip155:1/slip44:60',
decimals: 18,
name: 'Ethereum',
symbol: 'ETH',
},
]);
});

it('handles paginated queries', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);

const page1 = await service.getActivity(TEST_ADDRESS);

expect(page1.data).toHaveLength(3);

const page2 = await service.getActivity(TEST_ADDRESS, {
after: page1.pageInfo.endCursor,
});

expect(page2.data).toHaveLength(3);

expect(page2.data).not.toStrictEqual(page1.data);
});

it('handles paginated queries starting at a specific page', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);

const page2 = await service.getActivity(TEST_ADDRESS, {
after: TRANSACTIONS_PAGE_2_CURSOR,
});

expect(page2.data).toHaveLength(3);

const page3 = await service.getActivity(TEST_ADDRESS, {
after: page2.pageInfo.endCursor,
});

expect(page3.data).toHaveLength(3);

expect(page3.data).not.toStrictEqual(page2.data);
});

it('handles backwards queries starting at a specific page', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);

const page3 = await service.getActivity(TEST_ADDRESS, {
after: TRANSACTIONS_PAGE_3_CURSOR,
});

expect(page3.data).toHaveLength(3);

const page2 = await service.getActivity(TEST_ADDRESS, {
before: page3.pageInfo.startCursor,
});

expect(page2.data).toHaveLength(3);
expect(page2.data).not.toStrictEqual(page3.data);
});

it('emits `:cacheUpdated` events when cache is updated', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);

const publishSpy = jest.spyOn(messenger, 'publish');

await service.getAssets(MOCK_ASSETS);

const queryKey = ['ExampleDataService:getAssets', MOCK_ASSETS];

const hash = hashQueryKey(queryKey);

expect(publishSpy).toHaveBeenNthCalledWith(
6,
`ExampleDataService:cacheUpdated:${hash}`,
{
type: 'updated',
state: {
mutations: [],
queries: [
expect.objectContaining({
state: expect.objectContaining({
status: 'success',
data: [
{
assetId:
'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f',
decimals: 18,
name: 'Dai Stablecoin',
symbol: 'DAI',
},
{
assetId: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
decimals: 8,
name: 'Bitcoin',
symbol: 'BTC',
},
{
assetId: 'eip155:1/slip44:60',
decimals: 18,
name: 'Ethereum',
symbol: 'ETH',
},
],
}),
}),
],
},
},
);
});

it('emits `:cacheUpdated` events when cache entry is removed', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);

const publishSpy = jest.spyOn(messenger, 'publish');

await service.getAssets(MOCK_ASSETS);

// Wait for GC
await new Promise((resolve) => setTimeout(resolve, 0));

const queryKey = ['ExampleDataService:getAssets', MOCK_ASSETS];

const hash = hashQueryKey(queryKey);

expect(publishSpy).toHaveBeenNthCalledWith(
8,
`ExampleDataService:cacheUpdated:${hash}`,
{
type: 'removed',
state: null,
},
);
});

it('does not emit events after being destroyed', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);
const publishSpy = jest.spyOn(messenger, 'publish');

service.destroy();

await service.getAssets(MOCK_ASSETS);

expect(publishSpy).toHaveBeenCalledTimes(0);
});

it('invalidates queries when requested', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);
const publishSpy = jest.spyOn(messenger, 'publish');

await service.getAssets(MOCK_ASSETS);

expect(publishSpy).toHaveBeenCalledTimes(6);

const queryKey = ['ExampleDataService:getAssets', MOCK_ASSETS];
await service.invalidateQueries({ queryKey });

expect(publishSpy).toHaveBeenCalledTimes(8);
});

describe('service policy', () => {
beforeEach(() => {
cleanAll();
});

Comment on lines +217 to +220
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: This should already be happening here:

Suggested change
beforeEach(() => {
cleanAll();
});

it('retries failed queries using the service policy', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);

mockAssets({ status: 500 });
mockAssets({ status: 500 });
mockAssets();

const result = await service.getAssets(MOCK_ASSETS);

expect(result).toStrictEqual([
{
assetId: 'eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f',
decimals: 18,
name: 'Dai Stablecoin',
symbol: 'DAI',
},
{
assetId: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
decimals: 8,
name: 'Bitcoin',
symbol: 'BTC',
},
{
assetId: 'eip155:1/slip44:60',
decimals: 18,
name: 'Ethereum',
symbol: 'ETH',
},
]);
});

it('throws after exhausting service policy retries', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);

mockAssets({ status: 500 });
mockAssets({ status: 500 });
mockAssets({ status: 500 });

await expect(service.getAssets(MOCK_ASSETS)).rejects.toThrow(
'invalid json response body',
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 mock the endpoint to return valid JSON instead? Usually in production code we might see an HttpError if retries are exhausted.

);
});

it('breaks the circuit after consecutive failures', async () => {
const messenger = new Messenger({ namespace: serviceName });
const service = new ExampleDataService(messenger);

mockAssets({ status: 500 });
mockAssets({ status: 500 });
mockAssets({ status: 500 });

await expect(service.getAssets(MOCK_ASSETS)).rejects.toThrow(
'invalid json response body',
);

await expect(service.getAssets(MOCK_ASSETS)).rejects.toThrow(
BrokenCircuitError,
);
});
});
});
Loading
Loading