@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,238 @@
1
+ import { StateStore } from './StateStore';
2
+ import { debounce, type DebouncedFunc } from './utils';
3
+
4
+ export type SearchSourceType =
5
+ | 'channels'
6
+ | 'users'
7
+ | 'messages'
8
+ | (string & {});
9
+ export type QueryReturnValue<T> = { items: T[]; next?: string | null };
10
+ export type DebounceOptions = {
11
+ debounceMs: number;
12
+ };
13
+ type DebouncedExecQueryFunction = DebouncedFunc<
14
+ (searchString?: string) => Promise<void>
15
+ >;
16
+
17
+
18
+ export interface SearchSource<T = any> {
19
+ activate(): void;
20
+
21
+ cancelScheduledQuery(): void;
22
+
23
+ canExecuteQuery(newSearchString?: string): boolean;
24
+
25
+ deactivate(): void;
26
+
27
+ readonly hasNext: boolean;
28
+ readonly hasResults: boolean;
29
+ readonly initialState: SearchSourceState<T>;
30
+ readonly isActive: boolean;
31
+ readonly isLoading: boolean;
32
+ readonly items: T[] | undefined;
33
+ readonly lastQueryError: Error | undefined;
34
+ readonly next: string | undefined | null;
35
+ readonly offset: number | undefined;
36
+
37
+ resetState(): void;
38
+
39
+ search(text?: string): Promise<void> | undefined;
40
+
41
+ readonly searchQuery: string;
42
+
43
+ setDebounceOptions(options: DebounceOptions): void;
44
+
45
+ readonly state: StateStore<SearchSourceState<T>>;
46
+ readonly type: SearchSourceType;
47
+ }
48
+
49
+
50
+ export type SearchSourceState<T = any> = {
51
+ hasNext: boolean;
52
+ isActive: boolean;
53
+ isLoading: boolean;
54
+ items: T[] | undefined;
55
+ searchQuery: string;
56
+ lastQueryError?: Error;
57
+ next?: string | null;
58
+ offset?: number;
59
+ };
60
+ export type SearchSourceOptions = {
61
+ /** The number of milliseconds to debounce the search query. The default interval is 300ms. */
62
+ debounceMs?: number;
63
+ pageSize?: number;
64
+ };
65
+ const DEFAULT_SEARCH_SOURCE_OPTIONS: Required<SearchSourceOptions> = {
66
+ debounceMs: 300,
67
+ pageSize: 10,
68
+ } as const;
69
+
70
+ export abstract class BaseSearchSource<T> implements SearchSource<T> {
71
+ state: StateStore<SearchSourceState<T>>;
72
+ protected pageSize: number;
73
+ abstract readonly type: SearchSourceType;
74
+ protected searchDebounced!: DebouncedExecQueryFunction;
75
+
76
+ protected constructor(options?: SearchSourceOptions) {
77
+ const { debounceMs, pageSize } = {
78
+ ...DEFAULT_SEARCH_SOURCE_OPTIONS,
79
+ ...options,
80
+ };
81
+ this.pageSize = pageSize;
82
+ this.state = new StateStore<SearchSourceState<T>>(this.initialState);
83
+ this.setDebounceOptions({ debounceMs });
84
+ }
85
+
86
+ get lastQueryError() {
87
+ return this.state.getLatestValue().lastQueryError;
88
+ }
89
+
90
+ get hasNext() {
91
+ return this.state.getLatestValue().hasNext;
92
+ }
93
+
94
+ get hasResults() {
95
+ return Array.isArray(this.state.getLatestValue().items);
96
+ }
97
+
98
+ get isActive() {
99
+ return this.state.getLatestValue().isActive;
100
+ }
101
+
102
+ get isLoading() {
103
+ return this.state.getLatestValue().isLoading;
104
+ }
105
+
106
+ get initialState() {
107
+ return {
108
+ hasNext: true,
109
+ isActive: false,
110
+ isLoading: false,
111
+ items: undefined,
112
+ lastQueryError: undefined,
113
+ next: undefined,
114
+ offset: 0,
115
+ searchQuery: '',
116
+ };
117
+ }
118
+
119
+ get items() {
120
+ return this.state.getLatestValue().items;
121
+ }
122
+
123
+ get next() {
124
+ return this.state.getLatestValue().next;
125
+ }
126
+
127
+ get offset() {
128
+ return this.state.getLatestValue().offset;
129
+ }
130
+
131
+ get searchQuery() {
132
+ return this.state.getLatestValue().searchQuery;
133
+ }
134
+
135
+ protected abstract query(searchQuery: string): Promise<QueryReturnValue<T>>;
136
+
137
+ protected abstract filterQueryResults(items: T[]): T[] | Promise<T[]>;
138
+
139
+ setDebounceOptions = ({ debounceMs }: DebounceOptions) => {
140
+ this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
141
+ };
142
+
143
+ activate = () => {
144
+ if (this.isActive) return;
145
+ this.state.partialNext({ isActive: true });
146
+ };
147
+
148
+ deactivate = () => {
149
+ if (!this.isActive) return;
150
+ this.state.partialNext({ isActive: false });
151
+ };
152
+
153
+ canExecuteQuery = (newSearchString?: string) => {
154
+ const hasNewSearchQuery = typeof newSearchString !== 'undefined';
155
+ const searchString = newSearchString ?? this.searchQuery;
156
+ return !!(
157
+ this.isActive &&
158
+ !this.isLoading &&
159
+ (this.hasNext || hasNewSearchQuery) &&
160
+ searchString
161
+ );
162
+ };
163
+
164
+ protected getStateBeforeFirstQuery(
165
+ newSearchString: string,
166
+ ): SearchSourceState<T> {
167
+ return {
168
+ ...this.initialState,
169
+ isActive: this.isActive,
170
+ isLoading: true,
171
+ searchQuery: newSearchString,
172
+ };
173
+ }
174
+
175
+ protected getStateAfterQuery(
176
+ stateUpdate: Partial<SearchSourceState<T>>,
177
+ isFirstPage: boolean,
178
+ ): SearchSourceState<T> {
179
+ const current = this.state.getLatestValue();
180
+ return {
181
+ ...current,
182
+ lastQueryError: undefined, // reset lastQueryError that can be overridden by the stateUpdate
183
+ ...stateUpdate,
184
+ isLoading: false,
185
+ items: isFirstPage
186
+ ? stateUpdate.items
187
+ : [...(this.items ?? []), ...(stateUpdate.items ?? [])],
188
+ };
189
+ }
190
+
191
+ async executeQuery(newSearchString?: string) {
192
+ if (!this.canExecuteQuery(newSearchString)) return;
193
+ const hasNewSearchQuery = typeof newSearchString !== 'undefined';
194
+ const searchString = newSearchString ?? this.searchQuery;
195
+
196
+ if (hasNewSearchQuery) {
197
+ this.state.next(this.getStateBeforeFirstQuery(newSearchString ?? ''));
198
+ } else {
199
+ this.state.partialNext({ isLoading: true });
200
+ }
201
+
202
+ const stateUpdate: Partial<SearchSourceState<T>> = {};
203
+ try {
204
+ const results = await this.query(searchString);
205
+ if (!results) return;
206
+ const { items, next } = results;
207
+
208
+ if (typeof next === 'string' || next === null) {
209
+ stateUpdate.next = next;
210
+ stateUpdate.hasNext = !!next;
211
+ } else {
212
+ stateUpdate.offset = (this.offset ?? 0) + items.length;
213
+ stateUpdate.hasNext = items.length === this.pageSize;
214
+ }
215
+
216
+ stateUpdate.items = await this.filterQueryResults(items);
217
+ } catch (e) {
218
+ stateUpdate.lastQueryError = e as Error;
219
+ } finally {
220
+ this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery));
221
+ }
222
+ }
223
+
224
+ search = (searchQuery?: string) => this.searchDebounced(searchQuery);
225
+
226
+ cancelScheduledQuery() {
227
+ this.searchDebounced.cancel();
228
+ }
229
+
230
+ resetState() {
231
+ this.state.next(this.initialState);
232
+ }
233
+
234
+ resetStateAndActivate() {
235
+ this.resetState();
236
+ this.activate();
237
+ }
238
+ }
@@ -0,0 +1,51 @@
1
+ export class ConnectionIdManager {
2
+ loadConnectionIdPromise: Promise<string> | undefined;
3
+ connectionId?: string;
4
+ private resolve?: (connectionId: string) => void;
5
+ private reject?: (reason: any) => void;
6
+
7
+ reset = () => {
8
+ this.loadConnectionIdPromise = undefined;
9
+ this.resolve = undefined;
10
+ this.reject = undefined;
11
+ this.connectionId = undefined;
12
+ };
13
+
14
+ resetConnectionIdPromise = () => {
15
+ this.connectionId = undefined;
16
+ this.loadConnectionIdPromise = new Promise<string>((resolve, reject) => {
17
+ this.resolve = resolve;
18
+ this.reject = reject;
19
+ });
20
+ };
21
+
22
+ resolveConnectionidPromise = (connectionId: string) => {
23
+ this.connectionId = connectionId;
24
+ this.resolve?.(connectionId);
25
+ this.loadConnectionIdPromise = undefined;
26
+ this.resolve = undefined;
27
+ this.reject = undefined;
28
+ };
29
+
30
+ rejectConnectionIdPromise = (reason: any) => {
31
+ this.connectionId = undefined;
32
+ this.reject?.(reason);
33
+ this.loadConnectionIdPromise = undefined;
34
+ this.resolve = undefined;
35
+ this.reject = undefined;
36
+ };
37
+
38
+ getConnectionId = () => {
39
+ if (this.connectionId) {
40
+ return this.connectionId;
41
+ }
42
+
43
+ if (this.loadConnectionIdPromise) {
44
+ return this.loadConnectionIdPromise;
45
+ }
46
+
47
+ throw new Error(
48
+ `No connection id was provided when trying to query with presence/watch. You should call "connectUser" to get a WebSocket connection id`,
49
+ );
50
+ };
51
+ }
@@ -0,0 +1,52 @@
1
+ import { FeedsEvent } from '../types';
2
+ import { LogLevel } from './types';
3
+
4
+ export class EventDispatcher<
5
+ Type extends string = FeedsEvent['type'],
6
+ Event extends { type: string } = FeedsEvent,
7
+ > {
8
+ private subscribers: Partial<
9
+ Record<Type | 'all', Array<(event: Event) => void> | undefined>
10
+ > = {};
11
+
12
+ private readonly logger = (level: LogLevel, message: string, extra: any) => {
13
+ // TODO implement logging
14
+ console.log(level, message, extra);
15
+ };
16
+
17
+ dispatch = (event: Event) => {
18
+ const listeners = [
19
+ ...(this.subscribers[event.type as Type] ?? []),
20
+ ...(this.subscribers.all ?? []),
21
+ ];
22
+ for (const fn of listeners) {
23
+ try {
24
+ fn(event);
25
+ } catch (e) {
26
+ // TODO: do we really want to silence this error?
27
+ this.logger('warn', 'Listener failed with error', e);
28
+ }
29
+ }
30
+ };
31
+
32
+ on = (eventName: Type | 'all', handler: (event: Event) => void) => {
33
+ (this.subscribers[eventName] ??= []).push(handler);
34
+ return () => {
35
+ this.off(eventName, handler);
36
+ };
37
+ };
38
+
39
+ off = (eventName: Type | 'all', handler: (event: Event) => void) => {
40
+ this.subscribers[eventName] = (this.subscribers[eventName] ?? []).filter(
41
+ (f) => f !== handler,
42
+ );
43
+ };
44
+
45
+ offAll = (eventName?: Type | 'all') => {
46
+ if (eventName) {
47
+ this.subscribers[eventName] = [];
48
+ } else {
49
+ this.subscribers = {};
50
+ }
51
+ };
52
+ }
@@ -0,0 +1,94 @@
1
+ import { BaseSearchSource } from './BaseSearchSource';
2
+ import type { SearchSourceOptions } from './BaseSearchSource';
3
+
4
+ import { FeedsClient } from '../FeedsClient';
5
+ import { Feed } from '../Feed';
6
+
7
+ export class FeedSearchSource extends BaseSearchSource<Feed> {
8
+ readonly type = 'feed' 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 channelFilters: ChannelFilters = {
27
+ // members: { $in: [this.client.userID] },
28
+ // ...this.messageSearchChannelFilters,
29
+ // } as ChannelFilters;
30
+
31
+ // const messageFilters: MessageFilters = {
32
+ // text: searchQuery,
33
+ // type: 'regular', // FIXME: type: 'reply' resp. do not filter by type and allow to jump to a message in a thread - missing support
34
+ // ...this.messageSearchFilters,
35
+ // } as MessageFilters;
36
+
37
+ // const sort: SearchMessageSort = {
38
+ // created_at: -1,
39
+ // ...this.messageSearchSort,
40
+ // };
41
+
42
+ // const options = {
43
+ // limit: this.pageSize,
44
+ // next: this.next,
45
+ // sort,
46
+ // } as SearchOptions;
47
+
48
+ // const { next, results } = await this.client.search(
49
+ // channelFilters,
50
+ // messageFilters,
51
+ // options,
52
+ // );
53
+ // const items = results.map(({ message }) => message);
54
+
55
+ // const cids = Array.from(
56
+ // items.reduce((acc, message) => {
57
+ // if (message.cid && !this.client.activeChannels[message.cid])
58
+ // acc.add(message.cid);
59
+ // return acc;
60
+ // }, new Set<string>()), // keep the cids unique
61
+ // );
62
+ // const allChannelsLoadedLocally = cids.length === 0;
63
+ // if (!allChannelsLoadedLocally) {
64
+ // await this.client.queryChannels(
65
+ // {
66
+ // cid: { $in: cids },
67
+ // ...this.channelQueryFilters,
68
+ // } as ChannelFilters,
69
+ // {
70
+ // last_message_at: -1,
71
+ // ...this.channelQuerySort,
72
+ // },
73
+ // this.channelQueryOptions,
74
+ // );
75
+ // }
76
+
77
+ const { feeds: items, next } = await this.client.queryFeeds({
78
+ filter: {
79
+ group_id: 'user',
80
+ $or: [
81
+ { name: { $autocomplete: searchQuery } },
82
+ { description: { $autocomplete: searchQuery } },
83
+ { 'created_by.name': { $autocomplete: searchQuery } },
84
+ ],
85
+ },
86
+ });
87
+
88
+ return { items, next };
89
+ }
90
+
91
+ protected filterQueryResults(items: Feed[]) {
92
+ return items;
93
+ }
94
+ }