stream-chat 9.50.3 → 10.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/dist/cjs/index.browser.js +19798 -14472
  2. package/dist/cjs/index.browser.js.map +4 -4
  3. package/dist/cjs/index.node.js +19817 -14482
  4. package/dist/cjs/index.node.js.map +4 -4
  5. package/dist/esm/index.mjs +19795 -14469
  6. package/dist/esm/index.mjs.map +4 -4
  7. package/dist/types/ChannelManager.d.ts +221 -0
  8. package/dist/types/EventHandlerPipeline.d.ts +104 -0
  9. package/dist/types/api-client.d.ts +26 -0
  10. package/dist/types/campaign.d.ts +0 -44
  11. package/dist/types/channel.d.ts +454 -479
  12. package/dist/types/channel_batch_updater.d.ts +0 -93
  13. package/dist/types/channel_state.d.ts +53 -201
  14. package/dist/types/client.d.ts +451 -1920
  15. package/dist/types/client_state.d.ts +4 -4
  16. package/dist/types/configuration/InstanceConfigurationService.d.ts +26 -0
  17. package/dist/types/configuration/index.d.ts +1 -0
  18. package/dist/types/configuration/types.d.ts +53 -0
  19. package/dist/types/connection.d.ts +49 -41
  20. package/dist/types/connection_fallback.d.ts +6 -4
  21. package/dist/types/constants.d.ts +0 -4
  22. package/dist/types/custom_types.d.ts +2 -0
  23. package/dist/types/entityStore/EntityStore.d.ts +111 -0
  24. package/dist/types/entityStore/StoreBackedItemIndex.d.ts +60 -0
  25. package/dist/types/entityStore/applyReactionLocally.d.ts +26 -0
  26. package/dist/types/entityStore/index.d.ts +2 -0
  27. package/dist/types/errors.d.ts +10 -2
  28. package/dist/types/gen/chat/ChannelApi.d.ts +45 -0
  29. package/dist/types/gen/chat/ChatApi.d.ts +334 -0
  30. package/dist/types/gen/model-decoders/decoders.d.ts +3 -0
  31. package/dist/types/gen/model-decoders/event-decoder-mapping.d.ts +6 -0
  32. package/dist/types/gen/models/index.d.ts +8836 -0
  33. package/dist/types/gen/moderation/ModerationApi.d.ts +50 -0
  34. package/dist/types/gen-imports.d.ts +3 -0
  35. package/dist/types/index.d.ts +8 -7
  36. package/dist/types/insights.d.ts +9 -8
  37. package/dist/types/logger.d.ts +9 -0
  38. package/dist/types/messageComposer/LocationComposer.d.ts +7 -4
  39. package/dist/types/messageComposer/attachmentIdentity.d.ts +2 -2
  40. package/dist/types/messageComposer/attachmentManager.d.ts +8 -4
  41. package/dist/types/messageComposer/configuration/types.d.ts +11 -11
  42. package/dist/types/messageComposer/linkPreviewsManager.d.ts +25 -13
  43. package/dist/types/messageComposer/messageComposer.d.ts +10 -10
  44. package/dist/types/messageComposer/middleware/messageComposer/types.d.ts +3 -3
  45. package/dist/types/messageComposer/middleware/pollComposer/types.d.ts +4 -7
  46. package/dist/types/messageComposer/middleware/textComposer/commandUtils.d.ts +3 -3
  47. package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +7 -7
  48. package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +218 -4
  49. package/dist/types/messageComposer/middleware/textComposer/types.d.ts +9 -4
  50. package/dist/types/messageComposer/pollComposer.d.ts +5 -4
  51. package/dist/types/messageComposer/textComposer.d.ts +8 -3
  52. package/dist/types/messageComposer/types.d.ts +6 -28
  53. package/dist/types/messageDelivery/MessageDeliveryReporter.d.ts +40 -25
  54. package/dist/types/messageDelivery/MessageReceiptsTracker.d.ts +53 -12
  55. package/dist/types/messageOperations/MessageOperationStatePolicy.d.ts +19 -0
  56. package/dist/types/messageOperations/MessageOperations.d.ts +17 -0
  57. package/dist/types/messageOperations/index.d.ts +3 -0
  58. package/dist/types/messageOperations/types.d.ts +49 -0
  59. package/dist/types/moderation.d.ts +21 -203
  60. package/dist/types/notifications/types.d.ts +8 -7
  61. package/dist/types/offline-support/offline_support_api.d.ts +111 -87
  62. package/dist/types/offline-support/offline_sync_manager.d.ts +0 -7
  63. package/dist/types/offline-support/types.d.ts +19 -23
  64. package/dist/types/offline-support/util.d.ts +4 -2
  65. package/dist/types/pagination/FilterBuilder.d.ts +1 -1
  66. package/dist/types/pagination/ItemIndex.d.ts +25 -0
  67. package/dist/types/pagination/cursorDerivation/createdAtAroundPaginationFlags.d.ts +9 -0
  68. package/dist/types/pagination/cursorDerivation/idAroundPaginationFlags.d.ts +6 -0
  69. package/dist/types/pagination/cursorDerivation/index.d.ts +1 -0
  70. package/dist/types/pagination/cursorDerivation/linearPaginationFlags.d.ts +6 -0
  71. package/dist/types/pagination/filterCompiler.d.ts +7 -0
  72. package/dist/types/pagination/index.d.ts +1 -3
  73. package/dist/types/pagination/paginators/BasePaginator.d.ts +848 -0
  74. package/dist/types/pagination/paginators/ChannelPaginator.d.ts +212 -0
  75. package/dist/types/pagination/paginators/MessageIntervalPaginator.d.ts +282 -0
  76. package/dist/types/pagination/paginators/MessagePaginator.d.ts +208 -0
  77. package/dist/types/pagination/paginators/PinnedMessagePaginator.d.ts +39 -0
  78. package/dist/types/pagination/paginators/ReminderPaginator.d.ts +19 -0
  79. package/dist/types/pagination/paginators/UserGroupPaginator.d.ts +24 -0
  80. package/dist/types/pagination/paginators/index.d.ts +7 -0
  81. package/dist/types/pagination/paginators/stateThrottling.d.ts +2 -0
  82. package/dist/types/pagination/sortCompiler.d.ts +49 -0
  83. package/dist/types/pagination/types.normalization.d.ts +11 -0
  84. package/dist/types/pagination/utility.normalization.d.ts +18 -0
  85. package/dist/types/pagination/utility.queryChannel.d.ts +26 -0
  86. package/dist/types/pagination/utility.search.d.ts +13 -0
  87. package/dist/types/permissions.d.ts +1 -1
  88. package/dist/types/poll.d.ts +29 -32
  89. package/dist/types/poll_manager.d.ts +2 -2
  90. package/dist/types/reminders/Reminder.d.ts +2 -5
  91. package/dist/types/reminders/ReminderManager.d.ts +2 -9
  92. package/dist/types/search/MessageSearchSource.d.ts +4 -4
  93. package/dist/types/search/UserSearchSource.d.ts +1 -1
  94. package/dist/types/search/types.d.ts +3 -3
  95. package/dist/types/segment.d.ts +0 -34
  96. package/dist/types/signing.d.ts +105 -65
  97. package/dist/types/store.d.ts +1 -0
  98. package/dist/types/thread.d.ts +70 -34
  99. package/dist/types/thread_manager.d.ts +3 -3
  100. package/dist/types/token_manager.d.ts +15 -10
  101. package/dist/types/types.d.ts +197 -3404
  102. package/dist/types/utils/FixedSizeQueueCache.d.ts +12 -6
  103. package/dist/types/utils/WithSubscriptions.d.ts +2 -1
  104. package/dist/types/utils/concurrency.d.ts +4 -4
  105. package/dist/types/utils/mergeWith/mergeWith.d.ts +3 -3
  106. package/dist/types/utils/mergeWith/mergeWithCore.d.ts +8 -3
  107. package/dist/types/utils/retryable.d.ts +34 -0
  108. package/dist/types/utils/throttling/throttle.d.ts +36 -0
  109. package/dist/types/utils.d.ts +100 -194
  110. package/package.json +7 -3
  111. package/src/ChannelManager.ts +724 -0
  112. package/src/CooldownTimer.ts +2 -2
  113. package/src/EventHandlerPipeline.ts +228 -0
  114. package/src/LiveLocationManager.ts +12 -11
  115. package/src/api-client.ts +275 -0
  116. package/src/campaign.ts +1 -77
  117. package/src/channel.ts +1344 -1282
  118. package/src/channel_batch_updater.ts +1 -211
  119. package/src/channel_state.ts +144 -1013
  120. package/src/client.ts +936 -3995
  121. package/src/client_state.ts +4 -4
  122. package/src/configuration/InstanceConfigurationService.ts +73 -0
  123. package/src/configuration/index.ts +1 -0
  124. package/src/configuration/types.ts +81 -0
  125. package/src/connection.ts +145 -94
  126. package/src/connection_fallback.ts +29 -27
  127. package/src/constants.ts +1 -5
  128. package/src/custom_types.ts +4 -1
  129. package/src/entityStore/EntityStore.ts +208 -0
  130. package/src/entityStore/StoreBackedItemIndex.ts +115 -0
  131. package/src/entityStore/applyReactionLocally.ts +113 -0
  132. package/src/entityStore/index.ts +2 -0
  133. package/src/errors.ts +14 -2
  134. package/src/gen/chat/ChannelApi.ts +275 -0
  135. package/src/gen/chat/ChatApi.ts +2554 -0
  136. package/src/gen/model-decoders/decoders.ts +2680 -0
  137. package/src/gen/model-decoders/event-decoder-mapping.ts +198 -0
  138. package/src/gen/models/index.ts +12673 -0
  139. package/src/gen/moderation/ModerationApi.ts +607 -0
  140. package/src/gen-imports.ts +3 -0
  141. package/src/index.ts +11 -12
  142. package/src/insights.ts +6 -5
  143. package/src/logger.ts +28 -0
  144. package/src/messageComposer/LocationComposer.ts +9 -5
  145. package/src/messageComposer/MessageComposerEffectHandlers.ts +1 -0
  146. package/src/messageComposer/attachmentIdentity.ts +16 -9
  147. package/src/messageComposer/attachmentManager.ts +13 -8
  148. package/src/messageComposer/configuration/types.ts +11 -11
  149. package/src/messageComposer/linkPreviewsManager.ts +9 -10
  150. package/src/messageComposer/messageComposer.ts +57 -53
  151. package/src/messageComposer/middleware/messageComposer/attachments.ts +1 -2
  152. package/src/messageComposer/middleware/messageComposer/cleanData.ts +1 -1
  153. package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +2 -2
  154. package/src/messageComposer/middleware/messageComposer/messageComposerState.ts +2 -2
  155. package/src/messageComposer/middleware/messageComposer/sharedLocation.ts +4 -3
  156. package/src/messageComposer/middleware/messageComposer/textComposer.ts +2 -2
  157. package/src/messageComposer/middleware/messageComposer/types.ts +3 -4
  158. package/src/messageComposer/middleware/messageComposer/userDataInjection.ts +9 -5
  159. package/src/messageComposer/middleware/pollComposer/state.ts +0 -2
  160. package/src/messageComposer/middleware/pollComposer/types.ts +4 -7
  161. package/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts +10 -1
  162. package/src/messageComposer/middleware/textComposer/commandEffects.ts +4 -4
  163. package/src/messageComposer/middleware/textComposer/commandUtils.ts +3 -6
  164. package/src/messageComposer/middleware/textComposer/commands.ts +3 -3
  165. package/src/messageComposer/middleware/textComposer/mentionUtils.ts +1 -1
  166. package/src/messageComposer/middleware/textComposer/mentions.ts +31 -15
  167. package/src/messageComposer/middleware/textComposer/types.ts +9 -4
  168. package/src/messageComposer/pollComposer.ts +6 -8
  169. package/src/messageComposer/textComposer.ts +27 -2
  170. package/src/messageComposer/types.ts +6 -28
  171. package/src/messageDelivery/MessageDeliveryReporter.ts +101 -45
  172. package/src/messageDelivery/MessageReceiptsTracker.ts +294 -21
  173. package/src/messageOperations/MessageOperationStatePolicy.ts +77 -0
  174. package/src/messageOperations/MessageOperations.ts +212 -0
  175. package/src/messageOperations/index.ts +10 -0
  176. package/src/messageOperations/types.ts +64 -0
  177. package/src/moderation.ts +38 -431
  178. package/src/notifications/types.ts +8 -7
  179. package/src/offline-support/offline_support_api.ts +200 -133
  180. package/src/offline-support/offline_sync_manager.ts +23 -44
  181. package/src/offline-support/types.ts +23 -26
  182. package/src/offline-support/util.ts +5 -4
  183. package/src/pagination/FilterBuilder.ts +4 -1
  184. package/src/pagination/ItemIndex.ts +25 -0
  185. package/src/pagination/cursorDerivation/createdAtAroundPaginationFlags.ts +73 -0
  186. package/src/pagination/cursorDerivation/idAroundPaginationFlags.ts +53 -0
  187. package/src/pagination/cursorDerivation/index.ts +1 -0
  188. package/src/pagination/cursorDerivation/linearPaginationFlags.ts +86 -0
  189. package/src/pagination/filterCompiler.ts +196 -0
  190. package/src/pagination/index.ts +1 -3
  191. package/src/pagination/paginators/BasePaginator.ts +2859 -0
  192. package/src/pagination/paginators/ChannelPaginator.ts +729 -0
  193. package/src/pagination/paginators/MessageIntervalPaginator.ts +1116 -0
  194. package/src/pagination/paginators/MessagePaginator.ts +508 -0
  195. package/src/pagination/paginators/PinnedMessagePaginator.ts +108 -0
  196. package/src/pagination/paginators/ReminderPaginator.ts +107 -0
  197. package/src/pagination/paginators/UserGroupPaginator.ts +127 -0
  198. package/src/pagination/paginators/index.ts +7 -0
  199. package/src/pagination/paginators/stateThrottling.ts +31 -0
  200. package/src/pagination/sortCompiler.ts +193 -0
  201. package/src/pagination/types.normalization.ts +14 -0
  202. package/src/pagination/utility.normalization.ts +106 -0
  203. package/src/pagination/utility.queryChannel.ts +84 -0
  204. package/src/pagination/utility.search.ts +80 -0
  205. package/src/permissions.ts +7 -8
  206. package/src/poll.ts +101 -110
  207. package/src/poll_manager.ts +14 -8
  208. package/src/reminders/Reminder.ts +2 -5
  209. package/src/reminders/ReminderManager.ts +19 -21
  210. package/src/search/BaseSearchSource.ts +0 -3
  211. package/src/search/ChannelMemberSearchSource.ts +7 -1
  212. package/src/search/ChannelSearchSource.ts +10 -3
  213. package/src/search/MessageSearchSource.ts +29 -30
  214. package/src/search/UserSearchSource.ts +12 -8
  215. package/src/search/types.ts +3 -4
  216. package/src/segment.ts +1 -95
  217. package/src/signing.ts +107 -67
  218. package/src/store.ts +2 -1
  219. package/src/thread.ts +562 -238
  220. package/src/thread_manager.ts +37 -16
  221. package/src/token_manager.ts +21 -10
  222. package/src/types.ts +364 -4533
  223. package/src/uploadManager.ts +8 -0
  224. package/src/utils/FixedSizeQueueCache.ts +12 -6
  225. package/src/utils/WithSubscriptions.ts +2 -1
  226. package/src/utils/concurrency.ts +4 -4
  227. package/src/utils/mergeWith/mergeWith.ts +3 -3
  228. package/src/utils/mergeWith/mergeWithCore.ts +140 -164
  229. package/src/utils/mergeWith/mergeWithDiff.ts +3 -3
  230. package/src/utils/retryable.ts +117 -0
  231. package/src/utils/throttling/throttle.ts +130 -0
  232. package/src/utils.ts +208 -754
  233. package/dist/types/channel_manager.d.ts +0 -122
  234. package/dist/types/events.d.ts +0 -72
  235. package/dist/types/pagination/BasePaginator.d.ts +0 -69
  236. package/dist/types/pagination/ReminderPaginator.d.ts +0 -16
  237. package/dist/types/pagination/UserGroupPaginator.d.ts +0 -21
  238. package/src/channel_manager.ts +0 -830
  239. package/src/events.ts +0 -75
  240. package/src/pagination/BasePaginator.ts +0 -184
  241. package/src/pagination/ReminderPaginator.ts +0 -56
  242. package/src/pagination/UserGroupPaginator.ts +0 -93
