UNPKG

@amityco/ts-sdk-react-native

Version:

Amity Social Cloud Typescript SDK

238 lines (192 loc) 7.33 kB
import { pullFromCache, pushToCache, queryCache } from '~/cache/api'; import { getActiveClient } from '../../api/activeClient'; import { fireEvent } from '~/core/events'; import { markChannelsAsReadBySegment } from '~/channelRepository/api/markChannelsAsReadBySegment'; export class MessageReadReceiptSyncEngine { private client: Amity.Client; private isActive = true; private MAX_RETRY = 3; private JOB_QUEUE_SIZE = 120; private jobQueue: Amity.ReadReceiptSyncJob[] = []; private timer: NodeJS.Timer | undefined; // Interval for message read receipt sync in seconds private RECEIPT_SYNC_INTERVAL = 1; constructor() { this.client = getActiveClient(); // Get remaining unsync read receipts from cache this.getUnsyncJobs(); } // Call this when client call client.login startSyncReadReceipt() { // Start timer when start receipt sync this.timer = setInterval(() => { this.syncReadReceipts(); }, this.RECEIPT_SYNC_INTERVAL * 1000); } // Read receipt observer handling syncReadReceipts(): void { if (this.jobQueue.length === 0 || this.isActive === false) return; const readReceipts = this.getReadReceipts(); if (readReceipts) { this.markReadApi(readReceipts); } } private getUnsyncJobs(): void { // Get all read receipts that has latestSyncSegment < latestSegment const readReceipts = queryCache<Amity.ReadReceipt>(['readReceipt'])?.filter(({ data }) => { return data.latestSyncSegment < data.latestSegment; }); // Enqueue unsync read receipts to the job queue readReceipts?.forEach(({ data: readReceipt }) => { this.enqueueReadReceipt(readReceipt.channelId, readReceipt.latestSegment); }); } private getReadReceipts(): Amity.ReadReceiptSyncJob[] | undefined { // get all read receipts from queue, now the queue is empty const syncJob = this.jobQueue.splice(0, this.jobQueue.length); if (syncJob.length === 0) return; return syncJob.filter(job => { const readReceipt = pullFromCache<Amity.ReadReceipt>(['readReceipt', job.channelId])?.data; if (!readReceipt) return false; if (readReceipt.latestSegment > readReceipt.latestSyncSegment) return true; return false; }); } private async markReadApi(syncJobs: Amity.ReadReceiptSyncJob[]): Promise<void> { // constuct payload // example: [{ channelId: 'channelId', readToSegment: 2 }] const syncJobsPayload = syncJobs.map(job => { return { channelId: job.channelId, readToSegment: job.segment, }; }); const response = await markChannelsAsReadBySegment(syncJobsPayload); if (response) { for (let i = 0; i < syncJobs.length; i += 1) { // update lastestSyncSegment in read receipt cache const cacheKey = ['readReceipt', syncJobs[i].channelId]; const readReceiptCache = pullFromCache<Amity.ReadReceipt>(cacheKey)?.data; pushToCache(cacheKey, { ...readReceiptCache, latestSyncSegment: syncJobs[i].segment, }); } } else { for (let i = 0; i < syncJobs.length; i += 1) { // push them back to queue if the syncing is failed and retry count is less than max retry if (syncJobs[i].retryCount >= this.MAX_RETRY) return; const updatedJob = { ...syncJobs[i], syncState: Amity.ReadReceiptSyncState.CREATED, retryCount: syncJobs[i].retryCount + 1, }; this.enqueueJob(updatedJob); } } } private startObservingReadReceiptQueue(): void { if (this.client.useLegacyUnreadCount) { this.isActive = true; this.startSyncReadReceipt(); } } private stopObservingReadReceiptQueue(): void { this.isActive = false; this.jobQueue.map(job => { if (job.syncState === Amity.ReadReceiptSyncState.SYNCING) { return { ...job, syncState: Amity.ReadReceiptSyncState.CREATED }; } return job; }); if (this.timer) clearInterval(this.timer); } // Session Management onSessionEstablished(): void { this.startObservingReadReceiptQueue(); } onSessionDestroyed(): void { this.stopObservingReadReceiptQueue(); this.jobQueue = []; } onTokenExpired(): void { this.stopObservingReadReceiptQueue(); } // Network Connection Management onNetworkOffline(): void { // Stop observing to the read receipt queue. this.stopObservingReadReceiptQueue(); } onNetworkOnline(): void { // Resume observing to the read receipt queue. this.startObservingReadReceiptQueue(); } markRead(channelId: string, segment: number): void { // Step 1: Optimistic update of channelUnread.readToSegment to message.segment and update unreadCount value const cacheKey = ['channelUnread', 'get', channelId]; const channelUnread = pullFromCache<Amity.ChannelUnread>(cacheKey)?.data; if ( typeof channelUnread?.readToSegment === 'number' && channelUnread && segment > channelUnread.readToSegment ) { channelUnread.readToSegment = segment; channelUnread.unreadCount = Math.max(channelUnread.lastSegment - segment, 0); pushToCache(cacheKey, channelUnread); fireEvent('local.channelUnread.updated', [channelUnread]); } // Step 2: Enqueue the read receipt this.enqueueReadReceipt(channelId, segment); } private enqueueReadReceipt(channelId: string, segment: number): void { const readReceipt = pullFromCache<Amity.ReadReceipt>(['readReceipt', channelId])?.data; // Create new read receipt if it's not exists and add the job to queue if (!readReceipt) { const readReceiptChannel: Amity.ReadReceipt = { channelId, latestSegment: segment, latestSyncSegment: 0, }; pushToCache(['readReceipt', channelId], readReceiptChannel); } else if (readReceipt.latestSegment < segment) { // Update latestSegment in read receipt cache pushToCache(['readReceipt', channelId], { ...readReceipt, latestSegment: segment }); } else if (readReceipt.latestSyncSegment >= segment) { // Skip the job when lastSyncSegment > = segment return; } let syncJob: Amity.ReadReceiptSyncJob | null = this.getSyncJob(channelId); if (syncJob === null || syncJob.syncState === Amity.ReadReceiptSyncState.SYNCING) { syncJob = { channelId, segment, syncState: Amity.ReadReceiptSyncState.CREATED, retryCount: 0, }; this.enqueueJob(syncJob); } else if (syncJob.segment < segment) { syncJob.segment = segment; } } private getSyncJob(channelId: string): Amity.ReadReceiptSyncJob | null { const { jobQueue } = this; const targetJob = jobQueue.find(job => job.channelId === channelId); return targetJob || null; } private enqueueJob(syncJob: Amity.ReadReceiptSyncJob) { if (this.jobQueue.length < this.JOB_QUEUE_SIZE) { this.jobQueue.push(syncJob); } else { // Remove oldest job when queue reach maximum capacity this.jobQueue.shift(); this.jobQueue.push(syncJob); } } } let instance: MessageReadReceiptSyncEngine | null = null; export default { getInstance: () => { if (!instance) instance = new MessageReadReceiptSyncEngine(); return instance; }, };