@doswiftly/storefront-sdk 11.1.0 → 11.2.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.
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Cart recovery — DX-first auto-recovery for stale cart errors.
3
+ *
4
+ * Backend cart write operations enforce stricter checks than reads. Cart can
5
+ * return successfully from `cart(id)` query while `cartAddLines(id)` rejects with
6
+ * `userErrors[].code = 'CART_NOT_FOUND'` (ownership/lifetime drift after read).
7
+ * Same happens after checkout: cart transitions to CONVERTED status → next write
8
+ * yields `ALREADY_COMPLETED`.
9
+ *
10
+ * This module gives every SDK consumer (React, Vue, Svelte, mobile, CLI) the
11
+ * same automatic recovery without each integrator reimplementing detection,
12
+ * cookie cleanup, and retry orchestration.
13
+ *
14
+ * The runner classifies operations:
15
+ *
16
+ * - **Auto-replay** — operation provides `recreateAndRun` (atomic create-and-run
17
+ * against a fresh cart). Storefront caller sees the operation succeed; the
18
+ * swap is invisible. Typical fit: `addItems` via `cartCreate({ lines })`.
19
+ *
20
+ * - **Bail + event** — operation has no `recreateAndRun`, i.e. it references
21
+ * state from the dead cart (lineIds for update/remove, shippingMethodId for
22
+ * select). Replaying on an empty cart would silently lose user intent, so
23
+ * the runner clears the cookie, emits `cart-expired`, and throws
24
+ * `CartRecoveryNotPossibleError`. UI subscribes once globally and shows a
25
+ * toast / banner — caller code never writes `try / catch` per mutation.
26
+ *
27
+ * Detection is **code-based** (`err.userErrors[].code`) not message-based —
28
+ * resilient to PL/EN/future locales. Concurrent operations share one in-flight
29
+ * recovery promise so a double-click never produces two orphan carts.
30
+ *
31
+ * Core stays 0-deps: cookie I/O is injected via `CartCookieStore` port (caller
32
+ * provides browser / Next.js server / AsyncStorage / in-memory implementation).
33
+ */
34
+ import { StorefrontError } from '../errors';
35
+ import { CartClient, type CartMutationOutcome } from './cart-client';
36
+ import type { Cart, CartCreateInput } from './types';
37
+ /**
38
+ * Cart error codes that require recreation of the cart resource.
39
+ *
40
+ * - `CART_NOT_FOUND` — cart expired, missing, or owner mismatch (backend masks
41
+ * `UNAUTHORIZED` as `CART_NOT_FOUND` to avoid leaking ownership info).
42
+ * - `ALREADY_COMPLETED` — cart status not in `{ACTIVE, RECOVERED}` (CONVERTED
43
+ * after checkout, EXPIRED via TTL, or ABANDONED by purge worker).
44
+ *
45
+ * Other codes (`NOT_ENOUGH_IN_STOCK`, validation, fulfillment) are recoverable
46
+ * by user input, not by cart recreation — they propagate unchanged.
47
+ */
48
+ export declare const CART_RECOVERABLE_ERROR_CODES: readonly ["CART_NOT_FOUND", "ALREADY_COMPLETED"];
49
+ export type CartRecoverableErrorCode = (typeof CART_RECOVERABLE_ERROR_CODES)[number];
50
+ /**
51
+ * Type-safe predicate for "this error means the cart resource is gone".
52
+ *
53
+ * Inspects `err.userErrors[].code` (structured field) — never matches against
54
+ * `err.message` (locale-dependent, non-stable). Returns `true` only when a
55
+ * `StorefrontError` carries at least one userError with a recoverable code.
56
+ */
57
+ export declare function isCartRecoverableError(err: unknown): err is StorefrontError;
58
+ /**
59
+ * Cookie I/O port. Caller's responsibility — core never touches `document.cookie`.
60
+ *
61
+ * Three methods form a cohesive lifecycle (read existing, write new, clear after
62
+ * expiry). Implementations:
63
+ *
64
+ * - Browser: `createBrowserCartCookieStore()` (from `@doswiftly/storefront-sdk/react`)
65
+ * - Next.js Server Component / Route Handler: wrap `cookies()` from `next/headers`
66
+ * - React Native / mobile: AsyncStorage adapter
67
+ * - CLI / Node: file-based or in-memory `Map<string, string>`
68
+ * - Tests: in-memory stub
69
+ */
70
+ export interface CartCookieStore {
71
+ /** Returns the persisted cart id or null. Must be synchronous. */
72
+ get(): string | null;
73
+ /** Persists cart id. `options.maxAge` defaults to platform `CART_COOKIE_MAX_AGE`. */
74
+ set(cartId: string, options?: {
75
+ maxAge?: number;
76
+ }): void;
77
+ /** Removes the cookie. Called when the current cart became unrecoverable. */
78
+ clear(): void;
79
+ }
80
+ /**
81
+ * Description of an operation the runner will execute, with optional recovery
82
+ * strategy.
83
+ *
84
+ * `run` is the happy-path call against an existing `cartId`. The runner invokes
85
+ * it first; on success, returns its value.
86
+ *
87
+ * `recreateAndRun` is the **auto-replay strategy**: when defined, on recoverable
88
+ * error the runner clears the cookie, calls `recreateAndRun(cartClient)` which
89
+ * atomically creates a fresh cart with the same payload (e.g. `cartCreate({ lines })`
90
+ * for `addItems`), persists the new id, and returns the operation result in one
91
+ * round trip. When absent, the runner bails with `CartRecoveryNotPossibleError`
92
+ * and emits a `cart-expired` event — appropriate for operations that reference
93
+ * state from the dead cart (`updateItems(lineId)`, `selectShippingMethod`, etc.).
94
+ */
95
+ export interface CartRecoveryOperation<T> {
96
+ /** Happy-path call with current cart id. */
97
+ run: (cartId: string) => Promise<T>;
98
+ /**
99
+ * Optional atomic "create new cart and run with payload in one go" strategy.
100
+ * Define for replay-safe operations; omit for state-dependent ones.
101
+ */
102
+ recreateAndRun?: (cartClient: CartClient) => Promise<{
103
+ cart: Cart;
104
+ result: T;
105
+ }>;
106
+ /** Optional diagnostic label surfaced in `CartExpiredEvent.operation`. */
107
+ name?: string;
108
+ }
109
+ export type CartRecoveryFailureReason = 'state-dependent' | 'recreate-failed' | 'retry-also-failed';
110
+ /**
111
+ * Thrown when an operation hits a recoverable cart error but recovery is not
112
+ * possible (no `recreateAndRun`, or recovery itself failed). UI should clear
113
+ * local cart state and prompt the user to start over.
114
+ */
115
+ export declare class CartRecoveryNotPossibleError extends Error {
116
+ readonly reason: CartRecoveryFailureReason;
117
+ readonly cause: unknown;
118
+ constructor(reason: CartRecoveryFailureReason, cause: unknown, message?: string);
119
+ }
120
+ export interface CartExpiredEvent {
121
+ /** Why recovery did not produce a usable cart for the caller. */
122
+ reason: CartRecoveryFailureReason;
123
+ /** Cart id present in the cookie immediately before clearing (null if cookie was empty). */
124
+ oldCartId: string | null;
125
+ /** Operation name from `CartRecoveryOperation.name` if provided, otherwise 'unknown'. */
126
+ operation: string;
127
+ /** Original error that triggered recovery (or recreate failure). */
128
+ cause: unknown;
129
+ }
130
+ export interface ExecuteWithCartRecoveryOptions<T> {
131
+ cartClient: CartClient;
132
+ cookieStore: CartCookieStore;
133
+ operation: CartRecoveryOperation<T>;
134
+ /** Initial-create fallback when cookie is empty. Defaults to `cartClient.create()`. */
135
+ ensureCart?: () => Promise<Cart>;
136
+ /** Cookie `maxAge` override propagated to `cookieStore.set`. */
137
+ cookieMaxAge?: number;
138
+ /** Optional listener fired before the runner throws `CartRecoveryNotPossibleError`. */
139
+ onExpired?: (event: CartExpiredEvent) => void;
140
+ }
141
+ /**
142
+ * Runs an operation against the current cart, recovering once on
143
+ * `CART_NOT_FOUND` / `ALREADY_COMPLETED`. Pure async function — non-React
144
+ * consumers can wire this directly without the runner factory.
145
+ */
146
+ export declare function executeWithCartRecovery<T>(opts: ExecuteWithCartRecoveryOptions<T>): Promise<T>;
147
+ export interface CartRecoveryRunner {
148
+ /** Run any operation through the recovery pipeline. */
149
+ execute<T>(operation: CartRecoveryOperation<T>): Promise<T>;
150
+ /**
151
+ * Convenience read with auto-cookie-cleanup. Returns the cart or null.
152
+ * If the backend returns `CART_NOT_FOUND` via `userErrors`, clears the cookie
153
+ * and resolves null instead of throwing.
154
+ */
155
+ getCart(): Promise<Cart | null>;
156
+ /**
157
+ * Subscribe to `cart-expired` events fired when the runner bails or recreate
158
+ * fails. Returns an unsubscribe function. Multiple subscribers are supported.
159
+ */
160
+ onExpired(listener: (event: CartExpiredEvent) => void): () => void;
161
+ /** Underlying cart client (for advanced flows / read-only helpers). */
162
+ readonly cartClient: CartClient;
163
+ /** Underlying cookie store (for advanced flows). */
164
+ readonly cookieStore: CartCookieStore;
165
+ }
166
+ export interface CreateCartRecoveryRunnerOptions {
167
+ cartClient: CartClient;
168
+ cookieStore: CartCookieStore;
169
+ /** Initial-create payload override (default: empty cart). */
170
+ ensureCart?: () => Promise<Cart>;
171
+ /** Cookie `maxAge` propagated to all `cookieStore.set` calls. */
172
+ cookieMaxAge?: number;
173
+ }
174
+ /**
175
+ * Build a per-shop-session runner that shares a recovery coordinator across
176
+ * concurrent operations and exposes an `onExpired` listener pattern.
177
+ */
178
+ export declare function createCartRecoveryRunner(options: CreateCartRecoveryRunnerOptions): CartRecoveryRunner;
179
+ /**
180
+ * Build a `recreateAndRun` for any operation expressible as a single
181
+ * `cartCreate(input)` call. The fresh cart's outcome (`{ cart, warnings }`) is
182
+ * returned as `result`, matching what the equivalent post-create write would
183
+ * have returned — caller code stays uniform between happy and recovery paths.
184
+ *
185
+ * Use for: `addItems` (lines), `updateBuyerIdentity` (buyerIdentity),
186
+ * `setShippingAddress` (shippingAddress), `updateDiscountCodes` (discountCodes),
187
+ * `updateNote` (note), or any combination.
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * const operation: CartRecoveryOperation<CartMutationOutcome> = {
192
+ * run: (cartId) => cartClient.addItems(cartId, lines),
193
+ * recreateAndRun: recreateWithInput({ lines }),
194
+ * name: 'addItems',
195
+ * };
196
+ * ```
197
+ */
198
+ export declare function recreateWithInput(input: CartCreateInput): (cartClient: CartClient) => Promise<{
199
+ cart: Cart;
200
+ result: CartMutationOutcome;
201
+ }>;
202
+ /**
203
+ * Specialized alias for the most common case (`addItems` recovery).
204
+ * Equivalent to `recreateWithInput({ lines, ...extraInput })`.
205
+ */
206
+ export declare function recreateWithLines(lines: NonNullable<CartCreateInput['lines']>, extraInput?: Omit<CartCreateInput, 'lines'>): (cartClient: CartClient) => Promise<{
207
+ cart: Cart;
208
+ result: CartMutationOutcome;
209
+ }>;
210
+ //# sourceMappingURL=cart-recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-recovery.d.ts","sourceRoot":"","sources":["../../../src/core/cart/cart-recovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Cart recovery — DX-first auto-recovery for stale cart errors.
3
+ *
4
+ * Backend cart write operations enforce stricter checks than reads. Cart can
5
+ * return successfully from `cart(id)` query while `cartAddLines(id)` rejects with
6
+ * `userErrors[].code = 'CART_NOT_FOUND'` (ownership/lifetime drift after read).
7
+ * Same happens after checkout: cart transitions to CONVERTED status → next write
8
+ * yields `ALREADY_COMPLETED`.
9
+ *
10
+ * This module gives every SDK consumer (React, Vue, Svelte, mobile, CLI) the
11
+ * same automatic recovery without each integrator reimplementing detection,
12
+ * cookie cleanup, and retry orchestration.
13
+ *
14
+ * The runner classifies operations:
15
+ *
16
+ * - **Auto-replay** — operation provides `recreateAndRun` (atomic create-and-run
17
+ * against a fresh cart). Storefront caller sees the operation succeed; the
18
+ * swap is invisible. Typical fit: `addItems` via `cartCreate({ lines })`.
19
+ *
20
+ * - **Bail + event** — operation has no `recreateAndRun`, i.e. it references
21
+ * state from the dead cart (lineIds for update/remove, shippingMethodId for
22
+ * select). Replaying on an empty cart would silently lose user intent, so
23
+ * the runner clears the cookie, emits `cart-expired`, and throws
24
+ * `CartRecoveryNotPossibleError`. UI subscribes once globally and shows a
25
+ * toast / banner — caller code never writes `try / catch` per mutation.
26
+ *
27
+ * Detection is **code-based** (`err.userErrors[].code`) not message-based —
28
+ * resilient to PL/EN/future locales. Concurrent operations share one in-flight
29
+ * recovery promise so a double-click never produces two orphan carts.
30
+ *
31
+ * Core stays 0-deps: cookie I/O is injected via `CartCookieStore` port (caller
32
+ * provides browser / Next.js server / AsyncStorage / in-memory implementation).
33
+ */
34
+ import { StorefrontError } from '../errors';
35
+ // ---------------------------------------------------------------------------
36
+ // Error codes — must mirror backend CartErrorCode enum (guarded by drift test)
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * Cart error codes that require recreation of the cart resource.
40
+ *
41
+ * - `CART_NOT_FOUND` — cart expired, missing, or owner mismatch (backend masks
42
+ * `UNAUTHORIZED` as `CART_NOT_FOUND` to avoid leaking ownership info).
43
+ * - `ALREADY_COMPLETED` — cart status not in `{ACTIVE, RECOVERED}` (CONVERTED
44
+ * after checkout, EXPIRED via TTL, or ABANDONED by purge worker).
45
+ *
46
+ * Other codes (`NOT_ENOUGH_IN_STOCK`, validation, fulfillment) are recoverable
47
+ * by user input, not by cart recreation — they propagate unchanged.
48
+ */
49
+ export const CART_RECOVERABLE_ERROR_CODES = ['CART_NOT_FOUND', 'ALREADY_COMPLETED'];
50
+ const RECOVERABLE_CODE_SET = new Set(CART_RECOVERABLE_ERROR_CODES);
51
+ // ---------------------------------------------------------------------------
52
+ // Detection — code-based, locale-proof
53
+ // ---------------------------------------------------------------------------
54
+ /**
55
+ * Type-safe predicate for "this error means the cart resource is gone".
56
+ *
57
+ * Inspects `err.userErrors[].code` (structured field) — never matches against
58
+ * `err.message` (locale-dependent, non-stable). Returns `true` only when a
59
+ * `StorefrontError` carries at least one userError with a recoverable code.
60
+ */
61
+ export function isCartRecoverableError(err) {
62
+ if (!(err instanceof StorefrontError))
63
+ return false;
64
+ if (err.userErrors.length === 0)
65
+ return false;
66
+ return err.userErrors.some((ue) => typeof ue.code === 'string' && RECOVERABLE_CODE_SET.has(ue.code));
67
+ }
68
+ /**
69
+ * Thrown when an operation hits a recoverable cart error but recovery is not
70
+ * possible (no `recreateAndRun`, or recovery itself failed). UI should clear
71
+ * local cart state and prompt the user to start over.
72
+ */
73
+ export class CartRecoveryNotPossibleError extends Error {
74
+ reason;
75
+ cause;
76
+ constructor(reason, cause, message) {
77
+ super(message ?? defaultMessageFor(reason), { cause });
78
+ this.name = 'CartRecoveryNotPossibleError';
79
+ this.reason = reason;
80
+ this.cause = cause;
81
+ }
82
+ }
83
+ function defaultMessageFor(reason) {
84
+ switch (reason) {
85
+ case 'state-dependent':
86
+ return 'Cart expired and this operation cannot be safely replayed on a new cart';
87
+ case 'recreate-failed':
88
+ return 'Cart expired and creating a replacement cart failed';
89
+ case 'retry-also-failed':
90
+ return 'Cart expired; the replacement cart was also rejected by the backend';
91
+ }
92
+ }
93
+ function createCoordinator() {
94
+ return { inFlight: null };
95
+ }
96
+ async function acquireCartId(coord, factory) {
97
+ if (coord.inFlight)
98
+ return coord.inFlight;
99
+ const p = factory().finally(() => {
100
+ if (coord.inFlight === p)
101
+ coord.inFlight = null;
102
+ });
103
+ coord.inFlight = p;
104
+ return p;
105
+ }
106
+ /**
107
+ * Runs an operation against the current cart, recovering once on
108
+ * `CART_NOT_FOUND` / `ALREADY_COMPLETED`. Pure async function — non-React
109
+ * consumers can wire this directly without the runner factory.
110
+ */
111
+ export async function executeWithCartRecovery(opts) {
112
+ const { cartClient, cookieStore, operation, ensureCart, cookieMaxAge, onExpired } = opts;
113
+ const coord = opts.recoveryCoordinator ?? createCoordinator();
114
+ const opName = operation.name ?? 'unknown';
115
+ // Phase 0 — ensure cart exists (cookie may be empty on first interaction).
116
+ let cartId = cookieStore.get();
117
+ if (!cartId) {
118
+ cartId = await acquireCartId(coord, async () => {
119
+ const created = ensureCart ? await ensureCart() : (await cartClient.create()).cart;
120
+ cookieStore.set(created.id, cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : undefined);
121
+ return created.id;
122
+ });
123
+ }
124
+ // Phase 1 — happy path.
125
+ try {
126
+ return await operation.run(cartId);
127
+ }
128
+ catch (err) {
129
+ if (!isCartRecoverableError(err))
130
+ throw err;
131
+ const oldCartId = cartId;
132
+ // Phase 2a — bail path (operation has no replay strategy).
133
+ if (!operation.recreateAndRun) {
134
+ cookieStore.clear();
135
+ const event = {
136
+ reason: 'state-dependent',
137
+ oldCartId,
138
+ operation: opName,
139
+ cause: err,
140
+ };
141
+ onExpired?.(event);
142
+ throw new CartRecoveryNotPossibleError('state-dependent', err);
143
+ }
144
+ // Phase 2b — atomic recreate via operation strategy.
145
+ // Concurrent recoveries are intentionally NOT deduplicated here: each
146
+ // operation carries its own payload (different lines / address / etc.) and
147
+ // merging them would silently drop one. The Phase 0 mutex covers the much
148
+ // more common collision (multiple components mounting and observing an
149
+ // empty cookie at the same time). If two operations recover concurrently
150
+ // one new cart per operation is created — the last `cookieStore.set` wins,
151
+ // matching the natural "last-write" semantics of cookies.
152
+ let result;
153
+ try {
154
+ cookieStore.clear();
155
+ const recreated = await operation.recreateAndRun(cartClient);
156
+ cookieStore.set(recreated.cart.id, cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : undefined);
157
+ result = recreated.result;
158
+ }
159
+ catch (recreateErr) {
160
+ // If recreate itself returned a recoverable user error, treat as
161
+ // retry-also-failed — backend is rejecting fresh carts somehow, bail.
162
+ const reason = isCartRecoverableError(recreateErr)
163
+ ? 'retry-also-failed'
164
+ : 'recreate-failed';
165
+ const event = {
166
+ reason,
167
+ oldCartId,
168
+ operation: opName,
169
+ cause: recreateErr,
170
+ };
171
+ onExpired?.(event);
172
+ throw new CartRecoveryNotPossibleError(reason, recreateErr);
173
+ }
174
+ return result;
175
+ }
176
+ }
177
+ /**
178
+ * Build a per-shop-session runner that shares a recovery coordinator across
179
+ * concurrent operations and exposes an `onExpired` listener pattern.
180
+ */
181
+ export function createCartRecoveryRunner(options) {
182
+ const { cartClient, cookieStore, ensureCart, cookieMaxAge } = options;
183
+ const coordinator = createCoordinator();
184
+ const listeners = new Set();
185
+ function emit(event) {
186
+ for (const listener of listeners) {
187
+ try {
188
+ listener(event);
189
+ }
190
+ catch {
191
+ // Listeners must not break recovery flow — swallow listener exceptions.
192
+ }
193
+ }
194
+ }
195
+ return {
196
+ cartClient,
197
+ cookieStore,
198
+ async execute(operation) {
199
+ const internalOpts = {
200
+ cartClient,
201
+ cookieStore,
202
+ operation,
203
+ ensureCart,
204
+ cookieMaxAge,
205
+ recoveryCoordinator: coordinator,
206
+ onExpired: emit,
207
+ };
208
+ return executeWithCartRecovery(internalOpts);
209
+ },
210
+ async getCart() {
211
+ const cartId = cookieStore.get();
212
+ if (!cartId)
213
+ return null;
214
+ try {
215
+ return await cartClient.get(cartId);
216
+ }
217
+ catch (err) {
218
+ if (isCartRecoverableError(err)) {
219
+ cookieStore.clear();
220
+ emit({
221
+ reason: 'state-dependent',
222
+ oldCartId: cartId,
223
+ operation: 'getCart',
224
+ cause: err,
225
+ });
226
+ return null;
227
+ }
228
+ throw err;
229
+ }
230
+ },
231
+ onExpired(listener) {
232
+ listeners.add(listener);
233
+ return () => listeners.delete(listener);
234
+ },
235
+ };
236
+ }
237
+ // ---------------------------------------------------------------------------
238
+ // Helpers for common recreate strategies (DRY for SDK + template code)
239
+ // ---------------------------------------------------------------------------
240
+ /**
241
+ * Build a `recreateAndRun` for any operation expressible as a single
242
+ * `cartCreate(input)` call. The fresh cart's outcome (`{ cart, warnings }`) is
243
+ * returned as `result`, matching what the equivalent post-create write would
244
+ * have returned — caller code stays uniform between happy and recovery paths.
245
+ *
246
+ * Use for: `addItems` (lines), `updateBuyerIdentity` (buyerIdentity),
247
+ * `setShippingAddress` (shippingAddress), `updateDiscountCodes` (discountCodes),
248
+ * `updateNote` (note), or any combination.
249
+ *
250
+ * @example
251
+ * ```typescript
252
+ * const operation: CartRecoveryOperation<CartMutationOutcome> = {
253
+ * run: (cartId) => cartClient.addItems(cartId, lines),
254
+ * recreateAndRun: recreateWithInput({ lines }),
255
+ * name: 'addItems',
256
+ * };
257
+ * ```
258
+ */
259
+ export function recreateWithInput(input) {
260
+ return async (cartClient) => {
261
+ const outcome = await cartClient.create(input);
262
+ return { cart: outcome.cart, result: outcome };
263
+ };
264
+ }
265
+ /**
266
+ * Specialized alias for the most common case (`addItems` recovery).
267
+ * Equivalent to `recreateWithInput({ lines, ...extraInput })`.
268
+ */
269
+ export function recreateWithLines(lines, extraInput = {}) {
270
+ return recreateWithInput({ ...extraInput, lines });
271
+ }
@@ -48,6 +48,8 @@ export { StorefrontError, ErrorCodes, type StorefrontErrorOptions } from './erro
48
48
  export { cacheNone, cacheShort, cacheLong, cachePrivate, cacheCustom, generateCacheControlHeader, type CacheOverrides, } from './cache';
49
49
  export { CartClient } from './cart/cart-client';
50
50
  export type { CartMutationOutcome, CartCompleteOutcome } from './cart/cart-client';
51
+ export { CART_RECOVERABLE_ERROR_CODES, isCartRecoverableError, executeWithCartRecovery, createCartRecoveryRunner, CartRecoveryNotPossibleError, recreateWithInput, recreateWithLines, } from './cart/cart-recovery';
52
+ export type { CartRecoverableErrorCode, CartCookieStore, CartRecoveryOperation, CartRecoveryFailureReason, CartExpiredEvent, CartRecoveryRunner, CreateCartRecoveryRunnerOptions, ExecuteWithCartRecoveryOptions, } from './cart/cart-recovery';
51
53
  export type { Cart, CartLine, CartLineEdge, CartLineConnection, ProductVariant, ProductVariantWeight, ImageThumbnail, PageInfo, AttributeSelection, CartLineCost, CartCost, CartBuyerIdentity, CartDiscountCode, CartDiscountAllocation, CartLineInput, CartLineUpdateInput, CartCreateInput, CartBuyerIdentityInput, CartAttributeSelectionInput, SelectedOption, Money, CartAddressInput, CartSetShippingAddressInput, CartSetBillingAddressInput, CartSelectShippingMethodInput, CartSelectPaymentMethodInput, CartApplyGiftCardInput, CartRemoveGiftCardInput, CartUpdateGiftCardRecipientInput, CartCompleteInput, CartShippingMethod, CartAppliedGiftCard, CartSelectedPaymentMethod, PaymentMethodType, CartWarning, Order, PaymentInitiationFlow, PaymentErrorCode, PaymentSession, PaymentCreateInput, DiscountValidationResult, DiscountInfo, DiscountValidationError, DiscountErrorCode, DiscountApplicationType, } from './cart/types';
