stream-chat 9.3.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.
@@ -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
  }
@@ -2356,9 +2356,11 @@ export declare class ErrorFromResponse<T> extends Error {
2356
2356
  status: ErrorFromResponse<T>['status'];
2357
2357
  });
2358
2358
  toJSON(): {
2359
- message: string;
2360
- stack: string | undefined;
2361
- 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;
2362
2364
  };
2363
2365
  }
2364
2366
  export type QueryPollsResponse = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stream-chat",
3
- "version": "9.3.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/store.ts CHANGED
@@ -2,17 +2,43 @@ 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
+ // aliases
6
+ export type RemovePreprocessor = Unsubscribe;
7
+ export type Preprocessor<T> = Handler<T>;
5
8
 
6
9
  export const isPatch = <T>(value: ValueOrPatch<T>): value is Patch<T> =>
7
10
  typeof value === 'function';
8
11
 
12
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
13
+ const noop = () => {};
14
+
9
15
  export class StateStore<T extends Record<string, unknown>> {
10
- private handlerSet = new Set<Handler<T>>();
16
+ protected handlers = new Set<Handler<T>>();
17
+ protected preprocessors = new Set<Preprocessor<T>>();
18
+
19
+ constructor(protected value: T) {}
11
20
 
12
- constructor(private value: T) {}
21
+ /**
22
+ * Allows merging two stores only if their keys differ otherwise there's no way to ensure the data type stability.
23
+ * @experimental
24
+ * This method is experimental and may change in future versions.
25
+ */
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ public merge<Q extends StateStore<any>>(
28
+ stateStore: Q extends StateStore<infer L>
29
+ ? Extract<keyof T, keyof L> extends never
30
+ ? Q
31
+ : never
32
+ : never,
33
+ ) {
34
+ return new MergedStateStore<T, Q extends StateStore<infer L> ? L : never>({
35
+ original: this,
36
+ merged: stateStore,
37
+ });
38
+ }
13
39
 
14
- public next = (newValueOrPatch: ValueOrPatch<T>): void => {
15
- // newValue (or patch output) should never be mutated previous value
40
+ public next(newValueOrPatch: ValueOrPatch<T>): void {
41
+ // newValue (or patch output) should never be a mutated previous value
16
42
  const newValue = isPatch(newValueOrPatch)
17
43
  ? newValueOrPatch(this.value)
18
44
  : newValueOrPatch;
@@ -20,24 +46,28 @@ export class StateStore<T extends Record<string, unknown>> {
20
46
  // do not notify subscribers if the value hasn't changed
21
47
  if (newValue === this.value) return;
22
48
 
49
+ this.preprocessors.forEach((preprocessor) => preprocessor(newValue, this.value));
50
+
23
51
  const oldValue = this.value;
24
52
  this.value = newValue;
25
53
 
26
- this.handlerSet.forEach((handler) => handler(this.value, oldValue));
27
- };
54
+ this.handlers.forEach((handler) => handler(this.value, oldValue));
55
+ }
28
56
 
29
57
  public partialNext = (partial: Partial<T>): void =>
30
58
  this.next((current) => ({ ...current, ...partial }));
31
59
 
32
- public getLatestValue = (): T => this.value;
60
+ public getLatestValue(): T {
61
+ return this.value;
62
+ }
33
63
 
34
- public subscribe = (handler: Handler<T>): Unsubscribe => {
64
+ public subscribe(handler: Handler<T>): Unsubscribe {
35
65
  handler(this.value, undefined);
36
- this.handlerSet.add(handler);
66
+ this.handlers.add(handler);
37
67
  return () => {
38
- this.handlerSet.delete(handler);
68
+ this.handlers.delete(handler);
39
69
  };
40
- };
70
+ }
41
71
 
42
72
  public subscribeWithSelector = <
43
73
  O extends Readonly<Record<string, unknown>> | Readonly<unknown[]>,
@@ -74,4 +104,200 @@ export class StateStore<T extends Record<string, unknown>> {
74
104
 
75
105
  return this.subscribe(wrappedHandler);
76
106
  };
107
+
108
+ /**
109
+ * Registers a preprocessor function that will be called before the state is updated.
110
+ *
111
+ * Preprocessors are invoked with the new and previous values whenever `next` or `partialNext` methods
112
+ * are called, allowing you to mutate or react to the new value before it is set. Preprocessors run in the
113
+ * order they were registered.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * const store = new StateStore<{ count: number; isMaxValue: bool; }>({ count: 0, isMaxValue: false });
118
+ *
119
+ * store.addPreprocessor((nextValue, prevValue) => {
120
+ * if (nextValue.count > 10) {
121
+ * nextValue.count = 10; // Clamp the value to a maximum of 10
122
+ * }
123
+ *
124
+ * if (nextValue.count === 10) {
125
+ * nextValue.isMaxValue = true; // Set isMaxValue to true if count is 10
126
+ * } else {
127
+ * nextValue.isMaxValue = false; // Reset isMaxValue otherwise
128
+ * }
129
+ * });
130
+ *
131
+ * store.partialNext({ count: 15 });
132
+ *
133
+ * store.getLatestValue(); // { count: 10, isMaxValue: true }
134
+ *
135
+ * store.partialNext({ count: 5 });
136
+ *
137
+ * store.getLatestValue(); // { count: 5, isMaxValue: false }
138
+ * ```
139
+ *
140
+ * @param preprocessor - The function to be called with the next and previous values before the state is updated.
141
+ * @returns A `RemovePreprocessor` function that removes the preprocessor when called.
142
+ */
143
+ public addPreprocessor(preprocessor: Preprocessor<T>): RemovePreprocessor {
144
+ this.preprocessors.add(preprocessor);
145
+
146
+ return () => {
147
+ this.preprocessors.delete(preprocessor);
148
+ };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Represents a merged state store that combines two separate state stores into one.
154
+ *
155
+ * The MergedStateStore allows combining two stores with non-overlapping keys.
156
+ * It extends StateStore with the combined type of both source stores.
157
+ * Changes to either the original or merged store will propagate to the combined store.
158
+ *
159
+ * Note: Direct mutations (next, partialNext, addPreprocessor) are disabled on the merged store.
160
+ * You should instead call these methods on the original or merged stores.
161
+ *
162
+ * @template O The type of the original state store
163
+ * @template M The type of the merged state store
164
+ *
165
+ * @experimental
166
+ * This class is experimental and may change in future versions.
167
+ */
168
+ export class MergedStateStore<
169
+ O extends Record<string, unknown>,
170
+ M extends Record<string, unknown>,
171
+ > extends StateStore<O & M> {
172
+ public readonly original: StateStore<O>;
173
+ public readonly merged: StateStore<M>;
174
+ private cachedOriginalValue: O;
175
+ private cachedMergedValue: M;
176
+
177
+ constructor({ original, merged }: { original: StateStore<O>; merged: StateStore<M> }) {
178
+ const originalValue = original.getLatestValue();
179
+ const mergedValue = merged.getLatestValue();
180
+
181
+ super({
182
+ ...originalValue,
183
+ ...mergedValue,
184
+ });
185
+
186
+ this.cachedOriginalValue = originalValue;
187
+ this.cachedMergedValue = mergedValue;
188
+
189
+ this.original = original;
190
+ this.merged = merged;
191
+ }
192
+
193
+ /**
194
+ * Subscribes to changes in the merged state store.
195
+ *
196
+ * This method extends the base subscribe functionality to handle the merged nature of this store:
197
+ * 1. The first subscriber triggers registration of helper subscribers that listen to both source stores
198
+ * 2. Changes from either source store are propagated to this merged store
199
+ * 3. Source store values are cached to prevent unnecessary updates
200
+ *
201
+ * When the first subscriber is added, the method sets up listeners on both original and merged stores.
202
+ * These listeners update the combined store value whenever either source store changes.
203
+ * All subscriptions (helpers and the actual handler) are tracked so they can be properly cleaned up.
204
+ *
205
+ * @param handler - The callback function that will be executed when the state changes
206
+ * @returns An unsubscribe function that, when called, removes the subscription and any helper subscriptions
207
+ */
208
+ public subscribe(handler: Handler<O & M>) {
209
+ const unsubscribeFunctions: Unsubscribe[] = [];
210
+
211
+ // first subscriber will also register helpers which listen to changes of the
212
+ // "original" and "merged" stores, combined outputs will be emitted through super.next
213
+ // whenever cached values do not equal (always apart from the initial subscription)
214
+ // since the actual handler subscription is registered after helpers, the actual
215
+ // handler will run only once
216
+ if (!this.handlers.size) {
217
+ const base = (nextValue: O | M) => {
218
+ super.next((currentValue) => ({
219
+ ...currentValue,
220
+ ...nextValue,
221
+ }));
222
+ };
223
+
224
+ unsubscribeFunctions.push(
225
+ this.original.subscribe((nextValue) => {
226
+ if (nextValue === this.cachedOriginalValue) return;
227
+ this.cachedOriginalValue = nextValue;
228
+ base(nextValue);
229
+ }),
230
+ this.merged.subscribe((nextValue) => {
231
+ if (nextValue === this.cachedMergedValue) return;
232
+ this.cachedMergedValue = nextValue;
233
+ base(nextValue);
234
+ }),
235
+ );
236
+ }
237
+
238
+ unsubscribeFunctions.push(super.subscribe(handler));
239
+
240
+ return () => {
241
+ unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Retrieves the latest combined state from both original and merged stores.
247
+ *
248
+ * This method extends the base getLatestValue functionality to ensure the merged store
249
+ * remains in sync with its source stores even when there are no active subscribers.
250
+ *
251
+ * When there are no handlers registered, the method:
252
+ * 1. Fetches the latest values from both source stores
253
+ * 2. Compares them with the cached values to detect changes
254
+ * 3. If changes are detected, updates the internal value and caches
255
+ * the new source values to maintain consistency
256
+ *
257
+ * This approach ensures that calling getLatestValue() always returns the most
258
+ * up-to-date combined state, even if the merged store hasn't been actively
259
+ * receiving updates through subscriptions.
260
+ *
261
+ * @returns The latest combined state from both original and merged stores
262
+ */
263
+ public getLatestValue() {
264
+ // if there are no handlers registered to MergedStore then the local value might be out-of-sync
265
+ // pull latest and compare against cached - if they differ, cache latest and produce new combined
266
+ if (!this.handlers.size) {
267
+ const originalValue = this.original.getLatestValue();
268
+ const mergedValue = this.merged.getLatestValue();
269
+
270
+ if (
271
+ originalValue !== this.cachedOriginalValue ||
272
+ mergedValue !== this.cachedMergedValue
273
+ ) {
274
+ this.value = {
275
+ ...originalValue,
276
+ ...mergedValue,
277
+ };
278
+ this.cachedMergedValue = mergedValue;
279
+ this.cachedOriginalValue = originalValue;
280
+ }
281
+ }
282
+
283
+ return super.getLatestValue();
284
+ }
285
+
286
+ // override original methods and "disable" them
287
+ public next = () => {
288
+ console.warn(
289
+ `${MergedStateStore.name}.next is disabled, call original.next or merged.next instead`,
290
+ );
291
+ };
292
+ public partialNext = () => {
293
+ console.warn(
294
+ `${MergedStateStore.name}.partialNext is disabled, call original.partialNext or merged.partialNext instead`,
295
+ );
296
+ };
297
+ public addPreprocessor() {
298
+ console.warn(
299
+ `${MergedStateStore.name}.addPreprocessor is disabled, call original.addPreprocessor or merged.addPreprocessor instead`,
300
+ );
301
+ return noop;
302
+ }
77
303
  }
package/src/types.ts CHANGED
@@ -3261,7 +3261,9 @@ export class ErrorFromResponse<T> extends Error {
3261
3261
  message: `(${joinable.join(', ')}) - ${this.message}`,
3262
3262
  stack: this.stack,
3263
3263
  name: this.name,
3264
- };
3264
+ code: this.code,
3265
+ status: this.status,
3266
+ } as const;
3265
3267
  }
3266
3268
  }
3267
3269