wordeck-native/Sources/Core/Sync/GradeQueue.swift
Till JS 542082772a refactor(big-bang): cards-native → wordeck-native
Code + Identity-Rename zur Vorbereitung auf Apple-Dev-Portal-Aktion
(Bundle ev.mana.wordeck, App-Group group.ev.mana.wordeck, AASA
applinks:wordeck.com). Build bleibt funktional, aber gegen die
neue text-only-API können image-occlusion-Creates 422 zurückgeben —
das wird mit der Wordeck-Native v1.0-Welle (parallele Apple-Aktion)
sauber gemacht.

Umbenennung:
- 41 Files: cardecky/Cardecky → wordeck/Wordeck (Display, Strings,
  Kommentare)
- 57 Files: CardsNative → WordeckNative, CardsAPI → WordeckAPI,
  CardsTheme → WordeckTheme, CardsBrand → WordeckBrand, CardsWidget →
  WordeckWidget, CardsDueWidget → WordeckDueWidget
- Bundle-ID ev.mana.cardecky → ev.mana.wordeck (project.yml,
  Info.plist, entitlements, Keychain-Service, App-Group)
- AASA applinks:cardecky.mana.how → applinks:wordeck.com
- API-Base cardecky-api.mana.how → api.wordeck.com
- 10 Files renamed (App-Entry, API-Extensions, Theme, Widget,
  Entitlements, Tests)
- xcodeproj regenerated via xcodegen → WordeckNative.xcodeproj
- MaskRegionsTests.swift gelöscht (image-occlusion entfällt mit
  Wordeck text-only)

Forgejo-Repo git.mana.how/till/cards-native → wordeck-native umbenannt
(Auto-Redirect aktiv). Lokales Verzeichnis Code/cards-native/ bleibt
vorerst — wird beim nächsten Apple-Setup mit Bundle-Test umbenannt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 23:10:42 +02:00

94 lines
3 KiB
Swift

import Foundation
import ManaCore
import Observation
import SwiftData
/// Persistente Offline-Queue für Grade-Aktionen. Drain-Loop kann
/// vom UI ausgelöst werden (bei Reconnect oder App-Foreground).
@MainActor
@Observable
final class GradeQueue {
private(set) var isDraining = false
private(set) var lastDrainError: String?
private let api: WordeckAPI
private let context: ModelContext
init(api: WordeckAPI, context: ModelContext) {
self.api = api
self.context = context
}
/// Enqueue + sofort versuchen zu senden. Bei Fehler bleibt der
/// Eintrag in der Queue.
func submit(cardId: String, subIndex: Int, rating: Rating, reviewedAt: Date = .now) async {
let grade = PendingGrade(
cardId: cardId,
subIndex: subIndex,
rating: rating,
reviewedAt: reviewedAt
)
context.insert(grade)
try? context.save()
let rawRating = rating.rawValue
Log.study.info(
"Queued grade \(cardId, privacy: .public)/\(subIndex, privacy: .public): \(rawRating, privacy: .public)"
)
await drain()
}
/// Schickt alle pending grades in FIFO-Reihenfolge ab. Bei Server-
/// Erfolg: aus Queue löschen. Bei Netzfehler: Loop abbrechen
/// (kommender Drain probiert es nochmal).
func drain() async {
guard !isDraining else { return }
isDraining = true
defer { isDraining = false }
let descriptor = FetchDescriptor<PendingGrade>(
sortBy: [SortDescriptor(\.queuedAt, order: .forward)]
)
let pending = (try? context.fetch(descriptor)) ?? []
guard !pending.isEmpty else {
lastDrainError = nil
return
}
for grade in pending {
guard let rating = grade.rating else {
context.delete(grade)
continue
}
do {
_ = try await api.gradeReview(
cardId: grade.cardId,
subIndex: grade.subIndex,
rating: rating,
reviewedAt: grade.reviewedAt
)
context.delete(grade)
try? context.save()
} catch {
let msg = (error as? LocalizedError)?.errorDescription ?? String(describing: error)
grade.lastTryAt = .now
grade.lastError = msg
try? context.save()
lastDrainError = msg
let cid = grade.cardId
let sub = grade.subIndex
Log.study.notice(
"Drain stopped \(cid, privacy: .public)/\(sub, privacy: .public): \(msg, privacy: .public)"
)
return
}
}
lastDrainError = nil
Log.study.info("Drain complete")
}
/// Wie viele Grades hängen aktuell offline?
func pendingCount() -> Int {
let descriptor = FetchDescriptor<PendingGrade>()
return (try? context.fetchCount(descriptor)) ?? 0
}
}