UNPKG

@equinor/fusion-query

Version:

Reactive data fetching and caching library with observable streams and comprehensive event system

159 lines 8.6 kB
import { BehaviorSubject, Observable, fromEvent } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { QueryClientError } from './QueryClientError'; import { actions } from './actions'; /** * Represents a job for the QueryClient that extends an Observable. * It manages the lifecycle of a query operation. */ export class QueryClientJob extends Observable { /** * The current status of the job. */ #status$ = new BehaviorSubject('idle'); /** * Returns the current status of the job. */ get status() { return this.#status$.value; } get status$() { return this.#status$.asObservable(); } /** * The QueryClient instance associated with this job. */ #client; /** * Returns the QueryClient instance associated with this job. */ #transaction; /** * Returns the transaction identifier for this job. */ get transaction() { return this.#transaction; } created = Date.now(); /** * Indicates whether the query client job is closed. * A query client job is considered closed if its status is 'complete', 'error', or 'canceled'. */ get closed() { return this.status === 'complete' || this.status === 'error' || this.status === 'canceled'; } /** * Creates an instance of a QueryClientJob. * @param client The QueryClient instance. * @param args The arguments to be used for the query. * @param options Optional settings for the query client job. */ constructor(client, args, options) { super((subscriber) => { const { transaction } = this; // Filters the actions stream for actions related to this job's transaction. const jobActions = client.action$.pipe(filter((action) => action.meta.transaction === transaction)); // Subscribes to the state stream, updating the job's status when a related state entry is emitted. subscriber.add(client.state$ .pipe( // Filters the state stream for entries related to this job's transaction. filter((state) => !!state[transaction]), // Maps the filtered state to the specific entry for this job, extracting its current status. map((state) => state[transaction])) .subscribe((entry) => { // Updates the internal BehaviorSubject with the new status. // This change is then propagated to all subscribers of the status$ Observable, // allowing them to react to status changes in real-time. this.#status$.next(entry.status); })); // Subscribes to the actions stream, completing the job when a successful execution action is emitted. subscriber.add(jobActions .pipe(filter(actions.execute.success.match), // Maps the successful execution action to its payload, effectively extracting the result of the query operation. // This result is then passed along to the subscribers of the QueryClientJob, allowing them to react to the successful completion of the job. map(({ payload }) => payload)) .subscribe((result) => { // Updates the job's status to 'complete', indicating that the query operation has successfully finished. this.#status$.next('complete'); // Notifies subscribers of the QueryClientJob with the result of the query operation. subscriber.next(result); // Completes the subscription, signaling that the job has finished and no more values will be emitted. // This is an important step to ensure that subscribers can perform any necessary cleanup or final actions. subscriber.complete(); })); // Subscribes to the actions stream, emitting an error when an error action is emitted. subscriber.add(jobActions .pipe(filter(actions.error.match), map(({ payload }) => payload)) .subscribe((err) => { this.#status$.next('error'); // Emits an error to the subscriber with the error details from the payload. // This triggers the error handling mechanisms of the subscriber, allowing for // appropriate responses to the error condition. subscriber.error(err.error); })); // Subscribes to the actions stream, updating the job's status when a cancel action is emitted. subscriber.add(jobActions.pipe(filter(actions.cancel.match)).subscribe((action) => { this.#status$.next('canceled'); // When a cancel action is received, it updates the job's status to 'canceled' and emits an error with a detailed message. // This includes the reason for cancellation if provided, or a default message indicating the job was canceled. // Additionally, it constructs a new `QueryClientError` with the 'abort' type, providing context about the request // associated with the canceled job, allowing for more informed error handling and debugging. subscriber.error(new QueryClientError('abort', { message: action.payload.reason || `job: ${transaction} was canceled`, request: this.#client.getRequest(action.meta.transaction), })); })); // If an abort signal is provided, subscribes to the abort event and dispatches a cancel action. if (options?.signal) { // Subscribes to the abort event on the provided signal. When the abort event is triggered, // it dispatches a cancel action with a specific message indicating the job was aborted // due to an external signal. This allows for external cancellation mechanisms to interact // with the job lifecycle. subscriber.add(fromEvent(options?.signal, 'abort') .pipe( // Maps the abort event to a cancel action, including a message that specifies // the transaction ID and an optional reference provided in the options. // This ensures that the cancellation is traceable to the abort signal. map(() => actions.cancel(transaction, `job: ${transaction} was aborted on signal from caller: ${options?.ref}`))) // Subscribes to the resulting Observable and dispatches the cancel action // to the QueryClient. This integrates the external abort mechanism with // the QueryClient's internal state management and action handling. .subscribe((action) => client.next(action))); } // Adds a teardown logic to cancel the job if it's still active upon unsubscription. subscriber.add(() => { this.complete(); }); }); this.#client = client; // Dispatches a request action to start the job. const request = actions.request(args, { ref: options?.ref, retry: options?.retry }); this.#transaction = request.meta.transaction; this.#client.next(request); } /** * Completes the job if it's not already closed, marking it as 'complete'. * This method should be called to gracefully end a job that has finished its task. * It will also attempt to cancel the job with a completion reason, transitioning its status to 'canceled' * if it was in an 'active' state but hadn't yet completed. */ complete(reason) { if (!this.closed) { this.cancel(reason ?? `job: ${this.transaction} was completed`); } } /** * Cancels the job with an optional reason, marking its status as 'canceled'. * This method is used to prematurely end a job that cannot complete its task, either due to an error or a user-defined condition. * Upon cancellation, a cancellation action is dispatched to notify other parts of the application that the job has been canceled. * @param reason The reason for the cancellation, which will be included in the cancellation action. */ cancel(reason) { const transaction = this.transaction; this.#client.cancel(transaction, reason ?? `job: ${transaction} was canceled`); } [Symbol.dispose]() { this.complete(); } } //# sourceMappingURL=QueryClientJob.js.map