build(deps-dev): bump rollup
[poolifier.git] / src / utils.ts
1 import * as os from 'node:os'
2 import { webcrypto } from 'node:crypto'
3 import { Worker as ClusterWorker } from 'node:cluster'
4 import { Worker as ThreadWorker } from 'node:worker_threads'
5 import type {
6 MeasurementStatisticsRequirements,
7 WorkerChoiceStrategyOptions
8 } from './pools/selection-strategies/selection-strategies-types'
9 import type { KillBehavior } from './worker/worker-options'
10 import { type IWorker, type WorkerType, WorkerTypes } from './pools/worker'
11
12 /**
13 * Default task name.
14 */
15 export const DEFAULT_TASK_NAME = 'default'
16
17 /**
18 * An intentional empty function.
19 */
20 export const EMPTY_FUNCTION: () => void = Object.freeze(() => {
21 /* Intentionally empty */
22 })
23
24 /**
25 * Default worker choice strategy options.
26 */
27 export const DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS: WorkerChoiceStrategyOptions =
28 {
29 retries: 6,
30 runTime: { median: false },
31 waitTime: { median: false },
32 elu: { median: false }
33 }
34
35 /**
36 * Default measurement statistics requirements.
37 */
38 export const DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS: MeasurementStatisticsRequirements =
39 {
40 aggregate: false,
41 average: false,
42 median: false
43 }
44
45 /**
46 * Returns safe host OS optimized estimate of the default amount of parallelism a pool should use.
47 * Always returns a value greater than zero.
48 *
49 * @returns The host OS optimized maximum pool size.
50 */
51 export const availableParallelism = (): number => {
52 let availableParallelism = 1
53 try {
54 availableParallelism = os.availableParallelism()
55 } catch {
56 const cpus = os.cpus()
57 if (Array.isArray(cpus) && cpus.length > 0) {
58 availableParallelism = cpus.length
59 }
60 }
61 return availableParallelism
62 }
63
64 /**
65 * Returns the worker type of the given worker.
66 *
67 * @param worker - The worker to get the type of.
68 * @returns The worker type of the given worker.
69 * @internal
70 */
71 export const getWorkerType = (worker: IWorker): WorkerType | undefined => {
72 if (worker instanceof ThreadWorker) {
73 return WorkerTypes.thread
74 } else if (worker instanceof ClusterWorker) {
75 return WorkerTypes.cluster
76 }
77 }
78
79 /**
80 * Returns the worker id of the given worker.
81 *
82 * @param worker - The worker to get the id of.
83 * @returns The worker id of the given worker.
84 * @internal
85 */
86 export const getWorkerId = (worker: IWorker): number | undefined => {
87 if (worker instanceof ThreadWorker) {
88 return worker.threadId
89 } else if (worker instanceof ClusterWorker) {
90 return worker.id
91 }
92 }
93
94 /**
95 * Sleeps for the given amount of milliseconds.
96 *
97 * @param ms - The amount of milliseconds to sleep.
98 * @returns A promise that resolves after the given amount of milliseconds.
99 * @internal
100 */
101 export const sleep = async (ms: number): Promise<void> => {
102 await new Promise(resolve => {
103 setTimeout(resolve, ms)
104 })
105 }
106
107 /**
108 * Computes the retry delay in milliseconds using an exponential back off algorithm.
109 *
110 * @param retryNumber - The number of retries that have already been attempted
111 * @param delayFactor - The base delay factor in milliseconds
112 * @returns Delay in milliseconds
113 * @internal
114 */
115 export const exponentialDelay = (
116 retryNumber = 0,
117 delayFactor = 100
118 ): number => {
119 const delay = Math.pow(2, retryNumber) * delayFactor
120 const randomSum = delay * 0.2 * secureRandom() // 0-20% of the delay
121 return delay + randomSum
122 }
123
124 /**
125 * Computes the average of the given data set.
126 *
127 * @param dataSet - Data set.
128 * @returns The average of the given data set.
129 * @internal
130 */
131 export const average = (dataSet: number[]): number => {
132 if (Array.isArray(dataSet) && dataSet.length === 0) {
133 return 0
134 }
135 if (Array.isArray(dataSet) && dataSet.length === 1) {
136 return dataSet[0]
137 }
138 return (
139 dataSet.reduce((accumulator, number) => accumulator + number, 0) /
140 dataSet.length
141 )
142 }
143
144 /**
145 * Computes the median of the given data set.
146 *
147 * @param dataSet - Data set.
148 * @returns The median of the given data set.
149 * @internal
150 */
151 export const median = (dataSet: number[]): number => {
152 if (Array.isArray(dataSet) && dataSet.length === 0) {
153 return 0
154 }
155 if (Array.isArray(dataSet) && dataSet.length === 1) {
156 return dataSet[0]
157 }
158 const sortedDataSet = dataSet.slice().sort((a, b) => a - b)
159 return (
160 (sortedDataSet[(sortedDataSet.length - 1) >> 1] +
161 sortedDataSet[sortedDataSet.length >> 1]) /
162 2
163 )
164 }
165
166 /**
167 * Rounds the given number to the given scale.
168 * The rounding is done using the "round half away from zero" method.
169 *
170 * @param num - The number to round.
171 * @param scale - The scale to round to.
172 * @returns The rounded number.
173 * @internal
174 */
175 export const round = (num: number, scale = 2): number => {
176 const rounder = Math.pow(10, scale)
177 return Math.round(num * rounder * (1 + Number.EPSILON)) / rounder
178 }
179
180 /**
181 * Is the given object a plain object?
182 *
183 * @param obj - The object to check.
184 * @returns `true` if the given object is a plain object, `false` otherwise.
185 * @internal
186 */
187 export const isPlainObject = (obj: unknown): boolean =>
188 typeof obj === 'object' &&
189 obj !== null &&
190 obj?.constructor === Object &&
191 Object.prototype.toString.call(obj) === '[object Object]'
192
193 /**
194 * Detects whether the given value is a kill behavior or not.
195 *
196 * @typeParam KB - Which specific KillBehavior type to test against.
197 * @param killBehavior - Which kind of kill behavior to detect.
198 * @param value - Any value.
199 * @returns `true` if `value` was strictly equals to `killBehavior`, otherwise `false`.
200 * @internal
201 */
202 export const isKillBehavior = <KB extends KillBehavior>(
203 killBehavior: KB,
204 value: unknown
205 ): value is KB => {
206 return value === killBehavior
207 }
208
209 /**
210 * Detects whether the given value is an asynchronous function or not.
211 *
212 * @param fn - Any value.
213 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
214 * @internal
215 */
216 export const isAsyncFunction = (
217 fn: unknown
218 ): fn is (...args: unknown[]) => Promise<unknown> => {
219 return typeof fn === 'function' && fn.constructor.name === 'AsyncFunction'
220 }
221
222 /**
223 * Generates a cryptographically secure random number in the [0,1[ range
224 *
225 * @returns A number in the [0,1[ range
226 * @internal
227 */
228 export const secureRandom = (): number => {
229 return webcrypto.getRandomValues(new Uint32Array(1))[0] / 0x100000000
230 }
231
232 /**
233 * Returns the minimum of the given numbers.
234 * If no numbers are given, `Infinity` is returned.
235 *
236 * @param args - The numbers to get the minimum of.
237 * @returns The minimum of the given numbers.
238 * @internal
239 */
240 export const min = (...args: number[]): number =>
241 args.reduce((minimum, num) => (minimum < num ? minimum : num), Infinity)
242
243 /**
244 * Returns the maximum of the given numbers.
245 * If no numbers are given, `-Infinity` is returned.
246 *
247 * @param args - The numbers to get the maximum of.
248 * @returns The maximum of the given numbers.
249 * @internal
250 */
251 export const max = (...args: number[]): number =>
252 args.reduce((maximum, num) => (maximum > num ? maximum : num), -Infinity)