stream-chat
Version:
JS SDK for the Stream Chat API
299 lines (261 loc) • 9.26 kB
text/typescript
import { Reminder } from './Reminder';
import { DEFAULT_STOP_REFRESH_BOUNDARY_MS } from './ReminderTimer';
import { StateStore } from '../store';
import { ReminderPaginator } from '../pagination';
import { WithSubscriptions } from '../utils/WithSubscriptions';
import type { ReminderResponseBaseOrResponse } from './Reminder';
import type { StreamChat } from '../client';
import type {
CreateReminderOptions,
Event,
EventTypes,
LocalMessage,
MessageResponse,
ReminderResponse,
} from '../types';
const oneMinute = 60 * 1000;
const oneHour = 60 * oneMinute;
const oneDay = 24 * oneHour;
export const DEFAULT_REMINDER_MANAGER_CONFIG: ReminderManagerConfig = {
scheduledOffsetsMs: [
2 * oneMinute,
30 * oneMinute,
oneHour,
2 * oneHour,
8 * oneHour,
oneDay,
],
stopTimerRefreshBoundaryMs: DEFAULT_STOP_REFRESH_BOUNDARY_MS,
};
const isReminderExistsError = (error: Error) =>
error.message.match('already has reminder created for this message_id');
const isReminderDoesNotExistError = (error: Error) =>
error.message.match('reminder does not exist');
type MessageId = string;
export type ReminderEvent = {
cid: string;
created_at: string;
message_id: MessageId;
reminder: ReminderResponse;
type: EventTypes;
user_id: string;
};
export type ReminderManagerState = {
reminders: Map<MessageId, Reminder>;
};
export type ReminderManagerConfig = {
scheduledOffsetsMs: number[];
stopTimerRefreshBoundaryMs: number;
};
export type ReminderManagerOptions = {
client: StreamChat;
config?: Partial<ReminderManagerConfig>;
};
export class ReminderManager extends WithSubscriptions {
private client: StreamChat;
configState: StateStore<ReminderManagerConfig>;
state: StateStore<ReminderManagerState>;
paginator: ReminderPaginator;
constructor({ client, config }: ReminderManagerOptions) {
super();
this.client = client;
this.configState = new StateStore({
scheduledOffsetsMs:
config?.scheduledOffsetsMs ?? DEFAULT_REMINDER_MANAGER_CONFIG.scheduledOffsetsMs,
stopTimerRefreshBoundaryMs:
config?.stopTimerRefreshBoundaryMs ??
DEFAULT_REMINDER_MANAGER_CONFIG.stopTimerRefreshBoundaryMs,
});
this.state = new StateStore({ reminders: new Map<MessageId, Reminder>() });
this.paginator = new ReminderPaginator(client);
}
// Config API START //
updateConfig(config: Partial<ReminderManagerConfig>) {
if (
typeof config.stopTimerRefreshBoundaryMs === 'number' &&
config.stopTimerRefreshBoundaryMs !== this.stopTimerRefreshBoundaryMs
) {
this.reminders.forEach((reminder) => {
reminder.timer.stopRefreshBoundaryMs =
config?.stopTimerRefreshBoundaryMs as number;
});
}
this.configState.partialNext(config);
}
get stopTimerRefreshBoundaryMs() {
return this.configState.getLatestValue().stopTimerRefreshBoundaryMs;
}
get scheduledOffsetsMs() {
return this.configState.getLatestValue().scheduledOffsetsMs;
}
// Config API END //
// State API START //
get reminders() {
return this.state.getLatestValue().reminders;
}
getFromState(messageId: MessageId) {
return this.reminders.get(messageId);
}
upsertToState = ({
data,
overwrite = true,
}: {
data: ReminderResponseBaseOrResponse;
overwrite?: boolean;
}) => {
if (!this.client._cacheEnabled()) {
return;
}
const cachedReminder = this.getFromState(data.message_id);
if (!cachedReminder) {
const reminder = new Reminder({
data,
config: { stopRefreshBoundaryMs: this.stopTimerRefreshBoundaryMs },
});
this.state.partialNext({
reminders: new Map(this.reminders.set(data.message_id, reminder)),
});
} else if (overwrite) {
cachedReminder.setState(data);
}
return cachedReminder;
};
removeFromState = (messageId: string) => {
const cachedReminder = this.getFromState(messageId);
if (!cachedReminder) return;
cachedReminder.clearTimer();
const reminders = this.reminders;
reminders.delete(messageId);
this.state.partialNext({ reminders: new Map(reminders) });
};
hydrateState = (messages: MessageResponse[] | LocalMessage[]) => {
messages.forEach(({ reminder }) => {
if (reminder) {
this.upsertToState({ data: reminder });
}
});
};
// State API END //
// Timers API START //
initTimers = () => {
this.reminders.forEach((reminder) => reminder.initTimer());
};
clearTimers = () => {
this.reminders.forEach((reminder) => reminder.clearTimer());
};
// Timers API END //
// WS event handling START //
static isReminderWsEventPayload = (event: Event): event is ReminderEvent =>
!!event.reminder &&
(event.type.startsWith('reminder.') || event.type === 'notification.reminder_due');
public registerSubscriptions = () => {
if (this.hasSubscriptions) return;
this.addUnsubscribeFunction(this.subscribeReminderCreated());
this.addUnsubscribeFunction(this.subscribeReminderUpdated());
this.addUnsubscribeFunction(this.subscribeReminderDeleted());
this.addUnsubscribeFunction(this.subscribeNotificationReminderDue());
this.addUnsubscribeFunction(this.subscribeMessageDeleted());
this.addUnsubscribeFunction(this.subscribeMessageUndeleted());
this.addUnsubscribeFunction(this.subscribePaginatorStateUpdated());
this.addUnsubscribeFunction(this.subscribeConfigStateUpdated());
};
private subscribeReminderCreated = () =>
this.client.on('reminder.created', (event) => {
if (!ReminderManager.isReminderWsEventPayload(event)) return;
const { reminder } = event;
this.upsertToState({ data: reminder });
}).unsubscribe;
private subscribeReminderUpdated = () =>
this.client.on('reminder.updated', (event) => {
if (!ReminderManager.isReminderWsEventPayload(event)) return;
const { reminder } = event;
this.upsertToState({ data: reminder });
}).unsubscribe;
private subscribeReminderDeleted = () =>
this.client.on('reminder.deleted', (event) => {
if (!ReminderManager.isReminderWsEventPayload(event)) return;
this.removeFromState(event.message_id);
}).unsubscribe;
private subscribeMessageDeleted = () =>
this.client.on('message.deleted', (event) => {
if (!event.message?.id) return;
this.removeFromState(event.message.id);
}).unsubscribe;
private subscribeMessageUndeleted = () =>
this.client.on('message.undeleted', (event) => {
if (!event.message?.reminder) return;
// todo: not sure whether reminder specific event is emitted too and this can be ignored here
this.upsertToState({ data: event.message.reminder });
}).unsubscribe;
private subscribeNotificationReminderDue = () =>
this.client.on('notification.reminder_due', () => null).unsubscribe; // todo: what should be performed on this event?
private subscribePaginatorStateUpdated = () =>
this.paginator.state.subscribeWithSelector(
({ items }) => [items],
([items]) => {
if (!items) return;
for (const reminder of items) {
this.upsertToState({ data: reminder });
}
},
);
private subscribeConfigStateUpdated = () =>
this.configState.subscribeWithSelector(
({ stopTimerRefreshBoundaryMs }) => ({ stopTimerRefreshBoundaryMs }),
({ stopTimerRefreshBoundaryMs }, previousValue) => {
if (
typeof stopTimerRefreshBoundaryMs === 'number' &&
stopTimerRefreshBoundaryMs !== previousValue?.stopTimerRefreshBoundaryMs
) {
this.reminders.forEach((reminder: Reminder) => {
if (reminder.timer) {
reminder.timer.stopRefreshBoundaryMs = stopTimerRefreshBoundaryMs;
}
});
}
},
);
// WS event handling END //
// API calls START //
upsertReminder = async (options: CreateReminderOptions) => {
const { messageId } = options;
if (this.getFromState(messageId)) {
try {
return await this.updateReminder(options);
} catch (error) {
if (isReminderDoesNotExistError(error as Error)) {
return await this.createReminder(options);
}
throw error;
}
} else {
try {
return await this.createReminder(options);
} catch (error) {
if (isReminderExistsError(error as Error)) {
return await this.updateReminder(options);
}
throw error;
}
}
};
createReminder = async (options: CreateReminderOptions) => {
const { reminder } = await this.client.createReminder(options);
return this.upsertToState({ data: reminder, overwrite: false });
};
updateReminder = async (options: CreateReminderOptions) => {
const { reminder } = await this.client.updateReminder(options);
return this.upsertToState({ data: reminder });
};
deleteReminder = async (messageId: MessageId) => {
await this.client.deleteReminder(messageId);
this.removeFromState(messageId);
};
queryNextReminders = async () => {
await this.paginator.next();
};
queryPreviousReminders = async () => {
await this.paginator.prev();
};
// API calls END //
}