perspectapi-ts-sdk 6.5.9 → 7.0.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/README.md +46 -1011
- package/dist/chunk-K3T2AFYA.mjs +1393 -0
- package/dist/index-CWvUyMt3.d.mts +2224 -0
- package/dist/index-CWvUyMt3.d.ts +2224 -0
- package/dist/index.d.mts +130 -2221
- package/dist/index.d.ts +130 -2221
- package/dist/index.js +8 -2
- package/dist/index.mjs +13 -1364
- package/dist/v2/index.d.mts +1 -0
- package/dist/v2/index.d.ts +1 -0
- package/dist/v2/index.js +1419 -0
- package/dist/v2/index.mjs +40 -0
- package/docs/README.md +15 -0
- package/docs/v1-deprecated/README.md +9 -0
- package/docs/v1-deprecated/examples/README.md +324 -0
- package/docs/v1-deprecated/examples/basic-usage.ts +258 -0
- package/docs/v1-deprecated/examples/cloudflare-worker.ts +274 -0
- package/docs/v1-deprecated/examples/content-query-with-slug-prefix.ts +237 -0
- package/docs/v1-deprecated/examples/image-transforms.ts +200 -0
- package/docs/v1-deprecated/examples/site-user-checkout.ts +186 -0
- package/docs/v1-deprecated/examples/slug-prefix-examples.ts +491 -0
- package/docs/v1-deprecated/legacy-docs/caching.md +667 -0
- package/docs/v1-deprecated/legacy-docs/contact.md +1396 -0
- package/docs/v1-deprecated/legacy-docs/csrf-protection.md +664 -0
- package/docs/v1-deprecated/legacy-docs/image-transforms.md +523 -0
- package/docs/v1-deprecated/legacy-docs/loaders.md +304 -0
- package/docs/v1-deprecated/legacy-docs/newsletter.md +811 -0
- package/docs/v1-deprecated/legacy-docs/site-users.md +817 -0
- package/docs/v1-deprecated/legacy-notes/CHANGELOG-CHECKOUT.md +143 -0
- package/docs/v1-deprecated/legacy-notes/CSRF-CHECKOUT.md +271 -0
- package/docs/v1-deprecated/legacy-notes/IMAGE_TRANSFORMS_PORT.md +298 -0
- package/docs/v1-deprecated/sdk-readme.md +1076 -0
- package/examples/README.md +19 -0
- package/examples/basic-v2.ts +37 -0
- package/llms.txt +25 -0
- package/package.json +18 -7
- package/src/client/api-keys-client.ts +4 -0
- package/src/client/auth-client.ts +4 -0
- package/src/client/base-client.ts +7 -0
- package/src/client/bundles-client.ts +4 -0
- package/src/client/categories-client.ts +4 -0
- package/src/client/checkout-client.ts +4 -0
- package/src/client/contact-client.ts +4 -0
- package/src/client/content-client.ts +4 -0
- package/src/client/newsletter-client.ts +4 -0
- package/src/client/newsletter-management-client.ts +4 -0
- package/src/client/organizations-client.ts +4 -0
- package/src/client/products-client.ts +4 -0
- package/src/client/site-users-client.ts +10 -1
- package/src/client/sites-client.ts +4 -0
- package/src/client/webhooks-client.ts +4 -0
- package/src/deprecation.ts +2 -1
- package/src/index.ts +2 -1
- package/src/loaders.ts +59 -0
- package/src/perspect-api-client.ts +2 -2
- package/src/v2/client/orders-client.ts +6 -1
- package/src/v2/types.ts +3 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
# Caching and Invalidation in PerspectAPI SDK
|
|
2
|
+
|
|
3
|
+
> Deprecated v1 material. Do not copy these examples into new code. v1 sunsets
|
|
4
|
+
> on 2026-06-01; use `createPerspectApiV2Client` from
|
|
5
|
+
> `perspectapi-ts-sdk/v2` and `/api/v2`.
|
|
6
|
+
|
|
7
|
+
This document covers how to configure, use, and invalidate the cache in the PerspectAPI TypeScript SDK.
|
|
8
|
+
|
|
9
|
+
## Configuration
|
|
10
|
+
|
|
11
|
+
Enable caching when creating the client:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { PerspectApiClient } from 'perspectapi-ts-sdk';
|
|
15
|
+
|
|
16
|
+
const client = new PerspectApiClient({
|
|
17
|
+
baseUrl: 'https://api.perspect.commm',
|
|
18
|
+
apiKey: 'your-api-key',
|
|
19
|
+
cache: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
defaultTtlSeconds: 300, // 5 minutes
|
|
22
|
+
keyPrefix: 'perspectapi'
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Infinite (Non-Expiring) Caching
|
|
28
|
+
|
|
29
|
+
To cache indefinitely (until you explicitly invalidate), set `defaultTtlSeconds` to `0`:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { PerspectApiClient } from 'perspectapi-ts-sdk';
|
|
33
|
+
|
|
34
|
+
const client = new PerspectApiClient({
|
|
35
|
+
baseUrl: 'https://api.perspect.commm',
|
|
36
|
+
apiKey: 'your-api-key',
|
|
37
|
+
cache: {
|
|
38
|
+
enabled: true,
|
|
39
|
+
defaultTtlSeconds: 0,
|
|
40
|
+
keyPrefix: 'perspectapi'
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
With `defaultTtlSeconds: 0`, cached entries do not expire based on time. They will remain cached until:
|
|
46
|
+
|
|
47
|
+
- **You invalidate them** via tags/keys.
|
|
48
|
+
- **Your underlying cache adapter evicts them** (for example due to memory limits).
|
|
49
|
+
|
|
50
|
+
### Cache Adapters
|
|
51
|
+
|
|
52
|
+
The SDK supports pluggable cache adapters:
|
|
53
|
+
|
|
54
|
+
- **Memory adapter** (default): In-memory cache, suitable for development
|
|
55
|
+
- **Custom adapters**: Implement the `CacheAdapter` interface for Redis, Cloudflare KV, etc.
|
|
56
|
+
|
|
57
|
+
## How Caching Works
|
|
58
|
+
|
|
59
|
+
### Automatic Caching on Reads
|
|
60
|
+
|
|
61
|
+
All GET requests through the content, products, and newsletter clients are automatically cached with appropriate tags:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// This response is cached automatically
|
|
65
|
+
const content = await client.content.getContent('my-site', {
|
|
66
|
+
page_status: 'publish',
|
|
67
|
+
limit: 20
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Subsequent calls with the same parameters return cached data
|
|
71
|
+
const cachedContent = await client.content.getContent('my-site', {
|
|
72
|
+
page_status: 'publish',
|
|
73
|
+
limit: 20
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Newsletter list + campaign endpoints are cached too
|
|
77
|
+
const campaigns = await client.newsletter.getPublishedCampaigns('my-site', {
|
|
78
|
+
page: 1,
|
|
79
|
+
limit: 20
|
|
80
|
+
});
|
|
81
|
+
const sameCampaigns = await client.newsletter.getPublishedCampaigns('my-site', {
|
|
82
|
+
page: 1,
|
|
83
|
+
limit: 20
|
|
84
|
+
});
|
|
85
|
+
const campaign = await client.newsletter.getPublishedCampaignBySlug(
|
|
86
|
+
'my-site',
|
|
87
|
+
'spring-launch',
|
|
88
|
+
{ slugPrefix: 'updates' }
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Cache Keys
|
|
93
|
+
|
|
94
|
+
Cache keys are built from:
|
|
95
|
+
1. The API base path
|
|
96
|
+
2. The endpoint
|
|
97
|
+
3. Query parameters (sorted for consistency)
|
|
98
|
+
|
|
99
|
+
Example key: `perspectapi:api/v1:sites/my-site:{"limit":20,"page_status":"publish"}`
|
|
100
|
+
|
|
101
|
+
### Cache Tags
|
|
102
|
+
|
|
103
|
+
Every cached entry is tagged for targeted invalidation. Tags follow a hierarchical pattern:
|
|
104
|
+
|
|
105
|
+
| Tag Pattern | Description |
|
|
106
|
+
|-------------|-------------|
|
|
107
|
+
| `content` | All content entries |
|
|
108
|
+
| `content:site:{siteName}` | All content for a specific site |
|
|
109
|
+
| `content:slug:{siteName}:{slug}` | A specific content item by slug |
|
|
110
|
+
| `content:id:{id}` | A specific content item by ID |
|
|
111
|
+
| `content:prefix:{prefix}` | All content with a specific slug prefix |
|
|
112
|
+
| `products` | All product entries |
|
|
113
|
+
| `products:site:{siteName}` | All products for a specific site |
|
|
114
|
+
| `products:slug:{siteName}:{slug}` | A specific product by slug |
|
|
115
|
+
| `products:id:{id}` | A specific product by ID |
|
|
116
|
+
| `products:prefix:{prefix}` | All products with a specific slug prefix |
|
|
117
|
+
| `products:list` | Product list queries |
|
|
118
|
+
| `products:category` | Category-filtered product queries |
|
|
119
|
+
| `products:category:{siteName}:{slug}` | Products in a specific category |
|
|
120
|
+
| `newsletter` | All newsletter entries |
|
|
121
|
+
| `newsletter:site:{siteName}` | All newsletter entries for a specific site |
|
|
122
|
+
| `newsletter:lists` | Newsletter list queries |
|
|
123
|
+
| `newsletter:lists:site:{siteName}` | Newsletter lists for a specific site |
|
|
124
|
+
| `newsletter:campaigns` | Published newsletter campaign queries |
|
|
125
|
+
| `newsletter:campaigns:list:{siteName}` | Published campaign list queries for a site |
|
|
126
|
+
| `newsletter:campaigns:detail:{siteName}` | Published campaign detail queries for a site |
|
|
127
|
+
| `newsletter:campaigns:slug:{siteName}:{slug}` | A specific published campaign by slug |
|
|
128
|
+
| `newsletter:campaigns:id:{siteName}:{campaignId}` | A specific published campaign by campaign ID |
|
|
129
|
+
| `newsletter:prefix:{prefix}` | Newsletter entries with a specific slug prefix |
|
|
130
|
+
|
|
131
|
+
## Cache Policies
|
|
132
|
+
|
|
133
|
+
Override default caching behavior per-request:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// Skip cache entirely
|
|
137
|
+
const fresh = await client.content.getContent('my-site', params, {
|
|
138
|
+
skipCache: true
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Custom TTL
|
|
142
|
+
const shortLived = await client.content.getContent('my-site', params, {
|
|
143
|
+
ttlSeconds: 60
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Cache indefinitely for this request (until invalidation)
|
|
147
|
+
const cachedForever = await client.content.getContent('my-site', params, {
|
|
148
|
+
ttlSeconds: 0
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Add extra tags
|
|
152
|
+
const tagged = await client.content.getContent('my-site', params, {
|
|
153
|
+
tags: ['homepage', 'featured']
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Invalidation
|
|
158
|
+
|
|
159
|
+
### By Tags
|
|
160
|
+
|
|
161
|
+
Invalidate all cache entries matching specific tags:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// Invalidate all content for a site
|
|
165
|
+
await cacheManager.invalidate({
|
|
166
|
+
tags: ['content:site:my-site']
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Invalidate a specific content item
|
|
170
|
+
await cacheManager.invalidate({
|
|
171
|
+
tags: ['content:slug:my-site:blog/my-post']
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Invalidate multiple tags at once
|
|
175
|
+
await cacheManager.invalidate({
|
|
176
|
+
tags: ['content:site:my-site', 'products:site:my-site']
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### By Keys
|
|
181
|
+
|
|
182
|
+
Invalidate specific cache keys directly:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
await cacheManager.invalidate({
|
|
186
|
+
keys: ['perspectapi:api/v1:sites/my-site:{"limit":20}']
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Webhook-Driven Invalidation for Newsletters
|
|
191
|
+
|
|
192
|
+
The newsletter client includes a helper that converts webhook payloads to the correct cache tags and invalidates them for you.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const result = await client.newsletter.invalidatePublishedCampaignCacheFromWebhook(
|
|
196
|
+
webhookPayload,
|
|
197
|
+
'my-site' // optional fallback when payload omits site name
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (result.invalidated) {
|
|
201
|
+
console.log('Invalidated tags:', result.tags);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`invalidatePublishedCampaignCacheFromWebhook` intentionally invalidates only publish events (for example `newsletter.published`, `newsletter_campaign.published`, or `content.published` with `origin_type = newsletter_campaign`). Create/draft updates are ignored.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Slug Prefix Caching
|
|
210
|
+
|
|
211
|
+
### What is a Slug Prefix?
|
|
212
|
+
|
|
213
|
+
A slug prefix is the first path segment of a hierarchical slug. For example:
|
|
214
|
+
|
|
215
|
+
| Full Slug | Prefix |
|
|
216
|
+
|-----------|--------|
|
|
217
|
+
| `blog/my-first-post` | `blog` |
|
|
218
|
+
| `blog/2024/annual-review` | `blog` |
|
|
219
|
+
| `shop/electronics/laptop` | `shop` |
|
|
220
|
+
| `about` | *(none)* |
|
|
221
|
+
|
|
222
|
+
Slug prefixes are commonly used to organize content by section (blog, shop, docs) or by language (en, es, fr).
|
|
223
|
+
|
|
224
|
+
### Why Cache by Slug Prefix?
|
|
225
|
+
|
|
226
|
+
Consider a site with this content structure:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
/blog/post-1
|
|
230
|
+
/blog/post-2
|
|
231
|
+
/blog/post-3
|
|
232
|
+
/shop/product-a
|
|
233
|
+
/shop/product-b
|
|
234
|
+
/about
|
|
235
|
+
/contact
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
When a blog post is updated, you need to invalidate:
|
|
239
|
+
1. The individual post cache (`content:slug:my-site:blog/post-1`)
|
|
240
|
+
2. Any list queries that included that post
|
|
241
|
+
|
|
242
|
+
Without prefix tagging, you'd have to either:
|
|
243
|
+
- Invalidate ALL content caches (too broad, poor cache hit rate)
|
|
244
|
+
- Track every list query that might include the post (complex and error-prone)
|
|
245
|
+
|
|
246
|
+
With prefix tagging, you can surgically invalidate just the blog section:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
await cacheManager.invalidate({
|
|
250
|
+
tags: ['content:prefix:blog']
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
This invalidates:
|
|
255
|
+
- All blog post individual page caches
|
|
256
|
+
- All list queries filtered by `slug_prefix: 'blog'`
|
|
257
|
+
|
|
258
|
+
But preserves:
|
|
259
|
+
- Shop product caches
|
|
260
|
+
- Other page caches (`/about`, `/contact`)
|
|
261
|
+
- List queries for other prefixes
|
|
262
|
+
|
|
263
|
+
### How Prefix Tags Are Applied
|
|
264
|
+
|
|
265
|
+
#### List Queries
|
|
266
|
+
|
|
267
|
+
When fetching lists with a `slug_prefix` parameter, the cache is tagged with that prefix:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// Tagged with: content, content:site:my-site, content:prefix:blog
|
|
271
|
+
const blogPosts = await client.content.getContent('my-site', {
|
|
272
|
+
slug_prefix: 'blog',
|
|
273
|
+
page_status: 'publish'
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Tagged with: products, products:site:my-site, products:prefix:shop, products:list
|
|
277
|
+
const shopProducts = await client.products.getProducts('my-site', {
|
|
278
|
+
slug_prefix: 'shop'
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### Individual Item Fetches
|
|
283
|
+
|
|
284
|
+
When fetching by slug, the prefix is automatically extracted:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// Tagged with: content, content:site:my-site, content:slug:my-site:blog/my-post, content:prefix:blog
|
|
288
|
+
const post = await client.content.getContentBySlug('my-site', 'blog/my-post');
|
|
289
|
+
|
|
290
|
+
// Tagged with: products, products:site:my-site, products:slug:my-site:shop/widget, products:prefix:shop
|
|
291
|
+
const product = await client.products.getProductBySlug('my-site', 'shop/widget');
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Invalidation Patterns
|
|
295
|
+
|
|
296
|
+
#### When a single item changes
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// Invalidate the specific item AND all prefix-filtered lists
|
|
300
|
+
await cacheManager.invalidate({
|
|
301
|
+
tags: [
|
|
302
|
+
'content:slug:my-site:blog/my-post', // The specific post
|
|
303
|
+
'content:prefix:blog' // All blog lists and posts
|
|
304
|
+
]
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### When rebuilding a section
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// Invalidate everything under the 'docs' prefix
|
|
312
|
+
await cacheManager.invalidate({
|
|
313
|
+
tags: ['content:prefix:docs']
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### When a new item is added
|
|
318
|
+
|
|
319
|
+
Adding a new item to a prefix section should invalidate list queries:
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// New blog post added - invalidate blog lists so they include the new post
|
|
323
|
+
await cacheManager.invalidate({
|
|
324
|
+
tags: ['content:prefix:blog']
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
#### Site-wide content update
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
// Nuclear option: invalidate all content for a site
|
|
332
|
+
await cacheManager.invalidate({
|
|
333
|
+
tags: ['content:site:my-site']
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Multi-Language Sites
|
|
338
|
+
|
|
339
|
+
Slug prefixes work well for language-segmented sites:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// Fetch English content
|
|
343
|
+
const enContent = await client.content.getContent('my-site', {
|
|
344
|
+
slug_prefix: 'en'
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Fetch Spanish content
|
|
348
|
+
const esContent = await client.content.getContent('my-site', {
|
|
349
|
+
slug_prefix: 'es'
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Invalidate only Spanish content after translation update
|
|
353
|
+
await cacheManager.invalidate({
|
|
354
|
+
tags: ['content:prefix:es']
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Best Practices
|
|
359
|
+
|
|
360
|
+
1. **Use consistent prefix conventions**: Establish a clear URL structure (`/blog/*`, `/shop/*`, `/docs/*`) and stick to it.
|
|
361
|
+
|
|
362
|
+
2. **Invalidate by prefix when content changes**: When a CMS webhook fires for a content update, extract the prefix from the slug and invalidate that prefix tag.
|
|
363
|
+
|
|
364
|
+
3. **Don't over-invalidate**: Use the most specific tag that covers the affected content. Prefer `content:prefix:blog` over `content:site:my-site` when only blog content changed.
|
|
365
|
+
|
|
366
|
+
4. **Combine with item-specific tags**: For single-item updates, invalidate both the specific item and the prefix to ensure both individual fetches and list queries are refreshed.
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// Recommended pattern for content updates
|
|
370
|
+
async function onContentUpdated(siteName: string, slug: string) {
|
|
371
|
+
const prefix = slug.includes('/') ? slug.split('/')[0] : null;
|
|
372
|
+
|
|
373
|
+
const tags = [`content:slug:${siteName}:${slug}`];
|
|
374
|
+
if (prefix) {
|
|
375
|
+
tags.push(`content:prefix:${prefix}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await cacheManager.invalidate({ tags });
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
5. **Consider prefix hierarchies**: If you have nested prefixes like `docs/api/v1/*`, the SDK tags with the first segment (`docs`). For more granular control, use custom tags via cache policies.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Granular Caching and Invalidation
|
|
387
|
+
|
|
388
|
+
**Always prefer the most specific invalidation possible.** Over-invalidating defeats the purpose of caching and can cause unnecessary load on your API. Under-invalidating leads to stale data.
|
|
389
|
+
|
|
390
|
+
### The Granularity Hierarchy
|
|
391
|
+
|
|
392
|
+
From most specific (preferred) to least specific (avoid when possible):
|
|
393
|
+
|
|
394
|
+
| Level | Tag Pattern | Use When |
|
|
395
|
+
|-------|-------------|----------|
|
|
396
|
+
| 1. Item | `content:slug:my-site:blog/post-1` | A single item changed |
|
|
397
|
+
| 2. Prefix | `content:prefix:blog` | Multiple items in a section changed |
|
|
398
|
+
| 3. Site | `content:site:my-site` | Site-wide changes (settings, theme) |
|
|
399
|
+
| 4. Global | `content` | Schema changes, migrations |
|
|
400
|
+
|
|
401
|
+
### ❌ Anti-Pattern: Over-Invalidation
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// BAD: Invalidates ALL content when only one post changed
|
|
405
|
+
async function onBlogPostUpdated(siteName: string, slug: string) {
|
|
406
|
+
await cacheManager.invalidate({
|
|
407
|
+
tags: ['content:site:my-site'] // Nukes everything!
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
This invalidates:
|
|
413
|
+
- All blog posts (intended)
|
|
414
|
+
- All shop pages (unnecessary)
|
|
415
|
+
- All static pages (unnecessary)
|
|
416
|
+
- All list queries (unnecessary)
|
|
417
|
+
|
|
418
|
+
**Result**: Poor cache hit rate, increased API load, slower page loads.
|
|
419
|
+
|
|
420
|
+
### ✅ Correct Pattern: Targeted Invalidation
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// GOOD: Invalidates only what's affected
|
|
424
|
+
async function onBlogPostUpdated(siteName: string, slug: string) {
|
|
425
|
+
const prefix = slug.includes('/') ? slug.split('/')[0] : null;
|
|
426
|
+
|
|
427
|
+
const tags = [`content:slug:${siteName}:${slug}`];
|
|
428
|
+
if (prefix) {
|
|
429
|
+
tags.push(`content:prefix:${prefix}`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await cacheManager.invalidate({ tags });
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
This invalidates:
|
|
437
|
+
- The specific post (`content:slug:my-site:blog/post-1`)
|
|
438
|
+
- Blog list queries (`content:prefix:blog`)
|
|
439
|
+
|
|
440
|
+
**Result**: Shop pages, static pages, and other sections remain cached.
|
|
441
|
+
|
|
442
|
+
### Real-World Invalidation Scenarios
|
|
443
|
+
|
|
444
|
+
#### Scenario 1: Single Blog Post Edit
|
|
445
|
+
|
|
446
|
+
A user edits the title of `blog/my-post`.
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// Invalidate the post and any lists that might show it
|
|
450
|
+
await cacheManager.invalidate({
|
|
451
|
+
tags: [
|
|
452
|
+
'content:slug:my-site:blog/my-post',
|
|
453
|
+
'content:prefix:blog'
|
|
454
|
+
]
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
#### Scenario 2: New Product Added to Category
|
|
459
|
+
|
|
460
|
+
A new product is added to the "electronics" category.
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
// Invalidate category listings, not individual product pages
|
|
464
|
+
await cacheManager.invalidate({
|
|
465
|
+
tags: [
|
|
466
|
+
'products:category:my-site:electronics',
|
|
467
|
+
'products:prefix:shop' // If products use shop/* slugs
|
|
468
|
+
]
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### Scenario 3: Product Price Update
|
|
473
|
+
|
|
474
|
+
Only the price of a specific product changed.
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// Invalidate just that product - lists showing price need refresh too
|
|
478
|
+
await cacheManager.invalidate({
|
|
479
|
+
tags: [
|
|
480
|
+
'products:slug:my-site:shop/widget-pro',
|
|
481
|
+
'products:id:prod_12345'
|
|
482
|
+
]
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### Scenario 4: Bulk Import of Blog Posts
|
|
487
|
+
|
|
488
|
+
50 new blog posts were imported.
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
// Prefix invalidation is appropriate here
|
|
492
|
+
await cacheManager.invalidate({
|
|
493
|
+
tags: ['content:prefix:blog']
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### Scenario 5: Site Settings Changed (Logo, Footer)
|
|
498
|
+
|
|
499
|
+
Global site settings affect all pages.
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
// Site-wide invalidation is justified
|
|
503
|
+
await cacheManager.invalidate({
|
|
504
|
+
tags: ['content:site:my-site', 'products:site:my-site']
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### List Invalidation by Slug Prefix
|
|
509
|
+
|
|
510
|
+
When content is fetched with a `slug_prefix` parameter, the SDK automatically tags that cached response with `content:prefix:{prefix}`. This is the key mechanism for invalidating lists when new items are created.
|
|
511
|
+
|
|
512
|
+
**How it works:**
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
// This list query gets tagged with: content:prefix:blog
|
|
516
|
+
const blogPosts = await client.content.getContent('my-site', {
|
|
517
|
+
slug_prefix: 'blog',
|
|
518
|
+
page_status: 'publish',
|
|
519
|
+
limit: 10
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
When a new post `blog/new-article` is created, invalidating `content:prefix:blog` will:
|
|
524
|
+
- Clear the cached list above (so the next fetch includes the new post)
|
|
525
|
+
- Clear any other cached lists filtered by `slug_prefix: 'blog'`
|
|
526
|
+
- Clear individual post caches under the `blog/` prefix
|
|
527
|
+
|
|
528
|
+
**Example: New content created with a slug prefix**
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// A new blog post is created: blog/exciting-news
|
|
532
|
+
async function onContentCreated(siteName: string, slug: string) {
|
|
533
|
+
const prefix = slug.includes('/') ? slug.split('/')[0] : null;
|
|
534
|
+
|
|
535
|
+
if (prefix) {
|
|
536
|
+
// Invalidate ALL cached lists that used this prefix
|
|
537
|
+
// This ensures the new post appears in list queries
|
|
538
|
+
await cacheManager.invalidate({
|
|
539
|
+
tags: [`content:prefix:${prefix}`]
|
|
540
|
+
});
|
|
541
|
+
} else {
|
|
542
|
+
// Root-level content (no prefix) - must invalidate site-wide lists
|
|
543
|
+
await cacheManager.invalidate({
|
|
544
|
+
tags: [`content:site:${siteName}`]
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// When blog/exciting-news is created:
|
|
550
|
+
// - Invalidates: content:prefix:blog
|
|
551
|
+
// - Clears: all cached getContent() calls with slug_prefix: 'blog'
|
|
552
|
+
// - Preserves: shop/* caches, docs/* caches, individual non-blog pages
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Why this matters:**
|
|
556
|
+
|
|
557
|
+
Without prefix-based list invalidation, you'd face a dilemma:
|
|
558
|
+
1. **Over-invalidate**: Clear all site content caches (wasteful)
|
|
559
|
+
2. **Under-invalidate**: Only clear the new item's cache (lists show stale data, missing the new post)
|
|
560
|
+
|
|
561
|
+
The prefix tag gives you the middle ground: invalidate only the lists that could contain the new item.
|
|
562
|
+
|
|
563
|
+
### Using Custom Tags for Fine-Grained Control
|
|
564
|
+
|
|
565
|
+
When built-in tags aren't granular enough, add custom tags:
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// Tag featured content separately
|
|
569
|
+
const featured = await client.content.getContent('my-site',
|
|
570
|
+
{ page_status: 'publish', limit: 5 },
|
|
571
|
+
{ tags: ['homepage:featured'] }
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
// Tag sidebar content
|
|
575
|
+
const sidebar = await client.content.getContent('my-site',
|
|
576
|
+
{ slug_prefix: 'widgets' },
|
|
577
|
+
{ tags: ['layout:sidebar'] }
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
// Invalidate only homepage featured section
|
|
581
|
+
await cacheManager.invalidate({
|
|
582
|
+
tags: ['homepage:featured']
|
|
583
|
+
});
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Webhook Integration Example
|
|
587
|
+
|
|
588
|
+
Here's a complete example for handling CMS webhooks with granular invalidation:
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
import { CacheManager } from 'perspectapi-ts-sdk';
|
|
592
|
+
|
|
593
|
+
interface WebhookPayload {
|
|
594
|
+
event: 'content.created' | 'content.updated' | 'content.deleted' | 'content.bulk_update';
|
|
595
|
+
site: string;
|
|
596
|
+
slug?: string;
|
|
597
|
+
id?: string;
|
|
598
|
+
prefix?: string; // For bulk operations
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function handleWebhook(payload: WebhookPayload, cacheManager: CacheManager) {
|
|
602
|
+
const { event, site, slug, id, prefix } = payload;
|
|
603
|
+
|
|
604
|
+
switch (event) {
|
|
605
|
+
case 'content.created':
|
|
606
|
+
case 'content.deleted':
|
|
607
|
+
// New/deleted items affect list queries
|
|
608
|
+
if (slug) {
|
|
609
|
+
const slugPrefix = slug.includes('/') ? slug.split('/')[0] : null;
|
|
610
|
+
await cacheManager.invalidate({
|
|
611
|
+
tags: slugPrefix
|
|
612
|
+
? [`content:prefix:${slugPrefix}`]
|
|
613
|
+
: [`content:site:${site}`] // Fallback for root-level content
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
break;
|
|
617
|
+
|
|
618
|
+
case 'content.updated':
|
|
619
|
+
// Updated items: invalidate specific item + relevant lists
|
|
620
|
+
const tags: string[] = [];
|
|
621
|
+
if (slug) {
|
|
622
|
+
tags.push(`content:slug:${site}:${slug}`);
|
|
623
|
+
const slugPrefix = slug.includes('/') ? slug.split('/')[0] : null;
|
|
624
|
+
if (slugPrefix) {
|
|
625
|
+
tags.push(`content:prefix:${slugPrefix}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (id) {
|
|
629
|
+
tags.push(`content:id:${id}`);
|
|
630
|
+
}
|
|
631
|
+
if (tags.length > 0) {
|
|
632
|
+
await cacheManager.invalidate({ tags });
|
|
633
|
+
}
|
|
634
|
+
break;
|
|
635
|
+
|
|
636
|
+
case 'content.bulk_update':
|
|
637
|
+
// Bulk operations: use prefix if provided, otherwise site-wide
|
|
638
|
+
await cacheManager.invalidate({
|
|
639
|
+
tags: prefix
|
|
640
|
+
? [`content:prefix:${prefix}`]
|
|
641
|
+
: [`content:site:${site}`]
|
|
642
|
+
});
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Monitoring Cache Effectiveness
|
|
649
|
+
|
|
650
|
+
Track these metrics to ensure your invalidation strategy is working:
|
|
651
|
+
|
|
652
|
+
1. **Cache hit rate**: Should be >80% for read-heavy sites
|
|
653
|
+
2. **Invalidation frequency**: Spikes may indicate over-invalidation
|
|
654
|
+
3. **Stale content reports**: May indicate under-invalidation
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
// Example: Log invalidation events for monitoring
|
|
658
|
+
async function invalidateWithLogging(
|
|
659
|
+
cacheManager: CacheManager,
|
|
660
|
+
tags: string[]
|
|
661
|
+
) {
|
|
662
|
+
console.log(`[Cache] Invalidating tags: ${tags.join(', ')}`);
|
|
663
|
+
const start = Date.now();
|
|
664
|
+
await cacheManager.invalidate({ tags });
|
|
665
|
+
console.log(`[Cache] Invalidation completed in ${Date.now() - start}ms`);
|
|
666
|
+
}
|
|
667
|
+
```
|