// Copyright (c) 2025 The Brave Authors. All rights reserved. // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. import * as React from 'react' import { QueryClient, QueryObserver, useQuery, UseQueryResult as ReactQueryUseQueryResult, useMutation, UseMutationResult as ReactQueryUseMutationResult, UseMutationOptions, QueryObserverOptions, MutationOptions, MutationObserver, MutateOptions, } from '@tanstack/react-query' /** * Shared QueryClient singleton used by all APIs created with createInterfaceApi. * Each API differentiates its data via unique key prefixes. * Callers can still pass their own QueryClient if needed. */ let sharedQueryClient = new QueryClient() /** * Removes all data from the shared QueryClient used by all calls to * createInterfaceApi which do not provide their own QueryClient. */ export function clearAllDataForTesting() { // sharedQueryClient.clear() or .cancelQueries() can throw a CancelledError, // and it's probably best to move to a new instance in-between tests anyway. sharedQueryClient = new QueryClient() } type ChangeReturnType any, R> = T extends ( ...args: infer P ) => any ? (...args: P) => R : never export type NoPlaceholderQueryEndpointDefinition< P extends readonly any[], R, > = { query: (...args: P) => Promise prefetchWithArgs?: NoInfer

} & Omit< QueryObserverOptions>, 'queryKey' | 'queryFn' | 'placeholderData' > export type PlaceholderQueryEndpointDefinition< P extends readonly any[], R, > = NoPlaceholderQueryEndpointDefinition & { placeholderData: NoInfer } export type QueryEndpointDefinition

= | NoPlaceholderQueryEndpointDefinition | PlaceholderQueryEndpointDefinition export type MutationEndpointDefinition

= { mutation: (...args: P) => Promise } & Omit, unknown, P>, 'mutationKey' | 'mutationFn'> /** * Each endpoint must be one of: * { query: (...args: A) => Promise, prefetch?: boolean } * or * { mutation: (...args: A) => Promise } * * For query endpoints, you get `.fetch(...)`, `.useQuery(...)`, `.invalidate(...)`, and `.update(...)`. * For mutation endpoints, you get `.mutate(...)` and `.useMutation()`. */ export type EndpointDef

