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