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

Skip to content

Commit ccc08e3

Browse files
authored
Merge branch 'main' into refactor-rename-templatecache
2 parents 7e975fe + ba676b0 commit ccc08e3

33 files changed

+1143
-2444
lines changed

.codecov.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ coverage:
2222
target: 90%
2323
flags:
2424
- eslint-plugin
25+
isr:
26+
target: 90%
27+
flags:
28+
- isr
2529

2630
flags:
2731
state:
@@ -40,6 +44,10 @@ flags:
4044
paths:
4145
- libs/eslint-plugin
4246
carryforward: true
47+
isr:
48+
paths:
49+
- libs/isr
50+
carryforward: true
4351

4452
comment:
4553
layout: diff, flags, files

.commitlintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"demos",
1414
"schematics",
1515
"ci",
16-
"eslint"
16+
"eslint",
17+
"isr"
1718
]
1819
],
1920
"type-empty": [2, "never"],

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ Thumbs.db
4747
# Generated Docusaurus files
4848
.docusaurus/
4949
.cache-loader/
50+
51+
.angular

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
/**/images/**/*
77
.docusaurus/
88
CHANGELOG.md
9+
10+
.angular

apps/docs/docs/isr/_category_.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"label": "@rx-angular/isr"
3+
}

apps/docs/docs/isr/api.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
sidebar_label: API
3+
sidebar_position: 10
4+
title: API
5+
---
6+
7+
TODO: Add API docs

