| 
									
										
										
										
											2021-01-14 12:07:05 -06:00
										 |  |  | // Copyright 2019-2021 Signal Messenger, LLC
 | 
					
						
							| 
									
										
										
										
											2020-10-30 15:34:04 -05:00
										 |  |  | // SPDX-License-Identifier: AGPL-3.0-only
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  | import PQueue from 'p-queue'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-13 17:21:42 -05:00
										 |  |  | import { sleep } from './sleep'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-14 14:56:35 -07:00
										 |  |  | declare global { | 
					
						
							| 
									
										
										
										
											2021-01-14 12:07:05 -06:00
										 |  |  |   // We want to extend `window`'s properties, so we need an interface.
 | 
					
						
							|  |  |  |   // eslint-disable-next-line no-restricted-syntax
 | 
					
						
							| 
									
										
										
										
											2020-09-14 14:56:35 -07:00
										 |  |  |   interface Window { | 
					
						
							|  |  |  |     // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
					
						
							|  |  |  |     batchers: Array<BatcherType<any>>; | 
					
						
							|  |  |  |     waitForAllBatchers: () => Promise<unknown>; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  | window.batchers = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | window.waitForAllBatchers = async () => { | 
					
						
							| 
									
										
										
										
											2019-10-25 11:48:28 -07:00
										 |  |  |   await Promise.all(window.batchers.map(item => item.flushAndWait())); | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-13 10:37:29 -07:00
										 |  |  | export type BatcherOptionsType<ItemType> = { | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  |   wait: number; | 
					
						
							|  |  |  |   maxSize: number; | 
					
						
							| 
									
										
										
										
											2021-01-27 15:13:33 -06:00
										 |  |  |   processBatch: (items: Array<ItemType>) => void | Promise<void>; | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-13 10:37:29 -07:00
										 |  |  | export type BatcherType<ItemType> = { | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  |   add: (item: ItemType) => void; | 
					
						
							|  |  |  |   anyPending: () => boolean; | 
					
						
							|  |  |  |   onIdle: () => Promise<void>; | 
					
						
							| 
									
										
										
										
											2019-10-25 11:48:28 -07:00
										 |  |  |   flushAndWait: () => Promise<void>; | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  |   unregister: () => void; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function createBatcher<ItemType>( | 
					
						
							|  |  |  |   options: BatcherOptionsType<ItemType> | 
					
						
							|  |  |  | ): BatcherType<ItemType> { | 
					
						
							|  |  |  |   let batcher: BatcherType<ItemType>; | 
					
						
							| 
									
										
										
										
											2020-09-14 14:56:35 -07:00
										 |  |  |   let timeout: NodeJS.Timeout | null; | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  |   let items: Array<ItemType> = []; | 
					
						
							| 
									
										
										
										
											2020-09-18 13:40:41 -07:00
										 |  |  |   const queue = new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 }); | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   function _kickBatchOff() { | 
					
						
							|  |  |  |     const itemsRef = items; | 
					
						
							|  |  |  |     items = []; | 
					
						
							|  |  |  |     queue.add(async () => { | 
					
						
							|  |  |  |       await options.processBatch(itemsRef); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function add(item: ItemType) { | 
					
						
							|  |  |  |     items.push(item); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (timeout) { | 
					
						
							|  |  |  |       clearTimeout(timeout); | 
					
						
							|  |  |  |       timeout = null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (items.length >= options.maxSize) { | 
					
						
							|  |  |  |       _kickBatchOff(); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       timeout = setTimeout(() => { | 
					
						
							|  |  |  |         timeout = null; | 
					
						
							|  |  |  |         _kickBatchOff(); | 
					
						
							|  |  |  |       }, options.wait); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function anyPending(): boolean { | 
					
						
							|  |  |  |     return queue.size > 0 || queue.pending > 0 || items.length > 0; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async function onIdle() { | 
					
						
							|  |  |  |     while (anyPending()) { | 
					
						
							|  |  |  |       if (queue.size > 0 || queue.pending > 0) { | 
					
						
							| 
									
										
										
										
											2020-09-14 14:56:35 -07:00
										 |  |  |         // eslint-disable-next-line no-await-in-loop
 | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  |         await queue.onIdle(); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (items.length > 0) { | 
					
						
							| 
									
										
										
										
											2020-09-14 14:56:35 -07:00
										 |  |  |         // eslint-disable-next-line no-await-in-loop
 | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  |         await sleep(options.wait * 2); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function unregister() { | 
					
						
							| 
									
										
										
										
											2020-09-14 14:56:35 -07:00
										 |  |  |     window.batchers = window.batchers.filter(item => item !== batcher); | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-10-25 11:48:28 -07:00
										 |  |  |   async function flushAndWait() { | 
					
						
							|  |  |  |     if (timeout) { | 
					
						
							|  |  |  |       clearTimeout(timeout); | 
					
						
							|  |  |  |       timeout = null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (items.length) { | 
					
						
							|  |  |  |       _kickBatchOff(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return onIdle(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  |   batcher = { | 
					
						
							|  |  |  |     add, | 
					
						
							|  |  |  |     anyPending, | 
					
						
							|  |  |  |     onIdle, | 
					
						
							| 
									
										
										
										
											2019-10-25 11:48:28 -07:00
										 |  |  |     flushAndWait, | 
					
						
							| 
									
										
										
										
											2019-09-26 12:56:31 -07:00
										 |  |  |     unregister, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   window.batchers.push(batcher); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return batcher; | 
					
						
							|  |  |  | } |