alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
603 lines (517 loc) • 16.4 kB
text/typescript
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);
};