Skip to content

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Nov 4, 2025

Summary

Adds ping and poll pattern support to the Flutter SDK's streaming data source, enabling compatibility with the LaunchDarkly Relay Proxy. This implementation was requested for a customer POV and differs from iOS/Android SDKs by using async polling with in-order processing and retry mechanism.

Link to Devin run: https://app.devin.ai/sessions/566c53f955f743ed96eb59b2a22f564e
Requested by: Ryan Lamb ([email protected])
Original implementation by: John Winstead

Key Changes

  • Added 'ping' event type to SSEClient subscription
  • Implemented async polling triggered by ping events
  • Added generation counter to prevent out-of-order poll updates
  • Added retry mechanism with exponential backoff for failed polls
  • Added ETag support for efficient polling (304 Not Modified)
  • Refactored URI building and HTTP client setup into helper methods

Implementation Highlights

Async polling with in-order processing:

  • Uses a generation counter (_pollGeneration) that increments with each ping
  • In-flight polls from older generations are discarded if a newer ping arrives
  • Prevents race conditions when multiple pings arrive during polling

Retry mechanism:

  • Leverages existing Backoff utility from event_source_client
  • Only retries the most recent generation's poll
  • Distinguishes between recoverable (retry) and unrecoverable (shutdown) errors

ETag optimization:

  • Tracks last ETag from successful polls
  • Sends If-None-Match header on subsequent polls
  • Handles 304 Not Modified responses efficiently

Differences from iOS/Android SDKs

Per JW's notes:

  • vs Android: Async polling (Android uses synchronous poll-for-ping which can delay flag updates)
  • vs iOS: In-order processing (iOS is async but could apply updates out of order)
  • vs both: Retry mechanism (no mobile SDKs currently retry polls for pings)

Human Review Checklist

⚠️ Important: This code has not been tested locally (environment lacks dart/flutter) or in integration tests.

Please carefully review:

  1. Generation counter logic (_pollWithRetry, _isValidGeneration):

    • Does it correctly prevent out-of-order updates when pings arrive rapidly?
    • Is incrementing generation in stop() sufficient to cancel in-flight polls?
    • Could abandoned polls cause memory leaks?
  2. Retry and backoff behavior (_pollWithRetry, _waitForBackoff):

    • Is the backoff configuration appropriate?
    • Can it get stuck in infinite retry loops?
    • Does it properly terminate when stopped?
  3. Error handling (_handlePollingError):

    • Are the right status codes classified as recoverable vs unrecoverable?
    • Should any additional status codes trigger shutdown vs retry?
  4. ETag state management (_lastEtag, _buildPollingHeaders, _updateEtagFromResponse):

    • Is the ETag handling thread-safe in Dart's async model?
    • Are there edge cases where stale ETags could cause issues?
  5. Resource cleanup (stop()):

    • Are all polling resources properly cleaned up?
    • Is the _pollActiveSince reset sufficient?
    • Should polling client be explicitly closed?
  6. Integration with streaming:

    • Do streaming messages and polling responses coexist correctly?
    • Could there be conflicts in the data source event stream?

Requirements

  • I have added test coverage for new or changed functionality (Note: No tests added in this PR; integration testing recommended)
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions (Note: Not tested locally due to environment limitations)

Related Issues

This enables Relay Proxy compatibility for Flutter SDK, addressing customer POV requirements where ping streams are needed.

Testing Plan

Recommended testing:

  1. Test with Relay Proxy sending ping events at various frequencies
  2. Verify flag updates arrive in correct order when multiple pings occur
  3. Test retry behavior with temporary network failures
  4. Verify ETag handling reduces bandwidth on unchanged polls
  5. Test resource cleanup when stopping/restarting data source
  6. Verify no conflicts between streaming and polling paths

Additional Context

The Flutter SDK previously only supported streaming with put, patch, and delete events. The Relay Proxy uses ping events to signal when clients should poll for updates. This implementation adds that missing capability while improving upon the patterns used in iOS and Android SDKs.


Note

Adds ping-triggered polling with ETag and retry/backoff to streaming, refactors polling to a reusable requestor, and exposes Backoff/utilities needed.

  • Streaming Data Source (streaming_data_source.dart):
    • Handle ping SSE events; trigger async poll requests using HttpClient + DataSourceRequestor.
    • Add in-order request chaining, ETag handling, and retry with exponential backoff (Backoff).
    • Refactor URI building and polling client setup; close polling client on stop.
  • Polling Data Source (polling_data_source.dart):
    • Refactor to use DataSourceRequestor for requests, ETag, and response handling.
    • Simplify state; remove inline ETag/credential/env-id logic.
  • Shared Requestor (data_source_requestor.dart):
    • New helper encapsulating header/ETag management, environment-id extraction, recoverable/unrecoverable status handling, and chain-based staleness checks.
  • HTTP Client (http_client.dart):
    • Add close() to free underlying client resources.
  • Event Source Client:
    • Include 'ping' in subscribed SSE event types.
    • Export Backoff and use it in HtmlSseClient for reconnect/backoff.

Written by Cursor Bugbot for commit ddf87fc. This will update automatically on new commits. Configure here.

