@usethrottle/catalog 1.2.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 (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +7 -7
  3. package/dist/admin.cjs +16 -16
  4. package/dist/admin.d.cts +3 -2
  5. package/dist/admin.d.ts +3 -2
  6. package/dist/admin.js +1 -1
  7. package/dist/{catalog-error-yTBMAdCv.d.cts → catalog-error-BOaSKgY3.d.cts} +11 -1
  8. package/dist/{catalog-error-7DCsTyB2.d.ts → catalog-error-CSoPS0-d.d.ts} +11 -1
  9. package/dist/{chunk-B2DYAHNR.cjs → chunk-2YQNMBDI.cjs} +78 -23
  10. package/dist/{chunk-55G4ZTD5.js → chunk-3DLH56OX.js} +58 -3
  11. package/dist/{chunk-JNBKKAMP.js → chunk-CADIHOIR.js} +9 -1
  12. package/dist/{chunk-TDMDOHD3.cjs → chunk-GU63WFXI.cjs} +16 -8
  13. package/dist/{client-BrHJCyBP.d.cts → client-6j1Iz-iJ.d.cts} +1 -1
  14. package/dist/{client-Dm6I4nAZ.d.ts → client-B3mL2-xJ.d.ts} +1 -1
  15. package/dist/{contract-types-BPSzY_Co.d.ts → contract-types-BRyonOYH.d.cts} +11 -9
  16. package/dist/{contract-types-BPSzY_Co.d.cts → contract-types-BRyonOYH.d.ts} +11 -9
  17. package/dist/index.cjs +24 -2
  18. package/dist/index.d.cts +8 -5
  19. package/dist/index.d.ts +8 -5
  20. package/dist/index.js +25 -3
  21. package/dist/react-extras.cjs +8 -8
  22. package/dist/react-extras.d.cts +4 -3
  23. package/dist/react-extras.d.ts +4 -3
  24. package/dist/react-extras.js +2 -2
  25. package/dist/react.cjs +126 -54
  26. package/dist/react.d.cts +34 -10
  27. package/dist/react.d.ts +34 -10
  28. package/dist/react.js +108 -36
  29. package/dist/server.cjs +13 -13
  30. package/dist/server.d.cts +2 -1
  31. package/dist/server.d.ts +2 -1
  32. package/dist/server.js +1 -1
  33. package/dist/{types-BTrqSprj.d.cts → types-BfveENg1.d.ts} +43 -7
  34. package/dist/{types-BlYCKzB8.d.ts → types-D0PywKnN.d.cts} +43 -7
  35. package/dist/url.d.cts +2 -1
  36. package/dist/url.d.ts +2 -1
  37. package/dist/{use-query-QOEBuYlp.d.ts → use-query-CR7r5tam.d.ts} +1 -1
  38. package/dist/{use-query-BZgjxGPO.d.cts → use-query-UWf6Pi5g.d.cts} +1 -1
  39. package/package.json +3 -2
package/dist/react.js CHANGED
@@ -15,14 +15,15 @@ import {
15
15
  useRelated,
16
16
  useReviews,
17
17
  useSuggest
18
- } from "./chunk-JNBKKAMP.js";
18
+ } from "./chunk-CADIHOIR.js";
19
19
  import {
20
20
  CatalogError,
21
21
  RateLimitedError,
22
22
  SDK_VERSION,
23
23
  THROTTLE_CHECKOUT_URL,
24
+ formatMoney,
24
25
  isCatalogError
25
- } from "./chunk-55G4ZTD5.js";
26
+ } from "./chunk-3DLH56OX.js";
26
27
  import "./chunk-PKBMQBKP.js";
27
28
 
28
29
  // src/react/use-cart.ts
@@ -99,7 +100,8 @@ var EMPTY = {
99
100
  cartUrl: null,
100
101
  status: "idle",
101
102
  error: void 0,
102
- pending: false
103
+ pending: false,
104
+ warnings: []
103
105
  };
