Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e59ffb5

Browse filesBrowse 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
Copy full SHA for e59ffb5

File tree

11 files changed

+285
-274
lines changed
Filter options

11 files changed

+285
-274
lines changed

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

Copy file name to clipboardExpand all lines: apps/docs/docs/isr/cache-hooks.md
+17-7Lines 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

Copy file name to clipboardExpand all lines: apps/ssr-isr/server.ts
+22-13Lines 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

Copy file name to clipboardExpand all lines: apps/ssr-isr/src/app/dynamic.component.ts
+14-7Lines 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

Copy file name to clipboardExpand all lines: libs/isr/models/src/index.ts
+1Lines 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

Copy file name to clipboardExpand all lines: libs/isr/models/src/isr-handler-config.ts
+15-6Lines 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
/**
+122Lines 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

Copy file name to clipboardExpand all lines: libs/isr/server/src/cache-handlers/in-memory-cache-handler.ts
+1-14Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.