@stream-io/feeds-client 0.1.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.
- package/@react-bindings/index.ts +2 -0
- package/CHANGELOG.md +44 -0
- package/LICENSE +219 -0
- package/README.md +9 -0
- package/dist/@react-bindings/hooks/useComments.d.ts +12 -0
- package/dist/@react-bindings/hooks/useStateStore.d.ts +3 -0
- package/dist/@react-bindings/index.d.ts +2 -0
- package/dist/index-react-bindings.browser.cjs +56 -0
- package/dist/index-react-bindings.browser.cjs.map +1 -0
- package/dist/index-react-bindings.browser.js +53 -0
- package/dist/index-react-bindings.browser.js.map +1 -0
- package/dist/index-react-bindings.node.cjs +56 -0
- package/dist/index-react-bindings.node.cjs.map +1 -0
- package/dist/index-react-bindings.node.js +53 -0
- package/dist/index-react-bindings.node.js.map +1 -0
- package/dist/index.browser.cjs +5799 -0
- package/dist/index.browser.cjs.map +1 -0
- package/dist/index.browser.js +5782 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.node.cjs +5799 -0
- package/dist/index.node.cjs.map +1 -0
- package/dist/index.node.js +5782 -0
- package/dist/index.node.js.map +1 -0
- package/dist/src/Feed.d.ts +109 -0
- package/dist/src/FeedsClient.d.ts +63 -0
- package/dist/src/ModerationClient.d.ts +3 -0
- package/dist/src/common/ActivitySearchSource.d.ts +17 -0
- package/dist/src/common/ApiClient.d.ts +20 -0
- package/dist/src/common/BaseSearchSource.d.ts +87 -0
- package/dist/src/common/ConnectionIdManager.d.ts +11 -0
- package/dist/src/common/EventDispatcher.d.ts +11 -0
- package/dist/src/common/FeedSearchSource.d.ts +17 -0
- package/dist/src/common/Poll.d.ts +34 -0
- package/dist/src/common/SearchController.d.ts +41 -0
- package/dist/src/common/StateStore.d.ts +124 -0
- package/dist/src/common/TokenManager.d.ts +29 -0
- package/dist/src/common/UserSearchSource.d.ts +17 -0
- package/dist/src/common/gen-imports.d.ts +2 -0
- package/dist/src/common/rate-limit.d.ts +2 -0
- package/dist/src/common/real-time/StableWSConnection.d.ts +144 -0
- package/dist/src/common/real-time/event-models.d.ts +36 -0
- package/dist/src/common/types.d.ts +29 -0
- package/dist/src/common/utils.d.ts +54 -0
- package/dist/src/gen/feeds/FeedApi.d.ts +26 -0
- package/dist/src/gen/feeds/FeedsApi.d.ts +237 -0
- package/dist/src/gen/model-decoders/decoders.d.ts +3 -0
- package/dist/src/gen/model-decoders/event-decoder-mapping.d.ts +6 -0
- package/dist/src/gen/models/index.d.ts +3437 -0
- package/dist/src/gen/moderation/ModerationApi.d.ts +21 -0
- package/dist/src/gen-imports.d.ts +3 -0
- package/dist/src/state-updates/activity-reaction-utils.d.ts +10 -0
- package/dist/src/state-updates/activity-utils.d.ts +13 -0
- package/dist/src/state-updates/bookmark-utils.d.ts +14 -0
- package/dist/src/types-internal.d.ts +4 -0
- package/dist/src/types.d.ts +13 -0
- package/dist/src/utils.d.ts +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/index.ts +13 -0
- package/package.json +85 -0
- package/src/Feed.ts +1070 -0
- package/src/FeedsClient.ts +352 -0
- package/src/ModerationClient.ts +3 -0
- package/src/common/ActivitySearchSource.ts +46 -0
- package/src/common/ApiClient.ts +197 -0
- package/src/common/BaseSearchSource.ts +238 -0
- package/src/common/ConnectionIdManager.ts +51 -0
- package/src/common/EventDispatcher.ts +52 -0
- package/src/common/FeedSearchSource.ts +94 -0
- package/src/common/Poll.ts +313 -0
- package/src/common/SearchController.ts +152 -0
- package/src/common/StateStore.ts +314 -0
- package/src/common/TokenManager.ts +112 -0
- package/src/common/UserSearchSource.ts +93 -0
- package/src/common/gen-imports.ts +2 -0
- package/src/common/rate-limit.ts +23 -0
- package/src/common/real-time/StableWSConnection.ts +761 -0
- package/src/common/real-time/event-models.ts +38 -0
- package/src/common/types.ts +40 -0
- package/src/common/utils.ts +194 -0
- package/src/gen/feeds/FeedApi.ts +129 -0
- package/src/gen/feeds/FeedsApi.ts +2192 -0
- package/src/gen/model-decoders/decoders.ts +1877 -0
- package/src/gen/model-decoders/event-decoder-mapping.ts +150 -0
- package/src/gen/models/index.ts +5882 -0
- package/src/gen/moderation/ModerationApi.ts +270 -0
- package/src/gen-imports.ts +3 -0
- package/src/state-updates/activity-reaction-utils.test.ts +348 -0
- package/src/state-updates/activity-reaction-utils.ts +107 -0
- package/src/state-updates/activity-utils.test.ts +257 -0
- package/src/state-updates/activity-utils.ts +80 -0
- package/src/state-updates/bookmark-utils.test.ts +383 -0
- package/src/state-updates/bookmark-utils.ts +157 -0
- package/src/types-internal.ts +5 -0
- package/src/types.ts +20 -0
- package/src/utils.ts +4 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { FeedsApi } from './gen/feeds/FeedsApi';
|
|
2
|
+
import {
|
|
3
|
+
ActivityResponse,
|
|
4
|
+
FeedResponse,
|
|
5
|
+
FileUploadRequest,
|
|
6
|
+
ImageUploadRequest,
|
|
7
|
+
OwnUser,
|
|
8
|
+
PollResponse,
|
|
9
|
+
PollVotesResponse,
|
|
10
|
+
QueryFeedsRequest,
|
|
11
|
+
QueryPollVotesRequest,
|
|
12
|
+
UserRequest,
|
|
13
|
+
WSEvent,
|
|
14
|
+
} from './gen/models';
|
|
15
|
+
import { FeedsEvent } from './types';
|
|
16
|
+
import { StateStore } from './common/StateStore';
|
|
17
|
+
import { TokenManager } from './common/TokenManager';
|
|
18
|
+
import { ConnectionIdManager } from './common/ConnectionIdManager';
|
|
19
|
+
import { StableWSConnection } from './common/real-time/StableWSConnection';
|
|
20
|
+
import { EventDispatcher } from './common/EventDispatcher';
|
|
21
|
+
import { ApiClient } from './common/ApiClient';
|
|
22
|
+
import {
|
|
23
|
+
addConnectionEventListeners,
|
|
24
|
+
removeConnectionEventListeners,
|
|
25
|
+
} from './common/utils';
|
|
26
|
+
import { decodeWSEvent } from './gen/model-decoders/event-decoder-mapping';
|
|
27
|
+
import { Feed } from './Feed';
|
|
28
|
+
import {
|
|
29
|
+
FeedsClientOptions,
|
|
30
|
+
NetworkChangedEvent,
|
|
31
|
+
StreamResponse,
|
|
32
|
+
} from './common/types';
|
|
33
|
+
import { ModerationClient } from './ModerationClient';
|
|
34
|
+
import { StreamPoll } from './common/Poll';
|
|
35
|
+
|
|
36
|
+
export type FeedsClientState = {
|
|
37
|
+
connectedUser: OwnUser | undefined;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type FID = string;
|
|
41
|
+
|
|
42
|
+
export class FeedsClient extends FeedsApi {
|
|
43
|
+
readonly state: StateStore<FeedsClientState>;
|
|
44
|
+
readonly moderation: ModerationClient;
|
|
45
|
+
|
|
46
|
+
private readonly tokenManager: TokenManager;
|
|
47
|
+
private wsConnection?: StableWSConnection;
|
|
48
|
+
private readonly connectionIdManager: ConnectionIdManager;
|
|
49
|
+
private readonly eventDispatcher = new EventDispatcher<
|
|
50
|
+
FeedsEvent['type'],
|
|
51
|
+
FeedsEvent & { fid?: string }
|
|
52
|
+
>();
|
|
53
|
+
|
|
54
|
+
private readonly polls_by_id: Map<string, StreamPoll>;
|
|
55
|
+
|
|
56
|
+
private activeFeeds: Record<FID, Feed> = {};
|
|
57
|
+
|
|
58
|
+
constructor(apiKey: string, options?: FeedsClientOptions) {
|
|
59
|
+
const tokenManager = new TokenManager();
|
|
60
|
+
const connectionIdManager = new ConnectionIdManager();
|
|
61
|
+
const apiClient = new ApiClient(
|
|
62
|
+
apiKey,
|
|
63
|
+
tokenManager,
|
|
64
|
+
connectionIdManager,
|
|
65
|
+
options,
|
|
66
|
+
);
|
|
67
|
+
super(apiClient);
|
|
68
|
+
this.state = new StateStore<FeedsClientState>({
|
|
69
|
+
connectedUser: undefined,
|
|
70
|
+
});
|
|
71
|
+
this.moderation = new ModerationClient(apiClient);
|
|
72
|
+
this.tokenManager = tokenManager;
|
|
73
|
+
this.connectionIdManager = connectionIdManager;
|
|
74
|
+
this.polls_by_id = new Map();
|
|
75
|
+
this.on('all', (event) => {
|
|
76
|
+
const fid = event.fid;
|
|
77
|
+
|
|
78
|
+
const feed: Feed | undefined =
|
|
79
|
+
typeof fid === 'string' ? this.activeFeeds[fid] : undefined;
|
|
80
|
+
|
|
81
|
+
switch (event.type) {
|
|
82
|
+
case 'feeds.feed.created': {
|
|
83
|
+
if (feed) break;
|
|
84
|
+
|
|
85
|
+
this.getOrCreateActiveFeed(
|
|
86
|
+
event.feed.group_id,
|
|
87
|
+
event.feed.id,
|
|
88
|
+
event.feed,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'feeds.feed.deleted': {
|
|
94
|
+
feed?.handleWSEvent(event as unknown as WSEvent);
|
|
95
|
+
if (typeof fid === 'string') {
|
|
96
|
+
delete this.activeFeeds[fid];
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'feeds.poll.closed': {
|
|
101
|
+
if (event.poll?.id) {
|
|
102
|
+
this.pollFromState(event.poll.id)?.handlePollClosed(event);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'feeds.poll.deleted': {
|
|
107
|
+
if (event.poll?.id) {
|
|
108
|
+
this.polls_by_id.delete(event.poll.id);
|
|
109
|
+
|
|
110
|
+
for (const activeFeed of Object.values(this.activeFeeds)) {
|
|
111
|
+
const currentActivities = activeFeed.currentState.activities;
|
|
112
|
+
if (currentActivities) {
|
|
113
|
+
const newActivities = [];
|
|
114
|
+
let changed = false;
|
|
115
|
+
for (const activity of currentActivities) {
|
|
116
|
+
if (activity.poll?.id === event.poll.id) {
|
|
117
|
+
delete activity.poll;
|
|
118
|
+
changed = true;
|
|
119
|
+
}
|
|
120
|
+
newActivities.push(activity);
|
|
121
|
+
}
|
|
122
|
+
if (changed) {
|
|
123
|
+
activeFeed.state.partialNext({ activities: newActivities });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
case 'feeds.poll.updated': {
|
|
131
|
+
if (event.poll?.id) {
|
|
132
|
+
this.pollFromState(event.poll.id)?.handlePollUpdated(event);
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'feeds.poll.vote_casted': {
|
|
137
|
+
if (event.poll?.id) {
|
|
138
|
+
this.pollFromState(event.poll.id)?.handleVoteCasted(event);
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'feeds.poll.vote_changed': {
|
|
143
|
+
if (event.poll?.id) {
|
|
144
|
+
this.pollFromState(event.poll.id)?.handleVoteChanged(event);
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case 'feeds.poll.vote_removed': {
|
|
149
|
+
if (event.poll?.id) {
|
|
150
|
+
this.pollFromState(event.poll.id)?.handleVoteRemoved(event);
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'feeds.bookmark.added':
|
|
155
|
+
case 'feeds.bookmark.deleted':
|
|
156
|
+
case 'feeds.bookmark.updated': {
|
|
157
|
+
const activityId = event.bookmark.activity.id;
|
|
158
|
+
// TODO: find faster way later on
|
|
159
|
+
const feeds = this.findActiveFeedByActivityId(activityId);
|
|
160
|
+
feeds.forEach((f) => f.handleWSEvent(event));
|
|
161
|
+
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
default: {
|
|
165
|
+
feed?.handleWSEvent(event as unknown as WSEvent);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public pollFromState = (id: string) => this.polls_by_id.get(id);
|
|
172
|
+
|
|
173
|
+
public hydratePollCache(activities: ActivityResponse[]) {
|
|
174
|
+
for (const activity of activities) {
|
|
175
|
+
if (!activity.poll) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const pollResponse = activity.poll;
|
|
179
|
+
const pollFromCache = this.pollFromState(pollResponse.id);
|
|
180
|
+
if (!pollFromCache) {
|
|
181
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
182
|
+
const poll = new StreamPoll({ client: this, poll: pollResponse });
|
|
183
|
+
this.polls_by_id.set(poll.id, poll);
|
|
184
|
+
} else {
|
|
185
|
+
// @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
|
|
186
|
+
pollFromCache.reinitializeState(pollResponse);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
connectUser = async (
|
|
192
|
+
user: UserRequest,
|
|
193
|
+
tokenProvider: string | (() => Promise<string>),
|
|
194
|
+
) => {
|
|
195
|
+
if (
|
|
196
|
+
this.state.getLatestValue().connectedUser !== undefined ||
|
|
197
|
+
this.wsConnection
|
|
198
|
+
) {
|
|
199
|
+
throw new Error(`Can't connect a new user, call "disconnectUser" first`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.tokenManager.setTokenOrProvider(tokenProvider);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
addConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
206
|
+
this.wsConnection = new StableWSConnection(
|
|
207
|
+
{
|
|
208
|
+
user,
|
|
209
|
+
baseUrl: this.apiClient.webSocketBaseUrl,
|
|
210
|
+
},
|
|
211
|
+
this.tokenManager,
|
|
212
|
+
this.connectionIdManager,
|
|
213
|
+
[decodeWSEvent],
|
|
214
|
+
);
|
|
215
|
+
this.wsConnection.on('all', (event) =>
|
|
216
|
+
this.eventDispatcher.dispatch(event),
|
|
217
|
+
);
|
|
218
|
+
const connectedEvent = await this.wsConnection.connect();
|
|
219
|
+
this.state.partialNext({ connectedUser: connectedEvent?.me });
|
|
220
|
+
} catch (err) {
|
|
221
|
+
await this.disconnectUser();
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
closePoll = async (request: {
|
|
227
|
+
poll_id: string;
|
|
228
|
+
}): Promise<StreamResponse<PollResponse>> => {
|
|
229
|
+
return await this.updatePollPartial({
|
|
230
|
+
poll_id: request.poll_id,
|
|
231
|
+
set: {
|
|
232
|
+
is_closed: true,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// @ts-expect-error API spec says file should be a string
|
|
238
|
+
uploadFile = (request: Omit<FileUploadRequest, 'file'> & { file: File }) => {
|
|
239
|
+
return super.uploadFile({
|
|
240
|
+
// @ts-expect-error API spec says file should be a string
|
|
241
|
+
file: request.file,
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// @ts-expect-error API spec says file should be a string
|
|
246
|
+
uploadImage = (
|
|
247
|
+
request: Omit<ImageUploadRequest, 'file'> & { file: File },
|
|
248
|
+
) => {
|
|
249
|
+
return super.uploadImage({
|
|
250
|
+
// @ts-expect-error API spec says file should be a string
|
|
251
|
+
file: request.file,
|
|
252
|
+
// @ts-expect-error form data will only work if this is a string
|
|
253
|
+
upload_sizes: JSON.stringify(request.upload_sizes),
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
queryPollAnswers = async (
|
|
258
|
+
request: QueryPollVotesRequest & { poll_id: string; user_id?: string },
|
|
259
|
+
): Promise<StreamResponse<PollVotesResponse>> => {
|
|
260
|
+
const filter = request.filter ?? {};
|
|
261
|
+
const queryPollAnswersFilter = {
|
|
262
|
+
...filter,
|
|
263
|
+
is_answer: true,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const queryPollAnswersRequest = {
|
|
267
|
+
...request,
|
|
268
|
+
filter: queryPollAnswersFilter,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return await this.queryPollVotes(queryPollAnswersRequest);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
queryPollOptionVotes = async (
|
|
275
|
+
request: QueryPollVotesRequest & {
|
|
276
|
+
filter: QueryPollVotesRequest['filter'] & { option_id: string };
|
|
277
|
+
poll_id: string;
|
|
278
|
+
user_id?: string;
|
|
279
|
+
},
|
|
280
|
+
): Promise<StreamResponse<PollVotesResponse>> => {
|
|
281
|
+
return await this.queryPollVotes(request);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
disconnectUser = async () => {
|
|
285
|
+
if (this.wsConnection) {
|
|
286
|
+
this.wsConnection?.offAll();
|
|
287
|
+
await this.wsConnection?.disconnect();
|
|
288
|
+
this.wsConnection = undefined;
|
|
289
|
+
}
|
|
290
|
+
removeConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
291
|
+
|
|
292
|
+
this.connectionIdManager.reset();
|
|
293
|
+
this.tokenManager.reset();
|
|
294
|
+
this.state.partialNext({ connectedUser: undefined });
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
on = this.eventDispatcher.on;
|
|
298
|
+
off = this.eventDispatcher.off;
|
|
299
|
+
|
|
300
|
+
feed = (groupId: string, id: string) => {
|
|
301
|
+
return this.getOrCreateActiveFeed(groupId, id);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
async queryFeeds(request?: QueryFeedsRequest) {
|
|
305
|
+
const response = await this.feedsQueryFeeds(request);
|
|
306
|
+
|
|
307
|
+
const feeds = response.feeds.map((f) =>
|
|
308
|
+
this.getOrCreateActiveFeed(f.group_id, f.id, f),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
feeds,
|
|
313
|
+
next: response.next,
|
|
314
|
+
prev: response.prev,
|
|
315
|
+
metadata: response.metadata,
|
|
316
|
+
duration: response.duration,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private readonly getOrCreateActiveFeed = (
|
|
321
|
+
group: string,
|
|
322
|
+
id: string,
|
|
323
|
+
data?: FeedResponse,
|
|
324
|
+
) => {
|
|
325
|
+
const fid = `${group}:${id}`;
|
|
326
|
+
if (this.activeFeeds[fid]) {
|
|
327
|
+
return this.activeFeeds[fid];
|
|
328
|
+
} else {
|
|
329
|
+
const feed = new Feed(this, group, id, data);
|
|
330
|
+
this.activeFeeds[fid] = feed;
|
|
331
|
+
return feed;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
private readonly updateNetworkConnectionStatus = (
|
|
336
|
+
event: { type: 'online' | 'offline' } | Event,
|
|
337
|
+
) => {
|
|
338
|
+
const networkEvent: NetworkChangedEvent = {
|
|
339
|
+
type: 'network.changed',
|
|
340
|
+
online: event.type === 'online',
|
|
341
|
+
};
|
|
342
|
+
this.eventDispatcher.dispatch(networkEvent);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
private findActiveFeedByActivityId(activityId: string) {
|
|
346
|
+
return Object.values(this.activeFeeds).filter((feed) =>
|
|
347
|
+
feed.currentState.activities?.some(
|
|
348
|
+
(activity) => activity.id === activityId,
|
|
349
|
+
),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BaseSearchSource } from './BaseSearchSource';
|
|
2
|
+
import type { SearchSourceOptions } from './BaseSearchSource';
|
|
3
|
+
|
|
4
|
+
import { FeedsClient } from '../FeedsClient';
|
|
5
|
+
import { ActivityResponse } from '../gen/models';
|
|
6
|
+
|
|
7
|
+
export class ActivitySearchSource extends BaseSearchSource<ActivityResponse> {
|
|
8
|
+
readonly type = 'activity' as const;
|
|
9
|
+
private readonly client: FeedsClient;
|
|
10
|
+
// messageSearchChannelFilters: ChannelFilters | undefined;
|
|
11
|
+
// messageSearchFilters: MessageFilters | undefined;
|
|
12
|
+
// messageSearchSort: SearchMessageSort | undefined;
|
|
13
|
+
// channelQueryFilters: ChannelFilters | undefined;
|
|
14
|
+
// channelQuerySort: ChannelSort | undefined;
|
|
15
|
+
// channelQueryOptions: Omit<ChannelOptions, 'limit' | 'offset'> | undefined;
|
|
16
|
+
|
|
17
|
+
constructor(client: FeedsClient, options?: SearchSourceOptions) {
|
|
18
|
+
super(options);
|
|
19
|
+
this.client = client;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected async query(searchQuery: string) {
|
|
23
|
+
const { connectedUser } = this.client.state.getLatestValue();
|
|
24
|
+
if (!connectedUser) return { items: [] };
|
|
25
|
+
|
|
26
|
+
const { activities: items, next } = await this.client.queryActivities({
|
|
27
|
+
sort: [{ direction: -1, field: 'created_at' }],
|
|
28
|
+
filter: { text: { $autocomplete: searchQuery } },
|
|
29
|
+
limit: 10,
|
|
30
|
+
next: this.next ?? undefined,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return { items, next };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
protected filterQueryResults(items: ActivityResponse[]) {
|
|
37
|
+
return items;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
// filter: {
|
|
43
|
+
// 'feed.name': { $autocomplete: searchQuery }
|
|
44
|
+
// 'feed.description': { $autocomplete: searchQuery }
|
|
45
|
+
// 'created_by.name': { $autocomplete: searchQuery }
|
|
46
|
+
// },
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import axios, {
|
|
2
|
+
AxiosError,
|
|
3
|
+
AxiosInstance,
|
|
4
|
+
AxiosResponse,
|
|
5
|
+
RawAxiosRequestHeaders,
|
|
6
|
+
} from 'axios';
|
|
7
|
+
import { RequestMetadata, StreamApiError, FeedsClientOptions } from './types';
|
|
8
|
+
import { getRateLimitFromResponseHeader } from './rate-limit';
|
|
9
|
+
import { KnownCodes, randomId } from './utils';
|
|
10
|
+
import { TokenManager } from './TokenManager';
|
|
11
|
+
import { ConnectionIdManager } from './ConnectionIdManager';
|
|
12
|
+
|
|
13
|
+
export class ApiClient {
|
|
14
|
+
public readonly baseUrl: string;
|
|
15
|
+
private readonly axiosInstance: AxiosInstance;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
public readonly apiKey: string,
|
|
19
|
+
private readonly tokenManager: TokenManager,
|
|
20
|
+
private readonly connectionIdManager: ConnectionIdManager,
|
|
21
|
+
options?: FeedsClientOptions,
|
|
22
|
+
) {
|
|
23
|
+
this.baseUrl = options?.base_url ?? 'https://video.stream-io-api.com';
|
|
24
|
+
this.axiosInstance = axios.create({
|
|
25
|
+
baseURL: this.baseUrl,
|
|
26
|
+
timeout: options?.timeout ?? 3000,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
sendRequest = async <T>(
|
|
31
|
+
method: string,
|
|
32
|
+
url: string,
|
|
33
|
+
pathParams?: Record<string, string>,
|
|
34
|
+
queryParams?: Record<string, any>,
|
|
35
|
+
body?: any,
|
|
36
|
+
requestContentType?: string,
|
|
37
|
+
): Promise<{ body: T; metadata: RequestMetadata }> => {
|
|
38
|
+
queryParams = queryParams ?? {};
|
|
39
|
+
queryParams.api_key = this.apiKey;
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
queryParams?.watch ||
|
|
43
|
+
queryParams?.presence ||
|
|
44
|
+
queryParams?.payload?.watch ||
|
|
45
|
+
queryParams?.payload?.presence ||
|
|
46
|
+
body?.watch ||
|
|
47
|
+
body?.presence
|
|
48
|
+
) {
|
|
49
|
+
const connectionId = await this.connectionIdManager.getConnectionId();
|
|
50
|
+
queryParams.connection_id = connectionId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let requestUrl = url;
|
|
54
|
+
if (pathParams) {
|
|
55
|
+
Object.keys(pathParams).forEach((paramName) => {
|
|
56
|
+
requestUrl = requestUrl.replace(
|
|
57
|
+
`{${paramName}}`,
|
|
58
|
+
encodeURIComponent(pathParams[paramName]),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const client_request_id = randomId();
|
|
63
|
+
|
|
64
|
+
const token = await this.tokenManager.getToken();
|
|
65
|
+
|
|
66
|
+
const headers: RawAxiosRequestHeaders = {
|
|
67
|
+
...this.commonHeaders,
|
|
68
|
+
Authorization: token,
|
|
69
|
+
'Content-Type': requestContentType ?? 'application/json',
|
|
70
|
+
'x-client-request-id': client_request_id,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const encodedBody =
|
|
74
|
+
requestContentType === 'multipart/form-data' ? new FormData() : body;
|
|
75
|
+
if (requestContentType === 'multipart/form-data') {
|
|
76
|
+
Object.keys(body).forEach((key) => {
|
|
77
|
+
encodedBody.append(key, body[key]);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const response = await this.axiosInstance.request<T>({
|
|
83
|
+
url: requestUrl,
|
|
84
|
+
method,
|
|
85
|
+
headers,
|
|
86
|
+
params: queryParams,
|
|
87
|
+
paramsSerializer: params => this.queryParamsStringify(params),
|
|
88
|
+
data: encodedBody,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const metadata: RequestMetadata = this.getRequestMetadata(
|
|
92
|
+
client_request_id,
|
|
93
|
+
response,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return { body: response.data, metadata };
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
if (this.isAxiosError(error)) {
|
|
99
|
+
if (!error.response) {
|
|
100
|
+
throw new StreamApiError(`Stream error ${error.message}`);
|
|
101
|
+
} else {
|
|
102
|
+
// Stream specific error response
|
|
103
|
+
const data = error.response.data as StreamApiError;
|
|
104
|
+
const code = data?.code ?? error.response.status;
|
|
105
|
+
const message = data?.message ?? error.response.statusText;
|
|
106
|
+
if (
|
|
107
|
+
code === KnownCodes.TOKEN_EXPIRED &&
|
|
108
|
+
error.response.status === 401 &&
|
|
109
|
+
!this.tokenManager.isStatic()
|
|
110
|
+
) {
|
|
111
|
+
await this.tokenManager.loadToken();
|
|
112
|
+
return await this.sendRequest(
|
|
113
|
+
method,
|
|
114
|
+
url,
|
|
115
|
+
pathParams,
|
|
116
|
+
queryParams,
|
|
117
|
+
body,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
throw new StreamApiError(
|
|
121
|
+
`Stream error code ${code}: ${message}`,
|
|
122
|
+
this.getRequestMetadata(client_request_id, error.response),
|
|
123
|
+
code,
|
|
124
|
+
undefined,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
throw new Error('Unknown error received during an API call', {
|
|
129
|
+
cause: error,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
get webSocketBaseUrl() {
|
|
136
|
+
const params = new URLSearchParams();
|
|
137
|
+
params.set('api_key', this.apiKey);
|
|
138
|
+
Object.keys(this.commonHeaders).forEach((key) => {
|
|
139
|
+
params.set(key, this.commonHeaders[key]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const wsBaseURL = this.baseUrl
|
|
143
|
+
.replace('http', 'ws')
|
|
144
|
+
.replace(':3030', ':8800');
|
|
145
|
+
|
|
146
|
+
return `${wsBaseURL}/api/v2/connect?${params.toString()}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private get commonHeaders(): Record<string, string> {
|
|
150
|
+
return {
|
|
151
|
+
'stream-auth-type': 'jwt',
|
|
152
|
+
// TODO: add version here
|
|
153
|
+
'X-Stream-Client': 'stream-feeds-js-',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private isAxiosError(error: any | AxiosError): error is AxiosError {
|
|
158
|
+
return typeof error.request !== 'undefined';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private readonly queryParamsStringify = (params: Record<string, any>) => {
|
|
162
|
+
const newParams = [];
|
|
163
|
+
for (const k in params) {
|
|
164
|
+
const param = params[k];
|
|
165
|
+
if (Array.isArray(param)) {
|
|
166
|
+
newParams.push(`${k}=${encodeURIComponent(param.join(','))}`);
|
|
167
|
+
} else if (param instanceof Date) {
|
|
168
|
+
newParams.push(param.toISOString());
|
|
169
|
+
} else if (typeof param === 'object') {
|
|
170
|
+
newParams.push(`${k}=${encodeURIComponent(JSON.stringify(param))}`);
|
|
171
|
+
} else {
|
|
172
|
+
if (
|
|
173
|
+
typeof param === 'string' ||
|
|
174
|
+
typeof param === 'number' ||
|
|
175
|
+
typeof param === 'boolean'
|
|
176
|
+
) {
|
|
177
|
+
newParams.push(`${k}=${encodeURIComponent(param)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return newParams.join('&');
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
private readonly getRequestMetadata = (
|
|
186
|
+
requestId: string,
|
|
187
|
+
response: AxiosResponse,
|
|
188
|
+
) => {
|
|
189
|
+
const response_headers = response.headers as Record<string, string>;
|
|
190
|
+
return {
|
|
191
|
+
client_request_id: requestId,
|
|
192
|
+
response_headers,
|
|
193
|
+
response_code: response.status,
|
|
194
|
+
rate_limit: getRateLimitFromResponseHeader(response_headers),
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
}
|