From d062363b4f248566a71a19089cd15f4905b7fd36 Mon Sep 17 00:00:00 2001 From: webdevcom01-cell Date: Fri, 8 May 2026 17:05:14 +0200 Subject: [PATCH 1/2] feat(simple-cache-ttl-utility): SDLC [c1e3b8a6] --- src/lib/utils/cache.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/lib/utils/cache.ts diff --git a/src/lib/utils/cache.ts b/src/lib/utils/cache.ts new file mode 100644 index 0000000..1fe18b2 --- /dev/null +++ b/src/lib/utils/cache.ts @@ -0,0 +1,35 @@ +export class SimpleCache { + private store: Map + + constructor() { + this.store = new Map(); + } + + set(key: K, value: V, ttlMs?: number): void { + let expiresAt: number | undefined = undefined; + if (typeof ttlMs === 'number' && ttlMs > 0) { + expiresAt = Date.now() + ttlMs; + } + this.store.set(key, { value, expiresAt }); + } + + get(key: K): V | undefined { + const entry = this.store.get(key); + if (!entry) return undefined; + if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) { + this.store.delete(key); + return undefined; + } + return entry.value; + } + + has(key: K): boolean { + const entry = this.store.get(key); + if (!entry) return false; + if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) { + this.store.delete(key); + return false; + } + return true; + } +} From bb6e0bcc64a35e074b88ee8be0dc45a0aa5a8c9a Mon Sep 17 00:00:00 2001 From: webdevcom01-cell Date: Fri, 8 May 2026 17:05:15 +0200 Subject: [PATCH 2/2] test(simple-cache-ttl-utility): SDLC [c1e3b8a6] --- src/lib/utils/cache.test.ts | 66 +++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/lib/utils/cache.test.ts diff --git a/src/lib/utils/cache.test.ts b/src/lib/utils/cache.test.ts new file mode 100644 index 0000000..0f09469 --- /dev/null +++ b/src/lib/utils/cache.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from 'vitest' +import { SimpleCache } from './cache' + +describe('SimpleCache', () => { + it('basic set/get returns value', () => { + const cache = new SimpleCache() + cache.set('foo', 123) + expect(cache.get('foo')).toBe(123) + }) + + it('expired entry returns undefined', async () => { + const cache = new SimpleCache() + cache.set('bar', 'baz', 10) + await new Promise(r => setTimeout(r, 20)) + expect(cache.get('bar')).toBeUndefined() + }) + + it('has() returns true before expiry, false after', async () => { + const cache = new SimpleCache() + cache.set(1, 999, 30) + expect(cache.has(1)).toBe(true) + await new Promise(r => setTimeout(r, 35)) + expect(cache.has(1)).toBe(false) + }) + + it('overwriting existing key updates value and TTL', async () => { + const cache = new SimpleCache() + cache.set('dup', 'first', 50) + cache.set('dup', 'second', 10) + expect(cache.get('dup')).toBe('second') + await new Promise(r => setTimeout(r, 15)) + expect(cache.get('dup')).toBeUndefined() + }) + + it('cache entry without TTL never expires', async () => { + const cache = new SimpleCache() + cache.set('persist', 'value') + await new Promise(r => setTimeout(r, 30)) + expect(cache.get('persist')).toBe('value') + expect(cache.has('persist')).toBe(true) + }) + + it('returns undefined for missing key', () => { + const cache = new SimpleCache() + expect(cache.get('missing')).toBeUndefined() + expect(cache.has('missing')).toBe(false) + }) + + it('accepts empty string and zero as keys', () => { + const cache = new SimpleCache() + cache.set('', 'empty') + cache.set(0, 'zero') + expect(cache.get('')).toBe('empty') + expect(cache.get(0)).toBe('zero') + expect(cache.has('')).toBe(true) + expect(cache.has(0)).toBe(true) + }) + + it('negative TTL disables expiration (acts as no TTL)', async () => { + const cache = new SimpleCache() + cache.set('neg', 'negval', -100) + await new Promise(r => setTimeout(r, 10)) + expect(cache.get('neg')).toBe('negval') + expect(cache.has('neg')).toBe(true) + }) +})