Skip to content
Closed
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
2 changes: 1 addition & 1 deletion content/docs/(overview)/how-marfa-works.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Nothing reads or writes your data without a **credential**, and a credential rea

Every program that uses Marfa runs outside it and sends requests to its HTTP API. Programs differ in what sits between them and the API:

- **The Swift SDK and the CLI** embed the **core**, a Rust engine that sends their requests and can keep a working copy of the data they need. A program that keeps a working copy is a **device**: it reads offline and sends its writes when it can reach the instance, as [Devices](/devices) explains.
- **The Swift SDK and the CLI** embed the **core**, a Rust engine that sends their requests and can keep a working copy of the data they need. A program that keeps a working copy is a **device**: it reads offline and sends its writes when it can reach the instance, as [Devices](/devices) explains. The Swift SDK also sends the operations only the instance can do, such as registering a type, straight to the API.
- **The TypeScript SDK** sends each request straight to the API, and keeps no copy.
- **AI agents** use the CLI or the API like any script, with a key of their own, as [Use Marfa with AI agents](/guides/ai-agents) shows.
- **Connectors** are programs you run that bring in records from another service, such as Todoist, as [Connectors](/connectors) explains.
Expand Down
34 changes: 32 additions & 2 deletions content/docs/(reference)/swift-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Give an iOS or macOS app a working copy of your instance that reads

The Swift SDK gives an app on iPhone, iPad or Mac a **working copy** of an instance: a local database it reads with or without a connection, and a queue that holds its writes until it can send them. **An app built on it is a [device](/devices)**: that page explains the working copy, the queue and verdicts.