52
54
  export { AuthClient } from './auth/auth-client';
53
55
  export type { Customer, CustomerAccessToken, MailingAddress, AuthResult, CustomerCreateInput, } from './auth/types';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"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"}
@@ -52,6 +52,8 @@ export { StorefrontError, ErrorCodes } from './errors';
52
52
  export { cacheNone, cacheShort, cacheLong, cachePrivate, cacheCustom, generateCacheControlHeader, } from './cache';
53
53
  // Cart client
54
54
  export { CartClient } from './cart/cart-client';
55
+ // Cart recovery (stale cart auto-recovery — works without React)
56
+ export { CART_RECOVERABLE_ERROR_CODES, isCartRecoverableError, executeWithCartRecovery, createCartRecoveryRunner, CartRecoveryNotPossibleError, recreateWithInput, recreateWithLines, } from './cart/cart-recovery';
55
57
  // Auth client
56
58
  export { AuthClient } from './auth/auth-client';
57
59
  // Helpers
@@ -23,6 +23,27 @@ export declare function setCookie(name: string, value: string, options?: {
23
23
  * Delete cookie (client-side only).
24
24
  */
25
25
  export declare function deleteCookie(name: string, path?: string): void;
26
+ import type { CartCookieStore } from '../core/cart/cart-recovery';
27
+ /**
28
+ * Build a browser-side `CartCookieStore` backed by `document.cookie`.
29
+ *
30
+ * SSR-safe: when `document` is unavailable (Node / Edge SSR) `get` returns null
31
+ * and `set`/`clear` are no-ops — the runner falls back to creating a cart on
32
+ * first client-side interaction.
33
+ *
34
+ * Defaults: `maxAge = CART_COOKIE_MAX_AGE` (30 days), `samesite=lax`, `path=/`,
35
+ * `secure` auto-detected from `location.protocol`. Pass per-call `maxAge` via
36
+ * `set(cartId, { maxAge })` if needed.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * const runner = createCartRecoveryRunner({
41
+ * cartClient,
42
+ * cookieStore: createBrowserCartCookieStore(),
43
+ * });
44
+ * ```
45
+ */
46
+ export declare function createBrowserCartCookieStore(): CartCookieStore;
26
47
  /**
27
48
  * Get preferred currency from cookie (async — works with Next.js cookies()).
28
49
  * Falls back to document.cookie on client.
@@ -1 +1 @@
1
- {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/react/cookies.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpF,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAM,GAAG,IAAI,CAG3D;AAKD;;;GAGG;AACH,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYvE"}
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/react/cookies.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpF,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAM,GAAG,IAAI,CAG3D;AAID,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,4BAA4B,IAAI,eAAe,CAQ9D;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYvE"}
@@ -36,7 +36,35 @@ export function deleteCookie(name, path = '/') {
36
36
  document.cookie = `${name}=;max-age=0;path=${path}`;
37
37
  }
38
38
  import { CURRENCY_COOKIE_NAME } from '../core/currency/cookie-config';
39
- import { CART_COOKIE_NAME } from '../core/cart/cookie-config';
39
+ import { CART_COOKIE_NAME, CART_COOKIE_MAX_AGE } from '../core/cart/cookie-config';
40
+ /**
41
+ * Build a browser-side `CartCookieStore` backed by `document.cookie`.
42
+ *
43
+ * SSR-safe: when `document` is unavailable (Node / Edge SSR) `get` returns null
44
+ * and `set`/`clear` are no-ops — the runner falls back to creating a cart on
45
+ * first client-side interaction.
46
+ *
47
+ * Defaults: `maxAge = CART_COOKIE_MAX_AGE` (30 days), `samesite=lax`, `path=/`,
48
+ * `secure` auto-detected from `location.protocol`. Pass per-call `maxAge` via
49
+ * `set(cartId, { maxAge })` if needed.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * const runner = createCartRecoveryRunner({
54
+ * cartClient,
55
+ * cookieStore: createBrowserCartCookieStore(),
56
+ * });
57
+ * ```
58
+ */
59
+ export function createBrowserCartCookieStore() {
60
+ return {
61
+ get: () => getCookie(CART_COOKIE_NAME),
62
+ set: (cartId, options) => {
63
+ setCookie(CART_COOKIE_NAME, cartId, { maxAge: options?.maxAge ?? CART_COOKIE_MAX_AGE });
64
+ },
65
+ clear: () => deleteCookie(CART_COOKIE_NAME),
66
+ };
67
+ }
40
68
  /**
41
69
  * Get preferred currency from cookie (async — works with Next.js cookies()).
42
70
  * Falls back to document.cookie on client.