stream-chat 9.4.0 → 9.5.1

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 (53) hide show
  1. package/dist/cjs/index.browser.cjs +663 -135
  2. package/dist/cjs/index.browser.cjs.map +4 -4
  3. package/dist/cjs/index.node.cjs +673 -136
  4. package/dist/cjs/index.node.cjs.map +4 -4
  5. package/dist/esm/index.js +663 -135
  6. package/dist/esm/index.js.map +4 -4
  7. package/dist/types/client.d.ts +53 -20
  8. package/dist/types/events.d.ts +4 -0
  9. package/dist/types/index.d.ts +3 -1
  10. package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +2 -2
  11. package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +2 -2
  12. package/dist/types/messageComposer/middleware/textComposer/types.d.ts +1 -1
  13. package/dist/types/pagination/BasePaginator.d.ts +69 -0
  14. package/dist/types/pagination/ReminderPaginator.d.ts +12 -0
  15. package/dist/types/pagination/index.d.ts +2 -0
  16. package/dist/types/reminders/Reminder.d.ts +37 -0
  17. package/dist/types/reminders/ReminderManager.d.ts +65 -0
  18. package/dist/types/reminders/ReminderTimer.d.ts +17 -0
  19. package/dist/types/reminders/index.d.ts +3 -0
  20. package/dist/types/search/BaseSearchSource.d.ts +87 -0
  21. package/dist/types/search/ChannelSearchSource.d.ts +17 -0
  22. package/dist/types/search/MessageSearchSource.d.ts +23 -0
  23. package/dist/types/search/SearchController.d.ts +44 -0
  24. package/dist/types/search/UserSearchSource.d.ts +16 -0
  25. package/dist/types/search/index.d.ts +5 -0
  26. package/dist/types/types.d.ts +43 -0
  27. package/package.json +1 -1
  28. package/src/channel.ts +2 -1
  29. package/src/client.ts +109 -40
  30. package/src/events.ts +6 -0
  31. package/src/index.ts +3 -1
  32. package/src/messageComposer/middleware/pollComposer/state.ts +4 -5
  33. package/src/messageComposer/middleware/textComposer/commands.ts +2 -2
  34. package/src/messageComposer/middleware/textComposer/mentions.ts +2 -2
  35. package/src/messageComposer/middleware/textComposer/types.ts +1 -1
  36. package/src/messageComposer/pollComposer.ts +3 -2
  37. package/src/pagination/BasePaginator.ts +184 -0
  38. package/src/pagination/ReminderPaginator.ts +38 -0
  39. package/src/pagination/index.ts +2 -0
  40. package/src/reminders/Reminder.ts +89 -0
  41. package/src/reminders/ReminderManager.ts +284 -0
  42. package/src/reminders/ReminderTimer.ts +86 -0
  43. package/src/reminders/index.ts +3 -0
  44. package/src/search/BaseSearchSource.ts +227 -0
  45. package/src/search/ChannelSearchSource.ts +34 -0
  46. package/src/search/MessageSearchSource.ts +88 -0
  47. package/src/search/SearchController.ts +154 -0
  48. package/src/search/UserSearchSource.ts +35 -0
  49. package/src/search/index.ts +5 -0
  50. package/src/token_manager.ts +3 -1
  51. package/src/types.ts +88 -0
  52. package/dist/types/search_controller.d.ts +0 -174
  53. package/src/search_controller.ts +0 -523
