|
| 1 | +# @shamt/cache |
| 2 | + |
| 3 | +<p><strong>English</strong> | <a href="./README.zh-CN.md">中文</a></p> |
| 4 | + |
| 5 | +## Table of Contents |
| 6 | + |
| 7 | +- [Overview](#overview) |
| 8 | +- [Design and Architecture](#design-and-architecture) |
| 9 | +- [Inputs and Outputs](#inputs-and-outputs) |
| 10 | +- [Usage](#usage) |
| 11 | +- [Implementation Notes](#implementation-notes) |
| 12 | + |
| 13 | +## Overview |
| 14 | + |
| 15 | +`@shamt/cache` defines the shared cache abstraction for the workspace and provides the default in-memory implementation. It is designed to stay runtime-neutral enough for shared packages, Node-like runtimes, and web-compatible runtimes. |
| 16 | + |
| 17 | +The package currently includes: |
| 18 | + |
| 19 | +- `Cache`: abstract cache contract. |
| 20 | +- `MemoryCache`: LRU-based in-memory cache implementation. |
| 21 | +- `createCache`: factory that creates the default `MemoryCache`. |
| 22 | +- Utility functions for cache key normalization and value serialization. |
| 23 | + |
| 24 | +Platform-specific stores such as Redis and Cloudflare KV should live in the application layer and extend the same `Cache` contract. |
| 25 | + |
| 26 | +## Design and Architecture |
| 27 | + |
| 28 | +`@shamt/cache` separates the cache contract from concrete runtime storage: |
| 29 | + |
| 30 | +- The `Cache` base class stores the backing client in `this.store` and declares the core cache methods every implementation must provide: `set`, `get`, `del`, and `has`. |
| 31 | +- Lifecycle methods such as `connect` and `dispose` are implementation-specific and are not part of the base `Cache` contract. |
| 32 | +- Base methods throw `CacheMethodNotImplementedError` by default, so missing implementations fail early during development. |
| 33 | +- TTL values passed into the package are always handled as milliseconds. |
| 34 | +- Values are serialized at the cache boundary with JSON helpers from `@unimolecule/utils`, making the memory driver behave closer to string-based stores such as Redis and KV. |
| 35 | +- Keys can be namespaced with a prefix. `MemoryCache` defaults to `cache:`; |
| 36 | + non-empty prefixes are normalized to end with `:`, and an empty prefix disables |
| 37 | + namespacing. |
| 38 | + |
| 39 | +`MemoryCache` uses `lru-cache` for eviction, TTL, and max-size accounting. It is suitable for local development, tests, short-lived in-process caching, and runtime-neutral default behavior. |
| 40 | + |
| 41 | +## Inputs and Outputs |
| 42 | + |
| 43 | +Inputs: |
| 44 | + |
| 45 | +- Logical cache keys as strings. |
| 46 | +- JSON-serializable values. |
| 47 | +- Per-write options, such as `{ ttl }`. |
| 48 | +- Store-level options, such as `{ ttl, keyPrefix, maxSize }`. |
| 49 | + |
| 50 | +Outputs: |
| 51 | + |
| 52 | +- Write methods return `Promise<void>`. |
| 53 | +- Read methods return `Promise<T | undefined>`. |
| 54 | +- Existence checks return `Promise<boolean>`. |
| 55 | +- Invalid TTL values, non-serializable values, or missing required methods throw errors. |
| 56 | + |
| 57 | +Units: |
| 58 | + |
| 59 | +- `ttl` is always in milliseconds. |
| 60 | +- `maxSize` is always in bytes. |
| 61 | +- `keyPrefix` defaults to `cache:` for `MemoryCache` and `createCache`. |
| 62 | + |
| 63 | +## Usage |
| 64 | + |
| 65 | +Use the default factory: |
| 66 | + |
| 67 | +```ts |
| 68 | +import { createCache } from "@shamt/cache"; |
| 69 | + |
| 70 | +const cache = createCache({ |
| 71 | + ttl: 60_000, |
| 72 | + keyPrefix: "shop", |
| 73 | +}); |
| 74 | + |
| 75 | +await cache.set("settings", { currency: "USD" }); |
| 76 | + |
| 77 | +const settings = await cache.get<{ currency: string }>("settings"); |
| 78 | +const exists = await cache.has("settings"); |
| 79 | + |
| 80 | +await cache.del("settings"); |
| 81 | +``` |
| 82 | + |
| 83 | +Use `MemoryCache` directly: |
| 84 | + |
| 85 | +```ts |
| 86 | +import { MemoryCache } from "@shamt/cache"; |
| 87 | + |
| 88 | +const cache = new MemoryCache({ |
| 89 | + ttl: 5 * 60_000, |
| 90 | + keyPrefix: "session", |
| 91 | + maxSize: 1024 * 1024, |
| 92 | +}); |
| 93 | + |
| 94 | +await cache.connect(); |
| 95 | +await cache.set("offline:shop.myshopify.com", { |
| 96 | + accessToken: "token", |
| 97 | +}); |
| 98 | + |
| 99 | +const session = await cache.get<{ accessToken: string }>( |
| 100 | + "offline:shop.myshopify.com", |
| 101 | +); |
| 102 | + |
| 103 | +await cache.dispose(); |
| 104 | +``` |
| 105 | + |
| 106 | +Implement a platform-specific store: |
| 107 | + |
| 108 | +```ts |
| 109 | +import { |
| 110 | + Cache, |
| 111 | + deserializeCacheValue, |
| 112 | + serializeCacheValue, |
| 113 | + type CacheSetOptions, |
| 114 | +} from "@shamt/cache"; |
| 115 | + |
| 116 | +interface RedisClient { |
| 117 | + set: (key: string, value: string, options?: { px: number }) => Promise<void>; |
| 118 | + get: (key: string) => Promise<string | null>; |
| 119 | + del: (key: string) => Promise<void>; |
| 120 | + exists: (key: string) => Promise<number>; |
| 121 | + quit: () => Promise<void>; |
| 122 | +} |
| 123 | + |
| 124 | +class RedisCache extends Cache<RedisClient> { |
| 125 | + async connect() { |
| 126 | + // Open Redis connection. |
| 127 | + } |
| 128 | + |
| 129 | + override async set<T>(key: string, value: T, options: CacheSetOptions = {}) { |
| 130 | + const ttl = this.resolveTtl(options.ttl); |
| 131 | + await this.store.set( |
| 132 | + key, |
| 133 | + serializeCacheValue(value), |
| 134 | + ttl === undefined ? undefined : { px: ttl }, |
| 135 | + ); |
| 136 | + } |
| 137 | + |
| 138 | + override async get<T>(key: string): Promise<T | undefined> { |
| 139 | + return deserializeCacheValue<T>((await this.store.get(key)) ?? undefined); |
| 140 | + } |
| 141 | + |
| 142 | + override async del(key: string) { |
| 143 | + await this.store.del(key); |
| 144 | + } |
| 145 | + |
| 146 | + override async has(key: string) { |
| 147 | + return (await this.store.exists(key)) > 0; |
| 148 | + } |
| 149 | + |
| 150 | + async dispose() { |
| 151 | + await this.store.quit(); |
| 152 | + } |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +## Implementation Notes |
| 157 | + |
| 158 | +`@shamt/cache` currently only includes the memory driver. This avoids forcing Redis, Cloudflare KV, or other platform SDKs into the shared package dependency graph, keeping the package easier to reuse in node, web, and serverless/isolate environments. |
| 159 | + |
| 160 | +Applications can choose runtime-specific stores at the application layer while still reusing the same `Cache` abstraction. |
0 commit comments