Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion apps/desktop/src-tauri/src/macos_cloudkit_bridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,26 @@ static void mindwtr_ck_ensure_container(void) {
return strdup("{\"ok\":true}");
}

/// CloudKit server-side error code for queries against a record type that does
/// not yet exist in the container schema. Observed in the NSUnderlyingError of
/// CKErrorServerRejectedRequest when querying a Development-environment
/// container before any records of that type have been saved.
/// Apple does not publish this constant; it was determined empirically and is
/// stable across macOS 13–15 and iOS 16–18.
static const NSInteger kCKServerErrorUnknownRecordType = 2003;

/// Locale-independent check for "unknown record type" inside a
/// CKErrorServerRejectedRequest. Inspects the NSUnderlyingError code rather
/// than localizedDescription so this works on non-English systems.
static BOOL ck_is_unknown_record_type(NSError *error) {
NSError *underlying = error.userInfo[NSUnderlyingErrorKey];
if ([underlying isKindOfClass:[NSError class]] &&
underlying.code == kCKServerErrorUnknownRecordType) {
return YES;
}
return NO;
}

// ---------------------------------------------------------------------------
// MARK: - Field specs (mirrors CloudKitRecordMapper.swift exactly)
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -472,7 +492,21 @@ static void ck_apply_fields(NSDictionary *json, CKRecord *record, NSString *reco

long waited = dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, kTimeoutSec * NSEC_PER_SEC));
if (waited != 0) return ck_copy_json(@{@"error": @"fetch-timeout"});
if (batchError) return ck_error_json(batchError);
if (batchError) {
// Record type not yet created in CloudKit schema — treat as empty.
// The type is auto-created on first save in the Development environment.
// CKErrorUnknownItem: record type unknown to the client framework.
// CKErrorServerRejectedRequest: server rejects the query because the
// record type doesn't exist in the schema yet. We check the
// underlying server error code (2003 = "UNKNOWN_RECORD_TYPE") which
// is locale-independent, unlike localizedDescription.
if (batchError.code == CKErrorUnknownItem ||
(batchError.code == CKErrorServerRejectedRequest &&
ck_is_unknown_record_type(batchError))) {
return strdup("[]");
}
return ck_error_json(batchError);
}

for (CKRecord *r in batchRecords) {
[allResults addObject:ck_json_from_record(r)];
Expand Down
35 changes: 32 additions & 3 deletions apps/mobile/modules/cloudkit-sync/ios/CloudKitSyncManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,15 @@ final class CloudKitSyncManager {

// MARK: - Full Fetch

/// CloudKit server error code for queries against a record type that does
/// not yet exist in the container schema (Development environment).
/// Not published by Apple; determined empirically, stable across iOS 16-18.
private static let ckServerErrorUnknownRecordType = 2003

/// Fetches all records of a given type from the custom zone.
/// Returns an empty array when the record type does not exist yet in the
/// CloudKit schema (first sync). The subsequent write phase auto-creates
/// the record type in the Development environment.
func fetchAllRecords(recordType: String) async throws -> [CKRecord] {
var allRecords: [CKRecord] = []
var cursor: CKQueryOperation.Cursor?
Expand All @@ -356,9 +364,14 @@ final class CloudKitSyncManager {
initialOp.zoneID = zoneID
initialOp.qualityOfService = .userInitiated

let firstResult = try await runQueryOperation(initialOp)
allRecords.append(contentsOf: firstResult.records)
cursor = firstResult.cursor
do {
let firstResult = try await runQueryOperation(initialOp)
allRecords.append(contentsOf: firstResult.records)
cursor = firstResult.cursor
} catch {
if Self.isUnknownRecordTypeError(error) { return [] }
throw error
}

while let nextCursor = cursor {
let continueOp = CKQueryOperation(cursor: nextCursor)
Expand All @@ -372,6 +385,22 @@ final class CloudKitSyncManager {
return allRecords
}

/// Locale-independent check for a missing record type.
/// CKErrorUnknownItem: client framework doesn't recognize the type.
/// CKErrorServerRejectedRequest with underlying code 2003: server rejects
/// the query because the type doesn't exist in the schema yet.
private static func isUnknownRecordTypeError(_ error: Error) -> Bool {
if let ckError = error as? CKError {
if ckError.code == .unknownItem { return true }
if ckError.code == .serverRejectedRequest,
let underlying = ckError.userInfo[NSUnderlyingErrorKey] as? NSError,
underlying.code == ckServerErrorUnknownRecordType {
return true
}
}
return false
}

private func runQueryOperation(_ op: CKQueryOperation) async throws -> (records: [CKRecord], cursor: CKQueryOperation.Cursor?) {
return try await withCheckedThrowingContinuation { continuation in
var records: [CKRecord] = []
Expand Down