@stream-io/feeds-client 0.2.18 → 0.2.19

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 (84) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/index.js +94 -25
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/react-bindings.js +26 -55
  5. package/dist/cjs/react-bindings.js.map +1 -1
  6. package/dist/es/index.mjs +86 -17
  7. package/dist/es/index.mjs.map +1 -1
  8. package/dist/es/react-bindings.mjs +19 -48
  9. package/dist/es/react-bindings.mjs.map +1 -1
  10. package/dist/{index--koeDtxd.js → feeds-client-C09giTf1.js} +177 -79
  11. package/dist/feeds-client-C09giTf1.js.map +1 -0
  12. package/dist/{index-Zde8UE5f.mjs → feeds-client-CFadXO-B.mjs} +190 -92
  13. package/dist/feeds-client-CFadXO-B.mjs.map +1 -0
  14. package/dist/tsconfig.tsbuildinfo +1 -1
  15. package/dist/types/bindings/react/hooks/feed-state-hooks/useOwnCapabilities.d.ts +2 -32
  16. package/dist/types/bindings/react/hooks/feed-state-hooks/useOwnCapabilities.d.ts.map +1 -1
  17. package/dist/types/common/real-time/event-models.d.ts +7 -2
  18. package/dist/types/common/real-time/event-models.d.ts.map +1 -1
  19. package/dist/types/common/types.d.ts +1 -0
  20. package/dist/types/common/types.d.ts.map +1 -1
  21. package/dist/types/feed/event-handlers/activity/handle-activity-added.d.ts.map +1 -1
  22. package/dist/types/feed/event-handlers/activity/handle-activity-updated.d.ts.map +1 -1
  23. package/dist/types/feed/feed.d.ts +1 -1
  24. package/dist/types/feed/feed.d.ts.map +1 -1
  25. package/dist/types/feeds-client/feeds-client.d.ts +9 -1
  26. package/dist/types/feeds-client/feeds-client.d.ts.map +1 -1
  27. package/dist/types/utils/throttling/index.d.ts +3 -0
  28. package/dist/types/utils/throttling/index.d.ts.map +1 -0
  29. package/dist/types/utils/throttling/throttle.d.ts +34 -0
  30. package/dist/types/utils/throttling/throttle.d.ts.map +1 -0
  31. package/dist/types/utils/throttling/throttled-get-batched-own-capabilities.d.ts +14 -0
  32. package/dist/types/utils/throttling/throttled-get-batched-own-capabilities.d.ts.map +1 -0
  33. package/package.json +7 -3
  34. package/react-bindings.d.ts +11 -0
  35. package/react-bindings.js +7 -0
  36. package/react-bindings.mjs +11 -0
  37. package/src/bindings/react/hooks/feed-state-hooks/useOwnCapabilities.ts +21 -73
  38. package/src/common/real-time/event-models.ts +8 -2
  39. package/src/common/types.ts +1 -0
  40. package/src/feed/event-handlers/activity/handle-activity-added.ts +9 -1
  41. package/src/feed/event-handlers/activity/handle-activity-updated.ts +4 -0
  42. package/src/feed/feed.ts +18 -3
  43. package/src/feeds-client/feeds-client.ts +106 -3
  44. package/src/utils/throttling/index.ts +2 -0
  45. package/src/utils/throttling/throttle.ts +123 -0
  46. package/src/utils/throttling/throttled-get-batched-own-capabilities.ts +42 -0
  47. package/dist/index--koeDtxd.js.map +0 -1
  48. package/dist/index-Zde8UE5f.mjs.map +0 -1
  49. package/src/feed/event-handlers/activity/activity-marked-utils.test.ts +0 -208
  50. package/src/feed/event-handlers/activity/activity-reaction-utils.test.ts +0 -371
  51. package/src/feed/event-handlers/activity/handle-activity-added.test.ts +0 -97
  52. package/src/feed/event-handlers/activity/handle-activity-deleted.test.ts +0 -117
  53. package/src/feed/event-handlers/activity/handle-activity-pinned.test.ts +0 -60
  54. package/src/feed/event-handlers/activity/handle-activity-reaction-added.test.ts +0 -257
  55. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.test.ts +0 -317
  56. package/src/feed/event-handlers/activity/handle-activity-reaction-updated.test.ts +0 -282
  57. package/src/feed/event-handlers/activity/handle-activity-unpinned.test.ts +0 -95
  58. package/src/feed/event-handlers/activity/handle-activity-updated.test.ts +0 -245
  59. package/src/feed/event-handlers/add-aggregated-activities-to-state.test.ts +0 -510
  60. package/src/feed/event-handlers/bookmark/bookmark-utils.test.ts +0 -521
  61. package/src/feed/event-handlers/bookmark/handle-bookmark-added.test.ts +0 -178
  62. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.test.ts +0 -188
  63. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.test.ts +0 -196
  64. package/src/feed/event-handlers/comment/handle-comment-added.test.ts +0 -271
  65. package/src/feed/event-handlers/comment/handle-comment-deleted.test.ts +0 -255
  66. package/src/feed/event-handlers/comment/handle-comment-reaction-added.test.ts +0 -329
  67. package/src/feed/event-handlers/comment/handle-comment-reaction-deleted.test.ts +0 -343
  68. package/src/feed/event-handlers/comment/handle-comment-reaction-updated.test.ts +0 -350
  69. package/src/feed/event-handlers/comment/handle-comment-updated.test.ts +0 -267
  70. package/src/feed/event-handlers/comment/utils/update-comment-count.test.ts +0 -322
  71. package/src/feed/event-handlers/feed-member/handle-feed-member-added.test.ts +0 -75
  72. package/src/feed/event-handlers/feed-member/handle-feed-member-removed.test.ts +0 -82
  73. package/src/feed/event-handlers/feed-member/handle-feed-member-updated.test.ts +0 -84
  74. package/src/feed/event-handlers/follow/follow-state-update-queue.test.ts +0 -219
  75. package/src/feed/event-handlers/follow/handle-follow-created.test.ts +0 -250
  76. package/src/feed/event-handlers/follow/handle-follow-deleted.test.ts +0 -268
  77. package/src/feed/event-handlers/follow/handle-follow-updated.test.ts +0 -131
  78. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts +0 -182
  79. package/src/feed/event-handlers/story-feeds/handle-story-feeds-updated.test.ts +0 -45
  80. package/src/feed/feed.test.ts +0 -90
  81. package/src/feeds-client/event-handlers/user/handle-user-updated.test.ts +0 -53
  82. package/src/utils/event-triggered-by-connected-user.test.ts +0 -73
  83. package/src/utils/state-update-queue.test.ts +0 -129
  84. package/src/utils/unique-array-merge.test.ts +0 -179
