Skip to content

Stage 6: Polling Service Implementation

mandla-enkosi edited this page Apr 5, 2025 · 1 revision

1. Overview

  • Objective:
    • Implement the startPolling function.
    • Support:
      • Configurable intervals
      • Standard backoff strategies (exponential, linear, Fibonacci, fixed)
      • Network awareness (pauseWhenOffline)
      • Termination conditions
      • Cancellation via handle
  • Expected Result:
    • A reliable startPolling function that:
      • Uses the configured httpClient
      • Handles polling lifecycle and errors gracefully
      • Allows external control via the returned handle

2. Dependency Validation

  • Pre-Stage Requirements:
    • HTTP Client.
    • Platform Abstraction Layer providing INetworkInfo.
    • Internal State Management providing store setters.
  • External Dependencies: Internal httpClient, INetworkInfo.
  • Environment Configurations: Default polling parameters (interval, backoff).

3. Diagrams

  • Visual Aids:

    Polling Service Lifecycle

    sequenceDiagram
        participant App as Application
        participant PollingService as startPolling Func / Manager
        participant Timers as (setTimeout/clearTimeout)
        participant HTTPClient as IHttpClient
        participant Backoff as IBackoffStrategy
        participant Network as INetworkInfo
        participant StateStore as InternalStateStore
        participant Handle as PollingHandle
    
        App->>PollingService: startPolling(options)
        PollingService->>PollingService: Generate unique poll ID
        PollingService->>StateStore: Add ID to operations.activePolls
        PollingService->>PollingService: Create PollingHandle (with ID, stop method)
        PollingService-->>App: Return PollingHandle
        PollingService->>Timers: setTimeout(runPoll, options.initialDelay || options.interval)
    
        loop Poll Loop
            Note over Timers, PollingService: Timer fires...
            Timers-->>PollingService: runPoll()
            PollingService->>Network: getCurrentState()
            alt Network Online OR pauseWhenOffline=false
                Network-->>PollingService: Online State
                PollingService->>HTTPClient: request({ url, method, ..., signal: handle.abortController.signal })
                alt HTTP Request Success (2xx)
                    HTTPClient-->>PollingService: Success Response (data, status)
                    PollingService->>Backoff: reset() // Reset error backoff on success
                    PollingService->>PollingService: Check terminateWhen(data, status)
                    alt Condition Met OR terminateOnSuccessStatus=true
                        PollingService->>App: onSuccess Callback (final)
                        PollingService->>App: onTerminate Callback (CONDITION_MET)
                        PollingService->>PollingService: cleanupPoll(handle)
                        PollingService->>StateStore: Remove ID from operations.activePolls
                        Note over PollingService: Loop End
                    else Condition Not Met
                        PollingService->>App: onSuccess Callback (intermediate)
                        PollingService->>Timers: setTimeout(runPoll, options.interval)
                    end
                else HTTP Request Failed (Error)
                    HTTPClient-->>PollingService: Error (StandardError)
                    PollingService->>App: onError Callback (error, attempt)
                    alt Retryable Error AND attempts < maxAttempts AND time < pollingTimeout
                        PollingService->>Backoff: calculateDelay(attempt)
                        PollingService->>Timers: setTimeout(runPoll, calculatedDelay)
                    else Non-Retryable OR Limits Reached
                        PollingService->>App: onTerminate Callback (ERROR_LIMIT_REACHED / POLLING_TIMEOUT / etc., error)
                        PollingService->>PollingService: cleanupPoll(handle)
                        PollingService->>StateStore: Remove ID from operations.activePolls
                        Note over PollingService: Loop End
                    end
                end
            else Network Offline AND pauseWhenOffline=true
                Network-->>PollingService: Offline State
                PollingService->>Network: subscribe(networkChangeListener) // If not already subscribed
                Note over PollingService: Paused, waits for network online event...
            end
        end
    
        Note over App, PollingService: Later...
        App->>Handle: stop()
        Handle->>PollingService: signalStop(handle.id)
        PollingService->>PollingService: Abort ongoing HTTP request (handle.abortController.abort())
        PollingService->>Timers: clearTimeout(scheduledPollTimer)
        PollingService->>App: onTerminate Callback (CANCELLED / MANUAL_STOP)
        PollingService->>PollingService: cleanupPoll(handle)
        PollingService->>StateStore: Remove ID from operations.activePolls
    
    Loading

