NestJS — memory persistence
The shared-memory analog of NestJS's Redis WebSocket adapter: field regions mirror to a Redis hash so contract state survives restarts — and optionally replicate across processes over a pub/sub channel, so two app instances see each other's writes.
Wiring
The pool config's persistence option takes the adapter factory — the pool invokes it right after binding the contract and calls its stop() inside pool.terminate() (which AtollModule already runs on module destroy — the final flush lands there).
// shared-memory persistence — the Redis adapter as a pool option
import { AtollModule } from '@atolljs/nestjs';
import { redisMemoryAdapter, ioRedisSubscriber } from '@atolljs/node/redis';
import { incidentsMemory } from './incidents.contract';
// Static client in scope:
AtollModule.registerPool({
name: 'incidents',
worker: () => new Worker(new URL('./incidents.worker.ts', import.meta.url)),
sharedMemory: incidentsMemory,
persistence: redisMemoryAdapter(redis),
});
// Or inject the client — registerPoolAsync:
AtollModule.registerPoolAsync({
name: 'incidents',
useFactory: (redis: Redis) => ({
worker: () => new Worker(new URL('./incidents.worker.ts', import.meta.url)),
sharedMemory: incidentsMemory,
persistence: redisMemoryAdapter(redis, {
name: 'incidents', // hash key atoll:mem:incidents
syncIntervalMs: 100, // version-diff poll cadence
// Cross-process replication — a SECOND connection (pub/sub needs its own):
subscriber: ioRedisSubscriber(redis.duplicate()),
// fields: ['state.metrics', 'signals.seedProgress'], // persist a subset
}),
}),
inject: ['REDIS'],
});// attach by hand anywhere a bound contract exists — the pool
// keeps it alive (attach) and stops it (terminate → final flush)
import { persistSharedMemory } from '@atolljs/node/redis';
const persistence = persistSharedMemory(incidentsMemory, {
client: redis, // hset/hgetall/publish on base64 strings
name: 'incidents',
subscriber: ioRedisSubscriber(redis.duplicate()),
});
await persistence.ready; // initial HGETALL restored into the bufferHow it works
- The buffer stays the source of truth.
read()/write()remain synchronous memory ops — Redis sits behind the contract, never in front of it. - Per-field hash members —
atoll:mem:<name>holdspath → base64 bytesfor every field region (or thefieldssubset). - Version-diff flush — every connector write already bumps an Atomics counter; the adapter polls those counters on
syncIntervalMsandhsets only what moved. No instrumentation of the hot path. - Restore on attach —
readyresolves after the initialhgetallwrites bytes back into the buffer and bumps local versions, soobserve()watchers fire as if the writes were local.
Replication across instances
With subscriber set, each flush also publishes { src, path, b64 } to atoll:mem:<name>:ops. Subscribers write the bytes into their own buffer and bump the local version counter — same wake semantics as a local write. An instance id guards echoes (an applied remote write doesn't re-publish). Semantics are last-write-wins per field — Redis is coordination, not consensus; it suits dashboards/progress/read-models, not transactional state.
Caveats
- List fields only flush after
commit()— writes are detected via the version counter, same as observers. readyraces early worker binds: workers that bound before restore see zeros, then the version bumps land. Awaitreadybefore serving traffic that depends on restored state.- Pub/sub needs a dedicated connection —
ioRedisSubscriber(redis.duplicate())for ioredis; node-redis'ssubscribe(ch, cb)shape fitsRedisSubscriberdirectly.
The adapter itself is framework-free — Node.js → Persistence covers createNodePool usage and the client interface.