@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.
Files changed (96) hide show
  1. package/@react-bindings/index.ts +2 -0
  2. package/CHANGELOG.md +44 -0
  3. package/LICENSE +219 -0
  4. package/README.md +9 -0
  5. package/dist/@react-bindings/hooks/useComments.d.ts +12 -0
  6. package/dist/@react-bindings/hooks/useStateStore.d.ts +3 -0
  7. package/dist/@react-bindings/index.d.ts +2 -0
  8. package/dist/index-react-bindings.browser.cjs +56 -0
  9. package/dist/index-react-bindings.browser.cjs.map +1 -0
  10. package/dist/index-react-bindings.browser.js +53 -0
  11. package/dist/index-react-bindings.browser.js.map +1 -0
  12. package/dist/index-react-bindings.node.cjs +56 -0
  13. package/dist/index-react-bindings.node.cjs.map +1 -0
  14. package/dist/index-react-bindings.node.js +53 -0
  15. package/dist/index-react-bindings.node.js.map +1 -0
  16. package/dist/index.browser.cjs +5799 -0
  17. package/dist/index.browser.cjs.map +1 -0
  18. package/dist/index.browser.js +5782 -0
  19. package/dist/index.browser.js.map +1 -0
  20. package/dist/index.d.ts +13 -0
  21. package/dist/index.node.cjs +5799 -0
  22. package/dist/index.node.cjs.map +1 -0
  23. package/dist/index.node.js +5782 -0
  24. package/dist/index.node.js.map +1 -0
  25. package/dist/src/Feed.d.ts +109 -0
  26. package/dist/src/FeedsClient.d.ts +63 -0
  27. package/dist/src/ModerationClient.d.ts +3 -0
  28. package/dist/src/common/ActivitySearchSource.d.ts +17 -0
  29. package/dist/src/common/ApiClient.d.ts +20 -0
  30. package/dist/src/common/BaseSearchSource.d.ts +87 -0
  31. package/dist/src/common/ConnectionIdManager.d.ts +11 -0
  32. package/dist/src/common/EventDispatcher.d.ts +11 -0
  33. package/dist/src/common/FeedSearchSource.d.ts +17 -0
  34. package/dist/src/common/Poll.d.ts +34 -0
  35. package/dist/src/common/SearchController.d.ts +41 -0
  36. package/dist/src/common/StateStore.d.ts +124 -0
  37. package/dist/src/common/TokenManager.d.ts +29 -0
  38. package/dist/src/common/UserSearchSource.d.ts +17 -0
  39. package/dist/src/common/gen-imports.d.ts +2 -0
  40. package/dist/src/common/rate-limit.d.ts +2 -0
  41. package/dist/src/common/real-time/StableWSConnection.d.ts +144 -0
  42. package/dist/src/common/real-time/event-models.d.ts +36 -0
  43. package/dist/src/common/types.d.ts +29 -0
  44. package/dist/src/common/utils.d.ts +54 -0
  45. package/dist/src/gen/feeds/FeedApi.d.ts +26 -0
  46. package/dist/src/gen/feeds/FeedsApi.d.ts +237 -0
  47. package/dist/src/gen/model-decoders/decoders.d.ts +3 -0
  48. package/dist/src/gen/model-decoders/event-decoder-mapping.d.ts +6 -0
  49. package/dist/src/gen/models/index.d.ts +3437 -0
  50. package/dist/src/gen/moderation/ModerationApi.d.ts +21 -0
  51. package/dist/src/gen-imports.d.ts +3 -0
  52. package/dist/src/state-updates/activity-reaction-utils.d.ts +10 -0
  53. package/dist/src/state-updates/activity-utils.d.ts +13 -0
  54. package/dist/src/state-updates/bookmark-utils.d.ts +14 -0
  55. package/dist/src/types-internal.d.ts +4 -0
  56. package/dist/src/types.d.ts +13 -0
  57. package/dist/src/utils.d.ts +1 -0
  58. package/dist/tsconfig.tsbuildinfo +1 -0
  59. package/index.ts +13 -0
  60. package/package.json +85 -0
  61. package/src/Feed.ts +1070 -0
  62. package/src/FeedsClient.ts +352 -0
  63. package/src/ModerationClient.ts +3 -0
  64. package/src/common/ActivitySearchSource.ts +46 -0
  65. package/src/common/ApiClient.ts +197 -0
  66. package/src/common/BaseSearchSource.ts +238 -0
  67. package/src/common/ConnectionIdManager.ts +51 -0
  68. package/src/common/EventDispatcher.ts +52 -0
  69. package/src/common/FeedSearchSource.ts +94 -0
  70. package/src/common/Poll.ts +313 -0
  71. package/src/common/SearchController.ts +152 -0
  72. package/src/common/StateStore.ts +314 -0
  73. package/src/common/TokenManager.ts +112 -0
  74. package/src/common/UserSearchSource.ts +93 -0
  75. package/src/common/gen-imports.ts +2 -0
  76. package/src/common/rate-limit.ts +23 -0
  77. package/src/common/real-time/StableWSConnection.ts +761 -0
  78. package/src/common/real-time/event-models.ts +38 -0
  79. package/src/common/types.ts +40 -0
  80. package/src/common/utils.ts +194 -0
  81. package/src/gen/feeds/FeedApi.ts +129 -0
  82. package/src/gen/feeds/FeedsApi.ts +2192 -0
  83. package/src/gen/model-decoders/decoders.ts +1877 -0
  84. package/src/gen/model-decoders/event-decoder-mapping.ts +150 -0
  85. package/src/gen/models/index.ts +5882 -0
  86. package/src/gen/moderation/ModerationApi.ts +270 -0
  87. package/src/gen-imports.ts +3 -0
  88. package/src/state-updates/activity-reaction-utils.test.ts +348 -0
  89. package/src/state-updates/activity-reaction-utils.ts +107 -0
  90. package/src/state-updates/activity-utils.test.ts +257 -0
  91. package/src/state-updates/activity-utils.ts +80 -0
  92. package/src/state-updates/bookmark-utils.test.ts +383 -0
  93. package/src/state-updates/bookmark-utils.ts +157 -0
  94. package/src/types-internal.ts +5 -0
  95. package/src/types.ts +20 -0
  96. package/src/utils.ts +4 -0
@@ -0,0 +1,313 @@
1
+ import { StateStore } from './StateStore';
2
+ import type { FeedsClient } from '../FeedsClient';
3
+ import type {
4
+ PollVote,
5
+ QueryPollVotesRequest,
6
+ PollUpdatedFeedEvent,
7
+ WSEvent,
8
+ PollClosedFeedEvent,
9
+ PollVoteCastedFeedEvent,
10
+ PollVoteChangedFeedEvent,
11
+ PollVoteRemovedFeedEvent,
12
+ PollResponseData,
13
+ Poll as PollType,
14
+ } from '../gen/models';
15
+
16
+ const isPollUpdatedEvent = (
17
+ e: WSEvent,
18
+ ): e is { type: 'feeds.poll.updated' } & PollUpdatedFeedEvent =>
19
+ e.type === 'feeds.poll.updated';
20
+ const isPollClosedEventEvent = (
21
+ e: WSEvent,
22
+ ): e is { type: 'feeds.poll.closed' } & PollClosedFeedEvent =>
23
+ e.type === 'feeds.poll.closed';
24
+ const isPollVoteCastedEvent = (
25
+ e: WSEvent,
26
+ ): e is { type: 'feeds.poll.vote_casted' } & PollVoteCastedFeedEvent =>
27
+ e.type === 'feeds.poll.vote_casted';
28
+ const isPollVoteChangedEvent = (
29
+ e: WSEvent,
30
+ ): e is { type: 'feeds.poll.vote_changed' } & PollVoteChangedFeedEvent =>
31
+ e.type === 'feeds.poll.vote_changed';
32
+ const isPollVoteRemovedEvent = (
33
+ e: WSEvent,
34
+ ): e is { type: 'feeds.poll.vote_removed' } & PollVoteRemovedFeedEvent =>
35
+ e.type === 'feeds.poll.vote_removed';
36
+
37
+ export const isVoteAnswer = (vote: PollVote) => !!vote?.answer_text;
38
+
39
+ export type PollAnswersQueryParams = QueryPollVotesRequest & { poll_id: string; user_id?: string };
40
+
41
+ type OptionId = string;
42
+
43
+ export type PollState = Omit<PollType, 'own_votes' | 'id'> & {
44
+ last_activity_at: Date;
45
+ max_voted_option_ids: OptionId[];
46
+ own_votes_by_option_id: Record<OptionId, PollVote>;
47
+ own_answer?: PollVote; // each user can have only one answer
48
+ };
49
+
50
+ type PollInitOptions = {
51
+ client: FeedsClient;
52
+ poll: PollType;
53
+ };
54
+
55
+ export class StreamPoll {
56
+ public readonly state: StateStore<PollState>;
57
+ public id: string;
58
+ private readonly client: FeedsClient;
59
+
60
+ constructor({ client, poll }: PollInitOptions) {
61
+ this.client = client;
62
+ this.id = poll.id;
63
+
64
+ this.state = new StateStore<PollState>(
65
+ this.getInitialStateFromPollResponse(poll),
66
+ );
67
+ }
68
+
69
+ private readonly getInitialStateFromPollResponse = (poll: PollInitOptions['poll']) => {
70
+ const { own_votes, id, ...pollResponseForState } = poll;
71
+ const { ownAnswer, ownVotes } = own_votes?.reduce<{
72
+ ownVotes: PollVote[];
73
+ ownAnswer?: PollVote;
74
+ }>(
75
+ (acc, voteOrAnswer) => {
76
+ if (isVoteAnswer(voteOrAnswer)) {
77
+ acc.ownAnswer = voteOrAnswer;
78
+ } else {
79
+ acc.ownVotes.push(voteOrAnswer);
80
+ }
81
+ return acc;
82
+ },
83
+ { ownVotes: [] },
84
+ ) ?? { ownVotes: [] };
85
+
86
+ return {
87
+ ...pollResponseForState,
88
+ last_activity_at: new Date(),
89
+ max_voted_option_ids: getMaxVotedOptionIds(
90
+ pollResponseForState.vote_counts_by_option,
91
+ ),
92
+ own_answer: ownAnswer,
93
+ own_votes_by_option_id: getOwnVotesByOptionId(ownVotes),
94
+ };
95
+ };
96
+
97
+ public reinitializeState = (poll: PollInitOptions['poll']) => {
98
+ this.state.partialNext(this.getInitialStateFromPollResponse(poll));
99
+ };
100
+
101
+ get data(): PollState {
102
+ return this.state.getLatestValue();
103
+ }
104
+
105
+ public handlePollUpdated = (event: PollUpdatedFeedEvent) => {
106
+ if (event.poll?.id && event.poll.id !== this.id) return;
107
+ if (!isPollUpdatedEvent(event as WSEvent)) return;
108
+ const { id, ...pollData } = event.poll;
109
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
110
+ this.state.partialNext({
111
+ ...pollData,
112
+ last_activity_at: new Date(event.created_at),
113
+ });
114
+ };
115
+
116
+ public handlePollClosed = (event: PollClosedFeedEvent) => {
117
+ if (event.poll?.id && event.poll.id !== this.id) return;
118
+ if (!isPollClosedEventEvent(event as WSEvent)) return;
119
+ this.state.partialNext({
120
+ is_closed: true,
121
+ last_activity_at: new Date(event.created_at),
122
+ });
123
+ };
124
+
125
+ public handleVoteCasted = (event: PollVoteCastedFeedEvent) => {
126
+ if (event.poll?.id && event.poll.id !== this.id) return;
127
+ if (!isPollVoteCastedEvent(event as WSEvent)) return;
128
+ const currentState = this.data;
129
+ const isOwnVote = event.poll_vote.user_id === this.client.state.getLatestValue().connectedUser?.id;
130
+ let latestAnswers = [...currentState.latest_answers];
131
+ let ownAnswer = currentState.own_answer;
132
+ const ownVotesByOptionId = currentState.own_votes_by_option_id;
133
+ let maxVotedOptionIds = currentState.max_voted_option_ids;
134
+
135
+ if (isOwnVote) {
136
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
137
+ if (isVoteAnswer(event.poll_vote)) {
138
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
139
+ ownAnswer = event.poll_vote;
140
+ } else if (event.poll_vote.option_id) {
141
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
142
+ ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
143
+ }
144
+ }
145
+
146
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
147
+ if (isVoteAnswer(event.poll_vote)) {
148
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
149
+ latestAnswers = [event.poll_vote, ...latestAnswers];
150
+ } else {
151
+ maxVotedOptionIds = getMaxVotedOptionIds(
152
+ event.poll.vote_counts_by_option,
153
+ );
154
+ }
155
+
156
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option } = event.poll;
157
+ this.state.partialNext({
158
+ answers_count,
159
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
160
+ latest_votes_by_option,
161
+ vote_count,
162
+ vote_counts_by_option,
163
+ latest_answers: latestAnswers,
164
+ last_activity_at: new Date(event.created_at),
165
+ own_answer: ownAnswer,
166
+ own_votes_by_option_id: ownVotesByOptionId,
167
+ max_voted_option_ids: maxVotedOptionIds,
168
+ });
169
+ };
170
+
171
+ public handleVoteChanged = (event: PollVoteChangedFeedEvent) => {
172
+ // this event is triggered only when event.poll.enforce_unique_vote === true
173
+ if (event.poll?.id && event.poll.id !== this.id) return;
174
+ if (!isPollVoteChangedEvent(event as WSEvent)) return;
175
+ const currentState = this.data;
176
+ const isOwnVote = event.poll_vote.user_id === this.client.state.getLatestValue().connectedUser?.id;
177
+ let latestAnswers = [...currentState.latest_answers];
178
+ let ownAnswer = currentState.own_answer;
179
+ let ownVotesByOptionId = currentState.own_votes_by_option_id;
180
+ let maxVotedOptionIds = currentState.max_voted_option_ids;
181
+
182
+ if (isOwnVote) {
183
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
184
+ if (isVoteAnswer(event.poll_vote)) {
185
+ latestAnswers = [
186
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
187
+ event.poll_vote,
188
+ ...latestAnswers.filter((answer) => answer.id !== event.poll_vote.id),
189
+ ];
190
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
191
+ ownAnswer = event.poll_vote;
192
+ } else if (event.poll_vote.option_id) {
193
+ if (event.poll.enforce_unique_vote) {
194
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
195
+ ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote };
196
+ } else {
197
+ ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce<
198
+ Record<OptionId, PollVote>
199
+ >((acc, [optionId, vote]) => {
200
+ if (
201
+ optionId !== event.poll_vote.option_id &&
202
+ vote.id === event.poll_vote.id
203
+ ) {
204
+ return acc;
205
+ }
206
+ acc[optionId] = vote;
207
+ return acc;
208
+ }, {});
209
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
210
+ ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
211
+ }
212
+
213
+ if (ownAnswer?.id === event.poll_vote.id) {
214
+ ownAnswer = undefined;
215
+ }
216
+ maxVotedOptionIds = getMaxVotedOptionIds(
217
+ event.poll.vote_counts_by_option,
218
+ );
219
+ }
220
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
221
+ } else if (isVoteAnswer(event.poll_vote)) {
222
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
223
+ latestAnswers = [event.poll_vote, ...latestAnswers];
224
+ } else {
225
+ maxVotedOptionIds = getMaxVotedOptionIds(
226
+ event.poll.vote_counts_by_option,
227
+ );
228
+ }
229
+
230
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option } = event.poll;
231
+ this.state.partialNext({
232
+ answers_count,
233
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
234
+ latest_votes_by_option,
235
+ vote_count,
236
+ vote_counts_by_option,
237
+ latest_answers: latestAnswers,
238
+ last_activity_at: new Date(event.created_at),
239
+ own_answer: ownAnswer,
240
+ own_votes_by_option_id:ownVotesByOptionId,
241
+ max_voted_option_ids: maxVotedOptionIds,
242
+ });
243
+ };
244
+
245
+ public handleVoteRemoved = (event: PollVoteRemovedFeedEvent) => {
246
+ if (event.poll?.id && event.poll.id !== this.id) return;
247
+ if (!isPollVoteRemovedEvent(event as WSEvent)) return;
248
+ const currentState = this.data;
249
+ const isOwnVote = event.poll_vote.user_id === this.client.state.getLatestValue().connectedUser?.id;
250
+ let latestAnswers = [...currentState.latest_answers];
251
+ let ownAnswer = currentState.own_answer;
252
+ const ownVotesByOptionId = { ...currentState.own_votes_by_option_id };
253
+ let maxVotedOptionIds = currentState.max_voted_option_ids;
254
+
255
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
256
+ if (isVoteAnswer(event.poll_vote)) {
257
+ latestAnswers = latestAnswers.filter(
258
+ (answer) => answer.id !== event.poll_vote.id,
259
+ );
260
+ if (isOwnVote) {
261
+ ownAnswer = undefined;
262
+ }
263
+ } else {
264
+ maxVotedOptionIds = getMaxVotedOptionIds(
265
+ event.poll.vote_counts_by_option,
266
+ );
267
+ if (isOwnVote && event.poll_vote.option_id) {
268
+
269
+ delete ownVotesByOptionId[event.poll_vote.option_id];
270
+ }
271
+ }
272
+
273
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option } = event.poll;
274
+ this.state.partialNext({
275
+ answers_count,
276
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
277
+ latest_votes_by_option,
278
+ vote_count,
279
+ vote_counts_by_option,
280
+ latest_answers: latestAnswers,
281
+ last_activity_at: new Date(event.created_at),
282
+ own_answer: ownAnswer,
283
+ own_votes_by_option_id: ownVotesByOptionId,
284
+ max_voted_option_ids: maxVotedOptionIds,
285
+ });
286
+ };
287
+ }
288
+
289
+ function getMaxVotedOptionIds(
290
+ voteCountsByOption: PollResponseData['vote_counts_by_option'],
291
+ ) {
292
+ let maxVotes = 0;
293
+ let winningOptions: string[] = [];
294
+ for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
295
+ if (count > maxVotes) {
296
+ winningOptions = [id];
297
+ maxVotes = count;
298
+ } else if (count === maxVotes) {
299
+ winningOptions.push(id);
300
+ }
301
+ }
302
+ return winningOptions;
303
+ }
304
+
305
+ function getOwnVotesByOptionId(ownVotes: PollVote[]) {
306
+ return !ownVotes
307
+ ? ({} satisfies Record<OptionId, PollVote>)
308
+ : ownVotes.reduce<Record<OptionId, PollVote>>((acc, vote) => {
309
+ if (isVoteAnswer(vote) || !vote.option_id) return acc;
310
+ acc[vote.option_id] = vote;
311
+ return acc;
312
+ }, {});
313
+ }
@@ -0,0 +1,152 @@
1
+ import { StateStore } from './StateStore';
2
+ import type { SearchSource } from './BaseSearchSource';
3
+
4
+ export type SearchControllerState = {
5
+ isActive: boolean;
6
+ searchQuery: string;
7
+ sources: SearchSource[];
8
+ };
9
+
10
+ export type InternalSearchControllerState = {};
11
+
12
+ export type SearchControllerConfig = {
13
+ // The controller will make sure there is always exactly one active source. Enabled by default.
14
+ keepSingleActiveSource: boolean;
15
+ };
16
+
17
+ export type SearchControllerOptions = {
18
+ config?: Partial<SearchControllerConfig>;
19
+ sources?: SearchSource[];
20
+ };
21
+
22
+ export class SearchController {
23
+ /**
24
+ * Not intended for direct use by integrators, might be removed without notice resulting in
25
+ * broken integrations.
26
+ */
27
+ _internalState: StateStore<InternalSearchControllerState>;
28
+ state: StateStore<SearchControllerState>;
29
+ config: SearchControllerConfig;
30
+
31
+ constructor({ config, sources }: SearchControllerOptions = {}) {
32
+ this.state = new StateStore<SearchControllerState>({
33
+ isActive: false,
34
+ searchQuery: '',
35
+ sources: sources ?? [],
36
+ });
37
+ this._internalState = new StateStore<InternalSearchControllerState>({});
38
+ this.config = { keepSingleActiveSource: true, ...config };
39
+ }
40
+
41
+ get hasNext() {
42
+ return this.sources.some((source) => source.hasNext);
43
+ }
44
+
45
+ get sources() {
46
+ return this.state.getLatestValue().sources;
47
+ }
48
+
49
+ get activeSources() {
50
+ return this.state.getLatestValue().sources.filter((s) => s.isActive);
51
+ }
52
+
53
+ get isActive() {
54
+ return this.state.getLatestValue().isActive;
55
+ }
56
+
57
+ get searchQuery() {
58
+ return this.state.getLatestValue().searchQuery;
59
+ }
60
+
61
+ get searchSourceTypes(): Array<SearchSource['type']> {
62
+ return this.sources.map((s) => s.type);
63
+ }
64
+
65
+ addSource = (source: SearchSource) => {
66
+ this.state.partialNext({
67
+ sources: [...this.sources, source],
68
+ });
69
+ };
70
+
71
+ getSource = (sourceType: SearchSource['type']) =>
72
+ this.sources.find((s) => s.type === sourceType);
73
+
74
+ removeSource = (sourceType: SearchSource['type']) => {
75
+ const newSources = this.sources.filter((s) => s.type !== sourceType);
76
+ if (newSources.length === this.sources.length) return;
77
+ this.state.partialNext({ sources: newSources });
78
+ };
79
+
80
+ activateSource = (sourceType: SearchSource['type']) => {
81
+ const source = this.getSource(sourceType);
82
+ if (!source || source.isActive) return;
83
+ if (this.config.keepSingleActiveSource) {
84
+ this.sources.forEach((s) => {
85
+ if (s.type !== sourceType) {
86
+ s.deactivate();
87
+ }
88
+ });
89
+ }
90
+ source.activate();
91
+ this.state.partialNext({ sources: [...this.sources] });
92
+ };
93
+
94
+ deactivateSource = (sourceType: SearchSource['type']) => {
95
+ const source = this.getSource(sourceType);
96
+ if (!source?.isActive) return;
97
+ if (this.activeSources.length === 1) return;
98
+ source.deactivate();
99
+ this.state.partialNext({ sources: [...this.sources] });
100
+ };
101
+
102
+ activate = () => {
103
+ if (!this.activeSources.length) {
104
+ const sourcesToActivate = this.config.keepSingleActiveSource
105
+ ? this.sources.slice(0, 1)
106
+ : this.sources;
107
+ sourcesToActivate.forEach((s) => s.activate());
108
+ }
109
+ if (this.isActive) return;
110
+ this.state.partialNext({ isActive: true });
111
+ };
112
+
113
+ search = async (searchQuery?: string) => {
114
+ const searchedSources = this.activeSources;
115
+ this.state.partialNext({
116
+ searchQuery,
117
+ });
118
+ await Promise.all(
119
+ searchedSources.map((source) => source.search(searchQuery)),
120
+ );
121
+ };
122
+
123
+ cancelSearchQueries = () => {
124
+ this.activeSources.forEach((s) => s.cancelScheduledQuery());
125
+ };
126
+
127
+ clear = () => {
128
+ this.cancelSearchQueries();
129
+ this.sources.forEach((source) =>
130
+ source.state.next({ ...source.initialState, isActive: source.isActive }),
131
+ );
132
+ this.state.next((current) => ({
133
+ ...current,
134
+ isActive: true,
135
+ queriesInProgress: [],
136
+ searchQuery: '',
137
+ }));
138
+ };
139
+
140
+ exit = () => {
141
+ this.cancelSearchQueries();
142
+ this.sources.forEach((source) =>
143
+ source.state.next({ ...source.initialState, isActive: source.isActive }),
144
+ );
145
+ this.state.next((current) => ({
146
+ ...current,
147
+ isActive: false,
148
+ queriesInProgress: [],
149
+ searchQuery: '',
150
+ }));
151
+ };
152
+ }