@miiajs/messaging 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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/decorators.d.ts +149 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +112 -0
- package/dist/decorators.js.map +1 -0
- package/dist/group-name.d.ts +33 -0
- package/dist/group-name.d.ts.map +1 -0
- package/dist/group-name.js +25 -0
- package/dist/group-name.js.map +1 -0
- package/dist/idempotency.d.ts +66 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +42 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/in-memory-transport.d.ts +60 -0
- package/dist/in-memory-transport.d.ts.map +1 -0
- package/dist/in-memory-transport.js +143 -0
- package/dist/in-memory-transport.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/message-bus.d.ts +83 -0
- package/dist/message-bus.d.ts.map +1 -0
- package/dist/message-bus.js +218 -0
- package/dist/message-bus.js.map +1 -0
- package/dist/messaging.module.d.ts +76 -0
- package/dist/messaging.module.d.ts.map +1 -0
- package/dist/messaging.module.js +74 -0
- package/dist/messaging.module.js.map +1 -0
- package/dist/retry.d.ts +7 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +10 -0
- package/dist/retry.js.map +1 -0
- package/dist/tokens.d.ts +19 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +26 -0
- package/dist/tokens.js.map +1 -0
- package/dist/types.d.ts +165 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Logger } from '@miiajs/core';
|
|
2
|
+
import { dlqTopic, nextBackoffMs } from './retry.js';
|
|
3
|
+
import { DEFAULT_RETRY, } from './types.js';
|
|
4
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 5000;
|
|
5
|
+
/**
|
|
6
|
+
* In-process message transport. Fire-and-forget delivery via `queueMicrotask`,
|
|
7
|
+
* exponential backoff retry via `setTimeout`, auto-DLQ via re-publishing to
|
|
8
|
+
* `<topic>.dlq`.
|
|
9
|
+
*
|
|
10
|
+
* NOT persistent. Process crash mid-retry loses pending messages - do not use
|
|
11
|
+
* for durability-sensitive workloads. Use `@miiajs/messaging-redis` or another
|
|
12
|
+
* broker-backed transport in production.
|
|
13
|
+
*
|
|
14
|
+
* **Dispatch capability:** sliding-only, no competing consumers. The in-memory
|
|
15
|
+
* transport is single-process: there is no broker to round-robin messages
|
|
16
|
+
* between multiple handlers in a shared `group`. Auto-derived per-handler
|
|
17
|
+
* groups work normally (each handler is its own subscription, fan-out is
|
|
18
|
+
* automatic). Handlers requesting `mode: 'batch'` or sharing an explicit
|
|
19
|
+
* `group` are rejected at `MessageBus.onReady()`.
|
|
20
|
+
*/
|
|
21
|
+
export class InMemoryTransport {
|
|
22
|
+
supportedModes = ['sliding'];
|
|
23
|
+
defaultMode = 'sliding';
|
|
24
|
+
supportsCompetingConsumers = false;
|
|
25
|
+
subs = new Map();
|
|
26
|
+
retry;
|
|
27
|
+
cloneOnPublish;
|
|
28
|
+
drainTimeoutMs;
|
|
29
|
+
logger = new Logger('InMemoryTransport');
|
|
30
|
+
pendingTimers = new Set();
|
|
31
|
+
pendingDeliveries = new Set();
|
|
32
|
+
destroying = false;
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
35
|
+
this.cloneOnPublish = options.cloneOnPublish ?? false;
|
|
36
|
+
this.drainTimeoutMs = options.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
37
|
+
}
|
|
38
|
+
async publish(envelope) {
|
|
39
|
+
const subs = this.subs.get(envelope.topic);
|
|
40
|
+
if (!subs || subs.length === 0)
|
|
41
|
+
return;
|
|
42
|
+
// Fire-and-forget: resolve immediately, handlers run in microtask.
|
|
43
|
+
queueMicrotask(() => {
|
|
44
|
+
for (const sub of subs) {
|
|
45
|
+
this.deliver(sub, envelope);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async subscribe(topic, handler, _options) {
|
|
50
|
+
// SubscribeOptions.group and .concurrency are meaningless in a single
|
|
51
|
+
// process - local fan-out always delivers to every subscriber.
|
|
52
|
+
const sub = { topic, handler };
|
|
53
|
+
const list = this.subs.get(topic) ?? [];
|
|
54
|
+
list.push(sub);
|
|
55
|
+
this.subs.set(topic, list);
|
|
56
|
+
return {
|
|
57
|
+
unsubscribe: async () => {
|
|
58
|
+
const list = this.subs.get(topic);
|
|
59
|
+
if (!list)
|
|
60
|
+
return;
|
|
61
|
+
const idx = list.indexOf(sub);
|
|
62
|
+
if (idx >= 0)
|
|
63
|
+
list.splice(idx, 1);
|
|
64
|
+
if (list.length === 0)
|
|
65
|
+
this.subs.delete(topic);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async onDestroy() {
|
|
70
|
+
this.destroying = true;
|
|
71
|
+
await this.waitForDrain();
|
|
72
|
+
for (const timer of this.pendingTimers)
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
this.pendingTimers.clear();
|
|
75
|
+
this.subs.clear();
|
|
76
|
+
}
|
|
77
|
+
/** Tracking wrapper - every (initial and retry) delivery flows through here. */
|
|
78
|
+
deliver(sub, envelope) {
|
|
79
|
+
if (this.destroying)
|
|
80
|
+
return; // refuse new work during shutdown
|
|
81
|
+
const promise = this.runDelivery(sub, envelope);
|
|
82
|
+
this.pendingDeliveries.add(promise);
|
|
83
|
+
promise.finally(() => this.pendingDeliveries.delete(promise));
|
|
84
|
+
}
|
|
85
|
+
async runDelivery(sub, envelope) {
|
|
86
|
+
const delivered = this.cloneOnPublish ? { ...envelope, payload: structuredClone(envelope.payload) } : envelope;
|
|
87
|
+
let result;
|
|
88
|
+
try {
|
|
89
|
+
result = await sub.handler(delivered);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
result = { status: 'nack', error: err instanceof Error ? err : new Error(String(err)) };
|
|
93
|
+
}
|
|
94
|
+
if (result.status === 'ack')
|
|
95
|
+
return;
|
|
96
|
+
// Nack: retry or DLQ.
|
|
97
|
+
if (envelope.meta.attempt < this.retry.maxAttempts) {
|
|
98
|
+
const delay = nextBackoffMs(envelope.meta.attempt, this.retry);
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
this.pendingTimers.delete(timer);
|
|
101
|
+
// Retry routes back through deliver() so it's tracked in pendingDeliveries.
|
|
102
|
+
this.deliver(sub, {
|
|
103
|
+
...envelope,
|
|
104
|
+
meta: { ...envelope.meta, attempt: envelope.meta.attempt + 1 },
|
|
105
|
+
});
|
|
106
|
+
}, delay);
|
|
107
|
+
this.pendingTimers.add(timer);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Final failure.
|
|
111
|
+
this.logger.error(`Message ${envelope.id} on ${envelope.topic} exhausted ${this.retry.maxAttempts} attempts`, result.error.stack ?? result.error.message);
|
|
112
|
+
if (this.retry.dlq) {
|
|
113
|
+
await this.publish({
|
|
114
|
+
...envelope,
|
|
115
|
+
topic: dlqTopic(envelope.topic),
|
|
116
|
+
meta: { ...envelope.meta, lastError: result.error.message },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async waitForDrain() {
|
|
121
|
+
if (this.drainTimeoutMs <= 0 || this.pendingDeliveries.size === 0)
|
|
122
|
+
return;
|
|
123
|
+
const drainPromise = Promise.allSettled([...this.pendingDeliveries]);
|
|
124
|
+
let timer = null;
|
|
125
|
+
const timeoutPromise = new Promise((r) => {
|
|
126
|
+
timer = setTimeout(() => r('timeout'), this.drainTimeoutMs);
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
129
|
+
const result = await Promise.race([drainPromise.then(() => 'drained'), timeoutPromise]);
|
|
130
|
+
if (result === 'timeout') {
|
|
131
|
+
this.logger.warn(`Drain timeout: ${this.pendingDeliveries.size} handler(s) still in flight`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
if (timer)
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function inMemoryTransport(options = {}) {
|
|
141
|
+
return new InMemoryTransport(options);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=in-memory-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-transport.js","sourceRoot":"","sources":["../src/in-memory-transport.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { MessageBus } from './message-bus.js';
|
|
2
|
+
export { Idempotent, IDEMPOTENT, On, ON } from './decorators.js';
|
|
3
|
+
export type { IdempotentMeta, OnMeta } from './decorators.js';
|
|
4
|
+
export { IDEMPOTENCY_STORE, MemoryIdempotencyStore, memoryIdempotencyStore } from './idempotency.js';
|
|
5
|
+
export type { IdempotencyStore, MemoryIdempotencyStoreOptions } from './idempotency.js';
|
|
6
|
+
export { DEFAULT_RETRY, MESSAGE_TRANSPORT } from './types.js';
|
|
7
|
+
export type { DispatchMode, MessageEnvelope, MessageMeta, MessageTransport, HandlerResult, RetryConfig, SubscribeOptions, Subscription, } from './types.js';
|
|
8
|
+
export { dlqTopic, nextBackoffMs } from './retry.js';
|
|
9
|
+
export { deriveGroupName } from './group-name.js';
|
|
10
|
+
export type { DeriveGroupNameInput } from './group-name.js';
|
|
11
|
+
export { InMemoryTransport, inMemoryTransport } from './in-memory-transport.js';
|
|
12
|
+
export type { InMemoryTransportOptions } from './in-memory-transport.js';
|
|
13
|
+
export { MessagingModule } from './messaging.module.js';
|
|
14
|
+
export type { DispatchDefaults, MessagingModuleOptions } from './messaging.module.js';
|
|
15
|
+
export { getMessageBusToken, getMessageTransportToken, getIdempotencyStoreToken } from './tokens.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE7C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAE7D,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AACpG,YAAY,EAAE,gBAAgB,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAA;AAEvF,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC7D,YAAY,EACV,YAAY,EACZ,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,YAAY,GACb,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEpD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC/E,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AAExE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,YAAY,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAErF,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @miiajs/messaging - public API
|
|
2
|
+
export { MessageBus } from './message-bus.js';
|
|
3
|
+
export { Idempotent, IDEMPOTENT, On, ON } from './decorators.js';
|
|
4
|
+
export { IDEMPOTENCY_STORE, MemoryIdempotencyStore, memoryIdempotencyStore } from './idempotency.js';
|
|
5
|
+
export { DEFAULT_RETRY, MESSAGE_TRANSPORT } from './types.js';
|
|
6
|
+
export { dlqTopic, nextBackoffMs } from './retry.js';
|
|
7
|
+
export { deriveGroupName } from './group-name.js';
|
|
8
|
+
export { InMemoryTransport, inMemoryTransport } from './in-memory-transport.js';
|
|
9
|
+
export { MessagingModule } from './messaging.module.js';
|
|
10
|
+
export { getMessageBusToken, getMessageTransportToken, getIdempotencyStoreToken } from './tokens.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iCAAiC;AAEjC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE7C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAGhE,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAGpG,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAY7D,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEpD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAGjD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAG/E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAGvD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { IdempotencyStore } from './idempotency.js';
|
|
2
|
+
import { type DispatchMode, type MessageMeta, type MessageTransport } from './types.js';
|
|
3
|
+
/** Bus-level dispatch defaults forwarded from `MessagingModuleOptions.dispatch`. */
|
|
4
|
+
interface DispatchDefaults {
|
|
5
|
+
mode?: DispatchMode;
|
|
6
|
+
concurrency?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Options bag for the `MessageBus` constructor 4th argument. */
|
|
9
|
+
interface MessageBusOptions {
|
|
10
|
+
dispatch?: DispatchDefaults | null;
|
|
11
|
+
appName?: string | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Central message bus. Inject from any provider:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* @Injectable()
|
|
18
|
+
* class OrderService {
|
|
19
|
+
* private bus = inject(MessageBus)
|
|
20
|
+
*
|
|
21
|
+
* async placeOrder(order: Order) {
|
|
22
|
+
* await this.bus.publish('order.placed', order)
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* `@On` handlers are discovered via `DiscoveryService` during `onReady()`.
|
|
28
|
+
* Each `@On` becomes its own broker subscription with an auto-derived
|
|
29
|
+
* consumer group `<topic>__<ClassName>_<methodName>` (or
|
|
30
|
+
* `<appName>:<topic>__<ClassName>_<methodName>` when `MessagingModule.configure`
|
|
31
|
+
* provides `appName`). Handlers run independently - retry, ack/nack,
|
|
32
|
+
* mode/concurrency are isolated per handler.
|
|
33
|
+
*
|
|
34
|
+
* For multi-bus setups, use `MessagingModule.configure(opts, name)` and inject
|
|
35
|
+
* the named bus via `inject<MessageBus>(getMessageBusToken(name))`. Handlers
|
|
36
|
+
* target a specific bus via `@On(topic, { bus: name })`.
|
|
37
|
+
*
|
|
38
|
+
* `MessageBus.publish` and `MessageTransport.publish` share the method name
|
|
39
|
+
* but live at different layers: `MessageBus.publish(topic, payload, meta)`
|
|
40
|
+
* is the high-level API that fills in `id`/`timestamp`/`attempt` and hands
|
|
41
|
+
* a fully-formed envelope to `MessageTransport.publish(envelope)`. Both can
|
|
42
|
+
* appear in stack traces; signature shape (envelope vs payload+meta) makes
|
|
43
|
+
* the layer obvious.
|
|
44
|
+
*/
|
|
45
|
+
export declare class MessageBus {
|
|
46
|
+
private transport;
|
|
47
|
+
private idempotencyStore;
|
|
48
|
+
private busName;
|
|
49
|
+
private options;
|
|
50
|
+
private discovery;
|
|
51
|
+
private resolver;
|
|
52
|
+
private logger;
|
|
53
|
+
private subscriptions;
|
|
54
|
+
constructor(transport: MessageTransport, idempotencyStore: IdempotencyStore | null, busName: string | null, options?: MessageBusOptions);
|
|
55
|
+
onReady(): Promise<void>;
|
|
56
|
+
private getIdempotentMeta;
|
|
57
|
+
/**
|
|
58
|
+
* Publish a message. Meta fields are optional and additive - the framework
|
|
59
|
+
* fills in `id`, `timestamp`, and `attempt`. Composes onto
|
|
60
|
+
* `MessageTransport.publish(envelope)` internally.
|
|
61
|
+
*
|
|
62
|
+
* For distributed tracing, populate `traceparent` (and optionally `tracestate`)
|
|
63
|
+
* from your tracing library so consumers can continue the trace.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* import { trace } from '@opentelemetry/api'
|
|
68
|
+
*
|
|
69
|
+
* const span = trace.getActiveSpan()
|
|
70
|
+
* const ctx = span?.spanContext()
|
|
71
|
+
* await bus.publish('order.placed', order, {
|
|
72
|
+
* traceparent: ctx
|
|
73
|
+
* ? `00-${ctx.traceId}-${ctx.spanId}-${ctx.traceFlags.toString(16).padStart(2, '0')}`
|
|
74
|
+
* : undefined,
|
|
75
|
+
* })
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
publish<T>(topic: string, payload: T, meta?: Partial<Pick<MessageMeta, 'correlationId' | 'causationId' | 'traceparent' | 'tracestate'>>): Promise<void>;
|
|
79
|
+
onDestroy(): Promise<void>;
|
|
80
|
+
private dispatch;
|
|
81
|
+
}
|
|
82
|
+
export {};
|
|
83
|
+
//# sourceMappingURL=message-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-bus.d.ts","sourceRoot":"","sources":["../src/message-bus.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD,OAAO,EACL,KAAK,YAAY,EAGjB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EAEtB,MAAM,YAAY,CAAA;AAEnB,oFAAoF;AACpF,UAAU,gBAAgB;IACxB,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,iEAAiE;AACjE,UAAU,iBAAiB;IACzB,QAAQ,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAClC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,UAAU;IASnB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,OAAO;IAVjB,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,QAAQ,CAAmB;IAEnC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,aAAa,CAAqB;gBAGhC,SAAS,EAAE,gBAAgB,EAC3B,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,EACzC,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,OAAO,GAAE,iBAAsB;IAKnC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA8G9B,OAAO,CAAC,iBAAiB;IAKzB;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,OAAO,CAAC,CAAC,EACb,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,EACV,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,eAAe,GAAG,aAAa,GAAG,aAAa,GAAG,YAAY,CAAC,CAAC,GAChG,OAAO,CAAC,IAAI,CAAC;IAcV,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;YAOlB,QAAQ;CA6CvB"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { DiscoveryService, getMeta, inject, Logger, Resolver } from '@miiajs/core';
|
|
3
|
+
import { IDEMPOTENT, ON } from './decorators.js';
|
|
4
|
+
import { deriveGroupName } from './group-name.js';
|
|
5
|
+
import { getMessageBusToken } from './tokens.js';
|
|
6
|
+
import {} from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Central message bus. Inject from any provider:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* @Injectable()
|
|
12
|
+
* class OrderService {
|
|
13
|
+
* private bus = inject(MessageBus)
|
|
14
|
+
*
|
|
15
|
+
* async placeOrder(order: Order) {
|
|
16
|
+
* await this.bus.publish('order.placed', order)
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* `@On` handlers are discovered via `DiscoveryService` during `onReady()`.
|
|
22
|
+
* Each `@On` becomes its own broker subscription with an auto-derived
|
|
23
|
+
* consumer group `<topic>__<ClassName>_<methodName>` (or
|
|
24
|
+
* `<appName>:<topic>__<ClassName>_<methodName>` when `MessagingModule.configure`
|
|
25
|
+
* provides `appName`). Handlers run independently - retry, ack/nack,
|
|
26
|
+
* mode/concurrency are isolated per handler.
|
|
27
|
+
*
|
|
28
|
+
* For multi-bus setups, use `MessagingModule.configure(opts, name)` and inject
|
|
29
|
+
* the named bus via `inject<MessageBus>(getMessageBusToken(name))`. Handlers
|
|
30
|
+
* target a specific bus via `@On(topic, { bus: name })`.
|
|
31
|
+
*
|
|
32
|
+
* `MessageBus.publish` and `MessageTransport.publish` share the method name
|
|
33
|
+
* but live at different layers: `MessageBus.publish(topic, payload, meta)`
|
|
34
|
+
* is the high-level API that fills in `id`/`timestamp`/`attempt` and hands
|
|
35
|
+
* a fully-formed envelope to `MessageTransport.publish(envelope)`. Both can
|
|
36
|
+
* appear in stack traces; signature shape (envelope vs payload+meta) makes
|
|
37
|
+
* the layer obvious.
|
|
38
|
+
*/
|
|
39
|
+
export class MessageBus {
|
|
40
|
+
transport;
|
|
41
|
+
idempotencyStore;
|
|
42
|
+
busName;
|
|
43
|
+
options;
|
|
44
|
+
// Field-init for shared globals (idiomatic MiiaJS pattern).
|
|
45
|
+
discovery = inject(DiscoveryService);
|
|
46
|
+
resolver = inject(Resolver);
|
|
47
|
+
// Logger init in constructor body because it depends on busName.
|
|
48
|
+
logger;
|
|
49
|
+
subscriptions = [];
|
|
50
|
+
constructor(transport, idempotencyStore, busName, options = {}) {
|
|
51
|
+
this.transport = transport;
|
|
52
|
+
this.idempotencyStore = idempotencyStore;
|
|
53
|
+
this.busName = busName;
|
|
54
|
+
this.options = options;
|
|
55
|
+
this.logger = new Logger(busName ? `MessageBus:${busName}` : 'MessageBus');
|
|
56
|
+
}
|
|
57
|
+
async onReady() {
|
|
58
|
+
const allHandlers = this.discovery.getMethodsWithMeta(ON);
|
|
59
|
+
// Validation: every referenced bus name must point to a registered bus.
|
|
60
|
+
// Each bus instance runs this check; first onReady to fail throws.
|
|
61
|
+
for (const h of allHandlers) {
|
|
62
|
+
const refBus = h.metadata.bus ?? null;
|
|
63
|
+
const token = getMessageBusToken(refBus ?? undefined);
|
|
64
|
+
if (!this.resolver.has(token)) {
|
|
65
|
+
throw new Error(`[messaging] @On on ${h.ctor.name}.${h.methodName} references bus '${refBus ?? '<default>'}', ` +
|
|
66
|
+
`but no MessagingModule.configure(opts${refBus ? `, '${refBus}'` : ''}) is registered.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Filter to handlers targeting THIS bus.
|
|
70
|
+
const handlers = allHandlers.filter((h) => (h.metadata.bus ?? null) === this.busName);
|
|
71
|
+
const transportName = this.transport.constructor.name;
|
|
72
|
+
const dispatchDefaults = this.options.dispatch ?? null;
|
|
73
|
+
const appName = this.options.appName ?? null;
|
|
74
|
+
for (const h of handlers) {
|
|
75
|
+
const meta = h.metadata;
|
|
76
|
+
// Startup validation: @Idempotent without configured store → fail fast.
|
|
77
|
+
const idemMeta = this.getIdempotentMeta(h.ctor, h.methodName);
|
|
78
|
+
if (idemMeta && !this.idempotencyStore) {
|
|
79
|
+
throw new Error(`[messaging] @Idempotent on ${h.ctor.name}.${h.methodName} requires an IdempotencyStore. ` +
|
|
80
|
+
`Pass \`idempotency: memoryIdempotencyStore()\` (dev) or ` +
|
|
81
|
+
`\`idempotency: redisIdempotencyStore({...})\` (prod) to MessagingModule.configure().`);
|
|
82
|
+
}
|
|
83
|
+
// broadcast and explicit group are conceptually opposite intents.
|
|
84
|
+
if (meta.broadcast && meta.group) {
|
|
85
|
+
throw new Error(`[messaging] @On on ${h.ctor.name}.${h.methodName}: broadcast and group are mutually exclusive. ` +
|
|
86
|
+
`Use broadcast for cluster-wide fan-out, or group for an explicit competing-consumers pool, not both.`);
|
|
87
|
+
}
|
|
88
|
+
// @Idempotent default key is `${envelope.id}:${ctor}.${method}` - shared
|
|
89
|
+
// across replicas. With broadcast, every replica receives a copy; the
|
|
90
|
+
// first replica claims the key and the rest see a stale claim → silent
|
|
91
|
+
// skip → broadcast effectively becomes "first wins". Reject at startup
|
|
92
|
+
// unless the user provided an explicit key callback (presumably scoped
|
|
93
|
+
// per-process).
|
|
94
|
+
if (meta.broadcast && idemMeta && !idemMeta.key) {
|
|
95
|
+
throw new Error(`[messaging] @On on ${h.ctor.name}.${h.methodName} combines broadcast: true with @Idempotent ` +
|
|
96
|
+
`using the default key, but the default key is shared across replicas - the first replica to ` +
|
|
97
|
+
`claim it would silently skip the handler on every other replica, breaking broadcast semantics. ` +
|
|
98
|
+
`Either remove @Idempotent (broadcast usually targets non-idempotent local state like cache invalidation) ` +
|
|
99
|
+
`or provide an explicit \`key\` callback that scopes the claim per replica.`);
|
|
100
|
+
}
|
|
101
|
+
// Explicit broker group requires the transport to support
|
|
102
|
+
// competing-consumers semantic (broker round-robin within a group).
|
|
103
|
+
if (meta.group && !this.transport.supportsCompetingConsumers) {
|
|
104
|
+
throw new Error(`[messaging] @On on ${h.ctor.name}.${h.methodName} on bus '${this.busName ?? '<default>'}' ` +
|
|
105
|
+
`declares explicit group '${meta.group}', but transport ${transportName} does not support ` +
|
|
106
|
+
`competing consumers. Either remove the group option (use the auto-derived per-handler group) ` +
|
|
107
|
+
`or use a transport with broker-side consumer groups (e.g. RedisStreamsTransport).`);
|
|
108
|
+
}
|
|
109
|
+
const effectiveMode = meta.mode ?? dispatchDefaults?.mode ?? this.transport.defaultMode;
|
|
110
|
+
const effectiveConcurrency = meta.concurrency ?? dispatchDefaults?.concurrency ?? 1;
|
|
111
|
+
if (!this.transport.supportedModes.includes(effectiveMode)) {
|
|
112
|
+
const supported = this.transport.supportedModes.map((m) => `'${m}'`).join(', ');
|
|
113
|
+
throw new Error(`[messaging] @On on ${h.ctor.name}.${h.methodName} on bus '${this.busName ?? '<default>'}' ` +
|
|
114
|
+
`for topic '${meta.topic}' requests dispatch mode '${effectiveMode}' which is not supported by ` +
|
|
115
|
+
`${transportName}. Supported modes: [${supported}].`);
|
|
116
|
+
}
|
|
117
|
+
if (effectiveConcurrency <= 0 || !Number.isFinite(effectiveConcurrency)) {
|
|
118
|
+
throw new Error(`[messaging] @On on ${h.ctor.name}.${h.methodName} for topic '${meta.topic}' resolves to ` +
|
|
119
|
+
`concurrency=${effectiveConcurrency}, which is invalid. Concurrency must be a positive integer ` +
|
|
120
|
+
`(minimum 1).`);
|
|
121
|
+
}
|
|
122
|
+
const group = deriveGroupName({
|
|
123
|
+
topic: meta.topic,
|
|
124
|
+
ctorName: h.ctor.name,
|
|
125
|
+
methodName: h.methodName,
|
|
126
|
+
appName,
|
|
127
|
+
explicitGroup: meta.group,
|
|
128
|
+
broadcast: meta.broadcast,
|
|
129
|
+
});
|
|
130
|
+
const sub = await this.transport.subscribe(meta.topic, async (envelope) => this.dispatch(h, envelope), {
|
|
131
|
+
group,
|
|
132
|
+
concurrency: effectiveConcurrency,
|
|
133
|
+
mode: effectiveMode,
|
|
134
|
+
broadcast: meta.broadcast,
|
|
135
|
+
});
|
|
136
|
+
this.subscriptions.push(sub);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
getIdempotentMeta(ctor, methodName) {
|
|
140
|
+
const map = getMeta(ctor, IDEMPOTENT);
|
|
141
|
+
return map?.get(methodName);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Publish a message. Meta fields are optional and additive - the framework
|
|
145
|
+
* fills in `id`, `timestamp`, and `attempt`. Composes onto
|
|
146
|
+
* `MessageTransport.publish(envelope)` internally.
|
|
147
|
+
*
|
|
148
|
+
* For distributed tracing, populate `traceparent` (and optionally `tracestate`)
|
|
149
|
+
* from your tracing library so consumers can continue the trace.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* import { trace } from '@opentelemetry/api'
|
|
154
|
+
*
|
|
155
|
+
* const span = trace.getActiveSpan()
|
|
156
|
+
* const ctx = span?.spanContext()
|
|
157
|
+
* await bus.publish('order.placed', order, {
|
|
158
|
+
* traceparent: ctx
|
|
159
|
+
* ? `00-${ctx.traceId}-${ctx.spanId}-${ctx.traceFlags.toString(16).padStart(2, '0')}`
|
|
160
|
+
* : undefined,
|
|
161
|
+
* })
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
async publish(topic, payload, meta) {
|
|
165
|
+
const envelope = {
|
|
166
|
+
id: randomUUID(),
|
|
167
|
+
topic,
|
|
168
|
+
payload,
|
|
169
|
+
meta: {
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
attempt: 1,
|
|
172
|
+
...meta,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
await this.transport.publish(envelope);
|
|
176
|
+
}
|
|
177
|
+
async onDestroy() {
|
|
178
|
+
for (const sub of this.subscriptions) {
|
|
179
|
+
await sub.unsubscribe();
|
|
180
|
+
}
|
|
181
|
+
this.subscriptions = [];
|
|
182
|
+
}
|
|
183
|
+
async dispatch(h, envelope) {
|
|
184
|
+
const callHandler = () => h.instance[h.methodName](envelope.payload, envelope.meta);
|
|
185
|
+
const idemMeta = this.getIdempotentMeta(h.ctor, h.methodName);
|
|
186
|
+
if (!idemMeta) {
|
|
187
|
+
try {
|
|
188
|
+
await callHandler();
|
|
189
|
+
return { status: 'ack' };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
this.logger.error(`Handler ${h.ctor.name}.${h.methodName} failed for ${envelope.topic} (attempt ${envelope.meta.attempt})`, err instanceof Error ? (err.stack ?? err.message) : String(err));
|
|
193
|
+
return { status: 'nack', error: err instanceof Error ? err : new Error(String(err)) };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// @Idempotent path: claim → run → release on failure.
|
|
197
|
+
// Per-handler default key avoids cross-handler conflicts; user can override
|
|
198
|
+
// via `key` to widen scope (e.g. share across handlers).
|
|
199
|
+
const id = idemMeta.key?.(envelope.payload, envelope.meta) ?? `${envelope.id}:${h.ctor.name}.${h.methodName}`;
|
|
200
|
+
const claimed = await this.idempotencyStore.claim(id, idemMeta.ttl);
|
|
201
|
+
if (!claimed)
|
|
202
|
+
return { status: 'ack' }; // already processed - silently skip
|
|
203
|
+
try {
|
|
204
|
+
await callHandler();
|
|
205
|
+
return { status: 'ack' };
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
// Release so a retry can re-claim and re-process. Failure to release
|
|
209
|
+
// is logged but does not mask the original handler error.
|
|
210
|
+
await this.idempotencyStore.release(id).catch((releaseErr) => {
|
|
211
|
+
this.logger.error(`Failed to release idempotency claim ${id}`, String(releaseErr));
|
|
212
|
+
});
|
|
213
|
+
this.logger.error(`Handler ${h.ctor.name}.${h.methodName} failed for ${envelope.topic} (attempt ${envelope.meta.attempt})`, err instanceof Error ? (err.stack ?? err.message) : String(err));
|
|
214
|
+
return { status: 'nack', error: err instanceof Error ? err : new Error(String(err)) };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=message-bus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-bus.js","sourceRoot":"","sources":["../src/message-bus.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ConfiguredModule, OptionsOrFactory } from '@miiajs/core';
|
|
2
|
+
import { type IdempotencyStore } from './idempotency.js';
|
|
3
|
+
import { type DispatchMode, type MessageTransport } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Bus-level defaults for dispatch behavior. Per-handler `@On({ mode, concurrency })`
|
|
6
|
+
* overrides these; if neither this nor the handler declares a value, the
|
|
7
|
+
* transport's `defaultMode` and a concurrency of `1` apply.
|
|
8
|
+
*/
|
|
9
|
+
export interface DispatchDefaults {
|
|
10
|
+
mode?: DispatchMode;
|
|
11
|
+
concurrency?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface MessagingModuleOptions {
|
|
14
|
+
transport: MessageTransport;
|
|
15
|
+
/**
|
|
16
|
+
* Optional idempotency store for `@Idempotent` handlers. MessageBus throws
|
|
17
|
+
* at startup if any handler is annotated with `@Idempotent` but no store
|
|
18
|
+
* is configured here.
|
|
19
|
+
*/
|
|
20
|
+
idempotency?: IdempotencyStore;
|
|
21
|
+
/**
|
|
22
|
+
* Bus-wide dispatch defaults. Resolution chain at `MessageBus.onReady()`:
|
|
23
|
+
*
|
|
24
|
+
* `@On({ mode, concurrency })` > this `dispatch` > `transport.defaultMode` (and 1).
|
|
25
|
+
*/
|
|
26
|
+
dispatch?: DispatchDefaults;
|
|
27
|
+
/**
|
|
28
|
+
* Application namespace for auto-derived consumer group names. When set,
|
|
29
|
+
* groups become `${appName}:${topic}__${ClassName}_${methodName}`. Use to
|
|
30
|
+
* avoid collisions when multiple services share a broker and have handlers
|
|
31
|
+
* with overlapping class names - without `appName`, their auto-derived
|
|
32
|
+
* groups collide and the broker round-robins messages between unrelated
|
|
33
|
+
* services.
|
|
34
|
+
*
|
|
35
|
+
* Does NOT prefix:
|
|
36
|
+
* - topics (those are shared cross-service contract)
|
|
37
|
+
* - explicit `@On({ group: '...' })` (user-controlled, full-qualified)
|
|
38
|
+
*
|
|
39
|
+
* Recommended for any production deployment with shared infrastructure.
|
|
40
|
+
*/
|
|
41
|
+
appName?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Dynamic module that registers a message transport, optional idempotency
|
|
45
|
+
* store, and `MessageBus` in the container.
|
|
46
|
+
*
|
|
47
|
+
* Pass an optional `name` to register a **named bus** alongside the default
|
|
48
|
+
* one - useful when an app needs more than one transport (Kafka + Redis,
|
|
49
|
+
* internal in-memory bus + external Redis, etc.). Handlers target a specific
|
|
50
|
+
* bus via `@On(topic, { bus: name })`.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* // Static form, default bus
|
|
55
|
+
* MessagingModule.configure({ transport: inMemoryTransport() })
|
|
56
|
+
*
|
|
57
|
+
* // Factory form with DI access
|
|
58
|
+
* MessagingModule.configure((resolve) => ({
|
|
59
|
+
* transport: redisStreamsTransport({
|
|
60
|
+
* url: resolve(ConfigService).getOrThrow('REDIS_URL'),
|
|
61
|
+
* }),
|
|
62
|
+
* }))
|
|
63
|
+
*
|
|
64
|
+
* // Multi-bus: default + named
|
|
65
|
+
* MessagingModule.configure({ transport: redisStreamsTransport({ url: '...' }) })
|
|
66
|
+
* MessagingModule.configure({ transport: kafkaTransport({ ... }) }, 'kafka')
|
|
67
|
+
*
|
|
68
|
+
* // In a handler
|
|
69
|
+
* @On('order.placed') // default bus
|
|
70
|
+
* @On('legacy.user', { bus: 'kafka' }) // named bus
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export declare class MessagingModule {
|
|
74
|
+
static configure(optionsOrFactory: OptionsOrFactory<MessagingModuleOptions>, name?: string): ConfiguredModule;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=messaging.module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messaging.module.d.ts","sourceRoot":"","sources":["../src/messaging.module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAe,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAGnF,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAErE;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,gBAAgB,CAAA;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAC3B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,eAAe;IAC1B,MAAM,CAAC,SAAS,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,sBAAsB,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,gBAAgB;CAqC9G"}
|