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
@@ -1,4 +1,7 @@
1
- import type { ReadResponse, UserResponse } from '../types';
1
+ import type { ReadStateResponse, UserResponse } from '../types';
2
+ import { StateStore } from '../store';
3
+ import type { Channel } from '../channel';
4
+ import { WithSubscriptions } from '../utils/WithSubscriptions';
2
5
 
3
6
  type UserId = string;
4
7
  type MessageId = string;
@@ -11,11 +14,39 @@ export type UserProgress = {
11
14
  lastReadRef: MsgRef; // MIN_REF if none
12
15
  lastDeliveredRef: MsgRef; // MIN_REF if none; always >= readRef
13
16
  };
17
+ export type MessageReceiptsSnapshot = {
18
+ revision: number;
19
+ readersByMessageId: Record<MessageId, UserResponse[]>;
20
+ deliveredByMessageId: Record<MessageId, UserResponse[]>;
21
+ };
22
+ export type ReadStoreReconcileMeta = {
23
+ changedUserIds?: string[];
24
+ removedUserIds?: string[];
25
+ };
26
+ type ReadStoreUserState = {
27
+ last_read?: Date | string;
28
+ unread_messages?: number;
29
+ user?: UserResponse;
30
+ first_unread_message_id?: string;
31
+ last_read_message_id?: string;
32
+ last_delivered_at?: Date | string;
33
+ last_delivered_message_id?: string;
34
+ };
14
35
 
15
36
  // ---------- ordering utilities ----------
16
37
 
17
38
  const MIN_REF: MsgRef = { timestampMs: Number.NEGATIVE_INFINITY, msgId: '' } as const;
18
39
 
40
+ const toTimestampMs = (value: Date | string) =>
41
+ value instanceof Date ? value.getTime() : new Date(value).getTime();
42
+
43
+ const isValidReadState = (
44
+ readState: ReadStoreUserState | undefined,
45
+ ): readState is ReadStoreUserState & {
46
+ last_read: Date | string;
47
+ user: UserResponse;
48
+ } => !!readState?.user && !!readState.last_read;
49
+
19
50
  const compareRefsAsc = (a: MsgRef, b: MsgRef) =>
20
51
  a.timestampMs !== b.timestampMs ? a.timestampMs - b.timestampMs : 0;
21
52
 
@@ -31,10 +62,13 @@ const findIndex = <T>(arr: T[], target: MsgRef, keyOf: (x: T) => MsgRef): number
31
62
  };
32
63
 
33
64
  /**
34
- * For insertion after the last equal item. E.g. array [a] exists and b is being inserted -> we want [a,b], not [b,a].
35
- * @param arr
36
- * @param target
37
- * @param keyOf
65
+ * Finds the insertion index after the last equal item. E.g. when array `[a]` exists and `b` is
66
+ * being inserted we want `[a, b]`, not `[b, a]`.
67
+ *
68
+ * @param arr - The sorted array to search.
69
+ * @param target - The reference value to compare against.
70
+ * @param keyOf - Accessor that maps an item to its comparable reference.
71
+ * @returns The insertion index in `arr`.
38
72
  */
