05/16 Fix bugs 2

This commit is contained in:
Nguyen Ngo
2026-05-16 14:22:33 -04:00
parent 12d07cf250
commit 9c7aa72ff3
3 changed files with 45 additions and 13 deletions
+20 -3
View File
@@ -41,13 +41,30 @@ private struct _Envelope<T: Decodable & Sendable>: Decodable, Sendable {
// local JSONDecoder to avoid Swift 6 actor-isolation errors.
// Refresh-only envelope Sendable so it can cross actor boundaries in Swift 6.
// nonisolated init(from:) required on both types: without it the Swift 6 compiler
// infers @MainActor isolation on the Decodable conformance from the surrounding
// file context, producing "cannot be used in actor-isolated context" errors.
private struct _RefreshEnvelope: Decodable, Sendable {
struct Tokens: Decodable, Sendable {
let accessToken: String
let refreshToken: String
nonisolated init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
accessToken = try c.decode(String.self, forKey: .accessToken)
refreshToken = try c.decode(String.self, forKey: .refreshToken)
}
private enum CodingKeys: String, CodingKey { case accessToken, refreshToken }
}
let ok: Bool
let data: Tokens?
nonisolated init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
ok = try c.decode(Bool.self, forKey: .ok)
data = try? c.decode(Tokens.self, forKey: .data)
}
private enum CodingKeys: String, CodingKey { case ok, data }
}
actor APIClient {
@@ -107,7 +124,7 @@ actor APIClient {
// Photo Upload
func uploadPhoto(localPath: String, entityType: String) async throws -> String {
func uploadPhoto(localPath: String, entityType: String, retrying: Bool = false) async throws -> String {
let url = try buildURL("/api/v1/photos/upload")
guard let imageData = FileManager.default.contents(atPath: localPath) else {
@@ -133,9 +150,9 @@ actor APIClient {
let (data, response) = try await performRequest(req)
if shouldRefresh(response, retrying: false) {
if shouldRefresh(response, retrying: retrying) {
let refreshed = await refreshAccessToken()
if refreshed { return try await uploadPhoto(localPath: localPath, entityType: entityType) }
if refreshed { return try await uploadPhoto(localPath: localPath, entityType: entityType, retrying: true) }
throw APIError.notAuthenticated
}
+7
View File
@@ -38,6 +38,13 @@ final class LocalIssue {
var syncRetryCount: Int
var syncErrorMessage: String?
// Explicit inverse declared so SwiftData has an unambiguous relationship
// graph at schema-build time. Without it the relationship is implicit,
// which can cause migration warnings or incorrect cascade behaviour on some
// SwiftData versions. The deleteRule is .nullify (default) deleting the
// parent inspection cascades via LocalInspection.localIssues; this side
// only nullifies the back-pointer.
@Relationship(deleteRule: .nullify, inverse: \LocalInspection.localIssues)
var inspection: LocalInspection?
init(
+18 -10
View File
@@ -36,6 +36,22 @@ class SyncManager: ObservableObject {
private var pollTask: Task<Void, Never>? // replaces Timer Task.sleep works correctly
private let pollInterval: UInt64 = 60_000_000_000 // 60 seconds in nanoseconds
// Shared date formatters
// DateFormatter init is expensive allocating one per poll call or per
// issue would add measurable overhead at sync time. These are created
// once and reused across all calls. Both are nonisolated statics so they
// can be read from any context without actor-hopping.
//
// isoFormatter parses/formats ISO 8601 strings from the server API
// e.g. "2026-05-01T14:30:00"
// notifFormatter same format, used to advance the notification poll cursor
nonisolated static let isoFormatter: DateFormatter = {
let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return f
}()
static let shared = SyncManager()
private init() {}
@@ -106,9 +122,7 @@ class SyncManager: ObservableObject {
}
// Update the cursor to the newest notification's timestamp
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
let dates = notifications.compactMap { fmt.date(from: $0.createdAt) }
let dates = notifications.compactMap { Self.isoFormatter.date(from: $0.createdAt) }
if let newest = dates.max() {
lastNotificationFetch = newest
}
@@ -424,8 +438,6 @@ class SyncManager: ObservableObject {
if let sid = local.serverId { serverIdMap[sid] = local }
}
let isoFmt = ISO8601DateFormatter()
for api in apiIssues {
if let existing = serverIdMap[api.id] {
// Update mutable fields on existing record
@@ -455,11 +467,7 @@ class SyncManager: ObservableObject {
serverPaths.append(contentsOf: api.resultPhotos)
local.photoServerPaths = serverPaths
if let ts = api.reportedAt,
let date = isoFmt.date(from: ts) ?? {
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
return f.date(from: ts)
}() {
let date = Self.isoFormatter.date(from: ts) {
local.createdAt = date
}
context.insert(local)