= | QueryEndpointDefinition | MutationEndpointDefinition /** * EventDef is just a phantom object whose sole job is to carry * the tuple‐type `Args` (an array of key‐argument types) and the `Payload` type. * * At runtime `event()` returns a dummy object; we only use its type for inference. */ export type EventDef = { registerEmitter: (emitter: (...args: Payload) => void) => void /** * phantom field so we can extract `Args` at the type level */ __args: Args } // This function only exists to actually prevent inferring of // query args from prefetchWithArgs so that typescript avoids // confusion if the query() parameters don't match the prefetchWithArgs type // and informs the consumer that there's a mismatch that needs to be fixed. // Ideally consumers would not need to wrap their endpoints in query() // and they don't if they don't define prefetchWithArgs. export function query( endpoint: QueryEndpointDefinition, ): QueryEndpointDefinition { return endpoint } /** * Wrapper for an endpoint which creates data that is not fetched but is * updated by events and provided with initial data. * @param data optional initial data * @returns query endpoint definition */ // Overload: when data is provided, return type includes placeholderData export function state(data: R): PlaceholderQueryEndpointDefinition<[], R> // Overload: when no data, return type without placeholderData export function state(): NoPlaceholderQueryEndpointDefinition<[], R> // Implementation export function state(data?: R): QueryEndpointDefinition<[], R> { const base = { query: () => Promise.reject(new Error('State endpoint should not be called')), enabled: false, prefetchWithArgs: [] as [], staleTime: 'static' as const, } if (data !== undefined) { return { ...base, placeholderData: data } } return base } /** * Call this for each event in your config. For example: * events: { * onConversationDeleted: event<[string], boolean>(), * onUserRenamed: event<[number], { id: number; newName: string }>(), * } * * That tells the factory "`onConversationDeleted` takes one key‐arg (string), * and its payload is a boolean." */ export function event( registerEmitter: (emitter: (...args: Payload) => void) => void, ): EventDef { return { registerEmitter, __args: [] as any as Args, } } // Basis for a unique key for each call to createInterfaceApi for the current scope let globalRootInstanceCount = 0 /** * Factory function to create a subscribable API with a shared cache based off * (usually a mojom) interface which exposes features of Tanstack Query in a * similar fashion to RTK Query's createApi. * * Whilst this sets up a store that could be (ab)used to store, update and * subscribe to any data, it encourages its use only for remote-fetched data, * and not reactive state needed for the UI. The intention is for that kind of * state to be handled by the more featureful UI framework. * * See readme.md for detailed usage instructions and examples. */ export function createInterfaceApi< /** * ExposedActions is a record of simple actions that can be called directly from the UI. * These actions do not require any status helpers or long-term data caching. * For example: * { * sendMessage: (message: string) => void, * deleteConversation: (id: string) => void, * … * } */ const ExposedActions extends Record< string, Function | Record >, // Use `EndpointDef` (not `any[]`) so that specific endpoint // definitions remain assignable under `strictFunctionTypes`. With `any[]`, // mutation callbacks like `onMutate: (variables: [a, b]) => …` fail // contravariance checks against `(variables: any[]) => …` because tuples // are narrower than `any[]`. Switching to `any` makes the parameter slot // bivariant and side-steps the variance issue without losing inference on // the concrete `RawEndpoints` type below. const RawEndpoints extends Record>, EventDefinitions extends Record> = {}, >(config: { /** * Simple actions that can be called directly from the UI. These * don't need any status helpers or data caching. If helper data for status, * e.g. "in progress", would be useful then consider using a mutation endpoint instead. */ actions?: ExposedActions /** * Actions to expose to the UI, each have a result that will be cached or mutated */ endpoints: RawEndpoints /** An array of event names to broadcast */ events?: EventDefinitions }) { // Use shared QueryClient by default for proper React context integration. // All APIs share the same client; keys differentiate the data. const queryClient = sharedQueryClient const rootKey = globalRootInstanceCount++ type ValidKey = keyof RawEndpoints & string // For each endpoint K: // ArgsOf = the parameter‐tuple of raw[K].[query|mutation] // DataOf = the (Promise-d) returned type of raw[K].[query|mutation] type ArgsOf = RawEndpoints[K] extends QueryEndpointDefinition ? QueryArgs : RawEndpoints[K] extends MutationEndpointDefinition< infer MutationArgs, any > ? MutationArgs : never type DataOf = RawEndpoints[K] extends QueryEndpointDefinition ? QueryResult : RawEndpoints[K] extends MutationEndpointDefinition< any, infer MutationResult > ? MutationResult : never // If placeholder data is provided, we can assume that data is never undefined type EndpointDataForKey = RawEndpoints[K] extends { placeholderData: DataOf } ? DataOf : DataOf | undefined // Custom results type BaseUseQueryResult = ReactQueryUseQueryResult< DataOf > type UseQueryResult = BaseUseQueryResult & { // Convenience - a nicer name for data // *and* never undefined if placeholder data is provided, e.g. // `const { getThingsData } = useGetThings()` // instead of // ``` // const { data: getThingsData } = useSaveSomething() // if (!getThingsData) { // throw new Error('getThingsData should never be undefined as it has placeholder data') // } // ``` [P in K as `${P}Data`]: EndpointDataForKey // Note: We could add more convenience properties here, // e.g. `isMyQueryLoading`. } & { // Never undefined if placeholderData is provided data: EndpointDataForKey } type BaseUseMutationResult = ReactQueryUseMutationResult< DataOf, unknown, ArgsOf > & { // Nicer version of mutate where args are optional if no parameters mutate: ArgsOf extends [] ? () => ReturnType< ReactQueryUseMutationResult, unknown, ArgsOf>['mutate'] > : ReactQueryUseMutationResult, unknown, ArgsOf>['mutate'] } type UseMutationResult = BaseUseMutationResult & { // Convenience - a nicer name for mutate, e.g. // `const { saveSomething } = useSaveSomething()` // instead of // `const { mutate: saveSomething } = useSaveSomething()` [P in K]: BaseUseMutationResult['mutate'] } type APIActions = { [K in keyof ExposedActions]: ExposedActions[K] extends ( ...args: infer P ) => any ? ExposedActions[K] : { [P in keyof ExposedActions[K]]: ExposedActions[K][P] } } // Don't allow individual hook uses to specify events since those are specified either at the endpoint // definition or by the mutation function call. type APIUseMutationOptions = Omit< UseMutationOptions, unknown, ArgsOf>, 'onMutate' | 'onError' | 'onSuccess' | 'onSettled' > // Build out methods for every K in RawEndpoints, depending on whether it's a query or mutation type APIEndpoints = { [K in ValidKey]: RawEndpoints[K] extends QueryEndpointDefinition ? { /** * Call the underlying query and return the data. This will * also cause anyone subscribed to this endpoint to receive the * updated data. */ fetch: (...args: ArgsOf) => Promise> /** * Imperative (non-reactive) way to retrieve data for this endpoint. * Should only be used in callbacks or functions where reading the * latest data is necessary, e.g. for optimistic updates. * * Hint: Do not use this function inside a component, because it won't * receive updates. Use useQuery to create a QueryObserver that * subscribes to changes. */ current: (...args: ArgsOf) => EndpointDataForKey /** * Reset the cache for this endpoint * (specifying optional arguments as the key) * which will remove the data from the cache * and reset the state to undefined or placeholderData. */ reset: (...args: ArgsOf) => void /** * Invalidate the cache for this endpoint * (specifying optional arguments as the key to invalidate) * which will force a re-fetch of the data if currently * subscribed to, or on the next call to `fetch()`. */ invalidate: (...args: ArgsOf) => void /** * React hook for accessing state of a query endpoint including progress, cached data and re-fetching * See https://tanstack.com/query/latest/docs/framework/react/reference/useQuery */ useQuery: (...args: ArgsOf) => UseQueryResult /** * Manually update the cache for this endpoint * (specifying optional arguments as the key to update). * Usage: api.myEndpoint.update(arg1, arg2, ..., updaterFnOrUpdate) * where updaterFnOrUpdate has signature (old: Data | undefined) => Data * OR is a Partial to update the object directly. * * This is useful in event handlers from the remote interface, or * optimistic updates. */ update: ( ...argsAndUpdater: [ ...Params: ArgsOf, updater: | ((old: DataOf) => Partial>) | Partial>, ] ) => void } & (RawEndpoints[K] extends { placeholderData: DataOf } ? { // If placeholder data is provided, we know // the data will never be undefined, so we can // provide a potentially even more convenient hook. useData: (...args: ArgsOf) => DataOf } : {}) : RawEndpoints[K] extends MutationEndpointDefinition ? { /** * Call the underlying `raw[K].mutation(...args)` and return the data. */ mutate: ChangeReturnType< BaseUseMutationResult['mutate'], Promise> > /** * Hook: runs `useMutation` for this mutation endpoint. * You can call `mutate(variables)` or `mutateAsync(variables)` on the result. */ useMutation: ( options?: APIUseMutationOptions, ) => UseMutationResult } : never } const endpoints = {} as APIEndpoints // Build endpoints - Queries and Mutations // // Expose React Query's hooks and methods for each endpoint, // deciding if it's a query or mutation endpoint. // // ;(Object.keys(config.endpoints) as Array).forEach((name) => { const endpointDef = config.endpoints[name] const baseKey = [rootKey, name] if ('query' in endpointDef) { const { query, prefetchWithArgs, ...queryOptions } = endpointDef as QueryEndpointDefinition type QArgs = ArgsOf type QData = DataOf if (!queryOptions.staleTime) { // The default for staleTime is 0, meaning a result is stale as soon // as it is received. That results in every useQuery call causing a // re-fetch after returning the stale data. // Infinity will make data always considered fresh and re-fetch will // only occur when invalidated, manually refetched, or if refetchOnX is // set. queryOptions.staleTime = Infinity } // fetch(...args): // - calls `query(...args)` // - sets the result into cache under [...baseKey, ...args] // - returns the data const fetcher = async (...args: QArgs) => { const data = await queryClient.fetchQuery({ queryKey: [...baseKey, ...args], queryFn: () => { const queryResult = query(...args) return queryResult }, ...queryOptions, }) return data } const getCachedData = (...args: QArgs) => { const queryKey = [...baseKey, ...args] as readonly any[] let cachedData = queryClient.getQueryData(queryKey) if (cachedData === undefined && 'placeholderData' in queryOptions) { cachedData = queryOptions.placeholderData } return cachedData } // invalidates the cache so that it will be refetched // if the data is needed. const invalidate = (...args: QArgs) => { const queryKey = [...baseKey, ...args] queryClient.invalidateQueries({ queryKey, exact: true, }) } // Removes state and resets to undefined or placeholderData const reset = (...args: QArgs) => { const queryKey = [...baseKey, ...args] // We can use removeQueries if we don't want // to reset to placeholderData. queryClient.resetQueries({ queryKey, exact: true, }) } // useQuery(...args): wrapper around React-Query's useQuery const useQ = (...args: QArgs) => { const useQueryResult = useQuery( { queryKey: [...baseKey, ...args], queryFn: () => { const queryResult = query(...args) return queryResult }, ...queryOptions, }, queryClient, ) return { ...useQueryResult, // Typescript will handle whether this property // is accessible as definitely QData or undefined | QData. // We don't need to check if placeholderData is provided. [`${name}Data`]: useQueryResult.data, } as UseQueryResult } // update(...args, updaterFn): manually setQueryData for that key // We can accept a partial update only if the data is an object type AllowedUpdateParam = QData extends {} ? Partial : QData function updateFromOld( old: QData | undefined, update: AllowedUpdateParam, ): QData { if ( old === undefined || Array.isArray(old) || typeof old !== 'object' ) { // Technically we shouldn't allow this as it might just be partial return update as QData } // Objects can be combined return { ...old, ...(update as Partial) } as QData } const updater = ( ...argsAndUpdater: [ ...Params: QArgs, updaterFn: ((old: QData) => AllowedUpdateParam) | AllowedUpdateParam, ] ) => { const up = argsAndUpdater[argsAndUpdater.length - 1] as | ((old: QData) => AllowedUpdateParam) | Partial // args is everything except the updater function (or updated state) which is // always the last argument. const args = argsAndUpdater.slice( 0, argsAndUpdater.length - 1, ) as unknown as QArgs const queryKey = [...baseKey, ...args] queryClient.setQueryData(queryKey, (old) => { const updateData: AllowedUpdateParam = typeof up === 'function' ? up(old as QData) : (up as AllowedUpdateParam) const oldOrPlaceholder = getCachedData(...args) if (!old && oldOrPlaceholder) { // Warn only if this is a regular query. "state" queries will always // use the update mechanism - the initial data is always considered // placeholder. if (endpointDef.enabled) { console.warn( 'Updating data for query when base data has not yet' + ' been received. Placeholder data will convert to "real" data.' + ' `isPlaceholder` can no longer be relied upon for this query.', { queryKey, old, oldOrPlaceholder, updateData }, ) } old = oldOrPlaceholder } const newData = updateFromOld(old, updateData) return newData }) // Cancel and re-queue because our set could be replaced if there is a // query in-progress. if (queryClient.isFetching({ queryKey, exact: true })) { queryClient.cancelQueries({ queryKey, exact: true, }) // re-queue, but must first invalidate so that fetcher performs a new // fetch instead of return the cached result queryClient.invalidateQueries({ queryKey, exact: true, }) fetcher(...args) } } if (prefetchWithArgs && queryOptions.enabled !== false) { // If this endpoint is marked as prefetch, we prefetch it immediately // even if the UI doesn't call it yet - we don't // want to wait for React to initialize. fetcher(...(prefetchWithArgs as QArgs)) } ;(endpoints as any)[name] = { fetch: fetcher, current: getCachedData, invalidate, reset, useQuery: useQ, update: updater, } } else if ('mutation' in endpointDef) { const { mutation, ...mutationOptions } = endpointDef type MArgs = ArgsOf | never type MData = DataOf // Allow the caller to e.g. handle mutation results queryClient.setMutationDefaults(baseKey, mutationOptions) type EndpointUseMutationOptions = UseMutationOptions< MData, unknown, MArgs > // useMutation(): wrap React-Query's useMutation const useMut = ( options?: APIUseMutationOptions, ...args: MArgs ): UseMutationResult => { const mutationOptions: EndpointUseMutationOptions = { ...options } // Do not allow overriding the events - they can be specified in the // endpoint definition or at the mutation call. The typescript type prohibits // this but we should enforce it at runtime for JS or ignored TS errors in order // to avoid unexpected behavior. for (const key of [ 'onSuccess', 'onSettled', 'onError', 'onMutate', ] as Partial[]) { if (Object.hasOwn(mutationOptions, key)) { delete mutationOptions[key] } } const useMutationResult = useMutation( { mutationKey: [...baseKey, ...args], mutationFn: (variables) => mutation(...variables), ...mutationOptions, }, queryClient, ) return { ...useMutationResult, [name]: useMutationResult.mutate, } as UseMutationResult } const directMutateFn = ( args: MArgs, options?: MutateOptions, ) => { const observer = new MutationObserver( queryClient, { ...options, mutationKey: [...baseKey, ...(args ?? [])], mutationFn: (variables) => mutation(...variables), }, ) return observer.mutate(args ?? []) } ;(endpoints as any)[name] = { mutate: directMutateFn, useMutation: useMut, } } }) // Events // We expect `config.events` to be something like: // { // onSomethingHappened: event<[], { some: 'data' }>(), // onConversationDeleted: event<[conversationId: string], boolean>(), // onUserRenamed: event<[userId: number], { id:number; newName: string }>(), // } // // Where the user can subscribe to an argument-less event, // e.g. `onSomethingHappened`, or an event with key arguments, // e.g. `onConversationDeleted` which takes a `conversationId` as the key argument. // // That is different from the payload, which is the data that is sent // when the event is emitted. // e.g. // useOnConversationDeleted(conversationId, (result) => { // console.log(`Conversation ${conversationId} was deleted: ${result}`) // }) // // -or- // // useOnSomethingHappened((data) => {}) // // type EvAll = keyof NonNullable type EvDefs = NonNullable type KeyArgsOf = EvDefs[K] extends EventDef ? A : never type PayloadOf = EvDefs[K] extends EventDef ? P : never type EventInternalData = { payload: T; eventCount: number } function emitEvent( eventName: K, ...argsAndPayload: [...KeyArgsOf, PayloadOf] ) { const payload = argsAndPayload[argsAndPayload.length - 1] as PayloadOf const keyArgs = argsAndPayload.slice( 0, argsAndPayload.length - 1, ) as KeyArgsOf queryClient.setQueryData>>( [rootKey, eventName, ...keyArgs], (old) => ({ payload, eventCount: (old?.eventCount ?? 0) + 1 }), ) } type EventHooks = { /** * React hook to wrap useEffect with a subscription and unsubscription * to the event, firing the provided handler, and resubscribing * whenever the provided dependencies change. */ [K in EvAll as `use${Capitalize}`]: ( handler?: (...result: PayloadOf) => void, deps?: React.DependencyList, ...keyArgs: KeyArgsOf ) => void } & { /** * React hook to get the latest payload for the event. Useful for one-off * events without needing to add extra state to your UI component */ [K in EvAll as `useCurrent${Capitalize}`]: ( ...keyArgs: KeyArgsOf ) => { hasEmitted: boolean /** * The latest data for the event, or undefined if no data has been set */ data: PayloadOf | undefined } } & { /** * Subscribe and unsubscribe to the event */ [K in EvAll as `subscribeTo${Capitalize}`]: ( handler: (...result: PayloadOf) => void, ...keyArgs: KeyArgsOf ) => () => void } & { /** * Clear event data so that useCurrentMyEvent will return undefined until * the next time the event is emitted. */ [K in EvAll as `reset${Capitalize}`]: ( ...keyArgs: KeyArgsOf ) => void } // Events implementation const eventHooks = {} as EventHooks ;(Object.keys(config.events || {}) as Array).forEach((eventName) => { const cap = `${(eventName as string)[0].toUpperCase()}${(eventName as string).slice(1)}` const hookName = `use${cap}` const hookNameUseCurrent = `useCurrent${cap}` const subscribeName = `subscribeTo${cap}` const handledName = `reset${cap}` const keyBase = [rootKey, eventName] // register the emitter for the event // @ts-expect-error we need to fix the type of argsAndPayload ;(config.events as any)[eventName].registerEmitter((...args) => // @ts-expect-error emitEvent(eventName, args), ) // useCurrentMyEvent ;(eventHooks as any)[hookNameUseCurrent] = (...keyArgs: any[]) => { const hookData = useQuery>>( { queryKey: [...keyBase, ...keyArgs], enabled: false, queryFn: () => Promise.reject( new Error( `${eventName as string} is an event, not a query and should not try to fetch data`, ), ), }, queryClient, ) return { hasEmitted: hookData.isFetched, data: hookData.data?.payload, } } // subscribeToMyEvent ;(eventHooks as any)[subscribeName] = ( handler: (...result: PayloadOf) => {}, ...keyArgs: any[] ) => { const observer = new QueryObserver< EventInternalData> >(queryClient, { queryKey: [...keyBase, ...keyArgs], enabled: false, queryFn: () => Promise.reject( new Error( `${eventName as string} is an event, not a query and should not try to fetch data`, ), ), }) const unsubscribe = observer.subscribe((result) => { if (result.data !== undefined) { handler(...result.data.payload) } else { // We would only get here if there is never any payload type for // this event. // @ts-expect-error no args handler() } }) return unsubscribe } // useMyEvent ;(eventHooks as any)[hookName] = ( handler: (...result: PayloadOf) => {}, deps: React.DependencyList, ...keyArgs: any[] ) => { // Every time this function is called, we're probably going to get a new instance // of the handler, but we don't want to depend on it so that we re-subscribe // every time the handler changes. We also don't want to call an old version // of the handler when the event fires. Storing in a ref solves both requirements. const handlerRef = React.useRef(handler) handlerRef.current = handler React.useEffect(() => { const unsubscribe = (eventHooks as any)[subscribeName]( handlerRef.current, ...keyArgs, ) return () => { unsubscribe() } }, deps) } // Remove any emitted data for this event ;(eventHooks as any)[handledName] = (...keyArgs: any[]) => { queryClient.resetQueries({ queryKey: [...keyBase, ...keyArgs], exact: true, }) } }) type MutationKeys = { [K in keyof RawEndpoints]: RawEndpoints[K] extends MutationEndpointDefinition< any, any > ? K : never }[keyof RawEndpoints] & string type QueryKeys = { [K in keyof RawEndpoints]: RawEndpoints[K] extends QueryEndpointDefinition< any, any > ? K : never }[keyof RawEndpoints] & string // Build convenience root access to endpoints type RootEndpointMethods = // Queries { // Named query React hook [K in QueryKeys as `use${Capitalize}`]: ( ...args: ArgsOf ) => UseQueryResult } & { // Direct access to endpoint to get non-hook current data, invalidate the query, etc. [K in QueryKeys]: APIEndpoints[K] } & { // Queries with placeholder data always have data, so we can have a // named data convenience hook if components don't need progress state. [K in ValidKey as RawEndpoints[K] extends PlaceholderQueryEndpointDefinition< any, any > ? `use${Capitalize}Data` : never]: (...args: ArgsOf) => DataOf } & { // Mutations // useDoSomething: endpoints.doSomething.useMutation [K in MutationKeys as `use${Capitalize}`]: ( options?: APIUseMutationOptions, ) => UseMutationResult } & { // doSomething: endpoints.doSomething.mutate [K in MutationKeys]: Extract['mutate'] } const rootEndpointProperties = {} as RootEndpointMethods ;(Object.keys(config.endpoints) as Array).forEach( (name) => { const capitalized = `${(name as string)[0].toUpperCase()}${(name as string).slice(1)}` const hookName = `use${capitalized}` as `use${Capitalize}` if ('query' in config.endpoints[name]) { // @ts-expect-error: generated hook name rootEndpointProperties[hookName] = (endpoints as any)[name].useQuery // @ts-expect-error rootEndpointProperties[name] = (endpoints as any)[name] if ('placeholderData' in config.endpoints[name]) { // @ts-expect-error: generated hook name rootEndpointProperties[`use${capitalized}Data`] = ( ...args: any[] ) => { return (endpoints as any)[name].useQuery(...args).data } } } if ('mutation' in config.endpoints[name]) { // @ts-expect-error: we know that `endpoints[name].useMutation` matches the signature rootEndpointProperties[hookName] = (endpoints as any)[name].useMutation // @ts-expect-error rootEndpointProperties[name] = (endpoints as any)[name].mutate } }, ) const actions = config.actions as any as APIActions // Convenient access to query endpoint child functions const api = { emitEvent, ...actions, ...rootEndpointProperties, ...eventHooks, /** * Invalidates all queries for this API instance, causing them to refetch * if currently subscribed. Useful for Storybook/tests when any control * changes and you want all data to refresh from mock functions. */ invalidateAll: () => { queryClient.invalidateQueries({ queryKey: [rootKey], exact: false, }) }, close: () => { // Cancel all pending queries for this API key queryClient.cancelQueries({ queryKey: [rootKey], exact: false }) // Queries will be gc with no subscribers but we can remove them immediately queryClient.removeQueries({ queryKey: [rootKey], exact: false }) }, } return api }