-
Notifications
You must be signed in to change notification settings - Fork 0
feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
y-aithnini
merged 1 commit into
develop
from
feature/COMPT-55-icachestore-interface-adapters
Mar 31, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| /** | ||
| * @file in-memory-cache-store.adapter.ts | ||
| * | ||
| * In-memory implementation of ICacheStore backed by a plain JavaScript Map. | ||
| * | ||
| * Behaviour: | ||
| * - Values are JSON-serialized on write and JSON-parsed on read, matching | ||
| * the Redis adapter exactly so both can be swapped transparently. | ||
| * - TTL is enforced lazily: an expired entry is evicted the first time it | ||
| * is read, rather than via a background sweep timer. | ||
| * - A parse failure (malformed JSON) returns null instead of throwing. | ||
| * - No external dependencies — suitable for unit tests, local development, | ||
| * or lightweight production usage that does not require persistence. | ||
| * | ||
| * Exports: | ||
| * - CacheEntry → internal shape of stored entries (exported for tests) | ||
| * - InMemoryCacheStore → the concrete in-memory adapter class | ||
| */ | ||
|
|
||
| import type { ICacheStore } from "@ports/cache-store.port"; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Internal data shape | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * Shape of each entry held inside the backing Map. | ||
| * Exported so that unit tests can inspect the internal store if needed. | ||
| */ | ||
| export interface CacheEntry { | ||
| /** JSON-serialized representation of the cached value */ | ||
| value: string; | ||
|
|
||
| /** | ||
| * Absolute Unix timestamp (ms) at which this entry expires. | ||
| * null means the entry never expires. | ||
| */ | ||
| expiresAt: number | null; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Adapter | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * In-memory adapter for the ICacheStore port. | ||
| * | ||
| * Usage: | ||
| * ```typescript | ||
| * const store = new InMemoryCacheStore(); | ||
| * await store.set("session:abc", { userId: 1 }, 60); // expires in 60 s | ||
| * const session = await store.get<Session>("session:abc"); | ||
| * ``` | ||
| */ | ||
| export class InMemoryCacheStore implements ICacheStore { | ||
| /** | ||
| * The backing store. | ||
| * Maps every cache key to its serialized value and optional expiry timestamp. | ||
| */ | ||
| private readonly store = new Map<string, CacheEntry>(); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // ICacheStore implementation | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** {@inheritDoc ICacheStore.get} */ | ||
| async get<T>(key: string): Promise<T | null> { | ||
| // Look up the entry — undefined means the key was never set or was deleted | ||
| const entry = this.store.get(key); | ||
|
|
||
| // Key does not exist in the store | ||
| if (entry === undefined) return null; | ||
|
|
||
| // Lazy TTL expiry: check whether the entry has passed its deadline. | ||
| // Date.now() returns the current time in milliseconds. | ||
| if (entry.expiresAt !== null && Date.now() > entry.expiresAt) { | ||
| // Remove the stale entry and treat the lookup as a cache miss | ||
| this.store.delete(key); | ||
| return null; | ||
| } | ||
|
|
||
| // Deserialize the stored JSON string back to the caller's expected type. | ||
| // Return null on malformed JSON instead of propagating a SyntaxError. | ||
| try { | ||
| return JSON.parse(entry.value) as T; | ||
| } catch { | ||
| // Parse failure — treat as a cache miss | ||
y-aithnini marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** {@inheritDoc ICacheStore.set} */ | ||
| async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> { | ||
| // Compute the absolute expiry timestamp from the relative TTL. | ||
| // Multiply seconds by 1 000 to convert to milliseconds for Date.now() comparison. | ||
| // null signals "no expiry" so the entry lives until deleted or clear() is called. | ||
| const expiresAt = | ||
| ttlSeconds !== undefined && ttlSeconds > 0 | ||
| ? Date.now() + ttlSeconds * 1_000 | ||
| : null; | ||
|
|
||
| // Serialize the value to a JSON string before storing to match Redis adapter behaviour | ||
| this.store.set(key, { | ||
| value: JSON.stringify(value), | ||
| expiresAt, | ||
| }); | ||
| } | ||
|
|
||
| /** {@inheritDoc ICacheStore.delete} */ | ||
| async delete(key: string): Promise<void> { | ||
| // Map.delete is a no-op when the key does not exist — no guard required | ||
| this.store.delete(key); | ||
| } | ||
|
|
||
| /** {@inheritDoc ICacheStore.clear} */ | ||
| async clear(): Promise<void> { | ||
| // Remove every entry from the backing Map in O(1) | ||
| this.store.clear(); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.