@doswiftly/storefront-sdk 11.0.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.
@@ -1 +1 @@
1
- {"version":3,"file":"cart.store.d.ts","sourceRoot":"","sources":["../../../src/react/stores/cart.store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAKhF,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAMhF,gDAAgD;AAChD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,+DAA+D;IAC/D,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACxD,iDAAiD;IACjD,UAAU,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,oDAAoD;IACpD,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxE,4DAA4D;IAC5D,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjF,uDAAuD;IACvD,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CACvE;AAMD,iDAAiD;AACjD,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,WAAW,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;AAEhG,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,UAAU,EAAE,MAAM,WAAW,CAAC;IAC9B,wCAAwC;IACxC,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACzE,gCAAgC;IAChC,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACxE;AAMD,MAAM,WAAW,SAAS;IAExB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAGtB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,IAAI,CAAC;IAGvB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,SAAS,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,cAAc,EAAE,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAMD,eAAO,MAAM,YAAY,GAAI,OAAO,SAAS,kBAAiB,CAAC;AAC/D,eAAO,MAAM,gBAAgB,GAAI,OAAO,SAAS,YAAiB,CAAC;AACnE,eAAO,MAAM,mBAAmB,GAAI,OAAO,SAAS,YAAoB,CAAC;AAMzE,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,yCAkJtD"}
1
+ {"version":3,"file":"cart.store.d.ts","sourceRoot":"","sources":["../../../src/react/stores/cart.store.ts"],"names":[],"mappings":"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"}
@@ -1,27 +1,49 @@
1
1
  /**
2
- * Cart Store — DI-based cart state management with cookie persistence.
2
+ * Cart Store — DI-based cart state management with cookie persistence and
3
+ * automatic stale-cart recovery.
3
4
  *
4
- * SDK orchestrates cart lifecycle (init, mutations, error handling).
5
- * Template provides CartActions implementation via DI (getActions getter).
6
- * Cart ID persisted in cookie (SSR/edge visible) — follows currency store pattern.
5
+ * SDK orchestrates cart lifecycle (init, mutations, recovery, error handling).
6
+ * Template provides `CartActions` via DI (getActions getter). Cart id persisted
7
+ * in cookie (SSR/edge visible) — follows currency store pattern.
8
+ *
9
+ * Per-operation recovery strategy (DX-first — caller never thinks about it):
10
+ *
11
+ * - **`addToCart`** auto-replays on stale-cart errors. If the template
12
+ * implements the optional `actions.createCartWithLines`, recovery is atomic
13
+ * (single `cartCreate({ lines })` round trip). Otherwise falls back to
14
+ * `createCart()` + `addLines()` (2 round trips, same end result).
15
+ *
16
+ * - **`updateQuantity`** and **`removeFromCart`** bail on stale-cart errors:
17
+ * local cart id is cleared, `onExpired` listeners fire, error surfaces via
18
+ * `onMutationError`. Replaying on a fresh empty cart would silently lose
19
+ * user intent (the lineId no longer exists).
20
+ *
21
+ * Stale-cart detection inspects `err.userErrors[].code` (CART_NOT_FOUND /
22
+ * ALREADY_COMPLETED) — locale-proof, see {@link isCartRecoverableError}.
7
23
  *
8
24
  * @example
9
25
  * ```typescript
10
26
  * import { createCartStore, type CartActions } from '@doswiftly/storefront-sdk/react';
11
27
  *
12
28
  * const actions: CartActions = {
13
- * fetchCart: async (cartId) => api.getCart(cartId),
14
- * createCart: async () => api.createCart().then(c => c.id),
15
- * addLines: async (cartId, lines) => api.addLines(cartId, lines),
16
- * updateLines: async (cartId, lines) => api.updateLines(cartId, lines),
17
- * removeLines: async (cartId, lineIds) => api.removeLines(cartId, lineIds),
29
+ * fetchCart: (id) => api.getCart(id),
30
+ * createCart: () => api.createCart().then(c => c.id),
31
+ * addLines: (id, lines) => api.addLines(id, lines),
32
+ * updateLines: (id, lines) => api.updateLines(id, lines),
33
+ * removeLines: (id, ids) => api.removeLines(id, ids),
34
+ * // optional — enables atomic add-to-cart recovery
35
+ * createCartWithLines: (lines) => api.cartCreate({ lines }),
18
36
  * };
19
37
  *
20
- * const store = createCartStore({ getActions: () => actions });
38
+ * const store = createCartStore({
39
+ * getActions: () => actions,
40
+ * onExpired: (e) => toast.error('Koszyk wygasł, dodaj produkty ponownie'),
41
+ * });
21
42
  * ```
22
43
  */
23
44
  import { createStore } from 'zustand/vanilla';
24
45
  import { CART_COOKIE_NAME, CART_COOKIE_MAX_AGE } from '../../core/cart/cookie-config';
46
+ import { isCartRecoverableError, } from '../../core/cart/cart-recovery';
25
47
  import { getCookie, setCookie, deleteCookie } from '../cookies';
26
48
  // ---------------------------------------------------------------------------
27
49
  // Selectors
@@ -33,7 +55,10 @@ export const selectCartIsLoading = (state) => state.isLoading;
33
55
  // Factory
34
56
  // ---------------------------------------------------------------------------
35
57
  export function createCartStore(config) {
36
- // Deduplication: shared Promise in closure (not in state)
58
+ // Deduplication for initCart only first-create collision is the common
59
+ // case (multiple components mounting and seeing an empty cookie). Recovery
60
+ // recreates are NOT deduplicated: each carries its own lines payload and
61
+ // merging would silently drop the second caller's intent.
37
62
  let initPromise = null;
38
63
  // Read initial cartId from cookie (client-side only, returns null on server)
39
64
  const initialCartId = getCookie(CART_COOKIE_NAME);
@@ -44,6 +69,25 @@ export function createCartStore(config) {
44
69
  }
45
70
  catch { /* ignore */ }
46
71
  }
72
+ function emitExpired(event) {
73
+ if (!config.onExpired)
74
+ return;
75
+ try {
76
+ config.onExpired(event);
77
+ }
78
+ catch {
79
+ // Listener must not break recovery flow.
80
+ }
81
+ }
82
+ async function recreateWithLines(lines) {
83
+ const actions = config.getActions();
84
+ if (actions.createCartWithLines) {
85
+ return actions.createCartWithLines(lines);
86
+ }
87
+ // Fallback: two-step (create empty cart, then add lines)
88
+ const newCartId = await actions.createCart();
89
+ return actions.addLines(newCartId, lines);
90
+ }
47
91
  async function performInit(set, get) {
48
92
  const actions = config.getActions();
49
93
  set({ isLoading: true, error: null });
@@ -86,11 +130,12 @@ export function createCartStore(config) {
86
130
  });
87
131
  return initPromise;
88
132
  },