@@ -0,0 +1,724 @@
1
+ import { EventHandlerPipeline } from './EventHandlerPipeline';
2
+ import { WithSubscriptions } from './utils/WithSubscriptions';
3
+ import type { EventType } from './types';
4
+ import type { ChannelPaginator } from './pagination';
5
+ import type { StreamChat } from './client';
6
+ import type { Unsubscribe } from './store';
7
+ import { StateStore } from './store';
8
+ import type {
9
+ EventHandlerPipelineHandler,
10
+ FindEventHandlerParams,
11
+ InsertEventHandlerPayload,
12
+ LabeledEventHandler,
13
+ PipelineEvent,
14
+ } from './EventHandlerPipeline';
15
+ import { getChannel } from './pagination/utility.queryChannel';
16
+ import type { Channel } from './channel';
17
+
18
+ export type ChannelManagerEventHandlerContext = {
19
+ channelManager: ChannelManager;
20
+ };
21
+
22
+ type EventHandlerContext = ChannelManagerEventHandlerContext;
23
+
24
+ type SupportedEventType = EventType | (string & {});
25
+
26
+ /**
27
+ * Resolves which paginators should be the "owners" of a channel
28
+ * when the channel matches multiple paginator filters.
29
+ *
30
+ * Return a set of paginator ids that should keep/own the item.
31
+ * Returning an empty set means the channel will be removed everywhere.
32
+ */
33
+ export type PaginatorOwnershipResolver = (args: {
34
+ channel: Channel;
35
+ matchingPaginators: ChannelPaginator[];
36
+ }) => string[];
37
+
38
+ /**
39
+ * Convenience factory for a priority-based ownership resolver.
40
+ * - Provide an ordered list of paginator ids from highest to lowest priority.
41
+ * - If two or more paginators match a channel, the one with the highest priority wins.
42
+ * - If none of the matching paginator ids are in the priority list, all matches are kept (back-compat).
43
+ */
44
+ export const createPriorityOwnershipResolver = (
45
+ priority?: string[],
46
+ ): PaginatorOwnershipResolver => {
47
+ if (!priority) {
48
+ return ({ matchingPaginators }) => matchingPaginators.map((p) => p.id);
49
+ }
50
+ const rank = new Map<string, number>(priority.map((id, index) => [id, index]));
51
+ return ({ matchingPaginators }) => {
52
+ if (matchingPaginators.length <= 1) {
53
+ return matchingPaginators.map((p) => p.id);
54
+ }
55
+ // The winner is the first item in the sorted array of matching paginators
56
+ const winner = [...matchingPaginators].sort((a, b) => {
57
+ const rankA = rank.get(a.id);
58
+ const rankB = rank.get(b.id);
59
+ const valueA = rankA === undefined ? Number.POSITIVE_INFINITY : rankA;
60
+ const valueB = rankB === undefined ? Number.POSITIVE_INFINITY : rankB;
61
+ return valueA - valueB;
62
+ })[0];
63
+ const winnerValue = rank.get(winner.id);
64
+ // If no explicit priority is set for any, keep all (preserve current behavior)
65
+ if (winnerValue === undefined) {
66
+ return matchingPaginators.map((p) => p.id);
67
+ }
68
+ return [winner.id];
69
+ };
70
+ };
71
+
72
+ /**
73
+ * The cid the event refers to. Events are inconsistent about how they identify their channel: some carry
74
+ * a top-level `cid`, some only `channel_type` + `channel_id`, and some (e.g.
75
+ * `notification.added_to_channel`) have all three optional and identify the channel solely through the
76
+ * required `event.channel`.
77
+ */
78
+ const getCidFromEvent = (event: PipelineEvent): string | undefined => {
79
+ if (event.cid) return event.cid;
80
+ // todo: is there a central method to construct the cid from type and channel id?
81
+ if (event.channel_id && event.channel_type) {
82
+ return `${event.channel_type}:${event.channel_id}`;
83
+ }
84
+ return event.channel?.cid;
85
+ };
86
+
87
+ const getCachedChannelFromEvent = (
88
+ event: PipelineEvent,
89
+ cache: Record<string, Channel>,
90
+ ): Channel | undefined => {
91
+ const cid = getCidFromEvent(event);
92
+ return cid ? cache[cid] : undefined;
93
+ };
94
+
95
+ const reEmit: EventHandlerPipelineHandler<EventHandlerContext> = ({
96
+ event,
97
+ ctx: { channelManager },
98
+ }) => {
99
+ if (!event.cid) return;
100
+ const channel = channelManager.client.activeChannels[event.cid];
101
+ if (!channel) return;
102
+ channelManager.paginators.forEach((paginator) => {
103
+ const items = paginator.items;
104
+ const { state } = paginator.locateByItem(channel);
105
+ if ((state?.currentIndex ?? -1) > -1 && items) {
106
+ paginator.state.partialNext({ items: [...items] });
107
+ }
108
+ });
109
+ };
110
+
111
+ const removeItem: EventHandlerPipelineHandler<EventHandlerContext> = ({
112
+ event,
113
+ ctx: { channelManager },
114
+ }) => {
115
+ // `getCidFromEvent`, not `event.cid`: on `channel.deleted` the cid can arrive nested in
116
+ // `event.channel` only, and the legacy ChannelManager removed by `event.cid || event.channel?.cid`
117
+ const cid = getCidFromEvent(event);
118
+ if (!cid) return;
119
+ const channel = channelManager.client.activeChannels[cid];
120
+ channelManager.paginators.forEach((paginator) => {
121
+ paginator.removeItem({ id: cid, item: channel });
122
+ });
123
+ };
124
+
125
+ // todo: documentation: show how to implement allowNewMessagesFromUnfilteredChannels just by inserting event handler
126
+ // at the start of the handler pipeline and filter out events for unknown channels
127
+ export const ignoreEventsForUnknownChannels: EventHandlerPipelineHandler<
128
+ EventHandlerContext
129
+ > = ({ event, ctx: { channelManager } }) => {
130
+ const channel: Channel | undefined = getCachedChannelFromEvent(
131
+ event,
132
+ channelManager.client.activeChannels,
133
+ );
134
+ if (!channel) return { action: 'stop' };
135
+ };
136
+
137
+ const updateLists: EventHandlerPipelineHandler<EventHandlerContext> = async ({
138
+ event,
139
+ ctx: { channelManager },
140
+ }) => {
141
+ let channel: Channel | undefined = getCachedChannelFromEvent(
142
+ event,
143
+ channelManager.client.activeChannels,
144
+ );
145
+
146
+ if (!channel) {
147
+ const [type, id] = getCidFromEvent(event)?.split(':') ?? [];
148
+ if (!type) return;
149
+
150
+ channel = await getChannel({
151
+ client: channelManager.client,
152
+ id,
153
+ type,
154
+ });
155
+ }
156
+
157
+ if (!channel) return;
158
+
159
+ const matchingPaginators = channelManager.paginators.filter((p) =>
160
+ p.matchesFilter(channel),
161
+ );
162
+ const matchingIds = new Set(matchingPaginators.map((p) => p.id));
163
+
164
+ const ownerIds = channelManager.resolveOwnership(channel, matchingPaginators);
165
+
166
+ channelManager.paginators.forEach((paginator) => {
167
+ if (!matchingIds.has(paginator.id)) {
168
+ // remove if it does not match the filter anymore
169
+ paginator.removeItem({ item: channel });
170
+ return;
171
+ }
172
+
173
+ // Only if owners are specified, the items is removed from the non-owner matching paginators
174
+ if (ownerIds.size > 0 && !ownerIds.has(paginator.id)) {
175
+ // matched, but not selected to own - remove to enforce exclusivity
176
+ paginator.removeItem({ item: channel });
177
+ return;
178
+ }
179
+
180
+ // Selected owner: ingest. The manager never boosts by default on any event — the sort is the
181
+ // single source of truth for order, so a channel that just became relevant (new message, added,
182
+ // unhidden) relocates via its updated sort key. Boosting remains a public per-paginator primitive
183
+ // (`paginator.boost`) for integrators to opt into for specific channels (VIP/mention/deep-link).
184
+ paginator.ingestItem(channel);
185
+ });
186
+ };
187
+
188
+ // we have to make sure that client.activeChannels is always up-to-date
189
+ const channelDeletedHandler: LabeledEventHandler<EventHandlerContext> = {
190
+ handle: removeItem,
191
+ id: 'ChannelManager:default-handler:channel.deleted',
192
+ };
193
+
194
+ // fixme: this handler should not be handled by the channel manager but as Channel does not have reactive state,
195
+ // we need to re-emit the whole list to reflect the changes
196
+ const channelUpdatedHandler: LabeledEventHandler<EventHandlerContext> = {
197
+ handle: reEmit,
198
+ id: 'ChannelManager:default-handler:channel.updated',
199
+ };
200
+
201
+ // fixme: this handler should not be handled by the channel manager but as Channel does not have reactive state,
202
+ // we need to re-emit the whole list to reflect the changes
203
+ const channelTruncatedHandler: LabeledEventHandler<EventHandlerContext> = {
204
+ handle: reEmit,
205
+ id: 'ChannelManager:default-handler:channel.truncated',
206
+ };
207
+
208
+ const channelVisibleHandler: LabeledEventHandler<EventHandlerContext> = {
209
+ handle: updateLists,
210
+ id: 'ChannelManager:default-handler:channel.visible',
211
+ };
212
+
213
+ const channelHiddenHandler: LabeledEventHandler<EventHandlerContext> = {
214
+ handle: updateLists,
215
+ id: 'ChannelManager:default-handler:channel.hidden',
216
+ };
217
+
218
+ // members filter - should not be impacted as id is stable - cannot be updated
219
+ // member.user.name - can be impacted
220
+ const memberUpdatedHandler: LabeledEventHandler<EventHandlerContext> = {
221
+ handle: updateLists,
222
+ id: 'ChannelManager:default-handler:member.updated',
223
+ };
224
+
225
+ const messageNewHandler: LabeledEventHandler<EventHandlerContext> = {
226
+ handle: updateLists,
227
+ id: 'ChannelManager:default-handler:message.new',
228
+ };
229
+
230
+ const notificationAddedToChannelHandler: LabeledEventHandler<EventHandlerContext> = {
231
+ handle: updateLists,
232
+ id: 'ChannelManager:default-handler:notification.added_to_channel',
233
+ };
234
+
235
+ const notificationMessageNewHandler: LabeledEventHandler<EventHandlerContext> = {
236
+ handle: updateLists,
237
+ id: 'ChannelManager:default-handler:notification.message_new',
238
+ };
239
+
240
+ const notificationRemovedFromChannelHandler: LabeledEventHandler<EventHandlerContext> = {
241
+ handle: removeItem,
242
+ id: 'ChannelManager:default-handler:notification.removed_from_channel',
243
+ };
244
+
245
+ /**
246
+ * `muted` is filterable (resolved from `client.mutedChannels`), but a mute change emits only this
247
+ * user-level event — nothing about the channel — so without re-routing here a channel would keep its
248
+ * old list until some unrelated event touched it. The client applies `event.me.channel_mutes` before
249
+ * its listeners run, so the filters already see the new state.
250
+ */
251
+ const notificationChannelMutesUpdatedHandler: LabeledEventHandler<EventHandlerContext> = {
252
+ handle: ({ ctx: { channelManager } }) => {
253
+ const seen = new Set<string>();
254
+ channelManager.paginators.forEach((paginator) => {
255
+ (paginator.items ?? []).forEach((channel) => {
256
+ if (seen.has(channel.cid)) return; // prevent ingestig -> emitting more than once
257
+ seen.add(channel.cid);
258
+ channelManager.ingestChannel(channel);
259
+ });
260
+ });
261
+ },
262
+ id: 'ChannelManager:default-handler:notification.channel_mutes_updated',
263
+ };
264
+
265
+ // fixme: updates users for member object in all the channels which are loaded with that member - normalization would be beneficial
266
+ const userPresenceChangedHandler: LabeledEventHandler<EventHandlerContext> = {
267
+ handle: ({ event, ctx: { channelManager } }) => {
268
+ const eventUser = event.user;
269
+ if (!eventUser?.id) return;
270
+ channelManager.paginators.forEach((paginator) => {
271
+ const paginatorItems = paginator.items;
272
+ if (!paginatorItems) return;
273
+ let updated = false;
274
+ paginatorItems.forEach((channel) => {
275
+ if (channel.state.members[eventUser.id]) {
276
+ channel.state.members[eventUser.id].user = event.user;
277
+ updated = true;
278
+ }
279
+ if (channel.state.membership.user?.id === eventUser.id) {
280
+ channel.state.membership.user = eventUser;
281
+ updated = true;
282
+ }
283
+ });
284
+ if (updated) {
285
+ // fixme: user is not reactive and so the whole list has to be re-rendered
286
+ paginator.state.partialNext({ items: [...paginatorItems] });
287
+ }
288
+ });
289
+ },
290
+ id: 'ChannelManager:default-handler:user.presence.changed',
291
+ };
292
+
293
+ export type ChannelManagerState = {
294
+ paginators: ChannelPaginator[];
295
+ };
296
+
297
+ export type ChannelManagerEventHandlers = Partial<
298
+ Record<SupportedEventType, LabeledEventHandler<EventHandlerContext>[]>
299
+ >;
300
+
301
+ export type ChannelManagerOptions = {
302
+ client: StreamChat;
303
+ paginators?: ChannelPaginator[];
304
+ /**
305
+ * The complete set of event handlers to run, replacing the defaults rather than extending them.
306
+ * Start from `ChannelManager.getDefaultHandlers()` (a fresh copy) and enrich it, unless the
307
+ * intention really is to route events differently from scratch. Note that the manager the client
308
+ * instantiates (`client.channelManager`) is constructed without this option — customize that one
309
+ * through `addEventHandler` / `setEventHandlers` / `removeEventHandlers`.
310
+ */
311
+ eventHandlers?: ChannelManagerEventHandlers;
312
+ /**
313
+ * Decide which paginator(s) should own a channel when multiple match.
314
+ * Defaults to keeping the channel in all matching paginators.
315
+ * Channels are kept only in the paginators that are listed in the ownershipResolver array.
316
+ * Empty ownershipResolver array means that the channel is kept in all matching paginators.
317
+ */
318
+ ownershipResolver?: PaginatorOwnershipResolver | string[];
319
+ };
320
+
321
+ export class ChannelManager extends WithSubscriptions {
322
+ client: StreamChat;
323
+ state: StateStore<ChannelManagerState>;
324
+ protected _pipelines = new Map<
325
+ SupportedEventType,
326
+ EventHandlerPipeline<EventHandlerContext>
327
+ >();
328
+ protected ownershipResolver?: PaginatorOwnershipResolver;
329
+ /**
330
+ * The `filterQueryResults` each registered paginator had before this manager wrapped it.
331
+ *
332
+ * A server query bypasses the manager — the paginator fetches and ingests a page on its own, and
333
+ * the backend knows nothing about client-side ownership — so a "Work" (`{ team: 'work' }`) channel
334
+ * comes back in a catch-all list's page too and would render in both. `wrapPaginatorFiltering`
335
+ * therefore composes the ownership check onto that hook, which every page passes through.
336
+ *
337
+ * Keys = already wrapped (so re-inserting does not wrap twice); values = what to restore on
338
+ * removal, since a detached paginator must stop applying rules resolved against lists it left.
339
+ * Weakly keyed so a dropped paginator is not kept alive by this manager.
340
+ */
341
+ protected filterQueryResultsBeforeWrapping = new WeakMap<
342
+ ChannelPaginator,
343
+ ChannelPaginator['filterQueryResults']
344
+ >();
345
+
346
+ protected static readonly defaultEventHandlers: ChannelManagerEventHandlers = {
347
+ 'channel.deleted': [channelDeletedHandler],
348
+ 'channel.updated': [channelUpdatedHandler],
349
+ 'channel.truncated': [channelTruncatedHandler],
350
+ 'channel.hidden': [channelHiddenHandler],
351
+ 'channel.visible': [channelVisibleHandler],
352
+ 'member.updated': [memberUpdatedHandler],
353
+ 'message.new': [messageNewHandler],
354
+ 'notification.added_to_channel': [notificationAddedToChannelHandler],
355
+ 'notification.channel_mutes_updated': [notificationChannelMutesUpdatedHandler],
356
+ 'notification.message_new': [notificationMessageNewHandler],
357
+ 'notification.removed_from_channel': [notificationRemovedFromChannelHandler],
358
+ 'user.presence.changed': [userPresenceChangedHandler],
359
+ };
360
+
361
+ constructor({
362
+ client,
363
+ eventHandlers,
364
+ paginators,
365
+ ownershipResolver,
366
+ }: ChannelManagerOptions) {
367
+ super();
368
+ this.client = client;
369
+ this.state = new StateStore({ paginators: paginators ?? [] });
370
+ this.setOwnershipResolver(ownershipResolver);
371
+
372
+ // A supplied map replaces the defaults wholesale — an event type missing from it is simply not
373
+ // handled. `getDefaultHandlers()` returns a copy to enrich when that is what you want.
374
+ const finalEventHandlers = eventHandlers ?? ChannelManager.getDefaultHandlers();
375
+ for (const [type, handlers] of Object.entries(finalEventHandlers)) {
376
+ if (handlers) this.ensurePipeline(type).replaceAll(handlers);
377
+ }
378
+ // Ensure ownership rules are applied to initial paginators' query results
379
+ this.paginators.forEach((p) => this.wrapPaginatorFiltering(p));
380
+ }
381
+
382
+ get paginators(): ChannelPaginator[] {
383
+ return this.state.getLatestValue().paginators;
384
+ }
385
+
386
+ get pipelines(): Map<SupportedEventType, EventHandlerPipeline<EventHandlerContext>> {
387
+ return this._pipelines;
388
+ }
389
+
390
+ private get ctx(): EventHandlerContext {
391
+ return { channelManager: this };
392
+ }
393
+
394
+ /**
395
+ * Returns deep copy of default handlers mapping.
396
+ * The defaults can be enriched with custom handlers or the custom handlers can be replaced.
397
+ */
398
+ static getDefaultHandlers(): ChannelManagerEventHandlers {
399
+ const src = ChannelManager.defaultEventHandlers;
400
+ const out: ChannelManagerEventHandlers = {};
401
+ for (const [type, handlers] of Object.entries(src)) {
402
+ if (!handlers) continue;
403
+ out[type as SupportedEventType] = [...handlers];
404
+ }
405
+ return out;
406
+ }
407
+
408
+ /**
409
+ * Replace the rule deciding which paginator(s) own a channel matched by several of them. Pass an
410
+ * ordered array of paginator ids (highest priority first) for the built-in priority resolver, a
411
+ * custom resolver function, or `undefined` to go back to the default (a channel is kept in every
412
+ * paginator whose filter it matches).
413
+ *
414
+ * Available as a setter because the manager is constructed by the client, before the app has had
415
+ * a chance to create its paginators — their ids are therefore only known later.
416
+ */
417
+ setOwnershipResolver(ownershipResolver?: PaginatorOwnershipResolver | string[]) {
418
+ if (!ownershipResolver) {
419
+ this.ownershipResolver = undefined;
420
+ return;
421
+ }
422
+ this.ownershipResolver = Array.isArray(ownershipResolver)
423
+ ? createPriorityOwnershipResolver(ownershipResolver)
424
+ : ownershipResolver;
425
+ }
426
+
427
+ /**
428
+ * Which paginators should own the channel among the ones that matched.
429
+ * Default behavior keeps the channel in all matching paginators.
430
+ */
431
+ resolveOwnership(
432
+ channel: Channel,
433
+ matchingPaginators: ChannelPaginator[],
434
+ ): Set<string> {
435
+ return new Set(this.ownershipResolver?.({ channel, matchingPaginators }) ?? []);
436
+ }
437
+
438
+ /**
439
+ * Route a channel into the paginator(s) that should own it, and remove it from any list it
440
+ * no longer belongs to. Ownership is resolved exactly as for live WS updates — the channel is
441
+ * ingested into every paginator whose filter it matches (or, when an ownership resolver picks
442
+ * winners among several matches, only into the owner(s)).
443
+ *
444
+ * Use this to surface a channel the app just opened — a search result, a freshly created DM —
445
+ * in the list(s) without a full re-query. `ingestItem` dedupes by cid and inserts in sort
446
+ * order, so calling this repeatedly is safe.
447
+ *
448
+ * A channel that matches no paginator is not added anywhere. To have such channels still
449
+ * appear, register a catch-all paginator (empty filter) with the lowest ownership priority as
450
+ * a local fallback list.
451
+ */
452
+ ingestChannel(channel: Channel) {
453
+ const matchingPaginators = this.paginators.filter((p) => p.matchesFilter(channel));
454
+ const matchingIds = new Set(matchingPaginators.map((p) => p.id));
455
+ const ownerIds = this.resolveOwnership(channel, matchingPaginators);
456
+
457
+ this.paginators.forEach((paginator) => {
458
+ const isMatch = matchingIds.has(paginator.id);
459
+ const isOwner = ownerIds.size === 0 || ownerIds.has(paginator.id);
460
+ if (isMatch && isOwner) {
461
+ paginator.ingestItem(channel);
462
+ } else {
463
+ // Not a match, or matched but not the selected owner — enforce exclusivity.
464
+ paginator.removeItem({ item: channel });
465
+ }
466
+ });
467
+ }
468
+
469
+ /**
470
+ * Filter a page of query results for a specific paginator according to ownership rules.
471
+ * If no owners are specified by the resolver, all matching paginators keep the item.
472
+ */
473
+ protected filterItemsByOwnership({
474
+ paginator,
475
+ items,
476
+ }: {
477
+ paginator: ChannelPaginator;
478
+ items: Channel[];
479
+ }): Channel[] {
480
+ if (!items.length) return items;
481
+ const result: Channel[] = [];
482
+ for (const ch of items) {
483
+ const matchingPaginators = this.paginators.filter((p) => p.matchesFilter(ch));
484
+ const ownerIds = this.resolveOwnership(ch, matchingPaginators);
485
+ const noOwnersOrPaginatorIsOwner =
486
+ ownerIds.size === 0 || ownerIds.has(paginator.id);
487
+
488
+ if (noOwnersOrPaginatorIsOwner) {
489
+ result.push(ch);
490
+ }
491
+ }
492
+ return result;
493
+ }
494
+
495
+ /**
496
+ * Makes a registered paginator apply this manager's ownership rules to the pages it fetches
497
+ * itself, by composing `filterItemsByOwnership` onto its `filterQueryResults`.
498
+ *
499
+ * A channel can enter a list through two doors, and only one of them goes through the manager:
500
+ * - **WS events / `ingestChannel`** — the manager routes the channel and consults
501
+ * `resolveOwnership` before ingesting, so exclusivity holds by construction.
502
+ * - **A server query** — `paginator.next()` (a `ChannelList` scrolling, `reload()`) queries and
503
+ * ingests on its own; the manager is not involved. Without this wrapper a page would land in
504
+ * every list whose *server-side* filter matched it, so a channel matching both
505
+ * `channels:archived` and `channels:default` would show up in both lists until some later event
506
+ * re-routed it — the exact duplication the resolver exists to prevent.
507
+ *
508
+ * Wrapping (rather than subclassing or a constructor hook) is what lets the manager inject this
509
+ * into paginators it does not construct: `filterQueryResults` is an instance property, so the
510
+ * original is captured here and handed back by `unwrapPaginatorFiltering` when the paginator is
511
+ * detached. The paginator's own filtering runs first and stays authoritative — ownership only ever
512
+ * removes further items, never adds any back.
513
+ *
514
+ * Idempotent: the `WeakMap` guard keeps a re-inserted paginator from being wrapped twice, which
515
+ * would filter the same page repeatedly and, worse, lose the reference to the true original.
516
+ */
517
+ protected wrapPaginatorFiltering(paginator: ChannelPaginator) {
518
+ if (this.filterQueryResultsBeforeWrapping.has(paginator)) return;
519
+ const original = paginator.filterQueryResults.bind(paginator);
520
+ paginator.filterQueryResults = (items: Channel[]) => {
521
+ const filtered = original(items) as Channel[];
522
+ return this.filterItemsByOwnership({ paginator, items: filtered });
523
+ };
524
+ this.filterQueryResultsBeforeWrapping.set(paginator, original);
525
+ }
526
+
527
+ /**
528
+ * Undo `wrapPaginatorFiltering` — restore the `filterQueryResults` the paginator had before this
529
+ * manager wrapped it. A paginator that has left the manager must not keep applying its ownership
530
+ * rules: those are resolved against the paginators the manager still holds, so a detached
531
+ * paginator would drop channels owned by lists it is no longer part of.
532
+ */
533
+ protected unwrapPaginatorFiltering(paginator: ChannelPaginator) {
534
+ const original = this.filterQueryResultsBeforeWrapping.get(paginator);
535
+ if (!original) return;
536
+ paginator.filterQueryResults = original;
537
+ this.filterQueryResultsBeforeWrapping.delete(paginator);
538
+ }
539
+
540
+ getPaginatorById(id: string) {
541
+ return this.paginators.find((p) => p.id === id);
542
+ }
543
+
544
+ /**
545
+ * Replace the whole set of lists in a single state update — the primitive `insertPaginator` and
546
+ * `removePaginator` build on, and what to use for a wholesale swap instead of publishing an
547
+ * intermediate state per step. Paginators dropped from the set are detached exactly as
548
+ * `removePaginator` detaches them; a repeated id keeps only its first occurrence.
549
+ *
550
+ * @param paginators - The lists this manager should hold, in render order.
551
+ */
552
+ setPaginators(paginators: ChannelPaginator[]) {
553
+ const nextPaginators: ChannelPaginator[] = [];
554
+ const seenIds = new Set<string>();
555
+ for (const paginator of paginators) {
556
+ if (seenIds.has(paginator.id)) continue;
557
+ seenIds.add(paginator.id);
558
+ nextPaginators.push(paginator);
559
+ }
560
+
561
+ const currentPaginators = this.paginators;
562
+ // Publishing an equivalent set would hand subscribers a new array for no reason — a
563
+ // `state.paginators` selector shallow-compares by reference, so every list would re-render.
564
+ const isUnchanged =
565
+ currentPaginators.length === nextPaginators.length &&
566
+ currentPaginators.every((paginator, i) => paginator === nextPaginators[i]);
567
+ if (isUnchanged) return;
568
+
569
+ const detached = currentPaginators.filter(
570
+ (paginator) => !nextPaginators.includes(paginator),
571
+ );
572
+
573
+ this.state.partialNext({ paginators: nextPaginators });
574
+
575
+ detached.forEach((paginator) => {
576
+ this.unwrapPaginatorFiltering(paginator);
577
+ paginator.cancelScheduledQuery();
578
+ });
579
+ // Wrap the current set to enforce ownership on their query results
580
+ nextPaginators.forEach((paginator) => this.wrapPaginatorFiltering(paginator));
581
+ }
582
+
583
+ /**
584
+ * Detach every list at once — the teardown counterpart of `setPaginators`. Each paginator is
585
+ * released exactly as `removePaginator` releases it (ownership-filtering wrapper removed,
586
+ * scheduled query canceled, loaded items kept), in a single state update.
587
+ *
588
+ * @returns The paginators the manager held, in the order it held them.
589
+ */
590
+ clearPaginators(): ChannelPaginator[] {
591
+ const cleared = this.paginators;
592
+ this.setPaginators([]);
593
+ return cleared;
594
+ }
595
+
596
+ /**
597
+ * If paginator already exists → remove old, reinsert at new index.
598
+ * If index not provided → append at the end.
599
+ * If index provided → insert (or move) at that index.
600
+ *
601
+ * @param params - The insertion parameters.
602
+ * @param params.paginator - The paginator to insert or move.
603
+ * @param params.index - Target index; when omitted the paginator is appended.
604
+ */
605
+ insertPaginator({ paginator, index }: { paginator: ChannelPaginator; index?: number }) {
606
+ const paginators = [...this.paginators];
607
+ const existingIndex = paginators.findIndex((p) => p.id === paginator.id);
608
+ if (existingIndex > -1) {
609
+ paginators.splice(existingIndex, 1);
610
+ }
611
+ const validIndex = Math.max(
612
+ 0,
613
+ Math.min(index ?? paginators.length, paginators.length),
614
+ );
615
+ paginators.splice(validIndex, 0, paginator);
616
+ this.setPaginators(paginators);
617
+ }
618
+
619
+ /**
620
+ * Remove a paginator from the manager. The paginator stops receiving WS-driven updates and
621
+ * disappears from `state.paginators` (so UIs rendering one list per paginator drop its list), and
622
+ * gets its own `filterQueryResults` back — from here on it is an independent paginator again.
623
+ *
624
+ * Its loaded items are left untouched: the paginator can be re-inserted later, or kept and
625
+ * queried on its own. Any query scheduled through `nextDebounced` is canceled, so a list that was
626
+ * just removed does not fire one more request.
627
+ *
628
+ * @param paginatorOrId - The paginator to remove, or its id.
629
+ * @returns The removed paginator, or `undefined` when this manager did not hold it.
630
+ */
631
+ removePaginator(
632
+ paginatorOrId: ChannelPaginator | string,
633
+ ): ChannelPaginator | undefined {
634
+ const id = typeof paginatorOrId === 'string' ? paginatorOrId : paginatorOrId.id;
635
+ const removed = this.getPaginatorById(id);
636
+ if (!removed) return undefined;
637
+
638
+ this.setPaginators(this.paginators.filter((paginator) => paginator !== removed));
639
+ return removed;
640
+ }
641
+
642
+ /**
643
+ * Discard the loaded data of every registered list, keeping the registrations themselves: which
644
+ * lists exist is application configuration, while the channels in them belong to the connected
645
+ * user and become invalid on `disconnectUser`. Each paginator returns to "never queried"
646
+ * (`items: undefined`) with its debounced query canceled. Deliberately not named `resetState` —
647
+ * this manager's own `state` *is* the paginator list, which this must not touch.
648
+ */
649
+ resetPaginatorStates() {
650
+ this.paginators.forEach((paginator) => {
651
+ paginator.cancelScheduledQuery();
652
+ paginator.resetState();
653
+ });
654
+ }
655
+
656
+ addEventHandler({
657
+ eventType,
658
+ ...payload
659
+ }: {
660
+ eventType: SupportedEventType;
661
+ } & InsertEventHandlerPayload<EventHandlerContext>): Unsubscribe {
662
+ return this.ensurePipeline(eventType).insert(payload);
663
+ }
664
+
665
+ setEventHandlers({
666
+ eventType,
667
+ handlers,
668
+ }: {
669
+ eventType: SupportedEventType;
670
+ handlers: LabeledEventHandler<EventHandlerContext>[];
671
+ }) {
672
+ return this.ensurePipeline(eventType).replaceAll(handlers);
673
+ }
674
+
675
+ removeEventHandlers({
676
+ eventType,
677
+ handlers,
678
+ }: {
679
+ eventType: SupportedEventType;
680
+ handlers: FindEventHandlerParams<EventHandlerContext>[];
681
+ }) {
682
+ const pipeline = this._pipelines.get(eventType);
683
+ if (!pipeline) return;
684
+ handlers.forEach((params) => pipeline.remove(params));
685
+ }
686
+
687
+ /** Subscribe to WS (and more buses via attachBus) */
688
+ registerSubscriptions(): Unsubscribe {
689
+ if (!this.hasSubscriptions) {
690
+ this.addUnsubscribeFunction(
691
+ // todo: maybe we should have a wrapper here to decide, whether the event is a LocalEventBus event or else supported by client
692
+ this.client.on((event) => {
693
+ const pipe = this._pipelines.get(event.type);
694
+ if (pipe) {
695
+ pipe.run(event, this.ctx);
696
+ }
697
+ }).unsubscribe,
698
+ );
699
+ }
700
+
701
+ this.incrementRefCount();
702
+ return () => this.unregisterSubscriptions();
703
+ }
704
+
705
+ ensurePipeline(
706
+ eventType: SupportedEventType,
707
+ ): EventHandlerPipeline<EventHandlerContext> {
708
+ let pipe = this._pipelines.get(eventType);
709
+ if (!pipe) {
710
+ pipe = new EventHandlerPipeline<EventHandlerContext>({
711
+ id: `ChannelManager:${eventType}`,
712
+ });
713
+ this._pipelines.set(eventType, pipe);
714
+ }
715
+ return pipe;
716
+ }
717
+
718
+ reload = async () =>
719
+ await Promise.allSettled(
720
+ this.paginators.map(async (paginator) => {
721
+ await paginator.reload();
722
+ }),
723
+ );
724
+ }