diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index ceba8e49c2..633167a1d6 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -3,6 +3,10 @@ name: Release Notes on: push: branches: ["release/**"] + # Notes-only pushes (e.g. merging the drafter's own PR) must not + # retrigger drafting — that redrafts the redraft, forever (#3919/#3920). + paths-ignore: + - "docs/release-notes/**" workflow_dispatch: inputs: release-branch: diff --git a/Cargo.lock b/Cargo.lock index 0ec9983984..92e7eb5d50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1417,7 +1417,7 @@ dependencies = [ [[package]] name = "bindings_node" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "async-trait", "bindings_node", @@ -1451,7 +1451,7 @@ dependencies = [ [[package]] name = "bindings_wasm" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "alloy", "async-trait", @@ -1496,7 +1496,7 @@ dependencies = [ [[package]] name = "bindings_wasm_macros" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "proc-macro2", "quote", @@ -3018,7 +3018,7 @@ dependencies = [ [[package]] name = "error_glossary" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "clap", "syn 2.0.119", @@ -5289,7 +5289,7 @@ dependencies = [ [[package]] name = "keepalive-probe" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "anyhow", "bytes", @@ -5947,7 +5947,7 @@ dependencies = [ [[package]] name = "mls_validation_service" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "alloy", "async-trait", @@ -10453,7 +10453,7 @@ dependencies = [ [[package]] name = "xdbg" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "alloy-primitives", "alloy-signer-local", @@ -10518,7 +10518,7 @@ dependencies = [ [[package]] name = "xmtp-db-tools" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "anyhow", "clap", @@ -10650,7 +10650,7 @@ dependencies = [ [[package]] name = "xmtp_api" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "async-trait", "chrono", @@ -10676,7 +10676,7 @@ dependencies = [ [[package]] name = "xmtp_api_d14n" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "arc-swap", "async-trait", @@ -10717,7 +10717,7 @@ dependencies = [ [[package]] name = "xmtp_api_grpc" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "async-trait", "futures", @@ -10750,7 +10750,7 @@ dependencies = [ [[package]] name = "xmtp_archive" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "aes-gcm", "async-compression", @@ -10779,7 +10779,7 @@ dependencies = [ [[package]] name = "xmtp_common" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "async-trait", "const-hex", @@ -10814,7 +10814,7 @@ dependencies = [ [[package]] name = "xmtp_configuration" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "openmls", "xmtp-workspace-hack", @@ -10824,7 +10824,7 @@ dependencies = [ [[package]] name = "xmtp_content_types" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "aes-gcm", "base64 0.23.0", @@ -10845,7 +10845,7 @@ dependencies = [ [[package]] name = "xmtp_cryptography" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "alloy", "bincode", @@ -10874,7 +10874,7 @@ dependencies = [ [[package]] name = "xmtp_db" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "arc-swap", "ascii_table", @@ -10923,7 +10923,7 @@ dependencies = [ [[package]] name = "xmtp_id" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "alloy", "async-trait", @@ -10959,7 +10959,7 @@ dependencies = [ [[package]] name = "xmtp_logging" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "console_error_panic_hook", "opentelemetry", @@ -10981,7 +10981,7 @@ dependencies = [ [[package]] name = "xmtp_macro" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "num_cpus", "proc-macro2", @@ -10994,7 +10994,7 @@ dependencies = [ [[package]] name = "xmtp_mls" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "aes-gcm", "alloy", @@ -11078,7 +11078,7 @@ dependencies = [ [[package]] name = "xmtp_mls_common" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "bon", "const-hex", @@ -11105,7 +11105,7 @@ dependencies = [ [[package]] name = "xmtp_proto" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "async-trait", "bincode", @@ -11150,7 +11150,7 @@ dependencies = [ [[package]] name = "xmtpv3" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "alloy", "async-trait", @@ -11189,7 +11189,7 @@ dependencies = [ [[package]] name = "xnet-cli" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "clap", "color-eyre", @@ -11201,7 +11201,7 @@ dependencies = [ [[package]] name = "xnet-lib" -version = "1.11.0-dev" +version = "1.11.0" dependencies = [ "alloy", "ascii_table", diff --git a/Cargo.toml b/Cargo.toml index 9956acea44..1c551c98ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ resolver = "3" [workspace.package] license = "MIT" -version = "1.11.0-dev" +version = "1.11.0" rust-version = "1.94.0" publish = false edition = "2024" diff --git a/bindings/node/package.json b/bindings/node/package.json index 914d0f19f7..eb570580e1 100644 --- a/bindings/node/package.json +++ b/bindings/node/package.json @@ -1,6 +1,6 @@ { "name": "@xmtp/node-bindings", - "version": "1.11.0-dev", + "version": "1.11.0", "repository": { "type": "git", "url": "git+https://github.com/xmtp/libxmtp.git", diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index dc66b203da..fefb2d3edc 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@xmtp/wasm-bindings", - "version": "1.11.0-dev", + "version": "1.11.0", "description": "WASM bindings for the libXMTP rust library", "keywords": [ "xmtp", diff --git a/docs/release-notes/android/4.11.0.md b/docs/release-notes/android/4.11.0.md new file mode 100644 index 0000000000..84ef242ea5 --- /dev/null +++ b/docs/release-notes/android/4.11.0.md @@ -0,0 +1,137 @@ +--- +sdk = "android" +previous_release_version = "4.10.0" +previous_release_tag = "android-4.10.0" +--- + +# Android SDK 4.11.0 + +## Highlights + +- **Streams now follow your app's lifecycle automatically.** Backgrounding your app parks the shared streaming connection; foregrounding revives it. No code changes required. +- **New `catchUpToLive()`** for background jobs and cold start — a bounded, one-shot sync that brings the local store current and stops, instead of holding a live stream you are about to lose. +- **Idempotency keys on sends**, so a retried send produces the same message ID instead of a duplicate. +- **In-memory clients** via `Client.createInMemory(account, options)` for tests and ephemeral flows. +- **Group metadata migration read APIs**: `proposalsEnabled()` and `membershipCapabilities()`. + +This release contains breaking changes. Read the Breaking Changes section before upgrading. + +## What's Changed + +This release focuses on connection and lifecycle behavior. The two API-client connections libxmtp used to open per client have been collapsed into one, the `isSecure` option is now unused, and the streaming transport is shared process-wide and kept in step with app foreground/background transitions via `ProcessLifecycleOwner`. + +Alongside that, sends gained caller-settable idempotency keys, `ClientOptions` reorganized its database pool settings into a `DbPoolOptions` object and gained an option to wait for registration to become visible on the network, and groups gained read APIs for the in-progress migration to proposal-based metadata updates. + +Several reliability fixes land here too: a crash on the welcome stream after a group creator left and was re-added, false-positive fork detection after member removal, missed messages in streams following a recoverable error, and dropped group invites for members using smart contract wallets. + +The library now depends on `androidx.lifecycle:lifecycle-process` for stream lifecycle management. + +## Breaking Changes + +### `ClientOptions.maxDbPoolSize` and `minDbPoolSize` moved into `dbPoolOptions` + +**This is a breaking change.** + +```kotlin +// Before +ClientOptions(..., maxDbPoolSize = 25u, minDbPoolSize = 5u) + +// After +ClientOptions(..., dbPoolOptions = DbPoolOptions(maxPoolSize = 25u, minPoolSize = 5u)) +``` + +### `Client.connectToSyncApiBackend(api)` has been removed + +**This is a breaking change.** + +libxmtp no longer opens a separate sync connection — one API connection now serves both paths. Use `Client.connectToApiBackend(api)`. + +### `ClientOptions.Api.isSecure` is deprecated and no longer used + +`isSecure` no longer affects the connection and will be removed in a future release. It is also no longer part of the API client cache key. Remove it from your `ClientOptions.Api` constructor calls. + +## New Features + +### Automatic stream lifecycle management + +The streaming connection is shared across every `Client` in your process. The SDK now registers a `ProcessLifecycleOwner` observer on the first client you create, parks that connection when the app backgrounds, and revives it on foreground. + +This is on by default. To manage it yourself, set the process-global flag **before creating your first client**: + +```kotlin +Client.manageStreamLifecycle = false +``` + +A process started straight into the background (a service, a broadcast receiver, a WorkManager job) seeds the backgrounded state, so a stream opened there is born parked rather than opening a live connection you cannot use. One caveat: the seed lands a few async hops after the first client is created, so a stream opened immediately in that cold-start window can still briefly open live. Background work that only needs data should prefer `catchUpToLive()`, which uses its own connection. + +### `catchUpToLive(timeoutMs)` + +Bring the local store current with the network, then stop. Intended for background jobs and cold start, where holding a live stream is wasted work. + +```kotlin +val summary = client.catchUpToLive(timeoutMs = 20_000) +Log.d("XMTP", "${summary.messages} messages, ${summary.conversations} conversations, completed=${summary.completed}") +``` + +Pass `timeoutMs` to bound the run against a background budget (a WorkManager job, an FCM handler); `null` runs to the live edge. Cutting it short is safe — everything processed is persisted and a later call resumes from durable state. Check `summary.completed` before treating the counts as complete: on the deadline path it is `false` and the counts may undercount what was actually stored. + +### Idempotency keys on sends + +`SendOptions` and `MessageVisibilityOptions` accept an `idempotencyKey`. Re-sending identical content with the same key produces the same message ID and is deduplicated, so a retry after a timeout does not create a second message. Defaults to a timestamp when omitted. + +```kotlin +conversation.send( + content = "hello", + options = SendOptions(idempotencyKey = "my-stable-key"), +) +``` + +### In-memory clients + +```kotlin +val client = Client.createInMemory(account = signer, options = options) +``` + +No `.db3` file is created and no directory is touched. `dbPath` is `":memory:"`, `isInMemory` returns `true`, and `deleteLocalDatabase()`, `dropLocalDatabaseConnection()`, and `reconnectLocalDatabase()` are no-ops. `options.dbDirectory` and `options.dbEncryptionKey` are ignored. Intended for tests and ephemeral flows. + +### Wait for registration to become visible on the network + +`ClientOptions.waitForRegistrationVisible` accepts `VisibilityConfirmationOptions(quorumPercentage, quorumAbsolute, timeoutMs)`. When set, client creation waits until a quorum of nodes confirms the new identity and key package are visible before returning, which avoids racing a just-registered installation against propagation. Also available on `ffiRegisterIdentity(signatureRequest, visibilityConfirmationOptions)` if you drive registration yourself. + +### Group metadata migration read APIs + +- `group.proposalsEnabled()` — whether this group has migrated to proposal-based metadata updates. +- `group.membershipCapabilities()` — a snapshot of the group context's extension types plus, per member inbox and installation, the extension types each advertises. Use it to find which member is blocking migration. + +### `group.unstable.enableProposals()` (pre-release) + +Staging the migration itself is available, but only through a deliberate opt-in: it lives on `group.unstable` behind `@UnstableApi`. + +```kotlin +@OptIn(UnstableApi::class) +suspend fun migrate(group: Group) { + group.unstable.enableProposals() +} +``` + +The migration is one-way and irreversible — a migrated group cannot return to the legacy metadata path — and it hard-fails if any member's latest key package does not advertise the required proposal type (pass `force = true` to skip that pre-flight scan). When the API stabilizes it moves onto `Group` directly and the opt-in stops resolving, so you get a compile-time migration signal. + +### Native log level control + +`Client.setLibXMTPNativeLogLevel(logLevel)` sets the level for the native logcat layer independently of the persistent file log writer. Use `FfiLogLevel.TRACE` to capture span and activity events. + +## Bug Fixes + +- **Crash on the welcome stream** after a group's creator left the group and was later re-added. +- **False-positive fork detection** after a member was removed from a group. +- **Missed messages in streams** after a recoverable error: the stream resumed past envelopes it had not yet delivered. +- **Dropped group invites for members using smart contract wallets.** A transient failure while verifying an ERC-1271/6492 signature was classified as permanent, so the welcome cursor advanced and the invite was skipped for good. Those failures are now retryable and the welcome is reprocessed on the next sync. +- **New installations reliably added to your existing groups.** Reconciliation after a welcome now runs as a durable, retried task instead of one inline attempt whose per-group failures were swallowed, so a lost race no longer permanently skips a group. +- **Association-state cache returning wrong rows.** A pairwise filter in the database read cache was expanding into a cross product, so a batch inbox/installation lookup could return unrelated rows. +- **Database disconnect errors** now propagate out of background workers to the supervisor instead of being swallowed. +- **Downgrade resilience for group intents.** Intent rows written by a newer SDK version are now skipped rather than failing every intent query for that group, so downgrading to this version no longer wedges outbound messages in an affected group. +- **Archive export dropped migrated groups.** Groups that had migrated to proposal-based metadata were silently omitted from archives instead of exported. +- **Publish rollback.** A failed publish now resets the intent to `ToPublish` instead of leaving it stranded, and swallowed intent-error causes are preserved in `SyncSummary` rather than replaced with a generic error. +- **Membership adds during identity propagation.** `MissingSequenceId` is now retryable, so adding a member whose identity update has not fully propagated succeeds on retry instead of failing. +- **Groups stuck below the protocol version floor** now recover automatically once the client upgrades past the required floor, instead of staying paused. +- **Reduced log noise** across retry, sync, welcome, and worker paths — steady-state and per-item logs moved from info/error to debug/warn, and duplicate database pool-failure spam is deduplicated across workers. Already-processed welcomes log at warn rather than error. diff --git a/docs/release-notes/browser-sdk/7.1.0.md b/docs/release-notes/browser-sdk/7.1.0.md new file mode 100644 index 0000000000..b5c37e7fa3 --- /dev/null +++ b/docs/release-notes/browser-sdk/7.1.0.md @@ -0,0 +1,81 @@ +--- +sdk = "browser-sdk" +previous_release_version = "7.0.0" +--- + +# Browser SDK 7.1.0 + +## Highlights + +- **`@xmtp/browser-sdk` now ships from the [libxmtp](https://github.com/xmtp/libxmtp) monorepo**, released in lockstep with `@xmtp/wasm-bindings`. The package name and install instructions are unchanged. +- **Idempotency keys on sends**, so a retried send produces the same message ID instead of a duplicate. +- **The `send*` helpers take an options object** instead of a bare `isOptimistic` boolean — the one breaking change in this release. +- **Background worker tuning** via `workerConfig`, and an option to wait for a new installation to become visible on the network before using it. + +## What's Changed + +Sends now carry an optional idempotency key, and the per-content-type `send*` helpers take a `SendOpts` object so optimistic delivery and the idempotency key can be set together. The same change flows through the worker bridge, so the `conversation.send*` worker actions carry `opts` instead of `isOptimistic`. + +Underneath, the two API connections each client used to open have been collapsed into one, background workers are configurable via `workerConfig`, and client creation can wait for a quorum of nodes to confirm a new installation is visible. + +This release also picks up the reliability fixes in `@xmtp/wasm-bindings` 1.11.0 — see the [Bug Fixes](#bug-fixes) section. + +## Breaking Changes + +### The `send*` helpers take a `SendOpts` object instead of `isOptimistic` + +**This is a breaking change.** + +```ts +// Before +await conversation.sendText("gm", true); + +// After +await conversation.sendText("gm", { optimistic: true }); +``` + +This applies to `sendText`, `sendMarkdown`, `sendReaction`, `sendReadReceipt`, `sendReply`, `sendAttachment`, `sendRemoteAttachment`, `sendMultiRemoteAttachment`, `sendTransactionReference`, `sendWalletSendCalls`, `sendActions`, and `sendIntent`. + +`SendOpts` is exported from the package: + +```ts +import type { SendOpts } from "@xmtp/browser-sdk"; +``` + +If you drive the worker directly, the `conversation.send*` actions now take `opts?: SendOpts` in place of `isOptimistic?: boolean`. + +## New Features + +### Idempotency keys on sends + +Both `SendMessageOpts` (on `conversation.send`) and `SendOpts` (on the `send*` helpers) accept `idempotencyKey`. Re-sending identical content with the same key produces the same message ID and is deduplicated, so a retry after a dropped connection does not create a second message. Defaults to a timestamp when omitted. + +```ts +await conversation.sendText("gm", { idempotencyKey: "greeting-2026-07-28" }); +``` + +### Background worker configuration + +`ClientOptions.workerConfig` tunes the background worker scheduler — a global default interval, per-worker interval and jitter overrides, and a list of workers to disable. Kinds come from the `WorkerKind` enum, exported from the package: `DeviceSync`, `DisappearingMessages`, `KeyPackageCleaner`, `CommitLog`, `TaskRunner`. Use the enum members (`WorkerKind.DeviceSync`), not string literals. + +### Wait for registration to become visible on the network + +`ClientOptions.waitForRegistrationVisible` accepts `{ quorumPercentage?, quorumAbsolute?, timeoutMs? }`. When set, client creation waits until a quorum of nodes confirms the new identity and key package are visible before returning, which avoids racing a just-registered installation against propagation. + +## Bug Fixes + +- **`streamMessageDeletions` never invoked its close handler.** The stream now signals close when it ends, matching the other streams. +- **Logging initialization no longer fails hard** when a `tracing` subscriber is already installed — it warns and continues. +- **Crash on the welcome stream** after a group's creator left the group and was later re-added. +- **False-positive fork detection** after a member was removed from a group. +- **Missed messages in streams** after a recoverable error: the stream resumed past envelopes it had not yet delivered. +- **Cache read returning wrong rows.** A pairwise filter in the database read cache was expanding into a cross product, so a batch lookup could return unrelated rows. +- **Intentional transaction rollbacks** are no longer reported as telemetry errors, and database disconnect errors now propagate out of workers to the supervisor instead of being swallowed. +- **Dropped group invites for members using smart contract wallets.** A transient failure while verifying an ERC-1271/6492 signature was classified as permanent, so the welcome cursor advanced and the invite was skipped for good. Those failures are now retryable and the welcome is reprocessed on the next sync. +- **New installations reliably added to existing groups.** Reconciliation after a welcome now runs as a durable, retried task instead of one inline attempt whose per-group failures were swallowed, so a lost race no longer permanently skips a group. +- **Downgrade resilience for group intents.** Intent rows written by a newer version are now skipped rather than failing every intent query for that group, so downgrading to this version no longer wedges outbound messages in an affected group. +- **Archive export dropped migrated groups.** Groups that had migrated to proposal-based metadata were silently omitted from archives instead of exported. +- **Publish rollback.** A failed publish now resets the intent to `ToPublish` instead of leaving it stranded, and swallowed intent-error causes are preserved in the sync summary rather than replaced with a generic error. +- **Membership adds during identity propagation.** `MissingSequenceId` is now retryable, so adding a member whose identity update has not fully propagated succeeds on retry instead of failing. +- **Groups stuck below the protocol version floor** now recover automatically once the client upgrades past the required floor, instead of staying paused. +- **Reduced log noise** across retry, sync, welcome, and worker paths — steady-state and per-item logs moved from info/error to debug/warn. diff --git a/docs/release-notes/ios/4.11.0.md b/docs/release-notes/ios/4.11.0.md new file mode 100644 index 0000000000..35ca2a3da1 --- /dev/null +++ b/docs/release-notes/ios/4.11.0.md @@ -0,0 +1,160 @@ +--- +sdk = "ios" +previous_release_version = "4.10.0" +previous_release_tag = "ios-4.10.0" +--- + +# iOS SDK 4.11.0 + +## Highlights + +- **Streams now follow your app's lifecycle automatically.** Backgrounding your app parks the shared streaming connection; foregrounding revives it. No code changes required. +- **New `catchUpToLive()`** for background fetch and cold start — a bounded, one-shot sync that brings the local store current and stops, instead of holding a live stream you are about to lose. +- **Idempotency keys on sends**, so a retried send produces the same message ID instead of a duplicate. +- **A fix for a client memory leak**, which required a breaking change: `Group`, `Dm`, and `Conversation` now expose `clientInboxId` instead of a `client` back-reference. +- **In-memory clients** via `Client.createInMemory(account:options:)` for tests and ephemeral flows. + +This is a large release with several breaking changes. Read the Breaking Changes section before upgrading. + +## What's Changed + +This release focuses on connection and lifecycle behavior. The two API-client connections libxmtp used to open per client have been collapsed into one, the `isSecure` option is now unused, and the streaming transport is shared process-wide and kept in step with app foreground/background transitions. + +Alongside that, sends gained caller-settable idempotency keys, clients gained in-memory and DB-pool configuration, and groups gained read APIs for the in-progress migration to proposal-based metadata updates (`proposalsEnabled()`, `membershipCapabilities()`). + +Several reliability fixes land here too: a client reference cycle that leaked memory, a crash on the welcome stream after a group creator left and was re-added, false-positive fork detection after member removal, missed messages in streams following a recoverable error, remote attachment `contentLength` being written as `-1`, and a device-sync crash on Apple platforms caused by a missing TLS crypto provider. + +To depend on this release via Swift Package Manager: + +```swift +.package( + url: "https://github.com/xmtp/libxmtp.git", + revision: "ios-4.11.0" +), +``` + +## Breaking Changes + +### `Group`, `Dm`, and `Conversation` expose `clientInboxId` instead of `client` + +**This is a breaking change.** + +Conversation types no longer hold a reference back to the `Client` that created them. That reference formed a retain cycle with `Client.conversations` and leaked the client (and its database handles) for the lifetime of the process. + +```swift +// Before +let inboxId = group.client.inboxID + +// After +let inboxId = group.clientInboxId +``` + +If you were reaching through `conversation.client` to call client methods, hold your own reference to the `Client` instead. + +### `Client.connectToSyncApiBackend(api:)` has been removed + +**This is a breaking change.** + +libxmtp no longer opens a separate sync connection — one API connection now serves both paths. Use `Client.connectToApiBackend(api:)`. + +### `preferences.syncConsent()` has been removed + +**This is a breaking change.** + +It was deprecated in a prior release. Use `preferences.sync()`. + +### `ClientOptions.Api.isSecure` is deprecated and no longer used + +`isSecure` no longer affects the connection and will be removed in a future release. It is also no longer part of the API client cache key. Remove it from your `ClientOptions.Api` initializers. + +## New Features + +### Automatic stream lifecycle management + +The streaming connection is shared across every `Client` in your process. XMTPiOS now registers app-lifecycle observers on the first client you create, parks that connection when the app backgrounds, and revives it on foreground. + +This is on by default. To manage it yourself — for example in an app extension — set the process-global flag **before creating your first client**: + +```swift +Client.manageStreamLifecycle = false +``` + +A process launched straight into the background (silent push, `BGTask`) seeds the backgrounded state, so a stream opened there is born parked rather than opening a live connection you cannot use. + +### `catchUpToLive(timeoutMs:)` + +Bring the local store current with the network, then stop. Intended for background fetch and cold start, where holding a live stream is wasted work. + +```swift +let summary = try await client.catchUpToLive(timeoutMs: 20_000) +print(summary.messages, summary.conversations, summary.completed) +``` + +Pass `timeoutMs` to bound the run against an OS background budget; `nil` runs to the live edge. Cutting it short is safe — everything processed is persisted and a later call resumes from durable state. Check `summary.completed` before treating the counts as complete: on the deadline path it is `false` and the counts may undercount what was actually stored. + +### Idempotency keys on sends + +`SendOptions` and `MessageVisibilityOptions` accept an `idempotencyKey`. Re-sending identical content with the same key produces the same message ID and is deduplicated, so a retry after a timeout does not create a second message. Defaults to a timestamp when omitted. + +```swift +try await conversation.send( + content: "hello", + options: SendOptions(idempotencyKey: "my-stable-key") +) +``` + +### In-memory clients + +```swift +let client = try await Client.createInMemory(account: signer, options: options) +``` + +No `.db3` file is created and no directory is touched. `dbPath` is `":memory:"`, `isInMemory` returns `true`, and `deleteLocalDatabase()`, `dropLocalDatabaseConnection()`, and `reconnectLocalDatabase()` are no-ops. `options.dbDirectory` and `options.dbEncryptionKey` are ignored. Intended for tests and ephemeral flows. + +### Database pool configuration + +`ClientOptions.dbPoolOptions` accepts `DbPoolOptions(maxPoolSize:minPoolSize:)` to tune the local SQLite connection pool. + +### Wait for registration to become visible on the network + +`ClientOptions.waitForRegistrationVisible` accepts `VisibilityConfirmationOptions(quorumPercentage:quorumAbsolute:timeoutMs:)`. When set, client creation waits until a quorum of nodes confirms the new identity and key package are visible before returning, which avoids racing a just-registered installation against propagation. Also available on `ffiRegisterIdentity(signatureRequest:visibilityConfirmationOptions:)` if you drive registration yourself. + +### Group metadata migration read APIs + +- `group.proposalsEnabled()` — whether this group has migrated to proposal-based metadata updates. +- `group.membershipCapabilities()` — a snapshot of the group context's extension types plus, per member inbox and installation, the extension types each advertises. Use it to find which member is blocking migration. + +### `group.unstable.enableProposals()` (pre-release) + +Staging the migration itself is available, but only through a deliberate opt-in: it lives on `group.unstable` and is gated behind `@_spi(Unstable)`. + +```swift +@_spi(Unstable) import XMTPiOS + +try await group.unstable.enableProposals() +``` + +The migration is one-way and irreversible — a migrated group cannot return to the legacy metadata path — and it hard-fails with `ProposalsNotSupported` if any member's latest key package does not advertise the required proposal type (pass `force: true` to skip that pre-flight scan). When the API stabilizes it moves onto `Group` directly, and the `.unstable` form breaks at compile time so you get a migration signal. + +### Trace-level logging and a native log level control + +`Client.LogLevel` gained `.trace`, and `Client.setLibXMTPNativeLogLevel(_:)` sets the level for the native oslog layer independently of the persistent file log writer. At `.trace`, tracing spans surface as `os_signpost` activities visible in Console.app and Instruments. + +## Bug Fixes + +- **Client memory leak.** `Client` was retained indefinitely by a reference cycle with its `conversations` property. Conversation types no longer hold a client back-reference (see Breaking Changes). +- **Crash on the welcome stream** after a group's creator left the group and was later re-added. +- **False-positive fork detection** after a member was removed from a group. +- **Missed messages in streams** after a recoverable error: the stream resumed past envelopes it had not yet delivered. +- **Remote attachment `contentLength`.** `RemoteAttachmentCodec` wrote `contentLength: "-1"` when the length was unknown; it now omits the parameter, and both `RemoteAttachment` and `MultiRemoteAttachment` derive the length from the encrypted payload when it is not supplied. +- **Device sync crash on Apple platforms.** The rustls crypto provider is now installed explicitly instead of relying on a static constructor that does not run on Apple targets, which caused a "No provider set" panic in the device-sync worker's HTTP client. +- **Dropped group invites for members using smart contract wallets.** A transient failure while verifying an ERC-1271/6492 signature was classified as permanent, so the welcome cursor advanced and the invite was skipped for good. Those failures are now retryable and the welcome is reprocessed on the next sync. +- **New installations reliably added to your existing groups.** Reconciliation after a welcome now runs as a durable, retried task instead of one inline attempt whose per-group failures were swallowed, so a lost race no longer permanently skips a group. +- **Association-state cache returning wrong rows.** A pairwise filter in the database read cache was expanding into a cross product, so a batch inbox/installation lookup could return unrelated rows. +- **Database disconnect errors** now propagate out of background workers to the supervisor instead of being swallowed. +- **Downgrade resilience for group intents.** Intent rows written by a newer SDK version are now skipped rather than failing every intent query for that group, so downgrading to this version no longer wedges outbound messages in an affected group. +- **Archive export dropped migrated groups.** Groups that had migrated to proposal-based metadata were silently omitted from archives instead of exported. +- **Publish rollback.** A failed publish now resets the intent to `ToPublish` instead of leaving it stranded, and swallowed intent-error causes are preserved in `SyncSummary` rather than replaced with a generic error. +- **Membership adds during identity propagation.** `MissingSequenceId` is now retryable, so adding a member whose identity update has not fully propagated succeeds on retry instead of failing. +- **Groups stuck below the protocol version floor** now recover automatically once the client upgrades past the required floor, instead of staying paused. +- **Reduced log noise** across retry, sync, welcome, and worker paths — steady-state and per-item logs moved from info/error to debug/warn, and duplicate database pool-failure spam is deduplicated across workers. Already-processed welcomes log at warn rather than error. diff --git a/docs/release-notes/node-bindings/1.11.0.md b/docs/release-notes/node-bindings/1.11.0.md new file mode 100644 index 0000000000..d8c5926647 --- /dev/null +++ b/docs/release-notes/node-bindings/1.11.0.md @@ -0,0 +1,191 @@ +--- +sdk = "node-bindings" +previous_release_version = "1.11.0-dev" +--- + +# Node SDK 1.11.0 + +## Highlights + +- **`client.catchUpToLive()`** — a bounded, one-shot sync for agents that poll for new state instead of holding a live stream. +- **`client.close()`** for clean shutdown, so deleting the database file no longer races detached workers. +- **Idempotency keys on sends**, so a retried send produces the same message ID instead of a duplicate. +- **OpenTelemetry export of both spans and logs**, plus a standalone `initLogging()` you can call before creating a client. +- **Background worker tuning** via `workerConfig` — per-worker intervals, jitter, and enable/disable. +- **Single-connection database mode** for services running many clients in one process. + +This release contains breaking changes. Read the Breaking Changes section before upgrading. + +## What's Changed + +The two API connections each client used to open have been collapsed into one, and `isSecure` is gone from the connection path. Logging moved onto a dedicated pipeline that can export spans and logs over OTLP, with a separate stdout level so a log shipper does not duplicate what OTLP already carries. Every exported binding function now emits an error-case span, so failures are attributable per call. + +On the messaging side, sends accept an idempotency key; the top-level `send*` helpers take an options object instead of a bare boolean; and `updateAppData` takes an options object so future fields can be added without another break. Groups gained `proposalsEnabled()` and a `conversation.unstable.enableProposals()` entry point for the in-progress migration to proposal-based metadata updates. + +The binding also switches to mimalloc as its global allocator and installs the rustls crypto provider explicitly, which fixes a device-sync panic on platforms where the previous static constructor did not run. + +## Breaking Changes + +### `createClient` drops `isSecure` and both create functions gain `workerConfig` + +**This is a breaking change.** + +Two positional changes to `createClient`: + +- The `isSecure: boolean` third argument is **gone**. Every parameter after it shifts left by one. +- A `workerConfig?: WorkerConfigOptions` parameter was inserted between `deviceSyncWorkerMode` and `logOptions`. + +Before: `(v3Host, gatewayHost, isSecure, db, inboxId, accountIdentifier, deviceSyncWorkerMode, logOptions, …)` + +After: `(v3Host, gatewayHost, db, inboxId, accountIdentifier, deviceSyncWorkerMode, workerConfig, logOptions, …)` + +`createClientWithBackend` gains the same `workerConfig` parameter in the same position (pass `undefined` for previous behavior). The separate `syncApi` client argument has been removed from the internal wiring — one API client now serves both paths. + +### The top-level `send*` helpers take a `SendOpts` object instead of `isOptimistic` + +**This is a breaking change.** + +```ts +// Before +await conversation.sendText("hello", true); + +// After +await conversation.sendText("hello", { optimistic: true }); +``` + +This applies to `sendText`, `sendMarkdown`, `sendReaction`, `sendReadReceipt`, `sendReply`, `sendAttachment`, `sendRemoteAttachment`, `sendMultiRemoteAttachment`, `sendTransactionReference`, `sendWalletSendCalls`, `sendActions`, and `sendIntent`. + +### `updateAppData` takes an options object + +**This is a breaking change.** + +```ts +// Before +await conversation.updateAppData("payload"); + +// After +await conversation.updateAppData({ value: "payload" }); +``` + +### `registerIdentity` takes an optional second argument + +`registerIdentity(signatureRequest, visibilityConfirmationOptions?)`. Existing single-argument calls continue to work. + +### `prepareMessage` takes an optional third argument + +`prepareMessage(encodedContent, shouldPush, idempotencyKey?)`. Existing two-argument calls continue to work. + +### `getConversationById` validates the group ID + +Group IDs are now fixed-size values. A hex string that does not decode to a valid group ID is rejected with an error rather than producing a conversation handle that fails later. + +## New Features + +### `client.catchUpToLive()` + +Bring the local store current with the network, then stop — a bounded, one-shot sync for agents that poll rather than hold a live stream. + +```ts +const summary = await client.catchUpToLive(); +console.log(summary.messages, summary.conversations, summary.completed); +``` + +Counts are plain numbers (not `BigInt`). Idempotent: everything processed is persisted and catch-up does not advance durable message cursors, so a later call resumes from durable state and one with nothing owed reports zero. There is no caller deadline; the run is internally bounded by capped retries with backoff and a wire watchdog. Race it against your own timer if you need a hard wall-clock bound. + +### `client.close()` + +Cancel in-flight workers and streams, then release the database connection. Idempotent. `await` this before deleting the SQLite file or dropping the client reference to avoid late log spew from detached workers firing against a dead database. + +### Idempotency keys on sends + +`SendMessageOpts` and the new `SendOpts` both accept `idempotencyKey`. Re-sending identical content with the same key produces the same message ID and is deduplicated, so a retry after a timeout does not create a second message. Defaults to a timestamp when omitted. + +### OpenTelemetry span and log export + +`LogOptions` gained: + +- `otelEndpoint` — e.g. `"http://collector:4317"`. Spans **and** `tracing` events (as correlated logs) are exported via OTLP. +- `resourceAttributes` — attributes attached to all exported telemetry, e.g. `{ "service.instance.id": "worker-7" }`. +- `stdoutLevel` — level for the stdout console layer only. Defaults to `level`. Set it to `warn` to quiet stdout below the OTLP export level so a log shipper does not duplicate logs already exported. + +Two new process-global functions: + +- `initLogging(options?)` — install the logging pipeline before any client exists. It is `async`, so `await` it. Idempotent; the first call wins, so it is safe to call this early and still pass `logOptions` to `createClient`. +- `flushTelemetry()` — flush buffered spans on graceful shutdown. No-op if telemetry was never enabled. + +### Background worker configuration + +`WorkerConfigOptions` — passed to `createClient` / `createClientWithBackend` — lets you tune the background worker scheduler: + +```ts +const workerConfig = { + defaultIntervalNs: 5_000_000_000n, + workerIntervalsNs: [{ kind: "DeviceSync", intervalNs: 30_000_000_000n }], + workerJittersNs: [{ kind: "CommitLog", jitterNs: 1_000_000_000n }], + disabledWorkers: ["DisappearingMessages"], +}; +``` + +Worker kinds: `DeviceSync`, `DisappearingMessages`, `KeyPackageCleaner`, `CommitLog`, `TaskRunner`. Omitting the whole object preserves default behavior. + +### Single-connection database mode and pool sizing + +`DbOptions` gained `useSingleConnection`. When `true`, the native database uses one connection (one file descriptor) instead of a pool, and `maxDbPoolSize`/`minDbPoolSize` are ignored. Intended for services running many clients in one process. + +### `client.waitForRegistrationVisible(options?)` + +Wait until a quorum of nodes confirms this client's identity update and key package are visible, so you do not race a just-registered installation against propagation. Configurable with `quorumPercentage`, `quorumAbsolute`, and `timeoutMs` (default 30 000 ms). Also available inline as the second argument to `registerIdentity`. + +### Group metadata migration APIs + +- `conversation.proposalsEnabled()` — whether this group has migrated to proposal-based metadata updates. +- `conversation.unstable.enableProposals({ force?, minVersion? })` — stage the migration. One-way and irreversible; hard-fails if any member's latest key package does not advertise the required proposal type. The `.unstable` namespace is the deliberate opt-in and will break at compile time when the API graduates. + +### Error-case tracing on every exported function + +Every `#[napi]` function now records a span on the error path, so a failure is attributable to a specific binding call in your traces. + +### gRPC keepalive is tunable via environment variables + +For deployments on high-latency paths where the default fast keepalive causes spurious `UNAVAILABLE` disconnects. Defaults are unchanged. + +| Variable | Default | +| --- | --- | +| `XMTP_GRPC_KEEPALIVE_INTERVAL_SECS` | 16 | +| `XMTP_GRPC_KEEPALIVE_TIMEOUT_SECS` | 10 | +| `XMTP_GRPC_TCP_KEEPALIVE_SECS` (0 disables) | 16 | +| `XMTP_GRPC_KEEPALIVE_WHILE_IDLE` | true | + +### Opt-in stream-liveness watchdog + +A subscription whose transport wedges open — an L7 proxy that keeps answering HTTP/2 pings while the backend subscription is gone — used to deliver neither an error nor a close, and the consumer just hung. There is now an idle timer per stream that, when nothing arrives within the timeout, tears the stream down and re-subscribes from the persisted cursor. + +It is **off by default**: with no env vars set, streams behave exactly as before. Long-lived agents can opt in. + +| Variable | Default | +| --- | --- | +| `XMTP_STREAM_WATCHDOG_ENABLED` | false | +| `XMTP_STREAM_WATCHDOG_IDLE_TIMEOUT_SECS` | 300 | +| `XMTP_STREAM_WATCHDOG_RECONNECT_BASE_SECS` | 1 | +| `XMTP_STREAM_WATCHDOG_RECONNECT_JITTER_MS` (0 disables) | 1000 | + +Because there is no server-side keepalive on this path yet, the idle timeout is deliberately long — a healthy but dormant stream will also trip and reconnect roughly once per timeout. Values are read once per process at first use. + +## Bug Fixes + +- **Device sync panic on some platforms.** The rustls crypto provider is now installed explicitly instead of relying on a static constructor that does not run everywhere (notably Apple targets), which caused a "No provider set" panic in the device-sync worker's HTTP client. +- **Crash on the welcome stream** after a group's creator left the group and was later re-added. +- **False-positive fork detection** after a member was removed from a group. +- **Missed messages in streams** after a recoverable error: the stream resumed past envelopes it had not yet delivered. +- **Cache read returning wrong rows.** A pairwise filter in the database read cache was expanding into a cross product, so a batch lookup could return unrelated rows. +- **Intentional transaction rollbacks** are no longer reported as telemetry errors. +- **Database disconnect errors** now propagate out of workers to the supervisor instead of being swallowed. +- **Dropped group invites for members using smart contract wallets.** A transient failure while verifying an ERC-1271/6492 signature was classified as permanent, so the welcome cursor advanced and the invite was skipped for good. Those failures are now retryable and the welcome is reprocessed on the next sync. +- **New installations reliably added to existing groups.** Reconciliation after a welcome now runs as a durable, retried task instead of one inline attempt whose per-group failures were swallowed, so a lost race no longer permanently skips a group. +- **Downgrade resilience for group intents.** Intent rows written by a newer version are now skipped rather than failing every intent query for that group, so downgrading to this version no longer wedges outbound messages in an affected group. +- **Archive export dropped migrated groups.** Groups that had migrated to proposal-based metadata were silently omitted from archives instead of exported. +- **Publish rollback.** A failed publish now resets the intent to `ToPublish` instead of leaving it stranded, and swallowed intent-error causes are preserved in `SyncSummary` rather than replaced with a generic error. +- **Membership adds during identity propagation.** `MissingSequenceId` is now retryable, so adding a member whose identity update has not fully propagated succeeds on retry instead of failing. +- **Groups stuck below the protocol version floor** now recover automatically once the client upgrades past the required floor, instead of staying paused. +- **`GetNodes` DNS flakes** are now retried instead of failing the call. +- **Reduced log noise** across retry, sync, welcome, and worker paths — steady-state and per-item logs moved from info/error to debug/warn, and duplicate database pool-failure spam is deduplicated across workers. diff --git a/docs/release-notes/node-sdk/6.1.0.md b/docs/release-notes/node-sdk/6.1.0.md new file mode 100644 index 0000000000..c33419e58c --- /dev/null +++ b/docs/release-notes/node-sdk/6.1.0.md @@ -0,0 +1,136 @@ +--- +sdk = "node-sdk" +previous_release_version = "6.0.0" +--- + +# Node SDK 6.1.0 + +## Highlights + +- **`@xmtp/node-sdk` now ships from the [libxmtp](https://github.com/xmtp/libxmtp) monorepo**, released in lockstep with `@xmtp/node-bindings`. The package name and install instructions are unchanged. +- **Idempotency keys on sends**, so a retried send produces the same message ID instead of a duplicate. +- **The `send*` helpers take an options object** instead of a bare `isOptimistic` boolean — the one breaking change in this release. +- **Database pool and single-connection options**, for services running many clients in one process. +- **OpenTelemetry export of both spans and logs**, plus a standalone `initLogging()` you can call before creating a client. +- **Background worker tuning** via `workerConfig` — per-worker intervals, jitter, and enable/disable. + +## What's Changed + +Sends now carry an optional idempotency key, and the per-content-type `send*` helpers take a `SendOpts` object so optimistic delivery and the idempotency key can be set together. `Client.create` and `Client.build` also correctly preserve the `{ backend }` options arm when you pass codecs — previously TypeScript collapsed that union and rejected the option. + +Underneath, the two API connections each client used to open have been collapsed into one, background workers are configurable, logging can export spans and logs over OTLP with a separate stdout level, and the local database can run on a single connection instead of a pool. + +This release also picks up the reliability fixes in `@xmtp/node-bindings` 1.11.0 — see the [Bug Fixes](#bug-fixes) section. + +## Breaking Changes + +### The `send*` helpers take a `SendOpts` object instead of `isOptimistic` + +**This is a breaking change.** + +```ts +// Before +await conversation.sendText("gm", true); + +// After +await conversation.sendText("gm", { optimistic: true }); +``` + +This applies to `sendText`, `sendMarkdown`, `sendReaction`, `sendReadReceipt`, `sendReply`, `sendAttachment`, `sendRemoteAttachment`, `sendMultiRemoteAttachment`, `sendTransactionReference`, `sendWalletSendCalls`, `sendActions`, and `sendIntent`. + +`SendOpts` is exported from the package: + +```ts +import type { SendOpts } from "@xmtp/node-sdk"; +``` + +## New Features + +### Idempotency keys on sends + +Both `SendMessageOpts` (on `conversation.send`) and `SendOpts` (on the `send*` helpers) accept `idempotencyKey`. Re-sending identical content with the same key produces the same message ID and is deduplicated, so a retry after a timeout does not create a second message. Defaults to a timestamp when omitted. + +```ts +await conversation.sendText("gm", { idempotencyKey: "greeting-2026-07-28" }); +``` + +### Database pool configuration and single-connection mode + +`ClientOptions` gained three storage options: + +- `maxDbPoolSize` — maximum connections in the local database pool (default 25). +- `minDbPoolSize` — connections kept warm (default 5). +- `useSingleConnection` — when `true`, the native database uses one connection (one file descriptor) instead of a pool, and the pool-size options are ignored. Intended for services running many clients in one process. + +### Telemetry and logging + +- `otelEndpoint` — e.g. `"http://collector:4317"`. Spans **and** log events are exported via OTLP, so a downstream OpenTelemetry Collector can derive metrics from the spans and forward the correlated logs. +- `resourceAttributes` — attributes attached to all exported telemetry, e.g. `{ "service.instance.id": "worker-7" }`. +- `stdoutLoggingLevel` — level for the stdout console layer only. Defaults to `loggingLevel`. Set it to `LogLevel.Warn` to quiet stdout below the OTLP export level so a log shipper does not duplicate logs already exported. +- `initLogging(options?)` is now re-exported. `await` it before creating any client to install the logging pipeline early; it is idempotent, so passing `logOptions`-style fields to `Client.create` afterwards is still safe. +- Call `flushTelemetry()` on graceful shutdown to flush buffered spans. + +### Background worker configuration + +`ClientOptions.workerConfig` tunes the background worker scheduler — a global default interval, per-worker interval and jitter overrides, and a list of workers to disable. Worker kinds: `DeviceSync`, `DisappearingMessages`, `KeyPackageCleaner`, `CommitLog`, `TaskRunner`. + +```ts +const client = await Client.create(signer, { + env: "production", + workerConfig: { + workerIntervalsNs: [{ kind: "DeviceSync", intervalNs: 30_000_000_000n }], + disabledWorkers: ["DisappearingMessages"], + }, +}); +``` + +`WorkerJitterOverride` is now re-exported alongside `WorkerIntervalOverride` and `WorkerConfigOptions`. + +### Wait for registration to become visible on the network + +`ClientOptions.waitForRegistrationVisible` accepts `{ quorumPercentage?, quorumAbsolute?, timeoutMs? }`. When set, client creation waits until a quorum of nodes confirms the new identity and key package are visible before returning, which avoids racing a just-registered installation against propagation. + +### gRPC keepalive is tunable via environment variables + +For deployments on high-latency paths where the default fast keepalive causes spurious `UNAVAILABLE` disconnects. Defaults are unchanged, and no code change is needed — set the variables in your process environment. + +| Variable | Default | +| --- | --- | +| `XMTP_GRPC_KEEPALIVE_INTERVAL_SECS` | 16 | +| `XMTP_GRPC_KEEPALIVE_TIMEOUT_SECS` | 10 | +| `XMTP_GRPC_TCP_KEEPALIVE_SECS` (0 disables) | 16 | +| `XMTP_GRPC_KEEPALIVE_WHILE_IDLE` | true | + +### Opt-in stream-liveness watchdog + +A subscription whose transport wedges open — an L7 proxy that keeps answering HTTP/2 pings while the backend subscription is gone — used to deliver neither an error nor a close, and the consumer just hung. There is now an idle timer per stream that, when nothing arrives within the timeout, tears the stream down and re-subscribes from the persisted cursor. + +It is **off by default**: with no env vars set, streams behave exactly as before. Long-running agents can opt in. + +| Variable | Default | +| --- | --- | +| `XMTP_STREAM_WATCHDOG_ENABLED` | false | +| `XMTP_STREAM_WATCHDOG_IDLE_TIMEOUT_SECS` | 300 | +| `XMTP_STREAM_WATCHDOG_RECONNECT_BASE_SECS` | 1 | +| `XMTP_STREAM_WATCHDOG_RECONNECT_JITTER_MS` (0 disables) | 1000 | + +Because there is no server-side keepalive on this path yet, the idle timeout is deliberately long — a healthy but dormant stream will also trip and reconnect roughly once per timeout. Values are read once per process at first use. + +## Bug Fixes + +- **`Client.create` / `Client.build` rejected the `{ backend }` option when codecs were passed.** `Omit` collapsed the options union and dropped the `backend` arm; a distributive omit now preserves it. +- **Device sync panic on some platforms.** The rustls crypto provider is now installed explicitly instead of relying on a static constructor that does not run everywhere, which caused a "No provider set" panic in the device-sync worker's HTTP client. +- **Crash on the welcome stream** after a group's creator left the group and was later re-added. +- **False-positive fork detection** after a member was removed from a group. +- **Missed messages in streams** after a recoverable error: the stream resumed past envelopes it had not yet delivered. +- **Cache read returning wrong rows.** A pairwise filter in the database read cache was expanding into a cross product, so a batch lookup could return unrelated rows. +- **Intentional transaction rollbacks** are no longer reported as telemetry errors, and database disconnect errors now propagate out of workers to the supervisor instead of being swallowed. +- **Dropped group invites for members using smart contract wallets.** A transient failure while verifying an ERC-1271/6492 signature was classified as permanent, so the welcome cursor advanced and the invite was skipped for good. Those failures are now retryable and the welcome is reprocessed on the next sync. +- **New installations reliably added to existing groups.** Reconciliation after a welcome now runs as a durable, retried task instead of one inline attempt whose per-group failures were swallowed, so a lost race no longer permanently skips a group. +- **Downgrade resilience for group intents.** Intent rows written by a newer version are now skipped rather than failing every intent query for that group, so downgrading to this version no longer wedges outbound messages in an affected group. +- **Archive export dropped migrated groups.** Groups that had migrated to proposal-based metadata were silently omitted from archives instead of exported. +- **Publish rollback.** A failed publish now resets the intent to `ToPublish` instead of leaving it stranded, and swallowed intent-error causes are preserved in the sync summary rather than replaced with a generic error. +- **Membership adds during identity propagation.** `MissingSequenceId` is now retryable, so adding a member whose identity update has not fully propagated succeeds on retry instead of failing. +- **Groups stuck below the protocol version floor** now recover automatically once the client upgrades past the required floor, instead of staying paused. +- **`GetNodes` DNS flakes** are now retried instead of failing the call. +- **Reduced log noise** across retry, sync, welcome, and worker paths — steady-state and per-item logs moved from info/error to debug/warn, and duplicate database pool-failure spam is deduplicated across workers. diff --git a/docs/release-notes/wasm-bindings/1.11.0.md b/docs/release-notes/wasm-bindings/1.11.0.md new file mode 100644 index 0000000000..72901eb68d --- /dev/null +++ b/docs/release-notes/wasm-bindings/1.11.0.md @@ -0,0 +1,121 @@ +--- +sdk = "wasm-bindings" +previous_release_version = "1.11.0-dev" +--- + +# WASM SDK 1.11.0 + +## Highlights + +- **Idempotency keys on sends**, so a retried send produces the same message ID instead of a duplicate. +- **The top-level `send*` helpers now take an options object** instead of a bare `optimistic` boolean. +- **`client.waitForRegistrationVisible()`** — wait until a quorum of nodes confirms a new installation is visible before using it. +- **Background worker tuning** via `workerConfig` — per-worker intervals, jitter, and enable/disable. +- **Group metadata migration APIs**: `proposalsEnabled()` and `conversation.unstable.enableProposals()`. + +This release contains breaking changes. Read the Breaking Changes section before upgrading. + +## What's Changed + +The two API connections each client used to open have been collapsed into one, and `isSecure` is gone from the connection path. `createClient` and `createClientWithBackend` gained a `workerConfig` argument for tuning background workers, and `registerIdentity` can now optionally wait for the new installation to become visible across a quorum of nodes. + +On the messaging side, sends accept an idempotency key, the top-level `send*` helpers take an options object, and `updateAppData` takes an options object so future fields can be added without another break. Groups gained `proposalsEnabled()` and a `conversation.unstable.enableProposals()` entry point for the in-progress migration to proposal-based metadata updates. + +Logging initialization is now tolerant of a subscriber already installed by another crate — it warns instead of panicking. + +## Breaking Changes + +### `createClient` and `createClientWithBackend` take a `workerConfig` argument + +**This is a breaking change.** + +A `workerConfig?: WorkerConfigOptions` parameter was inserted between `deviceSyncMode` and `logOptions`. Positional callers must add it (pass `undefined` for previous behavior). The separate sync API client argument has been removed from the internal wiring — one API client now serves both paths. + +### The top-level `send*` helpers take a `SendOpts` object instead of `optimistic` + +**This is a breaking change.** + +```ts +// Before +await conversation.sendText("hello", true); + +// After +await conversation.sendText("hello", { optimistic: true }); +``` + +This applies to `sendText`, `sendMarkdown`, `sendReaction`, `sendReadReceipt`, `sendReply`, `sendAttachment`, `sendRemoteAttachment`, `sendMultiRemoteAttachment`, `sendTransactionReference`, `sendWalletSendCalls`, `sendActions`, and `sendIntent`. + +### `updateAppData` takes an options object + +**This is a breaking change.** + +```ts +// Before +await conversation.updateAppData("payload"); + +// After +await conversation.updateAppData({ value: "payload" }); +``` + +### `registerIdentity` takes an optional second argument + +`registerIdentity(signatureRequest, visibilityConfirmationOptions?)`. Existing single-argument calls continue to work. + +### `prepareMessage` takes an optional third argument + +`prepareMessage(encodedContent, shouldPush, idempotencyKey?)`. Existing two-argument calls continue to work. + +### `getConversationById` validates the group ID + +Group IDs are now fixed-size values. A hex string that does not decode to a valid group ID is rejected with an error rather than producing a conversation handle that fails later. + +## New Features + +### Idempotency keys on sends + +`SendMessageOpts` and the new `SendOpts` both accept `idempotencyKey`. Re-sending identical content with the same key produces the same message ID and is deduplicated, so a retry after a timeout does not create a second message. Defaults to a timestamp when omitted. + +### `client.waitForRegistrationVisible(options?)` + +Wait until this client's registration is visible on the network. For clients on the current network this falls back to checking `isRegistered`; on the decentralized network it polls nodes directly and returns once a quorum confirms both the identity-update and key-package envelopes are visible. + +Configurable with `quorumPercentage` (default 50%), `quorumAbsolute` (takes precedence when both are set), and `timeoutMs` (default 30 000 ms). Also available inline as the second argument to `registerIdentity`. + +### Background worker configuration + +`WorkerConfigOptions` — passed to `createClient` / `createClientWithBackend` — lets you tune the background worker scheduler: + +```ts +const workerConfig = { + defaultIntervalNs: 5_000_000_000n, + workerIntervalsNs: [{ kind: WorkerKind.DeviceSync, intervalNs: 30_000_000_000n }], + workerJittersNs: [{ kind: WorkerKind.CommitLog, jitterNs: 1_000_000_000n }], + disabledWorkers: [WorkerKind.DisappearingMessages], +}; +``` + +Worker kinds: `DeviceSync`, `DisappearingMessages`, `KeyPackageCleaner`, `CommitLog`, `TaskRunner`. Omitting the whole object preserves default behavior. + +### Group metadata migration APIs + +- `conversation.proposalsEnabled()` — whether this group has migrated to proposal-based metadata updates. +- `conversation.unstable.enableProposals({ force?, minVersion? })` — stage the migration. One-way and irreversible; hard-fails if any member's latest key package does not advertise the required proposal type. The `.unstable` namespace is the deliberate opt-in and will break at compile time when the API graduates. + +## Bug Fixes + +- **`streamMessageDeletions` never invoked `onClose`.** The stream now calls the close handler when it ends, matching the other streams. +- **Logging initialization no longer fails hard** when another crate has already installed a `tracing` subscriber — it logs a warning and continues instead of panicking. +- **Crash on the welcome stream** after a group's creator left the group and was later re-added. +- **False-positive fork detection** after a member was removed from a group. +- **Missed messages in streams** after a recoverable error: the stream resumed past envelopes it had not yet delivered. +- **Cache read returning wrong rows.** A pairwise filter in the database read cache was expanding into a cross product, so a batch lookup could return unrelated rows. +- **Intentional transaction rollbacks** are no longer reported as telemetry errors. +- **Database disconnect errors** now propagate out of workers to the supervisor instead of being swallowed. +- **Dropped group invites for members using smart contract wallets.** A transient failure while verifying an ERC-1271/6492 signature was classified as permanent, so the welcome cursor advanced and the invite was skipped for good. Those failures are now retryable and the welcome is reprocessed on the next sync. +- **New installations reliably added to existing groups.** Reconciliation after a welcome now runs as a durable, retried task instead of one inline attempt whose per-group failures were swallowed, so a lost race no longer permanently skips a group. +- **Downgrade resilience for group intents.** Intent rows written by a newer version are now skipped rather than failing every intent query for that group, so downgrading to this version no longer wedges outbound messages in an affected group. +- **Archive export dropped migrated groups.** Groups that had migrated to proposal-based metadata were silently omitted from archives instead of exported. +- **Publish rollback.** A failed publish now resets the intent to `ToPublish` instead of leaving it stranded, and swallowed intent-error causes are preserved in `SyncSummary` rather than replaced with a generic error. +- **Membership adds during identity propagation.** `MissingSequenceId` is now retryable, so adding a member whose identity update has not fully propagated succeeds on retry instead of failing. +- **Groups stuck below the protocol version floor** now recover automatically once the client upgrades past the required floor, instead of staying paused. +- **Reduced log noise** across retry, sync, welcome, and worker paths — steady-state and per-item logs moved from info/error to debug/warn. diff --git a/nix/package/node.nix b/nix/package/node.nix index c325a532b7..03e4e9d22f 100644 --- a/nix/package/node.nix +++ b/nix/package/node.nix @@ -81,11 +81,8 @@ rust.napiBuild ( ''; } // lib.optionalAttrs stdenv.hostPlatform.isDarwin { - # sigtool provides `codesign` on PATH for the postFixup re-sign. - # See https://github.com/xmtp/libxmtp/issues/3513. nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ darwin.autoSignDarwinBinariesHook - darwin.sigtool ]; postFixup = '' NODE_LIB=$(echo $out/dist/bindings_node.*.node) @@ -108,10 +105,11 @@ rust.napiBuild ( install_name_tool -change "$old" "/usr/lib/$(basename "$old")" "$NODE_LIB" done - # install_name_tool invalidates the ad-hoc signature applied by - # darwin.autoSignDarwinBinariesHook; re-sign so the .node loads under - # Gatekeeper on end-user macOS hosts. - codesign --force --sign - "$NODE_LIB" + # install_name_tool invalidates the ad-hoc signature; re-sign with the + # `sign` function from signingUtils (sourced via autoSignDarwinBinariesHook), + # which sets CODESIGN_ALLOCATE — bare sigtool codesign aborts now that + # nixpkgs' darwin stdenv no longer puts cctools on PATH. See #3513. + sign "$NODE_LIB" # Assert no /nix/store references remain — guards against silent # no-ops in the rewrites above and catches the 1.10.0 regression. diff --git a/sdks/android/gradle.properties b/sdks/android/gradle.properties index ff3d0b1ade..7cfa9f7958 100644 --- a/sdks/android/gradle.properties +++ b/sdks/android/gradle.properties @@ -23,4 +23,4 @@ kotlin.code.style=official android.nonTransitiveRClass=true android.nonFinalResIds=false # SDK version - managed by release-tools -version=4.10.0 +version=4.11.0 diff --git a/sdks/ios/XMTP.podspec b/sdks/ios/XMTP.podspec index b49e7ffc02..4f4b8ee88f 100644 --- a/sdks/ios/XMTP.podspec +++ b/sdks/ios/XMTP.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "XMTP" - spec.version = "4.10.0" + spec.version = "4.11.0" spec.summary = "XMTP SDK Cocoapod" diff --git a/sdks/js/browser-sdk/package.json b/sdks/js/browser-sdk/package.json index 874666e34a..cd199d4b43 100644 --- a/sdks/js/browser-sdk/package.json +++ b/sdks/js/browser-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@xmtp/browser-sdk", - "version": "7.0.0", + "version": "7.1.0", "description": "XMTP client SDK for browsers written in TypeScript", "keywords": [ "xmtp", diff --git a/sdks/js/node-sdk/package.json b/sdks/js/node-sdk/package.json index c140b7975e..abe3ad17c1 100644 --- a/sdks/js/node-sdk/package.json +++ b/sdks/js/node-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@xmtp/node-sdk", - "version": "6.0.0", + "version": "6.1.0", "description": "XMTP Node client SDK for interacting with XMTP networks", "keywords": [ "xmtp",