@mango-power/node-kit 0.0.2 → 0.0.3

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  Shared node toolkit package for MP services.
4
4
 
5
+ ## Included modules
6
+
7
+ - `createLogger`
8
+ - `PGClient`
9
+ - `RedisClient`
10
+ - `RmqService`
11
+ - `EmqxMqttService` / `EMQXMqttService`
12
+
5
13
  ## Build
6
14
 
7
15
  ```bash
@@ -25,6 +33,6 @@ registry=https://registry.npmjs.org/
25
33
  ## Install from other projects
26
34
 
27
35
  ```bash
28
- npm i @mango-power/node-kit@^0.0.1
36
+ npm i @mango-power/node-kit@^0.0.3
29
37
  ```
30
38
 
package/dist/index.d.ts CHANGED
@@ -1,9 +1,6 @@
1
- export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
2
- export interface AppLogger {
3
- debug: (...args: unknown[]) => void;
4
- info: (...args: unknown[]) => void;
5
- warn: (...args: unknown[]) => void;
6
- error: (...args: unknown[]) => void;
7
- fatal: (...args: unknown[]) => void;
8
- }
9
- export declare function createLogger(service: string): AppLogger;
1
+ export * from "./logger";
2
+ export * from "./pg.service";
3
+ export * from "./redis.service";
4
+ export * from "./rmq.service";
5
+ export * from "./mqtt/mqtt.type";
6
+ export * from "./mqtt/emqx-mqtt.service";
package/dist/index.js CHANGED
@@ -1,73 +1,22 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createLogger = createLogger;
4
- const LEVEL_WEIGHT = {
5
- debug: 10,
6
- info: 20,
7
- warn: 30,
8
- error: 40,
9
- fatal: 50,
10
- };
11
- function resolveMinLevel() {
12
- const raw = (process.env.LOG_LEVEL || "info").toLowerCase();
13
- if (raw === "debug" || raw === "info" || raw === "warn" || raw === "error" || raw === "fatal") {
14
- return raw;
15
- }
16
- return "info";
17
- }
18
- const minLevel = resolveMinLevel();
19
- function shouldLog(level) {
20
- return LEVEL_WEIGHT[level] >= LEVEL_WEIGHT[minLevel];
21
- }
22
- function normalizeErrorRecord(record) {
23
- const next = { ...record };
24
- const err = next.err ?? next.error;
25
- if (err instanceof Error) {
26
- next.error = {
27
- name: err.name,
28
- message: err.message,
29
- stack: err.stack,
30
- };
31
- delete next.err;
32
- }
33
- return next;
34
- }
35
- function prefix(service) {
36
- return `[${new Date().toISOString()}] [${service}]`;
37
- }
38
- function normalizeArgs(service, args) {
39
- if (args.length === 0) {
40
- return [prefix(service)];
41
- }
42
- const [first, ...rest] = args;
43
- if (typeof first === "string") {
44
- return [`${prefix(service)} ${first}`, ...rest];
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
45
7
  }
46
- if (first && typeof first === "object" && !Array.isArray(first)) {
47
- return [prefix(service), normalizeErrorRecord(first), ...rest];
48
- }
49
- return [prefix(service), first, ...rest];
50
- }
51
- function write(level, service, ...args) {
52
- if (!shouldLog(level))
53
- return;
54
- const out = normalizeArgs(service, args);
55
- if (level === "error" || level === "fatal") {
56
- console.error(...out);
57
- return;
58
- }
59
- if (level === "warn") {
60
- console.warn(...out);
61
- return;
62
- }
63
- console.log(...out);
64
- }
65
- function createLogger(service) {
66
- return {
67
- debug: (...args) => write("debug", service, ...args),
68
- info: (...args) => write("info", service, ...args),
69
- warn: (...args) => write("warn", service, ...args),
70
- error: (...args) => write("error", service, ...args),
71
- fatal: (...args) => write("fatal", service, ...args),
72
- };
73
- }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./logger"), exports);
18
+ __exportStar(require("./pg.service"), exports);
19
+ __exportStar(require("./redis.service"), exports);
20
+ __exportStar(require("./rmq.service"), exports);
21
+ __exportStar(require("./mqtt/mqtt.type"), exports);
22
+ __exportStar(require("./mqtt/emqx-mqtt.service"), exports);
@@ -0,0 +1,9 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
2
+ export interface AppLogger {
3
+ debug: (...args: unknown[]) => void;
4
+ info: (...args: unknown[]) => void;
5
+ warn: (...args: unknown[]) => void;
6
+ error: (...args: unknown[]) => void;
7
+ fatal: (...args: unknown[]) => void;
8
+ }
9
+ export declare function createLogger(service: string): AppLogger;
package/dist/logger.js ADDED
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLogger = createLogger;
4
+ const LEVEL_WEIGHT = {
5
+ debug: 10,
6
+ info: 20,
7
+ warn: 30,
8
+ error: 40,
9
+ fatal: 50,
10
+ };
11
+ function resolveMinLevel() {
12
+ const raw = (process.env.LOG_LEVEL || "info").toLowerCase();
13
+ if (raw === "debug" || raw === "info" || raw === "warn" || raw === "error" || raw === "fatal") {
14
+ return raw;
15
+ }
16
+ return "info";
17
+ }
18
+ const minLevel = resolveMinLevel();
19
+ function shouldLog(level) {
20
+ return LEVEL_WEIGHT[level] >= LEVEL_WEIGHT[minLevel];
21
+ }
22
+ function normalizeErrorRecord(record) {
23
+ const next = { ...record };
24
+ const err = next.err ?? next.error;
25
+ if (err instanceof Error) {
26
+ next.error = {
27
+ name: err.name,
28
+ message: err.message,
29
+ stack: err.stack,
30
+ };
31
+ delete next.err;
32
+ }
33
+ return next;
34
+ }
35
+ function prefix(service) {
36
+ return `[${new Date().toISOString()}] [${service}]`;
37
+ }
38
+ function normalizeArgs(service, args) {
39
+ if (args.length === 0) {
40
+ return [prefix(service)];
41
+ }
42
+ const [first, ...rest] = args;
43
+ if (typeof first === "string") {
44
+ return [`${prefix(service)} ${first}`, ...rest];
45
+ }
46
+ if (first && typeof first === "object" && !Array.isArray(first)) {
47
+ return [prefix(service), normalizeErrorRecord(first), ...rest];
48
+ }
49
+ return [prefix(service), first, ...rest];
50
+ }
51
+ function write(level, service, ...args) {
52
+ if (!shouldLog(level))
53
+ return;
54
+ const out = normalizeArgs(service, args);
55
+ if (level === "error" || level === "fatal") {
56
+ console.error(...out);
57
+ return;
58
+ }
59
+ if (level === "warn") {
60
+ console.warn(...out);
61
+ return;
62
+ }
63
+ console.log(...out);
64
+ }
65
+ function createLogger(service) {
66
+ return {
67
+ debug: (...args) => write("debug", service, ...args),
68
+ info: (...args) => write("info", service, ...args),
69
+ warn: (...args) => write("warn", service, ...args),
70
+ error: (...args) => write("error", service, ...args),
71
+ fatal: (...args) => write("fatal", service, ...args),
72
+ };
73
+ }
@@ -0,0 +1,44 @@
1
+ import mqtt from "mqtt";
2
+ import { MqttTopicRunner } from "./mqtt.type";
3
+ export interface MqttConfig {
4
+ url: string;
5
+ clientId?: string;
6
+ username?: string;
7
+ password?: string;
8
+ will?: any;
9
+ }
10
+ export type MqttQos = 0 | 1 | 2;
11
+ export interface MqttMessageContext {
12
+ topic: string;
13
+ payload: Buffer;
14
+ match?: string;
15
+ }
16
+ export interface MqttHandlerRegistration {
17
+ id?: string;
18
+ topicPattern: string;
19
+ qos: MqttQos;
20
+ handler: (ctx: MqttMessageContext) => Promise<void> | void;
21
+ }
22
+ export declare class EmqxMqttService {
23
+ client: mqtt.MqttClient;
24
+ private handlerMap;
25
+ private topicHandlerMap;
26
+ private hasConnectedOnce;
27
+ private readonly logger;
28
+ init(config: MqttConfig): Promise<void>;
29
+ private onError;
30
+ private onMessage;
31
+ private onConnect;
32
+ private subscribe;
33
+ private isTopicMatch;
34
+ private isWildcardPattern;
35
+ private getMatchValue;
36
+ publish(topic: string, message: string | Buffer, opts: mqtt.IClientPublishOptions): Promise<void>;
37
+ subscribeHandler(registration: MqttHandlerRegistration): Promise<string>;
38
+ unsubscribeHandler(id: string): Promise<void>;
39
+ registerService(service: MqttTopicRunner): Promise<string>;
40
+ unregisterService(id: string): Promise<void>;
41
+ clearService(): Promise<void>;
42
+ }
43
+ export declare class EMQXMqttService extends EmqxMqttService {
44
+ }
@@ -0,0 +1,217 @@
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.EMQXMqttService = exports.EmqxMqttService = void 0;
7
+ const mqtt_1 = __importDefault(require("mqtt"));
8
+ const crypto_1 = require("crypto");
9
+ const mqtt_pattern_1 = __importDefault(require("mqtt-pattern"));
10
+ const logger_1 = require("../logger");
11
+ class EmqxMqttService {
12
+ constructor() {
13
+ this.handlerMap = new Map();
14
+ this.topicHandlerMap = new Map();
15
+ this.hasConnectedOnce = false;
16
+ this.logger = (0, logger_1.createLogger)(EmqxMqttService.name);
17
+ }
18
+ async init(config) {
19
+ this.client = mqtt_1.default.connect(config.url, {
20
+ clientId: config.clientId || "mqtt-js-" + (0, crypto_1.randomUUID)(),
21
+ username: config.username,
22
+ password: config.password,
23
+ reconnectPeriod: 1000,
24
+ connectTimeout: 30000,
25
+ keepalive: 60,
26
+ resubscribe: true,
27
+ });
28
+ this.client.on("message", async (topic, message) => {
29
+ await this.onMessage(topic, message);
30
+ });
31
+ this.client.on("connect", async () => {
32
+ await this.onConnect();
33
+ });
34
+ this.client.on("close", () => {
35
+ if (this.hasConnectedOnce) {
36
+ this.logger.warn("mqtt.connection.closed");
37
+ }
38
+ else {
39
+ this.logger.info("mqtt.connection.closed.startup");
40
+ }
41
+ });
42
+ this.client.on("offline", () => {
43
+ if (this.hasConnectedOnce) {
44
+ this.logger.warn("mqtt.connection.offline");
45
+ }
46
+ else {
47
+ this.logger.info("mqtt.connection.offline.startup");
48
+ }
49
+ });
50
+ this.client.on("reconnect", () => {
51
+ if (this.hasConnectedOnce) {
52
+ this.logger.warn("mqtt.connection.reconnecting");
53
+ }
54
+ else {
55
+ this.logger.info("mqtt.connection.reconnecting.startup");
56
+ }
57
+ });
58
+ this.client.on("end", () => {
59
+ this.logger.warn("mqtt.connection.ended");
60
+ });
61
+ this.client.on("disconnect", (packet) => {
62
+ this.logger.warn({ packet }, "mqtt.connection.disconnected");
63
+ });
64
+ this.client.on("error", async (e) => {
65
+ if (this.hasConnectedOnce) {
66
+ this.logger.error({ err: e }, "mqtt.connection.error");
67
+ }
68
+ else {
69
+ this.logger.warn({ err: e }, "mqtt.connection.error.startup");
70
+ }
71
+ await this.onError();
72
+ });
73
+ await new Promise((resolve, reject) => {
74
+ const timeoutMs = 90000;
75
+ const timer = setTimeout(() => {
76
+ reject(new Error(`mqtt.init.timeout.${timeoutMs}ms`));
77
+ }, timeoutMs);
78
+ this.client.once("connect", () => {
79
+ clearTimeout(timer);
80
+ resolve(1);
81
+ });
82
+ });
83
+ }
84
+ async onError() {
85
+ this.logger.info("mqtt.error.handled");
86
+ }
87
+ async onMessage(topic, message) {
88
+ this.logger.debug({ topic, payloadSize: message.length }, "mqtt.message.received");
89
+ for (const [pattern, groupMap] of this.topicHandlerMap.entries()) {
90
+ if (!this.isTopicMatch(pattern, topic)) {
91
+ continue;
92
+ }
93
+ const matchValue = this.getMatchValue(pattern, topic);
94
+ for (const registration of groupMap.values()) {
95
+ this.logger.debug({ topic, topicPattern: registration.topicPattern, match: matchValue }, "mqtt.message.matched");
96
+ await registration.handler({ topic, payload: message, match: matchValue });
97
+ }
98
+ }
99
+ }
100
+ async onConnect() {
101
+ this.hasConnectedOnce = true;
102
+ this.logger.info("mqtt.connection.connected");
103
+ const topicQosMap = new Map();
104
+ for (const [topicPattern, groupMap] of this.topicHandlerMap.entries()) {
105
+ let maxQos = 0;
106
+ for (const registration of groupMap.values()) {
107
+ maxQos = Math.max(maxQos, registration.qos);
108
+ }
109
+ topicQosMap.set(topicPattern, maxQos);
110
+ }
111
+ for (const [topic, qos] of topicQosMap.entries()) {
112
+ await this.subscribe(topic, { qos });
113
+ }
114
+ if (topicQosMap.size > 0) {
115
+ this.logger.info({ count: topicQosMap.size }, "mqtt.subscription.restored");
116
+ }
117
+ }
118
+ async subscribe(topic, opts) {
119
+ await new Promise((resolve, reject) => {
120
+ this.client.subscribe(topic, opts, (err, granted) => {
121
+ if (err) {
122
+ this.logger.error({ err, topic }, "mqtt.subscription.failed");
123
+ reject(err);
124
+ return;
125
+ }
126
+ this.logger.info({ topic, granted }, "mqtt.subscription.succeeded");
127
+ resolve();
128
+ });
129
+ });
130
+ }
131
+ isTopicMatch(pattern, topic) {
132
+ if (!this.isWildcardPattern(pattern)) {
133
+ return pattern === topic;
134
+ }
135
+ return mqtt_pattern_1.default.matches(pattern, topic);
136
+ }
137
+ isWildcardPattern(pattern) {
138
+ return pattern.includes("+") || pattern.includes("#");
139
+ }
140
+ getMatchValue(pattern, topic) {
141
+ if (!this.isWildcardPattern(pattern)) {
142
+ return topic;
143
+ }
144
+ const plusIdx = pattern.indexOf("+");
145
+ const hashIdx = pattern.indexOf("#");
146
+ const idxList = [plusIdx, hashIdx].filter((idx) => idx >= 0);
147
+ if (idxList.length === 0) {
148
+ return topic;
149
+ }
150
+ const firstWildcardIndex = Math.min(...idxList);
151
+ const prefix = pattern.slice(0, firstWildcardIndex);
152
+ return topic.startsWith(prefix) ? (topic.slice(prefix.length) || "") : topic;
153
+ }
154
+ async publish(topic, message, opts) {
155
+ const payloadSize = typeof message === "string" ? Buffer.byteLength(message) : message.length;
156
+ this.logger.debug({ topic, payloadSize }, "mqtt.message.publishing");
157
+ await this.client.publish(topic, message, opts);
158
+ }
159
+ async subscribeHandler(registration) {
160
+ registration.id = registration.id || (0, crypto_1.randomUUID)();
161
+ this.handlerMap.set(registration.id, registration);
162
+ const groupMap = this.topicHandlerMap.get(registration.topicPattern);
163
+ const isNewTopic = !groupMap;
164
+ if (groupMap) {
165
+ groupMap.set(registration.id, registration);
166
+ }
167
+ else {
168
+ const gMap = new Map();
169
+ gMap.set(registration.id, registration);
170
+ this.topicHandlerMap.set(registration.topicPattern, gMap);
171
+ }
172
+ if (isNewTopic) {
173
+ await this.subscribe(registration.topicPattern, { qos: registration.qos });
174
+ }
175
+ this.logger.info({ topicPattern: registration.topicPattern, qos: registration.qos }, "mqtt.handler.registered");
176
+ return registration.id;
177
+ }
178
+ async unsubscribeHandler(id) {
179
+ const registration = this.handlerMap.get(id);
180
+ if (!registration) {
181
+ return;
182
+ }
183
+ const groupMap = this.topicHandlerMap.get(registration.topicPattern);
184
+ if (groupMap) {
185
+ groupMap.delete(id);
186
+ if (groupMap.size === 0) {
187
+ this.topicHandlerMap.delete(registration.topicPattern);
188
+ this.client.unsubscribe(registration.topicPattern);
189
+ }
190
+ }
191
+ this.handlerMap.delete(id);
192
+ }
193
+ async registerService(service) {
194
+ return this.subscribeHandler({
195
+ id: service.id,
196
+ topicPattern: service.topic,
197
+ qos: service.qos,
198
+ handler: async ({ topic, payload, match }) => {
199
+ await service.callback(topic, payload, match);
200
+ },
201
+ });
202
+ }
203
+ async unregisterService(id) {
204
+ await this.unsubscribeHandler(id);
205
+ }
206
+ async clearService() {
207
+ for (const topic of this.topicHandlerMap.keys()) {
208
+ this.client.unsubscribe(topic);
209
+ }
210
+ this.handlerMap.clear();
211
+ this.topicHandlerMap.clear();
212
+ }
213
+ }
214
+ exports.EmqxMqttService = EmqxMqttService;
215
+ class EMQXMqttService extends EmqxMqttService {
216
+ }
217
+ exports.EMQXMqttService = EMQXMqttService;
@@ -0,0 +1,7 @@
1
+ export declare class MqttTopicRunner {
2
+ id?: string;
3
+ name?: string;
4
+ topic: string;
5
+ qos: 0 | 1 | 2;
6
+ callback: (topic: string, message: Buffer, match?: string) => any;
7
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MqttTopicRunner = void 0;
4
+ const crypto_1 = require("crypto");
5
+ class MqttTopicRunner {
6
+ constructor() {
7
+ this.id = (0, crypto_1.randomUUID)();
8
+ this.name = (0, crypto_1.randomUUID)();
9
+ this.qos = 0;
10
+ }
11
+ }
12
+ exports.MqttTopicRunner = MqttTopicRunner;
@@ -0,0 +1,17 @@
1
+ import * as pg from "pg";
2
+ export declare class PGClientConfig {
3
+ host: string;
4
+ port: number;
5
+ database: string;
6
+ username: string;
7
+ password: string;
8
+ ssl?: any;
9
+ max?: number;
10
+ }
11
+ export declare class PGClient {
12
+ private pool;
13
+ init(config: PGClientConfig): Promise<void>;
14
+ query(sql: string, params?: any[]): Promise<any[]>;
15
+ queryOne(sql: string, params?: any[]): Promise<any>;
16
+ transaction(callback: (client: pg.PoolClient) => any): Promise<void>;
17
+ }
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PGClient = exports.PGClientConfig = void 0;
37
+ const pg = __importStar(require("pg"));
38
+ const humps_1 = require("humps");
39
+ class PGClientConfig {
40
+ constructor() {
41
+ this.ssl = false;
42
+ this.max = 4;
43
+ }
44
+ }
45
+ exports.PGClientConfig = PGClientConfig;
46
+ class PGClient {
47
+ async init(config) {
48
+ const pgConfig = {
49
+ host: config.host,
50
+ port: config.port,
51
+ database: config.database,
52
+ user: config.username,
53
+ password: config.password,
54
+ max: config.max,
55
+ };
56
+ if (config.ssl) {
57
+ pgConfig.ssl = {
58
+ rejectUnauthorized: false
59
+ };
60
+ }
61
+ this.pool = new pg.Pool(pgConfig);
62
+ const client = await this.pool.connect();
63
+ await client.query("select now()");
64
+ await client.release();
65
+ }
66
+ async query(sql, params) {
67
+ let result = [];
68
+ let error;
69
+ const client = await this.pool.connect();
70
+ try {
71
+ const { rows } = await client.query(sql, params);
72
+ result = (0, humps_1.camelizeKeys)(rows);
73
+ }
74
+ catch (e) {
75
+ console.error("db error.", e.stack);
76
+ error = e;
77
+ }
78
+ finally {
79
+ await client.release();
80
+ }
81
+ if (error) {
82
+ throw new Error(String(error));
83
+ }
84
+ return result;
85
+ }
86
+ async queryOne(sql, params) {
87
+ let result;
88
+ const client = await this.pool.connect();
89
+ try {
90
+ const { rows } = await client.query(sql, params);
91
+ result = (0, humps_1.camelizeKeys)(rows[0]) || {};
92
+ }
93
+ catch (e) {
94
+ console.error("db error.", e);
95
+ throw new Error(String(e));
96
+ }
97
+ finally {
98
+ await client.release();
99
+ }
100
+ return result;
101
+ }
102
+ async transaction(callback) {
103
+ const client = await this.pool.connect();
104
+ await client.query("BEGIN");
105
+ try {
106
+ await callback(client);
107
+ await client.query("COMMIT");
108
+ await client.release();
109
+ }
110
+ catch (e) {
111
+ console.error("Transaction error.", e);
112
+ await client.query("ROLLBACK");
113
+ await client.release();
114
+ throw e;
115
+ }
116
+ }
117
+ }
118
+ exports.PGClient = PGClient;
@@ -0,0 +1,21 @@
1
+ import Redis from "ioredis";
2
+ export declare class RedisClientConfig {
3
+ host: string;
4
+ port: number;
5
+ db: number;
6
+ username?: string;
7
+ password?: string;
8
+ tls?: any;
9
+ keyPrefix?: string;
10
+ cluster?: boolean;
11
+ }
12
+ export declare class RedisClient {
13
+ client: Redis;
14
+ private config;
15
+ init(config: RedisClientConfig): Promise<void>;
16
+ set(key: string, value: any, expire?: number): Promise<void>;
17
+ get(key: string): Promise<string | null>;
18
+ setJsonValue(key: string, value: any): Promise<"OK">;
19
+ getJsonValue(key: string): Promise<any>;
20
+ exists(key: string): Promise<number>;
21
+ }
@@ -0,0 +1,76 @@
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.RedisClient = exports.RedisClientConfig = void 0;
7
+ const ioredis_1 = __importDefault(require("ioredis"));
8
+ class RedisClientConfig {
9
+ }
10
+ exports.RedisClientConfig = RedisClientConfig;
11
+ class RedisClient {
12
+ constructor() {
13
+ this.config = null;
14
+ }
15
+ async init(config) {
16
+ this.config = config;
17
+ try {
18
+ this.client = new ioredis_1.default({
19
+ host: config.host,
20
+ port: config.port,
21
+ db: config.db,
22
+ username: config.username,
23
+ password: config.password,
24
+ disableClientInfo: true,
25
+ tls: config.tls || false,
26
+ reconnectOnError: (err) => {
27
+ console.warn("Reconnect on error:", err);
28
+ return true;
29
+ },
30
+ retryStrategy: (times) => {
31
+ const delay = Math.min(times * 50, 2000);
32
+ console.info(`Retrying connection in ${delay}ms`);
33
+ return delay;
34
+ },
35
+ });
36
+ this.client.on("error", (err) => {
37
+ console.error("Redis Error:", err);
38
+ });
39
+ const result = await this.client.ping();
40
+ console.debug(result);
41
+ }
42
+ catch (e) {
43
+ console.error(e);
44
+ }
45
+ }
46
+ async set(key, value, expire) {
47
+ const client = this.client;
48
+ try {
49
+ await client.multi().set(key, value).expire(key, expire || 300).exec();
50
+ }
51
+ catch (e) {
52
+ console.error(e);
53
+ }
54
+ }
55
+ async get(key) {
56
+ return await this.client.get(key);
57
+ }
58
+ async setJsonValue(key, value) {
59
+ return await this.client.set(key, JSON.stringify(value, null, 2));
60
+ }
61
+ async getJsonValue(key) {
62
+ const s = await this.client.get(key);
63
+ let r = {};
64
+ try {
65
+ r = JSON.parse(s);
66
+ }
67
+ catch (e) {
68
+ console.error(e);
69
+ }
70
+ return r;
71
+ }
72
+ async exists(key) {
73
+ return await this.client.exists(key);
74
+ }
75
+ }
76
+ exports.RedisClient = RedisClient;
@@ -0,0 +1,44 @@
1
+ import * as amqp from "amqplib";
2
+ import { Channel } from "amqplib";
3
+ export declare class QueueItem {
4
+ queue: string;
5
+ prefetch?: number;
6
+ }
7
+ export declare class ExchangeItem {
8
+ exchange: string;
9
+ queue: string;
10
+ routingKey: string;
11
+ }
12
+ export declare class RMQConfig {
13
+ host: string;
14
+ port: number;
15
+ vhost: string;
16
+ username: string;
17
+ password: string;
18
+ }
19
+ export declare class RmqService {
20
+ private readonly logger;
21
+ private config;
22
+ private connection;
23
+ private channelMap;
24
+ private consumerMap;
25
+ private consumingQueues;
26
+ private pubList;
27
+ private subList;
28
+ private reconnectPromise;
29
+ private readonly maxReconnectAttempts;
30
+ private readonly reconnectBaseDelayMs;
31
+ private readonly reconnectMaxDelayMs;
32
+ private initialized;
33
+ init(config: RMQConfig, pubList: ExchangeItem[], subList: QueueItem[]): Promise<void>;
34
+ private ensureReady;
35
+ private ensureConnection;
36
+ private handleDisconnect;
37
+ private reconnectWithBackoff;
38
+ createPublishChannel(): Promise<void>;
39
+ private restoreConsumers;
40
+ private startConsume;
41
+ private closeAndClearChannels;
42
+ publishToExchange(exchange: string, routingKey: string | undefined, message: any, options?: amqp.Options.Publish): Promise<void>;
43
+ consumer(queue: string, callback: (data: amqp.ConsumeMessage, channel: Channel) => any): Promise<void>;
44
+ }
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RmqService = exports.RMQConfig = exports.ExchangeItem = exports.QueueItem = void 0;
37
+ const amqp = __importStar(require("amqplib"));
38
+ const logger_1 = require("./logger");
39
+ class QueueItem {
40
+ }
41
+ exports.QueueItem = QueueItem;
42
+ class ExchangeItem {
43
+ }
44
+ exports.ExchangeItem = ExchangeItem;
45
+ class RMQConfig {
46
+ }
47
+ exports.RMQConfig = RMQConfig;
48
+ class RmqService {
49
+ constructor() {
50
+ this.logger = (0, logger_1.createLogger)("RmqService");
51
+ this.config = null;
52
+ this.connection = null;
53
+ this.channelMap = new Map();
54
+ this.consumerMap = new Map();
55
+ this.consumingQueues = new Set();
56
+ this.pubList = [];
57
+ this.subList = [];
58
+ this.reconnectPromise = null;
59
+ this.maxReconnectAttempts = Number(process.env.RMQ_MAX_RECONNECT_ATTEMPTS || 20);
60
+ this.reconnectBaseDelayMs = Number(process.env.RMQ_RECONNECT_BASE_DELAY_MS || 1000);
61
+ this.reconnectMaxDelayMs = Number(process.env.RMQ_RECONNECT_MAX_DELAY_MS || 30000);
62
+ this.initialized = false;
63
+ }
64
+ async init(config, pubList, subList) {
65
+ this.config = config;
66
+ this.pubList = pubList || [];
67
+ this.subList = subList || [];
68
+ this.initialized = true;
69
+ await this.ensureReady(false);
70
+ }
71
+ async ensureReady(restoreConsumers) {
72
+ await this.ensureConnection();
73
+ await this.createPublishChannel();
74
+ if (restoreConsumers) {
75
+ await this.restoreConsumers();
76
+ }
77
+ }
78
+ async ensureConnection() {
79
+ if (this.connection)
80
+ return;
81
+ if (!this.config) {
82
+ throw new Error("rmq config is not initialized");
83
+ }
84
+ this.connection = await amqp.connect({
85
+ protocol: "amqp",
86
+ hostname: this.config.host,
87
+ port: this.config.port,
88
+ username: this.config.username,
89
+ password: this.config.password,
90
+ vhost: this.config.vhost,
91
+ }, {
92
+ clientProperties: {
93
+ connection_name: "1",
94
+ },
95
+ });
96
+ this.connection.on("error", (err) => {
97
+ this.logger.error({ error: err }, "rmq.connection.error");
98
+ this.handleDisconnect("error");
99
+ });
100
+ this.connection.on("close", () => {
101
+ this.logger.warn("rmq.connection.closed");
102
+ this.handleDisconnect("close");
103
+ });
104
+ this.logger.info("rmq.connection.ready");
105
+ }
106
+ handleDisconnect(reason) {
107
+ if (!this.initialized)
108
+ return;
109
+ if (this.reconnectPromise)
110
+ return;
111
+ this.connection = null;
112
+ this.closeAndClearChannels();
113
+ this.reconnectPromise = this.reconnectWithBackoff(reason).finally(() => {
114
+ this.reconnectPromise = null;
115
+ });
116
+ }
117
+ async reconnectWithBackoff(reason) {
118
+ for (let attempt = 1;; attempt++) {
119
+ try {
120
+ await this.ensureReady(true);
121
+ this.logger.info({ reason }, "rmq.reconnect.ready");
122
+ return;
123
+ }
124
+ catch (err) {
125
+ this.logger.error({ attempt, error: err }, "rmq.reconnect.failed");
126
+ if (this.maxReconnectAttempts > 0 && attempt >= this.maxReconnectAttempts) {
127
+ this.logger.fatal({ attempts: attempt }, "rmq.reconnect.max_attempts_reached.exit");
128
+ process.exit(1);
129
+ }
130
+ const delayMs = Math.min(this.reconnectBaseDelayMs * Math.pow(2, attempt - 1), this.reconnectMaxDelayMs);
131
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
132
+ }
133
+ }
134
+ }
135
+ async createPublishChannel() {
136
+ if (!this.connection) {
137
+ throw new Error("RabbitMQ connection is not ready");
138
+ }
139
+ for (const item of this.pubList) {
140
+ if (this.channelMap.has(item.exchange + item.routingKey)) {
141
+ continue;
142
+ }
143
+ const channel = await this.connection.createConfirmChannel();
144
+ await channel.assertExchange(item.exchange, "fanout", { autoDelete: false });
145
+ await channel.assertQueue(item.queue, { autoDelete: false });
146
+ await channel.bindQueue(item.queue, item.exchange, item.routingKey);
147
+ this.channelMap.set(item.exchange + item.routingKey, channel);
148
+ }
149
+ for (const item of this.subList) {
150
+ if (this.channelMap.has(item.queue)) {
151
+ continue;
152
+ }
153
+ const channel = await this.connection.createChannel();
154
+ await channel.assertQueue(item.queue);
155
+ await channel.prefetch(item.prefetch || 1);
156
+ this.channelMap.set(item.queue, channel);
157
+ }
158
+ }
159
+ async restoreConsumers() {
160
+ for (const [queue, callback] of this.consumerMap.entries()) {
161
+ await this.startConsume(queue, callback);
162
+ }
163
+ }
164
+ async startConsume(queue, callback) {
165
+ if (this.consumingQueues.has(queue)) {
166
+ return;
167
+ }
168
+ const channel = this.channelMap.get(queue);
169
+ if (!channel) {
170
+ throw new Error(`consumer channel not found for queue: ${queue}`);
171
+ }
172
+ this.logger.info({ queue }, "rmq.consumer.start");
173
+ await channel.consume(queue, async (data) => {
174
+ if (!data)
175
+ return;
176
+ try {
177
+ await callback(data, channel);
178
+ }
179
+ catch (error) {
180
+ this.logger.error({ queue, error }, "rmq.consumer.callback_failed");
181
+ }
182
+ });
183
+ this.consumingQueues.add(queue);
184
+ }
185
+ closeAndClearChannels() {
186
+ this.channelMap.forEach((c) => {
187
+ try {
188
+ c.close();
189
+ }
190
+ catch (_e) {
191
+ }
192
+ });
193
+ this.channelMap.clear();
194
+ this.consumingQueues.clear();
195
+ }
196
+ async publishToExchange(exchange, routingKey = "", message, options) {
197
+ if (this.reconnectPromise) {
198
+ await this.reconnectPromise;
199
+ }
200
+ let ch = this.channelMap.get(exchange + routingKey);
201
+ if (!ch) {
202
+ await this.ensureReady(false);
203
+ ch = this.channelMap.get(exchange + routingKey);
204
+ }
205
+ if (!ch) {
206
+ throw new Error(`publish channel not found: ${exchange}:${routingKey}`);
207
+ }
208
+ try {
209
+ ch.publish(exchange, routingKey, Buffer.from(JSON.stringify(message)), options);
210
+ await ch.waitForConfirms();
211
+ }
212
+ catch (error) {
213
+ this.logger.error({ exchange, routingKey, error }, "rmq.publish.failed");
214
+ throw error;
215
+ }
216
+ }
217
+ async consumer(queue, callback) {
218
+ this.consumerMap.set(queue, callback);
219
+ if (this.reconnectPromise) {
220
+ await this.reconnectPromise;
221
+ }
222
+ if (!this.channelMap.get(queue)) {
223
+ await this.ensureReady(false);
224
+ }
225
+ await this.startConsume(queue, callback);
226
+ }
227
+ }
228
+ exports.RmqService = RmqService;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
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
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const index_1 = require("../index");
9
+ (0, node_test_1.default)("exports can be constructed", () => {
10
+ const logger = (0, index_1.createLogger)("smoke");
11
+ strict_1.default.equal(typeof logger.info, "function");
12
+ const pg = new index_1.PGClient();
13
+ const redis = new index_1.RedisClient();
14
+ const rmq = new index_1.RmqService();
15
+ const mqtt = new index_1.EmqxMqttService();
16
+ const mqttCompat = new index_1.EMQXMqttService();
17
+ const runner = new index_1.MqttTopicRunner();
18
+ strict_1.default.ok(pg);
19
+ strict_1.default.ok(redis);
20
+ strict_1.default.ok(rmq);
21
+ strict_1.default.ok(mqtt);
22
+ strict_1.default.ok(mqttCompat);
23
+ strict_1.default.ok(runner.id);
24
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mango-power/node-kit",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Shared node toolkit for mp services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,6 +14,7 @@
14
14
  "scripts": {
15
15
  "build": "npx tsc -p tsconfig.json",
16
16
  "typecheck": "npx tsc --noEmit -p tsconfig.json",
17
+ "test": "npm run build && node --test dist/test/smoke.test.js",
17
18
  "prepublishOnly": "npm run build",
18
19
  "release": "npm publish --access public"
19
20
  },
@@ -23,7 +24,18 @@
23
24
  "shared"
24
25
  ],
25
26
  "license": "ISC",
27
+ "dependencies": {
28
+ "amqplib": "^0.10.9",
29
+ "humps": "^2.0.1",
30
+ "ioredis": "^5.8.1",
31
+ "mqtt": "^5.14.1",
32
+ "mqtt-pattern": "^2.1.1",
33
+ "pg": "^8.16.3"
34
+ },
26
35
  "devDependencies": {
36
+ "@types/amqplib": "^0.10.8",
37
+ "@types/humps": "^2.0.6",
38
+ "@types/pg": "^8.15.6",
27
39
  "@types/node": "^24.11.0",
28
40
  "typescript": "^5.9.3"
29
41
  }