@equinor/fusion-query
Version:
Reactive data fetching and caching library with observable streams and comprehensive event system
354 lines (353 loc) • 21.5 kB
TypeScript
import { Observable } from 'rxjs';
import { QueryClient, type QueryClientCtorOptions } from './client';
import { QueryCache, type QueryCacheMutation, type QueryCacheRecord } from './cache';
import type { CacheOptions, QueryFn, QueryOptions, QueryQueueFn, QueryTaskCached, QueryTaskCompleted } from './types';
import type { QueryCacheCtorArgs } from './cache/QueryCache';
import { QueryEvent, type IQueryEvent, type QueryEvents } from './events';
/**
* Defines the constructor options for a QueryClient object.
* It includes a query function and optional additional options for the client.
*
* @template TDataType - The type of the data returned by the query.
* @template TQueryArguments - The type of the arguments passed to the query function.
*
* @property fn - The query function to be used for fetching data.
* @property options - Optional additional options for the client.
*
* @see {@link QueryClient} - The QueryClient class.
* @see {@link QueryClientCtorOptions} - The constructor options for the QueryClient class.
*/
interface QueryClientOptions<TDataType, TQueryArguments> {
fn: QueryFn<TDataType, TQueryArguments>;
options?: QueryClientCtorOptions;
}
/**
* Defines the constructor options for a Query object.
*
* @template TDataType - The type of the data returned by the query.
* @template TQueryArguments - The type of the arguments passed to the query function.
*
* @property client - The client instance or configuration to be used for fetching data.
* @property key - A function that generates a unique key for caching query results based on the arguments.
* @property cache - The cache instance or configuration to be used for caching query results.
* @property validate - A function that validates a cache entry before it is used.
* @property expire - The number of milliseconds before a cache entry expires.
* @property queue - The queueing strategy to use for the query.
* @property logger - The logger instance to use for logging events and operations within the Query class.
*
* @see {@link QueryClient} - The QueryClient class.
* @see {@link CacheOptions} - The options for the cache instance.
* @see {@link QueryCache} - The QueryCache class.
* @see {@link QueueOperatorType} - The type of queueing strategy.
* @see {@link QueryQueueFn} - The type of the queueing strategy function.
*/
export type QueryCtorOptions<TDataType, TQueryArguments> = {
/**
* The client instance or configuration to be used for fetching data.
* It can either be an instance of QueryClient or an object with a 'fn' property
* that is a query function and an optional 'options' object for additional QueryClient options.
*/
client: QueryClient<TDataType, TQueryArguments> | QueryClientOptions<TDataType, TQueryArguments>;
/**
* A function that generates a unique key for caching query results based on the arguments.
* This key is used to store and retrieve cache entries.
*/
key: CacheOptions<TDataType, TQueryArguments>['key'];
/**
* An optional function to validate cache entries. It receives a cache entry and the arguments
* and returns a boolean indicating whether the cache entry is still valid.
*/
validate?: CacheOptions<TDataType, TQueryArguments>['validate'];
/**
* An optional instance of QueryCache or constructor arguments for creating a new QueryCache instance.
* If not provided, a new QueryCache will be created with default options.
*/
cache?: QueryCache<TDataType, TQueryArguments> | QueryCacheCtorArgs<TDataType, TQueryArguments>;
/**
* The expiration time of the cache in milliseconds.
* If undefined or 0, caching is disabled.
* This attribute is used only when the 'validate' function is not provided.
*/
expire?: number;
/**
* Queue strategies determine how multiple concurrent query requests are handled.
*
* - 'switch': This strategy cancels the current active request when a new request comes in.
* Only the result from the latest request will be returned. This is useful when only the latest data is relevant.
*
* - 'merge': With this strategy, multiple requests can run in parallel without canceling each other.
* All responses will be returned as they arrive. This is useful when all requests need to be resolved,
* regardless of the order in which they were initiated.
*
* - 'concat': This strategy queues requests and executes them one after another in a sequential manner.
* A new request will only start after the previous one has completed. This is useful when the order of
* execution is important and each request must be completed before the next begins.
*/
queueOperator?: QueueOperatorType | QueryQueueFn<TDataType, TQueryArguments>;
};
/**
* A type alias for the predefined operator types that can be used to control query request queuing.
* These operator types are used to define different strategies for handling concurrent query requests.
*/
type QueueOperatorType = 'switch' | 'merge' | 'concat';
/**
* The primary use case for `Query` involves:
* - Asynchronous Data Fetching: Seamlessly fetching data from APIs or databases asynchronously without blocking the UI, improving the user experience.
* - Caching: Storing fetched data in a cache to improve performance by reducing the number of redundant requests to the server.
* - Automatic Updates: Automatically updating the UI when the underlying data changes, without requiring explicit refresh actions from the user.
* - Concurrent Requests Management: Efficiently handling multiple, concurrent data fetches through strategies like merging, switching, or concatenating requests.
* - Retry and Error Handling: Automatically retrying failed requests and handling errors gracefully to ensure application stability.
*
* ## Benefits
*
* Using a `Query` mechanism offers numerous benefits, including:
* 1. Improved Performance and Efficiency: By caching responses and reducing unnecessary server requests, applications load faster and use fewer resources, both on the client and server side.
* 2. Simplified Data Fetching Logic: It abstracts away the boilerplate code associated with fetching data, handling errors, and managing response states, leading to cleaner and more maintainable code.
* 3. Automatic Synchronization: `Query` libraries often come with features to automatically refetch data on certain triggers (e.g., window focus), ensuring the UI is always up-to-date with the latest server state without manual intervention.
* 4. Built-in Asynchronous Management: Handling asynchronous data fetches becomes straightforward, with built-in support for loading states, error handling, and data updates.
* 5. Scalability: Easily scalable for complex applications, supporting various fetching strategies to manage multiple data sources, endpoints, and concurrent requests effectively.
* 6. Developer Experience: By standardizing the approach to data fetching and state management, it enhances developer experience, reducing the cognitive load and making it easier to onboard new developers.
* 7. Robust Error and Retry Handling: Features to automatically retry requests and sophisticated mechanisms for error handling improve application reliability.
* 8. Customizable and Extendable: While offering sensible defaults for most use cases, `Query` implementations are usually highly customizable, allowing developers to tailor their behavior for specific needs, such as custom caching strategies, query deduplication, and more.
*
* ## Examples
* Example of creating a basic query with a query function:
* @example
* ```typescript
* import { Query, QueryFn } from '@equinor/fusion-query';
*
* type ExampleData = {
* id: string;
* name: string;
* value: number;
* }
*
* // create a query function
* const queryFn: QueryFn<ExampleData, {id: string}> = async (args, signal) => {
* const response = await fetch(`https://api.example.com/data?id=${args.id}`, {signal});
* return response.json();
* };
*
* const queryOptions = QueryCtorOptions<ExampleData, {id: string}> = {
* client: { fn: queryFn },
* key: (args) => args.id,
* // optional cache options
* // optional queue operator
* // optional logger
* };
*
* // create a new Query instance with the options
* const query = new Query(queryOptions);
*
* ```
*
* @see {@link QueryCtorOptions} for more details on the constructor options.
* @see {@link Query.query} for more details on executing a query.
* @see {@link Query.queryAsync} for more details on executing a query asynchronously.
* @see {@link Query.mutate} for more details on mutating cache entries.
* @see {@link Query.invalidate} for more details on invalidating cache entries.
* @see {@link QueueOperatorType} for more details on the available queue operators.
*/
export declare class Query<TDataType, TQueryArguments = any> {
#private;
/**
* Static utility that extracts the raw value from a query result Observable.
*
* Transforms a stream of `QueryTaskValue<TType>` into a plain `Observable<TType>`,
* stripping away query metadata such as status, transaction, and timestamps.
*
* @see {@link queryValue} for the standalone operator function.
*/
static extractQueryValue: <TType, TArgs>(source$: ReturnType<Query<TType, TArgs>["query"]>) => Observable<TType>;
/**
* A public getter for the client instance.
* TODO: Implement a proxy to control access to the client.
* This proxy would allow for additional functionality or restrictions when accessing the client instance.
*/
get client(): QueryClient<TDataType, TQueryArguments>;
/**
* Protected helper method to register events if an event observer is configured.
* @param type - The event type
* @param data - The event data
*/
protected _registerEvent<TKey extends keyof QueryEvents>(type: TKey, key: string, data?: QueryEvents[TKey] extends QueryEvent<infer T> ? T : undefined): void;
/**
* A public getter for the cache instance.
* TODO: Implement a proxy to control access to the cache.
* This proxy would allow for additional functionality or restrictions when accessing the cache instance.
*/
get cache(): QueryCache<TDataType, TQueryArguments>;
/**
* An Observable stream of Query events. It allows subscribers to react to
* query lifecycle events such as query creation, cache hits, task execution, etc.
* Also includes QueryClient events for complete observability.
* @returns {Observable<CombinedQueryEvent<TDataType, TQueryArguments>>} An Observable stream of events.
*/
get event$(): Observable<IQueryEvent>;
/**
* The constructor for the Query class.
* It initializes the query client, cache, and sets up the query request queue.
* The constructor takes an options object which can include a custom client, cache, key generation function,
* cache validation function, cache expiration time, queuing strategy, and a logger.
*
* @param options - The constructor options for the Query instance.
*/
constructor(options: QueryCtorOptions<TDataType, TQueryArguments>);
/**
* Executes a query and returns an Observable that emits the result.
* It will throw an error if the query was skipped or canceled.
* The returned Observable can be subscribed to in order to receive updates on the query's execution and results.
*
* @param args - The arguments to be passed to the query function.
* @param options - Optional additional options for the query.
* @returns An Observable that emits the result of the query.
*/
query(args: TQueryArguments, options?: QueryOptions<TDataType, TQueryArguments>): Observable<QueryTaskCached<TDataType> | QueryTaskCompleted<TDataType>>;
/**
* Executes an asynchronous query and returns a Promise that resolves with the result.
* If `skipResolve` is set to true, the Promise resolves as soon as the query is sent (using `firstValueFrom`).
* If `skipResolve` is false or not provided, the Promise resolves with the final result of the query (using `lastValueFrom`).
* This method is useful for cases where an asynchronous, one-time result is needed rather than a stream of updates.
* Note that skipping resolution may result in returning invalid cache.
*
* @example
* ```typescript
* try{
* const result = await query.queryAsync({ id: '123' });
* console.log(result);
* } catch (error) {
* console.error(error);
* }
* ```
*
* @param payload - The arguments to be passed to the query function.
* @param opt - Optional additional options for the query. The `skipResolve` option determines the resolution behavior of the Promise.
* @returns A Promise that resolves with the result of the query or rejects with an Error.
*/
queryAsync(payload: TQueryArguments, opt?: QueryOptions<TDataType, TQueryArguments> & {
skipResolve?: boolean;
}): Promise<QueryTaskCached<TDataType> | QueryTaskCompleted<TDataType>>;
/**
* Executes a query that remains subscribed to cache mutations after the initial fetch completes.
*
* Unlike {@link Query.query}, which completes after emitting the result, `persistentQuery`
* continues to emit whenever the underlying cache entry is updated or mutated.
* This is useful for scenarios where the UI must reflect optimistic updates,
* background refetches, or external cache mutations in real time.
*
* The returned Observable deduplicates emissions based on the transaction identifier
* and mutation timestamp, so subscribers only receive meaningful state changes.
*
* @param args - The arguments to pass to the query function.
* @param options - Optional query options including signal, retry, and cache validation overrides.
* @returns An Observable that emits cached and completed results, and continues emitting on cache mutations.
*/
persistentQuery(args: TQueryArguments, options?: QueryOptions<TDataType, TQueryArguments>): Observable<QueryTaskCached<TDataType> | QueryTaskCompleted<TDataType>>;
/**
* Generates a cache key based on the provided query arguments.
* This method is used internally to uniquely identify cache entries.
*
* @param args - The query arguments to be used for generating the cache key.
* @returns A string representing the cache key.
*/
generateCacheKey(args: TQueryArguments): string;
/**
* Performs a mutation on the cache entry associated with the given arguments.
* This method allows for updating the state of a cache entry without needing to perform a new query.
* The changes are applied by invoking the `mutate` method on the cache with the generated key and the changes function.
*
* @param args - The arguments that identify the specific cache entry to be mutated.
* @param changes - A function that defines the changes to be applied to the cache entry.
*/
mutate(args: TQueryArguments, changes: Parameters<QueryCache<TDataType, TQueryArguments>['mutate']>[1], options?: {
allowCreation?: boolean;
}): VoidFunction;
/**
* Invalidates a specific cache record or all records if no arguments are provided.
* When a specific record is invalidated, it is identified by the provided arguments.
* If no arguments are provided, all records in the cache are invalidated, effectively clearing the cache.
*
* @param args - Optional arguments that identify the specific cache record to be invalidated.
* If not provided, all cache records will be invalidated.
*/
invalidate(args?: TQueryArguments): void;
/**
* Completes all subscriptions and cleans up resources.
* This method should be called when the Query instance is no longer needed, to ensure that all resources are properly released.
* Failing to call `complete` could result in memory leaks due to lingering subscriptions.
*/
complete(): void;
/**
* Registers a callback function that will be invoked when a cache invalidation occurs.
* The callback function will receive an event object containing the details of the invalidation,
* including the affected cache entry, if available.
* The returned function can be called to unsubscribe the callback from further invalidation events.
*
* @param cb - The callback function to be registered.
* @returns A function that, when called, will unsubscribe the callback from further invalidation events.
*/
onInvalidate(cb: (e: {
detail: {
item?: QueryCacheRecord;
};
}) => void): VoidFunction;
/**
* Registers a callback function that will be invoked when a mutation occurs on the cache.
* The callback function will receive an event object containing the details of the mutation,
* including the changes made and the current state of the cache entry, if available.
*
* @param cb - The callback function to be registered.
* @returns A function that, when called, will unsubscribe the callback from further mutation events.
*/
onMutate(cb: (e: {
detail: {
changes: QueryCacheMutation;
current?: QueryCacheRecord<TDataType, TQueryArguments>;
};
}) => void): VoidFunction;
/**
* Internal method that executes a query and returns an Observable with the result.
*
* @param args - The arguments to be passed to the query function.
* @param options - Optional additional options for the query.
* @returns An Observable that emits the result of the query.
*/
protected _query(args: TQueryArguments, options?: QueryOptions<TDataType, TQueryArguments>): Observable<QueryTaskCached<TDataType> | QueryTaskCompleted<TDataType>>;
/**
* The `_createTask` method is responsible for initiating a query task, which involves either returning a cached result or
* starting a new query request. This method encapsulates the logic for creating and managing the lifecycle of a query task.
* It ensures that if a query with the same arguments is initiated multiple times, they will all share the same task observable,
* thus avoiding duplicate network requests and unnecessary computation.
*
* @param key - A string that uniquely identifies the cache entry associated with the query arguments. This key is used to
* check if there is already a cached result that can be returned immediately, avoiding the need for a new network request.
* @param args - The arguments that will be passed to the query function. These arguments are used to generate the cache key
* and are also passed to the query function when making a new request.
* @param options - An optional object containing additional options for the query. This can include custom cache validation
* logic and retry strategies. For example, options can specify whether to suppress the emission of invalid
* cache entries or to define a custom function for validating the cache.
*
* @returns An Observable that emits the result of the query task. If a valid cache entry is found, the Observable will emit
* the cached result. If no valid cache entry exists, the Observable will emit the result of a new query request once
* it completes. Subscribers to this Observable will receive updates on the query's execution and results.
*
* The method performs the following steps:
* 1. Creates a new Observable that represents the task to be executed.
* 2. Attempts to retrieve a cache entry using the provided `key`.
* 3. If a cache entry is found, it checks whether the entry is still valid using the provided validation function or the default one.
* - If the cache entry is valid, it emits the cached result and completes the Observable.
* - If the cache entry is not valid or does not exist, it proceeds to the next step.
* 4. Checks if there is already an ongoing task for the same query (identified by the `key`).
* - If there is an ongoing task, it subscribes the new Observable to the existing task.
* - If there is no ongoing task, it creates a new `QueryTask` instance and adds it to the ongoing tasks record.
* 5. Subscribes to the new or existing task, forwarding any emissions to the subscriber of the returned Observable.
* 6. Adds the new task to the query queue by emitting the `key` on the `#queue$` Subject, which will eventually trigger the processing
* of the task based on the queuing strategy.
* 7. The task is finalized (removed from the ongoing tasks record) when it completes or errors out.
*
* Note: The subscribers to the returned Observable are responsible for subscribing and unsubscribing to manage the lifecycle of the
* subscription. The method ensures that the task is properly cleaned up when it is no longer needed.
*/
protected _createTask(key: string, args: TQueryArguments, options?: QueryOptions<TDataType, TQueryArguments>): Observable<QueryTaskCached<TDataType> | QueryTaskCompleted<TDataType>>;
}
export default Query;