@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.
- package/@react-bindings/hooks/util/index.ts +1 -0
- package/CHANGELOG.md +15 -0
- package/dist/@react-bindings/hooks/util/index.d.ts +1 -0
- package/dist/@react-bindings/hooks/util/useBookmarkActions.d.ts +13 -0
- package/dist/@react-bindings/hooks/util/useReactionActions.d.ts +1 -1
- package/dist/index-react-bindings.browser.cjs +346 -140
- package/dist/index-react-bindings.browser.cjs.map +1 -1
- package/dist/index-react-bindings.browser.js +346 -141
- package/dist/index-react-bindings.browser.js.map +1 -1
- package/dist/index-react-bindings.node.cjs +346 -140
- package/dist/index-react-bindings.node.cjs.map +1 -1
- package/dist/index-react-bindings.node.js +346 -141
- package/dist/index-react-bindings.node.js.map +1 -1
- package/dist/index.browser.cjs +320 -139
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.js +320 -140
- package/dist/index.browser.js.map +1 -1
- package/dist/index.node.cjs +320 -139
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.js +320 -140
- package/dist/index.node.js.map +1 -1
- package/dist/src/Feed.d.ts +40 -9
- package/dist/src/FeedsClient.d.ts +8 -1
- package/dist/src/gen-imports.d.ts +1 -1
- package/dist/src/state-updates/follow-utils.d.ts +19 -0
- package/dist/src/state-updates/state-update-queue.d.ts +15 -0
- package/dist/src/utils.d.ts +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/Feed.ts +226 -192
- package/src/FeedsClient.ts +75 -3
- package/src/gen-imports.ts +1 -1
- package/src/state-updates/activity-reaction-utils.test.ts +1 -0
- package/src/state-updates/activity-utils.test.ts +1 -0
- package/src/state-updates/follow-utils.test.ts +552 -0
- package/src/state-updates/follow-utils.ts +126 -0
- package/src/state-updates/state-update-queue.test.ts +53 -0
- package/src/state-updates/state-update-queue.ts +35 -0
- package/src/utils.test.ts +175 -0
- 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
|
+
};
|