@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/dist/react.js CHANGED
@@ -1,454 +1,33 @@
1
1
  "use client";
2
2
  import {
3
- CACHE_MAX_ENTRIES,
3
+ CatalogProvider,
4
+ catalogQueryKey,
5
+ createMemoryCartStorage,
6
+ useCatalog,
7
+ useCatalogContext,
8
+ useCategories,
9
+ useCollection,
10
+ useConfig,
11
+ useCustomerKey,
12
+ useFilterValues,
13
+ useProduct,
14
+ useProducts,
15
+ useRelated,
16
+ useReviews,
17
+ useSuggest
18
+ } from "./chunk-CADIHOIR.js";
19
+ import {
4
20
  CatalogError,
5
- INDEXING_REVALIDATE_MS,
6
- RATE_LIMIT_DEFAULT_RETRY_S,
7
21
  RateLimitedError,
8
22
  SDK_VERSION,
9
- STALE_MS,
10
- SUGGEST_DEBOUNCE_MS,
11
- SUGGEST_MIN_CHARS,
12
23
  THROTTLE_CHECKOUT_URL,
24
+ formatMoney,
13
25
  isCatalogError
14
- } from "./chunk-JK2UY4CD.js";
26
+ } from "./chunk-3DLH56OX.js";
15
27
  import "./chunk-PKBMQBKP.js";
16
28
 
17
- // src/react/provider.tsx
18
- import {
19
- createContext,
20
- useContext,
21
- useEffect,
22
- useMemo,
23
- useRef
24
- } from "react";
25
-
26
- // src/react/store.ts
27
- var HOOK_CALL = Symbol.for("@usethrottle/catalog.hookCall");
28
- var EMPTY = {
29
- data: void 0,
30
- error: void 0,
31
- isValidating: false,
32
- fetchedAt: void 0
33
- };
34
- var done = Promise.resolve();
35
- function createStore(client, options = {}) {
36
- const now = options.now ?? Date.now;
37
- const entries = /* @__PURE__ */ new Map();
38
- let until = 0;
39
- let cause;
40
- let pauseTimer;
41
- const pausedError = () => new RateLimitedError({
42
- code: cause?.code ?? "RATE_LIMITED",
43
- message: cause?.message ?? "Too many requests.",
44
- statusCode: 429,
45
- source: cause?.source ?? "catalog",
46
- retryAfterSeconds: Math.ceil((until - now()) / 1e3)
47
- });
48
- const update = (entry, patch) => {
49
- entry.snap = { ...entry.snap, ...patch };
50
- for (const listener of [...entry.subs]) listener();
51
- };
52
- const touch = (key, initial, stale = false) => {
53
- let entry = entries.get(key);
54
- if (entry !== void 0) {
55
- entries.delete(key);
56
- } else {
57
- entry = {
58
- snap: initial === void 0 ? EMPTY : { ...EMPTY, data: initial, fetchedAt: stale ? void 0 : now() },
59
- subs: /* @__PURE__ */ new Set()
60
- };
61
- for (const [oldKey, old] of entries) {
62
- if (entries.size < CACHE_MAX_ENTRIES) break;
63
- if (old.subs.size === 0 && old.flight === void 0) {
64
- clearTimeout(old.timer);
65
- entries.delete(oldKey);
66
- }
67
- }
68
- }
69
- entries.set(key, entry);
70
- return entry;
71
- };
72
- const later = (key, entry, ms) => {
73
- entry.indexing = true;
74
- clearTimeout(entry.timer);
75
- entry.timer = setTimeout(() => {
76
- if (entries.get(key) === entry && entry.subs.size > 0 && entry.run)
77
- void fetchKey(key, entry.run, { force: true });
78
- else entry.indexing = false;
79
- }, ms);
80
- };
81
- function fetchKey(key, run, { force = false, staleMs = STALE_MS } = {}) {
82
- const entry = touch(key);
83
- entry.run = run;
84
- if (entry.promise !== void 0 && !entry.flight?.signal.aborted)
85
- return entry.promise;
86
- if (now() < until) {
87
- if (entry.snap.data === void 0 && entry.snap.error === void 0)
88
- update(entry, { error: pausedError() });
89
- return done;
90
- }
91
- const at = entry.snap.fetchedAt;
92
- if (!force && at !== void 0 && now() - at < staleMs) return done;
93
- const controller = new AbortController();
94
- const box = entry.etag === void 0 ? {} : { etag: entry.etag };
95
- const current = () => {
96
- if (entry.flight !== controller) return false;
97
- entry.flight = entry.promise = void 0;
98
- if (!controller.signal.aborted) return true;
99
- update(entry, { isValidating: false });
100
- return false;
101
- };
102
- entry.flight = controller;
103
- update(entry, { isValidating: true });
104
- let pending;
105
- try {
106
- pending = run(client, {
107
- signal: controller.signal,
108
- [HOOK_CALL]: box
109
- });
110
- } catch (error) {
111
- pending = Promise.reject(error);
112
- }
113
- const promise = pending.then(
114
- (value) => {
115
- if (!current()) return;
116
- const result = box.result ?? {
117
- notModified: false,
118
- data: value,
119
- etag: null
120
- };
121
- entry.etag = result.etag ?? void 0;
122
- if (result.notModified) {
123
- update(entry, {
124
- error: void 0,
125
- isValidating: false,
126
- fetchedAt: now()
127
- });
128
- return;
129
- }
130
- if (result.data?.indexing === true) {
131
- if (!entry.indexing) later(key, entry, INDEXING_REVALIDATE_MS);
132
- } else {
133
- entry.indexing = false;
134
- }
135
- update(entry, {
136
- data: result.data,
137
- error: void 0,
138
- isValidating: false,
139
- fetchedAt: now()
140
- });
141
- },
142
- (error) => {
143
- if (!current()) return;
144
- const retryMs = (error.retryAfterSeconds ?? RATE_LIMIT_DEFAULT_RETRY_S) * 1e3;
145
- if (error.statusCode === 429 && error.code === "RATE_LIMITED")
146
- pause(now() + retryMs, error);
147
- else if (error.code === "STOREFRONT_INDEXING" && !entry.indexing)
148
- later(key, entry, retryMs);
149
- update(entry, { error, isValidating: false });
150
- }
151
- );
152
- entry.promise = promise;
153
- return promise;
154
- }
155
- function pause(untilMs, error) {
156
- if (untilMs <= Math.max(until, now())) return;
157
- until = untilMs;
158
- cause = error;
159
- clearTimeout(pauseTimer);
160
- pauseTimer = setTimeout(() => {
161
- until = 0;
162
- for (const [key, entry] of [...entries]) {
163
- if (entry.subs.size > 0 && entry.run)
164
- void fetchKey(key, entry.run, { force: true });
165
- }
166
- }, untilMs - now());
167
- }
168
- function clear() {
169
- for (const [key, entry] of [...entries]) {
170
- clearTimeout(entry.timer);
171
- entry.flight?.abort();
172
- if (entry.subs.size === 0) {
173
- entries.delete(key);
174
- } else {
175
- entry.etag = entry.flight = entry.promise = void 0;
176
- entry.indexing = false;
177
- update(entry, EMPTY);
178
- }
179
- }
180
- }
181
- return {
182
- subscribe(key, listener) {
183
- const entry = touch(key);
184
- entry.subs.add(listener);
185
- return () => {
186
- entry.subs.delete(listener);
187
- void done.then(() => {
188
- if (entry.subs.size === 0) entry.flight?.abort();
189
- });
190
- };
191
- },
192
- get: (key, initial, stale) => key === null ? EMPTY : (entries.get(key) ?? (initial === void 0 ? void 0 : touch(key, initial, stale)))?.snap ?? EMPTY,
193
- getServer(key, initial, stale) {
194
- if (key === null || initial === void 0) return EMPTY;
195
- const entry = entries.get(key) ?? touch(key, initial, stale);
196
- return entry.server ?? (entry.server = { ...EMPTY, data: initial });
197
- },
198
- fetch: fetchKey,
199
- clear,
200
- pause,
201
- pausedUntil: () => until > now() ? until : 0,
202
- dispose() {
203
- clearTimeout(pauseTimer);
204
- until = 0;
205
- clear();
206
- for (const entry of entries.values()) entry.subs.clear();
207
- entries.clear();
208
- }
209
- };
210
- }
211
-
212
- // src/react/provider.tsx
213
- import { jsx } from "react/jsx-runtime";
214
- function createMemoryCartStorage() {
215
- let id = null;
216
- return {
217
- get: () => id,
218
- set: (value) => {
219
- id = value;
220
- },
221
- remove: () => {
222
- id = null;
223
- }
224
- };
225
- }
226
- var CatalogContext = createContext(null);
227
- function useCatalogContext() {
228
- const value = useContext(CatalogContext);
229
- if (value === null)
230
- throw new Error("Catalog hooks must be used inside a <CatalogProvider>.");
231
- return value;
232
- }
233
- function CatalogProvider(props) {
234
- const { client, children, ...rest } = props;
235
- const customerKey = props.customerKey ?? null;
236
- const mounted = useRef(null);
237
- const store = useMemo(() => {
238
- const next = createStore(client);
239
- next.pause(mounted.current?.pausedUntil() ?? 0);
240
- return next;
241
- }, [client, customerKey]);
242
- useEffect(() => {
243
- mounted.current = store;
244
- return () => {
245
- mounted.current = null;
246
- void Promise.resolve().then(() => {
247
- if (mounted.current !== store) store.dispose();
248
- });
249
- };
250
- }, [store]);
251
- const memory = useMemo(createMemoryCartStorage, []);
252
- const cartStorage = rest.cartStorage ?? memory;
253
- const shared = useRef({}).current;
254
- const cart = useMemo(
255
- () => ({ shared }),
256
- [shared, client, rest.throttleBaseUrl]
257
- );
258
- const urls = rest.cartUrls;
259
- const value = useMemo(
260
- () => ({ ...rest, client, customerKey, store, cartStorage, cart }),
261
- // `rest` is spread fresh each render; its fields are the dependencies.
262
- [
263
- client,
264
- customerKey,
265
- store,
266
- cartStorage,
267
- cart,
268
- rest.throttleBaseUrl,
269
- rest.throttleCheckoutUrl,
270
- urls?.returnUrl,
271
- urls?.cancelUrl,
272
- urls?.storefrontUrl
273
- ]
274
- );
275
- cart.context = value;
276
- shared.home = cart;
277
- return /* @__PURE__ */ jsx(CatalogContext.Provider, { value, children });
278
- }
279
- function useCatalog() {
280
- return useCatalogContext().client;
281
- }
282
-
283
- // src/react/use-query.ts
284
- import { useCallback, useEffect as useEffect2, useRef as useRef2, useSyncExternalStore } from "react";
285
- function canonical(value) {
286
- if (Array.isArray(value)) return value.map(canonical);
287
- if (value === null || typeof value !== "object") return value;
288
- const out = {};
289
- for (const name of Object.keys(value).sort()) {
290
- const item = value[name];
291
- if (item !== void 0) out[name] = canonical(item);
292
- }
293
- return out;
294
- }
295
- function catalogQueryKey(route, params) {
296
- return `${route}:${JSON.stringify(canonical(params ?? null))}`;
297
- }
298
- var noop = () => {
299
- };
300
- var unsubscribe = () => noop;
301
- function useQuery(route, params, run, options = {}) {
302
- const { store, customerKey } = useCatalogContext();
303
- const key = run === null || options.enabled === false ? null : `${customerKey ?? ""}\0${catalogQueryKey(route, params)}`;
304
- const first = useRef2({ store, key });
305
- const initial = first.current.store === store && first.current.key === key ? options.initialData : void 0;
306
- const subscribe = useCallback(
307
- (listener) => key === null ? unsubscribe() : store.subscribe(key, listener),
308
- [store, key]
309
- );
310
- const stale = customerKey !== null;
311
- const snap = useSyncExternalStore(
312
- subscribe,
313
- () => store.get(key, initial, stale),
314
- () => store.getServer(key, initial, stale)
315
- );
316
- const staleMs = options.staleTimeMs;
317
- useEffect2(() => {
318
- if (key !== null && run !== null)
319
- void store.fetch(key, run, staleMs === void 0 ? {} : { staleMs });
320
- }, [store, key, staleMs]);
321
- const refetch = useCallback(
322
- () => key === null || run === null ? Promise.resolve() : store.fetch(key, run, { force: true }),
323
- [store, key]
324
- );
325
- return {
326
- data: snap.data,
327
- error: snap.error,
328
- isLoading: key !== null && snap.data === void 0 && snap.error === void 0,
329
- isValidating: snap.isValidating,
330
- refetch
331
- };
332
- }
333
-
334
- // src/react/hooks.ts
335
- import { useEffect as useEffect3, useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore2 } from "react";
336
- function defined(options) {
337
- const out = {};
338
- for (const name in options) {
339
- if (options[name] !== void 0) out[name] = options[name];
340
- }
341
- return out;
342
- }
343
- function useProducts(query = {}, options) {
344
- return useQuery(
345
- "products",
346
- query,
347
- (client, call) => client.products.list(query, call),
348
- options
349
- );
350
- }
351
- function useProduct(handle, options = {}) {
352
- const extra = defined({ include: options.include });
353
- return useQuery(
354
- "product",
355
- { handle, ...extra },
356
- handle === null ? null : (client, call) => client.products.get(handle, { ...call, ...extra }),
357
- options
358
- );
359
- }
360
- function useRelated(handle, options = {}) {
361
- const extra = defined({ limit: options.limit });
362
- return useQuery(
363
- "related",
364
- { handle, ...extra },
365
- handle === null ? null : (client, call) => client.products.related(handle, { ...call, ...extra }),
366
- options
367
- );
368
- }
369
- function useDebounced(q, ms) {
370
- const box = useMemo2(() => {
371
- const listeners = /* @__PURE__ */ new Set();
372
- let value = q;
373
- return {
374
- get: () => value,
375
- set(next) {
376
- value = next;
377
- for (const listener of [...listeners]) listener();
378
- },
379
- subscribe(listener) {
380
- listeners.add(listener);
381
- return () => listeners.delete(listener);
382
- }
383
- };
384
- }, []);
385
- useEffect3(() => {
386
- const timer = setTimeout(() => box.set(q), ms);
387
- return () => clearTimeout(timer);
388
- }, [box, q, ms]);
389
- return useSyncExternalStore2(box.subscribe, box.get, box.get);
390
- }
391
- function useSuggest(q, options = {}) {
392
- const settled = useDebounced(q, options.debounceMs ?? SUGGEST_DEBOUNCE_MS);
393
- const extra = defined({ limit: options.limit });
394
- const long = (value) => value.trim().length >= SUGGEST_MIN_CHARS;
395
- return useQuery(
396
- "suggest",
397
- { q: settled, ...extra },
398
- long(q) && long(settled) ? (client, call) => client.suggest(settled, { ...call, ...extra }) : null,
399
- options
400
- );
401
- }
402
- function useCategories(options = {}) {
403
- const extra = defined({ depth: options.depth });
404
- return useQuery(
405
- "categories",
406
- extra,
407
- (client, call) => client.categories.tree({ ...call, ...extra }),
408
- options
409
- );
410
- }
411
- function useCollection(handle, options) {
412
- return useQuery(
413
- "collection",
414
- { handle },
415
- handle === null ? null : (client, call) => client.collections.get(handle, call),
416
- options
417
- );
418
- }
419
- function useReviews(handle, options = {}) {
420
- const extra = defined({
421
- sort: options.sort,
422
- rating: options.rating,
423
- limit: options.limit,
424
- cursor: options.cursor
425
- });
426
- return useQuery(
427
- "reviews",
428
- { handle, ...extra },
429
- handle === null ? null : (client, call) => client.reviews.list(handle, { ...call, ...extra }),
430
- options
431
- );
432
- }
433
- function useConfig(options) {
434
- return useQuery(
435
- "config",
436
- null,
437
- (client, call) => client.config(call),
438
- options
439
- );
440
- }
441
- function useFilterValues(key, query = {}, options) {
442
- return useQuery(
443
- "filterValues",
444
- { key, query },
445
- key === null ? null : (client, call) => client.filters.values(key, query, call),
446
- options
447
- );
448
- }
449
-
450
29
  // src/react/use-cart.ts
