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

Skip to content

Commit e59ffb5

Browse files
authored
fix(isr): use modifyGeneratedHtml in all cache generation process (#1760)
* refactor(isr): rename CacheRegeneration to CacheGeneration * refactor(isr): rename CacheRegeneration to CacheGeneration * fix(isr): handle modifyGeneratedHtml behavior consistantly #1758 * refactor(isr): use modifyGeneratedHtml instead * feat(isr): update the example to show modifyGeneratedHtml usage
1 parent 5af9ab2 commit e59ffb5

File tree

11 files changed

+285
-274
lines changed

11 files changed

+285
-274
lines changed

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,23 @@ server.get(
3232
return `${cachedHtml}<!-- Hello, I'm a modification to the original cache! -->`;
3333
},
3434
}),
35-
// Server side render the page and add to cache if needed
36-
async (req, res, next) =>
37-
await isr.render(req, res, next, {
38-
modifyGeneratedHtml: (req, html) => {
39-
return `${html}<!-- Hello, I'm modifying the generatedHtml before caching it! -->`;
40-
},
41-
})
35+
36+
const isr = new ISRHandler({
37+
indexHtml,
38+
invalidateSecretToken: 'MY_TOKEN', // replace with env secret key ex. process.env.REVALIDATE_SECRET_TOKEN
39+
enableLogging: true,
40+
serverDistFolder,
41+
browserDistFolder,
42+
bootstrap,
43+
commonEngine,
44+
modifyGeneratedHtml: (req, html) => {
45+
return `${html}<!-- Hello, I'm modifying the generatedHtml before caching it! -->`;
46+
},
47+
// cache: fsCacheHandler,
48+
});
49+
50+
// Server side render the page and add to cache if needed
51+
async (req, res, next) => await isr.render(req, res, next),
4252
);
4353
```
4454

apps/ssr-isr/server.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CommonEngine } from '@angular/ssr';
2+
import { ModifyHtmlCallbackFn } from '@rx-angular/isr/models';
23
import { ISRHandler } from '@rx-angular/isr/server';
3-
import express from 'express';
4+
import express, { Request } from 'express';
45
import { dirname, join, resolve } from 'node:path';
56
import { fileURLToPath } from 'node:url';
67
import { RESPONSE } from './src/app/redirect.component';
@@ -30,6 +31,7 @@ export function app(): express.Express {
3031
browserDistFolder,
3132
bootstrap,
3233
commonEngine,
34+
modifyGeneratedHtml: defaultModifyGeneratedHtml,
3335
// cache: fsCacheHandler,
3436
});
3537

@@ -46,32 +48,39 @@ export function app(): express.Express {
4648
// Example Express Rest API endpoints
4749
// server.get('/api/**', (req, res) => { });
4850
// Serve static files from /browser
49-
server.get(
50-
'*.*',
51-
express.static(browserDistFolder, {
52-
maxAge: '1y',
53-
}),
54-
);
51+
server.get('*.*', express.static(browserDistFolder, { maxAge: '1y' }));
5552

5653
server.get(
5754
'*',
5855
// Serve page if it exists in cache
5956
async (req, res, next) => await isr.serveFromCache(req, res, next),
57+
6058
// Server side render the page and add to cache if needed
6159
async (req, res, next) =>
6260
await isr.render(req, res, next, {
63-
providers: [
64-
{
65-
provide: RESPONSE,
66-
useValue: res,
67-
},
68-
],
61+
providers: [{ provide: RESPONSE, useValue: res }],
6962
}),
7063
);
7164

7265
return server;
7366
}
7467

68+
const defaultModifyGeneratedHtml: ModifyHtmlCallbackFn = (
69+
req: Request,
70+
html: string,
71+
revalidateTime?: number | null,
72+
): string => {
73+
const time = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
74+
75+
let msg = '<!-- ';
76+
msg += `\n🚀 ISR: Served from cache! \n⌛ Last updated: ${time}. `;
77+
if (revalidateTime)
78+
msg += `\n⏭️ Next refresh is after ${revalidateTime} seconds. `;
79+
msg += ' \n-->';
80+
html = html.replace('Original content', 'Modified content');
81+
return html + msg;
82+
};
83+
7584
function run(): void {
7685
const port = process.env['PORT'] || 4000;
7786

apps/ssr-isr/src/app/dynamic.component.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@ import { map, switchMap } from 'rxjs';
88
selector: 'app-dynamic-page',
99
template: `
1010
@if (post$ | async; as post) {
11+
<div>
12+
<h2>{{ post.title }}</h2>
13+
<p>{{ post.body }}</p>
14+
</div>
15+
}
1116
<div>
12-
<h2>{{ post.title }}</h2>
13-
<p>{{ post.body }}</p>
17+
<h2>
18+
Dynamically Modification (controlled by modifyGeneratedHtml in
19+
ISRHandlerConfig)
20+
</h2>
21+
<p>Original content</p>
1422
</div>
15-
}
1623
`,
1724
imports: [AsyncPipe],
1825
standalone: true,
@@ -22,14 +29,14 @@ export class DynamicPageComponent {
2229
private http = inject(HttpClient);
2330

2431
private postId$ = inject(ActivatedRoute).params.pipe(
25-
map((p) => p['id'] as string)
32+
map((p) => p['id'] as string),
2633
);
2734

2835
post$ = this.postId$.pipe(
2936
switchMap((id) =>
3037
this.http.get<{ title: string; body: string }>(
31-
`https://jsonplaceholder.typicode.com/posts/${id}`
32-
)
33-
)
38+
`https://jsonplaceholder.typicode.com/posts/${id}`,
39+
),
40+
),
3441
);
3542
}