4. Touched Parts

  • Modules/Files:
    • /packages/core/src/polling/index.ts: Export startPolling.
    • /packages/core/src/polling/pollingService.ts:
      • Implement startPolling logic
      • Manage internal poll states (timers, attempts, AbortControllers mapped by ID)
      • Handle scheduling
      • Interact with httpClient, backoff strategies, termination conditions, network info
    • /packages/core/src/polling/backoffStrategies.ts: Implement IBackoffStrategy for exponential, linear, fibonacci, fixed. Include jitter logic.
    • /packages/core/src/polling/terminationConditions.ts: Implement helper functions for checking maxAttempts, pollingTimeout, evaluating terminateWhen.
    • /packages/core/src/polling/types.ts: Define PollingOptions, PollingHandle, TerminationReason, BackoffOptions, BackoffStrategyName.
    • /packages/core/src/http/index.ts: Consumed to get httpClient.
    • /packages/core/src/platform/index.ts: Consumed to get INetworkInfo.
    • /packages/core/src/state/store.ts: Updated with operations.activePolls add/remove.
  • Functionalities:
    • startPolling setup and handle creation.
    • Polling loop management using setTimeout.
    • HTTP request execution via httpClient, passing AbortSignal.
    • Response handling: check termination, invoke callbacks, schedule next poll.
    • Error handling: invoke callback, calculate backoff, schedule retry or terminate.
    • Network awareness: Check INetworkInfo, pause/resume based on status and pauseWhenOffline.
    • Cancellation: Implement PollingHandle.stop() to clear timers and abort requests.
    • Resource cleanup on termination/stop.
    • Update internal state tracking active polls.

5. API Contracts & Interface Definitions

// --- Polling Types (/packages/core/src/polling/types.ts) ---

import { StandardError, HttpClientRequestConfig } from '../http/types';

/** Defines the available backoff strategy names. */
export type BackoffStrategyName = 'exponential' | 'linear' | 'fibonacci' | 'fixed';

/** Options for configuring backoff behavior on errors. */
export interface BackoffOptions {
  /** The strategy algorithm to use. */
  strategy: BackoffStrategyName;
  /**
   * The base delay (in ms) for the first retry. If not provided,
   * the poll's `interval` might be used as the base.
   */
  baseDelay?: number;
  /** Maximum delay (in ms) between retries, overriding calculated delay if it exceeds this. */
  maxDelay?: number;
  /** Factor for exponential growth (e.g., 2 for doubling). @default 2 */
  exponent?: number;
  /** Factor for linear growth (delay = base * factor * attempt). @default 1 */
  factor?: number;
  /** Apply random jitter to calculated delays to prevent thundering herd. @default true */
  jitter?: boolean;
  /**
   * Maximum number of consecutive retries allowed after errors before terminating the poll.
   * If undefined, retries continue until other termination conditions are met.
   */
  maxRetries?: number;
}

/** Configuration options for a polling operation. */
export interface PollingOptions<T = any> {
  /** The URL endpoint to poll. */
  url: string;
  /** HTTP method for polling request. @default 'GET' */
  method?: 'GET' | 'POST';
  /** Optional headers for the polling request. */
  headers?: Record<string, string>;
  /** Optional data for POST polling requests. */
  data?: any;

  /** Base interval (in ms) between successful polls, or before the first poll if initialDelay is not set. */
  interval: number;
  /** Configuration for backoff strategy on errors. Can be strategy name for defaults or full options object. */
  backoff?: BackoffOptions | BackoffStrategyName;
  /** Maximum number of total poll attempts (including retries) before termination. */
  maxAttempts?: number;
  /** Maximum total time (in ms) the polling operation should run before termination. */
  pollingTimeout?: number;

  /**
   * A function called with the successful response data and status.
   * Return true to terminate polling, false to continue.
   */
  terminateWhen: (data: T, status: number) => boolean;
  /**
   * If true, polling automatically terminates on any 2xx success status,
   * unless terminateWhen returns false explicitly.
   * @default true
   */
  terminateOnSuccessStatus?: boolean;

  // Callbacks
  /** Called on each successful (2xx) poll response. */
  onSuccess?: (data: T, status: number, handle: PollingHandle) => void;
  /** Called on each failed poll attempt (network error or non-2xx status). */
  onError?: (error: StandardError, attempt: number, handle: PollingHandle) => void;
  /** Called exactly once when polling stops for any reason. */
  onTerminate?: (reason: TerminationReason, lastResult?: T | StandardError, handle: PollingHandle) => void;

