@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.cjs CHANGED
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  CartResource: () => CartResource,
43
43
  CategoriesResource: () => CategoriesResource,
44
44
  CategoryResource: () => CategoryResource,
45
+ DEFAULT_CACHE_RULES: () => DEFAULT_CACHE_RULES,
45
46
  DEFAULT_QUERY_PAGE_LIMIT: () => DEFAULT_QUERY_PAGE_LIMIT,
46
47
  DeferredShopifyResource: () => DeferredShopifyResource,
47
48
  FILE_DATA_INCLUDE_QUERY: () => FILE_DATA_INCLUDE_QUERY,
@@ -7319,6 +7320,7 @@ var DEFAULT_OPTIONS = Object.freeze({
7319
7320
  });
7320
7321
  var NULL_VALUE = "__NULL__";
7321
7322
  var SWR_PROMISE_MAP = /* @__PURE__ */ new Map();
7323
+ var FETCH_PROMISE_MAP = /* @__PURE__ */ new Map();
7322
7324
  var Cache = class {
7323
7325
  client;
7324
7326
  workerCtx;
@@ -7331,8 +7333,30 @@ var Cache = class {
7331
7333
  ...options
7332
7334
  });
7333
7335
  }
7334
- async fetch(key, fetchFn, ttl) {
7335
- return this.client.wrap(key, fetchFn, ttl);
7336
+ /**
7337
+ * Always fetches fresh data and updates cache
7338
+ * Deduplicates concurrent requests with the same key
7339
+ *
7340
+ * @param key Cache key
7341
+ * @param fetchFn Function to fetch fresh data
7342
+ * @param ttl Time to live in milliseconds (default: DEFAULT_SWR_TTL)
7343
+ * @param isCacheable Whether to store result in cache (default: true)
7344
+ */
7345
+ async fetch(key, fetchFn, ttl = DEFAULT_SWR_TTL, isCacheable = true) {
7346
+ let promise = FETCH_PROMISE_MAP.get(key);
7347
+ if (!promise) {
7348
+ promise = Promise.resolve().then(fetchFn).then(resolveAsyncResources).then(async (value) => {
7349
+ const isNull = value === null || value === void 0;
7350
+ if (isCacheable) {
7351
+ await this.client.set(key, isNull ? NULL_VALUE : value, ttl);
7352
+ }
7353
+ return value;
7354
+ }).finally(() => {
7355
+ FETCH_PROMISE_MAP.delete(key);
7356
+ });
7357
+ FETCH_PROMISE_MAP.set(key, promise);
7358
+ }
7359
+ return await promise;
7336
7360
  }
7337
7361
  /**
7338
7362
  * Fetch cache using SWR (stale-while-revalidate)
@@ -8440,18 +8464,24 @@ var Swell = class _Swell {
8440
8464
  }
8441
8465
  return params;
8442
8466
  }
8467
+ /**
8468
+ * Checks if cache bypass is requested via X-Cache-Bypass header
8469
+ */
8470
+ shouldBypassCache() {
8471
+ return this.headers?.["x-cache-bypass"] === "revalidation";
8472
+ }
8443
8473
  /**
8444
8474
  * Fetches a resource.
8445
8475
  * First attempts to fetch from cache.
8446
8476
  */
8447
8477
  async getCachedResource(key, args, handler, isCacheble = true) {
8448
8478
  const cacheKey = getCacheKey(key, [this.instanceId, args]);
8449
- return this.getResourceCache().fetchSWR(
8450
- cacheKey,
8451
- handler,
8452
- void 0,
8453
- isCacheble
8454
- );
8479
+ const cache = this.getResourceCache();
8480
+ if (this.shouldBypassCache()) {
8481
+ return cache.fetch(cacheKey, handler, void 0, isCacheble);
8482
+ } else {
8483
+ return cache.fetchSWR(cacheKey, handler, void 0, isCacheble);
8484
+ }
8455
8485
  }
8456
8486
  async getAppSettings() {
8457
8487
  const settings = await this.get(
@@ -8618,11 +8648,17 @@ var Swell = class _Swell {
8618
8648
  data,
8619
8649
  opt
8620
8650
  ]);
8621
- return this.getRequestCache().fetchSWR(key, () => {
8651
+ const cache = this.getRequestCache();
8652
+ const fetchFn = () => {
8622
8653
  const requestUrl = id ? `${url}/${id}` : url;
8623
8654
  logger.debug("[SDK] Cacheable API request", { url: requestUrl, key });
8624
8655
  return storefrontRequest(method, url, id, data, opt);
8625
- });
8656
+ };
8657
+ if (this.shouldBypassCache()) {
8658
+ return cache.fetch(key, fetchFn);
8659
+ } else {
8660
+ return cache.fetchSWR(key, fetchFn);
8661
+ }
8626
8662
  }
8627
8663
  switch (method) {
8628
8664
  case "delete":
@@ -16021,7 +16057,7 @@ function ShopifyAddress(instance, address, account) {
16021
16057
  function joinAddressLines(...props) {
16022
16058
  return props.filter(Boolean).join("\n");
16023
16059
  }
16024
- function ShopifyCountry(_instance2, countryCode) {
16060
+ function ShopifyCountry(_instance, countryCode) {
16025
16061
  const currencyCode = getCurrencyByCountry(countryCode) || "USD";
16026
16062
  return new ShopifyResource(
16027
16063
  {
@@ -16354,7 +16390,7 @@ async function resolveLastOrder(instance, account) {
16354
16390
  }
16355
16391
 
16356
16392
  // src/compatibility/shopify-objects/font.ts
16357
- function ShopifyFont(_instance2, font) {
16393
+ function ShopifyFont(_instance, font) {
16358
16394
  if (font instanceof ShopifyResource) {
16359
16395
  return font.clone();
16360
16396
  }
@@ -16365,7 +16401,7 @@ function ShopifyFont(_instance2, font) {
16365
16401
  family: font.family,
16366
16402
  style: font.style,
16367
16403
  "system?": font.system,
16368
- variants: font.variants.map((variant) => ShopifyFont(_instance2, variant)),
16404
+ variants: font.variants.map((variant) => ShopifyFont(_instance, variant)),
16369
16405
  weight: font.weight
16370
16406
  });
16371
16407
  }
@@ -16378,7 +16414,7 @@ var SHOPIFY_FORMS = {
16378
16414
  })
16379
16415
  }
16380
16416
  };
16381
- function ShopifyForm(_instance2, form) {
16417
+ function ShopifyForm(_instance, form) {
16382
16418
  if (form instanceof ShopifyResource) {
16383
16419
  return form.clone();
16384
16420
  }
@@ -16675,7 +16711,7 @@ function ShopifyRecommendations(instance, product) {
16675
16711
  }
16676
16712
 
16677
16713
  // src/compatibility/shopify-objects/page.ts
16678
- function ShopifyPage(_instance2, page) {
16714
+ function ShopifyPage(_instance, page) {
16679
16715
  if (page instanceof ShopifyResource) {
16680
16716
  return page.clone();
16681
16717
  }
@@ -17887,7 +17923,7 @@ ${injects.join("\n")}</script>`;
17887
17923
  };
17888
17924
 
17889
17925
  // src/compatibility/shopify-objects/template.ts
17890
- function ShopifyTemplate(_instance2, template) {
17926
+ function ShopifyTemplate(_instance, template) {
17891
17927
  return new ShopifyResource(
17892
17928
  {
17893
17929
  directory: template.path,
@@ -20819,12 +20855,7 @@ var SwellTheme3 = class {
20819
20855
  // Default value (always StorefrontResource)
20820
20856
  () => this.fetchCart()
20821
20857
  ),
20822
- this.fetchSingletonResourceCached(
20823
- "account",
20824
- () => this.fetchAccount(),
20825
- () => null,
20826
- false
20827
- )
20858
+ this.fetchAccount()
20828
20859
  ]);
20829
20860
  if (!cart) {
20830
20861
  throw new Error("Failed to fetch cart");
@@ -22462,42 +22493,27 @@ function getResourceQuery(slug, query) {
22462
22493
 
22463
22494
  // src/cache/html-cache/html-cache.ts
22464
22495
  var CACHE_KEY_ORIGIN = "https://cache.swell.store";
22465
- var TTL_CONFIG = {
22466
- LIVE: {
22467
- DEFAULT: 20,
22468
- HOME: 20,
22469
- PRODUCT: 20,
22470
- COLLECTION: 20,
22471
- PAGE: 20,
22472
- BLOG: 20,
22473
- SWR: 60 * 60 * 24 * 7
22474
- // 1 week
22475
- },
22476
- PREVIEW: {
22477
- DEFAULT: 10,
22478
- HOME: 10,
22479
- PRODUCT: 10,
22480
- COLLECTION: 10,
22481
- PAGE: 10,
22482
- BLOG: 10,
22483
- SWR: 60 * 60 * 24 * 7
22484
- // 1 week
22485
- }
22496
+ var DEFAULT_CACHE_RULES = {
22497
+ defaults: {
22498
+ live: { ttl: 20, swr: 60 * 60 * 24 * 7 },
22499
+ // 20s TTL, 1 week SWR
22500
+ preview: { ttl: 10, swr: 60 * 60 * 24 * 7 }
22501
+ // 10s TTL, 1 week SWR
22502
+ },
22503
+ pathRules: [{ path: "/checkout/*", skip: true }]
22486
22504
  };
22487
22505
  var HtmlCache = class {
22488
22506
  epoch;
22489
22507
  backend;
22490
- constructor(epoch, backend) {
22508
+ cacheRules;
22509
+ constructor(epoch, backend, cacheRules = DEFAULT_CACHE_RULES) {
22491
22510
  this.epoch = epoch;
22492
22511
  this.backend = backend;
22512
+ this.cacheRules = cacheRules;
22493
22513
  }
22494
22514
  async get(request) {
22495
22515
  const trace = createTraceId();
22496
- if (request.method !== "GET") {
22497
- logger.debug("[SDK Html-cache] non-cacheable method", { trace });
22498
- return { found: false, cacheable: false };
22499
- }
22500
- if (!this.isCacheable(request)) {
22516
+ if (!this.canReadFromCache(request)) {
22501
22517
  logger.debug("[SDK Html-cache] non-cacheable request", { trace });
22502
22518
  return { found: false, cacheable: false };
22503
22519
  }
@@ -22565,31 +22581,27 @@ var HtmlCache = class {
22565
22581
  }
22566
22582
  async put(request, response) {
22567
22583
  const trace = createTraceId();
22568
- if (request.method !== "GET" || !response.ok) {
22569
- logger.debug("[SDK Html-cache] put skipped, invalid method or response", {
22570
- trace
22571
- });
22572
- return;
22573
- }
22574
- if (!this.isCacheable(request) || !this.isResponseCacheable(response)) {
22584
+ if (!this.canWriteToCache(request, response)) {
22575
22585
  logger.debug("[SDK Html-cache] put skipped, non-cacheable", { trace });
22576
22586
  return;
22577
22587
  }
22578
22588
  try {
22579
- let lowercaseHeaders2 = function(headers2) {
22580
- const out = {};
22581
- headers2.forEach((value, key) => {
22582
- out[key.toLowerCase()] = value;
22583
- });
22584
- return out;
22585
- };
22586
- var lowercaseHeaders = lowercaseHeaders2;
22587
22589
  const cacheKey = this.buildCacheKey(request);
22588
22590
  const ttl = this.getTTLForRequest(request);
22589
22591
  const swr = this.getSWRForRequest(request);
22590
22592
  const body = await response.text();
22593
+ if (!body || body.trim().length === 0) {
22594
+ logger.warn(
22595
+ "[SDK Html-cache] put skipped, empty or minimal response body",
22596
+ {
22597
+ trace,
22598
+ bodyLength: body.length
22599
+ }
22600
+ );
22601
+ return;
22602
+ }
22591
22603
  const cacheTimeISO = (/* @__PURE__ */ new Date()).toISOString();
22592
- const headers = lowercaseHeaders2(response.headers);
22604
+ const headers = this.normalizeHeaders(response.headers);
22593
22605
  const entry = {
22594
22606
  status: response.status,
22595
22607
  statusText: response.statusText,
@@ -22623,14 +22635,13 @@ var HtmlCache = class {
22623
22635
  });
22624
22636
  }
22625
22637
  }
22626
- isReadCacheCandidate(request) {
22627
- const m = request.method.toUpperCase();
22628
- return (m === "GET" || m === "HEAD") && this.isCacheable(request);
22638
+ canReadFromCache(request) {
22639
+ const method = request.method.toUpperCase();
22640
+ return (method === "GET" || method === "HEAD") && this.isRequestCacheable(request);
22629
22641
  }
22630
- isWriteCacheCandidate(request, response) {
22631
- if (request.method.toUpperCase() !== "GET") return false;
22632
- if (!this.isCacheable(request)) return false;
22633
- return this.isResponseCacheable(response);
22642
+ canWriteToCache(request, response) {
22643
+ const method = request.method.toUpperCase();
22644
+ return method === "GET" && response.ok && this.isRequestCacheable(request) && this.isResponseCacheable(response);
22634
22645
  }
22635
22646
  createRevalidationRequest(request) {
22636
22647
  const headers = new Headers(request.headers);
@@ -22646,7 +22657,7 @@ var HtmlCache = class {
22646
22657
  }
22647
22658
  buildClientResponse(entry, isStale, age) {
22648
22659
  const headers = new Headers(entry.headers);
22649
- headers.set("Cache-Control", "public, max-age=0, must-revalidate");
22660
+ headers.set("Cache-Control", "public, max-age=1, must-revalidate");
22650
22661
  headers.set(
22651
22662
  "Cloudflare-CDN-Cache-Control",
22652
22663
  `public, s-maxage=${entry.ttl}, stale-while-revalidate=${entry.swr}, stale-if-error=60`
@@ -22741,6 +22752,7 @@ var HtmlCache = class {
22741
22752
  const accept = headers.get("accept") || "";
22742
22753
  const versionFactors = {
22743
22754
  store: headers.get("swell-storefront-id") || "",
22755
+ app: (headers.get("swell-app-id") || "") + "@" + (swellData["swell-app-version"] || ""),
22744
22756
  auth: headers.get("swell-access-token") || "",
22745
22757
  theme: headers.get("swell-theme-version-hash") || "",
22746
22758
  modified: headers.get("swell-cache-modified") || "",
@@ -22763,11 +22775,16 @@ var HtmlCache = class {
22763
22775
  return {};
22764
22776
  }
22765
22777
  }
22766
- isCacheable(request) {
22778
+ isRequestCacheable(request) {
22767
22779
  const url = new URL(request.url);
22768
22780
  if (request.headers.get("swell-deployment-mode") === "editor") return false;
22769
- const skipPaths = ["/checkout"];
22770
- if (skipPaths.some((path) => url.pathname.startsWith(path))) return false;
22781
+ if (this.cacheRules.pathRules) {
22782
+ for (const rule of this.cacheRules.pathRules) {
22783
+ if (this.pathMatches(rule.path, url.pathname) && rule.skip) {
22784
+ return false;
22785
+ }
22786
+ }
22787
+ }
22771
22788
  if (request.headers.get("cache-control")?.includes("no-cache"))
22772
22789
  return false;
22773
22790
  return true;
@@ -22783,24 +22800,33 @@ var HtmlCache = class {
22783
22800
  }
22784
22801
  getDeploymentMode(headers) {
22785
22802
  const mode = headers.get("swell-deployment-mode");
22786
- return mode === "preview" || mode === "editor" ? mode : "live";
22803
+ return mode === "preview" ? "preview" : "live";
22787
22804
  }
22788
22805
  getTTLForRequest(request) {
22789
22806
  const url = new URL(request.url);
22790
22807
  const mode = this.getDeploymentMode(request.headers);
22791
- if (mode === "editor") return 0;
22792
- const config = mode === "preview" ? TTL_CONFIG.PREVIEW : TTL_CONFIG.LIVE;
22793
- if (url.pathname === "/") return config.HOME;
22794
- if (url.pathname.startsWith("/products/")) return config.PRODUCT;
22795
- if (url.pathname.startsWith("/categories/")) return config.COLLECTION;
22796
- if (url.pathname.startsWith("/pages/")) return config.PAGE;
22797
- if (url.pathname.startsWith("/blogs/")) return config.BLOG;
22798
- return config.DEFAULT;
22808
+ if (this.cacheRules.pathRules) {
22809
+ for (const rule of this.cacheRules.pathRules) {
22810
+ if (this.pathMatches(rule.path, url.pathname) && rule.ttl !== void 0) {
22811
+ return rule.ttl;
22812
+ }
22813
+ }
22814
+ }
22815
+ const defaults = this.cacheRules.defaults?.[mode];
22816
+ return defaults?.ttl ?? DEFAULT_CACHE_RULES.defaults[mode].ttl;
22799
22817
  }
22800
22818
  getSWRForRequest(request) {
22819
+ const url = new URL(request.url);
22801
22820
  const mode = this.getDeploymentMode(request.headers);
22802
- if (mode === "editor") return 0;
22803
- return mode === "preview" ? TTL_CONFIG.PREVIEW.SWR : TTL_CONFIG.LIVE.SWR;
22821
+ if (this.cacheRules.pathRules) {
22822
+ for (const rule of this.cacheRules.pathRules) {
22823
+ if (this.pathMatches(rule.path, url.pathname) && rule.swr !== void 0) {
22824
+ return rule.swr;
22825
+ }
22826
+ }
22827
+ }
22828
+ const defaults = this.cacheRules.defaults?.[mode];
22829
+ return defaults?.swr ?? DEFAULT_CACHE_RULES.defaults[mode].swr;
22804
22830
  }
22805
22831
  getEntryAge(entry) {
22806
22832
  const t = Date.parse(entry.cacheTimeISO);
@@ -22808,6 +22834,22 @@ var HtmlCache = class {
22808
22834
  const age = (Date.now() - t) / 1e3;
22809
22835
  return age < 0 ? 0 : age;
22810
22836
  }
22837
+ /**
22838
+ * Converts wildcard pattern to regex and tests against path.
22839
+ * - * matches any characters except /
22840
+ * - ** matches any characters including /
22841
+ */
22842
+ pathMatches(pattern, path) {
22843
+ const regex = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "___DOUBLE_STAR___").replace(/\*/g, "[^/]*").replace(/___DOUBLE_STAR___/g, ".*");
22844
+ return new RegExp(`^${regex}$`).test(path);
22845
+ }
22846
+ normalizeHeaders(headers) {
22847
+ const normalized = {};
22848
+ headers.forEach((value, key) => {
22849
+ normalized[key.toLowerCase()] = value;
22850
+ });
22851
+ return normalized;
22852
+ }
22811
22853
  normalizeSearchParams(searchParams) {
22812
22854
  const ignoredParams = [
22813
22855
  "utm_source",
@@ -22976,7 +23018,6 @@ var WorkerCacheBackend = class {
22976
23018
  statusText: entry.statusText,
22977
23019
  headers
22978
23020
  });
22979
- await cache.delete(request);
22980
23021
  await cache.put(request, response);
22981
23022
  }
22982
23023
  async delete(key) {
@@ -22987,18 +23028,15 @@ var WorkerCacheBackend = class {
22987
23028
  };
22988
23029
 
22989
23030
  // src/cache/html-cache/html-cache-factory.ts
22990
- var _instance = null;
22991
- function getHtmlCache(env) {
23031
+ function getHtmlCache(env, cacheRules) {
22992
23032
  const epoch = env?.HTML_CACHE_EPOCH;
22993
23033
  if (typeof epoch !== "string" || !epoch) return null;
22994
- if (_instance) return _instance;
22995
23034
  const kv = env?.NAMESPACE;
23035
+ const rules = cacheRules || env?.HTML_CACHE_RULES;
22996
23036
  if (env?.HTML_CACHE_BACKEND !== "worker" && kv) {
22997
- _instance = new HtmlCache(epoch, new KVCacheBackend(kv));
22998
- return _instance;
23037
+ return new HtmlCache(epoch, new KVCacheBackend(kv), rules);
22999
23038
  }
23000
- _instance = new HtmlCache(epoch, new WorkerCacheBackend(epoch));
23001
- return _instance;
23039
+ return new HtmlCache(epoch, new WorkerCacheBackend(epoch), rules);
23002
23040
  }
23003
23041
  // Annotate the CommonJS export names for ESM import in node:
23004
23042
  0 && (module.exports = {
@@ -23014,6 +23052,7 @@ function getHtmlCache(env) {
23014
23052
  CartResource,
23015
23053
  CategoriesResource,
23016
23054
  CategoryResource,
23055
+ DEFAULT_CACHE_RULES,
23017
23056
  DEFAULT_QUERY_PAGE_LIMIT,
23018
23057
  DeferredShopifyResource,
23019
23058
  FILE_DATA_INCLUDE_QUERY,