Skip to content
Open
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
38 changes: 38 additions & 0 deletions migrations/v11-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ onto Stream's OpenAPI-generated API client.
- [User Groups](#user-groups)
- [Link Previews](#link-previews)
- [Partial Updates](#partial-updates)
- [Channel Lifecycle](#channel-lifecycle)
- [Channel and Member Custom Data](#channel-and-member-custom-data)
- [Migration Checklist](#migration-checklist)
- [For AI Agents](#for-ai-agents)
Expand Down Expand Up @@ -83,6 +84,7 @@ from the spec, so don't subclass them or depend on their private constructors.
| [**Polls**](#polls) | Poll calls return a `Result`; eight responses become `PollResponse`, `PollOptionResponse` and `PollVoteResponse`; queries take `limit`/`next`/`prev`; `VotingVisibility` is an extension type; `extraData` is `custom` |
| [**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 |
| [**Channel and Member Custom Data**](#channel-and-member-custom-data) | `extraData` is `custom` on `ChannelModel`, `Member` and `Channel`, and holds the custom data verbatim; the channel and member flags it carried are fields |
| _(filled in per feature as PRs land)_ | |

Expand All @@ -108,6 +110,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 @@ -261,6 +264,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` |
| `ChannelModel.extraData` / `Member.extraData` | `ChannelModel.custom` / `Member.custom` | `renamed` | The wire's name for it; also the constructor and `copyWith` arguments |
| `Channel.extraData` / `Channel.extraDataStream` / `Channel(…, extraData:)` / `StreamChatClient.channel(…, extraData:)` | `Channel.custom` / `Channel.customStream` / `custom:` | `renamed` | Follows `ChannelModel.custom`; the setter is renamed too |
Expand Down Expand Up @@ -723,6 +729,38 @@ 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.
### Channel and Member Custom Data

**`extraData` is renamed `custom` on `ChannelModel`, `Member` and `Channel`.** That covers the fields, the
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
66 changes: 66 additions & 0 deletions openapi-migration/16-channel-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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 `channels_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] `copywith_audit.py` was run for every model that loses a v10 `copyWith`, every site it reports is
handled, and the semantics change is in the CHANGELOG and the guide ([README rule 2](README.md#domain-models)).
- [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,14 +22,15 @@ 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 | ☑ |
| [21](21-custom-data.md) | Custom Data Check — no model still says `extraData` | — | — | ☐ |

**Coverage:** 67 hand-written methods across 9 files, and all 129 generated operations, each claimed by exactly
**Coverage:** 64 hand-written methods across 9 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 @@ -196,6 +197,8 @@ surfaces before it reaches `Message` and `ChannelState`:
read nothing into `ChannelState`. It landed first; the full update waits in 11 for group 10's message mappers.
As the first group to map `ChannelModel` and `Member`, it promoted their parked fields and renamed their custom
data `custom` (rule 9).
- **16** is hiding, showing and deleting a channel, split out of 11 after 15 because they answer nothing
`channels_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
Loading
Loading