perspectapi-ts-sdk 1.5.2 → 2.1.0

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.js CHANGED
@@ -23,13 +23,16 @@ __export(index_exports, {
23
23
  ApiKeysClient: () => ApiKeysClient,
24
24
  AuthClient: () => AuthClient,
25
25
  BaseClient: () => BaseClient,
26
+ CacheManager: () => CacheManager,
26
27
  CategoriesClient: () => CategoriesClient,
27
28
  CheckoutClient: () => CheckoutClient,
28
29
  ContactClient: () => ContactClient,
29
30
  ContentClient: () => ContentClient,
30
31
  DEFAULT_IMAGE_SIZES: () => DEFAULT_IMAGE_SIZES,
31
32
  HttpClient: () => HttpClient,
33
+ InMemoryCacheAdapter: () => InMemoryCacheAdapter,
32
34
  NewsletterClient: () => NewsletterClient,
35
+ NoopCacheAdapter: () => NoopCacheAdapter,
33
36
  OrganizationsClient: () => OrganizationsClient,
34
37
  PerspectApiClient: () => PerspectApiClient,
35
38
  ProductsClient: () => ProductsClient,
@@ -263,13 +266,287 @@ function createApiError(error) {
263
266
  };
264
267
  }
265
268
 
269
+ // src/cache/in-memory-adapter.ts
270
+ var InMemoryCacheAdapter = class {
271
+ store = /* @__PURE__ */ new Map();
272
+ async get(key) {
273
+ const entry = this.store.get(key);
274
+ if (!entry) {
275
+ return void 0;
276
+ }
277
+ if (entry.expiresAt && entry.expiresAt <= Date.now()) {
278
+ this.store.delete(key);
279
+ return void 0;
280
+ }
281
+ return entry.value;
282
+ }
283
+ async set(key, value, options) {
284
+ const expiresAt = options?.ttlSeconds && options.ttlSeconds > 0 ? Date.now() + options.ttlSeconds * 1e3 : void 0;
285
+ this.store.set(key, { value, expiresAt });
286
+ }
287
+ async delete(key) {
288
+ this.store.delete(key);
289
+ }
290
+ async deleteMany(keys) {
291
+ keys.forEach((key) => this.store.delete(key));
292
+ }
293
+ async clear() {
294
+ this.store.clear();
295
+ }
296
+ };
297
+
298
+ // src/cache/noop-adapter.ts
299
+ var NoopCacheAdapter = class {
300
+ async get() {
301
+ return void 0;
302
+ }
303
+ async set() {
304
+ }
305
+ async delete() {
306
+ }
307
+ async deleteMany() {
308
+ }
309
+ async clear() {
310
+ }
311
+ };
312
+
313
+ // src/cache/cache-manager.ts
314
+ var TAG_PREFIX = "__tag__";
315
+ var CacheManager = class {
316
+ adapter;
317
+ defaultTtlSeconds;
318
+ keyPrefix;
319
+ enabled;
320
+ constructor(config) {
321
+ const defaultOptions = {
322
+ defaultTtlSeconds: 300,
323
+ keyPrefix: "perspectapi"
324
+ };
325
+ const mergedConfig = {
326
+ ...defaultOptions,
327
+ ...config
328
+ };
329
+ if (config && config.enabled === false) {
330
+ this.enabled = false;
331
+ this.adapter = new NoopCacheAdapter();
332
+ } else if (config && config.adapter) {
333
+ this.enabled = true;
334
+ this.adapter = config.adapter;
335
+ } else if (config) {
336
+ this.enabled = true;
337
+ this.adapter = new InMemoryCacheAdapter();
338
+ } else {
339
+ this.enabled = false;
340
+ this.adapter = new NoopCacheAdapter();
341
+ }
342
+ this.defaultTtlSeconds = mergedConfig.defaultTtlSeconds ?? 300;
343
+ this.keyPrefix = mergedConfig.keyPrefix ?? "perspectapi";
344
+ }
345
+ isEnabled() {
346
+ return this.enabled;
347
+ }
348
+ getKeyPrefix() {
349
+ return this.keyPrefix;
350
+ }
351
+ buildKey(parts) {
352
+ const normalized = parts.flatMap((part) => this.normalizeKeyPart(part)).filter((part) => part !== void 0 && part !== null && part !== "");
353
+ return normalized.join(":");
354
+ }
355
+ async getOrSet(key, resolveValue, policy) {
356
+ if (!this.enabled || policy?.skipCache) {
357
+ console.log("[Cache] Cache disabled or skipped", { key, enabled: this.enabled, skipCache: policy?.skipCache });
358
+ const value2 = await resolveValue();
359
+ if (this.enabled && !policy?.skipCache && policy?.ttlSeconds !== 0) {
360
+ await this.set(key, value2, policy);
361
+ }
362
+ return value2;
363
+ }
364
+ const namespacedKey = this.namespacedKey(key);
365
+ const cachedRaw = await this.adapter.get(namespacedKey);
366
+ if (cachedRaw) {
367
+ const entry = this.deserialize(cachedRaw);
368
+ if (!entry.expiresAt || entry.expiresAt > Date.now()) {
369
+ console.log("[Cache] \u2713 HIT", { key, tags: entry.tags });
370
+ return entry.value;
371
+ }
372
+ console.log("[Cache] \u2717 EXPIRED", { key, expiresAt: new Date(entry.expiresAt) });
373
+ await this.adapter.delete(namespacedKey);
374
+ if (entry.tags?.length) {
375
+ await this.removeKeyFromTags(namespacedKey, entry.tags);
376
+ }
377
+ } else {
378
+ console.log("[Cache] \u2717 MISS", { key });
379
+ }
380
+ const value = await resolveValue();
381
+ await this.set(key, value, policy);
382
+ return value;
383
+ }
384
+ async set(key, value, options) {
385
+ if (!this.enabled || options?.ttlSeconds === 0) {
386
+ return;
387
+ }
388
+ const namespacedKey = this.namespacedKey(key);
389
+ const ttlSeconds = options?.ttlSeconds ?? this.defaultTtlSeconds;
390
+ const entry = {
391
+ value,
392
+ expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1e3 : void 0,
393
+ tags: options?.tags,
394
+ metadata: options?.metadata
395
+ };
396
+ console.log("[Cache] SET", { key, ttlSeconds, tags: options?.tags });
397
+ await this.adapter.set(namespacedKey, this.serialize(entry), {
398
+ ttlSeconds: ttlSeconds > 0 ? ttlSeconds : void 0
399
+ });
400
+ if (options?.tags?.length) {
401
+ await this.registerKeyTags(namespacedKey, options.tags);
402
+ }
403
+ }
404
+ async delete(key) {
405
+ if (!this.enabled) {
406
+ return;
407
+ }
408
+ const namespacedKey = this.namespacedKey(key);
409
+ await this.adapter.delete(namespacedKey);
410
+ }
411
+ async invalidate(options) {
412
+ if (!this.enabled) {
413
+ return;
414
+ }
415
+ let totalInvalidated = 0;
416
+ if (options.keys?.length) {
417
+ const namespacedKeys = options.keys.map((key) => this.namespacedKey(key));
418
+ console.log("[Cache] INVALIDATE by keys", { count: options.keys.length, keys: options.keys });
419
+ if (this.adapter.deleteMany) {
420
+ await this.adapter.deleteMany(namespacedKeys);
421
+ } else {
422
+ await Promise.all(namespacedKeys.map((key) => this.adapter.delete(key)));
423
+ }
424
+ totalInvalidated += options.keys.length;
425
+ }
426
+ if (options.tags?.length) {
427
+ console.log("[Cache] INVALIDATE by tags", { tags: options.tags });
428
+ await Promise.all(
429
+ options.tags.map(async (tag) => {
430
+ const tagKey = this.tagKey(tag);
431
+ const payload = await this.adapter.get(tagKey);
432
+ if (!payload) {
433
+ console.log("[Cache] No entries for tag", { tag });
434
+ return;
435
+ }
436
+ const keys = this.deserializeTagSet(payload);
437
+ if (keys.length) {
438
+ console.log("[Cache] Invalidating entries for tag", { tag, count: keys.length });
439
+ if (this.adapter.deleteMany) {
440
+ await this.adapter.deleteMany(keys);
441
+ } else {
442
+ await Promise.all(keys.map((key) => this.adapter.delete(key)));
443
+ }
444
+ totalInvalidated += keys.length;
445
+ }
446
+ await this.adapter.delete(tagKey);
447
+ })
448
+ );
449
+ }
450
+ console.log("[Cache] \u2713 INVALIDATED", { totalEntries: totalInvalidated, keys: options.keys?.length || 0, tags: options.tags?.length || 0 });
451
+ }
452
+ namespacedKey(key) {
453
+ return `${this.keyPrefix}:${key}`;
454
+ }
455
+ tagKey(tag) {
456
+ return this.namespacedKey(`${TAG_PREFIX}:${tag}`);
457
+ }
458
+ serialize(entry) {
459
+ return JSON.stringify(entry);
460
+ }
461
+ deserialize(payload) {
462
+ try {
463
+ return JSON.parse(payload);
464
+ } catch {
465
+ return { value: payload };
466
+ }
467
+ }
468
+ deserializeTagSet(payload) {
469
+ try {
470
+ const parsed = JSON.parse(payload);
471
+ return Array.isArray(parsed) ? parsed : [];
472
+ } catch {
473
+ return [];
474
+ }
475
+ }
476
+ async registerKeyTags(namespacedKey, tags) {
477
+ await Promise.all(
478
+ tags.map(async (tag) => {
479
+ const tagKey = this.tagKey(tag);
480
+ const existingPayload = await this.adapter.get(tagKey);
481
+ const keys = existingPayload ? this.deserializeTagSet(existingPayload) : [];
482
+ if (!keys.includes(namespacedKey)) {
483
+ keys.push(namespacedKey);
484
+ await this.adapter.set(tagKey, JSON.stringify(keys));
485
+ }
486
+ })
487
+ );
488
+ }
489
+ async removeKeyFromTags(namespacedKey, tags) {
490
+ await Promise.all(
491
+ tags.map(async (tag) => {
492
+ const tagKey = this.tagKey(tag);
493
+ const existingPayload = await this.adapter.get(tagKey);
494
+ if (!existingPayload) {
495
+ return;
496
+ }
497
+ const keys = this.deserializeTagSet(existingPayload);
498
+ const updated = keys.filter((key) => key !== namespacedKey);
499
+ if (updated.length === 0) {
500
+ await this.adapter.delete(tagKey);
501
+ } else if (updated.length !== keys.length) {
502
+ await this.adapter.set(tagKey, JSON.stringify(updated));
503
+ }
504
+ })
505
+ );
506
+ }
507
+ normalizeKeyPart(part) {
508
+ if (part === void 0 || part === null || part === "") {
509
+ return [];
510
+ }
511
+ if (Array.isArray(part)) {
512
+ return part.flatMap((item) => this.normalizeKeyPart(item));
513
+ }
514
+ if (typeof part === "object") {
515
+ return [this.normalizeObject(part)];
516
+ }
517
+ return [String(part)];
518
+ }
519
+ normalizeObject(input) {
520
+ const sortedKeys = Object.keys(input).sort();
521
+ const normalized = {};
522
+ for (const key of sortedKeys) {
523
+ const value = input[key];
524
+ if (value === void 0) {
525
+ continue;
526
+ }
527
+ if (value && typeof value === "object" && !Array.isArray(value)) {
528
+ normalized[key] = JSON.parse(this.normalizeObject(value));
529
+ } else if (Array.isArray(value)) {
530
+ normalized[key] = value.map(
531
+ (item) => typeof item === "object" && item !== null ? JSON.parse(this.normalizeObject(item)) : item
532
+ );
533
+ } else {
534
+ normalized[key] = value;
535
+ }
536
+ }
537
+ return JSON.stringify(normalized);
538
+ }
539
+ };
540
+
266
541
  // src/client/base-client.ts
267
542
  var BaseClient = class {
268
543
  http;
269
544
  basePath;
270
- constructor(http, basePath) {
545
+ cache;
546
+ constructor(http, basePath, cache) {
271
547
  this.http = http;
272
548
  this.basePath = basePath;
549
+ this.cache = cache && cache.isEnabled() ? cache : void 0;
273
550
  }
274
551
  /**
275
552
  * Build a site-scoped endpoint relative to the API base path
@@ -329,12 +606,71 @@ var BaseClient = class {
329
606
  async delete(endpoint, csrfToken) {
330
607
  return this.http.delete(this.buildPath(endpoint), { csrfToken });
331
608
  }
609
+ /**
610
+ * Fetch a GET endpoint with optional caching support.
611
+ */
612
+ async fetchWithCache(endpoint, params, tags, policy, fetcher) {
613
+ if (!this.cache) {
614
+ return fetcher();
615
+ }
616
+ const cacheKey = this.buildCacheKey(endpoint, params);
617
+ const combinedPolicy = policy ? { ...policy, tags: this.mergeTags(tags, policy.tags) } : { tags };
618
+ return this.cache.getOrSet(cacheKey, fetcher, combinedPolicy);
619
+ }
620
+ /**
621
+ * Invalidate cache entries by keys or tags.
622
+ */
623
+ async invalidateCache(options) {
624
+ if (!this.cache) {
625
+ return;
626
+ }
627
+ await this.cache.invalidate(options);
628
+ }
629
+ /**
630
+ * Build a consistent cache key for an endpoint + params combination.
631
+ */
632
+ buildCacheKey(endpoint, params) {
633
+ const sanitizedEndpoint = endpoint.replace(/^\//, "");
634
+ const baseSegment = this.basePath.replace(/^\//, "");
635
+ const parts = [baseSegment, sanitizedEndpoint];
636
+ if (params && Object.keys(params).length > 0) {
637
+ parts.push(this.sortObject(params));
638
+ }
639
+ if (this.cache) {
640
+ return this.cache.buildKey(parts);
641
+ }
642
+ return parts.map((part) => typeof part === "string" ? part : JSON.stringify(part)).join(":");
643
+ }
644
+ mergeTags(defaultTags, overrideTags) {
645
+ const combined = /* @__PURE__ */ new Set();
646
+ defaultTags.forEach((tag) => combined.add(tag));
647
+ overrideTags?.forEach((tag) => combined.add(tag));
648
+ return Array.from(combined.values());
649
+ }
650
+ sortObject(input) {
651
+ const sortedKeys = Object.keys(input).sort();
652
+ const result = {};
653
+ for (const key of sortedKeys) {
654
+ const value = input[key];
655
+ if (value === void 0) continue;
656
+ if (Array.isArray(value)) {
657
+ result[key] = value.map(
658
+ (item) => typeof item === "object" && item !== null ? this.sortObject(item) : item
659
+ );
660
+ } else if (value && typeof value === "object") {
661
+ result[key] = this.sortObject(value);
662
+ } else {
663
+ result[key] = value;
664
+ }
665
+ }
666
+ return result;
667
+ }
332
668
  };
333
669
 
334
670
  // src/client/auth-client.ts
335
671
  var AuthClient = class extends BaseClient {
336
- constructor(http) {
337
- super(http, "/api/v1");
672
+ constructor(http, cache) {
673
+ super(http, "/api/v1", cache);
338
674
  }
339
675
  /**
340
676
  * Get CSRF token
@@ -395,28 +731,50 @@ var AuthClient = class extends BaseClient {
395
731
 
396
732
  // src/client/content-client.ts
397
733
  var ContentClient = class extends BaseClient {
398
- constructor(http) {
399
- super(http, "/api/v1");
734
+ constructor(http, cache) {
735
+ super(http, "/api/v1", cache);
400
736
  }
401
737
  /**
402
738
  * Get all content with pagination and filtering for a site
403
739
  */
404
- async getContent(siteName, params) {
405
- return this.http.get(this.buildPath(this.siteScopedEndpoint(siteName)), params);
740
+ async getContent(siteName, params, cachePolicy) {
741
+ const endpoint = this.siteScopedEndpoint(siteName);
742
+ const path = this.buildPath(endpoint);
743
+ return this.fetchWithCache(
744
+ endpoint,
745
+ params,
746
+ this.buildContentTags(siteName),
747
+ cachePolicy,
748
+ () => this.http.get(path, params)
749
+ );
406
750
  }
407
751
  /**
408
752
  * Get content by ID
409
753
  */
410
- async getContentById(id) {
411
- return this.getSingle(`/content/${id}`);
754
+ async getContentById(id, cachePolicy) {
755
+ const endpoint = `/content/${id}`;
756
+ const path = this.buildPath(endpoint);
757
+ return this.fetchWithCache(
758
+ endpoint,
759
+ void 0,
760
+ this.buildContentTags(void 0, void 0, id),
761
+ cachePolicy,
762
+ () => this.http.get(path)
763
+ );
412
764
  }
413
765
  /**
414
766
  * Get content by slug for a site
415
767
  */
416
- async getContentBySlug(siteName, slug) {
417
- return this.http.get(this.buildPath(
418
- this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`)
419
- ));
768
+ async getContentBySlug(siteName, slug, cachePolicy) {
769
+ const endpoint = this.siteScopedEndpoint(siteName, `/slug/${encodeURIComponent(slug)}`);
770
+ const path = this.buildPath(endpoint);
771
+ return this.fetchWithCache(
772
+ endpoint,
773
+ void 0,
774
+ this.buildContentTags(siteName, slug),
775
+ cachePolicy,
776
+ () => this.http.get(path)
777
+ );
420
778
  }
421
779
  /**
422
780
  * Create new content
@@ -478,12 +836,25 @@ var ContentClient = class extends BaseClient {
478
836
  async duplicateContent(id) {
479
837
  return this.create(`/content/${id}/duplicate`, {});
480
838
  }
839
+ buildContentTags(siteName, slug, id) {
840
+ const tags = /* @__PURE__ */ new Set(["content"]);
841
+ if (siteName) {
842
+ tags.add(`content:site:${siteName}`);
843
+ }
844
+ if (slug) {
845
+ tags.add(`content:slug:${siteName}:${slug}`);
846
+ }
847
+ if (typeof id === "number") {
848
+ tags.add(`content:id:${id}`);
849
+ }
850
+ return Array.from(tags.values());
851
+ }
481
852
  };
482
853
 
483
854
  // src/client/api-keys-client.ts
484
855
  var ApiKeysClient = class extends BaseClient {
485
- constructor(http) {
486
- super(http, "/api/v1");
856
+ constructor(http, cache) {
857
+ super(http, "/api/v1", cache);
487
858
  }
488
859
  /**
489
860
  * Get all API keys
@@ -549,8 +920,8 @@ var ApiKeysClient = class extends BaseClient {
549
920
 
550
921
  // src/client/organizations-client.ts
551
922
  var OrganizationsClient = class extends BaseClient {
552
- constructor(http) {
553
- super(http, "/api/v1");
923
+ constructor(http, cache) {
924
+ super(http, "/api/v1", cache);
554
925
  }
555
926
  /**
556
927
  * Get all organizations
@@ -622,8 +993,8 @@ var OrganizationsClient = class extends BaseClient {
622
993
 
623
994
  // src/client/sites-client.ts
624
995
  var SitesClient = class extends BaseClient {
625
- constructor(http) {
626
- super(http, "/api/v1");
996
+ constructor(http, cache) {
997
+ super(http, "/api/v1", cache);
627
998
  }
628
999
  /**
629
1000
  * Get all sites
@@ -719,13 +1090,13 @@ var SitesClient = class extends BaseClient {
719
1090
 
720
1091
  // src/client/products-client.ts
721
1092
  var ProductsClient = class extends BaseClient {
722
- constructor(http) {
723
- super(http, "/api/v1");
1093
+ constructor(http, cache) {
1094
+ super(http, "/api/v1", cache);
724
1095
  }
725
1096
  /**
726
1097
  * Get all products for a site
727
1098
  */
728
- async getProducts(siteName, params) {
1099
+ async getProducts(siteName, params, cachePolicy) {
729
1100
  const normalizeList = (value) => {
730
1101
  if (value === void 0 || value === null) {
731
1102
  return void 0;
@@ -749,30 +1120,61 @@ var ProductsClient = class extends BaseClient {
749
1120
  delete normalizedParams.category_id;
750
1121
  }
751
1122
  }
752
- return this.http.get(
753
- this.buildPath(this.siteScopedEndpoint(siteName, "/products", { includeSitesSegment: false })),
754
- normalizedParams
1123
+ const endpoint = this.siteScopedEndpoint(siteName, "/products", { includeSitesSegment: false });
1124
+ const path = this.buildPath(endpoint);
1125
+ return this.fetchWithCache(
1126
+ endpoint,
1127
+ normalizedParams,
1128
+ this.buildProductTags(siteName, ["products:list"]),
1129
+ cachePolicy,
1130
+ () => this.http.get(path, normalizedParams)
755
1131
  );
756
1132
  }
757
1133
  /**
758
1134
  * Get product by ID
759
1135
  */
760
- async getProductById(id) {
761
- return this.getSingle(`/products/${id}`);
1136
+ async getProductById(id, cachePolicy) {
1137
+ const endpoint = `/products/${id}`;
1138
+ const path = this.buildPath(endpoint);
1139
+ return this.fetchWithCache(
1140
+ endpoint,
1141
+ void 0,
1142
+ this.buildProductTags(void 0, [`products:id:${id}`]),
1143
+ cachePolicy,
1144
+ () => this.http.get(path)
1145
+ );
762
1146
  }
763
1147
  /**
764
1148
  * Get product by SKU
765
1149
  */
766
- async getProductBySku(sku) {
767
- return this.getSingle(`/products/sku/${sku}`);
1150
+ async getProductBySku(sku, cachePolicy) {
1151
+ const endpoint = `/products/sku/${encodeURIComponent(sku)}`;
1152
+ const path = this.buildPath(endpoint);
1153
+ return this.fetchWithCache(
1154
+ endpoint,
1155
+ void 0,
1156
+ this.buildProductTags(void 0, [`products:sku:${sku.toLowerCase()}`]),
1157
+ cachePolicy,
1158
+ () => this.http.get(path)
1159
+ );
768
1160
  }
769
1161
  /**
770
1162
  * Get product by slug and site name
771
1163
  */
772
- async getProductBySlug(siteName, slug) {
773
- return this.http.get(this.buildPath(
774
- this.siteScopedEndpoint(siteName, `/products/slug/${encodeURIComponent(slug)}`, { includeSitesSegment: false })
775
- ));
1164
+ async getProductBySlug(siteName, slug, cachePolicy) {
1165
+ const endpoint = this.siteScopedEndpoint(
1166
+ siteName,
1167
+ `/products/slug/${encodeURIComponent(slug)}`,
1168
+ { includeSitesSegment: false }
1169
+ );
1170
+ const path = this.buildPath(endpoint);
1171
+ return this.fetchWithCache(
1172
+ endpoint,
1173
+ void 0,
1174
+ this.buildProductTags(siteName, [`products:slug:${siteName}:${slug}`]),
1175
+ cachePolicy,
1176
+ () => this.http.get(path)
1177
+ );
776
1178
  }
777
1179
  /**
778
1180
  * Create new product
@@ -861,27 +1263,44 @@ var ProductsClient = class extends BaseClient {
861
1263
  /**
862
1264
  * Get products by category slug
863
1265
  */
864
- async getProductsByCategorySlug(siteName, categorySlug, params) {
1266
+ async getProductsByCategorySlug(siteName, categorySlug, params, cachePolicy) {
865
1267
  const queryParams = params ? {
866
1268
  limit: params.limit,
867
1269
  offset: params.page ? (params.page - 1) * (params.limit || 20) : void 0,
868
1270
  published: params.published,
869
1271
  search: params.search
870
1272
  } : void 0;
871
- return this.http.get(this.buildPath(
872
- this.siteScopedEndpoint(
873
- siteName,
874
- `/products/category/${encodeURIComponent(categorySlug)}`,
875
- { includeSitesSegment: false }
876
- )
877
- ), queryParams);
1273
+ const endpoint = this.siteScopedEndpoint(
1274
+ siteName,
1275
+ `/products/category/${encodeURIComponent(categorySlug)}`,
1276
+ { includeSitesSegment: false }
1277
+ );
1278
+ const path = this.buildPath(endpoint);
1279
+ return this.fetchWithCache(
1280
+ endpoint,
1281
+ queryParams,
1282
+ this.buildProductTags(siteName, [
1283
+ "products:category",
1284
+ `products:category:${siteName}:${categorySlug}`
1285
+ ]),
1286
+ cachePolicy,
1287
+ () => this.http.get(path, queryParams)
1288
+ );
1289
+ }
1290
+ buildProductTags(siteName, extraTags = []) {
1291
+ const tags = /* @__PURE__ */ new Set(["products"]);
1292
+ if (siteName) {
1293
+ tags.add(`products:site:${siteName}`);
1294
+ }
1295
+ extraTags.filter(Boolean).forEach((tag) => tags.add(tag));
1296
+ return Array.from(tags.values());
878
1297
  }
879
1298
  };
880
1299
 
881
1300
  // src/client/categories-client.ts
882
1301
  var CategoriesClient = class extends BaseClient {
883
- constructor(http) {
884
- super(http, "/api/v1");
1302
+ constructor(http, cache) {
1303
+ super(http, "/api/v1", cache);
885
1304
  }
886
1305
  /**
887
1306
  * Get all categories
@@ -904,14 +1323,20 @@ var CategoriesClient = class extends BaseClient {
904
1323
  /**
905
1324
  * Get product category by slug (for products)
906
1325
  */
907
- async getProductCategoryBySlug(siteName, slug) {
908
- return this.http.get(this.buildPath(
909
- this.siteScopedEndpoint(
910
- siteName,
911
- `/product_category/slug/${encodeURIComponent(slug)}`,
912
- { includeSitesSegment: false }
913
- )
914
- ));
1326
+ async getProductCategoryBySlug(siteName, slug, cachePolicy) {
1327
+ const endpoint = this.siteScopedEndpoint(
1328
+ siteName,
1329
+ `/product_category/slug/${encodeURIComponent(slug)}`,
1330
+ { includeSitesSegment: false }
1331
+ );
1332
+ const path = this.buildPath(endpoint);
1333
+ return this.fetchWithCache(
1334
+ endpoint,
1335
+ void 0,
1336
+ this.buildCategoryTags(siteName, slug),
1337
+ cachePolicy,
1338
+ () => this.http.get(path)
1339
+ );
915
1340
  }
916
1341
  /**
917
1342
  * Create new category
@@ -980,12 +1405,22 @@ var CategoriesClient = class extends BaseClient {
980
1405
  async searchCategories(query, params) {
981
1406
  return this.http.get(`/categories/search`, { q: query, ...params });
982
1407
  }
1408
+ buildCategoryTags(siteName, slug) {
1409
+ const tags = /* @__PURE__ */ new Set(["categories"]);
1410
+ if (siteName) {
1411
+ tags.add(`categories:site:${siteName}`);
1412
+ }
1413
+ if (slug) {
1414
+ tags.add(`categories:product:${siteName}:${slug}`);
1415
+ }
1416
+ return Array.from(tags.values());
1417
+ }
983
1418
  };
984
1419
 
985
1420
  // src/client/webhooks-client.ts
986
1421
  var WebhooksClient = class extends BaseClient {
987
- constructor(http) {
988
- super(http, "/api/v1");
1422
+ constructor(http, cache) {
1423
+ super(http, "/api/v1", cache);
989
1424
  }
990
1425
  /**
991
1426
  * Get all webhooks
@@ -1082,8 +1517,8 @@ var WebhooksClient = class extends BaseClient {
1082
1517
 
1083
1518
  // src/client/checkout-client.ts
1084
1519
  var CheckoutClient = class extends BaseClient {
1085
- constructor(http) {
1086
- super(http, "/api/v1");
1520
+ constructor(http, cache) {
1521
+ super(http, "/api/v1", cache);
1087
1522
  }
1088
1523
  /**
1089
1524
  * Get CSRF token for a specific site
@@ -1194,8 +1629,8 @@ var CheckoutClient = class extends BaseClient {
1194
1629
 
1195
1630
  // src/client/contact-client.ts
1196
1631
  var ContactClient = class extends BaseClient {
1197
- constructor(http) {
1198
- super(http, "/api/v1");
1632
+ constructor(http, cache) {
1633
+ super(http, "/api/v1", cache);
1199
1634
  }
1200
1635
  /**
1201
1636
  * Build a contact endpoint scoped to a site (without /sites prefix)
@@ -1301,8 +1736,8 @@ var ContactClient = class extends BaseClient {
1301
1736
 
1302
1737
  // src/client/newsletter-client.ts
1303
1738
  var NewsletterClient = class extends BaseClient {
1304
- constructor(http) {
1305
- super(http, "/api/v1");
1739
+ constructor(http, cache) {
1740
+ super(http, "/api/v1", cache);
1306
1741
  }
1307
1742
  /**
1308
1743
  * Build a newsletter endpoint scoped to a site (without /sites prefix)
@@ -1497,6 +1932,7 @@ var NewsletterClient = class extends BaseClient {
1497
1932
  // src/perspect-api-client.ts
1498
1933
  var PerspectApiClient = class {
1499
1934
  http;
1935
+ cache;
1500
1936
  // Service clients
1501
1937
  auth;
1502
1938
  content;
@@ -1514,17 +1950,18 @@ var PerspectApiClient = class {
1514
1950
  throw new Error("baseUrl is required in PerspectApiConfig");
1515
1951
  }
1516
1952
  this.http = new HttpClient(config);
1517
- this.auth = new AuthClient(this.http);
1518
- this.content = new ContentClient(this.http);
1519
- this.apiKeys = new ApiKeysClient(this.http);
1520
- this.organizations = new OrganizationsClient(this.http);
1521
- this.sites = new SitesClient(this.http);
1522
- this.products = new ProductsClient(this.http);
1523
- this.categories = new CategoriesClient(this.http);
1524
- this.webhooks = new WebhooksClient(this.http);
1525
- this.checkout = new CheckoutClient(this.http);
1526
- this.contact = new ContactClient(this.http);
1527
- this.newsletter = new NewsletterClient(this.http);
1953
+ this.cache = new CacheManager(config.cache);
1954
+ this.auth = new AuthClient(this.http, this.cache);
1955
+ this.content = new ContentClient(this.http, this.cache);
1956
+ this.apiKeys = new ApiKeysClient(this.http, this.cache);
1957
+ this.organizations = new OrganizationsClient(this.http, this.cache);
1958
+ this.sites = new SitesClient(this.http, this.cache);
1959
+ this.products = new ProductsClient(this.http, this.cache);
1960
+ this.categories = new CategoriesClient(this.http, this.cache);
1961
+ this.webhooks = new WebhooksClient(this.http, this.cache);
1962
+ this.checkout = new CheckoutClient(this.http, this.cache);
1963
+ this.contact = new ContactClient(this.http, this.cache);
1964
+ this.newsletter = new NewsletterClient(this.http, this.cache);
1528
1965
  }
1529
1966
  /**
1530
1967
  * Update authentication token
@@ -2105,13 +2542,16 @@ async function createCheckoutSession(options) {
2105
2542
  ApiKeysClient,
2106
2543
  AuthClient,
2107
2544
  BaseClient,
2545
+ CacheManager,
2108
2546
  CategoriesClient,
2109
2547
  CheckoutClient,
2110
2548
  ContactClient,
2111
2549
  ContentClient,
2112
2550
  DEFAULT_IMAGE_SIZES,
2113
2551
  HttpClient,
2552
+ InMemoryCacheAdapter,
2114
2553
  NewsletterClient,
2554
+ NoopCacheAdapter,
2115
2555
  OrganizationsClient,
2116
2556
  PerspectApiClient,
2117
2557
  ProductsClient,