@kronos-ts/rabbitmq 0.2.1 → 0.3.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.
@@ -0,0 +1,75 @@
1
+ import type { AmqpConnection } from "./connection.js";
2
+ import type { RabbitMqResolvedConfig } from "./rabbitmq.js";
3
+ /**
4
+ * Wire envelope for subscription-query update broadcasts.
5
+ *
6
+ * `kind: "update"` carries a payload to deliver to all matching local subscribers.
7
+ * `kind: "complete"` / `"completeExceptionally"` signal end-of-stream for all
8
+ * subscribers of the given query name on every process.
9
+ *
10
+ * `senderId` identifies the publishing instance so a process can drop its own
11
+ * loopback messages (the local query bus has already fanned out locally).
12
+ */
13
+ export interface RabbitMqQueryUpdateEnvelope {
14
+ readonly kind: "update" | "complete" | "completeExceptionally";
15
+ readonly senderId: string;
16
+ readonly queryName: string;
17
+ readonly update?: unknown;
18
+ readonly error?: {
19
+ readonly name?: string;
20
+ readonly message: string;
21
+ readonly stack?: string;
22
+ };
23
+ /**
24
+ * Serialized form of a structured `payloadEquals` filter. When present,
25
+ * receivers apply it against each local subscriber's stored query payload
26
+ * before delivering. Function filters do not serialize and therefore arrive
27
+ * without this field, causing the receiver to deliver to all local
28
+ * subscribers of {@link queryName}.
29
+ */
30
+ readonly payloadEquals?: Record<string, unknown>;
31
+ }
32
+ export interface RabbitMqQueryUpdatesTransport {
33
+ /** Publish an update / complete envelope to every subscribed instance. */
34
+ publish(envelope: RabbitMqQueryUpdateEnvelope): Promise<void>;
35
+ /** Bind this instance's queue to the routing key for the query name. Idempotent. */
36
+ bindQueryName(queryName: string): Promise<void>;
37
+ /** Unbind this instance's queue from the routing key. Idempotent. */
38
+ unbindQueryName(queryName: string): Promise<void>;
39
+ /** Set the in-process handler invoked when an inbound update arrives. */
40
+ setHandler(handler: (envelope: RabbitMqQueryUpdateEnvelope) => void): void;
41
+ /** Stable identifier for this publisher; appears as `senderId` on outbound envelopes. */
42
+ readonly senderId: string;
43
+ }
44
+ /**
45
+ * AMQP broadcast transport for subscription-query updates.
46
+ *
47
+ * Topology: a topic exchange (`<prefix>.query-updates`) and one exclusive
48
+ * auto-delete queue per instance (`<prefix>.query-updates.<service>.<instance>`).
49
+ * The bus dynamically binds the queue to a routing key per active query name.
50
+ *
51
+ * Consume mode is no-ack: updates are best-effort. Losing one update means a
52
+ * subscriber will see a stale view until the next emit — same recovery model
53
+ * as Axon Server when a broker round-trip is dropped.
54
+ */
55
+ export declare class AmqpRabbitMqQueryUpdatesTransport implements RabbitMqQueryUpdatesTransport {
56
+ private readonly config;
57
+ private readonly connection;
58
+ private channel;
59
+ private connectPromise;
60
+ private closed;
61
+ private handler;
62
+ private readonly boundKeys;
63
+ readonly senderId: string;
64
+ constructor(config: RabbitMqResolvedConfig, connection: AmqpConnection);
65
+ connect(): Promise<void>;
66
+ private doConnect;
67
+ close(): Promise<void>;
68
+ publish(envelope: RabbitMqQueryUpdateEnvelope): Promise<void>;
69
+ bindQueryName(queryName: string): Promise<void>;
70
+ unbindQueryName(queryName: string): Promise<void>;
71
+ setHandler(handler: (envelope: RabbitMqQueryUpdateEnvelope) => void): void;
72
+ private handleInbound;
73
+ private requireChannel;
74
+ }
75
+ //# sourceMappingURL=amqp-query-updates-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"amqp-query-updates-transport.d.ts","sourceRoot":"","sources":["../src/amqp-query-updates-transport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAE3D;;;;;;;;;GASG;AACH,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,uBAAuB,CAAA;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KACxB,CAAA;IACD;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjD;AAED,MAAM,WAAW,6BAA6B;IAC5C,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7D,oFAAoF;IACpF,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,qEAAqE;IACrE,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD,yEAAyE;IACzE,UAAU,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,2BAA2B,KAAK,IAAI,GAAG,IAAI,CAAA;IAC1E,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,iCAAkC,YAAW,6BAA6B;IAUnF,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAV7B,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,cAAc,CAA2B;IACjD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAA+D;IAC9E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAE9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;gBAGN,MAAM,EAAE,sBAAsB,EAC9B,UAAU,EAAE,cAAc;IAKvC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAMhB,SAAS;IA6BjB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,OAAO,CAAC,QAAQ,EAAE,2BAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IAa7D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc/C,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcvD,UAAU,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,2BAA2B,KAAK,IAAI,GAAG,IAAI;IAI1E,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,cAAc;CAIvB"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * AMQP broadcast transport for subscription-query updates.
3
+ *
4
+ * Topology: a topic exchange (`<prefix>.query-updates`) and one exclusive
5
+ * auto-delete queue per instance (`<prefix>.query-updates.<service>.<instance>`).
6
+ * The bus dynamically binds the queue to a routing key per active query name.
7
+ *
8
+ * Consume mode is no-ack: updates are best-effort. Losing one update means a
9
+ * subscriber will see a stale view until the next emit — same recovery model
10
+ * as Axon Server when a broker round-trip is dropped.
11
+ */
12
+ export class AmqpRabbitMqQueryUpdatesTransport {
13
+ config;
14
+ connection;
15
+ channel;
16
+ connectPromise;
17
+ closed = false;
18
+ handler;
19
+ boundKeys = new Set();
20
+ senderId;
21
+ constructor(config, connection) {
22
+ this.config = config;
23
+ this.connection = connection;
24
+ this.senderId = `${config.identity.serviceName}.${config.identity.instanceId}`;
25
+ }
26
+ async connect() {
27
+ if (this.connectPromise)
28
+ return this.connectPromise;
29
+ this.connectPromise = this.doConnect();
30
+ return this.connectPromise;
31
+ }
32
+ async doConnect() {
33
+ this.channel = await this.connection.channel();
34
+ await this.channel.assertExchange(this.config.topology.queryUpdatesExchange, "topic", {
35
+ durable: true,
36
+ });
37
+ await this.channel.assertQueue(this.config.topology.queryUpdatesQueue(), {
38
+ durable: false,
39
+ exclusive: true,
40
+ autoDelete: true,
41
+ });
42
+ await this.channel.consume(this.config.topology.queryUpdatesQueue(), (msg) => this.handleInbound(msg), { noAck: true });
43
+ // Rebind any routing keys requested before connect resolved.
44
+ for (const key of this.boundKeys) {
45
+ await this.channel.bindQueue(this.config.topology.queryUpdatesQueue(), this.config.topology.queryUpdatesExchange, key);
46
+ }
47
+ }
48
+ async close() {
49
+ this.closed = true;
50
+ await this.channel?.close().catch(() => { });
51
+ }
52
+ async publish(envelope) {
53
+ await this.connect();
54
+ if (this.closed)
55
+ return;
56
+ const channel = this.requireChannel();
57
+ const routingKey = this.config.topology.queryUpdatesRoutingKey(envelope.queryName);
58
+ channel.publish(this.config.topology.queryUpdatesExchange, routingKey, Buffer.from(JSON.stringify(envelope)), { contentType: "application/json", persistent: false });
59
+ }
60
+ async bindQueryName(queryName) {
61
+ const routingKey = this.config.topology.queryUpdatesRoutingKey(queryName);
62
+ if (this.boundKeys.has(routingKey))
63
+ return;
64
+ this.boundKeys.add(routingKey);
65
+ await this.connect();
66
+ if (this.closed)
67
+ return;
68
+ const channel = this.requireChannel();
69
+ await channel.bindQueue(this.config.topology.queryUpdatesQueue(), this.config.topology.queryUpdatesExchange, routingKey);
70
+ }
71
+ async unbindQueryName(queryName) {
72
+ const routingKey = this.config.topology.queryUpdatesRoutingKey(queryName);
73
+ if (!this.boundKeys.has(routingKey))
74
+ return;
75
+ this.boundKeys.delete(routingKey);
76
+ if (this.closed)
77
+ return;
78
+ const channel = this.channel;
79
+ if (!channel)
80
+ return;
81
+ await channel.unbindQueue(this.config.topology.queryUpdatesQueue(), this.config.topology.queryUpdatesExchange, routingKey);
82
+ }
83
+ setHandler(handler) {
84
+ this.handler = handler;
85
+ }
86
+ handleInbound(msg) {
87
+ if (!msg)
88
+ return;
89
+ if (!this.handler)
90
+ return;
91
+ try {
92
+ const envelope = JSON.parse(msg.content.toString("utf8"));
93
+ this.handler(envelope);
94
+ }
95
+ catch {
96
+ // Malformed envelopes are dropped — broadcast is best-effort.
97
+ }
98
+ }
99
+ requireChannel() {
100
+ if (!this.channel)
101
+ throw new Error("RabbitMQ query-updates transport is not connected");
102
+ return this.channel;
103
+ }
104
+ }
105
+ //# sourceMappingURL=amqp-query-updates-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"amqp-query-updates-transport.js","sourceRoot":"","sources":["../src/amqp-query-updates-transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,132 @@
1
+ import type { AmqpConnection } from "./connection.js";
2
+ import type { RabbitMqResolvedConfig } from "./rabbitmq.js";
3
+ /**
4
+ * A subscription known to the cluster — either owned locally on this instance
5
+ * or owned by another instance and learned over gossip.
6
+ */
7
+ export interface SubscriberRecord {
8
+ readonly subId: string;
9
+ readonly queryName: string;
10
+ readonly payload: unknown;
11
+ readonly ownerInstanceId: string;
12
+ }
13
+ export interface SerializedError {
14
+ readonly name?: string;
15
+ readonly message: string;
16
+ readonly stack?: string;
17
+ }
18
+ /**
19
+ * Targeted message delivered to the owner of a specific subscription. The
20
+ * owner applies it locally against the buffered subscriber stream.
21
+ */
22
+ export type DeliverEnvelope = {
23
+ readonly kind: "update";
24
+ readonly subId: string;
25
+ readonly update: unknown;
26
+ } | {
27
+ readonly kind: "complete";
28
+ readonly subId: string;
29
+ } | {
30
+ readonly kind: "completeExceptionally";
31
+ readonly subId: string;
32
+ readonly error: SerializedError;
33
+ };
34
+ /**
35
+ * Gossip envelopes broadcast to every instance over the fanout exchange.
36
+ *
37
+ * `claim`/`release` keep the cluster-wide subscriber mirror in sync. `sync`
38
+ * announces a fresh instance and prompts peers to re-emit their owned claims
39
+ * so the joiner can bootstrap its mirror.
40
+ *
41
+ * Every envelope carries the publisher's `ownerInstanceId` so receivers can
42
+ * drop their own loopback (the local mirror was already updated synchronously
43
+ * on the publish path).
44
+ */
45
+ export type GossipEnvelope = {
46
+ readonly kind: "claim";
47
+ readonly ownerInstanceId: string;
48
+ readonly subId: string;
49
+ readonly queryName: string;
50
+ readonly payload: unknown;
51
+ } | {
52
+ readonly kind: "release";
53
+ readonly ownerInstanceId: string;
54
+ readonly subId: string;
55
+ } | {
56
+ readonly kind: "syncRequest";
57
+ readonly requesterId: string;
58
+ };
59
+ export interface DistributedSubscriberRegistry {
60
+ /** Stable identifier for this process. */
61
+ readonly instanceId: string;
62
+ /** Add (or overwrite) a locally-owned subscriber and broadcast the claim. */
63
+ claim(record: Omit<SubscriberRecord, "ownerInstanceId">): Promise<void>;
64
+ /** Remove a locally-owned subscriber and broadcast the release. */
65
+ release(subId: string): Promise<void>;
66
+ /** Iterate every record in the cluster-wide mirror (locally owned + remote). */
67
+ records(): IterableIterator<SubscriberRecord>;
68
+ /**
69
+ * Route a delivery to the owner of `subId`. Local owners are dispatched
70
+ * synchronously to the in-process handler; remote owners receive a direct-
71
+ * queue publish keyed by their instanceId.
72
+ */
73
+ deliver(envelope: DeliverEnvelope): Promise<void>;
74
+ /** Set the handler invoked when a `DeliverEnvelope` for a local sub arrives. */
75
+ setDeliverHandler(handler: (envelope: DeliverEnvelope) => void): void;
76
+ connect(): Promise<void>;
77
+ close(): Promise<void>;
78
+ }
79
+ /**
80
+ * AMQP-backed implementation.
81
+ *
82
+ * Topology:
83
+ *
84
+ * - `<prefix>.subscribers.gossip` — fanout exchange. Every instance owns an
85
+ * exclusive auto-delete queue bound to it. Carries claim / release /
86
+ * syncRequest messages.
87
+ *
88
+ * - `<prefix>.subscribers.direct` — direct exchange. Every instance owns an
89
+ * exclusive auto-delete queue bound by routing key equal to its
90
+ * `instanceId`. Carries DeliverEnvelope messages targeted at the owner.
91
+ *
92
+ * Consume mode is no-ack on both queues — the registry is a best-effort eventual
93
+ * mirror. A dropped claim is healed by the next sync request; a dropped deliver
94
+ * looks like a missed update, the same failure mode the broker-routed model has
95
+ * when a stream segment is lost.
96
+ *
97
+ * Loopback dedup: the publisher applies its own claim/release to the local
98
+ * mirror synchronously before publishing, and ignores its own envelopes on the
99
+ * inbound side via `ownerInstanceId === instanceId`.
100
+ *
101
+ * Joiner protocol: on connect the new instance publishes a `syncRequest`.
102
+ * Existing peers respond by re-broadcasting each of their owned claims over
103
+ * the same fanout exchange. New instance fills its mirror as they arrive; in
104
+ * the meantime emits routed to subs the joiner has yet to learn about are
105
+ * dropped on the joiner side — by design, since the joiner can't have any of
106
+ * its own subscribers yet either. The window self-closes as the mirror fills.
107
+ */
108
+ export declare class AmqpDistributedSubscriberRegistry implements DistributedSubscriberRegistry {
109
+ private readonly config;
110
+ private readonly connection;
111
+ private channel;
112
+ private connectPromise;
113
+ private closed;
114
+ private deliverHandler;
115
+ private readonly mirror;
116
+ private readonly locallyOwnedSubIds;
117
+ readonly instanceId: string;
118
+ constructor(config: RabbitMqResolvedConfig, connection: AmqpConnection);
119
+ connect(): Promise<void>;
120
+ private doConnect;
121
+ close(): Promise<void>;
122
+ claim(record: Omit<SubscriberRecord, "ownerInstanceId">): Promise<void>;
123
+ release(subId: string): Promise<void>;
124
+ records(): IterableIterator<SubscriberRecord>;
125
+ deliver(envelope: DeliverEnvelope): Promise<void>;
126
+ setDeliverHandler(handler: (envelope: DeliverEnvelope) => void): void;
127
+ private publishGossip;
128
+ private handleGossip;
129
+ private handleDirect;
130
+ private requireChannel;
131
+ }
132
+ //# sourceMappingURL=distributed-subscriber-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distributed-subscriber-registry.d.ts","sourceRoot":"","sources":["../src/distributed-subscriber-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * AMQP-backed implementation.
3
+ *
4
+ * Topology:
5
+ *
6
+ * - `<prefix>.subscribers.gossip` — fanout exchange. Every instance owns an
7
+ * exclusive auto-delete queue bound to it. Carries claim / release /
8
+ * syncRequest messages.
9
+ *
10
+ * - `<prefix>.subscribers.direct` — direct exchange. Every instance owns an
11
+ * exclusive auto-delete queue bound by routing key equal to its
12
+ * `instanceId`. Carries DeliverEnvelope messages targeted at the owner.
13
+ *
14
+ * Consume mode is no-ack on both queues — the registry is a best-effort eventual
15
+ * mirror. A dropped claim is healed by the next sync request; a dropped deliver
16
+ * looks like a missed update, the same failure mode the broker-routed model has
17
+ * when a stream segment is lost.
18
+ *
19
+ * Loopback dedup: the publisher applies its own claim/release to the local
20
+ * mirror synchronously before publishing, and ignores its own envelopes on the
21
+ * inbound side via `ownerInstanceId === instanceId`.
22
+ *
23
+ * Joiner protocol: on connect the new instance publishes a `syncRequest`.
24
+ * Existing peers respond by re-broadcasting each of their owned claims over
25
+ * the same fanout exchange. New instance fills its mirror as they arrive; in
26
+ * the meantime emits routed to subs the joiner has yet to learn about are
27
+ * dropped on the joiner side — by design, since the joiner can't have any of
28
+ * its own subscribers yet either. The window self-closes as the mirror fills.
29
+ */
30
+ export class AmqpDistributedSubscriberRegistry {
31
+ config;
32
+ connection;
33
+ channel;
34
+ connectPromise;
35
+ closed = false;
36
+ deliverHandler;
37
+ mirror = new Map();
38
+ locallyOwnedSubIds = new Set();
39
+ instanceId;
40
+ constructor(config, connection) {
41
+ this.config = config;
42
+ this.connection = connection;
43
+ this.instanceId = `${config.identity.serviceName}.${config.identity.instanceId}`;
44
+ }
45
+ async connect() {
46
+ if (this.connectPromise)
47
+ return this.connectPromise;
48
+ this.connectPromise = this.doConnect();
49
+ return this.connectPromise;
50
+ }
51
+ async doConnect() {
52
+ this.channel = await this.connection.channel();
53
+ const ch = this.channel;
54
+ await ch.assertExchange(this.config.topology.subscribersGossipExchange, "fanout", {
55
+ durable: true,
56
+ });
57
+ await ch.assertExchange(this.config.topology.subscribersDirectExchange, "direct", {
58
+ durable: true,
59
+ });
60
+ const gossipQueue = this.config.topology.subscribersGossipQueue();
61
+ const directQueue = this.config.topology.subscribersDirectQueue();
62
+ await ch.assertQueue(gossipQueue, { durable: false, exclusive: true, autoDelete: true });
63
+ await ch.assertQueue(directQueue, { durable: false, exclusive: true, autoDelete: true });
64
+ await ch.bindQueue(gossipQueue, this.config.topology.subscribersGossipExchange, "");
65
+ await ch.bindQueue(directQueue, this.config.topology.subscribersDirectExchange, this.instanceId);
66
+ await ch.consume(gossipQueue, (msg) => this.handleGossip(msg), { noAck: true });
67
+ await ch.consume(directQueue, (msg) => this.handleDirect(msg), { noAck: true });
68
+ // Announce ourselves so existing peers re-broadcast their owned claims.
69
+ this.publishGossip({ kind: "syncRequest", requesterId: this.instanceId });
70
+ }
71
+ async close() {
72
+ this.closed = true;
73
+ await this.channel?.close().catch(() => { });
74
+ }
75
+ async claim(record) {
76
+ const full = { ...record, ownerInstanceId: this.instanceId };
77
+ this.mirror.set(full.subId, full);
78
+ this.locallyOwnedSubIds.add(full.subId);
79
+ await this.connect();
80
+ if (this.closed)
81
+ return;
82
+ this.publishGossip({
83
+ kind: "claim",
84
+ ownerInstanceId: this.instanceId,
85
+ subId: full.subId,
86
+ queryName: full.queryName,
87
+ payload: full.payload,
88
+ });
89
+ }
90
+ async release(subId) {
91
+ this.mirror.delete(subId);
92
+ this.locallyOwnedSubIds.delete(subId);
93
+ await this.connect();
94
+ if (this.closed)
95
+ return;
96
+ this.publishGossip({ kind: "release", ownerInstanceId: this.instanceId, subId });
97
+ }
98
+ *records() {
99
+ for (const record of this.mirror.values())
100
+ yield record;
101
+ }
102
+ async deliver(envelope) {
103
+ const record = this.mirror.get(envelope.subId);
104
+ if (!record)
105
+ return;
106
+ if (record.ownerInstanceId === this.instanceId) {
107
+ this.deliverHandler?.(envelope);
108
+ return;
109
+ }
110
+ await this.connect();
111
+ if (this.closed)
112
+ return;
113
+ const ch = this.requireChannel();
114
+ ch.publish(this.config.topology.subscribersDirectExchange, record.ownerInstanceId, Buffer.from(JSON.stringify(envelope)), { contentType: "application/json", persistent: false });
115
+ }
116
+ setDeliverHandler(handler) {
117
+ this.deliverHandler = handler;
118
+ }
119
+ publishGossip(envelope) {
120
+ if (this.closed)
121
+ return;
122
+ const ch = this.channel;
123
+ if (!ch)
124
+ return;
125
+ ch.publish(this.config.topology.subscribersGossipExchange, "", Buffer.from(JSON.stringify(envelope)), { contentType: "application/json", persistent: false });
126
+ }
127
+ handleGossip(msg) {
128
+ if (!msg)
129
+ return;
130
+ let envelope;
131
+ try {
132
+ envelope = JSON.parse(msg.content.toString("utf8"));
133
+ }
134
+ catch {
135
+ return;
136
+ }
137
+ if (envelope.kind === "claim") {
138
+ // Loopback — local mirror already updated synchronously by claim().
139
+ if (envelope.ownerInstanceId === this.instanceId)
140
+ return;
141
+ this.mirror.set(envelope.subId, {
142
+ subId: envelope.subId,
143
+ queryName: envelope.queryName,
144
+ payload: envelope.payload,
145
+ ownerInstanceId: envelope.ownerInstanceId,
146
+ });
147
+ }
148
+ else if (envelope.kind === "release") {
149
+ if (envelope.ownerInstanceId === this.instanceId)
150
+ return;
151
+ this.mirror.delete(envelope.subId);
152
+ }
153
+ else if (envelope.kind === "syncRequest") {
154
+ // Skip our own announcement; respond to every other instance by
155
+ // re-broadcasting our owned claims over the same fanout exchange.
156
+ if (envelope.requesterId === this.instanceId)
157
+ return;
158
+ for (const subId of this.locallyOwnedSubIds) {
159
+ const record = this.mirror.get(subId);
160
+ if (!record)
161
+ continue;
162
+ this.publishGossip({
163
+ kind: "claim",
164
+ ownerInstanceId: this.instanceId,
165
+ subId: record.subId,
166
+ queryName: record.queryName,
167
+ payload: record.payload,
168
+ });
169
+ }
170
+ }
171
+ }
172
+ handleDirect(msg) {
173
+ if (!msg)
174
+ return;
175
+ if (!this.deliverHandler)
176
+ return;
177
+ let envelope;
178
+ try {
179
+ envelope = JSON.parse(msg.content.toString("utf8"));
180
+ }
181
+ catch {
182
+ return;
183
+ }
184
+ this.deliverHandler(envelope);
185
+ }
186
+ requireChannel() {
187
+ if (!this.channel)
188
+ throw new Error("Distributed subscriber registry is not connected");
189
+ return this.channel;
190
+ }
191
+ }
192
+ //# sourceMappingURL=distributed-subscriber-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distributed-subscriber-registry.js","sourceRoot":"","sources":["../src/distributed-subscriber-registry.ts"],"names":[],"mappings":"AAmFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,OAAO,iCAAiC;IAWzB;IACA;IAXX,OAAO,CAAqB;IAC5B,cAAc,CAA2B;IACzC,MAAM,GAAG,KAAK,CAAA;IACd,cAAc,CAAmD;IACxD,MAAM,GAAG,IAAI,GAAG,EAA4B,CAAA;IAC5C,kBAAkB,GAAG,IAAI,GAAG,EAAU,CAAA;IAE9C,UAAU,CAAQ;IAE3B,YACmB,MAA8B,EAC9B,UAA0B;QAD1B,WAAM,GAAN,MAAM,CAAwB;QAC9B,eAAU,GAAV,UAAU,CAAgB;QAE3C,IAAI,CAAC,UAAU,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,IAAI,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAA;IAClF,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,cAAc;YAAE,OAAO,IAAI,CAAC,cAAc,CAAA;QACnD,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,SAAS,EAAE,CAAA;QACtC,OAAO,IAAI,CAAC,cAAc,CAAA;IAC5B,CAAC;IAEO,KAAK,CAAC,SAAS;QACrB,IAAI,CAAC,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAA;QAC9C,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAA;QAEvB,MAAM,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,EAAE,QAAQ,EAAE;YAChF,OAAO,EAAE,IAAI;SACd,CAAC,CAAA;QACF,MAAM,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,EAAE,QAAQ,EAAE;YAChF,OAAO,EAAE,IAAI;SACd,CAAC,CAAA;QAEF,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,sBAAsB,EAAE,CAAA;QACjE,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,sBAAsB,EAAE,CAAA;QAEjE,MAAM,EAAE,CAAC,WAAW,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAA;QACxF,MAAM,EAAE,CAAC,WAAW,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAA;QAExF,MAAM,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,EAAE,EAAE,CAAC,CAAA;QACnF,MAAM,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QAEhG,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/E,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAE/E,wEAAwE;QACxE,IAAI,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAA;IAC3E,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAClB,MAAM,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAC7C,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,MAAiD;QAC3D,MAAM,IAAI,GAAqB,EAAE,GAAG,MAAM,EAAE,eAAe,EAAE,IAAI,CAAC,UAAU,EAAE,CAAA;QAC9E,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;QACjC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvC,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;QACpB,IAAI,IAAI,CAAC,MAAM;YAAE,OAAM;QACvB,IAAI,CAAC,aAAa,CAAC;YACjB,IAAI,EAAE,OAAO;YACb,eAAe,EAAE,IAAI,CAAC,UAAU;YAChC,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,OAAO,EAAE,IAAI,CAAC,OAAO;SACtB,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,KAAa;QACzB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACzB,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACrC,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;QACpB,IAAI,IAAI,CAAC,MAAM;YAAE,OAAM;QACvB,IAAI,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;IAClF,CAAC;IAED,CAAC,OAAO;QACN,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;YAAE,MAAM,MAAM,CAAA;IACzD,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,QAAyB;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QAC9C,IAAI,CAAC,MAAM;YAAE,OAAM;QAEnB,IAAI,MAAM,CAAC,eAAe,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YAC/C,IAAI,CAAC,cAAc,EAAE,CAAC,QAAQ,CAAC,CAAA;YAC/B,OAAM;QACR,CAAC;QAED,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;QACpB,IAAI,IAAI,CAAC,MAAM;YAAE,OAAM;QACvB,MAAM,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,CAAA;QAChC,EAAE,CAAC,OAAO,CACR,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,EAC9C,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,EACrC,EAAE,WAAW,EAAE,kBAAkB,EAAE,UAAU,EAAE,KAAK,EAAE,CACvD,CAAA;IACH,CAAC;IAED,iBAAiB,CAAC,OAA4C;QAC5D,IAAI,CAAC,cAAc,GAAG,OAAO,CAAA;IAC/B,CAAC;IAEO,aAAa,CAAC,QAAwB;QAC5C,IAAI,IAAI,CAAC,MAAM;YAAE,OAAM;QACvB,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAA;QACvB,IAAI,CAAC,EAAE;YAAE,OAAM;QACf,EAAE,CAAC,OAAO,CACR,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,EAC9C,EAAE,EACF,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,EACrC,EAAE,WAAW,EAAE,kBAAkB,EAAE,UAAU,EAAE,KAAK,EAAE,CACvD,CAAA;IACH,CAAC;IAEO,YAAY,CAAC,GAA0B;QAC7C,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,IAAI,QAAwB,CAAA;QAC5B,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAmB,CAAA;QACvE,CAAC;QAAC,MAAM,CAAC;YACP,OAAM;QACR,CAAC;QAED,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC9B,oEAAoE;YACpE,IAAI,QAAQ,CAAC,eAAe,KAAK,IAAI,CAAC,UAAU;gBAAE,OAAM;YACxD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE;gBAC9B,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO;gBACzB,eAAe,EAAE,QAAQ,CAAC,eAAe;aAC1C,CAAC,CAAA;QACJ,CAAC;aAAM,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACvC,IAAI,QAAQ,CAAC,eAAe,KAAK,IAAI,CAAC,UAAU;gBAAE,OAAM;YACxD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QACpC,CAAC;aAAM,IAAI,QAAQ,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YAC3C,gEAAgE;YAChE,kEAAkE;YAClE,IAAI,QAAQ,CAAC,WAAW,KAAK,IAAI,CAAC,UAAU;gBAAE,OAAM;YACpD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;gBACrC,IAAI,CAAC,MAAM;oBAAE,SAAQ;gBACrB,IAAI,CAAC,aAAa,CAAC;oBACjB,IAAI,EAAE,OAAO;oBACb,eAAe,EAAE,IAAI,CAAC,UAAU;oBAChC,KAAK,EAAE,MAAM,CAAC,KAAK;oBACnB,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,OAAO,EAAE,MAAM,CAAC,OAAO;iBACxB,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAEO,YAAY,CAAC,GAA0B;QAC7C,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,IAAI,CAAC,IAAI,CAAC,cAAc;YAAE,OAAM;QAChC,IAAI,QAAyB,CAAA;QAC7B,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAoB,CAAA;QACxE,CAAC;QAAC,MAAM,CAAC;YACP,OAAM;QACR,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAA;IAC/B,CAAC;IAEO,cAAc;QACpB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;QACtF,OAAO,IAAI,CAAC,OAAO,CAAA;IACrB,CAAC;CACF"}
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { rabbitMq, resolveRabbitMqConfig, type RabbitMqExtensionConfig, type Rab
2
2
  export { createRabbitMqTopologyNames, type RabbitMqTopologyConfig, type RabbitMqTopologyNames, } from "./topology.js";
3
3
  export { createRabbitMqCommandBus, type RabbitMqCommandEnvelope, type RabbitMqCommandReplyEnvelope, type RabbitMqCommandTransport, type RabbitMqCommandBusOptions, } from "./command-bus.js";
4
4
  export { createRabbitMqQueryBus, type RabbitMqQueryEnvelope, type RabbitMqQueryReplyEnvelope, type RabbitMqQueryTransport, type RabbitMqQueryBusOptions, } from "./query-bus.js";
5
+ export { AmqpDistributedSubscriberRegistry, type DistributedSubscriberRegistry, type SubscriberRecord, type DeliverEnvelope, type GossipEnvelope, } from "./distributed-subscriber-registry.js";
5
6
  export { AmqpRabbitMqCommandTransport } from "./amqp-command-transport.js";
6
7
  export { AmqpRabbitMqQueryTransport } from "./amqp-query-transport.js";
7
8
  export { createAmqpConnection, type AmqpConnection, type AmqpConnect, } from "./connection.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,qBAAqB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,2BAA2B,EAChC,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,GAC5B,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,2BAA2B,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,wBAAwB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,sBAAsB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAA;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AAEtE,OAAO,EACL,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,qBAAqB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,2BAA2B,EAChC,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,GAC5B,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,2BAA2B,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,wBAAwB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,4BAA4B,EACjC,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,sBAAsB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,iCAAiC,EACjC,KAAK,6BAA6B,EAClC,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,sCAAsC,CAAA;AAE7C,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAA;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AAEtE,OAAO,EACL,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAA"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { rabbitMq, resolveRabbitMqConfig, } from "./rabbitmq.js";
2
2
  export { createRabbitMqTopologyNames, } from "./topology.js";
3
3
  export { createRabbitMqCommandBus, } from "./command-bus.js";
4
4
  export { createRabbitMqQueryBus, } from "./query-bus.js";
5
+ export { AmqpDistributedSubscriberRegistry, } from "./distributed-subscriber-registry.js";
5
6
  export { AmqpRabbitMqCommandTransport } from "./amqp-command-transport.js";
6
7
  export { AmqpRabbitMqQueryTransport } from "./amqp-query-transport.js";
7
8
  export { createAmqpConnection, } from "./connection.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,qBAAqB,GAMtB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,2BAA2B,GAG5B,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,wBAAwB,GAKzB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,sBAAsB,GAKvB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAA;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AAEtE,OAAO,EACL,oBAAoB,GAGrB,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,qBAAqB,GAMtB,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,2BAA2B,GAG5B,MAAM,eAAe,CAAA;AAEtB,OAAO,EACL,wBAAwB,GAKzB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,sBAAsB,GAKvB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,iCAAiC,GAKlC,MAAM,sCAAsC,CAAA;AAE7C,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAA;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AAEtE,OAAO,EACL,oBAAoB,GAGrB,MAAM,iBAAiB,CAAA"}
@@ -1,5 +1,6 @@
1
1
  import type { QueryBus, QueryMessage } from "@kronos-ts/messaging";
2
2
  import type { RabbitMqResolvedConfig } from "./rabbitmq.js";
3
+ import type { DistributedSubscriberRegistry } from "./distributed-subscriber-registry.js";
3
4
  export interface RabbitMqQueryEnvelope {
4
5
  readonly kind: "query";
5
6
  readonly requestId: string;
@@ -23,16 +24,31 @@ export interface RabbitMqQueryTransport {
23
24
  export interface RabbitMqQueryBusOptions {
24
25
  readonly localSegment: QueryBus;
25
26
  readonly transport: RabbitMqQueryTransport;
27
+ readonly subscriberRegistry?: DistributedSubscriberRegistry;
26
28
  readonly config: RabbitMqResolvedConfig;
27
29
  }
28
30
  /**
29
31
  * Distributed query bus decorator.
30
32
  *
31
- * Direct request/reply queries (`query` + `subscribe`) route over RabbitMQ.
32
- * Subscription queries (`subscriptionQuery`, `subscribeToUpdates`, `emitUpdate`,
33
- * `completeSubscription*`) remain process-local and delegate to the local
34
- * segment distributing subscription-query update streams over RabbitMQ is
35
- * intentionally out of scope for this version (see rabbitmq-extension-plan.md).
33
+ * Direct request/reply queries (`query` + `subscribe`) route over the
34
+ * request/reply transport.
35
+ *
36
+ * Subscription queries use a distributed-mirror model. Every subscribe
37
+ * publishes a `claim` over the gossip fanout exchange so every instance
38
+ * learns about it; every unsubscribe publishes a `release`. Each instance
39
+ * keeps a cluster-wide `Map<subId, SubscriberRecord>` mirror.
40
+ *
41
+ * `emitUpdate` walks the mirror locally (it holds every cluster-wide
42
+ * subscriber's payload), applies the filter — function predicates work
43
+ * because evaluation happens colocated with the payload, not over the wire —
44
+ * and routes per-subscriber delivery via the registry. Local subs are
45
+ * dispatched in-process; remote subs receive a `DeliverEnvelope` on the
46
+ * owner's direct queue.
47
+ *
48
+ * The same model handles `completeSubscription` and
49
+ * `completeSubscriptionExceptionally`.
50
+ *
51
+ * Falls back to local-only behaviour when no `subscriberRegistry` is supplied.
36
52
  */
37
53
  export declare function createRabbitMqQueryBus(options: RabbitMqQueryBusOptions): QueryBus;
38
54
  //# sourceMappingURL=query-bus.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"query-bus.d.ts","sourceRoot":"","sources":["../src/query-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAA2B,MAAM,sBAAsB,CAAA;AAG3F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAE3D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KACxB,CAAA;CACF;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAA;IAC9E,SAAS,CACP,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,OAAO,CAAC,0BAA0B,CAAC,GAChF,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAA;IAC/B,QAAQ,CAAC,SAAS,EAAE,sBAAsB,CAAA;IAC1C,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAA;CACxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,QAAQ,CA4EjF"}
1
+ {"version":3,"file":"query-bus.d.ts","sourceRoot":"","sources":["../src/query-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EAIb,MAAM,sBAAsB,CAAA;AAQ7B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAC3D,OAAO,KAAK,EAEV,6BAA6B,EAC9B,MAAM,sCAAsC,CAAA;AAE7C,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KACxB,CAAA;CACF;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAA;IAC9E,SAAS,CACP,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,OAAO,CAAC,0BAA0B,CAAC,GAChF,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAA;IAC/B,QAAQ,CAAC,SAAS,EAAE,sBAAsB,CAAA;IAC1C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,6BAA6B,CAAA;IAC3D,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAA;CACxC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,QAAQ,CAmOjF"}