@warp-drive-mirror/utilities 5.10.0-alpha.13 → 5.10.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.js","names":["isCompressibleMethod","method","SupportsRequestStreams","duplexAccessed","hasContentType","Request","body","ReadableStream","duplex","headers","has","DEFAULT_CONSTRAINTS","Blob","ArrayBuffer","TypedArray","DataView","String","Object","getPrototypeOf","Uint8Array","AutoCompress","constructor","options","opts","format","constraints","assign","allowStreaming","forceStreaming","request","next","shouldCompress","compress","canCompress","length","size","byteLength","response","Response","stream","pipeThrough","CompressionStream","Headers","set","encodingForFormat","req","resp","blob","then","type","test","Error","Gate","constructor","handler","checkFn","request","context","next","FastBoot","globalThis","addEventListener","sessionStorage","setItem","getTabId","crypto","randomUUID","tabId","getItem","tabClosed","newTabId","TAB_ID","TAB_ASSIGNED","Math","floor","Date","now","addTraceHeader","headers","set","MAX_URL_LENGTH","assertInvalidUrlLength","url","test","Error","length"],"sources":["../../../src/-private/handlers/auto-compress.ts","../../../src/-private/handlers/gated.ts","../../../src/-private/handlers/meta-doc.ts","../../../src/-private/handlers/utils.ts"],"sourcesContent":["import { assert } from '@warp-drive-mirror/core/build-config/macros';\nimport type { Future, Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { HTTPMethod, RequestContext } from '@warp-drive-mirror/core/types/request';\n\nfunction isCompressibleMethod(method?: HTTPMethod): boolean {\n return method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE';\n}\n\n/**\n * Whether the browser supports `ReadableStream` as a request body\n * in a `POST` request.\n *\n * @summary Whether the current browser can send a `ReadableStream` as a `fetch` request body, detected once at\n * module load.\n * @group Constants\n */\nexport const SupportsRequestStreams: boolean = (() => {\n let duplexAccessed = false;\n\n const hasContentType = new Request('', {\n body: new ReadableStream(),\n method: 'POST',\n // @ts-expect-error untyped\n get duplex() {\n duplexAccessed = true;\n return 'half';\n },\n }).headers.has('Content-Type');\n\n return duplexAccessed && !hasContentType;\n})();\n\ninterface Constraints {\n /**\n * The minimum size at which to compress blobs\n *\n * @default 1000\n */\n Blob?: number;\n /**\n * The minimum size at which to compress array buffers\n *\n * @default 1000\n */\n ArrayBuffer?: number;\n /**\n * The minimum size at which to compress typed arrays\n *\n * @default 1000\n */\n TypedArray?: number;\n /**\n * The minimum size at which to compress data views\n *\n * @default 1000\n */\n DataView?: number;\n /**\n * The minimum size at which to compress strings\n *\n * @default 1000\n */\n String?: number;\n}\n\n/**\n * Options for configuring the AutoCompress handler.\n *\n */\ninterface CompressionOptions {\n /**\n * The compression format to use. Must be a valid\n * compression format supported by [CompressionStream](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)\n *\n * The default is `gzip`.\n *\n */\n format?: CompressionFormat;\n\n /**\n * Some browsers support `ReadableStream` as a request body. This option\n * enables passing the compression stream as the request body instead of\n * the final compressed body when the browser supports doing so.\n *\n * This comes with several caveats:\n *\n * - the request will be put into `duplex: 'half'` mode. This should be\n * transparent to you, but it is worth noting.\n * - the request mode cannot be `no-cors` as requests with a `ReadableStream`\n * have no content length and thus are a new form of request that triggers\n * cors requirements and a preflight request.\n * - http/1.x is not supported.\n *\n * For additional reading about the restrictions of using `ReadableStream`\n * as a request body, see the [Chromium Documentation](https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests#restrictions)\n *\n * Streaming can be enabled per-request in browsers which support it by\n * setting `request.options.allowStreaming` to `true`.\n *\n * Streaming can be forced even when the browser does not support it by setting\n * `request.options.forceStreaming` to `true`. This is useful if later handlers\n * in the chain can handle the request body as a stream.\n *\n * @default false\n */\n allowStreaming?: boolean;\n\n /**\n * If `true`, the request will be forced into streaming mode even\n * if the browser does not support it. This is useful if later handlers\n * in the chain can handle the request body as a stream.\n *\n * @default false\n */\n forceStreaming?: boolean;\n\n /**\n * The constraints for the request body. This is used to determine\n * whether to compress the request body or not.\n *\n * The defaults are:\n *\n * ```ts\n * {\n * Blob: 1000, // blob.size\n * ArrayBuffer: 1000, // buffer.byteLength\n * TypedArray: 1000, // array.byteLength\n * DataView: 1000, // view.byteLength\n * String: 1000, // string.length\n * }\n * ```\n *\n * The following body types are never compressed unless explicitly\n * configured by the request:\n * - `FormData`\n * - `URLSearchParams`\n * - `ReadableStream`\n *\n * A request.options.compress value of `false` will disable\n * compression for a request body of any type. While a value of\n * `true` will enable compression for the request.\n *\n * An undefined value will use the default, a value of `0` will\n * enable compression for all values, and a value of `-1` will\n * disable compression.\n *\n */\n constraints?: Constraints;\n}\n\nconst DEFAULT_CONSTRAINTS = {\n Blob: 1000,\n ArrayBuffer: 1000,\n TypedArray: 1000,\n DataView: 1000,\n String: 1000,\n};\nconst TypedArray = Object.getPrototypeOf(Uint8Array) as typeof Uint8Array;\n\n/**\n * A request handler that automatically compresses the request body\n * if the request body is a string, array buffer, blob, or form data.\n *\n * This uses the [CompressionStream API](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)\n *\n * The compression format as well as the kinds of data to compress can be\n * configured using the `format` and `constraints` options.\n *\n * ```diff\n * +import { AutoCompress } from '@ember-data-mirror/request-utils/handlers';\n * import Fetch from '@ember-data-mirror/request/fetch';\n * import RequestManager from '@ember-data-mirror/request';\n * import Store from '@ember-data-mirror/store';\n *\n * class AppStore extends Store {\n * requestManager = new RequestManager()\n * .use([\n * + new AutoCompress(),\n * Fetch\n * ]);\n * }\n * ```\n *\n * @summary Request handler that compresses large `POST`, `PUT`, `PATCH`, and `DELETE` bodies with `CompressionStream`,\n * optionally streaming them.\n * @group Handlers\n * @public\n * @since 5.5.0\n */\nexport class AutoCompress implements Handler {\n /**\n * The resolved options this handler was configured with, with all\n * defaults (including `constraints` defaults) applied.\n */\n declare options: Required<CompressionOptions> & {\n /**\n * The resolved size constraints used to decide whether to compress a given request body.\n */\n constraints: Required<Constraints>;\n };\n\n constructor(options: CompressionOptions = {}) {\n const opts = {\n format: options.format ?? 'gzip',\n constraints: Object.assign({}, DEFAULT_CONSTRAINTS, options.constraints),\n allowStreaming: options.allowStreaming ?? false,\n forceStreaming: options.forceStreaming ?? false,\n };\n this.options = opts;\n }\n\n request<T>({ request }: RequestContext, next: NextFn<T>): Promise<T> | Future<T> {\n const { constraints } = this.options;\n const { body } = request;\n\n const shouldCompress =\n !!body &&\n isCompressibleMethod(request.method) &&\n request.options?.compress !== false &&\n // prettier-ignore\n (request.options?.compress ? true\n : typeof body === 'string' || body instanceof String ? canCompress('String', constraints, body.length)\n : body instanceof Blob ? canCompress('Blob', constraints, body.size)\n : body instanceof ArrayBuffer ? canCompress('ArrayBuffer', constraints, body.byteLength)\n : body instanceof DataView ? canCompress('DataView', constraints, body.byteLength)\n : body instanceof TypedArray ? canCompress('TypedArray', constraints, body.byteLength)\n : false);\n\n if (!shouldCompress) return next(request);\n\n // A convenient way to convert all of the supported body types to a readable\n // stream is to use a `Response` object body\n const response = new Response(request.body);\n const stream = response.body?.pipeThrough(new CompressionStream(this.options.format));\n const headers = new Headers(request.headers);\n headers.set('Content-Encoding', encodingForFormat(this.options.format));\n\n //\n // For browsers that support it, `fetch` can receive a `ReadableStream` as\n // the body, so all we need to do is to create a new `ReadableStream` and\n // compress it on the fly\n //\n const forceStreaming = request.options?.forceStreaming ?? this.options.forceStreaming;\n const allowStreaming = request.options?.allowStreaming ?? this.options.allowStreaming;\n if (forceStreaming || (SupportsRequestStreams && allowStreaming)) {\n const req = Object.assign({}, request, {\n body: stream,\n headers,\n });\n if (SupportsRequestStreams) {\n // @ts-expect-error untyped\n req.duplex = 'half';\n }\n\n return next(req);\n\n //\n // For non-chromium browsers, we have to \"pull\" the stream to get the final\n // bytes and supply the final byte array as the new request body.\n //\n }\n\n // we need to pull the stream to get the final bytes\n const resp = new Response(stream);\n return resp.blob().then((blob) => {\n const req = Object.assign({}, request, {\n body: blob,\n headers,\n });\n return next(req);\n }) as Promise<T>;\n }\n}\n\nfunction canCompress(type: keyof Constraints, constraints: Required<Constraints>, size: number): boolean {\n // if we have a value of 0, we can compress anything\n if (constraints[type] === 0) return true;\n if (constraints[type] === -1) return false;\n return size >= constraints[type];\n}\n\nfunction encodingForFormat(format: CompressionFormat): string {\n switch (format) {\n case 'gzip':\n case 'deflate':\n case 'deflate-raw':\n return format;\n default:\n assert(`Unsupported compression format: ${format as unknown as string}`);\n // @ts-expect-error - unreachable code is reachable in production\n return format;\n }\n}\n","import type { Future, Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { RequestContext, StructuredDataDocument } from '@warp-drive-mirror/core/types/request';\n\n/**\n * If CheckFn returns true, the wrapped handler will be used.\n * If CheckFn returns false, the wrapped handler will be skipped.\n */\ntype CheckFn = (context: RequestContext) => boolean;\n\n/**\n *\n * @summary Request handler that wraps another handler and runs it only when a check function returns true for the\n * request.\n * @group Handlers\n * @public\n */\nexport class Gate implements Handler {\n /**\n * The wrapped handler to invoke when {@link Gate.checkFn | checkFn} returns `true`.\n */\n declare handler: Handler;\n /**\n * The predicate used to decide whether {@link Gate.handler | handler} should run for a given request.\n */\n declare checkFn: CheckFn;\n\n constructor(handler: Handler, checkFn: CheckFn) {\n this.handler = handler;\n this.checkFn = checkFn;\n }\n\n request<T = unknown>(context: RequestContext, next: NextFn<T>): Promise<T | StructuredDataDocument<T>> | Future<T> {\n if (this.checkFn(context)) {\n return this.handler.request(context, next);\n }\n return next(context.request);\n }\n}\n","import type { StoreRequestContext } from '@warp-drive-mirror/core';\nimport type { Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { StructuredDataDocument } from '@warp-drive-mirror/core/types/request';\n\n/**\n * MetaDocHandler processes requests that are marked as meta requests.\n *\n * It treats the response body as \"entirely meta\" transforming\n *\n * ```ts\n * {\n * some: \"key\",\n * another: \"thing\"\n * }\n * ```\n *\n * into\n *\n * ```ts\n * {\n * \t meta: {\n * some: \"key\",\n * another: \"thing\"\n * }\n * }\n * ```\n *\n * To activate this handler, a request should specify\n *\n * ```ts\n * options.isMetaRequest = true\n * ```\n *\n * For instance\n *\n * ```ts\n * store.request({\n * url: '/example',\n * options: {\n * isMetaRequest: true\n * }\n * });\n * ```\n *\n * Errors are not processed by this handler, so if the request fails and the error response\n * is not in {json:api} format additional processing may be needed.\n *\n * @summary Request handler that wraps the whole response body in `meta` for requests with `options.isMetaRequest` set.\n * @group Handlers\n */\nexport const MetaDocHandler: Handler = {\n request<T>(context: StoreRequestContext, next: NextFn<T>): Promise<T | StructuredDataDocument<T> | Awaited<T>> {\n if (!context.request.options?.isMetaRequest) {\n return next(context.request);\n }\n\n return next(context.request).then((response) => {\n return processResponse<T>(response);\n });\n },\n};\n\nfunction processResponse<T>(response: StructuredDataDocument<T>): T {\n return {\n meta: response.content,\n } as T;\n}\n","import { assert } from '@warp-drive-mirror/core/build-config/macros';\n\nif (typeof FastBoot === 'undefined') {\n globalThis.addEventListener('beforeunload', function () {\n sessionStorage.setItem('tab-closed', 'true');\n });\n}\n\nfunction getTabId() {\n if (typeof sessionStorage === 'undefined') {\n return crypto.randomUUID();\n }\n\n const tabId = sessionStorage.getItem('tab-id');\n if (tabId) {\n const tabClosed = sessionStorage.getItem('tab-closed');\n if (tabClosed === 'true') {\n return tabId;\n }\n\n // fall through to generate a new tab id\n }\n\n const newTabId = crypto.randomUUID();\n sessionStorage.setItem('tab-id', newTabId);\n return newTabId;\n}\n\n/**\n * A unique identifier for the current browser tab\n * useful for observability/tracing and deduping\n * across multiple tabs.\n *\n * @summary Random UUID identifying the current browser tab, kept in `sessionStorage` across reloads, for request\n * tracing.\n * @group Constants\n */\nexport const TAB_ID: string = getTabId();\n/**\n * The epoch seconds at which the tab id was generated\n *\n * @summary Epoch seconds recorded when this module loaded, paired with `TAB_ID` in trace headers.\n * @group Constants\n */\nexport const TAB_ASSIGNED: number = Math.floor(Date.now() / 1000);\n\n/**\n * Adds the `X-Amzn-Trace-Id` header to support observability\n * tooling around request routing.\n *\n * This makes use of the {@link TAB_ID} and {@link TAB_ASSIGNED}\n * to enable tracking the browser tab of origin across multiple requests.\n *\n * Follows the template: `Root=1-${now}-${uuidv4};TabId=1-${epochSeconds}-${tab-uuid}`\n *\n * @summary Sets an `X-Amzn-Trace-Id` header carrying a per-request id and the tab id, so requests can be traced to\n * their browser tab.\n * @group Utility Functions\n */\nexport function addTraceHeader(headers: Headers): Headers {\n const now = Math.floor(Date.now() / 1000);\n headers.set('X-Amzn-Trace-Id', `Root=1-${now}-${crypto.randomUUID()};TabId=1-${TAB_ASSIGNED}-${TAB_ID}`);\n\n return headers;\n}\n\n/**\n * Source: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html\n * As of 2024-12-05 the maximum URL length is 8192 bytes.\n *\n * @summary The 8192-byte URL length limit, taken from AWS CloudFront, that `assertInvalidUrlLength` checks against.\n * @group Constants\n */\nexport const MAX_URL_LENGTH = 8192;\n\n/**\n * This assertion takes a URL and throws an error if the URL is longer than the maximum URL length.\n *\n * See also {@link MAX_URL_LENGTH}\n *\n * @summary Dev-mode assertion that throws when a URL is longer than `MAX_URL_LENGTH`, suggesting a `POST` or `QUERY`\n * request instead.\n * @group Utility Functions\n */\nexport function assertInvalidUrlLength(url: string | undefined): void {\n assert(\n `URL length ${url?.length} exceeds the maximum URL length of ${MAX_URL_LENGTH} bytes.\\n\\nConsider converting this request query a \\`/query\\` endpoint instead of a GET, or upgrade the current endpoint to be able to receive a POST request directly (ideally specifying the header HTTP-Method-Override: QUERY)\\n\\nThe Invalid URL is:\\n\\n${url}`,\n !url || url.length <= MAX_URL_LENGTH\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"handlers.js","names":["isCompressibleMethod","method","SupportsRequestStreams","duplexAccessed","hasContentType","Request","body","ReadableStream","duplex","headers","has","DEFAULT_CONSTRAINTS","Blob","ArrayBuffer","TypedArray","DataView","String","Object","getPrototypeOf","Uint8Array","AutoCompress","constructor","options","opts","format","constraints","assign","allowStreaming","forceStreaming","request","next","shouldCompress","compress","canCompress","length","size","byteLength","response","Response","stream","pipeThrough","CompressionStream","Headers","set","encodingForFormat","req","resp","blob","then","type","test","Error","Gate","constructor","handler","checkFn","request","context","next","FastBoot","globalThis","addEventListener","sessionStorage","setItem","getTabId","crypto","randomUUID","tabId","getItem","tabClosed","newTabId","TAB_ID","TAB_ASSIGNED","Math","floor","Date","now","addTraceHeader","headers","set","MAX_URL_LENGTH","assertInvalidUrlLength","url","test","Error","length"],"sources":["../../../src/-private/handlers/auto-compress.ts","../../../src/-private/handlers/gated.ts","../../../src/-private/handlers/meta-doc.ts","../../../src/-private/handlers/utils.ts"],"sourcesContent":["import { assert } from '@warp-drive-mirror/core/build-config/macros';\nimport type { Future, Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { HTTPMethod, RequestContext } from '@warp-drive-mirror/core/types/request';\n\nfunction isCompressibleMethod(method?: HTTPMethod): boolean {\n return method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE';\n}\n\n/**\n * Whether the browser supports `ReadableStream` as a request body\n * in a `POST` request.\n *\n * @summary Whether the current browser can send a `ReadableStream` as a `fetch` request body, detected once at\n * module load.\n * @group Constants\n */\nexport const SupportsRequestStreams: boolean = (() => {\n let duplexAccessed = false;\n\n const hasContentType = new Request('', {\n body: new ReadableStream(),\n method: 'POST',\n // @ts-expect-error untyped\n get duplex() {\n duplexAccessed = true;\n return 'half';\n },\n }).headers.has('Content-Type');\n\n return duplexAccessed && !hasContentType;\n})();\n\n/**\n * The minimum body size, per body type, at which {@link AutoCompress} compresses a request\n * body. Passed as {@link CompressionOptions.constraints}.\n *\n * @summary Minimum request body sizes, per body type, at which `AutoCompress` compresses a body.\n * @public\n */\nexport interface Constraints {\n /**\n * The minimum size at which to compress blobs\n *\n * @default 1000\n */\n Blob?: number;\n /**\n * The minimum size at which to compress array buffers\n *\n * @default 1000\n */\n ArrayBuffer?: number;\n /**\n * The minimum size at which to compress typed arrays\n *\n * @default 1000\n */\n TypedArray?: number;\n /**\n * The minimum size at which to compress data views\n *\n * @default 1000\n */\n DataView?: number;\n /**\n * The minimum size at which to compress strings\n *\n * @default 1000\n */\n String?: number;\n}\n\n/**\n * Options for configuring the {@link AutoCompress} handler.\n *\n * @summary Options for `new AutoCompress()`: the compression format, size constraints and whether to stream the body.\n * @public\n */\nexport interface CompressionOptions {\n /**\n * The compression format to use. Must be a valid\n * compression format supported by [CompressionStream](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)\n *\n * The default is `gzip`.\n *\n */\n format?: CompressionFormat;\n\n /**\n * Some browsers support `ReadableStream` as a request body. This option\n * enables passing the compression stream as the request body instead of\n * the final compressed body when the browser supports doing so.\n *\n * This comes with several caveats:\n *\n * - the request will be put into `duplex: 'half'` mode. This should be\n * transparent to you, but it is worth noting.\n * - the request mode cannot be `no-cors` as requests with a `ReadableStream`\n * have no content length and thus are a new form of request that triggers\n * cors requirements and a preflight request.\n * - http/1.x is not supported.\n *\n * For additional reading about the restrictions of using `ReadableStream`\n * as a request body, see the [Chromium Documentation](https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests#restrictions)\n *\n * Streaming can be enabled per-request in browsers which support it by\n * setting `request.options.allowStreaming` to `true`.\n *\n * Streaming can be forced even when the browser does not support it by setting\n * `request.options.forceStreaming` to `true`. This is useful if later handlers\n * in the chain can handle the request body as a stream.\n *\n * @default false\n */\n allowStreaming?: boolean;\n\n /**\n * If `true`, the request will be forced into streaming mode even\n * if the browser does not support it. This is useful if later handlers\n * in the chain can handle the request body as a stream.\n *\n * @default false\n */\n forceStreaming?: boolean;\n\n /**\n * The constraints for the request body. This is used to determine\n * whether to compress the request body or not.\n *\n * The defaults are:\n *\n * ```ts\n * {\n * Blob: 1000, // blob.size\n * ArrayBuffer: 1000, // buffer.byteLength\n * TypedArray: 1000, // array.byteLength\n * DataView: 1000, // view.byteLength\n * String: 1000, // string.length\n * }\n * ```\n *\n * The following body types are never compressed unless explicitly\n * configured by the request:\n * - `FormData`\n * - `URLSearchParams`\n * - `ReadableStream`\n *\n * A request.options.compress value of `false` will disable\n * compression for a request body of any type. While a value of\n * `true` will enable compression for the request.\n *\n * An undefined value will use the default, a value of `0` will\n * enable compression for all values, and a value of `-1` will\n * disable compression.\n *\n */\n constraints?: Constraints;\n}\n\nconst DEFAULT_CONSTRAINTS = {\n Blob: 1000,\n ArrayBuffer: 1000,\n TypedArray: 1000,\n DataView: 1000,\n String: 1000,\n};\nconst TypedArray = Object.getPrototypeOf(Uint8Array) as typeof Uint8Array;\n\n/**\n * A request handler that automatically compresses the request body\n * if the request body is a string, array buffer, blob, or form data.\n *\n * This uses the [CompressionStream API](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)\n *\n * The compression format as well as the kinds of data to compress can be\n * configured using the `format` and `constraints` options.\n *\n * ```diff\n * +import { AutoCompress } from '@ember-data-mirror/request-utils/handlers';\n * import Fetch from '@ember-data-mirror/request/fetch';\n * import RequestManager from '@ember-data-mirror/request';\n * import Store from '@ember-data-mirror/store';\n *\n * class AppStore extends Store {\n * requestManager = new RequestManager()\n * .use([\n * + new AutoCompress(),\n * Fetch\n * ]);\n * }\n * ```\n *\n * @summary Request handler that compresses large `POST`, `PUT`, `PATCH`, and `DELETE` bodies with `CompressionStream`,\n * optionally streaming them.\n * @group Handlers\n * @public\n * @since 5.5.0\n */\nexport class AutoCompress implements Handler {\n /**\n * The resolved options this handler was configured with, with all\n * defaults (including `constraints` defaults) applied.\n */\n declare options: Required<CompressionOptions> & {\n /**\n * The resolved size constraints used to decide whether to compress a given request body.\n */\n constraints: Required<Constraints>;\n };\n\n constructor(options: CompressionOptions = {}) {\n const opts = {\n format: options.format ?? 'gzip',\n constraints: Object.assign({}, DEFAULT_CONSTRAINTS, options.constraints),\n allowStreaming: options.allowStreaming ?? false,\n forceStreaming: options.forceStreaming ?? false,\n };\n this.options = opts;\n }\n\n request<T>({ request }: RequestContext, next: NextFn<T>): Promise<T> | Future<T> {\n const { constraints } = this.options;\n const { body } = request;\n\n const shouldCompress =\n !!body &&\n isCompressibleMethod(request.method) &&\n request.options?.compress !== false &&\n // prettier-ignore\n (request.options?.compress ? true\n : typeof body === 'string' || body instanceof String ? canCompress('String', constraints, body.length)\n : body instanceof Blob ? canCompress('Blob', constraints, body.size)\n : body instanceof ArrayBuffer ? canCompress('ArrayBuffer', constraints, body.byteLength)\n : body instanceof DataView ? canCompress('DataView', constraints, body.byteLength)\n : body instanceof TypedArray ? canCompress('TypedArray', constraints, body.byteLength)\n : false);\n\n if (!shouldCompress) return next(request);\n\n // A convenient way to convert all of the supported body types to a readable\n // stream is to use a `Response` object body\n const response = new Response(request.body);\n const stream = response.body?.pipeThrough(new CompressionStream(this.options.format));\n const headers = new Headers(request.headers);\n headers.set('Content-Encoding', encodingForFormat(this.options.format));\n\n //\n // For browsers that support it, `fetch` can receive a `ReadableStream` as\n // the body, so all we need to do is to create a new `ReadableStream` and\n // compress it on the fly\n //\n const forceStreaming = request.options?.forceStreaming ?? this.options.forceStreaming;\n const allowStreaming = request.options?.allowStreaming ?? this.options.allowStreaming;\n if (forceStreaming || (SupportsRequestStreams && allowStreaming)) {\n const req = Object.assign({}, request, {\n body: stream,\n headers,\n });\n if (SupportsRequestStreams) {\n // @ts-expect-error untyped\n req.duplex = 'half';\n }\n\n return next(req);\n\n //\n // For non-chromium browsers, we have to \"pull\" the stream to get the final\n // bytes and supply the final byte array as the new request body.\n //\n }\n\n // we need to pull the stream to get the final bytes\n const resp = new Response(stream);\n return resp.blob().then((blob) => {\n const req = Object.assign({}, request, {\n body: blob,\n headers,\n });\n return next(req);\n }) as Promise<T>;\n }\n}\n\nfunction canCompress(type: keyof Constraints, constraints: Required<Constraints>, size: number): boolean {\n // if we have a value of 0, we can compress anything\n if (constraints[type] === 0) return true;\n if (constraints[type] === -1) return false;\n return size >= constraints[type];\n}\n\nfunction encodingForFormat(format: CompressionFormat): string {\n switch (format) {\n case 'gzip':\n case 'deflate':\n case 'deflate-raw':\n return format;\n default:\n assert(`Unsupported compression format: ${format as unknown as string}`);\n // @ts-expect-error - unreachable code is reachable in production\n return format;\n }\n}\n","import type { Future, Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { RequestContext, StructuredDataDocument } from '@warp-drive-mirror/core/types/request';\n\n/**\n * If CheckFn returns true, the wrapped handler will be used.\n * If CheckFn returns false, the wrapped handler will be skipped.\n */\ntype CheckFn = (context: RequestContext) => boolean;\n\n/**\n *\n * @summary Request handler that wraps another handler and runs it only when a check function returns true for the\n * request.\n * @group Handlers\n * @public\n */\nexport class Gate implements Handler {\n /**\n * The wrapped handler to invoke when {@link Gate.checkFn | checkFn} returns `true`.\n */\n declare handler: Handler;\n /**\n * The predicate used to decide whether {@link Gate.handler | handler} should run for a given request.\n */\n declare checkFn: CheckFn;\n\n constructor(handler: Handler, checkFn: CheckFn) {\n this.handler = handler;\n this.checkFn = checkFn;\n }\n\n request<T = unknown>(context: RequestContext, next: NextFn<T>): Promise<T | StructuredDataDocument<T>> | Future<T> {\n if (this.checkFn(context)) {\n return this.handler.request(context, next);\n }\n return next(context.request);\n }\n}\n","import type { StoreRequestContext } from '@warp-drive-mirror/core';\nimport type { Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { StructuredDataDocument } from '@warp-drive-mirror/core/types/request';\n\n/**\n * MetaDocHandler processes requests that are marked as meta requests.\n *\n * It treats the response body as \"entirely meta\" transforming\n *\n * ```ts\n * {\n * some: \"key\",\n * another: \"thing\"\n * }\n * ```\n *\n * into\n *\n * ```ts\n * {\n * \t meta: {\n * some: \"key\",\n * another: \"thing\"\n * }\n * }\n * ```\n *\n * To activate this handler, a request should specify\n *\n * ```ts\n * options.isMetaRequest = true\n * ```\n *\n * For instance\n *\n * ```ts\n * store.request({\n * url: '/example',\n * options: {\n * isMetaRequest: true\n * }\n * });\n * ```\n *\n * Errors are not processed by this handler, so if the request fails and the error response\n * is not in {json:api} format additional processing may be needed.\n *\n * @summary Request handler that wraps the whole response body in `meta` for requests with `options.isMetaRequest` set.\n * @group Handlers\n */\nexport const MetaDocHandler: Handler = {\n request<T>(context: StoreRequestContext, next: NextFn<T>): Promise<T | StructuredDataDocument<T> | Awaited<T>> {\n if (!context.request.options?.isMetaRequest) {\n return next(context.request);\n }\n\n return next(context.request).then((response) => {\n return processResponse<T>(response);\n });\n },\n};\n\nfunction processResponse<T>(response: StructuredDataDocument<T>): T {\n return {\n meta: response.content,\n } as T;\n}\n","import { assert } from '@warp-drive-mirror/core/build-config/macros';\n\nif (typeof FastBoot === 'undefined') {\n globalThis.addEventListener('beforeunload', function () {\n sessionStorage.setItem('tab-closed', 'true');\n });\n}\n\nfunction getTabId() {\n if (typeof sessionStorage === 'undefined') {\n return crypto.randomUUID();\n }\n\n const tabId = sessionStorage.getItem('tab-id');\n if (tabId) {\n const tabClosed = sessionStorage.getItem('tab-closed');\n if (tabClosed === 'true') {\n return tabId;\n }\n\n // fall through to generate a new tab id\n }\n\n const newTabId = crypto.randomUUID();\n sessionStorage.setItem('tab-id', newTabId);\n return newTabId;\n}\n\n/**\n * A unique identifier for the current browser tab\n * useful for observability/tracing and deduping\n * across multiple tabs.\n *\n * @summary Random UUID identifying the current browser tab, kept in `sessionStorage` across reloads, for request\n * tracing.\n * @group Constants\n */\nexport const TAB_ID: string = getTabId();\n/**\n * The epoch seconds at which the tab id was generated\n *\n * @summary Epoch seconds recorded when this module loaded, paired with `TAB_ID` in trace headers.\n * @group Constants\n */\nexport const TAB_ASSIGNED: number = Math.floor(Date.now() / 1000);\n\n/**\n * Adds the `X-Amzn-Trace-Id` header to support observability\n * tooling around request routing.\n *\n * This makes use of the {@link TAB_ID} and {@link TAB_ASSIGNED}\n * to enable tracking the browser tab of origin across multiple requests.\n *\n * Follows the template: `Root=1-${now}-${uuidv4};TabId=1-${epochSeconds}-${tab-uuid}`\n *\n * @summary Sets an `X-Amzn-Trace-Id` header carrying a per-request id and the tab id, so requests can be traced to\n * their browser tab.\n * @group Utility Functions\n */\nexport function addTraceHeader(headers: Headers): Headers {\n const now = Math.floor(Date.now() / 1000);\n headers.set('X-Amzn-Trace-Id', `Root=1-${now}-${crypto.randomUUID()};TabId=1-${TAB_ASSIGNED}-${TAB_ID}`);\n\n return headers;\n}\n\n/**\n * Source: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html\n * As of 2024-12-05 the maximum URL length is 8192 bytes.\n *\n * @summary The 8192-byte URL length limit, taken from AWS CloudFront, that `assertInvalidUrlLength` checks against.\n * @group Constants\n */\nexport const MAX_URL_LENGTH = 8192;\n\n/**\n * This assertion takes a URL and throws an error if the URL is longer than the maximum URL length.\n *\n * See also {@link MAX_URL_LENGTH}\n *\n * @summary Dev-mode assertion that throws when a URL is longer than `MAX_URL_LENGTH`, suggesting a `POST` or `QUERY`\n * request instead.\n * @group Utility Functions\n */\nexport function assertInvalidUrlLength(url: string | undefined): void {\n assert(\n `URL length ${url?.length} exceeds the maximum URL length of ${MAX_URL_LENGTH} bytes.\\n\\nConsider converting this request query a \\`/query\\` endpoint instead of a GET, or upgrade the current endpoint to be able to receive a POST request directly (ideally specifying the header HTTP-Method-Override: QUERY)\\n\\nThe Invalid URL is:\\n\\n${url}`,\n !url || url.length <= MAX_URL_LENGTH\n );\n}\n"],"mappings":"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"}
@@ -4,6 +4,13 @@ import { n as extractCacheOptions, t as copyForwardUrlOptions } from "./builder-
4
4
  import { recordIdentifierFor } from "@warp-drive-mirror/core";
5
5
 
6
6
  //#region src/-private/json-api/-utils.ts
7
+ /**
8
+ * The configuration {@link setBuildURLConfig} accepts: the {@link BuildURLConfig} fields, plus
9
+ * the {json:api} profile and extension URIs to send in the `Accept` header.
10
+ *
11
+ * @summary Configuration for the JSON:API request builders: host, namespace, and the profile and extension URIs for `Accept`.
12
+ * @public
13
+ */
7
14
  const JsonApiAccept = "application/vnd.api+json";
8
15
  const DEFAULT_CONFIG = {
9
16
  host: "",
@@ -407,6 +414,13 @@ function updateRecord(record, options = {}) {
407
414
  //#endregion
408
415
  //#region src/-private/json-api/serialize.ts
409
416
  /**
417
+ * The resource object {@link serializePatch} produces: the resource's `type` and `id` (or `lid`
418
+ * for a new record) with only its changed attributes and relationships.
419
+ *
420
+ * @summary A JSON:API resource object holding only a record's changed attributes and relationships, as produced by `serializePatch`.
421
+ * @public
422
+ */
423
+ /**
410
424
  * :::warning ⚠️ **This util often won't produce the necessary body for a {json:api} request**
411
425
  *
412
426
  * While this may come as a surprise, they are intended to serialize cache state for more
@@ -1 +1 @@
1
- {"version":3,"file":"json-api.js","names":["recordIdentifierFor","buildBaseURL","pluralize","copyForwardUrlOptions","ACCEPT_HEADER_VALUE","isExisting","identifier","id","type","deleteRecord","record","options","test","Error","urlOptions","op","resourcePath","url","headers","Headers","append","method","data","records","createRecord","updateRecord","patch","patchRecord","opts","serializeResources","cache","identifiers","data","Array","isArray","map","identifier","_serializeResource","fixRef","id","lid","type","fixRelData","rel","ref","record","structuredClone","peek","test","Error","String","relationships","key","Object","keys","relationship","length","serializePatch","hasChangedAttrs","attrsChanges","changedAttrs","attributes","forEach","change","newVal","undefined","changedRelationships","size","diff","localState"],"sources":["../../../src/-private/json-api/-utils.ts","../../../src/-private/json-api/find-record.ts","../../../src/-private/json-api/query.ts","../../../src/-private/json-api/save-record.ts","../../../src/-private/json-api/serialize.ts"],"sourcesContent":["import type { QueryParamsSource } from '@warp-drive-mirror/core/types/params';\n\nimport type { BuildURLConfig } from '../../index.ts';\nimport { buildQueryParams as buildParams, setBuildURLConfig as setConfig } from '../../index.ts';\n\nexport interface JSONAPIConfig extends BuildURLConfig {\n profiles?: {\n pagination?: string;\n [key: string]: string | undefined;\n };\n extensions?: {\n atomic?: string;\n [key: string]: string | undefined;\n };\n}\n\nconst JsonApiAccept = 'application/vnd.api+json';\nconst DEFAULT_CONFIG: JSONAPIConfig = { host: '', namespace: '' };\nexport let CONFIG: JSONAPIConfig = DEFAULT_CONFIG;\nexport let ACCEPT_HEADER_VALUE = 'application/vnd.api+json';\n\n/**\n * Allows setting extensions and profiles to be used in the `Accept` header.\n *\n * Extensions and profiles are keyed by their namespace with the value being\n * their URI.\n *\n * The [Basic Usage](/guides/the-manual/cookbook/basic-usage#step-2-configure-some-request-defaults)\n * guide uses it to configure a host, a namespace and a pagination profile.\n *\n * Example:\n *\n * ```ts\n * setBuildURLConfig({\n * extensions: {\n * atomic: 'https://jsonapi.org/ext/atomic'\n * },\n * profiles: {\n * pagination: 'https://jsonapi.org/profiles/ethanresnick/cursor-pagination'\n * }\n * });\n * ```\n *\n * This also sets the global configuration for `buildBaseURL`\n * for host and namespace values for the global coniguration\n * done via `import { setBuildURLConfig } from '@warp-drive-mirror/utilities';`\n *\n * These values may still be overridden by passing\n * them to buildBaseURL directly.\n *\n * This method may be called as many times as needed\n *\n * ```ts\n * type BuildURLConfig = {\n * host: string;\n * namespace: string'\n * }\n * ```\n *\n * @summary Sets the JSON:API extensions and profiles sent in the `Accept` header, plus the global URL host and\n * namespace.\n * @public\n * @param {BuildURLConfig} config\n * @return {void}\n */\nexport function setBuildURLConfig(config: JSONAPIConfig): void {\n CONFIG = Object.assign({}, DEFAULT_CONFIG, config);\n\n if (config.profiles || config.extensions) {\n let accept = JsonApiAccept;\n if (config.profiles) {\n const profiles = Object.values(config.profiles);\n if (profiles.length) {\n accept += ';profile=\"' + profiles.join(' ') + '\"';\n }\n }\n if (config.extensions) {\n const extensions = Object.values(config.extensions);\n if (extensions.length) {\n accept += ';ext=' + extensions.join(' ');\n }\n }\n ACCEPT_HEADER_VALUE = accept;\n }\n\n setConfig(config);\n}\n\ninterface RelatedObject {\n [key: string]: string | string[] | RelatedObject;\n}\n\nexport type JsonApiQuery = {\n include?: string | string[] | RelatedObject;\n fields?: Record<string, string | string[]>;\n page?: {\n size?: number;\n after?: string;\n before?: string;\n };\n};\n\nfunction isJsonApiQuery(query: JsonApiQuery | QueryParamsSource): query is JsonApiQuery {\n if ('include' in query && query.include && typeof query.include === 'object') {\n return true;\n }\n if ('fields' in query || 'page' in query) {\n return true;\n }\n return false;\n}\n\nfunction collapseIncludePaths(basePath: string, include: RelatedObject, paths: string[]) {\n const keys = Object.keys(include);\n for (let i = 0; i < keys.length; i++) {\n // the key is always included too\n paths.push(`${basePath}.${keys[i]}`);\n const key = keys[i];\n const value = include[key];\n\n // include: { 'company': 'field1,field2' }\n if (typeof value === 'string') {\n value.split(',').forEach((field) => {\n paths.push(`${basePath}.${key}.${field}`);\n });\n\n // include: { 'company': ['field1', 'field2'] }\n } else if (Array.isArray(value)) {\n value.forEach((field) => {\n paths.push(`${basePath}.${key}.${field}`);\n });\n\n // include: { 'company': { 'nested': 'field1,field2' } }\n } else {\n collapseIncludePaths(`${basePath}.${key}`, value, paths);\n }\n }\n}\n\n/**\n * Sorts query params by both key and value, returning a query params string\n *\n * Treats `included` specially, splicing it into an array if it is a string and sorting the array.\n * - If `included` is an object we build paths dynamically for you\n * Treats `fields` specially, building JSON:API partial fields params from an object\n * Treats `page` specially, building cursor-pagination profile page params from an object\n *\n * ```ts\n * const params = buildQueryParams({\n * include: {\n * company: {\n * locations: 'address'\n * }\n * },\n * fields: {\n * company: ['name', 'ticker'],\n * person: 'name'\n * },\n * page: {\n * size: 10,\n * after: 'abc',\n * }\n * });\n *\n * // => 'fields[company]=name,ticker&fields[person]=name&include=company.locations,company.locations.address&page[after]=abc&page[size]=10'\n * ```\n *\n * Options:\n * - arrayFormat: 'bracket' | 'indices' | 'repeat' | 'comma'\n *\n * 'bracket': appends [] to the key for every value e.g. `ids[]=1&ids[]=2`\n * 'indices': appends [i] to the key for every value e.g. `ids[0]=1&ids[1]=2`\n * 'repeat': appends the key for every value e.g. `ids=1&ids=2`\n * 'comma' (default): appends the key once with a comma separated list of values e.g. `ids=1,2`\n *\n * @public\n * @param {URLSearchParams | Object} params\n * @param {Object} [options]\n * @return {String} A sorted query params string without the leading `?`\n */\nexport function buildQueryParams(query: JsonApiQuery | QueryParamsSource): string {\n if (query instanceof URLSearchParams) {\n return buildParams(query);\n }\n\n if (!isJsonApiQuery(query)) {\n return buildParams(query);\n }\n\n const { include, fields, page, ...rest } = query;\n const finalQuery: QueryParamsSource = {\n ...rest,\n };\n\n if ('include' in query) {\n // include: { 'company': 'field1,field2' }\n // include: { 'company': ['field1', 'field2'] }\n // include: { 'company': { 'nested': 'field1,field2' } }\n // include: { 'company': { 'nested': ['field1', 'field2'] } }\n if (include && !Array.isArray(include) && typeof include === 'object') {\n const includePaths: string[] = [];\n collapseIncludePaths('', include, includePaths);\n finalQuery.include = includePaths.sort();\n\n // include: 'field1,field2'\n // include: ['field1', 'field2']\n } else {\n finalQuery.include = include;\n }\n }\n\n if (fields) {\n const keys = Object.keys(fields).sort();\n for (let i = 0; i < keys.length; i++) {\n const resourceType = keys[i];\n const value = fields[resourceType];\n\n // fields: { 'company': ['field1', 'field2'] }\n if (Array.isArray(value)) {\n // toSorted: `value` belongs to the caller, sorting it in place would\n // reorder whatever else they are doing with it\n finalQuery[`fields[${resourceType}]`] = value.toSorted().join(',');\n\n // fields: { 'company': 'field1' }\n // fields: { 'company': 'field1,field2' }\n } else {\n finalQuery[`fields[${resourceType}]`] = value.split(',').sort().join(',');\n }\n }\n }\n\n if (page) {\n const keys = Object.keys(page).sort() as Array<'size' | 'after' | 'before'>;\n keys.forEach((key) => {\n const value = page[key];\n finalQuery[`page[${key}]`] = value!;\n });\n }\n\n return buildParams(finalQuery);\n}\n","import type { ReactiveDataDocument } from '@warp-drive-mirror/core/reactive';\nimport type { TypeFromInstance } from '@warp-drive-mirror/core/types/record';\nimport type {\n FindRecordOptions,\n FindRecordRequestOptions,\n RemotelyAccessibleIdentifier,\n} from '@warp-drive-mirror/core/types/request';\nimport type { ApiError } from '@warp-drive-mirror/core/types/spec/error';\nimport type { Meta } from '@warp-drive-mirror/core/types/spec/json-api-raw';\n\nimport { buildBaseURL, buildQueryParams, type FindRecordUrlOptions } from '../../index.ts';\nimport { pluralize } from '../../string.ts';\nimport { copyForwardUrlOptions, extractCacheOptions } from '../builder-utils.ts';\nimport { ACCEPT_HEADER_VALUE } from './-utils.ts';\n\n/**\n * Builds request options to fetch a single resource by a known id or identifier\n * configured for the url and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * :::tabs\n *\n * == Basic Usage\n *\n * ```ts\n * import { findRecord } from '@warp-drive-mirror/utilities/json-api';\n * import type { Person } from '#/data/types';\n *\n * const result = await store.request(\n * findRecord<Person>('person', '1')\n * );\n * ```\n *\n * == With Options\n *\n * ```ts\n * import { findRecord } from '@warp-drive-mirror/utilities/json-api';\n * import type { Person } from '#/data/types';\n *\n * const data = await store.request(\n * findRecord<Person>(\n * 'person', '1',\n * { include: ['pets', 'friends'] }\n * )\n * );\n * ```\n *\n * == With an Identifier\n *\n * ```ts\n * import { findRecord } from '@warp-drive-mirror/utilities/json-api';\n * import type { Person } from '#/data/types';\n *\n * const data = await store.request(\n * findRecord<Person>(\n * { type: 'person', id: '1' },\n * { include: ['pets', 'friends'] }\n * )\n * );\n * ```\n *\n * :::\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { findRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const data = await store.request(\n * findRecord(\n * 'person', '1',\n * { include: ['pets', 'friends'] },\n * { namespace: 'api/v2' }\n * )\n * );\n * ```\n *\n * @summary Builds a JSON:API `GET` request for one resource by type and id, with optional `include`.\n * @public\n */\nexport function findRecord<T, M extends Meta | undefined = Meta | undefined, E extends object = ApiError>(\n identifier: RemotelyAccessibleIdentifier<TypeFromInstance<T>>,\n options?: FindRecordOptions\n): FindRecordRequestOptions<ReactiveDataDocument<T, M, E>, T>;\nexport function findRecord(\n identifier: RemotelyAccessibleIdentifier,\n options?: FindRecordOptions\n): FindRecordRequestOptions;\nexport function findRecord<T, M extends Meta | undefined = Meta | undefined, E extends object = ApiError>(\n type: TypeFromInstance<T>,\n id: string,\n options?: FindRecordOptions\n): FindRecordRequestOptions<ReactiveDataDocument<T, M, E>, T>;\nexport function findRecord(type: string, id: string, options?: FindRecordOptions): FindRecordRequestOptions;\nexport function findRecord(\n arg1: string | RemotelyAccessibleIdentifier,\n arg2: string | FindRecordOptions | undefined,\n arg3?: FindRecordOptions\n): FindRecordRequestOptions {\n const identifier: RemotelyAccessibleIdentifier = typeof arg1 === 'string' ? { type: arg1, id: arg2 as string } : arg1;\n const options = ((typeof arg1 === 'string' ? arg3 : arg2) || {}) as FindRecordOptions;\n const cacheOptions = extractCacheOptions(options);\n const urlOptions: FindRecordUrlOptions = {\n identifier,\n op: 'findRecord',\n resourcePath: pluralize(identifier.type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n\n return {\n url: options.include?.length\n ? `${url}?${buildQueryParams({ include: options.include }, options.urlParamsSettings)}`\n : url,\n method: 'GET',\n headers,\n cacheOptions,\n op: 'findRecord',\n records: [identifier],\n };\n}\n\n/** @deprecated use {@link ReactiveDataDocument} */\nexport type FindRecordResultDocument<\n T,\n M extends Meta | undefined = Meta | undefined,\n E extends object = ApiError,\n> = ReactiveDataDocument<T, M, E>;\n","import type { ReactiveDataDocument } from '@warp-drive-mirror/core/reactive';\nimport type { QueryParamsSource } from '@warp-drive-mirror/core/types/params';\nimport type { TypedRecordInstance, TypeFromInstance } from '@warp-drive-mirror/core/types/record';\nimport type {\n CacheOptions,\n ConstrainedRequestOptions,\n PostQueryRequestOptions,\n QueryRequestOptions,\n} from '@warp-drive-mirror/core/types/request';\nimport type { ApiError } from '@warp-drive-mirror/core/types/spec/error';\nimport type { Meta } from '@warp-drive-mirror/core/types/spec/json-api-raw';\n\nimport { buildBaseURL, buildQueryParams, type QueryUrlOptions } from '../../index.ts';\nimport { pluralize } from '../../string.ts';\nimport { copyForwardUrlOptions, extractCacheOptions } from '../builder-utils.ts';\nimport { ACCEPT_HEADER_VALUE } from './-utils.ts';\n/**\n * Builds request options to query for resources, usually by a primary\n * type, configured for the url and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * The key difference between this and `postQuery` is that this method will send the query\n * as query params in the url of a \"GET\" request instead of as the JSON body of a \"POST\"\n * request.\n *\n * **Basic Usage**\n *\n * ```ts\n * import { query } from '@warp-drive-mirror/utilities/json-api';\n *\n * const data = await store.request(query('person'));\n * ```\n *\n * **With Query Params**\n *\n * ```ts\n * import { query } from '@warp-drive-mirror/utilities/json-api';\n *\n * const options = query('person', { include: ['pets', 'friends'] });\n * const data = await store.request(options);\n * ```\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { query } from '@warp-drive-mirror/utilities/json-api';\n *\n * const options = query('person', { include: ['pets', 'friends'] }, { reload: true });\n * const data = await store.request(options);\n * ```\n *\n * @summary Builds a JSON:API `GET` request that queries a resource type, sending the query as URL query params.\n * @public\n * @badge Builder\n */\nexport function query<\n T extends TypedRecordInstance,\n M extends Meta | undefined = Meta | undefined,\n E extends object = ApiError,\n>(\n type: TypeFromInstance<T>,\n query?: QueryParamsSource,\n options?: ConstrainedRequestOptions\n): QueryRequestOptions<ReactiveDataDocument<T[], M, E>>;\nexport function query(\n type: string,\n query?: QueryParamsSource,\n options?: ConstrainedRequestOptions\n): QueryRequestOptions;\nexport function query(\n type: string,\n // oxlint-disable-next-line no-shadow\n query: QueryParamsSource = {},\n options: ConstrainedRequestOptions = {}\n): QueryRequestOptions {\n const cacheOptions = extractCacheOptions(options);\n const urlOptions: QueryUrlOptions = {\n identifier: { type },\n op: 'query',\n resourcePath: pluralize(type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n const queryString = buildQueryParams(query, options.urlParamsSettings);\n\n return {\n url: queryString ? `${url}?${queryString}` : url,\n method: 'GET',\n headers,\n cacheOptions,\n op: 'query',\n };\n}\n\n/**\n * Builds request options to query for resources, usually by a primary\n * type, configured for the url and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * The key difference between this and `query` is that this method will send the query\n * as the JSON body of a \"POST\" request instead of as query params in the url of a \"GET\"\n * request.\n *\n * A CacheKey is generated from the url and query params, and used to cache the response\n * in the store.\n *\n * ```ts\n * import { postQuery } from '@warp-drive-mirror/utilities/json-api';\n *\n * const options = postQuery('person', { include: ['pets', 'friends'] });\n * const data = await store.request(options);\n * ```\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { postQuery } from '@warp-drive-mirror/utilities/json-api';\n *\n * const options = postQuery('person', { include: ['pets', 'friends'] }, { reload: true });\n * const data = await store.request(options);\n * ```\n *\n * @summary Builds a JSON:API `POST` request that sends a query as the JSON body, cached under a key built from the\n * query params.\n * @public\n * @param type - the name of the resource type to query\n * @param query - the query params to send with the request\n * @param options - options to modify the request behavior\n */\nexport function postQuery<T, M extends Meta | undefined = Meta | undefined, E extends object = ApiError>(\n type: TypeFromInstance<T>,\n query?: QueryParamsSource,\n options?: ConstrainedRequestOptions\n): PostQueryRequestOptions<ReactiveDataDocument<T[], M, E>>;\nexport function postQuery(\n type: string,\n query?: QueryParamsSource,\n options?: ConstrainedRequestOptions\n): PostQueryRequestOptions;\nexport function postQuery(\n type: string,\n // oxlint-disable-next-line no-shadow\n query: QueryParamsSource = {},\n options: ConstrainedRequestOptions = {}\n): PostQueryRequestOptions {\n const cacheOptions = extractCacheOptions(options);\n const urlOptions: QueryUrlOptions = {\n identifier: { type },\n op: 'query',\n resourcePath: options.resourcePath ?? pluralize(type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n\n const queryData = structuredClone(query);\n cacheOptions.key = cacheOptions.key ?? `${url}?${buildQueryParams(queryData, options.urlParamsSettings)}`;\n\n return {\n url,\n method: 'POST',\n body: JSON.stringify(query),\n headers,\n cacheOptions: cacheOptions as CacheOptions & { key: string },\n op: 'query',\n };\n}\n","import { recordIdentifierFor } from '@warp-drive-mirror/core';\nimport { assert } from '@warp-drive-mirror/core/build-config/macros';\nimport type { ReactiveDataDocument } from '@warp-drive-mirror/core/reactive';\nimport type { PersistedResourceKey, ResourceKey } from '@warp-drive-mirror/core/types/identifier';\nimport type { TypedRecordInstance } from '@warp-drive-mirror/core/types/record';\nimport type {\n ConstrainedRequestOptions,\n CreateRequestOptions,\n DeleteRequestOptions,\n UpdateRequestOptions,\n} from '@warp-drive-mirror/core/types/request';\nimport type { ApiError } from '@warp-drive-mirror/core/types/spec/error';\nimport type { Meta } from '@warp-drive-mirror/core/types/spec/json-api-raw';\n\nimport {\n buildBaseURL,\n type CreateRecordUrlOptions,\n type DeleteRecordUrlOptions,\n type UpdateRecordUrlOptions,\n} from '../../index.ts';\nimport { pluralize } from '../../string.ts';\nimport { copyForwardUrlOptions } from '../builder-utils.ts';\nimport { ACCEPT_HEADER_VALUE } from './-utils.ts';\n\nfunction isExisting(identifier: ResourceKey): identifier is PersistedResourceKey {\n return 'id' in identifier && identifier.id !== null && 'type' in identifier && identifier.type !== null;\n}\n\n/**\n * :::warning ⚠️ **These Mutation Builders DO NOT Set The Request Body**\n * While this may come as a surprise, the app providing the body ensures that only\n * desired and correctly formatted data is sent with the request.\n * :::\n *\n * Builds request options to delete record for resources,\n * configured for the url, method and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * **Basic Usage**\n *\n * ```ts\n * import { deleteRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.peekRecord('person', '1');\n *\n * // mark record as deleted\n * store.deleteRecord(person);\n *\n * // persist deletion\n * const data = await store.request(deleteRecord(person));\n * ```\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { deleteRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.peekRecord('person', '1');\n *\n * // mark record as deleted\n * store.deleteRecord(person);\n *\n * // persist deletion\n * const options = deleteRecord(person, { namespace: 'api/v1' });\n * const data = await store.request(options);\n * ```\n *\n * @summary Builds a JSON:API `DELETE` request that persists deletion of an existing record.\n * @public\n * @param record\n * @param options\n */\nexport function deleteRecord<T>(record: T, options?: ConstrainedRequestOptions): DeleteRequestOptions<T>;\nexport function deleteRecord(record: unknown, options?: ConstrainedRequestOptions): DeleteRequestOptions;\nexport function deleteRecord(record: unknown, options: ConstrainedRequestOptions = {}): DeleteRequestOptions {\n const identifier = recordIdentifierFor(record);\n assert(`Expected to be given a record instance`, identifier);\n assert(`Cannot delete a record that does not have an associated type and id.`, isExisting(identifier));\n\n const urlOptions: DeleteRecordUrlOptions = {\n identifier: identifier,\n op: 'deleteRecord',\n resourcePath: pluralize(identifier.type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n\n return {\n url,\n method: 'DELETE',\n headers,\n op: 'deleteRecord',\n data: {\n record: identifier,\n },\n records: [identifier],\n };\n}\n\n/**\n * :::warning ⚠️ **These Mutation Builders DO NOT Set The Necessary Request Body**\n * While this may come as a surprise, the app providing the body ensures that only\n * desired and correctly formatted data is sent with the request.\n * :::\n *\n * Builds request options to create new record for resources,\n * configured for the url, method and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * **Basic Usage**\n *\n * ```ts\n * import { cacheKeyFor } from '@warp-drive-mirror/core';\n * import { createRecord } from '@warp-drive-mirror/utilities/json-api';\n * import type { Person } from '#/data/types';\n *\n * const person = store.createRecord<Person>('person', { name: 'Ted' });\n * const init = createRecord(person);\n * init.body = JSON.stringify(\n * {\n * // it's likely you will want to transform this data\n * // somewhat\n * data: store.cache.peek(cacheKeyFor(person))\n * }\n * );\n * const data = await store.request(init);\n * ```\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { createRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.createRecord('person', { name: 'Ted' });\n * const options = createRecord(person, { namespace: 'api/v1' });\n * const data = await store.request(options);\n * ```\n *\n * @summary Builds a JSON:API `POST` request for saving a new record; the app must set the request body itself.\n * @public\n * @param record\n * @param options\n */\nexport function createRecord<T>(record: T, options?: ConstrainedRequestOptions): CreateRequestOptions<T>;\nexport function createRecord(record: unknown, options?: ConstrainedRequestOptions): CreateRequestOptions;\nexport function createRecord(record: unknown, options: ConstrainedRequestOptions = {}): CreateRequestOptions {\n const identifier = recordIdentifierFor(record);\n assert(`Expected to be given a record instance`, identifier);\n\n const urlOptions: CreateRecordUrlOptions = {\n identifier: identifier,\n op: 'createRecord',\n resourcePath: pluralize(identifier.type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n\n return {\n url,\n method: 'POST',\n headers,\n op: 'createRecord',\n data: {\n record: identifier,\n },\n records: [identifier],\n };\n}\n\n/**\n * :::warning ⚠️ **These Mutation Builders DO NOT Set The Necessary Request Body**\n * While this may come as a surprise, the app providing the body ensures that only\n * desired and correctly formatted data is sent with the request.\n * :::\n *\n * Builds request options to update existing record for resources,\n * configured for the url, method and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * **Example Usage**\n *\n * ```ts\n * import { cacheKeyFor } from '@warp-drive-mirror/core';\n * import { updateRecord } from '@warp-drive-mirror/utilities/json-api';\n * import type { EditablePerson } from '#/data/types';\n *\n * const mutable = await checkout<EditablePerson>(person);\n * mutable.name = 'Chris';\n * const init = updateRecord(mutable);\n *\n * init.body = JSON.stringify(\n * // it's likely you will want to transform this data\n * // somewhat, or serialize only specific properties instead\n * serializePatch(store.cache, cacheKeyFor(mutable))\n * );\n * const data = await store.request(init);\n * ```\n *\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `patch` - Allows caller to specify whether to use a PATCH request instead of a PUT request, defaults to `false`.\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { updateRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.peekRecord('person', '1');\n * person.name = 'Chris';\n * const options = updateRecord(person, { patch: true });\n * const data = await store.request(options);\n * ```\n *\n * @summary Builds a JSON:API `PUT` (or `PATCH`) request for saving an existing record; the app must set the request\n * body itself.\n * @public\n * @param record\n * @param options\n */\nexport function updateRecord<\n T extends TypedRecordInstance,\n RT extends TypedRecordInstance = T,\n M extends Meta | undefined = Meta | undefined,\n E extends object = ApiError,\n>(\n record: T,\n options?: ConstrainedRequestOptions & { patch?: boolean }\n): UpdateRequestOptions<ReactiveDataDocument<RT, M, E>, T>;\nexport function updateRecord(\n record: unknown,\n options?: ConstrainedRequestOptions & { patch?: boolean }\n): UpdateRequestOptions;\nexport function updateRecord(\n record: unknown,\n options: ConstrainedRequestOptions & { patch?: boolean } = {}\n): UpdateRequestOptions {\n const identifier = recordIdentifierFor(record);\n assert(`Expected to be given a record instance`, identifier);\n assert(`Cannot update a record that does not have an associated type and id.`, isExisting(identifier));\n\n const urlOptions: UpdateRecordUrlOptions = {\n identifier: identifier,\n op: 'updateRecord',\n resourcePath: pluralize(identifier.type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n\n return {\n url,\n method: options.patch ? 'PATCH' : 'PUT',\n headers,\n op: 'updateRecord',\n data: {\n record: identifier,\n },\n records: [identifier],\n };\n}\n\n/**\n * Builds request options to update existing record for resources,\n * configured for the url and header expectations of most JSON:API APIs\n * for a PATCH request.\n *\n * Note: This is a convenience method that calls `updateRecord` with the\n * supplied request with the `patch` option set to `true`.\n *\n * **Basic Usage**\n *\n * ```ts\n * import { patchRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.peekRecord('person', '1');\n * person.name = 'Chris';\n * const data = await store.request(patchRecord(person));\n * ```\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { patchRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.peekRecord('person', '1');\n * person.name = 'Chris';\n * const options = patchRecord(person);\n * const data = await store.request(options);\n * ```\n *\n * @public\n * @param record\n * @param options\n */\nexport function patchRecord<T>(record: T, options?: ConstrainedRequestOptions): UpdateRequestOptions<T>;\nexport function patchRecord(record: unknown, options?: ConstrainedRequestOptions): UpdateRequestOptions;\nexport function patchRecord(record: unknown, options: ConstrainedRequestOptions = {}): UpdateRequestOptions {\n const opts = options as ConstrainedRequestOptions & { patch: true };\n opts.patch = true;\n return updateRecord(record, opts);\n}\n","import { assert } from '@warp-drive-mirror/core/build-config/macros';\nimport type { Cache } from '@warp-drive-mirror/core/types/cache';\nimport type { Relationship } from '@warp-drive-mirror/core/types/cache/relationship';\nimport type { ResourceKey } from '@warp-drive-mirror/core/types/identifier';\nimport type { Value } from '@warp-drive-mirror/core/types/json/raw';\nimport type { InnerRelationshipDocument, ResourceObject } from '@warp-drive-mirror/core/types/spec/json-api-raw';\n\ntype ChangedRelationshipData = InnerRelationshipDocument;\n\nexport type JsonApiResourcePatch =\n | {\n type: string;\n id: string;\n attributes?: Record<string, Value>;\n relationships?: Record<string, ChangedRelationshipData>;\n }\n | {\n type: string;\n id: null;\n lid: string;\n attributes?: Record<string, Value>;\n relationships?: Record<string, ChangedRelationshipData>;\n };\n\n/**\n * :::warning ⚠️ **This util often won't produce the necessary body for a {json:api} request**\n *\n * While this may come as a surprise, they are intended to serialize cache state for more\n * generalized usage. {json:api} has a large variance in acceptable shapes, and only your\n * app can ensure that the body is correctly formatted and contains all necessary data.\n * :::\n *\n * Serializes the current state of a resource or array of resources for use with POST or PUT requests.\n *\n * @summary Copies the cached state of one or more resources into a JSON:API-style `{ data }` object, a starting point\n * for request bodies.\n * @public\n * @param cache - the cache to serialize the resource(s) from\n * @param identifiers - the resource(s) to serialize\n * @return an object with a `data` property containing the serialized resource(s)\n */\nexport function serializeResources(\n cache: Cache,\n identifiers: ResourceKey\n): {\n /**\n * The serialized resource.\n */\n data: ResourceObject;\n};\nexport function serializeResources(\n cache: Cache,\n identifiers: ResourceKey[]\n): {\n /**\n * The serialized resources.\n */\n data: ResourceObject[];\n};\nexport function serializeResources(\n cache: Cache,\n identifiers: ResourceKey | ResourceKey[]\n): { data: ResourceObject | ResourceObject[] } {\n return {\n data: Array.isArray(identifiers)\n ? identifiers.map((identifier) => _serializeResource(cache, identifier))\n : _serializeResource(cache, identifiers),\n };\n}\n\ntype SerializedRef =\n | {\n id: string;\n type: string;\n }\n | { id: null; lid: string; type: string };\n\nfunction fixRef({\n id,\n lid,\n type,\n}: { id: string; lid?: string; type: string } | { id: null; lid: string; type: string }): SerializedRef {\n if (id !== null) {\n return { id, type };\n }\n return { id, lid, type };\n}\n\nfunction fixRelData(\n rel: Relationship['data'] | InnerRelationshipDocument['data']\n): SerializedRef | SerializedRef[] | null {\n if (Array.isArray(rel)) {\n return rel.map((ref) => fixRef(ref));\n } else if (typeof rel === 'object' && rel !== null) {\n return fixRef(rel);\n }\n return null;\n}\n\nfunction _serializeResource(cache: Cache, identifier: ResourceKey): ResourceObject {\n const { id, lid, type } = identifier;\n // peek gives us everything we want, but since its referentially the same data\n // as is in the cache we clone it to avoid any accidental mutations\n const record = structuredClone(cache.peek(identifier)) as ResourceObject;\n assert(\n `A record with id ${String(id)} and type ${type} for lid ${lid} was not found not in the supplied Cache.`,\n record\n );\n\n // remove lid from anything that has an ID and slice any relationship arrays\n if (record.id !== null) {\n delete record.lid;\n }\n\n if (record.relationships) {\n for (const key of Object.keys(record.relationships)) {\n const relationship = record.relationships[key];\n if (Array.isArray(relationship.data)) {\n relationship.data = relationship.data.map((ref) => fixRef(ref));\n } else if (typeof relationship.data === 'object' && relationship.data !== null) {\n relationship.data = fixRef(relationship.data);\n } else if (Object.keys(relationship ?? {}).length === 0) {\n delete record.relationships[key];\n }\n }\n }\n\n return record;\n}\n\n/**\n * :::warning ⚠️ **This util often won't produce the necessary body for a {json:api} request**\n *\n * While this may come as a surprise, they are intended to serialize cache state for more\n * generalized usage. {json:api} has a large variance in acceptable shapes, and only your\n * app can ensure that the body is correctly formatted and contains all necessary data.\n * :::\n *\n * Serializes changes to a resource. Useful for use with building bodies for PATCH requests.\n *\n * Only attributes which are changed are serialized.\n * Only relationships which are changed are serialized.\n *\n * Collection relationships serialize the collection as a whole.\n *\n * If you would like to serialize updates to a collection more granularly\n * (for instance, as operations) request the diff from the store and\n * serialize as desired:\n *\n * ```ts\n * const relationshipDiffMap = cache.changedRelationships(identifier);\n * ```\n *\n * @summary Serializes only a resource's changed attributes and relationships from the cache into a JSON:API-style\n * `{ data }` patch.\n * @public\n * @param cache - the cache to serialize the resource's changes from\n * @param identifier - the resource whose changes should be serialized\n * @return an object with a `data` property containing the serialized resource patch\n */\nexport function serializePatch(\n cache: Cache,\n identifier: ResourceKey\n // options: { include?: string[] } = {}\n): {\n /**\n * The serialized resource patch.\n */\n data: JsonApiResourcePatch;\n} {\n const { id, lid, type } = identifier;\n assert(\n `A record with id ${String(id)} and type ${type} for lid ${lid} was not found not in the supplied Cache.`,\n cache.peek(identifier)\n );\n\n const data: JsonApiResourcePatch =\n id === null\n ? { type, lid, id }\n : {\n type,\n id,\n };\n\n if (cache.hasChangedAttrs(identifier)) {\n const attrsChanges = cache.changedAttrs(identifier);\n const attributes: ResourceObject['attributes'] = {};\n\n Object.keys(attrsChanges).forEach((key) => {\n const change = attrsChanges[key];\n const newVal = change[1];\n attributes[key] = newVal === undefined ? null : structuredClone(newVal);\n });\n\n data.attributes = attributes;\n }\n\n const changedRelationships = cache.changedRelationships(identifier);\n if (changedRelationships.size) {\n const relationships: Record<string, ChangedRelationshipData> = {};\n changedRelationships.forEach((diff, key) => {\n relationships[key] = { data: fixRelData(diff.localState) } as ChangedRelationshipData;\n });\n\n data.relationships = relationships;\n }\n\n return { data };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"json-api.js","names":["buildQueryParams","buildParams","setBuildURLConfig","setConfig","JsonApiAccept","DEFAULT_CONFIG","host","namespace","CONFIG","ACCEPT_HEADER_VALUE","config","Object","assign","profiles","extensions","accept","values","length","join","isJsonApiQuery","query","include","collapseIncludePaths","basePath","paths","keys","i","push","key","value","split","forEach","field","Array","isArray","URLSearchParams","fields","page","rest","finalQuery","includePaths","sort","resourceType","toSorted","recordIdentifierFor","buildBaseURL","pluralize","copyForwardUrlOptions","ACCEPT_HEADER_VALUE","isExisting","identifier","id","type","deleteRecord","record","options","test","Error","urlOptions","op","resourcePath","url","headers","Headers","append","method","data","records","createRecord","updateRecord","patch","patchRecord","opts","serializeResources","cache","identifiers","data","Array","isArray","map","identifier","_serializeResource","fixRef","id","lid","type","fixRelData","rel","ref","record","structuredClone","peek","test","Error","String","relationships","key","Object","keys","relationship","length","serializePatch","hasChangedAttrs","attrsChanges","changedAttrs","attributes","forEach","change","newVal","undefined","changedRelationships","size","diff","localState"],"sources":["../../../src/-private/json-api/-utils.ts","../../../src/-private/json-api/find-record.ts","../../../src/-private/json-api/query.ts","../../../src/-private/json-api/save-record.ts","../../../src/-private/json-api/serialize.ts"],"sourcesContent":["import type { QueryParamsSource } from '@warp-drive-mirror/core/types/params';\n\nimport type { BuildURLConfig } from '../../index.ts';\nimport { buildQueryParams as buildParams, setBuildURLConfig as setConfig } from '../../index.ts';\n\n/**\n * The configuration {@link setBuildURLConfig} accepts: the {@link BuildURLConfig} fields, plus\n * the {json:api} profile and extension URIs to send in the `Accept` header.\n *\n * @summary Configuration for the JSON:API request builders: host, namespace, and the profile and extension URIs for `Accept`.\n * @public\n */\nexport interface JSONAPIConfig extends BuildURLConfig {\n profiles?: {\n pagination?: string;\n [key: string]: string | undefined;\n };\n extensions?: {\n atomic?: string;\n [key: string]: string | undefined;\n };\n}\n\nconst JsonApiAccept = 'application/vnd.api+json';\nconst DEFAULT_CONFIG: JSONAPIConfig = { host: '', namespace: '' };\nexport let CONFIG: JSONAPIConfig = DEFAULT_CONFIG;\nexport let ACCEPT_HEADER_VALUE = 'application/vnd.api+json';\n\n/**\n * Allows setting extensions and profiles to be used in the `Accept` header.\n *\n * Extensions and profiles are keyed by their namespace with the value being\n * their URI.\n *\n * The [Basic Usage](/guides/the-manual/cookbook/basic-usage#step-2-configure-some-request-defaults)\n * guide uses it to configure a host, a namespace and a pagination profile.\n *\n * Example:\n *\n * ```ts\n * setBuildURLConfig({\n * extensions: {\n * atomic: 'https://jsonapi.org/ext/atomic'\n * },\n * profiles: {\n * pagination: 'https://jsonapi.org/profiles/ethanresnick/cursor-pagination'\n * }\n * });\n * ```\n *\n * This also sets the global configuration for `buildBaseURL`\n * for host and namespace values for the global coniguration\n * done via `import { setBuildURLConfig } from '@warp-drive-mirror/utilities';`\n *\n * These values may still be overridden by passing\n * them to buildBaseURL directly.\n *\n * This method may be called as many times as needed\n *\n * ```ts\n * type BuildURLConfig = {\n * host: string;\n * namespace: string'\n * }\n * ```\n *\n * @summary Sets the JSON:API extensions and profiles sent in the `Accept` header, plus the global URL host and\n * namespace.\n * @public\n * @param {BuildURLConfig} config\n * @return {void}\n */\nexport function setBuildURLConfig(config: JSONAPIConfig): void {\n CONFIG = Object.assign({}, DEFAULT_CONFIG, config);\n\n if (config.profiles || config.extensions) {\n let accept = JsonApiAccept;\n if (config.profiles) {\n const profiles = Object.values(config.profiles);\n if (profiles.length) {\n accept += ';profile=\"' + profiles.join(' ') + '\"';\n }\n }\n if (config.extensions) {\n const extensions = Object.values(config.extensions);\n if (extensions.length) {\n accept += ';ext=' + extensions.join(' ');\n }\n }\n ACCEPT_HEADER_VALUE = accept;\n }\n\n setConfig(config);\n}\n\ninterface RelatedObject {\n [key: string]: string | string[] | RelatedObject;\n}\n\nexport type JsonApiQuery = {\n include?: string | string[] | RelatedObject;\n fields?: Record<string, string | string[]>;\n page?: {\n size?: number;\n after?: string;\n before?: string;\n };\n};\n\nfunction isJsonApiQuery(query: JsonApiQuery | QueryParamsSource): query is JsonApiQuery {\n if ('include' in query && query.include && typeof query.include === 'object') {\n return true;\n }\n if ('fields' in query || 'page' in query) {\n return true;\n }\n return false;\n}\n\nfunction collapseIncludePaths(basePath: string, include: RelatedObject, paths: string[]) {\n const keys = Object.keys(include);\n for (let i = 0; i < keys.length; i++) {\n // the key is always included too\n paths.push(`${basePath}.${keys[i]}`);\n const key = keys[i];\n const value = include[key];\n\n // include: { 'company': 'field1,field2' }\n if (typeof value === 'string') {\n value.split(',').forEach((field) => {\n paths.push(`${basePath}.${key}.${field}`);\n });\n\n // include: { 'company': ['field1', 'field2'] }\n } else if (Array.isArray(value)) {\n value.forEach((field) => {\n paths.push(`${basePath}.${key}.${field}`);\n });\n\n // include: { 'company': { 'nested': 'field1,field2' } }\n } else {\n collapseIncludePaths(`${basePath}.${key}`, value, paths);\n }\n }\n}\n\n/**\n * Sorts query params by both key and value, returning a query params string\n *\n * Treats `included` specially, splicing it into an array if it is a string and sorting the array.\n * - If `included` is an object we build paths dynamically for you\n * Treats `fields` specially, building JSON:API partial fields params from an object\n * Treats `page` specially, building cursor-pagination profile page params from an object\n *\n * ```ts\n * const params = buildQueryParams({\n * include: {\n * company: {\n * locations: 'address'\n * }\n * },\n * fields: {\n * company: ['name', 'ticker'],\n * person: 'name'\n * },\n * page: {\n * size: 10,\n * after: 'abc',\n * }\n * });\n *\n * // => 'fields[company]=name,ticker&fields[person]=name&include=company.locations,company.locations.address&page[after]=abc&page[size]=10'\n * ```\n *\n * Options:\n * - arrayFormat: 'bracket' | 'indices' | 'repeat' | 'comma'\n *\n * 'bracket': appends [] to the key for every value e.g. `ids[]=1&ids[]=2`\n * 'indices': appends [i] to the key for every value e.g. `ids[0]=1&ids[1]=2`\n * 'repeat': appends the key for every value e.g. `ids=1&ids=2`\n * 'comma' (default): appends the key once with a comma separated list of values e.g. `ids=1,2`\n *\n * @public\n * @param {URLSearchParams | Object} params\n * @param {Object} [options]\n * @return {String} A sorted query params string without the leading `?`\n */\nexport function buildQueryParams(query: JsonApiQuery | QueryParamsSource): string {\n if (query instanceof URLSearchParams) {\n return buildParams(query);\n }\n\n if (!isJsonApiQuery(query)) {\n return buildParams(query);\n }\n\n const { include, fields, page, ...rest } = query;\n const finalQuery: QueryParamsSource = {\n ...rest,\n };\n\n if ('include' in query) {\n // include: { 'company': 'field1,field2' }\n // include: { 'company': ['field1', 'field2'] }\n // include: { 'company': { 'nested': 'field1,field2' } }\n // include: { 'company': { 'nested': ['field1', 'field2'] } }\n if (include && !Array.isArray(include) && typeof include === 'object') {\n const includePaths: string[] = [];\n collapseIncludePaths('', include, includePaths);\n finalQuery.include = includePaths.sort();\n\n // include: 'field1,field2'\n // include: ['field1', 'field2']\n } else {\n finalQuery.include = include;\n }\n }\n\n if (fields) {\n const keys = Object.keys(fields).sort();\n for (let i = 0; i < keys.length; i++) {\n const resourceType = keys[i];\n const value = fields[resourceType];\n\n // fields: { 'company': ['field1', 'field2'] }\n if (Array.isArray(value)) {\n // toSorted: `value` belongs to the caller, sorting it in place would\n // reorder whatever else they are doing with it\n finalQuery[`fields[${resourceType}]`] = value.toSorted().join(',');\n\n // fields: { 'company': 'field1' }\n // fields: { 'company': 'field1,field2' }\n } else {\n finalQuery[`fields[${resourceType}]`] = value.split(',').sort().join(',');\n }\n }\n }\n\n if (page) {\n const keys = Object.keys(page).sort() as Array<'size' | 'after' | 'before'>;\n keys.forEach((key) => {\n const value = page[key];\n finalQuery[`page[${key}]`] = value!;\n });\n }\n\n return buildParams(finalQuery);\n}\n","import type { ReactiveDataDocument } from '@warp-drive-mirror/core/reactive';\nimport type { TypeFromInstance } from '@warp-drive-mirror/core/types/record';\nimport type {\n FindRecordOptions,\n FindRecordRequestOptions,\n RemotelyAccessibleIdentifier,\n} from '@warp-drive-mirror/core/types/request';\nimport type { ApiError } from '@warp-drive-mirror/core/types/spec/error';\nimport type { Meta } from '@warp-drive-mirror/core/types/spec/json-api-raw';\n\nimport { buildBaseURL, buildQueryParams, type FindRecordUrlOptions } from '../../index.ts';\nimport { pluralize } from '../../string.ts';\nimport { copyForwardUrlOptions, extractCacheOptions } from '../builder-utils.ts';\nimport { ACCEPT_HEADER_VALUE } from './-utils.ts';\n\n/**\n * Builds request options to fetch a single resource by a known id or identifier\n * configured for the url and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * :::tabs\n *\n * == Basic Usage\n *\n * ```ts\n * import { findRecord } from '@warp-drive-mirror/utilities/json-api';\n * import type { Person } from '#/data/types';\n *\n * const result = await store.request(\n * findRecord<Person>('person', '1')\n * );\n * ```\n *\n * == With Options\n *\n * ```ts\n * import { findRecord } from '@warp-drive-mirror/utilities/json-api';\n * import type { Person } from '#/data/types';\n *\n * const data = await store.request(\n * findRecord<Person>(\n * 'person', '1',\n * { include: ['pets', 'friends'] }\n * )\n * );\n * ```\n *\n * == With an Identifier\n *\n * ```ts\n * import { findRecord } from '@warp-drive-mirror/utilities/json-api';\n * import type { Person } from '#/data/types';\n *\n * const data = await store.request(\n * findRecord<Person>(\n * { type: 'person', id: '1' },\n * { include: ['pets', 'friends'] }\n * )\n * );\n * ```\n *\n * :::\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { findRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const data = await store.request(\n * findRecord(\n * 'person', '1',\n * { include: ['pets', 'friends'] },\n * { namespace: 'api/v2' }\n * )\n * );\n * ```\n *\n * @summary Builds a JSON:API `GET` request for one resource by type and id, with optional `include`.\n * @public\n */\nexport function findRecord<T, M extends Meta | undefined = Meta | undefined, E extends object = ApiError>(\n identifier: RemotelyAccessibleIdentifier<TypeFromInstance<T>>,\n options?: FindRecordOptions\n): FindRecordRequestOptions<ReactiveDataDocument<T, M, E>, T>;\nexport function findRecord(\n identifier: RemotelyAccessibleIdentifier,\n options?: FindRecordOptions\n): FindRecordRequestOptions;\nexport function findRecord<T, M extends Meta | undefined = Meta | undefined, E extends object = ApiError>(\n type: TypeFromInstance<T>,\n id: string,\n options?: FindRecordOptions\n): FindRecordRequestOptions<ReactiveDataDocument<T, M, E>, T>;\nexport function findRecord(type: string, id: string, options?: FindRecordOptions): FindRecordRequestOptions;\nexport function findRecord(\n arg1: string | RemotelyAccessibleIdentifier,\n arg2: string | FindRecordOptions | undefined,\n arg3?: FindRecordOptions\n): FindRecordRequestOptions {\n const identifier: RemotelyAccessibleIdentifier = typeof arg1 === 'string' ? { type: arg1, id: arg2 as string } : arg1;\n const options = ((typeof arg1 === 'string' ? arg3 : arg2) || {}) as FindRecordOptions;\n const cacheOptions = extractCacheOptions(options);\n const urlOptions: FindRecordUrlOptions = {\n identifier,\n op: 'findRecord',\n resourcePath: pluralize(identifier.type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n\n return {\n url: options.include?.length\n ? `${url}?${buildQueryParams({ include: options.include }, options.urlParamsSettings)}`\n : url,\n method: 'GET',\n headers,\n cacheOptions,\n op: 'findRecord',\n records: [identifier],\n };\n}\n\n/** @deprecated use {@link ReactiveDataDocument} */\nexport type FindRecordResultDocument<\n T,\n M extends Meta | undefined = Meta | undefined,\n E extends object = ApiError,\n> = ReactiveDataDocument<T, M, E>;\n","import type { ReactiveDataDocument } from '@warp-drive-mirror/core/reactive';\nimport type { QueryParamsSource } from '@warp-drive-mirror/core/types/params';\nimport type { TypedRecordInstance, TypeFromInstance } from '@warp-drive-mirror/core/types/record';\nimport type {\n CacheOptions,\n ConstrainedRequestOptions,\n PostQueryRequestOptions,\n QueryRequestOptions,\n} from '@warp-drive-mirror/core/types/request';\nimport type { ApiError } from '@warp-drive-mirror/core/types/spec/error';\nimport type { Meta } from '@warp-drive-mirror/core/types/spec/json-api-raw';\n\nimport { buildBaseURL, buildQueryParams, type QueryUrlOptions } from '../../index.ts';\nimport { pluralize } from '../../string.ts';\nimport { copyForwardUrlOptions, extractCacheOptions } from '../builder-utils.ts';\nimport { ACCEPT_HEADER_VALUE } from './-utils.ts';\n/**\n * Builds request options to query for resources, usually by a primary\n * type, configured for the url and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * The key difference between this and `postQuery` is that this method will send the query\n * as query params in the url of a \"GET\" request instead of as the JSON body of a \"POST\"\n * request.\n *\n * **Basic Usage**\n *\n * ```ts\n * import { query } from '@warp-drive-mirror/utilities/json-api';\n *\n * const data = await store.request(query('person'));\n * ```\n *\n * **With Query Params**\n *\n * ```ts\n * import { query } from '@warp-drive-mirror/utilities/json-api';\n *\n * const options = query('person', { include: ['pets', 'friends'] });\n * const data = await store.request(options);\n * ```\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { query } from '@warp-drive-mirror/utilities/json-api';\n *\n * const options = query('person', { include: ['pets', 'friends'] }, { reload: true });\n * const data = await store.request(options);\n * ```\n *\n * @summary Builds a JSON:API `GET` request that queries a resource type, sending the query as URL query params.\n * @public\n * @badge Builder\n */\nexport function query<\n T extends TypedRecordInstance,\n M extends Meta | undefined = Meta | undefined,\n E extends object = ApiError,\n>(\n type: TypeFromInstance<T>,\n query?: QueryParamsSource,\n options?: ConstrainedRequestOptions\n): QueryRequestOptions<ReactiveDataDocument<T[], M, E>>;\nexport function query(\n type: string,\n query?: QueryParamsSource,\n options?: ConstrainedRequestOptions\n): QueryRequestOptions;\nexport function query(\n type: string,\n // oxlint-disable-next-line no-shadow\n query: QueryParamsSource = {},\n options: ConstrainedRequestOptions = {}\n): QueryRequestOptions {\n const cacheOptions = extractCacheOptions(options);\n const urlOptions: QueryUrlOptions = {\n identifier: { type },\n op: 'query',\n resourcePath: pluralize(type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n const queryString = buildQueryParams(query, options.urlParamsSettings);\n\n return {\n url: queryString ? `${url}?${queryString}` : url,\n method: 'GET',\n headers,\n cacheOptions,\n op: 'query',\n };\n}\n\n/**\n * Builds request options to query for resources, usually by a primary\n * type, configured for the url and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * The key difference between this and `query` is that this method will send the query\n * as the JSON body of a \"POST\" request instead of as query params in the url of a \"GET\"\n * request.\n *\n * A CacheKey is generated from the url and query params, and used to cache the response\n * in the store.\n *\n * ```ts\n * import { postQuery } from '@warp-drive-mirror/utilities/json-api';\n *\n * const options = postQuery('person', { include: ['pets', 'friends'] });\n * const data = await store.request(options);\n * ```\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { postQuery } from '@warp-drive-mirror/utilities/json-api';\n *\n * const options = postQuery('person', { include: ['pets', 'friends'] }, { reload: true });\n * const data = await store.request(options);\n * ```\n *\n * @summary Builds a JSON:API `POST` request that sends a query as the JSON body, cached under a key built from the\n * query params.\n * @public\n * @param type - the name of the resource type to query\n * @param query - the query params to send with the request\n * @param options - options to modify the request behavior\n */\nexport function postQuery<T, M extends Meta | undefined = Meta | undefined, E extends object = ApiError>(\n type: TypeFromInstance<T>,\n query?: QueryParamsSource,\n options?: ConstrainedRequestOptions\n): PostQueryRequestOptions<ReactiveDataDocument<T[], M, E>>;\nexport function postQuery(\n type: string,\n query?: QueryParamsSource,\n options?: ConstrainedRequestOptions\n): PostQueryRequestOptions;\nexport function postQuery(\n type: string,\n // oxlint-disable-next-line no-shadow\n query: QueryParamsSource = {},\n options: ConstrainedRequestOptions = {}\n): PostQueryRequestOptions {\n const cacheOptions = extractCacheOptions(options);\n const urlOptions: QueryUrlOptions = {\n identifier: { type },\n op: 'query',\n resourcePath: options.resourcePath ?? pluralize(type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n\n const queryData = structuredClone(query);\n cacheOptions.key = cacheOptions.key ?? `${url}?${buildQueryParams(queryData, options.urlParamsSettings)}`;\n\n return {\n url,\n method: 'POST',\n body: JSON.stringify(query),\n headers,\n cacheOptions: cacheOptions as CacheOptions & { key: string },\n op: 'query',\n };\n}\n","import { recordIdentifierFor } from '@warp-drive-mirror/core';\nimport { assert } from '@warp-drive-mirror/core/build-config/macros';\nimport type { ReactiveDataDocument } from '@warp-drive-mirror/core/reactive';\nimport type { PersistedResourceKey, ResourceKey } from '@warp-drive-mirror/core/types/identifier';\nimport type { TypedRecordInstance } from '@warp-drive-mirror/core/types/record';\nimport type {\n ConstrainedRequestOptions,\n CreateRequestOptions,\n DeleteRequestOptions,\n UpdateRequestOptions,\n} from '@warp-drive-mirror/core/types/request';\nimport type { ApiError } from '@warp-drive-mirror/core/types/spec/error';\nimport type { Meta } from '@warp-drive-mirror/core/types/spec/json-api-raw';\n\nimport {\n buildBaseURL,\n type CreateRecordUrlOptions,\n type DeleteRecordUrlOptions,\n type UpdateRecordUrlOptions,\n} from '../../index.ts';\nimport { pluralize } from '../../string.ts';\nimport { copyForwardUrlOptions } from '../builder-utils.ts';\nimport { ACCEPT_HEADER_VALUE } from './-utils.ts';\n\nfunction isExisting(identifier: ResourceKey): identifier is PersistedResourceKey {\n return 'id' in identifier && identifier.id !== null && 'type' in identifier && identifier.type !== null;\n}\n\n/**\n * :::warning ⚠️ **These Mutation Builders DO NOT Set The Request Body**\n * While this may come as a surprise, the app providing the body ensures that only\n * desired and correctly formatted data is sent with the request.\n * :::\n *\n * Builds request options to delete record for resources,\n * configured for the url, method and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * **Basic Usage**\n *\n * ```ts\n * import { deleteRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.peekRecord('person', '1');\n *\n * // mark record as deleted\n * store.deleteRecord(person);\n *\n * // persist deletion\n * const data = await store.request(deleteRecord(person));\n * ```\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { deleteRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.peekRecord('person', '1');\n *\n * // mark record as deleted\n * store.deleteRecord(person);\n *\n * // persist deletion\n * const options = deleteRecord(person, { namespace: 'api/v1' });\n * const data = await store.request(options);\n * ```\n *\n * @summary Builds a JSON:API `DELETE` request that persists deletion of an existing record.\n * @public\n * @param record\n * @param options\n */\nexport function deleteRecord<T>(record: T, options?: ConstrainedRequestOptions): DeleteRequestOptions<T>;\nexport function deleteRecord(record: unknown, options?: ConstrainedRequestOptions): DeleteRequestOptions;\nexport function deleteRecord(record: unknown, options: ConstrainedRequestOptions = {}): DeleteRequestOptions {\n const identifier = recordIdentifierFor(record);\n assert(`Expected to be given a record instance`, identifier);\n assert(`Cannot delete a record that does not have an associated type and id.`, isExisting(identifier));\n\n const urlOptions: DeleteRecordUrlOptions = {\n identifier: identifier,\n op: 'deleteRecord',\n resourcePath: pluralize(identifier.type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n\n return {\n url,\n method: 'DELETE',\n headers,\n op: 'deleteRecord',\n data: {\n record: identifier,\n },\n records: [identifier],\n };\n}\n\n/**\n * :::warning ⚠️ **These Mutation Builders DO NOT Set The Necessary Request Body**\n * While this may come as a surprise, the app providing the body ensures that only\n * desired and correctly formatted data is sent with the request.\n * :::\n *\n * Builds request options to create new record for resources,\n * configured for the url, method and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * **Basic Usage**\n *\n * ```ts\n * import { cacheKeyFor } from '@warp-drive-mirror/core';\n * import { createRecord } from '@warp-drive-mirror/utilities/json-api';\n * import type { Person } from '#/data/types';\n *\n * const person = store.createRecord<Person>('person', { name: 'Ted' });\n * const init = createRecord(person);\n * init.body = JSON.stringify(\n * {\n * // it's likely you will want to transform this data\n * // somewhat\n * data: store.cache.peek(cacheKeyFor(person))\n * }\n * );\n * const data = await store.request(init);\n * ```\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { createRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.createRecord('person', { name: 'Ted' });\n * const options = createRecord(person, { namespace: 'api/v1' });\n * const data = await store.request(options);\n * ```\n *\n * @summary Builds a JSON:API `POST` request for saving a new record; the app must set the request body itself.\n * @public\n * @param record\n * @param options\n */\nexport function createRecord<T>(record: T, options?: ConstrainedRequestOptions): CreateRequestOptions<T>;\nexport function createRecord(record: unknown, options?: ConstrainedRequestOptions): CreateRequestOptions;\nexport function createRecord(record: unknown, options: ConstrainedRequestOptions = {}): CreateRequestOptions {\n const identifier = recordIdentifierFor(record);\n assert(`Expected to be given a record instance`, identifier);\n\n const urlOptions: CreateRecordUrlOptions = {\n identifier: identifier,\n op: 'createRecord',\n resourcePath: pluralize(identifier.type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n\n return {\n url,\n method: 'POST',\n headers,\n op: 'createRecord',\n data: {\n record: identifier,\n },\n records: [identifier],\n };\n}\n\n/**\n * :::warning ⚠️ **These Mutation Builders DO NOT Set The Necessary Request Body**\n * While this may come as a surprise, the app providing the body ensures that only\n * desired and correctly formatted data is sent with the request.\n * :::\n *\n * Builds request options to update existing record for resources,\n * configured for the url, method and header expectations of most JSON:API APIs.\n *\n * See the [Builders](/guides/the-manual/requests/builders) guide for what request builders do and\n * when to use one.\n *\n * **Example Usage**\n *\n * ```ts\n * import { cacheKeyFor } from '@warp-drive-mirror/core';\n * import { updateRecord } from '@warp-drive-mirror/utilities/json-api';\n * import type { EditablePerson } from '#/data/types';\n *\n * const mutable = await checkout<EditablePerson>(person);\n * mutable.name = 'Chris';\n * const init = updateRecord(mutable);\n *\n * init.body = JSON.stringify(\n * // it's likely you will want to transform this data\n * // somewhat, or serialize only specific properties instead\n * serializePatch(store.cache, cacheKeyFor(mutable))\n * );\n * const data = await store.request(init);\n * ```\n *\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `patch` - Allows caller to specify whether to use a PATCH request instead of a PUT request, defaults to `false`.\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { updateRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.peekRecord('person', '1');\n * person.name = 'Chris';\n * const options = updateRecord(person, { patch: true });\n * const data = await store.request(options);\n * ```\n *\n * @summary Builds a JSON:API `PUT` (or `PATCH`) request for saving an existing record; the app must set the request\n * body itself.\n * @public\n * @param record\n * @param options\n */\nexport function updateRecord<\n T extends TypedRecordInstance,\n RT extends TypedRecordInstance = T,\n M extends Meta | undefined = Meta | undefined,\n E extends object = ApiError,\n>(\n record: T,\n options?: ConstrainedRequestOptions & { patch?: boolean }\n): UpdateRequestOptions<ReactiveDataDocument<RT, M, E>, T>;\nexport function updateRecord(\n record: unknown,\n options?: ConstrainedRequestOptions & { patch?: boolean }\n): UpdateRequestOptions;\nexport function updateRecord(\n record: unknown,\n options: ConstrainedRequestOptions & { patch?: boolean } = {}\n): UpdateRequestOptions {\n const identifier = recordIdentifierFor(record);\n assert(`Expected to be given a record instance`, identifier);\n assert(`Cannot update a record that does not have an associated type and id.`, isExisting(identifier));\n\n const urlOptions: UpdateRecordUrlOptions = {\n identifier: identifier,\n op: 'updateRecord',\n resourcePath: pluralize(identifier.type),\n };\n\n copyForwardUrlOptions(urlOptions, options);\n\n const url = buildBaseURL(urlOptions);\n const headers = new Headers();\n headers.append('Accept', ACCEPT_HEADER_VALUE);\n\n return {\n url,\n method: options.patch ? 'PATCH' : 'PUT',\n headers,\n op: 'updateRecord',\n data: {\n record: identifier,\n },\n records: [identifier],\n };\n}\n\n/**\n * Builds request options to update existing record for resources,\n * configured for the url and header expectations of most JSON:API APIs\n * for a PATCH request.\n *\n * Note: This is a convenience method that calls `updateRecord` with the\n * supplied request with the `patch` option set to `true`.\n *\n * **Basic Usage**\n *\n * ```ts\n * import { patchRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.peekRecord('person', '1');\n * person.name = 'Chris';\n * const data = await store.request(patchRecord(person));\n * ```\n *\n * **Supplying Options to Modify the Request Behavior**\n *\n * The following options are supported:\n *\n * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.\n * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.\n * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type\n * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this\n * option will delegate to the store's CachePolicy, defaulting to `false` if none is configured.\n * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the\n * promise with the cached value, not supplying this option will delegate to the store's CachePolicy,\n * defaulting to `false` if none is configured.\n * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)\n *\n * ```ts\n * import { patchRecord } from '@warp-drive-mirror/utilities/json-api';\n *\n * const person = store.peekRecord('person', '1');\n * person.name = 'Chris';\n * const options = patchRecord(person);\n * const data = await store.request(options);\n * ```\n *\n * @public\n * @param record\n * @param options\n */\nexport function patchRecord<T>(record: T, options?: ConstrainedRequestOptions): UpdateRequestOptions<T>;\nexport function patchRecord(record: unknown, options?: ConstrainedRequestOptions): UpdateRequestOptions;\nexport function patchRecord(record: unknown, options: ConstrainedRequestOptions = {}): UpdateRequestOptions {\n const opts = options as ConstrainedRequestOptions & { patch: true };\n opts.patch = true;\n return updateRecord(record, opts);\n}\n","import { assert } from '@warp-drive-mirror/core/build-config/macros';\nimport type { Cache } from '@warp-drive-mirror/core/types/cache';\nimport type { Relationship } from '@warp-drive-mirror/core/types/cache/relationship';\nimport type { ResourceKey } from '@warp-drive-mirror/core/types/identifier';\nimport type { Value } from '@warp-drive-mirror/core/types/json/raw';\nimport type { InnerRelationshipDocument, ResourceObject } from '@warp-drive-mirror/core/types/spec/json-api-raw';\n\ntype ChangedRelationshipData = InnerRelationshipDocument;\n\n/**\n * The resource object {@link serializePatch} produces: the resource's `type` and `id` (or `lid`\n * for a new record) with only its changed attributes and relationships.\n *\n * @summary A JSON:API resource object holding only a record's changed attributes and relationships, as produced by `serializePatch`.\n * @public\n */\nexport type JsonApiResourcePatch =\n | {\n type: string;\n id: string;\n attributes?: Record<string, Value>;\n relationships?: Record<string, ChangedRelationshipData>;\n }\n | {\n type: string;\n id: null;\n lid: string;\n attributes?: Record<string, Value>;\n relationships?: Record<string, ChangedRelationshipData>;\n };\n\n/**\n * :::warning ⚠️ **This util often won't produce the necessary body for a {json:api} request**\n *\n * While this may come as a surprise, they are intended to serialize cache state for more\n * generalized usage. {json:api} has a large variance in acceptable shapes, and only your\n * app can ensure that the body is correctly formatted and contains all necessary data.\n * :::\n *\n * Serializes the current state of a resource or array of resources for use with POST or PUT requests.\n *\n * @summary Copies the cached state of one or more resources into a JSON:API-style `{ data }` object, a starting point\n * for request bodies.\n * @public\n * @param cache - the cache to serialize the resource(s) from\n * @param identifiers - the resource(s) to serialize\n * @return an object with a `data` property containing the serialized resource(s)\n */\nexport function serializeResources(\n cache: Cache,\n identifiers: ResourceKey\n): {\n /**\n * The serialized resource.\n */\n data: ResourceObject;\n};\nexport function serializeResources(\n cache: Cache,\n identifiers: ResourceKey[]\n): {\n /**\n * The serialized resources.\n */\n data: ResourceObject[];\n};\nexport function serializeResources(\n cache: Cache,\n identifiers: ResourceKey | ResourceKey[]\n): { data: ResourceObject | ResourceObject[] } {\n return {\n data: Array.isArray(identifiers)\n ? identifiers.map((identifier) => _serializeResource(cache, identifier))\n : _serializeResource(cache, identifiers),\n };\n}\n\ntype SerializedRef =\n | {\n id: string;\n type: string;\n }\n | { id: null; lid: string; type: string };\n\nfunction fixRef({\n id,\n lid,\n type,\n}: { id: string; lid?: string; type: string } | { id: null; lid: string; type: string }): SerializedRef {\n if (id !== null) {\n return { id, type };\n }\n return { id, lid, type };\n}\n\nfunction fixRelData(\n rel: Relationship['data'] | InnerRelationshipDocument['data']\n): SerializedRef | SerializedRef[] | null {\n if (Array.isArray(rel)) {\n return rel.map((ref) => fixRef(ref));\n } else if (typeof rel === 'object' && rel !== null) {\n return fixRef(rel);\n }\n return null;\n}\n\nfunction _serializeResource(cache: Cache, identifier: ResourceKey): ResourceObject {\n const { id, lid, type } = identifier;\n // peek gives us everything we want, but since its referentially the same data\n // as is in the cache we clone it to avoid any accidental mutations\n const record = structuredClone(cache.peek(identifier)) as ResourceObject;\n assert(\n `A record with id ${String(id)} and type ${type} for lid ${lid} was not found not in the supplied Cache.`,\n record\n );\n\n // remove lid from anything that has an ID and slice any relationship arrays\n if (record.id !== null) {\n delete record.lid;\n }\n\n if (record.relationships) {\n for (const key of Object.keys(record.relationships)) {\n const relationship = record.relationships[key];\n if (Array.isArray(relationship.data)) {\n relationship.data = relationship.data.map((ref) => fixRef(ref));\n } else if (typeof relationship.data === 'object' && relationship.data !== null) {\n relationship.data = fixRef(relationship.data);\n } else if (Object.keys(relationship ?? {}).length === 0) {\n delete record.relationships[key];\n }\n }\n }\n\n return record;\n}\n\n/**\n * :::warning ⚠️ **This util often won't produce the necessary body for a {json:api} request**\n *\n * While this may come as a surprise, they are intended to serialize cache state for more\n * generalized usage. {json:api} has a large variance in acceptable shapes, and only your\n * app can ensure that the body is correctly formatted and contains all necessary data.\n * :::\n *\n * Serializes changes to a resource. Useful for use with building bodies for PATCH requests.\n *\n * Only attributes which are changed are serialized.\n * Only relationships which are changed are serialized.\n *\n * Collection relationships serialize the collection as a whole.\n *\n * If you would like to serialize updates to a collection more granularly\n * (for instance, as operations) request the diff from the store and\n * serialize as desired:\n *\n * ```ts\n * const relationshipDiffMap = cache.changedRelationships(identifier);\n * ```\n *\n * @summary Serializes only a resource's changed attributes and relationships from the cache into a JSON:API-style\n * `{ data }` patch.\n * @public\n * @param cache - the cache to serialize the resource's changes from\n * @param identifier - the resource whose changes should be serialized\n * @return an object with a `data` property containing the serialized resource patch\n */\nexport function serializePatch(\n cache: Cache,\n identifier: ResourceKey\n // options: { include?: string[] } = {}\n): {\n /**\n * The serialized resource patch.\n */\n data: JsonApiResourcePatch;\n} {\n const { id, lid, type } = identifier;\n assert(\n `A record with id ${String(id)} and type ${type} for lid ${lid} was not found not in the supplied Cache.`,\n cache.peek(identifier)\n );\n\n const data: JsonApiResourcePatch =\n id === null\n ? { type, lid, id }\n : {\n type,\n id,\n };\n\n if (cache.hasChangedAttrs(identifier)) {\n const attrsChanges = cache.changedAttrs(identifier);\n const attributes: ResourceObject['attributes'] = {};\n\n Object.keys(attrsChanges).forEach((key) => {\n const change = attrsChanges[key];\n const newVal = change[1];\n attributes[key] = newVal === undefined ? null : structuredClone(newVal);\n });\n\n data.attributes = attributes;\n }\n\n const changedRelationships = cache.changedRelationships(identifier);\n if (changedRelationships.size) {\n const relationships: Record<string, ChangedRelationshipData> = {};\n changedRelationships.forEach((diff, key) => {\n relationships[key] = { data: fixRelData(diff.localState) } as ChangedRelationshipData;\n });\n\n data.relationships = relationships;\n }\n\n return { data };\n}\n"],"mappings":"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"}
@@ -23,8 +23,17 @@ const SupportsRequestStreams = (() => {
23
23
  return duplexAccessed && !hasContentType;
24
24
  })();
25
25
  /**
26
- * Options for configuring the AutoCompress handler.
26
+ * The minimum body size, per body type, at which {@link AutoCompress} compresses a request
27
+ * body. Passed as {@link CompressionOptions.constraints}.
27
28
  *
29
+ * @summary Minimum request body sizes, per body type, at which `AutoCompress` compresses a body.
30
+ * @public
31
+ */
32
+ /**
33
+ * Options for configuring the {@link AutoCompress} handler.
34
+ *
35
+ * @summary Options for `new AutoCompress()`: the compression format, size constraints and whether to stream the body.
36
+ * @public
28
37
  */
29
38
  const DEFAULT_CONSTRAINTS = {
30
39
  Blob: 1e3,
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.js","names":["isCompressibleMethod","method","SupportsRequestStreams","duplexAccessed","hasContentType","Request","body","ReadableStream","duplex","headers","has","DEFAULT_CONSTRAINTS","Blob","ArrayBuffer","TypedArray","DataView","String","Object","getPrototypeOf","Uint8Array","AutoCompress","constructor","options","opts","format","constraints","assign","allowStreaming","forceStreaming","request","next","shouldCompress","compress","canCompress","length","size","byteLength","response","Response","stream","pipeThrough","CompressionStream","Headers","set","encodingForFormat","req","resp","blob","then","type","test","Error","Gate","constructor","handler","checkFn","request","context","next","FastBoot","globalThis","addEventListener","sessionStorage","setItem","getTabId","crypto","randomUUID","tabId","getItem","tabClosed","newTabId","TAB_ID","TAB_ASSIGNED","Math","floor","Date","now","addTraceHeader","headers","set","MAX_URL_LENGTH","assertInvalidUrlLength","url","test","Error","length"],"sources":["../../../src/-private/handlers/auto-compress.ts","../../../src/-private/handlers/gated.ts","../../../src/-private/handlers/meta-doc.ts","../../../src/-private/handlers/utils.ts"],"sourcesContent":["import { assert } from '@warp-drive-mirror/core/build-config/macros';\nimport type { Future, Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { HTTPMethod, RequestContext } from '@warp-drive-mirror/core/types/request';\n\nfunction isCompressibleMethod(method?: HTTPMethod): boolean {\n return method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE';\n}\n\n/**\n * Whether the browser supports `ReadableStream` as a request body\n * in a `POST` request.\n *\n * @summary Whether the current browser can send a `ReadableStream` as a `fetch` request body, detected once at\n * module load.\n * @group Constants\n */\nexport const SupportsRequestStreams: boolean = (() => {\n let duplexAccessed = false;\n\n const hasContentType = new Request('', {\n body: new ReadableStream(),\n method: 'POST',\n // @ts-expect-error untyped\n get duplex() {\n duplexAccessed = true;\n return 'half';\n },\n }).headers.has('Content-Type');\n\n return duplexAccessed && !hasContentType;\n})();\n\ninterface Constraints {\n /**\n * The minimum size at which to compress blobs\n *\n * @default 1000\n */\n Blob?: number;\n /**\n * The minimum size at which to compress array buffers\n *\n * @default 1000\n */\n ArrayBuffer?: number;\n /**\n * The minimum size at which to compress typed arrays\n *\n * @default 1000\n */\n TypedArray?: number;\n /**\n * The minimum size at which to compress data views\n *\n * @default 1000\n */\n DataView?: number;\n /**\n * The minimum size at which to compress strings\n *\n * @default 1000\n */\n String?: number;\n}\n\n/**\n * Options for configuring the AutoCompress handler.\n *\n */\ninterface CompressionOptions {\n /**\n * The compression format to use. Must be a valid\n * compression format supported by [CompressionStream](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)\n *\n * The default is `gzip`.\n *\n */\n format?: CompressionFormat;\n\n /**\n * Some browsers support `ReadableStream` as a request body. This option\n * enables passing the compression stream as the request body instead of\n * the final compressed body when the browser supports doing so.\n *\n * This comes with several caveats:\n *\n * - the request will be put into `duplex: 'half'` mode. This should be\n * transparent to you, but it is worth noting.\n * - the request mode cannot be `no-cors` as requests with a `ReadableStream`\n * have no content length and thus are a new form of request that triggers\n * cors requirements and a preflight request.\n * - http/1.x is not supported.\n *\n * For additional reading about the restrictions of using `ReadableStream`\n * as a request body, see the [Chromium Documentation](https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests#restrictions)\n *\n * Streaming can be enabled per-request in browsers which support it by\n * setting `request.options.allowStreaming` to `true`.\n *\n * Streaming can be forced even when the browser does not support it by setting\n * `request.options.forceStreaming` to `true`. This is useful if later handlers\n * in the chain can handle the request body as a stream.\n *\n * @default false\n */\n allowStreaming?: boolean;\n\n /**\n * If `true`, the request will be forced into streaming mode even\n * if the browser does not support it. This is useful if later handlers\n * in the chain can handle the request body as a stream.\n *\n * @default false\n */\n forceStreaming?: boolean;\n\n /**\n * The constraints for the request body. This is used to determine\n * whether to compress the request body or not.\n *\n * The defaults are:\n *\n * ```ts\n * {\n * Blob: 1000, // blob.size\n * ArrayBuffer: 1000, // buffer.byteLength\n * TypedArray: 1000, // array.byteLength\n * DataView: 1000, // view.byteLength\n * String: 1000, // string.length\n * }\n * ```\n *\n * The following body types are never compressed unless explicitly\n * configured by the request:\n * - `FormData`\n * - `URLSearchParams`\n * - `ReadableStream`\n *\n * A request.options.compress value of `false` will disable\n * compression for a request body of any type. While a value of\n * `true` will enable compression for the request.\n *\n * An undefined value will use the default, a value of `0` will\n * enable compression for all values, and a value of `-1` will\n * disable compression.\n *\n */\n constraints?: Constraints;\n}\n\nconst DEFAULT_CONSTRAINTS = {\n Blob: 1000,\n ArrayBuffer: 1000,\n TypedArray: 1000,\n DataView: 1000,\n String: 1000,\n};\nconst TypedArray = Object.getPrototypeOf(Uint8Array) as typeof Uint8Array;\n\n/**\n * A request handler that automatically compresses the request body\n * if the request body is a string, array buffer, blob, or form data.\n *\n * This uses the [CompressionStream API](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)\n *\n * The compression format as well as the kinds of data to compress can be\n * configured using the `format` and `constraints` options.\n *\n * ```diff\n * +import { AutoCompress } from '@ember-data-mirror/request-utils/handlers';\n * import Fetch from '@ember-data-mirror/request/fetch';\n * import RequestManager from '@ember-data-mirror/request';\n * import Store from '@ember-data-mirror/store';\n *\n * class AppStore extends Store {\n * requestManager = new RequestManager()\n * .use([\n * + new AutoCompress(),\n * Fetch\n * ]);\n * }\n * ```\n *\n * @summary Request handler that compresses large `POST`, `PUT`, `PATCH`, and `DELETE` bodies with `CompressionStream`,\n * optionally streaming them.\n * @group Handlers\n * @public\n * @since 5.5.0\n */\nexport class AutoCompress implements Handler {\n /**\n * The resolved options this handler was configured with, with all\n * defaults (including `constraints` defaults) applied.\n */\n declare options: Required<CompressionOptions> & {\n /**\n * The resolved size constraints used to decide whether to compress a given request body.\n */\n constraints: Required<Constraints>;\n };\n\n constructor(options: CompressionOptions = {}) {\n const opts = {\n format: options.format ?? 'gzip',\n constraints: Object.assign({}, DEFAULT_CONSTRAINTS, options.constraints),\n allowStreaming: options.allowStreaming ?? false,\n forceStreaming: options.forceStreaming ?? false,\n };\n this.options = opts;\n }\n\n request<T>({ request }: RequestContext, next: NextFn<T>): Promise<T> | Future<T> {\n const { constraints } = this.options;\n const { body } = request;\n\n const shouldCompress =\n !!body &&\n isCompressibleMethod(request.method) &&\n request.options?.compress !== false &&\n // prettier-ignore\n (request.options?.compress ? true\n : typeof body === 'string' || body instanceof String ? canCompress('String', constraints, body.length)\n : body instanceof Blob ? canCompress('Blob', constraints, body.size)\n : body instanceof ArrayBuffer ? canCompress('ArrayBuffer', constraints, body.byteLength)\n : body instanceof DataView ? canCompress('DataView', constraints, body.byteLength)\n : body instanceof TypedArray ? canCompress('TypedArray', constraints, body.byteLength)\n : false);\n\n if (!shouldCompress) return next(request);\n\n // A convenient way to convert all of the supported body types to a readable\n // stream is to use a `Response` object body\n const response = new Response(request.body);\n const stream = response.body?.pipeThrough(new CompressionStream(this.options.format));\n const headers = new Headers(request.headers);\n headers.set('Content-Encoding', encodingForFormat(this.options.format));\n\n //\n // For browsers that support it, `fetch` can receive a `ReadableStream` as\n // the body, so all we need to do is to create a new `ReadableStream` and\n // compress it on the fly\n //\n const forceStreaming = request.options?.forceStreaming ?? this.options.forceStreaming;\n const allowStreaming = request.options?.allowStreaming ?? this.options.allowStreaming;\n if (forceStreaming || (SupportsRequestStreams && allowStreaming)) {\n const req = Object.assign({}, request, {\n body: stream,\n headers,\n });\n if (SupportsRequestStreams) {\n // @ts-expect-error untyped\n req.duplex = 'half';\n }\n\n return next(req);\n\n //\n // For non-chromium browsers, we have to \"pull\" the stream to get the final\n // bytes and supply the final byte array as the new request body.\n //\n }\n\n // we need to pull the stream to get the final bytes\n const resp = new Response(stream);\n return resp.blob().then((blob) => {\n const req = Object.assign({}, request, {\n body: blob,\n headers,\n });\n return next(req);\n }) as Promise<T>;\n }\n}\n\nfunction canCompress(type: keyof Constraints, constraints: Required<Constraints>, size: number): boolean {\n // if we have a value of 0, we can compress anything\n if (constraints[type] === 0) return true;\n if (constraints[type] === -1) return false;\n return size >= constraints[type];\n}\n\nfunction encodingForFormat(format: CompressionFormat): string {\n switch (format) {\n case 'gzip':\n case 'deflate':\n case 'deflate-raw':\n return format;\n default:\n assert(`Unsupported compression format: ${format as unknown as string}`);\n // @ts-expect-error - unreachable code is reachable in production\n return format;\n }\n}\n","import type { Future, Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { RequestContext, StructuredDataDocument } from '@warp-drive-mirror/core/types/request';\n\n/**\n * If CheckFn returns true, the wrapped handler will be used.\n * If CheckFn returns false, the wrapped handler will be skipped.\n */\ntype CheckFn = (context: RequestContext) => boolean;\n\n/**\n *\n * @summary Request handler that wraps another handler and runs it only when a check function returns true for the\n * request.\n * @group Handlers\n * @public\n */\nexport class Gate implements Handler {\n /**\n * The wrapped handler to invoke when {@link Gate.checkFn | checkFn} returns `true`.\n */\n declare handler: Handler;\n /**\n * The predicate used to decide whether {@link Gate.handler | handler} should run for a given request.\n */\n declare checkFn: CheckFn;\n\n constructor(handler: Handler, checkFn: CheckFn) {\n this.handler = handler;\n this.checkFn = checkFn;\n }\n\n request<T = unknown>(context: RequestContext, next: NextFn<T>): Promise<T | StructuredDataDocument<T>> | Future<T> {\n if (this.checkFn(context)) {\n return this.handler.request(context, next);\n }\n return next(context.request);\n }\n}\n","import type { StoreRequestContext } from '@warp-drive-mirror/core';\nimport type { Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { StructuredDataDocument } from '@warp-drive-mirror/core/types/request';\n\n/**\n * MetaDocHandler processes requests that are marked as meta requests.\n *\n * It treats the response body as \"entirely meta\" transforming\n *\n * ```ts\n * {\n * some: \"key\",\n * another: \"thing\"\n * }\n * ```\n *\n * into\n *\n * ```ts\n * {\n * \t meta: {\n * some: \"key\",\n * another: \"thing\"\n * }\n * }\n * ```\n *\n * To activate this handler, a request should specify\n *\n * ```ts\n * options.isMetaRequest = true\n * ```\n *\n * For instance\n *\n * ```ts\n * store.request({\n * url: '/example',\n * options: {\n * isMetaRequest: true\n * }\n * });\n * ```\n *\n * Errors are not processed by this handler, so if the request fails and the error response\n * is not in {json:api} format additional processing may be needed.\n *\n * @summary Request handler that wraps the whole response body in `meta` for requests with `options.isMetaRequest` set.\n * @group Handlers\n */\nexport const MetaDocHandler: Handler = {\n request<T>(context: StoreRequestContext, next: NextFn<T>): Promise<T | StructuredDataDocument<T> | Awaited<T>> {\n if (!context.request.options?.isMetaRequest) {\n return next(context.request);\n }\n\n return next(context.request).then((response) => {\n return processResponse<T>(response);\n });\n },\n};\n\nfunction processResponse<T>(response: StructuredDataDocument<T>): T {\n return {\n meta: response.content,\n } as T;\n}\n","import { assert } from '@warp-drive-mirror/core/build-config/macros';\n\nif (typeof FastBoot === 'undefined') {\n globalThis.addEventListener('beforeunload', function () {\n sessionStorage.setItem('tab-closed', 'true');\n });\n}\n\nfunction getTabId() {\n if (typeof sessionStorage === 'undefined') {\n return crypto.randomUUID();\n }\n\n const tabId = sessionStorage.getItem('tab-id');\n if (tabId) {\n const tabClosed = sessionStorage.getItem('tab-closed');\n if (tabClosed === 'true') {\n return tabId;\n }\n\n // fall through to generate a new tab id\n }\n\n const newTabId = crypto.randomUUID();\n sessionStorage.setItem('tab-id', newTabId);\n return newTabId;\n}\n\n/**\n * A unique identifier for the current browser tab\n * useful for observability/tracing and deduping\n * across multiple tabs.\n *\n * @summary Random UUID identifying the current browser tab, kept in `sessionStorage` across reloads, for request\n * tracing.\n * @group Constants\n */\nexport const TAB_ID: string = getTabId();\n/**\n * The epoch seconds at which the tab id was generated\n *\n * @summary Epoch seconds recorded when this module loaded, paired with `TAB_ID` in trace headers.\n * @group Constants\n */\nexport const TAB_ASSIGNED: number = Math.floor(Date.now() / 1000);\n\n/**\n * Adds the `X-Amzn-Trace-Id` header to support observability\n * tooling around request routing.\n *\n * This makes use of the {@link TAB_ID} and {@link TAB_ASSIGNED}\n * to enable tracking the browser tab of origin across multiple requests.\n *\n * Follows the template: `Root=1-${now}-${uuidv4};TabId=1-${epochSeconds}-${tab-uuid}`\n *\n * @summary Sets an `X-Amzn-Trace-Id` header carrying a per-request id and the tab id, so requests can be traced to\n * their browser tab.\n * @group Utility Functions\n */\nexport function addTraceHeader(headers: Headers): Headers {\n const now = Math.floor(Date.now() / 1000);\n headers.set('X-Amzn-Trace-Id', `Root=1-${now}-${crypto.randomUUID()};TabId=1-${TAB_ASSIGNED}-${TAB_ID}`);\n\n return headers;\n}\n\n/**\n * Source: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html\n * As of 2024-12-05 the maximum URL length is 8192 bytes.\n *\n * @summary The 8192-byte URL length limit, taken from AWS CloudFront, that `assertInvalidUrlLength` checks against.\n * @group Constants\n */\nexport const MAX_URL_LENGTH = 8192;\n\n/**\n * This assertion takes a URL and throws an error if the URL is longer than the maximum URL length.\n *\n * See also {@link MAX_URL_LENGTH}\n *\n * @summary Dev-mode assertion that throws when a URL is longer than `MAX_URL_LENGTH`, suggesting a `POST` or `QUERY`\n * request instead.\n * @group Utility Functions\n */\nexport function assertInvalidUrlLength(url: string | undefined): void {\n assert(\n `URL length ${url?.length} exceeds the maximum URL length of ${MAX_URL_LENGTH} bytes.\\n\\nConsider converting this request query a \\`/query\\` endpoint instead of a GET, or upgrade the current endpoint to be able to receive a POST request directly (ideally specifying the header HTTP-Method-Override: QUERY)\\n\\nThe Invalid URL is:\\n\\n${url}`,\n !url || url.length <= MAX_URL_LENGTH\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"handlers.js","names":["isCompressibleMethod","method","SupportsRequestStreams","duplexAccessed","hasContentType","Request","body","ReadableStream","duplex","headers","has","DEFAULT_CONSTRAINTS","Blob","ArrayBuffer","TypedArray","DataView","String","Object","getPrototypeOf","Uint8Array","AutoCompress","constructor","options","opts","format","constraints","assign","allowStreaming","forceStreaming","request","next","shouldCompress","compress","canCompress","length","size","byteLength","response","Response","stream","pipeThrough","CompressionStream","Headers","set","encodingForFormat","req","resp","blob","then","type","test","Error","Gate","constructor","handler","checkFn","request","context","next","FastBoot","globalThis","addEventListener","sessionStorage","setItem","getTabId","crypto","randomUUID","tabId","getItem","tabClosed","newTabId","TAB_ID","TAB_ASSIGNED","Math","floor","Date","now","addTraceHeader","headers","set","MAX_URL_LENGTH","assertInvalidUrlLength","url","test","Error","length"],"sources":["../../../src/-private/handlers/auto-compress.ts","../../../src/-private/handlers/gated.ts","../../../src/-private/handlers/meta-doc.ts","../../../src/-private/handlers/utils.ts"],"sourcesContent":["import { assert } from '@warp-drive-mirror/core/build-config/macros';\nimport type { Future, Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { HTTPMethod, RequestContext } from '@warp-drive-mirror/core/types/request';\n\nfunction isCompressibleMethod(method?: HTTPMethod): boolean {\n return method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE';\n}\n\n/**\n * Whether the browser supports `ReadableStream` as a request body\n * in a `POST` request.\n *\n * @summary Whether the current browser can send a `ReadableStream` as a `fetch` request body, detected once at\n * module load.\n * @group Constants\n */\nexport const SupportsRequestStreams: boolean = (() => {\n let duplexAccessed = false;\n\n const hasContentType = new Request('', {\n body: new ReadableStream(),\n method: 'POST',\n // @ts-expect-error untyped\n get duplex() {\n duplexAccessed = true;\n return 'half';\n },\n }).headers.has('Content-Type');\n\n return duplexAccessed && !hasContentType;\n})();\n\n/**\n * The minimum body size, per body type, at which {@link AutoCompress} compresses a request\n * body. Passed as {@link CompressionOptions.constraints}.\n *\n * @summary Minimum request body sizes, per body type, at which `AutoCompress` compresses a body.\n * @public\n */\nexport interface Constraints {\n /**\n * The minimum size at which to compress blobs\n *\n * @default 1000\n */\n Blob?: number;\n /**\n * The minimum size at which to compress array buffers\n *\n * @default 1000\n */\n ArrayBuffer?: number;\n /**\n * The minimum size at which to compress typed arrays\n *\n * @default 1000\n */\n TypedArray?: number;\n /**\n * The minimum size at which to compress data views\n *\n * @default 1000\n */\n DataView?: number;\n /**\n * The minimum size at which to compress strings\n *\n * @default 1000\n */\n String?: number;\n}\n\n/**\n * Options for configuring the {@link AutoCompress} handler.\n *\n * @summary Options for `new AutoCompress()`: the compression format, size constraints and whether to stream the body.\n * @public\n */\nexport interface CompressionOptions {\n /**\n * The compression format to use. Must be a valid\n * compression format supported by [CompressionStream](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)\n *\n * The default is `gzip`.\n *\n */\n format?: CompressionFormat;\n\n /**\n * Some browsers support `ReadableStream` as a request body. This option\n * enables passing the compression stream as the request body instead of\n * the final compressed body when the browser supports doing so.\n *\n * This comes with several caveats:\n *\n * - the request will be put into `duplex: 'half'` mode. This should be\n * transparent to you, but it is worth noting.\n * - the request mode cannot be `no-cors` as requests with a `ReadableStream`\n * have no content length and thus are a new form of request that triggers\n * cors requirements and a preflight request.\n * - http/1.x is not supported.\n *\n * For additional reading about the restrictions of using `ReadableStream`\n * as a request body, see the [Chromium Documentation](https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests#restrictions)\n *\n * Streaming can be enabled per-request in browsers which support it by\n * setting `request.options.allowStreaming` to `true`.\n *\n * Streaming can be forced even when the browser does not support it by setting\n * `request.options.forceStreaming` to `true`. This is useful if later handlers\n * in the chain can handle the request body as a stream.\n *\n * @default false\n */\n allowStreaming?: boolean;\n\n /**\n * If `true`, the request will be forced into streaming mode even\n * if the browser does not support it. This is useful if later handlers\n * in the chain can handle the request body as a stream.\n *\n * @default false\n */\n forceStreaming?: boolean;\n\n /**\n * The constraints for the request body. This is used to determine\n * whether to compress the request body or not.\n *\n * The defaults are:\n *\n * ```ts\n * {\n * Blob: 1000, // blob.size\n * ArrayBuffer: 1000, // buffer.byteLength\n * TypedArray: 1000, // array.byteLength\n * DataView: 1000, // view.byteLength\n * String: 1000, // string.length\n * }\n * ```\n *\n * The following body types are never compressed unless explicitly\n * configured by the request:\n * - `FormData`\n * - `URLSearchParams`\n * - `ReadableStream`\n *\n * A request.options.compress value of `false` will disable\n * compression for a request body of any type. While a value of\n * `true` will enable compression for the request.\n *\n * An undefined value will use the default, a value of `0` will\n * enable compression for all values, and a value of `-1` will\n * disable compression.\n *\n */\n constraints?: Constraints;\n}\n\nconst DEFAULT_CONSTRAINTS = {\n Blob: 1000,\n ArrayBuffer: 1000,\n TypedArray: 1000,\n DataView: 1000,\n String: 1000,\n};\nconst TypedArray = Object.getPrototypeOf(Uint8Array) as typeof Uint8Array;\n\n/**\n * A request handler that automatically compresses the request body\n * if the request body is a string, array buffer, blob, or form data.\n *\n * This uses the [CompressionStream API](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)\n *\n * The compression format as well as the kinds of data to compress can be\n * configured using the `format` and `constraints` options.\n *\n * ```diff\n * +import { AutoCompress } from '@ember-data-mirror/request-utils/handlers';\n * import Fetch from '@ember-data-mirror/request/fetch';\n * import RequestManager from '@ember-data-mirror/request';\n * import Store from '@ember-data-mirror/store';\n *\n * class AppStore extends Store {\n * requestManager = new RequestManager()\n * .use([\n * + new AutoCompress(),\n * Fetch\n * ]);\n * }\n * ```\n *\n * @summary Request handler that compresses large `POST`, `PUT`, `PATCH`, and `DELETE` bodies with `CompressionStream`,\n * optionally streaming them.\n * @group Handlers\n * @public\n * @since 5.5.0\n */\nexport class AutoCompress implements Handler {\n /**\n * The resolved options this handler was configured with, with all\n * defaults (including `constraints` defaults) applied.\n */\n declare options: Required<CompressionOptions> & {\n /**\n * The resolved size constraints used to decide whether to compress a given request body.\n */\n constraints: Required<Constraints>;\n };\n\n constructor(options: CompressionOptions = {}) {\n const opts = {\n format: options.format ?? 'gzip',\n constraints: Object.assign({}, DEFAULT_CONSTRAINTS, options.constraints),\n allowStreaming: options.allowStreaming ?? false,\n forceStreaming: options.forceStreaming ?? false,\n };\n this.options = opts;\n }\n\n request<T>({ request }: RequestContext, next: NextFn<T>): Promise<T> | Future<T> {\n const { constraints } = this.options;\n const { body } = request;\n\n const shouldCompress =\n !!body &&\n isCompressibleMethod(request.method) &&\n request.options?.compress !== false &&\n // prettier-ignore\n (request.options?.compress ? true\n : typeof body === 'string' || body instanceof String ? canCompress('String', constraints, body.length)\n : body instanceof Blob ? canCompress('Blob', constraints, body.size)\n : body instanceof ArrayBuffer ? canCompress('ArrayBuffer', constraints, body.byteLength)\n : body instanceof DataView ? canCompress('DataView', constraints, body.byteLength)\n : body instanceof TypedArray ? canCompress('TypedArray', constraints, body.byteLength)\n : false);\n\n if (!shouldCompress) return next(request);\n\n // A convenient way to convert all of the supported body types to a readable\n // stream is to use a `Response` object body\n const response = new Response(request.body);\n const stream = response.body?.pipeThrough(new CompressionStream(this.options.format));\n const headers = new Headers(request.headers);\n headers.set('Content-Encoding', encodingForFormat(this.options.format));\n\n //\n // For browsers that support it, `fetch` can receive a `ReadableStream` as\n // the body, so all we need to do is to create a new `ReadableStream` and\n // compress it on the fly\n //\n const forceStreaming = request.options?.forceStreaming ?? this.options.forceStreaming;\n const allowStreaming = request.options?.allowStreaming ?? this.options.allowStreaming;\n if (forceStreaming || (SupportsRequestStreams && allowStreaming)) {\n const req = Object.assign({}, request, {\n body: stream,\n headers,\n });\n if (SupportsRequestStreams) {\n // @ts-expect-error untyped\n req.duplex = 'half';\n }\n\n return next(req);\n\n //\n // For non-chromium browsers, we have to \"pull\" the stream to get the final\n // bytes and supply the final byte array as the new request body.\n //\n }\n\n // we need to pull the stream to get the final bytes\n const resp = new Response(stream);\n return resp.blob().then((blob) => {\n const req = Object.assign({}, request, {\n body: blob,\n headers,\n });\n return next(req);\n }) as Promise<T>;\n }\n}\n\nfunction canCompress(type: keyof Constraints, constraints: Required<Constraints>, size: number): boolean {\n // if we have a value of 0, we can compress anything\n if (constraints[type] === 0) return true;\n if (constraints[type] === -1) return false;\n return size >= constraints[type];\n}\n\nfunction encodingForFormat(format: CompressionFormat): string {\n switch (format) {\n case 'gzip':\n case 'deflate':\n case 'deflate-raw':\n return format;\n default:\n assert(`Unsupported compression format: ${format as unknown as string}`);\n // @ts-expect-error - unreachable code is reachable in production\n return format;\n }\n}\n","import type { Future, Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { RequestContext, StructuredDataDocument } from '@warp-drive-mirror/core/types/request';\n\n/**\n * If CheckFn returns true, the wrapped handler will be used.\n * If CheckFn returns false, the wrapped handler will be skipped.\n */\ntype CheckFn = (context: RequestContext) => boolean;\n\n/**\n *\n * @summary Request handler that wraps another handler and runs it only when a check function returns true for the\n * request.\n * @group Handlers\n * @public\n */\nexport class Gate implements Handler {\n /**\n * The wrapped handler to invoke when {@link Gate.checkFn | checkFn} returns `true`.\n */\n declare handler: Handler;\n /**\n * The predicate used to decide whether {@link Gate.handler | handler} should run for a given request.\n */\n declare checkFn: CheckFn;\n\n constructor(handler: Handler, checkFn: CheckFn) {\n this.handler = handler;\n this.checkFn = checkFn;\n }\n\n request<T = unknown>(context: RequestContext, next: NextFn<T>): Promise<T | StructuredDataDocument<T>> | Future<T> {\n if (this.checkFn(context)) {\n return this.handler.request(context, next);\n }\n return next(context.request);\n }\n}\n","import type { StoreRequestContext } from '@warp-drive-mirror/core';\nimport type { Handler, NextFn } from '@warp-drive-mirror/core/request';\nimport type { StructuredDataDocument } from '@warp-drive-mirror/core/types/request';\n\n/**\n * MetaDocHandler processes requests that are marked as meta requests.\n *\n * It treats the response body as \"entirely meta\" transforming\n *\n * ```ts\n * {\n * some: \"key\",\n * another: \"thing\"\n * }\n * ```\n *\n * into\n *\n * ```ts\n * {\n * \t meta: {\n * some: \"key\",\n * another: \"thing\"\n * }\n * }\n * ```\n *\n * To activate this handler, a request should specify\n *\n * ```ts\n * options.isMetaRequest = true\n * ```\n *\n * For instance\n *\n * ```ts\n * store.request({\n * url: '/example',\n * options: {\n * isMetaRequest: true\n * }\n * });\n * ```\n *\n * Errors are not processed by this handler, so if the request fails and the error response\n * is not in {json:api} format additional processing may be needed.\n *\n * @summary Request handler that wraps the whole response body in `meta` for requests with `options.isMetaRequest` set.\n * @group Handlers\n */\nexport const MetaDocHandler: Handler = {\n request<T>(context: StoreRequestContext, next: NextFn<T>): Promise<T | StructuredDataDocument<T> | Awaited<T>> {\n if (!context.request.options?.isMetaRequest) {\n return next(context.request);\n }\n\n return next(context.request).then((response) => {\n return processResponse<T>(response);\n });\n },\n};\n\nfunction processResponse<T>(response: StructuredDataDocument<T>): T {\n return {\n meta: response.content,\n } as T;\n}\n","import { assert } from '@warp-drive-mirror/core/build-config/macros';\n\nif (typeof FastBoot === 'undefined') {\n globalThis.addEventListener('beforeunload', function () {\n sessionStorage.setItem('tab-closed', 'true');\n });\n}\n\nfunction getTabId() {\n if (typeof sessionStorage === 'undefined') {\n return crypto.randomUUID();\n }\n\n const tabId = sessionStorage.getItem('tab-id');\n if (tabId) {\n const tabClosed = sessionStorage.getItem('tab-closed');\n if (tabClosed === 'true') {\n return tabId;\n }\n\n // fall through to generate a new tab id\n }\n\n const newTabId = crypto.randomUUID();\n sessionStorage.setItem('tab-id', newTabId);\n return newTabId;\n}\n\n/**\n * A unique identifier for the current browser tab\n * useful for observability/tracing and deduping\n * across multiple tabs.\n *\n * @summary Random UUID identifying the current browser tab, kept in `sessionStorage` across reloads, for request\n * tracing.\n * @group Constants\n */\nexport const TAB_ID: string = getTabId();\n/**\n * The epoch seconds at which the tab id was generated\n *\n * @summary Epoch seconds recorded when this module loaded, paired with `TAB_ID` in trace headers.\n * @group Constants\n */\nexport const TAB_ASSIGNED: number = Math.floor(Date.now() / 1000);\n\n/**\n * Adds the `X-Amzn-Trace-Id` header to support observability\n * tooling around request routing.\n *\n * This makes use of the {@link TAB_ID} and {@link TAB_ASSIGNED}\n * to enable tracking the browser tab of origin across multiple requests.\n *\n * Follows the template: `Root=1-${now}-${uuidv4};TabId=1-${epochSeconds}-${tab-uuid}`\n *\n * @summary Sets an `X-Amzn-Trace-Id` header carrying a per-request id and the tab id, so requests can be traced to\n * their browser tab.\n * @group Utility Functions\n */\nexport function addTraceHeader(headers: Headers): Headers {\n const now = Math.floor(Date.now() / 1000);\n headers.set('X-Amzn-Trace-Id', `Root=1-${now}-${crypto.randomUUID()};TabId=1-${TAB_ASSIGNED}-${TAB_ID}`);\n\n return headers;\n}\n\n/**\n * Source: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html\n * As of 2024-12-05 the maximum URL length is 8192 bytes.\n *\n * @summary The 8192-byte URL length limit, taken from AWS CloudFront, that `assertInvalidUrlLength` checks against.\n * @group Constants\n */\nexport const MAX_URL_LENGTH = 8192;\n\n/**\n * This assertion takes a URL and throws an error if the URL is longer than the maximum URL length.\n *\n * See also {@link MAX_URL_LENGTH}\n *\n * @summary Dev-mode assertion that throws when a URL is longer than `MAX_URL_LENGTH`, suggesting a `POST` or `QUERY`\n * request instead.\n * @group Utility Functions\n */\nexport function assertInvalidUrlLength(url: string | undefined): void {\n assert(\n `URL length ${url?.length} exceeds the maximum URL length of ${MAX_URL_LENGTH} bytes.\\n\\nConsider converting this request query a \\`/query\\` endpoint instead of a GET, or upgrade the current endpoint to be able to receive a POST request directly (ideally specifying the header HTTP-Method-Override: QUERY)\\n\\nThe Invalid URL is:\\n\\n${url}`,\n !url || url.length <= MAX_URL_LENGTH\n );\n}\n"],"mappings":"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"}
@@ -4,6 +4,13 @@ import { n as extractCacheOptions, t as copyForwardUrlOptions } from "./builder-
4
4
  import { recordIdentifierFor } from "@warp-drive-mirror/core";
5
5
 
6
6
  //#region src/-private/json-api/-utils.ts
7
+ /**
8
+ * The configuration {@link setBuildURLConfig} accepts: the {@link BuildURLConfig} fields, plus
9
+ * the {json:api} profile and extension URIs to send in the `Accept` header.
10
+ *
11
+ * @summary Configuration for the JSON:API request builders: host, namespace, and the profile and extension URIs for `Accept`.
12
+ * @public
13
+ */
7
14
  const JsonApiAccept = "application/vnd.api+json";
8
15
  const DEFAULT_CONFIG = {
9
16
  host: "",
@@ -407,6 +414,13 @@ function updateRecord(record, options = {}) {
407
414
  //#endregion
408
415
  //#region src/-private/json-api/serialize.ts
409
416
  /**
417
+ * The resource object {@link serializePatch} produces: the resource's `type` and `id` (or `lid`
418
+ * for a new record) with only its changed attributes and relationships.
419
+ *
420
+ * @summary A JSON:API resource object holding only a record's changed attributes and relationships, as produced by `serializePatch`.
421
+ * @public
422
+ */
423
+ /**
410
424
  * :::warning ⚠️ **This util often won't produce the necessary body for a {json:api} request**
411
425
  *
412
426
  * While this may come as a surprise, they are intended to serialize cache state for more