apps/docs/docs/isr/benefits.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
sidebar_label: Benefits
3+
sidebar_position: 8
4+
title: Benefits
5+
---
6+
7+
- ✅ Improved TTFB metric (Time to first byte)
8+
- ✅ Less server resource usage because of caching
9+
- ✅ Don’t do the same work twice
10+
- ✅ Extendable API-s
11+
- ✅ DX
12+
- ✅ Open source library (MIT license)

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

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
---
2+
sidebar_label: Cache Handlers
3+
sidebar_position: 6
4+
title: Cache Handlers
5+
---
6+
7+
## Cache Handlers
8+
9+
Cache handlers are classes that extend the `CacheHandler` abstract class. They are responsible for handling the cache of the pages.
10+
11+
### InMemoryCacheHandler (default)
12+
13+
The default cache handler is the `InMemoryCacheHandler`. It stores the cached pages in memory (RAM). It uses the `Map` data structure to store the pages.
14+
15+
### FileSystemCacheHandler (prerendering on steroids)
16+
17+
There are cases where you want to store the cached pages in the file system. For example, if you want to deploy your app to a serverless environment, you can't use the `InMemoryCacheHandler` because the memory is not persistent. In this case, you can use the `FileSystemCacheHandler`.
18+
19+
The `FileSystemCacheHandler` stores the cached pages in the file system. It uses the `fs` module to read and write files. It stores the cached pages in the directory that you provide in the `cacheFolderPath` field.
20+
21+
The `FileSystemCacheHandler` has a field called `addPrerenderedPagesToCache`. If you set it to `true`, it will add the prerendered pages (from the path that you provide in the `prerenderedPagesPath` field) to the cache. If you set it to `false`, it will only add the pages that are cached using normal ISR. The default value is `false`.
22+
23+
```typescript
24+
const fsCacheHandler = new FileSystemCacheHandler({
25+
cacheFolderPath: join(distFolder, '/cache'),
26+
prerenderedPagesPath: distFolder,
27+
addPrerenderedPagesToCache: true,
28+
});
29+
```
30+
31+
And then, to register the cache handler, you need to pass it to the `cache` field in ISRHandler:
32+
33+
```typescript
34+
const isr = new ISRHandler({
35+
...
36+
// highlight-next-line
37+
cache: fsCacheHandler,
38+
});
39+
```
40+
41+
Now, the prerendered pages will be added to the cache. This means that the first request to a page will be served from the cache. And then, ISR will take over and revalidate the cache based on the `revalidate` field in the routes.
42+
43+
### Custom Cache Handler
44+
45+
The cache handling in ISR is **pluggable**. This means that you can use any cache handler that you want. You can also create your own cache handler.
46+
47+
To do that, you need to extend the `CacheHandler` abstract class.
48+
49+
To give you an idea of how to create a custom cache handler, let's take a look at this example of a custom cache handler that stores the cached pages in **redis**:
50+
51+
```typescript
52+
import Redis from 'ioredis';
53+
import { CacheData, CacheHandler, ISROptions } from '@rx-angular/isr/models';
54+
55+
type RedisCacheHandlerOptions = {
56+
/**
57+
* Redis connection string, e.g. "redis://localhost:6379"
58+
*/
59+
connectionString: string;
60+
/**
61+
* Redis key prefix, defaults to "isr:"
62+
*/
63+
keyPrefix?: string;
64+
};
65+
66+
export class RedisCacheHandler extends CacheHandler {
67+
private redis: Redis;
68+
69+
constructor(private readonly options: RedisCacheHandlerOptions) {
70+
super();
71+
72+
this.redis = new Redis(this.options.connectionString);
73+
console.log('RedisCacheHandler initialized 🚀');
74+
}
75+
76+
add(
77+
url: string,
78+
html: string,
79+
options: ISROptions = { revalidate: null }
80+
): Promise<void> {
81+
const htmlWithMsg = html + cacheMsg(options.revalidate);
82+
83+
return new Promise((resolve, reject) => {
84+
const cacheData: CacheData = {
85+
html: htmlWithMsg,
86+
options,
87+
createdAt: Date.now(),
88+
};
89+
const key = this.createKey(url);
90+
this.redis.set(key, JSON.stringify(cacheData)).then(() => {
91+
resolve();
92+
});
93+
});
94+
}
95+
96+
get(url: string): Promise<CacheData> {
97+
return new Promise((resolve, reject) => {
98+
const key = this.createKey(url);
99+
this.redis.get(key, (err, result) => {
100+
if (err || result === null || result === undefined) {
101+
reject('This url does not exist in cache!');
102+
} else {
103+
resolve(JSON.parse(result));
104+
}
105+
});
106+
});
107+
}
108+
109+
getAll(): Promise<string[]> {
110+
console.log('getAll() is not implemented for RedisCacheHandler');
111+
return Promise.resolve([]);
112+
}
113+
114+
has(url: string): Promise<boolean> {
115+
return new Promise((resolve, reject) => {
116+
const key = this.createKey(url);
117+
resolve(this.redis.exists(key).then((exists) => exists === 1));
118+
});
119+
}
120+
121+
delete(url: string): Promise<boolean> {
122+
return new Promise((resolve, reject) => {
123+
const key = this.createKey(url);
124+
resolve(this.redis.del(key).then((deleted) => deleted === 1));
125+
});
126+
}
127+
128+
clearCache?(): Promise<boolean> {
129+
throw new Error('Method not implemented.');
130+
}
131+
132+
private createKey(url: string): string {
133+
const prefix = this.options.keyPrefix || 'isr';
134+
return `${prefix}:${url}`;
135+
}
136+
}
137+
138+
const cacheMsg = (revalidateTime?: number | null): string => {
139+
const time = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
140+
141+
let msg = '<!-- ';
142+
143+
msg += `\n🚀 ISR: Served from Redis Cache! \n⌛ Last updated: ${time}. `;
144+
145+
if (revalidateTime) {
146+
msg += `\n⏭️ Next refresh is after ${revalidateTime} seconds. `;
147+
}
148+
149+
msg += ' \n-->';
150+
151+
return msg;
152+
};
153+
```
154+
155+
And then, to register the cache handler, you need to pass it to the `cache` field in ISRHandler:
156+
157+
```typescript title="server.ts"
158+
const redisCacheHandler = new RedisCacheHandler({
159+
connectionString: process.env['REDIS_CONNECTION_STRING'] || '' // e.g. "redis://localhost:6379"
160+
});
161+
162+
const isr = new ISRHandler({
163+
...
164+
// highlight-next-line
165+
cache: redisCacheHandler, // 👈 register the cache handler
166+
});
167+
```
168+
169+
And that's it! Now you have a custom cache handler that stores the cached pages in redis.
170+
171+
## Gotchas
172+
173+
When using ISR, you need to be aware of the following gotcha. When storing the cached pages in a redis storage, you need to separate cache pages based on the build id. This is because the build id is different for each build. If you don't do that, you will get javascript loading errors in the browser. This is because the browser will try to load the javascript files from the previous build, which don't exist anymore.
174+
175+
To solve this issue, we can use the `buildId` field in the `ISRHandler` options. This field is used to separate the cached pages based on the build id.
176+
177+
Where can we get the build id? We can add it in our `environment.ts` (for dev/prod) file:
178+
179+
```typescript
180+
export const environment = {
181+
...
182+
buildTimestamp: new Date().getTime(), // 👈 add this
183+
};
184+
```
185+
186+
And then, we can use it in our `ISRHandler` options:
187+
188+
```typescript
189+
const isr = new ISRHandler({
190+
...
191+
// highlight-next-line
192+
buildId: environment.buildTimestamp, // 👈 use it here
193+
});
194+
```
195+
196+
The buildId will help us separate the cached pages based on the build id. The moment the user ask for a page that we had cached in the previous build, we check if the build id of the cached page is the same as the current build id. If it's not, we server-side render the page again and cache it. If it's the same, we serve the cached page.
197+
198+
And that's it! Now you have a working ISR with a custom cache handler that stores the cached pages in redis and separates them based on the build id.
199+
200+
## Cache Handler API
201+
202+
The `CacheHandler` abstract class has the following API:
203+
204+
```typescript
205+
export abstract class CacheHandler {
206+
abstract add(url: string, html: string, options: ISROptions): Promise<void>;
207+
208+
abstract get(url: string): Promise<CacheData>;
209+
210+
abstract getAll(): Promise<string[]>;
211+
212+
abstract has(url: string): Promise<boolean>;
213+
214+
abstract delete(url: string): Promise<boolean>;
215+
216+
abstract clearCache?(): Promise<boolean>;
217+
}
218+
```
219+
220+
## Cache Data
221+
222+
The `CacheData` interface is used to store the cached pages in the cache handler. It has the following fields:
223+
224+
```typescript
225+
export interface CacheData {
226+
html: string;
227+
options: ISROptions;
228+
createdAt: number;
229+
}
230+
```

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
sidebar_label: Cache Hooks
3+
sidebar_position: 7
4+
title: Cache Hooks
5+
---
6+
7+
## Cache Hooks
8+
9+
There are cases where you want to modify the html that is served from cache or the html that is generated on the fly.
10+
11+
For example, you might want to add some tracking scripts to the html that is served from cache. Or you might want to add some custom html to the html that is generated on the fly.
12+
13+
To do that, you can use the `modifyCachedHtml` and `modifyGeneratedHtml` callbacks.
14+
15+
### modifyCachedHtml
16+
17+
The `modifyCachedHtml` callback is called when the html is served from cache (on every user request). It receives the request and the cached html as parameters. It should return the modified html.
18+
19+
### modifyGeneratedHtml
20+
21+
The `modifyGeneratedHtml` callback is called when the html is generated on the fly (before the cache is stored). It receives the request and the generated html as parameters. It should return the modified html.
22+
23+
### Example
24+
25+
```ts
26+
server.get(
27+
'*',
28+
// Serve page if it exists in cache
29+
async (req, res, next) =>
30+
await isr.serveFromCache(req, res, next, {
31+
modifyCachedHtml: (req, cachedHtml) => {
32+
return `${cachedHtml}<!-- Hello, I'm a modification to the original cache! -->`;
33+
},
34+
}),
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+
})
42+
);
43+
```
44+
45+
:::caution **Important:**
46+
Use these methods with caution as the logic written can increase the processing time.
47+
:::

0 commit comments

Comments
 (0)