Skip to content

feat(SDK-2185): FDv2 streaming base, initializer, and synchronizer#267

Draft
kinyoklion wants to merge 3 commits intomainfrom
rlamb/sdk-2185/fdv2-streaming
Draft

feat(SDK-2185): FDv2 streaming base, initializer, and synchronizer#267
kinyoklion wants to merge 3 commits intomainfrom
rlamb/sdk-2185/fdv2-streaming

Conversation

@kinyoklion
Copy link
Copy Markdown
Member

@kinyoklion kinyoklion commented May 6, 2026

Stacked on #266 (SSE capability surface). Mark this PR ready for review only after #266 merges and the base is rebased onto main.

Jira: SDK-2185

Scope

Phase B1 from the parallel plan — the streaming half of FDv2 sources, mirroring the polling vertical that landed in SDK-2183 / SDK-2184.

Three new files in packages/common_client/lib/src/data_sources/fdv2/:

streaming_base.dartFDv2StreamingBase

Wraps an SSEClient with FDv2 protocol semantics. Single-subscription StreamController<FDv2SourceResult>:

  • onListen → builds a fresh FDv2ProtocolHandler, subscribes to the SSE client's stream.
  • onCancel → tears down without emitting shutdown (subscriber-initiated).
  • close() → emits shutdown then closes; idempotent.

Per event:

  • Named SSE events get parsed as JSON, wrapped in an FDv2Event, fed to the handler.
  • ActionPayloadChangeSetResult with persist: true.
  • ActionGoodbye → goodbye StatusResult; closes the connection (server told us to disconnect).
  • ActionServerError / ActionError → interrupted StatusResult; SSE client's built-in backoff handles reconnect.
  • ActionNone → no emission.
  • Legacy ping events → injected PingHandler (one-shot poll); result is forwarded.
  • x-ld-fd-fallback: true on the OpenEvent → terminalError with fdv1Fallback: true; closes.
  • SSE transport error → interrupted; reconnect via SSE client.

Closed-ness tracked via a single Completer<void> _stoppedSignal (matches the polling synchronizer pattern from SDK-2184).

streaming_initializer.dartFDv2StreamingInitializer

Implements Initializer. Subscribes to the base, completes run() with the first emission, then closes the connection. close() before the first emission yields a shutdown result.

streaming_synchronizer.dartFDv2StreamingSynchronizer

Implements Synchronizer. Thin adapter forwarding the base's stream and close(), so the orchestrator can treat polling and streaming uniformly.

Auth wiring (deferred to orchestrator)

FDv2StreamingBase does not build URLs or set headers. The orchestrator (SDK-2186) constructs the SSEClient, deciding via sseClient.hasCapability(SSECapability.requestHeaders) whether to set the credential in the Authorization header or as the auth URL query parameter. The streaming source consumes whatever client it's handed.

Verification

  • 25 new tests across the three files; full FDv2 directory now at 185 tests.
  • dart analyze lib test clean in common_client/.
  • melos run analyze and melos run test both pass workspace-wide.

Follow-up (out of scope)

  • The orchestrator (C1 / SDK-2186) is what actually wires FDv2StreamingBase into the SDK. This PR delivers the building blocks.

@kinyoklion kinyoklion force-pushed the rlamb/sdk-2185/fdv2-streaming branch 2 times, most recently from 97eca5b to 6a9d5ec Compare May 7, 2026 17:16
Base automatically changed from rlamb/sse-client-capabilities to main May 7, 2026 20:25
@kinyoklion kinyoklion force-pushed the rlamb/sdk-2185/fdv2-streaming branch from 6a9d5ec to 42d879a Compare May 7, 2026 21:06
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 am undecided if we should support this. If we don't then we can combine the base into the synchronizer and discard it.

Flutter does run on web, but it is inherently a single page app. So I don't think one-shot mode is nearly as important as it is for js.

Lets test code assert that the SUT correctly tears the SSE
connection down. Test-only addition; the production SSEClient
implementations do not expose this state, and the TestSseClient
class is already documented as test-only with no semver guarantee.
kinyoklion added 2 commits May 7, 2026 15:15
- streaming_base.dart: wraps an SSEClient. Single-subscription
  StreamController<FDv2SourceResult>. On subscribe, opens the SSE
  stream and creates a fresh FDv2ProtocolHandler. Each named SSE
  event is parsed as JSON, wrapped in an FDv2Event, and fed to the
  handler. ActionPayload becomes a ChangeSetResult (persist: true);
  ActionGoodbye becomes a goodbye StatusResult and closes the
  connection; ActionServerError / ActionError become interrupted
  StatusResults; ActionNone does not emit. Legacy `ping` events
  invoke the injected PingHandler and forward its result. The
  `x-ld-fd-fallback` header on the OpenEvent emits terminalError
  with fdv1Fallback=true and closes. SSE transport errors surface as
  interrupted; the SSE client's built-in backoff handles reconnect.
  Closed-ness is tracked via a single Completer<void> _stoppedSignal
  matching the polling synchronizer.
- streaming_initializer.dart: implements Initializer. Subscribes to
  the base, completes run() with the first emission, then closes the
  connection. close() before the first emission yields a shutdown
  StatusResult.
- streaming_synchronizer.dart: implements Synchronizer. Thin adapter
  forwarding the base's stream so the orchestrator can treat polling
  and streaming uniformly.

Tests cover: lifecycle (open on subscribe, cancel teardown,
close+shutdown, idempotency), event handling (xfer-full payload,
environmentId from header, goodbye, malformed data, non-object
data, transport error), FDv1 fallback header (true / case-
insensitive / false ignored), legacy ping bridge (forwards result,
handles thrower), and synchronizer forwarding.

The orchestrator (SDK-2186) wires the SSEClient with the right URL
and auth strategy based on SSEClient.hasCapability(requestHeaders);
the streaming source consumes whatever client it's given.
Drops the per-file FakeSseClient stub and replaces it with the
shared TestSseClient from launchdarkly_event_source_client, driven
via the existing emitEvent / emitError API plus the new isClosed
observer. Net: ~50 lines of duplicated stub code removed across
three test files.
@kinyoklion kinyoklion force-pushed the rlamb/sdk-2185/fdv2-streaming branch from 96f16d0 to 24264af Compare May 7, 2026 22:17
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