39
73
  const findUpperIndex = <T>(arr: T[], target: MsgRef, keyOf: (x: T) => MsgRef): number => {
40
74
  let lo = 0,
@@ -71,7 +105,8 @@ const removeByOldKey = (
71
105
  };
72
106
 
73
107
  export type OwnMessageReceiptsTrackerOptions = {
74
- locateMessage: OwnMessageReceiptsTrackerMessageLocator;
108
+ channel: Channel;
109
+ locateMessage?: OwnMessageReceiptsTrackerMessageLocator;
75
110
  };
76
111
 
77
112
  /**
@@ -92,13 +127,14 @@ export type OwnMessageReceiptsTrackerOptions = {
92
127
  *
93
128
  * Construction
94
129
  * ------------
95
- * `new MessageReceiptsTracker({locateMessage})`
96
- * - `locateMessage(timestamp) => MsgRef | null` must resolve a message ref representation - `{ timestamp, msgId }`.
97
- * - If `locateMessage` returns `null`, the event is ignored (message unknown locally).
130
+ * `new MessageReceiptsTracker({ channel, locateMessage? })`
131
+ * - By default, message references are read through `channel.state.findMessageByTimestamp`.
132
+ * - `locateMessage` can override this lookup strategy.
133
+ * If a message cannot be resolved locally, the event is ignored.
98
134
  *
99
135
  * Event ingestion
100
136
  * ---------------
101
- * - `ingestInitial(rows: ReadResponse[])`: Builds initial state from server snapshot.
137
+ * - `ingestInitial(rows: ReadStateResponse[])`: Builds initial state from server snapshot.
102
138
  * If a user’s `last_read` is ahead of `last_delivered_at`, the tracker enforces
103
139
  * the invariant `lastDeliveredRef >= lastReadRef`.
104
140
  * - `onMessageRead(user, readAtISO)`:
@@ -131,18 +167,104 @@ export type OwnMessageReceiptsTrackerOptions = {
131
167
  * equal-timestamp plateau (upper-bound insertion), preserving intuitive arrival order.
132
168
  * - This tracker models **others’ progress toward own messages**;
133
169
  */
134
- export class MessageReceiptsTracker {
170
+ export class MessageReceiptsTracker extends WithSubscriptions {
135
171
  private byUser = new Map<UserId, UserProgress>();
136
172
  private readSorted: UserProgress[] = []; // asc by lastReadRef
137
173
  private deliveredSorted: UserProgress[] = []; // asc by lastDeliveredRef
174
+ private channel: Channel;
138
175
  private locateMessage: OwnMessageReceiptsTrackerMessageLocator;
176
+ private pendingReadStoreReconcileMeta?: ReadStoreReconcileMeta;
177
+ readonly snapshotStore = new StateStore<MessageReceiptsSnapshot>({
178
+ revision: 0,
179
+ readersByMessageId: {},
180
+ deliveredByMessageId: {},
181
+ });
182
+
183
+ constructor({ channel, locateMessage }: OwnMessageReceiptsTrackerOptions) {
184
+ super();
185
+ this.channel = channel;
186
+ this.locateMessage =
187
+ locateMessage ??
188
+ ((timestampMs: number) => {
189
+ const message = this.channel.messagePaginator.findItemByTimestamp(timestampMs);
190
+ return message ? { timestampMs, msgId: message.id } : null;
191
+ });
192
+ }
193
+
194
+ public registerSubscriptions = () => {
195
+ this.incrementRefCount();
196
+ if (this.hasSubscriptions) return;
197
+
198
+ this.addUnsubscribeFunction(
199
+ this.channel.state.readStore.subscribe((next, prev) => {
200
+ this.reconcileFromReadStore({
201
+ previousReadState: prev?.read,
202
+ nextReadState: next.read,
203
+ meta: this.pendingReadStoreReconcileMeta,
204
+ });
205
+ this.pendingReadStoreReconcileMeta = undefined;
206
+ }),
207
+ );
208
+ };
209
+
210
+ public unregisterSubscriptions = () => {
211
+ this.pendingReadStoreReconcileMeta = undefined;
212
+ return super.unregisterSubscriptions();
213
+ };
214
+
215
+ public setPendingReadStoreReconcileMeta(meta?: ReadStoreReconcileMeta) {
216
+ this.pendingReadStoreReconcileMeta = meta;
217
+ }
218
+
219
+ reconcileFromReadStore({
220
+ previousReadState,
221
+ nextReadState,
222
+ meta,
223
+ }: {
224
+ previousReadState?: Record<string, { user: UserResponse }>;
225
+ nextReadState: Record<string, ReadStoreUserState>;
226
+ meta?: ReadStoreReconcileMeta;
227
+ }) {
228
+ if (!previousReadState) {
229
+ this.ingestInitial(this.readStoreStateToResponses(nextReadState));
230
+ return;
231
+ }
232
+
233
+ // For non-bootstrap updates, we require patch metadata from channel read-store mutations.
234
+ if (!meta) return;
235
+
236
+ const removedUserIds = new Set(meta?.removedUserIds ?? []);
237
+ const changedUserIds = new Set(meta?.changedUserIds ?? []);
139
238
 
140
- constructor({ locateMessage }: OwnMessageReceiptsTrackerOptions) {
141
- this.locateMessage = locateMessage;
239
+ const changedOrRemovedUserIds = new Set<string>([
240
+ ...changedUserIds,
241
+ ...removedUserIds,
242
+ ]);
243
+
244
+ if (!changedOrRemovedUserIds.size) return;
245
+
246
+ let hasEffectiveChange = false;
247
+
248
+ for (const userId of changedOrRemovedUserIds) {
249
+ if (removedUserIds.has(userId) || !nextReadState[userId]) {
250
+ hasEffectiveChange = this.removeUserProgress(userId) || hasEffectiveChange;
251
+ continue;
252
+ }
253
+
254
+ const nextUserReadState = nextReadState[userId];
255
+ if (!isValidReadState(nextUserReadState)) continue;
256
+ const resolvedProgress = this.readStateToUserProgress(nextUserReadState);
257
+ hasEffectiveChange =
258
+ this.upsertUserProgress(resolvedProgress) || hasEffectiveChange;
259
+ }
260
+
261
+ if (hasEffectiveChange) {
262
+ this.emitSnapshot();
263
+ }
142
264
  }
143
265
 
144
266
  /** Build initial state from server snapshots (single pass + sort). */
145
- ingestInitial(responses: ReadResponse[]) {
267
+ ingestInitial(responses: ReadStateResponse[]) {
146
268
  this.byUser.clear();
147
269
  this.readSorted = [];
148
270
  this.deliveredSorted = [];
@@ -173,6 +295,8 @@ export class MessageReceiptsTracker {
173
295
  userProgress,
174
296
  );
175
297
  }
298
+
299
+ this.emitSnapshot();
176
300
  }
177
301
 
178
302
  /** message.delivered — user device confirmed delivery up to and including messageId. */
@@ -182,13 +306,13 @@ export class MessageReceiptsTracker {
182
306
  lastDeliveredMessageId,
183
307
  }: {
184
308
  user: UserResponse;
185
- deliveredAt: string;
309
+ deliveredAt: Date;
186
310
  lastDeliveredMessageId?: string;
187
311
  }) {
188
- const timestampMs = new Date(deliveredAt).getTime();
312
+ const timestampMs = deliveredAt.getTime();
189
313
  const msgRef = lastDeliveredMessageId
190
314
  ? { timestampMs, msgId: lastDeliveredMessageId }
191
- : this.locateMessage(new Date(deliveredAt).getTime());
315
+ : this.locateMessage(deliveredAt.getTime());
192
316
  if (!msgRef) return;
193
317
  const userProgress = this.ensureUser(user);
194
318
 
@@ -207,6 +331,7 @@ export class MessageReceiptsTracker {
207
331
  );
208
332
  userProgress.lastDeliveredRef = newDelivered;
209
333
  insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
334
+ this.emitSnapshot();
210
335
  }
211
336
 
212
337
  /** message.read — user read up to and including messageId. */
@@ -216,10 +341,10 @@ export class MessageReceiptsTracker {
216
341
  lastReadMessageId,
217
342
  }: {
218
343
  user: UserResponse;
219
- readAt: string;
344
+ readAt: Date;
220
345
  lastReadMessageId?: string;
221
346
  }) {
222
- const timestampMs = new Date(readAt).getTime();
347
+ const timestampMs = readAt.getTime();
223
348
  const msgRef = lastReadMessageId
224
349
  ? { timestampMs, msgId: lastReadMessageId }
225
350
  : this.locateMessage(timestampMs);
@@ -249,6 +374,8 @@ export class MessageReceiptsTracker {
249
374
  userProgress.lastDeliveredRef = userProgress.lastReadRef;
250
375
  insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
251
376
  }
377
+
378
+ this.emitSnapshot();
252
379
  }
253
380
 
254
381
  /** notification.mark_unread — user marked messages unread starting at `first_unread_message_id`.
@@ -263,13 +390,13 @@ export class MessageReceiptsTracker {
263
390
  lastReadMessageId,
264
391
  }: {
265
392
  user: UserResponse;
266
- lastReadAt?: string;
393
+ lastReadAt?: Date;
267
394
  lastReadMessageId?: string;
268
395
  }) {
269
396
  const userProgress = this.ensureUser(user);
270
397
 
271
398
  const newReadRef: MsgRef = lastReadAt
272
- ? { timestampMs: new Date(lastReadAt).getTime(), msgId: lastReadMessageId ?? '' }
399
+ ? { timestampMs: lastReadAt.getTime(), msgId: lastReadMessageId ?? '' }
273
400
  : { ...MIN_REF };
274
401
 
275
402
  // If no change, exit early.
@@ -300,6 +427,8 @@ export class MessageReceiptsTracker {
300
427
  userProgress.lastDeliveredRef = userProgress.lastReadRef;
301
428
  insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
302
429
  }
430
+
431
+ this.emitSnapshot();
303
432
  }
304
433
 
305
434
  /** All users who READ this message. */
@@ -414,4 +543,148 @@ export class MessageReceiptsTracker {
414
543
  }
415
544
  return up;
416
545
  }
546
+
547
+ private removeUserProgress(userId: string) {
548
+ const userProgress = this.byUser.get(userId);
549
+ if (!userProgress) return false;
550
+
551
+ removeByOldKey(
552
+ this.readSorted,
553
+ userProgress,
554
+ userProgress.lastReadRef,
555
+ (x) => x.lastReadRef,
556
+ );
557
+ removeByOldKey(
558
+ this.deliveredSorted,
559
+ userProgress,
560
+ userProgress.lastDeliveredRef,
561
+ (x) => x.lastDeliveredRef,
562
+ );
563
+ this.byUser.delete(userId);
564
+
565
+ return true;
566
+ }
567
+
568
+ private upsertUserProgress(nextUserProgress: UserProgress) {
569
+ const existingUserProgress = this.byUser.get(nextUserProgress.user.id);
570
+ if (!existingUserProgress) {
571
+ this.byUser.set(nextUserProgress.user.id, nextUserProgress);
572
+ insertByKey(this.readSorted, nextUserProgress, (x) => x.lastReadRef);
573
+ insertByKey(this.deliveredSorted, nextUserProgress, (x) => x.lastDeliveredRef);
574
+ return true;
575
+ }
576
+
577
+ const hasSameReadRef =
578
+ compareRefsAsc(existingUserProgress.lastReadRef, nextUserProgress.lastReadRef) ===
579
+ 0 &&
580
+ existingUserProgress.lastReadRef.msgId === nextUserProgress.lastReadRef.msgId;
581
+ const hasSameDeliveredRef =
582
+ compareRefsAsc(
583
+ existingUserProgress.lastDeliveredRef,
584
+ nextUserProgress.lastDeliveredRef,
585
+ ) === 0 &&
586
+ existingUserProgress.lastDeliveredRef.msgId ===
587
+ nextUserProgress.lastDeliveredRef.msgId;
588
+ const hasSameUser = existingUserProgress.user.id === nextUserProgress.user.id;
589
+
590
+ if (hasSameReadRef && hasSameDeliveredRef && hasSameUser) {
591
+ return false;
592
+ }
593
+
594
+ removeByOldKey(
595
+ this.readSorted,
596
+ existingUserProgress,
597
+ existingUserProgress.lastReadRef,
598
+ (x) => x.lastReadRef,
599
+ );
600
+ removeByOldKey(
601
+ this.deliveredSorted,
602
+ existingUserProgress,
603
+ existingUserProgress.lastDeliveredRef,
604
+ (x) => x.lastDeliveredRef,
605
+ );
606
+
607
+ existingUserProgress.user = nextUserProgress.user;
608
+ existingUserProgress.lastReadRef = nextUserProgress.lastReadRef;
609
+ existingUserProgress.lastDeliveredRef = nextUserProgress.lastDeliveredRef;
610
+
611
+ insertByKey(this.readSorted, existingUserProgress, (x) => x.lastReadRef);
612
+ insertByKey(this.deliveredSorted, existingUserProgress, (x) => x.lastDeliveredRef);
613
+
614
+ return true;
615
+ }
616
+
617
+ private readStateToUserProgress(readState: {
618
+ last_read: Date | string;
619
+ unread_messages?: number;
620
+ user: UserResponse;
621
+ first_unread_message_id?: string;
622
+ last_read_message_id?: string;
623
+ last_delivered_at?: Date | string;
624
+ last_delivered_message_id?: string;
625
+ }): UserProgress {
626
+ const lastReadTimestamp = toTimestampMs(readState.last_read);
627
+ const lastDeliveredTimestamp = readState.last_delivered_at
628
+ ? toTimestampMs(readState.last_delivered_at)
629
+ : null;
630
+ const lastReadRef = readState.last_read_message_id
631
+ ? { timestampMs: lastReadTimestamp, msgId: readState.last_read_message_id }
632
+ : (this.locateMessage(lastReadTimestamp) ?? MIN_REF);
633
+ let lastDeliveredRef = readState.last_delivered_message_id
634
+ ? {
635
+ timestampMs: lastDeliveredTimestamp ?? lastReadTimestamp,
636
+ msgId: readState.last_delivered_message_id,
637
+ }
638
+ : lastDeliveredTimestamp
639
+ ? (this.locateMessage(lastDeliveredTimestamp) ?? MIN_REF)
640
+ : MIN_REF;
641
+
642
+ if (compareRefsAsc(lastDeliveredRef, lastReadRef) < 0) {
643
+ lastDeliveredRef = lastReadRef;
644
+ }
645
+
646
+ return {
647
+ user: readState.user,
648
+ lastReadRef,
649
+ lastDeliveredRef,
650
+ };
651
+ }
652
+
653
+ private readStoreStateToResponses(
654
+ readState: Record<string, ReadStoreUserState>,
655
+ ): ReadStateResponse[] {
656
+ return Object.values(readState).reduce<ReadStateResponse[]>(
657
+ (responses, userReadState) => {
658
+ if (!isValidReadState(userReadState)) return responses;
659
+ const lastReadDate = new Date(userReadState.last_read);
660
+ if (Number.isNaN(lastReadDate.getTime())) return responses;
661
+
662
+ responses.push({
663
+ last_read: lastReadDate,
664
+ user: userReadState.user,
665
+ last_read_message_id: userReadState.last_read_message_id,
666
+ unread_messages: userReadState.unread_messages ?? 0,
667
+ last_delivered_at: userReadState.last_delivered_at
668
+ ? new Date(userReadState.last_delivered_at)
669
+ : undefined,
670
+ last_delivered_message_id: userReadState.last_delivered_message_id,
671
+ });
672
+
673
+ return responses;
674
+ },
675
+ [],
676
+ );
677
+ }
678
+
679
+ private emitSnapshot() {
680
+ const readersByMessageId = this.groupUsersByLastReadMessage();
681
+ const deliveredByMessageId = this.groupUsersByLastDeliveredMessage();
682
+ const currentSnapshot = this.snapshotStore.getLatestValue();
683
+
684
+ this.snapshotStore.next({
685
+ revision: currentSnapshot.revision + 1,
686
+ readersByMessageId,
687
+ deliveredByMessageId,
688
+ });
689
+ }
417
690
  }
