@teamvibe/poller 0.2.4 → 0.2.5
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/dist/auth-provider.d.ts +5 -0
- package/dist/auth-provider.js +19 -0
- package/dist/config.js +1 -1
- package/dist/poller.js +38 -2
- package/dist/sqs-poller.js +26 -1
- package/package.json +1 -1
package/dist/auth-provider.d.ts
CHANGED
|
@@ -14,5 +14,10 @@ export declare function getCredentials(): AuthCredentials;
|
|
|
14
14
|
export declare function getAuthConfig(): AuthConfig;
|
|
15
15
|
export declare function getPollerId(): string | null;
|
|
16
16
|
export declare function isTokenMode(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Force an immediate credential refresh. Used when SQS returns ExpiredTokenException.
|
|
19
|
+
* Throws if refresh fails (caller should handle).
|
|
20
|
+
*/
|
|
21
|
+
export declare function forceRefresh(): Promise<void>;
|
|
17
22
|
export declare function stopRefresh(): void;
|
|
18
23
|
export {};
|
package/dist/auth-provider.js
CHANGED
|
@@ -6,6 +6,9 @@ let currentConfig = null;
|
|
|
6
6
|
let currentPollerId = null;
|
|
7
7
|
let refreshTimer = null;
|
|
8
8
|
let heartbeatTimer = null;
|
|
9
|
+
// Stored for on-demand refresh
|
|
10
|
+
let storedApiUrl = null;
|
|
11
|
+
let storedToken = null;
|
|
9
12
|
const REFRESH_INTERVAL_MS = 50 * 60 * 1000; // 50 minutes (before 1hr expiry)
|
|
10
13
|
const HEARTBEAT_INTERVAL_MS = 60 * 1000; // 1 minute
|
|
11
14
|
async function sendHeartbeat(apiUrl, token, version) {
|
|
@@ -44,6 +47,8 @@ async function fetchCredentials(apiUrl, token) {
|
|
|
44
47
|
}
|
|
45
48
|
export async function initAuth(apiUrl, token) {
|
|
46
49
|
logger.info('Authenticating with TeamVibe API...');
|
|
50
|
+
storedApiUrl = apiUrl;
|
|
51
|
+
storedToken = token;
|
|
47
52
|
const auth = await fetchCredentials(apiUrl, token);
|
|
48
53
|
currentCredentials = auth.credentials;
|
|
49
54
|
currentConfig = auth.config;
|
|
@@ -87,6 +92,20 @@ export function getPollerId() {
|
|
|
87
92
|
export function isTokenMode() {
|
|
88
93
|
return currentCredentials !== null;
|
|
89
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Force an immediate credential refresh. Used when SQS returns ExpiredTokenException.
|
|
97
|
+
* Throws if refresh fails (caller should handle).
|
|
98
|
+
*/
|
|
99
|
+
export async function forceRefresh() {
|
|
100
|
+
if (!storedApiUrl || !storedToken) {
|
|
101
|
+
throw new Error('Cannot force refresh: auth not initialized (no stored API URL/token)');
|
|
102
|
+
}
|
|
103
|
+
logger.info('Force-refreshing credentials (token expired)...');
|
|
104
|
+
const auth = await fetchCredentials(storedApiUrl, storedToken);
|
|
105
|
+
currentCredentials = auth.credentials;
|
|
106
|
+
currentConfig = auth.config;
|
|
107
|
+
logger.info('Credentials force-refreshed successfully');
|
|
108
|
+
}
|
|
90
109
|
export function stopRefresh() {
|
|
91
110
|
if (refreshTimer) {
|
|
92
111
|
clearInterval(refreshTimer);
|
package/dist/config.js
CHANGED
|
@@ -44,7 +44,7 @@ const configSchema = z.object({
|
|
|
44
44
|
.transform((v) => v.toLowerCase() === 'true'),
|
|
45
45
|
BRAIN_UPDATE_COOLDOWN_MS: z.coerce.number().default(300000), // 5 minutes
|
|
46
46
|
// Brain heartbeat & maintenance (cron-based)
|
|
47
|
-
HEARTBEAT_CRON: z.string().default('*/
|
|
47
|
+
HEARTBEAT_CRON: z.string().default('0 */6 * * *'), // Every 6 hours
|
|
48
48
|
MAINTENANCE_CRON: z.string().default('30 3 * * *'), // Daily at 03:30 UTC
|
|
49
49
|
MAINTENANCE_ENABLED: z.string().default('true').transform((v) => v.toLowerCase() === 'true'),
|
|
50
50
|
// Auto-update settings
|
package/dist/poller.js
CHANGED
|
@@ -338,10 +338,34 @@ function deduplicateMessages(messages) {
|
|
|
338
338
|
}
|
|
339
339
|
return { unique, duplicates };
|
|
340
340
|
}
|
|
341
|
+
// Circuit breaker for consecutive auth/infrastructure failures
|
|
342
|
+
const AUTH_CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
343
|
+
const AUTH_CIRCUIT_BREAKER_MAX_BACKOFF_MS = 60_000;
|
|
344
|
+
let consecutiveAuthFailures = 0;
|
|
345
|
+
function isAuthError(error) {
|
|
346
|
+
if (!(error instanceof Error))
|
|
347
|
+
return false;
|
|
348
|
+
const name = error.name ?? '';
|
|
349
|
+
const code = error.Code ?? '';
|
|
350
|
+
const message = error.message ?? '';
|
|
351
|
+
return (name === 'ExpiredTokenException' ||
|
|
352
|
+
code === 'ExpiredToken' ||
|
|
353
|
+
code === 'InvalidClientTokenId' ||
|
|
354
|
+
code === 'UnrecognizedClientException' ||
|
|
355
|
+
message.includes('security token') ||
|
|
356
|
+
message.includes('Auth failed'));
|
|
357
|
+
}
|
|
341
358
|
async function pollLoop() {
|
|
342
359
|
logger.info('Poll loop started');
|
|
343
360
|
while (!shuttingDown) {
|
|
344
361
|
try {
|
|
362
|
+
// Circuit breaker: if too many consecutive auth failures, back off
|
|
363
|
+
if (consecutiveAuthFailures >= AUTH_CIRCUIT_BREAKER_THRESHOLD) {
|
|
364
|
+
const backoffMs = Math.min(5_000 * Math.pow(2, consecutiveAuthFailures - AUTH_CIRCUIT_BREAKER_THRESHOLD), AUTH_CIRCUIT_BREAKER_MAX_BACKOFF_MS);
|
|
365
|
+
logger.warn(`[CircuitBreaker] Auth degraded (${consecutiveAuthFailures} consecutive failures), ` +
|
|
366
|
+
`backing off ${backoffMs}ms before retry...`);
|
|
367
|
+
await sleep(backoffMs);
|
|
368
|
+
}
|
|
345
369
|
if (isAtCapacity()) {
|
|
346
370
|
logger.debug(`At capacity (${getActiveProcessCount()}/${config.MAX_CONCURRENT_SESSIONS}), waiting...`);
|
|
347
371
|
await sleep(1000);
|
|
@@ -351,6 +375,11 @@ async function pollLoop() {
|
|
|
351
375
|
logger.debug(`Polling for up to ${availableSlots} messages...`);
|
|
352
376
|
const messages = await pollMessages(availableSlots);
|
|
353
377
|
consecutiveNetFailures = 0; // Successful poll — reset circuit breaker
|
|
378
|
+
// Successful poll — reset circuit breaker
|
|
379
|
+
if (consecutiveAuthFailures > 0) {
|
|
380
|
+
logger.info(`[CircuitBreaker] Auth recovered after ${consecutiveAuthFailures} failure(s)`);
|
|
381
|
+
consecutiveAuthFailures = 0;
|
|
382
|
+
}
|
|
354
383
|
if (messages.length === 0) {
|
|
355
384
|
continue;
|
|
356
385
|
}
|
|
@@ -384,7 +413,13 @@ async function pollLoop() {
|
|
|
384
413
|
Promise.all(processPromises);
|
|
385
414
|
}
|
|
386
415
|
catch (error) {
|
|
387
|
-
if (
|
|
416
|
+
if (isAuthError(error)) {
|
|
417
|
+
consecutiveAuthFailures++;
|
|
418
|
+
logger.error(`[CircuitBreaker] Auth failure #${consecutiveAuthFailures}: ${error instanceof Error ? error.message : error}`);
|
|
419
|
+
emitEvent('poller.auth_failure', '', { consecutiveFailures: consecutiveAuthFailures });
|
|
420
|
+
await sleep(5000);
|
|
421
|
+
}
|
|
422
|
+
else if (isNetworkError(error)) {
|
|
388
423
|
consecutiveNetFailures++;
|
|
389
424
|
logger.error(`Network error in poll loop (${consecutiveNetFailures}/${config.MAX_CONSECUTIVE_NET_FAILURES}):`, error);
|
|
390
425
|
if (consecutiveNetFailures === config.MAX_CONSECUTIVE_NET_FAILURES) {
|
|
@@ -408,7 +443,8 @@ async function pollLoop() {
|
|
|
408
443
|
await sleep(backoffMs);
|
|
409
444
|
}
|
|
410
445
|
else {
|
|
411
|
-
// Non-network errors
|
|
446
|
+
// Non-network, non-auth errors — reset auth circuit breaker (infra is reachable)
|
|
447
|
+
consecutiveAuthFailures = 0;
|
|
412
448
|
logger.error('Error in poll loop:', error);
|
|
413
449
|
await sleep(5000);
|
|
414
450
|
}
|
package/dist/sqs-poller.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand, ChangeMessageVisibilityCommand, SendMessageCommand, } from '@aws-sdk/client-sqs';
|
|
2
2
|
import { config } from './config.js';
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
|
-
import { isTokenMode, getCredentials, getAuthConfig } from './auth-provider.js';
|
|
4
|
+
import { isTokenMode, getCredentials, getAuthConfig, forceRefresh } from './auth-provider.js';
|
|
5
5
|
import { emitError } from './event-queue.js';
|
|
6
|
+
function isExpiredTokenError(error) {
|
|
7
|
+
if (!(error instanceof Error))
|
|
8
|
+
return false;
|
|
9
|
+
const name = error.name ?? '';
|
|
10
|
+
const code = error.Code ?? '';
|
|
11
|
+
return name === 'ExpiredTokenException' || code === 'ExpiredToken';
|
|
12
|
+
}
|
|
6
13
|
function createSqsClient() {
|
|
7
14
|
if (isTokenMode()) {
|
|
8
15
|
const creds = getCredentials();
|
|
@@ -42,6 +49,24 @@ export async function pollMessages(maxMessages = 10) {
|
|
|
42
49
|
return response.Messages.map((msg) => parseMessage(msg)).filter((m) => m !== null);
|
|
43
50
|
}
|
|
44
51
|
catch (error) {
|
|
52
|
+
// Expired AWS STS token — force-refresh credentials and retry once
|
|
53
|
+
if (isTokenMode() && isExpiredTokenError(error)) {
|
|
54
|
+
logger.warn('SQS poll failed with ExpiredToken, attempting credential refresh...');
|
|
55
|
+
await forceRefresh(); // throws if refresh itself fails
|
|
56
|
+
// Retry poll with fresh credentials
|
|
57
|
+
const retryResponse = await createSqsClient().send(new ReceiveMessageCommand({
|
|
58
|
+
QueueUrl: getQueueUrl(),
|
|
59
|
+
MaxNumberOfMessages: Math.min(maxMessages, 10),
|
|
60
|
+
WaitTimeSeconds: config.POLL_WAIT_TIME_SECONDS,
|
|
61
|
+
VisibilityTimeout: config.VISIBILITY_TIMEOUT_SECONDS,
|
|
62
|
+
MessageAttributeNames: ['All'],
|
|
63
|
+
}));
|
|
64
|
+
if (!retryResponse.Messages || retryResponse.Messages.length === 0) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
logger.info(`Received ${retryResponse.Messages.length} message(s) after credential refresh`);
|
|
68
|
+
return retryResponse.Messages.map((msg) => parseMessage(msg)).filter((m) => m !== null);
|
|
69
|
+
}
|
|
45
70
|
logger.error('Error polling SQS:', error);
|
|
46
71
|
emitError('sqs', `Error polling SQS: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
72
|
throw error;
|