@absolutejs/sync-bus-redis 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +10 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @absolutejs/sync-bus-redis
|
|
2
|
+
|
|
3
|
+
Redis pub/sub `ClusterBus` for
|
|
4
|
+
[@absolutejs/sync](https://github.com/absolutejs/sync). Sibling to
|
|
5
|
+
[`@absolutejs/sync-bus-pg`](../bus-pg) — same `ClusterBus` contract,
|
|
6
|
+
different transport.
|
|
7
|
+
|
|
8
|
+
## When to use Redis vs Postgres
|
|
9
|
+
|
|
10
|
+
| Concern | `sync-bus-redis` | `sync-bus-pg` |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| Payload size | No cap — JSON through | 8KB NOTIFY cap (spill-table fallback) |
|
|
13
|
+
| Fan-out latency at 10+ subscribers | In-memory, low | WAL-replicated, higher |
|
|
14
|
+
| Geo-replication | Native (Redis Cluster, ElastiCache, Memorystore, Upstash) | Heavy ops (PG logical replication) |
|
|
15
|
+
| Delivery semantics | At-most-once (no message retention) | At-most-once (NOTIFY) + spill rows for oversized |
|
|
16
|
+
| Already in your stack? | If yes → win-win | If yes → win-win |
|
|
17
|
+
|
|
18
|
+
**The headline tradeoff.** Redis is in-memory pub/sub — a subscriber
|
|
19
|
+
that's disconnected when a message fires misses it. For
|
|
20
|
+
cross-instance resume past shard reboot, pair with
|
|
21
|
+
`engine.exportChangeLog()` / `importChangeLog()` (sync 1.19.0+)
|
|
22
|
+
regardless of which bus you use.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
bun add @absolutejs/sync @absolutejs/sync-bus-redis
|
|
28
|
+
# Plus a Redis client — bring your own:
|
|
29
|
+
bun add ioredis # OR
|
|
30
|
+
bun add redis # node-redis v4+
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The adapter has no hard runtime dep on a specific Redis client.
|
|
34
|
+
|
|
35
|
+
## Usage with `ioredis`
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { Redis } from 'ioredis';
|
|
39
|
+
import { createSyncEngine } from '@absolutejs/sync/engine';
|
|
40
|
+
import { createRedisClusterBus } from '@absolutejs/sync-bus-redis';
|
|
41
|
+
|
|
42
|
+
const publisher = new Redis(process.env.REDIS_URL!);
|
|
43
|
+
const subscriberClient = new Redis(process.env.REDIS_URL!);
|
|
44
|
+
|
|
45
|
+
// ioredis: a subscribed connection can't issue other commands.
|
|
46
|
+
// Bridge its EventEmitter API into our (channel, listener) shape:
|
|
47
|
+
const subscriber = {
|
|
48
|
+
subscribe: async (channel: string, listener: (msg: string) => void) => {
|
|
49
|
+
await subscriberClient.subscribe(channel);
|
|
50
|
+
const handler = (chan: string, msg: string) => {
|
|
51
|
+
if (chan === channel) listener(msg);
|
|
52
|
+
};
|
|
53
|
+
subscriberClient.on('message', handler);
|
|
54
|
+
return async () => {
|
|
55
|
+
subscriberClient.off('message', handler);
|
|
56
|
+
await subscriberClient.unsubscribe(channel);
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const bus = createRedisClusterBus({ publisher, subscriber });
|
|
62
|
+
const engine = createSyncEngine({ instanceId: 'shard-A' });
|
|
63
|
+
await engine.connectCluster(bus);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage with `node-redis` v4+
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { createClient } from 'redis';
|
|
70
|
+
import { createRedisClusterBus } from '@absolutejs/sync-bus-redis';
|
|
71
|
+
|
|
72
|
+
const publisher = createClient({ url: process.env.REDIS_URL });
|
|
73
|
+
const subscriberClient = publisher.duplicate();
|
|
74
|
+
await Promise.all([publisher.connect(), subscriberClient.connect()]);
|
|
75
|
+
|
|
76
|
+
// node-redis: subscribe takes a callback directly.
|
|
77
|
+
const subscriber = {
|
|
78
|
+
subscribe: async (channel: string, listener: (msg: string) => void) => {
|
|
79
|
+
await subscriberClient.subscribe(channel, listener);
|
|
80
|
+
return async () => {
|
|
81
|
+
await subscriberClient.unsubscribe(channel);
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const bus = createRedisClusterBus({ publisher, subscriber });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## API
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
createRedisClusterBus({
|
|
93
|
+
publisher, // RedisPublisher — publish(channel, message)
|
|
94
|
+
subscriber, // RedisSubscriber — subscribe(channel, listener) → unsubscribe fn
|
|
95
|
+
channel?, // default 'absolutejs_sync_cluster'
|
|
96
|
+
onError?, // logger for parse / delivery failures
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Returns `ClusterBus & { metrics() }`. Pass to `engine.connectCluster(bus)`.
|
|
101
|
+
|
|
102
|
+
### `bus.metrics()`
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
{
|
|
106
|
+
published: number; // PUBLISH calls
|
|
107
|
+
received: number; // messages parsed + delivered to onMessage
|
|
108
|
+
publishErrors: number; // publisher.publish() rejections
|
|
109
|
+
subscribeErrors: number; // JSON.parse failures on incoming messages
|
|
110
|
+
totalSubscribersReached: number; // sum of counts Redis returned from PUBLISH
|
|
111
|
+
// — a drop to 0 when you expect peers signals
|
|
112
|
+
// subscriber disconnects (partition, restart)
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
[Apache 2.0](../LICENSE). Tier B substrate-adjacent under the
|
|
119
|
+
AbsoluteJS licensing policy — rides `@absolutejs/sync` (BSL Tier A).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @absolutejs/sync-bus-redis — Redis pub/sub `ClusterBus` for
|
|
3
|
+
* `@absolutejs/sync`. Cross-instance fan-out via PUBLISH/SUBSCRIBE.
|
|
4
|
+
*
|
|
5
|
+
* **Why Redis (vs the `sync-bus-pg` sibling).**
|
|
6
|
+
*
|
|
7
|
+
* - **No payload cap.** Postgres `NOTIFY` is capped at 8000 bytes,
|
|
8
|
+
* forcing `sync-bus-pg` into a spill-table dance for large batches.
|
|
9
|
+
* Redis PUBLISH has no equivalent limit — JSON straight through.
|
|
10
|
+
* - **Lower latency at fan-out.** Redis pub/sub is in-memory; PG
|
|
11
|
+
* `LISTEN/NOTIFY` rides the WAL replication path. For multi-shard
|
|
12
|
+
* fan-out (10+ subscribers), Redis is typically faster.
|
|
13
|
+
* - **Better geo-replication story.** Redis Cluster / Redis
|
|
14
|
+
* Enterprise / managed Redis (ElastiCache, Memorystore, Upstash)
|
|
15
|
+
* all geo-replicate natively. Postgres logical replication is
|
|
16
|
+
* possible but heavier ops.
|
|
17
|
+
*
|
|
18
|
+
* **Why NOT Redis (when to use `sync-bus-pg` instead).**
|
|
19
|
+
*
|
|
20
|
+
* - **At-most-once delivery.** Redis pub/sub does NOT durably store
|
|
21
|
+
* messages. A subscriber that's disconnected when a message fires
|
|
22
|
+
* misses it. If you need a durable cluster bus for replay across
|
|
23
|
+
* shard reboot, use `sync-bus-pg` (which has the spill table) and
|
|
24
|
+
* pair with `engine.exportChangeLog()` / `importChangeLog()` for
|
|
25
|
+
* post-reboot resume.
|
|
26
|
+
* - **Already have Postgres in your stack.** Adding Redis just for
|
|
27
|
+
* the cluster bus is a new dependency to operate.
|
|
28
|
+
*
|
|
29
|
+
* **Client compatibility.**
|
|
30
|
+
*
|
|
31
|
+
* The adapter takes a narrow interface (`publisher.publish(channel,
|
|
32
|
+
* message)` and `subscriber.subscribe(channel, listener)`) so it works
|
|
33
|
+
* with any 2026-era Redis client:
|
|
34
|
+
*
|
|
35
|
+
* - `ioredis`: `const redis = new Redis(url); const sub = redis.duplicate();`
|
|
36
|
+
* - `node-redis` v4+: `const client = createClient({ url }); const sub = client.duplicate();`
|
|
37
|
+
*
|
|
38
|
+
* Each pub/sub-using consumer needs TWO connections — one for
|
|
39
|
+
* publishing (your existing client), one for subscribing (a duplicate
|
|
40
|
+
* dedicated to the subscription, because Redis prohibits other
|
|
41
|
+
* commands on a subscribed connection). Pass both to
|
|
42
|
+
* `createRedisClusterBus({ publisher, subscriber })`.
|
|
43
|
+
*
|
|
44
|
+
* See `README.md` for `ioredis` and `node-redis` adapter snippets.
|
|
45
|
+
*/
|
|
46
|
+
import type { ClusterBus } from '@absolutejs/sync/engine';
|
|
47
|
+
/**
|
|
48
|
+
* Minimal Redis publisher contract. Both `ioredis` and `node-redis` v4
|
|
49
|
+
* structurally satisfy this (`publish(channel, message)` is the
|
|
50
|
+
* canonical signature; both return a Promise that resolves to the
|
|
51
|
+
* number of subscribers that received the message).
|
|
52
|
+
*/
|
|
53
|
+
export type RedisPublisher = {
|
|
54
|
+
publish: (channel: string, message: string) => Promise<number | unknown>;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Minimal Redis subscriber contract. The shape diverges between
|
|
58
|
+
* ioredis (EventEmitter-based) and node-redis (callback-based);
|
|
59
|
+
* `RedisSubscriber` is the lowest-common-denominator:
|
|
60
|
+
*
|
|
61
|
+
* - `subscribe(channel, listener)` registers `listener` for messages
|
|
62
|
+
* on `channel` AND returns an unsubscribe function. The listener
|
|
63
|
+
* receives the raw message string.
|
|
64
|
+
*
|
|
65
|
+
* The README shows how to wrap ioredis (where you'd call
|
|
66
|
+
* `subscriber.on('message', handler)` once and route by channel) and
|
|
67
|
+
* node-redis (where `subscribe(channel, listener)` is the SDK call
|
|
68
|
+
* directly).
|
|
69
|
+
*/
|
|
70
|
+
export type RedisSubscriber = {
|
|
71
|
+
subscribe: (channel: string, listener: (message: string) => void) => Promise<() => Promise<void>>;
|
|
72
|
+
};
|
|
73
|
+
export type CreateRedisClusterBusOptions = {
|
|
74
|
+
/** The Redis client used for `publish()` calls. */
|
|
75
|
+
publisher: RedisPublisher;
|
|
76
|
+
/** The Redis subscriber. MUST be a dedicated connection (a
|
|
77
|
+
* `duplicate()` of your main client) — Redis forbids other
|
|
78
|
+
* commands on a subscribed connection. */
|
|
79
|
+
subscriber: RedisSubscriber;
|
|
80
|
+
/**
|
|
81
|
+
* Channel name. Defaults to `'absolutejs_sync_cluster'`. Two
|
|
82
|
+
* engines on the same Redis can scope themselves to different
|
|
83
|
+
* channels by overriding this.
|
|
84
|
+
*/
|
|
85
|
+
channel?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Called when message parsing / delivery fails. Defaults to
|
|
88
|
+
* `console.warn`. Note that Redis pub/sub doesn't surface delivery
|
|
89
|
+
* failures on the publisher side — `publish()` resolves to the
|
|
90
|
+
* count of subscribers that received the message (or 0); a 0 count
|
|
91
|
+
* does NOT fire onError (Redis treats "no subscribers" as success).
|
|
92
|
+
*/
|
|
93
|
+
onError?: (error: unknown) => void;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Cumulative metrics since `createRedisClusterBus()`. Same shape
|
|
97
|
+
* style as `sync-bus-pg`'s — minus the spill-table fields (Redis
|
|
98
|
+
* has no equivalent).
|
|
99
|
+
*/
|
|
100
|
+
export type RedisClusterBusMetrics = {
|
|
101
|
+
published: number;
|
|
102
|
+
received: number;
|
|
103
|
+
publishErrors: number;
|
|
104
|
+
subscribeErrors: number;
|
|
105
|
+
/**
|
|
106
|
+
* Cumulative count of subscribers reached on the publisher side.
|
|
107
|
+
* Returned by Redis's PUBLISH. Use it as a rough "is the cluster
|
|
108
|
+
* still wired up" signal — a drop to 0 when you expect peers means
|
|
109
|
+
* subscribers disconnected (replication lag, network partition).
|
|
110
|
+
*/
|
|
111
|
+
totalSubscribersReached: number;
|
|
112
|
+
};
|
|
113
|
+
export type RedisClusterBus = ClusterBus & {
|
|
114
|
+
metrics: () => RedisClusterBusMetrics;
|
|
115
|
+
};
|
|
116
|
+
export declare const createRedisClusterBus: (options: CreateRedisClusterBusOptions) => RedisClusterBus;
|
|
117
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,yBAAyB,CAAC;AAE1E;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAC5B,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC;CACzE,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,eAAe,GAAG;IAC7B,SAAS,EAAE,CACV,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,KAC/B,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IAC1C,mDAAmD;IACnD,SAAS,EAAE,cAAc,CAAC;IAC1B;;+CAE2C;IAC3C,UAAU,EAAE,eAAe,CAAC;IAC5B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,uBAAuB,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG;IAC1C,OAAO,EAAE,MAAM,sBAAsB,CAAC;CACtC,CAAC;AAEF,eAAO,MAAM,qBAAqB,GACjC,SAAS,4BAA4B,KACnC,eA+CF,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
var createRedisClusterBus = (options) => {
|
|
4
|
+
const channel = options.channel ?? "absolutejs_sync_cluster";
|
|
5
|
+
const onError = options.onError ?? ((error) => console.warn("[sync-bus-redis]", error));
|
|
6
|
+
const counters = {
|
|
7
|
+
publishErrors: 0,
|
|
8
|
+
published: 0,
|
|
9
|
+
received: 0,
|
|
10
|
+
subscribeErrors: 0,
|
|
11
|
+
totalSubscribersReached: 0
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
metrics: () => ({ ...counters }),
|
|
15
|
+
publish: async (message) => {
|
|
16
|
+
try {
|
|
17
|
+
const serialized = JSON.stringify(message);
|
|
18
|
+
const result = await options.publisher.publish(channel, serialized);
|
|
19
|
+
if (typeof result === "number") {
|
|
20
|
+
counters.totalSubscribersReached += result;
|
|
21
|
+
}
|
|
22
|
+
counters.published += 1;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
counters.publishErrors += 1;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
subscribe: async (onMessage) => {
|
|
29
|
+
const listener = (raw) => {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
counters.received += 1;
|
|
33
|
+
onMessage(parsed);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
counters.subscribeErrors += 1;
|
|
36
|
+
onError(error);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return options.subscriber.subscribe(channel, listener);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export {
|
|
44
|
+
createRedisClusterBus
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//# debugId=0C163B79C3CB22BF64756E2164756E21
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * @absolutejs/sync-bus-redis — Redis pub/sub `ClusterBus` for\n * `@absolutejs/sync`. Cross-instance fan-out via PUBLISH/SUBSCRIBE.\n *\n * **Why Redis (vs the `sync-bus-pg` sibling).**\n *\n * - **No payload cap.** Postgres `NOTIFY` is capped at 8000 bytes,\n * forcing `sync-bus-pg` into a spill-table dance for large batches.\n * Redis PUBLISH has no equivalent limit — JSON straight through.\n * - **Lower latency at fan-out.** Redis pub/sub is in-memory; PG\n * `LISTEN/NOTIFY` rides the WAL replication path. For multi-shard\n * fan-out (10+ subscribers), Redis is typically faster.\n * - **Better geo-replication story.** Redis Cluster / Redis\n * Enterprise / managed Redis (ElastiCache, Memorystore, Upstash)\n * all geo-replicate natively. Postgres logical replication is\n * possible but heavier ops.\n *\n * **Why NOT Redis (when to use `sync-bus-pg` instead).**\n *\n * - **At-most-once delivery.** Redis pub/sub does NOT durably store\n * messages. A subscriber that's disconnected when a message fires\n * misses it. If you need a durable cluster bus for replay across\n * shard reboot, use `sync-bus-pg` (which has the spill table) and\n * pair with `engine.exportChangeLog()` / `importChangeLog()` for\n * post-reboot resume.\n * - **Already have Postgres in your stack.** Adding Redis just for\n * the cluster bus is a new dependency to operate.\n *\n * **Client compatibility.**\n *\n * The adapter takes a narrow interface (`publisher.publish(channel,\n * message)` and `subscriber.subscribe(channel, listener)`) so it works\n * with any 2026-era Redis client:\n *\n * - `ioredis`: `const redis = new Redis(url); const sub = redis.duplicate();`\n * - `node-redis` v4+: `const client = createClient({ url }); const sub = client.duplicate();`\n *\n * Each pub/sub-using consumer needs TWO connections — one for\n * publishing (your existing client), one for subscribing (a duplicate\n * dedicated to the subscription, because Redis prohibits other\n * commands on a subscribed connection). Pass both to\n * `createRedisClusterBus({ publisher, subscriber })`.\n *\n * See `README.md` for `ioredis` and `node-redis` adapter snippets.\n */\n\nimport type { ClusterBus, ClusterMessage } from '@absolutejs/sync/engine';\n\n/**\n * Minimal Redis publisher contract. Both `ioredis` and `node-redis` v4\n * structurally satisfy this (`publish(channel, message)` is the\n * canonical signature; both return a Promise that resolves to the\n * number of subscribers that received the message).\n */\nexport type RedisPublisher = {\n\tpublish: (channel: string, message: string) => Promise<number | unknown>;\n};\n\n/**\n * Minimal Redis subscriber contract. The shape diverges between\n * ioredis (EventEmitter-based) and node-redis (callback-based);\n * `RedisSubscriber` is the lowest-common-denominator:\n *\n * - `subscribe(channel, listener)` registers `listener` for messages\n * on `channel` AND returns an unsubscribe function. The listener\n * receives the raw message string.\n *\n * The README shows how to wrap ioredis (where you'd call\n * `subscriber.on('message', handler)` once and route by channel) and\n * node-redis (where `subscribe(channel, listener)` is the SDK call\n * directly).\n */\nexport type RedisSubscriber = {\n\tsubscribe: (\n\t\tchannel: string,\n\t\tlistener: (message: string) => void\n\t) => Promise<() => Promise<void>>;\n};\n\nexport type CreateRedisClusterBusOptions = {\n\t/** The Redis client used for `publish()` calls. */\n\tpublisher: RedisPublisher;\n\t/** The Redis subscriber. MUST be a dedicated connection (a\n\t * `duplicate()` of your main client) — Redis forbids other\n\t * commands on a subscribed connection. */\n\tsubscriber: RedisSubscriber;\n\t/**\n\t * Channel name. Defaults to `'absolutejs_sync_cluster'`. Two\n\t * engines on the same Redis can scope themselves to different\n\t * channels by overriding this.\n\t */\n\tchannel?: string;\n\t/**\n\t * Called when message parsing / delivery fails. Defaults to\n\t * `console.warn`. Note that Redis pub/sub doesn't surface delivery\n\t * failures on the publisher side — `publish()` resolves to the\n\t * count of subscribers that received the message (or 0); a 0 count\n\t * does NOT fire onError (Redis treats \"no subscribers\" as success).\n\t */\n\tonError?: (error: unknown) => void;\n};\n\n/**\n * Cumulative metrics since `createRedisClusterBus()`. Same shape\n * style as `sync-bus-pg`'s — minus the spill-table fields (Redis\n * has no equivalent).\n */\nexport type RedisClusterBusMetrics = {\n\tpublished: number;\n\treceived: number;\n\tpublishErrors: number;\n\tsubscribeErrors: number;\n\t/**\n\t * Cumulative count of subscribers reached on the publisher side.\n\t * Returned by Redis's PUBLISH. Use it as a rough \"is the cluster\n\t * still wired up\" signal — a drop to 0 when you expect peers means\n\t * subscribers disconnected (replication lag, network partition).\n\t */\n\ttotalSubscribersReached: number;\n};\n\nexport type RedisClusterBus = ClusterBus & {\n\tmetrics: () => RedisClusterBusMetrics;\n};\n\nexport const createRedisClusterBus = (\n\toptions: CreateRedisClusterBusOptions\n): RedisClusterBus => {\n\tconst channel = options.channel ?? 'absolutejs_sync_cluster';\n\tconst onError =\n\t\toptions.onError ?? ((error) => console.warn('[sync-bus-redis]', error));\n\n\tconst counters: RedisClusterBusMetrics = {\n\t\tpublishErrors: 0,\n\t\tpublished: 0,\n\t\treceived: 0,\n\t\tsubscribeErrors: 0,\n\t\ttotalSubscribersReached: 0\n\t};\n\n\treturn {\n\t\tmetrics: () => ({ ...counters }),\n\t\tpublish: async (message: ClusterMessage): Promise<void> => {\n\t\t\ttry {\n\t\t\t\tconst serialized = JSON.stringify(message);\n\t\t\t\tconst result = await options.publisher.publish(\n\t\t\t\t\tchannel,\n\t\t\t\t\tserialized\n\t\t\t\t);\n\t\t\t\tif (typeof result === 'number') {\n\t\t\t\t\tcounters.totalSubscribersReached += result;\n\t\t\t\t}\n\t\t\t\tcounters.published += 1;\n\t\t\t} catch (error) {\n\t\t\t\tcounters.publishErrors += 1;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t\tsubscribe: async (\n\t\t\tonMessage: (message: ClusterMessage) => void\n\t\t): Promise<() => Promise<void>> => {\n\t\t\tconst listener = (raw: string): void => {\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = JSON.parse(raw) as ClusterMessage;\n\t\t\t\t\tcounters.received += 1;\n\t\t\t\t\tonMessage(parsed);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tcounters.subscribeErrors += 1;\n\t\t\t\t\tonError(error);\n\t\t\t\t}\n\t\t\t};\n\t\t\treturn options.subscriber.subscribe(channel, listener);\n\t\t}\n\t};\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;AA6HO,IAAM,wBAAwB,CACpC,YACqB;AAAA,EACrB,MAAM,UAAU,QAAQ,WAAW;AAAA,EACnC,MAAM,UACL,QAAQ,YAAY,CAAC,UAAU,QAAQ,KAAK,oBAAoB,KAAK;AAAA,EAEtE,MAAM,WAAmC;AAAA,IACxC,eAAe;AAAA,IACf,WAAW;AAAA,IACX,UAAU;AAAA,IACV,iBAAiB;AAAA,IACjB,yBAAyB;AAAA,EAC1B;AAAA,EAEA,OAAO;AAAA,IACN,SAAS,OAAO,KAAK,SAAS;AAAA,IAC9B,SAAS,OAAO,YAA2C;AAAA,MAC1D,IAAI;AAAA,QACH,MAAM,aAAa,KAAK,UAAU,OAAO;AAAA,QACzC,MAAM,SAAS,MAAM,QAAQ,UAAU,QACtC,SACA,UACD;AAAA,QACA,IAAI,OAAO,WAAW,UAAU;AAAA,UAC/B,SAAS,2BAA2B;AAAA,QACrC;AAAA,QACA,SAAS,aAAa;AAAA,QACrB,OAAO,OAAO;AAAA,QACf,SAAS,iBAAiB;AAAA,QAC1B,MAAM;AAAA;AAAA;AAAA,IAGR,WAAW,OACV,cACkC;AAAA,MAClC,MAAM,WAAW,CAAC,QAAsB;AAAA,QACvC,IAAI;AAAA,UACH,MAAM,SAAS,KAAK,MAAM,GAAG;AAAA,UAC7B,SAAS,YAAY;AAAA,UACrB,UAAU,MAAM;AAAA,UACf,OAAO,OAAO;AAAA,UACf,SAAS,mBAAmB;AAAA,UAC5B,QAAQ,KAAK;AAAA;AAAA;AAAA,MAGf,OAAO,QAAQ,WAAW,UAAU,SAAS,QAAQ;AAAA;AAAA,EAEvD;AAAA;",
|
|
8
|
+
"debugId": "0C163B79C3CB22BF64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/sync-bus-redis",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Redis pub/sub ClusterBus for @absolutejs/sync — cross-instance fan-out via PUBLISH/SUBSCRIBE. Sibling to @absolutejs/sync-bus-pg; faster fanout, better geo-replication story. Works with any Redis client (ioredis, node-redis, etc.) via a narrow tag-template interface.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/absolutejs/sync-adapters.git",
|
|
8
|
+
"directory": "redis"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"author": "Alex Kahn",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"absolutejs",
|
|
21
|
+
"sync",
|
|
22
|
+
"cluster",
|
|
23
|
+
"redis",
|
|
24
|
+
"pubsub",
|
|
25
|
+
"horizontal-scale",
|
|
26
|
+
"multi-region"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun --external @absolutejs/sync && tsc --project tsconfig.build.json",
|
|
30
|
+
"test": "bun test",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
33
|
+
"release": "bun run format && bun run build && bun publish"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@absolutejs/sync": ">= 1.17.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@absolutejs/sync": "^1.21.0",
|
|
40
|
+
"@types/bun": "1.3.14",
|
|
41
|
+
"prettier": "3.5.3",
|
|
42
|
+
"typescript": "5.8.3"
|
|
43
|
+
},
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"import": "./dist/index.js",
|
|
48
|
+
"default": "./dist/index.js"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist",
|
|
53
|
+
"README.md"
|
|
54
|
+
]
|
|
55
|
+
}
|