104
106
  var ENDED = /* @__PURE__ */ new Set([
105
107
  "cart_session_expired",
@@ -132,6 +134,7 @@ function createController(home) {
132
134
  moving: void 0,
133
135
  unremoved: false,
134
136
  reads: /* @__PURE__ */ new Map(),
137
+ slots: /* @__PURE__ */ new Set(),
135
138
  abort: new AbortController(),
136
139
  writing: Promise.resolve(),
137
140
  tail: Promise.resolve()
@@ -168,7 +171,7 @@ function createController(home) {
168
171
  if (signal.aborted) throw error;
169
172
  }
170
173
  }
171
- async function find(signal, wantedCurrency) {
174
+ async function find(signal, wantedCurrency, strict = false) {
172
175
  const source = ctx().customerCart;
173
176
  if (source === void 0 || wantedCurrency === void 0) return null;
174
177
  const asked = wantedCurrency.toUpperCase();
@@ -176,9 +179,27 @@ function createController(home) {
176
179
  const found = await live(signal, source.getOpenCart({ currency: asked }));
177
180
  return found !== null && found.currency.toUpperCase() === asked ? found : null;
178
181
  } catch (error) {
179
- if (signal.aborted) throw error;
182
+ if (signal.aborted || strict) throw error;
183
+ return null;
184
+ }
185
+ }
186
+ async function findTarget(signal, code, storage) {
187
+ if (resuming()) {
188
+ const found = await find(signal, code, true);
189
+ return found === null ? null : { id: found.sessionId, cartUrl: fallbackUrl(found.sessionId) };
190
+ }
191
+ if (ctx().customerKey !== null || storage.getFor === void 0) return null;
192
+ const id = await live(signal, storage.getFor(code));
193
+ if (id === null || id === "") return null;
194
+ try {
195
+ const answer = await live(signal, session(id).get({ signal }));
196
+ if (answer.cart.status !== "open" || answer.cart.currency.toUpperCase() !== code)
197
+ return null;
198
+ } catch (error) {
199
+ if (signal.aborted || !ended(error)) throw error;
180
200
  return null;
181
201
  }
202
+ return { id, cartUrl: fallbackUrl(id) };
182
203
  }
183
204
  async function findOnce(signal, storage) {
184
205
  var _a2;
@@ -197,13 +218,21 @@ function createController(home) {
197
218
  }
198
219
  if (found !== null) await soft(signal, () => adopt(signal, found, storage));
199
220
  }
221
+ async function forgetSlots(storage, codes) {
222
+ for (const code of codes)
223
+ if (storage.removeFor !== void 0) await storage.removeFor(code);
224
+ else await storage.setFor?.(code, "");
225
+ }
226
+ async function save(signal, storage, id) {
227
+ const write = storage.set(id);
228
+ me.writing = Promise.resolve(write).catch(() => void 0);
229
+ await live(signal, write);
230
+ }
200
231
  async function adopt(signal, found, storage) {
201
232
  const id = found.sessionId;
202
233
  const answer = await live(signal, session(id).get({ signal }));
203
234
  me.reads.set(id, Date.now());
204
- const write = storage.set(id);
205
- me.writing = Promise.resolve(write).catch(() => void 0);
206
- await live(signal, write);
235
+ await save(signal, storage, id);
207
236
  box.set({
208
237
  sessionId: id,
209
238
  cartUrl: fallbackUrl(id),
@@ -229,7 +258,7 @@ function createController(home) {
229
258
  if (converted(error)) reset();
230
259
  else box.set({ sessionId: null, cartUrl: null });
231
260
  }
232
- async function create(signal, options, items, whole = false) {
261
+ async function start(signal, options, items) {
233
262
  const context = ctx();
234
263
  const body = {
235
264
  items: [...items].map(([sku, quantity]) => ({ sku, quantity }))
@@ -253,28 +282,18 @@ function createController(home) {
253
282
  throw error;
254
283
  }
255
284
  const id = answer.sessionId;
256
- const cartUrl = isHttpUrl(answer.cartUrl) ? answer.cartUrl : fallbackUrl(id);
257
- if (whole) {
258
- const snapshot = await live(signal, session(id).get({ signal }));
259
- me.reads.set(id, Date.now());
260
- const write = context.cartStorage.set(id);
261
- me.writing = Promise.resolve(write).catch(() => void 0);
262
- await live(signal, write);
263
- box.set({
264
- sessionId: id,
265
- cartUrl,
266
- cart: snapshot.cart,
267
- status: "ready",
268
- error: void 0
269
- });
270
- return;
271
- }
285
+ return {
286
+ id,
287
+ cartUrl: isHttpUrl(answer.cartUrl) ? answer.cartUrl : fallbackUrl(id)
288
+ };
289
+ }
290
+ async function create(signal, options, items) {
291
+ const storage = ctx().cartStorage;
292
+ const { id, cartUrl } = await start(signal, options, items);
272
293
  box.set({ sessionId: id, cartUrl });
273
294
  let unsaved;
274
295
  try {
275
- const write = context.cartStorage.set(id);
276
- me.writing = Promise.resolve(write).catch(() => void 0);
277
- await live(signal, write);
296
+ await save(signal, storage, id);
278
297
  } catch (error) {
279
298
  if (signal.aborted) throw error;
280
299
  unsaved = error;
@@ -432,6 +451,11 @@ function createController(home) {
432
451
  if (signal.aborted) return;
433
452
  if (fresh && !me.unremoved) await restore(signal, guest, false);
434
453
  if (signal.aborted) return;
454
+ const codes = new Set(me.slots);
455
+ const shown = box.get().cart?.currency;
456
+ if (shown !== void 0) codes.add(shown.toUpperCase());
457
+ await live(signal, forgetSlots(guest, codes));
458
+ me.slots.clear();
435
459
  const id = box.get().sessionId;
436
460
  if (id === null) {
437
461
  if (box.get().cart === null && !me.unremoved) {
@@ -480,9 +504,15 @@ function createController(home) {
480
504
  const outgoing = prior ?? me.seen;
481
505
  const left = me.moving;
482
506
  me.moving = void 0;
507
+ const slots = [...me.slots];
508
+ me.slots.clear();
483
509
  const remove = async () => {
484
510
  await outgoing.remove();
485
- if (left !== void 0 && left !== outgoing) await left.remove();
511
+ await forgetSlots(outgoing, slots);
512
+ if (left !== void 0 && left !== outgoing) {
513
+ await left.remove();
514
+ await forgetSlots(left, slots);
515
+ }
486
516
  };
487
517
  me.tail = me.writing.then(remove).catch(remove).then(
488
518
  () => {
@@ -508,6 +538,7 @@ function createController(home) {
508
538
  mutate,
509
539
  create,
510
540
  session,
541
+ wanted,
511
542
  async switchCurrency(signal, options, next) {
512
543
  checkPause(ctx().store);
513
544
  const storage = ctx().cartStorage;
@@ -524,21 +555,60 @@ function createController(home) {
524
555
  }
525
556
  const before = currency;
526
557
  currency = next;
527
- const items = itemsOf(box.get().cart);
528
- if (items.size === 0) {
529
- if (box.get().sessionId !== null) await live(signal, storage.remove());
530
- reset();
558
+ const code = next.toUpperCase();
559
+ const from = box.get().sessionId;
560
+ const cart = box.get().cart;
561
+ const items = itemsOf(cart);
562
+ if (from === null || items.size === 0) {
563
+ if (from !== null) await live(signal, storage.remove());
564
+ if (items.size === 0) reset();
531
565
  return;
532
566
  }
567
+ if (cart?.currency.toUpperCase() === code) return;
568
+ let target;
569
+ let moved;
533
570
  try {
534
- await create(signal, options, items, true);
571
+ target = await findTarget(signal, code, storage) ?? await start(signal, options, /* @__PURE__ */ new Map());
572
+ moved = await live(signal, session(target.id).absorb(from, { signal }));
535
573
  } catch (error) {
536
574
  if (!signal.aborted) currency = before;
537
575
  throw error;
538
576
  }
577
+ me.reads.set(target.id, Date.now());
578
+ box.set({
579
+ sessionId: target.id,
580
+ cartUrl: target.cartUrl,
581
+ cart: moved.cart,
582
+ status: "ready",
583
+ error: void 0,
584
+ warnings: moved.warnings.filter(
585
+ (warning) => warning.code !== "lines_stale"
586
+ )
587
+ });
588
+ let unsaved;
589
+ try {
590
+ await save(signal, storage, target.id);
591
+ } catch (error) {
592
+ if (signal.aborted) throw error;
593
+ unsaved = error;
594
+ try {
595
+ await live(signal, storage.remove());
596
+ } catch (failure) {
597
+ if (signal.aborted) throw failure;
598
+ }
599
+ }
600
+ if (ctx().customerKey === null && storage.setFor !== void 0) {
601
+ me.slots.add(code);
602
+ await live(signal, storage.setFor(code, target.id));
603
+ }
604
+ if (unsaved !== void 0) throw unsaved;
539
605
  },
540
606
  async clear(signal) {
541
- await live(signal, ctx().cartStorage.remove());
607
+ const storage = ctx().cartStorage;
608
+ const shown = box.get().cart?.currency;
609
+ await live(signal, storage.remove());
610
+ if (shown !== void 0)
611
+ await live(signal, storage.removeFor?.(shown.toUpperCase()));
542
612
  currency = void 0;
543
613
  reset();
544
614
  }
@@ -656,7 +726,9 @@ function useCart(options = {}) {
656
726
  }, [controller, home, currency, locale]);
657
727
  const owner = controller.owner();
658
728
  const view = owner !== void 0 && owner !== null && owner !== customerKey ? { ...EMPTY, pending: state.pending } : state;
659
- return { ...view, ...actions };
729
+ const shownCurrency = view.cart?.currency ?? controller.wanted(options) ?? "";
730
+ const format = (minor) => formatMoney(minor, shownCurrency);
731
+ return { ...view, ...actions, format };
660
732
  }
661
733
 
662
734
  // src/react/use-submit-review.ts
package/dist/server.cjs CHANGED
@@ -9,14 +9,14 @@
9
9
 
10
10
 
11
11
 
12
- var _chunkB2DYAHNRcjs = require('./chunk-B2DYAHNR.cjs');
12
+ var _chunk2YQNMBDIcjs = require('./chunk-2YQNMBDI.cjs');
13
13
  require('./chunk-ETV4XYOV.cjs');
14
14
 
15
15
  // src/server/tags.ts
16
16
  function catalogTags(route, handle) {
17
- const tags = [_chunkB2DYAHNRcjs.CACHE_TAG, `${_chunkB2DYAHNRcjs.CACHE_TAG}:${route}`];
18
- if (handle !== void 0) tags.push(`${_chunkB2DYAHNRcjs.CACHE_TAG}:${route}:${handle}`);
19
- return tags.filter((tag) => tag.length <= _chunkB2DYAHNRcjs.TAG_MAX_CHARS);
17
+ const tags = [_chunk2YQNMBDIcjs.CACHE_TAG, `${_chunk2YQNMBDIcjs.CACHE_TAG}:${route}`];
18
+ if (handle !== void 0) tags.push(`${_chunk2YQNMBDIcjs.CACHE_TAG}:${route}:${handle}`);
19
+ return tags.filter((tag) => tag.length <= _chunk2YQNMBDIcjs.TAG_MAX_CHARS);
20
20
  }
21
21
  function decode(segment) {
22
22
  try {
@@ -78,9 +78,9 @@ function createServerCatalogClient(options) {
78
78
  keyInQuery: _ignored,
79
79
  ...rest
80
80
  } = options;
81
- const revalidate = _nullishCoalesce(_optionalChain([next, 'optionalAccess', _ => _.revalidate]), () => ( _chunkB2DYAHNRcjs.DEFAULT_REVALIDATE_S));
81
+ const revalidate = _nullishCoalesce(_optionalChain([next, 'optionalAccess', _ => _.revalidate]), () => ( _chunk2YQNMBDIcjs.DEFAULT_REVALIDATE_S));
82
82
  const extraTags = _nullishCoalesce(_optionalChain([next, 'optionalAccess', _2 => _2.tags]), () => ( []));
83
- const storefront = `${(_nullishCoalesce(rest.baseUrl, () => ( _chunkB2DYAHNRcjs.DEFAULT_BASE_URL))).replace(/\/+$/u, "")}/v1/storefront/`;
83
+ const storefront = `${(_nullishCoalesce(rest.baseUrl, () => ( _chunk2YQNMBDIcjs.DEFAULT_BASE_URL))).replace(/\/+$/u, "")}/v1/storefront/`;
84
84
  const base = rest.fetch;
85
85
  const serverFetch = (input, init = {}) => {
86
86
  const out = serverInit(
@@ -92,7 +92,7 @@ function createServerCatalogClient(options) {
92
92
  );
93
93
  return base !== void 0 ? base(input, out) : globalThis.fetch(input, out);
94
94
  };
95
- return _chunkB2DYAHNRcjs.createCatalogClient.call(void 0, {
95
+ return _chunk2YQNMBDIcjs.createCatalogClient.call(void 0, {
96
96
  ...rest,
97
97
  fetch: serverFetch
98
98
  });
@@ -112,16 +112,16 @@ async function syncCatalogGeneration(input) {
112
112
 
113
113
  // src/server/paging.ts
114
114
  function bounds(opts) {
115
- const pageSize = _nullishCoalesce(opts.pageSize, () => ( _chunkB2DYAHNRcjs.LIST_PAGE_SIZE));
116
- const max = _nullishCoalesce(opts.max, () => ( _chunkB2DYAHNRcjs.LIST_MAX_HANDLES));
117
- if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > _chunkB2DYAHNRcjs.LIST_PAGE_SIZE)
115
+ const pageSize = _nullishCoalesce(opts.pageSize, () => ( _chunk2YQNMBDIcjs.LIST_PAGE_SIZE));
116
+ const max = _nullishCoalesce(opts.max, () => ( _chunk2YQNMBDIcjs.LIST_MAX_HANDLES));
117
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > _chunk2YQNMBDIcjs.LIST_PAGE_SIZE)
118
118
  throw invalid("pageSize must be a whole number from 1 to 100.");
119
119
  if (!Number.isInteger(max) || max < 0)
120
120
  throw invalid("max must be a whole number of 0 or more.");
121
121
  return { pageSize, max };
122
122
  }
123
123
  function invalid(message) {
124
- return new (0, _chunkB2DYAHNRcjs.CatalogError)({
124
+ return new (0, _chunk2YQNMBDIcjs.CatalogError)({
125
125
  code: "INVALID_REQUEST",
126
126
  message,
127
127
  statusCode: 0,
@@ -191,7 +191,7 @@ function listAllCollectionHandles(client, opts = {}) {
191
191
  }
192
192
  async function listAllCategoryHandles(client, opts = {}) {
193
193
  const { categories } = await client.categories.tree({
194
- depth: _chunkB2DYAHNRcjs.CATEGORY_TREE_MAX_DEPTH,
194
+ depth: _chunk2YQNMBDIcjs.CATEGORY_TREE_MAX_DEPTH,
195
195
  ...call(opts.signal)
196
196
  });
197
197
  const out = [];
@@ -213,4 +213,4 @@ async function listAllCategoryHandles(client, opts = {}) {
213
213
 
214
214
 
215
215
 
216
- exports.SDK_VERSION = _chunkB2DYAHNRcjs.SDK_VERSION; exports.catalogTags = catalogTags; exports.createServerCatalogClient = createServerCatalogClient; exports.listAllCategoryHandles = listAllCategoryHandles; exports.listAllCollectionHandles = listAllCollectionHandles; exports.listAllProductHandles = listAllProductHandles; exports.staticProductParams = staticProductParams; exports.syncCatalogGeneration = syncCatalogGeneration;
216
+ exports.SDK_VERSION = _chunk2YQNMBDIcjs.SDK_VERSION; exports.catalogTags = catalogTags; exports.createServerCatalogClient = createServerCatalogClient; exports.listAllCategoryHandles = listAllCategoryHandles; exports.listAllCollectionHandles = listAllCollectionHandles; exports.listAllProductHandles = listAllProductHandles; exports.staticProductParams = staticProductParams; exports.syncCatalogGeneration = syncCatalogGeneration;
package/dist/server.d.cts CHANGED
@@ -1,7 +1,8 @@
1
1
  export { S as SDK_VERSION } from './version-ev3p03v2.cjs';
2
2
  import { CatalogClient } from './index.cjs';
3
3
  import { CatalogClientOptions, NextFetchOptions } from './index.cjs';
4
- import './contract-types-BPSzY_Co.cjs';
4
+ import '@usethrottle/money';
5
+ import './contract-types-BRyonOYH.cjs';
5
6
 
6
7
  type ServerCatalogClientOptions = Omit<CatalogClientOptions, 'keyInQuery'> & {
7
8
  /** `revalidate` defaults to 60 seconds; `tags` are appended to the route's own. */
package/dist/server.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export { S as SDK_VERSION } from './version-ev3p03v2.js';
2
2
  import { CatalogClient } from './index.js';
3
3
  import { CatalogClientOptions, NextFetchOptions } from './index.js';
4
- import './contract-types-BPSzY_Co.js';
4
+ import '@usethrottle/money';
5
+ import './contract-types-BRyonOYH.js';
5
6
 
6
7
  type ServerCatalogClientOptions = Omit<CatalogClientOptions, 'keyInQuery'> & {
7
8
  /** `revalidate` defaults to 60 seconds; `tags` are appended to the route's own. */
package/dist/server.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  SDK_VERSION,
10
10
  TAG_MAX_CHARS,
11
11
  createCatalogClient
12
- } from "./chunk-55G4ZTD5.js";
12
+ } from "./chunk-3DLH56OX.js";
13
13
  import "./chunk-PKBMQBKP.js";
14
14
 
15
15
  // src/server/tags.ts
@@ -1,4 +1,5 @@
1
- import { C as CartCheckAnswer, h as CartCreateRequest, g as CartCreateAnswer, b as StorefrontListAnswer, e as StorefrontProductAnswer, p as StorefrontRelatedAnswer, o as StorefrontFilterValuesAnswer, j as StorefrontCategoriesAnswer, k as StorefrontCategoryAnswer, s as StorefrontTagsAnswer, m as StorefrontCollectionsAnswer, l as StorefrontCollectionAnswer, r as StorefrontSuggestAnswer, n as StorefrontConfigAnswer, q as StorefrontReviewsAnswer, d as ReviewEligibilityAnswer, a as ReviewSubmitInput, R as ReviewSubmitAnswer } from './contract-types-BPSzY_Co.cjs';
1
+ import { CurrencyCode } from '@usethrottle/money';
2
+ import { C as CartCheckAnswer, h as CartCreateRequest, g as CartCreateAnswer, b as StorefrontListAnswer, e as StorefrontProductAnswer, p as StorefrontRelatedAnswer, o as StorefrontFilterValuesAnswer, j as StorefrontCategoriesAnswer, k as StorefrontCategoryAnswer, s as StorefrontTagsAnswer, m as StorefrontCollectionsAnswer, l as StorefrontCollectionAnswer, r as StorefrontSuggestAnswer, n as StorefrontConfigAnswer, q as StorefrontReviewsAnswer, d as ReviewEligibilityAnswer, a as ReviewSubmitInput, R as ReviewSubmitAnswer } from './contract-types-BRyonOYH.js';
2
3
 
3
4
  type QueryPairs = [string, string][];
4
5
 
@@ -141,7 +142,7 @@ type ThrottleLine = {
141
142
  /** Throttle's cart as a cart session answers it; other keys pass through. */
142
143
  type ThrottleCart = {
143
144
  cartId: string;
144
- currency: string;
145
+ currency: CurrencyCode;
145
146
  subtotal: number;
146
147
  total: number;
147
148
  unitCount: number;
@@ -198,6 +199,32 @@ type CartCheckout = {
198
199
  };
199
200
  /** Per-call options of a cart-session call. */
200
201
  type SessionCallOptions = Pick<CallOptions, 'signal' | 'timeoutMs'>;
202
+ /**
203
+ * What an absorb reports besides the cart (MC2-R39): lines of the other cart
204
+ * that could not move (no price in this cart's currency, or a custom line in
205
+ * another currency), a discount code this cart's currency refuses, and lines
206
+ * whose prices are unconfirmed until the next refresh or checkout.
207
+ */
208
+ type CartWarning = {
209
+ code: 'lines_removed';
210
+ lines: {
211
+ referenceId: string | null;
212
+ code: string;
213
+ }[];
214
+ message: string;
215
+ } | {
216
+ code: 'discount_dropped';
217
+ discountCode: string;
218
+ message: string;
219
+ } | {
220
+ code: 'lines_stale';
221
+ message: string;
222
+ };
223
+ /** `CartSession.absorb()`'s answer: this session's cart after the move. */
224
+ type CartAbsorb = {
225
+ cart: ThrottleCart;
226
+ warnings: CartWarning[];
227
+ };
201
228
  /** One Throttle cart session, addressed by its opaque id. */
202
229
  type CartSession = {
203
230
  readonly id: string;
@@ -212,6 +239,15 @@ type CartSession = {
212
239
  refresh(call?: SessionCallOptions): Promise<CartSessionSnapshot>;
213
240
  /** Refuses non-http(s) URLs before a request, and never returns a non-http(s) `checkoutUrl`. */
214
241
  checkout(input: CartCheckoutInput, call?: SessionCallOptions): Promise<CartCheckout>;
242
+ /**
243
+ * Moves the cart of session `fromSessionId` into this session's cart, in
244
+ * this cart's currency, and closes the other one (`abandoned`, reason
245
+ * `merged`; a merged cart refuses writes with `409 cart_not_open`).
246
+ * Throttle never creates a cart or changes a currency or an owner here: a
247
+ * customer's cart only moves into that customer's cart (`403
248
+ * cart_owner_mismatch`).
249
+ */
250
+ absorb(fromSessionId: string, call?: SessionCallOptions): Promise<CartAbsorb>;
215
251
  };
216
252
  type ProductsNamespace = {
217
253
  list(query?: ProductsQuery, call?: CallOptions): Promise<StorefrontListAnswer>;
@@ -290,7 +326,7 @@ type CatalogNamespaces = {
290
326
  type OpenCartRef = {
291
327
  sessionId: string;
292
328
  cartId: string;
293
- currency: string;
329
+ currency: CurrencyCode;
294
330
  };
295
331
  /**
296
332
  * Where a signed-in customer's cart is found (P8-R14). Structural: the SDK
@@ -299,9 +335,9 @@ type OpenCartRef = {
299
335
  * `currency`, or `null`; `whenClaimSettled` resolves once the sign-in's
300
336
  * guest-cart claim is done, so the merged lines can be read.
301
337
  * Resuming needs a currency: the hook's `currency` option or the client's;
302
- * without one `useCart` makes no lookup. Known limit: a write within 30 s of
303
- * a read that predates another device's merge, or after a re-read that
304
- * failed, reopens the emptied cart.
338
+ * without one `useCart` makes no lookup. A write to a cart another device
339
+ * merged is refused (`409 cart_not_open`, never reopened), and `useCart`
340
+ * then looks the customer's cart up.
305
341
  */
306
342
  type CustomerCartSource = {
307
343
  getOpenCart(input: {
@@ -310,4 +346,4 @@ type CustomerCartSource = {
310
346
  whenClaimSettled(): Promise<void>;
311
347
  };
312
348
 
313
- export type { CustomerCartSource as C, FieldsOption as F, NextFetchOptions as N, OpenCartRef as O, ProductsQuery as P, Redirect as R, SessionCallOptions as S, ThrottleCart as T, CategoryTreeDepth as a, CallOptions as b, CartCheckout as c, CartCheckoutInput as d, CartSession as e, CartSessionBranding as f, CartSessionDetail as g, CartSessionSnapshot as h, CartsNamespace as i, CatalogClientOptions as j, CatalogHttpCache as k, CatalogHttpCacheEntry as l, CatalogNamespaces as m, CategoriesNamespace as n, CollectionsNamespace as o, CustomerTokenSource as p, FiltersNamespace as q, ProductsNamespace as r, ReviewsNamespace as s, StorefrontSort as t, TagsNamespace as u, ThrottleLine as v, RequestSpec as w };
349
+ export type { CustomerCartSource as C, FieldsOption as F, NextFetchOptions as N, OpenCartRef as O, ProductsQuery as P, Redirect as R, SessionCallOptions as S, ThrottleCart as T, CategoryTreeDepth as a, CartWarning as b, CallOptions as c, CartAbsorb as d, CartCheckout as e, CartCheckoutInput as f, CartSession as g, CartSessionBranding as h, CartSessionDetail as i, CartSessionSnapshot as j, CartsNamespace as k, CatalogClientOptions as l, CatalogHttpCache as m, CatalogHttpCacheEntry as n, CatalogNamespaces as o, CategoriesNamespace as p, CollectionsNamespace as q, CustomerTokenSource as r, FiltersNamespace as s, ProductsNamespace as t, ReviewsNamespace as u, StorefrontSort as v, TagsNamespace as w, ThrottleLine as x, RequestSpec as y };
@@ -1,4 +1,5 @@
1
- import { C as CartCheckAnswer, h as CartCreateRequest, g as CartCreateAnswer, b as StorefrontListAnswer, e as StorefrontProductAnswer, p as StorefrontRelatedAnswer, o as StorefrontFilterValuesAnswer, j as StorefrontCategoriesAnswer, k as StorefrontCategoryAnswer, s as StorefrontTagsAnswer, m as StorefrontCollectionsAnswer, l as StorefrontCollectionAnswer, r as StorefrontSuggestAnswer, n as StorefrontConfigAnswer, q as StorefrontReviewsAnswer, d as ReviewEligibilityAnswer, a as ReviewSubmitInput, R as ReviewSubmitAnswer } from './contract-types-BPSzY_Co.js';
1
+ import { CurrencyCode } from '@usethrottle/money';
2
+ import { C as CartCheckAnswer, h as CartCreateRequest, g as CartCreateAnswer, b as StorefrontListAnswer, e as StorefrontProductAnswer, p as StorefrontRelatedAnswer, o as StorefrontFilterValuesAnswer, j as StorefrontCategoriesAnswer, k as StorefrontCategoryAnswer, s as StorefrontTagsAnswer, m as StorefrontCollectionsAnswer, l as StorefrontCollectionAnswer, r as StorefrontSuggestAnswer, n as StorefrontConfigAnswer, q as StorefrontReviewsAnswer, d as ReviewEligibilityAnswer, a as ReviewSubmitInput, R as ReviewSubmitAnswer } from './contract-types-BRyonOYH.cjs';
2
3
 
3
4
  type QueryPairs = [string, string][];
4
5
 
@@ -141,7 +142,7 @@ type ThrottleLine = {
141
142
  /** Throttle's cart as a cart session answers it; other keys pass through. */
142
143
  type ThrottleCart = {
143
144
  cartId: string;
144
- currency: string;
145
+ currency: CurrencyCode;
145
146
  subtotal: number;
146
147
  total: number;
147
148
  unitCount: number;
@@ -198,6 +199,32 @@ type CartCheckout = {
198
199
  };
199
200
  /** Per-call options of a cart-session call. */
200
201
  type SessionCallOptions = Pick<CallOptions, 'signal' | 'timeoutMs'>;
202
+ /**
203
+ * What an absorb reports besides the cart (MC2-R39): lines of the other cart
204
+ * that could not move (no price in this cart's currency, or a custom line in
205
+ * another currency), a discount code this cart's currency refuses, and lines
206
+ * whose prices are unconfirmed until the next refresh or checkout.
207
+ */
208
+ type CartWarning = {
209
+ code: 'lines_removed';
210
+ lines: {
211
+ referenceId: string | null;
212
+ code: string;
213
+ }[];
214
+ message: string;
215
+ } | {
216
+ code: 'discount_dropped';
217
+ discountCode: string;
218
+ message: string;
219
+ } | {
220
+ code: 'lines_stale';
221
+ message: string;
222
+ };
223
+ /** `CartSession.absorb()`'s answer: this session's cart after the move. */
224
+ type CartAbsorb = {
225
+ cart: ThrottleCart;
226
+ warnings: CartWarning[];
227
+ };
201
228
  /** One Throttle cart session, addressed by its opaque id. */
202
229
  type CartSession = {
203
230
  readonly id: string;
@@ -212,6 +239,15 @@ type CartSession = {
212
239
  refresh(call?: SessionCallOptions): Promise<CartSessionSnapshot>;
213
240
  /** Refuses non-http(s) URLs before a request, and never returns a non-http(s) `checkoutUrl`. */
214
241
  checkout(input: CartCheckoutInput, call?: SessionCallOptions): Promise<CartCheckout>;
242
+ /**
243
+ * Moves the cart of session `fromSessionId` into this session's cart, in
244
+ * this cart's currency, and closes the other one (`abandoned`, reason
245
+ * `merged`; a merged cart refuses writes with `409 cart_not_open`).
246
+ * Throttle never creates a cart or changes a currency or an owner here: a
247
+ * customer's cart only moves into that customer's cart (`403
248
+ * cart_owner_mismatch`).
249
+ */
250
+ absorb(fromSessionId: string, call?: SessionCallOptions): Promise<CartAbsorb>;
215
251
  };
216
252
  type ProductsNamespace = {
217
253
  list(query?: ProductsQuery, call?: CallOptions): Promise<StorefrontListAnswer>;
@@ -290,7 +326,7 @@ type CatalogNamespaces = {
290
326
  type OpenCartRef = {
291
327
  sessionId: string;
292
328
  cartId: string;
293
- currency: string;
329
+ currency: CurrencyCode;
294
330
  };
295
331
  /**
296
332
  * Where a signed-in customer's cart is found (P8-R14). Structural: the SDK
@@ -299,9 +335,9 @@ type OpenCartRef = {
299
335
  * `currency`, or `null`; `whenClaimSettled` resolves once the sign-in's
300
336
  * guest-cart claim is done, so the merged lines can be read.
301
337
  * Resuming needs a currency: the hook's `currency` option or the client's;
302
- * without one `useCart` makes no lookup. Known limit: a write within 30 s of
303
- * a read that predates another device's merge, or after a re-read that
304
- * failed, reopens the emptied cart.
338
+ * without one `useCart` makes no lookup. A write to a cart another device
339
+ * merged is refused (`409 cart_not_open`, never reopened), and `useCart`
340
+ * then looks the customer's cart up.
305
341
  */
306
342
  type CustomerCartSource = {
307
343
  getOpenCart(input: {
@@ -310,4 +346,4 @@ type CustomerCartSource = {
310
346
  whenClaimSettled(): Promise<void>;
311
347
  };
312
348
 
313
- export type { CustomerCartSource as C, FieldsOption as F, NextFetchOptions as N, OpenCartRef as O, ProductsQuery as P, Redirect as R, SessionCallOptions as S, ThrottleCart as T, CategoryTreeDepth as a, CallOptions as b, CartCheckout as c, CartCheckoutInput as d, CartSession as e, CartSessionBranding as f, CartSessionDetail as g, CartSessionSnapshot as h, CartsNamespace as i, CatalogClientOptions as j, CatalogHttpCache as k, CatalogHttpCacheEntry as l, CatalogNamespaces as m, CategoriesNamespace as n, CollectionsNamespace as o, CustomerTokenSource as p, FiltersNamespace as q, ProductsNamespace as r, ReviewsNamespace as s, StorefrontSort as t, TagsNamespace as u, ThrottleLine as v, RequestSpec as w };
349
+ export type { CustomerCartSource as C, FieldsOption as F, NextFetchOptions as N, OpenCartRef as O, ProductsQuery as P, Redirect as R, SessionCallOptions as S, ThrottleCart as T, CategoryTreeDepth as a, CartWarning as b, CallOptions as c, CartAbsorb as d, CartCheckout as e, CartCheckoutInput as f, CartSession as g, CartSessionBranding as h, CartSessionDetail as i, CartSessionSnapshot as j, CartsNamespace as k, CatalogClientOptions as l, CatalogHttpCache as m, CatalogHttpCacheEntry as n, CatalogNamespaces as o, CategoriesNamespace as p, CollectionsNamespace as q, CustomerTokenSource as r, FiltersNamespace as s, ProductsNamespace as t, ReviewsNamespace as u, StorefrontSort as v, TagsNamespace as w, ThrottleLine as x, RequestSpec as y };
package/dist/url.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ProductsQuery } from './index.cjs';
2
- import './contract-types-BPSzY_Co.cjs';
2
+ import '@usethrottle/money';
3
+ import './contract-types-BRyonOYH.cjs';
3
4
 
4
5
  /** The shop page's selection as URL state (P6-R16). */
5
6
  type ShopQuery = {
package/dist/url.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ProductsQuery } from './index.js';
2
- import './contract-types-BPSzY_Co.js';
2
+ import '@usethrottle/money';
3
+ import './contract-types-BRyonOYH.js';
3
4
 
4
5
  /** The shop page's selection as URL state (P6-R16). */
5
6
  type ShopQuery = {
@@ -1,4 +1,4 @@
1
- import { b as CatalogError } from './catalog-error-7DCsTyB2.js';
1
+ import { b as CatalogError } from './catalog-error-CSoPS0-d.js';
2
2
 
3
3
  type QueryResult<T> = {
4
4
  data: T | undefined;
@@ -1,4 +1,4 @@
1
- import { b as CatalogError } from './catalog-error-yTBMAdCv.cjs';
1
+ import { b as CatalogError } from './catalog-error-BOaSKgY3.cjs';
2
2
 
3
3
  type QueryResult<T> = {
4
4
  data: T | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usethrottle/catalog",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Storefront SDK for the Throttle Product Catalog: a typed client, React hooks, server helpers and URL helpers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -88,7 +88,8 @@
88
88
  "access": "public"
89
89
  },
90
90
  "dependencies": {
91
- "@usethrottle/errors": "^1.1.0"
91
+ "@usethrottle/errors": "^1.1.0",
92
+ "@usethrottle/money": "^1.0.0"
92
93
  },
93
94
  "peerDependencies": {
94
95
  "react": "^18.2.0 || ^19.0.0"