Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d948e90

Browse files
authored
Merge branch 'main' into fix/docs
2 parents 0d11837 + 084f4fa commit d948e90

File tree

6 files changed

+226
-14
lines changed

6 files changed

+226
-14
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,7 @@ We welcome contributions from the community to help improve RxAngular! To get st
133133
## License
134134

135135
This project is MIT licensed.
136+
137+
---
138+
139+
made with ❤ by [push-based.io](https://www.push-based.io)

apps/docs/docs/isr/cache-variants.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
---
2+
sidebar_label: Cache Variants
3+
sidebar_position: 11
4+
title: Cache Variants
5+
---
6+
7+
## Why do we need it?
8+
9+
In some apps, pages are displayed in different variants. This can be the case, for example, if the application has a login. Users who are already logged in may not see a login button or something similar, whereas an authenticated user will.
10+
11+
These scenarios would lead to a content shift after delivery of the cached variant that does not correspond to the user status.
12+
13+
## How to use it?
14+
15+
For these cases the configuration can be extended by any number of render variants by the `variants` property.
16+
17+
```typescript title="server.ts"
18+
const isr = new ISRHandler({
19+
...
20+
// highlight-next-line
21+
variants: [] // 👈 register your variants here
22+
});
23+
```
24+
25+
The `RenderVariant` interface is defined as follows:
26+
27+
```typescript
28+
export interface RenderVariant {
29+
identifier: string;
30+
detectVariant: (req: Request) => boolean;
31+
simulateVariant?: (req: Request) => Request;
32+
}
33+
```
34+
35+
### identifier
36+
37+
The `identifier` must be unique and is used next to the URL as a key for the cache.
38+
39+
### detectVariant
40+
41+
The `detectVariant` callback detects from the current request whether a defined variant must be delivered. This can happen e.g. on the basis of a cookie or similar. The return value is a boolean which determines whether this request fits the variant or not.
42+
43+
### simulateVariant
44+
45+
If on-demand revalidation is also to be used for the different variants, a way must be provided to modify the request so that it can be recognized again by the `detectVariant` callback.
46+
47+
For example, a placeholder for an authentication cookie can be added so that an authenticated variant of the app is rendered.
48+
For this we use the `simulateVariant` callback, which is called before rendering to modify the request.
49+
50+
### Example
51+
52+
```typescript title="server.ts"
53+
const isr = new ISRHandler({
54+
...
55+
variants: [
56+
{
57+
identifier: 'logged-in', // 👈 key to cache the variant
58+
detectVariant: (req) => {
59+
// 👇 Variant is recognized as soon as the request contains a cookie 'access_token'
60+
return req.cookies && req.cookies.access_token;
61+
},
62+
simulateVariant: (req) => {
63+
// 👇 For on-demand revalidation we insert a placeholder 'access_token' cookie
64+
req.cookies['access_token'] = 'isr';
65+
return req;
66+
}
67+
},
68+
]
69+
});
70+
```
71+
72+
## Gotchas
73+
74+
If more than one variant is registered, the one where the `detectVariant` returns `true` the first time is used.
75+
76+
If a variant is to become a combination of different variants, this state should be registered as an independent variant before the corresponding single variant.
77+
78+
```typescript title="server.ts"
79+
const isr = new ISRHandler({
80+
...
81+
variants: [
82+
{
83+
identifier: 'A_AND_B',
84+
detectVariant: (req) => req.cookies && req.cookies.is_A && req.cookies.is_B,
85+
simulateVariant: (req) => {
86+
req.cookies['is_A'] = true;
87+
req.cookies['is_B'] = true;
88+
return req;
89+
}
90+
},
91+
{
92+
identifier: 'A',
93+
detectVariant: (req) => req.cookies && req.cookies.is_A,
94+
simulateVariant: (req) => {
95+
req.cookies['is_A'] = true;
96+
return req;
97+
}
98+
},
99+
{
100+
identifier: 'B',
101+
detectVariant: (req) => req.cookies && req.cookies.is_B,
102+
simulateVariant: (req) => {
103+
req.cookies['is_B'] = true;
104+
return req;
105+
}
106+
},
107+
]
108+
});
109+
```
110+
111+
:::caution **Important:**
112+
For each variant entered here, a corresponding duplicate is cached per page. So be aware that depending on the cache handler this could significantly increase the disk or RAM usage.
113+
:::
114+
115+
Note that when using placeholder cookies or similar, the application should have appropriate exceptions defined to avoid runtime errors caused by invalid values.

libs/isr/models/src/cache-handler.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Request } from 'express';
2+
13
/**
24
* CacheISRConfig is the configuration object that is used to store the
35
* cache data in the cache handler.
@@ -14,6 +16,18 @@ export interface CacheData {
1416
createdAt: number;
1517
}
1618

19+
export interface RenderVariant {
20+
identifier: string;
21+
detectVariant: (req: Request) => boolean;
22+
simulateVariant?: (req: Request) => Request;
23+
}
24+
25+
export interface VariantRebuildItem {
26+
url: string;
27+
cacheKey: string;
28+
reqSimulator: (req: Request) => Request;
29+
}
30+
1731
export abstract class CacheHandler {
1832
abstract add(
1933
url: string,

libs/isr/models/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export {
33
CacheISRConfig,
44
CacheISRConfig as ISROptions,
55
CacheData,
6+
RenderVariant,
7+
VariantRebuildItem,
68
} from './cache-handler';
79

810
export {

libs/isr/models/src/isr-handler-config.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Provider } from '@angular/core';
2-
import { CacheHandler } from './cache-handler';
2+
import { CacheHandler, RenderVariant } from './cache-handler';
33
import { Request } from 'express';
44

55
export interface ISRHandlerConfig {
@@ -42,6 +42,30 @@ export interface ISRHandlerConfig {
4242
* If not provided, defaults to true.
4343
*/
4444
skipCachingOnHttpError?: boolean;
45+
46+
/**
47+
* An optional way to define multiple variants of a page.
48+
* This can be useful if the appearance page differs, for example,
49+
* based on a cookie and the cached variant would thus lead to a content shift.
50+
* Each variant needs an identifier and a callback function
51+
* to identify the variant. It is also possible to modify the request
52+
* to recreate the variant in case of on-demand cache invalidation.
53+
*
54+
* @example
55+
* variants: [
56+
* {
57+
* identifier: 'logged-in',
58+
* detectVariant: (req) => {
59+
* return req.cookies && req.cookies.access_token;
60+
* },
61+
* simulateVariant: (req) => {
62+
* req.cookies['access_token'] = 'isr';
63+
* return req;
64+
* },
65+
* },
66+
* ],
67+
*/
68+
variants?: RenderVariant[];
4569
}
4670

