|  |  |  | import _ from 'lodash'; | 
					
						
							|  |  |  | import { createOrUpdateItem, getItemById } from '../../../js/modules/data'; | 
					
						
							|  |  |  | import { PartialRawMessage, RawMessage } from '../types/RawMessage'; | 
					
						
							|  |  |  | import { ContentMessage } from '../messages/outgoing'; | 
					
						
							|  |  |  | import { PubKey } from '../types'; | 
					
						
							|  |  |  | import { MessageUtils } from '../utils'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // This is an abstraction for storing pending messages.
 | 
					
						
							|  |  |  | // Ideally we want to store pending messages in the database so that
 | 
					
						
							|  |  |  | // on next launch we can re-send the pending messages, but we don't want
 | 
					
						
							|  |  |  | // to constantly fetch pending messages from the database.
 | 
					
						
							|  |  |  | // Thus we have an intermediary cache which will store pending messagesin
 | 
					
						
							|  |  |  | // memory and sync its state with the database on modification (add or remove).
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export class PendingMessageCache { | 
					
						
							|  |  |  |   protected loadPromise: Promise<void> | undefined; | 
					
						
							|  |  |  |   protected cache: Array<RawMessage> = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public async getAllPending(): Promise<Array<RawMessage>> { | 
					
						
							|  |  |  |     await this.loadFromDBIfNeeded(); | 
					
						
							|  |  |  |     // Get all pending from cache, sorted with oldest first
 | 
					
						
							|  |  |  |     return [...this.cache].sort((a, b) => a.timestamp - b.timestamp); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public async getForDevice(device: PubKey): Promise<Array<RawMessage>> { | 
					
						
							|  |  |  |     const pending = await this.getAllPending(); | 
					
						
							|  |  |  |     return pending.filter(m => m.device === device.key); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public async getDevices(): Promise<Array<PubKey>> { | 
					
						
							|  |  |  |     await this.loadFromDBIfNeeded(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Gets all unique devices with pending messages
 | 
					
						
							|  |  |  |     const pubkeyStrings = _.uniq(this.cache.map(m => m.device)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return pubkeyStrings.map(PubKey.from).filter((k): k is PubKey => !!k); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public async add( | 
					
						
							|  |  |  |     device: PubKey, | 
					
						
							|  |  |  |     message: ContentMessage | 
					
						
							|  |  |  |   ): Promise<RawMessage> { | 
					
						
							|  |  |  |     await this.loadFromDBIfNeeded(); | 
					
						
							|  |  |  |     const rawMessage = MessageUtils.toRawMessage(device, message); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Does it exist in cache already?
 | 
					
						
							|  |  |  |     if (this.find(rawMessage)) { | 
					
						
							|  |  |  |       return rawMessage; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     this.cache.push(rawMessage); | 
					
						
							|  |  |  |     await this.saveToDB(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return rawMessage; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public async remove( | 
					
						
							|  |  |  |     message: RawMessage | 
					
						
							|  |  |  |   ): Promise<Array<RawMessage> | undefined> { | 
					
						
							|  |  |  |     await this.loadFromDBIfNeeded(); | 
					
						
							|  |  |  |     // Should only be called after message is processed
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Return if message doesn't exist in cache
 | 
					
						
							|  |  |  |     if (!this.find(message)) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Remove item from cache and sync with database
 | 
					
						
							|  |  |  |     const updatedCache = this.cache.filter( | 
					
						
							|  |  |  |       cached => | 
					
						
							|  |  |  |         !( | 
					
						
							|  |  |  |           cached.device === message.device && | 
					
						
							|  |  |  |           cached.timestamp === message.timestamp | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |     this.cache = updatedCache; | 
					
						
							|  |  |  |     await this.saveToDB(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return updatedCache; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public find(message: RawMessage): RawMessage | undefined { | 
					
						
							|  |  |  |     // Find a message in the cache
 | 
					
						
							|  |  |  |     return this.cache.find( | 
					
						
							|  |  |  |       m => m.device === message.device && m.timestamp === message.timestamp | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public async clear() { | 
					
						
							|  |  |  |     // Clears the cache and syncs to DB
 | 
					
						
							|  |  |  |     this.cache = []; | 
					
						
							|  |  |  |     await this.saveToDB(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   protected async loadFromDBIfNeeded() { | 
					
						
							|  |  |  |     if (!this.loadPromise) { | 
					
						
							|  |  |  |       this.loadPromise = this.loadFromDB(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     await this.loadPromise; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   protected async loadFromDB() { | 
					
						
							|  |  |  |     const messages = await this.getFromStorage(); | 
					
						
							|  |  |  |     this.cache = messages; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   protected async getFromStorage(): Promise<Array<RawMessage>> { | 
					
						
							|  |  |  |     const data = await getItemById('pendingMessages'); | 
					
						
							|  |  |  |     if (!data || !data.value) { | 
					
						
							|  |  |  |       return []; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const barePending = JSON.parse(String(data.value)) as Array< | 
					
						
							|  |  |  |       PartialRawMessage | 
					
						
							|  |  |  |     >; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Rebuild plainTextBuffer
 | 
					
						
							|  |  |  |     return barePending.map((message: PartialRawMessage) => { | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         ...message, | 
					
						
							|  |  |  |         plainTextBuffer: new Uint8Array(message.plainTextBuffer), | 
					
						
							|  |  |  |       } as RawMessage; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   protected async saveToDB() { | 
					
						
							|  |  |  |     // For each plainTextBuffer in cache, save in as a simple Array<number> to avoid
 | 
					
						
							|  |  |  |     // Node issues with JSON stringifying Buffer without strict typing
 | 
					
						
							|  |  |  |     const encodedCache = [...this.cache].map(item => { | 
					
						
							|  |  |  |       const plainTextBuffer = Array.from(item.plainTextBuffer); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return { ...item, plainTextBuffer }; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const encodedPendingMessages = JSON.stringify(encodedCache) || '[]'; | 
					
						
							|  |  |  |     await createOrUpdateItem({ | 
					
						
							|  |  |  |       id: 'pendingMessages', | 
					
						
							|  |  |  |       value: encodedPendingMessages, | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |