콘텐츠로 이동
한국어

In-memory cache

이 콘텐츠는 아직 번역되지 않았습니다.

InMemoryCache is the default Cache implementation. It’s a Map with three things layered on top: LRU eviction bounded by maxEntries, lazy per-entry TTL, and an optional background sweep that reclaims expired-but-untouched entries. In-process, zero dependencies, lost on restart.

import { InMemoryCache, InMemoryCacheOptions } from 'actor-ts';
// Defaults: maxEntries 10 000, cleanupMs 60 000.
const cache = new InMemoryCache();
// Or tune it with the builder:
const cacheOptions = InMemoryCacheOptions.create()
.withMaxEntries(50_000)
.withCleanupMs(30_000);
const tuned = new InMemoryCache(cacheOptions);

A plain object is the shorthand alternative — new InMemoryCache({ maxEntries: 50_000, cleanupMs: 30_000 }). A TTL is still supplied per call (see TTL handling), not at construction time.

FieldBuilderDefaultMeaning
maxEntrieswithMaxEntries(n)10000LRU cap on stored entries. Infinity = unbounded.
cleanupMswithCleanupMs(ms)60000Background expired-entry sweep interval (ms). 0 / Infinity disables the sweep.

Values are validated once, at construction, on the merged settings: maxEntries must be a positive integer (or Infinity) and cleanupMs a non-negative number (or Infinity). A bad value throws OptionsError — the builder, a plain object, and HOCON all hit the same check.

Three scenarios:

  1. Tests — fast, no IO, clean teardown via close().
  2. Single-process production — one process, no need to share cache state across pods.
  3. Dev / local — the same code without Redis on the laptop.

For multi-process deployments, use Redis or Memcached instead — each process would otherwise keep its own independent copy.

InMemoryCache implements the full Cache surface:

await cache.set('user:1', user, 60_000); // value + optional TTL (ms)
const hit = await cache.get<User>('user:1'); // Option<User> — None on miss/expiry
await cache.setIfAbsent('lock:job', '1', 30_000); // true iff it was stored
await cache.incr('ratelimit:1.2.3.4', 60_000); // atomic ++, returns the new count
const many = await cache.mget<User>(['user:1', 'user:2']); // Map<string, User>
await cache.mset(new Map([['a', user1], ['b', user2]]), 60_000);
await cache.delete('user:1', 'user:2'); // one or many keys
await cache.close(); // stops the sweep + clears the Map

setIfAbsent is atomic here for a structural reason rather than a protocol one: its read and write sit in the same synchronous block, and the single-threaded event loop cannot interleave another caller between them. That holds within one process only — two Node processes each have their own Map, so a lock:job key guards nothing across them. For cross-process locking, use acquireLock over RedisCache.

get returns an Option<V>None on a miss or after expiry.

await cache.set('key', value, 60_000); // expires at now + 60s
await cache.set('key', value); // no TTL — lives until evicted/deleted

Expiry has two paths. It is lazy on access — an entry’s deadline is checked on every get, mget, incr, and setIfAbsent, and an expired entry is dropped at that point. A background sweep then runs every cleanupMs (default 60 s) to reclaim expired entries that are never touched again, so they don’t sit in the Map waiting for a read. Set cleanupMs to 0 / Infinity to turn the sweep off and rely on lazy expiry alone.

incr sets the TTL only when it creates the counter (the value becomes 1); later increments don’t refresh it — the right semantics for a fixed-window rate limiter.

InMemoryCache is LRU-bounded at maxEntries (default 10 000): inserting a new key beyond the cap evicts the least-recently-used entry. get/incr/setIfAbsent count as a use and move a key to the most-recently-used end, so hot keys survive and cold ones are evicted first. This is what keeps a flood of distinct, never-re-read keys — attacker-chosen Idempotency-Key or rate-limit keys, for instance — from growing the map without limit.

Set maxEntries: Infinity to opt out of eviction entirely. Only do this when you control the key space — an unbounded map OOMs the process eventually.

import { CacheExtensionId, InMemoryCache } from 'actor-ts';
// The extension's `default` cache is an InMemoryCache out of the box:
const cache = system.extension(CacheExtensionId).cache();
// Override the default, or register a separate named cache:
system.extension(CacheExtensionId).setCache('default', new InMemoryCache());
const sessions = system.extension(CacheExtensionId).cache('sessions');

system.extension(CacheExtensionId).cache(name) resolves a cache by name — the default name is an InMemoryCache unless you replace it (via setCache, registerCache, or the HOCON path actor-ts.cache.<name>.plugin). HTTP middleware, projection actors, and your own code then share one configured instance instead of each building its own.

The in-memory cache built by the extension reads its defaults from HOCON:

actor-ts.cache.in-memory {
maxEntries = 50000 # LRU cap
cleanupMs = 30000 # background sweep interval, ms (0 disables)
}

For a throwaway cache, just construct new InMemoryCache() directly — no extension needed.