Files
daily-notification-plugin/packages/polling-contracts/src/backoff.ts
Matthew Raymer a5831b3c9f feat(polling-contracts): add generic polling interface with TypeScript types and Zod schemas
- Add @timesafari/polling-contracts package with comprehensive type definitions
- Implement GenericPollingRequest, PollingResult, and PollingScheduleConfig interfaces
- Add Zod schemas for StarredProjectsRequest/Response and DeepLinkParams validation
- Include calculateBackoffDelay utility with unified retry policy (exponential, linear, fixed)
- Add OutboxPressureManager for storage pressure controls and back-pressure signals
- Implement TelemetryManager with cardinality budgets and PII redaction
- Add ClockSyncManager for JWT timestamp validation and skew tolerance
- Include comprehensive unit tests with Jest snapshots and race condition testing
- Add JWT_ID_PATTERN regex for canonical JWT ID format validation
- Support idempotency with X-Idempotency-Key enforcement
- Implement watermark CAS (Compare-and-Swap) for race condition prevention

This establishes the foundation for the new generic polling system where host apps
define request/response schemas and the plugin provides robust polling logic.
2025-10-07 04:44:01 +00:00

110 lines
2.9 KiB
TypeScript

/**
* Unified backoff policy implementation
*/
import { BackoffPolicy } from './types';
import { DEFAULT_CONFIG } from './constants';
/**
* Calculate backoff delay with Retry-After + jittered exponential caps
*/
export function calculateBackoffDelay(
attempt: number,
policy: BackoffPolicy,
retryAfterMs?: number
): number {
let delay: number;
// Respect Retry-After header if present and enabled
if (policy.respectRetryAfter && retryAfterMs !== undefined) {
delay = Math.min(retryAfterMs, policy.retryAfterMaxMs || policy.maxDelayMs);
} else {
// Calculate base delay based on strategy
switch (policy.strategy) {
case 'exponential':
delay = policy.baseDelayMs * Math.pow(2, attempt - 1);
break;
case 'linear':
delay = policy.baseDelayMs * attempt;
break;
case 'fixed':
delay = policy.baseDelayMs;
break;
default:
delay = policy.baseDelayMs;
}
}
// Apply jitter if enabled
if (policy.jitterEnabled) {
const jitterRange = delay * policy.jitterFactor;
const jitter = (Math.random() - 0.5) * 2 * jitterRange;
delay = Math.max(0, delay + jitter);
}
// Cap at maximum delay
return Math.min(delay, policy.maxDelayMs);
}
/**
* Create default backoff policy
*/
export function createDefaultBackoffPolicy(): BackoffPolicy {
return {
maxAttempts: 3,
baseDelayMs: DEFAULT_CONFIG.baseDelayMs,
maxDelayMs: DEFAULT_CONFIG.maxDelayMs,
strategy: 'exponential',
jitterEnabled: true,
jitterFactor: DEFAULT_CONFIG.jitterFactor,
respectRetryAfter: DEFAULT_CONFIG.respectRetryAfter,
retryAfterMaxMs: DEFAULT_CONFIG.retryAfterMaxMs
};
}
/**
* Create backoff policy for rate limiting
*/
export function createRateLimitBackoffPolicy(retryAfterMs: number): BackoffPolicy {
return {
maxAttempts: 5,
baseDelayMs: retryAfterMs,
maxDelayMs: Math.max(retryAfterMs * 2, DEFAULT_CONFIG.maxDelayMs),
strategy: 'fixed',
jitterEnabled: true,
jitterFactor: 0.1, // ±10% jitter for rate limits
respectRetryAfter: true,
retryAfterMaxMs: retryAfterMs * 2
};
}
/**
* Create backoff policy for network errors
*/
export function createNetworkErrorBackoffPolicy(): BackoffPolicy {
return {
maxAttempts: 3,
baseDelayMs: DEFAULT_CONFIG.baseDelayMs,
maxDelayMs: DEFAULT_CONFIG.maxDelayMs,
strategy: 'exponential',
jitterEnabled: true,
jitterFactor: DEFAULT_CONFIG.jitterFactor,
respectRetryAfter: false
};
}
/**
* Create backoff policy for server errors (5xx)
*/
export function createServerErrorBackoffPolicy(): BackoffPolicy {
return {
maxAttempts: 3,
baseDelayMs: 2000, // Start with 2s for server errors
maxDelayMs: DEFAULT_CONFIG.maxDelayMs,
strategy: 'exponential',
jitterEnabled: true,
jitterFactor: 0.5, // ±50% jitter for server errors
respectRetryAfter: false
};
}