Skip to content

feat: add a reusable retry state controller for RETRY-conformant backoff - #2045

Merged
tanderson-ld merged 12 commits into
mainfrom
ta/SDK-2790/retry-state-controller
Sep 28, 2026
Merged

tanderson-ld merged 12 commits into
mainfrom
ta/SDK-2790/retry-state-controller

Conversation

@tanderson-ld

@tanderson-ld tanderson-ld commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a reusable retry controller to @launchdarkly/js-sdk-common that owns retry tracking, health checking, and delay calculation for long-running components. This is the foundation for RETRY-spec conformance in the Node server SDK (SDK-2790); the data-source wiring that consumes it arrives in a follow-up PR. There are no consumers in this PR — the additions are unused by product code and exercised only by tests.

The design ports the controller pattern from the Python server SDK (python-server-sdk#522, wired in #519), with deliberate differences noted below.

What's where

  • src/datasource/retry/ — the retry mechanics: the RetryState interface and its createRetryState factory (the controller), the ResetPolicy interface with its two implementations (AfterHealthyFor for streaming's healthy-duration reset, AfterConsecutiveSuccesses for polling's two-in-a-row reset), and the forStreaming/forPolling factories that bind the standard values (1s→30s normal and 5min→1hr extended regimes; 60s healthy window; two-success polling reset) with warn-and-default validation of the configured delay.
  • src/errors.ts — failure classification, placed beside the legacy helper it supersedes: FailureKind ('normal' | 'unexpected'), classifyHttpStatus (400/408/429 and 5xx and non-error statuses are normal; any other 4xx is unexpected), and classifyTransportFailure (always normal — in an all-HTTPS system certificate failures can't be reliably distinguished from transient faults). isHttpRecoverable now delegates to classifyHttpStatus — one table, no drift — and is documented as superseded; it stays undeprecated because the event-delivery pathway still legitimately consumes it until that pathway migrates.

Design points for review

  • RetryState is an interface, not a class. createRetryState(config) returns it, backed by a closure over local state; forStreaming/forPolling return the interface. This keeps the publicly exposed surface an interface (per the repo's prefer-interfaces guideline) and lets future mutators be added additively. The ResetPolicy implementations stay classes, since the ResetPolicy interface already fronts them everywhere they are consumed.
  • Clockless seams. No method of RetryState or ResetPolicy takes a timestamp. Time lives in exactly one place: AfterHealthyFor's constructor-injected clock, defaulting to a monotonic source (performance.now(), with a Date.now closure fallback for exotic runtimes). The controller itself holds no clock; its only injectable is random, for deterministic jitter tests. (The parameter is named clock rather than the codebase's timeStamper deliberately — it is not a timestamp source.)
  • Ceiling-bounded backoff, no exponent constant. The delay computation compares the base against the ceiling scaled down (base >= max / 2**exponent) rather than scaling the base up, so nothing can overflow the ceiling — the same compare-before-shift structure as the .NET implementation, expressed in lossless power-of-two float math. A zero base (legal: the spec forbids flooring server-directed retry values) short-circuits, closing a 0 × Infinity = NaN edge otherwise reachable when a zero-valued server-directed retry is followed by very many failures.
  • A configured delay above the normal ceiling clamps to it. In the normal regime the ceiling wins, matching the literal spec (1.3.2 + 1.4.2) and the majority of the SDK fleet (Go, Java, .NET). The extended regime is the opposite: its bounds are floored at the configured delay, which spec requirement 1.5.4.1 mandates ("a delay or ceiling that applies after an unexpected failure MUST NOT be less than the component's initial delay").
  • applyServerDirectedRetry(ms) — the SSE retry: entry point: sticky base that takes precedence over the regime's initial delay (including the extended regime's), doubling restarted, ceiling still applies, survives a healthy reset. Wire-level validation and the 1-hour cap live in the SSE library (launchdarkly/js-eventsource#40), not here.
  • Post-success wait = operating cadence, even while the retry state is raised — a recovering poller returns to schedule immediately rather than serving one more extended-regime wait.

Testing

89 tests across three suites (584 package-wide, all green):

  • RetryState.test.ts (48) — exact delay ladders for both regimes under injected clock/random (including the 5m/10m/20m/40m/1h/1h extended ladder), regime transition/ratchet/re-arm, anchor-once healthy-stretch discrimination, reset-before-count ordering, fast-second-poll cadence, poll-interval wait floor under real jitter, jitter range with the maximal-draw boundary (the exact-T/2 tie) and distinctness assertions, server-directed retry semantics (replace/clamp/persist/reject-invalid/later-wins, precedence over both regimes' initial delays, retry: 0 staying finite through 1,100 failures), normal-ceiling clamp and regime-collapse cases, the extended-floor mandate under direct construction, flapping-never-ratchets, high-n robustness, and factory validation matrices.
  • ResetPolicy.test.ts (8) — the policy seam directly: threshold boundary, anchor-once under repeated healthy reports, failure-clears, consecutive-success counting, and both default-clock closures (monotonic and the Date.now fallback).
  • errors.test.ts (33) — the full classification matrix including boundary and non-error statuses, transport classification, and parity pins on the legacy isHttpRecoverable truth table through the delegation.

The src/datasource/retry module is at 100% statement, branch, function, and line coverage. Coverage was cross-checked against the Python, Java, Go, and .NET RETRY test suites; the one technique deliberately not ported is .NET's BigInteger/randomized reference sweeps, which exist to exercise 64-bit integer shift surfaces that JS float arithmetic does not have.


Note

Overview
Introduces a RETRY-spec-oriented retry controller in @launchdarkly/js-sdk-common as shared library code only—no data sources or SDKs call it yet; follow-up PRs will wire streaming/polling.

Adds src/datasource/retry/ with RetryState (createRetryState, forStreaming, forPolling): exponential backoff with jitter, normal vs extended regimes after unexpected failures, pluggable ResetPolicy (healthy-for duration for streaming, consecutive successes for polling), and applyServerDirectedRetry for SSE retry: values. errors.ts gains FailureKind plus classifyHttpStatus / classifyTransportFailure; isHttpRecoverable now delegates to the same rules.

New retry APIs are re-exported from the datasource barrel and package index. CI package size limit for common ESM rises 29 000 → 29 500 bytes. Coverage is 89 new tests across retry and error classification.

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

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 29283 bytes
Compressed size limit: 29500
Uncompressed size: 141049 bytes

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 32636 bytes
Compressed size limit: 34000
Uncompressed size: 116992 bytes

@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 25437 bytes
Compressed size limit: 44000
Uncompressed size: 165420 bytes

@tanderson-ld
tanderson-ld marked this pull request as ready for review September 25, 2026 18:33
@tanderson-ld
tanderson-ld requested a review from a team as a code owner September 25, 2026 18:33
Comment thread packages/shared/common/src/datasource/retry/RetryState.ts Outdated
Export the retry controller (RetryState, factories, and reset policies)
from the package entry point so consumers can reach it. Clamp the normal
regime at its ceiling when a configured initial delay exceeds it, matching
the majority of the SDK fleet and the literal spec; the mandated extended
floor stays. Document the config trust boundary, the server-directed hint
precedence and ceiling bound, and the read-in-same-turn contract, and pin
the hint-precedence and jitter-boundary behaviors and the reset-policy and
default-cadence paths with tests.

Part of SDK-2790.
Convert RetryState from an exported class to an interface plus a
closure-backed createRetryState factory, per the prefer-interfaces
guideline for publicly exposed types. State lives in closure locals, so
the returned object is minifiable without the private-field convention and
future mutators are additive. The forStreaming/forPolling factories return
the interface; the reset-policy classes are unchanged, since the ResetPolicy
interface already fronts them. No behavioral change.

Part of SDK-2790.
@tanderson-ld

Copy link
Copy Markdown
Contributor Author

Need to discuss file size impact with @joker23

Comment thread packages/shared/common/src/datasource/retry/ResetPolicy.ts Outdated
Comment thread packages/shared/common/src/datasource/retry/ResetPolicy.ts Outdated
@tanderson-ld
tanderson-ld merged commit 721b559 into main Sep 28, 2026
58 checks passed
@tanderson-ld
tanderson-ld deleted the ta/SDK-2790/retry-state-controller branch September 28, 2026 20:13
@github-actions github-actions Bot mentioned this pull request Sep 28, 2026
joker23 pushed a commit that referenced this pull request Sep 29, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>akamai-edgeworker-sdk-common: 2.0.37</summary>

##
[2.0.37](akamai-edgeworker-sdk-common-v2.0.36...akamai-edgeworker-sdk-common-v2.0.37)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-server-sdk-common bumped from ^2.21.5 to ^2.21.6
</details>

<details><summary>akamai-server-base-sdk: 3.0.38</summary>

##
[3.0.38](akamai-server-base-sdk-v3.0.37...akamai-server-base-sdk-v3.0.38)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
* @launchdarkly/akamai-edgeworker-sdk-common bumped from ^2.0.36 to
^2.0.37
</details>

<details><summary>akamai-server-edgekv-sdk: 1.4.40</summary>

##
[1.4.40](akamai-server-edgekv-sdk-v1.4.39...akamai-server-edgekv-sdk-v1.4.40)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
* @launchdarkly/akamai-edgeworker-sdk-common bumped from ^2.0.36 to
^2.0.37
    * @launchdarkly/js-server-sdk-common bumped from ^2.21.5 to ^2.21.6
</details>

<details><summary>client-testing-plugin: 1.0.19</summary>

##
[1.0.19](client-testing-plugin-v1.0.18...client-testing-plugin-v1.0.19)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk-common bumped from 1.32.1 to 1.32.2
  * devDependencies
    * @launchdarkly/js-client-sdk bumped from 4.10.3 to 4.10.4
    * @launchdarkly/react-sdk bumped from 4.1.20 to 4.1.21
  * peerDependencies
    * @launchdarkly/js-client-sdk bumped from ^4.9.1 to ^4.10.4
    * @launchdarkly/react-sdk bumped from ^4.1.4 to ^4.1.21
</details>

<details><summary>cloudflare-server-sdk: 2.7.39</summary>

##
[2.7.39](cloudflare-server-sdk-v2.7.38...cloudflare-server-sdk-v2.7.39)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
* @launchdarkly/js-server-sdk-common-edge bumped from 2.6.34 to 2.6.35
</details>

<details><summary>fastly-server-sdk: 0.2.29</summary>

##
[0.2.29](fastly-server-sdk-v0.2.28...fastly-server-sdk-v0.2.29)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-server-sdk-common bumped from 2.21.5 to 2.21.6
</details>

<details><summary>jest: 1.0.31</summary>

##
[1.0.31](jest-v1.0.30...jest-v1.0.31)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
* @launchdarkly/react-native-client-sdk bumped from ~10.20.3 to ~10.20.4
</details>

<details><summary>js-client-sdk: 4.10.4</summary>

##
[4.10.4](js-client-sdk-v4.10.3...js-client-sdk-v4.10.4)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk-common bumped from 1.32.1 to 1.32.2
</details>

<details><summary>js-client-sdk-common: 1.32.2</summary>

##
[1.32.2](js-client-sdk-common-v1.32.1...js-client-sdk-common-v1.32.2)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-sdk-common bumped from 2.26.1 to 2.27.0
</details>

<details><summary>js-sdk-common: 2.27.0</summary>

##
[2.27.0](js-sdk-common-v2.26.1...js-sdk-common-v2.27.0)
(2026-09-29)


### Features

* add a reusable retry state controller for RETRY-conformant backoff
([#2045](#2045))
([721b559](721b559))


### Bug Fixes

* Stop format() from hanging on a trailing percent sign
([#2056](#2056))
([c5114ec](c5114ec))
</details>

<details><summary>js-server-sdk-common: 2.21.6</summary>

##
[2.21.6](js-server-sdk-common-v2.21.5...js-server-sdk-common-v2.21.6)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-sdk-common bumped from 2.26.1 to 2.27.0
</details>

<details><summary>js-server-sdk-common-edge: 2.6.35</summary>

##
[2.6.35](js-server-sdk-common-edge-v2.6.34...js-server-sdk-common-edge-v2.6.35)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-server-sdk-common bumped from 2.21.5 to 2.21.6
</details>

<details><summary>node-client-sdk: 4.1.4</summary>

##
[4.1.4](node-client-sdk-v4.1.3...node-client-sdk-v4.1.4)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk-common bumped from 1.32.1 to 1.32.2
</details>

<details><summary>node-server-sdk: 9.13.8</summary>

##
[9.13.8](node-server-sdk-v9.13.7...node-server-sdk-v9.13.8)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-server-sdk-common bumped from 2.21.5 to 2.21.6
</details>

<details><summary>node-server-sdk-dynamodb: 6.2.44</summary>

##
[6.2.44](node-server-sdk-dynamodb-v6.2.43...node-server-sdk-dynamodb-v6.2.44)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * devDependencies
    * @launchdarkly/node-server-sdk bumped from 9.13.7 to 9.13.8
  * peerDependencies
    * @launchdarkly/node-server-sdk bumped from >=9.11.3 to >=9.13.8
</details>

<details><summary>node-server-sdk-otel: 1.3.31</summary>

##
[1.3.31](node-server-sdk-otel-v1.3.30...node-server-sdk-otel-v1.3.31)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * devDependencies
    * @launchdarkly/node-server-sdk bumped from 9.13.7 to 9.13.8
  * peerDependencies
    * @launchdarkly/node-server-sdk bumped from >=9.11.3 to >=9.13.8
</details>

<details><summary>node-server-sdk-redis: 4.2.43</summary>

##
[4.2.43](node-server-sdk-redis-v4.2.42...node-server-sdk-redis-v4.2.43)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * devDependencies
    * @launchdarkly/node-server-sdk bumped from 9.13.7 to 9.13.8
  * peerDependencies
    * @launchdarkly/node-server-sdk bumped from >=9.11.3 to >=9.13.8
</details>

<details><summary>openfeature-cloudflare-server: 1.0.2</summary>

##
[1.0.2](openfeature-cloudflare-server-v1.0.1...openfeature-cloudflare-server-v1.0.2)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
* @launchdarkly/openfeature-js-server-common bumped from 2.0.2 to 2.0.3
  * devDependencies
    * @launchdarkly/cloudflare-server-sdk bumped from 2.7.38 to 2.7.39
  * peerDependencies
    * @launchdarkly/cloudflare-server-sdk bumped from ^2.7.0 to ^2.7.39
</details>

<details><summary>openfeature-js-server-common: 2.0.3</summary>

##
[2.0.3](openfeature-js-server-common-v2.0.2...openfeature-js-server-common-v2.0.3)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-sdk-common bumped from 2.26.1 to 2.27.0
</details>

<details><summary>openfeature-node-server: 2.0.7</summary>

##
[2.0.7](openfeature-node-server-v2.0.6...openfeature-node-server-v2.0.7)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
* @launchdarkly/openfeature-js-server-common bumped from 2.0.2 to 2.0.3
  * devDependencies
    * @launchdarkly/node-server-sdk bumped from 9.13.7 to 9.13.8
  * peerDependencies
    * @launchdarkly/node-server-sdk bumped from ^9.0.0 to ^9.13.8
</details>

<details><summary>react-native-client-sdk: 10.20.4</summary>

##
[10.20.4](react-native-client-sdk-v10.20.3...react-native-client-sdk-v10.20.4)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk-common bumped from 1.32.1 to 1.32.2
</details>

<details><summary>react-sdk: 4.1.21</summary>

##
[4.1.21](react-sdk-v4.1.20...react-sdk-v4.1.21)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk bumped from ^4.10.3 to ^4.10.4
    * @launchdarkly/js-server-sdk-common bumped from ^2.21.5 to ^2.21.6
</details>

<details><summary>server-sdk-ai: 2.0.8</summary>

##
[2.0.8](server-sdk-ai-v2.0.7...server-sdk-ai-v2.0.8)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * devDependencies
    * @launchdarkly/js-server-sdk-common bumped from 2.21.5 to 2.21.6
  * peerDependencies
    * @launchdarkly/js-server-sdk-common bumped from ^2.0.0 to ^2.21.6
</details>

<details><summary>server-sdk-ai-langchain: 0.8.18</summary>

##
[0.8.18](server-sdk-ai-langchain-v0.8.17...server-sdk-ai-langchain-v0.8.18)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * devDependencies
    * @launchdarkly/server-sdk-ai bumped from ^2.0.7 to ^2.0.8
  * peerDependencies
    * @launchdarkly/server-sdk-ai bumped from ^1.1.1 to ^2.0.8
</details>

<details><summary>server-sdk-ai-openai: 0.7.18</summary>

##
[0.7.18](server-sdk-ai-openai-v0.7.17...server-sdk-ai-openai-v0.7.18)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * devDependencies
    * @launchdarkly/js-server-sdk-common bumped from 2.21.5 to 2.21.6
    * @launchdarkly/server-sdk-ai bumped from ^2.0.7 to ^2.0.8
  * peerDependencies
    * @launchdarkly/server-sdk-ai bumped from ^1.1.1 to ^2.0.8
</details>

<details><summary>server-sdk-ai-vercel: 0.7.18</summary>

##
[0.7.18](server-sdk-ai-vercel-v0.7.17...server-sdk-ai-vercel-v0.7.18)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * devDependencies
    * @launchdarkly/server-sdk-ai bumped from ^2.0.7 to ^2.0.8
  * peerDependencies
    * @launchdarkly/server-sdk-ai bumped from ^1.1.1 to ^2.0.8
</details>

<details><summary>shopify-oxygen-sdk: 0.1.27</summary>

##
[0.1.27](shopify-oxygen-sdk-v0.1.26...shopify-oxygen-sdk-v0.1.27)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-server-sdk-common bumped from 2.21.5 to 2.21.6
</details>

<details><summary>vercel-server-sdk: 1.3.62</summary>

##
[1.3.62](vercel-server-sdk-v1.3.61...vercel-server-sdk-v1.3.62)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
* @launchdarkly/js-server-sdk-common-edge bumped from 2.6.34 to 2.6.35
</details>

<details><summary>vue-client-sdk: 3.0.2</summary>

##
[3.0.2](vue-client-sdk-v3.0.1...vue-client-sdk-v3.0.2)
(2026-09-29)


### Dependencies

* The following workspace dependencies were updated
  * dependencies
    * @launchdarkly/js-client-sdk bumped from 4.10.3 to 4.10.4
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> This is a **Release Please** cut that ships
**`@launchdarkly/js-sdk-common` 2.27.0** and bumps dependent packages
across the monorepo (client, server, edge, AI, OpenFeature, stores, and
tooling).
> 
> The only substantive product changes ride on **2.27.0**: a new
**RETRY-conformant backoff retry state controller** in shared common,
and a fix so **`format()` no longer hangs** when a format string ends
with a lone `%`. Everything else in the diff is version
alignment—`package.json` / changelog / `.release-please-manifest.json`
entries, pinned example deps, and **`x-release-please-version`** SDK
metadata strings.
> 
> Notable dependency moves beyond the common bump include
**`@launchdarkly/node-server-sdk` 9.13.8**, tightened OpenFeature peer
floors (e.g. Node server **^9.13.8**, Cloudflare **^2.7.39**), and AI
provider releases that pull **`@launchdarkly/server-sdk-ai` ^2.0.8**.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
37b84a9. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
tanderson-ld added a commit that referenced this pull request Sep 30, 2026
…ec (#2059)

## Summary

Wires the reusable retry controller (added to
`@launchdarkly/js-sdk-common` in #2045) into the Node server SDK's
**FDv1 streaming and polling** data sources, bringing them into
conformance with the RETRY spec. Streaming injects the controller
through the js-eventsource `retryDelayStrategy` seam; polling drives the
controller from its own scheduled loop. This replaces the old
recoverable / permanent-stop model.

Scope is **FDv1 streaming + polling in the Node server SDK** only. FDv2
(the composite data source and its synchronizers) is intentionally
untouched and will be conformed in a future effort.

> **Draft / dependency note.** The streaming seam ships in an unreleased
`launchdarkly-eventsource`; the committed pin here stays at `2.2.0`, so
CI does **not** yet exercise the injected controller end-to-end (2.2.0
ignores the option and falls back to its built-in backoff). The pin bump
to the release carrying the seam, and the long-running contract-test
verification, are follow-ups — this PR is up for review of the wiring
and behavior.

## Behavior changes (operator-facing)

- **No data-source failure is terminal.** Previously-fatal HTTP statuses
(401, 403, and other 4xx) no longer permanently stop the data source;
they retry indefinitely at the extended-regime cadence (5 min → 1 hr).
Normal/transient failures retry at the fast cadence (1 s → 30 s,
streaming) as before.
- **A bad SDK key no longer fast-fails initialization.**
`waitForInitialization()` resolves on success and rejects only on its
own timeout or on `close()`; it no longer rejects immediately with
"Authentication failed…" on a 401. A misconfigured client keeps retrying
and stays uninitialized until the timeout.
- **Malformed/empty data is recovered from, not fatal.** Malformed
stream JSON / empty payloads record a normal failure and re-establish
the stream (self-initiated close + recreate after the backoff);
malformed poll JSON records a normal failure and polls again on
schedule.
- **Data-source failures surface as logs only.** Consistent with how the
SDK has always treated recoverable/interrupted failures, and with the
Go/Java/.NET fleet: a fixed "will retry" message (`warn` for normal,
`error`-level for unexpected/invalid-data), with the reconnect delay in
the streaming `onretrying` info line. The `'error'`/`'failed'` events
and init-rejection remain reserved for genuinely terminal conditions
(today, only the untouched FDv2 composite reaches them).
- **Polling cadence is completion-anchored** (effective period ≈
interval + poll duration), matching the fleet.

Versioning: `feat:` / **minor** for all affected packages — matching how
Go (7.16.0), Java (7.16.0), and .NET (8.17.0) each shipped RETRY
conformance (minor, behavior changes documented in the PR, not marked
breaking).

## What's here

- `packages/shared/common` — `EventSourceRetryDelayStrategy` type +
optional `retryDelayStrategy` on the platform `EventSourceInitDict` (the
injection seam; `initialRetryDelayMillis`/`retryResetIntervalMillis`
made optional since they're unused under injection).
- `packages/shared/sdk-server` — `StreamingProcessor` and
`PollingProcessor` rewired to the controller (classification,
always-retry, malformed-data recovery via a guarded close-and-recreate,
completion-anchored polling). `LDClientImpl` is intentionally left
unchanged; the reserved `'error'`/`'failed'`/init-reject path is
untouched.
- `packages/sdk/server-node` — `NodeRequests` passes the strategy
through; contract-test capabilities `retry-conformance-fdv1-streaming` /
`-polling`; a nightly workflow (`server-node-nightly.yaml`) that runs
the long-running RETRY suite (schedule + manual dispatch, no push
trigger).

## Testing

Unit suites green: `@launchdarkly/js-server-sdk-common` (1053 pass) and
`@launchdarkly/node-server-sdk` (70 pass), covering the classification
ladders (via the controller), the streaming close+recreate on malformed
data (including a guard against a duplicate reconnect and a
deserialization that throws), polling recovery,
`stop()`-cancels-a-pending-retry interruptibility, status-less
transport-error classification, single-failure accounting for a
self-initiated restart, the reserved terminal
`'error'`/`'failed'`/init-reject path, and the restored `onretrying`
delay log. The behavioral RETRY conformance itself is validated by the
harness in the nightly (long-running), which is a post-merge /
manual-dispatch activity.

Part of SDK-2790.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Brings **FDv1 streaming and polling** in the Node server SDK in line
with the **RETRY** spec by driving reconnect/poll timing from shared
`RetryState` instead of permanent vs recoverable stops.
> 
> **Streaming** injects that state through a new
`EventSourceRetryDelayStrategy` on the platform `EventSourceInitDict`
(optional `initialRetryDelayMillis` / `retryResetIntervalMillis`;
browser defaults when omitted). **Polling** schedules the next request
from the same controller with completion-anchored delays. **401/403,
transport errors, and bad JSON** are logged and retried (extended
cadence for “unexpected” HTTP); they no longer surface data-source
`'error'` events or stop the loop. Malformed stream payloads trigger a
guarded close-and-reconnect; poll deserialization failures keep polling.
> 
> Also bumps **`launchdarkly-eventsource` to 2.3.0**, preapproves it in
**`.yarnrc.yml`** for the npm age gate, advertises
**`retry-conformance-fdv1-*`** contract capabilities, and adds a
**nightly GitHub workflow** for the long-running RETRY harness
(`--enable-long-running-tests`).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
54ba5dd. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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