stream-chat 9.2.0 → 9.4.0

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.
@@ -139,7 +139,7 @@ export declare class Channel {
139
139
  sendReaction(messageID: string, reaction: Reaction, options?: {
140
140
  enforce_unique?: boolean;
141
141
  skip_push?: boolean;
142
- }): Promise<ReactionAPIResponse | undefined>;
142
+ }): Promise<ReactionAPIResponse>;
143
143
  /**
144
144
  * sendReaction - Send a reaction about a message
145
145
  *
@@ -152,10 +152,8 @@ export declare class Channel {
152
152
  _sendReaction(messageID: string, reaction: Reaction, options?: {
153
153
  enforce_unique?: boolean;
154
154
  skip_push?: boolean;
155
- }): Promise<ReactionAPIResponse | undefined>;
156
- deleteReaction(messageID: string, reactionType: string, user_id?: string): Promise<(APIResponse & {
157
- message: MessageResponse;
158
- }) | undefined>;
155
+ }): Promise<ReactionAPIResponse>;
156
+ deleteReaction(messageID: string, reactionType: string, user_id?: string): Promise<ReactionAPIResponse>;
159
157
  /**
160
158
  * deleteReaction - Delete a reaction by user and type
161
159
  *
@@ -227,7 +227,24 @@ export declare class StreamChat {
227
227
  'data_template': 'data handlebars template',
228
228
  'apn_template': 'apn notification handlebars template under v2'
229
229
  },
230
- 'webhook_url': 'https://acme.com/my/awesome/webhook/'
230
+ 'webhook_url': 'https://acme.com/my/awesome/webhook/',
231
+ 'event_hooks': [
232
+ {
233
+ 'hook_type': 'webhook',
234
+ 'enabled': true,
235
+ 'event_types': ['message.new'],
236
+ 'webhook_url': 'https://acme.com/my/awesome/webhook/'
237
+ },
238
+ {
239
+ 'hook_type': 'sqs',
240
+ 'enabled': true,
241
+ 'event_types': ['message.new'],
242
+ 'sqs_url': 'https://sqs.us-east-1.amazonaws.com/1234567890/my-queue',
243
+ 'sqs_auth_type': 'key',
244
+ 'sqs_key': 'my-access-key',
245
+ 'sqs_secret': 'my-secret-key'
246
+ }
247
+ ]
231
248
  }
232
249
  */
233
250
  updateAppSettings(options: AppSettings): Promise<APIResponse>;
