Skip to content
39 changes: 39 additions & 0 deletions migrations/v11-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ onto Stream's OpenAPI-generated API client.
- [User Groups](#user-groups)
- [Link Previews](#link-previews)
- [Partial Updates](#partial-updates)
- [Channel Lifecycle](#channel-lifecycle)
- [Migration Checklist](#migration-checklist)
- [For AI Agents](#for-ai-agents)
- [Contributing to this guide](#contributing-to-this-guide)
Expand Down Expand Up @@ -80,6 +81,7 @@ from the spec, so don't subclass them or depend on their private constructors.
| [**Guest Users**](#guest-users) | `connectGuestUser` is unchanged; `StreamChatApi.guest` and `ConnectGuestUserResponse` are removed |
| [**File Upload**](#file-upload) | Uploads return a `Result<UploadedFile>` and deletes a `Result<void>` instead of throwing; `AttachmentFileUploaderProvider` receives a `Dio` |
| [**Partial Updates**](#partial-updates) | Channel and member partial updates — `updatePartial`, `updateName`, `updateImage`, slow mode, pin and archive — return a `Result` instead of throwing; their responses take the API's names, `UpdateChannelPartialResponse` and `UpdateMemberPartialResponse`, and `partialMemberUpdate` becomes `updateMemberPartial` |
| [**Channel Lifecycle**](#channel-lifecycle) | Hiding, showing and deleting a channel return a `Result` instead of throwing; stopping watching still throws |
| _(filled in per feature as PRs land)_ | |

---
Expand All @@ -104,6 +106,7 @@ search-and-replace you can apply directly. `Kind` is one of `renamed`, `removed`
| `UploadState`'s `Preparing` / `InProgress` / `Success` / `Failed` | `UploadStatePreparing` / `UploadStateInProgress` / `UploadStateSuccess` / `UploadStateFailed` | `renamed` | Frees `Success` for `Result` |
| `PagedValue.error(StreamChatError)` (`stream_chat_flutter_core`) | `PagedValue.error(StreamChatException)` | `retyped` | |
| `StreamChannelListController.muteChannel` / `unmuteChannel` → `Future<void>` (`stream_chat_flutter_core`) | `Future<Result<void>>` | `retyped` | Returns a `Result` instead of throwing, so a `try`/`catch` around either no longer catches a failed call. A subclass overriding one needs the new return type |
| `StreamChannelListController.deleteChannel` → `Future<void>` (`stream_chat_flutter_core`) | `Future<Result<DeleteChannelResponse>>` | `retyped` | Returns a `Result` instead of throwing, so a `try`/`catch` around it no longer catches a failed call. A subclass overriding it needs the new return type |
| `errorBuilder: Function(BuildContext, StreamChatError)` (scroll views) | `Function(BuildContext, StreamChatException)` | `retyped` | |
| `StreamAttachmentValidator.validate()` / `.validateCount()` returning `StreamChatError?` | returning `AttachmentValidationError?` | `retyped` | `stream_chat_flutter`. They always returned rather than threw; the return type now says so |
| `AttachmentLimitReachedError` / `AttachmentTooLargeError` / `AttachmentBlockedError` extending `StreamChatError` | extending `sealed AttachmentValidationError` | `retyped` | A refused attachment is not a failed call, so it is no longer one of the `StreamException` kinds. `switch` over them is exhaustive |
Expand Down Expand Up @@ -237,6 +240,9 @@ search-and-replace you can apply directly. `Kind` is one of `renamed`, `removed`
| `PartialUpdateChannelResponse()..channel = …`, `PartialUpdateMemberResponse()..channelMember = …` and their other setters | `UpdateChannelPartialResponse(duration: …, channel: …)` / `UpdateMemberPartialResponse(duration: …, channelMember: …)` | `retyped` | Plain classes with a const constructor and final fields |
| `PartialUpdateChannelResponse` / `PartialUpdateMemberResponse` identity `==` | value `==`, plus `copyWith` | `retyped` | Two instances with the same fields are now equal |
| `StreamChatApi.channel.updateChannelPartial` / `enableSlowdown` / `disableSlowdown` / `updateMemberPartial` | the `StreamChatClient` methods | `removed` | The endpoints moved to the generated client |
| `StreamChatClient.hideChannel` / `showChannel` / `deleteChannel` → `Future<EmptyResponse>` | `Future<Result<HideChannelResponse>>` / `Future<Result<ShowChannelResponse>>` / `Future<Result<DeleteChannelResponse>>` | `retyped` | Returns a `Result` instead of throwing |
| `Channel.hide` / `show` / `delete` → `Future<EmptyResponse>` | `Future<Result<HideChannelResponse>>` / `Future<Result<ShowChannelResponse>>` / `Future<Result<DeleteChannelResponse>>` | `retyped` | Returns a `Result` instead of throwing. Calling one before the channel is initialized still throws a `StateError` |
| `StreamChatApi.channel.hideChannel` / `showChannel` / `deleteChannel` | the `StreamChatClient` methods | `removed` | The endpoints moved to the generated client |
| `MemberUpdatePayload(pinned: true).toJson()` / `MemberUpdateType.pinned.name` | `{'pinned': true}` / `'pinned'` | `removed` | Same for `archived`. Or call `pinChannel` / `unpinChannel` / `archiveChannel` / `unarchiveChannel` |
| _(more added per feature as PRs land)_ | | | |

Expand Down Expand Up @@ -693,6 +699,39 @@ directly — `updateMemberPartial(channelId: …, channelType: …, set: {'pinne
> field the API adds later reaches you without another break. `MemberUpdatePayload` and `MemberUpdateType` named
> only two of the keys a membership accepts, so a plain map replaces them.

### Channel Lifecycle

**Hiding, showing and deleting a channel return a `Result` instead of throwing.** That covers
`StreamChatClient.hideChannel`, `showChannel` and `deleteChannel`, `Channel.hide`, `show` and `delete`, and
`StreamChannelListController.deleteChannel` in `stream_chat_flutter_core`. A `try`/`catch` around one still
compiles, but no longer catches a failed call: read the returned `Result` instead.

```dart
// v10
try {
await channel.delete();
Navigator.of(context).pop();
} on StreamChatException catch (e) {
report(e);
}

// v11
final result = await channel.delete();
result.fold(
onSuccess: (_) => Navigator.of(context).pop(),
onFailure: (error, _) => report(error),
);
```

**Each call answers its own response** where v10 answered `EmptyResponse`: `HideChannelResponse`,
`ShowChannelResponse` and `DeleteChannelResponse`, which carries the deleted channel in a nullable `channel`.

**`StreamChatApi.channel.hideChannel`, `showChannel` and `deleteChannel` are removed.** Call them on
`StreamChatClient`.

> **Why:** the endpoints moved onto the generated client, which returns a `Result` for every call. Each write
> answers its own envelope so a field the API adds later reaches you without another break.

### Moderation

**Muting, banning and flagging return a `Result` instead of throwing**, on both `StreamChatClient`
Expand Down
10 changes: 2 additions & 8 deletions openapi-migration/11-channels-and-members.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 11 — Channels, Members & Sync

**Goal:** The biggest group, and the one every controller above it reads through `ChannelState`. [15](15-partial-updates.md) split the partial channel and member updates out of it.
**Goal:** The biggest group, and the one every controller above it reads through `ChannelState`. [15](15-partial-updates.md) split the partial channel and member updates out of it, and [16](16-channel-lifecycle.md) hiding, showing and deleting a channel.

**Size:** 23 hand-written method(s) across 2 file(s) → 22 generated operation(s).
**Size:** 20 hand-written method(s) across 2 file(s) → 19 generated operation(s).

> This whole file is generated by `openapi-migration/tool/generate_plan.py`, prose included.
> Edit its `GROUPS` entry and re-run the script — edits made here are lost on the next run.
Expand All @@ -23,10 +23,7 @@
| `channel_api.dart` | `addMembers` | `AddMembersResponse` |
| `channel_api.dart` | `removeMembers` | `RemoveMembersResponse` |
| `channel_api.dart` | `sendEvent` | `EmptyResponse` |
| `channel_api.dart` | `deleteChannel` | `EmptyResponse` |
| `channel_api.dart` | `truncateChannel` | `EmptyResponse` |
| `channel_api.dart` | `hideChannel` | `EmptyResponse` |
| `channel_api.dart` | `showChannel` | `EmptyResponse` |
| `channel_api.dart` | `markRead` | `EmptyResponse` |
| `channel_api.dart` | `markUnread` | `EmptyResponse` |
| `channel_api.dart` | `markUnreadByTimestamp` | `EmptyResponse` |
Expand All @@ -41,15 +38,13 @@

| Verb | Path | Operation | Response |
| --- | --- | --- | --- |
| `DELETE` | `/api/v2/chat/channels/{type}/{id}` | `deleteChannel` | `DeleteChannelResponse` |
| `POST` | `/api/v2/chat/channels/delete` | `deleteChannels` | `DeleteChannelsResponse` |
| `GET` | `/api/v2/chat/channels/{type}/{id}` | `getChannel` | `ChannelStateResponse` |
| `GET` | `/api/v2/chat/channels/{type}/{id}/messages` | `getManyMessages` | `GetManyMessagesResponse` |
| `POST` | `/api/v2/chat/channels/{type}/{id}/query` | `getOrCreateChannel` | `ChannelStateResponse` |
| `POST` | `/api/v2/chat/channels/{type}/query` | `getOrCreateDistinctChannel` | `ChannelStateResponse` |
| `GET` | `/api/v2/chat/channels/{type}/{id}/pinned_messages` | `getPinnedMessages` | `GetPinnedMessagesResponse` |
| `POST` | `/api/v2/chat/channels/grouped` | `groupedQueryChannels` | `GroupedQueryChannelsResponse` |
| `POST` | `/api/v2/chat/channels/{type}/{id}/hide` | `hideChannel` | `HideChannelResponse` |
| `POST` | `/api/v2/chat/channels/read` | `markChannelsRead` | `MarkReadResponse` |
| `POST` | `/api/v2/chat/channels/delivered` | `markDelivered` | `MarkDeliveredResponse` |
| `POST` | `/api/v2/chat/channels/{type}/{id}/read` | `markRead` | `MarkReadResponse` |
Expand All @@ -58,7 +53,6 @@
| `GET` | `/api/v2/chat/members` | `queryMembers` | `MembersResponse` |
| `POST` | `/api/v2/chat/channels/{type}/{id}/event` | `sendEvent` | `EventResponse` |
| `POST` | `/api/v2/chat/channels/{type}/{id}/message` | `sendMessage` | `SendMessageResponse` |
| `POST` | `/api/v2/chat/channels/{type}/{id}/show` | `showChannel` | `ShowChannelResponse` |
| `POST` | `/api/v2/chat/channels/{type}/{id}/stop-watching` | `stopWatchingChannel` | `DurationResponse` |
| `POST` | `/api/v2/chat/sync` | `sync` | `SyncResponse` |
| `POST` | `/api/v2/chat/channels/{type}/{id}/truncate` | `truncateChannel` | `TruncateChannelResponse` |
Expand Down
64 changes: 64 additions & 0 deletions openapi-migration/16-channel-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# 16 — Channel Lifecycle

**Goal:** Move hiding, showing and deleting a channel ahead of [11](11-channels-and-members.md): they answer only a `duration` or a channel [15](15-partial-updates.md) already maps, so they need none of the message mappers.

**Size:** 0 hand-written method(s) across 0 file(s) → 3 generated operation(s).

> This whole file is generated by `openapi-migration/tool/generate_plan.py`, prose included.
> Edit its `GROUPS` entry and re-run the script — edits made here are lost on the next run.

## Scope

### Hand-written today

| File | Method | Returns |
| --- | --- | --- |

### Generated operations that cover it

| Verb | Path | Operation | Response |
| --- | --- | --- | --- |
| `DELETE` | `/api/v2/chat/channels/{type}/{id}` | `deleteChannel` | `DeleteChannelResponse` |
| `POST` | `/api/v2/chat/channels/{type}/{id}/hide` | `hideChannel` | `HideChannelResponse` |
| `POST` | `/api/v2/chat/channels/{type}/{id}/show` | `showChannel` | `ShowChannelResponse` |

## Decisions taken

- **Split out of [11](11-channels-and-members.md), ahead of it.** None of the three reads anything
into client state — the `channel.hidden`, `channel.visible` and `channel.deleted` events do that —
and the one channel they answer goes through `channel_mapper.dart`.
- **The v2 routes are the v1 handlers.** `lib/chat/routes.go` mounts hide, show and delete in the
shared `coreRoutes`; none is gated, in beta or deprecated.
- **Moved off `ChannelApi`:** `hideChannel`, `showChannel` and `deleteChannel`, each now a
`StreamChatClient` method over `ChannelsRepository`.
- **Each write answers its own envelope,** where v10 answered `EmptyResponse`: `HideChannelResponse`,
`ShowChannelResponse` and `DeleteChannelResponse`, freezed, in `models/response/`, with the spec's
names. `DeleteChannelResponse.channel` is nullable, as the spec declares it. `Channel.hide`, `show`
and `delete` keep their names.
- **`StreamChannelListController.deleteChannel` returns the `Result`,** as `muteChannel` does.
- **`hard_delete` is not exposed.** The server refuses it from a client-side token, and v10 never sent it.
- **Show sends no body,** where v1 sent `{}`; the server accepts both. Hide still sends `clear_history`.
- **The read and delivery receipts stay in 11 for now.** `markRead` and its siblings share endpoints
with the thread read calls, fabricate a response on the local-unread path, and feed
`MessageListUnreadController` and the public `ChannelDeliveryReporter`, which rely on a throw; they
move in a slice of their own.

## Risks


## Definition of done

- [x] Every method above either routes through `DefaultApi` or is listed here as deliberately left
hand-written, with the reason.
- [x] Public methods return `Future<Result<T>>`; no `getOrThrow()` inside the SDK.
- [x] Hand-written request/response DTOs for this group are deleted, or their retention is justified.
- [x] `melos run analyze` clean, `melos run test:dart` green, persistence tests green if this group
persists anything.
- [x] `migrations/v11-migration.md`: Symbol Map rows plus a feature section for every break.
- [x] CHANGELOG entry under `🛑️ Breaking` for each break; PR title `refactor(llc)!:`.
- [x] Decisions recorded in this file, and the status box ticked in `README.md`.
- [x] Public dartdoc follows [`STYLE_GUIDE.md` § Documentation](../STYLE_GUIDE.md#documentation)
and [`EFFECTIVE_DART_DOC.md`](../EFFECTIVE_DART_DOC.md), including on symbols this group
retyped but whose docs it left alone.
- [x] Tests follow [`TESTING.md`](../TESTING.md): no `group` organizing a file by method, each
name states its subject and behaviour.
7 changes: 5 additions & 2 deletions openapi-migration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ generated operations in scope, the decisions that group has to make, its risks,
| [08](08-moderation-and-blocklists.md) | Moderation & Blocklists | 0 | 34 | ☑ |
| [09](09-users.md) | Users | 9 | 9 | ☐ |
| [10](10-messages.md) | Messages & Search | 14 | 12 | ☐ |
| [11](11-channels-and-members.md) | Channels, Members & Sync | 23 | 22 | ☐ |
| [11](11-channels-and-members.md) | Channels, Members & Sync | 20 | 19 | ☐ |
| [12](12-uploads-cdn.md) | Uploads (CDN) | 8 | 8 | ☑ |
| [13](13-push-preferences.md) | Push Preferences | 1 | 1 | ☐ |
| [14](14-banned-users.md) | Banned Users — split out of 08 | 1 | 1 | ☐ |
| [15](15-partial-updates.md) | Partial Updates — split out of 11 | 0 | 2 | ☑ |
| [16](16-channel-lifecycle.md) | Channel Lifecycle — split out of 11 | 0 | 3 | ☑ |

**Coverage:** 80 hand-written methods across 10 files, and all 129 generated operations, each claimed by exactly
**Coverage:** 77 hand-written methods across 10 files, and all 129 generated operations, each claimed by exactly
one group. Verified mechanically — see [Keeping this plan honest](#keeping-this-plan-honest).


Expand Down Expand Up @@ -170,6 +171,8 @@ surfaces before it reaches `Message` and `ChannelState`:
- **10–11** are the core of the SDK, and carry the `custom` / `extraData` promotion problem.
- **15** is the partial channel and member updates, split out of 11 so `channel_mapper.dart` lands on calls that
read nothing into `ChannelState`. It landed first; the full update waits in 11 for group 10's message mappers.
- **16** is hiding, showing and deleting a channel, split out of 11 after 15 because they answer nothing
`channel_mapper.dart` cannot map; the read and delivery receipts stay in 11 for a slice of their own.
- **12** comes late because it needs its own hand-written multipart client and is the highest-traffic path in the
SDK.
- **09** is last. Every group before it maps users through `user_mapper.dart` onto today's `User`; 09 migrates the
Expand Down
41 changes: 39 additions & 2 deletions openapi-migration/tool/generate_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,9 +587,13 @@ def match(verb, path):
'/api/v2/chat/channels/{type}/{id}/file',
'/api/v2/chat/channels/{type}/{id}/image'),
unless_ops=('PATCH /api/v2/chat/channels/{type}/{id}',
'PATCH /api/v2/chat/channels/{type}/{id}/member')),
'PATCH /api/v2/chat/channels/{type}/{id}/member',
'POST /api/v2/chat/channels/{type}/{id}/hide',
'POST /api/v2/chat/channels/{type}/{id}/show',
'DELETE /api/v2/chat/channels/{type}/{id}')),
goal='The biggest group, and the one every controller above it reads through `ChannelState`. '
'[15](15-partial-updates.md) split the partial channel and member updates out of it.',
'[15](15-partial-updates.md) split the partial channel and member updates out of it, and '
'[16](16-channel-lifecycle.md) hiding, showing and deleting a channel.',
decisions=[
'`ChannelState`, `ChannelModel` and `Member` are public, persisted, and rebuilt from WebSocket '
'events. Keep ours and map.',
Expand Down Expand Up @@ -832,6 +836,39 @@ def match(verb, path):
],
done=DONE.replace('- [ ]', '- [x]'),
),
dict(
num='16', slug='channel-lifecycle', title='Channel Lifecycle',
hand=[],
match=only_ops('POST /api/v2/chat/channels/{type}/{id}/hide',
'POST /api/v2/chat/channels/{type}/{id}/show',
'DELETE /api/v2/chat/channels/{type}/{id}'),
goal='Move hiding, showing and deleting a channel ahead of '
'[11](11-channels-and-members.md): they answer only a `duration` or a channel '
'[15](15-partial-updates.md) already maps, so they need none of the message mappers.',
decisions=[],
taken=textwrap.dedent("""\
- **Split out of [11](11-channels-and-members.md), ahead of it.** None of the three reads anything
into client state — the `channel.hidden`, `channel.visible` and `channel.deleted` events do that —
and the one channel they answer goes through `channel_mapper.dart`.
- **The v2 routes are the v1 handlers.** `lib/chat/routes.go` mounts hide, show and delete in the
shared `coreRoutes`; none is gated, in beta or deprecated.
- **Moved off `ChannelApi`:** `hideChannel`, `showChannel` and `deleteChannel`, each now a
`StreamChatClient` method over `ChannelsRepository`.
- **Each write answers its own envelope,** where v10 answered `EmptyResponse`: `HideChannelResponse`,
`ShowChannelResponse` and `DeleteChannelResponse`, freezed, in `models/response/`, with the spec's
names. `DeleteChannelResponse.channel` is nullable, as the spec declares it. `Channel.hide`, `show`
and `delete` keep their names.
- **`StreamChannelListController.deleteChannel` returns the `Result`,** as `muteChannel` does.
- **`hard_delete` is not exposed.** The server refuses it from a client-side token, and v10 never sent it.
- **Show sends no body,** where v1 sent `{}`; the server accepts both. Hide still sends `clear_history`.
- **The read and delivery receipts stay in 11 for now.** `markRead` and its siblings share endpoints
with the thread read calls, fabricate a response on the local-unread path, and feed
`MessageListUnreadController` and the public `ChannelDeliveryReporter`, which rely on a throw; they
move in a slice of their own.
"""),
risks=[],
done=DONE.replace('- [ ]', '- [x]'),
),
]


Expand Down
Loading
Loading