@@ -0,0 +1,184 @@
1
+ import { StateStore } from '../store';
2
+ import { debounce, type DebouncedFunc } from '../utils';
3
+
4
+ type PaginationDirection = 'next' | 'prev';
5
+ type Cursor = { next: string | null; prev: string | null };
6
+ export type PaginationQueryParams = { direction: PaginationDirection };
7
+ export type PaginationQueryReturnValue<T> = { items: T[] } & {
8
+ next?: string;
9
+ prev?: string;
10
+ };
11
+ export type PaginatorDebounceOptions = {
12
+ debounceMs: number;
13
+ };
14
+ type DebouncedExecQueryFunction = DebouncedFunc<
15
+ (params: { direction: PaginationDirection }) => Promise<void>
16
+ >;
17
+
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ export type PaginatorState<T = any> = {
20
+ hasNext: boolean;
21
+ hasPrev: boolean;
22
+ isLoading: boolean;
23
+ items: T[] | undefined;
24
+ lastQueryError?: Error;
25
+ cursor?: Cursor;
26
+ offset?: number;
27
+ };
28
+
29
+ export type PaginatorOptions = {
30
+ /** The number of milliseconds to debounce the search query. The default interval is 300ms. */
31
+ debounceMs?: number;
32
+ pageSize?: number;
33
+ };
34
+ export const DEFAULT_PAGINATION_OPTIONS: Required<PaginatorOptions> = {
35
+ debounceMs: 300,
36
+ pageSize: 10,
37
+ } as const;
38
+
39
+ export abstract class BasePaginator<T> {
40
+ state: StateStore<PaginatorState<T>>;
41
+ protected pageSize: number;
42
+ protected _executeQueryDebounced!: DebouncedExecQueryFunction;
43
+ protected _isCursorPagination = false;
44
+
45
+ protected constructor(options?: PaginatorOptions) {
46
+ const { debounceMs, pageSize } = { ...DEFAULT_PAGINATION_OPTIONS, ...options };
47
+ this.pageSize = pageSize;
48
+ this.state = new StateStore<PaginatorState<T>>(this.initialState);
49
+ this.setDebounceOptions({ debounceMs });
50
+ }
51
+
52
+ get lastQueryError() {
53
+ return this.state.getLatestValue().lastQueryError;
54
+ }
55
+
56
+ get hasNext() {
57
+ return this.state.getLatestValue().hasNext;
58
+ }
59
+
60
+ get hasPrev() {
61
+ return this.state.getLatestValue().hasPrev;
62
+ }
63
+
64
+ get hasResults() {
65
+ return Array.isArray(this.state.getLatestValue().items);
66
+ }
67
+
68
+ get isLoading() {
69
+ return this.state.getLatestValue().isLoading;
70
+ }
71
+
72
+ get initialState(): PaginatorState {
73
+ return {
74
+ hasNext: true,
75
+ hasPrev: true, //todo: check if optimistic value does not cause problems in UI
76
+ isLoading: false,
77
+ items: undefined,
78
+ lastQueryError: undefined,
79
+ cursor: undefined,
80
+ offset: 0,
81
+ };
82
+ }
83
+
84
+ get items() {
85
+ return this.state.getLatestValue().items;
86
+ }
87
+
88
+ get cursor() {
89
+ return this.state.getLatestValue().cursor;
90
+ }
91
+
92
+ get offset() {
93
+ return this.state.getLatestValue().offset;
94
+ }
95
+
96
+ abstract query(params: PaginationQueryParams): Promise<PaginationQueryReturnValue<T>>;
97
+
98
+ abstract filterQueryResults(items: T[]): T[] | Promise<T[]>;
99
+
100
+ setDebounceOptions = ({ debounceMs }: PaginatorDebounceOptions) => {
101
+ this._executeQueryDebounced = debounce(this.executeQuery.bind(this), debounceMs);
102
+ };
103
+
104
+ canExecuteQuery = (direction: PaginationDirection) =>
105
+ (!this.isLoading && direction === 'next' && this.hasNext) ||
106
+ (direction === 'prev' && this.hasPrev);
107
+
108
+ protected getStateBeforeFirstQuery(): PaginatorState<T> {
109
+ return {
110
+ ...this.initialState,
111
+ isLoading: true,
112
+ };
113
+ }
114
+
115
+ protected getStateAfterQuery(
116
+ stateUpdate: Partial<PaginatorState<T>>,
117
+ isFirstPage: boolean,
118
+ ): PaginatorState<T> {
119
+ const current = this.state.getLatestValue();
120
+ return {
121
+ ...current,
122
+ lastQueryError: undefined, // reset lastQueryError that can be overridden by the stateUpdate
123
+ ...stateUpdate,
124
+ isLoading: false,
125
+ items: isFirstPage
126
+ ? stateUpdate.items
127
+ : [...(this.items ?? []), ...(stateUpdate.items || [])],
128
+ };
129
+ }
130
+
131
+ async executeQuery({ direction }: { direction: PaginationDirection }) {
132
+ if (!this.canExecuteQuery(direction)) return;
133
+ const isFirstPage = typeof this.items === 'undefined';
134
+ if (isFirstPage) {
135
+ this.state.next(this.getStateBeforeFirstQuery());
136
+ } else {
137
+ this.state.partialNext({ isLoading: true });
138
+ }
139
+
140
+ const stateUpdate: Partial<PaginatorState<T>> = {};
141
+ try {
142
+ const results = await this.query({ direction });
143
+ if (!results) return;
144
+ const { items, next, prev } = results;
145
+ if (isFirstPage && (next || prev)) {
146
+ this._isCursorPagination = true;
147
+ }
148
+
149
+ if (this._isCursorPagination) {
150
+ stateUpdate.cursor = { next: next || null, prev: prev || null };
151
+ stateUpdate.hasNext = !!next;
152
+ stateUpdate.hasPrev = !!prev;
153
+ } else {
154
+ stateUpdate.offset = (this.offset ?? 0) + items.length;
155
+ stateUpdate.hasNext = items.length === this.pageSize;
156
+ }
157
+
158
+ stateUpdate.items = await this.filterQueryResults(items);
159
+ } catch (e) {
160
+ stateUpdate.lastQueryError = e as Error;
161
+ } finally {
162
+ this.state.next(this.getStateAfterQuery(stateUpdate, isFirstPage));
163
+ }
164
+ }
165
+
166
+ cancelScheduledQuery() {
167
+ this._executeQueryDebounced.cancel();
168
+ }
169
+
170
+ resetState() {
171
+ this.state.next(this.initialState);
172
+ }
173
+
174
+ next = () => this.executeQuery({ direction: 'next' });
175
+
176
+ prev = () => this.executeQuery({ direction: 'prev' });
177
+
178
+ nextDebounced = () => {
179
+ this._executeQueryDebounced({ direction: 'next' });
180
+ };
181
+ prevDebounced = () => {
182
+ this._executeQueryDebounced({ direction: 'prev' });
183
+ };
184
+ }
@@ -0,0 +1,38 @@
1
+ import { BasePaginator } from './BasePaginator';
2
+ import type {
3
+ PaginationQueryParams,
4
+ PaginationQueryReturnValue,
5
+ PaginatorOptions,
6
+ } from './BasePaginator';
7
+ import type { ReminderFilters, ReminderResponse, ReminderSort } from '../types';
8
+ import type { StreamChat } from '../client';
9
+
10
+ export class ReminderPaginator extends BasePaginator<ReminderResponse> {
11
+ private client: StreamChat;
12
+ filters: ReminderFilters | undefined;
13
+ sort: ReminderSort | undefined;
14
+
15
+ constructor(client: StreamChat, options?: PaginatorOptions) {
16
+ super(options);
17
+ this.client = client;
18
+ }
19
+
20
+ query = async ({
21
+ direction,
22
+ }: PaginationQueryParams): Promise<PaginationQueryReturnValue<ReminderResponse>> => {
23
+ const cursor = this.cursor?.[direction];
24
+ const {
25
+ reminders: items,
26
+ next,
27
+ prev,
28
+ } = await this.client.queryReminders({
29
+ filter: this.filters,
30
+ sort: this.sort,
31
+ limit: this.pageSize,
32
+ [direction]: cursor,
33
+ });
34
+ return { items, next, prev };
35
+ };
36
+
37
+ filterQueryResults = (items: ReminderResponse[]) => items;
38
+ }
@@ -0,0 +1,2 @@
1
+ export * from './BasePaginator';
2
+ export * from './ReminderPaginator';
@@ -0,0 +1,89 @@
1
+ import { ReminderTimer } from './ReminderTimer';
2
+ import { StateStore } from '../store';
3
+ import type { ReminderTimerConfig } from './ReminderTimer';
4
+ import type { MessageResponse, ReminderResponseBase, UserResponse } from '../types';
5
+
6
+ export const timeLeftMs = (remindAt: number) => remindAt - new Date().getTime();
7
+
8
+ export type ReminderResponseBaseOrResponse = ReminderResponseBase & {
9
+ user?: UserResponse;
10
+ message?: MessageResponse;
11
+ };
12
+
13
+ export type ReminderState = {
14
+ channel_cid: string;
15
+ created_at: Date;
16
+ message: MessageResponse | null;
17
+ message_id: string;
18
+ remind_at: Date | null;
19
+ timeLeftMs: number | null;
20
+ updated_at: Date;
21
+ user: UserResponse | null;
22
+ user_id: string;
23
+ };
24
+
25
+ export type ReminderOptions = {
26
+ data: ReminderResponseBaseOrResponse;
27
+ config?: ReminderTimerConfig;
28
+ };
29
+
30
+ export class Reminder {
31
+ state: StateStore<ReminderState>;
32
+ timer: ReminderTimer;
33
+ constructor({ data, config }: ReminderOptions) {
34
+ this.state = new StateStore(Reminder.toStateValue(data));
35
+ this.timer = new ReminderTimer({ reminder: this, config });
36
+ this.initTimer();
37
+ }
38
+
39
+ static toStateValue = (data: ReminderResponseBaseOrResponse): ReminderState => ({
40
+ ...data,
41
+ created_at: new Date(data.created_at),
42
+ message: data.message || null,
43
+ remind_at: data.remind_at ? new Date(data.remind_at) : null,
44
+ timeLeftMs: data.remind_at ? timeLeftMs(new Date(data.remind_at).getTime()) : null,
45
+ updated_at: new Date(data.updated_at),
46
+ user: data.user || null,
47
+ });
48
+
49
+ get id() {
50
+ return this.state.getLatestValue().message_id;
51
+ }
52
+
53
+ get remindAt() {
54
+ return this.state.getLatestValue().remind_at;
55
+ }
56
+
57
+ get timeLeftMs() {
58
+ return this.state.getLatestValue().timeLeftMs;
59
+ }
60
+
61
+ setState = (data: ReminderResponseBaseOrResponse) => {
62
+ this.state.next((current) => {
63
+ const newState = { ...current, ...Reminder.toStateValue(data) };
64
+ if (newState.remind_at) {
65
+ newState.timeLeftMs = timeLeftMs(newState.remind_at.getTime());
66
+ }
67
+ return newState;
68
+ });
69
+
70
+ if (data.remind_at) {
71
+ this.initTimer();
72
+ } else if (!data.remind_at) {
73
+ this.clearTimer();
74
+ }
75
+ };
76
+
77
+ refreshTimeLeft = () => {
78
+ if (!this.remindAt) return;
79
+ this.state.partialNext({ timeLeftMs: timeLeftMs(this.remindAt.getTime()) });
80
+ };
81
+
82
+ initTimer = () => {
83
+ this.timer.init();
84
+ };
85
+
86
+ clearTimer = () => {
87
+ this.timer.clear();
88
+ };
89
+ }
@@ -0,0 +1,284 @@
1
+ import { Reminder } from './Reminder';
2
+ import { StateStore } from '../store';
3
+ import { ReminderPaginator } from '../pagination';
4
+ import { WithSubscriptions } from '../utils/WithSubscriptions';
5
+ import type { ReminderResponseBaseOrResponse } from './Reminder';
6
+ import type { StreamChat } from '../client';
7
+ import type {
8
+ CreateReminderOptions,
9
+ Event,
10
+ EventTypes,
11
+ LocalMessage,
12
+ MessageResponse,
13
+ ReminderResponse,
14
+ } from '../types';
15
+
16
+ const oneMinute = 60 * 1000;
17
+ const oneHour = 60 * oneMinute;
18
+ const oneDay = 24 * oneHour;
19
+
20
+ export const DEFAULT_REMINDER_MANAGER_CONFIG: ReminderManagerConfig = {
21
+ scheduledOffsetsMs: [
22
+ 2 * oneMinute,
23
+ 30 * oneMinute,
24
+ oneHour,
25
+ 2 * oneHour,
26
+ 8 * oneHour,
27
+ oneDay,
28
+ ],
29
+ };
30
+
31
+ const isReminderExistsError = (error: Error) =>
32
+ error.message.match('already has reminder created for this message_id');
33
+
34
+ const isReminderDoesNotExistError = (error: Error) =>
35
+ error.message.match('reminder does not exist');
36
+
37
+ type MessageId = string;
38
+
39
+ export type ReminderEvent = {
40
+ cid: string;
41
+ created_at: string;
42
+ message_id: MessageId;
43
+ reminder: ReminderResponse;
44
+ type: EventTypes;
45
+ user_id: string;
46
+ };
47
+
48
+ export type ReminderManagerState = {
49
+ reminders: Map<MessageId, Reminder>;
50
+ };
51
+
52
+ export type ReminderManagerConfig = {
53
+ scheduledOffsetsMs: number[];
54
+ stopTimerRefreshBoundaryMs?: number;
55
+ };
56
+
57
+ export type ReminderManagerOptions = {
58
+ client: StreamChat;
59
+ config?: ReminderManagerConfig;
60
+ };
61
+
62
+ export class ReminderManager extends WithSubscriptions {
63
+ private client: StreamChat;
64
+ configState: StateStore<ReminderManagerConfig>;
65
+ state: StateStore<ReminderManagerState>;
66
+ paginator: ReminderPaginator;
67
+
68
+ constructor({ client, config }: ReminderManagerOptions) {
69
+ super();
70
+ this.client = client;
71
+ this.configState = new StateStore({
72
+ scheduledOffsetsMs:
73
+ config?.scheduledOffsetsMs ?? DEFAULT_REMINDER_MANAGER_CONFIG.scheduledOffsetsMs,
74
+ });
75
+ this.state = new StateStore({ reminders: new Map<MessageId, Reminder>() });
76
+ this.paginator = new ReminderPaginator(client);
77
+ }
78
+
79
+ // Config API START //
80
+ updateConfig(config: Partial<ReminderManagerConfig>) {
81
+ this.configState.partialNext(config);
82
+ }
83
+
84
+ get stopTimerRefreshBoundaryMs() {
85
+ return this.configState.getLatestValue().stopTimerRefreshBoundaryMs;
86
+ }
87
+
88
+ get scheduledOffsetsMs() {
89
+ return this.configState.getLatestValue().scheduledOffsetsMs;
90
+ }
91
+ // Config API END //
92
+
93
+ // State API START //
94
+ get reminders() {
95
+ return this.state.getLatestValue().reminders;
96
+ }
97
+ getFromState(messageId: MessageId) {
98
+ return this.reminders.get(messageId);
99
+ }
100
+
101
+ upsertToState = ({
102
+ data,
103
+ overwrite = true,
104
+ }: {
105
+ data: ReminderResponseBaseOrResponse;
106
+ overwrite?: boolean;
107
+ }) => {
108
+ if (!this.client._cacheEnabled()) {
109
+ return;
110
+ }
111
+ const cachedReminder = this.getFromState(data.message_id);
112
+ if (!cachedReminder) {
113
+ const reminder = new Reminder({
114
+ data,
115
+ config: { stopRefreshBoundaryMs: this.stopTimerRefreshBoundaryMs },
116
+ });
117
+ this.state.partialNext({
118
+ reminders: new Map(this.reminders.set(data.message_id, reminder)),
119
+ });
120
+ } else if (overwrite) {
121
+ cachedReminder.setState(data);
122
+ }
123
+ return cachedReminder;
124
+ };
125
+
126
+ removeFromState = (messageId: string) => {
127
+ const cachedReminder = this.getFromState(messageId);
128
+ if (!cachedReminder) return;
129
+ cachedReminder.clearTimer();
130
+ const reminders = this.reminders;
131
+ reminders.delete(messageId);
132
+ this.state.partialNext({ reminders: new Map(reminders) });
133
+ };
134
+
135
+ hydrateState = (messages: MessageResponse[] | LocalMessage[]) => {
136
+ messages.forEach(({ reminder }) => {
137
+ if (reminder) {
138
+ this.upsertToState({ data: reminder });
139
+ }
140
+ });
141
+ };
142
+ // State API END //
143
+
144
+ // Timers API START //
145
+ initTimers = () => {
146
+ this.reminders.forEach((reminder) => reminder.initTimer());
147
+ };
148
+
149
+ clearTimers = () => {
150
+ this.reminders.forEach((reminder) => reminder.clearTimer());
151
+ };
152
+ // Timers API END //
153
+
154
+ // WS event handling START //
155
+ static isReminderWsEventPayload = (event: Event): event is ReminderEvent =>
156
+ !!event.reminder &&
157
+ (event.type.startsWith('reminder.') || event.type === 'notification.reminder_due');
158
+
159
+ public registerSubscriptions = () => {
160
+ if (this.hasSubscriptions) return;
161
+ this.addUnsubscribeFunction(this.subscribeReminderCreated());
162
+ this.addUnsubscribeFunction(this.subscribeReminderUpdated());
163
+ this.addUnsubscribeFunction(this.subscribeReminderDeleted());
164
+ this.addUnsubscribeFunction(this.subscribeNotificationReminderDue());
165
+ this.addUnsubscribeFunction(this.subscribeMessageDeleted());
166
+ this.addUnsubscribeFunction(this.subscribeMessageUndeleted());
167
+ this.addUnsubscribeFunction(this.subscribePaginatorStateUpdated());
168
+ this.addUnsubscribeFunction(this.subscribeConfigStateUpdated());
169
+ };
170
+
171
+ private subscribeReminderCreated = () =>
172
+ this.client.on('reminder.created', (event) => {
173
+ if (!ReminderManager.isReminderWsEventPayload(event)) return;
174
+ const { reminder } = event;
175
+ this.upsertToState({ data: reminder });
176
+ }).unsubscribe;
177
+
178
+ private subscribeReminderUpdated = () =>
179
+ this.client.on('reminder.updated', (event) => {
180
+ if (!ReminderManager.isReminderWsEventPayload(event)) return;
181
+ const { reminder } = event;
182
+ this.upsertToState({ data: reminder });
183
+ }).unsubscribe;
184
+
185
+ private subscribeReminderDeleted = () =>
186
+ this.client.on('reminder.deleted', (event) => {
187
+ if (!ReminderManager.isReminderWsEventPayload(event)) return;
188
+ this.removeFromState(event.message_id);
189
+ }).unsubscribe;
190
+
191
+ private subscribeMessageDeleted = () =>
192
+ this.client.on('message.deleted', (event) => {
193
+ if (!event.message?.id) return;
194
+ this.removeFromState(event.message.id);
195
+ }).unsubscribe;
196
+
197
+ private subscribeMessageUndeleted = () =>
198
+ this.client.on('message.undeleted', (event) => {
199
+ if (!event.message?.reminder) return;
200
+ // todo: not sure whether reminder specific event is emitted too and this can be ignored here
201
+ this.upsertToState({ data: event.message.reminder });
202
+ }).unsubscribe;
203
+
204
+ private subscribeNotificationReminderDue = () =>
205
+ this.client.on('notification.reminder_due', () => null).unsubscribe; // todo: what should be performed on this event?
206
+
207
+ private subscribePaginatorStateUpdated = () =>
208
+ this.paginator.state.subscribeWithSelector(
209
+ ({ items }) => [items],
210
+ ([items]) => {
211
+ if (!items) return;
212
+ for (const reminder of items) {
213
+ this.upsertToState({ data: reminder });
214
+ }
215
+ },
216
+ );
217
+
218
+ private subscribeConfigStateUpdated = () =>
219
+ this.configState.subscribeWithSelector(
220
+ ({ stopTimerRefreshBoundaryMs }) => ({ stopTimerRefreshBoundaryMs }),
221
+ ({ stopTimerRefreshBoundaryMs }, previousValue) => {
222
+ if (
223
+ typeof stopTimerRefreshBoundaryMs === 'number' &&
224
+ stopTimerRefreshBoundaryMs !== previousValue?.stopTimerRefreshBoundaryMs
225
+ ) {
226
+ this.reminders.forEach((reminder: Reminder) => {
227
+ if (reminder.timer) {
228
+ reminder.timer.stopRefreshBoundaryMs = stopTimerRefreshBoundaryMs;
229
+ }
230
+ });
231
+ }
232
+ },
233
+ );
234
+ // WS event handling END //
235
+
236
+ // API calls START //
237
+ upsertReminder = async (options: CreateReminderOptions) => {
238
+ const { messageId } = options;
239
+ if (this.getFromState(messageId)) {
240
+ try {
241
+ return await this.updateReminder(options);
242
+ } catch (error) {
243
+ if (isReminderDoesNotExistError(error as Error)) {
244
+ return await this.createReminder(options);
245
+ }
246
+ throw error;
247
+ }
248
+ } else {
249
+ try {
250
+ return await this.createReminder(options);
251
+ } catch (error) {
252
+ if (isReminderExistsError(error as Error)) {
253
+ return await this.updateReminder(options);
254
+ }
255
+ throw error;
256
+ }
257
+ }
258
+ };
259
+
260
+ createReminder = async (options: CreateReminderOptions) => {
261
+ const { reminder } = await this.client.createReminder(options);
262
+ return this.upsertToState({ data: reminder, overwrite: false });
263
+ };
264
+
265
+ updateReminder = async (options: CreateReminderOptions) => {
266
+ const { reminder } = await this.client.updateReminder(options);
267
+ return this.upsertToState({ data: reminder });
268
+ };
269
+
270
+ deleteReminder = async (messageId: MessageId) => {
271
+ await this.client.deleteReminder(messageId);
272
+ this.removeFromState(messageId);
273
+ };
274
+
275
+ queryNextReminders = async () => {
276
+ await this.paginator.next();
277
+ };
278
+
279
+ queryPreviousReminders = async () => {
280
+ await this.paginator.prev();
281
+ };
282
+
283
+ // API calls END //
284
+ }
@@ -0,0 +1,86 @@
1
+ import { timeLeftMs } from './Reminder';
2
+ import type { Reminder } from './Reminder';
3
+
4
+ const oneMinute = 60 * 1000;
5
+ const oneHour = 60 * oneMinute;
6
+ const oneDay = 24 * oneHour;
7
+ const oneWeek = 7 * oneDay;
8
+
9
+ const GROUP_BOUNDS = {
10
+ minute: { lower: oneMinute, upper: oneHour },
11
+ hour: { lower: oneHour, upper: oneDay },
12
+ day: { lower: oneDay, upper: oneWeek },
13
+ } as const;
14
+
15
+ export const DEFAULT_STOP_REFRESH_BOUNDARY_MS = 2 * oneWeek;
16
+
17
+ export type ReminderTimerConfig = {
18
+ stopRefreshBoundaryMs?: number;
19
+ };
20
+
21
+ export class ReminderTimer {
22
+ reminder: Reminder;
23
+ timeout: ReturnType<typeof setTimeout> | null = null;
24
+ stopRefreshBoundaryMs: number = DEFAULT_STOP_REFRESH_BOUNDARY_MS;
25
+
26
+ constructor({
27
+ reminder,
28
+ config,
29
+ }: {
30
+ reminder: Reminder;
31
+ config?: ReminderTimerConfig;
32
+ }) {
33
+ this.reminder = reminder;
34
+
35
+ if (typeof config?.stopRefreshBoundaryMs === 'number') {
36
+ this.stopRefreshBoundaryMs = config.stopRefreshBoundaryMs;
37
+ }
38
+ }
39
+
40
+ getRefreshIntervalLength = () => {
41
+ if (!this.reminder.remindAt) return null;
42
+ const distanceFromDeadlineMs = Math.abs(timeLeftMs(this.reminder.remindAt.getTime()));
43
+ let refreshInterval: number | null;
44
+ if (distanceFromDeadlineMs === 0) {
45
+ refreshInterval = oneMinute;
46
+ } else if (distanceFromDeadlineMs < GROUP_BOUNDS.minute.lower) {
47
+ refreshInterval = distanceFromDeadlineMs;
48
+ } else if (distanceFromDeadlineMs <= GROUP_BOUNDS.minute.upper) {
49
+ refreshInterval = oneMinute;
50
+ } else if (distanceFromDeadlineMs <= GROUP_BOUNDS.hour.upper) {
51
+ refreshInterval = oneHour;
52
+ } else {
53
+ refreshInterval = oneDay;
54
+ }
55
+ return refreshInterval;
56
+ };
57
+
58
+ init = () => {
59
+ if (!this.reminder.remindAt) return null;
60
+ const timeoutLength = this.getRefreshIntervalLength();
61
+ if (timeoutLength === null) return null;
62
+
63
+ const boundaryTimestamp =
64
+ this.reminder.remindAt?.getTime() + this.stopRefreshBoundaryMs;
65
+ const timeLeftToBoundary = boundaryTimestamp - Date.now();
66
+
67
+ if (timeLeftToBoundary <= 0) {
68
+ this.timeout = null;
69
+ return;
70
+ }
71
+
72
+ if (this.timeout) clearTimeout(this.timeout);
73
+
74
+ this.timeout = setTimeout(() => {
75
+ this.reminder.refreshTimeLeft();
76
+ this.init();
77
+ }, timeoutLength);
78
+ };
79
+
80
+ clear = () => {
81
+ if (this.timeout) {
82
+ clearInterval(this.timeout);
83
+ this.timeout = null;
84
+ }
85
+ };
86
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Reminder';
2
+ export * from './ReminderManager';
3
+ export * from './ReminderTimer';