@@ -1103,9 +1120,9 @@ export declare class StreamChat {
1103
1120
  partialUpdateMessage(id: string, partialMessageObject: PartialMessageUpdate, partialUserOrUserId?: string | {
1104
1121
  id: string;
1105
1122
  }, options?: UpdateMessageOptions): Promise<UpdateMessageAPIResponse>;
1106
- deleteMessage(messageID: string, hardDelete?: boolean): Promise<(APIResponse & {
1123
+ deleteMessage(messageID: string, hardDelete?: boolean): Promise<APIResponse & {
1107
1124
  message: MessageResponse;
1108
- }) | undefined>;
1125
+ }>;
1109
1126
  _deleteMessage(messageID: string, hardDelete?: boolean): Promise<APIResponse & {
1110
1127
  message: MessageResponse;
1111
1128
  }>;
@@ -522,11 +522,9 @@ export declare abstract class AbstractOfflineDB implements OfflineDBApi {
522
522
  * It will return the response from the execution if it succeeded.
523
523
  * @param task - the pending task we want to execute
524
524
  */
525
- queueTask: ({ task }: {
525
+ queueTask: <T>({ task }: {
526
526
  task: PendingTask;
527
- }) => Promise<(import("..").APIResponse & {
528
- message: import("..").MessageResponse;
529
- }) | undefined>;
527
+ }) => Promise<T>;
530
528
  /**
531
529
  * A utility method that determines if a failed task should be added to the
532
530
  * queue based on its error.
@@ -1,4 +1,4 @@
1
- import type { AppSettingsAPIResponse, ChannelAPIResponse, ChannelFilters, ChannelMemberResponse, ChannelResponse, ChannelSort, LocalMessage, Message, MessageResponse, PollResponse, ReactionFilters, ReactionResponse, ReactionSort, ReadResponse } from '../types';
1
+ import type { AppSettingsAPIResponse, ChannelAPIResponse, ChannelFilters, ChannelMemberResponse, ChannelResponse, ChannelSort, LocalMessage, MessageResponse, PollResponse, ReactionFilters, ReactionResponse, ReactionSort, ReadResponse } from '../types';
2
2
  import type { Channel } from '../channel';
3
3
  import type { StreamChat } from '../client';
4
4
  export type PrepareBatchDBQueries = [string] | [string, Array<unknown> | Array<Array<unknown>>];
@@ -322,6 +322,16 @@ export type PendingTask = {
322
322
  payload: Parameters<Channel['sendMessage']>;
323
323
  type: PendingTaskTypes['sendMessage'];
324
324
  });
325
- export type PendingTaskExtraData = {
326
- message?: Message;
327
- };
325
+ export type OfflineErrorType = 'connection:lost';
326
+ export declare class OfflineError extends Error {
327
+ type: OfflineErrorType;
328
+ name: string;
329
+ constructor(message: string, { type, }: {
330
+ type: OfflineError['type'];
331
+ });
332
+ toJSON(): {
333
+ message: string;
334
+ stack: string | undefined;
335
+ name: string;
336
+ };
337
+ }
@@ -2,14 +2,123 @@ export type Patch<T> = (value: T) => T;
2
2
  export type ValueOrPatch<T> = T | Patch<T>;
3
3
  export type Handler<T> = (nextValue: T, previousValue: T | undefined) => void;
4
4
  export type Unsubscribe = () => void;
5
+ export type RemovePreprocessor = Unsubscribe;
6
+ export type Preprocessor<T> = Handler<T>;
5
7
  export declare const isPatch: <T>(value: ValueOrPatch<T>) => value is Patch<T>;
6
8
  export declare class StateStore<T extends Record<string, unknown>> {
7
- private value;
8
- private handlerSet;
9
+ protected value: T;
10
+ protected handlers: Set<Handler<T>>;
11
+ protected preprocessors: Set<Preprocessor<T>>;
9
12
  constructor(value: T);
10
- next: (newValueOrPatch: ValueOrPatch<T>) => void;
13
+ /**
14
+ * Allows merging two stores only if their keys differ otherwise there's no way to ensure the data type stability.
15
+ * @experimental
16
+ * This method is experimental and may change in future versions.
17
+ */
18
+ merge<Q extends StateStore<any>>(stateStore: Q extends StateStore<infer L> ? Extract<keyof T, keyof L> extends never ? Q : never : never): MergedStateStore<T, Q extends StateStore<infer L extends Record<string, unknown>> ? L : never>;
19
+ next(newValueOrPatch: ValueOrPatch<T>): void;
11
20
  partialNext: (partial: Partial<T>) => void;
12
- getLatestValue: () => T;
13
- subscribe: (handler: Handler<T>) => Unsubscribe;
21
+ getLatestValue(): T;
22
+ subscribe(handler: Handler<T>): Unsubscribe;
14
23
  subscribeWithSelector: <O extends Readonly<Record<string, unknown>> | Readonly<unknown[]>>(selector: (nextValue: T) => O, handler: Handler<O>) => Unsubscribe;
24
+ /**
25
+ * Registers a preprocessor function that will be called before the state is updated.
26
+ *
27
+ * Preprocessors are invoked with the new and previous values whenever `next` or `partialNext` methods
28
+ * are called, allowing you to mutate or react to the new value before it is set. Preprocessors run in the
29
+ * order they were registered.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const store = new StateStore<{ count: number; isMaxValue: bool; }>({ count: 0, isMaxValue: false });
34
+ *
35
+ * store.addPreprocessor((nextValue, prevValue) => {
36
+ * if (nextValue.count > 10) {
37
+ * nextValue.count = 10; // Clamp the value to a maximum of 10
38
+ * }
39
+ *
40
+ * if (nextValue.count === 10) {
41
+ * nextValue.isMaxValue = true; // Set isMaxValue to true if count is 10
42
+ * } else {
43
+ * nextValue.isMaxValue = false; // Reset isMaxValue otherwise
44
+ * }
45
+ * });
46
+ *
47
+ * store.partialNext({ count: 15 });
48
+ *
49
+ * store.getLatestValue(); // { count: 10, isMaxValue: true }
50
+ *
51
+ * store.partialNext({ count: 5 });
52
+ *
53
+ * store.getLatestValue(); // { count: 5, isMaxValue: false }
54
+ * ```
55
+ *
56
+ * @param preprocessor - The function to be called with the next and previous values before the state is updated.
57
+ * @returns A `RemovePreprocessor` function that removes the preprocessor when called.
58
+ */
59
+ addPreprocessor(preprocessor: Preprocessor<T>): RemovePreprocessor;
60
+ }
61
+ /**
62
+ * Represents a merged state store that combines two separate state stores into one.
63
+ *
64
+ * The MergedStateStore allows combining two stores with non-overlapping keys.
65
+ * It extends StateStore with the combined type of both source stores.
66
+ * Changes to either the original or merged store will propagate to the combined store.
67
+ *
68
+ * Note: Direct mutations (next, partialNext, addPreprocessor) are disabled on the merged store.
69
+ * You should instead call these methods on the original or merged stores.
70
+ *
71
+ * @template O The type of the original state store
72
+ * @template M The type of the merged state store
73
+ *
74
+ * @experimental
75
+ * This class is experimental and may change in future versions.
76
+ */
77
+ export declare class MergedStateStore<O extends Record<string, unknown>, M extends Record<string, unknown>> extends StateStore<O & M> {
78
+ readonly original: StateStore<O>;
79
+ readonly merged: StateStore<M>;
80
+ private cachedOriginalValue;
81
+ private cachedMergedValue;
82
+ constructor({ original, merged }: {
83
+ original: StateStore<O>;
84
+ merged: StateStore<M>;
85
+ });
86
+ /**
87
+ * Subscribes to changes in the merged state store.
88
+ *
89
+ * This method extends the base subscribe functionality to handle the merged nature of this store:
90
+ * 1. The first subscriber triggers registration of helper subscribers that listen to both source stores
91
+ * 2. Changes from either source store are propagated to this merged store
92
+ * 3. Source store values are cached to prevent unnecessary updates
93
+ *
94
+ * When the first subscriber is added, the method sets up listeners on both original and merged stores.
95
+ * These listeners update the combined store value whenever either source store changes.
96
+ * All subscriptions (helpers and the actual handler) are tracked so they can be properly cleaned up.
97
+ *
98
+ * @param handler - The callback function that will be executed when the state changes
99
+ * @returns An unsubscribe function that, when called, removes the subscription and any helper subscriptions
100
+ */
101
+ subscribe(handler: Handler<O & M>): () => void;
102
+ /**
103
+ * Retrieves the latest combined state from both original and merged stores.
104
+ *
105
+ * This method extends the base getLatestValue functionality to ensure the merged store
106
+ * remains in sync with its source stores even when there are no active subscribers.
107
+ *
108
+ * When there are no handlers registered, the method:
109
+ * 1. Fetches the latest values from both source stores
110
+ * 2. Compares them with the cached values to detect changes
111
+ * 3. If changes are detected, updates the internal value and caches
112
+ * the new source values to maintain consistency
113
+ *
114
+ * This approach ensures that calling getLatestValue() always returns the most
115
+ * up-to-date combined state, even if the merged store hasn't been actively
116
+ * receiving updates through subscriptions.
117
+ *
118
+ * @returns The latest combined state from both original and merged stores
119
+ */
120
+ getLatestValue(): O & M;
121
+ next: () => void;
122
+ partialNext: () => void;
123
+ addPreprocessor(): () => void;
15
124
  }
@@ -90,6 +90,7 @@ export type AppSettingsAPIResponse = APIResponse & {
90
90
  disable_auth_checks?: boolean;
91
91
  disable_permissions_checks?: boolean;
92
92
  enforce_unique_usernames?: 'no' | 'app' | 'team';
93
+ event_hooks?: Array<EventHook>;
93
94
  file_upload_config?: FileUploadConfig;
94
95
  geofences?: Array<{
95
96
  country_codes: Array<string>;
@@ -1698,6 +1699,7 @@ export type AppSettings = {
1698
1699
  disable_auth_checks?: boolean;
1699
1700
  disable_permissions_checks?: boolean;
1700
1701
  enforce_unique_usernames?: 'no' | 'app' | 'team';
1702
+ event_hooks?: Array<EventHook> | null;
1701
1703
  file_upload_config?: FileUploadConfig;
1702
1704
  firebase_config?: {
1703
1705
  apn_template?: string;
@@ -2354,9 +2356,11 @@ export declare class ErrorFromResponse<T> extends Error {
2354
2356
  status: ErrorFromResponse<T>['status'];
2355
2357
  });
2356
2358
  toJSON(): {
2357
- message: string;
2358
- stack: string | undefined;
2359
- name: string;
2359
+ readonly message: `(${string}) - ${string}`;
2360
+ readonly stack: string | undefined;
2361
+ readonly name: string;
2362
+ readonly code: number | null;
2363
+ readonly status: number;
2360
2364
  };
2361
2365
  }
2362
2366
  export type QueryPollsResponse = {
@@ -2905,4 +2909,26 @@ export type ThreadFilters = QueryFilters<{
2905
2909
  } & {
2906
2910
  last_message_at?: RequireOnlyOne<Pick<QueryFilter<ThreadResponse['last_message_at']>, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> | PrimitiveFilter<ThreadResponse['last_message_at']>;
2907
2911
  }>;
2912
+ export type HookType = 'webhook' | 'sqs' | 'sns';
2913
+ export type EventHook = {
2914
+ id?: string;
2915
+ hook_type?: HookType;
2916
+ enabled?: boolean;
2917
+ event_types?: Array<string>;
2918
+ webhook_url?: string;
2919
+ sqs_queue_url?: string;
2920
+ sqs_region?: string;
2921
+ sqs_auth_type?: string;
2922
+ sqs_key?: string;
2923
+ sqs_secret?: string;
2924
+ sqs_role_arn?: string;
2925
+ sns_topic_arn?: string;
2926
+ sns_region?: string;
2927
+ sns_auth_type?: string;
2928
+ sns_key?: string;
2929
+ sns_secret?: string;
2930
+ sns_role_arn?: string;
2931
+ created_at?: string;
2932
+ updated_at?: string;
2933
+ };
2908
2934
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.2.0",
3
+ "version": "9.4.0",
4
4
  "description": "JS SDK for the Stream Chat API",
5
5
  "homepage": "https://getstream.io/chat/",
6
6
  "author": {
@@ -68,6 +68,7 @@
68
68
  "@types/base64-js": "^1.3.0",
69
69
  "@types/node": "^22.15.21",
70
70
  "@types/sinon": "^10.0.6",
71
+ "@vitest/coverage-v8": "3.1.4",
71
72
  "concurrently": "^9.1.2",
72
73
  "conventional-changelog-conventionalcommits": "^8.0.0",
73
74
  "dotenv": "^8.2.0",
@@ -77,7 +78,6 @@
77
78
  "globals": "^16.0.0",
78
79
  "husky": "^9.1.7",
79
80
  "lint-staged": "^15.2.2",
80
- "nyc": "^15.1.0",
81
81
  "prettier": "^3.5.3",
82
82
  "semantic-release": "^24.2.3",
83
83
  "sinon": "^12.0.1",
@@ -98,8 +98,8 @@
98
98
  "test": "yarn test-unit",
99
99
  "testwatch": "NODE_ENV=test nodemon ./node_modules/.bin/mocha --timeout 20000 --require test-entry.js test/test.js",
100
100
  "test-types": "node test/typescript/index.js && tsc --esModuleInterop true --noEmit true --strictNullChecks true --noImplicitAny true --strict true test/typescript/*.ts",
101
- "test-unit": "vitest test/unit/* --run",
102
- "test-coverage": "nyc yarn test-unit",
101
+ "test-unit": "vitest",
102
+ "test-coverage": "vitest run --coverage",
103
103
  "fix-staged": "lint-staged --config .lintstagedrc.fix.json --concurrent 1",
104
104
  "semantic-release": "semantic-release",
105
105
  "prepare": "husky; yarn run build"
package/src/channel.ts CHANGED
@@ -207,7 +207,7 @@ export class Channel {
207
207
  if (offlineDb) {
208
208
  const messageId = message.id;
209
209
  if (messageId) {
210
- return (await offlineDb.queueTask({
210
+ return await offlineDb.queueTask<SendMessageAPIResponse>({
211
211
  task: {
212
212
  channelId: this.id as string,
213
213
  channelType: this.type,
@@ -215,7 +215,7 @@ export class Channel {
215
215
  payload: [message, options],
216
216
  type: 'send-message',
217
217
  },
218
- })) as SendMessageAPIResponse;
218
+ });
219
219
  }
220
220
  }
221
221
  } catch (error) {
@@ -410,7 +410,7 @@ export class Channel {
410
410
  messageID: string,
411
411
  reaction: Reaction,
412
412
  options?: { enforce_unique?: boolean; skip_push?: boolean },
413
- ): Promise<ReactionAPIResponse | undefined> {
413
+ ) {
414
414
  if (!messageID) {
415
415
  throw Error(`Message id is missing`);
416
416
  }
@@ -454,7 +454,7 @@ export class Channel {
454
454
  messageID: string,
455
455
  reaction: Reaction,
456
456
  options?: { enforce_unique?: boolean; skip_push?: boolean },
457
- ): Promise<ReactionAPIResponse | undefined> {
457
+ ) {
458
458
  if (!messageID) {
459
459
  throw Error(`Message id is missing`);
460
460
  }
@@ -498,7 +498,7 @@ export class Channel {
498
498
  });
499
499
  }
500
500
 
501
- return await offlineDb.queueTask({
501
+ return await offlineDb.queueTask<ReactionAPIResponse>({
502
502
  task: {
503
503
  channelId: this.id as string,
504
504
  channelType: this.type,
package/src/client.ts CHANGED
@@ -777,7 +777,24 @@ export class StreamChat {
777
777
  'data_template': 'data handlebars template',
778
778
  'apn_template': 'apn notification handlebars template under v2'
779
779
  },
780
- 'webhook_url': 'https://acme.com/my/awesome/webhook/'
780
+ 'webhook_url': 'https://acme.com/my/awesome/webhook/',
781
+ 'event_hooks': [
782
+ {
783
+ 'hook_type': 'webhook',
784
+ 'enabled': true,
785
+ 'event_types': ['message.new'],
786
+ 'webhook_url': 'https://acme.com/my/awesome/webhook/'
787
+ },
788
+ {
789
+ 'hook_type': 'sqs',
790
+ 'enabled': true,
791
+ 'event_types': ['message.new'],
792
+ 'sqs_url': 'https://sqs.us-east-1.amazonaws.com/1234567890/my-queue',
793
+ 'sqs_auth_type': 'key',
794
+ 'sqs_key': 'my-access-key',
795
+ 'sqs_secret': 'my-secret-key'
796
+ }
797
+ ]
781
798
  }
782
799
  */
783
800
  async updateAppSettings(options: AppSettings) {
@@ -3019,13 +3036,15 @@ export class StreamChat {
3019
3036
  } else {
3020
3037
  await this.offlineDb.softDeleteMessage({ id: messageID });
3021
3038
  }
3022
- return await this.offlineDb.queueTask({
3023
- task: {
3024
- messageId: messageID,
3025
- payload: [messageID, hardDelete],
3026
- type: 'delete-message',
3039
+ return await this.offlineDb.queueTask<APIResponse & { message: MessageResponse }>(
3040
+ {
3041
+ task: {
3042
+ messageId: messageID,
3043
+ payload: [messageID, hardDelete],
3044
+ type: 'delete-message',
3045
+ },
3027
3046
  },
3028
- });
3047
+ );
3029
3048
  }
3030
3049
  } catch (error) {
3031
3050
  this.logger('error', `offlineDb:deleteMessage`, {
@@ -6,6 +6,7 @@ import type {
6
6
  PendingTask,
7
7
  PrepareBatchDBQueries,
8
8
  } from './types';
9
+ import { OfflineError } from './types';
9
10
  import type { StreamChat } from '../client';
10
11
  import type { AxiosError } from 'axios';
11
12
  import { OfflineDBSyncManager } from './offline_sync_manager';
@@ -986,22 +987,24 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
986
987
  * It will return the response from the execution if it succeeded.
987
988
  * @param task - the pending task we want to execute
988
989
  */
989
- public queueTask = async ({ task }: { task: PendingTask }) => {
990
- let response;
991
- try {
990
+ public queueTask = async <T>({ task }: { task: PendingTask }): Promise<T> => {
991
+ const attemptTaskExecution = async () => {
992
992
  if (!this.client.wsConnection?.isHealthy) {
993
- await this.addPendingTask(task);
994
- return;
993
+ throw new OfflineError(
994
+ 'Cannot execute task because the connection has been lost.',
995
+ { type: 'connection:lost' },
996
+ );
995
997
  }
996
- response = await this.executeTask({ task });
998
+ return (await this.executeTask({ task })) as T;
999
+ };
1000
+ try {
1001
+ return await attemptTaskExecution();
997
1002
  } catch (e) {
998
1003
  if (!this.shouldSkipQueueingTask(e as AxiosError<APIErrorResponse>)) {
999
1004
  await this.addPendingTask(task);
1000
- throw e;
1001
1005
  }
1006
+ throw e;
1002
1007
  }
1003
-
1004
- return response;
1005
1008
  };
1006
1009
 
1007
1010
  /**
@@ -6,7 +6,6 @@ import type {
6
6
  ChannelResponse,
7
7
  ChannelSort,
8
8
  LocalMessage,
9
- Message,
10
9
  MessageResponse,
11
10
  PollResponse,
12
11
  ReactionFilters,
@@ -398,6 +397,31 @@ export type PendingTask = {
398
397
  }
399
398
  );
400
399
 
401
- export type PendingTaskExtraData = {
402
- message?: Message;
403
- };
400
+ export type OfflineErrorType = 'connection:lost';
401
+
402
+ export class OfflineError extends Error {
403
+ public type: OfflineErrorType;
404
+ public name = 'OfflineError';
405
+
406
+ constructor(
407
+ message: string,
408
+ {
409
+ type,
410
+ }: {
411
+ type: OfflineError['type'];
412
+ },
413
+ ) {
414
+ super(message);
415
+ this.type = type;
416
+ }
417
+
418
+ // Vitest helper (serialized errors are too large to read)
419
+ // https://github.com/vitest-dev/vitest/blob/v3.1.3/packages/utils/src/error.ts#L60-L62
420
+ toJSON() {
421
+ return {
422
+ message: `${this.type} - ${this.message}`,
423
+ stack: this.stack,
424
+ name: this.name,
425
+ };
426
+ }
427
+ }