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
@@ -8,7 +8,11 @@ import {
8
8
  sleep,
9
9
  } from './utils';
10
10
  import { isAPIError, isConnectionIDError, isErrorRetryable } from './errors';
11
- import type { ConnectionOpen, Event, LogLevel, UR } from './types';
11
+ import { chatLoggerSystem } from './logger';
12
+ import type { ConnectionOpen, UR } from './types';
13
+ import type { WSEvent } from './gen/models';
14
+
15
+ const logger = chatLoggerSystem.getLogger('connection-fallback');
12
16
 
13
17
  export enum ConnectionState {
14
18
  Closed = 'CLOSED',
@@ -33,15 +37,8 @@ export class WSConnectionFallback {
33
37
  addConnectionEventListeners(this._onlineStatusChanged);
34
38
  }
35
39
 
36
- _log(msg: string, extra: UR = {}, level: LogLevel = 'info') {
37
- this.client.logger(level, 'WSConnectionFallback:' + msg, {
38
- tags: ['connection_fallback', 'connection'],
39
- ...extra,
40
- });
41
- }
42
-
43
40
  _setState(state: ConnectionState) {
44
- this._log(`_setState() - ${state}`);
41
+ logger.withExtraTags('_setState').debug(`Transitioning to state: ${state}.`);
45
42
 
46
43
  // transition from connecting => connected
47
44
  if (
@@ -60,7 +57,9 @@ export class WSConnectionFallback {
60
57
 
61
58
  /** @private */
62
59
  _onlineStatusChanged = (event: { type: string }) => {
63
- this._log(`_onlineStatusChanged() - ${event.type}`);
60
+ logger
61
+ .withExtraTags('_onlineStatusChanged')
62
+ .info(`Network status changed to ${event.type}.`);
64
63
 
65
64
  if (event.type === 'offline') {
66
65
  this._setState(ConnectionState.Closed);
@@ -85,24 +84,22 @@ export class WSConnectionFallback {
85
84
  }
86
85
 
87
86
  try {
88
- const res = await this.client.doAxiosRequest<T>(
87
+ const res = await this.client.api.doAxiosRequest<T>(
89
88
  'get',
90
89
  (this.client.baseURL as string).replace(':3030', ':8900') + '/longpoll', // replace port if present for testing with local API
91
90
  undefined,
92
- {
93
- config: { ...config, cancelToken: this.cancelToken?.token },
94
- params,
95
- },
91
+ { ...config, cancelToken: this.cancelToken?.token, params },
96
92
  );
97
93
 
98
94
  this.consecutiveFailures = 0; // always reset in case of no error
99
95
  return res;
100
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
96
  } catch (error: any) {
102
97
  this.consecutiveFailures += 1;
103
98
 
104
99
  if (retry && isErrorRetryable(error)) {
105
- this._log(`_req() - Retryable error, retrying request`);
100
+ logger
101
+ .withExtraTags('_req')
102
+ .debug('Encountered a retryable error. Retrying the request.');
106
103
  await sleep(retryInterval(this.consecutiveFailures));
107
104
  return this._req<T>(params, config, retry);
108
105
  }
@@ -116,7 +113,7 @@ export class WSConnectionFallback {
116
113
  while (this.state === ConnectionState.Connected) {
117
114
  try {
118
115
  const data = await this._req<{
119
- events: Event[];
116
+ events: WSEvent[];
120
117
  }>({}, { timeout: 30000 }, true); // 30s => API responds in 20s if there is no event
121
118
 
122
119
  if (data.events?.length) {
@@ -124,17 +121,18 @@ export class WSConnectionFallback {
124
121
  this.client.dispatchEvent(data.events[i]);
125
122
  }
126
123
  }
127
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
124
  } catch (error: any) {
129
125
  if (axios.isCancel(error)) {
130
- this._log(`_poll() - axios canceled request`);
126
+ logger.withExtraTags('_poll').debug('Axios canceled the request.');
131
127
  return;
132
128
  }
133
129
 
134
130
  /** client.doAxiosRequest will take care of TOKEN_EXPIRED error */
135
131
 
136
132
  if (isConnectionIDError(error)) {
137
- this._log(`_poll() - ConnectionID error, connecting without ID...`);
133
+ logger
134
+ .withExtraTags('_poll')
135
+ .warn('Received a connection ID error. Reconnecting without an ID.');
138
136
  this._setState(ConnectionState.Disconnected);
139
137
  this.connect(true);
140
138
  return;
@@ -152,15 +150,20 @@ export class WSConnectionFallback {
152
150
 
153
151
  /**
154
152
  * connect try to open a longpoll request
155
- * @param reconnect should be false for first call and true for subsequent calls to keep the connection alive and call recoverState
153
+ *
154
+ * @param reconnect - should be false for first call and true for subsequent calls to keep the connection alive and call recoverState
156
155
  */
157
156
  connect = async (reconnect = false) => {
158
157
  if (this.state === ConnectionState.Connecting) {
159
- this._log('connect() - connecting already in progress', { reconnect }, 'warn');
158
+ logger
159
+ .withExtraTags('connect')
160
+ .warn('A connection attempt is already in progress.', { reconnect });
160
161
  return;
161
162
  }
162
163
  if (this.state === ConnectionState.Connected) {
163
- this._log('connect() - already connected and polling', { reconnect }, 'warn');
164
+ logger
165
+ .withExtraTags('connect')
166
+ .warn('Already connected and polling.', { reconnect });
164
167
  return;
165
168
  }
166
169
 
@@ -175,7 +178,6 @@ export class WSConnectionFallback {
175
178
 
176
179
  this._setState(ConnectionState.Connected);
177
180
  this.connectionID = event.connection_id;
178
- // @ts-expect-error type mismatch
179
181
  this.client.dispatchEvent(event);
180
182
  this._poll();
181
183
  if (reconnect) {
@@ -205,9 +207,9 @@ export class WSConnectionFallback {
205
207
 
206
208
  try {
207
209
  await this._req({ close: true, connection_id }, { timeout }, false);
208
- this._log(`disconnect() - Closed connectionID`);
210
+ logger.withExtraTags('disconnect').info('Closed the connection ID.');
209
211
  } catch (err) {
210
- this._log(`disconnect() - Failed`, { err }, 'error');
212
+ logger.withExtraTags('disconnect').error('Disconnect failed.', { error: err });
211
213
  }
212
214
  };
213
215
  }
package/src/constants.ts CHANGED
@@ -1,9 +1,5 @@
1
1
  export const DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE = 25;
2
2
  export const DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE = 100;
3
- export const DEFAULT_MESSAGE_SET_PAGINATION = Object.freeze({
4
- hasNext: false,
5
- hasPrev: false,
6
- });
7
3
  export const DEFAULT_UPLOAD_SIZE_LIMIT_BYTES = 100 * 1024 * 1024; // 100 MB
8
4
  export const API_MAX_FILES_ALLOWED_PER_MESSAGE = 10;
9
5
  export const MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY = 100;
@@ -23,7 +19,7 @@ export const RESERVED_UPDATED_MESSAGE_FIELDS = Object.freeze({
23
19
  own_reactions: true,
24
20
  reaction_counts: true,
25
21
  reply_count: true,
26
- // Message text related fields that shouldn't be in update
22
+ // MessageRequest text related fields that shouldn't be in update
27
23
  i18n: true,
28
24
  type: true,
29
25
  html: true,
@@ -1,4 +1,7 @@
1
- export interface CustomAttachmentData {}
1
+ export interface CustomAttachmentData {
2
+ mime_type?: string;
3
+ file_size?: number;
4
+ }
2
5
  export interface CustomChannelData {}
3
6
  export interface CustomCommandData {}
4
7
  export interface CustomEventData {}
@@ -0,0 +1,208 @@
1
+ import type { Unsubscribe } from '../store';
2
+
3
+ /**
4
+ * A batch of entity-store changes delivered to a subscriber in a single notification.
5
+ *
6
+ * `changedIds` are the ids the subscriber watches whose canonical object changed reference this
7
+ * flush (an upsert, or a removal — a removal makes `store.get(id)` return `undefined`).
8
+ */
9
+ export type EntityStoreChangeBatch = {
10
+ changedIds: ReadonlySet<string>;
11
+ };
12
+
13
+ /**
14
+ * Anything that observes entities held in an {@link EntityStore}.
15
+ *
16
+ * A subscriber watches a *set* of entity ids (a paginator watches all ids in its
17
+ * intervals; a thread watches its single parent id). It is notified at most once
18
+ * per store transaction with the subset of its watched ids that changed.
19
+ */
20
+ export type EntityStoreSubscriber = {
21
+ onEntitiesChanged: (batch: EntityStoreChangeBatch) => void;
22
+ /**
23
+ * Optional: emit any throttled/pending state notification immediately. Called by
24
+ * {@link EntityStore.flushSubscribers} after an optimistic (local-user) write so the change
25
+ * renders without throttle delay. A paginator implements this by flushing its throttled window
26
+ * publish.
27
+ */
28
+ flushState?: () => void;
29
+ };
30
+
31
+ export type EntityStoreOptions<T> = {
32
+ /** Extracts the canonical id an entity is stored and addressed under. */
33
+ getEntityId: (entity: T) => string;
34
+ };
35
+
36
+ /**
37
+ * A client-global, normalized store for domain entities addressed by id.
38
+ *
39
+ * The store holds exactly **one** canonical `T` per id and lets any number of entities
40
+ * (paginators, threads, ad-hoc consumers) subscribe to individual ids. Every mutation of a
41
+ * stored entity becomes a single `upsert`, and the store fans the change out to exactly the
42
+ * subscribers holding that id — replacing the manual copy-to-copy fan-out that keeping N
43
+ * per-consumer copies in sync required.
44
+ *
45
+ * The store is entity-agnostic: it learns how to extract an id from a `T` via the `getEntityId`
46
+ * function passed to its constructor. The client holds one as `client.messageStore`, an
47
+ * `EntityStore<LocalMessage>`, for message content.
48
+ *
49
+ * ## Design
50
+ *
51
+ * - **Content:** `byId` (`Map<id, T>`) is the single source of truth. Objects are immutable
52
+ * snapshots: `upsert` *replaces*, never mutates in place, so the reference-equality
53
+ * short-circuit (mirroring {@link StateStore.next}) and every downstream selector keep working.
54
+ * - **Subscription registry / refcount:** `subscribers` (`Map<id, Set<subscriber>>`) is both the
55
+ * per-id notification list and the reference count — when the last subscriber of an id unlinks,
56
+ * the canonical copy is garbage-collected.
57
+ * - **Batching:** `transaction` coalesces a bulk write (e.g. a page of N entities) so each affected
58
+ * subscriber is notified **once** with the full changed-id set, instead of N times.
59
+ * - **Signal-then-pull:** subscribers receive the set of changed ids and pull the new content via
60
+ * `get`; the store never pushes objects.
61
+ *
62
+ * This is deliberately a hand-rolled per-id registry rather than a {@link StateStore}: a single
63
+ * `StateStore` would run every subscriber's selector on every change (O(all subscribers) per emit),
64
+ * and a store-per-entity would allocate a full `StateStore` per id with no cross-id batching. The
65
+ * `Map<id, Set>` gives O(subscribers-of-changed-id) fan-out — the same idiom as `PollManager`'s
66
+ * cache.
67
+ *
68
+ * @template T The domain entity type held by the store.
69
+ */
70
+ export class EntityStore<T> {
71
+ private byId = new Map<string, T>();
72
+ private subscribers = new Map<string, Set<EntityStoreSubscriber>>();
73
+ private readonly getEntityId: (entity: T) => string;
74
+
75
+ private transactionDepth = 0;
76
+ private pendingChanged = new Map<EntityStoreSubscriber, Set<string>>();
77
+
78
+ constructor({ getEntityId }: EntityStoreOptions<T>) {
79
+ this.getEntityId = getEntityId;
80
+ }
81
+
82
+ // ---- reads ----
83
+
84
+ get(id: string | undefined): T | undefined {
85
+ return typeof id === 'string' ? this.byId.get(id) : undefined;
86
+ }
87
+
88
+ has(id: string): boolean {
89
+ return this.byId.has(id);
90
+ }
91
+
92
+ // ---- writes ----
93
+
94
+ /**
95
+ * Replaces the canonical copy of `entity` and notifies every subscriber watching its id —
96
+ * except `subscriber`, the one that performed the write, which is expected to re-render itself
97
+ * (the paginator that performed the write already emits its own window inline, so it must not be
98
+ * notified a second time through the subscription).
99
+ */
100
+ upsert(entity: T, subscriber?: EntityStoreSubscriber): void {
101
+ const id = this.getEntityId(entity);
102
+ const previous = this.byId.get(id);
103
+ // do not notify if the value hasn't changed (mirrors StateStore.next)
104
+ if (previous === entity) return;
105
+ this.byId.set(id, entity);
106
+ this.markDirty(id, subscriber);
107
+ this.autoFlush();
108
+ }
109
+
110
+ // ---- subscription registry / refcount ----
111
+
112
+ /** Registers `subscriber` for `id` (notification target + refcount). */
113
+ link(id: string, subscriber: EntityStoreSubscriber): void {
114
+ let subscribers = this.subscribers.get(id);
115
+ if (!subscribers) {
116
+ subscribers = new Set();
117
+ this.subscribers.set(id, subscribers);
118
+ }
119
+ subscribers.add(subscriber);
120
+ }
121
+
122
+ /** Drops `subscriber` from `id`; GCs the canonical copy when none remain. */
123
+ unlink(id: string, subscriber: EntityStoreSubscriber): void {
124
+ const subscribers = this.subscribers.get(id);
125
+ if (!subscribers) return;
126
+ subscribers.delete(subscriber);
127
+ if (subscribers.size === 0) {
128
+ this.subscribers.delete(id);
129
+ // refcount GC: nobody holds this entity any longer.
130
+ this.byId.delete(id);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Sugar for atomic / ad-hoc consumers that watch a single id (e.g. a thread watching its parent
136
+ * message). Fires `handler` immediately with the current value (mirroring
137
+ * {@link StateStore.subscribe}) and on every subsequent change.
138
+ */
139
+ subscribe(id: string, handler: (entity: T | undefined) => void): Unsubscribe {
140
+ const subscriber: EntityStoreSubscriber = {
141
+ onEntitiesChanged: () => handler(this.byId.get(id)),
142
+ };
143
+ this.link(id, subscriber);
144
+ handler(this.byId.get(id));
145
+ return () => this.unlink(id, subscriber);
146
+ }
147
+
148
+ // ---- batching ----
149
+
150
+ /**
151
+ * Runs `fn`, coalescing all notifications produced by writes inside it into a single flush on
152
+ * exit. Re-entrant: nested transactions flush only when the outermost exits.
153
+ */
154
+ transaction<R>(fn: () => R): R {
155
+ this.transactionDepth += 1;
156
+ try {
157
+ return fn();
158
+ } finally {
159
+ this.transactionDepth -= 1;
160
+ if (this.transactionDepth === 0) this.flush();
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Immediately flushes any throttled/pending state publish on the subscribers of `id` (via
166
+ * {@link EntityStoreSubscriber.flushState}). Called after an optimistic (local-user) write to
167
+ * `id` so it renders without the throttle delay. Only that id's own subscribers are flushed — the
168
+ * write touched no other id — and flushing a subscriber with nothing pending is a no-op.
169
+ */
170
+ flushSubscribers(id: string): void {
171
+ const subscribers = this.subscribers.get(id);
172
+ if (!subscribers) return;
173
+ for (const subscriber of subscribers) subscriber.flushState?.();
174
+ }
175
+
176
+ /**
177
+ * Records `id` as changed for every subscriber watching it, except `subscriber` — the one that
178
+ * wrote it and is resposible for updating itself (see {@link EntityStore.upsert}).
179
+ */
180
+ private markDirty(id: string, subscriber?: EntityStoreSubscriber): void {
181
+ const subscribers = this.subscribers.get(id);
182
+ if (!subscribers) return;
183
+ for (const other of subscribers) {
184
+ if (other === subscriber) continue;
185
+ let changed = this.pendingChanged.get(other);
186
+ if (!changed) {
187
+ changed = new Set();
188
+ this.pendingChanged.set(other, changed);
189
+ }
190
+ changed.add(id);
191
+ }
192
+ }
193
+
194
+ private autoFlush(): void {
195
+ if (this.transactionDepth === 0) this.flush();
196
+ }
197
+
198
+ private flush(): void {
199
+ if (this.pendingChanged.size === 0) return;
200
+ // swap out the pending map before notifying so writes made from within a
201
+ // subscriber accumulate into the next flush rather than mutating this one.
202
+ const changedBySubscriber = this.pendingChanged;
203
+ this.pendingChanged = new Map();
204
+ for (const [subscriber, changedIds] of changedBySubscriber) {
205
+ subscriber.onEntitiesChanged({ changedIds });
206
+ }
207
+ }
208
+ }
@@ -0,0 +1,115 @@
1
+ import type { ItemIndexApi } from '../pagination/ItemIndex';
2
+ import { EntityStore, type EntityStoreSubscriber } from './EntityStore';
3
+
4
+ export type StoreBackedItemIndexOptions<T> = {
5
+ getEntityId: (item: T) => string;
6
+ /**
7
+ * The entity that owns this index; used as the store subscriber, which also holds its refcount.
8
+ * Optional: an index over a private store (see `store`) has no one to fan out to, so an
9
+ * owner-less index just uses an internal no-op subscriber to hold its refcount. Message paginators pass themselves so
10
+ * cross-collection updates reach them; single-home collections (channels, reminders, …) omit it.
11
+ */
12
+ owner?: EntityStoreSubscriber;
13
+ /**
14
+ * The shared, client-global store that holds the canonical content. Optional: when omitted the
15
+ * index provisions a **private** {@link EntityStore} of its own, so it behaves exactly like a
16
+ * plain, per-instance index — same code path, no shared content, no fan-out. This is the mode used
17
+ * by single-home collections and by detached paginators (e.g. built without a client, in tests).
18
+ */
19
+ store?: EntityStore<T>;
20
+ };
21
+
22
+ /** No-op subscriber that holds the refcount for an owner-less (private-store) index. */
23
+ const NOOP_OWNER: EntityStoreSubscriber = { onEntitiesChanged: () => undefined };
24
+
25
+ /**
26
+ * An {@link ItemIndexApi} implementation that keeps entity **content** in an {@link EntityStore}
27
+ * while keeping **membership** local.
28
+ *
29
+ * A consumer sees the same minimal CRUD surface ({@link ItemIndexApi}), but:
30
+ *
31
+ * - `get`/`has`/`values`/`entries` are scoped to *this* index's membership (`memberIds`), so
32
+ * `getItem(id)` still means "does THIS index hold the id" — even though the canonical object
33
+ * lives in the (possibly shared) store. This is what keeps e.g. reaction routing
34
+ * (`threadPaginator.getItem(id) ? thread : channel`) correct and keeps the `.values()` scans
35
+ * from ever walking other channels' entities.
36
+ * - `setOne` writes content once into the store and links the owner as a subscriber (drives both
37
+ * notification fan-out and refcount GC). The write passes the owner as the `subscriber` to skip, so
38
+ * the owner is not notified of its own write (it re-emits its window inline); other subscribers of
39
+ * the same id ARE notified and re-project.
40
+ * - `remove`/`clear` unlink the owner rather than hard-deleting content, so an entity still held by
41
+ * another index (e.g. a `show_in_channel` reply in both the channel list and its thread) survives;
42
+ * the store GCs it only when the last subscriber unlinks.
43
+ *
44
+ * When no shared store is supplied the index holds a private store — every id it links has exactly
45
+ * one subscriber (its own owner), so no fan-out ever fires and removal GCs immediately, matching a plain
46
+ * per-instance index.
47
+ *
48
+ * @template T The domain item type held by the index; collocated with the {@link EntityStore}'s.
49
+ */
50
+ export class StoreBackedItemIndex<T> implements ItemIndexApi<T> {
51
+ private memberIds = new Set<string>();
52
+ private readonly store: EntityStore<T>;
53
+ private readonly owner: EntityStoreSubscriber;
54
+ private readonly getEntityId: (item: T) => string;
55
+
56
+ constructor({ store, owner, getEntityId }: StoreBackedItemIndexOptions<T>) {
57
+ this.store = store ?? new EntityStore<T>({ getEntityId });
58
+ this.owner = owner ?? NOOP_OWNER;
59
+ this.getEntityId = getEntityId;
60
+ }
61
+
62
+ setMany(items: T[]) {
63
+ this.store.transaction(() => {
64
+ for (const item of items) this.setOne(item);
65
+ });
66
+ }
67
+
68
+ setOne(item: T) {
69
+ const id = this.getEntityId(item);
70
+ this.store.link(id, this.owner);
71
+ this.memberIds.add(id);
72
+ this.store.upsert(item, this.owner);
73
+ }
74
+
75
+ get(id: string): T | undefined {
76
+ return this.memberIds.has(id) ? this.store.get(id) : undefined;
77
+ }
78
+
79
+ has(id: string): boolean {
80
+ return this.memberIds.has(id);
81
+ }
82
+
83
+ remove(id: string) {
84
+ if (!this.memberIds.has(id)) return;
85
+ this.memberIds.delete(id);
86
+ this.store.unlink(id, this.owner);
87
+ }
88
+
89
+ clear() {
90
+ for (const id of this.memberIds) this.store.unlink(id, this.owner);
91
+ this.memberIds.clear();
92
+ }
93
+
94
+ entries(): [string, T][] {
95
+ const result: [string, T][] = [];
96
+ for (const id of this.memberIds) {
97
+ const item = this.store.get(id);
98
+ if (item) result.push([id, item]);
99
+ }
100
+ return result;
101
+ }
102
+
103
+ values(): T[] {
104
+ const result: T[] = [];
105
+ for (const id of this.memberIds) {
106
+ const item = this.store.get(id);
107
+ if (item) result.push(item);
108
+ }
109
+ return result;
110
+ }
111
+
112
+ batch<R>(fn: () => R): R {
113
+ return this.store.transaction(fn);
114
+ }
115
+ }
@@ -0,0 +1,113 @@
1
+ import type { StreamChat } from '../client';
2
+ import type { ReactionRequest, ReactionResponse, UserResponse } from '../types';
3
+ import {
4
+ computeOwnReactions,
5
+ messageWithReactionAdded,
6
+ messageWithReactionRemoved,
7
+ } from '../utils';
8
+
9
+ /**
10
+ * Applies a reaction to the single canonical copy of a message in the client-global
11
+ * {@link EntityStore} (`client.messageStore`), addressed purely by id — NOT through any paginator's
12
+ * membership. It reads the
13
+ * current message POJO (`store.get`), produces a new POJO with the reaction folded into its
14
+ * `reaction_groups` / `latest_reactions` (shared count helpers) and `own_reactions`
15
+ * ({@link computeOwnReactions}), and writes it back (`store.upsert`). The store then notifies every
16
+ * collection currently holding that id — the main-list / thread-reply paginators, a thread's
17
+ * subscribed parent, any future consumer — and each re-projects. The message stays a plain object;
18
+ * the store's per-id registry is the reactivity.
19
+ *
20
+ * Because it is keyed by id rather than by a paginator's `getItem`, it reaches a message held by ANY
21
+ * collection, including the thread parent that lives in no paginator — with no per-home wiring.
22
+ *
23
+ * Writes memory (synchronous) + the offline DB (fire-and-forget), and returns an `undo()` reversing
24
+ * both on the current state (inverse deltas, concurrency-safe), or `undefined` when the user isn't
25
+ * connected or the message isn't in the store.
26
+ */
27
+ export const applyReactionLocally = (
28
+ client: StreamChat,
29
+ {
30
+ enforceUnique = false,
31
+ messageId,
32
+ reaction,
33
+ removed = false,
34
+ }: {
35
+ messageId: string;
36
+ reaction: ReactionRequest;
37
+ enforceUnique?: boolean;
38
+ removed?: boolean;
39
+ },
40
+ ): (() => void) | undefined => {
41
+ const store = client.messageStore;
42
+ const user = client.user;
43
+ const existing = store.get(messageId);
44
+ if (!user || !existing) return;
45
+
46
+ const now = new Date();
47
+ // Spread `reaction` first so the authoritative fields below win, while still preserving any values
48
+ // the caller already carried (e.g. the original `created_at` when undo re-applies a captured
49
+ // reaction) via `?? now`. `message_id`/`user`/`user_id` are always derived from this message and
50
+ // the connected user, so they never need to come off `reaction`.
51
+ const reactionResponse: ReactionResponse = {
52
+ ...reaction,
53
+ created_at: reaction.created_at ?? now,
54
+ custom: reaction.custom ?? {},
55
+ message_id: messageId,
56
+ score: reaction.score ?? 1,
57
+ type: reaction.type,
58
+ updated_at: reaction.updated_at ?? now,
59
+ user: user as UserResponse,
60
+ user_id: user.id,
61
+ };
62
+
63
+ // Capture what this op removes so undo() can restore it faithfully (reaction spread last): the
64
+ // deleted reaction for a removal, or the user's displaced reactions for an enforce_unique add.
65
+ const removedReactions: ReactionResponse[] = removed
66
+ ? (existing.own_reactions?.filter((r) => r.type === reactionResponse.type) ?? [])
67
+ : enforceUnique
68
+ ? (existing.own_reactions ?? [])
69
+ : [];
70
+
71
+ const withCounts = removed
72
+ ? messageWithReactionRemoved(existing, reactionResponse)
73
+ : messageWithReactionAdded(existing, reactionResponse, enforceUnique);
74
+ const own_reactions = computeOwnReactions({
75
+ current: existing.own_reactions ?? [],
76
+ enforceUnique,
77
+ reaction: reactionResponse,
78
+ removed,
79
+ userId: user.id,
80
+ });
81
+ store.upsert({ ...withCounts, own_reactions });
82
+ store.flushSubscribers(messageId);
83
+
84
+ const persisted = store.get(messageId);
85
+ if (persisted) {
86
+ client.offlineDb?.executeQuerySafely(
87
+ (db) =>
88
+ removed
89
+ ? db.deleteReaction({ message: persisted, reaction: reactionResponse })
90
+ : enforceUnique
91
+ ? db.updateReaction({ message: persisted, reaction: reactionResponse })
92
+ : db.insertReaction({ message: persisted, reaction: reactionResponse }),
93
+ { method: 'applyReactionLocally' },
94
+ );
95
+ }
96
+
97
+ return () => {
98
+ if (!removed) {
99
+ applyReactionLocally(client, {
100
+ messageId,
101
+ reaction: reactionResponse,
102
+ removed: true,
103
+ });
104
+ }
105
+ for (const removedReaction of removedReactions) {
106
+ applyReactionLocally(client, {
107
+ messageId,
108
+ reaction: removedReaction,
109
+ removed: false,
110
+ });
111
+ }
112
+ };
113
+ };
@@ -0,0 +1,2 @@
1
+ export * from './applyReactionLocally';
2
+ export * from './EntityStore';
package/src/errors.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AxiosResponse } from 'axios';
2
- import type { APIErrorResponse } from './types';
2
+ import type { APIError as Gen_APIError } from './types';
3
3
 
4
4
  export const APIErrorCodes: Record<string, { name: string; retryable: boolean }> = {
5
5
  '-1': { name: 'InternalSystemError', retryable: true },
@@ -48,6 +48,18 @@ export function isErrorRetryable(error: APIError) {
48
48
  return err.retryable;
49
49
  }
50
50
 
51
+ /**
52
+ * Whether an error is EPHEMERAL — a transient failure worth queueing/retrying rather than a
53
+ * definitive rejection. True when the server never responded (connection/network/offline error - no
54
+ * `response`, i.e an axios network error or an `OfflineError`) and when the server responded with a
55
+ * retryable code (see {@link APIErrorCodes}); false only when the server responded with a
56
+ * non-retryable code (InputError 4, DoesNotExist 16, NotAllowed 17, …).
57
+ */
58
+ export function isEphemeral(error: Error): boolean {
59
+ if (!(error as { response?: unknown }).response) return true;
60
+ return isErrorRetryable(error as APIError);
61
+ }
62
+
51
63
  export function isConnectionIDError(error: APIError) {
52
64
  return error.code === 46; // ConnectionIDNotFoundError
53
65
  }
@@ -66,6 +78,6 @@ export function isWSFailure(err: APIError): boolean {
66
78
 
67
79
  export function isErrorResponse(
68
80
  res: AxiosResponse<unknown>,
69
- ): res is AxiosResponse<APIErrorResponse> {
81
+ ): res is AxiosResponse<Gen_APIError> {
70
82
  return !res.status || res.status < 200 || 300 <= res.status;
71
83
  }