libs/isr/models/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export {
99
export {
1010
InvalidateConfig,
1111
ISRHandlerConfig,
12+
ModifyHtmlCallbackFn,
1213
RenderConfig,
1314
RouteISRConfig,
1415
ServeFromCacheConfig,

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ export interface ISRHandlerConfig {
104104
* If provided as an empty array, no query params will be part of the cache key.
105105
*/
106106
allowedQueryParams?: string[];
107+
108+
/**
109+
* This callback lets you hook into the generated html and provide any modifications
110+
* necessary on-the-fly.
111+
* Use with caution as this may lead to a performance loss on serving the html.
112+
* If null, it will use `defaultModifyGeneratedHtml` function,
113+
* which only add commented text to the html to indicate when it was generated.
114+
*/
115+
modifyGeneratedHtml?: ModifyHtmlCallbackFn;
107116
}
108117

109118
export interface ServeFromCacheConfig {
@@ -124,14 +133,14 @@ export interface InvalidateConfig {
124133
providers?: Provider[];
125134
}
126135

136+
export type ModifyHtmlCallbackFn = (
137+
req: Request,
138+
html: string,
139+
revalidateTime?: number | null,
140+
) => string;
141+
127142
export interface RenderConfig {
128143
providers?: Provider[];
129-
/**
130-
* This callback lets you hook into the generated html and provide any modifications
131-
* necessary on-the-fly.
132-
* Use with caution as this may lead to a performance loss on serving the html.
133-
*/
134-
modifyGeneratedHtml?: (req: Request, html: string) => string;
135144
}
136145

137146
/**
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Provider } from '@angular/core';
2+
import { CacheHandler, ISRHandlerConfig } from '@rx-angular/isr/models';
3+
import { Request, Response } from 'express';
4+
import { ISRLogger } from './isr-logger';
5+
import { defaultModifyGeneratedHtml } from './modify-generated-html';
6+
import { getCacheKey, getVariant } from './utils/cache-utils';
7+
import { getRouteISRDataFromHTML } from './utils/get-isr-options';
8+
import { renderUrl, RenderUrlConfig } from './utils/render-url';
9+
10+
export interface IGeneratedResult {
11+
html?: string;
12+
errors?: string[];
13+
}
14+
15+
export class CacheGeneration {
16+
// TODO: make this pluggable because on serverless environments we can't share memory between functions
17+
// so we need to use a database or redis cache to store the urls that are on hold if we want to use this feature
18+
private urlsOnHold: string[] = []; // urls that have regeneration loading
19+
20+
constructor(
21+
public isrConfig: ISRHandlerConfig,
22+
public cache: CacheHandler,
23+
public logger: ISRLogger,
24+
) {}
25+
async generate(
26+
req: Request,
27+
res: Response,
28+
providers?: Provider[],
29+
mode: 'regenerate' | 'generate' = 'regenerate',
30+
): Promise<IGeneratedResult | void> {
31+
const { url } = req;
32+
const variant = getVariant(req, this.isrConfig.variants);
33+
const cacheKey = getCacheKey(
34+
url,
35+
this.isrConfig.allowedQueryParams,
36+
variant,
37+
);
38+
39+
return this.generateWithCacheKey(req, res, cacheKey, providers, mode);
40+
}
41+
async generateWithCacheKey(
42+
req: Request,
43+
res: Response,
44+
cacheKey: string,
45+
providers?: Provider[],
46+
mode: 'regenerate' | 'generate' = 'regenerate',
47+
): Promise<IGeneratedResult | void> {
48+
const { url } = req;
49+
50+
if (mode === 'regenerate') {
51+
// only regenerate will use queue to avoid multiple regenerations for the same url
52+
// generate mode is used for the request without cache
53+
if (this.urlsOnHold.includes(cacheKey)) {
54+
this.logger.log('Another generation is on-going for this url...');
55+
return;
56+
}
57+
this.logger.log(`The url: ${cacheKey} is being generated.`);
58+
59+
this.urlsOnHold.push(cacheKey);
60+
}
61+
const renderUrlConfig: RenderUrlConfig = {
62+
req,
63+
res,
64+
url,
65+
indexHtml: this.isrConfig.indexHtml,
66+
providers,
67+
commonEngine: this.isrConfig.commonEngine,
68+
bootstrap: this.isrConfig.bootstrap,
69+
browserDistFolder: this.isrConfig.browserDistFolder,
70+
inlineCriticalCss: this.isrConfig.inlineCriticalCss,
71+
};
72+
try {
73+
const html = await renderUrl(renderUrlConfig);
74+
const { revalidate, errors } = getRouteISRDataFromHTML(html);
75+
76+
// Apply the modify generation callback
77+
// If undefined, use the default modifyGeneratedHtml function
78+
const finalHtml = this.isrConfig.modifyGeneratedHtml
79+
? this.isrConfig.modifyGeneratedHtml(req, html, revalidate)
80+
: defaultModifyGeneratedHtml(req, html, revalidate);
81+
82+
// if there are errors, don't add the page to cache
83+
if (errors?.length && this.isrConfig.skipCachingOnHttpError) {
84+
// remove url from urlsOnHold because we want to try to regenerate it again
85+
if (mode === 'regenerate') {
86+
this.urlsOnHold = this.urlsOnHold.filter((x) => x !== cacheKey);
87+
}
88+
this.logger.log(
89+
`💥 ERROR: Url: ${cacheKey} was not regenerated!`,
90+
errors,
91+
);
92+
return { html: finalHtml, errors };
93+
}
94+
95+
// if revalidate is null we won't cache it
96+
// if revalidate is 0, we will never clear the cache automatically
97+
// if revalidate is x, we will clear cache every x seconds (after the last request) for that url
98+
if (revalidate === null || revalidate === undefined) {
99+
// don't do !revalidate because it will also catch "0"
100+
return { html: finalHtml };
101+
}
102+
// add the regenerated page to cache
103+
await this.cache.add(cacheKey, finalHtml, {
104+
revalidate,
105+
buildId: this.isrConfig.buildId,
106+
});
107+
if (mode === 'regenerate') {
108+
// remove from urlsOnHold because we are done
109+
this.urlsOnHold = this.urlsOnHold.filter((x) => x !== cacheKey);
110+
this.logger.log(`Url: ${cacheKey} was regenerated!`);
111+
}
112+
return { html: finalHtml };
113+
} catch (error) {
114+
this.logger.log(`Error regenerating url: ${cacheKey}`, error);
115+
if (mode === 'regenerate') {
116+
// Ensure removal from urlsOnHold in case of error
117+
this.urlsOnHold = this.urlsOnHold.filter((x) => x !== cacheKey);
118+
}
119+
throw error;
120+
}
121+
}
122+
}

libs/isr/server/src/cache-handlers/in-memory-cache-handler.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@ export class InMemoryCacheHandler extends CacheHandler {
2020
html: string,
2121
config: CacheISRConfig = defaultCacheISRConfig,
2222
): Promise<void> {
23-
const htmlWithMsg = html + cacheMsg(config.revalidate);
24-
2523
return new Promise((resolve) => {
2624
const cacheData: CacheData = {
27-
html: htmlWithMsg,
25+
html,
2826
options: config,
2927
createdAt: Date.now(),
3028
};
@@ -67,14 +65,3 @@ export class InMemoryCacheHandler extends CacheHandler {
6765
});
6866
}
6967
}
70-
71-
const cacheMsg = (revalidateTime?: number | null): string => {
72-
const time = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
73-
74-
let msg = '<!-- ';
75-
msg += `\n🚀 ISR: Served from cache! \n⌛ Last updated: ${time}. `;
76-
if (revalidateTime)
77-
msg += `\n⏭️ Next refresh is after ${revalidateTime} seconds. `;
78-
msg += ' \n-->';
79-
return msg;
80-
};

0 commit comments

Comments
 (0)