- 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.
110 lines
2.9 KiB
TypeScript
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
|
|
};
|
|
}
|