  /** If true, polling will automatically pause when network status is offline. @default true */
  pauseWhenOffline?: boolean;
  /** Delay (in ms) before the very first poll attempt. @default 0 */
  initialDelay?: number;

  /** Additional configuration to pass directly to the underlying httpClient request. */
  httpClientConfig?: Omit<HttpClientRequestConfig, 'url' | 'method' | 'data' | 'headers' | 'signal'>;
}

/** Handle returned by startPolling to control the polling operation. */
export interface PollingHandle {
  /** Unique identifier for this polling instance. */
  readonly id: string;
  /** Stops the polling operation immediately. Triggers onTerminate with CANCELLED or MANUAL_STOP. */
  stop(): void;
  /** Internal AbortController signal for cancelling HTTP requests. */
  // readonly abortController: AbortController; // Maybe keep internal
}

/** Reason why a polling operation terminated. */
export enum TerminationReason {
  CONDITION_MET = 'condition_met',      // terminateWhen returned true or success occurred
  MAX_ATTEMPTS_REACHED = 'max_attempts_reached',
  POLLING_TIMEOUT = 'polling_timeout',    // Max duration reached
  CANCELLED = 'cancelled',              // Internal cancellation (e.g., during cleanup)
  MANUAL_STOP = 'manual_stop',          // User called handle.stop()
  ERROR_LIMIT_REACHED = 'error_limit_reached', // Exceeded backoff.maxRetries
  INITIALIZATION_ERROR = 'initialization_error' // Error during setup
}

// --- Polling Service Export (/packages/core/src/polling/index.ts) ---

/**
 * Starts a polling operation based on the provided options.
 * @param options Configuration for the polling operation.
 * @returns A PollingHandle to control the operation.
 */
export declare function startPolling<T = any>(options: PollingOptions<T>): PollingHandle;

// --- Internal Interfaces ---

/** Internal interface for backoff strategy implementations. */
interface IBackoffStrategy {
  /** Calculates the delay for the given attempt number. */
  calculateDelay(attempt: number): number;
  /** Resets any internal state (like attempt count specific to backoff). */
  reset(): void;
}

6. Tests

  • Unit Tests: Test timer scheduling, backoff calculations, termination logic, callback invocation, cancellation logic, offline pause/resume logic using mocks (httpClient, INetworkInfo, timers).
  • Integration Tests: Test full polling lifecycle with mocked HTTP/Network. Test error backoff sequence. Test termination conditions. Test cancellation. Verify auth handled by underlying httpClient. Test behavior during token refresh. Verify activePolls state updates.
  • Edge Cases: Zero interval, immediate termination, rapid network changes, cancellation races.

7. Integration Checkpoints

  • startPolling correctly uses the httpClient instance for requests, passing relevant config and the internal AbortSignal.
  • Authentication is handled transparently by httpClient interceptors during poll requests (including refresh cycles).
    • If a token refresh occurs during a poll request, the poll attempt should fail (handled by onError) and potentially retry according to backoff rules after the refresh completes.
  • startPolling correctly uses INetworkInfo via the platform manager to implement pauseWhenOffline.
  • Cancellation via PollingHandle.stop() successfully aborts the in-flight HTTP request via the signal passed to httpClient.
  • The operations.activePolls slice in the internal state store is correctly updated (add on start, remove on terminate/stop).
  • Callbacks (onSuccess, onError, onTerminate) are invoked correctly.

8. Documentation Deliverables

  • Internal: Comments in pollingService.ts explaining the state machine/loop, timer handling, backoff integration, cancellation flow, offline handling.
  • External:
    • Guide: How to use startPolling, full explanation of PollingOptions (interval, backoff configs, termination, callbacks).
    • Examples: Common polling scenarios (check job status until 'completed', poll with error backoff).
    • Control: How to use the PollingHandle to stop() polling.
  • Changelog: Document the startPolling API and its options.

9. Considerations & Notes

  • Focus on reliable timers, backoff, termination, and cancellation.
  • Keep network awareness simple (pauseWhenOffline). Avoid complex background/battery awareness for V1.
  • Ensure proper cleanup of timers, abort controllers, and network listeners on termination/stop.
  • Jitter in backoff is important.

10. Platform-Specific Implementations

  • Core logic is platform-agnostic.
  • Relies on platform-agnostic httpClient and INetworkInfo.
  • Uses standard JavaScript timers (setTimeout/clearTimeout) and AbortController, which are available in both environments.