@shopify/hydrogen 2026.10.1 → 2026.10.2

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.
Files changed (57) hide show
  1. package/dist/cli/index.mjs +6 -3
  2. package/dist/client/client.mjs +2 -4
  3. package/dist/client/client.mjs.map +1 -1
  4. package/dist/client/i18n-variables.d.mts +13 -0
  5. package/dist/client/i18n-variables.mjs +84 -0
  6. package/dist/client/i18n-variables.mjs.map +1 -0
  7. package/dist/client/types.d.mts +13 -6
  8. package/dist/core/cart/cart.mjs +110 -23
  9. package/dist/core/cart/cart.mjs.map +1 -1
  10. package/dist/core/cart/queries.d.mts +57 -57
  11. package/dist/core/predictive-search/queries.d.mts +2 -2
  12. package/dist/core/request-context.mjs +1 -1
  13. package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
  14. package/dist/core/request-routing/registered-routes.d.mts +5 -1
  15. package/dist/core/request-routing/registered-routes.mjs +21 -7
  16. package/dist/core/request-routing/registered-routes.mjs.map +1 -1
  17. package/dist/core/request-routing/route-types.d.mts +16 -6
  18. package/dist/core/shopify-scripts/perfkit.mjs +8 -2
  19. package/dist/core/shopify-scripts/perfkit.mjs.map +1 -1
  20. package/dist/core/utils/load-script.mjs +6 -6
  21. package/dist/core/utils/load-script.mjs.map +1 -1
  22. package/dist/customer-account/client.mjs +1 -1
  23. package/dist/customer-account/session.mjs +1 -1
  24. package/dist/development/client/client.mjs +2 -4
  25. package/dist/development/client/client.mjs.map +1 -1
  26. package/dist/development/client/i18n-variables.d.mts +13 -0
  27. package/dist/development/client/i18n-variables.mjs +84 -0
  28. package/dist/development/client/i18n-variables.mjs.map +1 -0
  29. package/dist/development/client/types.d.mts +13 -6
  30. package/dist/development/core/cart/cart.mjs +110 -23
  31. package/dist/development/core/cart/cart.mjs.map +1 -1
  32. package/dist/development/core/request-context.mjs +1 -1
  33. package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
  34. package/dist/development/core/request-routing/registered-routes.d.mts +5 -1
  35. package/dist/development/core/request-routing/registered-routes.mjs +21 -7
  36. package/dist/development/core/request-routing/registered-routes.mjs.map +1 -1
  37. package/dist/development/core/request-routing/route-types.d.mts +16 -6
  38. package/dist/development/core/shopify-scripts/perfkit.mjs +8 -2
  39. package/dist/development/core/shopify-scripts/perfkit.mjs.map +1 -1
  40. package/dist/development/core/utils/load-script.mjs +6 -6
  41. package/dist/development/core/utils/load-script.mjs.map +1 -1
  42. package/dist/development/graphql/type-resolver.d.mts +27 -1
  43. package/dist/development/react/cart.mjs +79 -25
  44. package/dist/development/react/cart.mjs.map +1 -1
  45. package/dist/graphql/type-resolver.d.mts +27 -1
  46. package/dist/react/cart.mjs +79 -25
  47. package/dist/react/cart.mjs.map +1 -1
  48. package/dist/vite/index.d.mts +11 -1
  49. package/dist/vite/index.mjs +21 -1
  50. package/dist/vite/index.mjs.map +1 -0
  51. package/dist/vue/shopify-scripts.d.mts +1 -1
  52. package/package.json +1 -1
  53. package/skills/hydrogen-cart-ui/SKILL.md +2 -2
  54. package/skills/hydrogen-request-handlers/SKILL.md +2 -2
  55. package/skills/hydrogen-request-handlers/references/frameworks.md +2 -7
  56. package/skills/hydrogen-request-handlers/references/nuxt.md +1 -3
  57. package/skills/hydrogen-storefront-client/SKILL.md +4 -2
@@ -1048,16 +1048,17 @@ async function scaffoldTemplate(appRoot, env, runCommand, log) {
1048
1048
  }
1049
1049
  async function ensureHydrogenInstalled(appRoot, env, runCommand, log) {
1050
1050
  const packageJson = readPackageJson(appRoot);
1051
- if (hasHydrogenDependency(packageJson) && getInstalledPackageRoot(appRoot)) return;
1051
+ if (hasHydrogenDependency(packageJson) && getInstalledPackageRoot(appRoot)) return { addedHydrogen: false };
1052
1052
  if (hasHydrogenDependency(packageJson)) {
1053
1053
  const packageManager = detectPackageManager(appRoot, packageJson, env);
1054
1054
  log(`${PACKAGE_NAME} is declared but not installed. Running ${packageManager} install...`);
1055
1055
  await runCommand(packageManager, ["install"], { cwd: appRoot });
1056
- return;
1056
+ return { addedHydrogen: false };
1057
1057
  }
1058
1058
  const packageManager = detectPackageManager(appRoot, packageJson, env);
1059
1059
  log(`Installing ${PACKAGE_NAME} with ${packageManager}...`);
1060
1060
  await installHydrogen(appRoot, packageManager, runCommand);
1061
+ return { addedHydrogen: true };
1061
1062
  }
