feat(workflows): serve stale workflow detail while revalidating#6961
Merged
Conversation
Port of purchases-android#3540. getWorkflow now uses stale-while-revalidate like OfferingsManager: a stale-but-present detail is served immediately and the cache is refreshed in the background (a success updates the cache, a failure is logged and swallowed). Prefetch opts out via staleWhileRevalidate: false, so it keeps forcing a fresh fetch and persisting its envelope instead of serving a stale value. The background refresh captures its generation guard before serving the cached value, so a clearCache() (login/logout) racing the serve still drops the write, keeping the previous user's detail out of the new user's cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vegaro
approved these changes
Jun 9, 2026
vegaro
left a comment
Member
There was a problem hiding this comment.
I think this makes sense. We need to do the same for getWorkflows right? Otherwise it blocks on a stale cache
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist
purchases-androidand hybrids (this ports purchases-android#3540)Motivation
A workflow whose cached detail went stale currently blocks on a backend round-trip before the paywall can render. This brings the workflow detail fetch in line with how
OfferingsManageralready serves offerings: serve the cached value instantly and refresh in the background.Description
Port of purchases-android#3540 (detail only).
WorkflowManager.getWorkflownow uses stale-while-revalidate:flowchart TD A([getWorkflow]) --> B{cached entry present?} B -->|no| MISS["block on backend fetch,<br/>deliver result to caller"] B -->|yes| C{still fresh?} C -->|yes| FRESH["serve cached, no network"] C -->|stale| SWR{staleWhileRevalidate?} SWR -->|"false (prefetch)"| MISS SWR -->|"true (on-demand)"| STALE["capture generation G,<br/>serve stale value to caller,<br/>then refresh in background<br/>(failures logged, not delivered)"] MISS --> WRITE["guarded cache write"] STALE --> WRITE WRITE --> G{generation still G?} G -->|yes| OK["update cache + warm assets"] G -->|"no: clearCache on login/logout"| DROP["drop write,<br/>keeps prev user's detail out"]Prefetch passes
staleWhileRevalidate: false, so it keeps forcing a fresh fetch and persisting its envelope rather than serving and persisting a stale value.The background refresh captures its generation guard before serving, so a
clearCache()(login/logout) racing the serve still drops the write, keeping the previous user's detail out of the new user's cache (consistent with #6944).The workflows list is intentionally left as is: it already rides offerings' stale-while-revalidate, and the one place it blocks (
deliverEnsuringWorkflowsListon the fresh-offerings path) exists on purpose to guaranteecachedWorkflowId(forOfferingId:)resolves right aftergetOfferings.AI session context
Metadata
facu/workflow-detail-swr(off the fix(workflows): Don't leak the previous user's workflows after a login or logout #6944 merge commit)Goal
Give the workflow detail fetch the same stale-while-revalidate behavior offerings already have, mirroring purchases-android#3540. Detail only; the workflows list is explicitly out of scope.
Initial Prompt
The human asked whether iOS should serve-cache-then-fetch for workflows like Android #3540, framed as "do the same as offerings for both list and details". After comparing the current iOS code (offerings already SWR; detail blocks on stale; list blocks but rides offerings' SWR), the agent recommended porting #3540 for the detail and leaving the list alone. The human agreed ("let's port that then"), then asked to send the change to Codex for review.
Agent Contribution
staleWhileRevalidate: Bool = truetogetWorkflow; extracted the backend-fetch + cache + warm-up body into a privatefetchAndCacheWorkflowshared by the blocking path and the background refresh.false.PaywallsStrings.error_refreshing_workflowfor the swallowed background-refresh failure.truereds the persist-fresh test).Human Decisions
debug-view-locale- ios-17 #3540 for the detail; do NOT make the workflows list SWR (it already rides offerings' SWR, and the one blocking path guarantees workflow-id resolution right aftergetOfferings).Codex Review and Resolution
Task {}, opening an async window for a cross-user leak. That mechanism is incorrect: the helper is called synchronously, noTask, noawait.completion()between them. Fix applied: capture the generation before serving, so the background write binds to the serve-time generation (same guarantee the blocking path and fix(workflows): Don't leak the previous user's workflows after a login or logout #6944 have). The residual sub-statement race is the same lock-free read window fix(workflows): Don't leak the previous user's workflows after a login or logout #6944 accepted by design and is not chased here.WorkflowsAPI's callback cache). Added two coverage tests Codex suggested.Files / Symbols Touched
Sources/Purchasing/WorkflowManager.swift-getWorkflow(..., staleWhileRevalidate:), newfetchAndCacheWorkflow, prefetch opt-out.Sources/Logging/Strings/PaywallsStrings.swift-error_refreshing_workflow.Tests/UnitTests/Purchasing/WorkflowManagerTests.swift- 6 new tests.Validation
xcodebuild test(iPhone 15 sim) forWorkflowManagerTests(48),PurchasesWorkflowTests(42),WorkflowsCacheTests: 91 passing, 0 failures.swiftlinton changed files: clean.Validation Gaps
Review Focus
getWorkflow's generation is captured beforecompletionon the SWR path; the background refresh delivers no result to the caller.staleWhileRevalidate: false(a stale prefetch must fetch and persist fresh, not serve stale).WorkflowManageris internal and the new parameter is optional/defaulted, so noswiftinterfacechange.Non-goals / Out of Scope