Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 33 additions & 0 deletions .changeset/grumpy-dingos-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
"@opennextjs/cloudflare": minor
---

Bump aws to 3.6.0

Introduce support for the composable cache

BREAKING CHANGE: The interface for the Incremental cache has changed. The new interface use a Cache type instead of a boolean to distinguish between the different types of caches. It also includes a new Cache type for the composable cache. The new interface is as follows:

```ts
export type CacheEntryType = "cache" | "fetch" | "composable";

export type IncrementalCache = {
get<CacheType extends CacheEntryType = "cache">(
key: string,
cacheType?: CacheType
): Promise<WithLastModified<CacheValue<CacheType>> | null>;
set<CacheType extends CacheEntryType = "cache">(
key: string,
value: CacheValue<CacheType>,
isFetch?: CacheType
): Promise<void>;
delete(key: string): Promise<void>;
name: string;
};
```

NextModeTagCache also get a new function `getLastRevalidated` used for the composable cache:

```ts
getLastRevalidated(tags: string[]): Promise<number>;
```
2 changes: 1 addition & 1 deletion packages/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"homepage": "https://github.com/opennextjs/opennextjs-cloudflare",
"dependencies": {
"@dotenvx/dotenvx": "catalog:",
"@opennextjs/aws": "3.5.8",
"@opennextjs/aws": "3.6.0",
"enquirer": "^2.4.1",
"glob": "catalog:",
"ts-tqdm": "^0.8.6"
Expand Down
18 changes: 18 additions & 0 deletions packages/cloudflare/src/api/durable-objects/sharded-tag-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ export class DOShardedTagCache extends DurableObject<CloudflareEnv> {
});
}

async getLastRevalidated(tags: string[]): Promise<number> {
try {
const result = this.sql
.exec(
`SELECT MAX(revalidatedAt) AS time FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")})`,
...tags
)
.toArray();
if (result.length === 0) return 0;
// We only care about the most recent revalidation
return result[0]?.time as number;
} catch (e) {
console.error(e);
// By default we don't want to crash here, so we return 0
return 0;
}
}

async hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean> {
return (
this.sql
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { error } from "@opennextjs/aws/adapters/logger.js";
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
import type {
CacheEntryType,
CacheValue,
IncrementalCache,
WithLastModified,
} from "@opennextjs/aws/types/overrides.js";
import { IgnorableError } from "@opennextjs/aws/utils/error.js";

import { getCloudflareContext } from "../../cloudflare-context.js";
Expand All @@ -24,20 +29,17 @@ export const PREFIX_ENV_NAME = "NEXT_INC_CACHE_KV_PREFIX";
class KVIncrementalCache implements IncrementalCache {
readonly name = NAME;

async get<IsFetch extends boolean = false>(
async get<CacheType extends CacheEntryType = "cache">(
key: string,
isFetch?: IsFetch
): Promise<WithLastModified<CacheValue<IsFetch>> | null> {
cacheType?: CacheType
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
const kv = getCloudflareContext().env[BINDING_NAME];
if (!kv) throw new IgnorableError("No KV Namespace");

debugCache(`Get ${key}`);

try {
const entry = await kv.get<IncrementalCacheEntry<IsFetch> | CacheValue<IsFetch>>(
this.getKVKey(key, isFetch),
"json"
);
const entry = await kv.get<IncrementalCacheEntry<CacheType>>(this.getKVKey(key, cacheType), "json");

if (!entry) return null;

Expand All @@ -56,10 +58,10 @@ class KVIncrementalCache implements IncrementalCache {
}
}

async set<IsFetch extends boolean = false>(
async set<CacheType extends CacheEntryType = "cache">(
key: string,
value: CacheValue<IsFetch>,
isFetch?: IsFetch
value: CacheValue<CacheType>,
cacheType?: CacheType
): Promise<void> {
const kv = getCloudflareContext().env[BINDING_NAME];
if (!kv) throw new IgnorableError("No KV Namespace");
Expand All @@ -68,7 +70,7 @@ class KVIncrementalCache implements IncrementalCache {

try {
await kv.put(
this.getKVKey(key, isFetch),
this.getKVKey(key, cacheType),
JSON.stringify({
value,
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
Expand All @@ -90,17 +92,18 @@ class KVIncrementalCache implements IncrementalCache {
debugCache(`Delete ${key}`);

try {
await kv.delete(this.getKVKey(key, /* isFetch= */ false));
// Only cache that gets deleted is the ISR/SSG cache.
await kv.delete(this.getKVKey(key, "cache"));
} catch (e) {
error("Failed to delete from cache", e);
}
}

protected getKVKey(key: string, isFetch?: boolean): string {
protected getKVKey(key: string, cacheType?: CacheEntryType): string {
return computeCacheKey(key, {
prefix: getCloudflareContext().env[PREFIX_ENV_NAME],
buildId: process.env.NEXT_BUILD_ID,
isFetch,
cacheType,
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { error } from "@opennextjs/aws/adapters/logger.js";
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
import type {
CacheEntryType,
CacheValue,
IncrementalCache,
WithLastModified,
} from "@opennextjs/aws/types/overrides.js";
import { IgnorableError } from "@opennextjs/aws/utils/error.js";

import { getCloudflareContext } from "../../cloudflare-context.js";
Expand All @@ -21,17 +26,17 @@ export const PREFIX_ENV_NAME = "NEXT_INC_CACHE_R2_PREFIX";
class R2IncrementalCache implements IncrementalCache {
readonly name = NAME;

async get<IsFetch extends boolean = false>(
async get<CacheType extends CacheEntryType = "cache">(
key: string,
isFetch?: IsFetch
): Promise<WithLastModified<CacheValue<IsFetch>> | null> {
cacheType?: CacheType
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
const r2 = getCloudflareContext().env[BINDING_NAME];
if (!r2) throw new IgnorableError("No R2 bucket");

debugCache(`Get ${key}`);

try {
const r2Object = await r2.get(this.getR2Key(key, isFetch));
const r2Object = await r2.get(this.getR2Key(key, cacheType));
if (!r2Object) return null;

return {
Expand All @@ -44,18 +49,18 @@ class R2IncrementalCache implements IncrementalCache {
}
}

async set<IsFetch extends boolean = false>(
async set<CacheType extends CacheEntryType = "cache">(
key: string,
value: CacheValue<IsFetch>,
isFetch?: IsFetch
value: CacheValue<CacheType>,
cacheType?: CacheType
): Promise<void> {
const r2 = getCloudflareContext().env[BINDING_NAME];
if (!r2) throw new IgnorableError("No R2 bucket");

debugCache(`Set ${key}`);

try {
await r2.put(this.getR2Key(key, isFetch), JSON.stringify(value));
await r2.put(this.getR2Key(key, cacheType), JSON.stringify(value));
} catch (e) {
error("Failed to set to cache", e);
}
Expand All @@ -74,11 +79,11 @@ class R2IncrementalCache implements IncrementalCache {
}
}

protected getR2Key(key: string, isFetch?: boolean): string {
protected getR2Key(key: string, cacheType?: CacheEntryType): string {
return computeCacheKey(key, {
prefix: getCloudflareContext().env[PREFIX_ENV_NAME],
buildId: process.env.NEXT_BUILD_ID,
isFetch,
cacheType,
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { error } from "@opennextjs/aws/adapters/logger.js";
import { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
import {
CacheEntryType,
CacheValue,
IncrementalCache,
WithLastModified,
} from "@opennextjs/aws/types/overrides.js";

import { getCloudflareContext } from "../../cloudflare-context.js";
import { debugCache, FALLBACK_BUILD_ID, IncrementalCacheEntry } from "../internal.js";
Expand Down Expand Up @@ -37,8 +42,8 @@ type Options = {

interface PutToCacheInput {
key: string;
isFetch: boolean | undefined;
entry: IncrementalCacheEntry<boolean>;
cacheType?: CacheEntryType;
entry: IncrementalCacheEntry<CacheEntryType>;
}

/**
Expand All @@ -60,13 +65,13 @@ class RegionalCache implements IncrementalCache {
this.opts.shouldLazilyUpdateOnCacheHit ??= this.opts.mode === "long-lived";
}

async get<IsFetch extends boolean = false>(
async get<CacheType extends CacheEntryType = "cache">(
key: string,
isFetch?: IsFetch
): Promise<WithLastModified<CacheValue<IsFetch>> | null> {
cacheType?: CacheType
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
try {
const cache = await this.getCacheInstance();
const urlKey = this.getCacheUrlKey(key, isFetch);
const urlKey = this.getCacheUrlKey(key, cacheType);

// Check for a cached entry as this will be faster than the store response.
const cachedResponse = await cache.match(urlKey);
Expand All @@ -76,11 +81,11 @@ class RegionalCache implements IncrementalCache {
// Re-fetch from the store and update the regional cache in the background
if (this.opts.shouldLazilyUpdateOnCacheHit) {
getCloudflareContext().ctx.waitUntil(
this.store.get(key, isFetch).then(async (rawEntry) => {
this.store.get(key, cacheType).then(async (rawEntry) => {
const { value, lastModified } = rawEntry ?? {};

if (value && typeof lastModified === "number") {
await this.putToCache({ key, isFetch, entry: { value, lastModified } });
await this.putToCache({ key, cacheType, entry: { value, lastModified } });
}
})
);
Expand All @@ -89,12 +94,14 @@ class RegionalCache implements IncrementalCache {
return cachedResponse.json();
}

const rawEntry = await this.store.get(key, isFetch);
const rawEntry = await this.store.get(key, cacheType);
const { value, lastModified } = rawEntry ?? {};
if (!value || typeof lastModified !== "number") return null;

// Update the locale cache after retrieving from the store.
getCloudflareContext().ctx.waitUntil(this.putToCache({ key, isFetch, entry: { value, lastModified } }));
getCloudflareContext().ctx.waitUntil(
this.putToCache({ key, cacheType, entry: { value, lastModified } })
);

return { value, lastModified };
} catch (e) {
Expand All @@ -103,17 +110,17 @@ class RegionalCache implements IncrementalCache {
}
}

async set<IsFetch extends boolean = false>(
async set<CacheType extends CacheEntryType = "cache">(
key: string,
value: CacheValue<IsFetch>,
isFetch?: IsFetch
value: CacheValue<CacheType>,
cacheType?: CacheType
): Promise<void> {
try {
await this.store.set(key, value, isFetch);
await this.store.set(key, value, cacheType);

await this.putToCache({
key,
isFetch,
cacheType,
entry: {
value,
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
Expand Down Expand Up @@ -144,15 +151,13 @@ class RegionalCache implements IncrementalCache {
return this.localCache;
}

protected getCacheUrlKey(key: string, isFetch?: boolean) {
protected getCacheUrlKey(key: string, cacheType?: CacheEntryType) {
const buildId = process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
return (
"http://cache.local" + `/${buildId}/${key}`.replace(/\/+/g, "/") + `.${isFetch ? "fetch" : "cache"}`
);
return "http://cache.local" + `/${buildId}/${key}`.replace(/\/+/g, "/") + `.${cacheType ?? "cache"}`;
}

protected async putToCache({ key, isFetch, entry }: PutToCacheInput): Promise<void> {
const urlKey = this.getCacheUrlKey(key, isFetch);
protected async putToCache({ key, cacheType, entry }: PutToCacheInput): Promise<void> {
const urlKey = this.getCacheUrlKey(key, cacheType);
const cache = await this.getCacheInstance();

const age =
Expand Down Expand Up @@ -209,7 +214,7 @@ export function withRegionalCache(cache: IncrementalCache, opts: Options) {
/**
* Extract the list of tags from a cache entry.
*/
function getTagsFromCacheEntry(entry: IncrementalCacheEntry<boolean>): string[] | undefined {
function getTagsFromCacheEntry(entry: IncrementalCacheEntry<CacheEntryType>): string[] | undefined {
if ("tags" in entry.value && entry.value.tags) {
return entry.value.tags;
}
Expand All @@ -225,4 +230,7 @@ function getTagsFromCacheEntry(entry: IncrementalCacheEntry<boolean>): string[]
return rawTags.split(",");
}
}
if ("value" in entry.value) {
return entry.value.tags;
}
}
Loading