1062
1063
  function resolveSetupOptions(options) {
1063
1064
  const env = options.env ?? process.env;
@@ -1075,7 +1076,8 @@ function isEmptyDirectory(dir) {
1075
1076
  }
1076
1077
  async function setupHydrogen(options = {}) {
1077
1078
  const { appRoot, env, runCommand, log, prompt } = resolveSetupOptions(options);
1078
- if (hasPackageJson(appRoot)) await ensureHydrogenInstalled(appRoot, env, runCommand, log);
1079
+ let addedHydrogen = false;
1080
+ if (hasPackageJson(appRoot)) ({addedHydrogen} = await ensureHydrogenInstalled(appRoot, env, runCommand, log));
1079
1081
  else if (isEmptyDirectory(appRoot)) {
1080
1082
  let choice;
1081
1083
  if (prompt) choice = await prompt(log);
@@ -1091,6 +1093,7 @@ async function setupHydrogen(options = {}) {
1091
1093
  packageRoot: options.packageRoot,
1092
1094
  log
1093
1095
  });
1096
+ if (addedHydrogen) log("Setup is ready. Next: ask your agent to use the hydrogen-setup skill.");
1094
1097
  }
1095
1098
 
1096
1099
  //#endregion
@@ -4,10 +4,9 @@ import { createFetchWithCache } from "../core/cache/fetch-with-cache.mjs";
4
4
  import { DEFAULT_TIMEOUT_IN_MS, STOREFRONT_API_VERSION } from "../core/constants.mjs";
5
5
  import { HYDROGEN_VERSION_HEADER, REQUEST_GROUP_ID_HEADER, SDK_VARIANT_HEADER, SDK_VARIANT_SOURCE_HEADER, SDK_VERSION_HEADER, SHOPIFY_STOREFRONT_S_HEADER, SHOPIFY_STOREFRONT_Y_HEADER, SHOPIFY_UNIQUE_TOKEN_HEADER, SHOPIFY_VISIT_TOKEN_HEADER, STOREFRONT_ACCESS_TOKEN_HEADER, STOREFRONT_BUYER_IP_HEADER, STOREFRONT_ID_HEADER, STOREFRONT_PRIVATE_TOKEN_HEADER } from "../core/headers.mjs";
6
6
  import { StorefrontApiError, StorefrontTimeoutError } from "./errors.mjs";
7
+ import { getI18nVariableNames } from "./i18n-variables.mjs";
7
8
 
8
9
  //#region src/client/client.ts
9
- const COUNTRY_VAR_RE = /\$country\s*:/;
10
- const LANGUAGE_VAR_RE = /\$language\s*:/;
11
10
  const REQUEST_CACHE_KEY_HEADERS = new Set([
12
11
  "content-type",
13
12
  "user-agent",
@@ -174,8 +173,7 @@ function createStorefrontClient(args) {
174
173
  }
175
174
  function buildVariables(queryText, userVariables, resolvedI18n) {
176
175
  const variables = { ...userVariables };
177
- if (COUNTRY_VAR_RE.test(queryText)) variables.country = resolvedI18n.country;
178
- if (LANGUAGE_VAR_RE.test(queryText)) variables.language = resolvedI18n.language;
176
+ for (const name of getI18nVariableNames(queryText)) variables[name] = resolvedI18n[name];
179
177
  return variables;
180
178
  }
181
179
  function createStorefrontCacheOptions(apiUrl, init, queryText, strategy, shouldCacheResult, backgroundTimeoutInMs) {
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import type { TadaDocumentNode } from \"gql.tada\";\n\nimport { getHydrogenCacheStatus } from \"../core/cache/cache-status\";\nimport { createFetchWithCache } from \"../core/cache/fetch-with-cache\";\nimport type { FetchCacheOptions, FetchCacheResponseContext } from \"../core/cache/fetch-with-cache\";\nimport type { CacheInstance } from \"../core/cache/run-with-cache\";\nimport type { CachingStrategy } from \"../core/cache/strategies\";\nimport { DEFAULT_TIMEOUT_IN_MS, STOREFRONT_API_VERSION } from \"../core/constants\";\nimport {\n HYDROGEN_VERSION_HEADER,\n REQUEST_GROUP_ID_HEADER,\n SDK_VARIANT_HEADER,\n SDK_VARIANT_SOURCE_HEADER,\n SDK_VERSION_HEADER,\n SHOPIFY_STOREFRONT_S_HEADER,\n SHOPIFY_STOREFRONT_Y_HEADER,\n STOREFRONT_ACCESS_TOKEN_HEADER,\n STOREFRONT_BUYER_IP_HEADER,\n STOREFRONT_ID_HEADER,\n STOREFRONT_PRIVATE_TOKEN_HEADER,\n SHOPIFY_UNIQUE_TOKEN_HEADER,\n SHOPIFY_VISIT_TOKEN_HEADER,\n} from \"../core/headers\";\nimport type { I18nConfig, ShopifyRequestContext } from \"../core/request-context\";\nimport { normalizeStoreDomain } from \"../core/url\";\nimport type { AnyStorefrontQueryString } from \"../graphql\";\nimport { StorefrontApiError, StorefrontTimeoutError } from \"./errors\";\nimport type {\n ClientType,\n CreateStorefrontClientArgs,\n GraphQLFormattedError,\n StorefrontClient,\n StorefrontGraphqlResult,\n} from \"./types\";\n\ntype DocLike = TadaDocumentNode<unknown, unknown> | AnyStorefrontQueryString;\ntype FetchInput = Parameters<typeof globalThis.fetch>[0];\ntype FetchInit = Parameters<typeof globalThis.fetch>[1];\ntype ResolvedStorefrontFetch = (\n input: FetchInput,\n init: FetchInit,\n cacheOptions: FetchCacheOptions | undefined,\n) => Promise<Response>;\n\nconst COUNTRY_VAR_RE = /\\$country\\s*:/;\nconst LANGUAGE_VAR_RE = /\\$language\\s*:/;\nconst REQUEST_CACHE_KEY_HEADERS = new Set([\n \"content-type\",\n \"user-agent\",\n SDK_VARIANT_HEADER.toLowerCase(),\n SDK_VARIANT_SOURCE_HEADER.toLowerCase(),\n SDK_VERSION_HEADER.toLowerCase(),\n HYDROGEN_VERSION_HEADER.toLowerCase(),\n STOREFRONT_ACCESS_TOKEN_HEADER.toLowerCase(),\n STOREFRONT_PRIVATE_TOKEN_HEADER.toLowerCase(),\n]);\nconst REQUEST_IDENTITY_HEADERS = new Set([\n \"cookie\",\n REQUEST_GROUP_ID_HEADER.toLowerCase(),\n STOREFRONT_BUYER_IP_HEADER.toLowerCase(),\n SHOPIFY_UNIQUE_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_VISIT_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_Y_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_S_HEADER.toLowerCase(),\n]);\nconst CACHE_ENABLED_GRAPHQL_CLIENTS = new WeakSet<StorefrontClient[\"graphql\"]>();\nconst SHOULD_CACHE_RESULT = Symbol();\n\ntype ShouldCacheResult = (body: object) => boolean;\n\nclass StorefrontCacheConfigError extends Error {}\n\nexport function withStorefrontClientCache<TOptions extends object>(\n client: Pick<StorefrontClient, \"graphql\">,\n options: TOptions,\n strategy: CachingStrategy,\n shouldCacheResult?: ShouldCacheResult,\n): TOptions {\n if (!CACHE_ENABLED_GRAPHQL_CLIENTS.has(client.graphql)) return options;\n\n return { ...options, cache: strategy, [SHOULD_CACHE_RESULT]: shouldCacheResult };\n}\n\n/**\n * Creates a Storefront API client that runs typed queries and mutations against your store.\n *\n * Set `type` based on where your code runs and whether customer context is available:\n *\n * | | `public` | `private` | `private_no_buyer_context` |\n * |------------------|------------------------------|-------------------------------|------------------------------|\n * | Runs in | Browser or mobile | Server-side rendering | Background server code |\n * | Token | Public access token, or none | Private access token | Private access token |\n * | Customer context | Browser request context | You forward trusted `buyerIp` | None |\n * | Best for | Client-side fetches | SSR with customer context | Webhooks, background jobs |\n *\n * Pass a request context from `createShopifyRequestContext`. The client forwards the incoming\n * request's cookies and `Sec-GPC` header to the Storefront API, and fills `$country`\n * and `$language` variables from the context's locale. The client reads the request headers once,\n * when you create the client. Create a client for each incoming request, and create a client at\n * module scope only with a static request context.\n *\n * Keep private clients in server code, where the token can't reach browsers. The function throws an\n * error when you create either private client type in a browser. The function throws a `TypeError`\n * when the request context of a `private` client has no `buyerIp`.\n *\n * @see [Storefront API authentication](https://shopify.dev/docs/api/storefront#authentication)\n * @publicDocs\n */\nexport function createStorefrontClient<\n const Type extends ClientType,\n const RequestContext extends ShopifyRequestContext,\n const CacheConfig extends CacheInstance | undefined = undefined,\n>(\n args: CreateStorefrontClientArgs<RequestContext, Type, CacheConfig>,\n): StorefrontClient<\n // Tuple-wrapped to stay non-distributive so an explicit\n // `CacheInstance | undefined` type argument yields one client type, not a\n // union. Note a *value* of that type still unlocks cache options: optional\n // property inference strips `undefined` before it reaches CacheConfig, so\n // definiteness is undecidable here — the runtime guard rejects cache\n // options when no cache instance was actually configured.\n [CacheConfig] extends [CacheInstance] ? { cache?: CachingStrategy } : {},\n Type,\n RequestContext\n>;\n/**\n * @param args The client type, request context, and store configuration for the new client.\n * @returns The client, with a `graphql()` method that runs queries and mutations.\n */\nexport function createStorefrontClient(args: CreateStorefrontClientArgs): StorefrontClient {\n const { config, requestContext, type: clientType } = args;\n\n if (!config.storeDomain) {\n throw new Error(\"storeDomain is required and must be non-empty\");\n }\n\n const storeUrl = normalizeStoreDomain(config.storeDomain);\n const apiVersion = config.apiVersion ?? STOREFRONT_API_VERSION;\n const apiUrl = `${storeUrl}/api/${apiVersion}/graphql.json`;\n\n const originFetch = config.fetch ?? globalThis.fetch;\n if (typeof originFetch !== \"function\") {\n throw new Error(\n \"No fetch function available. Pass a fetch option or ensure globalThis.fetch exists.\",\n );\n }\n const resolvedFetch = config.cache\n ? createFetchWithCache({\n cache: config.cache,\n waitUntil: config.waitUntil,\n fetch: originFetch as typeof globalThis.fetch,\n })\n : originFetch;\n\n const timeoutInMs = config.defaultTimeoutInMs ?? DEFAULT_TIMEOUT_IN_MS;\n if (timeoutInMs < 0 || Number.isNaN(timeoutInMs)) {\n throw new Error(\"defaultTimeoutInMs must be a non-negative number\");\n }\n\n const staticHeaders: Record<string, string> = {\n \"content-type\": \"application/json\",\n };\n if (config.storefrontId) {\n staticHeaders[STOREFRONT_ID_HEADER] = config.storefrontId;\n }\n\n switch (clientType) {\n case \"private\":\n case \"private_no_buyer_context\":\n if (typeof document !== \"undefined\") {\n throw new Error(\n \"private storefront tokens cannot be used in a browser context. Use a publicStorefrontToken instead.\",\n );\n }\n if (!config.privateStorefrontToken) {\n throw new Error(\"privateStorefrontToken is required\");\n }\n staticHeaders[STOREFRONT_PRIVATE_TOKEN_HEADER] = config.privateStorefrontToken;\n break;\n case \"public\":\n if (config.publicStorefrontToken !== undefined && !config.publicStorefrontToken) {\n throw new Error(\"publicStorefrontToken must be non-empty when provided\");\n }\n if (config.publicStorefrontToken) {\n staticHeaders[STOREFRONT_ACCESS_TOKEN_HEADER] = config.publicStorefrontToken;\n }\n break;\n default:\n clientType satisfies never;\n throw new Error(`Unsupported client type: ${clientType}`);\n }\n\n const i18n = requestContext.i18n;\n const requestHeaders = new Headers(staticHeaders);\n requestContext.applyStorefrontRequestHeaders(requestHeaders);\n\n if (clientType === \"private\") {\n const { buyerIp } = requestContext;\n if (!buyerIp) {\n throw new TypeError(\"requestContext.buyerIp is required for private Storefront API clients\");\n }\n requestHeaders.set(STOREFRONT_BUYER_IP_HEADER, buyerIp);\n }\n\n async function graphql(\n doc: DocLike | string,\n ...rest: [options?: Record<string, unknown>]\n ): Promise<StorefrontGraphqlResult<DocLike>> {\n const opts = (rest[0] ?? {}) as {\n variables?: Record<string, unknown>;\n signal?: AbortSignal;\n cache?: CachingStrategy;\n [SHOULD_CACHE_RESULT]?: ShouldCacheResult;\n };\n\n const queryText = typeof doc === \"string\" ? doc : (doc as unknown as string);\n if (typeof queryText !== \"string\") {\n throw new StorefrontApiError(\n \"Expected gql() to return a string at runtime. Check gql.tada version compatibility.\",\n );\n }\n\n if (opts.cache && !config.cache) {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache options require a cache configured in createStorefrontClient().\",\n );\n }\n\n if (opts.cache?.mode === \"private\") {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache mode 'private' is not supported yet. Use Cache() without private mode, or cache buyer-specific work with createRunWithCache() and an explicit private key.\",\n );\n }\n\n const variables = buildVariables(queryText, opts.variables, i18n);\n const externalSignals = [requestContext.signal, opts.signal].filter(\n (signal): signal is AbortSignal => Boolean(signal),\n );\n\n const earlyAbortSignal = externalSignals.find((signal) => signal.aborted);\n if (earlyAbortSignal) {\n throw (\n earlyAbortSignal.reason ??\n new DOMException(\"signal is aborted without reason\", \"AbortError\")\n );\n }\n\n let requestId: string | undefined;\n let responseHeaders: Headers;\n let responseText: string;\n let status: number;\n let timeoutSignal: AbortSignal | undefined;\n\n if (timeoutInMs > 0) {\n timeoutSignal = AbortSignal.timeout(timeoutInMs);\n externalSignals.push(timeoutSignal);\n }\n\n try {\n const init: PlainRequestInit = {\n method: \"POST\",\n headers: new Headers(requestHeaders),\n body: JSON.stringify({ query: queryText, variables }),\n signal: externalSignals.length > 0 ? AbortSignal.any(externalSignals) : undefined,\n };\n\n const cacheOptions = createStorefrontCacheOptions(\n apiUrl,\n init,\n queryText,\n opts.cache,\n opts[SHOULD_CACHE_RESULT],\n timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT_IN_MS,\n );\n const response = await (resolvedFetch as ResolvedStorefrontFetch)(apiUrl, init, cacheOptions);\n\n responseHeaders = response.headers;\n const hydrogenCacheStatus = getHydrogenCacheStatus(responseHeaders);\n if (hydrogenCacheStatus !== \"hit\") {\n requestContext.captureSubrequestHeaders(responseHeaders);\n }\n\n requestId = responseHeaders.get(\"x-request-id\") ?? undefined;\n status = response.status;\n\n if (!response.ok) {\n const bodyHint = await response.text().catch(() => \"\");\n throw new StorefrontApiError(\n `SFAPI responded with ${status}${bodyHint ? `: ${bodyHint.slice(0, 200)}` : \"\"}`,\n { status, requestId, queryText, variables },\n );\n }\n\n responseText = await response.text();\n } catch (error) {\n if (error instanceof StorefrontApiError) throw error;\n if (error instanceof StorefrontTimeoutError) throw error;\n if (error instanceof StorefrontCacheConfigError) throw error;\n if (\n error instanceof DOMException &&\n error.name === \"TimeoutError\" &&\n timeoutSignal?.aborted\n ) {\n throw new StorefrontTimeoutError(timeoutInMs);\n }\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n throw new StorefrontApiError(\"SFAPI request failed\", { cause: error });\n }\n\n let json: unknown;\n try {\n json = JSON.parse(responseText);\n } catch (cause) {\n throw new StorefrontApiError(\n `Failed to parse SFAPI response as JSON (${apiUrl}, status ${status})` +\n (responseText ? `: ${responseText.slice(0, 200)}` : \"\"),\n { status, requestId, cause, queryText, variables },\n );\n }\n\n if (typeof json !== \"object\" || json === null || Array.isArray(json)) {\n throw new StorefrontApiError(\n `SFAPI returned unexpected JSON type: ${Array.isArray(json) ? \"array\" : typeof json}`,\n { status, requestId, queryText, variables },\n );\n }\n\n const body = json as { data?: unknown; errors?: GraphQLFormattedError[] };\n // Branch to populate the two arms of the StorefrontGraphqlResult union: callers\n // narrowing on `errors` get a non-null `data` only when no errors are present.\n if (body.errors) {\n return {\n data: body.data ?? null,\n errors: body.errors,\n headers: responseHeaders,\n };\n }\n return {\n data: body.data,\n headers: responseHeaders,\n };\n }\n\n const client: StorefrontClient = {\n type: clientType,\n i18n,\n graphql: graphql as StorefrontClient[\"graphql\"],\n apiUrl,\n storeUrl,\n storefrontId: config.storefrontId,\n requestContext,\n };\n\n if (config.cache) CACHE_ENABLED_GRAPHQL_CLIENTS.add(client.graphql);\n\n return client;\n}\n\nfunction buildVariables(\n queryText: string,\n userVariables: Record<string, unknown> | undefined,\n resolvedI18n: I18nConfig,\n): Record<string, unknown> {\n const variables: Record<string, unknown> = { ...userVariables };\n\n if (COUNTRY_VAR_RE.test(queryText)) {\n variables.country = resolvedI18n.country;\n }\n if (LANGUAGE_VAR_RE.test(queryText)) {\n variables.language = resolvedI18n.language;\n }\n\n return variables;\n}\n\ntype PlainRequestInit = RequestInit & { headers: Headers; body: string; method: string };\n\nfunction createStorefrontCacheOptions(\n apiUrl: string,\n init: PlainRequestInit,\n queryText: string,\n strategy: CachingStrategy | undefined,\n shouldCacheResult: ShouldCacheResult | undefined,\n backgroundTimeoutInMs: number,\n): FetchCacheOptions | undefined {\n if (!strategy) return undefined;\n\n // Runtime only needs a safety belt for untyped callers. False positives are\n // acceptable because they bypass cache rather than risking mutation caching.\n if (/\\bmutation\\b/.test(queryText)) return undefined;\n\n return {\n key: createStorefrontCacheKey(apiUrl, init),\n strategy,\n shouldCacheResponse: async ({ json }: FetchCacheResponseContext) => {\n if (!json) return false;\n\n const body = await json().catch(() => undefined);\n if (typeof body !== \"object\" || body == null || Array.isArray(body) || \"errors\" in body) {\n return false;\n }\n\n return shouldCacheResult?.(body) ?? true;\n },\n // A refresh outlives the request that triggered it, so request and caller\n // aborts must not cancel it. It still needs a deadline, even when the caller\n // disabled the client timeout to own the foreground one.\n backgroundSignal: () => AbortSignal.timeout(backgroundTimeoutInMs),\n };\n}\n\nfunction createStorefrontCacheKey(apiUrl: string, init: PlainRequestInit) {\n return [\n \"storefront-api\",\n apiUrl,\n init.method,\n init.body,\n [...init.headers]\n .filter(([name]) => {\n const normalized = name.toLowerCase();\n return (\n REQUEST_CACHE_KEY_HEADERS.has(normalized) && !REQUEST_IDENTITY_HEADERS.has(normalized)\n );\n })\n .toSorted(([left], [right]) => left.localeCompare(right))\n .map(([name, value]) => `${name}:${value}`)\n .join(\"\\n\"),\n ] as const;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import type { TadaDocumentNode } from \"gql.tada\";\n\nimport { getHydrogenCacheStatus } from \"../core/cache/cache-status\";\nimport { createFetchWithCache } from \"../core/cache/fetch-with-cache\";\nimport type { FetchCacheOptions, FetchCacheResponseContext } from \"../core/cache/fetch-with-cache\";\nimport type { CacheInstance } from \"../core/cache/run-with-cache\";\nimport type { CachingStrategy } from \"../core/cache/strategies\";\nimport { DEFAULT_TIMEOUT_IN_MS, STOREFRONT_API_VERSION } from \"../core/constants\";\nimport {\n HYDROGEN_VERSION_HEADER,\n REQUEST_GROUP_ID_HEADER,\n SDK_VARIANT_HEADER,\n SDK_VARIANT_SOURCE_HEADER,\n SDK_VERSION_HEADER,\n SHOPIFY_STOREFRONT_S_HEADER,\n SHOPIFY_STOREFRONT_Y_HEADER,\n STOREFRONT_ACCESS_TOKEN_HEADER,\n STOREFRONT_BUYER_IP_HEADER,\n STOREFRONT_ID_HEADER,\n STOREFRONT_PRIVATE_TOKEN_HEADER,\n SHOPIFY_UNIQUE_TOKEN_HEADER,\n SHOPIFY_VISIT_TOKEN_HEADER,\n} from \"../core/headers\";\nimport type { I18nConfig, ShopifyRequestContext } from \"../core/request-context\";\nimport { normalizeStoreDomain } from \"../core/url\";\nimport type { AnyStorefrontQueryString } from \"../graphql\";\nimport { StorefrontApiError, StorefrontTimeoutError } from \"./errors\";\nimport { getI18nVariableNames } from \"./i18n-variables\";\nimport type {\n ClientType,\n CreateStorefrontClientArgs,\n GraphQLFormattedError,\n StorefrontClient,\n StorefrontGraphqlResult,\n} from \"./types\";\n\ntype DocLike = TadaDocumentNode<unknown, unknown> | AnyStorefrontQueryString;\ntype FetchInput = Parameters<typeof globalThis.fetch>[0];\ntype FetchInit = Parameters<typeof globalThis.fetch>[1];\ntype ResolvedStorefrontFetch = (\n input: FetchInput,\n init: FetchInit,\n cacheOptions: FetchCacheOptions | undefined,\n) => Promise<Response>;\n\nconst REQUEST_CACHE_KEY_HEADERS = new Set([\n \"content-type\",\n \"user-agent\",\n SDK_VARIANT_HEADER.toLowerCase(),\n SDK_VARIANT_SOURCE_HEADER.toLowerCase(),\n SDK_VERSION_HEADER.toLowerCase(),\n HYDROGEN_VERSION_HEADER.toLowerCase(),\n STOREFRONT_ACCESS_TOKEN_HEADER.toLowerCase(),\n STOREFRONT_PRIVATE_TOKEN_HEADER.toLowerCase(),\n]);\nconst REQUEST_IDENTITY_HEADERS = new Set([\n \"cookie\",\n REQUEST_GROUP_ID_HEADER.toLowerCase(),\n STOREFRONT_BUYER_IP_HEADER.toLowerCase(),\n SHOPIFY_UNIQUE_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_VISIT_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_Y_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_S_HEADER.toLowerCase(),\n]);\nconst CACHE_ENABLED_GRAPHQL_CLIENTS = new WeakSet<StorefrontClient[\"graphql\"]>();\nconst SHOULD_CACHE_RESULT = Symbol();\n\ntype ShouldCacheResult = (body: object) => boolean;\n\nclass StorefrontCacheConfigError extends Error {}\n\nexport function withStorefrontClientCache<TOptions extends object>(\n client: Pick<StorefrontClient, \"graphql\">,\n options: TOptions,\n strategy: CachingStrategy,\n shouldCacheResult?: ShouldCacheResult,\n): TOptions {\n if (!CACHE_ENABLED_GRAPHQL_CLIENTS.has(client.graphql)) return options;\n\n return { ...options, cache: strategy, [SHOULD_CACHE_RESULT]: shouldCacheResult };\n}\n\n/**\n * Creates a Storefront API client that runs typed queries and mutations against your store.\n *\n * Set `type` based on where your code runs and whether customer context is available:\n *\n * | | `public` | `private` | `private_no_buyer_context` |\n * |------------------|------------------------------|-------------------------------|------------------------------|\n * | Runs in | Browser or mobile | Server-side rendering | Background server code |\n * | Token | Public access token, or none | Private access token | Private access token |\n * | Customer context | Browser request context | You forward trusted `buyerIp` | None |\n * | Best for | Client-side fetches | SSR with customer context | Webhooks, background jobs |\n *\n * Pass a request context from `createShopifyRequestContext`. The client forwards the incoming\n * request's cookies and `Sec-GPC` header to the Storefront API, and fills `$country`\n * and `$language` variables from the context's locale. The client reads the request headers once,\n * when you create the client. Create a client for each incoming request, and create a client at\n * module scope only with a static request context.\n *\n * Keep private clients in server code, where the token can't reach browsers. The function throws an\n * error when you create either private client type in a browser. The function throws a `TypeError`\n * when the request context of a `private` client has no `buyerIp`.\n *\n * @see [Storefront API authentication](https://shopify.dev/docs/api/storefront#authentication)\n * @publicDocs\n */\nexport function createStorefrontClient<\n const Type extends ClientType,\n const RequestContext extends ShopifyRequestContext,\n const CacheConfig extends CacheInstance | undefined = undefined,\n>(\n args: CreateStorefrontClientArgs<RequestContext, Type, CacheConfig>,\n): StorefrontClient<\n // Tuple-wrapped to stay non-distributive so an explicit\n // `CacheInstance | undefined` type argument yields one client type, not a\n // union. Note a *value* of that type still unlocks cache options: optional\n // property inference strips `undefined` before it reaches CacheConfig, so\n // definiteness is undecidable here — the runtime guard rejects cache\n // options when no cache instance was actually configured.\n [CacheConfig] extends [CacheInstance] ? { cache?: CachingStrategy } : {},\n Type,\n RequestContext\n>;\n/**\n * @param args The client type, request context, and store configuration for the new client.\n * @returns The client, with a `graphql()` method that runs queries and mutations.\n */\nexport function createStorefrontClient(args: CreateStorefrontClientArgs): StorefrontClient {\n const { config, requestContext, type: clientType } = args;\n\n if (!config.storeDomain) {\n throw new Error(\"storeDomain is required and must be non-empty\");\n }\n\n const storeUrl = normalizeStoreDomain(config.storeDomain);\n const apiVersion = config.apiVersion ?? STOREFRONT_API_VERSION;\n const apiUrl = `${storeUrl}/api/${apiVersion}/graphql.json`;\n\n const originFetch = config.fetch ?? globalThis.fetch;\n if (typeof originFetch !== \"function\") {\n throw new Error(\n \"No fetch function available. Pass a fetch option or ensure globalThis.fetch exists.\",\n );\n }\n const resolvedFetch = config.cache\n ? createFetchWithCache({\n cache: config.cache,\n waitUntil: config.waitUntil,\n fetch: originFetch as typeof globalThis.fetch,\n })\n : originFetch;\n\n const timeoutInMs = config.defaultTimeoutInMs ?? DEFAULT_TIMEOUT_IN_MS;\n if (timeoutInMs < 0 || Number.isNaN(timeoutInMs)) {\n throw new Error(\"defaultTimeoutInMs must be a non-negative number\");\n }\n\n const staticHeaders: Record<string, string> = {\n \"content-type\": \"application/json\",\n };\n if (config.storefrontId) {\n staticHeaders[STOREFRONT_ID_HEADER] = config.storefrontId;\n }\n\n switch (clientType) {\n case \"private\":\n case \"private_no_buyer_context\":\n if (typeof document !== \"undefined\") {\n throw new Error(\n \"private storefront tokens cannot be used in a browser context. Use a publicStorefrontToken instead.\",\n );\n }\n if (!config.privateStorefrontToken) {\n throw new Error(\"privateStorefrontToken is required\");\n }\n staticHeaders[STOREFRONT_PRIVATE_TOKEN_HEADER] = config.privateStorefrontToken;\n break;\n case \"public\":\n if (config.publicStorefrontToken !== undefined && !config.publicStorefrontToken) {\n throw new Error(\"publicStorefrontToken must be non-empty when provided\");\n }\n if (config.publicStorefrontToken) {\n staticHeaders[STOREFRONT_ACCESS_TOKEN_HEADER] = config.publicStorefrontToken;\n }\n break;\n default:\n clientType satisfies never;\n throw new Error(`Unsupported client type: ${clientType}`);\n }\n\n const i18n = requestContext.i18n;\n const requestHeaders = new Headers(staticHeaders);\n requestContext.applyStorefrontRequestHeaders(requestHeaders);\n\n if (clientType === \"private\") {\n const { buyerIp } = requestContext;\n if (!buyerIp) {\n throw new TypeError(\"requestContext.buyerIp is required for private Storefront API clients\");\n }\n requestHeaders.set(STOREFRONT_BUYER_IP_HEADER, buyerIp);\n }\n\n async function graphql(\n doc: DocLike | string,\n ...rest: [options?: Record<string, unknown>]\n ): Promise<StorefrontGraphqlResult<DocLike>> {\n const opts = (rest[0] ?? {}) as {\n variables?: Record<string, unknown>;\n signal?: AbortSignal;\n cache?: CachingStrategy;\n [SHOULD_CACHE_RESULT]?: ShouldCacheResult;\n };\n\n const queryText = typeof doc === \"string\" ? doc : (doc as unknown as string);\n if (typeof queryText !== \"string\") {\n throw new StorefrontApiError(\n \"Expected gql() to return a string at runtime. Check gql.tada version compatibility.\",\n );\n }\n\n if (opts.cache && !config.cache) {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache options require a cache configured in createStorefrontClient().\",\n );\n }\n\n if (opts.cache?.mode === \"private\") {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache mode 'private' is not supported yet. Use Cache() without private mode, or cache buyer-specific work with createRunWithCache() and an explicit private key.\",\n );\n }\n\n const variables = buildVariables(queryText, opts.variables, i18n);\n const externalSignals = [requestContext.signal, opts.signal].filter(\n (signal): signal is AbortSignal => Boolean(signal),\n );\n\n const earlyAbortSignal = externalSignals.find((signal) => signal.aborted);\n if (earlyAbortSignal) {\n throw (\n earlyAbortSignal.reason ??\n new DOMException(\"signal is aborted without reason\", \"AbortError\")\n );\n }\n\n let requestId: string | undefined;\n let responseHeaders: Headers;\n let responseText: string;\n let status: number;\n let timeoutSignal: AbortSignal | undefined;\n\n if (timeoutInMs > 0) {\n timeoutSignal = AbortSignal.timeout(timeoutInMs);\n externalSignals.push(timeoutSignal);\n }\n\n try {\n const init: PlainRequestInit = {\n method: \"POST\",\n headers: new Headers(requestHeaders),\n body: JSON.stringify({ query: queryText, variables }),\n signal: externalSignals.length > 0 ? AbortSignal.any(externalSignals) : undefined,\n };\n\n const cacheOptions = createStorefrontCacheOptions(\n apiUrl,\n init,\n queryText,\n opts.cache,\n opts[SHOULD_CACHE_RESULT],\n timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT_IN_MS,\n );\n const response = await (resolvedFetch as ResolvedStorefrontFetch)(apiUrl, init, cacheOptions);\n\n responseHeaders = response.headers;\n const hydrogenCacheStatus = getHydrogenCacheStatus(responseHeaders);\n if (hydrogenCacheStatus !== \"hit\") {\n requestContext.captureSubrequestHeaders(responseHeaders);\n }\n\n requestId = responseHeaders.get(\"x-request-id\") ?? undefined;\n status = response.status;\n\n if (!response.ok) {\n const bodyHint = await response.text().catch(() => \"\");\n throw new StorefrontApiError(\n `SFAPI responded with ${status}${bodyHint ? `: ${bodyHint.slice(0, 200)}` : \"\"}`,\n { status, requestId, queryText, variables },\n );\n }\n\n responseText = await response.text();\n } catch (error) {\n if (error instanceof StorefrontApiError) throw error;\n if (error instanceof StorefrontTimeoutError) throw error;\n if (error instanceof StorefrontCacheConfigError) throw error;\n if (\n error instanceof DOMException &&\n error.name === \"TimeoutError\" &&\n timeoutSignal?.aborted\n ) {\n throw new StorefrontTimeoutError(timeoutInMs);\n }\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n throw new StorefrontApiError(\"SFAPI request failed\", { cause: error });\n }\n\n let json: unknown;\n try {\n json = JSON.parse(responseText);\n } catch (cause) {\n throw new StorefrontApiError(\n `Failed to parse SFAPI response as JSON (${apiUrl}, status ${status})` +\n (responseText ? `: ${responseText.slice(0, 200)}` : \"\"),\n { status, requestId, cause, queryText, variables },\n );\n }\n\n if (typeof json !== \"object\" || json === null || Array.isArray(json)) {\n throw new StorefrontApiError(\n `SFAPI returned unexpected JSON type: ${Array.isArray(json) ? \"array\" : typeof json}`,\n { status, requestId, queryText, variables },\n );\n }\n\n const body = json as { data?: unknown; errors?: GraphQLFormattedError[] };\n // Branch to populate the two arms of the StorefrontGraphqlResult union: callers\n // narrowing on `errors` get a non-null `data` only when no errors are present.\n if (body.errors) {\n return {\n data: body.data ?? null,\n errors: body.errors,\n headers: responseHeaders,\n };\n }\n return {\n data: body.data,\n headers: responseHeaders,\n };\n }\n\n const client: StorefrontClient = {\n type: clientType,\n i18n,\n graphql: graphql as StorefrontClient[\"graphql\"],\n apiUrl,\n storeUrl,\n storefrontId: config.storefrontId,\n requestContext,\n };\n\n if (config.cache) CACHE_ENABLED_GRAPHQL_CLIENTS.add(client.graphql);\n\n return client;\n}\n\nfunction buildVariables(\n queryText: string,\n userVariables: Record<string, unknown> | undefined,\n resolvedI18n: I18nConfig,\n): Record<string, unknown> {\n const variables: Record<string, unknown> = { ...userVariables };\n\n for (const name of getI18nVariableNames(queryText)) {\n variables[name] = resolvedI18n[name];\n }\n\n return variables;\n}\n\ntype PlainRequestInit = RequestInit & { headers: Headers; body: string; method: string };\n\nfunction createStorefrontCacheOptions(\n apiUrl: string,\n init: PlainRequestInit,\n queryText: string,\n strategy: CachingStrategy | undefined,\n shouldCacheResult: ShouldCacheResult | undefined,\n backgroundTimeoutInMs: number,\n): FetchCacheOptions | undefined {\n if (!strategy) return undefined;\n\n // Runtime only needs a safety belt for untyped callers. False positives are\n // acceptable because they bypass cache rather than risking mutation caching.\n if (/\\bmutation\\b/.test(queryText)) return undefined;\n\n return {\n key: createStorefrontCacheKey(apiUrl, init),\n strategy,\n shouldCacheResponse: async ({ json }: FetchCacheResponseContext) => {\n if (!json) return false;\n\n const body = await json().catch(() => undefined);\n if (typeof body !== \"object\" || body == null || Array.isArray(body) || \"errors\" in body) {\n return false;\n }\n\n return shouldCacheResult?.(body) ?? true;\n },\n // A refresh outlives the request that triggered it, so request and caller\n // aborts must not cancel it. It still needs a deadline, even when the caller\n // disabled the client timeout to own the foreground one.\n backgroundSignal: () => AbortSignal.timeout(backgroundTimeoutInMs),\n };\n}\n\nfunction createStorefrontCacheKey(apiUrl: string, init: PlainRequestInit) {\n return [\n \"storefront-api\",\n apiUrl,\n init.method,\n init.body,\n [...init.headers]\n .filter(([name]) => {\n const normalized = name.toLowerCase();\n return (\n REQUEST_CACHE_KEY_HEADERS.has(normalized) && !REQUEST_IDENTITY_HEADERS.has(normalized)\n );\n })\n .toSorted(([left], [right]) => left.localeCompare(right))\n .map(([name, value]) => `${name}:${value}`)\n .join(\"\\n\"),\n ] as const;\n}\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ //#region src/client/i18n-variables.d.ts
2
+ /**
3
+ * Variables the client fills from `requestContext.i18n`, with the named type
4
+ * each must be declared as. Same-named variables of any other type belong to
5
+ * the caller.
6
+ */
7
+ declare const I18N_VARIABLE_TYPES: {
8
+ readonly country: "CountryCode";
9
+ readonly language: "LanguageCode";
10
+ };
11
+ //#endregion
12
+ export { I18N_VARIABLE_TYPES };
13
+ //# sourceMappingURL=i18n-variables.d.mts.map
@@ -0,0 +1,84 @@
1
+ //#region src/client/i18n-variables.ts
2
+ /**
3
+ * Variables the client fills from `requestContext.i18n`, with the named type
4
+ * each must be declared as. Same-named variables of any other type belong to
5
+ * the caller.
6
+ */
7
+ const I18N_VARIABLE_TYPES = {
8
+ country: "CountryCode",
9
+ language: "LanguageCode"
10
+ };
11
+ const TOKEN_RE = /"""(?:\\"""|[^])*?"""|"(?:\\.|[^"\\\n\r])*"|#[^\n\r]*|[_A-Za-z][_0-9A-Za-z]*|[^\s,]/g;
12
+ const NAME_RE = /^[_A-Za-z]/;
13
+ const OPERATION_TYPES = new Set([
14
+ "query",
15
+ "mutation",
16
+ "subscription"
17
+ ]);
18
+ const OPENING = new Set([
19
+ "(",
20
+ "[",
21
+ "{"
22
+ ]);
23
+ const CLOSING = new Set([
24
+ ")",
25
+ "]",
26
+ "}"
27
+ ]);
28
+ /**
29
+ * Returns the i18n variables that the document's first operation declares as
30
+ * `$country: CountryCode` or `$language: LanguageCode`, nullable or non-null.
31
+ * This is the same rule the `graphql()` variables type applies.
32
+ */
33
+ function getI18nVariableNames(source) {
34
+ const names = [];
35
+ for (const [name, colon, ...rest] of readFirstOperationVariables(tokenize(source))) {
36
+ if (colon !== ":" || !isI18nVariableName(name)) continue;
37
+ const typeEnd = rest.findIndex((token) => token === "=" || token === "@");
38
+ const type = rest.slice(0, typeEnd === -1 ? void 0 : typeEnd).join("");
39
+ const expected = I18N_VARIABLE_TYPES[name];
40
+ if (type === expected || type === `${expected}!`) names.push(name);
41
+ }
42
+ return names;
43
+ }
44
+ function isI18nVariableName(name) {
45
+ return name !== void 0 && Object.hasOwn(I18N_VARIABLE_TYPES, name);
46
+ }
47
+ function* tokenize(source) {
48
+ for (const [token] of source.matchAll(TOKEN_RE)) if (token[0] !== "#") yield token;
49
+ }
50
+ function readFirstOperationVariables(tokens) {
51
+ let depth = 0;
52
+ let inFragment = false;
53
+ for (const token of tokens) {
54
+ if (depth === 0 && !inFragment) {
55
+ if (OPERATION_TYPES.has(token)) return readOperationVariables(tokens);
56
+ if (token === "{") return [];
57
+ inFragment = token === "fragment";
58
+ }
59
+ if (OPENING.has(token)) depth++;
60
+ else if (CLOSING.has(token) && --depth === 0 && token === "}") inFragment = false;
61
+ }
62
+ return [];
63
+ }
64
+ function readOperationVariables(tokens) {
65
+ let next = tokens.next().value;
66
+ if (next && NAME_RE.test(next)) next = tokens.next().value;
67
+ return next === "(" ? readVariableDefinitions(tokens) : [];
68
+ }
69
+ function readVariableDefinitions(tokens) {
70
+ const definitions = [];
71
+ let depth = 0;
72
+ for (const token of tokens) {
73
+ if (depth === 0 && token === ")") break;
74
+ if (depth === 0 && token === "$") definitions.push([]);
75
+ else definitions.at(-1)?.push(token);
76
+ if (OPENING.has(token)) depth++;
77
+ else if (CLOSING.has(token)) depth--;
78
+ }
79
+ return definitions;
80
+ }
81
+
82
+ //#endregion
83
+ export { getI18nVariableNames };
84
+ //# sourceMappingURL=i18n-variables.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n-variables.mjs","names":[],"sources":["../../src/client/i18n-variables.ts"],"sourcesContent":["/**\n * Variables the client fills from `requestContext.i18n`, with the named type\n * each must be declared as. Same-named variables of any other type belong to\n * the caller.\n */\nexport const I18N_VARIABLE_TYPES = {\n country: \"CountryCode\",\n language: \"LanguageCode\",\n} as const;\n\nexport type I18nVariableName = keyof typeof I18N_VARIABLE_TYPES;\n\n// Significant GraphQL tokens plus comments, which are dropped. Matching whole\n// strings keeps their contents from being read as declarations.\nconst TOKEN_RE =\n /\"\"\"(?:\\\\\"\"\"|[^])*?\"\"\"|\"(?:\\\\.|[^\"\\\\\\n\\r])*\"|#[^\\n\\r]*|[_A-Za-z][_0-9A-Za-z]*|[^\\s,]/g;\nconst NAME_RE = /^[_A-Za-z]/;\nconst OPERATION_TYPES = new Set([\"query\", \"mutation\", \"subscription\"]);\nconst OPENING = new Set([\"(\", \"[\", \"{\"]);\nconst CLOSING = new Set([\")\", \"]\", \"}\"]);\n\n/**\n * Returns the i18n variables that the document's first operation declares as\n * `$country: CountryCode` or `$language: LanguageCode`, nullable or non-null.\n * This is the same rule the `graphql()` variables type applies.\n */\nexport function getI18nVariableNames(source: string): I18nVariableName[] {\n const names: I18nVariableName[] = [];\n for (const [name, colon, ...rest] of readFirstOperationVariables(tokenize(source))) {\n if (colon !== \":\" || !isI18nVariableName(name)) continue;\n const typeEnd = rest.findIndex((token) => token === \"=\" || token === \"@\");\n const type = rest.slice(0, typeEnd === -1 ? undefined : typeEnd).join(\"\");\n const expected = I18N_VARIABLE_TYPES[name];\n if (type === expected || type === `${expected}!`) names.push(name);\n }\n return names;\n}\n\nfunction isI18nVariableName(name: string | undefined): name is I18nVariableName {\n return name !== undefined && Object.hasOwn(I18N_VARIABLE_TYPES, name);\n}\n\nfunction* tokenize(source: string): Generator<string> {\n for (const [token] of source.matchAll(TOKEN_RE)) {\n if (token[0] !== \"#\") yield token;\n }\n}\n\nfunction readFirstOperationVariables(tokens: Generator<string>): string[][] {\n let depth = 0;\n let inFragment = false;\n for (const token of tokens) {\n if (depth === 0 && !inFragment) {\n if (OPERATION_TYPES.has(token)) return readOperationVariables(tokens);\n if (token === \"{\") return [];\n inFragment = token === \"fragment\";\n }\n if (OPENING.has(token)) depth++;\n else if (CLOSING.has(token) && --depth === 0 && token === \"}\") inFragment = false;\n }\n return [];\n}\n\nfunction readOperationVariables(tokens: Generator<string>): string[][] {\n let next = tokens.next().value;\n if (next && NAME_RE.test(next)) next = tokens.next().value;\n return next === \"(\" ? readVariableDefinitions(tokens) : [];\n}\n\n// Default values and directive arguments can nest `$` and `)`, so only depth 0 delimits definitions.\nfunction readVariableDefinitions(tokens: Generator<string>): string[][] {\n const definitions: string[][] = [];\n let depth = 0;\n for (const token of tokens) {\n if (depth === 0 && token === \")\") break;\n if (depth === 0 && token === \"$\") definitions.push([]);\n else definitions.at(-1)?.push(token);\n if (OPENING.has(token)) depth++;\n else if (CLOSING.has(token)) depth--;\n }\n return definitions;\n}\n"],"mappings":";;;;;;AAKA,MAAa,sBAAsB;CACjC,SAAS;CACT,UAAU;CACX;AAMD,MAAM,WACJ;AACF,MAAM,UAAU;AAChB,MAAM,kBAAkB,IAAI,IAAI;CAAC;CAAS;CAAY;CAAe,CAAC;AACtE,MAAM,UAAU,IAAI,IAAI;CAAC;CAAK;CAAK;CAAI,CAAC;AACxC,MAAM,UAAU,IAAI,IAAI;CAAC;CAAK;CAAK;CAAI,CAAC;;;;;;AAOxC,SAAgB,qBAAqB,QAAoC;CACvE,MAAM,QAA4B,EAAE;AACpC,MAAK,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,4BAA4B,SAAS,OAAO,CAAC,EAAE;AAClF,MAAI,UAAU,OAAO,CAAC,mBAAmB,KAAK,CAAE;EAChD,MAAM,UAAU,KAAK,WAAW,UAAU,UAAU,OAAO,UAAU,IAAI;EACzE,MAAM,OAAO,KAAK,MAAM,GAAG,YAAY,KAAK,SAAY,QAAQ,CAAC,KAAK,GAAG;EACzE,MAAM,WAAW,oBAAoB;AACrC,MAAI,SAAS,YAAY,SAAS,GAAG,SAAS,GAAI,OAAM,KAAK,KAAK;;AAEpE,QAAO;;AAGT,SAAS,mBAAmB,MAAoD;AAC9E,QAAO,SAAS,UAAa,OAAO,OAAO,qBAAqB,KAAK;;AAGvE,UAAU,SAAS,QAAmC;AACpD,MAAK,MAAM,CAAC,UAAU,OAAO,SAAS,SAAS,CAC7C,KAAI,MAAM,OAAO,IAAK,OAAM;;AAIhC,SAAS,4BAA4B,QAAuC;CAC1E,IAAI,QAAQ;CACZ,IAAI,aAAa;AACjB,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,UAAU,KAAK,CAAC,YAAY;AAC9B,OAAI,gBAAgB,IAAI,MAAM,CAAE,QAAO,uBAAuB,OAAO;AACrE,OAAI,UAAU,IAAK,QAAO,EAAE;AAC5B,gBAAa,UAAU;;AAEzB,MAAI,QAAQ,IAAI,MAAM,CAAE;WACf,QAAQ,IAAI,MAAM,IAAI,EAAE,UAAU,KAAK,UAAU,IAAK,cAAa;;AAE9E,QAAO,EAAE;;AAGX,SAAS,uBAAuB,QAAuC;CACrE,IAAI,OAAO,OAAO,MAAM,CAAC;AACzB,KAAI,QAAQ,QAAQ,KAAK,KAAK,CAAE,QAAO,OAAO,MAAM,CAAC;AACrD,QAAO,SAAS,MAAM,wBAAwB,OAAO,GAAG,EAAE;;AAI5D,SAAS,wBAAwB,QAAuC;CACtE,MAAM,cAA0B,EAAE;CAClC,IAAI,QAAQ;AACZ,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,UAAU,KAAK,UAAU,IAAK;AAClC,MAAI,UAAU,KAAK,UAAU,IAAK,aAAY,KAAK,EAAE,CAAC;MACjD,aAAY,GAAG,GAAG,EAAE,KAAK,MAAM;AACpC,MAAI,QAAQ,IAAI,MAAM,CAAE;WACf,QAAQ,IAAI,MAAM,CAAE;;AAE/B,QAAO"}
@@ -1,7 +1,9 @@
1
1
  import { CacheInstance, WaitUntil } from "../core/cache/run-with-cache.mjs";
2
2
  import { I18nConfig, ShopifyRequestContext, ShopifyRequestContextWithBuyerIp } from "../core/request-context.mjs";
3
- import { InferOperationKind, InferResult, InferVariables } from "../graphql/type-resolver.mjs";
3
+ import { introspection } from "../graphql/generated/graphql-env.mjs";
4
+ import { InferOperationKind, InferResult, InferVariableNamesDeclaredAs, InferVariables } from "../graphql/type-resolver.mjs";
4
5
  import { AnyStorefrontQueryString, SourceOf, StorefrontQueryString } from "../graphql/graphql.mjs";
6
+ import { I18N_VARIABLE_TYPES } from "./i18n-variables.mjs";
5
7
  import { ResultOf, TadaDocumentNode, VariablesOf } from "gql.tada";
6
8
 
7
9
  //#region src/client/types.d.ts
@@ -140,9 +142,13 @@ type CreateStorefrontClientArgs<RequestContext extends ShopifyRequestContext = S
140
142
  cache?: CacheConfig;
141
143
  };
142
144
  };
143
- type AutoAddedVariableNames = "country" | "language";
144
- /** The variables that you pass for a document, without `country` and `language`. The client fills both from the request context. */
145
- type UserVariables<Doc> = Omit<VariablesOfDoc<Doc>, AutoAddedVariableNames>;
145
+ type I18nVariableTypes = typeof I18N_VARIABLE_TYPES;
146
+ type I18nEnum<K extends keyof I18nVariableTypes> = introspection["types"][I18nVariableTypes[K]]["enumValues"];
147
+ type IsExactly<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
148
+ type I18nVariableNamesByValue<Variables> = { [K in keyof Variables & keyof I18nVariableTypes]-?: IsExactly<NonNullable<Variables[K]>, I18nEnum<K>> extends true ? K : never }[keyof Variables & keyof I18nVariableTypes];
149
+ type I18nVariableNamesOfDoc<Doc> = [SourceText<Doc>] extends [never] ? I18nVariableNamesByValue<VariablesOfDoc<Doc>> : string extends SourceText<Doc> ? I18nVariableNamesByValue<VariablesOfDoc<Doc>> : InferVariableNamesDeclaredAs<SourceText<Doc>, I18nVariableTypes>;
150
+ /** Variables the caller passes: everything except the i18n variables the client injects. */
151
+ type UserVariables<Doc> = Omit<VariablesOfDoc<Doc>, I18nVariableNamesOfDoc<Doc>>;
146
152
  /** Resolves to `true` when an object type has no required keys. */
147
153
  type HasNoRequiredKeys<T> = Record<string, never> extends T ? true : false;
148
154
  /**
@@ -213,8 +219,9 @@ type StorefrontClient<Extra extends Record<string, unknown> = {}, Type extends C
213
219
  i18n: RequestContext["i18n"];
214
220
  /**
215
221
  * Runs a Storefront API query or mutation and returns a typed result. Pass a `gql` document or a
216
- * plain query string. When the operation declares `$country` or `$language`, the client sets the
217
- * variable from the request context and overrides any value you pass.
222
+ * plain query string. When the operation declares `$country: CountryCode` or
223
+ * `$language: LanguageCode`, the client sets the variable from the request context and overrides
224
+ * any value you pass. Variables with other types stay caller-owned.
218
225
  *
219
226
  * The method throws an error when a query passes a `cache` strategy and the client has no cache,
220
227
  * or when the strategy uses `private` mode. A catch block for `StorefrontApiError` doesn't catch
@@ -203,13 +203,18 @@ function reuseVisibleReferences(previous, next) {
203
203
  pending
204
204
  };
205
205
  }
206
- function derivePending(state, transactions) {
206
+ function getVisiblePendingKeys(store, transaction) {
207
+ if (transaction.type !== "change_lines_quantity") return transaction.pendingKeys;
208
+ const ownedKeys = getOwnedSignalKeys(store, transaction);
209
+ return transaction.pendingKeys.filter((key) => !transaction.signalKeys.includes(key) || ownedKeys.includes(key));
210
+ }
211
+ function derivePending(store, state) {
207
212
  const lines = /* @__PURE__ */ new Set();
208
213
  const discountCodes = /* @__PURE__ */ new Set();
209
214
  let note = false;
210
215
  let attributes = false;
211
216
  let cost = false;
212
- for (const transaction of transactions) for (const key of transaction.pendingKeys) {
217
+ for (const transaction of store.transactions) for (const key of getVisiblePendingKeys(store, transaction)) {
213
218
  if (key === NOTE_KEY) note = true;
214
219
  if (key === ATTRIBUTES_KEY) attributes = true;
215
220
  if (key.startsWith(LINE_KEY_PREFIX)) {
@@ -236,10 +241,13 @@ function derivePending(state, transactions) {
236
241
  }
237
242
  function projectVisibleState(store) {
238
243
  let state = store.settled;
239
- for (const transaction of store.transactions) state = transaction.projectPayload(state);
244
+ for (const transaction of store.transactions) {
245
+ const ownedKeys = getOwnedSignalKeys(store, transaction);
246
+ state = transaction.projectPayload(state, (key) => ownedKeys.includes(key));
247
+ }
240
248
  state = {
241
249
  ...state,
242
- pending: derivePending(state, store.transactions)
250
+ pending: derivePending(store, state)
243
251
  };
244
252
  for (const error of store.projectedErrors) state = error.project(state);
245
253
  if (store.revalidation.visible) state = {
@@ -288,9 +296,9 @@ function createOwner(store, keys) {
288
296
  }
289
297
  return owner;
290
298
  }
291
- function ownsSignalKeys(store, transaction) {
292
- if (!transaction.owner) return true;
293
- return transaction.signalKeys.every((key) => store.keyedOwners.get(key)?.token === transaction.owner?.token);
299
+ function getOwnedSignalKeys(store, transaction) {
300
+ if (!transaction.owner) return transaction.signalKeys;
301
+ return transaction.signalKeys.filter((key) => store.keyedOwners.get(key)?.token === transaction.owner?.token);
294
302
  }
295
303
  function releaseSignalKeys(store, transaction) {
296
304
  if (!transaction.owner) return;
@@ -413,7 +421,9 @@ function writeNetworkError(state, error, timestampMs) {
413
421
  }
414
422
  function projectLineErrors(state, failure, lineIds, timestampMs) {
415
423
  if (!failure) return state;
416
- const grouped = groupLineErrors(failure, lineIds);
424
+ return writeLineErrors(state, groupLineErrors(failure, lineIds), timestampMs);
425
+ }
426
+ function writeLineErrors(state, grouped, timestampMs) {
417
427
  return {
418
428
  ...state,
419
429
  errors: {
@@ -567,6 +577,22 @@ function mergeServerLine(previous, next) {
567
577
  ...merchandise ? { merchandise } : {}
568
578
  };
569
579
  }
580
+ function addOwnedLineErrors(failure, lineIds, ownedLineIds, addError) {
581
+ const grouped = groupLineErrors(failure, lineIds);
582
+ for (const [lineId, group] of grouped.lines) {
583
+ if (lineIds.includes(lineId) && !ownedLineIds.has(lineId)) continue;
584
+ const lines = new Map([[lineId, group]]);
585
+ addError((current, timestampMs) => writeLineErrors(current, {
586
+ lines,
587
+ cart: createEmptyErrorGroup()
588
+ }, timestampMs), [lineKey(lineId)]);
589
+ }
590
+ if (grouped.cart.userErrors.length === 0 && grouped.cart.warnings.length === 0) return;
591
+ addError((current, timestampMs) => writeLineErrors(current, {
592
+ lines: /* @__PURE__ */ new Map(),
593
+ cart: grouped.cart
594
+ }, timestampMs), [...ownedLineIds].map(lineKey));
595
+ }
570
596
  function hasProjectedErrors(result) {
571
597
  return (result?.userErrors?.length ?? 0) > 0 || (result?.warnings?.length ?? 0) > 0;
572
598
  }
@@ -579,6 +605,31 @@ function transportAddToCart(payload, signal, updateCart, cartId) {
579
605
  ...payload.eventDetail && { event: { detail: payload.eventDetail } }
580
606
  });
581
607
  }
608
+ function projectLineQuantity(state, payload) {
609
+ const previous = getLines(state.data);
610
+ if (!previous.some((line) => line.id === payload.lineId)) return state;
611
+ const lines = payload.quantity === 0 ? previous.filter((line) => line.id !== payload.lineId) : previous.map((line) => line.id === payload.lineId ? {
612
+ ...line,
613
+ quantity: payload.quantity
614
+ } : line);
615
+ return {
616
+ ...state,
617
+ data: reconcileCartLines(state.data, lines)
618
+ };
619
+ }
620
+ function settleLineQuantity(state, serverLines, payload, requestRevalidation) {
621
+ const previous = getLines(state.data);
622
+ const matching = serverLines.find((line) => line.id === payload.lineId);
623
+ if (!matching && payload.quantity > 0) {
624
+ requestRevalidation();
625
+ return state;
626
+ }
627
+ const lines = matching ? previous.map((line) => line.id === payload.lineId ? mergeServerLine(line, matching) : line) : previous.filter((line) => line.id !== payload.lineId);
628
+ return {
629
+ ...state,
630
+ data: reconcileCartLines(state.data, lines)
631
+ };
632
+ }
582
633
  const CART_TRANSACTION_TYPES = defineTransactionTypes({
583
634
  add_to_cart: {
584
635
  payload: {},
@@ -702,6 +753,22 @@ const CART_TRANSACTION_TYPES = defineTransactionTypes({
702
753
  },
703
754
  getSignalKeys: (_state, payload) => lineKey(payload.lineId)
704
755
  },
756
+ change_lines_quantity: {
757
+ payload: {},
758
+ transport: (payload, signal, updateCart) => updateCart({ lines: payload.lines.map(({ lineId, quantity }) => ({
759
+ id: lineId,
760
+ quantity
761
+ })) }, { signal }),
762
+ projectPayload: (state, payload, ownsKey) => payload.lines.filter(({ lineId }) => ownsKey(lineKey(lineId))).reduce(projectLineQuantity, state),
763
+ projectPromise: (state, result, payload, addError, { ownsKey, requestRevalidation }) => {
764
+ const owned = payload.lines.filter(({ lineId }) => ownsKey(lineKey(lineId)));
765
+ if (hasProjectedErrors(result)) addOwnedLineErrors(result, payload.lines.map(({ lineId }) => lineId), new Set(owned.map(({ lineId }) => lineId)), addError);
766
+ if (!result.cart) return state;
767
+ const serverLines = getLines(cartResponseFromStandardEvent(result.cart));
768
+ return owned.reduce((current, line) => settleLineQuantity(current, serverLines, line, requestRevalidation), state);
769
+ },
770
+ getSignalKeys: (_state, payload) => payload.lines.map(({ lineId }) => lineKey(lineId))
771
+ },
705
772
  set_discount_codes: {
706
773
  payload: {},
707
774
  transport: (payload, signal, updateCart) => updateCart({ discountCodes: payload.discountCodes }, { signal }),
@@ -844,8 +911,12 @@ function createPendingTransaction(store, type, definition, payload, promise, res
844
911
  requiresRevalidation,
845
912
  owner,
846
913
  promise,
847
- projectPayload: (current) => definition.projectPayload(current, payload),
848
- projectPromise: (current, result, addError) => definition.projectPromise(current, result, payload, addError, { mergeServerCart: !transaction.requiresRevalidation }),
914
+ projectPayload: (current, ownsKey) => definition.projectPayload(current, payload, ownsKey),
915
+ projectPromise: (current, result, addError, ownsKey, requestRevalidation) => definition.projectPromise(current, result, payload, addError, {
916
+ mergeServerCart: !transaction.requiresRevalidation,
917
+ ownsKey,
918
+ requestRevalidation
919
+ }),
849
920
  trimAfter: (successful) => trimPendingTransaction(store, type, definition, payload, promise, {
850
921
  id,
851
922
  sequence,
@@ -915,15 +986,16 @@ function fulfillTransaction(store, transactionId, result) {
915
986
  const transaction = store.transactions.find((candidate) => candidate.id === transactionId);
916
987
  if (!transaction || transaction.generation !== store.generation) return;
917
988
  store.observedPromises.delete(transaction.promise);
918
- if (!ownsSignalKeys(store, transaction)) {
989
+ const ownedKeys = getOwnedSignalKeys(store, transaction);
990
+ if (ownedKeys.length === 0 && transaction.signalKeys.length > 0) {
919
991
  removeTransaction(store, transactionId);
920
992
  publishVisibleState(store);
921
993
  revalidateCartWhenIdle(store);
922
994
  return;
923
995
  }
924
- store.settled = transaction.projectPromise(store.settled, result, (projector, keys) => addProjectedError(store, keys ?? transaction.errorKeys, projector));
996
+ store.settled = transaction.projectPromise(store.settled, result, (projector, keys) => addProjectedError(store, keys ?? transaction.errorKeys, projector), (key) => ownedKeys.includes(key), () => requestCartRevalidation(store));
925
997
  store.settled = addSnapshotIdentity(store.settled, result);
926
- if (result.cart && !transaction.requiresRevalidation && transaction.sequence >= store.lastSnapshotSequence) {
998
+ if (result.cart && ownedKeys.length === transaction.signalKeys.length && !transaction.requiresRevalidation && transaction.sequence >= store.lastSnapshotSequence) {
927
999
  store.settled = addSnapshotFields(store.settled, result);
928
1000
  store.lastSnapshotSequence = transaction.sequence;
929
1001
  }
@@ -943,13 +1015,14 @@ function rejectTransaction(store, transactionId, error) {
943
1015
  const transaction = store.transactions.find((candidate) => candidate.id === transactionId);
944
1016
  if (!transaction || transaction.generation !== store.generation) return;
945
1017
  store.observedPromises.delete(transaction.promise);
946
- const owned = ownsSignalKeys(store, transaction);
1018
+ const ownedKeys = getOwnedSignalKeys(store, transaction);
1019
+ const ownsAllKeys = ownedKeys.length === transaction.signalKeys.length;
947
1020
  removeTransaction(store, transactionId);
948
1021
  releaseSignalKeys(store, transaction);
949
- if (owned) {
1022
+ if (ownedKeys.length > 0 || ownsAllKeys) {
950
1023
  const failure = extractCartActionFailure(error);
951
- if (failure) transaction.projectPromise(store.settled, rejectedResult(failure), (projector, keys) => addProjectedError(store, keys ?? transaction.errorKeys, projector));
952
- if (!failure && !isAbortError(error)) addProjectedError(store, transaction.errorKeys, (state, timestampMs) => writeNetworkError(state, error, timestampMs));
1024
+ if (failure) transaction.projectPromise(store.settled, rejectedResult(failure), (projector, keys) => addProjectedError(store, keys ?? transaction.errorKeys, projector), (key) => ownedKeys.includes(key), NOOP);
1025
+ if (!failure && !isAbortError(error)) addProjectedError(store, ownsAllKeys ? transaction.errorKeys : ownedKeys, (state, timestampMs) => writeNetworkError(state, error, timestampMs));
953
1026
  }
954
1027
  publishVisibleState(store);
955
1028
  revalidateCartWhenIdle(store);
@@ -980,7 +1053,11 @@ function enqueueTransaction(store, type, payload, promise, reservedTransaction,
980
1053
  promise.then((result) => fulfillTransaction(store, transaction.id, result), (error) => rejectTransaction(store, transaction.id, error));
981
1054
  }
982
1055
  function markOverlappingTransactionForRevalidation(store, transaction) {
983
- if (store.transactions.length === 0 && !store.revalidation.active && store.activeMutationTransports.size === 0) return;
1056
+ if (store.transactions.length === 0 && !store.revalidation.active && store.activeMutationTransports.size === 0 && !store.activeCartLoad) return;
1057
+ if (store.activeCartLoad) store.settled = {
1058
+ ...store.settled,
1059
+ loading: false
1060
+ };
984
1061
  transaction.requiresRevalidation = true;
985
1062
  for (const pending of store.transactions) pending.requiresRevalidation = true;
986
1063
  requestCartRevalidation(store);
@@ -1173,12 +1250,16 @@ function handleLinesEvent(store, event) {
1173
1250
  enqueueTransaction(store, "add_to_cart", payloadFromAddEvent(event), event.promise, void 0, eventToken);
1174
1251
  return;
1175
1252
  }
1176
- if (event.lines.length !== 1) return;
1177
- const line = event.lines[0];
1178
- enqueueTransaction(store, "change_line_quantity", {
1253
+ const lines = event.lines.map((line) => ({
1179
1254
  lineId: line.id,
1180
1255
  quantity: line.quantity
1181
- }, event.promise, void 0, eventToken);
1256
+ }));
1257
+ if (lines.length === 0) return;
1258
+ if (lines.length === 1) {
1259
+ enqueueTransaction(store, "change_line_quantity", lines[0], event.promise, void 0, eventToken);
1260
+ return;
1261
+ }
1262
+ enqueueTransaction(store, "change_lines_quantity", { lines }, event.promise, void 0, eventToken);
1182
1263
  }
1183
1264
  function handleDiscountEvent(store, event) {
1184
1265
  enqueueTransaction(store, "set_discount_codes", { discountCodes: event.discountCodes.map((discount) => discount.code) }, event.promise, void 0, getTransactionEventToken(event.detail));
@@ -1201,6 +1282,10 @@ function connectCartStore(store, handlers) {
1201
1282
  document.addEventListener("shopify:cart:note-update", handlers.note);
1202
1283
  document.addEventListener("shopify:cart:attributes-update", handlers.attributes);
1203
1284
  getShopifyStandardActions().catch(() => {});
1285
+ if (store.revalidateOnConnect) {
1286
+ store.revalidateOnConnect = false;
1287
+ refreshCartInStore(store);
1288
+ }
1204
1289
  return true;
1205
1290
  }
1206
1291
  function cancelCartRevalidation(store) {
@@ -1218,7 +1303,6 @@ function clearPendingTransactions(store) {
1218
1303
  store.transactions = [];
1219
1304
  store.projectedErrors = [];
1220
1305
  store.observedPromises.clear();
1221
- store.activeMutationTransports.clear();
1222
1306
  store.expectedEvents = [];
1223
1307
  store.reservation = null;
1224
1308
  store.identityTransportGate.active = null;
@@ -1227,6 +1311,7 @@ function clearPendingTransactions(store) {
1227
1311
  cancelCartRevalidation(store);
1228
1312
  }
1229
1313
  function destroyCartStore(store, handlers) {
1314
+ store.revalidateOnConnect ||= store.revalidation.requested || store.revalidation.active !== null;
1230
1315
  clearPendingTransactions(store);
1231
1316
  if (typeof document !== "undefined" && store.cartSyncAttached) {
1232
1317
  document.removeEventListener("shopify:cart:lines-update", handlers.lines);
@@ -1323,6 +1408,7 @@ function resetCartStore(store) {
1323
1408
  const hadActiveRevalidation = store.revalidation.active !== null;
1324
1409
  const shouldReloadAfterReset = store.cartSyncAttached && !hadActiveRevalidation && (hadActiveCartLoad || hasLocalCartData(store.observable.state));
1325
1410
  clearPendingTransactions(store);
1411
+ store.activeMutationTransports.clear();
1326
1412
  invalidateActiveCartLoad(store);
1327
1413
  cancelCartRevalidation(store);
1328
1414
  store.lastSnapshotSequence = 0;
@@ -1435,6 +1521,7 @@ function createCartStore(options = {}) {
1435
1521
  waiting: []
1436
1522
  },
1437
1523
  cartSyncAttached: false,
1524
+ revalidateOnConnect: false,
1438
1525
  reservation: null,
1439
1526
  lastSnapshotSequence: 0
1440
1527
  };