@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.
- package/CHANGELOG.md +63 -0
- package/README.md +83 -0
- package/dist/__tests__/unit/use-cart-manager.test.d.ts +2 -0
- package/dist/__tests__/unit/use-cart-manager.test.d.ts.map +1 -0
- package/dist/__tests__/unit/use-cart-manager.test.js +400 -0
- package/dist/core/cart/cart-client.d.ts +19 -1
- package/dist/core/cart/cart-client.d.ts.map +1 -1
- package/dist/core/cart/cart-client.js +23 -1
- package/dist/core/cart/cart-recovery.d.ts +210 -0
- package/dist/core/cart/cart-recovery.d.ts.map +1 -0
- package/dist/core/cart/cart-recovery.js +271 -0
- package/dist/core/cart/types.d.ts +47 -0
- package/dist/core/cart/types.d.ts.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/operations/cart.d.ts +7 -0
- package/dist/core/operations/cart.d.ts.map +1 -1
- package/dist/core/operations/cart.js +28 -0
- package/dist/react/cookies.d.ts +21 -0
- package/dist/react/cookies.d.ts.map +1 -1
- package/dist/react/cookies.js +29 -1
- package/dist/react/hooks/use-cart-manager.d.ts +41 -15
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +96 -187
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/stores/cart.store.d.ts +57 -10
- package/dist/react/stores/cart.store.d.ts.map +1 -1
- package/dist/react/stores/cart.store.js +115 -17
- package/package.json +7 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cart.store.d.ts","sourceRoot":"","sources":["../../../src/react/stores/cart.store.ts"],"names":[],"mappings":"AAAA
|
|
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
|
|
6
|
-
*
|
|
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:
|
|
14
|
-
* createCart:
|
|
15
|
-
* addLines:
|
|
16
|
-
* updateLines:
|
|
17
|
-
* removeLines:
|
|
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({
|
|
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
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 (
|
|
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
|
|
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.
|
|
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",
|