@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 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
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.0.5"
2
+ ".": "1.2.0"
3
3
  }
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': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
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
 
@@ -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
- // Error classes
11
- class TimeoutError extends Error {
12
- constructor(message) {
4
+ class ApiError extends Error {
5
+ constructor(statusCode, message) {
13
6
  super(message);
14
- this.name = 'TimeoutError';
7
+ this.name = 'ApiError';
8
+ this.statusCode = statusCode;
9
+ this.retryable = statusCode >= 500;
15
10
  }
16
11
  }
17
12
 
18
- class AuthError extends Error {
13
+ class TimeoutError extends Error {
19
14
  constructor(message) {
20
15
  super(message);
21
- this.name = 'AuthError';
22
- this.isAuth = true;
16
+ this.name = 'TimeoutError';
17
+ this.retryable = true;
23
18
  }
24
19
  }
25
20
 
26
- class ClientError extends Error {
27
- constructor(message) {
21
+ class NetworkError extends Error {
22
+ constructor(message, originalError) {
28
23
  super(message);
29
- this.name = 'ClientError';
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
- const maxRetries = options.retries !== undefined ? options.retries : this.retries;
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 ServerError(`${res.statusCode}: ${data}`));
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;
@@ -60,47 +60,48 @@ class Worker {
60
60
  async _processMessage(job) {
61
61
  const messageData = job.data;
62
62
 
63
- const result = await this.apiClient.submitMessage(messageData);
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
- console.warn(`[WORKER] Message failed: ${messageData.address} - ${result.error}`);
77
+ } catch (error) {
78
+ console.warn(`[WORKER] Message failed: ${messageData.address} - ${error.message}`);
78
79
 
79
- // If API is unhealthy, throw to trigger queue retry
80
- if (!this.health.isHealthy) {
81
- throw new Error(`API unhealthy: ${result.error}`);
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
- // For other failures, throw only for retryable classes
85
- if (result.error && result.error.includes('401')) {
86
- throw new Error('API Authentication failed - will not retry');
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
- if (result.error && !result.error.includes('4')) {
90
- // Server-side errors should be retried
91
- throw new Error(result.error);
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
- if (this.callbacks.onMessageFailed) {
95
- this.callbacks.onMessageFailed({
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.5",
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
- "prepare": "husky"
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
- "husky": "^9.1.7",
66
+ "lint-staged": "^16.3.2",
55
67
  "prettier": "^3.4.2",
56
68
  "vitest": "^2.1.8"
57
69
  }