This implementation adds support for the ping and poll pattern to the Flutter
SDK's streaming data source, enabling compatibility with the LaunchDarkly Relay
Proxy. This is needed for customer POVs that require Relay Proxy support.

Key features:
- Async polling triggered by ping events (improves on Android's synchronous approach)
- In-order poll processing with generation counter (prevents out-of-order updates vs iOS)
- Retry mechanism with exponential backoff for failed polls (adds resilience not in other mobile SDKs)
- ETag support for efficient polling (304 Not Modified responses)
- Proper error handling and cleanup

Changes:
- Added 'ping' event type to SSEClient subscription
- Added polling infrastructure with HttpClient, URIs, and request methods
- Added poll generation counter to prevent race conditions
- Added retry logic with Backoff utility from event_source_client
- Added ETag support for conditional requests
- Updated stop() method to properly clean up polling state

Implementation by: John Winstead
Integrated by: Devin AI

Co-Authored-By: [email protected] <[email protected]>
@devin-ai-integration devin-ai-integration bot requested a review from a team as a code owner November 4, 2025 18:04
@devin-ai-integration
Copy link
Contributor Author

Prompt hidden (unlisted session)

@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

cursor[bot]

This comment was marked as outdated.

…ce leak

- Add close() method to HttpClient class that closes underlying http.Client
- Call _pollingClient.close() in StreamingDataSource.stop() method
- Fixes dart analyzer error about unclosed HttpClient resource

Addresses Bugbot finding about resource leak.

Co-Authored-By: [email protected] <[email protected]>
cursor[bot]

This comment was marked as outdated.

devin-ai-integration bot and others added 4 commits November 4, 2025 18:19
…error

- Add Backoff to public exports in launchdarkly_event_source_client.dart
- Update import in streaming_data_source.dart to use public API instead of internal /src path
- Fixes dart analyzer implementation_imports violation that was causing CI failures

Co-Authored-By: [email protected] <[email protected]>
- Remove redundant import since Backoff is now available through public export
- Resolves dart analyzer unnecessary_import warning
- Completes the fix for exporting Backoff publicly

Co-Authored-By: [email protected] <[email protected]>
- Create shared DataSourceRequestor class for HTTP request handling
- Replace pollGeneration counter with request chain tracking
- Refactor PollingDataSource to use requestor
- Refactor StreamingDataSource to use requestor for ping-triggered polling
- Consolidate duplicate request/response handling code

This provides better request lifecycle management by tracking request
chains instead of generation counters, automatically discarding responses
from stale request chains.

Co-Authored-By: [email protected] <[email protected]>
- Add missing data_source.dart import to DataSourceRequestor
- Add back _getEnvironmentIdFromHeaders method to StreamingDataSource
- Remove unused imports from PollingDataSource (http, credential_type, default_config, get_environment_id)
- Remove unused imports from StreamingDataSource (http)
- Remove unused _credential field from PollingDataSource

These fixes address all 12 analyzer issues found in CI:
- 4 errors (undefined class/methods)
- 8 warnings (unused imports/fields)

Co-Authored-By: [email protected] <[email protected]>
cursor[bot]

This comment was marked as outdated.

…onstructor

The _credential field was removed but the constructor initializer list
still referenced it, causing a compilation error.

Co-Authored-By: [email protected] <[email protected]>
@kinyoklion kinyoklion closed this Nov 4, 2025
…ne limit

Split long lines at line 224 (logger.error call) and line 249 (_buildUri call)
to meet Dart's 80-character line length requirement.

Co-Authored-By: [email protected] <[email protected]>
@devin-ai-integration devin-ai-integration bot reopened this Nov 4, 2025
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Incorrect Delay Calculation Neglects Elapsed Time

Incorrect delay calculation: The max() function compares (_pollingInterval.inMilliseconds - timeSincePoll.inMilliseconds) with _pollingInterval.inMilliseconds. Since timeSincePoll is always positive (time elapsed during the poll), the first argument will always be less than the second, so max() will always return _pollingInterval.inMilliseconds. This means the delay will always be the full polling interval, completely ignoring the time already spent polling. The correct logic should use max(0, _pollingInterval.inMilliseconds - timeSincePoll.inMilliseconds) to ensure the delay is never negative while properly accounting for elapsed time.

packages/common_client/lib/src/data_sources/polling_data_source.dart#L168-L171

// we want to poll after 25 seconds.
final delay = Duration(
milliseconds: max(
_pollingInterval.inMilliseconds - timeSincePoll.inMilliseconds,

Fix in Cursor Fix in Web


Bug: Resource leak: close HTTP client on stop requested

Resource leak: The HTTP client _client is never closed when the polling data source stops. Unlike the streaming data source which properly closes _pollingClient in its stop() method (line 158 in streaming_data_source.dart), the polling data source's stop() method doesn't close the client, leading to a resource leak. The stop() method should call _client.close() to properly clean up resources.

packages/common_client/lib/src/data_sources/polling_data_source.dart#L188-L193

void restart() {
// For polling there is no persistent connection, so this function
// has no effect.
}
@override

Fix in Cursor Fix in Web


@kinyoklion kinyoklion closed this Nov 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants