> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ditto.live/llms.txt
> Use this file to discover all available pages before exploring further.

# Swift V4→V5 API Migration Guide

> Guide for migrating Swift apps from Ditto v4 to v5.

## Overview

This guide covers the essential changes needed to migrate your Ditto Swift app from v4 to v5. The main architectural shift is moving from identity-based initialization to a four-phase configuration model with async/await APIs.

**Also required:** v5 uses DQL (Ditto Query Language) for all data operations. See the [DQL Migration Guide](./swift-dql) for query migration steps.

***

## AI Agent Prompt

Use this prompt when working with an AI coding assistant to migrate your Ditto Swift app from v4 to v5.

<Accordion title="Copy AI Migration Prompt (Click to Expand)">
  ````text theme={null}
  I need help migrating a Ditto Swift application from v4 to v5. Focus on these critical changes:

  VERIFY AGAINST GROUND TRUTH:
  Before changing any call site, verify the exact v5 signature against the API reference rather than trusting prose. The v5 reference is a JS-rendered DocC site that returns a blank shell to web-fetch tools, but the underlying DocC JSON is machine-readable and gives exact signatures and member lists. Fetch the per-symbol JSON at:
    https://software.ditto.live/cocoa/DittoSwift/<version>/api-reference/data/documentation/dittoswift/<symbol>.json
  The SDK version is part of the URL — replace `<version>` with the exact version you are upgrading TO (e.g. `5.0.1`, `5.0.0`, or any later `5.x` release). Signatures can differ between patch releases, so use your target version, not the example. For example, for 5.0.1 the `DittoStore` symbol is at `.../DittoSwift/5.0.1/api-reference/data/documentation/dittoswift/dittostore.json`, `DittoStoreObserver` at `.../dittoswift/dittostoreobserver.json`, etc. Lowercase the symbol name. When a signature below disagrees with the DocC JSON for your version, trust the JSON.
  When you need a member (initializer, method, property, nested enum), do NOT hand-construct its slug from a guessed signature — that is the main source of 404s (e.g. assuming a `databaseID:connect:persistenceDirectory:identityID:...` init that doesn't exist; the real init has no `identityID`). Instead fetch the parent TYPE's JSON (e.g. `dittoconfig.json`): its `topicSections` lists the exact slug of each member and its declaration fragments give the authoritative signature, so you anchor on real symbols rather than guessing. To browse symbol names, the human-readable index is at `https://software.ditto.live/cocoa/DittoSwift/<version>/api-reference/documentation/dittoswift/`.

  REMOVED PLATFORM SUPPORT (CHECK FIRST):
  Before migrating, confirm the app does not target a platform that v5 dropped. v5 removes support for:
  - tvOS — no longer supported in v5.0
  - visionOS (beta) — no longer supported in v5.0
  If the project targets tvOS or visionOS, STOP and flag this: there is no v5 SDK for these platforms, so the app cannot upgrade to v5 while it ships on them. The user must drop the platform target or stay on v4. Do not attempt to migrate these targets — surface the blocker and have them reach out to Ditto customer support.

  INITIALIZATION:


  - Parameter labels are case-sensitive: `databaseID` (uppercase `ID`), and `ditto.config.databaseID` replaces `ditto.appID`.
  - `DittoSwift` no longer re-exports `Foundation`. In v4, importing `DittoSwift` transitively brought in `Foundation` symbols (`URL`, `Date`, `NSLog`, `Notification`, etc.); in v5 it does not. Any file that uses a Foundation type now needs an explicit `import Foundation`. Audit every file that imports `DittoSwift` without also importing `Foundation`.
  - In v4 when adding the `DittoSwift` package, it would add in the `DittoObjC` package product dependencies. In v5, `DittoObjC` is no longer a separate product — it's bundled into `DittoSwift`. This means that you no longer need to add the `DittoObjC` package product dependencies to your project and you need to remove the `DittoObjC` package product dependencies from your project in order to get the project to build.
  - These are the following places the `DittoObjC` package product dependencies are usually used:
      - 1. `PBXBuildFile` section — removed the `DittoObjC in Frameworks` entry
      - 2. `PBXFrameworksBuildPhase` — removed `DittoObjC in Frameworks` from the `files` array
      - 3. `PBXNativeTarget.packageProductDependencies` — removed the `DittoObjC` reference
      - 4. `XCSwiftPackageProductDependency` — removed the entire `DittoObjC` dependency entry
  - Replace `Ditto(identity:)` with `try await Ditto.open(config:)` using async/await.
  - Create a `DittoConfig` with `databaseID` and a connect mode
  - `.onlinePlayground(appID:token:enableDittoCloudSync:customAuthURL:)` → `DittoConfig(databaseID: appID, connect: .server(url:))` + the `token` moves to auth: set `ditto.auth?.expirationHandler` and call `login(token:provider: .development)` inside it. Do NOT drop the token — only `appID` and the connect mode go into `DittoConfig`; the token is no longer part of the identity/config.
  - `.onlineWithAuthentication(appID:enableDittoCloudSync:customAuthURL:)` → `DittoConfig(databaseID: appID, connect: .server(url:))` + auth `expirationHandler`
  - `.server(url:)` takes a `URL`, not a `String`. The case is `case server(url: URL)`. Emit `.server(url: URL(string: "https://...")!)` (or a safely-unwrapped `URL`) — a string literal will not compile.
  - The `.server` URL is environment-specific: copy the **URL** from the Ditto Portal (Connect via SDK) as-is. Do NOT construct it from the database ID. This is the portal's **URL** value — an `https://` URL. v5 takes no separate WebSocket (`wss://`) URL; do not paste a `wss://` value here.
  - `.offlinePlayground(appID:)` → `DittoConfig(databaseID: appID, connect: .smallPeersOnly())`
  - `.offlinePlayground()` (zero-arg form, no `appID`) → `DittoConfig(databaseID: "<your-database-id>", connect: .smallPeersOnly())`. The zero-arg v4 form had no `appID`, but v5 `databaseID` is required — supply any non-empty string. For tests/playgrounds, `"test"` is fine.
  - `.sharedKey(appID:sharedKey:)` → `DittoConfig(databaseID: appID, connect: .smallPeersOnly(privateKey: sharedKey))`
  - `.manual(certificateConfig:)` → **No replacement** — removed in v5
  - `.smallPeersOnly()` is equivalent to `.smallPeersOnly(privateKey: nil)` — the parameter has a default value of `nil`, so prefer the shorter form when there is no shared key.
  - The `Ditto(identity:)` initializer is removed. You MUST use `try await Ditto.open(config:)` or `Ditto.openSync(config:)`
  - Remove `disableSyncWithV3()` calls — v3 protocol is fully dropped in v5
  - Remove `updateTransportConfig` workaround patterns for adding websocket URLs — `DittoConfig` handles the server URL directly. Note: `updateTransportConfig` itself still exists in v5 for advanced transport configuration
  - Remove `ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")` — v5 defaults to `false`
  - IMPORTANT: If your v4 app used the default (strict=true), set `DQL_STRICT_MODE=true` BEFORE starting sync to maintain v4 behavior. Objects that were replaced whole in v4 will silently merge at field level in v5 without this.
  - v5 default persistence directory is `ditto-{databaseID}` (was `ditto` in v4). Set `persistenceDirectory` explicitly if maintaining v4 directory structure.
  - `persistenceDirectory` is an initializer parameter on `DittoConfig`. The full init signature is:
    ```swift
    public init(databaseID: String,
                connect: DittoConfigConnect,
                persistenceDirectory: URL? = nil,
                experimental: DittoConfigExperimental = .init())
    ```
    Pass it at construction:
    ```swift
    let config = DittoConfig(
        databaseID: "my-app",
        connect: .server(url: serverURL),
        persistenceDirectory: URL(fileURLWithPath: "/app/sandbox/ditto")
    )
    ```
  - Configure logging via the `DittoLogger` type (e.g. `DittoLogger.minimumLogLevel = .info`); `DittoConfig` itself exposes only `databaseID`, `connect`, `persistenceDirectory`, and `experimental`.
  - Create the instance with `try await Ditto.open(config:)` (async) or `try Ditto.openSync(config:)` (sync). Where to use which: use `try await Ditto.open(config:)` from an async context (`Task`, async function). Use `try Ditto.openSync(config:)` from a non-async context — most importantly a SwiftUI `App.init()`, which is not async and cannot `await`. `App.init` is an extremely common host for Ditto setup, so prefer `openSync` there rather than wrapping setup in a detached `Task`.

  SYNC:
  The sync API moved from top-level Ditto methods to the `ditto.sync` sub-object.
  - `try ditto.startSync()` → `try ditto.sync.start()`
  - `ditto.stopSync()` → `ditto.sync.stop()`
  - `ditto.isSyncActive` → `ditto.sync.isActive`

  AUTHENTICATION:
  The `DittoAuthenticationDelegate` protocol is removed. Replace with the `expirationHandler` closure.
  - Set `ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in ... }` after `Ditto.open(config:)`
  - The handler signature is `@Sendable (_ ditto: Ditto, _ timeUntilExpiration: TimeInterval) async -> Void`
  - Inside the handler, call `expiredDitto.auth?.login(token:provider:completion:)` (use a distinct closure parameter name to avoid shadowing the outer `ditto`)
  - Use `.development` provider for playground/test tokens
  - The handler is called when `timeUntilExpiration == 0` (initial auth or expired) and when `timeUntilExpiration > 0` (token expiring soon — refresh). Handle both cases.
  - `authenticator.loginWithToken(_:provider:completion:)` → `ditto.auth?.login(token:provider:completion:)`
  - `DittoAuthenticationDelegate.authenticationRequired(authenticator:)` → removed, use `expirationHandler` with `timeUntilExpiration == 0`
  - `DittoAuthenticationDelegate.authenticationExpiringSoon(authenticator:secondsRemaining:)` → removed, use `expirationHandler` with `timeUntilExpiration > 0`
  - `ditto.delegate = MyAuthDelegate()` → `ditto.auth?.expirationHandler = { ... }`

  OBSERVERS:
  The callback-based `registerObserver` API is the same in v5 — it returns `DittoStoreObserver` with callback handlers.
  - `ditto.store.registerObserver(query:arguments:deliverOn:handler:)` → same API, returns `DittoStoreObserver`
  - v5 adds `Codable` argument overloads: `registerObserver(query:arguments: Codable, deliverOn:handler:)`
  - Call `item.dematerialize()` after extracting data from `DittoQueryResultItem` to free native memory — do not use the item after this call
  - Tear down with `observer.cancel()`.

  BACK PRESSURE (Signal Next):
  Use `registerObserver(query:arguments:deliverOn:handlerWithSignalNext:)` when your handler needs time to process results.
  - Changes that occur while your handler is processing are coalesced — you get one callback with the latest state, not every intermediate change
  - You must call `signalNext()` to receive the next update
  - Standard observer auto-signals after handler returns

  COLLECTION API → DQL:
  All `.collection()` APIs are **removed**. Replace with `store.execute()` using DQL.
  - `ditto.store.collection("cars").find("color == 'blue'").exec()` → `try await ditto.store.execute(query: "SELECT * FROM cars WHERE color = :color", arguments: ["color": "blue"])`
  - `ditto.store.collection("cars").findByID(id).exec()` → `try await ditto.store.execute(query: "SELECT * FROM cars WHERE _id = :id", arguments: ["id": id])`
  - `ditto.store.collection("cars").upsert(value)` → `try await ditto.store.execute(query: "INSERT INTO cars DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE", arguments: ["doc": value])`
  - `ditto.store.collection("cars").findByID(id).update { doc in doc?["color"].set("green") }` → `try await ditto.store.execute(query: "UPDATE cars SET color = :color WHERE _id = :id", arguments: ["color": "green", "id": id])`
  - `ditto.store.collection("cars").findByID(id).remove()` → `try await ditto.store.execute(query: "DELETE FROM cars WHERE _id = :id", arguments: ["id": id])`
  - `ditto.store.collection("cars").findByID(id).evict()` → `try await ditto.store.execute(query: "EVICT FROM cars WHERE _id = :id", arguments: ["id": id])`
  - Always use parameterized queries with `:paramName` — NEVER use string interpolation in DQL queries

  LIVE QUERIES → STORE OBSERVERS:
  - `collection.find("...").observeLocal { docs, event in ... }` → `ditto.store.registerObserver(query: "SELECT * FROM ...", arguments: [...]) { result in ... }`
  - `liveQuery?.stop()` → `observer.cancel()`
  - v4 handler: `(docs: [DittoDocument], event: DittoLiveQueryEvent) -> Void`
  - v5 handler: `(result: DittoQueryResult) -> Void` where result has `.items` containing `DittoQueryResultItem` objects
  - Extract your model from `item.jsonData()` then call `item.dematerialize()` to free native memory
  - When using `item.jsonData()`, you need to make sure you import the Swift Foundation framework

  SUBSCRIPTIONS:
  - `ditto.store.collection("cars").find("color == 'red'").subscribe()` → `try ditto.sync.registerSubscription(query: "SELECT * FROM cars WHERE color = :color", arguments: ["color": "red"])`
  - `arguments:` is optional — the signature is `registerSubscription(query:arguments:)` with `arguments: [String: Any?]? = nil`. For an unparameterized subscription like `SELECT *`, call `try ditto.sync.registerSubscription(query: "SELECT * FROM cars")` with no `arguments:`.
  - `DittoSubscription` → replaced by `DittoSyncSubscription`
  - Tear down a subscription with `subscription.cancel()`.

  TRANSACTIONS:
  - Existing `store.transaction(hint:with:)` calls still compile in v5 (the new parameters have defaults), so no change is required.
  - v5 signature is `transaction(hint: String? = nil, isReadOnly: Bool = false, with scope: DittoTransactionScope<T>)` where `DittoTransactionScope<T> = (DittoTransaction) async throws -> T`.
  - For read-only transactions, pass `isReadOnly: true` — it lets Ditto optimize the transaction.

  COUNTERS:
  - `doc?["viewCount"].counter?.increment(by: 1.0)` → `try await ditto.store.execute(query: "UPDATE cars APPLY viewCount PN_INCREMENT BY :n WHERE _id = :id", arguments: ["n": 1, "id": id])`
  - DO NOT initialize counter fields in insert documents. Counters are created on first `PN_INCREMENT`. Inserting `0` creates a REGISTER, not a COUNTER.
  - Decrement by passing a negative value: `["n": -1]`

  PRESENCE:
  - `peer.peerKeyString` → `peer.peerKey` (type is `String` in both v4 and v5, but property renamed)
  - `peer.osV2` → `peer.os`
  - `peer.isConnectedToDittoCloud` → `peer.isConnectedToDittoServer`
  - `connection.peerKeyString1` / `peerKeyString2` → `connection.peer1` / `connection.peer2` (now `String`)
  - `connection.approximateDistanceInMeters` → **removed, no replacement**
  - `ditto.observePeers(_:)` → `ditto.presence.observe(didChangeHandler:)`
  - `DittoRemotePeer` struct (deprecated) → use `DittoPeer` in `DittoPresenceGraph`
  - `DittoAddress` struct → removed

  DITTO CLASS:
  - `Ditto(identity:)` → `try await Ditto.open(config:)` or `Ditto.openSync(config:)`
  - `ditto.activated` → `ditto.isActivated`
  - `ditto.appID` → `ditto.config.databaseID`
  - `ditto.transportDiagnostics()` → removed
  - `ditto.runGarbageCollection()` → removed (automatic in v5)
  - `ditto.disableSyncWithV3()` → removed (v3 protocol dropped)
  - `ditto.isHistoryTrackingEnabled` → removed

  DISK USAGE:
  - `DiskUsage` → `DittoDiskUsage`
  - `diskUsage.exec` → `diskUsage.item`
  - `DiskUsageObserverHandle` → `DittoDiskUsageObserver`
  - `DiskUsageItem` → `DittoDiskUsageItem` (property names `.path`, `.sizeInBytes`, `.childItems` are unchanged)

  LOGGING:
  - `DittoLogger` class name is unchanged in both v4 and v5, but is now `final` and `Sendable` in v5
  - `DittoLogger.enabled` → `DittoLogger.isEnabled` (e.g. `DittoLogger.enabled = false` → `DittoLogger.isEnabled = false`)
  - `DittoLogger.setLogFile()` → removed — use `export(to:)` instead

  ERRORS:
  - `DittoSwiftError` typealias → removed, use `DittoError` directly
  - `migrationError` error case → removed

  TRANSPORT CONFIG:
  - `DittoHTTPListenConfig.staticContentPath` → removed
  - Default `webSocketSync` — check if your app relied on v4 default; explicitly set `true` if needed

  TYPES & PROTOCOLS:
  - `DittoConnectionPriority` enum → removed
  - `DittoTransportSnapshot` struct → removed (with transport diagnostics)
  - `DittoExperimental.jsonByTranscoding(cbor:)` → removed
  - All public types are now `final` and `Sendable` for Swift concurrency
  - `DittoConfig` and `DittoConfigConnect` conform to `Sendable` and `Codable`
  - `DittoConnection` now conforms to `Identifiable`, `Equatable`, `Hashable`, and `Sendable`

  SWIFT 6 COMPATIBILITY:
  - `DittoConnection` gaining `Identifiable` conformance causes `filter` ambiguity in Swift 6. Fix: annotate closure parameter explicitly:
    ```swift
    // BROKEN in Swift 6
    connections.filter { $0.peer1 == localKey }
    // FIXED
    connections.filter { (conn: DittoConnection) in conn.peer1 == localKey }
    ```
  - Auth handler: `expirationHandler` closure is `@Sendable` and `async`. If your Ditto manager is an `actor`, use `await` when accessing actor-isolated state inside `Task { }` blocks within the handler.
  - DELETE any v4-era retroactive `Sendable` shims. v5 declares `Sendable` in-module for `Ditto`, `DittoQueryResult`, `DittoTransactionCompletionAction`, `DittoStoreObserver`, and more. A leftover `extension X: @retroactive @unchecked Sendable {}` (common in Swift 6 codebases that adopted strict concurrency before the SDK did) now produces: `conformance of 'X' to protocol 'Sendable' was already stated in the type's module 'DittoSwift'`. Search for `@retroactive` and `@unchecked Sendable` extensions on Ditto types and remove them.

  REMOVED APIs (no replacement):
  - `ditto.transportDiagnostics()` — low-level transport debug info
  - `ditto.runGarbageCollection()` — CRDT GC controls
  - `ditto.disableSyncWithV3()` — v3 protocol fully dropped
  - `ditto.isHistoryTrackingEnabled` — history tracking removed
  - `DittoIdentity.manual(certificateConfig:)` — manual identity type
  - `Connection.approximateDistanceInMeters` — peer distance estimation
  - `DittoHTTPListenConfig.staticContentPath` — static file serving
  - `DittoStore.queriesHash(queries:)` — hash-based query deduplication
  - `DittoStore.queriesHashMnemonic(queries:)` — query mnemonic hashing
  - `DittoConnectionPriority` enum — connection priority
  - `DittoTransportSnapshot` struct — removed with transport diagnostics
  - `DittoRemotePeer` struct (deprecated) — use `DittoPeer` in `DittoPresenceGraph`
  - `DittoAddress` struct — removed
  - `DittoExperimental.jsonByTranscoding(cbor:)` — CBOR-to-JSON utility
  - `DittoSwiftError` typealias — use `DittoError` directly
  - `migrationError` error case — removed
  - `DittoLogger.setLogFile()` — use `export(to:)` instead

  NEW APIs:
  - `Ditto.open(config:): async throws -> Ditto` — async factory method
  - `Ditto.openSync(config:): throws -> Ditto` — synchronous factory alternative
  - `ditto.config: DittoConfig` — read the config used to open this instance
  - `ditto.isActivated: Bool` — replaces `ditto.activated`
  - `DittoStore.newAttachment(data: Data, metadata:)` — create attachment from in-memory `Data` (v4 was file path only)
  - `DittoStore.registerObserver(query:arguments: some Codable, ...)` — overloads accepting `Codable` query arguments
  - `DittoSync.registerSubscription(query:arguments: some Codable)` — overload accepting `Codable` arguments
  - `DittoDiskUsageObserver` — standalone observer class (was nested `DiskUsage.DiskUsageObserverHandle`)
  - `DittoDiskUsageItem` — renamed from `DiskUsageItem`
  - `DittoSyncSubscription.queryArgumentsCBORData` / `.queryArgumentsJSONData` / `.id` — introspection properties
  - `DittoConnection` conforms to `Identifiable`, `Equatable`, `Hashable`, and `Sendable`
  - All public types now `final` and `Sendable` for Swift concurrency
  - `DittoConfig` and `DittoConfigConnect` conform to `Sendable` and `Codable`
  - `DittoSmallPeerInfo.metadataJSONString` removed — use `metadataJSONData` instead (which already existed in v4)

  BEFORE (v4):
  ```swift
  let ditto = Ditto(
      identity: .onlinePlayground(
          appID: "your-app-id",
          token: "your-token",
          enableDittoCloudSync: false,
          customAuthURL: URL(string: "REPLACE_ME_WITH_YOUR_URL")
      )
  )

  ditto.updateTransportConfig { config in
      config.connect.webSocketURLs.insert("wss://...")
  }
  ditto.disableSyncWithV3()
  try? ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")
  try? ditto.startSync()

  // Observer — callback with manual cleanup
  var observer: DittoStoreObserver?
  observer = ditto.store.registerObserver(
      query: "SELECT * FROM cars WHERE color = :color",
      arguments: ["color": "red"]
  ) { result in
      DispatchQueue.main.async {
          self.cars = result.items.compactMap { Car($0.jsonData()) }
      }
  }

  // Must manually cancel in deinit
  deinit { observer?.cancel() }

  // Presence
  ditto.observePeers { peers in
      for peer in peers {
          print(peer.peerKeyString, peer.isConnectedToDittoCloud)
      }
  }

  // Cleanup
  observer?.cancel()
  ditto.stopSync()
  ```

  AFTER (v5):
  ```swift
  let config = DittoConfig(
      databaseID: "your-database-id",
      connect: .server(url: URL(string: "REPLACE_ME_WITH_YOUR_URL")!)
  )
  let ditto = try await Ditto.open(config: config)

  // Authentication
  ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in
      expiredDitto.auth?.login(token: "your-token", provider: .development) { clientInfo, error in
          if let error = error {
              print("Ditto auth failed: \(error), client info: \(String(describing: clientInfo)), time until expiration: \(timeUntilExpiration)s")
          }
      }
  }

  // If maintaining v4 behavior, set strict mode BEFORE starting sync
  // try await ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = true")

  try ditto.sync.start()

  // Observer — callback-based, same pattern as v4 but with dematerialize()
  let observer = try ditto.store.registerObserver(
      query: "SELECT * FROM cars WHERE color = :color",
      arguments: ["color": "red"]
  ) { result in
      let cars = result.items.compactMap { item -> Car? in
          let car = Car(item.jsonData())
          item.dematerialize()  // Free native memory — do not use item after this
          return car
      }
      DispatchQueue.main.async { self.cars = cars }
  }
  // Cleanup: observer.cancel()

  // Subscription — DQL-based
  let sub = try ditto.sync.registerSubscription(
      query: "SELECT * FROM cars WHERE color = :color",
      arguments: ["color": "red"]
  )

  // Presence
  ditto.presence.observe { graph in
      let allPeers = [graph.localPeer] + graph.remotePeers
      for peer in allPeers {
          print(peer.peerKey, peer.isConnectedToDittoServer)
      }
  }

  // Cleanup
  sub.cancel()
  ditto.sync.stop()
  ```

  CHECKLIST:

  Initialization:
  - Replace `Ditto(identity:)` with `try await Ditto.open(config:)`
  - Add `try await` for initialization — `Ditto.open(config:)` is async and throws
  - Create `DittoConfig` with `databaseID` and connect mode (`.server(url:)` or `.smallPeersOnly(privateKey:)`)
  - Set `DQL_STRICT_MODE=true` BEFORE `sync.start()` if maintaining v4 strict behavior
  - Set `persistenceDirectory` if maintaining v4 directory structure
  - Remove `disableSyncWithV3()` calls
  - Remove `updateTransportConfig` workaround patterns for websocket URLs (method still exists for advanced use)
  - Remove `ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")` (v5 default)

  Authentication:
  - Remove `DittoAuthenticationDelegate` conformance and class
  - Remove `ditto.delegate = MyAuthDelegate()` assignment
  - Set `ditto.auth?.expirationHandler` closure after `Ditto.open(config:)`
  - Handler signature is `@Sendable (_ ditto: Ditto, _ timeUntilExpiration: TimeInterval) async -> Void`
  - Use `timeUntilExpiration == 0` check for initial auth vs `> 0` for refresh
  - Use `.development` provider for playground/test tokens

  Sync:
  - Replace `try ditto.startSync()` with `try ditto.sync.start()`
  - Replace `ditto.stopSync()` with `ditto.sync.stop()`
  - Replace `ditto.isSyncActive` with `ditto.sync.isActive`

  Observers:
  - Replace `collection.observeLocal { }` with `store.registerObserver(query:arguments:handler:)`
  - Call `item.dematerialize()` after extracting data from `DittoQueryResultItem`
  - Use `handlerWithSignalNext:` for back-pressure control on expensive operations
  - Cleanup with `observer.cancel()`

  Data Operations:
  - Replace all `.collection("x").find(...)` chains with `store.execute(query:arguments:)` DQL
  - Use parameterized queries with `:paramName` — never string interpolation
  - Replace `.upsert()` with `INSERT INTO ... DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE`
  - Replace `.update { }` closures with `UPDATE SET` DQL
  - Replace `.remove()` with `DELETE FROM`
  - Replace `.evict()` with `EVICT FROM`
  ````
</Accordion>

***

## Removed Platform Support

<Warning>
  **Breaking Change**: v5 removes support for the following platforms. If your app targets one of these, you cannot upgrade to v5 while shipping on that platform — drop the platform target or stay on v4.

  * **tvOS** — No longer supported in v5.0
  * **visionOS** (beta) — No longer supported in v5.0
</Warning>

Check this before starting the rest of the migration. If you have concerns about this change, please reach out to Ditto customer support.

***

## DittoObjC Removal

In v4 when adding the `DittoSwift` package, it would add in the `DittoObjC` package product dependencies. In v5, `DittoObjC` is no longer a separate product — it's bundled into `DittoSwift`. This means that you no longer need to add the `DittoObjC` package product dependencies to your project and you need to remove the `DittoObjC` package product dependencies from your project in order to get the project to build.

These are the following places the `DittoObjC` package product dependencies are used:

1. `PBXBuildFile` section — removed the `DittoObjC in Frameworks` entry
2. `PBXFrameworksBuildPhase` — removed `DittoObjC in Frameworks` from the `files` array
3. `PBXNativeTarget.packageProductDependencies` — removed the `DittoObjC` reference
4. `XCSwiftPackageProductDependency` — removed the entire `DittoObjC` dependency entry

## Ditto Instance Initialization

v5 separates initialization into four distinct phases for better clarity and control:

<CodeGroup>
  ```swift SWIFT (v5) theme={null}
  // 1. Configure
  let config = DittoConfig(
      databaseID: "your-database-id",
      connect: .server(url: URL(string: "REPLACE_ME_WITH_YOUR_URL")!)
  )

  // 2. Initialize (async)
  let ditto = try await Ditto.open(config: config)

  // 3. Authenticate
  ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in
      expiredDitto.auth?.login(token: "your-token", provider: .development) { clientInfo, error in
          if let error = error {
              print("Ditto auth failed: \(error), client info: \(String(describing: clientInfo)), time until expiration: \(timeUntilExpiration)s")
          }
      }
  }

  // 4. Start sync
  try ditto.sync.start()

  ```

  ```swift SWIFT (v4) theme={null}
  let ditto = Ditto(
      identity: .onlinePlayground(
          appID: "your-app-id",
          token: "your-token",
          enableDittoCloudSync: false,
          customAuthURL: URL(string: "REPLACE_ME_WITH_YOUR_URL")
      )
  )

  ditto.updateTransportConfig { config in
      config.connect.webSocketURLs.insert("wss://...")
  }
  try? ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")
  ditto.disableSyncWithV3()

  try? ditto.startSync()
  ```
</CodeGroup>

**These changes provide:**

* Clear separation of connection, initialization, and authentication concerns
* Async initialization prevents blocking the main thread
* No workarounds needed (transport config, DQL strict mode, v3 sync disable)
* Type-safe configuration with compile-time validation
* Ditto.open() returns a non-optional Ditto instance

**Initialization Migration Steps**

<Steps>
  <Step title="Replace Configuration and Initialization">
    Replace `Ditto(identity:)` constructor with `Ditto.open(config:)`.

    <CodeGroup>
      ```swift SWIFT (v5) theme={null}
      let config = DittoConfig(
          databaseID: "your-database-id",
          connect: .server(url: URL(string: "REPLACE_ME_WITH_YOUR_URL")!)
      )
      let ditto = try await Ditto.open(config: config)
      ```

      ```swift SWIFT (v4) theme={null}
      let ditto = Ditto(
          identity: .onlinePlayground(
              appID: "your-app-id",
              token: "your-token",
              enableDittoCloudSync: false,
              customAuthURL: URL(string: "REPLACE_ME_WITH_YOUR_URL")
          )
      )
      ```
    </CodeGroup>

    **Key changes:**

    * Use `DittoConfig` instead of `DittoIdentity`
    * `appID` → `databaseID`
    * `.onlinePlayground` → `.server(url:)`
    * `.offlinePlayground` → `.smallPeersOnly(privateKey: nil)`
    * Add `try await` for async initialization
    * Remove `updateTransportConfig`, `disableSyncWithV3()`, and DQL strict mode queries
  </Step>

  <Step title="Update Authentication">
    Set authentication handler separately after initialization.

    <CodeGroup>
      ```swift SWIFT (v5) theme={null}
      // Set handler after Ditto.open()
      // The handler is async and receives the Ditto instance as its first parameter
      ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in
              // Initial auth
              expiredDitto.auth?.login(
                  token: "your-token",
                  provider: .development
              ) { clientInfo, error in
                  if let error = error {
                      print("Ditto auth failed: \(error), client info: \(String(describing: clientInfo))")
                  }
              }
      }
      ```

      ```swift SWIFT (v4) theme={null}
      // Auth mixed with identity
      let ditto = Ditto(
          identity: .onlinePlayground(
              appID: "...",
              token: "...",  // Token in identity
              enableDittoCloudSync: false,
              customAuthURL: URL(...)
          )
      )
      ```
    </CodeGroup>

    **Key changes:**

    * Authentication now uses `expirationHandler` closure
    * The handler signature is `@Sendable (_ ditto: Ditto, _ timeUntilExpiration: TimeInterval) async -> Void`
    * Handler called when auth required (`timeUntilExpiration == 0`) or token expiring
    * Use `.development` provider for playground tokens
    * Custom auth: extend `DittoAuthenticationProvider` with a static property, or use `DittoAuthenticationProvider("your-provider")`
  </Step>

  <Step title="Start Sync">
    Start sync after initialization.

    <CodeGroup>
      ```swift SWIFT (v5) theme={null}
      try ditto.sync.start()
      ```

      ```swift SWIFT (v4) theme={null}
      try? ditto.startSync()
      ```
    </CodeGroup>

    **Key changes:**

    * Namespace change from `ditto.startSync()` to `ditto.sync.start()`
  </Step>
</Steps>

### Auth Handler: Swift 6 Actor Isolation Requirement

If your `DittoManager` (or whatever class holds the `Ditto` instance) is an `actor`,
the `expirationHandler` closure is `@Sendable` and executes off the actor. Any access
to actor-isolated state inside a `Task { }` block requires `await`:

```swift theme={null}
// BROKEN — Swift 6 strict concurrency error
ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in
    if let error = someError {
        Task {
            self.myActorIsolatedProperty = error  // ← error: actor-isolated
        }
    }
}

// FIXED
ditto.auth?.expirationHandler = { expiredDitto, timeUntilExpiration in
    if let error = someError {
        Task {
            await self.myActorIsolatedProperty = error  // ← correct
        }
    }
}
```

## Additional Changes

### DQL Strict Mode Behavior Change

<Warning>
  **Breaking Change**: v5 defaults to `DQL_STRICT_MODE=false`, which fundamentally changes how DQL queries behave.

  * **v4 default**: Objects treated as **REGISTER** (whole-object replacement)
  * **v5 default**: Objects treated as **MAP** (field-level merging)

  This affects the behavior of all DQL `SELECT`, `INSERT`, and `UPDATE` operations.
</Warning>

Choose the appropriate migration path based on your current v4 configuration:

<AccordionGroup>
  <Accordion title="Currently Using DQL with DQL_STRICT_MODE=true (v4 default)">
    **If you're currently using the v4 default (`DQL_STRICT_MODE=true`)**, you **must** explicitly set strict mode to `true` in v5 before starting sync or executing any queries.

    <Warning>
      Failing to set strict mode will cause objects to merge at the field level instead of replacing entirely, which can result in unexpected data behavior and perceived data loss.
    </Warning>

    <Note>
      To migrate to `DQL_STRICT_MODE=false` (the new v5 default), contact Ditto Customer Support for guidance.
    </Note>

    <CodeGroup>
      ```swift SWIFT (v5 - Maintain v4 Behavior) highlight={6,7} theme={null}
      let config = DittoConfig(
          databaseID: "your-database-id",
          connect: .server(url: URL(string: "REPLACE_ME_WITH_YOUR_URL")!)
      )
      let ditto = try await Ditto.open(config: config)

      // IMPORTANT: Set strict mode BEFORE starting sync or running queries
      try await ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = true")

      // Now safe to start sync
      try ditto.sync.start()
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Currently Using DQL with DQL_STRICT_MODE=false">
    **If you explicitly set `DQL_STRICT_MODE=false` in v4**, no changes are required.

    v5 uses `DQL_STRICT_MODE=false` as the default, so your existing DQL queries will behave identically. You can upgrade freely.

    **Recommended**: Remove the explicit `ALTER SYSTEM SET DQL_STRICT_MODE = false` statement entirely — the v5 default already matches your intent, so the line is now redundant.

    <CodeGroup>
      ```swift SWIFT (v5) theme={null}
      let config = DittoConfig(
          databaseID: "your-database-id",
          connect: .server(url: URL(string: "REPLACE_ME_WITH_YOUR_URL")!)
      )
      let ditto = try await Ditto.open(config: config)

      // No need to set DQL_STRICT_MODE - false is now the default
      try ditto.sync.start()
      ```

      ```swift SWIFT (v4 - Your Current Setup) theme={null}
      let ditto = Ditto(
          identity: .onlinePlayground(
              appID: "your-app-id",
              token: "your-token",
              enableDittoCloudSync: false,
              customAuthURL: URL(string: "REPLACE_ME_WITH_YOUR_URL")
          )
      )

      // You explicitly set this in v4
      try? ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")

      try? ditto.startSync()
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Currently Using Legacy Query Builder">
    The Legacy Query Builder has been removed in v5. All queries must be converted to DQL before upgrading.

    **Good news**: Legacy Query Builder functionality has 1:1 support with `DQL_STRICT_MODE=false` (the v5 default), making migrations straightforward.

    **Migration Steps**:

    1. **In v4: Set `DQL_STRICT_MODE=false`** after initialization:
           <CodeGroup>
             ```swift SWIFT (v4 - Enable Non-Strict Mode) theme={null}
             let ditto = Ditto(
                 identity: .onlinePlayground(
                     appID: "your-app-id",
                     token: "your-token",
                     enableDittoCloudSync: false,
                     customAuthURL: URL(string: "REPLACE_ME_WITH_YOUR_URL")
                 )
             )

             // Set DQL_STRICT_MODE to false for Query Builder compatibility
             try? ditto.store.execute(query: "ALTER SYSTEM SET DQL_STRICT_MODE = false")

             try? ditto.startSync()
             ```
           </CodeGroup>

    2. **Convert all queries from Legacy Query Builder to DQL**
       See the [Swift Legacy→DQL Migration Guide](./swift-dql) for detailed conversion examples.

    3. **Upgrade to v5**
       No DQL configuration changes required—v5 defaults to `DQL_STRICT_MODE=false`.
  </Accordion>
</AccordionGroup>

For additional guidance or questions, contact Ditto Customer Support.

### Default Persistence Directory

v5 includes the database ID in the default directory name: `ditto-{databaseID}` instead of `ditto`. This is for only new databases created in v5. v5 does not migrate existing databases to the new structure.

**Provide custom persistence directory:**

```swift theme={null}
let config = DittoConfig(
    databaseID: "your-database-id",
    connect: .server(url: URL(...)),
    persistenceDirectory: URL(fileURLWithPath: "/app/sandbox/ditto")  // custom path to save the database
)
```

### Observer Changes

v4 and v5 both use callback-based observers. The core `registerObserver` API pattern is the same in both versions, returning a `DittoStoreObserver` that you retain and call `.cancel()` on for cleanup.

<CodeGroup>
  ```swift SWIFT (v5) theme={null}
  var observer: DittoStoreObserver?

  observer = try ditto.store.registerObserver(
      query: "SELECT * FROM cars WHERE color = :color",
      arguments: ["color": "red"]
  ) { result in
      let cars = result.items.compactMap { item -> Car? in
          let car = Car(item.jsonData())
          item.dematerialize()  // Free native memory — do not use item after this
          return car
      }
      DispatchQueue.main.async { self.cars = cars }
  }

  // Cleanup
  deinit {
      observer?.cancel()
  }
  ```

  ```swift SWIFT (v4) theme={null}
  var observer: DittoStoreObserver?

  observer = ditto.store.registerObserver(
      query: "SELECT * FROM cars WHERE color = :color",
      arguments: ["color": "red"]
  ) { result in
      DispatchQueue.main.async {
          self.cars = result.items.compactMap { Car($0.jsonData()) }
      }
  }

  deinit {
      observer?.cancel()
  }
  ```
</CodeGroup>

**Key changes:**

* v5 adds `Codable` argument overloads for `registerObserver`
* Call `item.dematerialize()` after extracting data from `DittoQueryResultItem` to free native memory
* The observer lifecycle pattern (retain, `.cancel()`, cleanup) is the same as v4

### Back Pressure (Signal Next)

The standard `registerObserver` automatically signals readiness for the next callback after your handler returns. For handlers that need time to process results — such as expensive rendering or batch operations — use `registerObserver(query:arguments:deliverOn:handlerWithSignalNext:)` to control when the next callback is delivered.

<Note>
  **Coalescing behavior**: While your handler is processing, Ditto coalesces any
  intermediate changes. When you call `signalNext()`, you receive a single
  callback with the latest state — not every intermediate change.
</Note>

```swift theme={null}
// Standard observer — signalNext called automatically after handler returns
let observer = try ditto.store.registerObserver(
    query: "SELECT * FROM cars WHERE color = :color",
    arguments: ["color": "blue"]
) { result in
    updateUI(result.items)
    // signalNext() called automatically here
}

// Back-pressure observer — you control when the next callback fires
let observer = try ditto.store.registerObserver(
    query: "SELECT * FROM cars WHERE color = :color",
    arguments: ["color": "blue"],
    handlerWithSignalNext: { result, signalNext in
        Task {
            await expensiveRenderOperation(result.items)
            signalNext()  // Ready for next update
        }
    }
)
```

**When to use signal next:**

* Processing updates is computationally expensive
* You're performing async work (network calls, heavy rendering) on each update
* Query results change very frequently and you want to process only the latest state

### DittoDiskUsageItem Property Names (v5, Unchanged)

`DiskUsageItem` was renamed to `DittoDiskUsageItem`, however, the property names
are **unchanged** in v5:

* `.path: String` — unchanged
* `.sizeInBytes: Int` — unchanged
* `.childItems: [DittoDiskUsageItem]` — unchanged (type renamed, property name same)

### `DittoConnection` Property Renames

| v4.14                                    | v5.0                         |
| ---------------------------------------- | ---------------------------- |
| `connection.peerKeyString1`              | `connection.peer1`           |
| `connection.peerKeyString2`              | `connection.peer2`           |
| `connection.approximateDistanceInMeters` | **REMOVED — no replacement** |

### DittoPeer Property Renames (v5)

| v4.14                          | v5.0                            |
| ------------------------------ | ------------------------------- |
| `peer.peerKeyString`           | `peer.peerKey`                  |
| `peer.osV2: DittoPeerOS?`      | `peer.os: DittoPeerOS?`         |
| `peer.isConnectedToDittoCloud` | `peer.isConnectedToDittoServer` |

### **Breaking**: DittoConnection Conformances Cause Swift 6 `filter` Ambiguity

In v5, `DittoConnection` gains `Identifiable`, `Equatable`, `Hashable`, and `Sendable` conformances.
In Swift 6, `Foundation.filter(_:Predicate)` takes precedence over `Sequence.filter(_:)` when the
element type conforms to these protocols, causing an unexpected compile error.

**Error:** `trailing closure passed to parameter of type 'Predicate<DittoConnection>'`

**Fix:** Annotate the closure parameter type explicitly:

```swift theme={null}
// BROKEN in Swift 6
connections.filter { $0.peer1 == localKey }

// FIXED
connections.filter { (conn: DittoConnection) in conn.peer1 == localKey }
```

### Remove Retroactive `Sendable` Conformances

v5 declares `Sendable` in-module for its public types, including `Ditto`, `DittoQueryResult`,
`DittoTransactionCompletionAction`, and `DittoStoreObserver`. If your v4-era code added its own
retroactive conformance to satisfy Swift 6 strict concurrency, it now conflicts with the SDK's
declaration and fails to compile:

**Error:** `conformance of 'X' to protocol 'Sendable' was already stated in the type's module 'DittoSwift'`

**Fix:** Delete the retroactive shim:

```swift theme={null}
// REMOVE — v5 already declares this conformance
extension DittoStoreObserver: @retroactive @unchecked Sendable {}
```

### DittoLogger Changes

`DittoLogger` retains the same class name in both v4 and v5. The key changes are:

* v5 marks `DittoLogger` as `final` and `Sendable` for Swift concurrency safety
* `DittoLogger.enabled` is renamed to `DittoLogger.isEnabled` — `DittoLogger.enabled = false` becomes `DittoLogger.isEnabled = false`. Leaving the old name produces a compile error: `type 'DittoLogger' has no member 'enabled'`
* `DittoLogger.setLogFile()` is removed in v5 (was deprecated in v4 — use `export(to:)` instead)

### API Changes

| v4.14                                                                | v5.0                                                                     |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `ditto.startSync()`                                                  | `ditto.sync.start()`                                                     |
| `ditto.stopSync()`                                                   | `ditto.sync.stop()`                                                      |
| `ditto.isSyncActive`                                                 | `ditto.sync.isActive`                                                    |
| `ditto.observePeers(_:)` / `observePeersV2(_:)`                      | `ditto.presence.observe(didChangeHandler:)`                              |
| `ditto.activated`                                                    | `ditto.isActivated`                                                      |
| `peerKeyString`                                                      | `peerKey`                                                                |
| `isConnectedToDittoCloud`                                            | `isConnectedToDittoServer`                                               |
| `Ditto(identity:)`                                                   | `Ditto.open(config:)` (async) or `Ditto.openSync(config:)`               |
| `ditto.siteID`                                                       | Not a standalone property — access via `ditto.presence.graph.localPeer`  |
| `ditto.appID`                                                        | `ditto.config.databaseID`                                                |
| `DiskUsage` class                                                    | `DittoDiskUsage` class                                                   |
| `ditto.diskUsage` (typed `DiskUsage`)                                | `ditto.diskUsage` (now typed `DittoDiskUsage`)                           |
| `DiskUsage.exec: DiskUsageItem`                                      | `DittoDiskUsage.item: DittoDiskUsageItem`                                |
| `DiskUsage.DiskUsageObserverHandle` (nested)                         | `DittoDiskUsageObserver` (standalone class)                              |
| `DiskUsage.observe(eventHandler:)` returns `DiskUsageObserverHandle` | `DittoDiskUsage.observe(eventHandler:)` returns `DittoDiskUsageObserver` |
| `DiskUsageItem`                                                      | `DittoDiskUsageItem`                                                     |
| `DittoSwiftError` typealias                                          | Removed — use `DittoError` directly                                      |
| `DittoError.migrationError(reason:)`                                 | Removed                                                                  |
| `MigrationErrorReason` enum                                          | Removed                                                                  |
| `DittoSmallPeerInfo.syncScope: DittoSmallPeerInfoSyncScope`          | DQL: `ALTER SYSTEM SET sync_scope = '...'`                               |
| `DittoSmallPeerInfoSyncScope` enum                                   | Removed — configure via DQL                                              |
| `DittoSmallPeerInfo.metadataJSONString`                              | `DittoSmallPeerInfo.metadataJSONData`                                    |
| `DittoSmallPeerInfo.setMetadataJSONString(_:)`                       | `DittoSmallPeerInfo.setMetadataJSONData(_:)`                             |
| `DittoLogger.enabled: Bool`                                          | `DittoLogger.isEnabled: Bool` — renamed                                  |
| `DittoLogger.setLogFile()`                                           | Removed — use `export(to:)` instead                                      |

### WebSocket Sync Default Changed

<Warning>
  `DittoHTTPListenConfig.webSocketSync` default changed from `true` to `false`.
  If your app relied on the v4 default, you must now explicitly enable it.
</Warning>

### Sync Subscription Changes

`DittoSync.subscriptions` is now computed rather than a stored Swift `Set`. Do not cache this collection — query it fresh when needed. The same applies to `DittoStore.observers`.

***

## APIs Removed

These v4 APIs were removed.

| Removed API                                                     | Notes                                   |
| --------------------------------------------------------------- | --------------------------------------- |
| `Ditto.transportDiagnostics()` / `DittoTransportDiagnostics`    | Low-level transport debug info removed  |
| `Ditto.runGarbageCollection()`                                  | GC is now handled automatically         |
| `Ditto.disableSyncWithV3()`                                     | v3 protocol support fully dropped       |
| `Ditto.isHistoryTrackingEnabled` / history tracking initializer | History tracking removed entirely       |
| `DittoIdentity.manual(certificateConfig:)`                      | Manual certificate identity removed     |
| `DittoConnection.approximateDistanceInMeters`                   | Peer distance estimation removed        |
| `DittoHTTPListenConfig.staticContentPath`                       | Static HTTP content serving removed     |
| `DittoStore.queriesHash(queries:)`                              | Query set hashing removed               |
| `DittoStore.queriesHashMnemonic(queries:)`                      | Query mnemonic hashing removed          |
| `DittoConnectionPriority` enum                                  | Connection priority control removed     |
| `DittoTransportSnapshot` struct                                 | Removed with transport diagnostics      |
| `DittoRemotePeer` struct (deprecated)                           | Use `DittoPeer` in `DittoPresenceGraph` |
| `DittoAddress` struct                                           | Removed                                 |
| `DittoExperimental.jsonByTranscoding(cbor:)`                    | CBOR-to-JSON utility removed            |
| `DittoLogger.setLogFile()`                                      | Use `export(to:)` instead               |

***

## New APIs in v5

* **`Ditto.open(config:): async throws -> Ditto`** — async factory method
* **`Ditto.openSync(config:): throws -> Ditto`** — synchronous factory alternative
* **`ditto.config: DittoConfig`** — read the config used to open this instance
* **`ditto.isActivated: Bool`** — replaces `ditto.activated`
* **`DittoStore.newAttachment(data: Data, metadata:)`** — create attachment from in-memory `Data` (v4 was file path only)
* **`DittoStore.registerObserver(query:arguments: some Codable, ...)`** — overloads accepting `Codable` query arguments
* **`DittoSync.registerSubscription(query:arguments: some Codable)`** — overload accepting `Codable` arguments
* **`DittoDiskUsageObserver`** — standalone observer class (was nested `DiskUsage.DiskUsageObserverHandle`)
* **`DittoDiskUsageItem`** — renamed from `DiskUsageItem`
* **`DittoSyncSubscription.queryArgumentsCBORData`** / **`.queryArgumentsJSONData`** / **`.id`** — new properties for introspection
* **`DittoConnection`** now conforms to `Identifiable`, `Equatable`, `Hashable`, and `Sendable`
* All public types now `final` and `Sendable` for Swift concurrency
* `DittoConfig` and `DittoConfigConnect` now conform to `Sendable` and `Codable`

***

## Migration Checklist

### Initialization

* [ ] Replace `Ditto(identity:)` with `Ditto.open(config:)`
* [ ] Create `DittoConfig` with `databaseID` and `connect` mode
* [ ] Add `try await` for async initialization
* [ ] Update `.onlinePlayground` → `.server(url:)`
* [ ] Update `.offlinePlayground` → `.smallPeersOnly(privateKey: nil)`
* [ ] Remove `updateTransportConfig` calls
* [ ] Remove `disableSyncWithV3()` calls
* [ ] **Set `DQL_STRICT_MODE=true` BEFORE starting sync if maintaining v4 behavior**
* [ ] Update `startSync()` → `sync.start()`

### Authentication

* [ ] Set `expirationHandler` closure after initialization
* [ ] Use `.development` provider for playground tokens
* [ ] Remove authentication from identity configuration

### Observers

* [ ] Call `item.dematerialize()` after extracting data from `DittoQueryResultItem` to free native memory
* [ ] If processing is expensive, use `handlerWithSignalNext:` for back-pressure control
* [ ] v5 adds `Codable` argument overloads — adopt where convenient

### Data Operations

* [ ] Replace all `.collection("x").find(...)` chains with `store.execute()` DQL
* [ ] Use parameterized queries with `:paramName` — never string interpolation
* [ ] Replace `.upsert()` with `INSERT INTO ... ON ID CONFLICT DO UPDATE`
* [ ] Replace `.update {}` closures with `UPDATE SET` DQL
* [ ] Replace `.remove()` with `DELETE FROM`
* [ ] Replace `.evict()` with `EVICT FROM`
* [ ] Replace `.observeLocal {}` with `store.registerObserver()` callback
* [ ] Replace `.subscribe()` with `sync.registerSubscription()`
* [ ] Replace `counter?.increment(by:)` with `UPDATE APPLY ... PN_INCREMENT BY`

### Breaking Changes

* [ ] Set `persistenceDirectory` if maintaining v4 directory structure
* [ ] Update `peerKeyString` → `peerKey`
* [ ] Update `diskUsage` usage: `DiskUsage` → `DittoDiskUsage`, `.exec` → `.item`, `DiskUsageObserverHandle` → `DittoDiskUsageObserver`
* [ ] Update `DittoConnection.peer1`/`peer2` comparisons: now `String` instead of `Data`
* [ ] Update `isConnectedToDittoCloud` → `isConnectedToDittoServer`
* [ ] Update `ditto.observePeers(_:)` → `ditto.presence.observe(didChangeHandler:)`
* [ ] Update `ditto.activated` → `ditto.isActivated`
* [ ] Remove all `DittoSwiftError` references — use `DittoError` directly
* [ ] Update `DittoSmallPeerInfo.metadataJSONString` → `metadataJSONData`
* [ ] Check for explicit `webSocketSync = true` if your app relied on v4 default
* [ ] Update `DittoLogger.enabled` → `DittoLogger.isEnabled`
* [ ] Update `DittoLogger.setLogFile()` calls — use `export(to:)` instead

### Verification

* [ ] Build compiles with zero errors
* [ ] No deprecated API warnings
* [ ] Observers update UI immediately
* [ ] Authentication works before sync starts
* [ ] No memory leaks on navigation
* [ ] No memory leaks (Instruments: Allocations, Leaks)

***

## Common Pitfalls

1. **DQL\_STRICT\_MODE silent change**: Not setting `DQL_STRICT_MODE=true` when your v4 app used the default. Objects that were replaced whole will now merge at field level — causes unexpected data merging.

2. **Forgetting `try await`**: `Ditto.open(config:)` is async. Not awaiting it causes a compile error.

3. **Storing `DittoQueryResultItem` outside callback**: These hold native memory. Always call `dematerialize()` and extract your model before the callback returns.

4. **String interpolation in queries**: Never `"SELECT * FROM cars WHERE color = '\(color)'"`. Always use `arguments: ["color": color]`.

5. **Counter initialization**: Don't put `DittoCounter()` or `0` in insert documents. Counters are created on first `PN_INCREMENT`. Inserting `0` creates a REGISTER, not a COUNTER.

6. **Missing `ON ID CONFLICT`**: INSERT fails if document `_id` already exists without a conflict clause.

7. **Not checking `timeUntilExpiration`** in auth handler: Handler is called for both initial auth (0) and token refresh (>0). Handle both cases.
