shop-client 3.8.2 → 3.9.1

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 (60) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +158 -1
  3. package/dist/ai/enrich.d.ts +93 -0
  4. package/dist/ai/enrich.js +25 -0
  5. package/dist/checkout.js +5 -114
  6. package/dist/{chunk-2KBOKOAD.mjs → chunk-2MF53V33.js} +32 -13
  7. package/dist/{chunk-BWKBRM2Z.mjs → chunk-CN7L3BHG.js} +12 -1
  8. package/dist/chunk-CXUCPK6X.js +460 -0
  9. package/dist/{chunk-QCTICSBE.mjs → chunk-MOBWPEY4.js} +29 -7
  10. package/dist/chunk-ROH545KI.js +274 -0
  11. package/dist/{chunk-QL5OUZGP.mjs → chunk-RR6YTQWP.js} +0 -1
  12. package/dist/{chunk-O4BPIIQ6.mjs → chunk-V52MFQZE.js} +11 -281
  13. package/dist/{chunk-WTK5HUFI.mjs → chunk-VPPCOJC3.js} +13 -435
  14. package/dist/collections.d.ts +2 -1
  15. package/dist/collections.js +7 -539
  16. package/dist/index.d.ts +28 -87
  17. package/dist/index.js +109 -2597
  18. package/dist/products.d.ts +2 -1
  19. package/dist/products.js +7 -1205
  20. package/dist/store.d.ts +53 -1
  21. package/dist/store.js +8 -697
  22. package/dist/{store-CJVUz2Yb.d.ts → types-luPg5O08.d.ts} +1 -208
  23. package/dist/utils/detect-country.d.ts +32 -0
  24. package/dist/utils/detect-country.js +6 -0
  25. package/dist/utils/func.d.ts +61 -0
  26. package/dist/utils/func.js +24 -0
  27. package/dist/utils/rate-limit.d.ts +5 -0
  28. package/dist/utils/rate-limit.js +7 -200
  29. package/package.json +21 -10
  30. package/dist/checkout.d.mts +0 -31
  31. package/dist/checkout.js.map +0 -1
  32. package/dist/checkout.mjs +0 -7
  33. package/dist/checkout.mjs.map +0 -1
  34. package/dist/chunk-2KBOKOAD.mjs.map +0 -1
  35. package/dist/chunk-BWKBRM2Z.mjs.map +0 -1
  36. package/dist/chunk-O4BPIIQ6.mjs.map +0 -1
  37. package/dist/chunk-QCTICSBE.mjs.map +0 -1
  38. package/dist/chunk-QL5OUZGP.mjs.map +0 -1
  39. package/dist/chunk-WTK5HUFI.mjs.map +0 -1
  40. package/dist/collections.d.mts +0 -64
  41. package/dist/collections.js.map +0 -1
  42. package/dist/collections.mjs +0 -9
  43. package/dist/collections.mjs.map +0 -1
  44. package/dist/index.d.mts +0 -233
  45. package/dist/index.js.map +0 -1
  46. package/dist/index.mjs +0 -702
  47. package/dist/index.mjs.map +0 -1
  48. package/dist/products.d.mts +0 -63
  49. package/dist/products.js.map +0 -1
  50. package/dist/products.mjs +0 -9
  51. package/dist/products.mjs.map +0 -1
  52. package/dist/store-CJVUz2Yb.d.mts +0 -608
  53. package/dist/store.d.mts +0 -1
  54. package/dist/store.js.map +0 -1
  55. package/dist/store.mjs +0 -9
  56. package/dist/store.mjs.map +0 -1
  57. package/dist/utils/rate-limit.d.mts +0 -25
  58. package/dist/utils/rate-limit.js.map +0 -1
  59. package/dist/utils/rate-limit.mjs +0 -11
  60. package/dist/utils/rate-limit.mjs.map +0 -1
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 ShopSearch
3
+ Copyright (c) 2025 ShopClient
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -6,6 +6,18 @@
6
6
 
7
7
  `shop-client` is a powerful, type-safe TypeScript library for fetching and transforming product data from Shopify stores. Perfect for building e-commerce applications, product catalogs, price comparison tools, and automated store analysis.
8
8
 
