Skip to content

Add a streams-only mode (skip SUBSCRIBE/PUBLISH) for environments without Pub/Sub support #46

Description

@manyfun99

Issue title

Add a streams-only mode (skip SUBSCRIBE/PUBLISH) for environments without Pub/Sub support

Issue body

Summary

Since 0.3.0 (released 2026-02-09), this adapter unconditionally calls SUBSCRIBE when the RedisStreamsAdapter is constructed, and uses PUBLISH for ephemeral cluster messages (broadcast-with-ack, serverSideEmit(), fetchSockets()) and for doPublishResponse. This breaks the adapter on Redis-compatible deployments that intentionally do not support Pub/Sub.

The adapter previously (0.2.x) operated entirely on Redis streams, which is the property that originally made it the only viable choice for those environments. I'd like to ask whether the maintainers would accept a flag that restores that streams-only behavior.

Motivation

We run Socket.IO on a Redis-compatible cluster at production scale. It does not support the Pub/Sub command family (SUBSCRIBE, PUBLISH, PSUBSCRIBE, PUNSUBSCRIBE, UNSUBSCRIBE, PUBSUB) and there are no plans to add it to the current version. The official guidance to teams in this position is to use Redis streams instead, which is exactly what this adapter offers up to 0.2.3.

Other Redis-compatible deployments are also stream-friendly but Pub/Sub-restricted (for example, hosted Redis variants where Pub/Sub is disabled or charged at a different tier, or proxied clusters where Pub/Sub is unstable across shards). Each of these benefits from a single supported escape hatch in this adapter rather than maintaining a fork.

Where the Pub/Sub coupling lives in 0.3.0

(lib/adapter.ts line numbers reference the 0.3.0 tag.)

  1. Constructor — unconditional SUBSCRIBE (lines 295–308):

    subClientPromise.then((subClient) => {
      (this.#opts.useShardedPubSub ? SSUBSCRIBE : SUBSCRIBE)(
        subClient,
        [this.#publicChannel, privateChannel],
        (payload: Buffer) => { /* ... */ }
      );
    });

    On a backend that rejects SUBSCRIBE outright, the adapter starts in a partially-initialized state. There is no option to skip this step.

  2. doPublish — ephemeral branch uses PUBLISH (lines 314–322):

    if (isEphemeral(message)) {
      // ephemeral messages are sent with Redis PUB/SUB
      const payload = Buffer.from(encode(message));
      (this.#opts.useShardedPubSub ? SPUBLISH : PUBLISH)(
        this.#redisClient,
        this.#publicChannel,
        payload
      );
      return Promise.resolve("");
    }
    return XADD(/* ... */);

    isEphemeral() covers BROADCAST with a requestId, SERVER_SIDE_EMIT, and FETCH_SOCKETS.

  3. doPublishResponse — always PUBLISH (lines 333–346):

    protected doPublishResponse(requesterUid, response): Promise<void> {
      const responseChannel = `${this.#opts.channelPrefix}#${this.nsp.name}#${requesterUid}#`;
      const payload = Buffer.from(encode(response));
      return (this.#opts.useShardedPubSub ? SPUBLISH : PUBLISH)(
        this.#redisClient, responseChannel, payload
      ).then();
    }

In 0.2.3 all of these paths went through XADD/XREAD, which is what made the adapter compatible with stream-only backends.

Proposal

Add a single boolean option that selects the 0.2.x-style streams-only behavior. Working name: useStreamsForEphemeralMessages.

io.adapter(
  createAdapter(redisClient, {
    useStreamsForEphemeralMessages: true, // default false: current 0.3.x behavior
  })
);

When the flag is set:

  • The adapter does not call SUBSCRIBE/SSUBSCRIBE in the constructor and does not create a sub-client.
  • doPublish uses XADD for every message type, regardless of isEphemeral(message).
  • doPublishResponse uses XADD (matches the 0.2.x behavior, which delegated to doPublish).
  • The trade-off — slightly higher latency for fetchSockets() / serverSideEmit() / broadcast-with-ack — is accepted by the consumer who opted in. This should be documented in the README.

I think this can be expressed as a small change confined to lib/adapter.ts (the constructor, doPublish, and doPublishResponse). The polling, encoding, and CSR paths all stay the same.

Alternatives considered

  • Pin to 0.2.3 indefinitely: works for small deployments, but ages quickly as upstream security and bug fixes accumulate on the 0.3.x line.
  • Maintain a private fork that strips SUBSCRIBE/PUBLISH: viable but duplicates upstream maintenance work and makes rebases brittle. Multiple users in the same situation each forking is the worst outcome.
  • Wrap the Redis client to swallow SUBSCRIBE/PUBLISH: hides the real failure mode, the adapter still behaves as if it has Pub/Sub, and doPublishResponse would silently drop responses to serverSideEmit() / fetchSockets().

Backward compatibility

The proposed flag defaults to false, which preserves the current 0.3.x behavior. Users on Redis (or compatible) backends with full Pub/Sub support are unaffected. Users opting in accept that fetchSockets(), serverSideEmit(), and broadcast-with-ack go through streams instead of Pub/Sub.

Willing to contribute

I'd be happy to send a PR with the option, the README update, and a test that exercises the streams-only path. Before I do, would the maintainers accept this direction in principle? I want to avoid sending a PR that's structurally rejected.

Thanks for the work on this adapter — it's the only thing that made our setup possible at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions