@stream-io/feeds-client 0.1.8 → 0.1.9

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 (40) hide show
  1. package/@react-bindings/hooks/util/index.ts +1 -0
  2. package/CHANGELOG.md +15 -0
  3. package/dist/@react-bindings/hooks/util/index.d.ts +1 -0
  4. package/dist/@react-bindings/hooks/util/useBookmarkActions.d.ts +13 -0
  5. package/dist/@react-bindings/hooks/util/useReactionActions.d.ts +1 -1
  6. package/dist/index-react-bindings.browser.cjs +346 -140
  7. package/dist/index-react-bindings.browser.cjs.map +1 -1
  8. package/dist/index-react-bindings.browser.js +346 -141
  9. package/dist/index-react-bindings.browser.js.map +1 -1
  10. package/dist/index-react-bindings.node.cjs +346 -140
  11. package/dist/index-react-bindings.node.cjs.map +1 -1
  12. package/dist/index-react-bindings.node.js +346 -141
  13. package/dist/index-react-bindings.node.js.map +1 -1
  14. package/dist/index.browser.cjs +320 -139
  15. package/dist/index.browser.cjs.map +1 -1
  16. package/dist/index.browser.js +320 -140
  17. package/dist/index.browser.js.map +1 -1
  18. package/dist/index.node.cjs +320 -139
  19. package/dist/index.node.cjs.map +1 -1
  20. package/dist/index.node.js +320 -140
  21. package/dist/index.node.js.map +1 -1
  22. package/dist/src/Feed.d.ts +40 -9
  23. package/dist/src/FeedsClient.d.ts +8 -1
  24. package/dist/src/gen-imports.d.ts +1 -1
  25. package/dist/src/state-updates/follow-utils.d.ts +19 -0
  26. package/dist/src/state-updates/state-update-queue.d.ts +15 -0
  27. package/dist/src/utils.d.ts +1 -0
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +1 -1
  30. package/src/Feed.ts +226 -192
  31. package/src/FeedsClient.ts +75 -3
  32. package/src/gen-imports.ts +1 -1
  33. package/src/state-updates/activity-reaction-utils.test.ts +1 -0
  34. package/src/state-updates/activity-utils.test.ts +1 -0
  35. package/src/state-updates/follow-utils.test.ts +552 -0
  36. package/src/state-updates/follow-utils.ts +126 -0
  37. package/src/state-updates/state-update-queue.test.ts +53 -0
  38. package/src/state-updates/state-update-queue.ts +35 -0
  39. package/src/utils.test.ts +175 -0
  40. package/src/utils.ts +20 -0
@@ -0,0 +1,126 @@
1
+ import { FeedState } from '../Feed';
2
+ import { FollowResponse, FeedResponse } from '../gen/models';
3
+ import { UpdateStateResult } from '../types-internal';
4
+
5
+ const isFeedResponse = (
6
+ follow: FeedResponse | { fid: string },
7
+ ): follow is FeedResponse => {
8
+ return 'created_by' in follow;
9
+ };
10
+
11
+ export const handleFollowCreated = (
12
+ follow: FollowResponse,
13
+ currentState: FeedState,
14
+ currentFeedId: string,
15
+ connectedUserId?: string,
16
+ ): UpdateStateResult<{ data: FeedState }> => {
17
+ // filter non-accepted follows (the way getOrCreate does by default)
18
+ if (follow.status !== 'accepted') {
19
+ return { changed: false, data: currentState };
20
+ }
21
+
22
+ let newState: FeedState = { ...currentState };
23
+
24
+ // this feed followed someone
25
+ if (follow.source_feed.fid === currentFeedId) {
26
+ newState = {
27
+ ...newState,
28
+ // Update FeedResponse fields, that has the new follower/following count
29
+ ...follow.source_feed,
30
+ };
31
+
32
+ // Only update if following array already exists
33
+ if (currentState.following !== undefined) {
34
+ newState.following = [follow, ...currentState.following];
35
+ }
36
+ } else if (
37
+ // someone followed this feed
38
+ follow.target_feed.fid === currentFeedId
39
+ ) {
40
+ const source = follow.source_feed;
41
+
42
+ newState = {
43
+ ...newState,
44
+ // Update FeedResponse fields, that has the new follower/following count
45
+ ...follow.target_feed,
46
+ };
47
+
48
+ if (source.created_by.id === connectedUserId) {
49
+ newState.own_follows = currentState.own_follows
50
+ ? currentState.own_follows.concat(follow)
51
+ : [follow];
52
+ }
53
+
54
+ // Only update if followers array already exists
55
+ if (currentState.followers !== undefined) {
56
+ newState.followers = [follow, ...currentState.followers];
57
+ }
58
+ }
59
+
60
+ return { changed: true, data: newState };
61
+ };
62
+
63
+ export const handleFollowDeleted = (
64
+ follow:
65
+ | FollowResponse
66
+ | { source_feed: { fid: string }; target_feed: { fid: string } },
67
+ currentState: FeedState,
68
+ currentFeedId: string,
69
+ connectedUserId?: string,
70
+ ): UpdateStateResult<{ data: FeedState }> => {
71
+ let newState: FeedState = { ...currentState };
72
+
73
+ // this feed unfollowed someone
74
+ if (follow.source_feed.fid === currentFeedId) {
75
+ newState = {
76
+ ...newState,
77
+ // Update FeedResponse fields, that has the new follower/following count
78
+ ...follow.source_feed,
79
+ };
80
+
81
+ // Only update if following array already exists
82
+ if (currentState.following !== undefined) {
83
+ newState.following = currentState.following.filter(
84
+ (followItem) => followItem.target_feed.fid !== follow.target_feed.fid,
85
+ );
86
+ }
87
+ } else if (
88
+ // someone unfollowed this feed
89
+ follow.target_feed.fid === currentFeedId
90
+ ) {
91
+ const source = follow.source_feed;
92
+
93
+ newState = {
94
+ ...newState,
95
+ // Update FeedResponse fields, that has the new follower/following count
96
+ ...follow.target_feed,
97
+ };
98
+
99
+ if (
100
+ isFeedResponse(source) &&
101
+ source.created_by.id === connectedUserId &&
102
+ currentState.own_follows !== undefined
103
+ ) {
104
+ newState.own_follows = currentState.own_follows.filter(
105
+ (followItem) => followItem.source_feed.fid !== follow.source_feed.fid,
106
+ );
107
+ }
108
+
109
+ // Only update if followers array already exists
110
+ if (currentState.followers !== undefined) {
111
+ newState.followers = currentState.followers.filter(
112
+ (followItem) => followItem.source_feed.fid !== follow.source_feed.fid,
113
+ );
114
+ }
115
+ }
116
+
117
+ return { changed: true, data: newState };
118
+ };
119
+
120
+ export const handleFollowUpdated = (
121
+ currentState: FeedState,
122
+ ): UpdateStateResult<{ data: FeedState }> => {
123
+ // For now, we'll treat follow updates as no-ops since the current implementation does
124
+ // This can be enhanced later if needed
125
+ return { changed: false, data: currentState };
126
+ };
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import { shouldUpdateState } from './state-update-queue';
4
+
5
+ describe('state-update-queue', () => {
6
+ describe('shouldUpdateState', () => {
7
+ it('should return true when watch is false', () => {
8
+ const result = shouldUpdateState({
9
+ stateUpdateId: 'test-id',
10
+ stateUpdateQueue: new Set(['other-id']),
11
+ watch: false,
12
+ });
13
+
14
+ expect(result).toBe(true);
15
+ });
16
+
17
+ it('should return true when watch is true but stateUpdateId is not in queue', () => {
18
+ const stateUpdateQueue = new Set(['other-id-1', 'other-id-2']);
19
+
20
+ const result = shouldUpdateState({
21
+ stateUpdateId: 'test-id',
22
+ stateUpdateQueue: stateUpdateQueue,
23
+ watch: true,
24
+ });
25
+
26
+ expect(stateUpdateQueue).toContain('test-id');
27
+ expect(result).toBe(true);
28
+ });
29
+
30
+ it('should return false and remove stateUpdateId from queue when watch is true and stateUpdateId is in queue', () => {
31
+ const stateUpdateQueue = new Set(['test-id', 'other-id']);
32
+
33
+ const result = shouldUpdateState({
34
+ stateUpdateId: 'test-id',
35
+ stateUpdateQueue,
36
+ watch: true,
37
+ });
38
+
39
+ expect(result).toBe(false);
40
+ expect(stateUpdateQueue).toEqual(new Set(['other-id']));
41
+ });
42
+
43
+ it('should handle empty queue when watch is true', () => {
44
+ const result = shouldUpdateState({
45
+ stateUpdateId: 'test-id',
46
+ stateUpdateQueue: new Set(),
47
+ watch: true,
48
+ });
49
+
50
+ expect(result).toBe(true);
51
+ });
52
+ });
53
+ });
@@ -0,0 +1,35 @@
1
+ import { FollowResponse } from '../gen/models';
2
+
3
+ export const shouldUpdateState = ({
4
+ stateUpdateId,
5
+ stateUpdateQueue,
6
+ watch,
7
+ }: {
8
+ stateUpdateId: string;
9
+ stateUpdateQueue: Set<string>;
10
+ watch: boolean;
11
+ }) => {
12
+ if (!watch) {
13
+ return true;
14
+ }
15
+
16
+ if (watch && stateUpdateQueue.has(stateUpdateId)) {
17
+ stateUpdateQueue.delete(stateUpdateId);
18
+ return false;
19
+ }
20
+
21
+ stateUpdateQueue.add(stateUpdateId);
22
+ return true;
23
+ };
24
+
25
+ export const getStateUpdateQueueIdForFollow = (follow: FollowResponse) => {
26
+ return `follow${follow.source_feed.fid}-${follow.target_feed.fid}`;
27
+ };
28
+
29
+ export const getStateUpdateQueueIdForUnfollow = (
30
+ follow:
31
+ | FollowResponse
32
+ | { source_feed: { fid: string }; target_feed: { fid: string } },
33
+ ) => {
34
+ return `unfollow${follow.source_feed.fid}-${follow.target_feed.fid}`;
35
+ };
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import { uniqueArrayMerge } from './utils';
4
+
5
+ describe('utils', () => {
6
+ describe('uniqueMerge', () => {
7
+ it('should merge arrays with unique objects based on key', () => {
8
+ const existingArray = [
9
+ { id: '1', name: 'Alice' },
10
+ { id: '2', name: 'Bob' },
11
+ ];
12
+ const arrayToMerge = [
13
+ { id: '2', name: 'Bob Updated' },
14
+ { id: '3', name: 'Charlie' },
15
+ ];
16
+ const getKey = (item: { id: string; name: string }) => item.id;
17
+
18
+ const result = uniqueArrayMerge(existingArray, arrayToMerge, getKey);
19
+
20
+ expect(result).toEqual([
21
+ { id: '1', name: 'Alice' },
22
+ { id: '2', name: 'Bob' },
23
+ { id: '3', name: 'Charlie' },
24
+ ]);
25
+ });
26
+
27
+ it('should preserve order of existing array and append new items', () => {
28
+ const existingArray = [
29
+ { id: '1', name: 'Alice' },
30
+ { id: '2', name: 'Bob' },
31
+ ];
32
+ const arrayToMerge = [
33
+ { id: '3', name: 'Charlie' },
34
+ { id: '4', name: 'David' },
35
+ ];
36
+ const getKey = (item: { id: string; name: string }) => item.id;
37
+
38
+ const result = uniqueArrayMerge(existingArray, arrayToMerge, getKey);
39
+
40
+ expect(result).toEqual([
41
+ { id: '1', name: 'Alice' },
42
+ { id: '2', name: 'Bob' },
43
+ { id: '3', name: 'Charlie' },
44
+ { id: '4', name: 'David' },
45
+ ]);
46
+ });
47
+
48
+ it('should filter out duplicate keys from array to merge', () => {
49
+ const existingArray = [
50
+ { id: '1', name: 'Alice' },
51
+ { id: '2', name: 'Bob' },
52
+ ];
53
+ const arrayToMerge = [
54
+ { id: '1', name: 'Alice Updated' },
55
+ { id: '2', name: 'Bob Updated' },
56
+ { id: '3', name: 'Charlie' },
57
+ ];
58
+ const getKey = (item: { id: string; name: string }) => item.id;
59
+
60
+ const result = uniqueArrayMerge(existingArray, arrayToMerge, getKey);
61
+
62
+ expect(result).toEqual([
63
+ { id: '1', name: 'Alice' },
64
+ { id: '2', name: 'Bob' },
65
+ { id: '3', name: 'Charlie' },
66
+ ]);
67
+ });
68
+
69
+ it('should handle empty existing array', () => {
70
+ const existingArray: Array<{ id: string; name: string }> = [];
71
+ const arrayToMerge = [
72
+ { id: '1', name: 'Alice' },
73
+ { id: '2', name: 'Bob' },
74
+ ];
75
+ const getKey = (item: { id: string; name: string }) => item.id;
76
+
77
+ const result = uniqueArrayMerge(existingArray, arrayToMerge, getKey);
78
+
79
+ expect(result).toEqual([
80
+ { id: '1', name: 'Alice' },
81
+ { id: '2', name: 'Bob' },
82
+ ]);
83
+ });
84
+
85
+ it('should handle empty array to merge', () => {
86
+ const existingArray = [
87
+ { id: '1', name: 'Alice' },
88
+ { id: '2', name: 'Bob' },
89
+ ];
90
+ const arrayToMerge: Array<{ id: string; name: string }> = [];
91
+ const getKey = (item: { id: string; name: string }) => item.id;
92
+
93
+ const result = uniqueArrayMerge(existingArray, arrayToMerge, getKey);
94
+
95
+ expect(result).toEqual([
96
+ { id: '1', name: 'Alice' },
97
+ { id: '2', name: 'Bob' },
98
+ ]);
99
+ });
100
+
101
+ it('should handle both arrays being empty', () => {
102
+ const existingArray: Array<{ id: string; name: string }> = [];
103
+ const arrayToMerge: Array<{ id: string; name: string }> = [];
104
+ const getKey = (item: { id: string; name: string }) => item.id;
105
+
106
+ const result = uniqueArrayMerge(existingArray, arrayToMerge, getKey);
107
+
108
+ expect(result).toEqual([]);
109
+ });
110
+
111
+ it('should work with different key functions', () => {
112
+ const existingArray = [
113
+ { id: '1', name: 'Alice', email: 'alice@example.com' },
114
+ { id: '2', name: 'Bob', email: 'bob@example.com' },
115
+ ];
116
+ const arrayToMerge = [
117
+ { id: '3', name: 'Charlie', email: 'alice@example.com' },
118
+ { id: '4', name: 'David', email: 'david@example.com' },
119
+ ];
120
+ const getKeyByEmail = (item: {
121
+ id: string;
122
+ name: string;
123
+ email: string;
124
+ }) => item.email;
125
+
126
+ const result = uniqueArrayMerge(existingArray, arrayToMerge, getKeyByEmail);
127
+
128
+ expect(result).toEqual([
129
+ { id: '1', name: 'Alice', email: 'alice@example.com' },
130
+ { id: '2', name: 'Bob', email: 'bob@example.com' },
131
+ { id: '4', name: 'David', email: 'david@example.com' },
132
+ ]);
133
+ });
134
+
135
+ it('should handle complex nested objects', () => {
136
+ const existingArray = [
137
+ { id: '1', data: { nested: { value: 'a' } } },
138
+ { id: '2', data: { nested: { value: 'b' } } },
139
+ ];
140
+ const arrayToMerge = [
141
+ { id: '2', data: { nested: { value: 'b_updated' } } },
142
+ { id: '3', data: { nested: { value: 'c' } } },
143
+ ];
144
+ const getKey = (item: {
145
+ id: string;
146
+ data: { nested: { value: string } };
147
+ }) => item.id;
148
+
149
+ const result = uniqueArrayMerge(existingArray, arrayToMerge, getKey);
150
+
151
+ expect(result).toEqual([
152
+ { id: '1', data: { nested: { value: 'a' } } },
153
+ { id: '2', data: { nested: { value: 'b' } } },
154
+ { id: '3', data: { nested: { value: 'c' } } },
155
+ ]);
156
+ });
157
+
158
+ it('should preserve original arrays (immutability)', () => {
159
+ const existingArray = [
160
+ { id: '1', name: 'Alice' },
161
+ { id: '2', name: 'Bob' },
162
+ ];
163
+ const arrayToMerge = [{ id: '3', name: 'Charlie' }];
164
+ const getKey = (item: { id: string; name: string }) => item.id;
165
+
166
+ const originalExisting = [...existingArray];
167
+ const originalToMerge = [...arrayToMerge];
168
+
169
+ uniqueArrayMerge(existingArray, arrayToMerge, getKey);
170
+
171
+ expect(existingArray).toEqual(originalExisting);
172
+ expect(arrayToMerge).toEqual(originalToMerge);
173
+ });
174
+ });
175
+ });
package/src/utils.ts CHANGED
@@ -26,3 +26,23 @@ export const isCommentResponse = (
26
26
  export const Constants = {
27
27
  DEFAULT_COMMENT_PAGINATION: 'first',
28
28
  } as const;
29
+
30
+ export const uniqueArrayMerge = <T>(
31
+ existingArray: T[],
32
+ arrayToMerge: T[],
33
+ getKey: (v: T) => string,
34
+ ) => {
35
+ const existing = new Set<string>();
36
+
37
+ existingArray.forEach((value) => {
38
+ const key = getKey(value);
39
+ existing.add(key);
40
+ });
41
+
42
+ const filteredArrayToMerge = arrayToMerge.filter((value) => {
43
+ const key = getKey(value);
44
+ return !existing.has(key);
45
+ });
46
+
47
+ return existingArray.concat(filteredArrayToMerge);
48
+ };