import { Awaitable, CallContext, CallContextLike, HoistService, LoadSpec, LoadSpecConfig, PlainObject, TrackOptions, Span } from '@xh/hoist/core'; import { HoistException } from '@xh/hoist/exception'; import { PromiseTimeoutSpec } from '@xh/hoist/promise'; import { StatusCodes } from 'http-status-codes'; import { IStringifyOptions } from 'qs'; export interface FetchServiceDefaults { autoGenCorrelationIds?: boolean | ((opts: FetchOptions) => boolean); correlationIdHeaderKey?: string; genCorrelationId?: () => string; } /** * Service for making managed HTTP requests, both to the app's own Hoist server and to remote APIs. * * Typically accessed via `XH.fetchService` or the matching convenience aliases on `XH` * (`XH.fetchJson()`, `XH.postJson()`, etc.), which delegate here. * * Wraps the standard Fetch API with CORS enabled, credentials included, and redirects followed. * Provides JSON convenience methods (`fetchJson`, `postJson`, `putJson`, `patchJson`, * `deleteJson`, `getJson`) that handle serialization and content-type headers automatically, * plus `fetchNdjson` for consuming streamed NDJSON responses incrementally. * * Key features: * - Configurable timeouts (default 30s) via {@link FetchOptions.timeout} * - Auto-abort of duplicate in-flight requests via {@link FetchOptions.autoAbortKey} * - Optional correlation IDs for request tracking (see `defaults.autoGenCorrelationIds`) * - Request/response interceptors via {@link addInterceptor} * - Default headers for all requests via {@link addDefaultHeaders} * - Rich exception handling with HTTP status, server messages, and trace IDs * * All convenience methods accept the same {@link FetchOptions} as the main `fetch()` entry point. * * @see FetchOptions */ export declare class FetchService extends HoistService { static instance: FetchService; /** App-level defaults for FetchService. Instance options take precedence. */ static defaults: FetchServiceDefaults; NO_JSON_RESPONSES: StatusCodes[]; /** * Regex applied during failed response handling to determine if contentType indicates JSON. * Matches `application/json` as well as variants such as `application/problem+json` */ JSON_CONTENT_TYPE_RE: RegExp; private autoAborters; private _defaultHeaders; private _interceptors; private interners; /** Default timeout to be used for all requests made via this service */ defaultTimeout: PromiseTimeoutSpec; /** Default headers to be sent with all subsequent requests. */ get defaultHeaders(): DefaultHeaders[]; /** * Promise handlers to be executed before fulfilling or rejecting returned Promise. * * Use the `onRejected` handler for apps requiring common handling for particular exceptions. * Useful for recognizing 401s (i.e., session end), or wrapping, logging, or enhancing exceptions. * The simplest onRejected handler will simply rethrow the passed exception, or a wrapped version of it. * Such handlers may also return `never()` to prevent further processing of the request -- this * is useful, i.e., if the handler is going to redirect the entire app, or otherwise end normal * app processing. Rejected handlers may also be able to retry and return valid results via * another call to fetch. * * Use the `onFulfilled` handler for enhancing, tracking, or even rejecting "successful" returns. * For example, a handler of this form could be used to transform a 200 response returned by * an API with an "error" flag into a proper client-side exception. */ addInterceptor(handler: FetchInterceptor): void; /** * Add default headers to be sent with all subsequent requests. * @param headers - to be sent with all fetch requests, or a function to generate. */ addDefaultHeaders(headers: DefaultHeaders): void; /** * Send a request via the underlying fetch API. * * This is the main entry point for this API, and can be used to satisfy all * requests. Other shortcut variants will delegate to this method, after setting * default options and pre-processing content. * * Set `asJson` to true return a parsed JSON result, rather than the raw Response. * Note that shortcut variant of this method (e.g. `fetchJson`, `postJson`) will set this * flag for you. * * @param opts - request options. * @param ctx - optional {@link CallContextLike} supplying parent span and load context. * @returns Promise which resolves to a Response or JSON. */ fetch(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send an HTTP request and decode the response as JSON. * @returns the decoded JSON object, or null if the response has status in {@link NO_JSON_RESPONSES}. */ fetchJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send a GET request and decode the response as JSON. * @returns the decoded JSON object, or null if the response status is in {@link NO_JSON_RESPONSES}. */ getJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send an HTTP request and decode the response body incrementally as NDJSON - newline * delimited JSON, aka JSON Lines / JSONL. Returns an {@link NdjsonResult} whose `lines` * generator yields parsed records one at a time as they arrive off the network. No more * than one network chunk of raw text is buffered, making this suitable for consuming * very large or long-running streamed responses. * * The natural source for {@link Store.loadDataAsync} - e.g. * `store.loadDataAsync(XH.fetchNdjson({url}).lines)` - or iterate `lines` directly via * `for await` for non-Store streaming. * * Set {@link NdjsonFetchOptions.firstLineIsMeta} to treat the first record in the stream as * out-of-band metadata, delivered via the result's `meta` promise rather than `lines`. The * promise resolves as soon as the record arrives - before `lines` is consumed - so callers * can use it to decide how to process the balance of the stream. * * Tracing spans and `track` cover the full lifetime of the stream, through complete * consumption. Note that `timeout` covers the request phase only - no timeout applies while * the stream is being read. * * A stream truncated by a server-side failure surfaces as a 'Fetch Stream Failed' exception - * hoist-core's `renderNdjson` guarantees such a stream ends with an unparseable line. */ fetchNdjson(opts: NdjsonFetchOptions, ctx?: CallContextLike): NdjsonResult; /** * Send a POST request with a JSON body and decode the response as JSON. * @returns the decoded JSON object, or null if the response status is in {@link NO_JSON_RESPONSES}. */ postJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send a PUT request with a JSON body and decode the response as JSON. * @returns the decoded JSON object, or null if the response status is in {@link NO_JSON_RESPONSES}. */ putJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send a PATCH request with a JSON body and decode the response as JSON. * @returns the decoded JSON object, or null if the response status is in {@link NO_JSON_RESPONSES}. */ patchJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Send a DELETE request with optional JSON body and decode the optional response as JSON. * @returns the decoded JSON object, or null if the response status is in {@link NO_JSON_RESPONSES}. */ deleteJson(opts: FetchOptions, ctx?: CallContextLike): Promise; /** * Manually abort any pending request for a given autoAbortKey. * @returns false if no request pending for the given key. */ abort(autoAbortKey: string): boolean; /** * Clear string-interning caches maintained for {@link FetchOptions.internStrings} - all of * them, or just the cache for a single key. * * Interned strings referenced by live records remain retained by those records - this * releases only the cache's own references. Useful after tearing down large views whose * datasets will not be refetched, where the cache would otherwise continue to retain the * last response's distinct values. * * @param key - specific {@link StringInternSpec.key} to clear, or omit to clear all. */ clearInternCaches(key?: string): void; /** * Snapshot of string-interning stats for each active {@link FetchOptions.internStrings} * key, covering the most recently completed response per key: total string values * processed, distinct values retained (with % of processed - lower = more duplication * removed), and values carried over from the prior generation (with % of retained - * higher = more stability across refreshes). * * Convenient from the console via `console.table(XH.fetchService.getInternStats())`. */ getInternStats(): PlainObject[]; private static readonly defaultIdGenerator; /** * @param forStreaming - true when called by fetchNdjson, which applies its own span and * track across the full stream lifetime - suppresses both here. */ private fetchInternalAsync; private sendJsonInternalAsync; private withCorrelationId; private withTraceId; private withResolvedHeadersAsync; private managedFetchAsync; private abortableFetchAsync; private parseJsonAsync; private safeResponseTextAsync; private createSpanConfig; /** Prefix relative URLs with {@link XH.baseUrl}; leave absolute/root-relative URLs as-is. */ private resolveUrl; private buildFullUrl; private qsFilterFn; /** * Create an Error to throw when a fetch call returns a !ok response. * @param fetchOptions - original options passed to FetchService. * @param response - return value of native fetch. * @param responseText - optional additional details from the server. */ private exceptionFromResponse; /** * Get or create the {@link StringInterner} for the given spec's key, or null if interning * was not requested. Interners are retained per key with the latest spec adopted on each * call - see {@link clearInternCaches} to reset. */ private getInterner; /** * Create an Error to throw when a fetchJson call encounters a SyntaxError. * @param fetchOptions - original options passed to FetchService. * @param cause - object thrown by native {@link response.json}. */ private jsonParseException; /** * Create an Error to throw when a fetch call is aborted. * @param fetchOptions - original options passed to FetchService. * @param cause - object thrown by native fetch */ private abortedException; /** * Create an Error to throw when a fetch call times out. * @param fetchOptions - original options the app passed when calling FetchService. * @param cause - underlying timeout exception * @param message - optional custom message * * @returns an exception that is both a TimeoutException, and a FetchException, with the * underlying TimeoutException as its cause. */ private timeoutException; /** * Create an Error to throw when a fetch call fails while reading or parsing its streamed * response body. * @param fetchOptions - original options passed to FetchService. * @param response - response whose body was being streamed. * @param cause - underlying error raised while reading or parsing the stream. */ private streamFailedException; /** * Create an Error for when the server called by fetch does not respond * @param fetchOptions - original options the app passed to FetchService.fetch * @param cause - object thrown by native fetch */ private serverUnavailableException; private createException; private safeParseJson; private extractMessage; } /** Headers to be applied to all requests. Specified as object, or dynamic function to create. */ export type DefaultHeaders = PlainObject | ((opts: FetchOptions) => Awaitable); /** Handlers to be executed before fufilling or rejecting any exception to caller. */ export interface FetchInterceptor { onFulfilled: (opts: FetchOptions, value: any) => Promise; onRejected: (opts: FetchOptions, cause: unknown) => Promise; } /** * Standard options to pass through to fetch, with some additions. * See MDN for available options - {@link https://developer.mozilla.org/en-US/docs/Web/API/Request}. */ export interface FetchOptions { /** URL for the request. Relative urls will be appended to XH.baseUrl. */ url: string; /** * Data to send in the request body (for POSTs/PUTs of JSON). * When using `fetch`, provide a string. Otherwise, provide a JSON Serializable object */ body?: any; /** * Unique identifier for this request, used for tracking and logging. If `false`, no * `correlationId` will be set. If `true`, one will be auto-generated. */ correlationId?: string | boolean; /** * Parameters to encode and append as a query string, or send with the request body * (for POSTs/PUTs sending form-url-encoded). */ params?: PlainObject; /** * HTTP Request method to use for the request. If not specified, the method will be set to POST * if there are params, otherwise GET. */ method?: string; /** * Headers to send with this request. A Content-Type header will be set if not provided by * the caller directly or via one of the xxxJson convenience methods. */ headers?: PlainObject; /** * MS to wait for response before rejecting with a timeout exception. Defaults to 30 seconds, * but may be specified as null to specify no timeout. */ timeout?: PromiseTimeoutSpec; /** * Optional metadata about the underlying request. Passed through for downstream processing by * utils such as {@link ExceptionHandler}. * * @deprecated Pass a {@link CallContextLike} as the second argument to the fetch method instead. */ loadSpec?: LoadSpec | LoadSpecConfig; /** * Options to pass to the underlying fetch request. * @see https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch */ fetchOpts?: PlainObject; /** * Options for qs, the library used to encode query strings. */ qsOpts?: Partial; /** * If set, any pending requests made with the same autoAbortKey will be immediately * aborted in favor of the new request. */ autoAbortKey?: string; /** * If set, intern string values in array-based JSON and NDJSON responses to reduce retained * memory on large tabular datasets - each distinct string value is stored once and shared * across all rows, rather than duplicated per row as produced by JSON parsing. * * Applies to string values at the root level of each object within an array response (or * each NDJSON record). A single plain-object response is treated as a root record and * processed likewise. Nested values are not processed, with the exception of recursion * into child records via `childrenKey`. No-op for response payloads of any other shape. * * Interned values are also optionally shared across successive responses with the same `key` - * e.g. a polling refresh of the same grid - with cache retention per each key's * {@link StringInternSpec.retainMode}, by default bounded to the values present in the most * recent complete response. */ internStrings?: StringInternSpec; /** * True to decode the HTTP response as JSON. Default false. */ asJson?: boolean; /** * If set, the request will be tracked via Hoist activity tracking. (Do not set `correlationId` * here - use the top-level `correlationId` property instead.) */ track?: string | TrackOptions; /** * Parent span for this fetch request. Use to nest fetch calls under a business-level span. * * @deprecated Pass a {@link CallContextLike} as the second argument to the fetch method instead. */ span?: Span; /** * Distributed trace ID for this request. Set automatically by FetchService * @internal */ traceId?: string; } /** Options for {@link FetchService.fetchNdjson}. */ export interface NdjsonFetchOptions extends FetchOptions { /** * True to treat the first record in the stream as metadata, delivered via * {@link NdjsonResult.meta} rather than yielded with the data records. Default false. */ firstLineIsMeta?: boolean; } /** Streamed result returned by {@link FetchService.fetchNdjson}. */ export interface NdjsonResult { /** Parsed data records, yielded individually as they arrive off the network. */ lines: AsyncGenerator; /** * Leading metadata record - null unless requested via * {@link NdjsonFetchOptions.firstLineIsMeta}. Resolves as soon as the record arrives, * without requiring `lines` to be consumed - null if the stream was empty. */ meta: Promise | null; } /** * Spec for string-value interning of a fetch response. * @see FetchOptions.internStrings */ export interface StringInternSpec { /** * Identifies the logical dataset. Successive responses fetched with the same key share * interned values across fetches, with cache retention bounded to the latest response. * Cleared via {@link FetchService.clearInternCaches}. */ key: string; /** * Property of each record containing nested child records to recurse into, for tree data - * typically 'children'. Match to the consuming Store's `loadTreeDataFrom` config. Default * null - no recursion. */ childrenKey?: string; /** * Record properties to skip when interning. Use for fields whose values are known to be * unique or nearly so - e.g. UUIDs, formatted timestamps, or free-text notes - where * interning adds per-value lookup time and per-distinct-value cache entries with little or * no deduplication benefit. Default null - all string-valued properties are interned. */ excludeFields?: string[]; /** * How long interned values are held for reuse by later responses with the same key. * Default 'nextCall'. * * - 'nextCall' (default) - hold the values in each committed response for reuse by the next. * Values not re-seen are evicted, bounding the cache to the latest response - the right * mode for polling/refresh of a comparable dataset. * - 'always' - hold every value ever committed, until {@link FetchService.clearInternCaches}. * Useful when successive responses cover different slices of a dataset (e.g. paging, or * alternating filters), where 'nextCall' would evict values about to recur. * - 'never' - intern within each response only. Appropriate for large one-shot datasets that * will not be refetched, where a retained cache would pin the last response's distinct * values for no future benefit. * * May be varied across calls sharing a key without resetting the cache - the mode governs * only how each completing response's values are installed for reuse. */ retainMode?: 'never' | 'nextCall' | 'always'; } /** * Exception thrown to indicate an HTTP error resulting from a call to FetchService. */ export interface FetchException extends HoistException { /** Http Status code associated with exception. 0 if no response received. */ httpStatus: number; /** Rich object or string containing details about the exception as sent by server. */ serverDetails: string | PlainObject; /** Options of underlying fetch call. */ fetchOptions: FetchOptions; /** CallContext (parent span / load context) in effect when the fetch was issued. */ callContext: CallContext; /** Distributed trace ID associated with the failed request, if tracing was enabled. */ traceId: string; /** * True if exception resulted from the fetch being aborted by fetchService, or the application. * @see FetchService.abort * @see FetchOptions.autoAbortKey */ isFetchAborted: boolean; }