-
-
Notifications
You must be signed in to change notification settings - Fork 276
feat: Implement BaseDataService
#8039
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
FrederikBolding
wants to merge
54
commits into
main
Choose a base branch
from
fb/data-service-base
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,854
−43
Open
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 79d2f3a
Support pagination
FrederikBolding 4406b48
Improve pagination
FrederikBolding 5f45528
Add invalidateQueries action
FrederikBolding e24309e
Account for fetch direction
FrederikBolding ea3f10c
Follow conventions for referencing local packages
FrederikBolding 55c8c59
Improve typing
FrederikBolding 643a866
Bring test over from other branch
FrederikBolding 6c745c3
Add example types
FrederikBolding e8ceb4c
Add createUIQueryClient + lint
FrederikBolding 46fb4d9
Improve tests
FrederikBolding 4346286
Add export
FrederikBolding 65315d5
Fix pagination test
FrederikBolding 4069266
Fix missing assertion
FrederikBolding 346738d
Revert accidental change
FrederikBolding c0dc77e
Add test for paginated observers
FrederikBolding eb7b92c
Fix lint
FrederikBolding e635305
Fix issues with pagination when query was not already called once
FrederikBolding 864ad52
Improve example
FrederikBolding 60fec5a
Add working test for backwards pagination
FrederikBolding fba5acc
Fix lint
FrederikBolding b7c9049
Unsubscribe cache listeners
FrederikBolding ed24453
Add :cacheUpdate messenger event
FrederikBolding 0aa17b2
Improve typing
FrederikBolding 3367cb1
Improve handling of non data service queries
FrederikBolding c85ab2e
Simplify
FrederikBolding e80a458
Fix type issue in test
FrederikBolding bfc45aa
Use messenger API for subscriptions
FrederikBolding 5f48224
Adjust staleTime and add utility hooks
FrederikBolding a274f47
Add basic test for hooks
FrederikBolding 4323908
Allow configuring query client
FrederikBolding a4331f5
Add comment
FrederikBolding 5411578
Add CHANGELOG entry
FrederikBolding 2c83246
Allow inconsistent tanstack dependency for now
FrederikBolding d0e5a82
Fix bugbot flagged issues
FrederikBolding 2195da1
Address PR comments
FrederikBolding bdc4c21
Address more PR comments
FrederikBolding 4ca85e5
Add cacheUpdated event test
FrederikBolding 496403a
Fix hook return type
FrederikBolding b2dfa26
Add logic for destroying the service
FrederikBolding fbf0898
Emit additional type information for cache updates to facilitate prop…
FrederikBolding 1d47520
Use named exports
FrederikBolding 806c757
Simplify
FrederikBolding 229f8bc
Add service policy support
FrederikBolding 2134475
Run Prettier on tsconfig
FrederikBolding 090e534
Address more PR comments
FrederikBolding 3adfec4
Move QueryKey type
FrederikBolding 0d3c01d
Move UI-specific code to react-data-query
FrederikBolding 76aad78
Update CHANGELOG
FrederikBolding 8a83840
Fix lint
FrederikBolding c5acf3e
Add missing test
FrederikBolding d0d303c
Fix formatting
FrederikBolding 1fdda05
Ignore React peer deps
FrederikBolding e0a7d03
More cleanup
FrederikBolding File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| }); | ||
|
|
||
| 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', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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: