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.)
-
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.
-
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.
-
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.
Issue title
Issue body
Summary
Since
0.3.0(released 2026-02-09), this adapter unconditionally callsSUBSCRIBEwhen theRedisStreamsAdapteris constructed, and usesPUBLISHfor ephemeral cluster messages (broadcast-with-ack,serverSideEmit(),fetchSockets()) and fordoPublishResponse. 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 to0.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.tsline numbers reference the0.3.0tag.)Constructor — unconditional
SUBSCRIBE(lines 295–308):On a backend that rejects
SUBSCRIBEoutright, the adapter starts in a partially-initialized state. There is no option to skip this step.doPublish— ephemeral branch usesPUBLISH(lines 314–322):isEphemeral()coversBROADCASTwith arequestId,SERVER_SIDE_EMIT, andFETCH_SOCKETS.doPublishResponse— alwaysPUBLISH(lines 333–346):In
0.2.3all of these paths went throughXADD/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.When the flag is set:
SUBSCRIBE/SSUBSCRIBEin the constructor and does not create a sub-client.doPublishusesXADDfor every message type, regardless ofisEphemeral(message).doPublishResponseusesXADD(matches the0.2.xbehavior, which delegated todoPublish).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, anddoPublishResponse). The polling, encoding, and CSR paths all stay the same.Alternatives considered
0.2.3indefinitely: works for small deployments, but ages quickly as upstream security and bug fixes accumulate on the0.3.xline.SUBSCRIBE/PUBLISH: viable but duplicates upstream maintenance work and makes rebases brittle. Multiple users in the same situation each forking is the worst outcome.SUBSCRIBE/PUBLISH: hides the real failure mode, the adapter still behaves as if it has Pub/Sub, anddoPublishResponsewould silently drop responses toserverSideEmit()/fetchSockets().Backward compatibility
The proposed flag defaults to
false, which preserves the current0.3.xbehavior. Users on Redis (or compatible) backends with full Pub/Sub support are unaffected. Users opting in accept thatfetchSockets(),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.