mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
a37eca4c8efb04bbbfa154341b6f0c8cc4bd25c3
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8925524293 |
feat(remote-config): health-check-gated API source failover in HTTPClient (#7294)
* feat(remote-config): health-check-gated API source failover in HTTPClient Wires the API source failover into the request pipeline, still behind the usesRemoteConfigAPISources dangerous setting (internal and defaulting to false, so released behavior is unchanged): - Requests resolve their API source once (in start) and carry the handle, so a failure can report the exact source unhealthy and ETag/backoff retries keep targeting the same host. - On a failure that points at the host, the source is health-checked before failing over: a healthy source surfaces the original error, an unhealthy one is reported and the request transparently retries on the next source (capped at 5 source attempts per request). The static per-endpoint fallback hosts stay the last resort. - Unlike Android, device-connectivity errors (reusing NetworkError.isAllowedToRetryWithFallbackHost) skip the health check entirely and surface the original error: switching hosts can't fix a device that is offline. - Both Backend HTTPClients share one APISourceFailover, so they walk one source list and share one health-check cache. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(remote-config): end-to-end API source failover integration tests Port of purchases-android's BackendAPISourceFailoverIntegrationTest: real Backend.getCustomerInfo through the production wiring (real HTTPClients, shared APISourceFailover, real SourceHealthChecker probing over the URL loading system) and a real RemoteConfigSourceProvider fed by an in-memory sources topic, with OHHTTPStubs standing in for the source hosts and recording exact per-host request order. Adds the iOS-only device-connectivity scenario and a dangerous-setting-off wiring check on top of Android's seven scenarios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: model the request's API source as a single state enum Replaces the three loosely coupled Request fields (apiSource, apiSourceResolved, apiSourceAttemptCount) with one APISourceState enum (unresolved / noSource / source(_, attemptCount:)), making inconsistent combinations unrepresentable. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: adopt the narrowed APISourceFailover initializer Passes just the usesRemoteConfigAPISources flag at the two construction sites, following the signature change in the base branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: clarify why static fallback still runs when the source is healthy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(remote-config): raise the API failover restart interval to 2 hours The api source list now returns to the primary source 2 hours after it first failed over instead of 10 minutes. Timer tests derive their time values from the constant so future changes are single-line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(remote-config): cover shared failover and probe-in-flight ordering Addresses review feedback: - Both HTTPClient lanes share one APISourceFailover: a failover on the main lane advances the source list the remote-config lane resolves. - A request enqueued while a health check is in flight stays queued until the probe resolves and runs after the front-queued failover retry. - Bind the source and attempt count in one pattern match at the failover guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: drop the APISourceState.attemptCount accessor Its only caller pattern-matches instead, surfacing the "must already target a source" assumption at the one place it matters rather than papering over impossible states with a 0 default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a268c9b286 |
feat(remote-config): add health checker and API source failover components (#7293)
* feat(remote-config): add health checker and API source failover components Standalone pieces for health-check-gated API source failover, ported from purchases-android. Nothing consumes them yet, so released behavior is unchanged: - SourceHealthChecker: probes <source>/v1/health/connectivity; only a 2xx is healthy. Concurrent checks per source share one request and results are cached for 10s. - APISourceFailover: the decision layer. After a failed request, the current source is health-checked: a 2xx health response means the source is healthy and the original error should surface instead of switching hosts; otherwise the source is reported unhealthy and the next one takes over. Malformed source URLs are reported unhealthy and skipped. - RemoteConfigSourceProvider: the API failover list restarts from the top 10 minutes after it first advances, so the SDK periodically retries the primary source (and re-arms an exhausted list) instead of sticking on a backup forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add new files to xcodeproj * refactor: address API source failover review feedback - APISourceFailover takes only the usesRemoteConfigAPISources flag it reads instead of the whole DangerousSettings (safe as a plain value: the settings chain is immutable per SDK instance). - Require a scheme and host when parsing source URLs: URL(string:) alone accepts scheme-less or relative strings on modern Foundation, which would build requests that can only fail instead of skipping and reporting the source as intended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c4d824d68c |
feat(remote-config): source API base host from remote-config sources (#7123)
* feat(remote-config): wire manager behind feature flag * fix(remote-config): only mark refresh fresh after cache write * feat(remote-config): source API base host from remote-config sources Resolve the API base host from the shared RemoteConfigSourceProvider (with the embedded defaults as a floor) instead of the hardcoded apiBaseURL, so requests can fail over across API hosts. proxyURL and an overridden apiBaseURL still pin the host and bypass API sources, and per-endpoint fallback URLs are unchanged. The provider is created once in the composition root (gated on remoteConfigEnabled) and injected explicitly into Backend/HTTPClient; nil preserves the existing SystemInfo.apiBaseURL behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): derive nullable API source provider and manager from remoteConfigEnabled Create the RemoteConfigSourceProvider (and its shared on-disk cache) up front, gated on remoteConfigEnabled, and build the RemoteConfigManager from the provider's nullability after Backend exists so it can reuse backend.remoteConfigAPI. Keeps a single shared source provider + disk cache across the networking layer and the manager, and avoids touching the disk cache directory when remote config is disabled. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): drop redundant remote-config wiring comment Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): drop redundant remote-config manager comment Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): simplify APISourceProviding doc comment Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): always wire API source provider, gate only disk cache Create the RemoteConfigSourceProvider unconditionally and inject it into Backend/HTTPClient so main-API host resolution runs even when remote config is disabled (embedded default sources as the floor). Only the on-disk cache and the real RemoteConfigManager stay gated on remoteConfigEnabled; the provider's topic store is now optional (nil means defaults-only). Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): rename APISourceProviding to APISourceProviderType Co-authored-by: Cursor <cursoragent@cursor.com> * feat(remote-config): resolve web billing host from API sources The web billing paths are hosted on the same api.revenuecat.com host as the main API, so they should resolve their base host from the API source provider too (with failover) instead of the static serverHostURL. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): drop redundant web billing API-sources comments The comments restated what the code and surrounding context already convey. Co-authored-by: Cursor <cursoragent@cursor.com> * test(remote-config): assert ETag retry stays on the API source host Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): drop APISourceProviderType, use RemoteConfigSourceProviderType directly The networking layer depended on the narrow APISourceProviderType protocol, an extra abstraction over RemoteConfigSourceProviderType that added little. Fold currentAPISource() into RemoteConfigSourceProviderType and have HTTPClient/Backend depend on it directly. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(remote-config): wire apiSourceProvider into dedicated remote-config lane The main-into-branch merge left `HTTPClient.dedicatedRemoteConfig` (and its call site) without the new `apiSourceProvider` argument, breaking compilation. Thread the provider through so the dedicated `/config` lane also resolves its base host from remote-config API sources, matching the main lane. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: gate remote-config API sources behind usesRemoteConfigAPISources Resolve main-API request hosts from the remote-config API sources only when the `usesRemoteConfigAPISources` dangerous setting is enabled. When disabled (the default), requests keep targeting the static `SystemInfo.apiBaseURL`. Enable the setting in the API-source `HTTPClient` tests and add a test asserting an injected provider is ignored while the setting is off. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(test): pass apiSourceProvider in BackendRemoteConfigLaneTests HTTPClient init The lane test constructed HTTPClient without the now-required apiSourceProvider argument, breaking compilation of the unit test target after merging main. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Rick van der Linden <rick.vanderlinden@revenuecat.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
5fe329ff64 |
feat(remote-config): add APIs for reading topic/blob data from RemoteConfigManager (#7134)
* feat(remote-config): add read facade for blob data * docs(remote-config): document read facade APIs * refactor(remote-config): clarify refresh waiter continuations * docs(remote-config): document manager refresh state * fix(remote-config): update purchases test manager mock * fix(remote-config): read facade cache data off caller executor * fix(remote-config): avoid async work in nimble autoclosures * fix(remote-config): honor disabled state after topic reads * fix(remote-config): avoid async unwrap autoclosures * test(remote-config): fix disabled blob read assertion * refactor(remote-config): move topic store protocol near disk cache * fix(remote-config): stop reads after manager closes * fix(remote-config): avoid repeated read refreshes * fix(remote-config): guard committed reads against cache clears * test(remote-config): make stale read tests deterministic |
||
|
|
f320e95384 |
fix(remote-config): rearm exhausted blob sources + in memory knownRefs (#7120)
* fix(remote-config): rearm exhausted blob sources * refactor(remote-config): clarify blob prefetch filtering * fix(remote-config): validate cached blob refs on contains * fix(remote-config): retry blob ref scans after failures |
||
|
|
22644c9a31 |
feat(remote-config): drive RemoteConfigSourceProvider from a topic store (#7106)
* feat(remote-config): drive RemoteConfigSourceProvider from a topic store Make RemoteConfigSourceProvider read the `sources` topic lazily from a new RemoteConfigTopicStoreType seam and rebuild its api/blob failover lists only when that topic changes (compared via Equatable). New generations are token- seeded past the previous one so stale reports from before a rebuild are ignored. iOS counterpart of purchases-android #3677 (using Equatable instead of a content hash, so the #3676 port is unnecessary). Co-authored-by: Cursor <cursoragent@cursor.com> * feat(remote-config): bootstrap source provider with embedded defaults While the `sources` topic is absent, RemoteConfigSourceProvider now falls back to embedded default sources (https://api.revenuecat.com for api, https://config.revenuecat-static.com/{blob_ref} for blob) so the SDK can reach config before any config is fetched. A fetched topic replaces the defaults. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): use topic.map for default source fallback Replace the `topic == nil ? defaults : parseSources(...)` ternaries with `topic.map { parseSources($0...) } ?? defaults`, letting parseSources take a non-optional topic. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): skip redundant restart when a rebuild occurred rebuildIfNeeded now returns whether it rebuilt; restart(for:) only resets the failover when no rebuild happened, since a rebuild already starts the new list from the top. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): skip reportUnhealthy when a rebuild occurred A rebuild discards the old generation, so a report tied to it is stale. Mirror the restart guard and skip reportUnhealthy when rebuildIfNeeded rebuilt. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(remote-config): fall back to defaults when the sources topic has no sources Use the embedded defaults not only when the topic is absent but also when it carries an empty/unusable source list. Also rename the `builtTopic` property to `sourcesTopic` and simplify the sources() helper. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(remote-config): drop the embedded default blob source Blob sources are only useful alongside a fetched config (which carries its own blob sources), and blob URLs are project-scoped so there is no universal fallback. Keep the embedded api default. Blob now stays empty until a config is fetched. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(remote-config): add api.rc-backup.com as a second embedded api default Ship both api.revenuecat.com and api.rc-backup.com as embedded fallbacks, at priorities 100_000 and 100_001 (lower number wins) so any source from a fetched `sources` topic outranks them and they only act as a last resort. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(remote-config): use trailing slash on embedded api default urls The config endpoint returns api source urls with a trailing slash (e.g. https://api.revenuecat.com/). Match that canonical form so the embedded defaults dedupe correctly against config-provided sources. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
0e09c9fc83 |
refactor(remote-config): prefer lowest priority number for source ordering (#7112)
* refactor(remote-config): prefer lowest priority number for source ordering Sources are now ordered lowest `priority` first, and dedupe keeps the occurrence with the lowest priority number (highest priority). Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): clarify duplicate source log message Note that the kept duplicate is the lowest priority number. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
deb6f5b7cc |
feat(remote-config): add RemoteConfigSourceProvider for source failover (#7093)
* feat(remote-config): add RemoteConfigSourceProvider for source failover Adds RemoteConfigSourceProvider, which orders config sources via WeightedSourceSelector and hands out the current healthy endpoint, falling back to the next one when an endpoint is reported unhealthy. A per-handout UUID token discards stale unhealthy reports so a single failing endpoint can't advance the order more than once. Not yet wired into networking. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(remote-config): trim RemoteConfigSourceProvider comments Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): make RemoteConfigSourceProvider an api/blob address book Take a RemoteConfigSources object (api + blob), dedup each list by url, and fail over the two kinds independently via a private SourceFailover wrapping WeightedSourceSelector. Use the endpoint url as identity (dropping the UUID token) so stale/concurrent reports are ignored, and build endpoints once during dedup so reads never re-map. Add RemoteConfigSourceProviderType and MockRemoteConfigSourceProvider for downstream mocking. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): refine RemoteConfigSourceProvider endpoint model Embed RemoteConfigSource in RemoteConfigEndpoint (forwarding url/priority/ weight) so endpoints are built once and never re-mapped, and compare endpoints by kind+url. Make restart() per-kind, drop unused WeightedSource/Equatable conformances and default inits, and collapse duplicate source urls to the highest-priority occurrence (tie-broken by weight), logging conflicts. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): drop redundant init and Equatable conformance Rely on the synthesized memberwise init for RemoteConfigSources and remove the unused Equatable conformance on RemoteConfigEndpoint (tests compare urls). Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): clarify source handle naming and strengthen concurrency test Rename RemoteConfigEndpoint to RemoteConfigSourceHandle and its Kind enum to Purpose, expose currentAPISource/currentBlobSource, and use a named parameter in restart(for:). Also rewrite the concurrent test to deterministically assert that many threads reporting the same source advance the provider exactly once. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): simplify RemoteConfigSourceProvider API Drop the RemoteConfigSources wrapper in favor of apiSources/blobSources init params, and replace currentAPISource/currentBlobSource with a single getCurrent(for:) keyed by purpose, consistent with restart(for:). Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(remote-config): log conflicting duplicate sources as a warning Conflicting priority/weight on a shared URL signals misconfigured input, a degraded state better surfaced via Logger.warn than Logger.debug. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(remote-config): ignore stale unhealthy reports across restart Identify each handout with an opaque monotonic token that bumps on every fallback and on restart, instead of using the source url. reportUnhealthy only advances when the reported handle still carries the current token, so reports left over from before a restart can no longer skip a source in the new cycle. The token also subsumes the previous url-based dedup of stale or concurrent reports within a cycle. RemoteConfigSource now drives WeightedSourceSelector (it is the WeightedSource), and SourceFailover stamps the current token onto each handle at handout time. The handle is reduced to purpose + source + token (+ url accessor) and its init is fileprivate so handles can only originate from getCurrent. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |