[Swift] Scope preload state to per-cache entry#454
Conversation
18b6dc0 to
6bd0117
Compare
2cac69b to
4087102
Compare
00bddec to
1472f29
Compare
4087102 to
0c56668
Compare
1472f29 to
bc1be61
Compare
0c56668 to
9e05f19
Compare
9e05f19 to
00d2d04
Compare
00d2d04 to
c8f76e8
Compare
c8f76e8 to
2cfbff0
Compare
912a186 to
6c6f096
Compare
6876cd9 to
599dafd
Compare
599dafd to
aaf688c
Compare
b0aff16 to
e67aa39
Compare
e67aa39 to
a896563
Compare
Install this buildOpen Tophat, select your target device, then click Install. Links open on the Mac running Tophat.
Checkout Kit E2E results
|
|
Moving the timer state handling into the Entry is a good direction and I think moving the other two I commented on below would also be consistent I'm struggling to get my head around the cache work here so forgive the rambling here! On the I picturing prep work for this to look something like this: If I (+claude) am reading things right, it seems like every preload creates a new I don't think the There also doesn't seem to be a Just to play the YAGNI devils advocate - if we aren't supporting multi cache entry in this release should we hold off on this until we are able to fully build out the multi slot cache? (Keeping the timer moves into the |
| private func startKeepAlive(for entry: Entry) { | ||
| entry.keepAliveTimer?.invalidate() | ||
| let timer = Timer.scheduledTimer(withTimeInterval: Self.keepAliveInterval, repeats: true) { [weak self, weak entry] _ in | ||
| Task { @MainActor in | ||
| guard let entry else { return } | ||
| do { | ||
| _ = try await view?.evaluateJavaScript("void 0") | ||
| _ = try await entry.view.evaluateJavaScript("void 0") | ||
| } catch { | ||
| OSLogger.shared.debug("Preload keep-alive failed; invalidating preload cache") | ||
| self?.keepAliveDidFail() | ||
| } | ||
| } | ||
| } | ||
| timer.tolerance = Self.keepAliveInterval / 2 | ||
| keepAliveTimer = timer | ||
| entry.keepAliveTimer = timer | ||
| } | ||
|
|
||
| private func startExpiryTimer(after interval: TimeInterval) { | ||
| stopExpiryTimer() | ||
| private func startExpiryTimer(for entry: Entry) { | ||
| entry.expiryTimer?.invalidate() | ||
| let interval = entry.remainingTTL | ||
| let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in | ||
| Task { @MainActor in | ||
| self?.expire() | ||
| } | ||
| } | ||
| timer.tolerance = min(interval / 2, 1) | ||
| expiryTimer = timer | ||
| entry.expiryTimer = timer | ||
| } |
There was a problem hiding this comment.
Feels like it'd make sense to roll these into the Entry?
They can reference keepAliveDidFail / self.expire via weak ref back to the parent
Yeah went back and forth on this but I've been thinking the same @kieran-osgood-shopify. It sets us up for multi-slot but it's easier if we just do it when we need it. I think I'll defer it until then |

Promotes the preload cache's single global slot into a per-entry state machine, so each preloaded checkout owns its own lifecycle state, observer, and keep-alive/expiry timers. This removes the singleton-observer behavior and is forward-compatible with a future multi-entry keyed cache.
What changed
Entrypromoted to a@MainActorclass carryingstate, a weakobserver, and its own keep-alive/expiry timers (transition(to:),stopTimers()).PreloadCachekeys observers byPreloadKeyvia ahandlesregistry (register(_:for:)). State changes route per entry throughtransition(_ view:to:)(view-guarded), plusmarkIdle()and apreloadedViewaccessor.didFinish,didFail,handleResponse) transition the matching entry instead of a shared cache-level slot.CheckoutPreloadis key-bound and owns its ownstate, so distinct preload keys observe independently rather than sharing one slot.Public API surface is unchanged (
api/ShopifyCheckoutKit.jsonunmodified).PreloadObservabilityTestsrewritten to per-entry semantics;testNewObserverReplacesPreviousreplaced withtestDistinctKeysObserveIndependently.