Skip to content

Commit

Permalink
feat: kv binding support for suspense cache (#548)
Browse files Browse the repository at this point in the history
  • Loading branch information
james-elicx authored Nov 26, 2023
1 parent f364224 commit 888ae85
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 20 deletions.
10 changes: 10 additions & 0 deletions .changeset/twelve-cobras-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@cloudflare/next-on-pages': minor
---

Add support for using a KV to implement the Suspense Cache via naming convention

With this change users can have their suspense cache implemented via a KV binding, in order to
opt-in to such implementation they simply need to make sure that their application has a KV binding
named `__NEXT_ON_PAGES__KV_SUSPENSE_CACHE` (the next-on-pages worker will pick up such
binding and use it to implement the suspense cache instead of using the default workers cache API).
8 changes: 7 additions & 1 deletion packages/next-on-pages/docs/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Storage Options

There are various different bindings and storage options that one could use for caching. At the moment, `@cloudflare/next-on-pages` supports the Cache API out-of-the-box.
There are various different bindings and storage options that one could use for caching. At the moment, `@cloudflare/next-on-pages` supports the Cache API and Workers KV out-of-the-box.

In the future, support will be available for creating custom cache interfaces and using different bindings.

Expand All @@ -13,3 +13,9 @@ In the future, support will be available for creating custom cache interfaces an
The [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) is a per data-center cache that is ideal for storing data that is not required to be accessible globally. It is worth noting that Vercel's Data Cache is regional, like with the Cache API, so there is no difference in terms of data availability.

Due to how the Cache API works, you need to be using a domain for your deployment for it to take effect.

### Workers KV

[Workers KV](https://developers.cloudflare.com/kv/) is a low-latency key-value store that is ideal for storing data that should be globally distributed. KV is eventually consistent, which means that it will take up to 60 seconds for updates to be reflected globally.

To use Workers KV for caching, you need to add a binding to your Pages project with the name `__NEXT_ON_PAGES__KV_SUSPENSE_CACHE`, and map it to a KV namespace.
1 change: 1 addition & 0 deletions packages/next-on-pages/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ declare global {
npm_config_user_agent?: string;
CF_PAGES?: string;
SHELL?: string;
__NEXT_ON_PAGES__KV_SUSPENSE_CACHE?: KVNamespace;
[key: string]: string | Fetcher;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ async function prepareAndBuildWorker(
const functionsDir = resolve('.vercel', 'output', 'functions');
const workerJsDir = join(outputDir, '_worker.js');
const nopDistDir = join(workerJsDir, '__next-on-pages-dist__');
const templatesDir = join(__dirname, '..', 'templates');

if (!(await validateDir(functionsDir))) {
cliLog(
Expand All @@ -132,7 +133,7 @@ async function prepareAndBuildWorker(

const outputtedWorkerPath = await buildWorkerFile(
processedVercelOutput,
outputDir,
{ outputDir, workerJsDir, nopDistDir, templatesDir },
!disableWorkerMinification,
);

Expand Down
24 changes: 21 additions & 3 deletions packages/next-on-pages/src/buildApplication/buildWorkerFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function constructBuildOutputRecord(

export async function buildWorkerFile(
{ vercelConfig, vercelOutput }: ProcessedVercelOutput,
outputDir: string,
{ outputDir, workerJsDir, nopDistDir, templatesDir }: BuildWorkerFileOpts,
minify: boolean,
): Promise<string> {
const functionsFile = join(
Expand All @@ -59,10 +59,10 @@ export async function buildWorkerFile(
.join(',')}};`,
);

const outputFile = join(outputDir, '_worker.js', 'index.js');
const outputFile = join(workerJsDir, 'index.js');

await build({
entryPoints: [join(__dirname, '..', 'templates', '_worker.js')],
entryPoints: [join(templatesDir, '_worker.js')],
banner: {
js: generateGlobalJs(),
},
Expand All @@ -79,5 +79,23 @@ export async function buildWorkerFile(
minify,
});

await build({
entryPoints: ['adaptor.ts', 'cache-api.ts', 'kv.ts'].map(fileName =>
join(templatesDir, 'cache', fileName),
),
bundle: false,
target: 'es2022',
platform: 'neutral',
outdir: join(nopDistDir, 'cache'),
minify,
});

return relative('.', outputFile);
}

type BuildWorkerFileOpts = {
outputDir: string;
workerJsDir: string;
nopDistDir: string;
templatesDir: string;
};
21 changes: 18 additions & 3 deletions packages/next-on-pages/templates/_worker.js/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { CacheAdaptor, IncrementalCacheValue } from '../../cache';
import { SUSPENSE_CACHE_URL } from '../../cache';
import { CacheApiAdaptor } from '../../cache/cache-api';

/**
* Handles an internal request to the suspense cache.
Expand Down Expand Up @@ -71,6 +70,22 @@ export async function handleSuspenseCacheRequest(request: Request) {
* @returns Adaptor for the suspense cache.
*/
export async function getSuspenseCacheAdaptor(): Promise<CacheAdaptor> {
// TODO: Try to lazy import the custom cache adaptor.
return new CacheApiAdaptor();
if (process.env.__NEXT_ON_PAGES__KV_SUSPENSE_CACHE) {
return getInternalCacheAdaptor('kv');
}

return getInternalCacheAdaptor('cache-api');
}

/**
* Gets an internal cache adaptor.
*
* @param type The type of adaptor to get.
* @returns A new instance of the adaptor.
*/
async function getInternalCacheAdaptor(
type: 'kv' | 'cache-api',
): Promise<CacheAdaptor> {
const adaptor = await import(`./__next-on-pages-dist__/cache/${type}.js`);
return new adaptor.default();
}
10 changes: 10 additions & 0 deletions packages/next-on-pages/templates/cache/adaptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ export class CacheAdaptor {

await this.saveTagsManifest();
}

/**
* Builds the full cache key for the suspense cache.
*
* @param key Key for the item in the suspense cache.
* @returns The fully-formed cache key for the suspense cache.
*/
public buildCacheKey(key: string) {
return `https://${SUSPENSE_CACHE_URL}/entry/${key}`;
}
}

// https://github.com/vercel/next.js/blob/261db49/packages/next/src/server/lib/incremental-cache/file-system-cache.ts#L17
Expand Down
14 changes: 2 additions & 12 deletions packages/next-on-pages/templates/cache/cache-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CacheAdaptor, SUSPENSE_CACHE_URL } from './adaptor';
import { CacheAdaptor } from './adaptor.js';

/** Suspense Cache adaptor for the Cache API. */
export class CacheApiAdaptor extends CacheAdaptor {
export default class CacheApiAdaptor extends CacheAdaptor {
/** Name of the cache to open in the Cache API. */
public cacheName = 'suspense-cache';

Expand Down Expand Up @@ -29,14 +29,4 @@ export class CacheApiAdaptor extends CacheAdaptor {
});
await cache.put(this.buildCacheKey(key), response);
}

/**
* Builds the full cache key for the suspense cache.
*
* @param key Key for the item in the suspense cache.
* @returns The fully-formed cache key for the suspense cache.
*/
public buildCacheKey(key: string) {
return `https://${SUSPENSE_CACHE_URL}/entry/${key}`;
}
}
23 changes: 23 additions & 0 deletions packages/next-on-pages/templates/cache/kv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CacheAdaptor } from './adaptor.js';

/** Suspense Cache adaptor for Workers KV. */
export default class KVAdaptor extends CacheAdaptor {
constructor(ctx: Record<string, unknown> = {}) {
super(ctx);
}

public override async retrieve(key: string) {
const value = await process.env.__NEXT_ON_PAGES__KV_SUSPENSE_CACHE?.get(
this.buildCacheKey(key),
);

return value ?? null;
}

public override async update(key: string, value: string) {
await process.env.__NEXT_ON_PAGES__KV_SUSPENSE_CACHE?.put(
this.buildCacheKey(key),
value,
);
}
}

0 comments on commit 888ae85

Please sign in to comment.