UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

603 lines (517 loc) 16.4 kB
import { randomUUID } from "node:crypto"; import { Alepha, type Service } from "alepha"; import { $cache, CacheProvider, cacheOptions, MemoryCacheProvider, } from "alepha/cache"; import { DateTimeProvider } from "alepha/datetime"; import { expect } from "vitest"; export class TestCache { cursor_a = 0; cursor_b = 0; a = $cache({ key: (args) => args.name, ttl: [5, "seconds"], handler: async (user: { name: string }) => { return `${user.name}:${this.cursor_a++}`; }, }); b = $cache({ provider: "memory", key: (args) => args.name, ttl: 0, handler: async (user: { name: string }) => { return `${user.name}:${this.cursor_b++}`; }, }); } export const testCacheBasic = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(TestCache); const time = app.inject(DateTimeProvider); await app.start(); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "B" })).toBe("B:1"); expect(await test.a({ name: "B" })).toBe("B:1"); if (!(app.inject(CacheProvider) instanceof MemoryCacheProvider)) { return; // we can mock only MemoryCacheProvider } await time.travel([2, "seconds"]); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "B" })).toBe("B:1"); await time.travel([4, "seconds"]); expect(await test.a({ name: "A" })).toBe("A:2"); expect(await test.a({ name: "A" })).toBe("A:2"); expect(await test.a({ name: "B" })).toBe("B:3"); }; export const testCacheStop = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(TestCache); await app.start(); await app.stop(); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:1"); expect(await test.a({ name: "A" })).toBe("A:2"); }; export const testCacheMissingProvider = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(TestCache); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:1"); }; export const testCacheDisabled = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); app.store.mut(cacheOptions, (current) => ({ ...current, enabled: false })); configure(app); const test = app.inject(TestCache); await app.start(); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:1"); expect(await test.a({ name: "B" })).toBe("B:2"); }; export const testCacheInvalidateByKey = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(TestCache); await app.start(); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "B" })).toBe("B:1"); await test.a.invalidate("A"); expect(await test.a({ name: "B" })).toBe("B:1"); expect(await test.a({ name: "A" })).toBe("A:2"); }; export const testCacheInvalidateByArgs = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(TestCache); await app.start(); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "B" })).toBe("B:1"); await test.a.invalidate(test.a.key({ name: "A" })); expect(await test.a({ name: "B" })).toBe("B:1"); expect(await test.a({ name: "A" })).toBe("A:2"); }; export const testCacheInvalidateAll = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(TestCache); await app.start(); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:0"); await test.a.invalidate(); expect(await test.a({ name: "A" })).toBe("A:1"); }; export const testCacheClear = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(TestCache); await app.start(); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.b({ name: "A" })).toBe("A:0"); expect(await test.b({ name: "A" })).toBe("A:0"); await Promise.all(app.primitives($cache).map((cache) => cache.invalidate())); expect(await test.a({ name: "A" })).toBe("A:1"); expect(await test.b({ name: "A" })).toBe("A:1"); }; export const testCacheReturnTypes = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { class Types { json = $cache({ handler: () => ({ a: 1 }), }); int = $cache({ handler: () => 1, }); bool = $cache({ handler: () => true, }); string = $cache({ handler: () => '{ "a": 1 }', }); } const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(Types); expect(await test.json()).toEqual({ a: 1 }); expect(await test.json()).toEqual({ a: 1 }); expect(await test.json()).toEqual({ a: 1 }); expect(await test.int()).toBe(1); expect(await test.int()).toBe(1); expect(await test.int()).toBe(1); expect(await test.bool()).toBe(true); expect(await test.bool()).toBe(true); expect(await test.bool()).toBe(true); expect(await test.string()).toBe('{ "a": 1 }'); expect(await test.string()).toBe('{ "a": 1 }'); }; export const testCacheKeys = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ) => { const alepha = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(alepha); class TestApp { cache = $cache<string>(); } const app = alepha.inject(TestApp); const provider = alepha.inject(CacheProvider); await alepha.start(); app.cache.set("test:A", "A"); app.cache.set("test:B", "B"); app.cache.set("hello", "C"); expect(await provider.keys("TestApp:cache").then((it) => it.length)).toEqual( 3, ); await app.cache.invalidate("test:*"); expect(await provider.keys("TestApp:cache").then((it) => it.length)).toEqual( 1, ); }; export const testSimpleKeyMappingHandler = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { class App { i = 0; run = $cache({ key: (name: string) => name, ttl: [5, "seconds"], handler: async (name: string) => { this.i++; return `${name}=${this.i}`; }, }); } const alepha = Alepha.create() .with({ provide: CacheProvider, use: cacheProvider, }) .with(App); configure(alepha); await alepha.start(); const app = alepha.inject(App); expect(await app.run("A")).toBe("A=1"); expect(await app.run("A")).toBe("A=1"); expect(await app.run("A")).toBe("A=1"); expect(await app.run("A")).toBe("A=1"); expect(await app.run("B")).toBe("B=2"); expect(await app.run("B")).toBe("B=2"); expect(await app.run("B")).toBe("B=2"); expect(await app.run("C")).toBe("C=3"); }; export const testCacheIncr = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const provider = app.inject(CacheProvider); await app.start(); const name = "test-incr"; const key = `counter:${randomUUID()}`; // First increment creates the key const val1 = await provider.incr(name, key, 1); expect(val1).toBe(1); // Subsequent increments add to existing value const val2 = await provider.incr(name, key, 5); expect(val2).toBe(6); // Negative amounts work (decrement) const val3 = await provider.incr(name, key, -2); expect(val3).toBe(4); }; export const testCacheFalsyValues = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { class FalsyCache { zeroCount = 0; emptyCount = 0; falseCount = 0; nullCount = 0; zero = $cache({ handler: () => { this.zeroCount++; return 0; }, }); empty = $cache({ handler: () => { this.emptyCount++; return ""; }, }); bool = $cache({ handler: () => { this.falseCount++; return false; }, }); nil = $cache({ handler: () => { this.nullCount++; return null; }, }); } const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(FalsyCache); await app.start(); // Each value should be cached after first call expect(await test.zero()).toBe(0); expect(await test.zero()).toBe(0); expect(await test.zero()).toBe(0); expect(test.zeroCount).toBe(1); expect(await test.empty()).toBe(""); expect(await test.empty()).toBe(""); expect(test.emptyCount).toBe(1); expect(await test.bool()).toBe(false); expect(await test.bool()).toBe(false); expect(test.falseCount).toBe(1); expect(await test.nil()).toBe(null); expect(await test.nil()).toBe(null); expect(test.nullCount).toBe(1); }; export const testCacheSetDisabled = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { class ManualCache { store = $cache<string>(); } const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); app.store.mut(cacheOptions, (current) => ({ ...current, enabled: false })); configure(app); const test = app.inject(ManualCache); const provider = app.inject(CacheProvider); await app.start(); // set() should be a no-op when cache is disabled await test.store.set("key1", "value1"); expect(await provider.has("ManualCache:store", "key1")).toBe(false); }; export const testCachePrimitiveIncr = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { class CounterApp { counter = $cache<number>(); } const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(CounterApp); await app.start(); // incr through the primitive expect(await test.counter.incr("views")).toBe(1); expect(await test.counter.incr("views")).toBe(2); expect(await test.counter.incr("views", 5)).toBe(7); expect(await test.counter.incr("views", -3)).toBe(4); }; export const testCacheProviderClear = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { class TestClearCache { cursor_a = 0; cursor_b = 0; a = $cache({ key: (args) => args.name, ttl: [5, "seconds"], handler: async (user: { name: string }) => { return `${user.name}:${this.cursor_a++}`; }, }); b = $cache({ key: (args) => args.name, handler: async (user: { name: string }) => { return `${user.name}:${this.cursor_b++}`; }, }); } const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(TestClearCache); const provider = app.inject(CacheProvider); await app.start(); // Set some cache values expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.a({ name: "B" })).toBe("B:1"); expect(await test.b({ name: "C" })).toBe("C:0"); expect(await test.b({ name: "C" })).toBe("C:0"); // Verify cache is working expect(await test.a({ name: "A" })).toBe("A:0"); expect(await test.b({ name: "C" })).toBe("C:0"); // Clear all cache await provider.clear(); // Verify cache was cleared - new values should be generated expect(await test.a({ name: "A" })).toBe("A:2"); expect(await test.a({ name: "B" })).toBe("B:3"); expect(await test.b({ name: "C" })).toBe("C:1"); }; export const testCacheCompress = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { class CompressedCache { count = 0; data = $cache({ compress: true, handler: () => { this.count++; return { users: Array.from({ length: 100 }, (_, i) => ({ id: i, name: `User ${i}`, email: `user${i}@example.com`, })), }; }, }); } const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(CompressedCache); await app.start(); const result1 = await test.data(); expect(result1.users).toHaveLength(100); expect(result1.users[0]).toEqual({ id: 0, name: "User 0", email: "user0@example.com", }); // Second call returns cached (and compressed) value const result2 = await test.data(); expect(result2).toEqual(result1); expect(test.count).toBe(1); // Verify stored data has COMPRESSED prefix and is smaller than raw JSON const provider = app.inject(CacheProvider); const raw = await provider.get("CompressedCache:data", "[]"); expect(raw).toBeDefined(); expect(raw![0]).toBe(0x04); const uncompressedJson = JSON.stringify(result1); expect(raw!.length).toBeLessThan(uncompressedJson.length); }; export const testCacheCompressTypes = async ( configure: (app: Alepha) => void = () => {}, cacheProvider: Service<CacheProvider> = MemoryCacheProvider, ): Promise<void> => { class CompressedTypes { json = $cache({ compress: true, handler: () => ({ key: "value".repeat(50) }), }); str = $cache({ compress: true, handler: () => "hello".repeat(50), }); num = $cache({ compress: true, handler: () => 42, }); bool = $cache({ compress: true, handler: () => true, }); } const app = Alepha.create().with({ provide: CacheProvider, use: cacheProvider, }); configure(app); const test = app.inject(CompressedTypes); await app.start(); expect(await test.json()).toEqual({ key: "value".repeat(50) }); expect(await test.json()).toEqual({ key: "value".repeat(50) }); expect(await test.str()).toBe("hello".repeat(50)); expect(await test.str()).toBe("hello".repeat(50)); expect(await test.num()).toBe(42); expect(await test.num()).toBe(42); expect(await test.bool()).toBe(true); expect(await test.bool()).toBe(true); };