4771
export interface ServeFromCacheConfig {

libs/isr/server/src/isr-handler.ts

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@rx-angular/isr/models';
1313
import { getRouteISRDataFromHTML } from './utils/get-isr-options';
1414
import { renderUrl, RenderUrlConfig } from './utils/render-url';
15+
import { RenderVariant, VariantRebuildItem } from '@rx-angular/isr/models';
1516

1617
export class ISRHandler {
1718
protected cache!: CacheHandler;
@@ -70,19 +71,26 @@ export class ISRHandler {
7071
const notInCache: string[] = [];
7172
const urlWithErrors: Record<string, any> = {};
7273

73-
for (const url of urlsToInvalidate) {
74+
// Include all possible variants in the list of URLs to be invalidated including
75+
// their modified request to regenerate the pages
76+
const variantUrlsToInvalidate =
77+
this.getVariantUrlsToInvalidate(urlsToInvalidate);
78+
79+
for (const variantUrl of variantUrlsToInvalidate) {
80+
const { cacheKey, url, reqSimulator } = variantUrl;
81+
7482
// check if the url is in cache
75-
const urlExists = await this.cache.has(url);
83+
const urlExists = await this.cache.has(cacheKey);
7684

7785
if (!urlExists) {
78-
notInCache.push(url);
86+
notInCache.push(cacheKey);
7987
continue;
8088
}
8189

8290
try {
8391
// re-render the page again
8492
const html = await renderUrl({
85-
req,
93+
req: reqSimulator(req),
8694
res,
8795
url,
8896
indexHtml,
@@ -94,23 +102,25 @@ export class ISRHandler {
94102

95103
// if there are errors when rendering the site we throw an error
96104
if (errors?.length && this.config.skipCachingOnHttpError) {
97-
urlWithErrors[url] = errors;
105+
urlWithErrors[cacheKey] = errors;
98106
}
99107

100108
// add the regenerated page to cache
101109
const cacheConfig: CacheISRConfig = {
102110
revalidate,
103111
buildId: this.config.buildId,
104112
};
105-
await this.cache.add(req.url, html, cacheConfig);
113+
await this.cache.add(cacheKey, html, cacheConfig);
106114
} catch (err) {
107-
urlWithErrors[url] = err;
115+
urlWithErrors[cacheKey] = err;
108116
}
109117
}
110118

111-
const invalidatedUrls = urlsToInvalidate.filter(
112-
(url) => !notInCache.includes(url) && !urlWithErrors[url]
113-
);
119+
const invalidatedUrls = variantUrlsToInvalidate
120+
.map((val) => val.cacheKey)
121+
.filter(
122+
(cacheKey) => !notInCache.includes(cacheKey) && !urlWithErrors[cacheKey]
123+
);
114124

115125
if (notInCache.length) {
116126
this.logger.log(
@@ -139,14 +149,38 @@ export class ISRHandler {
139149
return res.json(response);
140150
}
141151

152+
getVariantUrlsToInvalidate(urlsToInvalidate: string[]): VariantRebuildItem[] {
153+
const variants = this.config.variants || [];
154+
const result: VariantRebuildItem[] = [];
155+
156+
const defaultVariant = (req: Request) => req;
157+
158+
for (const url of urlsToInvalidate) {
159+
result.push({ url, cacheKey: url, reqSimulator: defaultVariant });
160+
for (const variant of variants) {
161+
result.push({
162+
url,
163+
cacheKey: getCacheKey(url, variant),
164+
reqSimulator: variant.simulateVariant
165+
? variant.simulateVariant
166+
: defaultVariant,
167+
});
168+
}
169+
}
170+
171+
return result;
172+
}
173+
142174
async serveFromCache(
143175
req: Request,
144176
res: Response,
145177
next: NextFunction,
146178
config?: ServeFromCacheConfig
147179
): Promise<any> {
148180
try {
149-
const cacheData = await this.cache.get(req.url);
181+
const variant = this.getVariant(req);
182+
183+
const cacheData = await this.cache.get(getCacheKey(req.url, variant));
150184
const { html, options: cacheConfig, createdAt } = cacheData;
151185

152186
const cacheHasBuildId =
@@ -184,7 +218,10 @@ export class ISRHandler {
184218
}
185219

186220
// Cache exists. Send it.
187-
this.logger.log(`Page was retrieved from cache: `, req.url);
221+
this.logger.log(
222+
`Page was retrieved from cache: `,
223+
getCacheKey(req.url, variant)
224+
);
188225
return res.send(finalHtml);
189226
} catch (error) {
190227
// Cache does not exist. Serve user using SSR
@@ -230,14 +267,25 @@ export class ISRHandler {
230267
return res.send(finalHtml);
231268
}
232269

270+
const variant = this.getVariant(req);
271+
233272
// Cache the rendered `html` for this request url to use for subsequent requests
234-
await this.cache.add(req.url, finalHtml, {
273+
await this.cache.add(getCacheKey(req.url, variant), finalHtml, {
235274
revalidate,
236275
buildId: this.config.buildId,
237276
});
238277
return res.send(finalHtml);
239278
});
240279
}
280+
281+
protected getVariant(req: Request): RenderVariant | null {
282+
if (!this.config.variants) {
283+
return null;
284+
}
285+
return (
286+
this.config.variants.find((variant) => variant.detectVariant(req)) || null
287+
);
288+
}
241289
}
242290

243291
const extractDataFromBody = (
@@ -246,3 +294,8 @@ const extractDataFromBody = (
246294
const { urlsToInvalidate, token } = req.body;
247295
return { urlsToInvalidate, token };
248296
};
297+
298+
const getCacheKey = (url: string, variant: RenderVariant | null): string => {
299+
if (!variant) return url;
300+
return `${url}<variantId:${variant.identifier}>`;
301+
};

0 commit comments

Comments
 (0)