9
+ ## Contents
10
+
11
+ - [Installation](#-installation)
12
+ - [Quick Start](#-quick-start)
13
+ - [Store Info Caching & Concurrency](#store-info-caching--concurrency)
14
+ - [Browser Usage](#browser-usage)
15
+ - [Server/Edge Usage](#serveredge-usage)
16
+ - [Deep Imports & Tree-Shaking](#deep-imports--tree-shaking)
17
+ - [Rate Limiting](#rate-limiting)
18
+ - [Migration: Barrel → Subpath Imports](#migration-barrel--subpath-imports)
19
+ - [API Docs](#api-docs)
20
+
9
21
  ## 🚀 Features
10
22
 
11
23
  - **Complete Store Data Access**: Fetch products, collections, and store information
@@ -17,6 +29,66 @@
17
29
  - **Zero Dependencies**: Lightweight with minimal external dependencies
18
30
  - **Store Type Classification**: Infers audience and verticals from showcased products (body_html-only)
19
31
 
32
+ ## 🧠 Store Info Caching & Concurrency
33
+
34
+ `getInfo()` uses time-based caching and in-flight request deduping to avoid redundant network calls:
35
+
36
+ - Cache window: `5 minutes` (`cacheExpiry`). Fresh cached results return immediately.
37
+ - You can configure this TTL via the `ShopClient` constructor option `cacheTTL` (milliseconds).
38
+ - Cached fields: `infoCacheValue` (last `StoreInfo`) and `infoCacheTimestamp` (last fetch time).
39
+ - In-flight deduping: concurrent calls share a single request via an internal promise; result is cached and returned to all callers.
40
+ - Failure handling: the in-flight marker clears in a `finally` block so subsequent calls can retry.
41
+
42
+ Behavior:
43
+ - Cached and fresh → returns cached `StoreInfo`.
44
+ - Stale/missing and request in-flight → awaits shared promise.
45
+ - Stale/missing and no in-flight → performs fetch, caches, returns.
46
+
47
+ Example: configure cache TTL
48
+
49
+ ```ts
50
+ import { ShopClient } from "shop-client";
51
+
52
+ // Set store info + validation cache TTL to 10 seconds
53
+ const shop = new ShopClient("https://exampleshop.com", { cacheTTL: 10_000 });
54
+ const info = await shop.getInfo();
55
+ ```
56
+
57
+ Manual invalidation
58
+
59
+ ```ts
60
+ import { ShopClient } from "shop-client";
61
+
62
+ const shop = new ShopClient("https://exampleshop.com", { cacheTTL: 60_000 });
63
+
64
+ // Fetch and cache
65
+ await shop.getInfo();
66
+
67
+ // Invalidate cache proactively (e.g., after a content update)
68
+ shop.clearInfoCache();
69
+
70
+ // Next call refetches and repopulates cache
71
+ await shop.getInfo();
72
+ ```
73
+
74
+ Force refetch
75
+
76
+ ```ts
77
+ import { ShopClient } from "shop-client";
78
+
79
+ const shop = new ShopClient("https://exampleshop.com", { cacheTTL: 60_000 });
80
+
81
+ // First call caches within TTL
82
+ await shop.getInfo();
83
+
84
+ // Force a fresh network fetch even if the cache is still fresh
85
+ const fresh = await shop.getInfo({ force: true });
86
+ ```
87
+
88
+ See also:
89
+ - Architecture: [Caching Strategy](./ARCHITECTURE.md#caching-strategy)
90
+ - API Reference (LLM): [ShopClientOptions, getInfo(force), clearInfoCache](./.llm/api-reference.md#constructor)
91
+
20
92
  ## 📦 Installation
21
93
 
22
94
  ```bash
@@ -133,6 +205,21 @@ Notes:
133
205
 
134
206
  ### Migration: Barrel → Subpath Imports
135
207
 
208
+ #### Package Rename: `shop-search` → `shop-client` (v3.8.2)
209
+ - Install: `npm i shop-client` (replaces `shop-search`)
210
+ - Update imports to `shop-client` (API unchanged)
211
+
212
+ TypeScript:
213
+ ```ts
214
+ // Before (pre-rename: shop-search)
215
+ import { Store } from 'shop-search';
216
+ const store = new Store("your-store.myshopify.com");
217
+
218
+ // After (post-rename: shop-client v3.8.2+)
219
+ import { ShopClient } from 'shop-client';
220
+ const client = new ShopClient("your-store.myshopify.com");
221
+ ```
222
+
136
223
  You can keep using the root entry (`shop-client`), but for smaller bundles switch to deep imports. The API remains the same—only the import paths change.
137
224
 
138
225
  Examples:
@@ -165,7 +252,12 @@ Notes:
165
252
  - No bundler changes required; deep imports are exposed via `exports`.
166
253
  - The root entry continues to work; prefer deep imports for production apps.
167
254
 
168
- ## ��️ Rate Limiting
255
+ ## API Docs
256
+
257
+ - TypeDoc builds automatically on pushes to `main` and publishes to GitHub Pages.
258
+ - Visit: `https://peppyhop.github.io/shop-client/` (after the first successful workflow run).
259
+
260
+ ## Rate Limiting
169
261
 
170
262
  `shop-client` ships with an opt-in, global rate limiter that transparently throttles all internal HTTP requests (products, collections, store info, enrichment). This helps avoid `429 Too Many Requests` responses and keeps crawling stable.
171
263
 
@@ -530,6 +622,45 @@ Notes:
530
622
  - npm publishes use OIDC with provenance; no `NPM_TOKEN` secret is required.
531
623
  - Ensure your npm package settings add this GitHub repo as a trusted publisher and set the environment name to `npm-publish`.
532
624
 
625
+ ### Additional Utilities
626
+
627
+ ```typescript
628
+ import {
629
+ calculateDiscount,
630
+ extractDomainWithoutSuffix,
631
+ generateStoreSlug,
632
+ genProductSlug,
633
+ detectShopCountry,
634
+ } from 'shop-client';
635
+
636
+ // Discount calculation (percentage, rounded to nearest integer)
637
+ calculateDiscount(8000, 10000); // 20
638
+
639
+ // Extract base domain without public suffix
640
+ extractDomainWithoutSuffix('www.example.co.uk'); // 'example'
641
+
642
+ // Create an SEO-friendly store slug
643
+ generateStoreSlug('https://shop.example.com'); // 'shop-example-com'
644
+
645
+ // Create a product slug from product data
646
+ genProductSlug({
647
+ title: 'Summer Dress',
648
+ handle: 'summer-dress',
649
+ vendor: 'Acme',
650
+ }); // 'acme-summer-dress'
651
+
652
+ // Detect Shopify store country with confidence score
653
+ const result = await detectShopCountry('anuki.in');
654
+ // result.country → 'IN', result.confidence → 0.9
655
+ ```
656
+
657
+ Notes:
658
+ - `calculateDiscount` expects prices in the same unit (e.g., cents) and returns an integer percentage.
659
+ - `extractDomainWithoutSuffix` removes known TLDs/suffixes, leaving the registrable label.
660
+ - `generateStoreSlug` preserves domain components and replaces separators with hyphens.
661
+ - `genProductSlug` builds a stable, vendor-prefixed slug using product fields.
662
+ - `detectShopCountry` combines multiple signals to infer store country and confidence.
663
+
533
664
  ### Store Type Classification
534
665
 
535
666
  Determine the store’s primary verticals and target audiences using showcased products. Classification uses only each product’s `body_html` content and aggregates per-product results, optionally pruned by store-level signals.
@@ -565,6 +696,32 @@ Details:
565
696
  - If `OPENROUTER_API_KEY` is absent or `OPENROUTER_OFFLINE=1`, uses offline regex heuristics.
566
697
  - Applies store-level pruning based on title/description to improve consistency.
567
698
 
699
+ ### AI Enrichment
700
+
701
+ ```typescript
702
+ import { classifyProduct, generateSEOContent } from 'shop-client';
703
+
704
+ // Classify a single product (offline or via OpenRouter when configured)
705
+ const classification = await classifyProduct({
706
+ title: 'Organic Cotton T-Shirt',
707
+ bodyHtml: '<p>Soft, breathable cotton tee</p>',
708
+ tags: ['organic', 'cotton', 'unisex'],
709
+ });
710
+ // classification → { adult_unisex: { clothing: ['t-shirts'] } }
711
+
712
+ // Generate basic SEO content for a product
713
+ const seo = await generateSEOContent({
714
+ title: 'Organic Cotton T-Shirt',
715
+ description: 'Soft, breathable tee for everyday wear',
716
+ tags: ['organic', 'cotton', 'unisex'],
717
+ });
718
+ // seo.metaTitle, seo.metaDescription, seo.keywords
719
+ ```
720
+
721
+ Notes:
722
+ - `classifyProduct` mirrors the store-level classification logic but operates on a single product.
723
+ - `generateSEOContent` produces lightweight, deterministic metadata suitable for catalogs and PDPs.
724
+
568
725
  ## 🏗️ Type Definitions
569
726
 
570
727
  ### StoreInfo
@@ -0,0 +1,93 @@
1
+ import { k as ProductClassification, l as SEOContent, a as ShopifySingleProduct } from '../types-luPg5O08.js';
2
+
3
+ interface EnrichedProductResult {
4
+ bodyHtml: string;
5
+ pageHtml: string;
6
+ extractedMainHtml: string;
7
+ mergedMarkdown: string;
8
+ }
9
+ /**
10
+ * Fetch Shopify Product AJAX API
11
+ * /products/{handle}.js
12
+ */
13
+ declare function fetchAjaxProduct(domain: string, handle: string): Promise<ShopifySingleProduct>;
14
+ /**
15
+ * Fetch full product page HTML
16
+ */
17
+ declare function fetchProductPage(domain: string, handle: string): Promise<string>;
18
+ /**
19
+ * Extract the main Shopify product section WITHOUT cheerio
20
+ * Uses regex + indexing (fast & reliable)
21
+ */
22
+ declare function extractMainSection(html: string): string | null;
23
+ /**
24
+ * Convert HTML → Clean Markdown using Turndown
25
+ * Includes Shopify cleanup rules + GFM support
26
+ */
27
+ declare function htmlToMarkdown(html: string | null, options?: {
28
+ useGfm?: boolean;
29
+ }): string;
30
+ /**
31
+ * Merge the two markdown sources using OpenAI GPT
32
+ */
33
+ declare function mergeWithLLM(bodyInput: string, pageInput: string, options?: {
34
+ apiKey?: string;
35
+ inputType?: "markdown" | "html";
36
+ model?: string;
37
+ outputFormat?: "markdown" | "json";
38
+ }): Promise<string>;
39
+ /**
40
+ * MAIN WORKFLOW
41
+ */
42
+ declare function enrichProduct(domain: string, handle: string, options?: {
43
+ apiKey?: string;
44
+ useGfm?: boolean;
45
+ inputType?: "markdown" | "html";
46
+ model?: string;
47
+ outputFormat?: "markdown" | "json";
48
+ }): Promise<EnrichedProductResult>;
49
+ /**
50
+ * Classify product content into a three-tier hierarchy using LLM.
51
+ * Returns strictly validated JSON with audience, vertical, and optional category/subCategory.
52
+ */
53
+ declare function classifyProduct(productContent: string, options?: {
54
+ apiKey?: string;
55
+ model?: string;
56
+ }): Promise<ProductClassification>;
57
+ /**
58
+ * Generate SEO and marketing content for a product. Returns strictly validated JSON.
59
+ */
60
+ declare function generateSEOContent(product: {
61
+ title: string;
62
+ description?: string;
63
+ vendor?: string;
64
+ price?: number;
65
+ tags?: string[];
66
+ }, options?: {
67
+ apiKey?: string;
68
+ model?: string;
69
+ }): Promise<SEOContent>;
70
+ /**
71
+ * Determine store type (primary vertical and audience) from store information.
72
+ * Accepts flexible input for showcase products/collections (titles or handles) and returns
73
+ * strictly validated `vertical` and `audience` values.
74
+ */
75
+ declare function determineStoreType(storeInfo: {
76
+ title: string;
77
+ description?: string | null;
78
+ showcase: {
79
+ products: Array<{
80
+ title: string;
81
+ productType?: string | null;
82
+ }> | string[];
83
+ collections: Array<{
84
+ title: string;
85
+ }> | string[];
86
+ };
87
+ }, options?: {
88
+ apiKey?: string;
89
+ model?: string;
90
+ }): Promise<Partial<Record<ProductClassification["audience"], Partial<Record<ProductClassification["vertical"], string[]>>>>>;
91
+ declare function pruneBreakdownForSignals(breakdown: Partial<Record<ProductClassification["audience"], Partial<Record<ProductClassification["vertical"], string[]>>>>, text: string): Partial<Record<ProductClassification["audience"], Partial<Record<ProductClassification["vertical"], string[]>>>>;
92
+
93
+ export { type EnrichedProductResult, classifyProduct, determineStoreType, enrichProduct, extractMainSection, fetchAjaxProduct, fetchProductPage, generateSEOContent, htmlToMarkdown, mergeWithLLM, pruneBreakdownForSignals };
@@ -0,0 +1,25 @@
1
+ import {
2
+ classifyProduct,
3
+ determineStoreType,
4
+ enrichProduct,
5
+ extractMainSection,
6
+ fetchAjaxProduct,
7
+ fetchProductPage,
8
+ generateSEOContent,
9
+ htmlToMarkdown,
10
+ mergeWithLLM,
11
+ pruneBreakdownForSignals
12
+ } from "../chunk-VPPCOJC3.js";
13
+ import "../chunk-2MF53V33.js";
14
+ export {
15
+ classifyProduct,
16
+ determineStoreType,
17
+ enrichProduct,
18
+ extractMainSection,
19
+ fetchAjaxProduct,
20
+ fetchProductPage,
21
+ generateSEOContent,
22
+ htmlToMarkdown,
23
+ mergeWithLLM,
24
+ pruneBreakdownForSignals
25
+ };
package/dist/checkout.js CHANGED
@@ -1,115 +1,6 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/checkout.ts
21
- var checkout_exports = {};
22
- __export(checkout_exports, {
23
- createCheckoutOperations: () => createCheckoutOperations
24
- });
25
- module.exports = __toCommonJS(checkout_exports);
26
- function createCheckoutOperations(baseUrl) {
27
- return {
28
- /**
29
- * Creates a Shopify checkout URL with pre-filled customer information and cart items.
30
- *
31
- * @param params - Checkout parameters
32
- * @param params.email - Customer's email address (must be valid email format)
33
- * @param params.items - Array of products to add to cart
34
- * @param params.items[].productVariantId - Shopify product variant ID
35
- * @param params.items[].quantity - Quantity as string (must be positive number)
36
- * @param params.address - Customer's shipping address
37
- * @param params.address.firstName - Customer's first name
38
- * @param params.address.lastName - Customer's last name
39
- * @param params.address.address1 - Street address
40
- * @param params.address.city - City name
41
- * @param params.address.zip - Postal/ZIP code
42
- * @param params.address.country - Country name
43
- * @param params.address.province - State/Province name
44
- * @param params.address.phone - Phone number
45
- *
46
- * @returns {string} Complete Shopify checkout URL with pre-filled information
47
- *
48
- * @throws {Error} When email is invalid, items array is empty, or required address fields are missing
49
- *
50
- * @example
51
- * ```typescript
52
- * const shop = new ShopClient('https://exampleshop.com');
53
- * const checkoutUrl = await shop.checkout.create([
54
- * { variantId: '123', quantity: 2 },
55
- * { variantId: '456', quantity: 1 }
56
- * ]);
57
- * console.log(checkoutUrl);
58
- * ```
59
- */
60
- createUrl: ({
61
- email,
62
- items,
63
- address
64
- }) => {
65
- if (!email || !email.includes("@")) {
66
- throw new Error("Invalid email address");
67
- }
68
- if (!items || items.length === 0) {
69
- throw new Error("Items array cannot be empty");
70
- }
71
- for (const item of items) {
72
- if (!item.productVariantId || !item.quantity) {
73
- throw new Error("Each item must have productVariantId and quantity");
74
- }
75
- const qty = Number.parseInt(item.quantity, 10);
76
- if (Number.isNaN(qty) || qty <= 0) {
77
- throw new Error("Quantity must be a positive number");
78
- }
79
- }
80
- const requiredFields = [
81
- "firstName",
82
- "lastName",
83
- "address1",
84
- "city",
85
- "zip",
86
- "country"
87
- ];
88
- for (const field of requiredFields) {
89
- if (!address[field]) {
90
- throw new Error(`Address field '${field}' is required`);
91
- }
92
- }
93
- const cartPath = items.map(
94
- (item) => `${encodeURIComponent(item.productVariantId)}:${encodeURIComponent(item.quantity)}`
95
- ).join(",");
96
- const params = new URLSearchParams({
97
- "checkout[email]": email,
98
- "checkout[shipping_address][first_name]": address.firstName,
99
- "checkout[shipping_address][last_name]": address.lastName,
100
- "checkout[shipping_address][address1]": address.address1,
101
- "checkout[shipping_address][city]": address.city,
102
- "checkout[shipping_address][zip]": address.zip,
103
- "checkout[shipping_address][country]": address.country,
104
- "checkout[shipping_address][province]": address.province,
105
- "checkout[shipping_address][phone]": address.phone
106
- });
107
- return `${baseUrl}cart/${cartPath}?${params.toString()}`;
108
- }
109
- };
110
- }
111
- // Annotate the CommonJS export names for ESM import in node:
112
- 0 && (module.exports = {
1
+ import {
113
2
  createCheckoutOperations
114
- });
115
- //# sourceMappingURL=checkout.js.map
3
+ } from "./chunk-RR6YTQWP.js";
4
+ export {
5
+ createCheckoutOperations
6
+ };
@@ -17,12 +17,6 @@ var RateLimiter = class {
17
17
  this.refillTimer.unref();
18
18
  }
19
19
  }
20
- stopRefill() {
21
- if (this.refillTimer) {
22
- clearInterval(this.refillTimer);
23
- this.refillTimer = null;
24
- }
25
- }
26
20
  ensureRefillStarted() {
27
21
  if (!this.refillTimer) {
28
22
  this.startRefill();
@@ -151,16 +145,42 @@ function configureRateLimit(options) {
151
145
  }
152
146
  }
153
147
  }
148
+ function sleep(ms) {
149
+ return new Promise((resolve) => setTimeout(resolve, ms));
150
+ }
154
151
  async function rateLimitedFetch(input, init) {
155
- var _a;
156
- if (!enabled) {
157
- return fetch(input, init);
158
- }
152
+ var _a, _b, _c, _d, _e, _f, _g;
159
153
  const klass = init == null ? void 0 : init.rateLimitClass;
160
154
  const byClass = klass ? classLimiters.get(klass) : void 0;
161
155
  const byHost = getHostLimiter(getHost(input));
162
- const eff = (_a = byClass != null ? byClass : byHost) != null ? _a : limiter;
163
- return eff.schedule(() => fetch(input, init));
156
+ const eff = enabled ? (_a = byClass != null ? byClass : byHost) != null ? _a : limiter : void 0;
157
+ const maxRetries = Math.max(0, (_c = (_b = init == null ? void 0 : init.retry) == null ? void 0 : _b.maxRetries) != null ? _c : 2);
158
+ const baseDelayMs = Math.max(0, (_e = (_d = init == null ? void 0 : init.retry) == null ? void 0 : _d.baseDelayMs) != null ? _e : 200);
159
+ const retryOnStatuses = (_g = (_f = init == null ? void 0 : init.retry) == null ? void 0 : _f.retryOnStatuses) != null ? _g : [429, 503];
160
+ let attempt = 0;
161
+ let lastError = null;
162
+ let response = null;
163
+ while (attempt <= maxRetries) {
164
+ try {
165
+ if (eff) {
166
+ response = await eff.schedule(() => fetch(input, init));
167
+ } else {
168
+ response = await fetch(input, init);
169
+ }
170
+ if (!response || response.ok || !retryOnStatuses.includes(response.status)) {
171
+ return response;
172
+ }
173
+ } catch (err) {
174
+ lastError = err;
175
+ }
176
+ attempt += 1;
177
+ if (attempt > maxRetries) break;
178
+ const jitter = Math.floor(Math.random() * 100);
179
+ const delay = baseDelayMs * 2 ** (attempt - 1) + jitter;
180
+ await sleep(delay);
181
+ }
182
+ if (response) return response;
183
+ throw lastError != null ? lastError : new Error("rateLimitedFetch failed without response");
164
184
  }
165
185
  function getRateLimitStatus() {
166
186
  return {
@@ -174,4 +194,3 @@ export {
174
194
  rateLimitedFetch,
175
195
  getRateLimitStatus
176
196
  };
177
- //# sourceMappingURL=chunk-2KBOKOAD.mjs.map
@@ -110,6 +110,17 @@ function buildVariantOptionsMap(optionNames, variants) {
110
110
  }
111
111
  return map;
112
112
  }
113
+ function buildVariantKey(obj) {
114
+ const parts = [];
115
+ for (const [name, value] of Object.entries(obj)) {
116
+ if (value) {
117
+ parts.push(`${normalizeKey(name)}#${normalizeKey(value)}`);
118
+ }
119
+ }
120
+ if (parts.length === 0) return "";
121
+ parts.sort((a, b) => a.localeCompare(b));
122
+ return parts.join("##");
123
+ }
113
124
  function formatPrice(amountInCents, currency) {
114
125
  try {
115
126
  return new Intl.NumberFormat(void 0, {
@@ -131,6 +142,6 @@ export {
131
142
  safeParseDate,
132
143
  normalizeKey,
133
144
  buildVariantOptionsMap,
145
+ buildVariantKey,
134
146
  formatPrice
135
147
  };
136
- //# sourceMappingURL=chunk-BWKBRM2Z.mjs.map