@@ -0,0 +1,77 @@
1
+ import type { LocalMessage, MessageResponse, StreamAPIError } from '../types';
2
+ import { formatMessage } from '../utils';
3
+
4
+ export type MessageOperationStatePolicyContext = {
5
+ ingest: (m: LocalMessage) => void;
6
+ get: (id: string) => LocalMessage | undefined;
7
+ };
8
+
9
+ const parseError = (error: unknown): StreamAPIError => {
10
+ const stringError = JSON.stringify(error);
11
+ return (stringError ? JSON.parse(stringError) : {}) as StreamAPIError;
12
+ };
13
+
14
+ const isAlreadyExistsError = (error: unknown, parsed: StreamAPIError) =>
15
+ parsed.code === 4 && error instanceof Error && error.message.includes('already exists');
16
+
17
+ export class MessageOperationStatePolicy {
18
+ private ctx: MessageOperationStatePolicyContext;
19
+
20
+ constructor(ctx: MessageOperationStatePolicyContext) {
21
+ this.ctx = ctx;
22
+ }
23
+
24
+ optimistic(localMessage: LocalMessage) {
25
+ this.ctx.ingest({
26
+ ...localMessage,
27
+ error: undefined,
28
+ status:
29
+ !localMessage.status || localMessage.status === 'failed'
30
+ ? 'sending'
31
+ : localMessage.status,
32
+ });
33
+ }
34
+
35
+ success({
36
+ messageFromResponse,
37
+ messageId,
38
+ }: {
39
+ messageFromResponse: MessageResponse;
40
+ messageId: string;
41
+ }) {
42
+ const formatted = formatMessage({ ...messageFromResponse, status: 'received' });
43
+ const existing = this.ctx.get(messageId);
44
+
45
+ const serverNewer =
46
+ !existing || formatted.updated_at.getTime() > existing.updated_at.getTime();
47
+ const serverSameOrNewer =
48
+ !existing || formatted.updated_at.getTime() >= existing.updated_at.getTime();
49
+ const existingIsOurOptimisticSend = existing?.status === 'sending';
50
+
51
+ if (serverNewer || (existingIsOurOptimisticSend && serverSameOrNewer)) {
52
+ this.ctx.ingest(formatted);
53
+ }
54
+ }
55
+
56
+ failure({
57
+ error,
58
+ localMessage,
59
+ messageId,
60
+ }: {
61
+ error: unknown;
62
+ localMessage: LocalMessage;
63
+ messageId: string;
64
+ }) {
65
+ const parsed = parseError(error);
66
+
67
+ if (isAlreadyExistsError(error, parsed)) {
68
+ const existing = this.ctx.get(messageId);
69
+ if (existing?.status === 'sending') {
70
+ this.ctx.ingest({ ...localMessage, status: 'received' });
71
+ }
72
+ return;
73
+ }
74
+
75
+ this.ctx.ingest({ ...localMessage, status: 'failed', error: parsed });
76
+ }
77
+ }