Persist workflow detail envelopes for recovery when backend is down#3537
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3537 +/- ##
==========================================
+ Coverage 80.11% 80.15% +0.04%
==========================================
Files 371 371
Lines 15166 15210 +44
Branches 2100 2110 +10
==========================================
+ Hits 12150 12192 +42
- Misses 2166 2168 +2
Partials 850 850 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This stack of pull requests is managed by Graphite. Learn more about stacking. |
…recovery Prefetched workflow detail was kept in memory only, so a cold start with the backend down could restore the offeringId -> workflowId map but couldn't render a single workflow. This persists each prefetched workflow's resolved WorkflowDataResult to a new DeviceCache region, mirroring how the workflows list is stored, and restores it into the in-memory cache on a list-fetch failure so a later getWorkflow is a cache hit with no failed network call. - Persist on the prefetch path only (persistDetail flag), after a successful fetch, so a persisted detail is always renderable offline. - Prune the on-disk detail store to the current list on each list write, and clear it on identity transitions alongside the list disk cache. - Restore details fresh so getWorkflow serves them offline; the list restores stale so it refetches once the backend is back. Port of RevenueCat/purchases-android#3537 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Added two focused reproductions for the workflow envelope caching concerns in this PR:
|
facumenzella
left a comment
There was a problem hiding this comment.
Just opened two PRs to expose some edge cases. I think those are valid, even though edgie. Let me know what you think. We can totally tackle that later
…recovery Prefetched workflow detail was kept in memory only, so a cold start with the backend down could restore the offeringId -> workflowId map but couldn't render a single workflow. This persists each prefetched workflow's resolved WorkflowDataResult to a new DeviceCache region, mirroring how the workflows list is stored, and restores it into the in-memory cache on a list-fetch failure so a later getWorkflow is a cache hit with no failed network call. - Persist on the prefetch path only (persistDetail flag), after a successful fetch, so a persisted detail is always renderable offline. - Prune the on-disk detail store to the current list on each list write, and clear it on identity transitions alongside the list disk cache. - Restore details fresh so getWorkflow serves them offline; the list restores stale so it refetches once the backend is back. Port of RevenueCat/purchases-android#3537 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…recovery Prefetched workflow detail was kept in memory only, so a cold start with the backend down could restore the offeringId -> workflowId map but couldn't render a single workflow. This persists each prefetched workflow's resolved WorkflowDataResult to a new DeviceCache region, mirroring how the workflows list is stored, and restores it into the in-memory cache on a list-fetch failure so a later getWorkflow is a cache hit with no failed network call. - Persist on the prefetch path only (persistDetail flag), after a successful fetch, so a persisted detail is always renderable offline. - Prune the on-disk detail store to the current list on each list write, and clear it on identity transitions alongside the list disk cache. - Restore details fresh so getWorkflow serves them offline; the list restores stale so it refetches once the backend is back. Port of RevenueCat/purchases-android#3537 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(workflows): persist prefetched workflow detail for backend-down recovery Prefetched workflow detail was kept in memory only, so a cold start with the backend down could restore the offeringId -> workflowId map but couldn't render a single workflow. This persists each prefetched workflow's resolved WorkflowDataResult to a new DeviceCache region, mirroring how the workflows list is stored, and restores it into the in-memory cache on a list-fetch failure so a later getWorkflow is a cache hit with no failed network call. - Persist on the prefetch path only (persistDetail flag), after a successful fetch, so a persisted detail is always renderable offline. - Prune the on-disk detail store to the current list on each list write, and clear it on identity transitions alongside the list disk cache. - Restore details fresh so getWorkflow serves them offline; the list restores stale so it refetches once the backend is back. Port of RevenueCat/purchases-android#3537 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(workflows): address review on detail-cache persistence - Move workflow-detail disk restore into WorkflowsCache (next to the list restore) so all disk-restore logic lives in one place. - Add a batched in-memory cache(workflows:) and persist prefetched details to disk in a single write at the end of prefetchWorkflows instead of one write per workflow. - Guard the disk write with a generation token bumped on clearCache, so an in-flight prefetch from a since-logged-out user can't write its details back after the store was cleared (cross-user leak). The clear + generation bump run under the same lock as the write. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(workflows): guard workflow detail cache against cross-user races Address Bugbot findings on the detail-cache persistence: - Extend the generation guard to the in-memory write so an in-flight fetch for the previous user can't repopulate memory after an identity change (workflow detail is user-scoped). The in-memory clear now runs under the same lock as the generation bump. - restoreWorkflowDetailsFromDisk fills only ids missing from memory, so a stale disk snapshot can't clobber a fresher on-demand fetch, and holds the disk-read + memory-write under the lock so a clear can't interleave. - persistWorkflowDetailsToDisk filters the persisted map to the current list ids, keeping the on-disk store a subset of the latest list when a slower prefetch from an earlier list lands after a prune. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(workflows): drop unused in-memory cache helpers cache(workflow:workflowId:) and cache(workflows:) have no production callers since getWorkflow writes via the generation-guarded cache(workflow:workflowId:ifGeneration:) and restore inlines its own fill-gaps write. Remove both and seed the in-memory cache in tests through the guarded path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(workflows): correct restore-freshness comment The restored details are stamped fresh so they serve offline, but the old comment implied the stale-list restore drives their refresh. It doesn't: once the backend is back, getWorkflow cache-hits the fresh details (no backend call) until their own foreground TTL expires. The stale list only drives the list/map refetch. Clarify both comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(workflows): drop a late list fetch from the previous user on identity change If a workflows list request issued for the previous user finished after an identity change cleared the cache, the success path still cached that list and prefetched its details, persisting the prior user's targeted workflow details to the shared on-disk map for the new session. Capture the cache generation when the list request is issued and, on success, drop the response (no list cache, no prefetch) when the generation no longer matches, still firing onComplete so callers aren't blocked. Thread that issue-time generation into the prefetch disk persist instead of re-capturing it after the clear, so a clear landing mid-prefetch is also caught. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

This PR persists the detail envelopes to disk, mirroring what we do with
OfferingsCache, so a cold start with the backend down renders the prefetched workflows. We are already saving to disk the list of workflows, and we were missing this part.Note
Medium Risk
Changes paywall/workflow delivery and disk cache behavior on network failure and cold start; scope is bounded (prefetch + prune) but incorrect recovery could affect paywall rendering.
Overview
Adds disk persistence of workflow detail envelopes (alongside the existing workflows list cache) so prefetched paywalls can be re-resolved offline after restart or when the backend is unreachable.
Prefetch-only writes:
getWorkflowgainspersistEnvelopeOnResolve(defaultfalse). Only the workflows-list prefetch path sets ittrueafter a successful resolve, storing the rawWorkflowDetailResponseinDeviceCacheviaWorkflowsCache(merge-by-workflow-id map). On-demand fetches do not persist, to avoid unbounded disk use.Backend-down recovery: When
getWorkflowsListfails, the manager still restores the list from disk; if persisted envelopes exist, it re-resolves them in parallel into the in-memory workflow cache (restoreWorkflowFromEnvelope), so subsequentgetWorkflowcan be a cache hit without calling the backend. Failures per envelope are logged and isolated;onCompletestill runs once.Cache hygiene:
cacheWorkflowsListprunes stored envelopes to IDs in the latest list;clearCacheclears the envelope store on identity transitions.WorkflowJsonParseradds parsing for the envelope map.Reviewed by Cursor Bugbot for commit bc39f94. Bugbot is set up for automated code reviews on this repo. Configure here.