Repository navigation
feat: Add the override store, overlay, and data system wiring - #2051
kinyoklion wants to merge 9 commits into
Conversation
9e22472 to
e6ad1d1
Compare
9bc7846 to
0c00688
Compare
e6ad1d1 to
30eb906
Compare
0c00688 to
e42e2cb
Compare
30eb906 to
11aaea9
Compare
e42e2cb to
8e9fd05
Compare
|
@launchdarkly/js-client-sdk-common size report |
|
@launchdarkly/js-sdk-common size report |
|
@launchdarkly/js-client-sdk size report |
11aaea9 to
91d45a2
Compare
8e9fd05 to
71a1426
Compare
91d45a2 to
7874143
Compare
71a1426 to
a9e83b1
Compare
7874143 to
4ecf628
Compare
a9e83b1 to
a5614f5
Compare
4ecf628 to
2be4e36
Compare
a5614f5 to
d0ed461
Compare
2be4e36 to
fa5493d
Compare
d0ed461 to
667c330
Compare
fa5493d to
f8012e2
Compare
667c330 to
431c72b
Compare
f8012e2 to
5605b28
Compare
431c72b to
980675a
Compare
5605b28 to
f5a88e9
Compare
14bcbd6 to
cb45af0
Compare
62c63e6 to
b21f974
Compare
cb45af0 to
9b941d7
Compare
b21f974 to
525a8ba
Compare
9b941d7 to
50819bc
Compare
525a8ba to
6e47ed9
Compare
50819bc to
4550837
Compare
6e47ed9 to
e5a78d1
Compare
4550837 to
8650066
Compare
e5a78d1 to
f045d43
Compare
| * prepares LaunchDarkly data, and marks it. The source's object is never modified. | ||
| */ | ||
| function prepareFlag(item: LDKeyedFeatureStoreItem): LayerEntry { | ||
| const json = JSON.stringify(item); |
There was a problem hiding this comment.
nit: we should assess https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone for deep copy. I'm not too partial to it because there may be compatibility issues.
There was a problem hiding this comment.
I am not convinced this copy is required. So I will check that.
There was a problem hiding this comment.
Checked. The copy itself is needed only to keep the sink contract (the SDK never modifies the objects a source supplies; the other SDKs make the same promise and preparation here writes into nested data), but it was hand-rolled, and the serialized text it produced was also being used for change detection, which is not order-stable. Fixed in 7933b13: the layer now copies with the clone helper from js-sdk-common and stores only the prepared item, and the sink compares entries with fastDeepEqual, the same way the client flag manager computes changed keys. No serialized text is kept, so structuredClone is not needed. A test re-supplies a definition with every object's properties reversed and expects no notification.
Written by Claude Code on behalf of @kinyoklion.
8650066 to
405de3e
Compare
f045d43 to
d29754c
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ 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 d29754c. Configure here.
405de3e to
95c87a5
Compare
2533bdc to
754c0f6
Compare
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.
…load Every evaluation path, and the all-flags state, was chained onto the promise of the override source's start so that an override present at startup would apply from the first evaluation. Evaluation must not wait on I/O: a source whose start never settles, such as a read of a hung network file system or a custom source waiting on an unreachable endpoint, held every flag evaluation and every caller's promise forever, and close could not release them. The wait is removed. The initial load is part of starting the client, so the promise that waitForInitialization returns now resolves after the load as well, bounded by the caller's timeout, which is the asynchronous start operation the specification describes. An evaluation made before the load completes reads the layer as it is at that moment. A migration variation whose value is not a stage replaces the reason; the replacement now keeps the override-affected marking, as the typed variation methods do. Negative controls: the migration test fails when the marking is dropped from the replacement reason; the two waitForInitialization tests fail when the load is left out of the start handle; the evaluation tests fail against the previous gate.
…ation requires The SDK stores flag and segment definitions in their JSON form and has no typed deserialization, so a hand-written override definition could reach the evaluator with a field of the wrong type: a flag without variations, a target or clause without values, rules that are not an array. The evaluator then threw instead of returning a malformed-flag result, and the all flags state could never resolve. SDKs with typed deserialization reject such a definition when they read it. The override layer now checks each definition before it prepares and stores it. The checks cover the fields that evaluation and the preparation read with an assumed type: lists are arrays, list entries are objects, and attribute names that become attribute references are strings. Only a flag's variations and the values of a target or clause are required, because those are the fields evaluation reads without a guard. Every other field may be absent or null, as it may be in LaunchDarkly data. A definition that fails the check throws an Error that names the kind, the key, and the field, and the layer keeps its previous contents, so a snapshot with one bad entry applies nothing. Negative controls: each rejection test fails when its check is removed or disabled; the acceptance tests fail when the optional lists are made required, when null stops counting as absent, or when the string check is inverted; the snapshot and client tests fail when the layer builds the new snapshot in its live contents.
fb16ec6 to
dc6c2f8
Compare
… shared clone helper The override layer copied each supplied definition with an inline JSON round trip and kept the serialized text on the entry so the sink could tell which entries changed between snapshots. JSON.stringify is not canonical: the same definition supplied with its properties in another order, for example after a file was reformatted or by a source that builds objects in varying order, produced different text and a spurious flag change notification. The sink now compares the prepared entries with fastDeepEqual, which ignores property order, the same way the client SDK's flag manager computes changed keys. The layer no longer stores text: an entry is the prepared, marked copy itself, made with the clone helper from js-sdk-common instead of a hand-rolled round trip. A new test supplies a definition again with every object's properties reversed and expects no notification.
…ent marking The override indicator helper that the evaluator kept private now lives with the other marker functions, and the client uses it for the two wrong-type error paths instead of re-implementing the spread. The sink's merged view is the existing feature store storage type. The definition shape checks use the common validators for the plain-object and nullish tests and export the plain-object check for the document parser. The tests use the common sleep helper and one shared jest mock logger.
…ent, and harden the layer against odd keys and fields Before initialization, an evaluation asked the data store whether it was initialized before it looked in the override layer. With a persistent store that check is I/O, so an override could wait on a store that was slow or unreachable, in the incident the overrides exist for. The layer is now consulted first, and the source is created before the store so an invalid override configuration fails construction before a store connects. The start handle that waitForInitialization returns now settles after the override source's initial load whether initialization succeeded or failed, including when it had already failed, so an application that handles a failed start and evaluates sees the overrides present at startup. The layer's maps and the merged views no longer have a prototype, so a key such as "constructor" or "__proto__" is an ordinary entry and a key the layer does not hold is not found through Object.prototype. The sink treats an entry whose comparison throws, for a definition with a property named like a method of Object, as changed instead of dropping the snapshot's notifications. The shape check now requires a string key, requires the on switch to be a boolean when present, and rejects the fields the SDK sets when it prepares a definition (the generated target sets and the compiled bucket-by reference), which evaluation would otherwise read with the wrong type.

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
overridesproperty 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 publicLDOverrideSourceandLDOverrideSinkinterfaces 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, and before it asks the data store anything: with a persistent store the initialization check is I/O, and an overridden flag must not wait on a store that is slow or unreachable. 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. Evaluation never waits for the source: an evaluation reads the override store as it is at that moment. The source's initial load is part of starting the client, so the promise that
waitForInitializationreturns settles after the load as well, whether initialization succeeded or failed, bounded by the caller's timeout; for the file-based source that is a local read that completes within milliseconds of construction. A failed start of the source is logged and the client runs without overrides. The source is created before the data store, so an invalid override configuration fails construction before a persistent store has connected.The override layer checks the shape of every definition before it stores it. The SDK keeps definitions in their JSON form, so a hand-written definition with a field of the wrong type (a flag without
variations, a target or clause withoutvalues,rulesthat are not an array, anonswitch that is a string) would otherwise make evaluation throw instead of returning a malformed-flag result. The fields the SDK itself sets when it prepares a definition, such as the sets it generates for large target lists, cannot be supplied. A definition that fails the check is rejected with an error that names the kind, the key, and the field, and no part of that snapshot is applied: the layer keeps its previous contents. Only the key,variations, andvaluesare required; every other field may be absent, as in LaunchDarkly data, so LaunchDarkly definitions, value-only overrides, and TestData flags all pass. The store's maps have no prototype, so a key such asconstructoror__proto__is an ordinary key.The all-flags state keeps override-affected flags with their values, versions, and marked reasons, and presents them with
trackEventsandtrackReasonfalse and nodebugEventsUntilDate. 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/segment override layer to the FDv2 server SDK via
dataSystem.overrides, wired through publicLDOverrideSource/LDOverrideSinkAPIs (source or factory).Override snapshots replace an in-memory
OverrideLayerand are read through aReadStoreOverlayso per-key overrides win over LaunchDarkly data without changing init or data-source status.LDClientImplserves overridden flags before the not-ready short-circuit (including when the persistent store is slow), marks evaluations withoverrideAffected, adjustsallFlagsState(override-only when uninitialized, strips tracking for affected flags), fans change notifications throughOverrideSink, and makeswaitForInitializationwait for the source’s initial load while evaluations do not.Definitions are validated and prepared atomically per snapshot; malformed snapshots are rejected and the previous layer is kept. OVERRIDE spec vectors and broad unit/integration tests cover precedence, dependencies, lifecycle, and configuration edge cases.
Reviewed by Cursor Bugbot for commit 27aa6a8. Bugbot is set up for automated code reviews on this repo. Configure here.