@swell/apps-sdk 1.0.151 → 1.0.152

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
@@ -94,6 +94,7 @@ __export(index_exports, {
94
94
  ThemeFont: () => ThemeFont,
95
95
  ThemeForm: () => ThemeForm,
96
96
  ThemeFormErrors: () => ThemeFormErrors,
97
+ WorkerHtmlCache: () => WorkerHtmlCache,
97
98
  adaptShopifyFontData: () => adaptShopifyFontData,
98
99
  adaptShopifyFormData: () => adaptShopifyFormData,
99
100
  adaptShopifyMenuData: () => adaptShopifyMenuData,
@@ -160,72 +161,6 @@ var import_qs2 = __toESM(require("qs"), 1);
160
161
  var import_keyv = require("keyv");
161
162
  var import_cache_manager = require("cache-manager");
162
163
 
163
- // src/cache/cf-worker-kv-keyv-adapter.ts
164
- var CFWorkerKVKeyvAdapter = class {
165
- store;
166
- namespace;
167
- // magically passed in from Keyv
168
- opts;
169
- constructor(store) {
170
- this.store = store;
171
- this.opts = null;
172
- this.namespace = "dummy";
173
- }
174
- async has(key) {
175
- const stream = await this.store.get(key, "stream");
176
- if (stream !== null) {
177
- await stream.cancel();
178
- return true;
179
- }
180
- return false;
181
- }
182
- async get(key) {
183
- const value = await this.store.get(key);
184
- return value !== null ? value : void 0;
185
- }
186
- set(key, value, ttl) {
187
- if (typeof ttl === "number") {
188
- ttl = Math.max(60, ttl / 1e3);
189
- }
190
- return this.store.put(key, value, { expirationTtl: ttl });
191
- }
192
- async delete(key) {
193
- await this.store.delete(key);
194
- return true;
195
- }
196
- async clear() {
197
- let cursor = "";
198
- let complete = false;
199
- const prefix = `${this.namespace}:`;
200
- do {
201
- const response = await this.store.list({
202
- prefix,
203
- cursor: cursor || void 0
204
- });
205
- cursor = response.cursor ?? "";
206
- complete = response.list_complete;
207
- if (response.keys.length > 0) {
208
- await Promise.all(
209
- response.keys.map((key) => {
210
- return this.store.delete(key.name);
211
- })
212
- );
213
- }
214
- } while (!complete);
215
- }
216
- on(_event, _listener) {
217
- return this;
218
- }
219
- };
220
-
221
- // src/utils/index.ts
222
- var import_qs = __toESM(require("qs"), 1);
223
- var import_lodash_es3 = require("lodash-es");
224
- var import_json52 = __toESM(require("json5"), 1);
225
-
226
- // src/resources.ts
227
- var import_lodash_es2 = require("lodash-es");
228
-
229
164
  // src/utils/logger.ts
230
165
  var logLevels = {
231
166
  error: 0,
@@ -317,6 +252,80 @@ function createTraceId(data) {
317
252
  return (hash >>> 0).toString(16);
318
253
  }
319
254
 
255
+ // src/cache/cf-worker-kv-keyv-adapter.ts
256
+ var CFWorkerKVKeyvAdapter = class {
257
+ store;
258
+ namespace;
259
+ // magically passed in from Keyv
260
+ opts;
261
+ constructor(store) {
262
+ this.store = store;
263
+ this.opts = null;
264
+ this.namespace = "dummy";
265
+ }
266
+ async has(key) {
267
+ const stream = await this.store.get(key, "stream");
268
+ if (stream !== null) {
269
+ await stream.cancel();
270
+ return true;
271
+ }
272
+ return false;
273
+ }
274
+ async get(key) {
275
+ const trace = createTraceId();
276
+ logger.debug("[SDK] kv.get", { key, trace });
277
+ const value = await this.store.get(key);
278
+ const result = value !== null ? value : void 0;
279
+ logger.debug(`[SDK] kv.get ${value !== null ? "HIT" : "MISS"}`, {
280
+ key,
281
+ trace
282
+ });
283
+ return result;
284
+ }
285
+ set(key, value, ttl) {
286
+ if (typeof ttl === "number") {
287
+ ttl = Math.max(60, ttl / 1e3);
288
+ }
289
+ logger.debug("[SDK] kv.set", { key });
290
+ return this.store.put(key, value, { expirationTtl: ttl });
291
+ }
292
+ async delete(key) {
293
+ await this.store.delete(key);
294
+ return true;
295
+ }
296
+ async clear() {
297
+ let cursor = "";
298
+ let complete = false;
299
+ const prefix = `${this.namespace}:`;
300
+ do {
301
+ const response = await this.store.list({
302
+ prefix,
303
+ cursor: cursor || void 0
304
+ });
305
+ cursor = response.cursor ?? "";
306
+ complete = response.list_complete;
307
+ if (response.keys.length > 0) {
308
+ await Promise.all(
309
+ response.keys.map((key) => {
310
+ return this.store.delete(key.name);
311
+ })
312
+ );
313
+ }
314
+ } while (!complete);
315
+ }
316
+ on(_event, _listener) {
317
+ return this;
318
+ }
319
+ };
320
+
321
+ // src/utils/index.ts
322
+ var import_qs = __toESM(require("qs"), 1);
323
+ var import_lodash_es3 = require("lodash-es");
324
+ var import_json52 = __toESM(require("json5"), 1);
325
+
326
+ // src/resources.ts
327
+ var import_lodash_es2 = require("lodash-es");
328
+
320
329
  // src/liquid/utils.ts
321
330
  var import_liquidjs = require("liquidjs");
322
331
 
@@ -960,6 +969,7 @@ var SwellStorefrontCollection = class _SwellStorefrontCollection extends SwellSt
960
969
  this._collection,
961
970
  this._query,
962
971
  this._swell.queryParams,
972
+ // TODO: consider to exlcude
963
973
  this._getterHash
964
974
  ],
965
975
  getter,
@@ -1088,6 +1098,7 @@ var SwellStorefrontRecord = class extends SwellStorefrontResource {
1088
1098
  this._id,
1089
1099
  this._query,
1090
1100
  this._swell.queryParams,
1101
+ // TODO: consider to exlcude
1091
1102
  this._getterHash
1092
1103
  ],
1093
1104
  getter,
@@ -7316,8 +7327,6 @@ var Cache = class {
7316
7327
  * This will always return the cached value immediately if exists
7317
7328
  */
7318
7329
  async fetchSWR(key, fetchFn, ttl = DEFAULT_SWR_TTL, isCacheble = true) {
7319
- const trace = createTraceId();
7320
- logger.debug("[SDK] Cache fetch start", { key, trace });
7321
7330
  const cacheValue = isCacheble ? await this.client.get(key) : void 0;
7322
7331
  let promise = SWR_PROMISE_MAP.get(key);
7323
7332
  if (promise === void 0) {
@@ -7325,7 +7334,6 @@ var Cache = class {
7325
7334
  const isNull = value === null || value === void 0;
7326
7335
  if (isCacheble) {
7327
7336
  await this.client.set(key, isNull ? NULL_VALUE : value, ttl);
7328
- logger.debug("[SDK] Cache update done", { key, trace });
7329
7337
  }
7330
7338
  return value;
7331
7339
  }).finally(() => {
@@ -7337,12 +7345,9 @@ var Cache = class {
7337
7345
  this.workerCtx.waitUntil(promise);
7338
7346
  }
7339
7347
  if (cacheValue !== void 0) {
7340
- logger.debug("[SDK] Cache check done", { status: "HIT", key, trace });
7341
7348
  return cacheValue === NULL_VALUE ? null : cacheValue;
7342
7349
  }
7343
- logger.debug("[SDK] Cache check done", { status: "MISS", key, trace });
7344
7350
  const result = await promise;
7345
- logger.debug("[SDK] Cache fetch end", { key, trace });
7346
7351
  return result;
7347
7352
  }
7348
7353
  async get(key) {
@@ -7849,6 +7854,326 @@ var WorkerCacheProxy = class {
7849
7854
  }
7850
7855
  };
7851
7856
 
7857
+ // src/cache/worker-html-cache.ts
7858
+ var CACHE_NAME2 = "swell-html-v1";
7859
+ var CACHE_KEY_ORIGIN2 = "https://cache.swell.store";
7860
+ var TTL_CONFIG = {
7861
+ DEFAULT: 300,
7862
+ // 5 minutes
7863
+ DEFAULT_SWR: 3600,
7864
+ // 1 hour stale-while-revalidate
7865
+ HOME: 300,
7866
+ // 5 minutes
7867
+ PRODUCT: 600,
7868
+ // 10 minutes
7869
+ COLLECTION: 900,
7870
+ // 15 minutes
7871
+ PAGE: 3600,
7872
+ // 1 hour
7873
+ BLOG: 1800
7874
+ // 30 minutes
7875
+ };
7876
+ var WorkerHtmlCache = class {
7877
+ epoch;
7878
+ constructor(epoch) {
7879
+ this.epoch = epoch;
7880
+ }
7881
+ async get(request) {
7882
+ const trace = createTraceId();
7883
+ if (request.method !== "GET") {
7884
+ logger.debug("[SDK Html-cache] non-cacheable", { trace });
7885
+ return { found: false, cacheable: false };
7886
+ }
7887
+ if (!this.isCacheable(request)) {
7888
+ logger.debug("[SDK Html-cache] non-cacheable", { trace });
7889
+ return { found: false, cacheable: false };
7890
+ }
7891
+ try {
7892
+ const cache = await caches.open(CACHE_NAME2 + this.epoch);
7893
+ const cacheKey = this.buildCacheKey(request);
7894
+ const cached = await cache.match(cacheKey);
7895
+ if (!cached) {
7896
+ logger.debug("[SDK Html-cache] cacheable, MISS", { trace });
7897
+ return { found: false, cacheable: true };
7898
+ }
7899
+ const age = this.getResponseAge(cached);
7900
+ const ttl = parseInt(cached.headers.get("X-Original-TTL") || "") || this.getTTLForRequest(request);
7901
+ const swr = parseInt(cached.headers.get("X-Original-SWR") || "") || TTL_CONFIG.DEFAULT_SWR;
7902
+ const isStale = age >= ttl;
7903
+ const isExpired = age >= ttl + swr;
7904
+ if (!isExpired) {
7905
+ logger.debug("[SDK Html-cache] cacheable, HIT", {
7906
+ stale: isStale,
7907
+ age,
7908
+ trace
7909
+ });
7910
+ const clientResponse = this.buildClientResponse(
7911
+ cached,
7912
+ ttl,
7913
+ swr,
7914
+ isStale,
7915
+ age
7916
+ );
7917
+ return {
7918
+ found: true,
7919
+ stale: isStale,
7920
+ response: clientResponse,
7921
+ cacheable: true,
7922
+ age: Math.floor(age)
7923
+ };
7924
+ }
7925
+ logger.debug("[SDK Html-cache] cacheable, hit, expired", { trace });
7926
+ return { found: false, cacheable: true };
7927
+ } catch (_) {
7928
+ logger.warn("[SDK Html-cache] no get support", { trace });
7929
+ return null;
7930
+ }
7931
+ }
7932
+ async put(request, response) {
7933
+ const trace = createTraceId();
7934
+ if (request.method !== "GET" || !response.ok) {
7935
+ logger.debug("[SDK Html-cache] put skipped", { trace });
7936
+ return;
7937
+ }
7938
+ if (!this.isCacheable(request) || !this.isResponseCacheable(response)) {
7939
+ logger.debug("[SDK Html-cache] put skipped, non-cacheable", {
7940
+ trace
7941
+ });
7942
+ return;
7943
+ }
7944
+ try {
7945
+ const cache = await caches.open(CACHE_NAME2 + this.epoch);
7946
+ const cacheKey = this.buildCacheKey(request);
7947
+ const ttl = this.getTTLForRequest(request);
7948
+ const swr = TTL_CONFIG.DEFAULT_SWR;
7949
+ const headers = new Headers(response.headers);
7950
+ const existingCacheControl = response.headers.get("Cache-Control");
7951
+ if (!existingCacheControl || existingCacheControl === "public") {
7952
+ const internalMaxAge = ttl + swr;
7953
+ headers.set("Cache-Control", `public, max-age=${internalMaxAge}`);
7954
+ }
7955
+ headers.set("X-Cache-Time", (/* @__PURE__ */ new Date()).toISOString());
7956
+ headers.set("X-Original-TTL", ttl.toString());
7957
+ headers.set("X-Original-SWR", swr.toString());
7958
+ const cacheableResponse = new Response(response.body, {
7959
+ status: response.status,
7960
+ statusText: response.statusText,
7961
+ headers
7962
+ });
7963
+ await cache.put(cacheKey, cacheableResponse);
7964
+ logger.debug("[SDK Html-cache] put done", { trace });
7965
+ } catch (_) {
7966
+ logger.warn("[SDK Html-cache] no put support", { trace });
7967
+ }
7968
+ }
7969
+ /**
7970
+ * Build client response with correct headers
7971
+ */
7972
+ buildClientResponse(cachedResponse, ttl, swr, isStale, age) {
7973
+ const headers = new Headers(cachedResponse.headers);
7974
+ headers.set(
7975
+ "Cache-Control",
7976
+ `public, max-age=${ttl}, stale-while-revalidate=${swr}`
7977
+ );
7978
+ headers.set("X-Cache-Status", isStale ? "STALE" : "HIT");
7979
+ headers.set("X-Cache-Age", Math.floor(age).toString());
7980
+ headers.delete("X-Original-TTL");
7981
+ headers.delete("X-Original-SWR");
7982
+ return new Response(cachedResponse.body, {
7983
+ status: cachedResponse.status,
7984
+ statusText: cachedResponse.statusText,
7985
+ headers
7986
+ });
7987
+ }
7988
+ /**
7989
+ * Build cache key from request using two-level structure
7990
+ */
7991
+ buildCacheKey(request) {
7992
+ const url = new URL(request.url);
7993
+ const versionHash = this.generateVersionHash(request.headers);
7994
+ const normalizedQuery = this.normalizeSearchParams(url.searchParams);
7995
+ const cacheKeyPath = `${versionHash}${url.pathname}`;
7996
+ const keyUrl = new URL(`${CACHE_KEY_ORIGIN2}${cacheKeyPath}`);
7997
+ if (normalizedQuery) {
7998
+ keyUrl.search = `?${normalizedQuery}`;
7999
+ }
8000
+ const sanitizedHeaders = this.sanitizeHeaders(request.headers);
8001
+ return new Request(keyUrl.toString(), {
8002
+ method: "GET",
8003
+ headers: sanitizedHeaders
8004
+ });
8005
+ }
8006
+ /**
8007
+ * Sanitize headers for cache operations - minimal whitelist approach
8008
+ */
8009
+ sanitizeHeaders(originalHeaders) {
8010
+ const CACHE_RELEVANT_HEADERS = [
8011
+ // Content negotiation (affects response format)
8012
+ "accept",
8013
+ "accept-encoding",
8014
+ "accept-language",
8015
+ // Cache control directives from client/proxy
8016
+ "cache-control",
8017
+ "pragma",
8018
+ // Legacy cache control
8019
+ // Conditional request headers (for 304 responses)
8020
+ "if-none-match",
8021
+ "if-modified-since"
8022
+ ];
8023
+ const sanitized = new Headers();
8024
+ CACHE_RELEVANT_HEADERS.forEach((header) => {
8025
+ const value = originalHeaders.get(header);
8026
+ if (value) {
8027
+ sanitized.set(header, value);
8028
+ }
8029
+ });
8030
+ return sanitized;
8031
+ }
8032
+ /**
8033
+ * Generate version hash from headers and cookies
8034
+ */
8035
+ generateVersionHash(headers) {
8036
+ const swellData = this.extractSwellData(headers);
8037
+ const versionFactors = {
8038
+ store: headers.get("swell-storefront-id") || "",
8039
+ auth: headers.get("swell-access-token") || "",
8040
+ theme: headers.get("swell-theme-version-hash") || "",
8041
+ currency: swellData["swell-currency"] || "USD",
8042
+ locale: headers.get("x-locale") || headers.get("accept-language")?.split(",")[0] || "default",
8043
+ context: headers.get("swell-storefront-context"),
8044
+ epoch: this.epoch
8045
+ };
8046
+ return md5(JSON.stringify(versionFactors));
8047
+ }
8048
+ /**
8049
+ * Extract swell-data from cookies
8050
+ */
8051
+ extractSwellData(headers) {
8052
+ const cookie = headers.get("cookie");
8053
+ if (!cookie) return {};
8054
+ const swellDataMatch = cookie.match(/swell-data=([^;]+)/);
8055
+ if (!swellDataMatch) return {};
8056
+ try {
8057
+ const parsed = JSON.parse(decodeURIComponent(swellDataMatch[1]));
8058
+ if (typeof parsed === "object" && parsed !== null) {
8059
+ return parsed;
8060
+ }
8061
+ return {};
8062
+ } catch {
8063
+ return {};
8064
+ }
8065
+ }
8066
+ isCacheable(request) {
8067
+ const url = new URL(request.url);
8068
+ const headers = request.headers;
8069
+ if (headers.get("swell-deployment-mode") === "editor") {
8070
+ return false;
8071
+ }
8072
+ const skipPaths = ["/checkout"];
8073
+ if (skipPaths.some((path) => url.pathname.startsWith(path))) {
8074
+ return false;
8075
+ }
8076
+ if (headers.get("cache-control")?.includes("no-cache")) {
8077
+ return false;
8078
+ }
8079
+ return true;
8080
+ }
8081
+ isResponseCacheable(response) {
8082
+ const contentType = response.headers.get("content-type");
8083
+ if (!contentType?.includes("text/html")) {
8084
+ return false;
8085
+ }
8086
+ const cacheControl = response.headers.get("cache-control");
8087
+ if (cacheControl?.includes("no-store") || cacheControl?.includes("private")) {
8088
+ return false;
8089
+ }
8090
+ return true;
8091
+ }
8092
+ getTTLForRequest(request) {
8093
+ const url = new URL(request.url);
8094
+ const path = url.pathname;
8095
+ if (path === "/") {
8096
+ return TTL_CONFIG.HOME;
8097
+ }
8098
+ if (path.startsWith("/products/")) {
8099
+ return TTL_CONFIG.PRODUCT;
8100
+ }
8101
+ if (path.startsWith("/categories/")) {
8102
+ return TTL_CONFIG.COLLECTION;
8103
+ }
8104
+ if (path.startsWith("/pages/")) {
8105
+ return TTL_CONFIG.PAGE;
8106
+ }
8107
+ if (path.startsWith("/blogs/")) {
8108
+ return TTL_CONFIG.BLOG;
8109
+ }
8110
+ return TTL_CONFIG.DEFAULT;
8111
+ }
8112
+ getResponseAge(response) {
8113
+ const cacheTime = response.headers.get("X-Cache-Time");
8114
+ if (!cacheTime) {
8115
+ return Infinity;
8116
+ }
8117
+ const cacheDate = new Date(cacheTime);
8118
+ if (isNaN(cacheDate.getTime())) {
8119
+ return Infinity;
8120
+ }
8121
+ const age = (Date.now() - cacheDate.getTime()) / 1e3;
8122
+ return Math.max(0, age);
8123
+ }
8124
+ getMaxAge(response) {
8125
+ const cacheControl = response.headers.get("Cache-Control");
8126
+ if (!cacheControl) {
8127
+ return TTL_CONFIG.DEFAULT;
8128
+ }
8129
+ const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
8130
+ if (maxAgeMatch) {
8131
+ return parseInt(maxAgeMatch[1], 10);
8132
+ }
8133
+ return TTL_CONFIG.DEFAULT;
8134
+ }
8135
+ getStaleWindow(response) {
8136
+ const cacheControl = response.headers.get("Cache-Control");
8137
+ if (!cacheControl) {
8138
+ return TTL_CONFIG.DEFAULT_SWR;
8139
+ }
8140
+ const swrMatch = cacheControl.match(/stale-while-revalidate=(\d+)/);
8141
+ if (swrMatch) {
8142
+ return parseInt(swrMatch[1], 10);
8143
+ }
8144
+ return TTL_CONFIG.DEFAULT_SWR;
8145
+ }
8146
+ /**
8147
+ * Normalize search params for cache key
8148
+ */
8149
+ normalizeSearchParams(searchParams) {
8150
+ const ignoredParams = [
8151
+ "utm_source",
8152
+ "utm_medium",
8153
+ "utm_campaign",
8154
+ "utm_content",
8155
+ "utm_term",
8156
+ "fbclid",
8157
+ "gclid",
8158
+ "gbraid",
8159
+ "wbraid",
8160
+ "ref",
8161
+ "source",
8162
+ "mc_cid",
8163
+ "mc_eid"
8164
+ ];
8165
+ const relevantParams = [];
8166
+ searchParams.forEach((value, key) => {
8167
+ if (!ignoredParams.includes(key)) {
8168
+ relevantParams.push(
8169
+ `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
8170
+ );
8171
+ }
8172
+ });
8173
+ return relevantParams.sort().join("&");
8174
+ }
8175
+ };
8176
+
7852
8177
  // src/resources/addresses.ts
7853
8178
  var SwellAddresses = class extends SwellStorefrontCollection {
7854
8179
  constructor(swell, query) {
@@ -8676,13 +9001,19 @@ var Swell = class _Swell {
8676
9001
  const storefrontRequest = storefront.request;
8677
9002
  return (method, url, id, data, opt) => {
8678
9003
  if (this.isStorefrontRequestCacheable(method, url, opt)) {
8679
- return this.getRequestCache().fetchSWR(
8680
- getCacheKey("request", [this.instanceId, method, url, id, data, opt]),
8681
- () => {
8682
- logger.info("[SDK] Storefront request", { method, url, id, data });
8683
- return storefrontRequest(method, url, id, data, opt);
8684
- }
8685
- );
9004
+ const key = getCacheKey("request", [
9005
+ this.instanceId,
9006
+ method,
9007
+ url,
9008
+ id,
9009
+ data,
9010
+ opt
9011
+ ]);
9012
+ return this.getRequestCache().fetchSWR(key, () => {
9013
+ const requestUrl = id ? `${url}/${id}` : url;
9014
+ logger.debug("[SDK] Cacheable API request", { url: requestUrl, key });
9015
+ return storefrontRequest(method, url, id, data, opt);
9016
+ });
8686
9017
  }
8687
9018
  switch (method) {
8688
9019
  case "delete":
@@ -8729,7 +9060,9 @@ var Swell = class _Swell {
8729
9060
  };
8730
9061
  function getCacheKey(key, args) {
8731
9062
  if (Array.isArray(args) && args.length > 0) {
8732
- return `${key}_${md5(JSON.stringify(args))}`;
9063
+ const fullKey = `${key}_${md5(JSON.stringify(args))}`;
9064
+ logger.debug(`[SDK] make cache key: ${fullKey}`);
9065
+ return fullKey;
8733
9066
  }
8734
9067
  return key;
8735
9068
  }
@@ -22504,6 +22837,7 @@ function getResourceQuery(slug, query) {
22504
22837
  ThemeFont,
22505
22838
  ThemeForm,
22506
22839
  ThemeFormErrors,
22840
+ WorkerHtmlCache,
22507
22841
  adaptShopifyFontData,
22508
22842
  adaptShopifyFormData,
22509
22843
  adaptShopifyMenuData,