Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.myme.so/llms.txt

Use this file to discover all available pages before exploring further.

Conflict handling

See Errors — version_conflict for the enriched 409 payload and Conflicts for the resolution model. The Swift SDK exposes the three modes as ClientConfiguration.conflictStrategy and UpdateOptions.conflict.
try await client.items.update(
  id: note.id,
  properties: ["body": .string("...")],
  options: UpdateOptions(
    version: note.version,
    conflict: .callback,
    resolve: { conflict in
      return mergeManually(conflict)
    }
  )
)
.auto (default), .manual (throws ConflictError with current, ancestor, conflictingFields, clientPatch, mergePolicy), and .callback are the three modes. .auto is policy-aware. The SDK reads the type’s merge_policy directly from the 409 response and resolves each conflicting field per its strategy. last_writer_wins fields take the server’s value; keep_both_copies fields spawn a sibling item of the same type, tagged conflicted-copy, populated from the client’s in-flight values for the keep-both fields and the server’s current values for the rest. The original item retains the server’s winning state for the conflicted keep-both fields; non-keep-both client changes apply normally. The SyncEngine.events stream emits .conflictAutoMerged(payload:) whenever a replay auto-merge fires:
public struct ConflictAutoMergedPayload: Sendable {
  public let itemId: String           // the original item
  public let mergedItemId: String     // the original item id (post-retry)
  public let conflictedCopyId: String?  // the new sibling, when keep-both spawned one
  public let fields: [String]         // the conflicting fields
  public let strategy: ConflictResolutionOutcome  // .lastWriterWins, .keepBothCopies, .mixed
}
Apps typically toast when conflictedCopyId != nil — the user’s in-flight edit was preserved as a sibling rather than discarded. In synced mode the chosen conflict strategy is captured with the queued mutation so it applies on replay against any 409 the server returns. The .callback resolver closure can’t be queued (closures aren’t Codable); on replay, .callback degrades to .auto.

Errors

Errors are subclasses of a base MarfaError. Pattern-match on the subclass:
do {
  _ = try await client.items.get(id: "bogus")
} catch let error as NotFoundError {
  // missing item
} catch let error as ConflictError {
  // error.current, error.ancestor, error.conflictingFields
} catch let error as LocalModeUnsupportedError {
  // remote-only call (blobs, types, keys, webhooks) on a pure-local client
} catch let error as MarfaError {
  // everything else — error.code (String), error.message, error.status, error.details
}
MarfaError.code is a String, not an enum. Branch on the subclass where one exists; fall through to MarfaError and inspect code for anything else. Error codes are stable strings ("version_conflict", "rate_limited") — branch on them, not on the message.
Subclasses: NotFoundError, ValidationError, UnauthorizedError, ForbiddenError, ConflictError, NetworkError, ResponseDecodingError, LocalModeUnsupportedError.

Testing

MarfaSDKTestSupport ships as a separate product alongside MarfaSDK and provides scaffolding for unit tests:
  • MockTransport — stub for the Transport protocol. Queue responses, assert on captured requests.
  • InMemoryKeychain — in-memory SecureStorage substitute. SPM test binaries run unsigned and can’t reach the real Keychain.
  • MarfaSDKTest.makeInMemoryClient() — pure-local MarfaClient backed by an in-memory ModelContainer. Drop-in replacement for MarfaClient.local(path:) in test setups; sidesteps the cross-test container issues that show up when sharing a file-backed store across cases in the same process.
Add MarfaSDKTestSupport as a dependency on your test target only. The product is non-semver-stable across SDK minor versions and is not intended for production code. The Transport protocol is public. MockTransport exists primarily for mocking the network in unit tests; the production MarfaClient init always instantiates URLSessionTransport. The protocol includes rawUpload(method:path:body:contentType:query:onBytesSent:) — the progress-aware upload hook that forwards URLSessionTaskDelegate.didSendBodyData — so stubs exercising byte-progress code paths (e.g. against BlobUploadProgressQuery) can drive those callbacks deterministically from a mock.

Concurrency model

  • MarfaClient is Sendable — safe to pass across actors.
  • Namespace methods are async throws and don’t require caller isolation.
  • LocalStore, MutationQueue, SyncEngine, ConnectionStateManager are Swift actors. Shared state is actor-isolated; callers never synchronise manually.
  • MarfaStore and query classes are @MainActor @Observable — observe them directly from SwiftUI views.
  • The whole package builds under Swift 6 strict concurrency with zero @unchecked Sendable leaks in the public surface.