Files
edgeever/apps/ios/EdgeEver/Data/Sync/SyncEngine.swift
T
tianma-if cd4f010d8f feat(ios): add native SwiftUI client with Android UI parity
Replace the abandoned RN iOS path with a SwiftUI app (iOS 17+) that reuses
the shared TipTap editor bundle, GRDB local mirror/outbox, and Android-aligned
shell chrome for list, create, detail, and Me. Package the editor HTML in the
app resources so Markdown renders, present edit from the workspace root with a
UIKit FAB, and keep caret stable while typing.
2026-08-07 02:52:01 +08:00

89 lines
3.5 KiB
Swift

import Foundation
actor SyncEngine {
private let mirror: LocalMirrorRepository
private let client: APIClient
private var inFlight: [String: Task<Int, Error>] = [:]
init(mirror: LocalMirrorRepository, client: APIClient) {
self.mirror = mirror
self.client = client
}
func sync(scope: String, onBootstrapProgress: (@Sendable (BootstrapProgress) -> Void)? = nil) async throws -> Int {
if let existing = inFlight[scope] {
return try await existing.value
}
let task = Task { try await performSync(scope: scope, onBootstrapProgress: onBootstrapProgress) }
inFlight[scope] = task
defer { inFlight[scope] = nil }
return try await task.value
}
private func performSync(scope: String, onBootstrapProgress: (@Sendable (BootstrapProgress) -> Void)?) async throws -> Int {
var cursor = try mirror.getCursor(scope: scope)
var identity = try mirror.getIdentity(scope: scope)
if cursor == nil || identity == nil {
try mirror.clearRemoteMirrorPreservingLocal(scope: scope)
var afterId: String?
var loaded = 0
var snapshotCursor = 0
var snapshotIdentity = "legacy"
var firstPage = true
while true {
let page = try await client.getMobileSyncBootstrapPage(
afterId: afterId,
limit: SyncProtocol.bootstrapPageSize
)
if firstPage {
snapshotCursor = page.snapshotCursor
snapshotIdentity = page.syncIdentity ?? "legacy"
firstPage = false
}
let batches = SyncProtocol.splitBootstrapWriteBatches(
page.memos,
batchSize: SyncProtocol.bootstrapWriteBatchSize
)
for (index, batch) in batches.enumerated() {
try mirror.applyBootstrapBatch(
scope: scope,
notebooks: index == 0 ? page.notebooks : nil,
memos: batch
)
loaded += batch.count
onBootstrapProgress?(BootstrapProgress(loadedCount: loaded, totalCount: page.totalCount))
}
guard let next = page.nextAfterId else { break }
afterId = next
}
cursor = snapshotCursor
identity = snapshotIdentity
try mirror.setCursor(scope: scope, cursor: snapshotCursor)
try mirror.setIdentity(scope: scope, identity: snapshotIdentity)
}
var currentCursor = cursor ?? 0
var currentIdentity = identity ?? "legacy"
while true {
let page = try await client.getMobileSyncChanges(
cursor: currentCursor,
limit: SyncProtocol.changePageSize
)
if SyncProtocol.hasCursorRewound(localCursor: currentCursor, serverCursor: page.serverCursor)
|| SyncProtocol.hasIdentityChanged(localIdentity: currentIdentity, serverIdentity: page.syncIdentity)
{
try mirror.clearSyncCursor(scope: scope)
return try await performSync(scope: scope, onBootstrapProgress: onBootstrapProgress)
}
try mirror.applyChanges(scope: scope, changes: page.changes, cursor: page.cursor)
currentCursor = page.cursor
if !page.hasMore {
return currentCursor
}
}
}
}