perf(auth): cache auth session lookups by token hash (#2225)

This commit is contained in:
Hampus
2026-08-31 01:04:51 +02:00
committed by GitHub
parent 3594cbd5ca
commit 8dc2bad843
4 changed files with 246 additions and 16 deletions
+9 -3
View File
@@ -10,6 +10,8 @@ interface CacheMSetEntry<T> {
export type CacheLookupResult<T> = {hit: true; value: T} | {hit: false};
type CacheTtlSeconds<T> = number | ((value: T) => number);
export abstract class ICacheService {
private readonly inflightValues = new Map<string, Promise<unknown>>();
@@ -56,7 +58,7 @@ export abstract class ICacheService {
return entry.hit ? entry.value : null;
}
async getOrSet<T>(key: string, valueFactory: () => Promise<T>, ttlSeconds?: number): Promise<T> {
async getOrSet<T>(key: string, valueFactory: () => Promise<T>, ttlSeconds?: CacheTtlSeconds<T>): Promise<T> {
const existing = await this.getEntry<T>(key);
if (existing.hit) {
return existing.value;
@@ -75,9 +77,13 @@ export abstract class ICacheService {
return await pending;
}
private async produceAndStore<T>(key: string, valueFactory: () => Promise<T>, ttlSeconds?: number): Promise<T> {
private async produceAndStore<T>(
key: string,
valueFactory: () => Promise<T>,
ttlSeconds?: CacheTtlSeconds<T>,
): Promise<T> {
const value = await valueFactory();
await this.set(key, value, ttlSeconds);
await this.set(key, value, typeof ttlSeconds === 'function' ? ttlSeconds(value) : ttlSeconds);
return value;
}
}
+21 -3
View File
@@ -5,19 +5,25 @@ import {KVCacheProvider} from '@pkgs/cache/src/providers/KVCacheProvider';
import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider';
import {describe, expect, it, vi} from 'vitest';
function createKVCacheProvider(): {provider: KVCacheProvider; store: Map<string, string>} {
function createKVCacheProvider(): {
provider: KVCacheProvider;
store: Map<string, string>;
ttls: Array<[string, number]>;
} {
const store = new Map<string, string>();
const ttls: Array<[string, number]> = [];
const client = {
get: async (key: string) => store.get(key) ?? null,
set: async (key: string, value: string) => {
store.set(key, value);
return 'OK';
},
setex: async (key: string, _ttlSeconds: number, value: string) => {
setex: async (key: string, ttlSeconds: number, value: string) => {
ttls.push([key, ttlSeconds]);
store.set(key, value);
},
} as unknown as IKVProvider;
return {provider: new KVCacheProvider({client}), store};
return {provider: new KVCacheProvider({client}), store, ttls};
}
function delay(ms: number): Promise<void> {
@@ -76,6 +82,18 @@ describe('ICacheService.getOrSet', () => {
expect(factory).toHaveBeenCalledTimes(1);
});
it('resolves the ttl from the produced value', async () => {
const {provider, store, ttls} = createKVCacheProvider();
const resolver = (value: number | null) => (value === null ? 5 : 30);
await expect(provider.getOrSet<number | null>('present', async () => 1, resolver)).resolves.toBe(1);
await expect(provider.getOrSet<number | null>('absent', async () => null, resolver)).resolves.toBeNull();
expect(ttls).toEqual([
['present', 30],
['absent', 5],
]);
expect(store.get('absent')).toBe('null');
});
it('rejects every waiter and retries on the next call when the factory fails', async () => {
const cache = new InMemoryProvider();
const failing = vi.fn(async () => {