From 8edcc6f4ca0a08b5f6aefc2dfb711cb2b3407b0d Mon Sep 17 00:00:00 2001 From: Harry Talbot Date: Wed, 27 Aug 2025 13:16:53 +0100 Subject: [PATCH 1/5] chore: copy from neshca --- .../nextjs-cache-handler/src/constants.ts | 2 + .../src/functions/functions.ts | 1 + .../src/functions/nesh-classic-cache.ts | 349 ++++++++++++++++++ 3 files changed, 352 insertions(+) create mode 100644 packages/nextjs-cache-handler/src/functions/functions.ts create mode 100644 packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts diff --git a/packages/nextjs-cache-handler/src/constants.ts b/packages/nextjs-cache-handler/src/constants.ts index 35f5218..2241016 100644 --- a/packages/nextjs-cache-handler/src/constants.ts +++ b/packages/nextjs-cache-handler/src/constants.ts @@ -1 +1,3 @@ +export const TIME_ONE_YEAR = 31536000; + export const REVALIDATED_TAGS_KEY = "__revalidated_tags__"; diff --git a/packages/nextjs-cache-handler/src/functions/functions.ts b/packages/nextjs-cache-handler/src/functions/functions.ts new file mode 100644 index 0000000..4c0c400 --- /dev/null +++ b/packages/nextjs-cache-handler/src/functions/functions.ts @@ -0,0 +1 @@ +export { neshClassicCache } from './nesh-classic-cache'; \ No newline at end of file diff --git a/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts b/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts new file mode 100644 index 0000000..84f3ca1 --- /dev/null +++ b/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts @@ -0,0 +1,349 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import type { Revalidate } from '@repo/next-common'; +import { staticGenerationAsyncStorage } from 'next/dist/client/components/static-generation-async-storage.external.js'; +import type { IncrementalCache } from 'next/dist/server/lib/incremental-cache'; +import type { CacheHandler } from '../cache-handler'; +import { TIME_ONE_YEAR } from '../constants'; + +declare global { + var __incrementalCache: IncrementalCache | undefined; +} + +function hashCacheKey(url: string): string { + // this should be bumped anytime a fix is made to cache entries + // that should bust the cache + const MAIN_KEY_PREFIX = 'nesh-pages-cache-v1'; + + const cacheString = JSON.stringify([MAIN_KEY_PREFIX, url]); + + return createHash('sha256').update(cacheString).digest('hex'); +} + +/** + * Serializes the given arguments into a string representation. + * + * @param object - The arguments to be serialized. + * + * @returns The serialized string representation of the arguments. + */ +function serializeArguments(object: object): string { + return JSON.stringify(object); +} + +/** + * Serializes the given object into a string representation. + * + * @param object - The object to be serialized. + * + * @returns The serialized string representation of the object. + */ +function serializeResult(object: object): string { + return Buffer.from(JSON.stringify(object), 'utf-8').toString('base64'); +} + +/** + * Deserializes a string representation of an object into its original form. + * + * @param string - The string representation of the object. + * + * @returns The deserialized object. + */ +function deserializeResult(string: string): T { + return JSON.parse(Buffer.from(string, 'base64').toString('utf-8')); +} + +/** + * @template Arguments - The type of the arguments passed to the callback function. + * + * @template Result - The type of the value returned by the callback function. + */ +type Callback = ( + ...args: Arguments +) => Result; + +/** + * An object containing options for the cache. + */ +type NeshClassicCacheOptions = { + /** + * The response context object. + * It is used to set the cache headers. + */ + responseContext?: object & { + setHeader( + name: string, + value: number | string | readonly string[], + ): unknown; + }; + /** + * An array of tags to associate with the cached result. + * Tags are used to revalidate the cache using the `revalidateTag` function. + */ + tags?: string[]; + /** + * The revalidation interval in seconds. + * Must be a positive integer or `false` to disable revalidation. + * + * @default revalidate // of the current route + */ + revalidate?: Revalidate; + /** + * A custom cache key to be used instead of creating one from the arguments. + */ + cacheKey?: string; + /** + * A function that serializes the arguments passed to the callback function. + * Use it to create a cache key. + * + * @default (args) => JSON.stringify(args) + * + * @param callbackArguments - The arguments passed to the callback function. + */ + argumentsSerializer?(callbackArguments: Arguments): string; + /** + * + * A function that serializes the result of the callback function. + * + * @default (result) => Buffer.from(JSON.stringify(result)).toString('base64') + * + * @param result - The result of the callback function. + */ + resultSerializer?(result: Result): string; + /** + * A function that deserializes the string representation of the result of the callback function. + * + * @default (string) => JSON.parse(Buffer.from(string, 'base64').toString('utf-8')) + * + * @param string - The string representation of the result of the callback function. + */ + resultDeserializer?(string: string): Result; +}; + +/** + * An object containing common options for the cache. + */ +type CommonNeshClassicCacheOptions = Omit< + NeshClassicCacheOptions, + 'cacheKey' | 'responseContext' +>; + +/** + * Experimental implementation of the "`unstable_cache`" for classic Next.js Pages Router. + * It allows to cache data in the `getServerSideProps` and API routes. + * + * The API may change in the future. Use with caution. + * + * Caches the result of a callback function and returns a cached version if available. + * If not available, it executes the callback function, caches the result, and returns it. + * + * @param callback - The callback function to be cached. + * + * @param options - An object containing options for the cache. + * + * @param options.responseContext - The response context object. + * It is used to set the cache headers. + * + * @param options.tags - An array of tags to associate with the cached result. + * Tags are used to revalidate the cache using the `revalidateTag` function. + * + * @param options.revalidate - The revalidation interval in seconds. + * Must be a positive integer or `false` to disable revalidation. + * Defaults to `export const revalidate = time;` in the current route. + * + * @param options.argumentsSerializer - A function that serializes the arguments passed to the callback function. + * Use it to create a cache key. Defaults to `JSON.stringify(args)`. + * + * @param options.resultSerializer - A function that serializes the result of the callback function. + * Defaults to `Buffer.from(JSON.stringify(data)).toString('base64')`. + * + * @param options.resultDeserializer - A function that deserializes the string representation of the result of the callback function. + * Defaults to `JSON.parse(Buffer.from(data, 'base64').toString('utf-8'))`. + * + * @returns The callback wrapped in a caching function. + * First argument is the cache options which can be used to override the common options. + * In addition, there is a `cacheKey` option that can be used to provide a custom cache key. + * + * @throws If the `neshClassicCache` function is not used in a Next.js Pages directory or if the `revalidate` option is invalid. + * + * @example file: `src/pages/api/api-example.js` + * + * ```js + * import { neshClassicCache } from '@neshca/cache-handler/functions'; + * import axios from 'axios'; + * + * export const config = { + * runtime: 'nodejs', + * }; + * + * async function getViaAxios(url) { + * try { + * return (await axios.get(url.href)).data; + * } catch (_error) { + * return null; + * } + * } + * + * const cachedAxios = neshClassicCache(getViaAxios); + * + * export default async function handler(request, response) { + * if (request.method !== 'GET') { + * return response.status(405).send(null); + * } + * + * const revalidate = 5; + * + * const url = new URL('https://api.example.com/data.json'); + * + * // Add tags to be able to revalidate the cache + * const data = await cachedAxios({ revalidate, tags: [url.pathname], responseContext: response }, url); + * + * if (!data) { + * response.status(404).send('Not found'); + * + * return; + * } + * + * response.json(data); + * } + * ``` + * + * @remarks + * - This function is intended to be used in a Next.js Pages directory. + * + * @since 1.8.0 + */ +export function neshClassicCache< + Arguments extends unknown[], + Result extends Promise, +>( + callback: Callback, + commonOptions?: CommonNeshClassicCacheOptions, +) { + if (commonOptions?.resultSerializer && !commonOptions?.resultDeserializer) { + throw new Error( + 'neshClassicCache: if you provide a resultSerializer, you must provide a resultDeserializer.', + ); + } + + if (commonOptions?.resultDeserializer && !commonOptions?.resultSerializer) { + throw new Error( + 'neshClassicCache: if you provide a resultDeserializer, you must provide a resultSerializer.', + ); + } + + const commonRevalidate = commonOptions?.revalidate ?? false; + const commonArgumentsSerializer = + commonOptions?.argumentsSerializer ?? serializeArguments; + const commonResultSerializer = + commonOptions?.resultSerializer ?? serializeResult; + const commonResultDeserializer = + commonOptions?.resultDeserializer ?? deserializeResult; + + async function cachedCallback( + options: NeshClassicCacheOptions, + ...args: Arguments + ): Promise { + const store = staticGenerationAsyncStorage.getStore(); + + assert( + !store?.incrementalCache, + 'neshClassicCache must be used in a Next.js Pages directory.', + ); + + const cacheHandler = globalThis?.__incrementalCache?.cacheHandler as + | InstanceType + | undefined; + + assert( + cacheHandler, + 'neshClassicCache must be used in a Next.js Pages directory.', + ); + + const { + responseContext, + tags = [], + revalidate = commonRevalidate, + cacheKey, + argumentsSerializer = commonArgumentsSerializer, + resultDeserializer = commonResultDeserializer, + resultSerializer = commonResultSerializer, + } = options ?? {}; + + assert( + revalidate === false || (revalidate > 0 && Number.isInteger(revalidate)), + 'neshClassicCache: revalidate must be a positive integer or false.', + ); + + responseContext?.setHeader( + 'Cache-Control', + `public, s-maxage=${revalidate}, stale-while-revalidate`, + ); + + const uniqueTags = new Set(); + + for (const tag of tags) { + if (typeof tag === 'string') { + uniqueTags.add(tag); + } else { + console.warn( + `neshClassicCache: Invalid tag: ${tag}. Skipping it. Expected a string.`, + ); + } + } + + const allTags = Array.from(uniqueTags); + + const key = hashCacheKey( + `nesh-classic-cache-${cacheKey ?? argumentsSerializer(args)}`, + ); + + const cacheData = await cacheHandler.get(key, { + revalidate, + tags: allTags, + kindHint: 'fetch', + fetchUrl: 'neshClassicCache', + }); + + if ( + cacheData?.value?.kind === 'FETCH' && + cacheData.lifespan && + cacheData.lifespan.staleAt > Date.now() / 1000 + ) { + return resultDeserializer(cacheData.value.data.body); + } + + const data: Result = await callback(...args); + + cacheHandler.set( + key, + { + kind: 'FETCH', + data: { + body: resultSerializer(data), + headers: {}, + url: 'neshClassicCache', + }, + revalidate: revalidate || TIME_ONE_YEAR, + }, + { + revalidate, + tags, + fetchCache: true, + fetchUrl: 'neshClassicCache', + }, + ); + + if ( + cacheData?.value?.kind === 'FETCH' && + cacheData?.lifespan && + cacheData.lifespan.expireAt > Date.now() / 1000 + ) { + return resultDeserializer(cacheData.value.data.body); + } + + return data; + } + + return cachedCallback; +} \ No newline at end of file From 5f8b62e838bca3e8c9e6c80bbaa9577a3cb73b06 Mon Sep 17 00:00:00 2001 From: Harry Talbot Date: Wed, 27 Aug 2025 13:18:52 +0100 Subject: [PATCH 2/5] refactor: use workAsyncStorage --- .../nextjs-cache-handler/src/functions/nesh-classic-cache.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts b/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts index 84f3ca1..ab1d0b5 100644 --- a/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts +++ b/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import type { Revalidate } from '@repo/next-common'; -import { staticGenerationAsyncStorage } from 'next/dist/client/components/static-generation-async-storage.external.js'; +import { workAsyncStorage } from 'next/dist/server/app-render/work-async-storage.external.js'; import type { IncrementalCache } from 'next/dist/server/lib/incremental-cache'; import type { CacheHandler } from '../cache-handler'; import { TIME_ONE_YEAR } from '../constants'; @@ -244,7 +244,7 @@ export function neshClassicCache< options: NeshClassicCacheOptions, ...args: Arguments ): Promise { - const store = staticGenerationAsyncStorage.getStore(); + const store = workAsyncStorage.getStore(); assert( !store?.incrementalCache, From a1a146015df061e63d0c212f769d2a086f1a5e22 Mon Sep 17 00:00:00 2001 From: Harry Talbot Date: Wed, 27 Aug 2025 13:25:56 +0100 Subject: [PATCH 3/5] style: format to match code style --- .../src/functions/functions.ts | 2 +- .../src/functions/nesh-classic-cache.ts | 364 +++++++++--------- 2 files changed, 183 insertions(+), 183 deletions(-) diff --git a/packages/nextjs-cache-handler/src/functions/functions.ts b/packages/nextjs-cache-handler/src/functions/functions.ts index 4c0c400..84a5d9d 100644 --- a/packages/nextjs-cache-handler/src/functions/functions.ts +++ b/packages/nextjs-cache-handler/src/functions/functions.ts @@ -1 +1 @@ -export { neshClassicCache } from './nesh-classic-cache'; \ No newline at end of file +export { neshClassicCache } from "./nesh-classic-cache"; diff --git a/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts b/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts index ab1d0b5..ce803d9 100644 --- a/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts +++ b/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts @@ -1,23 +1,23 @@ -import assert from 'node:assert/strict'; -import { createHash } from 'node:crypto'; -import type { Revalidate } from '@repo/next-common'; -import { workAsyncStorage } from 'next/dist/server/app-render/work-async-storage.external.js'; -import type { IncrementalCache } from 'next/dist/server/lib/incremental-cache'; -import type { CacheHandler } from '../cache-handler'; -import { TIME_ONE_YEAR } from '../constants'; +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import type { Revalidate } from "@repo/next-common"; +import { workAsyncStorage } from "next/dist/server/app-render/work-async-storage.external.js"; +import type { IncrementalCache } from "next/dist/server/lib/incremental-cache"; +import type { CacheHandler } from "../cache-handler"; +import { TIME_ONE_YEAR } from "../constants"; declare global { - var __incrementalCache: IncrementalCache | undefined; + var __incrementalCache: IncrementalCache | undefined; } function hashCacheKey(url: string): string { - // this should be bumped anytime a fix is made to cache entries - // that should bust the cache - const MAIN_KEY_PREFIX = 'nesh-pages-cache-v1'; + // this should be bumped anytime a fix is made to cache entries + // that should bust the cache + const MAIN_KEY_PREFIX = "nesh-pages-cache-v1"; - const cacheString = JSON.stringify([MAIN_KEY_PREFIX, url]); + const cacheString = JSON.stringify([MAIN_KEY_PREFIX, url]); - return createHash('sha256').update(cacheString).digest('hex'); + return createHash("sha256").update(cacheString).digest("hex"); } /** @@ -28,7 +28,7 @@ function hashCacheKey(url: string): string { * @returns The serialized string representation of the arguments. */ function serializeArguments(object: object): string { - return JSON.stringify(object); + return JSON.stringify(object); } /** @@ -39,7 +39,7 @@ function serializeArguments(object: object): string { * @returns The serialized string representation of the object. */ function serializeResult(object: object): string { - return Buffer.from(JSON.stringify(object), 'utf-8').toString('base64'); + return Buffer.from(JSON.stringify(object), "utf-8").toString("base64"); } /** @@ -50,7 +50,7 @@ function serializeResult(object: object): string { * @returns The deserialized object. */ function deserializeResult(string: string): T { - return JSON.parse(Buffer.from(string, 'base64').toString('utf-8')); + return JSON.parse(Buffer.from(string, "base64").toString("utf-8")); } /** @@ -59,73 +59,73 @@ function deserializeResult(string: string): T { * @template Result - The type of the value returned by the callback function. */ type Callback = ( - ...args: Arguments + ...args: Arguments ) => Result; /** * An object containing options for the cache. */ type NeshClassicCacheOptions = { - /** - * The response context object. - * It is used to set the cache headers. - */ - responseContext?: object & { - setHeader( - name: string, - value: number | string | readonly string[], - ): unknown; - }; - /** - * An array of tags to associate with the cached result. - * Tags are used to revalidate the cache using the `revalidateTag` function. - */ - tags?: string[]; - /** - * The revalidation interval in seconds. - * Must be a positive integer or `false` to disable revalidation. - * - * @default revalidate // of the current route - */ - revalidate?: Revalidate; - /** - * A custom cache key to be used instead of creating one from the arguments. - */ - cacheKey?: string; - /** - * A function that serializes the arguments passed to the callback function. - * Use it to create a cache key. - * - * @default (args) => JSON.stringify(args) - * - * @param callbackArguments - The arguments passed to the callback function. - */ - argumentsSerializer?(callbackArguments: Arguments): string; - /** - * - * A function that serializes the result of the callback function. - * - * @default (result) => Buffer.from(JSON.stringify(result)).toString('base64') - * - * @param result - The result of the callback function. - */ - resultSerializer?(result: Result): string; - /** - * A function that deserializes the string representation of the result of the callback function. - * - * @default (string) => JSON.parse(Buffer.from(string, 'base64').toString('utf-8')) - * - * @param string - The string representation of the result of the callback function. - */ - resultDeserializer?(string: string): Result; + /** + * The response context object. + * It is used to set the cache headers. + */ + responseContext?: object & { + setHeader( + name: string, + value: number | string | readonly string[], + ): unknown; + }; + /** + * An array of tags to associate with the cached result. + * Tags are used to revalidate the cache using the `revalidateTag` function. + */ + tags?: string[]; + /** + * The revalidation interval in seconds. + * Must be a positive integer or `false` to disable revalidation. + * + * @default revalidate // of the current route + */ + revalidate?: Revalidate; + /** + * A custom cache key to be used instead of creating one from the arguments. + */ + cacheKey?: string; + /** + * A function that serializes the arguments passed to the callback function. + * Use it to create a cache key. + * + * @default (args) => JSON.stringify(args) + * + * @param callbackArguments - The arguments passed to the callback function. + */ + argumentsSerializer?(callbackArguments: Arguments): string; + /** + * + * A function that serializes the result of the callback function. + * + * @default (result) => Buffer.from(JSON.stringify(result)).toString('base64') + * + * @param result - The result of the callback function. + */ + resultSerializer?(result: Result): string; + /** + * A function that deserializes the string representation of the result of the callback function. + * + * @default (string) => JSON.parse(Buffer.from(string, 'base64').toString('utf-8')) + * + * @param string - The string representation of the result of the callback function. + */ + resultDeserializer?(string: string): Result; }; /** * An object containing common options for the cache. */ type CommonNeshClassicCacheOptions = Omit< - NeshClassicCacheOptions, - 'cacheKey' | 'responseContext' + NeshClassicCacheOptions, + "cacheKey" | "responseContext" >; /** @@ -214,136 +214,136 @@ type CommonNeshClassicCacheOptions = Omit< * @since 1.8.0 */ export function neshClassicCache< - Arguments extends unknown[], - Result extends Promise, + Arguments extends unknown[], + Result extends Promise, >( - callback: Callback, - commonOptions?: CommonNeshClassicCacheOptions, + callback: Callback, + commonOptions?: CommonNeshClassicCacheOptions, ) { - if (commonOptions?.resultSerializer && !commonOptions?.resultDeserializer) { - throw new Error( - 'neshClassicCache: if you provide a resultSerializer, you must provide a resultDeserializer.', - ); - } + if (commonOptions?.resultSerializer && !commonOptions?.resultDeserializer) { + throw new Error( + "neshClassicCache: if you provide a resultSerializer, you must provide a resultDeserializer.", + ); + } - if (commonOptions?.resultDeserializer && !commonOptions?.resultSerializer) { - throw new Error( - 'neshClassicCache: if you provide a resultDeserializer, you must provide a resultSerializer.', - ); - } + if (commonOptions?.resultDeserializer && !commonOptions?.resultSerializer) { + throw new Error( + "neshClassicCache: if you provide a resultDeserializer, you must provide a resultSerializer.", + ); + } - const commonRevalidate = commonOptions?.revalidate ?? false; - const commonArgumentsSerializer = - commonOptions?.argumentsSerializer ?? serializeArguments; - const commonResultSerializer = - commonOptions?.resultSerializer ?? serializeResult; - const commonResultDeserializer = - commonOptions?.resultDeserializer ?? deserializeResult; + const commonRevalidate = commonOptions?.revalidate ?? false; + const commonArgumentsSerializer = + commonOptions?.argumentsSerializer ?? serializeArguments; + const commonResultSerializer = + commonOptions?.resultSerializer ?? serializeResult; + const commonResultDeserializer = + commonOptions?.resultDeserializer ?? deserializeResult; - async function cachedCallback( - options: NeshClassicCacheOptions, - ...args: Arguments - ): Promise { - const store = workAsyncStorage.getStore(); - - assert( - !store?.incrementalCache, - 'neshClassicCache must be used in a Next.js Pages directory.', - ); - - const cacheHandler = globalThis?.__incrementalCache?.cacheHandler as - | InstanceType - | undefined; + async function cachedCallback( + options: NeshClassicCacheOptions, + ...args: Arguments + ): Promise { + const store = workAsyncStorage.getStore(); - assert( - cacheHandler, - 'neshClassicCache must be used in a Next.js Pages directory.', - ); + assert( + !store?.incrementalCache, + "neshClassicCache must be used in a Next.js Pages directory.", + ); - const { - responseContext, - tags = [], - revalidate = commonRevalidate, - cacheKey, - argumentsSerializer = commonArgumentsSerializer, - resultDeserializer = commonResultDeserializer, - resultSerializer = commonResultSerializer, - } = options ?? {}; + const cacheHandler = globalThis?.__incrementalCache?.cacheHandler as + | InstanceType + | undefined; - assert( - revalidate === false || (revalidate > 0 && Number.isInteger(revalidate)), - 'neshClassicCache: revalidate must be a positive integer or false.', - ); + assert( + cacheHandler, + "neshClassicCache must be used in a Next.js Pages directory.", + ); - responseContext?.setHeader( - 'Cache-Control', - `public, s-maxage=${revalidate}, stale-while-revalidate`, - ); + const { + responseContext, + tags = [], + revalidate = commonRevalidate, + cacheKey, + argumentsSerializer = commonArgumentsSerializer, + resultDeserializer = commonResultDeserializer, + resultSerializer = commonResultSerializer, + } = options ?? {}; - const uniqueTags = new Set(); + assert( + revalidate === false || (revalidate > 0 && Number.isInteger(revalidate)), + "neshClassicCache: revalidate must be a positive integer or false.", + ); - for (const tag of tags) { - if (typeof tag === 'string') { - uniqueTags.add(tag); - } else { - console.warn( - `neshClassicCache: Invalid tag: ${tag}. Skipping it. Expected a string.`, - ); - } - } + responseContext?.setHeader( + "Cache-Control", + `public, s-maxage=${revalidate}, stale-while-revalidate`, + ); - const allTags = Array.from(uniqueTags); + const uniqueTags = new Set(); - const key = hashCacheKey( - `nesh-classic-cache-${cacheKey ?? argumentsSerializer(args)}`, + for (const tag of tags) { + if (typeof tag === "string") { + uniqueTags.add(tag); + } else { + console.warn( + `neshClassicCache: Invalid tag: ${tag}. Skipping it. Expected a string.`, ); + } + } - const cacheData = await cacheHandler.get(key, { - revalidate, - tags: allTags, - kindHint: 'fetch', - fetchUrl: 'neshClassicCache', - }); + const allTags = Array.from(uniqueTags); - if ( - cacheData?.value?.kind === 'FETCH' && - cacheData.lifespan && - cacheData.lifespan.staleAt > Date.now() / 1000 - ) { - return resultDeserializer(cacheData.value.data.body); - } + const key = hashCacheKey( + `nesh-classic-cache-${cacheKey ?? argumentsSerializer(args)}`, + ); - const data: Result = await callback(...args); + const cacheData = await cacheHandler.get(key, { + revalidate, + tags: allTags, + kindHint: "fetch", + fetchUrl: "neshClassicCache", + }); - cacheHandler.set( - key, - { - kind: 'FETCH', - data: { - body: resultSerializer(data), - headers: {}, - url: 'neshClassicCache', - }, - revalidate: revalidate || TIME_ONE_YEAR, - }, - { - revalidate, - tags, - fetchCache: true, - fetchUrl: 'neshClassicCache', - }, - ); + if ( + cacheData?.value?.kind === "FETCH" && + cacheData.lifespan && + cacheData.lifespan.staleAt > Date.now() / 1000 + ) { + return resultDeserializer(cacheData.value.data.body); + } + + const data: Result = await callback(...args); - if ( - cacheData?.value?.kind === 'FETCH' && - cacheData?.lifespan && - cacheData.lifespan.expireAt > Date.now() / 1000 - ) { - return resultDeserializer(cacheData.value.data.body); - } + cacheHandler.set( + key, + { + kind: "FETCH", + data: { + body: resultSerializer(data), + headers: {}, + url: "neshClassicCache", + }, + revalidate: revalidate || TIME_ONE_YEAR, + }, + { + revalidate, + tags, + fetchCache: true, + fetchUrl: "neshClassicCache", + }, + ); - return data; + if ( + cacheData?.value?.kind === "FETCH" && + cacheData?.lifespan && + cacheData.lifespan.expireAt > Date.now() / 1000 + ) { + return resultDeserializer(cacheData.value.data.body); } - return cachedCallback; -} \ No newline at end of file + return data; + } + + return cachedCallback; +} From 83575ae4e623d9fda09058007132c7094d129dee Mon Sep 17 00:00:00 2001 From: Harry Talbot Date: Wed, 27 Aug 2025 13:29:19 +0100 Subject: [PATCH 4/5] build: update tsup + publish config --- packages/nextjs-cache-handler/package.json | 7 +++++++ packages/nextjs-cache-handler/tsup.config.ts | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/nextjs-cache-handler/package.json b/packages/nextjs-cache-handler/package.json index ac95e9a..45945aa 100644 --- a/packages/nextjs-cache-handler/package.json +++ b/packages/nextjs-cache-handler/package.json @@ -31,6 +31,10 @@ "require": "./dist/handlers/*.cjs", "import": "./dist/handlers/*.js" }, + "./functions": { + "require": "./dist/functions/functions.cjs", + "import": "./dist/functions/functions.js" + }, "./instrumentation": { "require": "./dist/instrumentation/instrumentation.cjs", "import": "./dist/instrumentation/instrumentation.js" @@ -41,6 +45,9 @@ "*": [ "dist/handlers/cache-handler.d.ts" ], + "functions": [ + "dist/functions/functions.d.ts" + ], "instrumentation": [ "dist/instrumentation/instrumentation.d.ts" ], diff --git a/packages/nextjs-cache-handler/tsup.config.ts b/packages/nextjs-cache-handler/tsup.config.ts index c7e83ee..ea0009f 100644 --- a/packages/nextjs-cache-handler/tsup.config.ts +++ b/packages/nextjs-cache-handler/tsup.config.ts @@ -2,7 +2,11 @@ import { defineConfig } from "tsup"; export const tsup = defineConfig({ name: "Build cache-handler", - entry: ["src/handlers/*.ts", "src/instrumentation/*.ts"], + entry: [ + "src/handlers/*.ts", + "src/functions/*.ts", + "src/instrumentation/*.ts", + ], splitting: false, outDir: "dist", clean: false, From 508f83c367df2b48a1baad3c0c86f97911d3ebee Mon Sep 17 00:00:00 2001 From: Harry Talbot Date: Wed, 27 Aug 2025 13:48:34 +0100 Subject: [PATCH 5/5] fix: imports --- packages/nextjs-cache-handler/src/constants.ts | 2 -- .../src/functions/nesh-classic-cache.ts | 13 +++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/nextjs-cache-handler/src/constants.ts b/packages/nextjs-cache-handler/src/constants.ts index 2241016..35f5218 100644 --- a/packages/nextjs-cache-handler/src/constants.ts +++ b/packages/nextjs-cache-handler/src/constants.ts @@ -1,3 +1 @@ -export const TIME_ONE_YEAR = 31536000; - export const REVALIDATED_TAGS_KEY = "__revalidated_tags__"; diff --git a/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts b/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts index ce803d9..9f88c97 100644 --- a/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts +++ b/packages/nextjs-cache-handler/src/functions/nesh-classic-cache.ts @@ -1,10 +1,11 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import type { Revalidate } from "@repo/next-common"; +import { Revalidate } from "../handlers/cache-handler.types"; import { workAsyncStorage } from "next/dist/server/app-render/work-async-storage.external.js"; import type { IncrementalCache } from "next/dist/server/lib/incremental-cache"; -import type { CacheHandler } from "../cache-handler"; -import { TIME_ONE_YEAR } from "../constants"; +import { CacheHandler } from "../handlers/cache-handler"; +import { CACHE_ONE_YEAR } from "next/dist/lib/constants"; +import { CachedRouteKind } from "next/dist/server/response-cache"; declare global { var __incrementalCache: IncrementalCache | undefined; @@ -301,7 +302,7 @@ export function neshClassicCache< const cacheData = await cacheHandler.get(key, { revalidate, tags: allTags, - kindHint: "fetch", + kind: "FETCH" as unknown as any, fetchUrl: "neshClassicCache", }); @@ -318,13 +319,13 @@ export function neshClassicCache< cacheHandler.set( key, { - kind: "FETCH", + kind: "FETCH" as CachedRouteKind.FETCH, data: { body: resultSerializer(data), headers: {}, url: "neshClassicCache", }, - revalidate: revalidate || TIME_ONE_YEAR, + revalidate: revalidate || CACHE_ONE_YEAR, }, { revalidate,