@clamator/over-redis 0.1.0

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 ADDED
@@ -0,0 +1,88 @@
1
+ # @clamator/over-redis
2
+
3
+ Redis-streams transport for [clamator](https://www.npmjs.com/package/@clamator/protocol). Implements the `Transport` interface from `@clamator/protocol` so JSON-RPC traffic flows over Redis streams between processes — typically a TS service and a Py service, or two TS services on different hosts.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @clamator/over-redis @clamator/protocol ioredis
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ Define the contract in TypeScript:
14
+
15
+ ```typescript
16
+ // contracts/arith.ts
17
+ import { z } from 'zod';
18
+ import { defineContract, defineMethod, defineNotification } from '@clamator/protocol';
19
+
20
+ export const arithContract = defineContract('arith', {
21
+ add: defineMethod({
22
+ params: z.object({ a: z.number(), b: z.number() }),
23
+ result: z.object({ sum: z.number() }),
24
+ }),
25
+ ping: defineNotification({ params: z.object({}) }),
26
+ });
27
+ ```
28
+
29
+ Run [`@clamator/codegen`](https://www.npmjs.com/package/@clamator/codegen) to emit a typed client and a service interface from that contract:
30
+
31
+ ```bash
32
+ npx @clamator/codegen --src contracts --out-ts generated --ts-contract-import '../contracts/arith.js'
33
+ ```
34
+
35
+ Server — implement the generated `ArithService` interface:
36
+
37
+ ```typescript
38
+ // server.ts
39
+ import { RedisRpcServer } from '@clamator/over-redis';
40
+ import { arithContract } from './contracts/arith.js';
41
+ import type { ArithService } from './generated/arith.js';
42
+
43
+ const handlers: ArithService = {
44
+ add: async ({ a, b }) => ({ sum: a + b }),
45
+ ping: async () => {},
46
+ };
47
+
48
+ const server = new RedisRpcServer({ keyPrefix: 'my-app' });
49
+ server.registerService(arithContract, handlers);
50
+ await server.start();
51
+ process.on('SIGTERM', () => { void server.stop(); });
52
+ ```
53
+
54
+ Client — call methods on the generated `ArithClient`:
55
+
56
+ ```typescript
57
+ // client.ts
58
+ import { RedisRpcClient } from '@clamator/over-redis';
59
+ import { ArithClient } from './generated/arith.js';
60
+
61
+ const transport = new RedisRpcClient({ keyPrefix: 'my-app' });
62
+ await transport.start();
63
+
64
+ const arith = new ArithClient(transport);
65
+ const result = await arith.add({ a: 2, b: 3 });
66
+ console.log(result); // { sum: 5 }
67
+
68
+ await transport.stop();
69
+ ```
70
+
71
+ By default the connection is built from `$REDIS_URL` (or `redis://localhost:6379`). Pass `redisUrl` for a different URL, or `redis` for a pre-built `ioredis` instance.
72
+
73
+ ## Key surface
74
+
75
+ - `RedisRpcServer({ keyPrefix, redis?, redisUrl?, ... })` — `registerService(contract, handlers)`, `start()`, `stop()`.
76
+ - `RedisRpcClient({ keyPrefix, redis?, redisUrl?, defaultTimeoutMs? })` — `start()`, `stop()`. The instance is also a `ClamatorClient`, so it can be wrapped by a generated `*Client` proxy.
77
+
78
+ ## When to reach for this vs. `@clamator/over-memory`
79
+
80
+ - [`@clamator/over-memory`](https://www.npmjs.com/package/@clamator/over-memory) — tests, embedded scenarios, anything single-process.
81
+ - `@clamator/over-redis` — cross-process, cross-host, durable streams, production.
82
+
83
+ ## Links
84
+
85
+ - Sibling (Python): [`clamator-over-redis`](https://pypi.org/project/clamator-over-redis/)
86
+ - Codegen: [`@clamator/codegen`](https://www.npmjs.com/package/@clamator/codegen)
87
+ - Design spec: [`docs/2026-05-07-clamator-design.md`](../../../docs/2026-05-07-clamator-design.md)
88
+ - Agent rules: [`AGENTS.md`](./AGENTS.md)
@@ -0,0 +1,12 @@
1
+ export interface BackoffOptions {
2
+ initialMs: number;
3
+ maxMs: number;
4
+ factor?: number;
5
+ jitter?: boolean;
6
+ }
7
+ export declare function expBackoff(opts: BackoffOptions): () => number;
8
+ export declare function resetable(opts: BackoffOptions): {
9
+ next: () => number;
10
+ reset: () => void;
11
+ };
12
+ //# sourceMappingURL=backoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,MAAM,CAS7D;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,IAAI,CAAA;CAAE,CAMzF"}
@@ -0,0 +1,19 @@
1
+ export function expBackoff(opts) {
2
+ const factor = opts.factor ?? 2;
3
+ let current = opts.initialMs;
4
+ return () => {
5
+ const value = current;
6
+ current = Math.min(opts.maxMs, current * factor);
7
+ if (opts.jitter ?? true)
8
+ return Math.floor(value * (0.5 + Math.random() * 0.5));
9
+ return value;
10
+ };
11
+ }
12
+ export function resetable(opts) {
13
+ let next = expBackoff(opts);
14
+ return {
15
+ next: () => next(),
16
+ reset: () => { next = expBackoff(opts); },
17
+ };
18
+ }
19
+ //# sourceMappingURL=backoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backoff.js","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,UAAU,CAAC,IAAoB;IAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;IAChC,IAAI,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;IAC7B,OAAO,GAAG,EAAE;QACV,MAAM,KAAK,GAAG,OAAO,CAAC;QACtB,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;QACjD,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;QAChF,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAoB;IAC5C,IAAI,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO;QACL,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE;QAClB,KAAK,EAAE,GAAG,EAAE,GAAG,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;KAC1C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { type Redis } from 'ioredis';
2
+ import { type Transport, type SendOptions, type Dispatcher } from '@clamator/protocol';
3
+ export interface ClientTransportOptions {
4
+ /** Existing ioredis instance. Mutually exclusive with `redisUrl`. */
5
+ redis?: Redis;
6
+ /** Redis URL. If neither `redis` nor `redisUrl` is provided, falls back to `process.env.REDIS_URL` then `redis://localhost:6379`. */
7
+ redisUrl?: string;
8
+ keyPrefix: string;
9
+ instanceId?: string;
10
+ defaultTimeoutMs?: number;
11
+ }
12
+ export declare class ClientRedisTransport implements Transport {
13
+ readonly instanceId: string;
14
+ private readonly redis;
15
+ private readonly ownsRedis;
16
+ private readonly keyPrefix;
17
+ private readonly replyStream;
18
+ private state;
19
+ private pending;
20
+ private replyLoop;
21
+ private replyLoopAbort;
22
+ private replyRedis;
23
+ constructor(opts: ClientTransportOptions);
24
+ registerService(_name: string, _dispatch: Dispatcher): Promise<void>;
25
+ send(env: Record<string, unknown>, sendOpts: SendOptions): Promise<Record<string, unknown>>;
26
+ notify(env: Record<string, unknown>): Promise<void>;
27
+ start(): Promise<void>;
28
+ stop(): Promise<void>;
29
+ private runReplyLoop;
30
+ }
31
+ //# sourceMappingURL=client-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-transport.d.ts","sourceRoot":"","sources":["../src/client-transport.ts"],"names":[],"mappings":"AACA,OAAgB,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAEL,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAClD,MAAM,oBAAoB,CAAC;AAG5B,MAAM,WAAW,sBAAsB;IACrC,qEAAqE;IACrE,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,qIAAqI;IACrI,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAQD,qBAAa,oBAAqB,YAAW,SAAS;IACpD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,KAAK,CAA0C;IACvD,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,cAAc,CAAS;IAG/B,OAAO,CAAC,UAAU,CAAsB;gBAE5B,IAAI,EAAE,sBAAsB;IAgBlC,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpE,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IA6B3F,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAenD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IActB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAoBb,YAAY;CAgC3B"}
@@ -0,0 +1,162 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import IORedis, {} from 'ioredis';
3
+ import { parseEnvelope, EnvelopeKind, ClamatorTransportError, } from '@clamator/protocol';
4
+ import { commandStream, replyStream } from './keys.js';
5
+ export class ClientRedisTransport {
6
+ instanceId;
7
+ redis;
8
+ ownsRedis;
9
+ keyPrefix;
10
+ replyStream;
11
+ state = 'idle';
12
+ pending = new Map();
13
+ replyLoop = null;
14
+ replyLoopAbort = false;
15
+ // Dedicated connection for the blocking XREAD reply loop, so that
16
+ // xadd calls in send() are not queued behind the blocking read.
17
+ replyRedis = null;
18
+ constructor(opts) {
19
+ if (opts.redis && opts.redisUrl)
20
+ throw new ClamatorTransportError('provide either `redis` or `redisUrl`, not both');
21
+ if (opts.redis) {
22
+ this.redis = opts.redis;
23
+ this.ownsRedis = false;
24
+ }
25
+ else {
26
+ const url = opts.redisUrl ?? process.env.REDIS_URL ?? 'redis://localhost:6379';
27
+ this.redis = new IORedis(url);
28
+ this.ownsRedis = true;
29
+ }
30
+ this.keyPrefix = opts.keyPrefix;
31
+ this.instanceId = opts.instanceId ?? randomUUID();
32
+ this.replyStream = replyStream(opts.keyPrefix, this.instanceId);
33
+ }
34
+ async registerService(_name, _dispatch) {
35
+ throw new Error('client transport cannot host services');
36
+ }
37
+ async send(env, sendOpts) {
38
+ if (this.state !== 'started')
39
+ throw new ClamatorTransportError(`transport not started (state=${this.state})`);
40
+ const parsed = parseEnvelope(env);
41
+ if (parsed.kind !== EnvelopeKind.Request)
42
+ throw new ClamatorTransportError('send requires a request envelope');
43
+ const idStr = String(parsed.id);
44
+ return new Promise((resolve, reject) => {
45
+ const timer = setTimeout(() => {
46
+ this.pending.delete(idStr);
47
+ reject(new ClamatorTransportError('call timeout'));
48
+ }, sendOpts.timeoutMs);
49
+ this.pending.set(idStr, { resolve, reject, timer });
50
+ void this.redis.xadd(commandStream(this.keyPrefix, parsed.service), '*', 'type', 'rpc', 'envelope', JSON.stringify(env), 'reply-to', this.replyStream).catch(err => {
51
+ const p = this.pending.get(idStr);
52
+ if (!p)
53
+ return;
54
+ this.pending.delete(idStr);
55
+ clearTimeout(p.timer);
56
+ p.reject(new ClamatorTransportError('xadd failed', err));
57
+ });
58
+ });
59
+ }
60
+ async notify(env) {
61
+ if (this.state !== 'started')
62
+ throw new ClamatorTransportError(`transport not started (state=${this.state})`);
63
+ const parsed = parseEnvelope(env);
64
+ if (parsed.kind !== EnvelopeKind.Notification)
65
+ throw new ClamatorTransportError('notify requires a notification envelope');
66
+ await this.redis.xadd(commandStream(this.keyPrefix, parsed.service), '*', 'type', 'rpc', 'envelope', JSON.stringify(env));
67
+ }
68
+ async start() {
69
+ if (this.state === 'stopped')
70
+ throw new ClamatorTransportError('transport has been stopped');
71
+ if (this.state === 'started')
72
+ return;
73
+ this.state = 'started';
74
+ this.replyLoopAbort = false;
75
+ // Use a dedicated duplicate connection for the blocking XREAD so that
76
+ // xadd commands in send() are not queued behind the blocking read.
77
+ this.replyRedis = this.redis.duplicate();
78
+ this.replyLoop = this.runReplyLoop().catch(err => {
79
+ // surface fatal loop errors
80
+ console.error('[clamator/over-redis] reply loop fatal:', err);
81
+ });
82
+ }
83
+ async stop() {
84
+ if (this.state !== 'started') {
85
+ this.state = 'stopped';
86
+ return;
87
+ }
88
+ this.replyLoopAbort = true;
89
+ // Disconnect the reply connection to unblock the XREAD immediately.
90
+ try {
91
+ this.replyRedis?.disconnect();
92
+ }
93
+ catch { /* ignore */ }
94
+ try {
95
+ await this.replyLoop;
96
+ }
97
+ catch { /* ignore */ }
98
+ for (const [id, p] of this.pending.entries()) {
99
+ clearTimeout(p.timer);
100
+ p.reject(new ClamatorTransportError('transport stopped'));
101
+ this.pending.delete(id);
102
+ }
103
+ try {
104
+ await this.redis.del(this.replyStream);
105
+ }
106
+ catch { /* best effort */ }
107
+ try {
108
+ await this.replyRedis?.quit();
109
+ }
110
+ catch { /* best effort */ }
111
+ this.replyRedis = null;
112
+ if (this.ownsRedis) {
113
+ try {
114
+ await this.redis.quit();
115
+ }
116
+ catch { /* best effort */ }
117
+ }
118
+ this.state = 'stopped';
119
+ }
120
+ async runReplyLoop() {
121
+ // Use '0-0' rather than '$' so replies are never missed between iterations.
122
+ // The reply stream is unique per client instance, so reading from the
123
+ // beginning is always correct and safe.
124
+ let lastId = '0-0';
125
+ while (!this.replyLoopAbort) {
126
+ try {
127
+ const result = await this.replyRedis.xread('BLOCK', 1000, 'STREAMS', this.replyStream, lastId);
128
+ if (!result)
129
+ continue;
130
+ for (const [, entries] of result) {
131
+ for (const [entryId, fields] of entries) {
132
+ lastId = entryId;
133
+ const idx = fields.indexOf('envelope');
134
+ if (idx < 0)
135
+ continue;
136
+ const json = fields[idx + 1] ?? '';
137
+ let parsed;
138
+ try {
139
+ parsed = JSON.parse(json);
140
+ }
141
+ catch {
142
+ continue;
143
+ }
144
+ const replyId = String(parsed.id);
145
+ const pending = this.pending.get(replyId);
146
+ if (!pending)
147
+ continue; // late or stranger reply
148
+ this.pending.delete(replyId);
149
+ clearTimeout(pending.timer);
150
+ pending.resolve(parsed);
151
+ }
152
+ }
153
+ }
154
+ catch (err) {
155
+ if (this.replyLoopAbort)
156
+ return;
157
+ await new Promise(r => setTimeout(r, 100));
158
+ }
159
+ }
160
+ }
161
+ }
162
+ //# sourceMappingURL=client-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-transport.js","sourceRoot":"","sources":["../src/client-transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { RpcClientCore } from '@clamator/protocol';
2
+ import { type ClientTransportOptions } from './client-transport.js';
3
+ export interface RedisRpcClientOptions extends ClientTransportOptions {
4
+ }
5
+ export declare class RedisRpcClient extends RpcClientCore {
6
+ constructor(opts: RedisRpcClientOptions);
7
+ }
8
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAwB,KAAK,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE1F,MAAM,WAAW,qBAAsB,SAAQ,sBAAsB;CAAG;AAExE,qBAAa,cAAe,SAAQ,aAAa;gBACnC,IAAI,EAAE,qBAAqB;CAMxC"}
package/dist/client.js ADDED
@@ -0,0 +1,8 @@
1
+ import { RpcClientCore } from '@clamator/protocol';
2
+ import { ClientRedisTransport } from './client-transport.js';
3
+ export class RedisRpcClient extends RpcClientCore {
4
+ constructor(opts) {
5
+ super(new ClientRedisTransport(opts), { defaultTimeoutMs: opts.defaultTimeoutMs ?? 30_000 });
6
+ }
7
+ }
8
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAA+B,MAAM,uBAAuB,CAAC;AAI1F,MAAM,OAAO,cAAe,SAAQ,aAAa;IAC/C,YAAY,IAA2B;QACrC,KAAK,CACH,IAAI,oBAAoB,CAAC,IAAI,CAAC,EAC9B,EAAE,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,IAAI,MAAM,EAAE,CACtD,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ export { RedisRpcServer, type RedisRpcServerOptions } from './server.js';
2
+ export { RedisRpcClient, type RedisRpcClientOptions } from './client.js';
3
+ export { ServerRedisTransport } from './server-transport.js';
4
+ export { ClientRedisTransport } from './client-transport.js';
5
+ export * from './keys.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,cAAc,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { RedisRpcServer } from './server.js';
2
+ export { RedisRpcClient } from './client.js';
3
+ export { ServerRedisTransport } from './server-transport.js';
4
+ export { ClientRedisTransport } from './client-transport.js';
5
+ export * from './keys.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAA8B,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,cAAc,EAA8B,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,cAAc,WAAW,CAAC"}
package/dist/keys.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare function commandStream(prefix: string, service: string): string;
2
+ export declare function replyStream(prefix: string, instanceId: string): string;
3
+ export declare function consumerGroupName(service: string): string;
4
+ export declare function consumerName(service: string, instanceId: string): string;
5
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAExE"}
package/dist/keys.js ADDED
@@ -0,0 +1,13 @@
1
+ export function commandStream(prefix, service) {
2
+ return `${prefix}:cmds:${service}`;
3
+ }
4
+ export function replyStream(prefix, instanceId) {
5
+ return `${prefix}:replies:${instanceId}`;
6
+ }
7
+ export function consumerGroupName(service) {
8
+ return service;
9
+ }
10
+ export function consumerName(service, instanceId) {
11
+ return `${service}:${instanceId}`;
12
+ }
13
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,OAAe;IAC3D,OAAO,GAAG,MAAM,SAAS,OAAO,EAAE,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,UAAkB;IAC5D,OAAO,GAAG,MAAM,YAAY,UAAU,EAAE,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,UAAkB;IAC9D,OAAO,GAAG,OAAO,IAAI,UAAU,EAAE,CAAC;AACpC,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { type Redis } from 'ioredis';
2
+ import { type Transport, type Dispatcher } from '@clamator/protocol';
3
+ export interface ServerTransportOptions {
4
+ /** Existing ioredis instance. Mutually exclusive with `redisUrl`. */
5
+ redis?: Redis;
6
+ /** Redis URL. If neither `redis` nor `redisUrl` is provided, falls back to `process.env.REDIS_URL` then `redis://localhost:6379`. */
7
+ redisUrl?: string;
8
+ keyPrefix: string;
9
+ instanceId?: string;
10
+ consumerClaimIdleMs?: number;
11
+ replyStreamMaxLen?: number;
12
+ shutdownGraceMs?: number;
13
+ }
14
+ export declare class ServerRedisTransport implements Transport {
15
+ readonly instanceId: string;
16
+ private dispatchers;
17
+ private state;
18
+ private loops;
19
+ private abort;
20
+ private reclaimWakers;
21
+ private blockingConns;
22
+ private readonly redis;
23
+ private readonly ownsRedis;
24
+ private readonly keyPrefix;
25
+ private readonly replyStreamMaxLen;
26
+ private readonly consumerClaimIdleMs;
27
+ private readonly shutdownGraceMs;
28
+ constructor(opts: ServerTransportOptions);
29
+ registerService(name: string, dispatch: Dispatcher): Promise<void>;
30
+ send(): Promise<Record<string, unknown>>;
31
+ notify(): Promise<void>;
32
+ start(): Promise<void>;
33
+ stop(): Promise<void>;
34
+ private runConsumerLoop;
35
+ private interruptibleSleep;
36
+ private runReclaimLoop;
37
+ private handleEntry;
38
+ }
39
+ //# sourceMappingURL=server-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-transport.d.ts","sourceRoot":"","sources":["../src/server-transport.ts"],"names":[],"mappings":"AACA,OAAgB,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAEL,KAAK,SAAS,EAAE,KAAK,UAAU,EAChC,MAAM,oBAAoB,CAAC;AAG5B,MAAM,WAAW,sBAAsB;IACrC,qEAAqE;IACrE,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,qIAAqI;IACrI,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,oBAAqB,YAAW,SAAS;IACpD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,KAAK,CAA0C;IACvD,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,aAAa,CAA8B;IAInD,OAAO,CAAC,aAAa,CAAe;IAEpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAE7B,IAAI,EAAE,sBAAsB;IAkBlC,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlE,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAIxC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAIvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAuBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YA0Bb,eAAe;IAuB7B,OAAO,CAAC,kBAAkB;YAQZ,cAAc;YAuBd,WAAW;CA6B1B"}
@@ -0,0 +1,202 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import IORedis, {} from 'ioredis';
3
+ import { parseEnvelope, EnvelopeKind, ClamatorTransportError, } from '@clamator/protocol';
4
+ import { commandStream, consumerGroupName, consumerName } from './keys.js';
5
+ export class ServerRedisTransport {
6
+ instanceId;
7
+ dispatchers = new Map();
8
+ state = 'idle';
9
+ loops = [];
10
+ abort = false;
11
+ reclaimWakers = new Set();
12
+ // Dedicated connection per service for the blocking XREADGROUP call so that
13
+ // xadd (reply), xack, and xautoclaim on the main connection are never
14
+ // queued behind the blocking read.
15
+ blockingConns = [];
16
+ redis;
17
+ ownsRedis;
18
+ keyPrefix;
19
+ replyStreamMaxLen;
20
+ consumerClaimIdleMs;
21
+ shutdownGraceMs;
22
+ constructor(opts) {
23
+ if (opts.redis && opts.redisUrl)
24
+ throw new ClamatorTransportError('provide either `redis` or `redisUrl`, not both');
25
+ if (opts.redis) {
26
+ this.redis = opts.redis;
27
+ this.ownsRedis = false;
28
+ }
29
+ else {
30
+ const url = opts.redisUrl ?? process.env.REDIS_URL ?? 'redis://localhost:6379';
31
+ this.redis = new IORedis(url);
32
+ this.ownsRedis = true;
33
+ }
34
+ this.keyPrefix = opts.keyPrefix;
35
+ this.instanceId = opts.instanceId ?? randomUUID();
36
+ this.replyStreamMaxLen = opts.replyStreamMaxLen ?? 1024;
37
+ this.consumerClaimIdleMs = opts.consumerClaimIdleMs ?? 60_000;
38
+ this.shutdownGraceMs = opts.shutdownGraceMs ?? 5_000;
39
+ }
40
+ async registerService(name, dispatch) {
41
+ this.dispatchers.set(name, dispatch);
42
+ }
43
+ async send() {
44
+ throw new Error('server transport cannot send requests');
45
+ }
46
+ async notify() {
47
+ throw new Error('server transport cannot send notifications');
48
+ }
49
+ async start() {
50
+ if (this.state === 'stopped')
51
+ throw new ClamatorTransportError('transport has been stopped');
52
+ if (this.state === 'started')
53
+ return;
54
+ this.state = 'started';
55
+ this.abort = false;
56
+ for (const service of this.dispatchers.keys()) {
57
+ const stream = commandStream(this.keyPrefix, service);
58
+ const group = consumerGroupName(service);
59
+ try {
60
+ await this.redis.xgroup('CREATE', stream, group, '$', 'MKSTREAM');
61
+ }
62
+ catch (e) {
63
+ const msg = e.message;
64
+ if (!msg.includes('BUSYGROUP'))
65
+ throw e;
66
+ }
67
+ // Dedicated connection for blocking XREADGROUP so the main connection
68
+ // stays free for non-blocking operations (xadd, xack, xautoclaim).
69
+ const blockingRedis = this.redis.duplicate();
70
+ this.blockingConns.push(blockingRedis);
71
+ this.loops.push(this.runConsumerLoop(service, blockingRedis));
72
+ this.loops.push(this.runReclaimLoop(service));
73
+ }
74
+ }
75
+ async stop() {
76
+ if (this.state !== 'started') {
77
+ this.state = 'stopped';
78
+ return;
79
+ }
80
+ this.abort = true;
81
+ // Wake up any sleeping reclaim loops so they can check abort and exit promptly.
82
+ for (const wake of this.reclaimWakers)
83
+ wake();
84
+ this.reclaimWakers.clear();
85
+ // Disconnect blocking connections to unblock XREADGROUP immediately.
86
+ for (const conn of this.blockingConns) {
87
+ try {
88
+ conn.disconnect();
89
+ }
90
+ catch { /* ignore */ }
91
+ }
92
+ const grace = this.shutdownGraceMs;
93
+ await Promise.race([
94
+ Promise.allSettled(this.loops),
95
+ new Promise(r => setTimeout(r, grace)),
96
+ ]);
97
+ // Clean up dedicated blocking connections.
98
+ for (const conn of this.blockingConns) {
99
+ try {
100
+ await conn.quit();
101
+ }
102
+ catch { /* best effort */ }
103
+ }
104
+ this.blockingConns = [];
105
+ if (this.ownsRedis) {
106
+ try {
107
+ await this.redis.quit();
108
+ }
109
+ catch { /* best effort */ }
110
+ }
111
+ this.state = 'stopped';
112
+ }
113
+ async runConsumerLoop(service, blockingRedis) {
114
+ const stream = commandStream(this.keyPrefix, service);
115
+ const group = consumerGroupName(service);
116
+ const consumer = consumerName(service, this.instanceId);
117
+ while (!this.abort) {
118
+ try {
119
+ const result = await blockingRedis.xreadgroup('GROUP', group, consumer, 'COUNT', 16, 'BLOCK', 1000, 'STREAMS', stream, '>');
120
+ if (!result)
121
+ continue;
122
+ for (const [, entries] of result) {
123
+ for (const [entryId, fields] of entries) {
124
+ await this.handleEntry(service, stream, group, entryId, fields);
125
+ }
126
+ }
127
+ }
128
+ catch (err) {
129
+ if (this.abort)
130
+ return;
131
+ await new Promise(r => setTimeout(r, 100));
132
+ }
133
+ }
134
+ }
135
+ interruptibleSleep(ms) {
136
+ return new Promise(resolve => {
137
+ const timer = setTimeout(() => { this.reclaimWakers.delete(wake); resolve(); }, ms);
138
+ const wake = () => { clearTimeout(timer); this.reclaimWakers.delete(wake); resolve(); };
139
+ this.reclaimWakers.add(wake);
140
+ });
141
+ }
142
+ async runReclaimLoop(service) {
143
+ const stream = commandStream(this.keyPrefix, service);
144
+ const group = consumerGroupName(service);
145
+ const consumer = consumerName(service, this.instanceId);
146
+ const idleThreshold = this.consumerClaimIdleMs;
147
+ while (!this.abort) {
148
+ try {
149
+ await this.interruptibleSleep(Math.max(1000, idleThreshold / 4));
150
+ if (this.abort)
151
+ return;
152
+ const claimed = await this.redis.xautoclaim(stream, group, consumer, idleThreshold, '0', 'COUNT', 32);
153
+ const entries = claimed[1];
154
+ if (!entries || entries.length === 0)
155
+ continue;
156
+ for (const [entryId, fields] of entries) {
157
+ await this.handleEntry(service, stream, group, entryId, fields);
158
+ }
159
+ }
160
+ catch (err) {
161
+ if (this.abort)
162
+ return;
163
+ }
164
+ }
165
+ }
166
+ async handleEntry(service, stream, group, entryId, fields) {
167
+ const envIdx = fields.indexOf('envelope');
168
+ if (envIdx < 0) {
169
+ await this.redis.xack(stream, group, entryId);
170
+ return;
171
+ }
172
+ const replyToIdx = fields.indexOf('reply-to');
173
+ const replyTo = replyToIdx >= 0 ? fields[replyToIdx + 1] : null;
174
+ let envObj;
175
+ try {
176
+ envObj = JSON.parse(fields[envIdx + 1] ?? '');
177
+ }
178
+ catch {
179
+ await this.redis.xack(stream, group, entryId);
180
+ return;
181
+ }
182
+ let parsed;
183
+ try {
184
+ parsed = parseEnvelope(envObj);
185
+ }
186
+ catch {
187
+ await this.redis.xack(stream, group, entryId);
188
+ return;
189
+ }
190
+ const dispatcher = this.dispatchers.get(service);
191
+ if (!dispatcher) {
192
+ await this.redis.xack(stream, group, entryId);
193
+ return;
194
+ }
195
+ const reply = await dispatcher(parsed);
196
+ if (replyTo && reply) {
197
+ await this.redis.xadd(replyTo, 'MAXLEN', '~', String(this.replyStreamMaxLen), '*', 'type', 'rpc', 'envelope', JSON.stringify(reply));
198
+ }
199
+ await this.redis.xack(stream, group, entryId);
200
+ }
201
+ }
202
+ //# sourceMappingURL=server-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-transport.js","sourceRoot":"","sources":["../src/server-transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { RpcServerCore } from '@clamator/protocol';
2
+ import { type ServerTransportOptions } from './server-transport.js';
3
+ export interface RedisRpcServerOptions extends ServerTransportOptions {
4
+ }
5
+ export declare class RedisRpcServer extends RpcServerCore {
6
+ constructor(opts: RedisRpcServerOptions);
7
+ }
8
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAwB,KAAK,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE1F,MAAM,WAAW,qBAAsB,SAAQ,sBAAsB;CAAG;AAExE,qBAAa,cAAe,SAAQ,aAAa;gBACnC,IAAI,EAAE,qBAAqB;CAGxC"}
package/dist/server.js ADDED
@@ -0,0 +1,8 @@
1
+ import { RpcServerCore } from '@clamator/protocol';
2
+ import { ServerRedisTransport } from './server-transport.js';
3
+ export class RedisRpcServer extends RpcServerCore {
4
+ constructor(opts) {
5
+ super(new ServerRedisTransport(opts));
6
+ }
7
+ }
8
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAA+B,MAAM,uBAAuB,CAAC;AAI1F,MAAM,OAAO,cAAe,SAAQ,aAAa;IAC/C,YAAY,IAA2B;QACrC,KAAK,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC;IACxC,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@clamator/over-redis",
3
+ "version": "0.1.0",
4
+ "description": "Redis-streams transport for clamator (pre-1.0).",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "LICENSE"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "ioredis": "^5.4.0",
25
+ "@clamator/protocol": "0.1.0"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^5.6.0",
29
+ "vitest": "^2.1.0",
30
+ "zod": "^3.23.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json",
34
+ "clean": "rm -rf dist .tsbuildinfo",
35
+ "lint": "tsc -p tsconfig.json --noEmit",
36
+ "test": "vitest run"
37
+ }
38
+ }