Skip to content

refactor(remote-config): add RemoteConfigManager read facade (topic/body)#3703

Merged
tonidero merged 10 commits into
mainfrom
phase_6_read_facade_topic_body
Jul 3, 2026
Merged

refactor(remote-config): add RemoteConfigManager read facade (topic/body)#3703
tonidero merged 10 commits into
mainfrom
phase_6_read_facade_topic_body

Conversation

@tonidero

@tonidero tonidero commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Adds a read API to RemoteConfigManager so consumers can read remote-config data:

  • topic(name) — a topic's committed metadata.
  • blobData(topicName, itemKey) — a resolved item payload (its inline content, or its blob).

On a cache miss, a read resolves from an in-progress or on-demand sync instead of returning empty.


Note

Medium Risk
New suspend read paths can block on network and trigger on-demand /v1/config syncs; behavior is complex but heavily tested and gated on identity/disable state.

Overview
Adds a suspend read API on RemoteConfigManager: topic(RemoteConfigTopic) for committed item metadata, and blobData (transform + JSON reified) for blob-backed payloads. Reads run on IO, respect the session 4xx kill-switch (isUnavailableisDisabled), and on a cache miss wait for an in-flight sync or trigger a foreground on-demand refresh when an app user ID is available (wired from PurchasesFactory via appUserIDProvider).

Introduces RemoteConfigTopic and threads it through RemoteConfigTopicStore / source parsing. Renames config item inline fields contentmetadata and documents that payloads without blob_ref are index-only (blobData returns null).

syncAttributesAndOfferingsIfNeeded now calls refreshRemoteConfig before fetching offerings. Refresh completion is signaled with CompletableDeferred so blocked readers unblock on 204, errors, clearCache, and close. Unit, integration, and production golden tests cover the facade and waiting behavior.

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

@tonidero tonidero changed the title feat(remote-config): add RemoteConfigManager read facade (topic/body) refactor(remote-config): add RemoteConfigManager read facade (topic/body) Jul 2, 2026
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.87097% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.59%. Comparing base (3183def) to head (e3f74fb).

Files with missing lines Patch % Lines
...rchases/common/remoteconfig/RemoteConfigManager.kt 84.31% 3 Missing and 5 partials ⚠️
.../com/revenuecat/purchases/PurchasesOrchestrator.kt 0.00% 0 Missing and 1 partial ⚠️
.../common/remoteconfig/RemoteConfigSourceProvider.kt 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3703      +/-   ##
==========================================
+ Coverage   80.57%   80.59%   +0.02%     
==========================================
  Files         395      396       +1     
  Lines       16316    16366      +50     
  Branches     2322     2337      +15     
==========================================
+ Hits        13146    13190      +44     
- Misses       2243     2245       +2     
- Partials      927      931       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tonidero tonidero marked this pull request as ready for review July 2, 2026 15:41
@tonidero tonidero requested a review from a team as a code owner July 2, 2026 15:41
@tonidero tonidero force-pushed the phase_6_read_facade_topic_body branch from 57d60c2 to 9d78258 Compare July 3, 2026 07:47
@tonidero tonidero force-pushed the disable_remote_config_on_4xx branch from 5659295 to 8cfe71a Compare July 3, 2026 07:47

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

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 9d78258. Configure here.

Base automatically changed from disable_remote_config_on_4xx to main July 3, 2026 09:13
@tonidero tonidero force-pushed the phase_6_read_facade_topic_body branch from 9d78258 to 0932eb0 Compare July 3, 2026 09:39
tonidero and others added 8 commits July 3, 2026 13:03
Adds suspend topic()/body() reads to RemoteConfigManager. On a cache
miss, body() waits for an in-flight sync or triggers one on demand,
unless the endpoint is disabled or no app user is known.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
topic()/body() and RemoteConfigTopicStore now take a closed
RemoteConfigTopic (Workflows/UiConfig/Sources) instead of a raw topic
string, so consumers can only read known topics. The disk cache stays
string-keyed; the enum maps to its wire name at the store boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two integration tests that read back through RemoteConfigManager's
public topic()/body() facade after a sync persists to disk:

- RemoteConfigManagerIntegrationTest: real disk cache + blob store, mocked
  backend; reads an inline-only item's topic and body off disk.
- ProductionRemoteConfigIntegrationTest: real backend; a cold body() drives
  a genuine /v1/config fetch + persist, then reads an inlined workflows
  blob back off disk through the facade.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gate reads when disabled

Refines the read facade on the RemoteConfigManager:

- Rename `body(topic, itemKey)` to `blobData`, with two overloads: a
  reified `blobData<T>` that parses the resolved blob as JSON into a
  `@Serializable` T via the shared JsonProvider, and a `blobData(..., transform)`
  overload for non-JSON blobs the caller decodes itself.
- Drop the inline-content fallback: an item with no `blob_ref` now resolves
  to `null` (its inline data is exposed only through `topic()`), instead of
  returning the config JSON as a payload.
- Rename `RemoteConfiguration.ConfigItem.content` to `metadata`.
- Rename the session kill-switch `isUnavailable`/`unavailable` to
  `isDisabled`/`disabled`, and gate reads: `topic()` and `blobData()` return
  `null` once the endpoint is disabled (4xx), rather than serving cached data.
- Refresh remote config as part of syncAttributesAndOfferingsIfNeeded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…reads

- PurchasesCommonTest: syncAttributesAndOfferingsIfNeeded triggers
  RemoteConfigManager.refreshRemoteConfig via the sync completion.
- RemoteConfigManagerTest: reified blobData<T> fetches on demand then
  deserializes, matching the transform overload's wait/download path.
- ProductionRemoteConfigIntegrationTest: end-to-end typed blobData<T> read of
  a workflows blob. Uses a minimal test-only type rather than PublishedWorkflow,
  which the shared defaultJson can't decode (polymorphic component tree) and
  which these lazy paywall workflows omit ui_config for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make RemoteConfigManager.topic() mirror blobData(): when no topic is
committed yet, wait for an in-flight refresh (or trigger one on demand)
and re-read before returning null, so a topic read during the initial
sync returns fresh data instead of a spurious empty result. A committed
topic still returns immediately, and a disabled endpoint or unknown user
returns null with no network call.

Simplify the integration tests to read topic() directly (the cold-load
priming step is now redundant) and add unit coverage mirroring the
blobData self-prime suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@rickvdl rickvdl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Awesome work! Couple of notes / questions

suspend inline fun <reified T> blobData(topic: RemoteConfigTopic, itemKey: String): T? =
blobData(topic, itemKey) { bytes ->
try {
JsonProvider.defaultJson.decodeFromString<T>(bytes.decodeToString())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this is what you meant over slack; but decoding errors are now also returned as null (which is expected in it's current state), but indeed maybe some typed errors would be nice

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.

Right, in the end I guess it depends on the consumers and whether and how we want to expose this later on. We already log these internally, so a consumer of this API right now can only know that something went wrong, but not what... So yeah, adding a better response might make more sense, but might leave that for a separate PR and also see if consumers do actually need that info.

Comment thread purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesFactory.kt Outdated

@rickvdl rickvdl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Awesome work! Almost there 💪

@tonidero tonidero added this pull request to the merge queue Jul 3, 2026
Merged via the queue into main with commit 2710b6f Jul 3, 2026
37 checks passed
@tonidero tonidero deleted the phase_6_read_facade_topic_body branch July 3, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants