Skip to content

feat(workflows): add WorkflowsCache and disk persistence for workflows list#6881

Merged
facumenzella merged 12 commits into
mainfrom
port/3508-workflows-cache-pr1
Jun 2, 2026
Merged

feat(workflows): add WorkflowsCache and disk persistence for workflows list#6881
facumenzella merged 12 commits into
mainfrom
port/3508-workflows-cache-pr1

Conversation

@facumenzella

@facumenzella facumenzella commented Jun 2, 2026

Copy link
Copy Markdown
Member

Port of RevenueCat/purchases-android#3508 (1 of 3 stacked PRs).

Android #3508 wires up workflows fetching end-to-end and adds in-memory + disk caching. We already have the workflow networking layer on iOS (WorkflowsAPI, the getWorkflow/getWorkflows endpoints, response models), so this port is really about the orchestration + caching + lifecycle we're missing. Splitting it into three stacked PRs:

  1. This one — caching foundation
  2. WorkflowManager + list fetch + offeringId→workflowId resolution
  3. Lifecycle wiring (OfferingsManager timing, IdentityManager cache clearing) + gating

What Changed

  • New WorkflowsCache, owner of in-memory workflow state: resolved per-workflow WorkflowDataResults plus the workflows list and its offeringId → workflowId map. Same 5 min / 25 hr foreground/background TTL as offerings. It also owns the disk copy of the list (persist on cache, restore on backend failure, wipe on clear), same as DeviceCache does for offerings.
  • DeviceCache gets disk persistence for the workflows-list response (cachedWorkflowsListResponse / cache(workflowsListResponse:) / clearWorkflowsListResponseCache)

Nothing calls this yet, it's just the foundation the next two PRs build on.

Notes

  • WorkflowsCache keeps its own timestamps via an injected DateProvider instead of reusing InMemoryCachedObject (whose staleness is tied to the real wall clock) so the TTL is actually testable. Same as Android.
  • Didn't port Android's network-level request dedup (pendingCompletionCallbacks/FetchDecision), our CallbackCache already dedups in-flight requests. PR 2 only needs the higher-level "fire once the list + prefetches are done" piece.

Note

Low Risk
Self-contained caching with broad unit tests and no production wiring yet; the non-user-scoped workflows list key relies on future identity-transition clearing.

Overview
Adds a workflows caching foundation that is not wired into fetch/orchestration yet.

WorkflowsCache holds in-memory per-workflow WorkflowDataResult entries and the workflows list, including an offeringId → workflowId map built when the list is cached. Staleness uses the same foreground/background TTL as offerings, driven by an injectable DateProvider (not InMemoryCachedObject) so TTL behavior is testable. clearCache() clears memory and the on-disk list.

DeviceCache gains disk persistence for WorkflowsListResponse under a single non-user-scoped key (workflowsListResponse), stored via large-item/file cache (not UserDefaults). cacheDurationInSeconds no longer takes isSandbox; callers pass only background state and sandbox is read from systemInfo.

Duplicate offeringId entries in a workflows list log a backend warning; the last workflow wins for lookup.

Unit tests cover WorkflowsCache TTL/staleness, list↔offering mapping, disk round-trip, and DeviceCache workflows-list caching.

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

…s list

First of three stacked PRs porting purchases-android#3508 (wire workflows
fetching + cache persistence) to iOS. This PR adds the caching foundation only.

- Add `WorkflowsCache`: in-memory cache for resolved per-workflow
  `WorkflowDataResult`s and the workflows list (plus its derived
  offeringId to workflowId map), with the same 5 min foreground / 25 hr
  background TTL as offerings. It also owns the disk copy of the list.
- Add `DeviceCache` disk persistence for the workflows-list response
  (`cachedWorkflowsListResponse` / `cache(workflowsListResponse:)` /
  `clearWorkflowsListResponseCache`), following the offerings-response pattern.
  Single non-user-scoped key; cross-user safety handled by clearing on
  identity transitions (a later PR), mirroring offerings.
- Expose an internal `DeviceCache.cacheDurationInSeconds(isAppBackgrounded:)`
  so other caches reuse the identical TTL policy.

`WorkflowsCache` manages its own timestamps via an injected `DateProvider`
(rather than `InMemoryCachedObject`, whose staleness is tied to the real wall
clock) so cache-expiry is deterministically testable, mirroring Android.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@facumenzella facumenzella marked this pull request as ready for review June 2, 2026 08:10
@facumenzella facumenzella requested a review from a team as a code owner June 2, 2026 08:10
@facumenzella facumenzella requested a review from vegaro June 2, 2026 08:11
Comment thread Sources/Caching/DeviceCache.swift Outdated
Comment thread Sources/Caching/WorkflowsCache.swift Outdated
…ache

- Drop the "mirroring the Android SDK" mention from the doc comment.
- Correct the `@unchecked Sendable` rationale now that the class is `final`.

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

Copy link
Copy Markdown
Member Author

I'll simplify some comments because they are just too much 😆

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

Looking great! Left some small comments but nothing blocking. Will leave approval to someone with more context though 🙏

Comment thread Sources/Caching/DeviceCache.swift Outdated
Comment thread Sources/Caching/DeviceCache.swift Outdated
Comment thread Sources/Caching/WorkflowsCache.swift
Comment thread Sources/Caching/WorkflowsCache.swift Outdated
Comment thread Sources/Caching/WorkflowsCache.swift Outdated
Comment thread Sources/Caching/WorkflowsCache.swift Outdated

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

Looking good! Left some comments

Comment thread Sources/Caching/DeviceCache.swift Outdated
Comment thread Sources/Caching/DeviceCache.swift Outdated
Comment thread Sources/Caching/DeviceCache.swift Outdated
Comment thread Sources/Caching/WorkflowsCache.swift
facumenzella and others added 2 commits June 2, 2026 11:26
- Remove dead deleteOldFileIfNeeded calls for the workflows-list key: the
  cache is brand new and never wrote to the pre-largeItemCache documents
  directory, so there is nothing to clean up there.
- Correct the workflows-list scoping comment (iOS offerings disk cache is
  user-scoped, so the "just like offerings" framing was misleading).
- Rename the offeringIdMap parameter/field to workflowIdByOfferingId so the
  map direction is unambiguous.
- Trim verbose doc comments that documented current context rather than the code.

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

Drop the passed-in map parameter from cache(workflowsList:) and derive the
workflowId for an offering directly from WorkflowSummary.offeringId. workflowId(
forOfferingId:) now reads from the in-memory list and falls back to the disk copy,
so it resolves after a disk restore before the next fetch repopulates memory.

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

Copy link
Copy Markdown
Member Author

Thank yo so much @tonidero @ajpallares would you mind doing another pass whenever you can? :)

Comment thread Sources/Caching/WorkflowsCache.swift
Comment thread Sources/Caching/WorkflowsCache.swift
Comment thread Sources/Caching/WorkflowsCache.swift Outdated
Resolve offering->workflow ids from a precomputed in-memory map built when
the list is cached, instead of falling back to disk on every lookup. The
disk fallback re-ran a synchronized UserDefaults write plus a file read and
JSON decode on each call without ever repopulating memory; restoring the
persisted list into memory after a backend failure is the caller's job.

Duplicate offeringIds keep last-wins resolution and now log a warning so the
collision isn't silent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@facumenzella facumenzella requested a review from ajpallares June 2, 2026 13:51

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

some nits and tests. looks good after fixing

Comment thread Sources/Caching/DeviceCache.swift
Comment thread Tests/UnitTests/Caching/WorkflowsCacheTests.swift Outdated
Comment thread Tests/UnitTests/Caching/WorkflowsCacheTests.swift

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

Thank you for iterating on this! I think this looks good!

Comment thread Sources/Caching/WorkflowsCache.swift
Comment thread Tests/UnitTests/Caching/WorkflowsCacheTests.swift
Comment thread Sources/Caching/DeviceCache.swift Outdated
facumenzella and others added 2 commits June 2, 2026 17:49
…ache-pr1

# Conflicts:
#	Sources/Logging/Strings/BackendErrorStrings.swift
Rename testCacheWorkflowsListPersistsResponseToDisk to reflect that it
only verifies the response is forwarded to DeviceCache, and add
backgrounded-TTL coverage for the workflows list cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
facumenzella and others added 2 commits June 2, 2026 17:52
…ments

Per review, keep the doc comments to what the methods do and drop the
contextual notes about identity-transition races and caller hydration
responsibility that can drift out of date.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The isSandbox parameter was always passed self.systemInfo.isSandbox, so
fold it into cacheDurationInSeconds(isAppBackgrounded:) which reads it
internally, and update the offerings staleness call sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@facumenzella facumenzella enabled auto-merge (squash) June 2, 2026 15:58
@facumenzella facumenzella merged commit 9fab961 into main Jun 2, 2026
18 of 20 checks passed
@facumenzella facumenzella deleted the port/3508-workflows-cache-pr1 branch June 2, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants