Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VDC Support for Catalyst Middleware #2

Closed
wants to merge 3 commits into from
Closed
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
11 changes: 11 additions & 0 deletions core/app/api/temp-cache-headers/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { credentialsFromFetch } from '~/lib/vdc/credentials-from-fetch';

export async function GET(request: Request) {
const data = await credentialsFromFetch(request.headers);

return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
},
});
}
65 changes: 65 additions & 0 deletions core/lib/vdc/credentials-from-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* eslint-disable no-async-promise-executor */
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-base-to-string */
/* eslint-disable @typescript-eslint/no-misused-promises */
export async function credentialsFromFetch(headers: Headers) {
return new Promise<Record<string, string>>(async (resolve, reject) => {
if (process.env.NODE_ENV !== 'production') {
resolve({});

return;
}

const host = headers.get('x-vercel-sc-host');

if (!host) {
reject(new Error('Missing x-vercel-sc-host header'));
}

const basepath = headers.get('x-vercel-sc-basepath');
const original = globalThis.fetch;
const sentinelUrl = `https://vercel.com/robots.txt?id=${Math.random()}`;

globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
if (!input.toString().startsWith(`https://${host}/`)) {
return original(input, init);
}

const h = new Headers(init?.headers);
const url = h.get('x-vercel-cache-item-name');

if (url !== sentinelUrl) {
return original(input, init);
}

console.log('h', input, url, h);

const authorization = h.get('authorization');

if (!authorization) {
reject(new Error('Missing cache authorization header'));
}

resolve({
'x-vercel-sc-headers': JSON.stringify({
authorization: h.get('authorization'),
}),
'x-vercel-sc-host': host || '',
'x-vercel-sc-basepath': basepath || '',
});
globalThis.fetch = original;

return new Response(JSON.stringify({}), {
status: 510,
});
};

try {
await fetch(sentinelUrl, {
cache: 'force-cache',
});
} catch (e) {
console.info(e);
}
});
}
99 changes: 99 additions & 0 deletions core/lib/vdc/in-memory-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable max-classes-per-file */
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/consistent-type-assertions */
/* eslint-disable @typescript-eslint/require-await */
import { CacheHandler, CacheHandlerValue, IncrementalCacheValue, Revalidate } from './types';

let instance: InMemoryCacheHandlerInternal;

interface SetContext {
revalidate?: Revalidate;
fetchCache?: boolean;
fetchUrl?: string;
fetchIdx?: number;
tags?: string[];
isRoutePPREnabled?: boolean;
isFallback?: boolean;
}

export class InMemoryCacheHandler implements CacheHandler {
constructor() {
instance = instance ?? new InMemoryCacheHandlerInternal();
}
get(key: string) {
return instance.get(key);
}
set(key: string, data: IncrementalCacheValue | null, ctx: SetContext) {
return instance.set(key, data, ctx);
}
revalidateTag(tags: string | string[]) {
return instance.revalidateTag(tags);
}
resetRequestCache() {
instance.resetRequestCache();
}
}

class InMemoryCacheHandlerInternal implements CacheHandler {
readonly cache: Map<
string,
{
value: string;
lastModified: number;
tags: string[];
revalidate: number | undefined;
}
>;

constructor() {
this.cache = new Map();
}

async get(key: string): Promise<CacheHandlerValue | null> {
const content = this.cache.get(key);

if (content?.revalidate && content.lastModified + content.revalidate * 1000 < Date.now()) {
this.cache.delete(key);

return null;
}

if (content) {
return {
value: JSON.parse(content.value) as IncrementalCacheValue | null,
lastModified: content.lastModified,
age: Date.now() - content.lastModified,
};
}

return null;
}

async set(key: string, data: IncrementalCacheValue | null, ctx: SetContext) {
// This could be stored anywhere, like durable storage
this.cache.set(key, {
value: JSON.stringify(data),
lastModified: Date.now(),
tags: ctx.tags || [],
revalidate: ctx.revalidate ? ctx.revalidate : undefined,
});
}

async revalidateTag(tag: string | string[]) {
const tags = [tag].flat();

// Iterate over all entries in the cache
for (const [key, value] of this.cache) {
// If the value's tags include the specified tag, delete this entry
if (value.tags.some((tag: string) => tags.includes(tag))) {
this.cache.delete(key);
}
}
}

resetRequestCache() {}
}
205 changes: 205 additions & 0 deletions core/lib/vdc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-confusing-void-expression */
/* eslint-disable @typescript-eslint/consistent-type-assertions */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable no-multi-assign */
/* eslint-disable no-underscore-dangle */
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { InMemoryCacheHandler } from './in-memory-cache';
import { CachedRouteKind, CacheHandler, CacheHandlerContext, IncrementalCacheKind } from './types';

export interface ComputeCache<T> {
get: (key: string) => Promise<T | undefined>;
set: (
key: string,
value: any,
options?: { tags?: string[]; revalidate?: number },
) => Promise<void>;
revalidateTag: (tag: string) => Promise<void>;
}

export async function getComputeCache<T>(
requestHeadersOverride?: Record<string, string | string[] | undefined> | Request | Headers,
): Promise<ComputeCache<T>> {
const requestHeaders = await getCacheHeaders(getHeadersRecord(requestHeadersOverride));

console.log('requestHeaders', JSON.stringify(requestHeaders));

const internalCache = new FetchCacheConstructor({
fetchCacheKeyPrefix: 'compute-cache',
revalidatedTags: [],
// Constructor deletes the headers, so we need to clone them
_requestHeaders: Object.assign({}, requestHeaders) as Record<string, string>,
maxMemoryCacheSize: 0,
});

internalCache.resetRequestCache();

return {
get: async (key: string): Promise<T | undefined> => {
const fullKey = getKey(key);
const content = await internalCache.get(fullKey, {
kind: IncrementalCacheKind.FETCH,
isFallback: false,
fetchUrl: `https://vercel.cache/${fullKey}`,
});

if (content) {
console.log('get', fullKey, content);

const lastModified = content.lastModified;
const revalidate = content.value?.revalidate;

if (revalidate && lastModified && lastModified + revalidate * 1000 < Date.now()) {
internalCache.resetRequestCache();

return;
}

const value = content.value;

if (!value) {
return;
}

return JSON.parse(value.data.body);
}
},
set: async (key: string, value: any, options?: { tags?: string[]; revalidate?: number }) => {
const fullKey = getKey(key);
const r = await internalCache.set(
fullKey,
{
kind: CachedRouteKind.FETCH,
data: {
headers: {},
body: JSON.stringify(value),
url: `https://vercel.cache/${fullKey}`,
status: 200,
},
// Magic tag for forever
revalidate: options?.revalidate ?? 0xfffffffe,
},
{
fetchCache: true,
fetchUrl: `https://vercel.cache/${fullKey}`,
revalidate: options?.revalidate,
isFallback: false,
tags: options?.tags,
},
);

console.log('set', fullKey, value, {
fetchCache: true,
fetchUrl: `https://vercel.cache/${fullKey}`,
revalidate: options?.revalidate,
isFallback: false,
tags: options?.tags,
});

// Temp hack. For some reason we cannot turn off the in-memory cache.
if (options?.revalidate) {
internalCache.resetRequestCache();
}

return r;
},
revalidateTag: (tag: string | string[]) => {
const r = internalCache.revalidateTag(tag);

internalCache.resetRequestCache();

return r;
},
} as const;
}

function getHeadersRecord(
requestHeadersOverride:
| Record<string, string | string[] | undefined>
| Request
| Headers
| undefined,
): Record<string, string | string[] | undefined> | undefined {
if (!requestHeadersOverride) {
return undefined;
}

if (requestHeadersOverride instanceof Headers) {
return Object.fromEntries([...requestHeadersOverride.entries()]);
}

if (requestHeadersOverride instanceof Request) {
return Object.fromEntries([...requestHeadersOverride.headers.entries()]);
}

return requestHeadersOverride;
}

let FetchCacheConstructor: new (ctx: CacheHandlerContext) => CacheHandler;

initializeComputeCache();

export function initializeComputeCache() {
const cacheHandlersSymbol = Symbol.for('@next/cache-handlers');
const _globalThis: typeof globalThis & {
[cacheHandlersSymbol]?: {
FetchCache?: new (ctx: CacheHandlerContext) => CacheHandler;
};
} = globalThis;
const cacheHandlers = (_globalThis[cacheHandlersSymbol] ??= {});
let { FetchCache } = cacheHandlers;

if (!FetchCache) {
FetchCache = InMemoryCacheHandler;

if (process.env.NODE_ENV === 'production') {
console.error('Cache handler not found');
}
}

FetchCacheConstructor = FetchCache;
}

let cacheHeaders: Promise<Record<string, string>> | undefined;

async function getCacheHeadersFromDeployedFunction() {
const r = await fetch(`https://${process.env.VERCEL_URL}/api/temp-cache-headers`, {
cache: 'no-store',
headers: {
'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET || '',
},
});

if (!r.ok) {
cacheHeaders = undefined;
throw new Error('Failed to fetch cache headers');
}

return r.json();
}

async function getCacheHeaders(
requestHeaders?: Record<string, string | string[] | undefined>,
): Promise<Record<string, string | string[] | undefined>> {
if (process.env.NODE_ENV !== 'production') {
return {};
}

if (cacheHeaders) {
return cacheHeaders;
}

if (requestHeaders?.['x-vercel-sc-headers']) {
return requestHeaders;
}

cacheHeaders = getCacheHeadersFromDeployedFunction();

return cacheHeaders;
}

function getKey(key: string) {
return `compute-cache/${process.env.VERCEL_ENV || 'development'}/${key}`;
}
Loading
Loading