Skip to content

feat: Add the override store, overlay, and data system wiring - #2051

Draft
kinyoklion wants to merge 4 commits into
rlamb/overrides-node-evaluator-markingfrom
rlamb/overrides-node-override-layer
Draft

kinyoklion wants to merge 4 commits into
rlamb/overrides-node-evaluator-markingfrom
rlamb/overrides-node-override-layer

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR is based on the evaluator marking branch because it uses the marker and the marking that branch adds.

This change adds the override layer described by the OVERRIDE specification to the FDv2 data system of the Node.js server SDK.

An override source is configured with the new overrides property of the data system options, which accepts a source object or a factory function. A source is not a data source: it takes no part in the initializer and synchronizer pipeline and has no effect on initialization status, data availability, or data source status. The SDK starts it when the client is created, before the data source, passes it a sink, and closes it with the client. In offline mode the source is not started. The public LDOverrideSource and LDOverrideSink interfaces let an application supply a custom source. A configuration that is not a source fails client construction, the way an unsupported data source configuration does.

The source supplies complete snapshots to the override store. Each snapshot replaces the whole store in one assignment, so the store holds exactly one snapshot at any instant. The store holds prepared, marked copies of the definitions and never modifies the objects the source supplied. An overlay at the store read boundary returns the override entry for a key in preference to LaunchDarkly data, and an enumeration includes every flag the store holds. Evaluation, prerequisite and segment resolution, and the all-flags read all go through the overlay.

The client consults the override store before the not-initialized short circuit. An overridden flag is served before the client has LaunchDarkly data, while a flag the store does not hold still gets the not-ready default. Because the client is created synchronously and a file read is not, the evaluation methods and allFlagsState wait for the source's initial load to complete, so an override present at startup takes effect from the first evaluation. A failed start is logged and the client runs without overrides.

The all-flags state keeps override-affected flags with their values, versions, and marked reasons, and presents them with trackEvents and trackReason false and no debugEventsUntilDate. While the client is not initialized and the store holds entries, the state is valid and holds only those flags. A type mismatch keeps the marking of the evaluation it replaces. Adding, changing, or removing an override fires the normal flag change notifications, including for the flags that depend on a changed flag or segment.

The OVERRIDE specification's test vectors are vendored and run as a unit test through the full client stack, asserting the value, the variation index, and the reason.

SDK-3247


Note

Overview
Adds an experimental flag override layer to the FDv2 data system via dataSystem.overrides, wired through new LDOverrideSource / LDOverrideSink APIs.

An override source pushes full snapshots into an OverrideLayer; reads go through a ReadStoreOverlay so per-key overrides win over LaunchDarkly data without affecting client initialization or data-source status. LDClientImpl starts the source before the data source (skipped in offline mode), waits on async initial load before variation* / allFlagsState, and serves overridden flags even when the client is not initialized.

Override-affected evaluations get overrideAffected on reasons; allFlagsState strips per-flag tracking fields for those flags while keeping values/reasons. OverrideSink fans out flag-change notifications using dependency tracking (prerequisites and segments). OVERRIDE spec vectors are vendored and exercised through the full client stack.

Also exports the override types from the public API and validates override configuration in Configuration (invalid types warn and drop; malformed sources throw at construction).

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

@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from 9e22472 to e6ad1d1 Compare September 28, 2026 20:28
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from 9bc7846 to 0c00688 Compare September 28, 2026 20:28
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from e6ad1d1 to 30eb906 Compare September 28, 2026 21:20
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from 0c00688 to e42e2cb Compare September 28, 2026 21:20
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from 30eb906 to 11aaea9 Compare September 29, 2026 15:20
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from e42e2cb to 8e9fd05 Compare September 29, 2026 15:20
@kinyoklion
kinyoklion marked this pull request as ready for review September 30, 2026 17:07
@kinyoklion
kinyoklion requested a review from a team as a code owner September 30, 2026 17:07
@kinyoklion
kinyoklion added this pull request to stack #2061 September 30, 2026 17:08
@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

@github-actions

Copy link
Copy Markdown
Contributor

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 27065 bytes
Compressed size limit: 29000
Uncompressed size: 131676 bytes

@kinyoklion
kinyoklion marked this pull request as draft September 30, 2026 17:10
@github-actions

