@nipigev2/messaging-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/consumer.d.ts +27 -0
- package/dist/consumer.d.ts.map +1 -0
- package/dist/consumer.js +141 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/publisher.d.ts +17 -0
- package/dist/publisher.d.ts.map +1 -0
- package/dist/publisher.js +32 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @nipigev2/messaging-client
|
|
2
|
+
|
|
3
|
+
Shared Redis Streams messaging for Nipige services — one source of truth for the
|
|
4
|
+
stream publisher, the resilient consumer-group utility, and the shared event/intent
|
|
5
|
+
types. Replaces the per-service `notify.publisher.ts` copies and the duplicated
|
|
6
|
+
`amqpConsumer.ts`, as the platform moves off RabbitMQ onto Redis Streams.
|
|
7
|
+
|
|
8
|
+
Inject your service's existing `ioredis` client — the package does no config or
|
|
9
|
+
connection management of its own.
|
|
10
|
+
|
|
11
|
+
## Publish
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { publishToStream, publishNotification, streamName } from '@nipigev2/messaging-client';
|
|
15
|
+
import { redisClient } from '../utils/cache';
|
|
16
|
+
import logger from '../utils/logger';
|
|
17
|
+
|
|
18
|
+
// domain event → its business stream (fire-and-forget; never throws)
|
|
19
|
+
await publishToStream(redisClient, streamName('booking_events'), bookingEvent, { logger });
|
|
20
|
+
|
|
21
|
+
// notification intent → the unified notification bus (notify:stream)
|
|
22
|
+
await publishNotification(redisClient, intent, { logger });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Consume
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { startStreamConsumer, streamName } from '@nipigev2/messaging-client';
|
|
29
|
+
import { redisClient } from '../utils/cache';
|
|
30
|
+
import logger from '../utils/logger';
|
|
31
|
+
|
|
32
|
+
const handle = startStreamConsumer(redisClient, {
|
|
33
|
+
name: 'dispatch-service.bookingEvents',
|
|
34
|
+
stream: streamName('booking_events'),
|
|
35
|
+
group: 'dispatch-service', // one group per consuming service (fan-out)
|
|
36
|
+
handler: async (event) => { /* switch on event.eventType */ },
|
|
37
|
+
logger,
|
|
38
|
+
});
|
|
39
|
+
// on shutdown: await handle.stop();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Semantics: at-least-once (manual `XACK`), crash recovery (`XPENDING` + `XCLAIM`
|
|
43
|
+
reclaim sweep), poison-pill dead-lettering after `maxDeliveries`, load-balancing
|
|
44
|
+
across replicas within a group. Blocking reads run on a duplicated connection.
|
|
45
|
+
|
|
46
|
+
## Conventions
|
|
47
|
+
- One stream per former exchange: `streamName('<exchange>')` → `<exchange>:stream`.
|
|
48
|
+
- One consumer **group per consuming service** (fan-out). Multiple instances of a
|
|
49
|
+
service share its group (load-balanced).
|
|
50
|
+
- Entries store the JSON payload under the single field `payload` (`STREAM_FIELD`).
|
|
51
|
+
|
|
52
|
+
## Build / publish
|
|
53
|
+
`npm run build` (tsc → `dist/`), then publish to the private registry. Consumers
|
|
54
|
+
depend on it like `@nipigev2/rbac-client`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis';
|
|
2
|
+
import { MessagingLogger } from './types';
|
|
3
|
+
export interface StreamConsumerOptions<T> {
|
|
4
|
+
/** Identifier used in logs, e.g. 'dispatch-service.bookingEvents'. */
|
|
5
|
+
name: string;
|
|
6
|
+
stream: string;
|
|
7
|
+
group: string;
|
|
8
|
+
/** Handler for one parsed message. Throw to leave the entry pending (redelivered). */
|
|
9
|
+
handler: (payload: T) => Promise<void>;
|
|
10
|
+
/** Max delivery attempts before an entry is dead-lettered. Default 5. */
|
|
11
|
+
maxDeliveries?: number;
|
|
12
|
+
/** XREADGROUP block window in ms. Default 5000. */
|
|
13
|
+
blockMs?: number;
|
|
14
|
+
/** Max entries fetched per read. Default 10. */
|
|
15
|
+
count?: number;
|
|
16
|
+
/** Idle threshold (ms) before a pending entry is reclaimed. Default 60000. */
|
|
17
|
+
reclaimIdleMs?: number;
|
|
18
|
+
/** How often the reclaim sweep runs (ms). Default 30000. */
|
|
19
|
+
reclaimIntervalMs?: number;
|
|
20
|
+
logger?: MessagingLogger;
|
|
21
|
+
}
|
|
22
|
+
export interface StreamConsumerHandle {
|
|
23
|
+
isRunning: () => boolean;
|
|
24
|
+
stop: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare function startStreamConsumer<T = unknown>(redis: Redis, opts: StreamConsumerOptions<T>): StreamConsumerHandle;
|
|
27
|
+
//# sourceMappingURL=consumer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../src/consumer.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAgB,eAAe,EAAc,MAAM,SAAS,CAAC;AAEpE,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,CAAC,GAAG,OAAO,EAC7C,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC7B,oBAAoB,CA8HtB"}
|
package/dist/consumer.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startStreamConsumer = startStreamConsumer;
|
|
7
|
+
/**
|
|
8
|
+
* Resilient Redis Streams consumer (consumer-group based) — the Streams analogue
|
|
9
|
+
* of an AMQP resilient consumer. Preserves at-least-once delivery, per-message
|
|
10
|
+
* ack, redelivery on crash, and load-balancing across replicas. Lifted from the
|
|
11
|
+
* production-proven implementation in the communication service.
|
|
12
|
+
*
|
|
13
|
+
* Inject the service's own `ioredis` client; the utility runs blocking reads on a
|
|
14
|
+
* dedicated duplicated connection (XREADGROUP BLOCK monopolises a connection).
|
|
15
|
+
*/
|
|
16
|
+
const os_1 = __importDefault(require("os"));
|
|
17
|
+
const types_1 = require("./types");
|
|
18
|
+
function startStreamConsumer(redis, opts) {
|
|
19
|
+
const logger = opts.logger ?? types_1.noopLogger;
|
|
20
|
+
const maxDeliveries = opts.maxDeliveries ?? 5;
|
|
21
|
+
const blockMs = opts.blockMs ?? 5000;
|
|
22
|
+
const count = opts.count ?? 10;
|
|
23
|
+
const reclaimIdleMs = opts.reclaimIdleMs ?? 60000;
|
|
24
|
+
const reclaimIntervalMs = opts.reclaimIntervalMs ?? 30000;
|
|
25
|
+
const consumerName = `${os_1.default.hostname()}-${process.pid}`;
|
|
26
|
+
// Dedicated connection — XREADGROUP BLOCK monopolises it.
|
|
27
|
+
const conn = redis.duplicate();
|
|
28
|
+
conn.on('error', (err) => logger.error({ consumer: opts.name, err }, 'stream consumer: redis error'));
|
|
29
|
+
let stopped = false;
|
|
30
|
+
let running = false;
|
|
31
|
+
let reclaimTimer = null;
|
|
32
|
+
const ensureGroup = async () => {
|
|
33
|
+
try {
|
|
34
|
+
await conn.xgroup('CREATE', opts.stream, opts.group, '$', 'MKSTREAM');
|
|
35
|
+
logger.info({ consumer: opts.name, stream: opts.stream, group: opts.group }, 'stream consumer: group created');
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (String(err?.message || '').includes('BUSYGROUP'))
|
|
39
|
+
return;
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const fieldValue = (fields) => {
|
|
44
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
45
|
+
if (fields[i] === types_1.STREAM_FIELD)
|
|
46
|
+
return fields[i + 1];
|
|
47
|
+
}
|
|
48
|
+
return fields[1];
|
|
49
|
+
};
|
|
50
|
+
const processEntry = async (id, fields, deliveries) => {
|
|
51
|
+
const raw = fieldValue(fields);
|
|
52
|
+
if (raw == null) {
|
|
53
|
+
logger.error({ consumer: opts.name, id }, 'stream consumer: empty entry — dead-lettering');
|
|
54
|
+
await conn.xack(opts.stream, opts.group, id);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let payload;
|
|
58
|
+
try {
|
|
59
|
+
payload = JSON.parse(raw);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logger.error({ consumer: opts.name, id, err }, 'stream consumer: unparseable payload — dead-lettering');
|
|
63
|
+
await conn.xack(opts.stream, opts.group, id);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
await opts.handler(payload);
|
|
68
|
+
await conn.xack(opts.stream, opts.group, id);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (deliveries >= maxDeliveries) {
|
|
72
|
+
logger.error({ consumer: opts.name, id, deliveries, err }, 'stream consumer: max deliveries exceeded — dead-lettering');
|
|
73
|
+
await conn.xack(opts.stream, opts.group, id);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
logger.warn({ consumer: opts.name, id, deliveries, err }, 'stream consumer: handler failed — will retry');
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const readLoop = async () => {
|
|
80
|
+
while (!stopped) {
|
|
81
|
+
try {
|
|
82
|
+
const res = (await conn.xreadgroup('GROUP', opts.group, consumerName, 'COUNT', count, 'BLOCK', blockMs, 'STREAMS', opts.stream, '>'));
|
|
83
|
+
if (!res)
|
|
84
|
+
continue;
|
|
85
|
+
for (const [, entries] of res) {
|
|
86
|
+
for (const [id, fields] of entries) {
|
|
87
|
+
await processEntry(id, fields, 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (stopped)
|
|
93
|
+
break;
|
|
94
|
+
logger.error({ consumer: opts.name, err }, 'stream consumer: read loop error — retrying shortly');
|
|
95
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const reclaimSweep = async () => {
|
|
100
|
+
try {
|
|
101
|
+
const pending = (await conn.xpending(opts.stream, opts.group, 'IDLE', reclaimIdleMs, '-', '+', 50));
|
|
102
|
+
if (!pending?.length)
|
|
103
|
+
return;
|
|
104
|
+
for (const [id, , , deliveries] of pending) {
|
|
105
|
+
const claimed = (await conn.xclaim(opts.stream, opts.group, consumerName, reclaimIdleMs, id));
|
|
106
|
+
if (!claimed?.length)
|
|
107
|
+
continue;
|
|
108
|
+
const [, fields] = claimed[0];
|
|
109
|
+
await processEntry(id, fields, deliveries);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
logger.error({ consumer: opts.name, err }, 'stream consumer: reclaim sweep error');
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
void (async () => {
|
|
117
|
+
try {
|
|
118
|
+
await ensureGroup();
|
|
119
|
+
running = true;
|
|
120
|
+
logger.info({ consumer: opts.name, consumerName }, 'stream consumer started');
|
|
121
|
+
reclaimTimer = setInterval(() => { void reclaimSweep(); }, reclaimIntervalMs);
|
|
122
|
+
await readLoop();
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
running = false;
|
|
126
|
+
logger.error({ consumer: opts.name, err }, 'stream consumer: failed to start');
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
return {
|
|
130
|
+
isRunning: () => running,
|
|
131
|
+
stop: async () => {
|
|
132
|
+
stopped = true;
|
|
133
|
+
running = false;
|
|
134
|
+
if (reclaimTimer) {
|
|
135
|
+
clearInterval(reclaimTimer);
|
|
136
|
+
reclaimTimer = null;
|
|
137
|
+
}
|
|
138
|
+
conn.disconnect();
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { STREAM_FIELD, NOTIFY_STREAM, NOTIFY_GROUP, streamName, noopLogger, } from './types';
|
|
2
|
+
export type { NotificationChannel, NotificationRecipient, NotificationIntent, DomainEvent, MessagingLogger, } from './types';
|
|
3
|
+
export { publishToStream, publishNotification } from './publisher';
|
|
4
|
+
export type { PublishOptions } from './publisher';
|
|
5
|
+
export { startStreamConsumer } from './consumer';
|
|
6
|
+
export type { StreamConsumerOptions, StreamConsumerHandle } from './consumer';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,UAAU,EACV,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,WAAW,EACX,eAAe,GAChB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startStreamConsumer = exports.publishNotification = exports.publishToStream = exports.noopLogger = exports.streamName = exports.NOTIFY_GROUP = exports.NOTIFY_STREAM = exports.STREAM_FIELD = void 0;
|
|
4
|
+
var types_1 = require("./types");
|
|
5
|
+
Object.defineProperty(exports, "STREAM_FIELD", { enumerable: true, get: function () { return types_1.STREAM_FIELD; } });
|
|
6
|
+
Object.defineProperty(exports, "NOTIFY_STREAM", { enumerable: true, get: function () { return types_1.NOTIFY_STREAM; } });
|
|
7
|
+
Object.defineProperty(exports, "NOTIFY_GROUP", { enumerable: true, get: function () { return types_1.NOTIFY_GROUP; } });
|
|
8
|
+
Object.defineProperty(exports, "streamName", { enumerable: true, get: function () { return types_1.streamName; } });
|
|
9
|
+
Object.defineProperty(exports, "noopLogger", { enumerable: true, get: function () { return types_1.noopLogger; } });
|
|
10
|
+
var publisher_1 = require("./publisher");
|
|
11
|
+
Object.defineProperty(exports, "publishToStream", { enumerable: true, get: function () { return publisher_1.publishToStream; } });
|
|
12
|
+
Object.defineProperty(exports, "publishNotification", { enumerable: true, get: function () { return publisher_1.publishNotification; } });
|
|
13
|
+
var consumer_1 = require("./consumer");
|
|
14
|
+
Object.defineProperty(exports, "startStreamConsumer", { enumerable: true, get: function () { return consumer_1.startStreamConsumer; } });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis';
|
|
2
|
+
import { NotificationIntent, MessagingLogger } from './types';
|
|
3
|
+
export interface PublishOptions {
|
|
4
|
+
/** Approximate stream cap (XADD MAXLEN ~ N). Default 100000. */
|
|
5
|
+
maxLen?: number;
|
|
6
|
+
logger?: MessagingLogger;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* XADD a JSON payload to a stream. Fire-and-forget: on Redis error it logs and
|
|
10
|
+
* returns null rather than throwing, so a messaging failure never breaks the
|
|
11
|
+
* caller's business path. Pass a real `ioredis` client (or null in STUB/dev —
|
|
12
|
+
* then it no-ops).
|
|
13
|
+
*/
|
|
14
|
+
export declare function publishToStream(redis: Redis | null | undefined, stream: string, payload: unknown, opts?: PublishOptions): Promise<string | null>;
|
|
15
|
+
/** Convenience: publish a NotificationIntent to the unified notification bus. */
|
|
16
|
+
export declare function publishNotification(redis: Redis | null | undefined, intent: NotificationIntent, opts?: PublishOptions): Promise<string | null>;
|
|
17
|
+
//# sourceMappingURL=publisher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publisher.d.ts","sourceRoot":"","sources":["../src/publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAA+B,kBAAkB,EAAE,eAAe,EAAc,MAAM,SAAS,CAAC;AAIvG,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,KAAK,GAAG,IAAI,GAAG,SAAS,EAC/B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAcxB;AAED,iFAAiF;AACjF,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,KAAK,GAAG,IAAI,GAAG,SAAS,EAC/B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAExB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.publishToStream = publishToStream;
|
|
4
|
+
exports.publishNotification = publishNotification;
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
const DEFAULT_MAXLEN = 100000;
|
|
7
|
+
/**
|
|
8
|
+
* XADD a JSON payload to a stream. Fire-and-forget: on Redis error it logs and
|
|
9
|
+
* returns null rather than throwing, so a messaging failure never breaks the
|
|
10
|
+
* caller's business path. Pass a real `ioredis` client (or null in STUB/dev —
|
|
11
|
+
* then it no-ops).
|
|
12
|
+
*/
|
|
13
|
+
async function publishToStream(redis, stream, payload, opts = {}) {
|
|
14
|
+
const logger = opts.logger ?? types_1.noopLogger;
|
|
15
|
+
if (!redis) {
|
|
16
|
+
logger.warn({ stream }, '[STUB] publishToStream: no Redis client');
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const id = await redis.xadd(stream, 'MAXLEN', '~', opts.maxLen ?? DEFAULT_MAXLEN, '*', types_1.STREAM_FIELD, JSON.stringify(payload));
|
|
21
|
+
logger.info({ stream, id }, 'published to stream');
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
logger.error({ err, stream }, 'publishToStream failed — dropped');
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Convenience: publish a NotificationIntent to the unified notification bus. */
|
|
30
|
+
async function publishNotification(redis, intent, opts = {}) {
|
|
31
|
+
return publishToStream(redis, types_1.NOTIFY_STREAM, intent, opts);
|
|
32
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Shared Redis Streams messaging types + constants. */
|
|
2
|
+
/** Stream entries store the JSON payload under this single field. */
|
|
3
|
+
export declare const STREAM_FIELD = "payload";
|
|
4
|
+
/** The unified notification bus. */
|
|
5
|
+
export declare const NOTIFY_STREAM = "notify:stream";
|
|
6
|
+
export declare const NOTIFY_GROUP = "comm";
|
|
7
|
+
/** Conventional stream name for a domain exchange (e.g. 'booking_events' → 'booking_events:stream'). */
|
|
8
|
+
export declare const streamName: (exchange: string) => string;
|
|
9
|
+
export type NotificationChannel = 'push' | 'email' | 'sms' | 'inapp';
|
|
10
|
+
export interface NotificationRecipient {
|
|
11
|
+
userId?: string;
|
|
12
|
+
userType?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
phone?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The notification-bus contract. Producers author it; the communication service
|
|
19
|
+
* dispatches it. Email/SMS carry a template `concern`; push/in-app carry rendered
|
|
20
|
+
* text; in-app also supports a concern/template variant.
|
|
21
|
+
*/
|
|
22
|
+
export interface NotificationIntent {
|
|
23
|
+
id: string;
|
|
24
|
+
tenant: string;
|
|
25
|
+
source: string;
|
|
26
|
+
eventType: string;
|
|
27
|
+
recipient: NotificationRecipient;
|
|
28
|
+
channels: NotificationChannel[];
|
|
29
|
+
push?: {
|
|
30
|
+
title: string;
|
|
31
|
+
body: string;
|
|
32
|
+
data?: Record<string, string>;
|
|
33
|
+
};
|
|
34
|
+
inapp?: {
|
|
35
|
+
title?: string;
|
|
36
|
+
body?: string;
|
|
37
|
+
service?: string;
|
|
38
|
+
concern?: string;
|
|
39
|
+
meta?: Record<string, unknown>;
|
|
40
|
+
data?: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
email?: {
|
|
43
|
+
concern: string;
|
|
44
|
+
subject?: string;
|
|
45
|
+
data?: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
sms?: {
|
|
48
|
+
concern: string;
|
|
49
|
+
data?: Record<string, unknown>;
|
|
50
|
+
};
|
|
51
|
+
timestamp: string;
|
|
52
|
+
}
|
|
53
|
+
/** Generic domain-event envelope for business streams (booking/order/ride/etc.). */
|
|
54
|
+
export interface DomainEvent<T = Record<string, unknown>> {
|
|
55
|
+
eventType: string;
|
|
56
|
+
tenant?: string;
|
|
57
|
+
timestamp: string;
|
|
58
|
+
payload?: T;
|
|
59
|
+
[k: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
/** Minimal logger the utilities use; defaults to a no-op. */
|
|
62
|
+
export interface MessagingLogger {
|
|
63
|
+
info: (obj: unknown, msg?: string) => void;
|
|
64
|
+
warn: (obj: unknown, msg?: string) => void;
|
|
65
|
+
error: (obj: unknown, msg?: string) => void;
|
|
66
|
+
debug?: (obj: unknown, msg?: string) => void;
|
|
67
|
+
}
|
|
68
|
+
export declare const noopLogger: MessagingLogger;
|
|
69
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,wDAAwD;AAExD,qEAAqE;AACrE,eAAO,MAAM,YAAY,YAAY,CAAC;AAEtC,oCAAoC;AACpC,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAC7C,eAAO,MAAM,YAAY,SAAS,CAAC;AAEnC,wGAAwG;AACxG,eAAO,MAAM,UAAU,GAAI,UAAU,MAAM,KAAG,MAA8B,CAAC;AAE7E,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;AAErE,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,qBAAqB,CAAC;IACjC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;IACtE,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IAC9I,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IAC9E,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IAC1D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,oFAAoF;AACpF,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,CAAC;IACZ,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C;AAED,eAAO,MAAM,UAAU,EAAE,eAKxB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Shared Redis Streams messaging types + constants. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.noopLogger = exports.streamName = exports.NOTIFY_GROUP = exports.NOTIFY_STREAM = exports.STREAM_FIELD = void 0;
|
|
5
|
+
/** Stream entries store the JSON payload under this single field. */
|
|
6
|
+
exports.STREAM_FIELD = 'payload';
|
|
7
|
+
/** The unified notification bus. */
|
|
8
|
+
exports.NOTIFY_STREAM = 'notify:stream';
|
|
9
|
+
exports.NOTIFY_GROUP = 'comm';
|
|
10
|
+
/** Conventional stream name for a domain exchange (e.g. 'booking_events' → 'booking_events:stream'). */
|
|
11
|
+
const streamName = (exchange) => `${exchange}:stream`;
|
|
12
|
+
exports.streamName = streamName;
|
|
13
|
+
exports.noopLogger = {
|
|
14
|
+
info: () => { },
|
|
15
|
+
warn: () => { },
|
|
16
|
+
error: () => { },
|
|
17
|
+
debug: () => { },
|
|
18
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nipigev2/messaging-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared Redis Streams messaging (publisher + consumer-group utility) for Nipige services",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"clean": "rm -rf dist",
|
|
14
|
+
"test": "jest --forceExit",
|
|
15
|
+
"prepublishOnly": "npm run clean && npm run build && npm test"
|
|
16
|
+
},
|
|
17
|
+
"jest": {
|
|
18
|
+
"preset": "ts-jest",
|
|
19
|
+
"testEnvironment": "node",
|
|
20
|
+
"testMatch": ["**/__tests__/**/*.test.ts"],
|
|
21
|
+
"moduleFileExtensions": ["ts", "js", "json"]
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"redis",
|
|
28
|
+
"streams",
|
|
29
|
+
"messaging",
|
|
30
|
+
"nipige"
|
|
31
|
+
],
|
|
32
|
+
"author": "Nipige",
|
|
33
|
+
"license": "UNLICENSED",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"ioredis": ">=5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"ioredis": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/jest": "^29.5.14",
|
|
44
|
+
"@types/node": "^20.0.0",
|
|
45
|
+
"ioredis": "^5.10.1",
|
|
46
|
+
"ioredis-mock": "^8.13.1",
|
|
47
|
+
"jest": "^29.7.0",
|
|
48
|
+
"ts-jest": "^29.4.9",
|
|
49
|
+
"typescript": "^5.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|