@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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/decorators.d.ts +149 -0
  4. package/dist/decorators.d.ts.map +1 -0
  5. package/dist/decorators.js +112 -0
  6. package/dist/decorators.js.map +1 -0
  7. package/dist/group-name.d.ts +33 -0
  8. package/dist/group-name.d.ts.map +1 -0
  9. package/dist/group-name.js +25 -0
  10. package/dist/group-name.js.map +1 -0
  11. package/dist/idempotency.d.ts +66 -0
  12. package/dist/idempotency.d.ts.map +1 -0
  13. package/dist/idempotency.js +42 -0
  14. package/dist/idempotency.js.map +1 -0
  15. package/dist/in-memory-transport.d.ts +60 -0
  16. package/dist/in-memory-transport.d.ts.map +1 -0
  17. package/dist/in-memory-transport.js +143 -0
  18. package/dist/in-memory-transport.js.map +1 -0
  19. package/dist/index.d.ts +16 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +11 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/message-bus.d.ts +83 -0
  24. package/dist/message-bus.d.ts.map +1 -0
  25. package/dist/message-bus.js +218 -0
  26. package/dist/message-bus.js.map +1 -0
  27. package/dist/messaging.module.d.ts +76 -0
  28. package/dist/messaging.module.d.ts.map +1 -0
  29. package/dist/messaging.module.js +74 -0
  30. package/dist/messaging.module.js.map +1 -0
  31. package/dist/retry.d.ts +7 -0
  32. package/dist/retry.d.ts.map +1 -0
  33. package/dist/retry.js +10 -0
  34. package/dist/retry.js.map +1 -0
  35. package/dist/tokens.d.ts +19 -0
  36. package/dist/tokens.d.ts.map +1 -0
  37. package/dist/tokens.js +26 -0
  38. package/dist/tokens.js.map +1 -0
  39. package/dist/types.d.ts +165 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +15 -0
  42. package/dist/types.js.map +1 -0
  43. package/package.json +55 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruslan Matiushev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @miiajs/messaging
