Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silver-pets-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/aws": minor
---

introduce a new optional mode for the tag cache
214 changes: 92 additions & 122 deletions packages/open-next/src/adapters/cache.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,12 @@
import type {
CacheHandlerValue,
IncrementalCacheContext,
IncrementalCacheValue,
} from "types/cache";
import { getTagsFromValue, hasBeenRevalidated } from "utils/cache";
import { isBinaryContentType } from "../utils/binary";
import { debug, error, warn } from "./logger";

interface CachedFetchValue {
kind: "FETCH";
data: {
headers: { [k: string]: string };
body: string;
url: string;
status?: number;
tags?: string[];
};
revalidate: number;
}

interface CachedRedirectValue {
kind: "REDIRECT";
props: Object;
}

interface CachedRouteValue {
kind: "ROUTE" | "APP_ROUTE";
// this needs to be a RenderResult so since renderResponse
// expects that type instead of a string
body: Buffer;
status: number;
headers: Record<string, undefined | string | string[]>;
}

interface CachedImageValue {
kind: "IMAGE";
etag: string;
buffer: Buffer;
extension: string;
isMiss?: boolean;
isStale?: boolean;
}

interface IncrementalCachedPageValue {
kind: "PAGE" | "PAGES";
// this needs to be a string since the cache expects to store
// the string value
html: string;
pageData: Object;
status?: number;
headers?: Record<string, undefined | string>;
}

interface IncrementalCachedAppPageValue {
kind: "APP_PAGE";
// this needs to be a string since the cache expects to store
// the string value
html: string;
rscData: Buffer;
headers?: Record<string, undefined | string | string[]>;
postponed?: string;
status?: number;
}

type IncrementalCacheValue =
| CachedRedirectValue
| IncrementalCachedPageValue
| IncrementalCachedAppPageValue
| CachedImageValue
| CachedFetchValue
| CachedRouteValue;

type IncrementalCacheContext = {
revalidate?: number | false | undefined;
fetchCache?: boolean | undefined;
fetchUrl?: string | undefined;
fetchIdx?: number | undefined;
tags?: string[] | undefined;
};

interface CacheHandlerValue {
lastModified?: number;
age?: number;
cacheState?: string;
value: IncrementalCacheValue | null;
}

