mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-07 05:45:59 +08:00
* feat(ios): read-only offline cache for chat sessions and transcripts Cache-first cold open for the iOS chat tab: the last known transcript and session list render immediately from a local SQLite cache, then live gateway history replaces them wholesale through the existing reconciliation path. When the gateway is unreachable, recent sessions and transcripts stay browsable read-only; sending remains gated by the existing connection state. - New OpenClawChatTranscriptCache protocol seam plus SQLite-backed store in OpenClawChatUI (raw SQLite3, no new dependencies), scoped per gateway stableID so transcripts never leak across paired gateways. - Bounds: 50 sessions, 50 transcripts, 200 messages per session; text rows only (attachment/binary payloads and tool arguments are stripped). - Disposable cache: schema versioned via user_version; any open/schema/decode mismatch drops and rebuilds silently. File protection completeUntilFirstUserAuthentication on iOS. - View model pre-paints from cache only until a live response applies (hasAppliedLiveHistory/hasAppliedLiveSessions guards); write-through is chained and off the render path. - iOS wiring keys the chat view model identity on transport mode plus cache gateway ID so switching paired gateways rebuilds the view model. Part of #100194 * fix(ios): harden offline transcript cache * fix(ios): expose offline cached sessions * fix(ios): isolate transcript caches by gateway * chore(i18n): sync native inventory * fix(ios): allow cache extension to replace messages
67 lines
2.8 KiB
Swift
67 lines
2.8 KiB
Swift
import Foundation
|
|
|
|
// Offline transcript cache integration. The cache only pre-paints cold opens
|
|
// and covers offline browsing; live gateway responses are always the source
|
|
// of truth and replace cached rows wholesale.
|
|
|
|
extension OpenClawChatViewModel {
|
|
struct SessionSnapshot {
|
|
var key: String
|
|
var generation: UInt64
|
|
}
|
|
|
|
func replaceMessages(_ messages: [OpenClawChatMessage]) {
|
|
guard self.messages != messages else { return }
|
|
self.messages = messages
|
|
self.markTimelineChanged()
|
|
}
|
|
|
|
func persistTranscriptToCache(sessionKey: String, messages: [OpenClawChatMessage]) {
|
|
guard let transcriptCache else { return }
|
|
// Chain writes so an older snapshot can never land after a newer one;
|
|
// detached tasks alone give no ordering guarantee across awaits.
|
|
let previous = self.pendingCacheWriteTask
|
|
self.pendingCacheWriteTask = Task.detached {
|
|
await previous?.value
|
|
await transcriptCache.storeTranscript(sessionKey: sessionKey, messages: messages)
|
|
}
|
|
}
|
|
|
|
func persistSessionsToCache(_ sessions: [OpenClawChatSessionEntry]) {
|
|
guard let transcriptCache else { return }
|
|
let previous = self.pendingCacheWriteTask
|
|
self.pendingCacheWriteTask = Task.detached {
|
|
await previous?.value
|
|
await transcriptCache.storeSessions(sessions)
|
|
}
|
|
}
|
|
|
|
/// Cache-first cold open: pre-paint the cached transcript/session list
|
|
/// while the live requests are in flight (or failing while offline).
|
|
/// Live history replaces the painted rows wholesale via the normal
|
|
/// applyHistoryPayload reconciliation path.
|
|
func paintFromCacheIfNeeded(session: SessionSnapshot) {
|
|
guard let transcriptCache else { return }
|
|
if self.sessions.isEmpty, !self.hasAppliedLiveSessions {
|
|
Task { [weak self] in
|
|
let cached = await transcriptCache.loadSessions()
|
|
guard let self, !cached.isEmpty else { return }
|
|
// A live sessions response (even an empty one) is authoritative;
|
|
// a slow cache read must never repaint over it.
|
|
guard self.sessions.isEmpty, !self.hasAppliedLiveSessions else { return }
|
|
self.sessions = cached
|
|
}
|
|
}
|
|
guard self.messages.isEmpty, !self.hasAppliedLiveHistory else { return }
|
|
Task { [weak self] in
|
|
let cached = await transcriptCache.loadTranscript(sessionKey: session.key)
|
|
guard let self, !cached.isEmpty else { return }
|
|
guard self.isCurrentSession(session), !self.hasAppliedLiveHistory, self.messages.isEmpty else {
|
|
return
|
|
}
|
|
self.replaceMessages(cached)
|
|
self.isShowingCachedTranscript = true
|
|
}
|
|
}
|
|
}
|