2
+
3
+ Decorator-driven message bus for MiiaJS - retry with auto-DLQ, idempotency, named buses, W3C tracing.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @miiajs/messaging
9
+ ```
10
+
11
+ ## Documentation
12
+
13
+ **[miiajs.com/docs/packages/messaging](https://miiajs.com/docs/packages/messaging)**
14
+
15
+ ## License
16
+
17
+ MIT
@@ -0,0 +1,149 @@
1
+ import { type DiscoverableMethodMeta } from '@miiajs/core';
2
+ import type { DispatchMode, MessageMeta } from './types.js';
3
+ export declare const ON: unique symbol;
4
+ export declare const IDEMPOTENT: unique symbol;
5
+ export interface OnMeta extends DiscoverableMethodMeta {
6
+ topic: string;
7
+ /**
8
+ * Explicit broker consumer group (competing-consumers worker pool). When
9
+ * omitted, the bus auto-derives a per-handler group `<topic>__<Class>_<method>`
10
+ * (optionally `<appName>:` prefixed). Setting this option puts multiple
11
+ * handlers (across classes or replicas) into the same broker group so the
12
+ * broker round-robins each message to one of them. Requires
13
+ * `transport.supportsCompetingConsumers === true`.
14
+ */
15
+ group?: string;
16
+ concurrency?: number;
17
+ /**
18
+ * Target bus name when multiple `MessagingModule.configure(opts, name)` are
19
+ * registered. Omit (or `undefined`) to target the default bus. Throws at
20
+ * startup if the referenced bus name has no matching `MessagingModule`.
21
+ */
22
+ bus?: string;
23
+ /**
24
+ * Per-handler override of the bus / transport dispatch mode. Resolution
25
+ * order: this field > `MessagingModule.configure({ dispatch: { mode } })` >
26
+ * `transport.defaultMode`. Validated against `transport.supportedModes` at
27
+ * `MessageBus.onReady()`.
28
+ */
29
+ mode?: DispatchMode;
30
+ /**
31
+ * Cluster-wide fan-out for this handler. When `true`, the auto-derived group
32
+ * is suffixed with `__<hostname>_<pid>` so every replica gets a unique broker
33
+ * group and the broker delivers a copy of each message to every replica.
34
+ * Mutually exclusive with `group` - explicit `group` is for shared work
35
+ * across handlers/replicas, broadcast replicates work to every replica.
36
+ *
37
+ * Use for in-process state that every replica must update on its own
38
+ * (cache invalidation, websocket broadcast).
39
+ */
40
+ broadcast?: boolean;
41
+ }
42
+ export interface IdempotentMeta {
43
+ /** Claim lifetime in milliseconds. */
44
+ ttl: number;
45
+ key?: (payload: unknown, meta: MessageMeta) => string;
46
+ }
47
+ /**
48
+ * Marks a method as a message handler for `topic`. At app startup, MessageBus
49
+ * discovers all `@On` methods via DiscoveryService (during `onReady`) and
50
+ * subscribes each one to the configured transport.
51
+ *
52
+ * @param topic Free-form string. Transports do not interpret dots/slashes.
53
+ * @param options.group Explicit broker consumer group (competing-consumers
54
+ * worker pool). Without it, an auto-derived
55
+ * per-handler group is used.
56
+ * @param options.concurrency Per-handler subscription concurrency. In sliding
57
+ * mode = number of lanes.
58
+ * @param options.mode Per-handler dispatch mode. Resolution: this
59
+ * field > bus default > `transport.defaultMode`.
60
+ * @param options.bus Target bus for multi-bus setups.
61
+ * @param options.broadcast Cluster-wide fan-out (every replica gets a copy).
62
+ * Mutually exclusive with `group`.
63
+ *
64
+ * **Subscription model.** Each `@On` becomes its own broker subscription with
65
+ * an auto-derived consumer group `<topic>__<ClassName>_<methodName>` (or
66
+ * `<appName>:<topic>__<ClassName>_<methodName>` if `MessagingModule.configure`
67
+ * provides `appName`). Within one process every handler runs independently:
68
+ * retry, ack/nack, mode/concurrency are isolated per handler.
69
+ *
70
+ * For replicas of the same handler running in N processes, broker round-robins
71
+ * each message to exactly one of the N consumers (load balance scaling).
72
+ *
73
+ * For cluster-wide fan-out (each replica processes its own copy), set
74
+ * `broadcast: true`. For competing-consumers worker pool across multiple
75
+ * handler classes, pass explicit `group: 'pool-name'` (requires
76
+ * `transport.supportsCompetingConsumers === true`).
77
+ *
78
+ * **Multi-topic handlers.** Decorating one method with multiple `@On` works
79
+ * naturally - each decoration creates its own subscription with its own
80
+ * auto-derived group. Useful when a single handler responds to several
81
+ * topics:
82
+ * ```ts
83
+ * @On('user.created')
84
+ * @On('user.updated')
85
+ * async syncToCRM(user: User) { ... }
86
+ * // → 2 subscriptions: user.created__SyncService_syncToCRM, user.updated__...
87
+ * ```
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * @On('user.created') // 1 worker, fan-out across handlers
92
+ * @On('cache.invalidate', { broadcast: true }) // copy to every replica
93
+ * @On('jobs', { group: 'workers' }) // explicit competing-consumers pool
94
+ * ```
95
+ */
96
+ export declare const On: (topic: string, options?: {
97
+ group?: string;
98
+ concurrency?: number;
99
+ bus?: string;
100
+ mode?: DispatchMode;
101
+ broadcast?: boolean;
102
+ } | undefined) => (target: Function, context: ClassMethodDecoratorContext) => void;
103
+ /**
104
+ * Skip the handler when the same logical message has already been processed.
105
+ *
106
+ * When applied to an `@On` handler, MessageBus claims an idempotency key in the
107
+ * configured `IdempotencyStore` before invoking the handler. If the claim
108
+ * already exists (duplicate delivery from XAUTOCLAIM, network blip, etc.),
109
+ * the handler is silently skipped and the message is acked.
110
+ *
111
+ * Default key is `${envelope.id}:${ClassName}.${methodName}` - per-handler
112
+ * scope, so two `@Idempotent` handlers on the same topic do NOT conflict
113
+ * by default. Pass an explicit `key` to override (for example, dedupe on a
114
+ * business identifier from payload, or share a single key across handlers).
115
+ *
116
+ * **NOT exactly-once.** If the consumer crashes after `claim()` but before
117
+ * the message is acked back to the broker, the claim stays in the store
118
+ * while the broker still considers the message un-acked. After redelivery
119
+ * the next consumer sees a stale claim and skips the handler, effectively
120
+ * losing the message. For business-critical workflows pair `@Idempotent`
121
+ * with a transactional outbox or use idempotent-by-design handlers
122
+ * (`UPDATE WHERE id=?`) instead.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * @Injectable()
127
+ * class PaymentService {
128
+ * @On('order.placed')
129
+ * @Idempotent({ ttl: 24 * 60 * 60 * 1000 }) // dedupe within 24h
130
+ * async chargeCard(order: Order) {
131
+ * await this.payments.charge(order.cardId, order.total)
132
+ * }
133
+ *
134
+ * // Custom key when the upstream may republish the same business event
135
+ * // with different envelope.id values:
136
+ * @On('payment.received')
137
+ * @Idempotent({ ttl: 24 * 60 * 60 * 1000, key: (p: Payment) => `payment:${p.transactionId}` })
138
+ * async onPayment(payment: Payment) { ... }
139
+ * }
140
+ * ```
141
+ *
142
+ * @throws at app startup if any `@Idempotent` handler exists but no
143
+ * `IdempotencyStore` was passed to `MessagingModule.configure()`.
144
+ */
145
+ export declare const Idempotent: (options: {
146
+ ttl: number;
147
+ key?: (payload: unknown, meta: MessageMeta) => string;
148
+ }) => (target: Function, context: ClassMethodDecoratorContext) => void;
149
+ //# sourceMappingURL=decorators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,KAAK,sBAAsB,EAAE,MAAM,cAAc,CAAA;AACzG,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE3D,eAAO,MAAM,EAAE,eAA8B,CAAA;AAE7C,eAAO,MAAM,UAAU,eAAsC,CAAA;AAE7D,MAAM,WAAW,MAAO,SAAQ,sBAAsB;IACpD,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,KAAK,MAAM,CAAA;CACtD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,EAAE;YAGS,MAAM;kBAAgB,MAAM;UAAQ,MAAM;WAAS,YAAY;gBAAc,OAAO;kFAY1G,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,UAAU;SACJ,MAAM;UAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,KAAK,MAAM;sEAM9E,CAAA"}
@@ -0,0 +1,112 @@
1
+ import { createMethodDecorator, pushMeta, setInMapMeta } from '@miiajs/core';
2
+ export const ON = Symbol('miia:messaging:on');
3
+ export const IDEMPOTENT = Symbol('miia:messaging:idempotent');
4
+ /**
5
+ * Marks a method as a message handler for `topic`. At app startup, MessageBus
6
+ * discovers all `@On` methods via DiscoveryService (during `onReady`) and
7
+ * subscribes each one to the configured transport.
8
+ *
9
+ * @param topic Free-form string. Transports do not interpret dots/slashes.
10
+ * @param options.group Explicit broker consumer group (competing-consumers
11
+ * worker pool). Without it, an auto-derived
12
+ * per-handler group is used.
13
+ * @param options.concurrency Per-handler subscription concurrency. In sliding
14
+ * mode = number of lanes.
15
+ * @param options.mode Per-handler dispatch mode. Resolution: this
16
+ * field > bus default > `transport.defaultMode`.
17
+ * @param options.bus Target bus for multi-bus setups.
18
+ * @param options.broadcast Cluster-wide fan-out (every replica gets a copy).
19
+ * Mutually exclusive with `group`.
20
+ *
21
+ * **Subscription model.** Each `@On` becomes its own broker subscription with
22
+ * an auto-derived consumer group `<topic>__<ClassName>_<methodName>` (or
23
+ * `<appName>:<topic>__<ClassName>_<methodName>` if `MessagingModule.configure`
24
+ * provides `appName`). Within one process every handler runs independently:
25
+ * retry, ack/nack, mode/concurrency are isolated per handler.
26
+ *
27
+ * For replicas of the same handler running in N processes, broker round-robins
28
+ * each message to exactly one of the N consumers (load balance scaling).
29
+ *
30
+ * For cluster-wide fan-out (each replica processes its own copy), set
31
+ * `broadcast: true`. For competing-consumers worker pool across multiple
32
+ * handler classes, pass explicit `group: 'pool-name'` (requires
33
+ * `transport.supportsCompetingConsumers === true`).
34
+ *
35
+ * **Multi-topic handlers.** Decorating one method with multiple `@On` works
36
+ * naturally - each decoration creates its own subscription with its own
37
+ * auto-derived group. Useful when a single handler responds to several
38
+ * topics:
39
+ * ```ts
40
+ * @On('user.created')
41
+ * @On('user.updated')
42
+ * async syncToCRM(user: User) { ... }
43
+ * // → 2 subscriptions: user.created__SyncService_syncToCRM, user.updated__...
44
+ * ```
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * @On('user.created') // 1 worker, fan-out across handlers
49
+ * @On('cache.invalidate', { broadcast: true }) // copy to every replica
50
+ * @On('jobs', { group: 'workers' }) // explicit competing-consumers pool
51
+ * ```
52
+ */
53
+ export const On = createMethodDecorator((_target, ctx, topic, options) => {
54
+ pushMeta(ctx.metadata, ON, {
55
+ handlerName: ctx.name,
56
+ topic,
57
+ group: options?.group,
58
+ concurrency: options?.concurrency,
59
+ bus: options?.bus,
60
+ mode: options?.mode,
61
+ broadcast: options?.broadcast,
62
+ });
63
+ });
64
+ /**
65
+ * Skip the handler when the same logical message has already been processed.
66
+ *
67
+ * When applied to an `@On` handler, MessageBus claims an idempotency key in the
68
+ * configured `IdempotencyStore` before invoking the handler. If the claim
69
+ * already exists (duplicate delivery from XAUTOCLAIM, network blip, etc.),
70
+ * the handler is silently skipped and the message is acked.
71
+ *
72
+ * Default key is `${envelope.id}:${ClassName}.${methodName}` - per-handler
73
+ * scope, so two `@Idempotent` handlers on the same topic do NOT conflict
74
+ * by default. Pass an explicit `key` to override (for example, dedupe on a
75
+ * business identifier from payload, or share a single key across handlers).
76
+ *
77
+ * **NOT exactly-once.** If the consumer crashes after `claim()` but before
78
+ * the message is acked back to the broker, the claim stays in the store
79
+ * while the broker still considers the message un-acked. After redelivery
80
+ * the next consumer sees a stale claim and skips the handler, effectively
81
+ * losing the message. For business-critical workflows pair `@Idempotent`
82
+ * with a transactional outbox or use idempotent-by-design handlers
83
+ * (`UPDATE WHERE id=?`) instead.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * @Injectable()
88
+ * class PaymentService {
89
+ * @On('order.placed')
90
+ * @Idempotent({ ttl: 24 * 60 * 60 * 1000 }) // dedupe within 24h
91
+ * async chargeCard(order: Order) {
92
+ * await this.payments.charge(order.cardId, order.total)
93
+ * }
94
+ *
95
+ * // Custom key when the upstream may republish the same business event
96
+ * // with different envelope.id values:
97
+ * @On('payment.received')
98
+ * @Idempotent({ ttl: 24 * 60 * 60 * 1000, key: (p: Payment) => `payment:${p.transactionId}` })
99
+ * async onPayment(payment: Payment) { ... }
100
+ * }
101
+ * ```
102
+ *
103
+ * @throws at app startup if any `@Idempotent` handler exists but no
104
+ * `IdempotencyStore` was passed to `MessagingModule.configure()`.
105
+ */
106
+ export const Idempotent = createMethodDecorator((_target, ctx, options) => {
107
+ setInMapMeta(ctx.metadata, IDEMPOTENT, String(ctx.name), {
108
+ ttl: options.ttl,
109
+ key: options.key,
110
+ });
111
+ });
112
+ //# sourceMappingURL=decorators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators.js","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,YAAY,EAA+B,MAAM,cAAc,CAAA;AAGzG,MAAM,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAA;AAE7C,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,2BAA2B,CAAC,CAAA;AA8C7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,CAAC,MAAM,EAAE,GAAG,qBAAqB,CAKrC,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;IACjC,QAAQ,CAAC,GAAG,CAAC,QAAS,EAAE,EAAE,EAAE;QAC1B,WAAW,EAAE,GAAG,CAAC,IAAc;QAC/B,KAAK;QACL,KAAK,EAAE,OAAO,EAAE,KAAK;QACrB,WAAW,EAAE,OAAO,EAAE,WAAW;QACjC,GAAG,EAAE,OAAO,EAAE,GAAG;QACjB,IAAI,EAAE,OAAO,EAAE,IAAI;QACnB,SAAS,EAAE,OAAO,EAAE,SAAS;KACb,CAAC,CAAA;AACrB,CAAC,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,qBAAqB,CAE7C,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE;IAC1B,YAAY,CAAC,GAAG,CAAC,QAAS,EAAE,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QACxD,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,GAAG,EAAE,OAAO,CAAC,GAAG;KACQ,CAAC,CAAA;AAC7B,CAAC,CAAC,CAAA"}
@@ -0,0 +1,33 @@
1
+ export interface DeriveGroupNameInput {
2
+ topic: string;
3
+ ctorName: string;
4
+ methodName: string;
5
+ appName: string | null;
6
+ /**
7
+ * Explicit user-provided group from `@On({ group: '...' })`. When set,
8
+ * returned as-is - explicit groups are full-qualified by the user and do
9
+ * NOT receive `appName` prefix.
10
+ */
11
+ explicitGroup?: string;
12
+ /**
13
+ * Cluster-wide fan-out opt-in. When `true`, the group is suffixed with
14
+ * `__<hostname>_<pid>` so every replica gets a unique broker group and
15
+ * the broker delivers a copy of each message to every replica. Mutually
16
+ * exclusive with `explicitGroup`.
17
+ */
18
+ broadcast?: boolean;
19
+ }
20
+ /**
21
+ * Build the broker consumer group name for an `@On` handler.
22
+ *
23
+ * Resolution order:
24
+ * - `explicitGroup` provided → returned as-is (no `appName` prefix, no broadcast suffix)
25
+ * - `broadcast: true` → `${appName ? appName + ':' : ''}${topic}__${ctor}_${method}__${hostname}_${pid}`
26
+ * - default → `${appName ? appName + ':' : ''}${topic}__${ctor}_${method}`
27
+ *
28
+ * The broadcast suffix uses only `hostname` + `pid` (no random component) so that
29
+ * orphan-cleanup logic in transports can reliably match previous-incarnation
30
+ * groups by stable host pattern when the process restarts.
31
+ */
32
+ export declare function deriveGroupName(input: DeriveGroupNameInput): string;
33
+ //# sourceMappingURL=group-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-name.d.ts","sourceRoot":"","sources":["../src/group-name.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAWnE"}
@@ -0,0 +1,25 @@
1
+ import { hostname } from 'node:os';
2
+ /**
3
+ * Build the broker consumer group name for an `@On` handler.
4
+ *
5
+ * Resolution order:
6
+ * - `explicitGroup` provided → returned as-is (no `appName` prefix, no broadcast suffix)
7
+ * - `broadcast: true` → `${appName ? appName + ':' : ''}${topic}__${ctor}_${method}__${hostname}_${pid}`
8
+ * - default → `${appName ? appName + ':' : ''}${topic}__${ctor}_${method}`
9
+ *
10
+ * The broadcast suffix uses only `hostname` + `pid` (no random component) so that
11
+ * orphan-cleanup logic in transports can reliably match previous-incarnation
12
+ * groups by stable host pattern when the process restarts.
13
+ */
14
+ export function deriveGroupName(input) {
15
+ if (input.explicitGroup)
16
+ return input.explicitGroup;
17
+ const base = input.appName
18
+ ? `${input.appName}:${input.topic}__${input.ctorName}_${input.methodName}`
19
+ : `${input.topic}__${input.ctorName}_${input.methodName}`;
20
+ if (input.broadcast) {
21
+ return `${base}__${hostname()}_${process.pid}`;
22
+ }
23
+ return base;
24
+ }
25
+ //# sourceMappingURL=group-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-name.js","sourceRoot":"","sources":["../src/group-name.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAsBlC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,KAA2B;IACzD,IAAI,KAAK,CAAC,aAAa;QAAE,OAAO,KAAK,CAAC,aAAa,CAAA;IAEnD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO;QACxB,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,UAAU,EAAE;QAC1E,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,UAAU,EAAE,CAAA;IAE3D,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QACpB,OAAO,GAAG,IAAI,KAAK,QAAQ,EAAE,IAAI,OAAO,CAAC,GAAG,EAAE,CAAA;IAChD,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pluggable idempotency primitive used by `@Idempotent` handlers in MessageBus.
3
+ *
4
+ * `claim(id, ttl)` is the atomic "first time?" check; `release(id)` lets the
5
+ * next retry attempt re-claim after a handler error. The store is transport-
6
+ * agnostic - a Redis Streams transport may pair with a Postgres-backed store,
7
+ * a NATS transport may use the broker's native dedup window plus a memory
8
+ * store for cross-restart safety.
9
+ *
10
+ * Implementations shipped:
11
+ * - `memoryIdempotencyStore()` - in-process Map with LRU eviction; default
12
+ * for dev/tests; not safe across processes.
13
+ * - `redisIdempotencyStore()` from `@miiajs/messaging-redis` - production-ready,
14
+ * atomic via SET NX EX.
15
+ *
16
+ * Custom backends (Postgres `INSERT ON CONFLICT`, DynamoDB conditional put)
17
+ * implement this interface directly.
18
+ */
19
+ export interface IdempotencyStore {
20
+ /**
21
+ * Try to claim an ID for the given TTL (in milliseconds).
22
+ *
23
+ * - returns `true` → first claim, the framework proceeds with the handler
24
+ * - returns `false` → already claimed, the handler is silently skipped
25
+ * (treated as "already processed")
26
+ *
27
+ * Must be atomic: concurrent calls with the same id must produce exactly
28
+ * one `true` and the rest `false`.
29
+ */
30
+ claim(id: string, ttlMs: number): Promise<boolean>;
31
+ /**
32
+ * Release a previously-claimed id so a future delivery can re-claim.
33
+ * Called by MessageBus when the handler errors. Idempotent (no-op if id
34
+ * is not present).
35
+ */
36
+ release(id: string): Promise<void>;
37
+ onDestroy?(): Promise<void>;
38
+ }
39
+ /**
40
+ * DI token for the optional idempotency store.
41
+ *
42
+ * Always registered by `MessagingModule.configure()` (value is `null` when
43
+ * the user did not configure one). MessageBus reads it via `injectOptional`
44
+ * and only uses the store when a handler has `@Idempotent`.
45
+ */
46
+ export declare const IDEMPOTENCY_STORE = "miia:messaging:idempotency-store";
47
+ export interface MemoryIdempotencyStoreOptions {
48
+ /**
49
+ * Max entries kept before LRU eviction. Default 10000.
50
+ *
51
+ * If an entry is evicted before its TTL, a duplicate may be re-processed.
52
+ * Acceptable trade-off for in-process stores; production deployments
53
+ * should use a Redis-backed store with bounded memory pressure.
54
+ */
55
+ maxSize?: number;
56
+ }
57
+ export declare class MemoryIdempotencyStore implements IdempotencyStore {
58
+ private entries;
59
+ private maxSize;
60
+ constructor(options?: MemoryIdempotencyStoreOptions);
61
+ claim(id: string, ttlMs: number): Promise<boolean>;
62
+ release(id: string): Promise<void>;
63
+ onDestroy(): Promise<void>;
64
+ }
65
+ export declare function memoryIdempotencyStore(options?: MemoryIdempotencyStoreOptions): IdempotencyStore;
66
+ //# sourceMappingURL=idempotency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAElD;;;;OAIG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAElC,SAAS,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,qCAAqC,CAAA;AAInE,MAAM,WAAW,6BAA6B;IAC5C;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAID,qBAAa,sBAAuB,YAAW,gBAAgB;IAC7D,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,OAAO,CAAQ;gBAEX,OAAO,GAAE,6BAAkC;IAIjD,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBlD,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAGjC;AAED,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,6BAA6B,GAAG,gBAAgB,CAEhG"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * DI token for the optional idempotency store.
3
+ *
4
+ * Always registered by `MessagingModule.configure()` (value is `null` when
5
+ * the user did not configure one). MessageBus reads it via `injectOptional`
6
+ * and only uses the store when a handler has `@Idempotent`.
7
+ */
8
+ export const IDEMPOTENCY_STORE = 'miia:messaging:idempotency-store';
9
+ const DEFAULT_MAX_SIZE = 10_000;
10
+ export class MemoryIdempotencyStore {
11
+ entries = new Map(); // id → expiresAtMs
12
+ maxSize;
13
+ constructor(options = {}) {
14
+ this.maxSize = options.maxSize ?? DEFAULT_MAX_SIZE;
15
+ }
16
+ async claim(id, ttlMs) {
17
+ const now = Date.now();
18
+ const existing = this.entries.get(id);
19
+ if (existing !== undefined && existing > now)
20
+ return false;
21
+ // Stale entry treated as absent. Delete-then-set keeps Map insertion
22
+ // order accurate for LRU eviction below.
23
+ this.entries.delete(id);
24
+ this.entries.set(id, now + ttlMs);
25
+ if (this.entries.size > this.maxSize) {
26
+ const oldest = this.entries.keys().next().value;
27
+ if (oldest !== undefined)
28
+ this.entries.delete(oldest);
29
+ }
30
+ return true;
31
+ }
32
+ async release(id) {
33
+ this.entries.delete(id);
34
+ }
35
+ async onDestroy() {
36
+ this.entries.clear();
37
+ }
38
+ }
39
+ export function memoryIdempotencyStore(options) {
40
+ return new MemoryIdempotencyStore(options);
41
+ }
42
+ //# sourceMappingURL=idempotency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAyCA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,kCAAkC,CAAA;AAenE,MAAM,gBAAgB,GAAG,MAAM,CAAA;AAE/B,MAAM,OAAO,sBAAsB;IACzB,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAA,CAAC,mBAAmB;IACvD,OAAO,CAAQ;IAEvB,YAAY,UAAyC,EAAE;QACrD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAA;IACpD,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,EAAU,EAAE,KAAa;QACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACrC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,GAAG,GAAG;YAAE,OAAO,KAAK,CAAA;QAE1D,qEAAqE;QACrE,yCAAyC;QACzC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACvB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,GAAG,KAAK,CAAC,CAAA;QAEjC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;YACrC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAA;YAC/C,IAAI,MAAM,KAAK,SAAS;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACvD,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,EAAU;QACtB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,SAAS;QACb,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;IACtB,CAAC;CACF;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAuC;IAC5E,OAAO,IAAI,sBAAsB,CAAC,OAAO,CAAC,CAAA;AAC5C,CAAC"}
@@ -0,0 +1,60 @@
1
+ import { type DispatchMode, type MessageEnvelope, type MessageTransport, type HandlerResult, type RetryConfig, type SubscribeOptions, type Subscription } from './types.js';
2
+ export interface InMemoryTransportOptions {
3
+ retry?: Partial<RetryConfig>;
4
+ /**
5
+ * When true, payload is `structuredClone`d before each handler sees it.
6
+ * Prevents cross-handler mutation at the cost of one clone per delivery.
7
+ * Default false (same object identity across handlers - consistent with
8
+ * Node EventEmitter semantics).
9
+ */
10
+ cloneOnPublish?: boolean;
11
+ /**
12
+ * Max time `onDestroy()` waits for in-flight handlers to settle before
13
+ * forcing cleanup. Default 5000ms. Set 0 to skip drain (immediate cleanup).
14
+ *
15
+ * The drain phase blocks new deliveries (including scheduled retries) and
16
+ * awaits all currently-running handlers. Reaching the timeout logs a warn
17
+ * and continues with cleanup; pending handlers will keep running but their
18
+ * results are discarded.
19
+ */
20
+ drainTimeoutMs?: number;
21
+ }
22
+ /**
23
+ * In-process message transport. Fire-and-forget delivery via `queueMicrotask`,
24
+ * exponential backoff retry via `setTimeout`, auto-DLQ via re-publishing to
25
+ * `<topic>.dlq`.
26
+ *
27
+ * NOT persistent. Process crash mid-retry loses pending messages - do not use
28
+ * for durability-sensitive workloads. Use `@miiajs/messaging-redis` or another
29
+ * broker-backed transport in production.
30
+ *
31
+ * **Dispatch capability:** sliding-only, no competing consumers. The in-memory
32
+ * transport is single-process: there is no broker to round-robin messages
33
+ * between multiple handlers in a shared `group`. Auto-derived per-handler
34
+ * groups work normally (each handler is its own subscription, fan-out is
35
+ * automatic). Handlers requesting `mode: 'batch'` or sharing an explicit
36
+ * `group` are rejected at `MessageBus.onReady()`.
37
+ */
38
+ export declare class InMemoryTransport implements MessageTransport {
39
+ readonly supportedModes: readonly ["sliding"];
40
+ readonly defaultMode: DispatchMode;
41
+ readonly supportsCompetingConsumers = false;
42
+ private subs;
43
+ private retry;
44
+ private cloneOnPublish;
45
+ private drainTimeoutMs;
46
+ private logger;
47
+ private pendingTimers;
48
+ private pendingDeliveries;
49
+ private destroying;
50
+ constructor(options?: InMemoryTransportOptions);
51
+ publish(envelope: MessageEnvelope): Promise<void>;
52
+ subscribe(topic: string, handler: (envelope: MessageEnvelope) => Promise<HandlerResult>, _options: SubscribeOptions): Promise<Subscription>;
53
+ onDestroy(): Promise<void>;
54
+ /** Tracking wrapper - every (initial and retry) delivery flows through here. */
55
+ private deliver;
56
+ private runDelivery;
57
+ private waitForDrain;
58
+ }
59
+ export declare function inMemoryTransport(options?: InMemoryTransportOptions): MessageTransport;
60
+ //# sourceMappingURL=in-memory-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory-transport.d.ts","sourceRoot":"","sources":["../src/in-memory-transport.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;IAC5B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AASD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,iBAAkB,YAAW,gBAAgB;IACxD,QAAQ,CAAC,cAAc,uBAAyD;IAChF,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAY;IAC9C,QAAQ,CAAC,0BAA0B,SAAQ;IAE3C,OAAO,CAAC,IAAI,CAAgC;IAC5C,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,aAAa,CAA2C;IAChE,OAAO,CAAC,iBAAiB,CAA2B;IACpD,OAAO,CAAC,UAAU,CAAQ;gBAEd,OAAO,GAAE,wBAA6B;IAM5C,OAAO,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjD,SAAS,CACb,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,QAAQ,EAAE,eAAe,KAAK,OAAO,CAAC,aAAa,CAAC,EAC9D,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC,YAAY,CAAC;IAmBlB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAQhC,gFAAgF;IAChF,OAAO,CAAC,OAAO;YAQD,WAAW;YAyCX,YAAY;CAkB3B;AAED,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,gBAAgB,CAE1F"}