Node.js — memory persistence
@atolljs/node/redis persists a shared-memory contract's field regions to Redis — restart durability out of the box, and cross-process replication when a subscriber is attached. The SharedArrayBuffer stays the synchronous source of truth; Redis mirrors it behind the scenes.
Wiring
// persistence as a pool option — attaches after bind, stops on terminate()
import { createNodePool } from '@atolljs/node';
import { redisMemoryAdapter, ioRedisSubscriber } from '@atolljs/node/redis';
import { incidentsMemory } from './incidents.contract';
const pool = createNodePool({
worker: () => new Worker(new URL('./incidents.worker.ts', import.meta.url)),
sharedMemory: incidentsMemory,
persistence: redisMemoryAdapter(redis, {
name: 'incidents', // hash: atoll:mem:incidents
syncIntervalMs: 100,
subscriber: ioRedisSubscriber(redis.duplicate()), // optional replication
}),
});
await pool.persistence?.ready; // state restored into the bufferpersistence is a factory the pool invokes right after the contract binds — redisMemoryAdapter(client, opts) produces one. Or attach standalone with persistSharedMemory(memory, opts) anywhere a bound contract exists (e.g. an Express bootstrap that shares the buffer).
What lands in Redis
atoll:mem:<name>— a hash; member = field path (state.metrics), value = base64 of the field's byte region.atoll:mem:<name>:ops— the pub/sub channel; each dirty field publishes{ src, path, b64 }.- The version-counter block is not persisted — per-process coordination, not state.
Client surface
Three hash commands plus optional publish — deliberately narrow so any Redis client qualifies:
// the client surface — ioredis and node-redis both satisfy it
interface RedisHashClient {
hset(key: string, field: string, value: string): Promise<unknown>;
hgetall(key: string): Promise<Record<string, string>>;
publish?(channel: string, message: string): Promise<unknown>; // replication only
}
// values are base64 strings — any client works, no Buffer mode neededFor replication pass a subscriber: node-redis's subscribe(channel, listener) already matches; wrap an ioredis connection with ioRedisSubscriber(client).
Caveats
- Restore is async —
await persistence.ready(orpool.persistence?.ready) before serving traffic that depends on restored state. - List
writeAtonly becomes visible to the flush loop aftercommit()— same rule as observers. - Replication is last-write-wins per field — coordination, not consensus.
- One process's adapter instance does the flushing — workers write to the shared buffer, the main thread's adapter sees the version bumps.
NestJS usage — including injecting the client via registerPoolAsync — is under NestJS → Persistence.