Skip to content

feat: add polling and cache sources for FDv2#261

Open
kinyoklion wants to merge 2 commits intomainfrom
rlamb/sdk-2184/fdv2-polling-cache-sources
Open

feat: add polling and cache sources for FDv2#261
kinyoklion wants to merge 2 commits intomainfrom
rlamb/sdk-2184/fdv2-polling-cache-sources

Conversation

@kinyoklion
Copy link
Copy Markdown
Member

@kinyoklion kinyoklion commented Apr 29, 2026

Implements the four building blocks Phase 3 needs from Stream A:

  • calculate_poll_delay.dart: pure helper. Given freshness and interval, returns the time remaining in the interval; zero when overdue; full interval when there's no prior freshness or the freshness is in the future (clock skew clamp).
  • polling_initializer.dart: one-shot Initializer. Calls the injected PollFunction up to 3 times. ChangeSetResult and terminal status results return immediately. Interrupted results retry after a 1s delay. After 3 interrupted attempts the last is escalated to terminalError so the orchestrator stops retrying at this layer. close() interrupts a pending retry delay and yields shutdown.
  • polling_synchronizer.dart: long-lived Synchronizer. Single- subscription StreamController that polls immediately on subscribe, then schedules subsequent polls via calculatePollDelay over the freshness of the most recent successful result. Interrupted results pass through but do not advance freshness, so a transient failure does not delay the catch-up poll. Injected TimerFactory and now() for deterministic tests.
  • cache_initializer.dart: Initializer that reads the persistence cache via an injected CachedFlagsReader. Cache hit emits a full ChangeSetResult with persist=false and an empty selector (the cache does not track server-side selector state). Cache miss or reader exception emits a none-type ChangeSetResult so the initializer chain advances. The reader typedef leaves the actual persistence wiring to the orchestrator (Phase C1).

PollFunction, DelayFunction, TimerFactory, and CachedFlagsReader are typedefs rather than concrete dependencies so the orchestrator can wire real implementations and tests can inject scripted ones, mirroring the abstraction style established in SDK-2183.

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

Provide links to any issues in this repository or elsewhere relating to this pull request.

Describe the solution you've provided

Provide a clear and concise description of what you expect to happen.

Describe alternatives you've considered

Provide a clear and concise description of any alternative solutions or features you've considered.

Additional context

Add any other context about the pull request here.


Note

Medium Risk
Introduces new FDv2 initialization and steady-state polling components that affect when/how the SDK fetches and applies flag updates, with potential timing/retry behavior changes. Logic is well-tested but touches core data source orchestration paths.

Overview
Adds new FDv2 data-source building blocks for cache-based startup and polling-based initialization/steady-state sync.

Implements CacheInitializer to load persisted flag evaluations into a full ChangeSetResult (with persist: false) and to fall through on cache misses/errors via a PayloadType.none result. Adds FDv2PollingInitializer with bounded retries on interrupted results and early-abort shutdown semantics, plus a long-lived FDv2PollingSynchronizer that emits poll results on a stream and schedules the next poll using calculatePollDelay based on the last successful freshness (not advanced by transient failures). Includes comprehensive unit tests for all new components.

Reviewed by Cursor Bugbot for commit 07cd384. Bugbot is set up for automated code reviews on this repo. Configure here.

Implements the four building blocks Phase 3 needs from Stream A:

- calculate_poll_delay.dart: pure helper. Given freshness and
  interval, returns the time remaining in the interval; zero when
  overdue; full interval when there's no prior freshness or the
  freshness is in the future (clock skew clamp).
- polling_initializer.dart: one-shot Initializer. Calls the injected
  PollFunction up to 3 times. ChangeSetResult and terminal status
  results return immediately. Interrupted results retry after a 1s
  delay. After 3 interrupted attempts the last is escalated to
  terminalError so the orchestrator stops retrying at this layer.
  close() interrupts a pending retry delay and yields shutdown.
- polling_synchronizer.dart: long-lived Synchronizer. Single-
  subscription StreamController that polls immediately on subscribe,
  then schedules subsequent polls via calculatePollDelay over the
  freshness of the most recent successful result. Interrupted
  results pass through but do not advance freshness, so a transient
  failure does not delay the catch-up poll. Injected TimerFactory
  and now() for deterministic tests.
- cache_initializer.dart: Initializer that reads the persistence
  cache via an injected CachedFlagsReader. Cache hit emits a full
  ChangeSetResult with persist=false and an empty selector (the
  cache does not track server-side selector state). Cache miss or
  reader exception emits a none-type ChangeSetResult so the
  initializer chain advances. The reader typedef leaves the actual
  persistence wiring to the orchestrator (Phase C1).

PollFunction, DelayFunction, TimerFactory, and CachedFlagsReader are
typedefs rather than concrete dependencies so the orchestrator can
wire real implementations and tests can inject scripted ones,
mirroring the abstraction style established in SDK-2183.
/// Caps the returned delay at [interval] so a freshness timestamp from
/// the future (clock skew, manually adjusted system time) cannot push
/// the next poll arbitrarily far out.
Duration calculatePollDelay({
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For now we are always going always poll immediately on a cache initializer. This is used for the synchronizer though.

/// retry delay returns immediately and [run] resolves to a
/// [SourceState.shutdown] result.
final class FDv2PollingInitializer implements Initializer {
static const int _maxAttempts = 3;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think we formalized this into the spec, but for web based/web potential SDKs, I think it makes sense.

@kinyoklion kinyoklion marked this pull request as ready for review April 30, 2026 22:20
@kinyoklion kinyoklion requested a review from a team as a code owner April 30, 2026 22:20
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.

1 participant