Skip to content

Commit

Permalink
Rework context to actually share memory cache between middleware and …
Browse files Browse the repository at this point in the history
…handlers for all requests (#2358)
  • Loading branch information
jpreynat authored Jun 25, 2024
1 parent e3f5f81 commit db45c6d
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 18 deletions.
8 changes: 4 additions & 4 deletions src/lib/async.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MaybePromise } from 'p-map';

import { waitUntil, getGlobalContext } from './waitUntil';
import { waitUntil, getRequestContext } from './waitUntil';

/**
* Execute a function for each input in parallel and return the first result.
Expand Down Expand Up @@ -238,7 +238,7 @@ export function singleton<R>(execute: () => Promise<R>): () => Promise<R> {
}

// Promises are not shared between requests in Cloudflare Workers
const ctx = await getGlobalContext();
const ctx = await getRequestContext();
const current = states.get(ctx);
if (current) {
return current;
Expand Down Expand Up @@ -269,7 +269,7 @@ export function singletonMap<Key extends string, Args extends any[], Result>(
const states = new WeakMap<object, Map<string, Promise<Result>>>();

const fn: SingletonFunction<Key, Args, Result> = async (key, ...args) => {
const ctx = await getGlobalContext();
const ctx = await getRequestContext();
let current = states.get(ctx);
if (current) {
const existing = current.get(key);
Expand All @@ -292,7 +292,7 @@ export function singletonMap<Key extends string, Args extends any[], Result>(
};

fn.isRunning = async (key: string) => {
const ctx = await getGlobalContext();
const ctx = await getRequestContext();
const current = states.get(ctx);
return current?.has(key) ?? false;
};
Expand Down
24 changes: 17 additions & 7 deletions src/lib/cache/memory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CacheBackend } from './types';
import { CacheBackend, CacheEntry } from './types';
import { NON_IMMUTABLE_LOCAL_CACHE_MAX_AGE_SECONDS, isCacheEntryImmutable } from './utils';
import { singleton } from '../async';
import { getGlobalContext } from '../waitUntil';

export const memoryCache: CacheBackend = {
name: 'memory',
Expand Down Expand Up @@ -64,9 +64,19 @@ export const memoryCache: CacheBackend = {
};

/**
* With next-on-pages, the code seems to be isolated between the middleware and the handler.
* To share the cache between the two, we use a global variable.
* By using a singleton, we ensure that the cache is only created once and stored in the
* current request context.
* In memory cache shared globally.
*/
const getMemoryCache = singleton(async () => new Map());
async function getMemoryCache(): Promise<Map<string, CacheEntry>> {
const ctx: Awaited<ReturnType<typeof getGlobalContext>> & {
gitbookMemoryCache?: Map<string, CacheEntry>;
} = await getGlobalContext();

if (ctx.gitbookMemoryCache) {
return ctx.gitbookMemoryCache;
}

const gitbookMemoryCache = new Map<string, CacheEntry>();
ctx.gitbookMemoryCache = gitbookMemoryCache;

return gitbookMemoryCache;
}
29 changes: 22 additions & 7 deletions src/lib/waitUntil.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';

let pendings: Array<Promise<unknown>> = [];

/**
* Get a global context object for the current execution.
* This object can be used to store data that should be shared between the middleware and the handler
* and re-used for all requests.
*/
export async function getGlobalContext(): Promise<ExecutionContext | object> {
if (process.env.NODE_ENV === 'test') {
// Do not try loading the next-on-pages package in tests as it'll fail
return globalThis;
}

// We lazy-load the next-on-pages package to avoid errors when running tests because of 'server-only'.
const { getOptionalRequestContext } = await import('@cloudflare/next-on-pages');
return getOptionalRequestContext()?.ctx ?? globalThis;
}

/**
* Get a global context object for the current request.
* This object can be used as a key to store request-specific data in a WeakMap.
*/
export async function getGlobalContext(): Promise<object> {
export async function getRequestContext(): Promise<IncomingRequestCfProperties | object> {
if (process.env.NODE_ENV === 'test') {
// Do not try loading the next-on-pages package in tests as it'll fail
return globalThis;
Expand All @@ -29,12 +47,9 @@ export async function waitUntil(promise: Promise<unknown>) {
return;
}

// We lazy-load the next-on-pages package to avoid errors when running tests because of 'server-only'.
const { getOptionalRequestContext } = await import('@cloudflare/next-on-pages');

const cloudflare = getOptionalRequestContext();
if (cloudflare) {
cloudflare.ctx.waitUntil(promise);
const cloudflareContext = await getGlobalContext();
if ('waitUntil' in cloudflareContext) {
cloudflareContext.waitUntil(promise);
} else {
await promise;
}
Expand Down

0 comments on commit db45c6d

Please sign in to comment.