Skip to content

Commit 2482a35

Browse files
committed
basic implementation
1 parent 6884444 commit 2482a35

File tree

11 files changed

+236
-0
lines changed

11 files changed

+236
-0
lines changed

packages/open-next/src/adapters/cache.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,25 @@ export default class Cache {
433433

434434
// Update all keys with the given tag with revalidatedAt set to now
435435
await globalThis.tagCache.writeTags(toInsert);
436+
437+
// We can now invalidate all paths in the CDN
438+
const uniquePaths = Array.from(
439+
new Set(
440+
toInsert
441+
// We need to filter fetch cache key as they are not in the CDN
442+
.filter((t) => t.tag.startsWith("_N_T_/"))
443+
.map((t) => t.path),
444+
),
445+
);
446+
if (uniquePaths.length > 0) {
447+
await globalThis.cdnInvalidationHandler.invalidatePaths(
448+
uniquePaths.map((path) => ({
449+
path,
450+
// Here we can be sure that the path is from app router
451+
isAppRouter: true,
452+
})),
453+
);
454+
}
436455
}
437456
} catch (e) {
438457
error("Failed to revalidate tag", e);

packages/open-next/src/core/createMainHandler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { debug } from "../adapters/logger";
44
import { generateUniqueId } from "../adapters/util";
55
import { openNextHandler } from "./requestHandler";
66
import {
7+
resolveCdnInvalidation,
78
resolveConverter,
89
resolveIncrementalCache,
910
resolveProxyRequest,
@@ -38,6 +39,10 @@ export async function createMainHandler() {
3839
thisFunction.override?.proxyExternalRequest,
3940
);
4041

42+
globalThis.cdnInvalidationHandler = await resolveCdnInvalidation(
43+
thisFunction.override?.cdnInvalidation
44+
)
45+
4146
globalThis.lastModified = {};
4247

4348
// From the config, we create the converter

packages/open-next/src/core/resolve.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,17 @@ export async function resolveProxyRequest(
141141
const m_1 = await import("../overrides/proxyExternalRequest/node.js");
142142
return m_1.default;
143143
}
144+
145+
146+
/**
147+
* @__PURE__
148+
*/
149+
export async function resolveCdnInvalidation(
150+
cdnInvalidation: OverrideOptions["cdnInvalidation"]
151+
) {
152+
if(typeof cdnInvalidation === "function") {
153+
return cdnInvalidation()
154+
}
155+
const m_1 = await import("../overrides/cdnInvalidation/dummy.js");
156+
return m_1.default
157+
}

packages/open-next/src/core/routing/util.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,34 @@ export function createServerResponse(
420420
internalEvent.rawPath,
421421
_headers,
422422
);
423+
await invalidateCDNOnRequest({
424+
rawPath: internalEvent.rawPath,
425+
isIsrRevalidation: internalEvent.headers["x-isr"] === "1",
426+
headers: _headers,
427+
});
423428
},
424429
responseStream,
425430
headers,
426431
);
427432
}
433+
434+
export async function invalidateCDNOnRequest(params: {
435+
//TODO: use the initialPath instead of rawPath, a rewrite could have happened and would make cdn invalidation fail
436+
rawPath: string;
437+
isIsrRevalidation?: boolean;
438+
headers: OutgoingHttpHeaders;
439+
}) {
440+
const { rawPath, isIsrRevalidation, headers } = params;
441+
if (
442+
!isIsrRevalidation &&
443+
headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED"
444+
) {
445+
await globalThis.cdnInvalidationHandler.invalidatePaths([
446+
{
447+
path: rawPath,
448+
//TODO: Here we assume that the path is for page router, this might not be the case
449+
isAppRouter: false,
450+
},
451+
]);
452+
}
453+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { CDNInvalidationHandler } from "types/overrides";
2+
3+
export default {
4+
name: "dummy",
5+
invalidatePaths: (_) => {
6+
return Promise.resolve();
7+
},
8+
} satisfies CDNInvalidationHandler;

packages/open-next/src/plugins/resolve.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface IPluginSettings {
2727
| IncludedOriginResolver;
2828
warmer?: LazyLoadedOverride<Warmer> | IncludedWarmer;
2929
proxyExternalRequest?: OverrideOptions["proxyExternalRequest"];
30+
cdnInvalidation?: OverrideOptions["cdnInvalidation"]
3031
};
3132
fnName?: string;
3233
}
@@ -52,6 +53,7 @@ const nameToFolder = {
5253
originResolver: "originResolver",
5354
warmer: "warmer",
5455
proxyExternalRequest: "proxyExternalRequest",
56+
cdnInvalidation: "cdnInvalidation"
5557
};
5658

5759
const defaultOverrides = {
@@ -64,6 +66,7 @@ const defaultOverrides = {
6466
originResolver: "pattern-env",
6567
warmer: "aws-lambda",
6668
proxyExternalRequest: "node",
69+
cdnInvalidation: "dummy"
6770
};
6871

6972
/**

packages/open-next/src/types/global.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AsyncLocalStorage } from "node:async_hooks";
22
import type { OutgoingHttpHeaders } from "node:http";
33

44
import type {
5+
CDNInvalidationHandler,
56
IncrementalCache,
67
ProxyExternalRequest,
78
Queue,
@@ -206,4 +207,11 @@ declare global {
206207
* Defined in `createMainHandler`.
207208
*/
208209
var proxyExternalRequest: ProxyExternalRequest;
210+
211+
/**
212+
* The function that will be called when the CDN needs invalidating (either from `revalidateTag` or from `res.revalidate`)
213+
* Available in main functions
214+
* Defined in `createMainHandler`
215+
*/
216+
var cdnInvalidationHandler: CDNInvalidationHandler;
209217
}

