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)/devices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The hydration **empties the copy first** if the instance's event log now ends be

**For any other reason, the copy refuses reads and writes until a hydration, which empties it first.** Examples are another instance answering at the same address, or a credential whose reach changed.

A copy made by an earlier build of the core can have a different shape. The device **refuses to open it** with `wrong_schema`, deletes nothing, and says how many writes the file holds that the server hasn't taken, because only the build that made it can send them. If some are of a kind the current build no longer sends, it names the kind. [Troubleshooting](/guides/troubleshooting#staying-in-step) says what to do. An app built on the core's Node or Swift binding can replace a refused copy that holds nothing unsent with an empty one.
A copy made by an earlier build of the core can have a different shape. The device **refuses to open it** with `wrong_schema`, deletes nothing, and says how many writes the file holds that the server hasn't taken, because only the build that made it can send them. If some are of a kind the current build no longer sends, it names the kind. [Troubleshooting](/guides/troubleshooting#staying-in-step) says what to do. An app built on the core's Node or Swift binding can replace a refused copy that holds nothing unsent with an empty one; the files the copy downloaded stay.

## A write made offline

Expand Down
41 changes: 41 additions & 0 deletions content/docs/(reference)/swift-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,49 @@ let copy = try await WorkingCopy.open(store: storeURL, server: Server(url: serve
| Call | What it does |
| --- | --- |
| `WorkingCopy.open(store:server:fileBudget:)` | Opens the file. Without a `Server`, the copy works offline only. A `fileBudget` sets the [file budget](#keep-files-on-the-device) as the copy opens, and a reader refuses it with `readingHandle`. |
| `WorkingCopy.replaceRefused(store:server:fileBudget:)` | Replaces a store that `open` refuses with `wrongSchema` by an empty one, and opens it. [Replace a refused copy](#replace-a-refused-copy) says what it keeps. |
| `WorkingCopy.openReader(store:)` | Opens a store another process writes, to read only. It refuses a path where no store exists. |
| `handle` | `.writer` or `.reader`. A reader refuses writes, hydration and catch-up with `readingHandle`. |
| `useKey(_:)` | Gives the open copy a new credential. Held `changes()` streams go on. |
| `close()` | Ends every stream and releases the store. Later calls throw `MarfaError.closed`. |
| `Server(url:key:)` | The instance's URL and a key or an app's access token. |
| `Server.isPlainHTTPToAnotherHost`, `takePlainHTTPNotice()` | Whether the address is plain http on another host, and a notice to show the first time. [Reach a plain http server](#reach-a-plain-http-server) explains both. |
| `Server.fromEnvironment()` | For a command-line tool: reads `MARFA_API_URL` and `MARFA_API_KEY`. Returns `nil` when both are absent, and throws when only one is set or the URL names no server. |

### Replace a refused copy

A store that another build of the core made throws `wrongSchema` from `open`, and nothing is deleted. Its `unsent` count is the number of writes the instance has not taken. **When it is `0`**, `WorkingCopy.replaceRefused(store:server:fileBudget:)` replaces the file with an empty one and opens it. Hydrate the new copy as you would a new one.

- **The files the copy downloaded stay.** The core keeps them by content, beside the store, not in it.
- **What only the old store held is gone:** pins, the position in the instance's events, declared types and the file budget's setting. Pass `fileBudget` to set it again; otherwise it is the default.
- **A store that holds an unsent write is left as it is**, and the call throws `wrongSchema` with the count. A store that opens as it is, or a path with no store, throws `invalid`. [Troubleshooting](/guides/troubleshooting#staying-in-step) covers what to do with unsent writes.

## Reach a plain http server

An `http` address sends the key in the clear. **The SDK warns about that only through your app**: it shows nothing itself. `Server.isPlainHTTPToAnotherHost` is `true` for an `http` address on any host but the machine itself, by the same rule as the command line. `localhost`, every address in `127.0.0.0/8` and `::1` are the machine itself; an IPv4-mapped address such as `::ffff:127.0.0.1` is not. `takePlainHTTPNotice()` returns a `PlainHTTPNotice` the first time any server with that origin asks in the process, and `nil` after, for `https` and for the machine itself:

```swift
if let notice = server.takePlainHTTPNotice() {
cleartext = notice // an @State var of type PlainHTTPNotice?
}
// In the view:
.alert(cleartext?.title ?? "", isPresented: Binding(get: { cleartext != nil }, set: { _ in cleartext = nil })) {
Button("OK") {}
} message: {
Text(cleartext?.message ?? "")
}
```

**App Transport Security (ATS) applies to some calls and not others.** Apple's documentation says ATS requires that connections made with the URL Loading System, typically `URLSession`, use HTTPS ([NSAppTransportSecurity](https://developer.apple.com/documentation/bundleresources/information-property-list/nsapptransportsecurity)). The core makes its own requests with a Rust HTTP client, not `URLSession`, so **ATS doesn't affect the working copy's hydration, catch-up and drain**. The SDK's sign-in requests, and any client of your own built on `URLSession`, are subject to it.

For those, declare the server in your app's Info.plist. As Apple documents them for iOS 17, iPadOS 17 and macOS 14 and later:

- **A DNS name:** add it to `NSExceptionDomains` with `NSExceptionAllowsInsecureHTTPLoads` set to `YES`. Use lowercase, no port, and `NSIncludesSubdomains` for subdomains ([NSExceptionDomains](https://developer.apple.com/documentation/bundleresources/information-property-list/nsapptransportsecurity/nsexceptiondomains)).
- **An IP address:** ATS no longer allows IP addresses by default. Add the address, or a CIDR range such as `192.168.42.0/24`, to `NSExceptionDomains`. An exception for a name doesn't cover the same host reached by address, and the reverse ([NSExceptionDomains](https://developer.apple.com/documentation/bundleresources/information-property-list/nsapptransportsecurity/nsexceptiondomains)).
- **Unqualified names and `.local` names on the local network:** `NSAllowsLocalNetworking` set to `YES` allows them, and Apple recommends setting it as a declaration of intent ([NSAllowsLocalNetworking](https://developer.apple.com/documentation/bundleresources/information-property-list/nsapptransportsecurity/nsallowslocalnetworking)).

Prefer an `https` address. Apple warns that loosening ATS reduces your app's security.

## Identify the connected instance

Use **`Server.instanceDescription()`** to display the connected instance's name. It reads the [public instance description](/api-reference/instance/getInstance) through the core, without sending the key or opening a working copy:
Expand Down Expand Up @@ -97,6 +133,10 @@ The `HydrateReport` counts this call's listing pages in `pages` and includes ear

While a fill runs, `status()` gives `itemsListed`, the items the fill has brought so far, and `itemsExpected`, how many the slice held when the fill began, where the instance counted them. **Compare those two for progress**, not `items`, which also includes pinned items and queued creates.

### Read the instance's types before the first fill

`copy.catalog.fromServer()` asks the instance for its item types and edge types now, so an app can offer a choice of types before it hydrates. It returns a `ServerCatalog` and changes nothing in the copy, whose own `catalog` reads keep answering from the store. A copy without a server throws `noServer`, and one that can't reach the instance throws `network`. Cancel the task to end the request.

### Read while the copy fills

While a hydration is in progress, a list or a search answers the items that have arrived and says so. `items.list(_:sort:)`, `search(_:filters:limit:)` and the saved-view reads return `Rows`, a collection of `rows` with a `completeness`:
Expand Down Expand Up @@ -147,6 +187,7 @@ Besides `items`, the copy has `edges` (`from`, `to`, `ofType`, `create`, `update
| `delete(_:)`, `restore(_:)`, `transition(_:to:)` | Move an item to the bin or out of it, or change its `ItemState`. |
| `bin(type:after:limit:)`, `purge(_:version:)` | Read a page of the server's bin, and destroy an item in it. Both need the server and neither is queued. A page can be short or empty and still carry a `nextCursor`. A purge needs `items.purge` and write on the type. |
| `attach(to:file:_:)`, `embed(file:in:_:)` | Queue the upload, a file item and an `attached-to` edge, each with its own verdict. `embed` also adds the file's embed to the body, and throws `EmbedFailure` if that step fails. |
| `addFile(_:_:tags:)` | Adds a file as an item of its own, attached to nothing: it queues the upload and the file item, and a write for each tag. It returns an `Added` with the first two. |
| `links(in:)`, `embedText(of:in:)` | The links and file embeds in a body, and the `![[title]]` text for an attached file. |
| `conflictedCopies(of:)`, `original(ofConflictedCopy:)` | An item's held conflicted copies, and the ID of the item a copy was made from. |

Expand Down
2 changes: 1 addition & 1 deletion content/docs/guides/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ A working copy is a file that one build of the core made, and a build with a dif

The message says how many writes the file holds that the server has not taken: waiting, blocked, refused or dead. Only the build that made the file can send or show them, so what you do depends on that count:

- **None.** Every write the file holds reached the server. Run `marfa device hydrate` to build a new working copy. An app built on the core's Node or Swift binding can call `replaceRefused` instead: it replaces the old file with an empty copy, so what only the old file held, such as pins, is lost.
- **None.** Every write the file holds reached the server. Run `marfa device hydrate` to build a new working copy. An app built on the core's Node binding can call `replaceRefused` instead, and one built on the [Swift SDK](/swift-sdk#replace-a-refused-copy) can call `WorkingCopy.replaceRefused(store:server:fileBudget:)`: it replaces the old file with an empty copy, so what only the old file held, such as pins, is lost. The files the copy downloaded stay.
- **Some.** Open the file with the build that made it, run `marfa device drain` to send the writes, and use `marfa device discard` on any it refuses that you don't want. Then hydrate a new copy with the newer build. Keep the old file until you have done this, because deleting it loses those writes.

If some of the writes are of a kind the current build no longer queues, the message names the kind and how many, such as `1 write of kind merge_metadata`. That one is a tag merge, which an instance at v0.0.24 or later answers `404 not_found`. Read the tags it carries with `marfa device queue`, and add them with `marfa items tag`.
Expand Down
Loading