Skip to content

feat: Propagate store errors and the availability check through the persistence wrapper - #2010

Open
joker23 wants to merge 3 commits into
mainfrom
skz/sdk-3083/persistent-store-write-back-recovery-wrapper-plumbing
Open

joker23 wants to merge 3 commits into
mainfrom
skz/sdk-3083/persistent-store-write-back-recovery-wrapper-plumbing

Conversation

@joker23

@joker23 joker23 commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This is the wrapper-plumbing layer of the persistent store write-back recovery stack (follows #2009; the recovery engine itself lands in the next PR).

  • PersistentDataStoreWrapper now forwards the underlying store's init and upsert errors to its callbacks instead of dropping them. A failed mirrored write is the signal the transactional store will use to detect an outage.
  • The wrapper exposes isStoreAvailable only when the underlying PersistentDataStore implements it, so recovery logic can use the method's presence to decide whether it can poll for the store coming back.
  • InMemoryFeatureStore.getAllRaw() returns a snapshot of the full data set, including deletion tombstones, for writing the in-memory state back into a recovered persistent store.
  • The wrapper's init now populates the all-items cache with tombstones filtered out, matching the filtering the cache-miss path applies. Write-back init payloads can contain tombstones, and all() must never return them from the warm cache.

Note

Overview
Persistent store wrapper plumbing for write-back recovery: init and upsert now forward underlying store errors to their callbacks instead of swallowing them. On a failed init, the wrapper logs, clears item/all caches, and leaves _isInitialized false so reads and initialized() reflect persistence, not rejected in-memory data.

isStoreAvailable is exposed on the wrapper only when the core PersistentDataStore implements it, so recovery can detect poll support.

InMemoryFeatureStore.getAllRaw() adds an internal immutable snapshot of every kind, including deletion tombstones, for full-state write-back to a recovered store.

Cache fix on init: the warm all() cache is populated with tombstones filtered out, matching cache-miss behavior so all() never returns deleted items after init payloads that include tombstones.

Tests cover raw snapshots, cached all() with deletes, init/upsert error propagation, failed-init cache clearing, and isStoreAvailable forwarding.

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

@joker23
joker23 added this pull request to stack #2014 September 11, 2026 19:18
@joker23
joker23 force-pushed the skz/sdk-3083/persistent-store-write-back-recovery-wrapper-plumbing branch from 1a58c9d to 2ca7187 Compare September 17, 2026 16:40
@github-actions

github-actions Bot commented Sep 17, 2026 •

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

@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: 26983 bytes
Compressed size limit: 29000
Uncompressed size: 131377 bytes

@github-actions

github-actions Bot commented Sep 17, 2026 •

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

github-actions Bot commented Sep 17, 2026 •

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

Base automatically changed from skz/sdk-3083/persistent-store-write-back-recovery-contracts to main September 18, 2026 20:06
@joker23
joker23 force-pushed the skz/sdk-3083/persistent-store-write-back-recovery-wrapper-plumbing branch 2 times, most recently from 88698a2 to 6e8b65a Compare September 18, 2026 20:31
@joker23
joker23 marked this pull request as ready for review September 18, 2026 21:34
@joker23
joker23 requested a review from a team as a code owner September 18, 2026 21:34
devin-ai-integration[bot]

This comment was marked as resolved.

cursor[bot]

This comment was marked as resolved.

@joker23

joker23 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

@cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 10ebf5c. Configure here.

@joker23
joker23 force-pushed the skz/sdk-3083/persistent-store-write-back-recovery-wrapper-plumbing branch 2 times, most recently from 53f796d to bb5c1b8 Compare September 23, 2026 22:10
@joker23
joker23 force-pushed the skz/sdk-3083/persistent-store-write-back-recovery-wrapper-plumbing branch from bb5c1b8 to 8b35e1e Compare September 24, 2026 15:25

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +55 to +57
Object.entries(this._allData).forEach(([namespace, items]) => {
snapshot[namespace] = { ...items };
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Recovery snapshot changes during serialization

When recovery serializes a segment, getAllRaw() shares its item object with the live store. replacer mutates that object, so the snapshot changes after capture.

Learn more

A raw snapshot copies each namespace map, but it retains references to the original flags and segments. Serializing a segment with generated target sets mutates its object in replacer. That changes both the supposedly fixed snapshot and the live in-memory store when the snapshot is passed to a persistence write. The same alias also lets any later in-place mutation of a stored item change the captured data.

Example: A segment has generated_includedSet = new Set(['alice']). After getAllRaw(), serializing that snapshot adds included: ['alice'] and deletes generated_includedSet on both the snapshot and the live segment; the captured object no longer has its original fields.

Recommended fix: Copy the item objects and nested mutable data when taking the snapshot, preserving Set values and other internal data types used by evaluation. Verify that passing the snapshot through sortDataSet does not mutate the live store or the captured data.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This does not affect correctness AFAIK

@joker23
joker23 requested a review from kinyoklion September 24, 2026 16:57

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.

2 participants