Write now. Sync in 50ms.

One map.
Every process.

A tiny TypeScript library that gives each Node.js process a fast local Map—then coalesces and synchronizes changes through Redis.

$ npm i redis-dist-map ioredis
Quick start
API · 01local Map
42
WORKER · 02local Map
42
EDGE · 03local Map
42
REDIS Hash + Stream

EVENT 18446744073709551615-0 SYNCED

SYNC READS BATCHED WRITES WRITER-ONLY MODE ORDERED EVENTS FULLY TYPED
01 / HOW

Local on the hot path.
Batched on the wire.

Reads and writes touch memory immediately. Redis receives one coalesced, durable patch every 50 milliseconds.

01

Read at RAM speed

get, has, size, and iteration are synchronous operations on an in-process Map.

02

Coalesce, then commit

Repeated updates collapse by key, then the Hash and ten-second Stream history update in one Redis transaction.

03

Converge everywhere

Blocking Stream readers apply ordered events to every live instance. Revision gaps trigger a snapshot only when recovery is needed.

02 / MARKET DATA

Absorb the firehose.
Serve from memory.

Let one process handle the market feed. Every API instance gets the latest price in memory—without polling or a Redis call per read.

SHARED VALUE PriceTick { ask: number; bid: number; volume: number }
SERVER 01 MARKET FEED
ABSORB THE FEED

Keep the hot side lightweight.

Publish only. No snapshot, local Map, or XREAD connection.

import { createDistributedMapWriter } from
  "redis-dist-map";

const writer = createDistributedMapWriter<PriceTick>(
  "market:prices",
  { client: redis },
);

feed.on("tick", ({ symbol, ask, bid, volume }) => {
  writer.set(symbol, { ask, bid, volume });
});
AAPL TICK RECEIVED 213.43 ASK
SERVER 02 PRICING API
SERVE FROM RAM

Keep the request path fast.

Stream updates arrive before the next customer request.

import { createDistributedMap } from
  "redis-dist-map";

const tickers = await createDistributedMap<PriceTick>(
  "market:prices",
  { client: redis },
);

// synchronous local read — no Redis round trip
const ask = tickers.get("AAPL")?.ask;
AAPL · LOCAL MAP 213.43 ASK

Ingest once. Fan out automatically. Every pricing endpoint reads the latest value at local Map speed.

03 / ROUTE CACHE

Miss once.
Hit everywhere.

Whichever API instance fills the cache first shares that response. Every other instance can answer from its own memory.

SHARED VALUE TickerPage { title: string; summary: string }
API INSTANCE 01 SAME ROUTE
FIRST REQUEST · MISS

One instance pays the miss.

Load the response, return it, and share it automatically.

import { createDistributedMap } from
  "redis-dist-map";

const cache = await createDistributedMap<TickerPage>(
  "cache:ticker-pages",
  { client: redis },
);

app.get("/ticker/:name", async (request) => {
  const { name } = request.params;
  const hit = cache.get(name);
  if (hit !== undefined) return hit;

  const ticker = await db.getWikipage(name);
  cache.set(name, ticker);
  return ticker;
});
/TICKER/AAPL · DB LOAD SHARED
API INSTANCE 02…N SAME ROUTE
NEXT REQUEST · HIT

Every instance inherits the hit.

The load balancer can send the next request anywhere.

import { createDistributedMap } from
  "redis-dist-map";

const cache = await createDistributedMap<TickerPage>(
  "cache:ticker-pages",
  { client: redis },
);

app.get("/ticker/:name", async (request) => {
  const { name } = request.params;

  // already synced from instance 01
  const hit = cache.get(name);
  if (hit !== undefined) return hit;

  const ticker = await db.getWikipage(name);
  cache.set(name, ticker);
  return ticker;
});
/TICKER/AAPL · LOCAL MAP <1MS HIT

Same route, every instance. One cache fill turns into local hits across the whole API fleet.

04 / RATE LIMIT

Count once.
Enforce everywhere.

Every API instance increments the same atomic counter. Request 101 is rejected wherever the load balancer sends it.

ATOMIC PRIMITIVE SharedCounter { inc(key): Promise<number> }
API INSTANCE 01…N REQUEST MIDDLEWARE
ONE SHARED WINDOW

Let Redis make every request count.

No local guess, write buffer, or synchronization delay.

import { createSharedCounter } from
  "redis-dist-map";

const requests = createSharedCounter(
  "rate-limit:api",
  { client: redis, ttlMs: 60_000 },
);

app.use(async (req, res, next) => {
  const current = await requests.inc(req.user.id);

  if (current > 100) {
    return res.status(429).send({
      error: "RATE_LIMITED",
      ok: false,
    });
  }

  next();
});
USER 42 · REQUEST 100 ALLOW
GLOBAL DECISION FIXED WINDOW
REQUEST 101 · BLOCKED

The limit follows the user.

Any server sees the same count and returns the same decision.

KEYrate-limit:api / user-42
COUNT101 / 100
WINDOW60,000 MS
HTTP RESPONSE 429 { "error": "RATE_LIMITED", "ok": false }
USER 42 · REQUEST 101 BLOCK

INCR and first-write expiry run in one Lua script. No increment is buffered, coalesced, or lost between API instances.

05 / START

Reactive state
without polling.

Bring an ioredis client. Pick a namespaced key. The first snapshot and background listener are ready when the promise resolves.

  • ESM and CommonJS
  • TypeScript declarations included
  • Reactive key subscriptions
  • No-sync writer-only mode
feature-flags.ts
import Redis from "ioredis";
import { createDistributedMap } from
  "redis-dist-map";

const redis = new Redis(process.env.REDIS_URL);

const flags = await createDistributedMap<boolean>(
  "app:prod:flags",
  { client: redis },
);

flags.onChange("new-checkout", (enabled) =>
  console.log(enabled)
);

flags.set("new-checkout", true);

// synchronous — no Redis round trip
flags.get("new-checkout"); // true

// explicit durability boundary
await flags.flush();
TYPESCRIPT UTF-8   LN 15, COL 41
06 / API

Small surface.
Familiar shape.

createSharedCounter()→ counter

Create an atomic Redis counter namespace.

counter.inc(key)→ Promise<number>

Increment immediately with optional first-write expiry.

.get(key)→ T | undefined

Read from the local cache.

.onChange(key, fn)→ unsubscribe()

React to local and remote changes.

.set(key, value)→ void

Update locally and queue a value.

.delete(key)→ boolean

Delete locally and queue the key.

.clear()→ void

Clear locally and queue the reset.

.flush()→ Promise<void>

Persist and broadcast pending changes.

.synchronize()→ Promise<void>

Reload the Redis snapshot.

.destroy()→ Promise<void>

Stop listening and clean up.

createDistributedMapWriter()→ writer

Publish without a snapshot or XREAD connection.

READY?

Keep your state
close.

One familiar abstraction. Every process in sync.

Copied to clipboard