function isFetchCache(
options?:
| boolean
Expand Down Expand Up @@ -134,14 +61,15 @@ export default class Cache {

if (cachedEntry?.value === undefined) return null;

const _lastModified = await globalThis.tagCache.getLastModified(
const _tags = [...(tags ?? []), ...(softTags ?? [])];
const _lastModified = cachedEntry.lastModified ?? Date.now();
const _hasBeenRevalidated = await hasBeenRevalidated(
key,
cachedEntry?.lastModified,
_tags,
cachedEntry,
);
if (_lastModified === -1) {
// If some tags are stale we need to force revalidation
return null;
}

if (_hasBeenRevalidated) return null;

// For cases where we don't have tags, we need to ensure that the soft tags are not being revalidated
// We only need to check for the path as it should already contain all the tags
Expand All @@ -154,11 +82,12 @@ export default class Cache {
!tag.endsWith("page"),
);
if (path) {
const pathLastModified = await globalThis.tagCache.getLastModified(
const hasPathBeenUpdated = await hasBeenRevalidated(
path.replace("_N_T_/", ""),
cachedEntry.lastModified,
[],
cachedEntry,
);
if (pathLastModified === -1) {
if (hasPathBeenUpdated) {
// In case the path has been revalidated, we don't want to use the fetch cache
return null;
}
Expand All @@ -184,20 +113,23 @@ export default class Cache {
return null;
}

const meta = cachedEntry.value.meta;
const _lastModified = await globalThis.tagCache.getLastModified(
const cacheData = cachedEntry?.value;

const meta = cacheData?.meta;
const tags = getTagsFromValue(cacheData);
const _lastModified = cachedEntry.lastModified ?? Date.now();
const _hasBeenRevalidated = await hasBeenRevalidated(
key,
cachedEntry?.lastModified,
tags,
cachedEntry,
);
if (_lastModified === -1) {
// If some tags are stale we need to force revalidation
return null;
}
const cacheData = cachedEntry?.value;
if (cacheData === undefined || _hasBeenRevalidated) return null;

const store = globalThis.__openNextAls.getStore();
if (store) {
store.lastModified = _lastModified;
}

if (cacheData?.type === "route") {
return {
lastModified: _lastModified,
Expand Down Expand Up @@ -363,32 +295,8 @@ export default class Cache {
break;
}
}
// Write derivedTags to dynamodb
// If we use an in house version of getDerivedTags in build we should use it here instead of next's one
const derivedTags: string[] =
data?.kind === "FETCH"
? (ctx?.tags ?? data?.data?.tags ?? []) // before version 14 next.js used data?.data?.tags so we keep it for backward compatibility
: data?.kind === "PAGE"
? (data.headers?.["x-next-cache-tags"]?.split(",") ?? [])
: [];
debug("derivedTags", derivedTags);
// Get all tags stored in dynamodb for the given key
// If any of the derived tags are not stored in dynamodb for the given key, write them
const storedTags = await globalThis.tagCache.getByPath(key);
const tagsToWrite = derivedTags.filter(
(tag) => !storedTags.includes(tag),
);
if (tagsToWrite.length > 0) {
await globalThis.tagCache.writeTags(
tagsToWrite.map((tag) => ({
path: key,
tag: tag,
// In case the tags are not there we just need to create them
// but we don't want them to return from `getLastModified` as they are not stale
revalidatedAt: 1,
})),
);
}

await this.updateTagsOnSet(key, data, ctx);
debug("Finished setting cache");
} catch (e) {
error("Failed to set cache", e);
Expand All @@ -405,6 +313,29 @@ export default class Cache {
}
try {
const _tags = Array.isArray(tags) ? tags : [tags];
if (globalThis.tagCache.mode === "nextMode") {
const paths = (await globalThis.tagCache.getPathsByTags?.(_tags)) ?? [];

await globalThis.tagCache.writeTags(_tags);
if (paths.length > 0) {
// TODO: we should introduce a new method in cdnInvalidationHandler to invalidate paths by tags for cdn that supports it
// It also means that we'll need to provide the tags used in every request to the wrapper or converter.
await globalThis.cdnInvalidationHandler.invalidatePaths(
paths.map((path) => ({
initialPath: path,
rawPath: path,
resolvedRoutes: [
{
route: path,
// TODO: ideally here we should check if it's an app router page or route
type: "app",
},
],
})),
);
}
return;
}
for (const tag of _tags) {
debug("revalidateTag", tag);
// Find all keys with the given tag
Expand Down Expand Up @@ -468,4 +399,43 @@ export default class Cache {
error("Failed to revalidate tag", e);
}
}

private async updateTagsOnSet(
key: string,
data?: IncrementalCacheValue,
ctx?: IncrementalCacheContext,
) {
if (
globalThis.openNextConfig.dangerous?.disableTagCache ||
globalThis.tagCache.mode === "nextMode"
) {
return;
}

// Write derivedTags to the tag cache
// If we use an in house version of getDerivedTags in build we should use it here instead of next's one
const derivedTags: string[] =
data?.kind === "FETCH"
? (ctx?.tags ?? data?.data?.tags ?? []) // before version 14 next.js used data?.data?.tags so we keep it for backward compatibility
: data?.kind === "PAGE"
? (data.headers?.["x-next-cache-tags"]?.split(",") ?? [])
: [];
debug("derivedTags", derivedTags);

// Get all tags stored in dynamodb for the given key
// If any of the derived tags are not stored in dynamodb for the given key, write them
const storedTags = await globalThis.tagCache.getByPath(key);
const tagsToWrite = derivedTags.filter((tag) => !storedTags.includes(tag));
if (tagsToWrite.length > 0) {
await globalThis.tagCache.writeTags(
tagsToWrite.map((tag) => ({
path: key,
tag: tag,
// In case the tags are not there we just need to create them
// but we don't want them to return from `getLastModified` as they are not stale
revalidatedAt: 1,
})),
);
}
}
}
8 changes: 8 additions & 0 deletions packages/open-next/src/adapters/dynamo-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ async function defaultHandler(
async function insert(
requestType: InitializationFunctionEvent["requestType"],
): Promise<InitializationFunctionEvent> {
// If it is in nextMode, we don't need to do anything
if (tagCache.mode === "nextMode") {
return {
type: "initializationFunction",
requestType,
resourceId: PHYSICAL_RESOURCE_ID,
};
}
const file = readFileSync("dynamodb-cache.json", "utf8");

const data: DataType[] = JSON.parse(file);
Expand Down
21 changes: 11 additions & 10 deletions packages/open-next/src/core/routing/cacheInterceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import type { InternalEvent, InternalResult } from "types/open-next";
import type { CacheValue } from "types/overrides";
import { emptyReadableStream, toReadableStream } from "utils/stream";

import { debug } from "../../adapters/logger.js";
import { localizePath } from "./i18n/index.js";
import { generateMessageGroupId } from "./queue.js";
import { getTagsFromValue, hasBeenRevalidated } from "utils/cache";
import { debug } from "../../adapters/logger";
import { localizePath } from "./i18n";
import { generateMessageGroupId } from "./queue";

const CACHE_ONE_YEAR = 60 * 60 * 24 * 365;
const CACHE_ONE_MONTH = 60 * 60 * 24 * 30;
Expand Down Expand Up @@ -161,15 +162,15 @@ export async function cacheInterceptor(
if (!cachedData?.value) {
return event;
}

if (cachedData?.value?.type === "app") {
// We need to check the tag cache now
const _lastModified = await globalThis.tagCache.getLastModified(
// We need to check the tag cache now
if (cachedData.value?.type === "app") {
const tags = getTagsFromValue(cachedData.value);
const _hasBeenRevalidated = await hasBeenRevalidated(
localizedPath,
cachedData.lastModified,
tags,
cachedData,
);
if (_lastModified === -1) {
// If some tags are stale we need to force revalidation
if (_hasBeenRevalidated) {
return event;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const maxCacheSize = process.env.OPEN_NEXT_LOCAL_CACHE_SIZE
: 1000;

const localCache = new LRUCache<{
value: CacheValue<false>;
value: CacheValue<any>;
lastModified: number;
}>(maxCacheSize);

Expand Down
1 change: 1 addition & 0 deletions packages/open-next/src/overrides/tagCache/dummy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { TagCache } from "types/overrides";
// We don't want to throw error on this one because we might use it when we don't need tag cache
const dummyTagCache: TagCache = {
name: "dummy",
mode: "original",
getByPath: async () => {
return [];
},
Expand Down
1 change: 1 addition & 0 deletions packages/open-next/src/overrides/tagCache/dynamodb-lite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ function buildDynamoObject(path: string, tags: string, revalidatedAt?: number) {
}

const tagCache: TagCache = {
mode: "original",
async getByPath(path) {
try {
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
Expand Down
Loading
Loading