@pagermon/ingest-core 1.0.5 → 1.2.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/.lefthook.yml +15 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +24 -0
- package/commitlint.config.js +35 -0
- package/eslint.config.mjs +9 -1
- package/lib/core/ApiClient.js +18 -106
- package/lib/core/Worker.js +26 -25
- package/package.json +15 -3
package/.lefthook.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Lefthook configuration for PagerMon Ingest Core
|
|
2
|
+
# @see https://github.com/evilmartians/lefthook
|
|
3
|
+
|
|
4
|
+
pre-commit:
|
|
5
|
+
commands:
|
|
6
|
+
lint-and-format:
|
|
7
|
+
run: npx lint-staged
|
|
8
|
+
skip:
|
|
9
|
+
- merge
|
|
10
|
+
- rebase
|
|
11
|
+
|
|
12
|
+
commit-msg:
|
|
13
|
+
commands:
|
|
14
|
+
commitlint:
|
|
15
|
+
run: npx commitlint --edit $GIT_PARAMS
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0](https://github.com/eopo/pagermon-ingest-core/compare/v1.1.0...v1.2.0) (2026-03-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* update ApiClient integration and unit tests to simplify response handling ([bc0d0ed](https://github.com/eopo/pagermon-ingest-core/commit/bc0d0ed22c28419080e9d6cbfdb892dec83c2225))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* skip lefthook install in production builds ([1ace8ac](https://github.com/eopo/pagermon-ingest-core/commit/1ace8ac6883d32bc7a2f067d67eede7c4544a9d2))
|
|
14
|
+
|
|
15
|
+
## [1.1.0](https://github.com/eopo/pagermon-ingest-core/compare/v1.0.5...v1.1.0) (2026-03-10)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* add adapter rebuild trigger to release workflow ([b916bfe](https://github.com/eopo/pagermon-ingest-core/commit/b916bfe491208144f07b5b033e2e910b6d61bf67))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* enhance 'no-unused-vars' rule configuration in ESLint ([f6f1a69](https://github.com/eopo/pagermon-ingest-core/commit/f6f1a69feaf785e164d90d61b0171c70e1dd496f))
|
|
26
|
+
|
|
3
27
|
## [1.0.5](https://github.com/eopo/pagermon-ingest-core/compare/v1.0.4...v1.0.5) (2026-03-10)
|
|
4
28
|
|
|
5
29
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commitlint configuration for Conventional Commits
|
|
3
|
+
* Used by Release-Please for automatic changelog generation
|
|
4
|
+
*
|
|
5
|
+
* @see https://commitlint.js.org/
|
|
6
|
+
* @see https://www.conventionalcommits.org/
|
|
7
|
+
*/
|
|
8
|
+
export default {
|
|
9
|
+
extends: ['@commitlint/config-conventional'],
|
|
10
|
+
rules: {
|
|
11
|
+
'type-enum': [
|
|
12
|
+
2,
|
|
13
|
+
'always',
|
|
14
|
+
[
|
|
15
|
+
'feat', // New features
|
|
16
|
+
'fix', // Bug fixes
|
|
17
|
+
'docs', // Documentation changes
|
|
18
|
+
'style', // Code style changes (formatting, etc.)
|
|
19
|
+
'refactor', // Code refactoring
|
|
20
|
+
'perf', // Performance improvements
|
|
21
|
+
'test', // Test changes
|
|
22
|
+
'build', // Build system changes
|
|
23
|
+
'ci', // CI/CD changes
|
|
24
|
+
'chore', // Maintenance tasks
|
|
25
|
+
'revert', // Revert previous commit
|
|
26
|
+
],
|
|
27
|
+
],
|
|
28
|
+
'subject-case': [2, 'never', ['upper-case']], // No UPPERCASE subjects
|
|
29
|
+
'subject-full-stop': [2, 'never', '.'], // No period at end
|
|
30
|
+
'subject-empty': [2, 'never'], // Subject required
|
|
31
|
+
'type-empty': [2, 'never'], // Type required
|
|
32
|
+
'body-leading-blank': [2, 'always'], // Blank line before body
|
|
33
|
+
'footer-leading-blank': [2, 'always'], // Blank line before footer
|
|
34
|
+
},
|
|
35
|
+
};
|
package/eslint.config.mjs
CHANGED
|
@@ -25,7 +25,15 @@ export default [
|
|
|
25
25
|
camelcase: ['warn', { properties: 'never', ignoreDestructuring: true }],
|
|
26
26
|
|
|
27
27
|
// Code quality
|
|
28
|
-
'no-unused-vars': [
|
|
28
|
+
'no-unused-vars': [
|
|
29
|
+
'error',
|
|
30
|
+
{
|
|
31
|
+
argsIgnorePattern: '^_',
|
|
32
|
+
varsIgnorePattern: '^_|^err$',
|
|
33
|
+
caughtErrors: 'all',
|
|
34
|
+
caughtErrorsIgnorePattern: '^_|^err$',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
29
37
|
'no-unused-expressions': 'off',
|
|
30
38
|
'no-use-before-define': ['error', { functions: false, classes: true, variables: true }],
|
|
31
39
|
|
package/lib/core/ApiClient.js
CHANGED
|
@@ -1,49 +1,33 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API Client - PagerMon API communication
|
|
3
|
-
*
|
|
4
|
-
* Handles HTTP communication with the PagerMon API.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import http from 'http';
|
|
8
2
|
import https from 'https';
|
|
9
3
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
constructor(message) {
|
|
4
|
+
class ApiError extends Error {
|
|
5
|
+
constructor(statusCode, message) {
|
|
13
6
|
super(message);
|
|
14
|
-
this.name = '
|
|
7
|
+
this.name = 'ApiError';
|
|
8
|
+
this.statusCode = statusCode;
|
|
9
|
+
this.retryable = statusCode >= 500;
|
|
15
10
|
}
|
|
16
11
|
}
|
|
17
12
|
|
|
18
|
-
class
|
|
13
|
+
class TimeoutError extends Error {
|
|
19
14
|
constructor(message) {
|
|
20
15
|
super(message);
|
|
21
|
-
this.name = '
|
|
22
|
-
this.
|
|
16
|
+
this.name = 'TimeoutError';
|
|
17
|
+
this.retryable = true;
|
|
23
18
|
}
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
class
|
|
27
|
-
constructor(message) {
|
|
21
|
+
class NetworkError extends Error {
|
|
22
|
+
constructor(message, originalError) {
|
|
28
23
|
super(message);
|
|
29
|
-
this.name = '
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class ServerError extends Error {
|
|
34
|
-
constructor(message) {
|
|
35
|
-
super(message);
|
|
36
|
-
this.name = 'ServerError';
|
|
24
|
+
this.name = 'NetworkError';
|
|
25
|
+
this.retryable = true;
|
|
26
|
+
this.originalError = originalError;
|
|
37
27
|
}
|
|
38
28
|
}
|
|
39
29
|
|
|
40
30
|
class ApiClient {
|
|
41
|
-
/**
|
|
42
|
-
* @param {Object} config
|
|
43
|
-
* @param {string} config.url - API Base URL
|
|
44
|
-
* @param {string} config.apiKey - API key for authentication
|
|
45
|
-
* @param {Object} [options] - Additional options
|
|
46
|
-
*/
|
|
47
31
|
constructor(config, options = {}) {
|
|
48
32
|
if (!config.url) throw new Error('ApiClient requires config.url');
|
|
49
33
|
if (!config.apiKey) throw new Error('ApiClient requires config.apiKey');
|
|
@@ -51,80 +35,27 @@ class ApiClient {
|
|
|
51
35
|
this.url = config.url;
|
|
52
36
|
this.apiKey = config.apiKey;
|
|
53
37
|
this.timeout = options.timeout || 10000;
|
|
54
|
-
this.retries = options.retries || 3;
|
|
55
|
-
this.retryDelay = options.retryDelay || 1000;
|
|
56
38
|
}
|
|
57
39
|
|
|
58
|
-
/**
|
|
59
|
-
* Submit a message to the API
|
|
60
|
-
* @param {Message|Object} message - Message with address, message, format, etc.
|
|
61
|
-
* @returns {Promise<Object>} API response
|
|
62
|
-
*/
|
|
63
40
|
async submitMessage(message) {
|
|
64
41
|
const payload = message.toPayload ? message.toPayload() : message;
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const result = await this._request('POST', '/api/messages', payload);
|
|
68
|
-
return { success: true, data: result };
|
|
69
|
-
} catch (err) {
|
|
70
|
-
return { success: false, error: err.message };
|
|
71
|
-
}
|
|
42
|
+
return await this._request('POST', '/api/messages', payload);
|
|
72
43
|
}
|
|
73
44
|
|
|
74
|
-
/**
|
|
75
|
-
* Check API health
|
|
76
|
-
* @returns {Promise<boolean>}
|
|
77
|
-
*/
|
|
78
45
|
async checkHealth() {
|
|
79
46
|
try {
|
|
80
|
-
const result = await this._request('GET', '/api/health', null, {
|
|
81
|
-
timeout: 5000,
|
|
82
|
-
retries: 1,
|
|
83
|
-
});
|
|
47
|
+
const result = await this._request('GET', '/api/health', null, { timeout: 5000 });
|
|
84
48
|
return result && result.status === 'ok';
|
|
85
49
|
} catch {
|
|
86
50
|
return false;
|
|
87
51
|
}
|
|
88
52
|
}
|
|
89
53
|
|
|
90
|
-
/**
|
|
91
|
-
* Make HTTP request with retry logic
|
|
92
|
-
* @private
|
|
93
|
-
*/
|
|
94
54
|
async _request(method, path, body = null, options = {}) {
|
|
95
55
|
const timeout = options.timeout || this.timeout;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
let lastErr;
|
|
99
|
-
|
|
100
|
-
// Sequential retry with exponential backoff is intentional here.
|
|
101
|
-
/* eslint-disable no-await-in-loop */
|
|
102
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
103
|
-
try {
|
|
104
|
-
return await this._makeRequest(method, path, body, timeout);
|
|
105
|
-
} catch (err) {
|
|
106
|
-
lastErr = err;
|
|
107
|
-
|
|
108
|
-
// Only retry on transient errors
|
|
109
|
-
if (!this._isTransientError(err)) {
|
|
110
|
-
throw err;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (attempt < maxRetries) {
|
|
114
|
-
const delay = this.retryDelay * Math.pow(2, attempt); // Exponential backoff
|
|
115
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/* eslint-enable no-await-in-loop */
|
|
120
|
-
|
|
121
|
-
throw lastErr;
|
|
56
|
+
return await this._makeRequest(method, path, body, timeout);
|
|
122
57
|
}
|
|
123
58
|
|
|
124
|
-
/**
|
|
125
|
-
* Actually execute the HTTP request
|
|
126
|
-
* @private
|
|
127
|
-
*/
|
|
128
59
|
_makeRequest(method, path, body, timeout) {
|
|
129
60
|
return new Promise((resolve, reject) => {
|
|
130
61
|
const url = new URL(path, this.url);
|
|
@@ -158,12 +89,8 @@ class ApiClient {
|
|
|
158
89
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
159
90
|
const parsed = data ? JSON.parse(data) : {};
|
|
160
91
|
resolve(parsed);
|
|
161
|
-
} else if (res.statusCode === 401) {
|
|
162
|
-
reject(new AuthError('Unauthorized'));
|
|
163
|
-
} else if (res.statusCode >= 400 && res.statusCode < 500) {
|
|
164
|
-
reject(new ClientError(`${res.statusCode}: ${data}`));
|
|
165
92
|
} else {
|
|
166
|
-
reject(new
|
|
93
|
+
reject(new ApiError(res.statusCode, `${res.statusCode}: ${data}`));
|
|
167
94
|
}
|
|
168
95
|
} catch (err) {
|
|
169
96
|
reject(err);
|
|
@@ -177,7 +104,7 @@ class ApiClient {
|
|
|
177
104
|
});
|
|
178
105
|
|
|
179
106
|
req.on('error', (err) => {
|
|
180
|
-
reject(err);
|
|
107
|
+
reject(new NetworkError(`Network error: ${err.message}`, err));
|
|
181
108
|
});
|
|
182
109
|
|
|
183
110
|
if (bodyStr) {
|
|
@@ -187,21 +114,6 @@ class ApiClient {
|
|
|
187
114
|
req.end();
|
|
188
115
|
});
|
|
189
116
|
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Determine if an error is transient (retryable)
|
|
193
|
-
* @private
|
|
194
|
-
*/
|
|
195
|
-
_isTransientError(err) {
|
|
196
|
-
if (err instanceof TimeoutError) return true;
|
|
197
|
-
if (err instanceof ServerError) return true;
|
|
198
|
-
if (err instanceof ClientError) return false; // 4xx errors don't retry
|
|
199
|
-
if (err instanceof AuthError) return false; // 401 doesn't retry
|
|
200
|
-
if (err.code === 'ECONNREFUSED') return true;
|
|
201
|
-
if (err.code === 'ETIMEDOUT') return true;
|
|
202
|
-
if (err.code === 'EHOSTUNREACH') return true;
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
117
|
}
|
|
206
118
|
|
|
207
119
|
export default ApiClient;
|
package/lib/core/Worker.js
CHANGED
|
@@ -60,47 +60,48 @@ class Worker {
|
|
|
60
60
|
async _processMessage(job) {
|
|
61
61
|
const messageData = job.data;
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
try {
|
|
64
|
+
const result = await this.apiClient.submitMessage(messageData);
|
|
64
65
|
|
|
65
|
-
if (result.success) {
|
|
66
66
|
console.debug(`[WORKER] Message sent: ${messageData.address}`);
|
|
67
67
|
|
|
68
68
|
if (this.callbacks.onMessageProcessed) {
|
|
69
69
|
this.callbacks.onMessageProcessed({
|
|
70
70
|
jobId: job.id,
|
|
71
71
|
message: messageData,
|
|
72
|
+
response: result,
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
return result;
|
|
76
|
-
}
|
|
77
|
-
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.warn(`[WORKER] Message failed: ${messageData.address} - ${error.message}`);
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// Emit failure callback with retry information
|
|
81
|
+
if (this.callbacks.onMessageFailed) {
|
|
82
|
+
this.callbacks.onMessageFailed({
|
|
83
|
+
jobId: job.id,
|
|
84
|
+
message: messageData,
|
|
85
|
+
error: error.message,
|
|
86
|
+
statusCode: error.statusCode,
|
|
87
|
+
retryable: error.retryable ?? true, // Default to retryable for unknown errors
|
|
88
|
+
});
|
|
89
|
+
}
|
|
83
90
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
// Check if API is unhealthy - always retry regardless of error type
|
|
92
|
+
if (!this.health.isHealthy) {
|
|
93
|
+
throw new Error(`API unhealthy: ${error.message}`);
|
|
94
|
+
}
|
|
88
95
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
// Use error.retryable property to decide if BullMQ should retry
|
|
97
|
+
if (error.retryable === false) {
|
|
98
|
+
// Non-retryable error (auth, client errors) - complete without retry
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
jobId: job.id,
|
|
97
|
-
message: messageData,
|
|
98
|
-
error: result.error,
|
|
99
|
-
});
|
|
102
|
+
// Retryable error - throw so BullMQ handles exponential backoff
|
|
103
|
+
throw error;
|
|
100
104
|
}
|
|
101
|
-
|
|
102
|
-
// Return a soft-failure payload without throwing
|
|
103
|
-
return { failed: true, error: result.error };
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagermon/ingest-core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "PagerMon ingest core runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -17,7 +17,16 @@
|
|
|
17
17
|
"format": "prettier --write \"**/*.{js,mjs,json,md}\"",
|
|
18
18
|
"format:check": "prettier --check \"**/*.{js,mjs,json,md}\"",
|
|
19
19
|
"check": "npm run format:check && npm run lint",
|
|
20
|
-
"
|
|
20
|
+
"postinstall": "[ -z \"$CI\" ] && lefthook install || true"
|
|
21
|
+
},
|
|
22
|
+
"lint-staged": {
|
|
23
|
+
"*.{js,mjs}": [
|
|
24
|
+
"eslint --fix",
|
|
25
|
+
"prettier --write"
|
|
26
|
+
],
|
|
27
|
+
"*.{json,md}": [
|
|
28
|
+
"prettier --write"
|
|
29
|
+
]
|
|
21
30
|
},
|
|
22
31
|
"keywords": [
|
|
23
32
|
"pagermon",
|
|
@@ -46,12 +55,15 @@
|
|
|
46
55
|
"ioredis": "^5.8.2"
|
|
47
56
|
},
|
|
48
57
|
"devDependencies": {
|
|
58
|
+
"@commitlint/cli": "^20.4.3",
|
|
59
|
+
"@commitlint/config-conventional": "^20.4.3",
|
|
60
|
+
"@evilmartians/lefthook": "^2.1.3",
|
|
49
61
|
"@vitest/coverage-v8": "^2.1.8",
|
|
50
62
|
"eslint": "^9.16.0",
|
|
51
63
|
"eslint-config-prettier": "^9.1.0",
|
|
52
64
|
"eslint-plugin-prettier": "^5.2.1",
|
|
53
65
|
"globals": "^15.14.0",
|
|
54
|
-
"
|
|
66
|
+
"lint-staged": "^16.3.2",
|
|
55
67
|
"prettier": "^3.4.2",
|
|
56
68
|
"vitest": "^2.1.8"
|
|
57
69
|
}
|