Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion packages/base-data-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"@metamask/messenger": "^2.0.0",
"@metamask/storage-service": "^1.0.2",
"@metamask/utils": "^11.11.0",
"@tanstack/query-core": "^4.43.0",
"@tanstack/query-core": "^5.62.16",
"cockatiel": "^3.1.2",
"fast-deep-equal": "^3.1.3",
"lodash": "^4.17.21"
Expand Down
7 changes: 4 additions & 3 deletions packages/base-data-service/src/BaseDataService.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MOCK_ANY_NAMESPACE, Messenger } from '@metamask/messenger';
import { hashQueryKey } from '@tanstack/query-core';
import { hashKey } from '@tanstack/query-core';
import { BrokenCircuitError } from 'cockatiel';
import { cleanAll } from 'nock';

Expand Down Expand Up @@ -131,7 +131,7 @@ describe('BaseDataService', () => {

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

const hash = hashQueryKey(queryKey);
const hash = hashKey(queryKey);

expect(publishSpy).toHaveBeenNthCalledWith(
6,
Expand Down Expand Up @@ -186,7 +186,7 @@ describe('BaseDataService', () => {

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

const hash = hashQueryKey(queryKey);
const hash = hashKey(queryKey);

expect(publishSpy).toHaveBeenNthCalledWith(
8,
Expand Down Expand Up @@ -333,6 +333,7 @@ describe('BaseDataService', () => {
state: {
queries: [
{
dehydratedAt: expect.any(Number),
queryHash:
'["ExampleDataService:getAssets",["eip155:1/slip44:60","bip122:000000000019d6689c085ae165831e93/slip44:0","eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f"]]',
queryKey: [
Expand Down
144 changes: 102 additions & 42 deletions packages/base-data-service/src/BaseDataService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
Messenger,
ActionConstraint,
EventConstraint,
} from '@metamask/messenger';
import { Messenger, BaseMessenger } from '@metamask/messenger';
import type {
StorageServiceGetItemAction,
StorageServiceRemoveItemAction,
Expand All @@ -11,16 +7,21 @@
import { Duration, inMilliseconds } from '@metamask/utils';
import type { Json } from '@metamask/utils';
import {
DefaultError,
DefaultOptions,
DehydratedState,
FetchInfiniteQueryOptions,
FetchQueryOptions,
GetNextPageParamFunction,
InfiniteData,
InfiniteQueryPageParamsOptions,
InvalidateOptions,
InvalidateQueryFilters,
OmitKeyof,
QueryClient,
QueryClientConfig,
QueryFunction,

Check failure on line 23 in packages/base-data-service/src/BaseDataService.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (lint:eslint) (24.x)

'QueryFunction' is defined but never used
SkipToken,
WithRequired,
dehydrate,
hydrate,
Expand Down Expand Up @@ -53,10 +54,10 @@

export type DataServiceInvalidateQueriesAction<ServiceName extends string> = {
type: `${ServiceName}:invalidateQueries`;
handler: (
filters?: InvalidateQueryFilters<Json>,
options?: InvalidateOptions,
) => Promise<void>;
handler: BaseDataService<
ServiceName,
BaseMessenger<ServiceName>
>['invalidateQueries'];
};

type DataServiceActions<ServiceName extends string> =
Expand Down Expand Up @@ -118,15 +119,7 @@

export class BaseDataService<
ServiceName extends string,
ServiceMessenger extends Messenger<
ServiceName,
ActionConstraint,
EventConstraint,
// Use `any` to allow any parent to be set. `any` is harmless in a type constraint anyway,
// it's the one totally safe place to use it.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
>,
ServiceMessenger extends BaseMessenger<ServiceName>,
> {
public readonly name: ServiceName;

Expand Down Expand Up @@ -238,23 +231,42 @@
/**
* Fetch a query.
*
* @param options - The options defining the query. Keep in mind that `queryKey` and `queryFn` are required when using data services.
* Additionally `retry` and `retryDelay` are not available, retries can be customized using the `servicePolicyOptions`.
* @param options - The options defining the query. Note that although this
* method wraps `fetchQuery` from `@tanstack/query-core`, there are a few
* restrictions:
* - `queryKey` and `queryFn` are required
* - `queryFn` must be a function, not a skip token
* - `retry` and `retryDelay` are not available (retries can be customized
* using the constructor's `servicePolicyOptions`).
* @returns The query results.
*/
protected async fetchQuery<
TQueryFnData extends Json,
TError = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam extends Json = Json,
>(
options: WithRequired<
OmitKeyof<
FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'retry' | 'retryDelay'
FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
'retry' | 'retryDelay' | 'queryFn'
>,
'queryKey' | 'queryFn'
>,
'queryKey'
> & {
queryFn: NonNullable<
Exclude<
FetchQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>['queryFn'],
SkipToken
>
>;
},
): Promise<TData> {
return this.#queryClient.fetchQuery({
...options,
Expand All @@ -266,30 +278,73 @@
/**
* Fetch a paginated query.
*
* @param options - The options defining the query. Keep in mind that `queryKey` and `queryFn` are required when using data services.
* Additionally `retry` and `retryDelay` are not available, retries can be customized using the `servicePolicyOptions`.
* @param options - The options defining the query. Note that although this
* method wraps `fetchInfiniteQuery` from `@tanstack/query-core`, there are a
* few differences:
* - `queryKey` and `queryFn` are required
* - `queryFn` must be a function, not a skip token
* - `retry` and `retryDelay` are not available (retries can be customized
* using the constructor's `servicePolicyOptions`).
* - This function returns a page's worth of data (the same thing that
* `queryFn` returns), not a list of pages
* @param pageParam - An optional page parameter.
* @returns The query result, exclusively the requested page is returned.
* @returns The requested page.
*/
protected async fetchInfiniteQuery<
TQueryFnData extends Json,
TError = unknown,
TData extends TQueryFnData = TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam extends Json = Json,
>(
options: WithRequired<
OmitKeyof<
FetchInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'retry' | 'retryDelay'
FetchInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>,
'retry' | 'retryDelay' | 'queryFn'
>,
'queryKey' | 'queryFn'
>,
'queryKey'
> & {
queryFn: NonNullable<
Exclude<
FetchInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
>['queryFn'],
SkipToken
>
>;
} & (
| {
pages?: never;
}
| {
pages: number;
getNextPageParam: GetNextPageParamFunction<
TPageParam,
TQueryFnData
>;
}
) &
InfiniteQueryPageParamsOptions<TQueryFnData, TPageParam>,
pageParam?: TPageParam,
): Promise<TData> {
// ): Promise<InfiniteData<TData, TPageParam> | TQueryFnData | TData> {
): Promise<TQueryFnData> {
const cache = this.#queryClient.getQueryCache();

const query = cache.find<TQueryFnData, TError, InfiniteData<TData>>({
const query = cache.find<
TQueryFnData,
TError,
InfiniteData<TQueryFnData, TPageParam>
>({
queryKey: options.queryKey,
});

Expand All @@ -304,20 +359,25 @@
}),
),
});

return result.pages[0];
// We have to assume that `fetchInfiniteQuery` returns the same data
// that `queryFn` returns.
return result.pages[0] as unknown as TQueryFnData;
}

const { pages } = query.state.data;
const previous = options.getPreviousPageParam?.(pages[0], pages);
const { pages, pageParams } = query.state.data;
const previous = options.getPreviousPageParam?.(
pages[0],
pages,
pageParams[0],
pageParams,
);

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

const result = await query.fetch(undefined, {
meta: {
fetchMore: {
direction,
pageParam,
},
},
});
Expand All @@ -337,7 +397,7 @@
* @returns Nothing.
*/
async invalidateQueries(
filters?: InvalidateQueryFilters<Json>,
filters?: InvalidateQueryFilters<Json[]>,
options?: InvalidateOptions,
): Promise<void> {
return this.#queryClient.invalidateQueries(filters, options);
Expand Down
32 changes: 24 additions & 8 deletions packages/base-data-service/tests/ExampleDataService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Messenger } from '@metamask/messenger';
import { CaipAssetId, Duration, inMilliseconds, Json } from '@metamask/utils';
import { DefaultError } from '@tanstack/query-core';
import { ConstantBackoff } from 'cockatiel';

import {
Expand All @@ -8,6 +9,7 @@ import {
DataServiceCacheUpdatedEvent,
DataServiceGranularCacheUpdatedEvent,
PersistenceConfiguration,
QueryKey,
} from '../src/BaseDataService.js';
import { ExampleDataServiceMethodActions } from './ExampleDataService-method-action-types.js';

Expand Down Expand Up @@ -49,7 +51,8 @@ export type PageParam =
| {
before: string;
}
| { after: string };
| { after: string }
| null;

const MESSENGER_EXPOSED_METHODS = ['getAssets', 'getActivity'] as const;

Expand Down Expand Up @@ -101,15 +104,21 @@ export class ExampleDataService extends BaseDataService<
return response.json();
},
staleTime: inMilliseconds(1, Duration.Day),
cacheTime: inMilliseconds(1, Duration.Day),
gcTime: inMilliseconds(1, Duration.Day),
});
}

async getActivity(
address: string,
page?: PageParam,
page: PageParam = null,
): Promise<GetActivityResponse> {
return this.fetchInfiniteQuery<GetActivityResponse>(
return this.fetchInfiniteQuery<
GetActivityResponse,
DefaultError,
GetActivityResponse,
QueryKey,
PageParam
>(
{
queryKey: [`${this.name}:getActivity`, address],
queryFn: async ({ pageParam }) => {
Expand All @@ -118,10 +127,16 @@ export class ExampleDataService extends BaseDataService<
`${this.#accountsBaseUrl}/v4/multiaccount/transactions?limit=3&accountAddresses=${caipAddress}`,
);

if (pageParam?.after) {
url.searchParams.set('after', pageParam.after);
} else if (pageParam?.before) {
url.searchParams.set('before', pageParam.before);
if (pageParam !== null) {
// We need to discriminate this union.
// eslint-disable-next-line no-restricted-syntax
if ('after' in pageParam) {
url.searchParams.set('after', pageParam.after);
// We need to discriminate this union.
// eslint-disable-next-line no-restricted-syntax
} else if ('before' in pageParam) {
url.searchParams.set('before', pageParam.before);
}
}

const response = await fetch(url);
Expand All @@ -134,6 +149,7 @@ export class ExampleDataService extends BaseDataService<

return response.json();
},
initialPageParam: null,
getPreviousPageParam: ({ pageInfo }) =>
pageInfo.hasPreviousPage
? { before: pageInfo.startCursor }
Expand Down
16 changes: 16 additions & 0 deletions packages/messenger/src/Messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,22 @@ type DelegatedMessenger = Pick<
type StripNamespace<Namespaced extends NamespacedName> =
Namespaced extends `${string}:${infer Name}` ? Name : never;

/**
* The supertype of all messengers, scoped to a namespace.
*
* @template Namespace - The namespace for the messenger's own actions and
* events.
*/
export type BaseMessenger<Namespace extends string> = Messenger<
Namespace,
ActionConstraint,
EventConstraint,
// Use `any` to allow any parent to be set. `any` is harmless in a type constraint anyway,
// it's the one totally safe place to use it.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
>;

/**
* A message broker for "actions" and "events".
*
Expand Down
1 change: 1 addition & 0 deletions packages/messenger/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type {
ActionHandler,
BaseMessenger,
ExtractActionParameters,
ExtractActionResponse,
ExtractEventHandler,
Expand Down
Loading
Loading