stream-chat 9.52.0 → 9.52.1

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.
@@ -491,14 +491,16 @@ export declare class Channel {
491
491
  */
492
492
  markRead(data?: MarkReadOptions): Promise<EventAPIResponse | null>;
493
493
  /**
494
- * markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is enabled
494
+ * markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is
495
+ * enabled and the user holds the `read-events` capability on this channel
495
496
  *
496
497
  * @param {MarkReadOptions} data
497
498
  * @return {Promise<EventAPIResponse | null>} Description
498
499
  */
499
500
  markAsReadRequest(data?: MarkReadOptions): Promise<EventAPIResponse | null>;
500
501
  /**
501
- * markUnread - Mark the channel as unread from messageID, only works if the `read_events` setting is enabled
502
+ * markUnread - Mark the channel as unread from messageID, only works if the `read_events` setting is
503
+ * enabled and the user holds the `read-events` capability on this channel
502
504
  *
503
505
  * @param {MarkUnreadOptions} data
504
506
  * @return {APIResponse} An API response
@@ -12,6 +12,15 @@ import type { AxiosRequestConfig } from 'axios';
12
12
  *
13
13
  */
14
14
  export declare function logChatPromiseExecution<T>(promise: Promise<T>, name: string): void;
15
+ /**
16
+ * The default no-op client logger.
17
+ */
18
+ export declare const noopLogger: Logger;
19
+ /**
20
+ * Invokes a single event listener in isolation, so that a faulty listener cannot
21
+ * abort the dispatch loop or the bookkeeping that follows it.
22
+ */
23
+ export declare const invokeEventListener: <T>(listener: (event: T) => unknown, event: T, logger: Logger) => void;
15
24
  export declare const sleep: (m: number) => Promise<void>;
16
25
  export declare function isFunction(value: unknown): value is (...args: unknown[]) => unknown;
17
26
  export declare const chatCodes: {
@@ -28,11 +37,6 @@ export declare const channelHasReadEvents: (channel?: Channel) => boolean;
28
37
  * channelTracksReadLocally - Whether a channel maintains a client local unread count.
29
38
  */
30
39
  export declare const channelTracksReadLocally: (channel?: Channel) => boolean;
31
- /**
32
- * userHasReadReceipts - Whether the current user allows read receipts, per their privacy settings.
33
- * Read receipts are treated as enabled unless the user has explicitly disabled them.
34
- */
35
- export declare const userHasReadReceipts: (client: StreamChat) => boolean;
36
40
  export declare function addFileToFormData(uri: string | NodeJS.ReadableStream | Buffer | File, name?: string, contentType?: string): FormData;
37
41
  export declare function normalizeQuerySort<T extends Record<string, AscDesc | undefined>>(sort: T | T[]): {
38
42
  direction: AscDesc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.52.0",
3
+ "version": "9.52.1",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
package/src/channel.ts CHANGED
@@ -6,6 +6,7 @@ import { MessageReceiptsTracker } from './messageDelivery';
6
6
  import {
7
7
  channelHasReadEvents,
8
8
  generateChannelTempCid,
9
+ invokeEventListener,
9
10
  logChatPromiseExecution,
10
11
  messageSetPagination,
11
12
  normalizeQuerySort,
@@ -1221,7 +1222,8 @@ export class Channel {
1221
1222
  }
1222
1223
 
1223
1224
  /**
1224
- * markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is enabled
1225
+ * markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is
1226
+ * enabled and the user holds the `read-events` capability on this channel
1225
1227
  *
1226
1228
  * @param {MarkReadOptions} data
1227
1229
  * @return {Promise<EventAPIResponse | null>} Description
@@ -1229,7 +1231,13 @@ export class Channel {
1229
1231
  async markAsReadRequest(data: MarkReadOptions = {}) {
1230
1232
  this._checkInitialized();
1231
1233
 
1232
- if (!this.getConfig()?.read_events && !this.getClient()._isUsingServerAuth()) {
1234
+ // Server-side auth acts on behalf of a user with no own capabilities, so only the client-side
1235
+ // flow is gated. Channels the user has no `read-events` capability on do not track read state
1236
+ // on the backend at all - see `markReadLocally` for the client-local unread count.
1237
+ if (
1238
+ !this.getClient()._isUsingServerAuth() &&
1239
+ (!this.getConfig()?.read_events || !channelHasReadEvents(this))
1240
+ ) {
1233
1241
  return null;
1234
1242
  }
1235
1243
 
@@ -1239,7 +1247,8 @@ export class Channel {
1239
1247
  }
1240
1248
 
1241
1249
  /**
1242
- * markUnread - Mark the channel as unread from messageID, only works if the `read_events` setting is enabled
1250
+ * markUnread - Mark the channel as unread from messageID, only works if the `read_events` setting is
1251
+ * enabled and the user holds the `read-events` capability on this channel
1243
1252
  *
1244
1253
  * @param {MarkUnreadOptions} data
1245
1254
  * @return {APIResponse} An API response
@@ -1247,7 +1256,11 @@ export class Channel {
1247
1256
  async markUnread(data: MarkUnreadOptions) {
1248
1257
  this._checkInitialized();
1249
1258
 
1250
- if (!this.getConfig()?.read_events && !this.getClient()._isUsingServerAuth()) {
1259
+ // Gated the same way as `markAsReadRequest` - see the comment there.
1260
+ if (
1261
+ !this.getClient()._isUsingServerAuth() &&
1262
+ (!this.getConfig()?.read_events || !channelHasReadEvents(this))
1263
+ ) {
1251
1264
  return Promise.resolve(null);
1252
1265
  }
1253
1266
 
@@ -2354,22 +2367,17 @@ export class Channel {
2354
2367
  }
2355
2368
 
2356
2369
  _callChannelListeners = (event: Event) => {
2357
- // eslint-disable-next-line @typescript-eslint/no-this-alias
2358
- const channel = this;
2359
- // gather and call the listeners
2360
- const listeners = [];
2361
- if (channel.listeners.all) {
2362
- listeners.push(...channel.listeners.all);
2363
- }
2364
- if (channel.listeners[event.type]) {
2365
- listeners.push(...channel.listeners[event.type]);
2366
- }
2370
+ // snapshot before dispatching: `on` pushes into these arrays in place, so a
2371
+ // listener that subscribes while handling an event must not be invoked for it
2372
+ const listeners = [
2373
+ ...(this.listeners.all ?? []),
2374
+ ...(this.listeners[event.type] ?? []),
2375
+ ];
2367
2376
 
2368
- // call the event and send it to the listeners
2377
+ const { logger } = this._client;
2369
2378
  for (const listener of listeners) {
2370
- if (typeof listener !== 'string') {
2371
- listener(event);
2372
- }
2379
+ if (typeof listener === 'string') continue;
2380
+ invokeEventListener(listener, event, logger);
2373
2381
  }
2374
2382
  };
2375
2383
 
package/src/client.ts CHANGED
@@ -29,10 +29,12 @@ import {
29
29
  axiosParamsSerializer,
30
30
  chatCodes,
31
31
  generateChannelTempCid,
32
+ invokeEventListener,
32
33
  isFunction,
33
34
  isOnline,
34
35
  isOwnUserBaseProperty,
35
36
  messageSetPagination,
37
+ noopLogger,
36
38
  normalizeQuerySort,
37
39
  randomId,
38
40
  retryInterval,
@@ -575,7 +577,7 @@ export class StreamChat {
575
577
  * channel: object
576
578
  * }
577
579
  */
578
- this.logger = isFunction(inputOptions.logger) ? inputOptions.logger : () => null;
580
+ this.logger = isFunction(inputOptions.logger) ? inputOptions.logger : noopLogger;
579
581
  this.recoverStateOnReconnect = this.options.recoverStateOnReconnect;
580
582
  this.threads = new ThreadManager({ client: this });
581
583
  this.polls = new PollManager({ client: this });
@@ -1700,20 +1702,15 @@ export class StreamChat {
1700
1702
  }
1701
1703
 
1702
1704
  _callClientListeners = (event: Event) => {
1703
- // eslint-disable-next-line @typescript-eslint/no-this-alias
1704
- const client = this;
1705
- // gather and call the listeners
1706
- const listeners: Array<(event: Event) => void> = [];
1707
- if (client.listeners.all) {
1708
- listeners.push(...client.listeners.all);
1709
- }
1710
- if (client.listeners[event.type]) {
1711
- listeners.push(...client.listeners[event.type]);
1712
- }
1705
+ // snapshot before dispatching: `on` pushes into these arrays in place, so a
1706
+ // listener that subscribes while handling an event must not be invoked for it
1707
+ const listeners = [
1708
+ ...(this.listeners.all ?? []),
1709
+ ...(this.listeners[event.type] ?? []),
1710
+ ];
1713
1711
 
1714
- // call the event and send it to the listeners
1715
1712
  for (const listener of listeners) {
1716
- listener(event);
1713
+ invokeEventListener(listener, event, this.logger);
1717
1714
  }
1718
1715
  };
1719
1716
 
@@ -10,7 +10,7 @@ import type {
10
10
  MarkReadOptions,
11
11
  } from '../types';
12
12
  import { type APIErrorResponse } from '../types';
13
- import { throttle, userHasReadReceipts } from '../utils';
13
+ import { throttle } from '../utils';
14
14
  import { isAPIError, isErrorRetryable } from '../errors';
15
15
 
16
16
  const MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD = 100 as const;
@@ -279,8 +279,6 @@ export class MessageDeliveryReporter {
279
279
  * @param options
280
280
  */
281
281
  public markRead = async (collection: Channel | Thread, options?: MarkReadOptions) => {
282
- if (!userHasReadReceipts(this.client)) return null;
283
-
284
282
  let result: EventAPIResponse | null = null;
285
283
  if (isChannel(collection)) {
286
284
  result = await collection.markAsReadRequest(options);
@@ -291,7 +289,10 @@ export class MessageDeliveryReporter {
291
289
  });
292
290
  }
293
291
 
294
- this.removeCandidateFor(collection);
292
+ // A read implies delivery, so the candidate is only dropped once the request actually went
293
+ // out. `markAsReadRequest` returns null for channels that do not track read state on the
294
+ // backend, and those candidates must survive for the next delivery report.
295
+ if (result) this.removeCandidateFor(collection);
295
296
  return result;
296
297
  };
297
298
 
package/src/utils.ts CHANGED
@@ -41,6 +41,36 @@ export function logChatPromiseExecution<T>(promise: Promise<T>, name: string) {
41
41
  });
42
42
  }
43
43
 
44
+ /**
45
+ * The default no-op client logger.
46
+ */
47
+ export const noopLogger: Logger = () => null;
48
+
49
+ const logError = <T>(logger: Logger, error: unknown, event: T) => {
50
+ const message = 'Unhandled error in event listener';
51
+ logger('error', message, { error, event });
52
+ if (logger === noopLogger) console.error(message, error, event);
53
+ };
54
+
55
+ /**
56
+ * Invokes a single event listener in isolation, so that a faulty listener cannot
57
+ * abort the dispatch loop or the bookkeeping that follows it.
58
+ */
59
+ export const invokeEventListener = <T>(
60
+ listener: (event: T) => unknown,
61
+ event: T,
62
+ logger: Logger,
63
+ ) => {
64
+ try {
65
+ const result = listener(event);
66
+ if (result && typeof (result as PromiseLike<unknown>).then === 'function') {
67
+ Promise.resolve(result).catch((error) => logError(logger, error, event));
68
+ }
69
+ } catch (error) {
70
+ logError(logger, error, event);
71
+ }
72
+ };
73
+
44
74
  export const sleep = (m: number): Promise<void> => new Promise((r) => setTimeout(r, m));
45
75
 
46
76
  export function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
@@ -126,13 +156,6 @@ export const channelTracksReadLocally = (channel?: Channel) =>
126
156
  !channelHasReadEvents(channel) &&
127
157
  !!channel?.getClient().options.isLocalUnreadCountEnabled;
128
158
 
129
- /**
130
- * userHasReadReceipts - Whether the current user allows read receipts, per their privacy settings.
131
- * Read receipts are treated as enabled unless the user has explicitly disabled them.
132
- */
133
- export const userHasReadReceipts = (client: StreamChat) =>
134
- client.user?.privacy_settings?.read_receipts?.enabled ?? true;
135
-
136
159
  export function addFileToFormData(
137
160
  uri: string | NodeJS.ReadableStream | Buffer | File,
138
161
  name?: string,