Skip to content
Merged
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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Add CloudSaveKit to your package dependencies:
```swift
.package(
url: "https://github.com/thatfactory/cloudsavekit.git",
from: "0.1.1"
from: "0.2.4"
)
```

Expand Down Expand Up @@ -90,6 +90,8 @@ try await engine.syncNow()

Automatic synchronization should remain enabled in production. Explicit operations complement the system scheduler; they do not replace durable local saves or make offline networking possible.

For a complete integration checklist, including application capabilities, signing verification, durable-state requirements, shared-zone rules, explicit-operation semantics, and diagnostic guidance, read the [CloudSaveKit documentation](https://thatfactory.github.io/cloudsavekit/documentation/cloudsavekit/).

Explicit operations are serialized per engine. An explicit fetch waits for any fetch already active when the request arrives, then requires a fetch generation that began after the request before reporting success. Because CKSyncEngine completes its API call only after related delegate events finish, success is both a freshness barrier and an apply barrier for the configured zone. A post-request automatic fetch may satisfy the barrier; a pre-request fetch cannot. `freshFetchNotObserved` is raised instead of reporting false success if CKSyncEngine returns without a qualifying generation. A configured-zone fetch error also fails its qualifying explicit request. Privacy-safe stage logs report database discovery, configured-zone delivery counts, per-zone completion, and dirty-state transitions without exposing zone or record identities.

Call `start()` successfully before any explicit synchronization. `fetchNow()` and `sendNow()` throw `CloudSaveEngineError.notStarted` before startup and `CloudSaveEngineError.hostRecoveryRequired` after a host persistence callback fails. Once the local store is healthy again, call `start()` to rebuild from the last successfully persisted CKSyncEngine checkpoint and the host's current durable pending-change ledger.
Expand Down Expand Up @@ -128,4 +130,6 @@ CloudSaveKit logs concise synchronization lifecycle information through [AppLogg
- Swift 6.4
- Xcode 27
- iOS, macOS, tvOS, watchOS, or visionOS 26+
- A CloudKit container with CloudKit and Remote Notifications capabilities
- A CloudKit container and deployed record schema for the intended environment
- iCloud with CloudKit, Push Notifications, and the Remote notifications background mode on application targets
- A signed physical-device build whose application entitlements and provisioning profile contain the expected iCloud container and APNs environment
9 changes: 9 additions & 0 deletions Sources/CloudSaveKit/CloudSaveKit.docc/CloudSaveKit.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ CloudSaveKit wraps Apple's `CKSyncEngine` lifecycle and delegate surface without

Create the engine early in application launch, call ``CloudSaveEngine/start()``, and enqueue changes only after their corresponding local transactions succeed. Observe ``CloudSaveEngine/statusUpdates`` to project synchronization state into the host architecture. The current-state stream begins with ``CloudSaveStatus/idle`` and retains only its latest unconsumed value rather than preserving an event history.

Before integrating the engine, follow <doc:IntegrationGuide> to configure the application target, signing, local-store boundary, and durable state. For user-initiated refresh behavior, see <doc:ExplicitSynchronization>. For private-zone sharing, see <doc:SharingZones>. When server state and device behavior disagree, use <doc:Troubleshooting>.

Automatic synchronization remains enabled by default. Use ``CloudSaveEngine/fetchNow()``, ``CloudSaveEngine/sendNow()``, or ``CloudSaveEngine/syncNow()`` only at user-visible checkpoints where immediate work is useful. Explicit operations are serialized per engine. An explicit fetch waits for pre-request fetch work to drain and requires a post-request fetch generation to complete, including every related host apply, before returning successfully. A newly initialized nil-state engine may reconcile its first matching sign-in while that first explicit fetch is in flight without invalidating the fetch; every later account transition still invalidates current work. Explicit synchronization requires a successful ``CloudSaveEngine/start()`` and raises ``CloudSaveEngineError`` when the engine has not started, host recovery is required, or CKSyncEngine returns without the required fresh generation.

CloudSaveKit forwards only records, record deletions, and custom-zone deletions from its configured custom zone. Owned zones may be created and recovered. Shared zones retain their exact owner-qualified identifier and are never recreated by a participant; initial sign-in and account transitions restore the participant's durable record changes without scheduling a zone save, while lost access requires host reconfiguration. If the host cannot persist a sync-engine checkpoint or apply a CloudKit result, the engine cancels the current work and waits for the host to call ``CloudSaveEngine/start()`` after local recovery. Host callback failures are reported as ``CloudSaveFailure/localPersistence``.
Expand All @@ -16,6 +18,13 @@ CKSyncEngine retains recoverable transport failures and schedules their retries;

## Topics

### Essentials

- <doc:IntegrationGuide>
- <doc:ExplicitSynchronization>
- <doc:SharingZones>
- <doc:Troubleshooting>

### Engine

- ``CloudSaveEngine``
Expand Down
44 changes: 44 additions & 0 deletions Sources/CloudSaveKit/CloudSaveKit.docc/ExplicitSynchronization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Explicit Synchronization

Understand the guarantees and limits of fetch, send, and combined synchronization.

## Overview

Automatic synchronization is the normal production mode. Use ``CloudSaveEngine/fetchNow()``, ``CloudSaveEngine/sendNow()``, or ``CloudSaveEngine/syncNow()`` at user-visible checkpoints where immediate progress matters. These methods complement CKSyncEngine's scheduler; they do not replace durable local commits, guarantee network availability, or disable system-scheduled work.

Call ``CloudSaveEngine/start()`` successfully first. Explicit operations throw ``CloudSaveEngineError/notStarted`` before startup and ``CloudSaveEngineError/hostRecoveryRequired`` after a host persistence failure.

## Fetch freshness and apply

An explicit fetch is both a freshness barrier and an apply barrier for the configured zone. It waits for fetch work already active when the request arrives, then requires a fetch generation that began after the request. CKSyncEngine finishes its API call only after the related delegate events and host apply callbacks complete.

A post-request automatic generation may satisfy the barrier; a pre-request generation cannot. This preserves CKSyncEngine's scheduler while preventing a manual refresh from reporting success merely because older work completed. If no qualifying generation is observed, the operation raises ``CloudSaveEngineError/freshFetchNotObserved``. If the configured zone fails during the qualifying generation, it raises ``CloudSaveEngineError/configuredZoneFetchFailed``.

Explicit operations are serialized per engine. Repeated taps do not create overlapping explicit engine work.

## Send reconciliation

``CloudSaveEngine/sendNow()`` reloads the host's durable pending ledger before sending. It materializes current records through ``CloudSaveClient/record(for:)`` and revalidates asynchronous results against the current engine lifecycle.

A `nil` materialization means the record no longer exists and is reconciled with the current ledger. A thrown materialization error is a host failure and stops synchronization. After CloudKit acknowledges a save or deletion, CloudSaveKit rereads the durable ledger before removing completed work so a newer mutation of the same record is not erased by an older acknowledgement.

## Interpreting transfer counts

An application's operation summary should be interpreted as a snapshot of work attributed to that explicit operation, not as an engine-lifetime counter. CKSyncEngine can run a push-driven scheduled generation immediately before or during a user action. That scheduled generation may apply the remote record before the explicit generation completes, leaving the final manual snapshot with zero downloads even though the visible data is fresh.

When diagnosing a manual refresh, correlate the complete ordered timeline:

```text
scheduled or manual generation starts
database reports configured zone changed
configured zone delivers records or deletions
host applies the batch
qualifying generation completes
user-visible operation completes
```

Do not infer that no download occurred from the final count alone.

## Lifecycle invalidation

Account changes, host failures, cancellation, and engine replacement invalidate stale explicit operations and asynchronous materialization. A newly initialized nil-state engine has one narrow exception: its first matching sign-in event establishes the initial account and may finish reconciliation without invalidating the bootstrap fetch. Every later sign-in, sign-out, or account switch retains full lifecycle invalidation.
63 changes: 63 additions & 0 deletions Sources/CloudSaveKit/CloudSaveKit.docc/IntegrationGuide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Integrating CloudSaveKit

Configure a host application for durable private or shared CloudKit synchronization.

## Overview

CloudSaveKit coordinates CKSyncEngine, but the host remains the source of truth for local data. A correct integration combines application capabilities and signing, a durable ``CloudSaveClient``, an owner-aware zone configuration, and lifecycle orchestration that starts synchronization only after account-scoped persistence is ready.

## Configure the application target

Enable iCloud with CloudKit, select the intended container, enable Push Notifications, and enable the Remote notifications background mode. Deploy the record schema to every CloudKit environment the application will use.

The target's Xcode settings are not proof that a distributed or locally installed binary has the required capabilities. For physical-device testing, inspect both the signed application entitlements and the embedded provisioning profile. They must contain the expected iCloud container and an APNs environment appropriate to the build. Reinstall after changing capabilities or profiles.

Push capability is operationally important even when the application exposes a manual refresh button. CKSyncEngine relies on CloudKit notifications to discover database changes efficiently. A build without a valid APNs entitlement may upload successfully while a receiving device repeatedly completes fetch calls without discovering a changed zone.

## Implement the durable client boundary

Implement ``CloudSaveClient`` in the actor that owns the local store. Its callbacks form a transactional durability boundary:

- ``CloudSaveClient/pendingChanges()`` returns every locally committed change not yet acknowledged by CloudKit.
- ``CloudSaveClient/record(for:)`` materializes the latest local representation for a pending save.
- ``CloudSaveClient/persist(stateSerialization:)`` stores every opaque CKSyncEngine checkpoint.
- ``CloudSaveClient/applyFetchedChanges(records:deletedRecordIDs:)`` applies one fetched batch atomically.
- ``CloudSaveClient/didSave(records:)`` persists returned system fields before acknowledging uploads.
- ``CloudSaveClient/didDelete(recordIDs:)`` acknowledges successfully sent local pending deletions after CloudKit reports them in `SentRecordZoneChanges`; fetched deletions arrive through ``CloudSaveClient/applyFetchedChanges(records:deletedRecordIDs:)``.
- Account, conflict, deleted-zone, and failure callbacks update host-owned state and policy.

Commit application data and its pending ledger entry in one local transaction before calling ``CloudSaveEngine/enqueue(_:)``. Treat the durable ledger, not CKSyncEngine's in-memory queue, as the source of truth across termination and recovery.

Store server system fields with local records so later updates retain CloudKit change tags. Resolve conflicts using application semantics and base retries on the supplied server record.

## Preserve checkpoint provenance

`CKSyncEngine.State.Serialization` is opaque. Store it durably after every callback and restore it through ``CloudSaveConfiguration/stateSerialization``. A host that supports multiple accounts, inventories, database scopes, or zones should bind each serialization to that exact context and reject mismatches before creating an engine. CloudSaveKit cannot infer whether an otherwise valid opaque checkpoint belongs to the host's current domain identity.

If the host intentionally performs a nil-state recovery, keep it bounded and recoverable. Do not erase user records or pending mutations merely to reset CloudKit state, and do not send pending changes until the recovered checkpoint and topology have been validated.

## Configure one exact zone

For an owned zone, use the private database and a ``CloudSaveConfiguration`` initialized with the zone. For a shared zone, use the shared database and the exact owner-qualified zone identifier:

```swift
let configuration = CloudSaveConfiguration(
database: container.sharedCloudDatabase,
stateSerialization: restoredState,
sharedZoneID: acceptedZoneID
)
```

The initializers enforce the database-scope pairing. Preserve the complete `CKRecordZone.ID`, including its owner name. A zone name alone is not sufficient for a shared zone.

## Start and recover

Construct the engine only after restoring the matching checkpoint and durable pending ledger, then call ``CloudSaveEngine/start()``. Do not call explicit operations before startup succeeds.

If a host persistence callback fails, CloudSaveKit invalidates current work and raises ``CloudSaveEngineError/hostRecoveryRequired`` for new explicit operations. Repair the local-store problem and call ``CloudSaveEngine/start()`` again; the engine rebuilds from the last checkpoint that the host successfully persisted and reloads the durable ledger.

Account transitions invalidate old operations before the host switches account-scoped persistence. In ``CloudSaveClient/handle(accountChange:)``, the host switches or validates account-scoped local persistence and restores the new account's durable pending ledger. The callback cannot replace serialized CKSyncEngine state in the running engine: state serialization is supplied only when constructing ``CloudSaveConfiguration``, and CKSyncEngine performs its own account-change state reset. Never allow one account's pending records or host-bound checkpoint provenance to enter another account's persistence context.

## Verify the integration

Test local-first offline creation, relaunch with pending work, conflict resolution, deletion, account transitions, host callback failure and restart, shared-zone revocation, concurrent enqueue and send, and fetched changes applied before user-visible completion. For sharing, perform a two-device test with separate iCloud accounts and verify both upload directions.
42 changes: 42 additions & 0 deletions Sources/CloudSaveKit/CloudSaveKit.docc/SharingZones.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Sharing Custom Zones

Use one owner-created private zone from both private and shared database views.

## Overview

CloudKit zone sharing has two roles:

- The owner creates a custom zone in the private database and shares that zone.
- A participant accepts the share and accesses the owner's exact zone through the shared database.

CloudSaveKit keeps those lifecycles distinct. An owned configuration may create or recover its zone. A shared configuration never creates the owner's zone.

## Create and present a share

Use ``CloudSaveSharingCoordinator/ensureZoneWideShare(for:)`` with an owned zone identifier. The returned `CKShare` is UI-independent; the host decides how to present the system sharing interface and which permissions to offer.

Sharing an individual root record is not supported. CloudSaveKit expects a zone-wide share so every synchronized record in the configured zone has one consistent topology.

## Accept and retain the invitation

Pass system-provided `CKShare.Metadata` to ``CloudSaveSharingCoordinator/accept(metadata:)``. CloudSaveKit validates that the invitation belongs to the coordinator's container and represents a zone-wide share, then returns the owner-qualified `CKRecordZone.ID`.

Persist that complete identifier with the host's inventory or account binding. Configure ``CloudSaveConfiguration`` with the shared database and `sharedZoneID`. Never reduce the identity to the zone name: two owners can use the same zone name.

The host application must receive share metadata through its platform lifecycle and decide which accepted shared zone belongs to its product. ``CloudSaveSharingCoordinator/sharedRecordZones()`` can discover accessible shared zones, but CloudSaveKit does not choose among them.

## Handle lost access

If the participant loses access or the shared zone disappears, CloudSaveKit raises ``CloudSaveEngineError/reconfigurationRequired``. Return the application to its sharing or inventory-selection flow. Do not silently create a private zone with the same name; that would fork the data into a different inventory.

## Test both views

Use two physical devices signed into separate iCloud accounts:

1. Create and share the owner's zone.
2. Accept the invitation on the participant account.
3. Upload one unique record from the owner and verify it appears on the participant after automatic or one explicit synchronization.
4. Upload a different record from the participant and verify it appears on the owner.
5. Confirm the participant remains configured with the shared database and the owner's full zone identifier throughout relaunch.

Simulator share-link handoff and push behavior can differ from physical devices. Use simulators for deterministic application tests, but treat a two-device signed-build run as the significant end-to-end acceptance.
Loading