89
- // Orchestrated: addToCart (auto-init if no cartId)
133
+ // Orchestrated: addToCart auto-replay on stale cart.
90
134
  addToCart: async (lines) => {
91
135
  const actions = config.getActions();
92
136
  set({ isLoading: true, error: null });
93
137
  try {
138
+ // Phase 0 — ensure cart exists.
94
139
  let cartId = get().cartId;
95
140
  if (!cartId) {
96
141
  await get().initCart();
@@ -99,16 +144,45 @@ export function createCartStore(config) {
99
144
  if (!cartId) {
100
145
  throw new Error('Failed to initialize cart');
101
146
  }
102
- const cart = await actions.addLines(cartId, lines);
103
- set({ isLoading: false, error: null });
104
- config.onMutationSuccess?.('addToCart', cart);
147
+ // Phase 1 happy path.
148
+ try {
149
+ const cart = await actions.addLines(cartId, lines);
150
+ set({ isLoading: false, error: null });
151
+ config.onMutationSuccess?.('addToCart', cart);
152
+ return;
153
+ }
154
+ catch (err) {
155
+ if (!isCartRecoverableError(err))
156
+ throw err;
157
+ // Phase 2 — auto-replay against a fresh cart.
158
+ const oldCartId = cartId;
159
+ try {
160
+ const cart = await recreateWithLines(lines);
161
+ set({ cartId: cart.id, isLoading: false, error: null });
162
+ config.onMutationSuccess?.('addToCart', cart);
163
+ }
164
+ catch (recoverErr) {
165
+ const reason = isCartRecoverableError(recoverErr)
166
+ ? 'retry-also-failed'
167
+ : 'recreate-failed';
168
+ // Clear local cart id so next interaction starts clean.
169
+ set({ cartId: null, isLoading: false, error: recoverErr });
170
+ config.onMutationError?.('addToCart', recoverErr);
171
+ emitExpired({
172
+ reason,
173
+ oldCartId,
174
+ operation: 'addToCart',
175
+ cause: recoverErr,
176
+ });
177
+ }
178
+ }
105
179
  }
106
180
  catch (error) {
107
181
  set({ error, isLoading: false });
108
182
  config.onMutationError?.('addToCart', error);
109
183
  }
110
184
  },
111
- // Orchestrated: updateQuantity (error if no cartId)
185
+ // Orchestrated: updateQuantity — bail on stale cart (lineId is dead).
112
186
  updateQuantity: async (lines) => {
113
187
  const actions = config.getActions();
114
188
  const cartId = get().cartId;
@@ -125,11 +199,24 @@ export function createCartStore(config) {
125
199
  config.onMutationSuccess?.('updateQuantity', cart);
126
200
  }
127
201
  catch (error) {
202
+ if (isCartRecoverableError(error)) {
203
+ // Clear local cart — replaying on a fresh cart would lose the user's
204
+ // intent (the lineId no longer exists).
205
+ set({ cartId: null, isLoading: false, error });
206
+ config.onMutationError?.('updateQuantity', error);
207
+ emitExpired({
208
+ reason: 'state-dependent',
209
+ oldCartId: cartId,
210
+ operation: 'updateQuantity',
211
+ cause: error,
212
+ });
213
+ return;
214
+ }
128
215
  set({ error, isLoading: false });
129
216
  config.onMutationError?.('updateQuantity', error);
130
217
  }
131
218
  },
132
- // Orchestrated: removeFromCart (silent return if no cartId)
219
+ // Orchestrated: removeFromCart bail on stale cart (lineId is dead).
133
220
  removeFromCart: async (lineIds) => {
134
221
  const cartId = get().cartId;
135
222
  if (!cartId)
@@ -142,6 +229,17 @@ export function createCartStore(config) {
142
229
  config.onMutationSuccess?.('removeFromCart', cart);
143
230
  }
144
231
  catch (error) {
232
+ if (isCartRecoverableError(error)) {
233
+ set({ cartId: null, isLoading: false, error });
234
+ config.onMutationError?.('removeFromCart', error);
235
+ emitExpired({
236
+ reason: 'state-dependent',
237
+ oldCartId: cartId,
238
+ operation: 'removeFromCart',
239
+ cause: error,
240
+ });
241
+ return;
242
+ }
145
243
  set({ error, isLoading: false });
146
244
  config.onMutationError?.('removeFromCart', error);
147
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-sdk",
3
- "version": "11.0.0",
3
+ "version": "11.2.0",
4
4
  "description": "Storefront runtime SDK for DoSwiftly Commerce — layered transport, middleware pipeline, React providers, Zustand stores, cache strategies. 0 runtime dependencies in core.",
5
5
  "type": "module",
6
6
  "files": [
@@ -39,16 +39,19 @@
39
39
  ],
40
40
  "author": "DoSwiftly Team",
41
41
  "license": "MIT",
42
- "dependencies": {},
43
42
  "devDependencies": {
43
+ "@testing-library/react": "^16.1.0",
44
44
  "@types/node": "^22.10.2",
45
45
  "@types/react": "^18.3.0 || ^19.0.0",
46
+ "@types/react-dom": "^19.0.0",
46
47
  "fast-check": "^3.23.2",
48
+ "jsdom": "^25.0.1",
47
49
  "next": "^16.2.3",
50
+ "react": "^19.0.0",
51
+ "react-dom": "^19.0.0",
48
52
  "typescript": "^5.7.2",
49
53
  "vitest": "^4.1.0",
50
- "zustand": "^5.0.2",
51
- "react": "^19.0.0"
54
+ "zustand": "^5.0.2"
52
55
  },
53
56
  "peerDependencies": {
54
57
  "react": "^18.0.0 || ^19.0.0",