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

Feat add handler code #3

Merged
merged 10 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
974 changes: 946 additions & 28 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@trieb.work/nextjs-turbo-redis-cache",
"version": "1.0.0",
"scripts": {
"dev": "vitest --watch",
"dev": "pnpm test",
"build": "tsc",
"lint": "eslint -c eslint.config.mjs --fix",
"fmt": "prettier --write 'src/**/*.ts' 'src/*.ts'",
Expand Down Expand Up @@ -32,10 +32,10 @@
"devDependencies": {
"@commitlint/cli": "^19.6.0",
"@commitlint/config-conventional": "^19.6.0",
"@semantic-release/github": "^11.0.1",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.1",
"@semantic-release/npm": "^12.0.1",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^2.1.5",
Expand All @@ -47,5 +47,8 @@
"typescript": "^5.6.3",
"vitest": "^2.1.5"
},
"dependencies": {}
"dependencies": {
"next": "^15.0.3",
"redis": "^4.7.0"
}
}
25 changes: 25 additions & 0 deletions src/CachedHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CacheHandler } from "next/dist/server/lib/incremental-cache";
import RedisStringsHandler, { CreateRedisStringsHandlerOptions } from "./RedisStringsHandler";

let cachedHandler: RedisStringsHandler;

export default class CachedHandler implements CacheHandler {
constructor(options: CreateRedisStringsHandlerOptions) {
if (!cachedHandler) {
console.log("created cached handler");
cachedHandler = new RedisStringsHandler(options);
}
}
get(...args: Parameters<RedisStringsHandler["get"]>): ReturnType<RedisStringsHandler["get"]> {
return cachedHandler.get(...args);
}
set(...args: Parameters<RedisStringsHandler["set"]>): ReturnType<RedisStringsHandler["set"]> {
return cachedHandler.set(...args);
}
revalidateTag(...args: Parameters<RedisStringsHandler["revalidateTag"]>): ReturnType<RedisStringsHandler["revalidateTag"]> {
return cachedHandler.revalidateTag(...args);
}
resetRequestCache(...args: Parameters<RedisStringsHandler["resetRequestCache"]>): ReturnType<RedisStringsHandler["resetRequestCache"]> {
return cachedHandler.resetRequestCache(...args);
}
}
68 changes: 68 additions & 0 deletions src/DeduplicatedRequestHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
let counter = 0;

export class DeduplicatedRequestHandler<
T extends (...args: [never, never]) => Promise<K>,
K,
> {
private inMemoryDeduplicationCache = new Map<string, Promise<K>>([]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache entries which are set in in-memory request will not get deleted by revalidateTag across all nodes (only on local instance). Add keyspace notification to make sure this will get deleted as well.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This problem does not exist if inMemoryCaching is set to 0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If inMemoryCaching is enabled. It will degrade caching consistency from strong to eventual consistency. Add this to the README

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if redis will always serve a get after a set.

private cachingTimeMs: number;
private fn: T;

constructor(fn: T, cachingTimeMs: number) {
this.fn = fn;
this.cachingTimeMs = cachingTimeMs;
console.log('this 1', this);
}

// Getter to access the cache externally
public get cache(): Map<string, Promise<K>> {
return this.inMemoryDeduplicationCache;
}

// Method to manually seed a result into the cache
seedRequestReturn(key: string, value: K): void {
const resultPromise = new Promise<K>((res) => res(value));
this.inMemoryDeduplicationCache.set(key, resultPromise);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now we have two problems:

  • everything seeded by seedRequestReturn will not get deleted after cachingTimeMs. Add the same timeout as in deduplicated function:
        setTimeout(() => {
          self.inMemoryDeduplicationCache.delete(key);
        }, self.cachingTimeMs);

}

// Method to handle deduplicated requests
deduplicatedFunction = (key: string): T => {
//eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const dedupedFn = async (...args: [never, never]): Promise<K> => {
const cnt = `${key}_${counter++}`;

// If there's already a pending request with the same key, return it
if (
self.inMemoryDeduplicationCache &&
self.inMemoryDeduplicationCache.has(key)
) {
console.log(`redis get in-mem ${cnt} started`);
console.time(`redis get in-mem ${cnt}`);
const res = await self.inMemoryDeduplicationCache
.get(key)!
.then((v) => structuredClone(v));
console.timeEnd(`redis get in-mem ${cnt}`);
return res;
}

// If no pending request, call the original function and store the promise
const promise = self.fn(...args);
self.inMemoryDeduplicationCache.set(key, promise);

try {
console.log(`redis get origin ${cnt} started`);
console.time(`redis get origin ${cnt}`);
const result = await promise;
console.timeEnd(`redis get origin ${cnt}`);
return structuredClone(result);
} finally {
// Once the promise is resolved/rejected, remove it from the map
setTimeout(() => {
self.inMemoryDeduplicationCache.delete(key);
}, self.cachingTimeMs);
}
};
return dedupedFn as T;
};
}
Loading
Loading