@caldik/client 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  # Caldik SDK
2
-
3
- The official Caldik SDK for interacting with Caldik storefronts, products, carts, checkouts, and customer accounts.
2
+ TThe official Caldik SDK for interacting with Caldik storefronts, products, carts, checkouts, and customer accounts.
4
3
 
5
4
  ## Installation
6
5
 
@@ -37,9 +36,48 @@ const caldik = new CaldikClient({
37
36
 
38
37
  > **Security:** Private API keys must only be used in trusted server-side environments. Never expose `sk_` keys in client-side code.
39
38
 
39
+
40
40
  ## Orders and Items
41
41
 
42
- ### Categories
42
+ ### List Products
43
+
44
+ Retrieve products associated with the current store.
45
+
46
+ **Basic usage**
47
+
48
+ ```ts
49
+ const { products } = await caldik.products.list()
50
+ ```
51
+
52
+ **Limit results**
53
+
54
+ Pass a number to limit the number of products returned.
55
+
56
+ ```ts
57
+ const { products } = await caldik.products.list(20)
58
+ ```
59
+
60
+ ### Get Product
61
+
62
+ Retrieve a specific product by its ID.
63
+
64
+ **Basic usage**
65
+
66
+ ```ts
67
+ const { product } = await caldik.products.get("product_123")
68
+ ```
69
+
70
+ ### Get Category
71
+
72
+ Retrieve a specific category by its ID.
73
+
74
+ **Basic usage**
75
+
76
+ ```ts
77
+ const { category } = await caldik.categories.get("CAT_ID")
78
+ ```
79
+
80
+ ### List Categories
43
81
 
44
82
  Retrieve categories associated with the current store.
45
83
 
@@ -68,6 +106,8 @@ const { categories } = await caldik.categories.list({
68
106
  })
69
107
  ```
70
108
 
109
+ **Optional fields**
110
+
71
111
  The following fields are omitted by default unless explicitly requested using `include`:
72
112
 
73
113
  | Field | Description |
@@ -75,16 +115,23 @@ The following fields are omitted by default unless explicitly requested using `i
75
115
  | `productCount` | Number of products associated with the category. |
76
116
  | `startingPrice` | Lowest available product price in the category, in cents. |
77
117
 
118
+
78
119
  ## Cart and Discounts
79
120
 
80
121
  ### Cart Management
81
122
 
82
- Add products to the cart, update quantities, or remove items.
123
+ **Get the cart**
124
+
125
+ Refresh the current items and applied discount from the server.
126
+
127
+ ```ts
128
+ const { items, discount } = await caldik.cart.get()
129
+ ```
83
130
 
84
131
  **Add an item**
85
132
 
86
133
  ```ts
87
- const cart = caldik.cart.add({
134
+ const cart = await caldik.cart.add({
88
135
  productId: "PRODUCT_...",
89
136
  productVariantId: "PRODUCT_VARIANT_...",
90
137
  quantity: 1,
@@ -93,30 +140,90 @@ const cart = caldik.cart.add({
93
140
 
94
141
  **Update item quantity**
95
142
 
143
+ Set an item's quantity. A quantity of zero removes it.
144
+
96
145
  ```ts
97
- caldik.cart.updateQuantity(cart.items[0].id, 3)
146
+ await caldik.cart.updateQuantity(cart.items[0].id, 3)
98
147
  ```
99
148
 
100
149
  **Remove an item**
101
150
 
102
151
  ```ts
103
- caldik.cart.remove(cart.items[0].id)
152
+ await caldik.cart.remove(cart.items[0].id)
153
+ ```
154
+
155
+ **Clear the cart**
156
+
157
+ Remove all items and the applied discount from the persisted cart.
158
+
159
+ ```ts
160
+ await caldik.cart.clear()
161
+ ```
162
+
163
+ **Check a discount**
164
+
165
+ Get a price and discount quote for the current items without applying the code.
166
+
167
+ ```ts
168
+ const quote = await caldik.cart.checkDiscount("SAVE10")
104
169
  ```
105
170
 
106
- ### Apply Discounts
171
+ **Apply a discount**
107
172
 
108
- Apply a discount code to the current cart.
173
+ Validate the code and save it to the current cart.
109
174
 
110
175
  ```ts
111
- await caldik.cart.applyDiscount("SAVE10")
176
+ const discountedCart = await caldik.cart.applyDiscount("SAVE10")
112
177
  ```
113
178
 
114
- ### Checkout
179
+ **Remove the discount**
115
180
 
116
- Create a checkout session using the current cart or explicitly provided items.
181
+ Remove the applied code while keeping the cart's items.
182
+
183
+ ```ts
184
+ await caldik.cart.removeDiscount()
185
+ ```
186
+
187
+ **Checkout the cart**
188
+
189
+ Create an order using the persisted cart. The API revalidates prices and discounts.
117
190
 
118
191
  ```ts
119
192
  const result = await caldik.cart.checkout({
193
+ paymentMethod: "stripe",
194
+ termsAccepted: acceptedTermsCheckbox,
195
+ successUrl: "https://shop.example/success",
196
+ cancelUrl: "https://shop.example/cart",
197
+ })
198
+ ```
199
+
200
+ ### Server-rendered Storefronts
201
+
202
+ Create a client for each incoming request so shopper sessions stay isolated.
203
+ Pass the request and forward returned session cookies to the outgoing response:
204
+
205
+ ```ts
206
+ const responseHeaders = new Headers()
207
+ const caldik = new CaldikClient({
208
+ request,
209
+ onSetCookie: (cookie) => responseHeaders.append("Set-Cookie", cookie),
210
+ // External servers also supply storeId and their private apiKey.
211
+ })
212
+ const cart = await caldik.cart.get()
213
+ // Include responseHeaders in your framework's HTTP response.
214
+ ```
215
+
216
+ Browser and server calls must share the same cookie context (for example through
217
+ a storefront API proxy). Cookies set only on a separate API hostname are not
218
+ included in requests to the storefront server. A private store key does not
219
+ replace a shopper's customer session.
220
+
221
+ ### Checkout with Explicit Items
222
+
223
+ Use `caldik.checkout.create()` to create an order from explicitly provided items.
224
+
225
+ ```ts
226
+ const result = await caldik.checkout.create({
120
227
  paymentMethod: "stripe",
121
228
  items: [
122
229
  {
package/dist/index.d.mts CHANGED
@@ -349,6 +349,8 @@ interface RequestOptions {
349
349
  edgeCache?: boolean;
350
350
  /** Internal authorization policy. Protected hosted calls are the default. */
351
351
  access?: "public" | "runtime";
352
+ /** Scoped credentials used by stateful storefront resources. */
353
+ headers?: Record<string, string>;
352
354
  }
353
355
  //#endregion
354
356
  //#region src/internal/client.d.ts
@@ -356,13 +358,22 @@ type HttpClientOptions = {
356
358
  apiKey?: string;
357
359
  apiUrl?: string;
358
360
  storeId?: string;
361
+ request?: Request;
362
+ onSetCookie?: (cookie: string) => void | Promise<void>;
359
363
  };
360
364
  declare class CaldikHttpClient {
361
365
  private readonly apiKey?;
362
366
  private readonly apiUrl;
363
367
  private readonly storeId?;
368
+ private readonly cookies;
369
+ private readonly onSetCookie?;
370
+ private readonly authenticationListeners;
364
371
  constructor(options?: HttpClientOptions);
365
372
  storeContextId(): string | null;
373
+ hasCartSession(): boolean;
374
+ onCustomerAuthentication(listener: () => Promise<unknown>): void;
375
+ customerAuthenticated(): Promise<void>;
376
+ private captureCookies;
366
377
  private baseUrl;
367
378
  private request;
368
379
  get<T>(path: string, params?: Record<string, string | number>, options?: RequestOptions): Promise<T>;
@@ -476,10 +487,8 @@ declare class DiscountsApi {
476
487
  }
477
488
  //#endregion
478
489
  //#region src/cart.d.ts
479
- interface CartItem {
490
+ interface CartItem extends ProductCommerceItem {
480
491
  id: string;
481
- productId: string;
482
- productVariantId?: string;
483
492
  quantity: number;
484
493
  }
485
494
  interface CartDiscount {
@@ -494,24 +503,35 @@ interface CartSnapshot {
494
503
  items: CartItem[];
495
504
  discount: CartDiscount | null;
496
505
  }
497
- type CartCheckoutInput = Omit<CreateOrderInput, "items" | "discountCode">;
506
+ type CartCheckoutInput = Omit<CreateOrderInput, "items" | "discountCode" | "customerId">;
507
+ /** One persisted cart, automatically scoped to the current shopper session. */
498
508
  declare class CartApi {
499
509
  private client;
500
- private memory;
510
+ private cache;
511
+ private queue;
512
+ private initialized;
501
513
  constructor(client: CaldikHttpClient);
502
- private storageKey;
503
- private read;
514
+ private storage;
515
+ private key;
516
+ private readCache;
517
+ private persistCache;
504
518
  private write;
505
- get(): CartSnapshot;
506
- add(item: ProductCommerceItem): CartSnapshot;
507
- updateQuantity(itemId: string, nextQuantity: number): CartSnapshot;
508
- remove(itemId: string): CartSnapshot;
509
- clear(): CartSnapshot;
510
- private commerceItems;
519
+ private enqueue;
520
+ private options;
521
+ private resolve;
522
+ private reset;
523
+ private mutate;
524
+ private save;
525
+ private quote;
526
+ get(): Promise<CartSnapshot>;
527
+ add(item: ProductCommerceItem): Promise<CartSnapshot>;
528
+ updateQuantity(itemId: string, nextQuantity: number): Promise<CartSnapshot>;
529
+ remove(itemId: string): Promise<CartSnapshot>;
530
+ clear(): Promise<CartSnapshot>;
511
531
  checkDiscount(code: string): Promise<DiscountQuoteResponse>;
512
532
  applyDiscount(code: string): Promise<CartSnapshot>;
513
- removeDiscount(): CartSnapshot;
514
- checkout(input: CartCheckoutInput): Promise<OrderResponse>;
533
+ removeDiscount(): Promise<CartSnapshot>;
534
+ checkout(input?: CartCheckoutInput): Promise<OrderResponse>;
515
535
  }
516
536
  //#endregion
517
537
  //#region src/index.d.ts
@@ -521,6 +541,10 @@ interface CaldikClientOptions {
521
541
  /** Private API key for external server integrations. Never use in browsers. */
522
542
  apiKey?: string;
523
543
  apiUrl?: string;
544
+ /** Incoming storefront request. Create one client per server request to isolate shoppers. */
545
+ request?: Request;
546
+ /** Forward session cookies to the outgoing storefront response in server code. */
547
+ onSetCookie?: (cookie: string) => void | Promise<void>;
524
548
  }
525
549
  declare class CaldikClient {
526
550
  readonly categories: CategoriesApi;
package/dist/index.mjs CHANGED
@@ -26,6 +26,14 @@ function defaultApiUrl(browser) {
26
26
  //#region src/internal/client.ts
27
27
  const STORE_ID_PATTERN = /^STORE_[a-f0-9]{32}$/i;
28
28
  const STOREFRONT_STORE_ID_ATTRIBUTE = "data-caldik-store-id";
29
+ var CaldikHttpError = class extends Error {
30
+ status;
31
+ constructor(message, status) {
32
+ super(message);
33
+ this.status = status;
34
+ this.name = "CaldikHttpError";
35
+ }
36
+ };
29
37
  function isBrowserRuntime() {
30
38
  return typeof window !== "undefined" && typeof document !== "undefined";
31
39
  }
@@ -48,9 +56,17 @@ var CaldikHttpClient = class {
48
56
  apiKey;
49
57
  apiUrl;
50
58
  storeId;
59
+ cookies = /* @__PURE__ */ new Map();
60
+ onSetCookie;
61
+ authenticationListeners = /* @__PURE__ */ new Set();
51
62
  constructor(options = {}) {
52
63
  this.apiKey = options.apiKey?.trim() || void 0;
53
64
  this.apiUrl = options.apiUrl || defaultApiUrl(isBrowserRuntime());
65
+ this.onSetCookie = options.onSetCookie;
66
+ if (!isBrowserRuntime()) for (const part of (options.request?.headers.get("Cookie") || "").split(";")) {
67
+ const separator = part.indexOf("=");
68
+ if (separator > 0) this.cookies.set(part.slice(0, separator).trim(), part.slice(separator + 1).trim());
69
+ }
54
70
  const configuredStoreId = options.storeId?.trim() || void 0;
55
71
  if (this.apiKey && !this.apiKey.startsWith("sk_")) throw new Error("Caldik external integrations require a private sk_ API key");
56
72
  if (configuredStoreId && !STORE_ID_PATTERN.test(configuredStoreId)) throw new Error("Caldik storeId is invalid");
@@ -60,6 +76,31 @@ var CaldikHttpClient = class {
60
76
  storeContextId() {
61
77
  return this.storeId || boundRuntimeStoreId();
62
78
  }
79
+ hasCartSession() {
80
+ return this.cookies.has(`caldik_cart_session_${this.storeContextId()}`);
81
+ }
82
+ onCustomerAuthentication(listener) {
83
+ this.authenticationListeners.add(listener);
84
+ }
85
+ async customerAuthenticated() {
86
+ await Promise.allSettled([...this.authenticationListeners].map((listener) => listener()));
87
+ }
88
+ async captureCookies(response) {
89
+ for (const cookie of response.headers.getSetCookie()) {
90
+ const [pair = "", ...attributes] = cookie.split(";");
91
+ const separator = pair.indexOf("=");
92
+ if (separator < 1) continue;
93
+ const name = pair.slice(0, separator).trim();
94
+ if (attributes.some((attribute) => {
95
+ const [key, ...value] = attribute.trim().split("=");
96
+ if (key?.toLowerCase() === "max-age") return Number(value.join("=")) <= 0;
97
+ if (key?.toLowerCase() === "expires") return Date.parse(value.join("=")) <= Date.now();
98
+ return false;
99
+ })) this.cookies.delete(name);
100
+ else this.cookies.set(name, pair.slice(separator + 1).trim());
101
+ await this.onSetCookie?.(cookie);
102
+ }
103
+ }
63
104
  baseUrl() {
64
105
  if (isBrowserRuntime()) {
65
106
  const storeId = this.storeId;
@@ -76,8 +117,9 @@ var CaldikHttpClient = class {
76
117
  async request(method, path, input, params, options) {
77
118
  const url = new URL(`${this.baseUrl()}${path}`);
78
119
  if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, String(value));
79
- const headers = {};
120
+ const headers = { ...options?.headers };
80
121
  if (!isBrowserRuntime()) headers["User-Agent"] = "CaldikClient/0.0.2";
122
+ if (!isBrowserRuntime() && options?.access === "public" && this.cookies.size) headers.Cookie = [...this.cookies].map(([name, value]) => `${name}=${value}`).join("; ");
81
123
  if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
82
124
  else if (options?.access === "public" && (!isBrowserRuntime() || isLocalBrowserRuntime())) headers["X-Caldik-Public-Request"] = "1";
83
125
  if (input !== void 0) headers["Content-Type"] = "application/json";
@@ -89,10 +131,10 @@ var CaldikHttpClient = class {
89
131
  cache: "default",
90
132
  credentials: "include"
91
133
  });
134
+ if (!isBrowserRuntime() && options?.access === "public") await this.captureCookies(response);
92
135
  if (!response.ok) {
93
136
  const error = await response.clone().json().catch(() => null);
94
- const message = typeof error?.message === "string" && error.message ? error.message : `${response.status} ${response.statusText}`.trim();
95
- throw new Error(message);
137
+ throw new CaldikHttpError(typeof error?.message === "string" && error.message ? error.message : `${response.status} ${response.statusText}`.trim(), response.status);
96
138
  }
97
139
  return response.json();
98
140
  }
@@ -207,7 +249,11 @@ var CustomerApi = class {
207
249
  };
208
250
  let pendingChallengeId;
209
251
  async function login(input, options) {
210
- if (typeof input !== "string" && "password" in input) return client.post("/customers/login/password", input, { access: "public" });
252
+ if (typeof input !== "string" && "password" in input) {
253
+ const response = await client.post("/customers/login/password", input, { access: "public" });
254
+ await client.customerAuthenticated();
255
+ return response;
256
+ }
211
257
  const response = await client.post("/customers/login", typeof input === "string" ? {
212
258
  email: input,
213
259
  ...options
@@ -225,6 +271,7 @@ verify: async (code, challengeId = pendingChallengeId) => {
225
271
  challengeId
226
272
  }, { access: "public" });
227
273
  if (pendingChallengeId === challengeId) pendingChallengeId = void 0;
274
+ await client.customerAuthenticated();
228
275
  return response;
229
276
  } });
230
277
  let pendingRegistrationId;
@@ -243,6 +290,7 @@ verify: async (code, challengeId = pendingRegistrationId) => {
243
290
  challengeId
244
291
  }, { access: "public" });
245
292
  if (pendingRegistrationId === challengeId) pendingRegistrationId = void 0;
293
+ await client.customerAuthenticated();
246
294
  return response;
247
295
  } });
248
296
  }
@@ -279,20 +327,18 @@ var DiscountsApi = class {
279
327
  };
280
328
  //#endregion
281
329
  //#region src/cart.ts
282
- const CART_VERSION = 1;
283
330
  const MAX_QUANTITY = 999999;
284
- function cartItemId(item) {
285
- return item.productVariantId ? `${item.productId}:${item.productVariantId}` : item.productId;
286
- }
287
- function quantity(value, allowZero = false) {
288
- const normalized = value ?? 1;
331
+ function quantity(value = 1, allowZero = false) {
289
332
  const minimum = allowZero ? 0 : 1;
290
- if (!Number.isInteger(normalized) || normalized < minimum || normalized > MAX_QUANTITY) throw new Error(`Cart quantity must be an integer between ${minimum} and ${MAX_QUANTITY}`);
291
- return normalized;
333
+ if (!Number.isInteger(value) || value < minimum || value > MAX_QUANTITY) throw new Error(`Cart quantity must be an integer between ${minimum} and ${MAX_QUANTITY}`);
334
+ return value;
292
335
  }
293
336
  function cloneSnapshot(state) {
294
337
  return {
295
- items: state.items.map((item) => ({ ...item })),
338
+ items: state.items.map((item) => ({
339
+ ...item,
340
+ ...item.customFields ? { customFields: { ...item.customFields } } : {}
341
+ })),
296
342
  discount: state.discount ? { ...state.discount } : null
297
343
  };
298
344
  }
@@ -303,140 +349,267 @@ function isCartItem(value) {
303
349
  }
304
350
  function isCartDiscount(value) {
305
351
  if (!value || typeof value !== "object") return false;
306
- const discount = value;
307
- return typeof discount.code === "string" && typeof discount.percentage === "string" && typeof discount.subtotal === "string" && typeof discount.discountAmount === "string" && typeof discount.total === "string" && typeof discount.currency === "string";
352
+ return [
353
+ "code",
354
+ "percentage",
355
+ "subtotal",
356
+ "discountAmount",
357
+ "total",
358
+ "currency"
359
+ ].every((key) => typeof value[key] === "string");
308
360
  }
361
+ function isReference(value) {
362
+ if (!value || typeof value !== "object") return false;
363
+ const reference = value;
364
+ return typeof reference.id === "string" && /^CART_[a-f0-9]{32}$/.test(reference.id) && (reference.token == null || typeof reference.token === "string");
365
+ }
366
+ function commerceItems(items) {
367
+ return items.map(({ id: _id, ...item }) => item);
368
+ }
369
+ function discountFromQuote(quote) {
370
+ return {
371
+ ...quote.discount,
372
+ subtotal: quote.subtotal,
373
+ discountAmount: quote.discountAmount,
374
+ total: quote.total,
375
+ currency: quote.currency
376
+ };
377
+ }
378
+ /** One persisted cart, automatically scoped to the current shopper session. */
309
379
  var CartApi = class {
310
380
  client;
311
- memory = {
312
- version: CART_VERSION,
313
- items: [],
314
- discount: null
381
+ cache = {
382
+ version: 2,
383
+ reference: null,
384
+ snapshot: {
385
+ items: [],
386
+ discount: null
387
+ }
315
388
  };
389
+ queue = Promise.resolve();
390
+ initialized = false;
316
391
  constructor(client) {
317
392
  this.client = client;
393
+ client.onCustomerAuthentication(() => {
394
+ this.readCache();
395
+ return this.initialized || this.cache.reference || this.cache.snapshot.items.length || client.hasCartSession() ? this.enqueue(async () => {
396
+ await this.resolve();
397
+ }) : Promise.resolve();
398
+ });
399
+ }
400
+ storage() {
401
+ try {
402
+ return typeof window !== "undefined" && typeof document !== "undefined" && typeof localStorage !== "undefined" ? localStorage : null;
403
+ } catch {
404
+ return null;
405
+ }
318
406
  }
319
- storageKey() {
320
- const storeId = this.client.storeContextId();
321
- return storeId ? `caldik:cart:${storeId}` : null;
322
- }
323
- read() {
324
- const key = this.storageKey();
325
- if (key && typeof localStorage !== "undefined") try {
326
- const value = localStorage.getItem(key);
327
- if (value) {
328
- const parsed = JSON.parse(value);
329
- if (parsed.version === CART_VERSION && Array.isArray(parsed.items) && parsed.items.every(isCartItem)) this.memory = {
330
- version: CART_VERSION,
407
+ key() {
408
+ return `caldik:cart:${this.client.storeContextId()}`;
409
+ }
410
+ readCache() {
411
+ const storage = this.storage();
412
+ if (!storage) return;
413
+ try {
414
+ const parsed = JSON.parse(storage.getItem(this.key()) || "null");
415
+ if (parsed?.version === 2) {
416
+ const snapshot = parsed.snapshot;
417
+ if (snapshot && Array.isArray(snapshot.items) && snapshot.items.every(isCartItem) && (parsed.reference === null || isReference(parsed.reference))) this.cache = {
418
+ version: 2,
419
+ reference: parsed.reference,
420
+ snapshot: cloneSnapshot({
421
+ items: snapshot.items,
422
+ discount: isCartDiscount(snapshot.discount) ? snapshot.discount : null
423
+ })
424
+ };
425
+ } else if (parsed?.version === 1 && Array.isArray(parsed.items) && parsed.items.every(isCartItem)) this.cache = {
426
+ version: 2,
427
+ reference: null,
428
+ snapshot: cloneSnapshot({
331
429
  items: parsed.items,
332
430
  discount: isCartDiscount(parsed.discount) ? parsed.discount : null
333
- };
431
+ })
432
+ };
433
+ if (!this.cache.reference) {
434
+ const legacy = JSON.parse(storage.getItem(`caldik:server-cart:${this.client.storeContextId()}`) || "null");
435
+ if (isReference(legacy)) this.cache.reference = legacy;
334
436
  }
335
437
  } catch {}
336
- return this.memory;
337
438
  }
338
- write(state) {
339
- this.memory = {
340
- version: CART_VERSION,
341
- ...cloneSnapshot(state)
342
- };
343
- const key = this.storageKey();
344
- if (key && typeof localStorage !== "undefined") try {
345
- localStorage.setItem(key, JSON.stringify(this.memory));
439
+ persistCache() {
440
+ try {
441
+ const storage = this.storage();
442
+ storage?.setItem(this.key(), JSON.stringify(this.cache));
443
+ storage?.removeItem(`caldik:server-cart:${this.client.storeContextId()}`);
346
444
  } catch {}
347
- return this.get();
445
+ }
446
+ write(cart, token = this.cache.reference?.token, discount = null) {
447
+ this.cache = {
448
+ version: 2,
449
+ reference: {
450
+ id: cart.id,
451
+ token: cart.customerId ? null : token
452
+ },
453
+ snapshot: cloneSnapshot({
454
+ items: cart.items,
455
+ discount
456
+ })
457
+ };
458
+ this.persistCache();
459
+ return cloneSnapshot(this.cache.snapshot);
460
+ }
461
+ enqueue(operation) {
462
+ const run = () => {
463
+ const locks = this.storage() && typeof navigator !== "undefined" ? navigator.locks : void 0;
464
+ return locks ? locks.request(this.key(), operation) : operation();
465
+ };
466
+ const result = this.queue.then(run, run);
467
+ this.queue = result.catch(() => void 0);
468
+ return result;
469
+ }
470
+ options() {
471
+ return {
472
+ access: "public",
473
+ ...this.cache.reference?.token ? { headers: { "X-Caldik-Cart-Token": this.cache.reference.token } } : {}
474
+ };
475
+ }
476
+ async resolve(checkout = false, seed = true) {
477
+ this.readCache();
478
+ const input = this.cache.reference ? { cartId: this.cache.reference.id } : seed && this.cache.snapshot.items.length ? {
479
+ items: commerceItems(this.cache.snapshot.items),
480
+ discountCode: this.cache.snapshot.discount?.code
481
+ } : {};
482
+ const response = await this.client.post("/carts/session", {
483
+ ...input,
484
+ ...checkout ? { checkout: true } : {}
485
+ }, this.options());
486
+ this.initialized = true;
487
+ this.write(response.cart, response.token ?? null);
488
+ if (!checkout && response.cart.status === "checked_out") {
489
+ this.reset();
490
+ return this.resolve(false, false);
491
+ }
492
+ return response.cart;
493
+ }
494
+ reset() {
495
+ this.cache = {
496
+ version: 2,
497
+ reference: null,
498
+ snapshot: {
499
+ items: [],
500
+ discount: null
501
+ }
502
+ };
503
+ this.persistCache();
504
+ }
505
+ async mutate(operation, seed = true) {
506
+ let cart = await this.resolve(false, seed);
507
+ let response;
508
+ try {
509
+ response = await operation(cart);
510
+ } catch (error) {
511
+ if (!(error instanceof CaldikHttpError) || error.status !== 409) throw error;
512
+ const refreshed = await this.resolve();
513
+ if (refreshed.id !== cart.id || refreshed.status !== "active") throw error;
514
+ cart = refreshed;
515
+ response = await operation(cart);
516
+ }
517
+ return this.write(response.cart);
518
+ }
519
+ save(cart, items, discountCode) {
520
+ return this.client.post(`/carts/${cart.id}`, {
521
+ items: commerceItems(items),
522
+ version: cart.version,
523
+ discountCode
524
+ }, this.options());
525
+ }
526
+ quote(cart, code) {
527
+ if (!cart.items.length) throw new Error("Cannot check a discount on an empty cart");
528
+ return this.client.post("/discounts/check", {
529
+ code,
530
+ items: commerceItems(cart.items)
531
+ }, { access: "public" });
348
532
  }
349
533
  get() {
350
- return cloneSnapshot(this.read());
534
+ return this.enqueue(async () => {
535
+ let cart = await this.resolve();
536
+ let discount = null;
537
+ if (cart.discountCode) try {
538
+ discount = discountFromQuote(await this.quote(cart, cart.discountCode));
539
+ } catch (error) {
540
+ if (!(error instanceof CaldikHttpError) || ![
541
+ 400,
542
+ 404,
543
+ 422
544
+ ].includes(error.status)) throw error;
545
+ cart = (await this.save(cart, cart.items, null)).cart;
546
+ }
547
+ return this.write(cart, this.cache.reference?.token, discount);
548
+ });
351
549
  }
352
550
  add(item) {
353
- const productId = item.productId.trim();
354
- const productVariantId = item.productVariantId?.trim() || "";
355
- if (!productId) throw new Error("Cart productId is required");
356
- const state = this.get();
357
- const id = cartItemId({
358
- productId,
359
- productVariantId
551
+ return this.enqueue(async () => {
552
+ if (!item.productId.trim()) throw new Error("Cart productId is required");
553
+ const normalized = {
554
+ ...item,
555
+ productId: item.productId.trim(),
556
+ quantity: quantity(item.quantity)
557
+ };
558
+ return this.mutate((cart) => this.client.post(`/carts/${cart.id}/items`, {
559
+ item: normalized,
560
+ version: cart.version
561
+ }, this.options()));
360
562
  });
361
- const existing = state.items.find((entry) => entry.id === id);
362
- const addedQuantity = quantity(item.quantity);
363
- if (existing) existing.quantity = quantity(existing.quantity + addedQuantity);
364
- else state.items.push({
365
- id,
366
- productId,
367
- ...productVariantId ? { productVariantId } : {},
368
- quantity: addedQuantity
369
- });
370
- state.discount = null;
371
- return this.write(state);
372
563
  }
373
564
  updateQuantity(itemId, nextQuantity) {
374
- const state = this.get();
375
- const normalized = quantity(nextQuantity, true);
376
- const index = state.items.findIndex((item) => item.id === itemId);
377
- if (index === -1) throw new Error("Cart item not found");
378
- if (normalized === 0) state.items.splice(index, 1);
379
- else state.items[index].quantity = normalized;
380
- state.discount = null;
381
- return this.write(state);
565
+ return this.enqueue(async () => {
566
+ const normalized = quantity(nextQuantity, true);
567
+ return this.mutate((cart) => {
568
+ if (!cart.items.some((item) => item.id === itemId)) throw new Error("Cart item not found");
569
+ const items = cart.items.map((item) => item.id === itemId ? {
570
+ ...item,
571
+ quantity: normalized
572
+ } : item).filter((item) => item.quantity > 0);
573
+ return this.save(cart, items, null);
574
+ });
575
+ });
382
576
  }
383
577
  remove(itemId) {
384
- const state = this.get();
385
- const items = state.items.filter((item) => item.id !== itemId);
386
- if (items.length === state.items.length) throw new Error("Cart item not found");
387
- return this.write({
388
- items,
389
- discount: null
390
- });
578
+ return this.updateQuantity(itemId, 0);
391
579
  }
392
580
  clear() {
393
- return this.write({
394
- items: [],
395
- discount: null
581
+ return this.enqueue(() => this.mutate((cart) => this.save(cart, [], null), false));
582
+ }
583
+ checkDiscount(code) {
584
+ return this.enqueue(async () => this.quote(await this.resolve(), code));
585
+ }
586
+ applyDiscount(code) {
587
+ return this.enqueue(async () => {
588
+ let quote;
589
+ const snapshot = await this.mutate(async (cart) => {
590
+ quote = await this.quote(cart, code);
591
+ return this.save(cart, cart.items, quote.discount.code);
592
+ });
593
+ snapshot.discount = discountFromQuote(quote);
594
+ this.cache.snapshot = cloneSnapshot(snapshot);
595
+ this.persistCache();
596
+ return snapshot;
396
597
  });
397
598
  }
398
- commerceItems() {
399
- return this.get().items.map(({ productId, productVariantId, quantity }) => ({
400
- productId,
401
- ...productVariantId ? { productVariantId } : {},
402
- quantity
403
- }));
404
- }
405
- async checkDiscount(code) {
406
- const items = this.commerceItems();
407
- if (items.length === 0) throw new Error("Cannot check a discount on an empty cart");
408
- return this.client.post("/discounts/check", {
409
- code,
410
- items
411
- }, { access: "public" });
412
- }
413
- async applyDiscount(code) {
414
- const quote = await this.checkDiscount(code);
415
- const state = this.get();
416
- state.discount = {
417
- ...quote.discount,
418
- subtotal: quote.subtotal,
419
- discountAmount: quote.discountAmount,
420
- total: quote.total,
421
- currency: quote.currency
422
- };
423
- return this.write(state);
424
- }
425
599
  removeDiscount() {
426
- const state = this.get();
427
- state.discount = null;
428
- return this.write(state);
429
- }
430
- async checkout(input) {
431
- const state = this.get();
432
- if (state.items.length === 0) throw new Error("Cannot checkout an empty cart");
433
- const response = await this.client.post("/checkouts", {
434
- ...input,
435
- items: this.commerceItems(),
436
- ...state.discount ? { discountCode: state.discount.code } : {}
437
- }, { access: "public" });
438
- this.clear();
439
- return response;
600
+ return this.enqueue(() => this.mutate((cart) => this.save(cart, cart.items, null)));
601
+ }
602
+ checkout(input = {}) {
603
+ return this.enqueue(async () => {
604
+ const cart = await this.resolve(true);
605
+ if (!cart.items.length) throw new Error("Cannot checkout an empty cart");
606
+ const response = await this.client.post(`/carts/${cart.id}/checkout`, {
607
+ ...input,
608
+ version: cart.version
609
+ }, this.options());
610
+ this.reset();
611
+ return response;
612
+ });
440
613
  }
441
614
  };
442
615
  //#endregion
@@ -497,7 +670,9 @@ var CaldikClient = class {
497
670
  const client = new CaldikHttpClient({
498
671
  apiKey: options.apiKey,
499
672
  apiUrl: options.apiUrl,
500
- storeId: options.storeId
673
+ storeId: options.storeId,
674
+ request: options.request,
675
+ onSetCookie: options.onSetCookie
501
676
  });
502
677
  this.categories = new CategoriesApi(client);
503
678
  this.products = new ProductsApi(client);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caldik/client",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",