fix: fix project access cache issue
This commit is contained in:
@@ -21,12 +21,6 @@ import { getCookiesFn } from '@/hooks/use-cookie-store';
|
|||||||
import { useSessionExtension } from '@/hooks/use-session-extension';
|
import { useSessionExtension } from '@/hooks/use-session-extension';
|
||||||
import { op } from '@/utils/op';
|
import { op } from '@/utils/op';
|
||||||
import type { AppRouter } from '@openpanel/trpc';
|
import type { AppRouter } from '@openpanel/trpc';
|
||||||
import { createServerOnlyFn } from '@tanstack/react-start';
|
|
||||||
import {
|
|
||||||
getCookie,
|
|
||||||
getCookies,
|
|
||||||
getRequestHeaders,
|
|
||||||
} from '@tanstack/react-start/server';
|
|
||||||
import type { TRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
import type { TRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
||||||
|
|
||||||
op.init();
|
op.init();
|
||||||
|
|||||||
@@ -12,23 +12,36 @@ import {
|
|||||||
} from '@tanstack/react-router';
|
} from '@tanstack/react-router';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
const IGNORE_ORGANIZATION_IDS = [
|
const IGNORE_ORGANIZATION_IDS = ['.well-known', 'onboarding', 'assets'];
|
||||||
'.well-known',
|
|
||||||
'robots.txt',
|
const FILE_EXTENSIONS = [
|
||||||
'sitemap.xml',
|
'jpg',
|
||||||
'favicon.ico',
|
'jpeg',
|
||||||
'manifest.json',
|
'png',
|
||||||
'sw.js',
|
'gif',
|
||||||
'service-worker.js',
|
'bmp',
|
||||||
'onboarding',
|
'tiff',
|
||||||
|
'ico',
|
||||||
|
'js',
|
||||||
|
'xml',
|
||||||
|
'txt',
|
||||||
|
'json',
|
||||||
|
'webmanifest',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const isStaticFile = (path: string) => {
|
||||||
|
return FILE_EXTENSIONS.some((extension) => path.endsWith(`.${extension}`));
|
||||||
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute('/_app/$organizationId')({
|
export const Route = createFileRoute('/_app/$organizationId')({
|
||||||
component: Component,
|
component: Component,
|
||||||
beforeLoad: async ({ context, params }) => {
|
beforeLoad: async ({ context, params }) => {
|
||||||
if (IGNORE_ORGANIZATION_IDS.includes(params.organizationId)) {
|
if (IGNORE_ORGANIZATION_IDS.includes(params.organizationId)) {
|
||||||
throw notFound();
|
throw notFound();
|
||||||
}
|
}
|
||||||
|
if (isStaticFile(params.organizationId)) {
|
||||||
|
throw notFound();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
loader: async ({ context, params }) => {
|
loader: async ({ context, params }) => {
|
||||||
await context.queryClient.prefetchQuery(
|
await context.queryClient.prefetchQuery(
|
||||||
|
|||||||
499
packages/redis/cachable.test.ts
Normal file
499
packages/redis/cachable.test.ts
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { cacheable, getCache } from './cachable';
|
||||||
|
import { getRedisCache } from './redis';
|
||||||
|
|
||||||
|
describe('cachable', () => {
|
||||||
|
let redis: any;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
redis = getRedisCache();
|
||||||
|
// Clear any existing cache data for clean tests
|
||||||
|
const keys = await redis.keys('cachable:*');
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await redis.del(...keys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Clean up after each test
|
||||||
|
const keys = await redis.keys('cachable:*');
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await redis.del(...keys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCache', () => {
|
||||||
|
it('should return cached data when available', async () => {
|
||||||
|
const mockData = { id: 1, name: 'test' };
|
||||||
|
const mockDate = new Date('2023-01-01T00:00:00Z');
|
||||||
|
const cachedData = { ...mockData, createdAt: mockDate };
|
||||||
|
|
||||||
|
// First, cache some data
|
||||||
|
await redis.setex('test-key', 3600, JSON.stringify(cachedData));
|
||||||
|
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async () => {
|
||||||
|
fnCalled = true;
|
||||||
|
return mockData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await getCache('test-key', 3600, fn);
|
||||||
|
|
||||||
|
expect(result).toEqual(cachedData);
|
||||||
|
expect(fnCalled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call function and cache result when no cache exists', async () => {
|
||||||
|
const mockData = { id: 1, name: 'test' };
|
||||||
|
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async () => {
|
||||||
|
fnCalled = true;
|
||||||
|
return mockData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await getCache('test-key-2', 3600, fn);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockData);
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify it was cached
|
||||||
|
const cached = await redis.get('test-key-2');
|
||||||
|
expect(cached).toBe(JSON.stringify(mockData));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Date objects from cached JSON', async () => {
|
||||||
|
const mockDate = new Date('2023-01-01T00:00:00Z');
|
||||||
|
const cachedData = { id: 1, createdAt: mockDate };
|
||||||
|
|
||||||
|
// Cache the data first
|
||||||
|
await redis.setex('test-key', 3600, JSON.stringify(cachedData));
|
||||||
|
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async () => {
|
||||||
|
fnCalled = true;
|
||||||
|
return { id: 1 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await getCache('test-key', 3600, fn);
|
||||||
|
|
||||||
|
expect((result as any).createdAt).toBeInstanceOf(Date);
|
||||||
|
expect((result as any).createdAt.getTime()).toBe(mockDate.getTime());
|
||||||
|
expect(fnCalled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cacheable', () => {
|
||||||
|
it('should create a cached function with function and expire time', async () => {
|
||||||
|
const mockData = { id: 1, name: 'test' };
|
||||||
|
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string, arg2: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return mockData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1', 'arg2');
|
||||||
|
|
||||||
|
expect(result).toEqual(mockData);
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify it was cached
|
||||||
|
const key = cachedFn.getKey('arg1', 'arg2');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBe(JSON.stringify(mockData));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a cached function with name, function and expire time', async () => {
|
||||||
|
const mockData = { id: 1, name: 'test' };
|
||||||
|
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string, arg2: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return mockData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable('testFunction', fn, 3600);
|
||||||
|
const result = await cachedFn('arg1', 'arg2');
|
||||||
|
|
||||||
|
expect(result).toEqual(mockData);
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify it was cached
|
||||||
|
const key = cachedFn.getKey('arg1', 'arg2');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBe(JSON.stringify(mockData));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return cached result when available', async () => {
|
||||||
|
const mockData = { id: 1, name: 'test' };
|
||||||
|
|
||||||
|
// First cache some data
|
||||||
|
const cachedFn = cacheable(
|
||||||
|
'testFunction',
|
||||||
|
async (arg1: string, arg2: string) => mockData,
|
||||||
|
3600,
|
||||||
|
);
|
||||||
|
await cachedFn('arg1', 'arg2');
|
||||||
|
|
||||||
|
// Now test that it returns cached data
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string, arg2: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return { id: 2, name: 'different' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const newCachedFn = cacheable('testFunction', fn, 3600);
|
||||||
|
const result = await newCachedFn('arg1', 'arg2');
|
||||||
|
|
||||||
|
expect(result).toEqual(mockData);
|
||||||
|
expect(fnCalled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cache undefined results', async () => {
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify nothing was cached
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cache null results', async () => {
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify nothing was cached
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cache empty strings', async () => {
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toBe('');
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify nothing was cached
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cache empty arrays', async () => {
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify nothing was cached
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cache empty objects', async () => {
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toEqual({});
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify nothing was cached
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache non-empty strings', async () => {
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return 'hello';
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toBe('hello');
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify it was cached
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBe('"hello"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache non-empty arrays', async () => {
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return [1, 2, 3];
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toEqual([1, 2, 3]);
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify it was cached
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBe('[1,2,3]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache non-empty objects', async () => {
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return { id: 1 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toEqual({ id: 1 });
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify it was cached
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBe('{"id":1}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache booleans', async () => {
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify it was cached
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache numbers', async () => {
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return 42;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
const result = await cachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toBe(42);
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
|
||||||
|
// Verify it was cached
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBe('42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle cache parsing errors gracefully', async () => {
|
||||||
|
const mockData = { id: 1, name: 'test' };
|
||||||
|
|
||||||
|
// First, manually set invalid JSON in cache
|
||||||
|
const cachedFn = cacheable(
|
||||||
|
'testFunction',
|
||||||
|
async (arg1: string) => mockData,
|
||||||
|
3600,
|
||||||
|
);
|
||||||
|
const key = cachedFn.getKey('arg1');
|
||||||
|
await redis.set(key, 'invalid json');
|
||||||
|
|
||||||
|
const consoleSpy = vi
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return mockData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const newCachedFn = cacheable('testFunction', fn, 3600);
|
||||||
|
const result = await newCachedFn('arg1');
|
||||||
|
|
||||||
|
expect(result).toEqual(mockData);
|
||||||
|
expect(fnCalled).toBe(true);
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
'Failed to parse cache',
|
||||||
|
expect.any(Error),
|
||||||
|
);
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse Date objects from cached JSON', async () => {
|
||||||
|
const mockDate = new Date('2023-01-01T00:00:00Z');
|
||||||
|
const cachedData = { id: 1, createdAt: mockDate };
|
||||||
|
|
||||||
|
// First cache some data with Date
|
||||||
|
const cachedFn = cacheable(
|
||||||
|
'testFunction',
|
||||||
|
async (arg1: string) => cachedData,
|
||||||
|
3600,
|
||||||
|
);
|
||||||
|
await cachedFn('arg1');
|
||||||
|
|
||||||
|
// Now test that it returns cached data with proper Date parsing
|
||||||
|
let fnCalled = false;
|
||||||
|
const fn = async (arg1: string) => {
|
||||||
|
fnCalled = true;
|
||||||
|
return { id: 2 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const newCachedFn = cacheable('testFunction', fn, 3600);
|
||||||
|
const result = await newCachedFn('arg1');
|
||||||
|
|
||||||
|
expect((result as any).createdAt).toBeInstanceOf(Date);
|
||||||
|
expect((result as any).createdAt.getTime()).toBe(mockDate.getTime());
|
||||||
|
expect(fnCalled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide getKey method', () => {
|
||||||
|
const fn = async (arg1: string, arg2: string) => ({});
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
|
||||||
|
expect(typeof cachedFn.getKey).toBe('function');
|
||||||
|
const key = cachedFn.getKey('arg1', 'arg2');
|
||||||
|
expect(key).toMatch(/^cachable:.*:\[arg1,arg2\]$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide clear method', async () => {
|
||||||
|
const fn = async (arg1: string, arg2: string) => ({ id: 1 });
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
|
||||||
|
// First cache some data
|
||||||
|
await cachedFn('arg1', 'arg2');
|
||||||
|
|
||||||
|
// Verify it's cached
|
||||||
|
const key = cachedFn.getKey('arg1', 'arg2');
|
||||||
|
let cached = await redis.get(key);
|
||||||
|
expect(cached).not.toBeNull();
|
||||||
|
|
||||||
|
// Clear it
|
||||||
|
const result = await cachedFn.clear('arg1', 'arg2');
|
||||||
|
expect(result).toBe(1);
|
||||||
|
|
||||||
|
// Verify it's cleared
|
||||||
|
cached = await redis.get(key);
|
||||||
|
expect(cached).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide set method', async () => {
|
||||||
|
const fn = async (arg1: string, arg2: string) => ({});
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
|
||||||
|
const payload = { id: 1, name: 'test' };
|
||||||
|
await cachedFn.set('arg1', 'arg2')(payload);
|
||||||
|
|
||||||
|
// Verify it was set
|
||||||
|
const key = cachedFn.getKey('arg1', 'arg2');
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
expect(cached).toBe(JSON.stringify(payload));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when function is not provided', () => {
|
||||||
|
expect(() => {
|
||||||
|
cacheable('test', 3600);
|
||||||
|
}).toThrow('fn is not a function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when expire time is not provided', () => {
|
||||||
|
const fn = async (arg1: string, arg2: string) => ({});
|
||||||
|
expect(() => {
|
||||||
|
cacheable(fn, undefined as any);
|
||||||
|
}).toThrow('expireInSec is not a number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate consistent cache keys for same arguments', () => {
|
||||||
|
const fn = async (arg1: { a: number; b: number }, arg2: string) => ({});
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
|
||||||
|
const key1 = cachedFn.getKey({ a: 1, b: 2 }, 'test');
|
||||||
|
const key2 = cachedFn.getKey({ b: 2, a: 1 }, 'test'); // Different order
|
||||||
|
|
||||||
|
expect(key1).toBe(key2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex argument types in cache keys', () => {
|
||||||
|
const fn = async (
|
||||||
|
arg1: string,
|
||||||
|
arg2: number,
|
||||||
|
arg3: boolean,
|
||||||
|
arg4: null,
|
||||||
|
arg5: undefined,
|
||||||
|
arg6: number[],
|
||||||
|
arg7: { a: number; b: number },
|
||||||
|
arg8: Date,
|
||||||
|
) => ({});
|
||||||
|
const cachedFn = cacheable(fn, 3600);
|
||||||
|
|
||||||
|
const key = cachedFn.getKey(
|
||||||
|
'string',
|
||||||
|
123,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
[1, 2, 3],
|
||||||
|
{ a: 1, b: 2 },
|
||||||
|
new Date('2023-01-01T00:00:00Z'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(key).toMatch(/^cachable:.*:/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -46,6 +46,31 @@ function stringify(obj: unknown): string {
|
|||||||
return String(obj);
|
return String(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasResult(result: unknown): boolean {
|
||||||
|
// Don't cache undefined or null
|
||||||
|
if (result === undefined || result === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't cache empty strings
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
return result.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't cache empty arrays
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return result.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't cache empty objects
|
||||||
|
if (typeof result === 'object' && result !== null) {
|
||||||
|
return Object.keys(result).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache everything else (booleans, numbers, etc.)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function cacheable<T extends (...args: any) => any>(
|
export function cacheable<T extends (...args: any) => any>(
|
||||||
fnOrName: T | string,
|
fnOrName: T | string,
|
||||||
fnOrExpireInSec: number | T,
|
fnOrExpireInSec: number | T,
|
||||||
@@ -84,7 +109,7 @@ export function cacheable<T extends (...args: any) => any>(
|
|||||||
const cached = await getRedisCache().get(key);
|
const cached = await getRedisCache().get(key);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(cached, (_, value) => {
|
const parsed = JSON.parse(cached, (_, value) => {
|
||||||
if (
|
if (
|
||||||
typeof value === 'string' &&
|
typeof value === 'string' &&
|
||||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z$/.test(value)
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z$/.test(value)
|
||||||
@@ -93,13 +118,16 @@ export function cacheable<T extends (...args: any) => any>(
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
|
if (hasResult(parsed)) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to parse cache', e);
|
console.error('Failed to parse cache', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const result = await fn(...(args as any));
|
const result = await fn(...(args as any));
|
||||||
|
|
||||||
if (result !== undefined || result !== null) {
|
if (hasResult(result)) {
|
||||||
getRedisCache().setex(key, expireInSec, JSON.stringify(result));
|
getRedisCache().setex(key, expireInSec, JSON.stringify(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user