Menu 0.6.0 preview

Integrate

Agent Docs: SDK API

Integrating trueseal-sync

Your app starts, and before the first screen appears it needs to know whether this device is in a group, who else is in it, and whether the relay is reachable. Then it needs to hear about new messages and changes for as long as it runs. This page covers that wiring. Pairing, membership and messages each have their own page.

All three SDKs expose one object, TrueSeal, with the same operations, names, events and errors. Only the syntax differs. Swift uses async throws, Kotlin suspend functions and exceptions, and TypeScript promises. The examples here use neutral syntax.

Opening it

trueseal = await TrueSeal.open(relay, storage, namespace = "default")
  • relay is the Relay Address, one string with the relay’s host, ports and public key: trueseal://<relay public key hex>@host[:receivePort][?push=pushPort]. Ship it as a build-time constant. Relay Address and keypair explains where it comes from.
  • storage is the directory for Session State. Use a directory that belongs to your app, so no other app shares its identity. On Android, build the Storage from your app’s context; the API reference has each platform’s spelling.
  • namespace selects one independent identity and group inside that directory. One namespace holds one group, so an app that needs a device in two groups opens two objects with two namespaces.
  • maxPayloadBytes is optional. Pass a value lower than the Protocol Size Limit (61,440 bytes (60 KiB)) if your relay’s Operator set a lower Relay Size Limit.

open() returns before the relay connection is up and connects in the background. Once the device is a member, send() works whether the relay is reachable or not, and messages wait in the outbox until it is. Don’t write reconnect logic; the library retries on its own.

Open one object per namespace when the app starts and keep it for the life of the process. It survives removal, leaving and Destroy Group. After each, the library wipes the namespace, gives the device a new identity, and the same object can pair again.

open() can fail with:

CaseWhat happenedWhat to do
invalidRelayAddress open() was given a Relay Address it can't parse. Fix the configured Relay Address. Copy it as the relay CLI prints it.
invalidNamespace open() was given a namespace it refuses. Pass another namespace, or leave the default.
storage Session State can't be read or written, or a migration failed; the old store is left intact. Check that the storage location exists and is writable, then call TrueSeal.open again. The old store is intact.
storeTooNew open() found Session State written by a newer release; the store is left unchanged. Ship the newer release again. The store is unchanged, so nothing is lost.

Ship the Relay Address as a build-time constant, so invalidRelayAddress means fixing that constant. Upgrading covers storeTooNew.

State

These values describe the device at any moment. Each SDK makes them observable in its usual way: @Observable in Swift, StateFlow in Kotlin, and getters with change events in TypeScript.

  • status is one of notJoined, pendingJoin, member, leaving or destroying.
  • connection is one of connecting, connected, disconnected or relayVersionUnsupported{min, max}.
  • me is this device and members every other member, each a Member{id, name}.

Read status and connection separately. A device can be connected with no group yet, or in a group with the relay out of reach. Neither is an error. relayVersionUnsupported is the one connection state the user can’t wait out. The relay speaks a different protocol version, and messages stay queued until your app’s library and the relay speak the same version again.

Events and handlers

trueseal.onEvent(event => …)
trueseal.onMessage(async message => …)
trueseal.onDeliveryIssue(issue => …)
  • onEvent takes any number of listeners. The cases are statusChanged(status, reason), membersChanged(members), joinRequest(request), connectionChanged(connection) and admissionDropped(name, reason). Events raised before the first listener exists are kept and delivered to it.
  • onMessage registers the one message handler. The library takes no message from the relay until it exists, so register it once your app can store what arrives. A second call fails with messageHandlerAlreadySet.
  • onDeliveryIssue is optional. It reports messages the library couldn’t deliver or handle.

Every on… call returns a handle that cancels the registration.

Sending and receiving explains what the message handler must do, and Delivery issues what each issue means.

Closing it

close() stops the connection and the handlers and keeps Session State. Every later call on that object fails with closed. Open a new object to start again.

Conflicts are your app’s to resolve

TrueSeal hands each device’s messages to your handler in the order that device sent them, and doesn’t order messages from different devices. When two devices change the same data while apart, your app decides which change wins. Common patterns:

  • Append-only records. Each change is a new record, and the app shows all of them. Nothing conflicts. Fits chat, activity feeds and logs.
  • Last writer wins. Put a timestamp or a counter in each message and keep the newest. Fits single values such as a setting or a location.
  • CRDTs. Send the updates of a CRDT library such as Yjs or Automerge as message bodies. The library merges them in any order.

A new member starts empty

A device that joins receives only messages sent after the other members learn it joined. Nothing is replayed from before. When membersChanged shows a new member, your app decides what to send it: everything, a snapshot of current state, or nothing for data that only matters from now on, such as a live location.

It works when

  • A second device appears in members on both devices after pairing.
  • A send() on one device runs the message handler on the other, in both directions.
  • A message sent while the relay was unreachable arrives after it comes back.