@usethrottle/catalog 0.1.1 → 1.1.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.
- package/CHANGELOG.md +18 -0
- package/README.md +142 -11
- package/dist/admin.cjs +681 -0
- package/dist/admin.d.cts +660 -0
- package/dist/admin.d.ts +660 -0
- package/dist/admin.js +681 -0
- package/dist/{catalog-error-BWUoZCzY.d.ts → catalog-error-C_zLOtYA.d.ts} +3 -3
- package/dist/{catalog-error-DO6ANrGR.d.cts → catalog-error-s1BINNkt.d.cts} +3 -3
- package/dist/{chunk-CR7ILYCT.cjs → chunk-4ORXUW2U.cjs} +4 -2
- package/dist/{chunk-I3QRBPWD.js → chunk-JK2UY4CD.js} +3 -1
- package/dist/{client-BNtBTEyh.d.cts → client-BAoq6jmM.d.cts} +1 -1
- package/dist/{client-CHLvULRQ.d.ts → client-c2F-eCaL.d.ts} +1 -1
- package/dist/{types-DYfIvIaI.d.cts → contract-types-DhiThMEr.d.cts} +1 -272
- package/dist/{types-DYfIvIaI.d.ts → contract-types-DhiThMEr.d.ts} +1 -272
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/react.cjs +36 -25
- package/dist/react.d.cts +11 -5
- package/dist/react.d.ts +11 -5
- package/dist/react.js +20 -9
- package/dist/server.cjs +14 -13
- package/dist/server.d.cts +3 -2
- package/dist/server.d.ts +3 -2
- package/dist/server.js +3 -2
- package/dist/types-DsjoG8ce.d.cts +276 -0
- package/dist/types-xQLID-4j.d.ts +276 -0
- package/dist/url.d.cts +1 -0
- package/dist/url.d.ts +1 -0
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @usethrottle/catalog
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1234c50: Add `@usethrottle/catalog/admin`, a server-only entry for the catalog push API: `createCatalogAdmin` (sync jobs, single batches, run and job polling, error and stock reports, archive, restore, rename, stock set and adjust) and `verifyCatalogWebhook`, typed from the generated push types. It is experimental: it is outside the 1.x freeze, so a breaking change to it is a minor release.
|
|
8
|
+
|
|
9
|
+
The category `depth` option now takes `1` to `10` (`CategoryTreeDepth`; it was `1 | 2 | 3`) on `categories.tree` and `useCategories`, and `listAllCategoryHandles` now lists every level of the tree. A call without `depth` still sends none.
|
|
10
|
+
|
|
11
|
+
## 1.0.0
|
|
12
|
+
|
|
13
|
+
### Major Changes
|
|
14
|
+
|
|
15
|
+
- 9508bce: 1.0.0: API freeze. No change from 0.1.x beyond the patches listed; from 1.0.0, breaking changes come only in a major version.
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 22949b4: `useCart().switchCurrency` lands whole or not at all: the new cart is read back and saved before it replaces the old one, so when that read or save fails the call rejects and the old cart, its stored id and the old currency stay. Before, a failed read left the new cart's id stored and its currency kept while the call rejected.
|
|
20
|
+
|
|
3
21
|
## 0.1.1
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -36,16 +36,17 @@ Prices are integers in minor units (cents for USD). The client talks to `https:/
|
|
|
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`).
|
|
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).
|
|
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). |
|
|
45
46
|
|
|
46
47
|
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.
|
|
47
48
|
|
|
48
|
-
The client's namespaces: `products` (`list`, `get`, `related`), `filters.values`, `categories` (`tree`, `get`), `tags.list`, `collections` (`list`, `get`), `suggest`, `config`, `reviews` (`list`, `eligibility`, `submit`) and `carts` (`check`, `create`, `session`). `products.get` always asks for the full product; lists default to the lite projection (`fields: 'full'` or a list of keys changes it).
|
|
49
|
+
The client's namespaces: `products` (`list`, `get`, `related`), `filters.values`, `categories` (`tree`, `get`; `tree({ depth })` takes 1 to 10 levels, and a call without `depth` gets the storefront's default of 10), `tags.list`, `collections` (`list`, `get`), `suggest`, `config`, `reviews` (`list`, `eligibility`, `submit`) and `carts` (`check`, `create`, `session`). `products.get` always asks for the full product; lists default to the lite projection (`fields: 'full'` or a list of keys changes it).
|
|
49
50
|
|
|
50
51
|
## Keys
|
|
51
52
|
|
|
@@ -130,7 +131,7 @@ import {
|
|
|
130
131
|
type CartStorage,
|
|
131
132
|
} from '@usethrottle/catalog/react';
|
|
132
133
|
|
|
133
|
-
// One client for the life of the app.
|
|
134
|
+
// One client per currency and language (see Versioning), kept for the life of the app.
|
|
134
135
|
const client = createCatalogClient({
|
|
135
136
|
publishableKey: process.env.NEXT_PUBLIC_CATALOG_KEY ?? '',
|
|
136
137
|
customerToken: () => auth.getAccessToken(),
|
|
@@ -187,8 +188,8 @@ function ProductGrid() {
|
|
|
187
188
|
|
|
188
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.
|
|
189
190
|
- **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.
|
|
190
|
-
- **`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`
|
|
191
|
-
- **`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`. 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).
|
|
191
|
+
- **`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).
|
|
192
193
|
- **Rate limits:** a catalog `429` pauses every read of the provider until its retry time; each mounted key then refetches once.
|
|
193
194
|
- **`useSubmitReview(handle)`** submits a review with one idempotency key per submission.
|
|
194
195
|
|
|
@@ -254,16 +255,146 @@ history.pushState(null, '', `?${next}`);
|
|
|
254
255
|
|
|
255
256
|
Ranges stay strings (`10..50`); `parseRange` and `formatRange` convert them.
|
|
256
257
|
|
|
258
|
+
## Admin (server only, experimental)
|
|
259
|
+
|
|
260
|
+
> **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
|
+
|
|
262
|
+
`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`.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { createCatalogAdmin } from '@usethrottle/catalog/admin';
|
|
266
|
+
|
|
267
|
+
const admin = createCatalogAdmin({ key: process.env.CATALOG_ADMIN_KEY ?? '' });
|
|
268
|
+
|
|
269
|
+
// A whole feed as one all-or-nothing job: batches of up to 1,000 rows and 5 MB.
|
|
270
|
+
const job = await admin.jobs.run(rows, { mode: 'replace', fullSync: true });
|
|
271
|
+
const done = await job.wait(); // resolves when the job has finished; read done.failureCode
|
|
272
|
+
if (done.failureCode !== null) console.error(await job.errorsCsv());
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`jobs.run` takes any iterable or async iterable of rows, opens the job with the first batch, sends batches of at most `batchRows` (default and maximum 1,000) rows that also stay under 5 MB, closes the job and returns the handle. The rows of one product must be next to each other: they are never split across batches (so `replace` stays safe), and a product with more than 1,000 rows throws `INVALID_REQUEST`. A `429` (`PUSH_QUEUE_FULL`) waits `retryAfterSeconds`, then sends the same batch again with the same key, at most 3 times; after that the `RateLimitedError` is thrown. A job that was opened and then failed client-side is left open and expires after 24 hours, and its staged batches are discarded, never applied.
|
|
276
|
+
|
|
277
|
+
| Method | What it does |
|
|
278
|
+
| --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
279
|
+
| `jobs.open({ mode?, fullSync? })`, `jobs.get(id)`, `jobs.resume(id)` | A job handle: `send(rows, { idempotencyKey? })`, `close()`, `get({ includeBatches? })`, `wait({ intervalMs, timeoutMs, signal })`, `errorsCsv()`, `stockResults()`, `stockCsv()`. `resume` sends no request. |
|
|
280
|
+
| `jobs.run(rows, { mode?, fullSync?, batchRows? })` | Open, send, close, as above. |
|
|
281
|
+
| `batches.send(rows, { mode?, idempotencyKey? })` | One batch to `/v1/catalog/variants`, answered with a run. `mode` needs a vendor key; with a store key it throws `INVALID_REQUEST` (use `jobs.run`). |
|
|
282
|
+
| `runs.get(id)`, `runs.wait(id, options)`, `runs.errorsCsv(id)`, `runs.stockResults(id)`, `runs.stockCsv(id)` | Follow a run. `wait` resolves on `completed` or `failed`; a vendor run answer has `pushMode` beside `mode`. |
|
|
283
|
+
| `products.get(ref)`, `products.list({ cursor?, limit? })`, `products.archive(refs)`, `products.restore(refs)`, `products.rename(ref, handle)` | A product ref is exactly one of `{ externalId }`, `{ sku }` or `{ handle }`. |
|
|
284
|
+
| `inventory.set(rows)`, `inventory.adjust(rows, { idempotencyKey? })` | `PATCH /v1/catalog/inventory`. `adjust` sends every row with `quantityMode: 'adjust'`. |
|
|
285
|
+
|
|
286
|
+
**Stock results.** A push row that wrote a quantity records the result: `stockResults()` pages `…/stock` and yields `{ sequence, rowNumber, sku, externalId, quantity, sourceWins }` for each; `stockCsv()` returns the same as CSV.
|
|
287
|
+
|
|
288
|
+
**Idempotency.** A batch or an inventory request with an `adjust` row needs an `Idempotency-Key`, or the server answers `400 IDEMPOTENCY_KEY_REQUIRED`. The SDK generates a UUID for every job batch, vendor batch and `adjust` call that has none, and one retry after a network error reuses it, so a delta is never applied twice. A key you pass is 1 to 200 characters.
|
|
289
|
+
|
|
290
|
+
**Vendor batches.** A vendor key's `batches.send` takes canonical rows. A mapping profile cannot set `externalId`, so a vendor batch that needs `externalId` must not use one.
|
|
291
|
+
|
|
292
|
+
**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
|
+
|
|
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).
|
|
295
|
+
|
|
296
|
+
**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
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import {
|
|
300
|
+
verifyCatalogWebhook,
|
|
301
|
+
type PushWebhookEvent,
|
|
302
|
+
} from '@usethrottle/catalog/admin';
|
|
303
|
+
|
|
304
|
+
const ok = await verifyCatalogWebhook({
|
|
305
|
+
rawBody, // the body text exactly as received
|
|
306
|
+
signature: request.headers.get('x-catalog-signature') ?? '',
|
|
307
|
+
secret: process.env.CATALOG_WEBHOOK_SECRET ?? '',
|
|
308
|
+
});
|
|
309
|
+
if (ok) handle(JSON.parse(rawBody) as PushWebhookEvent);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Failed deliveries retry on a schedule whose entries are gaps after the previous attempt, about 44.6 hours in all.
|
|
313
|
+
|
|
257
314
|
## Module formats
|
|
258
315
|
|
|
259
316
|
The package ships ESM and CommonJS builds. Within one format, every entry shares one copy of the client and the error classes, so `instanceof` works across `/react`, `/server` and the root. An app that loads the package through both `import` and `require` gets two copies: `instanceof RateLimitedError` (or any other class) is then true only for errors from the same format. `isCatalogError` works across both, because it also checks a shared brand; use it, then branch on `error.code`.
|
|
260
317
|
|
|
261
318
|
## Versioning
|
|
262
319
|
|
|
263
|
-
|
|
320
|
+
From 1.0.0 the package follows semantic versioning: a breaking change to an exported name, a type, an error class or code, or a hook's behaviour comes only in a major version. New answer keys, new error codes in `(string & {})` positions and new optional options are minor. Deprecated names keep working for at least one major. The reference store at https://throttle-demo-store.vercel.app runs this version.
|
|
321
|
+
|
|
322
|
+
The SDK targets the current catalog. Against an older one, everything works except the route-miss marker behind `FeatureUnavailableError`, the OpenAPI document and the cart return URLs (an older catalog refuses them with `400 INVALID_REQUEST`).
|
|
264
323
|
|
|
265
324
|
The OpenAPI 3.1 document of the storefront API is at https://catalog.usethrottle.dev/v1/storefront/openapi.json.
|
|
266
325
|
|
|
326
|
+
### One client per currency and language
|
|
327
|
+
|
|
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.
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
'use client';
|
|
332
|
+
import { useMemo, useState, type ReactNode } from 'react';
|
|
333
|
+
import { createCatalogClient } from '@usethrottle/catalog';
|
|
334
|
+
import { CatalogProvider, useCart } from '@usethrottle/catalog/react';
|
|
335
|
+
|
|
336
|
+
export function Shop({ children }: { children: ReactNode }) {
|
|
337
|
+
const [prefs, setPrefs] = useState({ currency: 'USD', locale: 'en' });
|
|
338
|
+
const client = useMemo(
|
|
339
|
+
() =>
|
|
340
|
+
createCatalogClient({
|
|
341
|
+
publishableKey: process.env.NEXT_PUBLIC_CATALOG_KEY ?? '',
|
|
342
|
+
currency: prefs.currency,
|
|
343
|
+
locale: prefs.locale,
|
|
344
|
+
customerToken: () => auth.getAccessToken(),
|
|
345
|
+
}),
|
|
346
|
+
[prefs.currency, prefs.locale],
|
|
347
|
+
);
|
|
348
|
+
// Pass customerKey, cartStorage and cartUrls as in the React example.
|
|
349
|
+
return (
|
|
350
|
+
<CatalogProvider client={client}>
|
|
351
|
+
<CurrencyPicker
|
|
352
|
+
currency={prefs.currency}
|
|
353
|
+
onSwitched={(currency) => setPrefs((p) => ({ ...p, currency }))}
|
|
354
|
+
/>
|
|
355
|
+
{children}
|
|
356
|
+
</CatalogProvider>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function CurrencyPicker(props: {
|
|
361
|
+
currency: string;
|
|
362
|
+
onSwitched: (currency: string) => void;
|
|
363
|
+
}) {
|
|
364
|
+
const cart = useCart();
|
|
365
|
+
const choose = async (currency: string) => {
|
|
366
|
+
await cart.switchCurrency(currency); // rejects and keeps the old cart on failure
|
|
367
|
+
props.onSwitched(currency); // only now swap the client
|
|
368
|
+
};
|
|
369
|
+
return (
|
|
370
|
+
<select
|
|
371
|
+
value={props.currency}
|
|
372
|
+
disabled={cart.pending}
|
|
373
|
+
onChange={(e) => void choose(e.target.value).catch(() => undefined)}
|
|
374
|
+
>
|
|
375
|
+
<option>USD</option>
|
|
376
|
+
<option>EUR</option>
|
|
377
|
+
</select>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
A language change needs no cart step: swap the client.
|
|
383
|
+
|
|
384
|
+
### 1.x follow-ups
|
|
385
|
+
|
|
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.
|
|
387
|
+
|
|
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
|
+
- 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
|
+
|
|
267
398
|
## License
|
|
268
399
|
|
|
269
400
|
MIT
|