packages/open-next/src/types/open-next.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ReadableStream } from "node:stream/web";
33
import type { Writable } from "node:stream";
44
import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function";
55
import type {
6+
CDNInvalidationHandler,
67
Converter,
78
ImageLoader,
89
IncrementalCache,
@@ -152,6 +153,8 @@ export type IncludedWarmer = "aws-lambda" | "dummy";
152153

153154
export type IncludedProxyExternalRequest = "node" | "fetch" | "dummy";
154155

156+
export type IncludedCDNInvalidationHandler = "cloudfront" | "dummy"
157+
155158
export interface DefaultOverrideOptions<
156159
E extends BaseEventOrResult = InternalEvent,
157160
R extends BaseEventOrResult = InternalResult,
@@ -203,6 +206,14 @@ export interface OverrideOptions extends DefaultOverrideOptions {
203206
proxyExternalRequest?:
204207
| IncludedProxyExternalRequest
205208
| LazyLoadedOverride<ProxyExternalRequest>;
209+
210+
/**
211+
* Add possibility to override the default cdn invalidation for On Demand Revalidation
212+
* @default "dummy"
213+
*/
214+
cdnInvalidation?:
215+
| IncludedCDNInvalidationHandler
216+
| LazyLoadedOverride<CDNInvalidationHandler>;
206217
}
207218

208219
export interface InstallOptions {

packages/open-next/src/types/overrides.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,12 @@ export type OriginResolver = BaseOverride & {
139139
export type ProxyExternalRequest = BaseOverride & {
140140
proxy: (event: InternalEvent) => Promise<InternalResult>;
141141
};
142+
143+
type CDNPath = {
144+
path: string;
145+
isAppRouter: boolean;
146+
};
147+
148+
export type CDNInvalidationHandler = BaseOverride & {
149+
invalidatePaths: (paths: CDNPath[]) => Promise<void>;
150+
};

packages/tests-unit/tests/adapters/cache.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { vi } from "vitest";
44

55
declare global {
66
var disableIncrementalCache: boolean;
7+
var disableDynamoDBCache: boolean;
78
var isNextAfter15: boolean;
89
}
910

@@ -39,6 +40,12 @@ describe("CacheHandler", () => {
3940
};
4041
globalThis.tagCache = tagCache;
4142

43+
const invalidateCdnHandler = {
44+
name: "mock",
45+
invalidatePaths: vi.fn(),
46+
};
47+
globalThis.cdnInvalidationHandler = invalidateCdnHandler;
48+
4249
globalThis.__openNextAls = {
4350
getStore: vi.fn().mockReturnValue({
4451
requestId: "123",
@@ -470,4 +477,72 @@ describe("CacheHandler", () => {
470477
).not.toThrow();
471478
});
472479
});
480+
481+
describe("revalidateTag", () => {
482+
beforeEach(() => {
483+
globalThis.disableDynamoDBCache = false;
484+
globalThis.disableIncrementalCache = false;
485+
});
486+
it("Should do nothing if disableIncrementalCache is true", async () => {
487+
globalThis.disableIncrementalCache = true;
488+
489+
await cache.revalidateTag("tag");
490+
491+
expect(tagCache.writeTags).not.toHaveBeenCalled();
492+
});
493+
494+
it("Should do nothing if disableTagCache is true", async () => {
495+
globalThis.disableDynamoDBCache = true;
496+
497+
await cache.revalidateTag("tag");
498+
499+
expect(tagCache.writeTags).not.toHaveBeenCalled();
500+
});
501+
502+
it("Should call tagCache.writeTags", async () => {
503+
globalThis.tagCache.getByTag.mockResolvedValueOnce(["/path"]);
504+
await cache.revalidateTag("tag");
505+
506+
expect(globalThis.tagCache.getByTag).toHaveBeenCalledWith("tag");
507+
508+
expect(tagCache.writeTags).toHaveBeenCalled();
509+
expect(tagCache.writeTags).toHaveBeenCalledWith([
510+
{
511+
path: "/path",
512+
tag: "tag",
513+
},
514+
]);
515+
});
516+
517+
it("Should call invalidateCdnHandler.invalidatePaths", async () => {
518+
globalThis.tagCache.getByTag.mockResolvedValueOnce(["/path"]);
519+
globalThis.tagCache.getByPath.mockResolvedValueOnce([]);
520+
await cache.revalidateTag("_N_T_/path");
521+
522+
expect(tagCache.writeTags).toHaveBeenCalled();
523+
expect(tagCache.writeTags).toHaveBeenCalledWith([
524+
{
525+
path: "/path",
526+
tag: "_N_T_/path",
527+
},
528+
]);
529+
530+
expect(invalidateCdnHandler.invalidatePaths).toHaveBeenCalled();
531+
});
532+
533+
it("Should not call invalidateCdnHandler.invalidatePaths for fetch cache key ", async () => {
534+
globalThis.tagCache.getByTag.mockResolvedValueOnce(["123456"]);
535+
await cache.revalidateTag("tag");
536+
537+
expect(tagCache.writeTags).toHaveBeenCalled();
538+
expect(tagCache.writeTags).toHaveBeenCalledWith([
539+
{
540+
path: "123456",
541+
tag: "tag",
542+
},
543+
]);
544+
545+
expect(invalidateCdnHandler.invalidatePaths).not.toHaveBeenCalled();
546+
});
547+
});
473548
});

0 commit comments

Comments
 (0)