@usethrottle/catalog 1.1.0 → 1.3.0

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 (41) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +135 -24
  3. package/dist/admin.cjs +16 -16
  4. package/dist/admin.d.cts +6 -2
  5. package/dist/admin.d.ts +6 -2
  6. package/dist/admin.js +1 -1
  7. package/dist/{catalog-error-s1BINNkt.d.cts → catalog-error-BOaSKgY3.d.cts} +12 -2
  8. package/dist/{catalog-error-C_zLOtYA.d.ts → catalog-error-CSoPS0-d.d.ts} +12 -2
  9. package/dist/{chunk-4ORXUW2U.cjs → chunk-2YQNMBDI.cjs} +119 -33
  10. package/dist/{chunk-JK2UY4CD.js → chunk-3DLH56OX.js} +99 -13
  11. package/dist/chunk-CADIHOIR.js +496 -0
  12. package/dist/chunk-GU63WFXI.cjs +496 -0
  13. package/dist/{client-c2F-eCaL.d.ts → client-6j1Iz-iJ.d.cts} +2 -9
  14. package/dist/{client-BAoq6jmM.d.cts → client-B3mL2-xJ.d.ts} +2 -9
  15. package/dist/{contract-types-DhiThMEr.d.cts → contract-types-BRyonOYH.d.cts} +23 -21
  16. package/dist/{contract-types-DhiThMEr.d.ts → contract-types-BRyonOYH.d.ts} +23 -21
  17. package/dist/index.cjs +24 -2
  18. package/dist/index.d.cts +13 -7
  19. package/dist/index.d.ts +13 -7
  20. package/dist/index.js +25 -3
  21. package/dist/react-extras.cjs +42 -0
  22. package/dist/react-extras.d.cts +23 -0
  23. package/dist/react-extras.d.ts +23 -0
  24. package/dist/react-extras.js +42 -0
  25. package/dist/react.cjs +286 -491
  26. package/dist/react.d.cts +44 -38
  27. package/dist/react.d.ts +44 -38
  28. package/dist/react.js +289 -494
  29. package/dist/server.cjs +19 -14
  30. package/dist/server.d.cts +4 -3
  31. package/dist/server.d.ts +4 -3
  32. package/dist/server.js +7 -2
  33. package/dist/{types-DsjoG8ce.d.cts → types-BfveENg1.d.ts} +76 -3
  34. package/dist/{types-xQLID-4j.d.ts → types-D0PywKnN.d.cts} +76 -3
  35. package/dist/url.d.cts +2 -1
  36. package/dist/url.d.ts +2 -1
  37. package/dist/use-query-CR7r5tam.d.ts +40 -0
  38. package/dist/use-query-UWf6Pi5g.d.cts +40 -0
  39. package/dist/version-ev3p03v2.d.cts +8 -0
  40. package/dist/version-ev3p03v2.d.ts +8 -0
  41. package/package.json +13 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @usethrottle/catalog
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e159fc9: Multi-currency slice 2. Everything is additive: without the new calls the SDK behaves as 1.2.0.
8
+
9
+ - Money helpers from `@usethrottle/money` (a new dependency, `^1.0.0`): the `money` namespace and, by name, `formatMoney`, `currencySymbol`, `toDecimalString`, `toMinor`, `parseMoney`, `minorUnitsOf`, `isKnownCurrency`, `moneyValue`, `CURRENCIES`, `UnknownCurrencyError` and the `CurrencyCode` and `MoneyValue` types. Prices are integers in the minor units of their currency; format them with `formatMoney`.
10
+ - `CurrencyCode` typing on every price object's `currency` (product, variant, price list and cart line), `CartView.currency` and `OpenCartRef.currency`. `CartOptions.currency` stays `string`.
11
+ - `useCart().format(minor)` formats an amount in the cart's currency.
12
+ - `useCart().switchCurrency(next)` finds or creates the shopper's cart in `next` and moves the current cart's lines into it through Throttle's absorb route. Signed in, it finds the customer's open cart in that currency; as a guest, it reads the optional `CartStorage` per-currency slots (`getFor`, `setFor`, `removeFor`, implemented by `createMemoryCartStorage`). With no target it creates an empty cart in `next`. Removed lines and a dropped discount are reported through the hook's `warnings`. Any failure keeps the old cart, its stored id and its currency.
13
+ - The catalog Worker now accepts an empty cart create (`items: []`), which the switch needs. Older clients are unaffected.
14
+
15
+ ## 1.2.0
16
+
17
+ ### Minor Changes
18
+
19
+ - d6435da: Storefront Part 8 (1.2.0). The API is additive (no name is removed or changed), but two defaults change for everyone: `suggest` never sends the customer token, so suggestions show list prices (an integrator who showed group prices in suggestions now shows list prices), and failed revalidations retry up to twice. Everything else needs the new options.
20
+
21
+ - `keyInQuery` (opt-in): a browser read sends the publishable key as `?key=` and no `x-catalog-key` header, so it needs no CORS preflight. Reads only (a `POST` always sends the header); `createServerCatalogClient` ignores it. A server key in a URL is refused with the new error code `KEY_NOT_ALLOWED_IN_URL` (`403`).
22
+ - `suggest` sends no customer token in any mode, so suggestions are list prices (cached under the guest partition without `keyInQuery`). With `keyInQuery` it sends only `accept` plus any client `headers` (no `If-None-Match`; custom `headers` bring the preflight back) and bypasses the SDK's ETag cache. A customer token is never put in a URL.
23
+ - `customerCart` on `CatalogProvider` (types `CustomerCartSource` and `OpenCartRef`, exported from `.` and `/react`): `useCart` resumes a signed-in customer's open cart on any device. It resumes only when a currency is known (the client `currency` option or `useCart({ currency })`).
24
+ - New hooks `useTags`, `useCollections` and `useReviewEligibility`, imported from `@usethrottle/catalog/react/extras` (a new entry that shares `/react`'s provider and cache; it keeps `/react` within its 6,144 B budget). `HOOKS_ENTRY` is `@usethrottle/catalog/react/extras`.
25
+ - `useCustomerKey()` reads the provider's `customerKey`; the `ReviewEligibility` type is exported from `.` and `/react`.
26
+ - `keepPreviousData` option, with `dataFor` (the params the shown `data` answers; for `useSuggest`, the query echo `dataFor.q`) and `isPreviousData` on the result. Data is never kept across a `customerKey` change.
27
+ - `isStale` on the result and automatic retries: a failed revalidation keeps its data, sets `error`, and retries a `NetworkError`, `TimeoutError` or 5xx at 2 s and 12 s (two retries). Other 4xx are not retried and a `429` pause holds.
28
+ - The `640` size key in answers: `sizes` and `media.versions` can now carry `640` (types updated).
29
+ - `KEY_NOT_ALLOWED_IN_URL` joins the error codes.
30
+
3
31
  ## 1.1.0
4
32
 
5
33
  ### Minor Changes
package/README.md CHANGED
@@ -32,17 +32,18 @@ for (const product of products) {
32
32
  }
33
33
  ```
34
34
 
35
- Prices are integers in minor units (cents for USD). The client talks to `https://catalog.usethrottle.dev` by default (`baseUrl` changes it). Every call takes `{ signal, timeoutMs }`; the default timeout is 10 seconds.
35
+ Prices are integers in the minor units of their currency; format them with `formatMoney` (`formatMoney(product.priceRange.minMinor, product.priceRange.currency)` gives `$19.99` for USD and `¥1,999` for JPY, the same on the server and in the browser). The root re-exports `@usethrottle/money` as `money` and by name (`formatMoney`, `currencySymbol`, `minorUnitsOf`, `toDecimalString`, `toMinor`, `parseMoney`, `moneyValue`, `CURRENCIES`, `isKnownCurrency`, `UnknownCurrencyError`, and the types `CurrencyCode` and `MoneyValue`); every `currency` the SDK answers is a `CurrencyCode`, which is still assignable to `string`. The client talks to `https://catalog.usethrottle.dev` by default (`baseUrl` changes it). Every call takes `{ signal, timeoutMs }`; the default timeout is 10 seconds.
36
36
 
37
37
  ## Entries
38
38
 
39
- | Import | What it holds |
40
- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
- | `@usethrottle/catalog` | `createCatalogClient`, the error classes and `isCatalogError`, and the answer types. No DOM and no Node-only API. |
42
- | `@usethrottle/catalog/react` | `CatalogProvider`, the read hooks, `useCart` and `useSubmitReview` (marked `"use client"`). |
43
- | `@usethrottle/catalog/server` | `createServerCatalogClient`, `catalogTags`, `syncCatalogGeneration` and the paging helpers (`listAllProductHandles`, `staticProductParams`, `listAllCollectionHandles`, `listAllCategoryHandles`: it asks for `CATEGORY_TREE_MAX_DEPTH` levels, 10, so a sitemap lists the whole tree). |
44
- | `@usethrottle/catalog/url` | `parseShopQuery`, `serializeShopQuery`, `toProductsQuery`, `parseRange` and `formatRange`. Standalone: none of the client or React code (the ESM and CommonJS builds each load one tiny shared helper file). |
45
- | `@usethrottle/catalog/admin` | **Experimental, server only.** `createCatalogAdmin` and `verifyCatalogWebhook` for the catalog push API (sync jobs, single batches, runs, products, stock), and the push request and answer types. See [Admin](#admin-server-only-experimental). |
39
+ | Import | What it holds |
40
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | `@usethrottle/catalog` | `createCatalogClient`, the error classes and `isCatalogError`, the answer types, and the `@usethrottle/money` helpers (`money`, `formatMoney`, …). No DOM and no Node-only API. |
42
+ | `@usethrottle/catalog/react` | `CatalogProvider`, the read hooks, `useCart`, `useSubmitReview` and `useCustomerKey` (marked `"use client"`). |
43
+ | `@usethrottle/catalog/react/extras` | `useTags`, `useCollections` and `useReviewEligibility` (marked `"use client"`). They share `CatalogProvider` and the cache with `/react`: import the provider from `/react` and these three from here. |
44
+ | `@usethrottle/catalog/server` | `createServerCatalogClient`, `catalogTags`, `syncCatalogGeneration` and the paging helpers (`listAllProductHandles`, `staticProductParams`, `listAllCollectionHandles`, `listAllCategoryHandles`: it asks for `CATEGORY_TREE_MAX_DEPTH` levels, 10, so a sitemap lists the whole tree). |
45
+ | `@usethrottle/catalog/url` | `parseShopQuery`, `serializeShopQuery`, `toProductsQuery`, `parseRange` and `formatRange`. Standalone: none of the client or React code (the ESM and CommonJS builds each load one tiny shared helper file). |
46
+ | `@usethrottle/catalog/admin` | **Experimental, server only.** `createCatalogAdmin` and `verifyCatalogWebhook` for the catalog push API (sync jobs, single batches, runs, products, stock), and the push request and answer types. See [Admin](#admin-server-only-experimental). |
46
47
 
47
48
  Everything `/react` exports is a client reference in a server component, `SDK_VERSION` and `catalogQueryKey` included: in server code import `SDK_VERSION` from the root or `/server`, and call `catalogQueryKey` only in client code.
48
49
 
@@ -80,7 +81,7 @@ Every error the SDK throws is a `CatalogError` (which extends `ThrottleError` fr
80
81
  | Class | When |
81
82
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
82
83
  | `RateLimitedError` | every `429`; `retryAfterSeconds` comes from `Retry-After` (60 when it is missing or unusable). The client never retries a `429` itself. |
83
- | `OriginNotAllowedError` | `ORIGIN_NOT_ALLOWED`, `SERVER_KEY_IN_BROWSER`, and Throttle's `origin_not_allowed`. |
84
+ | `OriginNotAllowedError` | `ORIGIN_NOT_ALLOWED`, `SERVER_KEY_IN_BROWSER`, `KEY_NOT_ALLOWED_IN_URL`, and Throttle's `origin_not_allowed`. |
84
85
  | `CustomerTokenInvalidError` | a refused customer token on a call that cannot fall back to list prices. |
85
86
  | `CartLineError` | `CART_ITEMS_INVALID` and Throttle's `quantity_unavailable` and `line_not_purchasable`, with `lines: { sku, code, maxQuantity? }[]`. |
86
87
  | `TimeoutError` | `TIMEOUT`: no answer within `timeoutMs`. |
@@ -104,7 +105,7 @@ try {
104
105
 
105
106
  ## Carts and checkout
106
107
 
107
- Carts are resolved by the catalog: the browser sends SKUs and quantities, never prices. `carts.create` answers Throttle's hosted cart URL (`cartUrl`) and a session id. It takes three optional URLs, each an absolute `https` URL on the store's Checkout settings › Allowed return websites (otherwise `422 RETURN_URL_NOT_ALLOWED`, one issue per refused field):
108
+ Carts are resolved by the catalog: the browser sends SKUs and quantities, never prices. `carts.create` answers Throttle's hosted cart URL (`cartUrl`) and a session id; `items` may be empty, for an empty cart in a currency. It takes three optional URLs, each an absolute `https` URL on the store's Checkout settings › Allowed return websites (otherwise `422 RETURN_URL_NOT_ALLOWED`, one issue per refused field):
108
109
 
109
110
  ```ts
110
111
  const cart = await catalog.carts.create({
@@ -116,7 +117,7 @@ const cart = await catalog.carts.create({
116
117
  // cart.returnUrlApplied is false when Throttle did not store them.
117
118
  ```
118
119
 
119
- Without them the shopper ends on Throttle's own completion page. To build your own cart page instead, use `catalog.carts.session(cart.sessionId)` (`get`, `add`, `update`, `remove`, `refresh`, `checkout({ returnUrl, cancelUrl })`). `checkout()` answers the checkout URL; it never navigates. Both are checked before any request: `carts.create` refuses a URL that is not absolute `https` (`INVALID_REQUEST`, naming the field), and `checkout()` one that is not absolute `http` or `https`.
120
+ Without them the shopper ends on Throttle's own completion page. To build your own cart page instead, use `catalog.carts.session(cart.sessionId)` (`get`, `add`, `update`, `remove`, `refresh`, `checkout({ returnUrl, cancelUrl })`, and `absorb(fromSessionId)`, which moves another session's cart into this one in this cart's currency, answering `{ cart, warnings }`; `useCart().switchCurrency` is built on it). `checkout()` answers the checkout URL; it never navigates. Both are checked before any request: `carts.create` refuses a URL that is not absolute `https` (`INVALID_REQUEST`, naming the field), and `checkout()` one that is not absolute `http` or `https`.
120
121
 
121
122
  ## React
122
123
 
@@ -134,6 +135,7 @@ import {
134
135
  // One client per currency and language (see Versioning), kept for the life of the app.
135
136
  const client = createCatalogClient({
136
137
  publishableKey: process.env.NEXT_PUBLIC_CATALOG_KEY ?? '',
138
+ keyInQuery: true, // browser reads need no preflight (see Key transport)
137
139
  customerToken: () => auth.getAccessToken(),
138
140
  });
139
141
 
@@ -186,13 +188,128 @@ function ProductGrid() {
186
188
  }
187
189
  ```
188
190
 
189
- - **Hooks:** `useProducts`, `useProduct`, `useRelated`, `useFilterValues`, `useCategories`, `useCollection`, `useSuggest` (debounced 200 ms, 2 characters at least), `useReviews` and `useConfig`, each answering `{ data, error, isLoading, isValidating, refetch }` and taking `enabled`, `initialData` (for an SSR hand-off) and `staleTimeMs`. With a signed-in customer (`customerKey` set), `initialData` is shown and revalidated at once, since a server render with the server key is anonymous; to avoid the brief list-price paint, render the server side with the customer's token.
191
+ - **Hooks:** `useProducts`, `useProduct`, `useRelated`, `useFilterValues`, `useCategories`, `useCollection`, `useSuggest` (debounced 200 ms, 2 characters at least), `useReviews` and `useConfig` from `/react`; `useTags`, `useCollections` and `useReviewEligibility` from `/react/extras` (below). Each answers `{ data, dataFor, error, isLoading, isPreviousData, isStale, isValidating, refetch }` and takes `enabled`, `initialData` (for an SSR hand-off), `staleTimeMs` and `keepPreviousData`. With a signed-in customer (`customerKey` set), `initialData` is shown and revalidated at once, since a server render with the server key is anonymous; to avoid the brief list-price paint, render the server side with the customer's token.
190
192
  - **The cache** is per provider: answers are shown at once and refetched when older than 60 seconds, and calls for one key share one request. Two providers never share it.
191
193
  - **`customerKey`** partitions the cache by customer. Change it in the same update as the token: a change starts an empty cache and every mounted hook refetches, so one customer never sees another's prices. It is required whenever `customerToken` can return a token: `useCart` drops the cart on a sign-out or a customer change only by watching it, so without it one customer's cart stays for the next shopper. Keep one `client` per currency and language (see [One client per currency and language](#one-client-per-currency-and-language)) and pass `customerToken` as a function.
192
- - **`useCart`** keeps one cart per provider and runs its changes one at a time. It creates the cart on the first `add`, recreates it once if Throttle ended the session, and makes a new cart on `switchCurrency`. A switch lands whole or not at all: if the new cart cannot be made, read back or saved, `switchCurrency` rejects and the old cart, its stored id and the old currency stay. The cart is dropped when the signed-in customer signs out or changes, and kept on sign-in. `checkout()` uses the provider's `cartUrls` unless given URLs, and answers the checkout URL. `cartUrls` go on every cart create, so they must be absolute `https` URLs on the store's Allowed return websites, or every `add` fails; otherwise (on `http://localhost`, say) leave them out and pass the URLs to `checkout(input)`. The default `cartStorage` is memory only; key a persistent one by customer, as above: a new adapter object per customer, never one stable object that reads the current customer when called (a drop removes the outgoing customer's entry through the outgoing object, so such an adapter would delete the incoming customer's cart instead).
194
+ - **`useCart`** keeps one cart per provider and runs its changes one at a time. It creates the cart on the first `add` and recreates it once if Throttle ended the session. `format(minor)` formats an amount in the cart's currency (`useCart().format(4763)` on a EUR cart is `€47.63`). **`switchCurrency(currency)`** keeps one cart per currency: it moves the cart into the shopper's open cart in that currency (a signed-in customer's from `customerCart`; a guest's from `cartStorage.getFor`, used only while that cart is still open), or into a new empty cart when there is none, and Throttle closes the old cart as merged. Lines with no price in the new currency stay behind and a discount code the new currency refuses is dropped; both are reported in `warnings` (`lines_removed`, `discount_dropped`). If anything fails before the move lands (the lookup, the new cart, the move itself, for example `422 currency_not_available_in_production`), `switchCurrency` rejects and the old cart, its stored id and the old currency stay. A signed-in switch needs `customerToken` on the client: the new cart is created as the customer's, and Throttle moves a customer's cart only into that customer's cart (`403 cart_owner_mismatch` otherwise). A `cartStorage` adapter may implement the optional `getFor(currency)`, `setFor(currency, id)` and `removeFor(currency)` (`createMemoryCartStorage` does); without them a guest switch always makes a new cart. These are guest slots: a signed-in switch never writes them, and the provider forgets them on a sign-in, a sign-out or a customer change (through `removeFor`, or `setFor(currency, '')` when the adapter has no `removeFor`), so the next guest never reaches a customer's cart. The cart is dropped when the signed-in customer signs out or changes, and kept on sign-in. `checkout()` uses the provider's `cartUrls` unless given URLs, and answers the checkout URL. `cartUrls` go on every cart create, so they must be absolute `https` URLs on the store's Allowed return websites, or every `add` fails; otherwise (on `http://localhost`, say) leave them out and pass the URLs to `checkout(input)`. The default `cartStorage` is memory only; key a persistent one by customer, as above: a new adapter object per customer, never one stable object that reads the current customer when called (a drop removes the outgoing customer's entry through the outgoing object, so such an adapter would delete the incoming customer's cart instead).
193
195
  - **Rate limits:** a catalog `429` pauses every read of the provider until its retry time; each mounted key then refetches once.
194
196
  - **`useSubmitReview(handle)`** submits a review with one idempotency key per submission.
195
197
 
198
+ ### Customer carts
199
+
200
+ With `@usethrottle/auth` 0.12 or later, a signed-in customer's open cart comes back on any device. This example needs `@usethrottle/auth` >= 0.12.0, whose `account.getOpenCart` and `whenClaimSettled` it uses. Pass `customerCart` to the provider; it is structural, so the SDK never holds a Throttle key and never imports `@usethrottle/auth`:
201
+
202
+ ```tsx
203
+ import { useMemo } from 'react';
204
+ import { useAuth } from '@usethrottle/auth/react';
205
+
206
+ function Shell({ customerId, children }) {
207
+ const { auth, account } = useAuth();
208
+ // Memoised: a new object on every render would re-render every consumer.
209
+ const customerCart = useMemo(
210
+ () => ({
211
+ // The customer's most recently updated open cart in `currency`, or null.
212
+ // Both members are optional on the auth SDK, so fall back when absent.
213
+ getOpenCart: ({ currency }) =>
214
+ account.getOpenCart?.({ currency }) ?? Promise.resolve(null),
215
+ // Resolves once the sign-in's guest-cart claim is done.
216
+ whenClaimSettled: () => auth.whenClaimSettled?.() ?? Promise.resolve(),
217
+ }),
218
+ [auth, account],
219
+ );
220
+ return (
221
+ <CatalogProvider
222
+ client={client}
223
+ customerKey={customerId}
224
+ customerCart={customerCart}
225
+ >
226
+ {children}
227
+ </CatalogProvider>
228
+ );
229
+ }
230
+ ```
231
+
232
+ `useCart` looks the cart up once per visit when the slot is empty (or ended), and before it would create a new cart. **It resumes only when a currency is known:** set `currency` on the client (`createCatalogClient({ currency: 'USD' })`) or pass it to the hook, `useCart({ currency: 'USD' })`. With neither, `useCart` makes no lookup and the first `add` creates a cart as before. A found cart in another currency is not adopted. Without `customerCart` nothing changes from 1.1.0. Throttle refuses a write to a cart another device merged (`409 cart_not_open`), so it is never reopened; `useCart` then looks the customer's cart up. `switchCurrency` while signed in looks up the customer's open cart in the new currency and moves the cart into it, so a customer keeps one cart per currency across devices. The lookup runs once per visit, so a currency that becomes known only after mount does not trigger it; the first `add` finds the cart instead. With two `useCart` call sites that pass different currencies, the lookup and the create can use different currencies; pass one currency (or set it on the client).
233
+
234
+ ### Key transport
235
+
236
+ By default every request sends the publishable key in the `x-catalog-key` header, which makes a browser preflight each new URL. Set `keyInQuery: true` on a browser client and a **read** (a `GET`) sends `?key=` instead, with no custom header, so a plain read needs no preflight (a revalidation with `If-None-Match`, or any client `headers`, still preflights; only `suggest` is preflight-free when the client sets no custom `headers`):
237
+
238
+ ```ts
239
+ const client = createCatalogClient({
240
+ publishableKey: process.env.NEXT_PUBLIC_CATALOG_KEY ?? '',
241
+ keyInQuery: true,
242
+ });
243
+ ```
244
+
245
+ - Reads only: a `POST` always sends the header. `createServerCatalogClient` ignores the option; a server key in a URL is refused (`403 KEY_NOT_ALLOWED_IN_URL`), so use `keyInQuery` with browser and app keys only. A URL is recorded wherever full URLs are kept (CDN and zone logs, analytics, browser history), so rotate any server key that was ever sent in a URL, even though it was refused.
246
+ - The customer token is never put in a URL, in any mode. It travels only in `X-Customer-Token`, so a read that carries a token still preflights.
247
+ - **`suggest` never sends the customer token**, whatever `keyInQuery` is. Suggestions are list prices (the answer has no group prices). Without `keyInQuery` they are cached under the guest partition; with it on, suggest bypasses the SDK's ETag cache and sends no `If-None-Match`. A suggest request then sends only `accept` plus any client `headers` you set, so every keystroke is one request, and a client with custom `headers` preflights again.
248
+
249
+ ### Keeping previous data
250
+
251
+ `keepPreviousData: true` keeps a hook's last settled answer on screen while the next key loads, so a filter change or a typed query does not flash empty:
252
+
253
+ ```tsx
254
+ const { data, dataFor, isPreviousData } = useProducts(
255
+ { q, limit: 24 },
256
+ { keepPreviousData: true },
257
+ );
258
+ // data is the old answer while isPreviousData is true; dataFor is the params it answers.
259
+ ```
260
+
261
+ `isLoading` stays `false` while the old answer shows. If the new key fails, `error` is set and the old data stays (`isPreviousData` true). `dataFor` is the params object the shown `data` answers, so for `useSuggest` the query echo is `dataFor.q`. While `data` is undefined (a first load, or a new key without `keepPreviousData`), `dataFor` is the params just asked for, so check `data` before treating `dataFor` as its params. When a new key fails and the previous data stays, the hook shows `isPreviousData` together with `error`; `isStale` is for revalidation failures of the data's own key. Data is never kept across a `customerKey` change: one customer's answer is not shown to the next.
262
+
263
+ ### Refresh errors
264
+
265
+ `isStale` is `true` while the shown `data` was not confirmed by the latest attempt: a revalidation failed, or an `initialData` seed under a `customerKey` is still being revalidated. A failed revalidation keeps the data, sets `error`, and retries by itself when the failure is retryable (a `NetworkError`, a `TimeoutError` or a 5xx): 2 seconds after the failure and again 10 seconds after that, so two retries. Other 4xx answers are not retried, and a `429` keeps its pause with no extra retry. A success clears `error` and `isStale`.
266
+
267
+ ```tsx
268
+ import type { StorefrontListAnswer } from '@usethrottle/catalog';
269
+
270
+ function Prices({ initialData }: { initialData: StorefrontListAnswer }) {
271
+ const { data, error, isStale, refetch } = useProducts(
272
+ { limit: 24 },
273
+ { initialData },
274
+ );
275
+ // Signed in: initialData is the anonymous server answer. If revalidating it fails,
276
+ // the list prices stay and isStale tells you they may be out of date.
277
+ return isStale && error ? (
278
+ <button onClick={() => void refetch()}>
279
+ Prices may be out of date. Retry
280
+ </button>
281
+ ) : null;
282
+ }
283
+ ```
284
+
285
+ ### The new hooks
286
+
287
+ `useTags`, `useCollections` and `useReviewEligibility` are in `@usethrottle/catalog/react/extras`. They use the provider and cache of `/react`:
288
+
289
+ ```tsx
290
+ import { CatalogProvider } from '@usethrottle/catalog/react';
291
+ import {
292
+ useCollections,
293
+ useReviewEligibility,
294
+ useTags,
295
+ } from '@usethrottle/catalog/react/extras';
296
+
297
+ function Filters() {
298
+ const tags = useTags({ q: 'fr', limit: 5 }); // client.tags.list({ q, limit })
299
+ const collections = useCollections({ limit: 6 }); // client.collections.list({ limit })
300
+ return /* tags.data?.tags, collections.data?.collections */ null;
301
+ }
302
+
303
+ function ReviewButton({ handle }: { handle: string }) {
304
+ // No request while signed out (customerKey null) or handle is null.
305
+ // Asked again on every mount (staleTimeMs 0), and kept per customer.
306
+ const { data } = useReviewEligibility(handle);
307
+ return data?.canReview ? <a href="#review">Write a review</a> : null;
308
+ }
309
+ ```
310
+
311
+ `useCustomerKey()` (from `/react`) returns the provider's `customerKey`, and the `ReviewEligibility` type (from either entry) is the answer of `reviews.eligibility`.
312
+
196
313
  ## Next.js
197
314
 
198
315
  On the server, use a **server** key and `createServerCatalogClient`. Every catalog read's `fetch` carries `next: { revalidate, tags }` (60 seconds by default); carts, Throttle calls and any request with a customer token are `cache: 'no-store'`. Next.js itself is never imported.
@@ -257,6 +374,8 @@ Ranges stay strings (`10..50`); `parseRange` and `formatRange` convert them.
257
374
 
258
375
  ## Admin (server only, experimental)
259
376
 
377
+ Full documentation of the push API: https://usethrottle.dev/docs/product-catalog/push/ (a first-feed guide, concepts, limits, troubleshooting and a reference for AI agents). The OpenAPI document is https://catalog.usethrottle.dev/v1/catalog/openapi.json.
378
+
260
379
  > **Experimental.** `@usethrottle/catalog/admin` is outside the 1.x freeze: a breaking change to it ships as a minor release, named in the changelog, until it is frozen. Every export carries a TSDoc `@experimental` tag.
261
380
 
262
381
  `createCatalogAdmin` wraps the catalog push API (`/v1/catalog/jobs`, `/variants`, `/runs`, `/products`, `/inventory`). It holds a secret key (`pc_sk_`) or a vendor key (`pc_vk_`), so it throws `CatalogError` `SERVER_ONLY` when it is created in a browser environment, and `INVALID_ADMIN_KEY` for any other key (a publishable `pc_pk_` key gets its own message). The key is never part of an error message. Errors are the same classes as the rest of the package: `CatalogError` with the server's `code`, a `RateLimitedError` (with `retryAfterSeconds`) for every `429`, `TimeoutError` and `NetworkError`.
@@ -291,7 +410,7 @@ if (done.failureCode !== null) console.error(await job.errorsCsv());
291
410
 
292
411
  **Reports.** Job report row numbers are job-wide, not per batch. A job whose error or stock CSV is too large (above 900 parts) answers `409 REPORT_TOO_LARGE`; `stockResults()` pages, so read stock rows that way.
293
412
 
294
- **Images.** Image copying stays on today's cron (about 20 per minute) and today's per-store resize budget (about 20k resizes a month, roughly 6,700 images). For a 120k-product feed most images are therefore served from the vendor's URL for months. The follow-up is a copy queue and a per-store resize cap (about $1.50 per 1,000 images, one-off).
413
+ **Images.** Image copying stays on today's path (about 200–285 copies a minute at peak while a push job applies, measured once on 2026-11-01; leftovers drain on the 5-minute cron at about 20 a minute) and today's per-store resize budget (about 20k resizes a month, roughly 6,700 images). For a 120k-product feed most images are therefore served from the vendor's URL for months. The follow-up is a copy queue and a per-store resize cap (about $1.50 per 1,000 images, one-off).
295
414
 
296
415
  **Webhooks.** `verifyCatalogWebhook` checks a catalog webhook with WebCrypto only. Pass the raw body, the `x-catalog-signature` header and the signing secret; it returns `true` or `false` and never throws. The header is `t=<seconds>,v1=<hex>` with one `t` and 1 to 8 `v1` digests (any match passes, so a secret can rotate), HMAC-SHA256 over `<t>.<rawBody>`, with a tolerance of 300 seconds.
297
416
 
@@ -325,7 +444,7 @@ The OpenAPI 3.1 document of the storefront API is at https://catalog.usethrottle
325
444
 
326
445
  ### One client per currency and language
327
446
 
328
- Hooks take no per-call currency or language: both come from the client. A store with a currency or language switcher keeps one client per `(currency, locale)`, memoised on both, so the client changes only when one of them does. On a currency switch, run `useCart().switchCurrency()` first and swap the client only once it resolves. The switch lands whole or not at all: when it rejects, the cart is still in the old currency, so keep the old client.
447
+ Hooks take no per-call currency or language: both come from the client. A store with a currency or language switcher keeps one client per `(currency, locale)`, memoised on both, so the client changes only when one of them does. On a currency switch, run `useCart().switchCurrency()` first and swap the client only once it resolves. When it rejects, the cart is still in the old currency, so keep the old client; when it resolves, show `cart.warnings` (lines left behind, a dropped discount code).
329
448
 
330
449
  ```tsx
331
450
  'use client';
@@ -383,17 +502,9 @@ A language change needs no cart step: swap the client.
383
502
 
384
503
  ### 1.x follow-ups
385
504
 
386
- 1.0.0 freezes the surface the reference store proved; these are additive and planned for 1.x minors. Until then the reference store's code shows the workaround.
505
+ 1.2.0 shipped `useTags`, `useCollections`, `useReviewEligibility`, `useCustomerKey`, `ReviewEligibility`, `keepPreviousData` with the query echo (`dataFor`), refresh errors with `isStale`, the customer's cart on any device, and the query key (`keyInQuery`) with token-free suggestions. What remains additive for 1.x minors:
387
506
 
388
- - `useTags` and `useCollections` hooks. Today: `client.tags.list()` and `client.collections.list()`, or the server client.
389
- - A `useReviewEligibility(handle)` hook and a `ReviewEligibility` type export. Today: `useCatalog().reviews.eligibility(handle, { signal })`, typed `ReviewEligibilityAnswer`.
390
- - A keep-previous-data option, so a hook shows its last answer while a new key loads.
391
- - `useSuggest` names the query its `data` answers.
392
- - A way to read the provider's `customerKey` from inside it.
393
- - A retry or an error surface when a signed-in customer's revalidation of `initialData` fails. Today the anonymous answer (list prices) stays until the next navigation.
394
- - A signed-in customer's open cart coming back after a sign-out and a sign-in with no guest cart. This needs an authenticated open-cart read for the customer in Throttle too.
395
507
  - A first `add` whose confirming read fails rejects while the line is already in the new cart; that case will settle one consistent way.
396
- - Fewer CORS preflights for suggestions: every query is a new URL carrying the key header, so the browser preflights each keystroke.
397
508
 
398
509
  ## License
399
510
 
package/dist/admin.cjs CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
 
12
12
 
13
- var _chunk4ORXUW2Ucjs = require('./chunk-4ORXUW2U.cjs');
13
+ var _chunk2YQNMBDIcjs = require('./chunk-2YQNMBDI.cjs');
14
14
  require('./chunk-ETV4XYOV.cjs');
15
15
 
16
16
  // src/generated/push-types.ts
@@ -45,7 +45,7 @@ var MAX_IDEMPOTENCY_KEY = 200;
45
45
  var HEADER_SAFE = /^[\x21-\x7e]+$/u;
46
46
  var MAX_RETRY_AFTER_S = 86400;
47
47
  function invalidRequest(message) {
48
- return new (0, _chunk4ORXUW2Ucjs.CatalogError)({
48
+ return new (0, _chunk2YQNMBDIcjs.CatalogError)({
49
49
  code: "INVALID_REQUEST",
50
50
  message,
51
51
  statusCode: 0,
@@ -54,7 +54,7 @@ function invalidRequest(message) {
54
54
  });
55
55
  }
56
56
  function clientError(code, message) {
57
- return new (0, _chunk4ORXUW2Ucjs.CatalogError)({
57
+ return new (0, _chunk2YQNMBDIcjs.CatalogError)({
58
58
  code,
59
59
  message,
60
60
  statusCode: 0,
@@ -126,13 +126,13 @@ function toError(status, headers, text) {
126
126
  requestId
127
127
  };
128
128
  if (status === 429) {
129
- return new (0, _chunk4ORXUW2Ucjs.RateLimitedError)({
129
+ return new (0, _chunk2YQNMBDIcjs.RateLimitedError)({
130
130
  ...init,
131
131
  retryable: true,
132
- retryAfterSeconds: retryAfter !== null && retryAfter >= 1 ? retryAfter : _chunk4ORXUW2Ucjs.RATE_LIMIT_DEFAULT_RETRY_S
132
+ retryAfterSeconds: retryAfter !== null && retryAfter >= 1 ? retryAfter : _chunk2YQNMBDIcjs.RATE_LIMIT_DEFAULT_RETRY_S
133
133
  });
134
134
  }
135
- return new (0, _chunk4ORXUW2Ucjs.CatalogError)(init);
135
+ return new (0, _chunk2YQNMBDIcjs.CatalogError)(init);
136
136
  }
137
137
  function guarded(work, callerSignal, timeoutMs) {
138
138
  if (_optionalChain([callerSignal, 'optionalAccess', _8 => _8.aborted])) return Promise.reject(callerSignal.reason);
@@ -151,7 +151,7 @@ function guarded(work, callerSignal, timeoutMs) {
151
151
  const timer = setTimeout(() => {
152
152
  finish();
153
153
  controller.abort();
154
- reject(new (0, _chunk4ORXUW2Ucjs.TimeoutError)({ timeoutMs }));
154
+ reject(new (0, _chunk2YQNMBDIcjs.TimeoutError)({ timeoutMs }));
155
155
  }, timeoutMs);
156
156
  _optionalChain([callerSignal, 'optionalAccess', _12 => _12.addEventListener, 'call', _13 => _13("abort", onAbort, { once: true })]);
157
157
  work(controller.signal).then(
@@ -194,7 +194,7 @@ function createHttp(key, baseUrl, fetchFn, timeoutMs) {
194
194
  signal
195
195
  });
196
196
  } catch (cause) {
197
- throw new (0, _chunk4ORXUW2Ucjs.NetworkError)({ cause });
197
+ throw new (0, _chunk2YQNMBDIcjs.NetworkError)({ cause });
198
198
  }
199
199
  try {
200
200
  return {
@@ -203,14 +203,14 @@ function createHttp(key, baseUrl, fetchFn, timeoutMs) {
203
203
  text: await response.text()
204
204
  };
205
205
  } catch (cause) {
206
- throw new (0, _chunk4ORXUW2Ucjs.NetworkError)({ cause });
206
+ throw new (0, _chunk2YQNMBDIcjs.NetworkError)({ cause });
207
207
  }
208
208
  },
209
209
  spec.signal,
210
210
  timeoutMs
211
211
  );
212
212
  } catch (error) {
213
- if (error instanceof _chunk4ORXUW2Ucjs.NetworkError && attempt < attempts) continue;
213
+ if (error instanceof _chunk2YQNMBDIcjs.NetworkError && attempt < attempts) continue;
214
214
  throw error;
215
215
  }
216
216
  }
@@ -223,7 +223,7 @@ function createHttp(key, baseUrl, fetchFn, timeoutMs) {
223
223
  throw toError(answer.status, answer.headers, answer.text);
224
224
  const parsed = parseJson(answer.text);
225
225
  if (parsed === void 0)
226
- throw new (0, _chunk4ORXUW2Ucjs.CatalogError)({
226
+ throw new (0, _chunk2YQNMBDIcjs.CatalogError)({
227
227
  code: "INVALID_RESPONSE",
228
228
  message: "The server answered with a body that is not JSON.",
229
229
  statusCode: answer.status,
@@ -278,7 +278,7 @@ function refQuery(ref) {
278
278
  return [[field, value]];
279
279
  }
280
280
  function unreadable() {
281
- return new (0, _chunk4ORXUW2Ucjs.CatalogError)({
281
+ return new (0, _chunk2YQNMBDIcjs.CatalogError)({
282
282
  code: "INVALID_RESPONSE",
283
283
  message: "The server answered with an unexpected shape.",
284
284
  statusCode: 200,
@@ -367,7 +367,7 @@ async function pollUntil(read, done, options = {}) {
367
367
  const value = await read(options.signal);
368
368
  if (done(value)) return value;
369
369
  const remaining = deadline - Date.now();
370
- if (remaining <= 0) throw new (0, _chunk4ORXUW2Ucjs.TimeoutError)({ timeoutMs });
370
+ if (remaining <= 0) throw new (0, _chunk2YQNMBDIcjs.TimeoutError)({ timeoutMs });
371
371
  await sleep(Math.min(intervalMs, remaining), options.signal);
372
372
  }
373
373
  }
@@ -452,7 +452,7 @@ function createJobs(http) {
452
452
  await handle.send(rows, { idempotencyKey });
453
453
  return;
454
454
  } catch (error) {
455
- if (!(error instanceof _chunk4ORXUW2Ucjs.RateLimitedError) || retry >= QUEUE_RETRIES)
455
+ if (!(error instanceof _chunk2YQNMBDIcjs.RateLimitedError) || retry >= QUEUE_RETRIES)
456
456
  throw error;
457
457
  await sleep(error.retryAfterSeconds * 1e3, signal);
458
458
  }
@@ -589,7 +589,7 @@ function assertServer() {
589
589
  function createCatalogAdmin(options) {
590
590
  assertServer();
591
591
  const key = checkKey(options.key);
592
- const baseUrl = (_nullishCoalesce(options.baseUrl, () => ( _chunk4ORXUW2Ucjs.DEFAULT_BASE_URL))).replace(/\/+$/u, "");
592
+ const baseUrl = (_nullishCoalesce(options.baseUrl, () => ( _chunk2YQNMBDIcjs.DEFAULT_BASE_URL))).replace(/\/+$/u, "");
593
593
  if (!/^https?:\/\/[^\s/]+/iu.test(baseUrl))
594
594
  throw invalidRequest("baseUrl must be an absolute http or https URL.");
595
595
  const http = createHttp(
@@ -678,4 +678,4 @@ async function verifyCatalogWebhook(input) {
678
678
 
679
679
 
680
680
 
681
- exports.CartLineError = _chunk4ORXUW2Ucjs.CartLineError; exports.CatalogError = _chunk4ORXUW2Ucjs.CatalogError; exports.CustomerTokenInvalidError = _chunk4ORXUW2Ucjs.CustomerTokenInvalidError; exports.FeatureUnavailableError = _chunk4ORXUW2Ucjs.FeatureUnavailableError; exports.NetworkError = _chunk4ORXUW2Ucjs.NetworkError; exports.OriginNotAllowedError = _chunk4ORXUW2Ucjs.OriginNotAllowedError; exports.RateLimitedError = _chunk4ORXUW2Ucjs.RateLimitedError; exports.TimeoutError = _chunk4ORXUW2Ucjs.TimeoutError; exports.createCatalogAdmin = createCatalogAdmin; exports.isCatalogError = _chunk4ORXUW2Ucjs.isCatalogError; exports.verifyCatalogWebhook = verifyCatalogWebhook;
681
+ exports.CartLineError = _chunk2YQNMBDIcjs.CartLineError; exports.CatalogError = _chunk2YQNMBDIcjs.CatalogError; exports.CustomerTokenInvalidError = _chunk2YQNMBDIcjs.CustomerTokenInvalidError; exports.FeatureUnavailableError = _chunk2YQNMBDIcjs.FeatureUnavailableError; exports.NetworkError = _chunk2YQNMBDIcjs.NetworkError; exports.OriginNotAllowedError = _chunk2YQNMBDIcjs.OriginNotAllowedError; exports.RateLimitedError = _chunk2YQNMBDIcjs.RateLimitedError; exports.TimeoutError = _chunk2YQNMBDIcjs.TimeoutError; exports.createCatalogAdmin = createCatalogAdmin; exports.isCatalogError = _chunk2YQNMBDIcjs.isCatalogError; exports.verifyCatalogWebhook = verifyCatalogWebhook;
package/dist/admin.d.cts CHANGED
@@ -1,5 +1,6 @@
1
- export { C as CartLineError, a as CartLineProblem, b as CatalogError, c as CatalogErrorCode, d as CatalogErrorInit, e as CatalogErrorIssue, f as CatalogErrorSource, g as ClientErrorCode, h as CustomerTokenInvalidError, F as FeatureUnavailableError, N as NetworkError, O as OriginNotAllowedError, R as RateLimitedError, T as TimeoutError, i as isCatalogError } from './catalog-error-s1BINNkt.cjs';
2
- import './contract-types-DhiThMEr.cjs';
1
+ export { C as CartLineError, a as CartLineProblem, b as CatalogError, c as CatalogErrorCode, d as CatalogErrorInit, e as CatalogErrorIssue, f as CatalogErrorSource, g as ClientErrorCode, h as CustomerTokenInvalidError, F as FeatureUnavailableError, N as NetworkError, O as OriginNotAllowedError, R as RateLimitedError, T as TimeoutError, i as isCatalogError } from './catalog-error-BOaSKgY3.cjs';
2
+ import './contract-types-BRyonOYH.cjs';
3
+ import '@usethrottle/money';
3
4
 
4
5
  type JobBatchAnswer = {
5
6
  batchId: string;
@@ -131,6 +132,7 @@ type PushProduct = {
131
132
  base: string;
132
133
  separator: '-' | '_' | '.' | '';
133
134
  } | null | undefined;
135
+ taxCode?: string | null | undefined;
134
136
  externalId: string | null;
135
137
  handle: string;
136
138
  status: 'draft' | 'active' | 'archived';
@@ -243,6 +245,7 @@ type PushRow = {
243
245
  base: string;
244
246
  separator: '-' | '_' | '.' | '';
245
247
  } | null | undefined;
248
+ taxCode?: string | null | undefined;
246
249
  } | undefined;
247
250
  variant?: {
248
251
  title?: string | null | undefined;
@@ -382,6 +385,7 @@ type PushVariantsRequest = {
382
385
  base: string;
383
386
  separator: '-' | '_' | '.' | '';
384
387
  } | null | undefined;
388
+ taxCode?: string | null | undefined;
385
389
  } | undefined;
386
390
  variant?: {
387
391
  title?: string | null | undefined;
package/dist/admin.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- export { C as CartLineError, a as CartLineProblem, b as CatalogError, c as CatalogErrorCode, d as CatalogErrorInit, e as CatalogErrorIssue, f as CatalogErrorSource, g as ClientErrorCode, h as CustomerTokenInvalidError, F as FeatureUnavailableError, N as NetworkError, O as OriginNotAllowedError, R as RateLimitedError, T as TimeoutError, i as isCatalogError } from './catalog-error-C_zLOtYA.js';
2
- import './contract-types-DhiThMEr.js';
1
+ export { C as CartLineError, a as CartLineProblem, b as CatalogError, c as CatalogErrorCode, d as CatalogErrorInit, e as CatalogErrorIssue, f as CatalogErrorSource, g as ClientErrorCode, h as CustomerTokenInvalidError, F as FeatureUnavailableError, N as NetworkError, O as OriginNotAllowedError, R as RateLimitedError, T as TimeoutError, i as isCatalogError } from './catalog-error-CSoPS0-d.js';
2
+ import './contract-types-BRyonOYH.js';
3
+ import '@usethrottle/money';
3
4
 
4
5
  type JobBatchAnswer = {
5
6
  batchId: string;
@@ -131,6 +132,7 @@ type PushProduct = {
131
132
  base: string;
132
133
  separator: '-' | '_' | '.' | '';
133
134
  } | null | undefined;
135
+ taxCode?: string | null | undefined;
134
136
  externalId: string | null;
135
137
  handle: string;
136
138
  status: 'draft' | 'active' | 'archived';
@@ -243,6 +245,7 @@ type PushRow = {
243
245
  base: string;
244
246
  separator: '-' | '_' | '.' | '';
245
247
  } | null | undefined;
248
+ taxCode?: string | null | undefined;
246
249
  } | undefined;
247
250
  variant?: {
248
251
  title?: string | null | undefined;
@@ -382,6 +385,7 @@ type PushVariantsRequest = {
382
385
  base: string;
383
386
  separator: '-' | '_' | '.' | '';
384
387
  } | null | undefined;
388
+ taxCode?: string | null | undefined;
385
389
  } | undefined;
386
390
  variant?: {
387
391
  title?: string | null | undefined;
package/dist/admin.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  RateLimitedError,
11
11
  TimeoutError,
12
12
  isCatalogError
13
- } from "./chunk-JK2UY4CD.js";
13
+ } from "./chunk-3DLH56OX.js";
14
14
  import "./chunk-PKBMQBKP.js";
15
15
 
16
16
  // src/generated/push-types.ts
@@ -1,4 +1,4 @@
1
- import { g as CatalogContractErrorCode } from './contract-types-DhiThMEr.cjs';
1
+ import { i as CatalogContractErrorCode } from './contract-types-BRyonOYH.cjs';
2
2
 
3
3
  /**
4
4
  * Shared error type for every Throttle SDK.
@@ -153,6 +153,16 @@ declare const THROTTLE_CART_CODES: {
153
153
  readonly statuses: readonly [501];
154
154
  readonly retryable: false;
155
155
  };
156
+ /** The absorb (MC2-R39): a customer's cart never moves into a guest's or another customer's. */
157
+ readonly cart_owner_mismatch: {
158
+ readonly statuses: readonly [403];
159
+ readonly retryable: false;
160
+ };
161
+ /** The absorb: the two carts are priced by different apps. */
162
+ readonly cart_resolver_mismatch: {
163
+ readonly statuses: readonly [422];
164
+ readonly retryable: false;
165
+ };
156
166
  };
157
167
  type ThrottleCartCode = keyof typeof THROTTLE_CART_CODES;
158
168
 
@@ -206,7 +216,7 @@ declare class RateLimitedError extends CatalogError {
206
216
  retryAfterSeconds: number;
207
217
  });
208
218
  }
209
- /** `ORIGIN_NOT_ALLOWED`, `SERVER_KEY_IN_BROWSER` and Throttle `origin_not_allowed`. */
219
+ /** `ORIGIN_NOT_ALLOWED`, `SERVER_KEY_IN_BROWSER`, `KEY_NOT_ALLOWED_IN_URL` and Throttle `origin_not_allowed`. */
210
220
  declare class OriginNotAllowedError extends CatalogError {
211
221
  constructor(init: CatalogErrorInit);
212
222
  }
@@ -1,4 +1,4 @@
1
- import { g as CatalogContractErrorCode } from './contract-types-DhiThMEr.js';
1
+ import { i as CatalogContractErrorCode } from './contract-types-BRyonOYH.js';
2
2
 
3
3
  /**
4
4
  * Shared error type for every Throttle SDK.
@@ -153,6 +153,16 @@ declare const THROTTLE_CART_CODES: {
153
153
  readonly statuses: readonly [501];
154
154
  readonly retryable: false;
155
155
  };
156
+ /** The absorb (MC2-R39): a customer's cart never moves into a guest's or another customer's. */
157
+ readonly cart_owner_mismatch: {
158
+ readonly statuses: readonly [403];
159
+ readonly retryable: false;
160
+ };
161
+ /** The absorb: the two carts are priced by different apps. */
162
+ readonly cart_resolver_mismatch: {
163
+ readonly statuses: readonly [422];
164
+ readonly retryable: false;
165
+ };
156
166
  };
157
167
  type ThrottleCartCode = keyof typeof THROTTLE_CART_CODES;
158
168
 
@@ -206,7 +216,7 @@ declare class RateLimitedError extends CatalogError {
206
216
  retryAfterSeconds: number;
207
217
  });
208
218
  }
209
- /** `ORIGIN_NOT_ALLOWED`, `SERVER_KEY_IN_BROWSER` and Throttle `origin_not_allowed`. */
219
+ /** `ORIGIN_NOT_ALLOWED`, `SERVER_KEY_IN_BROWSER`, `KEY_NOT_ALLOWED_IN_URL` and Throttle `origin_not_allowed`. */
210
220
  declare class OriginNotAllowedError extends CatalogError {
211
221
  constructor(init: CatalogErrorInit);
212
222
  }