Copy link
Copy Markdown
Contributor

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

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/shared/sdk-server/src/LDClientImpl.ts
Comment thread packages/shared/sdk-server/src/LDClientImpl.ts Outdated
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from 11aaea9 to 91d45a2 Compare September 30, 2026 18:17
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from 8e9fd05 to 71a1426 Compare September 30, 2026 18:17
@kinyoklion
kinyoklion removed this pull request from stack #2061 September 30, 2026 18:37
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from 91d45a2 to 7874143 Compare September 30, 2026 20:16
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from 71a1426 to a9e83b1 Compare September 30, 2026 20:16
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from 7874143 to 4ecf628 Compare September 30, 2026 20:23
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from a9e83b1 to a5614f5 Compare September 30, 2026 20:23
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from 4ecf628 to 2be4e36 Compare September 30, 2026 21:01
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from a5614f5 to d0ed461 Compare September 30, 2026 21:01
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from 2be4e36 to fa5493d Compare September 30, 2026 21:04
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from d0ed461 to 667c330 Compare September 30, 2026 21:04
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from fa5493d to f8012e2 Compare September 30, 2026 22:22
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from 667c330 to 431c72b Compare September 30, 2026 22:23
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from f8012e2 to 5605b28 Compare September 30, 2026 22:27
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from 431c72b to 980675a Compare September 30, 2026 22:27
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from 5605b28 to f5a88e9 Compare October 1, 2026 23:07
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from 980675a to d3694fb Compare October 1, 2026 23:07
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from f5a88e9 to 08abea8 Compare October 1, 2026 23:52
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from d3694fb to d1d703d Compare October 1, 2026 23:52
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch 2 times, most recently from 5605b28 to 08abea8 Compare October 2, 2026 16:21
Adds the override layer described by the OVERRIDE specification to the FDv2
data system of the server SDK.

An override source is configured with the new overrides option of the data
system options. It is not a data source: it takes no part in the initializer
and synchronizer pipeline and has no effect on initialization status. The SDK
starts it when the client is created, passes it a sink, and closes it with the
client. The public LDOverrideSource and LDOverrideSink interfaces let an
application supply a custom source.

The source supplies complete snapshots to the override store. Each snapshot
replaces the whole store in one assignment. The store holds prepared, marked
copies of the definitions and never modifies the objects the source supplied.
An overlay at the store read boundary returns the override entry for a key in
preference to LaunchDarkly data. Evaluation, prerequisite and segment
resolution, and the all-flags read go through the overlay.

The client consults the override store before the not-initialized short
circuit, so an overridden flag is served before the client has LaunchDarkly
data while a non-overridden flag still gets the not-ready default. Evaluations
wait for the source's asynchronous initial load, so an override present at
startup takes effect from the first evaluation. The all-flags state keeps
override-affected flags with their values and marked reasons but turns their
event tracking fields off. A type mismatch keeps the marking of the evaluation
it replaces. Adding, changing, or removing an override fires the normal flag
change notifications, including for the flags that depend on a changed entry.

The OVERRIDE specification's test vectors run as a unit test through the full
client stack.
…the override layer

A throwing override source close is logged and the rest of the shutdown continues. Before initialization, the all flags state uses the override layer only when it holds at least one flag.
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-evaluator-marking branch from 08abea8 to 4b7069e Compare October 3, 2026 00:40
@kinyoklion
kinyoklion force-pushed the rlamb/overrides-node-override-layer branch from 557ef18 to 4fc2262 Compare October 3, 2026 00:40
@kinyoklion

Copy link
Copy Markdown
Member Author

bugbot review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4fc2262. Configure here.

this._logger?.debug(`Override update affected ${flagKeys.length} flag(s)`);
}
flagKeys.forEach((key) => this._onChange(key));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Override dependents miss LD change notifications

Medium Severity

OverrideSink fans change notifications through the merged view only when the override layer itself is replaced. LaunchDarkly upserts still go through TransactionalDataSourceUpdates on the raw feature store, whose dependency graph never includes override definitions. A flag or segment that exists only in the overlay, or whose override edges differ from LaunchDarkly data, is not notified when the LaunchDarkly prerequisite or segment it reads actually changes, so listeners can keep a stale evaluation.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4fc2262. Configure here.

Comment thread packages/shared/sdk-server/src/options/Configuration.ts

This branch has not been deployed

No deployments
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