@swell/apps-sdk 1.0.161 → 1.0.163

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/dist/index.mjs CHANGED
@@ -7172,6 +7172,7 @@ var DEFAULT_OPTIONS = Object.freeze({
7172
7172
  });
7173
7173
  var NULL_VALUE = "__NULL__";
7174
7174
  var SWR_PROMISE_MAP = /* @__PURE__ */ new Map();
7175
+ var FETCH_PROMISE_MAP = /* @__PURE__ */ new Map();
7175
7176
  var Cache = class {
7176
7177
  client;
7177
7178
  workerCtx;
@@ -7184,8 +7185,30 @@ var Cache = class {
7184
7185
  ...options
7185
7186
  });
7186
7187
  }
7187
- async fetch(key, fetchFn, ttl) {
7188
- return this.client.wrap(key, fetchFn, ttl);
7188
+ /**
7189
+ * Always fetches fresh data and updates cache
7190
+ * Deduplicates concurrent requests with the same key
7191
+ *
7192
+ * @param key Cache key
7193
+ * @param fetchFn Function to fetch fresh data
7194
+ * @param ttl Time to live in milliseconds (default: DEFAULT_SWR_TTL)
7195
+ * @param isCacheable Whether to store result in cache (default: true)
7196
+ */
7197
+ async fetch(key, fetchFn, ttl = DEFAULT_SWR_TTL, isCacheable = true) {
7198
+ let promise = FETCH_PROMISE_MAP.get(key);
7199
+ if (!promise) {
7200
+ promise = Promise.resolve().then(fetchFn).then(resolveAsyncResources).then(async (value) => {
7201
+ const isNull = value === null || value === void 0;
7202
+ if (isCacheable) {
7203
+ await this.client.set(key, isNull ? NULL_VALUE : value, ttl);
7204
+ }
7205
+ return value;
7206
+ }).finally(() => {
7207
+ FETCH_PROMISE_MAP.delete(key);
7208
+ });
7209
+ FETCH_PROMISE_MAP.set(key, promise);
7210
+ }
7211
+ return await promise;
7189
7212
  }
7190
7213
  /**
7191
7214
  * Fetch cache using SWR (stale-while-revalidate)
@@ -8293,18 +8316,24 @@ var Swell = class _Swell {
8293
8316
  }
8294
8317
  return params;
8295
8318
  }
8319
+ /**
8320
+ * Checks if cache bypass is requested via X-Cache-Bypass header
8321
+ */
8322
+ shouldBypassCache() {
8323
+ return this.headers?.["x-cache-bypass"] === "revalidation";
8324
+ }
8296
8325
  /**
8297
8326
  * Fetches a resource.
8298
8327
  * First attempts to fetch from cache.
8299
8328
  */
8300
8329
  async getCachedResource(key, args, handler, isCacheble = true) {
8301
8330
  const cacheKey = getCacheKey(key, [this.instanceId, args]);
8302
- return this.getResourceCache().fetchSWR(
8303
- cacheKey,
8304
- handler,
8305
- void 0,
8306
- isCacheble
8307
- );
8331
+ const cache = this.getResourceCache();
8332
+ if (this.shouldBypassCache()) {
8333
+ return cache.fetch(cacheKey, handler, void 0, isCacheble);
8334
+ } else {
8335
+ return cache.fetchSWR(cacheKey, handler, void 0, isCacheble);
8336
+ }
8308
8337
  }
8309
8338
  async getAppSettings() {
8310
8339
  const settings = await this.get(
@@ -8471,11 +8500,17 @@ var Swell = class _Swell {
8471
8500
  data,
8472
8501
  opt
8473
8502
  ]);
8474
- return this.getRequestCache().fetchSWR(key, () => {
8503
+ const cache = this.getRequestCache();
8504
+ const fetchFn = () => {
8475
8505
  const requestUrl = id ? `${url}/${id}` : url;
8476
8506
  logger.debug("[SDK] Cacheable API request", { url: requestUrl, key });
8477
8507
  return storefrontRequest(method, url, id, data, opt);
8478
- });
8508
+ };
8509
+ if (this.shouldBypassCache()) {
8510
+ return cache.fetch(key, fetchFn);
8511
+ } else {
8512
+ return cache.fetchSWR(key, fetchFn);
8513
+ }
8479
8514
  }
8480
8515
  switch (method) {
8481
8516
  case "delete":
@@ -15874,7 +15909,7 @@ function ShopifyAddress(instance, address, account) {
15874
15909
  function joinAddressLines(...props) {
15875
15910
  return props.filter(Boolean).join("\n");
15876
15911
  }
15877
- function ShopifyCountry(_instance2, countryCode) {
15912
+ function ShopifyCountry(_instance, countryCode) {
15878
15913
  const currencyCode = getCurrencyByCountry(countryCode) || "USD";
15879
15914
  return new ShopifyResource(
15880
15915
  {
@@ -16207,7 +16242,7 @@ async function resolveLastOrder(instance, account) {
16207
16242
  }
16208
16243
 
16209
16244
  // src/compatibility/shopify-objects/font.ts
16210
- function ShopifyFont(_instance2, font) {
16245
+ function ShopifyFont(_instance, font) {
16211
16246
  if (font instanceof ShopifyResource) {
16212
16247
  return font.clone();
16213
16248
  }
@@ -16218,7 +16253,7 @@ function ShopifyFont(_instance2, font) {
16218
16253
  family: font.family,
16219
16254
  style: font.style,
16220
16255
  "system?": font.system,
16221
- variants: font.variants.map((variant) => ShopifyFont(_instance2, variant)),
16256
+ variants: font.variants.map((variant) => ShopifyFont(_instance, variant)),
16222
16257
  weight: font.weight
16223
16258
  });
16224
16259
  }
@@ -16231,7 +16266,7 @@ var SHOPIFY_FORMS = {
16231
16266
  })
16232
16267
  }
16233
16268
  };
16234
- function ShopifyForm(_instance2, form) {
16269
+ function ShopifyForm(_instance, form) {
16235
16270
  if (form instanceof ShopifyResource) {
16236
16271
  return form.clone();
16237
16272
  }
@@ -16528,7 +16563,7 @@ function ShopifyRecommendations(instance, product) {
16528
16563
  }
16529
16564
 
16530
16565
  // src/compatibility/shopify-objects/page.ts
16531
- function ShopifyPage(_instance2, page) {
16566
+ function ShopifyPage(_instance, page) {
16532
16567
  if (page instanceof ShopifyResource) {
16533
16568
  return page.clone();
16534
16569
  }
@@ -17740,7 +17775,7 @@ ${injects.join("\n")}</script>`;
17740
17775
  };
17741
17776
 
17742
17777
  // src/compatibility/shopify-objects/template.ts
17743
- function ShopifyTemplate(_instance2, template) {
17778
+ function ShopifyTemplate(_instance, template) {
17744
17779
  return new ShopifyResource(
17745
17780
  {
17746
17781
  directory: template.path,
@@ -20680,12 +20715,7 @@ var SwellTheme3 = class {
20680
20715
  // Default value (always StorefrontResource)
20681
20716
  () => this.fetchCart()
20682
20717
  ),
20683
- this.fetchSingletonResourceCached(
20684
- "account",
20685
- () => this.fetchAccount(),
20686
- () => null,
20687
- false
20688
- )
20718
+ this.fetchAccount()
20689
20719
  ]);
20690
20720
  if (!cart) {
20691
20721
  throw new Error("Failed to fetch cart");
@@ -22323,42 +22353,27 @@ function getResourceQuery(slug, query) {
22323
22353
 
22324
22354
  // src/cache/html-cache/html-cache.ts
22325
22355
  var CACHE_KEY_ORIGIN = "https://cache.swell.store";
22326
- var TTL_CONFIG = {
22327
- LIVE: {
22328
- DEFAULT: 20,
22329
- HOME: 20,
22330
- PRODUCT: 20,
22331
- COLLECTION: 20,
22332
- PAGE: 20,
22333
- BLOG: 20,
22334
- SWR: 60 * 60 * 24 * 7
22335
- // 1 week
22336
- },
22337
- PREVIEW: {
22338
- DEFAULT: 10,
22339
- HOME: 10,
22340
- PRODUCT: 10,
22341
- COLLECTION: 10,
22342
- PAGE: 10,
22343
- BLOG: 10,
22344
- SWR: 60 * 60 * 24 * 7
22345
- // 1 week
22346
- }
22356
+ var DEFAULT_CACHE_RULES = {
22357
+ defaults: {
22358
+ live: { ttl: 20, swr: 60 * 60 * 24 * 7 },
22359
+ // 20s TTL, 1 week SWR
22360
+ preview: { ttl: 10, swr: 60 * 60 * 24 * 7 }
22361
+ // 10s TTL, 1 week SWR
22362
+ },
22363
+ pathRules: [{ path: "/checkout/*", skip: true }]
22347
22364
  };
22348
22365
  var HtmlCache = class {
22349
22366
  epoch;
22350
22367
  backend;
22351
- constructor(epoch, backend) {
22368
+ cacheRules;
22369
+ constructor(epoch, backend, cacheRules = DEFAULT_CACHE_RULES) {
22352
22370
  this.epoch = epoch;
22353
22371
  this.backend = backend;
22372
+ this.cacheRules = cacheRules;
22354
22373
  }
22355
22374
  async get(request) {
22356
22375
  const trace = createTraceId();
22357
- if (request.method !== "GET") {
22358
- logger.debug("[SDK Html-cache] non-cacheable method", { trace });
22359
- return { found: false, cacheable: false };
22360
- }
22361
- if (!this.isCacheable(request)) {
22376
+ if (!this.canReadFromCache(request)) {
22362
22377
  logger.debug("[SDK Html-cache] non-cacheable request", { trace });
22363
22378
  return { found: false, cacheable: false };
22364
22379
  }
@@ -22426,31 +22441,27 @@ var HtmlCache = class {
22426
22441
  }
22427
22442
  async put(request, response) {
22428
22443
  const trace = createTraceId();
22429
- if (request.method !== "GET" || !response.ok) {
22430
- logger.debug("[SDK Html-cache] put skipped, invalid method or response", {
22431
- trace
22432
- });
22433
- return;
22434
- }
22435
- if (!this.isCacheable(request) || !this.isResponseCacheable(response)) {
22444
+ if (!this.canWriteToCache(request, response)) {
22436
22445
  logger.debug("[SDK Html-cache] put skipped, non-cacheable", { trace });
22437
22446
  return;
22438
22447
  }
22439
22448
  try {
22440
- let lowercaseHeaders2 = function(headers2) {
22441
- const out = {};
22442
- headers2.forEach((value, key) => {
22443
- out[key.toLowerCase()] = value;
22444
- });
22445
- return out;
22446
- };
22447
- var lowercaseHeaders = lowercaseHeaders2;
22448
22449
  const cacheKey = this.buildCacheKey(request);
22449
22450
  const ttl = this.getTTLForRequest(request);
22450
22451
  const swr = this.getSWRForRequest(request);
22451
22452
  const body = await response.text();
22453
+ if (!body || body.trim().length === 0) {
22454
+ logger.warn(
22455
+ "[SDK Html-cache] put skipped, empty or minimal response body",
22456
+ {
22457
+ trace,
22458
+ bodyLength: body.length
22459
+ }
22460
+ );
22461
+ return;
22462
+ }
22452
22463
  const cacheTimeISO = (/* @__PURE__ */ new Date()).toISOString();
22453
- const headers = lowercaseHeaders2(response.headers);
22464
+ const headers = this.normalizeHeaders(response.headers);
22454
22465
  const entry = {
22455
22466
  status: response.status,
22456
22467
  statusText: response.statusText,
@@ -22484,14 +22495,13 @@ var HtmlCache = class {
22484
22495
  });
22485
22496
  }
22486
22497
  }
22487
- isReadCacheCandidate(request) {
22488
- const m = request.method.toUpperCase();
22489
- return (m === "GET" || m === "HEAD") && this.isCacheable(request);
22498
+ canReadFromCache(request) {
22499
+ const method = request.method.toUpperCase();
22500
+ return (method === "GET" || method === "HEAD") && this.isRequestCacheable(request);
22490
22501
  }
22491
- isWriteCacheCandidate(request, response) {
22492
- if (request.method.toUpperCase() !== "GET") return false;
22493
- if (!this.isCacheable(request)) return false;
22494
- return this.isResponseCacheable(response);
22502
+ canWriteToCache(request, response) {
22503
+ const method = request.method.toUpperCase();
22504
+ return method === "GET" && response.ok && this.isRequestCacheable(request) && this.isResponseCacheable(response);
22495
22505
  }
22496
22506
  createRevalidationRequest(request) {
22497
22507
  const headers = new Headers(request.headers);
@@ -22507,7 +22517,7 @@ var HtmlCache = class {
22507
22517
  }
22508
22518
  buildClientResponse(entry, isStale, age) {
22509
22519
  const headers = new Headers(entry.headers);
22510
- headers.set("Cache-Control", "public, max-age=0, must-revalidate");
22520
+ headers.set("Cache-Control", "public, max-age=1, must-revalidate");
22511
22521
  headers.set(
22512
22522
  "Cloudflare-CDN-Cache-Control",
22513
22523
  `public, s-maxage=${entry.ttl}, stale-while-revalidate=${entry.swr}, stale-if-error=60`
@@ -22602,6 +22612,7 @@ var HtmlCache = class {
22602
22612
  const accept = headers.get("accept") || "";
22603
22613
  const versionFactors = {
22604
22614
  store: headers.get("swell-storefront-id") || "",
22615
+ app: (headers.get("swell-app-id") || "") + "@" + (swellData["swell-app-version"] || ""),
22605
22616
  auth: headers.get("swell-access-token") || "",
22606
22617
  theme: headers.get("swell-theme-version-hash") || "",
22607
22618
  modified: headers.get("swell-cache-modified") || "",
@@ -22624,11 +22635,16 @@ var HtmlCache = class {
22624
22635
  return {};
22625
22636
  }
22626
22637
  }
22627
- isCacheable(request) {
22638
+ isRequestCacheable(request) {
22628
22639
  const url = new URL(request.url);
22629
22640
  if (request.headers.get("swell-deployment-mode") === "editor") return false;
22630
- const skipPaths = ["/checkout"];
22631
- if (skipPaths.some((path) => url.pathname.startsWith(path))) return false;
22641
+ if (this.cacheRules.pathRules) {
22642
+ for (const rule of this.cacheRules.pathRules) {
22643
+ if (this.pathMatches(rule.path, url.pathname) && rule.skip) {
22644
+ return false;
22645
+ }
22646
+ }
22647
+ }
22632
22648
  if (request.headers.get("cache-control")?.includes("no-cache"))
22633
22649
  return false;
22634
22650
  return true;
@@ -22644,24 +22660,33 @@ var HtmlCache = class {
22644
22660
  }
22645
22661
  getDeploymentMode(headers) {
22646
22662
  const mode = headers.get("swell-deployment-mode");
22647
- return mode === "preview" || mode === "editor" ? mode : "live";
22663
+ return mode === "preview" ? "preview" : "live";
22648
22664
  }
22649
22665
  getTTLForRequest(request) {
22650
22666
  const url = new URL(request.url);
22651
22667
  const mode = this.getDeploymentMode(request.headers);
22652
- if (mode === "editor") return 0;
22653
- const config = mode === "preview" ? TTL_CONFIG.PREVIEW : TTL_CONFIG.LIVE;
22654
- if (url.pathname === "/") return config.HOME;
22655
- if (url.pathname.startsWith("/products/")) return config.PRODUCT;
22656
- if (url.pathname.startsWith("/categories/")) return config.COLLECTION;
22657
- if (url.pathname.startsWith("/pages/")) return config.PAGE;
22658
- if (url.pathname.startsWith("/blogs/")) return config.BLOG;
22659
- return config.DEFAULT;
22668
+ if (this.cacheRules.pathRules) {
22669
+ for (const rule of this.cacheRules.pathRules) {
22670
+ if (this.pathMatches(rule.path, url.pathname) && rule.ttl !== void 0) {
22671
+ return rule.ttl;
22672
+ }
22673
+ }
22674
+ }
22675
+ const defaults = this.cacheRules.defaults?.[mode];
22676
+ return defaults?.ttl ?? DEFAULT_CACHE_RULES.defaults[mode].ttl;
22660
22677
  }
22661
22678
  getSWRForRequest(request) {
22679
+ const url = new URL(request.url);
22662
22680
  const mode = this.getDeploymentMode(request.headers);
22663
- if (mode === "editor") return 0;
22664
- return mode === "preview" ? TTL_CONFIG.PREVIEW.SWR : TTL_CONFIG.LIVE.SWR;
22681
+ if (this.cacheRules.pathRules) {
22682
+ for (const rule of this.cacheRules.pathRules) {
22683
+ if (this.pathMatches(rule.path, url.pathname) && rule.swr !== void 0) {
22684
+ return rule.swr;
22685
+ }
22686
+ }
22687
+ }
22688
+ const defaults = this.cacheRules.defaults?.[mode];
22689
+ return defaults?.swr ?? DEFAULT_CACHE_RULES.defaults[mode].swr;
22665
22690
  }
22666
22691
  getEntryAge(entry) {
22667
22692
  const t = Date.parse(entry.cacheTimeISO);
@@ -22669,6 +22694,22 @@ var HtmlCache = class {
22669
22694
  const age = (Date.now() - t) / 1e3;
22670
22695
  return age < 0 ? 0 : age;
22671
22696
  }
22697
+ /**
22698
+ * Converts wildcard pattern to regex and tests against path.
22699
+ * - * matches any characters except /
22700
+ * - ** matches any characters including /
22701
+ */
22702
+ pathMatches(pattern, path) {
22703
+ const regex = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "___DOUBLE_STAR___").replace(/\*/g, "[^/]*").replace(/___DOUBLE_STAR___/g, ".*");
22704
+ return new RegExp(`^${regex}$`).test(path);
22705
+ }
22706
+ normalizeHeaders(headers) {
22707
+ const normalized = {};
22708
+ headers.forEach((value, key) => {
22709
+ normalized[key.toLowerCase()] = value;
22710
+ });
22711
+ return normalized;
22712
+ }
22672
22713
  normalizeSearchParams(searchParams) {
22673
22714
  const ignoredParams = [
22674
22715
  "utm_source",
@@ -22837,7 +22878,6 @@ var WorkerCacheBackend = class {
22837
22878
  statusText: entry.statusText,
22838
22879
  headers
22839
22880
  });
22840
- await cache.delete(request);
22841
22881
  await cache.put(request, response);
22842
22882
  }
22843
22883
  async delete(key) {
@@ -22848,18 +22888,15 @@ var WorkerCacheBackend = class {
22848
22888
  };
22849
22889
 
22850
22890
  // src/cache/html-cache/html-cache-factory.ts
22851
- var _instance = null;
22852
- function getHtmlCache(env) {
22891
+ function getHtmlCache(env, cacheRules) {
22853
22892
  const epoch = env?.HTML_CACHE_EPOCH;
22854
22893
  if (typeof epoch !== "string" || !epoch) return null;
22855
- if (_instance) return _instance;
22856
22894
  const kv = env?.NAMESPACE;
22895
+ const rules = cacheRules || env?.HTML_CACHE_RULES;
22857
22896
  if (env?.HTML_CACHE_BACKEND !== "worker" && kv) {
22858
- _instance = new HtmlCache(epoch, new KVCacheBackend(kv));
22859
- return _instance;
22897
+ return new HtmlCache(epoch, new KVCacheBackend(kv), rules);
22860
22898
  }
22861
- _instance = new HtmlCache(epoch, new WorkerCacheBackend(epoch));
22862
- return _instance;
22899
+ return new HtmlCache(epoch, new WorkerCacheBackend(epoch), rules);
22863
22900
  }
22864
22901
  export {
22865
22902
  AccountAddressesResource,
@@ -22874,6 +22911,7 @@ export {
22874
22911
  CartResource,
22875
22912
  CategoriesResource,
22876
22913
  CategoryResource,
22914
+ DEFAULT_CACHE_RULES,
22877
22915
  DEFAULT_QUERY_PAGE_LIMIT,
22878
22916
  DeferredShopifyResource,
22879
22917
  FILE_DATA_INCLUDE_QUERY,