npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fieldnotes/sync-redis

v0.3.0

Published

Redis-backed HubBackend for Field Notes real-time sync relay

Downloads

453

Readme

@fieldnotes/sync-redis

A HubBackend that persists Field Notes relay room state in Redis — state survives relay restarts and is shared across relay instances.

Install

pnpm add @fieldnotes/sync-redis
# plus your Redis client of choice, e.g.:
pnpm add redis        # node-redis v4
# or
pnpm add ioredis

@fieldnotes/sync-redis has no Redis dependency of its own — you inject a client that satisfies a minimal RedisHashClient interface (hGetAll / hGet / hSet / hDel / del).

node-redis v4 (direct)

node-redis v4's client already matches RedisHashClient, so you can pass it straight through:

import { createClient } from 'redis';
import { createSyncServer } from '@fieldnotes/sync-server';
import { RedisHubBackend } from '@fieldnotes/sync-redis';

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
createSyncServer({ port: 8080, backend: new RedisHubBackend(client) });

ioredis (shim)

ioredis uses lowercased method names, so wrap it in a small adapter:

import Redis from 'ioredis';
import { RedisHubBackend } from '@fieldnotes/sync-redis';

const io = new Redis(process.env.REDIS_URL);
const client = {
  hGetAll: (k: string) => io.hgetall(k),
  hGet: (k: string, f: string) => io.hget(k, f),
  hSet: (k: string, f: string, v: string) => io.hset(k, f, v),
  hDel: (k: string, f: string) => io.hdel(k, f),
  del: (k: string) => io.del(k),
};
const backend = new RedisHubBackend(client);

Key schema

Each room is stored as a Redis HASH at {keyPrefix}{room} (default prefix fieldnotes:room:):

  • field = element id
  • value = JSON.stringify(element)

node-redis v4 conforms to RedisHashClient directly (it has hGet); RedisHubBackend.get(room, id) (via HGET) powers the relay's ownership lookups for write authorization (D2).

The prefix is configurable:

new RedisHubBackend(client, { keyPrefix: 'myapp:room:' });

Single vs. multiple relay instances

Persistence and shared state work with any number of relay instances — every instance reads and writes the same Redis, so room state survives restarts and is visible to all of them.

For clients connected to different relay instances to see each other's live ops, you also need cross-instance fan-out (Redis pub/sub) — available via RedisHubFanout (below). A single relay instance is fully live on its own.

Cross-instance fan-out (RedisHubFanout)

RedisHubFanout is a HubFanout (from @fieldnotes/sync-server) over Redis pub/sub: each relay instance publishes its live ops to a single channel and every other instance forwards them to its local connections. Like the backend, it has no Redis dependency of its own — you inject two connections via the RedisPublisher / RedisSubscriber seams.

The subscriber MUST be a dedicated connection. Redis forbids any other command on a connection that is in subscribe mode, so publish and subscribe cannot share one connection — that is why RedisHubFanout takes two.

node-redis v4

import { createClient } from 'redis';
import { createSyncServer } from '@fieldnotes/sync-server';
import { RedisHubBackend, RedisHubFanout } from '@fieldnotes/sync-redis';

const hash = createClient({ url: process.env.REDIS_URL });
await hash.connect(); // HASH backend (persistence)

const publisher = createClient({ url: process.env.REDIS_URL });
await publisher.connect();
const subscriber = publisher.duplicate();
await subscriber.connect(); // DEDICATED subscriber — a subscriber connection cannot also publish

createSyncServer({
  port: 8080,
  backend: new RedisHubBackend(hash),
  fanout: new RedisHubFanout(publisher, subscriber),
});

ioredis

ioredis lowercases publish/subscribe and delivers messages via a 'message' event, so wrap each client:

import Redis from 'ioredis';
import { RedisHubFanout } from '@fieldnotes/sync-redis';

const pubIo = new Redis(process.env.REDIS_URL); // publish connection
const subIo = new Redis(process.env.REDIS_URL); // SECOND, dedicated subscriber connection

const publisher = { publish: (c: string, m: string) => pubIo.publish(c, m) };
const subscriber = {
  subscribe: (channel: string, listener: (m: string) => void) => {
    subIo.on('message', (ch, msg) => {
      if (ch === channel) listener(msg);
    });
    return subIo.subscribe(channel);
  },
};

const fanout = new RedisHubFanout(publisher, subscriber);

Channel

All instances must share one channel (default fieldnotes:fanout, configurable):

new RedisHubFanout(publisher, subscriber, { channel: 'myapp:fanout' });

Precondition: shared fanout AND shared backend

Multi-instance live sync requires both a shared fanout and a shared backend (both Redis). The fanout forwards live ops; the shared backend keeps snapshots consistent. A shared fanout without a shared backend leaves a new joiner's snapshot stale — it would catch up from whichever instance it happened to hit, missing edits applied elsewhere. Pair RedisHubFanout with RedisHubBackend for full multi-instance real-time sync.