451
- import { useEffect as useEffect4, useMemo as useMemo3, useSyncExternalStore as useSyncExternalStore3 } from "react";
30
+ import { useEffect, useMemo, useSyncExternalStore } from "react";
452
31
 
453
32
  // src/react/mutation.ts
454
33
  function newKey() {
@@ -515,13 +94,14 @@ function createBox(initial) {
515
94
  }
516
95
 
517
96
  // src/react/use-cart.ts
518
- var EMPTY2 = {
97
+ var EMPTY = {
519
98
  cart: null,
520
99
  sessionId: null,
521
100
  cartUrl: null,
522
101
  status: "idle",
523
102
  error: void 0,
524
- pending: false
103
+ pending: false,
104
+ warnings: []
525
105
  };
526
106
  var ENDED = /* @__PURE__ */ new Set([
527
107
  "cart_session_expired",
@@ -543,6 +123,8 @@ function itemsOf(cart) {
543
123
  return items;
544
124
  }
545
125
  var skuOf = (cart, lineId) => cart?.lineItems?.find((line) => line.id === lineId)?.referenceId ?? null;
126
+ var CART_REREAD_MS = 3e4;
127
+ var TRANSPORT = Symbol.for("@usethrottle/catalog.transport");
546
128
  function createController(home) {
547
129
  var _a;
548
130
  const ctx = () => home.context;
@@ -551,16 +133,19 @@ function createController(home) {
551
133
  seen: void 0,
552
134
  moving: void 0,
553
135
  unremoved: false,
136
+ reads: /* @__PURE__ */ new Map(),
137
+ slots: /* @__PURE__ */ new Set(),
554
138
  abort: new AbortController(),
555
139
  writing: Promise.resolve(),
556
140
  tail: Promise.resolve()
557
141
  });
558
142
  const retired = () => home.shared.home !== home;
559
- const box = createBox(EMPTY2);
143
+ const box = createBox(EMPTY);
560
144
  let queued = 0;
561
145
  let loaded = false;
562
146
  let currency;
563
- const reset = () => box.set({ ...EMPTY2, pending: queued > 0 });
147
+ let hinted;
148
+ const reset = () => box.set({ ...EMPTY, pending: queued > 0 });
564
149
  const session = (id) => {
565
150
  const { client, throttleBaseUrl } = ctx();
566
151
  return client.carts.session(
@@ -574,12 +159,106 @@ function createController(home) {
574
159
  if (signal.aborted) throw signal.reason;
575
160
  return out;
576
161
  };
162
+ const resuming = () => ctx().customerCart !== void 0 && ctx().customerKey !== null;
163
+ const wanted = (options) => {
164
+ const transport = ctx().client[TRANSPORT];
165
+ return (currency ?? options?.currency ?? hinted ?? transport?.context?.().currency)?.toUpperCase();
166
+ };
167
+ async function soft(signal, step) {
168
+ try {
169
+ await step();
170
+ } catch (error) {
171
+ if (signal.aborted) throw error;
172
+ }
173
+ }
174
+ async function find(signal, wantedCurrency, strict = false) {
175
+ const source = ctx().customerCart;
176
+ if (source === void 0 || wantedCurrency === void 0) return null;
177
+ const asked = wantedCurrency.toUpperCase();
178
+ try {
179
+ const found = await live(signal, source.getOpenCart({ currency: asked }));
180
+ return found !== null && found.currency.toUpperCase() === asked ? found : null;
181
+ } catch (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;
200
+ return null;
201
+ }
202
+ return { id, cartUrl: fallbackUrl(id) };
203
+ }
204
+ async function findOnce(signal, storage) {
205
+ var _a2;
206
+ if (!resuming()) return;
207
+ const looked = (_a2 = home.shared).looked ?? (_a2.looked = /* @__PURE__ */ new Set());
208
+ const asked = wanted();
209
+ if (asked === void 0) return;
210
+ const key = `${ctx().customerKey}\0${asked}`;
211
+ if (looked.has(key)) return;
212
+ looked.add(key);
213
+ let found = null;
214
+ try {
215
+ found = await find(signal, asked);
216
+ } finally {
217
+ if (found === null) looked.delete(key);
218
+ }
219
+ if (found !== null) await soft(signal, () => adopt(signal, found, storage));
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
+ }
231
+ async function adopt(signal, found, storage) {
232
+ const id = found.sessionId;
233
+ const answer = await live(signal, session(id).get({ signal }));
234
+ me.reads.set(id, Date.now());
235
+ await save(signal, storage, id);
236
+ box.set({
237
+ sessionId: id,
238
+ cartUrl: fallbackUrl(id),
239
+ cart: answer.cart,
240
+ status: "ready",
241
+ error: void 0
242
+ });
243
+ }
244
+ async function supersede(signal, answer, storage) {
245
+ if (answer.cart.status !== "abandoned" || !resuming()) return;
246
+ const found = await find(signal, answer.cart.currency);
247
+ if (found !== null && found.cartId !== answer.cart.cartId)
248
+ await adopt(signal, found, storage);
249
+ }
250
+ async function read(signal, id) {
251
+ const answer = await live(signal, session(id).get({ signal }));
252
+ me.reads.set(id, Date.now());
253
+ box.set({ cart: answer.cart, status: "ready", error: void 0 });
254
+ return answer;
255
+ }
577
256
  async function endSession(signal, error, storage) {
578
257
  await live(signal, storage.remove());
579
258
  if (converted(error)) reset();
580
259
  else box.set({ sessionId: null, cartUrl: null });
581
260
  }
582
- async function create(signal, options, items, whole = false) {
261
+ async function start(signal, options, items) {
583
262
  const context = ctx();
584
263
  const body = {
585
264
  items: [...items].map(([sku, quantity]) => ({ sku, quantity }))
@@ -603,33 +282,25 @@ function createController(home) {
603
282
  throw error;
604
283
  }
605
284
  const id = answer.sessionId;
606
- const cartUrl = isHttpUrl(answer.cartUrl) ? answer.cartUrl : fallbackUrl(id);
607
- if (whole) {
608
- const snapshot = await live(signal, session(id).get({ signal }));
609
- const write = context.cartStorage.set(id);
610
- me.writing = Promise.resolve(write).catch(() => void 0);
611
- await live(signal, write);
612
- box.set({
613
- sessionId: id,
614
- cartUrl,
615
- cart: snapshot.cart,
616
- status: "ready",
617
- error: void 0
618
- });
619
- return;
620
- }
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);
621
293
  box.set({ sessionId: id, cartUrl });
622
294
  let unsaved;
623
295
  try {
624
- const write = context.cartStorage.set(id);
625
- me.writing = Promise.resolve(write).catch(() => void 0);
626
- await live(signal, write);
296
+ await save(signal, storage, id);
627
297
  } catch (error) {
628
298
  if (signal.aborted) throw error;
629
299
  unsaved = error;
630
300
  }
631
301
  try {
632
302
  const snapshot = await live(signal, session(id).get({ signal }));
303
+ me.reads.set(id, Date.now());
633
304
  box.set({ cart: snapshot.cart, status: "ready", error: void 0 });
634
305
  } catch (error) {
635
306
  if (!signal.aborted) box.set({ status: "error" });
@@ -640,18 +311,52 @@ function createController(home) {
640
311
  async function mutate(signal, options, call, apply) {
641
312
  checkPause(ctx().store);
642
313
  const storage = ctx().cartStorage;
643
- const id = box.get().sessionId;
644
314
  let met;
645
- if (id !== null) {
315
+ const attempt = async (id2) => {
646
316
  try {
647
- const answer = await live(signal, call(session(id), signal));
317
+ const answer = await live(signal, call(session(id2), signal));
648
318
  if (answer !== null)
649
319
  box.set({ cart: answer.cart, status: "ready", error: void 0 });
650
- return void 0;
320
+ return true;
651
321
  } catch (error) {
652
322
  if (signal.aborted || !ended(error)) throw error;
653
323
  await endSession(signal, error, storage);
654
324
  met = error;
325
+ return false;
326
+ }
327
+ };
328
+ const stale = box.get().sessionId;
329
+ if (stale !== null && resuming() && Date.now() - (me.reads.get(stale) ?? 0) > CART_REREAD_MS) {
330
+ let answer;
331
+ try {
332
+ answer = await read(signal, stale);
333
+ } catch (error) {
334
+ if (signal.aborted) throw error;
335
+ if (ended(error)) {
336
+ await endSession(signal, error, storage);
337
+ met = error;
338
+ }
339
+ }
340
+ if (answer !== void 0) {
341
+ const seen = answer;
342
+ await soft(signal, () => supersede(signal, seen, storage));
343
+ }
344
+ }
345
+ const id = box.get().sessionId;
346
+ if (id !== null && await attempt(id)) return void 0;
347
+ if (resuming()) {
348
+ const found = await find(signal, wanted(options));
349
+ if (found !== null) {
350
+ let adopted = true;
351
+ await soft(signal, async () => {
352
+ try {
353
+ await adopt(signal, found, storage);
354
+ } catch (error) {
355
+ adopted = false;
356
+ throw error;
357
+ }
358
+ });
359
+ if (adopted && await attempt(found.sessionId)) return void 0;
655
360
  }
656
361
  }
657
362
  const cart = box.get().cart;
@@ -687,13 +392,16 @@ function createController(home) {
687
392
  me.tail = result.catch(() => void 0);
688
393
  return result;
689
394
  }
690
- async function restore(signal, storage = ctx().cartStorage) {
395
+ async function restore(signal, storage = ctx().cartStorage, look = true) {
691
396
  const id = await live(signal, storage.get());
692
- if (id === null || id === "") return;
397
+ if (id === null || id === "") {
398
+ if (look) await findOnce(signal, storage);
399
+ return;
400
+ }
693
401
  box.set({ sessionId: id, cartUrl: fallbackUrl(id), status: "loading" });
402
+ let answer;
694
403
  try {
695
- const answer = await live(signal, session(id).get({ signal }));
696
- box.set({ cart: answer.cart, status: "ready", error: void 0 });
404
+ answer = await read(signal, id);
697
405
  } catch (error) {
698
406
  if (signal.aborted) return;
699
407
  if (!ended(error)) {
@@ -707,7 +415,10 @@ function createController(home) {
707
415
  return;
708
416
  }
709
417
  reset();
418
+ if (look) await findOnce(signal, storage);
419
+ return;
710
420
  }
421
+ if (look) await soft(signal, () => supersede(signal, answer, storage));
711
422
  }
712
423
  function load() {
713
424
  if (loaded) return;
@@ -716,6 +427,17 @@ function createController(home) {
716
427
  const signal = me.abort.signal;
717
428
  me.tail = me.tail.then(() => me.unremoved ? void 0 : restore(signal)).catch(() => void 0);
718
429
  }
430
+ async function claimSettled(signal) {
431
+ const source = ctx().customerCart;
432
+ if (source === void 0) return false;
433
+ try {
434
+ await live(signal, source.whenClaimSettled());
435
+ return true;
436
+ } catch (error) {
437
+ if (signal.aborted) throw error;
438
+ return false;
439
+ }
440
+ }
719
441
  function follow(guest, fresh) {
720
442
  const signal = me.abort.signal;
721
443
  me.moving = guest;
@@ -727,18 +449,32 @@ function createController(home) {
727
449
  me.tail = me.tail.then(async () => {
728
450
  await me.writing;
729
451
  if (signal.aborted) return;
730
- if (fresh && !me.unremoved) await restore(signal, guest);
452
+ if (fresh && !me.unremoved) await restore(signal, guest, false);
731
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();
732
459
  const id = box.get().sessionId;
733
460
  if (id === null) {
734
- if (box.get().cart === null && !me.unremoved) return restore(signal);
461
+ if (box.get().cart === null && !me.unremoved) {
462
+ await claimSettled(signal);
463
+ return restore(signal);
464
+ }
735
465
  return;
736
466
  }
737
467
  const next = ctx().cartStorage;
738
468
  await write(next, id);
739
- if (guest === next) return;
740
- await live(signal, guest.remove());
741
- if (await live(signal, next.get()) !== id) await write(next, id);
469
+ if (guest !== next) {
470
+ await live(signal, guest.remove());
471
+ if (await live(signal, next.get()) !== id) await write(next, id);
472
+ }
473
+ if (resuming() && await claimSettled(signal))
474
+ await soft(
475
+ signal,
476
+ async () => supersede(signal, await read(signal, id), next)
477
+ );
742
478
  }).catch((error) => {
743
479
  if (!signal.aborted) box.set({ error: wrap(error) });
744
480
  }).finally(() => {
@@ -758,6 +494,9 @@ function createController(home) {
758
494
  if (was === void 0 || was === null || was === key) return;
759
495
  me.abort.abort();
760
496
  me.abort = new AbortController();
497
+ me.reads.clear();
498
+ for (const key2 of home.shared.looked ?? [])
499
+ if (key2.startsWith(`${was}\0`)) home.shared.looked?.delete(key2);
761
500
  currency = void 0;
762
501
  loaded = true;
763
502
  reset();
@@ -765,9 +504,15 @@ function createController(home) {
765
504
  const outgoing = prior ?? me.seen;
766
505
  const left = me.moving;
767
506
  me.moving = void 0;
507
+ const slots = [...me.slots];
508
+ me.slots.clear();
768
509
  const remove = async () => {
769
510
  await outgoing.remove();
770
- 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
+ }
771
516
  };
772
517
  me.tail = me.writing.then(remove).catch(remove).then(
773
518
  () => {
@@ -784,12 +529,16 @@ function createController(home) {
784
529
  box,
785
530
  /** The customer the cart belongs to (`undefined` before the first look). */
786
531
  owner: () => me.owner,
532
+ hint: (next) => {
533
+ hinted = next;
534
+ },
787
535
  identify,
788
536
  load,
789
537
  run,
790
538
  mutate,
791
539
  create,
792
540
  session,
541
+ wanted,
793
542
  async switchCurrency(signal, options, next) {
794
543
  checkPause(ctx().store);
795
544
  const storage = ctx().cartStorage;
@@ -806,21 +555,60 @@ function createController(home) {
806
555
  }
807
556
  const before = currency;
808
557
  currency = next;
809
- const items = itemsOf(box.get().cart);
810
- if (items.size === 0) {
811
- if (box.get().sessionId !== null) await live(signal, storage.remove());
812
- 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();
813
565
  return;
814
566
  }
567
+ if (cart?.currency.toUpperCase() === code) return;
568
+ let target;
569
+ let moved;
815
570
  try {
816
- 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 }));
817
573
  } catch (error) {
818
574
  if (!signal.aborted) currency = before;
819
575
  throw error;
820
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;
821
605
  },
822
606
  async clear(signal) {
823
- 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()));
824
612
  currency = void 0;
825
613
  reset();
826
614
  }
@@ -830,18 +618,22 @@ function useCart(options = {}) {
830
618
  const context = useCatalogContext();
831
619
  const home = context.cart;
832
620
  const controller = home.current ?? (home.current = createController(home));
833
- const state = useSyncExternalStore3(
621
+ const state = useSyncExternalStore(
834
622
  controller.box.subscribe,
835
623
  controller.box.get,
836
624
  controller.box.get
837
625
  );
838
626
  const { customerKey } = context;
839
- useEffect4(() => {
627
+ const hooked = options.currency;
628
+ useEffect(() => {
629
+ controller.hint(hooked);
630
+ }, [controller, hooked]);
631
+ useEffect(() => {
840
632
  controller.identify(customerKey);
841
633
  controller.load();
842
634
  }, [controller, customerKey]);
843
635
  const { currency, locale } = options;
844
- const actions = useMemo3(() => {
636
+ const actions = useMemo(() => {
845
637
  const opts = {};
846
638
  if (currency !== void 0) opts.currency = currency;
847
639
  if (locale !== void 0) opts.locale = locale;
@@ -933,12 +725,14 @@ function useCart(options = {}) {
933
725
  };
934
726
  }, [controller, home, currency, locale]);
935
727
  const owner = controller.owner();
936
- const view = owner !== void 0 && owner !== null && owner !== customerKey ? { ...EMPTY2, pending: state.pending } : state;
937
- return { ...view, ...actions };
728
+ const view = owner !== void 0 && owner !== null && owner !== customerKey ? { ...EMPTY, pending: state.pending } : state;
729
+ const shownCurrency = view.cart?.currency ?? controller.wanted(options) ?? "";
730
+ const format = (minor) => formatMoney(minor, shownCurrency);
731
+ return { ...view, ...actions, format };
938
732
  }
939
733
 
940
734
  // src/react/use-submit-review.ts
941
- import { useCallback as useCallback2, useMemo as useMemo4, useSyncExternalStore as useSyncExternalStore4 } from "react";
735
+ import { useCallback, useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore2 } from "react";
942
736
  var IDLE = {
943
737
  status: "idle",
944
738
  error: void 0,
@@ -946,7 +740,7 @@ var IDLE = {
946
740
  };
947
741
  function useSubmitReview(handle) {
948
742
  const { client, store, customerKey } = useCatalogContext();
949
- const scope = useMemo4(
743
+ const scope = useMemo2(
950
744
  () => ({
951
745
  box: createBox(IDLE),
952
746
  flight: null,
@@ -956,8 +750,8 @@ function useSubmitReview(handle) {
956
750
  [handle, customerKey]
957
751
  );
958
752
  const { box } = scope;
959
- const state = useSyncExternalStore4(box.subscribe, box.get, box.get);
960
- const submit = useCallback2(
753
+ const state = useSyncExternalStore2(box.subscribe, box.get, box.get);
754
+ const submit = useCallback(
961
755
  (input) => {
962
756
  if (scope.flight !== null) return scope.flight;
963
757
  const fail = (error) => {
@@ -992,7 +786,7 @@ function useSubmitReview(handle) {
992
786
  },
993
787
  [client, store, scope, box, handle]
994
788
  );
995
- const reset = useCallback2(() => {
789
+ const reset = useCallback(() => {
996
790
  scope.flight = scope.key = null;
997
791
  box.set(IDLE);
998
792
  }, [scope, box]);
@@ -1008,6 +802,7 @@ export {
1008
802
  useCategories,
1009
803
  useCollection,
1010
804
  useConfig,
805
+ useCustomerKey,
1011
806
  useFilterValues,
1012
807
  useProduct,
1013
808
  useProducts,