@@ -14,6 +14,7 @@ import type {
14
14
  FollowBatchRequest,
15
15
  FollowRequest,
16
16
  ImageUploadRequest,
17
+ OwnCapabilitiesBatchRequest,
17
18
  OwnUser,
18
19
  PollResponse,
19
20
  PollVotesResponse,
@@ -66,15 +67,26 @@ import {
66
67
  handleWatchStopped,
67
68
  } from '../feed';
68
69
  import { handleUserUpdated } from './event-handlers';
69
- import type { SyncFailure } from '../common/real-time/event-models';
70
- import { UnhandledErrorType } from '../common/real-time/event-models';
70
+ import {
71
+ type SyncFailure,
72
+ UnhandledErrorType,
73
+ } from '../common/real-time/event-models';
71
74
  import { updateCommentCount } from '../feed/event-handlers/comment/utils';
72
75
  import { configureLoggers } from '../utils';
73
76
  import { handleCommentReactionUpdated } from '../feed/event-handlers/comment/handle-comment-reaction-updated';
77
+ import {
78
+ throttle,
79
+ DEFAULT_BATCH_OWN_CAPABILITIES_THROTTLING_INTERVAL,
80
+ type GetBatchedOwnCapabilitiesThrottledCallback,
81
+ queueBatchedOwnCapabilities,
82
+ type ThrottledGetBatchedOwnCapabilities,
83
+ clearQueuedFeeds,
84
+ } from '../utils/throttling';
74
85
 
75
86
  export type FeedsClientState = {
76
87
  connected_user: OwnUser | undefined;
77
88
  is_ws_connection_healthy: boolean;
89
+ own_capabilities_by_fid: Record<string, FeedResponse['own_capabilities']>;
78
90
  };
79
91
 
80
92
  type FID = string;
@@ -97,6 +109,10 @@ export class FeedsClient extends FeedsApi {
97
109
 
98
110
  private healthyConnectionChangedEventCount = 0;
99
111
 
112
+ protected throttledGetBatchOwnCapabilities!: ThrottledGetBatchedOwnCapabilities;
113
+ private cancelGetBatchOwnCapabilitiesTimer!: () => void;
114
+ private query_batch_own_capabilties_throttling_interval!: number;
115
+
100
116
  constructor(apiKey: string, options?: FeedsClientOptions) {
101
117
  const tokenManager = new TokenManager();
102
118
  const connectionIdManager = new ConnectionIdManager();
@@ -110,12 +126,17 @@ export class FeedsClient extends FeedsApi {
110
126
  this.state = new StateStore<FeedsClientState>({
111
127
  connected_user: undefined,
112
128
  is_ws_connection_healthy: false,
129
+ own_capabilities_by_fid: {},
113
130
  });
114
131
  this.moderation = new ModerationClient(apiClient);
115
132
  this.tokenManager = tokenManager;
116
133
  this.connectionIdManager = connectionIdManager;
117
134
  this.polls_by_id = new Map();
118
135
 
136
+ this.query_batch_own_capabilties_throttling_interval =
137
+ options?.query_batch_own_capabilties_throttling_interval ??
138
+ DEFAULT_BATCH_OWN_CAPABILITIES_THROTTLING_INTERVAL;
139
+
119
140
  configureLoggers(options?.configure_loggers_options);
120
141
 
121
142
  this.on('all', (event) => {
@@ -231,6 +252,32 @@ export class FeedsClient extends FeedsApi {
231
252
  });
232
253
  }
233
254
 
255
+ private setGetBatchOwnCapabilitiesThrottlingInterval = (
256
+ throttlingMs: number,
257
+ ) => {
258
+ const {
259
+ throttledFn: throttledGetBatchOwnCapabilities,
260
+ cancelTimer: cancel,
261
+ } = throttle<GetBatchedOwnCapabilitiesThrottledCallback>(
262
+ (feeds, callback) => {
263
+ this.ownCapabilitiesBatch({
264
+ feeds,
265
+ }).catch((error) => {
266
+ this.eventDispatcher.dispatch({
267
+ type: 'errors.unhandled',
268
+ error_type: UnhandledErrorType.FetchingOwnCapabilitiesOnNewActivity,
269
+ error,
270
+ });
271
+ });
272
+ callback(feeds);
273
+ },
274
+ throttlingMs,
275
+ { trailing: true },
276
+ );
277
+ this.throttledGetBatchOwnCapabilities = throttledGetBatchOwnCapabilities;
278
+ this.cancelGetBatchOwnCapabilitiesTimer = cancel;
279
+ };
280
+
234
281
  private recoverOnReconnect = async () => {
235
282
  this.healthyConnectionChangedEventCount++;
236
283
 
@@ -276,6 +323,34 @@ export class FeedsClient extends FeedsApi {
276
323
  }
277
324
  }
278
325
 
326
+ public hydrateCapabilitiesCache(
327
+ feedResponses: Array<Pick<FeedResponse, 'feed' | 'own_capabilities'>>,
328
+ ) {
329
+ let ownCapabilitiesCache =
330
+ this.state.getLatestValue().own_capabilities_by_fid;
331
+
332
+ const capabilitiesToFetchQueue: string[] = [];
333
+
334
+ for (const feedResponse of feedResponses) {
335
+ const { feed, own_capabilities } = feedResponse;
336
+
337
+ if (!Object.prototype.hasOwnProperty.call(ownCapabilitiesCache, feed)) {
338
+ if (own_capabilities) {
339
+ ownCapabilitiesCache = {
340
+ ...ownCapabilitiesCache,
341
+ [feed]: own_capabilities,
342
+ };
343
+ } else {
344
+ capabilitiesToFetchQueue.push(feed);
345
+ }
346
+ }
347
+ }
348
+
349
+ queueBatchedOwnCapabilities.bind(this)({ feeds: capabilitiesToFetchQueue });
350
+
351
+ this.state.partialNext({ own_capabilities_by_fid: ownCapabilitiesCache });
352
+ }
353
+
279
354
  connectUser = async (user: UserRequest, tokenProvider: TokenOrProvider) => {
280
355
  if (
281
356
  this.state.getLatestValue().connected_user !== undefined ||
@@ -286,6 +361,10 @@ export class FeedsClient extends FeedsApi {
286
361
 
287
362
  this.tokenManager.setTokenOrProvider(tokenProvider);
288
363
 
364
+ this.setGetBatchOwnCapabilitiesThrottlingInterval(
365
+ this.query_batch_own_capabilties_throttling_interval,
366
+ );
367
+
289
368
  try {
290
369
  addConnectionEventListeners(this.updateNetworkConnectionStatus);
291
370
  this.wsConnection = new StableWSConnection(
@@ -516,10 +595,18 @@ export class FeedsClient extends FeedsApi {
516
595
 
517
596
  this.connectionIdManager.reset();
518
597
  this.tokenManager.reset();
598
+
599
+ // clear all caches
600
+ this.polls_by_id.clear();
601
+
519
602
  this.state.partialNext({
520
603
  connected_user: undefined,
521
604
  is_ws_connection_healthy: false,
605
+ own_capabilities_by_fid: {},
522
606
  });
607
+
608
+ this.cancelGetBatchOwnCapabilitiesTimer();
609
+ clearQueuedFeeds();
523
610
  };
524
611
 
525
612
  on = this.eventDispatcher.on;
@@ -542,7 +629,9 @@ export class FeedsClient extends FeedsApi {
542
629
  async queryFeeds(request?: QueryFeedsRequest) {
543
630
  const response = await this._queryFeeds(request);
544
631
 
545
- const feeds = response.feeds.map((feedResponse) =>
632
+ const feedResponses = response.feeds;
633
+
634
+ const feeds = feedResponses.map((feedResponse) =>
546
635
  this.getOrCreateActiveFeed(
547
636
  feedResponse.group_id,
548
637
  feedResponse.id,
@@ -551,6 +640,8 @@ export class FeedsClient extends FeedsApi {
551
640
  ),
552
641
  );
553
642
 
643
+ this.hydrateCapabilitiesCache(feedResponses);
644
+
554
645
  return {
555
646
  feeds,
556
647
  next: response.next,
@@ -560,6 +651,18 @@ export class FeedsClient extends FeedsApi {
560
651
  };
561
652
  }
562
653
 
654
+ async ownCapabilitiesBatch(request: OwnCapabilitiesBatchRequest) {
655
+ const response = await super.ownCapabilitiesBatch(request);
656
+ const feedResponses = Object.entries(response.capabilities).map(
657
+ ([feed, own_capabilities]) => ({
658
+ feed,
659
+ own_capabilities,
660
+ }),
661
+ );
662
+ this.hydrateCapabilitiesCache(feedResponses);
663
+ return response;
664
+ }
665
+
563
666
  updateNetworkConnectionStatus = (
564
667
  event: { type: 'online' | 'offline' } | Event,
565
668
  ) => {
@@ -0,0 +1,2 @@
1
+ export * from './throttle';
2
+ export * from './throttled-get-batched-own-capabilities'
@@ -0,0 +1,123 @@
1
+ export type ThrottledCallback = (...args: unknown[]) => unknown;
2
+
3
+ export type ThrottledFunction<T extends unknown[]> = (...args: T) => void;
4
+
5
+ /**
6
+ * Throttle a function so it runs at most once per `timeout` ms.
7
+ *
8
+ * - `leading`: fire immediately when the window opens
9
+ * - `trailing`: remember the latest args/this and fire once when the window closes
10
+ *
11
+ * defaults: `{ leading: true, trailing: false }`
12
+ *
13
+ * notes:
14
+ * - make one throttled instance and reuse it; re-creating it resets internal state
15
+ *
16
+ * @typeParam T - the function type being throttled
17
+ * @param fn - function to throttle
18
+ * @param timeout - minimum time between invocations (ms)
19
+ * @param options - behavior switches
20
+ * @param options.leading - call on the leading edge (default: true)
21
+ * @param options.trailing - call once at the end of the window with the latest args (default: false)
22
+ * @returns a throttled function with the same call signature as `fn`
23
+ *
24
+ * @example
25
+ * const send = (payload: Data) => api.post('/endpoint', payload);
26
+ * const sendThrottled = throttle(send, 2000, { leading: true, trailing: true });
27
+ * // call `sendThrottled` freely; it won’t invoke `send` more than once every 2s
28
+ */
29
+ export const throttle = <T extends unknown[]>(
30
+ fn: (...args: T) => void,
31
+ timeout = 200,
32
+ {
33
+ leading = true,
34
+ trailing = false,
35
+ }: { leading?: boolean; trailing?: boolean } = {},
36
+ ) => {
37
+ let timer: NodeJS.Timeout | null = null;
38
+ let storedArgs: T | null = null;
39
+ let storedThis: unknown = null;
40
+ let lastInvokeTime: number | undefined; // timestamp of last actual invocation
41
+
42
+ const invoke = (args: T, thisArg: unknown) => {
43
+ lastInvokeTime = Date.now();
44
+ fn.apply(thisArg, args);
45
+ };
46
+
47
+ const scheduleTrailing = (delay: number) => {
48
+ if (timer) return;
49
+ timer = setTimeout(() => {
50
+ timer = null;
51
+ if (trailing && storedArgs) {
52
+ invoke(storedArgs, storedThis);
53
+ storedArgs = null;
54
+ storedThis = null;
55
+ }
56
+ }, delay);
57
+ };
58
+
59
+ return {
60
+ throttledFn: function (this: unknown, ...args: T) {
61
+ const now = Date.now();
62
+
63
+ const hasBeenInvoked = lastInvokeTime != null;
64
+
65
+ // if we have never invoked and `leading` is `false`, treat `lastInvokeTime` as now
66
+ if (!hasBeenInvoked && !leading) lastInvokeTime = now;
67
+
68
+ const timeSinceLast = hasBeenInvoked ? now - lastInvokeTime! : timeout;
69
+ const remaining = timeout - timeSinceLast;
70
+
71
+ // capture latest args for possible trailing invocation
72
+ if (trailing) {
73
+ storedArgs = args;
74
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
75
+ storedThis = this;
76
+ }
77
+
78
+ // if enough time has passed, invoke immediately
79
+ if (remaining <= 0) {
80
+ // if there's a pending timer, clear it because we're invoking now
81
+ if (timer) {
82
+ clearTimeout(timer);
83
+ timer = null;
84
+ }
85
+
86
+ // leading: call now
87
+ if (leading) {
88
+ // if trailing is also `true`, we've already stored args above;
89
+ // make sure we don't call the same args twice
90
+ if (trailing) {
91
+ // if the `storedArgs` are exactly the args we're about to call,
92
+ // clear storedArgs to avoid double invocation by trailing (comparing
93
+ // by reference is fine because the `args` array is new each call)
94
+ if (storedArgs === args) {
95
+ storedArgs = null;
96
+ storedThis = null;
97
+ }
98
+ }
99
+ invoke(args, this);
100
+ } else {
101
+ // not leading but trailing: schedule a trailing call after `timeout`
102
+ if (trailing) scheduleTrailing(timeout);
103
+ }
104
+
105
+ return;
106
+ }
107
+
108
+ // not enough time passed: we're in cooldown, so if
109
+ // trailing is requested, ensure a trailing invocation
110
+ // is scheduled at the end of the remaining time
111
+ if (trailing && !timer) {
112
+ scheduleTrailing(remaining);
113
+ }
114
+ // if `trailing` is `false`, we simply drop the call (throttle)
115
+ },
116
+ cancelTimer: () => {
117
+ if (timer) {
118
+ clearTimeout(timer);
119
+ timer = null;
120
+ }
121
+ },
122
+ };
123
+ };
@@ -0,0 +1,42 @@
1
+ import type { FeedsClient } from '@self';
2
+ import type { ThrottledFunction } from './throttle';
3
+
4
+ export type GetBatchedOwnCapabilities = {
5
+ feeds: string[];
6
+ };
7
+
8
+ export type GetBatchedOwnCapabilitiesThrottledCallback = [
9
+ feeds: string[],
10
+ callback: (feedsToClear: string[]) => void | Promise<void>,
11
+ ];
12
+
13
+ export type ThrottledGetBatchedOwnCapabilities =
14
+ ThrottledFunction<GetBatchedOwnCapabilitiesThrottledCallback>;
15
+
16
+ export const DEFAULT_BATCH_OWN_CAPABILITIES_THROTTLING_INTERVAL = 2000;
17
+
18
+ const queuedFeeds: Set<string> = new Set();
19
+
20
+ export function queueBatchedOwnCapabilities(
21
+ this: FeedsClient,
22
+ { feeds }: GetBatchedOwnCapabilities,
23
+ ) {
24
+ for (const feed of feeds) {
25
+ queuedFeeds.add(feed);
26
+ }
27
+
28
+ if (queuedFeeds.size > 0) {
29
+ this.throttledGetBatchOwnCapabilities(
30
+ [...queuedFeeds],
31
+ (feedsToClear: string[]) => {
32
+ for (const feed of feedsToClear) {
33
+ queuedFeeds.delete(feed);
34
+ }
35
+ },
36
+ );
37
+ }
38
+ }
39
+
40
+ export function clearQueuedFeeds() {
41
+ queuedFeeds.clear();
42
+ }