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.
This commit is contained in:
109
packages/polling-contracts/src/backoff.ts
Normal file
109
packages/polling-contracts/src/backoff.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user