Use it for an app that must keep working offline. A script or a server needs no working copy: use the [TypeScript SDK](/typescript-sdk) or the [HTTP API](/api-reference). [Build an app with Swift](/guides/swift-app) builds a SwiftUI app on it.
Use it for an app that must keep working offline. A script or a server needs no working copy: use the [TypeScript SDK](/typescript-sdk) or the [HTTP API](/api-reference). For the calls only the instance can answer, such as registering a type, the SDK also has a [client](#call-the-server-directly). [Build an app with Swift](/guides/swift-app) builds a SwiftUI app on it.

## Install

Expand All @@ -24,6 +24,7 @@ Each release carries Marfa's core already built, so your app needs no Rust toolc
| Product | Holds |
| --- | --- |
| `Marfa` | The SDK. Add it to your app target. |
| `MarfaAPI` | `Client`, for the [calls only the instance can answer](#call-the-server-directly). It includes `MarfaTypes`. |
| `MarfaTesting` | `WorkingCopy.fake(items:itemTypes:)`, a working copy for tests and previews with no store, server or core. Pass its `copy` where your code takes a `WorkingCopy`. It compiles empty in a release build, so wrap its uses in `#if DEBUG`. |
| `MarfaTypes` | The server's wire types, generated from the API description, and `marfaContractVersion`. |

Expand Down Expand Up @@ -182,6 +183,34 @@ try await copy.declareTypes([

`declareTypes(_:)` replaces the app's whole set, so pass every type on each call. **Until the copy first hydrates, a queued create is checked against these declarations** and the types Marfa ships, so the app works offline from its first launch. On hydration, the copy registers each declared type the instance lacks, if the credential may register types, and the `HydrateReport` lists them in `registeredTypes` and `unregisteredTypes`. [Define a type](/guides/define-a-type) explains what a definition holds.

## Call the server directly

Some operations only the instance can do: registering, replacing and deleting a type, managing keys and webhooks, reading an item's versions, stats and occurrences, and reading the audit log. **`Client` from the `MarfaAPI` product sends these straight to the instance**, with the same `Server` the working copy uses:

```swift
import MarfaAPI

let api = Client(server)
let title = Components.Schemas.FieldDefinition(_type: .string, required: true)
let entry = Components.Schemas.TypeDefinitionInput(
fields: .init(additionalProperties: ["title": title]), id: "app.readinglist.entry")

_ = try await api.check(itemType: entry)
_ = try await api.register(itemType: entry)
```

| Call | Operation |
| --- | --- |
| `check(itemType:)` | [Check a type definition](/api-reference/types/normalizeType) without registering it. |
| `register(itemType:)`, `replace(itemType:with:)`, `delete(itemType:force:)` | [Register](/api-reference/types/registerType), [replace](/api-reference/types/replaceType) or [delete](/api-reference/types/deleteType) an item type. |
| `register(edgeType:)`, `edgeTypes()`, `delete(edgeType:force:)` | [Register](/api-reference/edge-types/registerEdgeType), [list](/api-reference/edge-types/listEdgeTypes) or [delete](/api-reference/edge-types/deleteEdgeType) an edge type. |

Every other operation the client makes is a method named after its operation ID, such as `listItemVersions(path:)` or `listAuditLog()`. A refusal throws a `MarfaError`, so read a success from the answer's `ok` case: `try await api.listAuditLog().ok.body.json`. **The client has no call that creates, gets, changes or deletes one item or edge, and none for search or events**: those go through the working copy, so there is one path for each item, and bulk changes aren't offered. It does read what only the instance holds, such as an item's versions, statistics and occurrences. Connector operations and those for the owner's session alone are not in it either.

**Each call needs a connection.** On a device that is offline, a call throws `noConnection` and sends nothing; it isn't queued. A call waits 30 seconds for the instance to send more, for the answer to start and again between the parts of a streamed body, then throws `network`. Pass `Client(server, timeout:)` a longer time for a call that answers only when its work is done, such as `runHousekeeping`. When the instance refuses an expired access token, the client renews the sign-in once and sends the call again, and once the instance ends the sign-in, calls throw `signedOut`. **It never follows a redirect**, so the credential reaches no other host.

`declareTypes(_:)` registers an app's types when the copy hydrates, which suits an app that must start offline. Use `register(itemType:)` when the app needs the type on the instance now, or to change or delete one.

## Follow changes

Each `Change` names the `itemId` or `edgeId` it concerns, and its `origin` says where it came from:
Expand Down Expand Up @@ -361,13 +390,14 @@ Its `FolderEvent` values are `.watching` when it starts and `.passed` after a pa

## Handle errors

Every call on a working copy or `Folders` throws a `MarfaError`, whose `message` is fit to show a person. `Keychain` throws `Keychain.Failure`, and `Server.fromEnvironment()` throws `Server.EnvironmentError`.
Every call on a working copy, `Folders` or `Client` throws a `MarfaError`, whose `message` is fit to show a person. `Keychain` throws `Keychain.Failure`, and `Server.fromEnvironment()` throws `Server.EnvironmentError`.

| Case | Meaning |
| --- | --- |
| `notFound`, `unauthorized`, `forbidden`, `validation`, `server` | A refusal, from the server or from the copy before anything was sent. Each carries a `code`: [Errors](/api-reference/errors) lists the server's. |
| `rateLimited` | The server asked for a wait. `retryAfterSeconds` says how long, when it said. |
| `network`, `unnamed` | The server couldn't be reached, or something in front of it answered naming no contract. A queued write stays queued. |
| `noConnection` | A `Client` call found the device offline, so nothing was sent. |
| `contractMismatch` | The server speaks a contract this build wasn't made for. A write it answered stays queued, and goes again once the app speaks the server's contract. |
| `wrongServer` | The server at the copy's address is another instance than the copy recorded. The copy sends it no credential or data: [Troubleshooting](/guides/troubleshooting) explains. |
| `copyExpired`, `hydrationIncomplete`, `readingHandle`, `firstSyncWaiting`, `noServer`, `closed` | States covered on this page. |
Expand Down
25 changes: 24 additions & 1 deletion content/docs/guides/define-a-type.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,16 @@ const definition = JSON.parse(await readFile("receipt.json", "utf8"));
const { data, error } = await marfa.POST("/types", { body: definition });
```

The TypeScript examples use a client made as [the TypeScript SDK](/typescript-sdk) shows. Marfa answers `201` with the type as it stored it:
```swift tab="Swift"
import MarfaAPI

let api = Client(server)
let definition = try JSONDecoder().decode(
Components.Schemas.TypeDefinitionInput.self, from: Data(contentsOf: receiptURL))
let registered = try await api.register(itemType: definition)
```

The TypeScript examples use a client made as [the TypeScript SDK](/typescript-sdk) shows, and the Swift examples a `Client` made as [the Swift SDK](/swift-sdk#call-the-server-directly) shows, with `receiptURL` the file's location. Marfa answers `201` with the type as it stored it:

```json
{
Expand Down Expand Up @@ -130,6 +139,10 @@ Content-Type: application/json
const { data, error } = await marfa.POST("/types/normalize", { body: definition });
```

```swift tab="Swift"
let normalized = try await api.check(itemType: definition)
```

The answer's `type` is your definition with its defaults made explicit, and `registered_type` is the registered one in the same form. Compare those two, not `resolved_type`, which adds what the registered type inherits. Both registered forms are `null` when the identifier isn't registered. An unknown schema key, anywhere in a definition, is refused `400 invalid_schema`.

Then replace the type:
Expand All @@ -153,6 +166,12 @@ const { data, error } = await marfa.PUT("/types/{id}", {
});
```

```swift tab="Swift"
let update = try JSONDecoder().decode(
Components.Schemas.TypeDefinitionUpdate.self, from: Data(contentsOf: receiptURL))
_ = try await api.replace(itemType: "app.example.receipt", with: update)
```

A replacement **changes no item you already hold**. An item meets a property you made required the next time its properties are written. A property you leave out is gone from the type, but items keep their values for it, and a later write isn't refused for them.

Send the `version` every time: a replacement without one sets it back to 0. You can't replace or delete a type Marfa ships.
Expand All @@ -176,6 +195,10 @@ await marfa.DELETE("/types/{id}", {
});
```

```swift tab="Swift"
try await api.delete(itemType: "app.example.receipt")
```

Otherwise it refuses with `409 type_in_use` when items hold the type, `409 type_has_subtypes` when a type names it as its parent, and `422 compatible_with_violation` when a type names it in `compatible_with`.

<Callout type="warn">
Expand Down
Loading