{"id":45261670,"url":"https://github.com/skydiver/newton-cache","last_synced_at":"2026-02-21T00:27:57.261Z","repository":{"id":325864575,"uuid":"1102673534","full_name":"skydiver/newton-cache","owner":"skydiver","description":"Lightweight cache library with pluggable adapters. Zero dependencies, TTL support, TypeScript-first","archived":false,"fork":false,"pushed_at":"2025-11-23T22:03:00.000Z","size":0,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-11-23T22:15:06.276Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skydiver.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-23T21:49:42.000Z","updated_at":"2025-11-23T22:04:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/skydiver/newton-cache","commit_stats":null,"previous_names":["skydiver/newton-cache"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/skydiver/newton-cache","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydiver%2Fnewton-cache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydiver%2Fnewton-cache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydiver%2Fnewton-cache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydiver%2Fnewton-cache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skydiver","download_url":"https://codeload.github.com/skydiver/newton-cache/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydiver%2Fnewton-cache/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29668685,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T00:11:43.526Z","status":"ssl_error","status_checked_at":"2026-02-20T23:52:33.807Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-21T00:27:55.626Z","updated_at":"2026-02-21T00:27:57.252Z","avatar_url":"https://github.com/skydiver.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# newton-cache\n\nLightweight async cache library with pluggable adapters. Zero dependencies, TTL support, TypeScript-first. Ships as an ES module with complete type definitions. All cache operations return Promises.\n\n## Install\n\n```bash\nnpm install newton-cache\n```\n\n## Usage\n\n### Choosing an Adapter\n\nAll adapters implement the same `CacheAdapter` interface. Currently available:\n\n- **FileCache** - Persistent file-based storage (survives restarts)\n- **FlatFileCache** - Persistent single-file storage (all keys in one JSON file)\n- **MemoryCache** - Fast in-memory storage (data lost on restart)\n\n#### FileCache vs FlatFileCache Comparison\n\n| Feature | FileCache | FlatFileCache |\n|---------|-----------|---------------|\n| **Storage** | One file per key | Single JSON file |\n| **Best for** | Large caches (\u003e1000 keys) | Small-medium caches (\u003c1000 keys) |\n| **Write performance** | Fast (only affected key) | Slower (rewrites entire cache) |\n| **Read performance** | O(1) file read | O(1) after initial load |\n| **Memory usage** | Minimal | Full cache loaded in memory |\n| **Backup/restore** | Copy directory | Copy single file ✨ |\n| **Inode usage** | One per key | Just one file ✨ |\n| **Inspection** | `ls` cache directory | Read JSON file directly ✨ |\n| **Persistence** | Survives restarts ✅ | Survives restarts ✅ |\n\n**Use FileCache when:** You have many keys (\u003e1000), high write frequency, or large values per key.\n\n**Use FlatFileCache when:** You want easy backup (single file), minimal inode usage, or simple inspection of all cached data.\n\n### Initializing\n\n**FileCache** (persistent, survives restarts):\n```ts\nimport { FileCache } from 'newton-cache';\n\n// Stores files in the OS tmp directory by default\nconst cache = new FileCache\u003cstring\u003e();\n\n// Or provide your own directory:\nconst cache = new FileCache({ cachePath: \"/var/tmp/my-cache\" });\n```\n\n**FlatFileCache** (single file, survives restarts):\n```ts\nimport { FlatFileCache } from 'newton-cache';\n\n// Stores all entries in a single JSON file in the OS tmp directory\nconst cache = new FlatFileCache\u003cstring\u003e();\n\n// Or provide your own file path:\nconst cache = new FlatFileCache({\n  filePath: \"/var/cache/my-app.json\"\n});\n```\n\n**MemoryCache** (fast, in-memory only):\n```ts\nimport { MemoryCache } from 'newton-cache';\n\n// Stores data in memory\nconst cache = new MemoryCache\u003cstring\u003e();\n```\n\nAll adapters implement the same interface, so you can easily switch between them.\n\n### Getting items from the cache\n\n```ts\n// If a file named \"answer\" exists in the cache directory, read it:\nconst value = await cache.get('answer'); // parsed value, or undefined if missing\n\n// Provide a default if the file doesn't exist or is unreadable:\nconst fallback = await cache.get('missing-key', 'default');\n\n// Or pass a factory/closure so the default is only computed when needed:\nconst fromFactory = await cache.get('missing', () =\u003e expensiveLookup());\n\n// You can also pass the function reference directly:\nconst directFactory = await cache.get('missing', expensiveLookup);\n\n// Inline anonymous factory\nconst twoLine = await cache.get('computed', () =\u003e {\n  const value = expensiveLookup();\n  return value;\n});\n```\n\n### Checking existence\n\n```ts\n// Returns true when the file exists and contains a defined value.\nif (await cache.has('answer')) {\n  // ...\n}\n```\n\n### Storing items\n\n```ts\n// Store with a 10-second TTL:\nawait cache.put('key', 'value', 10);\n\n// Store indefinitely (no TTL):\nawait cache.put('key', 'value');\n\n// Store permanently (alias for put without TTL):\nawait cache.forever('key', 'value');\n```\n\n### Store only when missing\n\n```ts\n// Add only when missing; returns true if stored:\nconst added = await cache.add('key', 'value', 10);\n```\n\n### Deleting items\n\n```ts\n// Remove and return whether it existed:\nconst removed = await cache.forget('key');\n\n// Clear all cached entries:\nawait cache.flush();\n```\n\n### Special (compose read + write)\n\n```ts\n// Retrieve or compute and store for 60 seconds (TTL is in seconds):\nconst users = await cache.remember('users', 60, () =\u003e fetchUsers());\n\n// Store forever when missing:\nconst usersAlways = await cache.rememberForever('users', () =\u003e fetchUsers());\n\n// Retrieve and remove the cached value. Returns undefined when missing.\nconst pulled = await cache.pull('answer');\n\n// Provide a static default:\nconst staticDefault = await cache.pull('missing', 'default');\n\n// Provide a default (or factory) when missing:\nconst fallback = await cache.pull('missing', () =\u003e expensiveLookup());\n```\n\nIf the entry is missing or expired, the factory runs and the result is written to disk. Otherwise, the cached value is returned. `pull` removes the file after reading.\n\n### Introspection\n\n```ts\n// Get all cache keys\nconst keys = await cache.keys(); // ['user:1', 'user:2', 'session:abc']\n\n// Count cached items\nconst count = await cache.count(); // 3\n\n// Get total cache size in bytes\nconst bytes = await cache.size(); // 1024\nconsole.log(`Cache size: ${(bytes / 1024).toFixed(2)} KB`);\n```\n\n### Cleanup\n\n```ts\n// Remove expired entries (keeps valid ones)\nconst removed = await cache.prune();\nconsole.log(`Removed ${removed} expired entries`);\n\n// Clear everything (removes all entries)\nawait cache.flush();\n```\n\n### TTL management\n\n```ts\n// Get remaining time-to-live in seconds\nawait cache.put('session', data, 3600); // 1 hour\nconst ttl = await cache.ttl('session'); // e.g., 3599\n\n// Extend TTL of existing entry\nawait cache.touch('session', 7200); // Extend to 2 hours from now\n\n// Remove expiration\nawait cache.touch('session', Number.POSITIVE_INFINITY);\n```\n\n### Atomic counters\n\n```ts\n// Increment counters\nawait cache.increment('page-views');       // 1\nawait cache.increment('page-views');       // 2\nawait cache.increment('page-views', 10);   // 12\n\n// Decrement counters\nawait cache.put('credits', 100);\nawait cache.decrement('credits');          // 99\nawait cache.decrement('credits', 20);      // 79\n\n// Use together\nawait cache.increment('balance', 50);      // 50\nawait cache.decrement('balance', 10);      // 40\n```\n\n### Batch operations\n\nProcess multiple cache keys in a single operation:\n\n```ts\n// Store multiple key-value pairs at once\nawait cache.putMany({\n  'user:1': { name: 'Alice', role: 'admin' },\n  'user:2': { name: 'Bob', role: 'user' },\n  'user:3': { name: 'Charlie', role: 'user' },\n}, 3600); // Optional TTL applies to all\n\n// Retrieve multiple values\nconst users = await cache.getMany(['user:1', 'user:2', 'user:3']);\n// Returns: { 'user:1': {...}, 'user:2': {...}, 'user:3': {...} }\n\n// Missing keys return undefined\nconst partial = await cache.getMany(['user:1', 'missing', 'user:3']);\n// Returns: { 'user:1': {...}, 'missing': undefined, 'user:3': {...} }\n\n// Remove multiple keys\nconst removed = await cache.forgetMany(['user:1', 'user:2']);\n// Returns: 2 (number of keys actually removed)\n```\n\nBatch operations are ideal for:\n- Bulk user data loading\n- Multi-key cache warming\n- Batch invalidation\n- Reducing I/O overhead when working with multiple keys\n\n## Real-world Examples\n\n### Rate Limiting\n\n```ts\nconst cache = new FileCache\u003cnumber\u003e();\n\nasync function checkRateLimit(userId: string): Promise\u003cboolean\u003e {\n  const key = `rate-limit:${userId}`;\n  const requests = await cache.get(key, 0);\n\n  if (requests \u003e= 100) {\n    return false; // Rate limit exceeded\n  }\n\n  await cache.increment(key);\n  await cache.touch(key, 3600); // 1 hour window\n  return true;\n}\n```\n\n### API Response Caching\n\n```ts\nconst cache = new FileCache\u003cAPIResponse\u003e();\n\nasync function fetchUserProfile(userId: string) {\n  return await cache.remember(`user:${userId}`, 300, async () =\u003e {\n    const response = await fetch(`/api/users/${userId}`);\n    return response.json();\n  });\n}\n```\n\n### Session Storage\n\n```ts\nconst sessions = new FileCache\u003cSessionData\u003e();\n\nasync function createSession(userId: string, data: SessionData) {\n  const sessionId = generateId();\n  await sessions.put(sessionId, data, 86400); // 24 hours\n  return sessionId;\n}\n\nasync function extendSession(sessionId: string) {\n  await sessions.touch(sessionId, 86400); // Extend by 24 hours\n}\n```\n\n### Feature Flags\n\n```ts\nconst flags = new FileCache\u003cboolean\u003e();\n\nasync function isFeatureEnabled(feature: string): Promise\u003cboolean\u003e {\n  return await flags.remember(feature, 60, () =\u003e {\n    // Fetch from remote config service\n    return fetchFeatureFlag(feature);\n  });\n}\n```\n\n### Job Queue Deduplication\n\n```ts\nconst jobs = new FileCache\u003cstring\u003e();\n\nasync function enqueueJob(jobId: string, payload: any) {\n  const added = await jobs.add(`job:${jobId}`, payload, 3600);\n  if (!added) {\n    console.log('Job already queued');\n    return false;\n  }\n  return true;\n}\n```\n\n## Performance Characteristics\n\n### Read Performance\n- **Cache hit**: ~0.5-2ms (includes file I/O, JSON parsing, TTL check)\n- **Cache miss**: ~0.1-0.5ms (file existence check only)\n- Long keys (\u003e200 chars) have no performance penalty (hashed)\n\n### Write Performance\n- **Single write**: ~1-3ms (JSON serialization + file write)\n- **Atomic counters**: ~2-4ms (read + increment + write)\n- **Batch operations**: Linear with key count (delegates to individual operations)\n\n### Scalability\n- **Sweet spot**: 1-10,000 entries\n- **Memory footprint**: Minimal (only metadata in memory, values on disk)\n- **Disk usage**: ~100-500 bytes per entry (depends on value size)\n\n### Trade-offs\n- **Slower than in-memory** caches (Redis, node-cache) but survives restarts\n- **Faster than databases** for simple key-value operations\n- **Thread-safe reads** but not atomic across processes (see limitations)\n- **`has()` reads full file** to check expiration (accurate but slower)\n\n## Limitations\n\n### Thread Safety\n⚠️ **Not thread-safe across multiple processes**\n- Safe for single-process applications\n- Not suitable for multi-process/cluster mode without external locking\n- Race conditions possible with concurrent writes to the same key\n\n```ts\n// ❌ Unsafe in cluster mode\ncluster.fork();\nawait cache.increment('counter'); // Race condition!\n\n// ✅ Safe in single process\nawait cache.increment('counter');\n```\n\n### Filesystem Dependencies\n- **Requires write access** to cache directory\n- **Not suitable for serverless** with read-only filesystems (use `/tmp` with caution)\n- **Disk I/O** adds latency compared to in-memory caches\n- **No ACID guarantees** (writes are not transactional)\n\n### Platform-Specific\n- **File path limits**: Keys \u003e200 chars are hashed (irreversible)\n- **Case sensitivity**: Depends on filesystem (HFS+ vs ext4)\n- **Permissions**: Ensure cache directory is writable\n\n### Error Handling\n- **Silent failures** by design (returns defaults instead of throwing)\n- **No error callbacks** or logging hooks\n- Corrupted cache files are silently removed during operations\n\n### Not Suitable For\n- ❌ High-frequency writes (\u003e10K ops/sec)\n- ❌ Large values (\u003e1MB per entry)\n- ❌ Distributed systems without coordination\n- ❌ Critical data requiring persistence guarantees\n\n### Best Suited For\n- ✅ API response caching\n- ✅ Session storage\n- ✅ Rate limiting\n- ✅ Feature flags\n- ✅ Temporary computations\n- ✅ Single-server applications\n\n## How it works\n\n### FileCache\n- Files are stored under a cache directory (`\u003cos tmp\u003e/node-cache` by default)\n- Keys are URL-encoded to form the filename (e.g., key `answer` → `/tmp/node-cache/answer`)\n- Very long keys (\u003e200 chars) are SHA-256 hashed to prevent filesystem limits\n- Each file stores a JSON payload: `{ \"value\": \u003cdata\u003e, \"expiresAt\": \u003ctimestamp|undefined\u003e, \"key\": \"\u003coriginal\u003e\" }`\n- `get` reads and JSON-parses the file, deleting expired entries automatically\n- Writes use atomic temp file + rename for data safety\n\n### FlatFileCache\n- All entries stored in a single JSON file (`\u003cos tmp\u003e/newton-cache.json` by default)\n- File format: `{ \"key1\": { \"value\": \u003cdata\u003e, \"expiresAt\": \u003ctimestamp\u003e }, \"key2\": {...} }`\n- Lazy loading: cache loaded into memory on first access\n- **Every write operation (put, increment, forget) rewrites the entire file**\n- Expired entries auto-pruned during load and on access\n- Writes use atomic temp file + rename for data safety\n\n### MemoryCache\n- Data stored in JavaScript `Map` (in-memory only, lost on restart)\n- No filesystem I/O, fastest performance\n- Expired entries removed lazily on access or during `prune()`\n\n### All Adapters\n- `get` returns `undefined` or caller-provided default when missing/invalid/expired\n- `has` returns true only when entry exists, is not expired, and has a defined value\n- `remember` writes with `expiresAt` timestamp when TTL provided, omits for forever\n- `prune()` removes only expired entries; `flush()` removes everything\n- Counters (`increment`/`decrement`) are atomic within a single process and preserve existing TTL\n\n## Scripts\n\n- `npm run build` — compile TypeScript to `dist/`.\n- `npm test` — build then run Node's built-in test runner against compiled output.\n- `npm run clean` — remove build artifacts.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskydiver%2Fnewton-cache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskydiver%2Fnewton-cache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskydiver%2Fnewton-cache/lists"}