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'
6 MeasurementStatisticsRequirements
,
7 WorkerChoiceStrategyOptions
8 } from
'./pools/selection-strategies/selection-strategies-types'
9 import type { KillBehavior
} from
'./worker/worker-options'
12 type MeasurementStatistics
,
15 } from
'./pools/worker'
20 export const DEFAULT_TASK_NAME
= 'default'
23 * An intentional empty function.
25 export const EMPTY_FUNCTION
: () => void = Object.freeze(() => {
26 /* Intentionally empty */
30 * Default worker choice strategy options.
32 export const DEFAULT_WORKER_CHOICE_STRATEGY_OPTIONS
: WorkerChoiceStrategyOptions
=
35 runTime
: { median
: false },
36 waitTime
: { median
: false },
37 elu
: { median
: false }
41 * Default measurement statistics requirements.
43 export const DEFAULT_MEASUREMENT_STATISTICS_REQUIREMENTS
: MeasurementStatisticsRequirements
=
51 * Returns safe host OS optimized estimate of the default amount of parallelism a pool should use.
52 * Always returns a value greater than zero.
54 * @returns The host OS optimized maximum pool size.
57 export const availableParallelism
= (): number => {
58 let availableParallelism
= 1
60 availableParallelism
= os
.availableParallelism()
62 const numberOfCpus
= os
.cpus()
63 if (Array.isArray(numberOfCpus
) && numberOfCpus
.length
> 0) {
64 availableParallelism
= numberOfCpus
.length
67 return availableParallelism
71 * Sleeps for the given amount of milliseconds.
73 * @param ms - The amount of milliseconds to sleep.
74 * @returns A promise that resolves after the given amount of milliseconds.
76 export const sleep
= async (ms
: number): Promise
<void> => {
77 await new Promise((resolve
) => {
78 setTimeout(resolve
, ms
)
83 * Computes the retry delay in milliseconds using an exponential back off algorithm.
85 * @param retryNumber - The number of retries that have already been attempted
86 * @param delayFactor - The base delay factor in milliseconds
87 * @returns Delay in milliseconds
90 export const exponentialDelay
= (
94 const delay
= Math.pow(2, retryNumber
) * delayFactor
95 const randomSum
= delay
* 0.2 * secureRandom() // 0-20% of the delay
96 return delay
+ randomSum
100 * Computes the average of the given data set.
102 * @param dataSet - Data set.
103 * @returns The average of the given data set.
106 export const average
= (dataSet
: number[]): number => {
107 if (Array.isArray(dataSet
) && dataSet
.length
=== 0) {
110 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
114 dataSet
.reduce((accumulator
, number) => accumulator
+ number, 0) /
120 * Returns the worker type of the given worker.
122 * @param worker - The worker to get the type of.
123 * @returns The worker type of the given worker.
126 export const getWorkerType
= <Worker
extends IWorker
>(
128 ): WorkerType
| undefined => {
129 if (worker
instanceof ThreadWorker
) {
130 return WorkerTypes
.thread
132 if (worker
instanceof ClusterWorker
) {
133 return WorkerTypes
.cluster
138 * Returns the worker id of the given worker.
140 * @param worker - The worker to get the id of.
141 * @returns The worker id of the given worker.
144 export const getWorkerId
= <Worker
extends IWorker
>(
146 ): number | undefined => {
147 if (worker
instanceof ThreadWorker
) {
148 return worker
.threadId
149 } else if (worker
instanceof ClusterWorker
) {
155 * Computes the median of the given data set.
157 * @param dataSet - Data set.
158 * @returns The median of the given data set.
161 export const median
= (dataSet
: number[]): number => {
162 if (Array.isArray(dataSet
) && dataSet
.length
=== 0) {
165 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
168 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
)
170 (sortedDataSet
[(sortedDataSet
.length
- 1) >> 1] +
171 sortedDataSet
[sortedDataSet
.length
>> 1]) /
177 * Rounds the given number to the given scale.
178 * The rounding is done using the "round half away from zero" method.
180 * @param num - The number to round.
181 * @param scale - The scale to round to.
182 * @returns The rounded number.
184 export const round
= (num
: number, scale
= 2): number => {
185 const rounder
= Math.pow(10, scale
)
186 return Math.round(num
* rounder
* (1 + Number.EPSILON
)) / rounder
190 * Is the given object a plain object?
192 * @param obj - The object to check.
193 * @returns `true` if the given object is a plain object, `false` otherwise.
195 export const isPlainObject
= (obj
: unknown
): boolean =>
196 typeof obj
=== 'object' &&
198 obj
?.constructor
=== Object &&
199 Object.prototype
.toString
.call(obj
) === '[object Object]'
202 * Detects whether the given value is a kill behavior or not.
204 * @typeParam KB - Which specific KillBehavior type to test against.
205 * @param killBehavior - Which kind of kill behavior to detect.
206 * @param value - Any value.
207 * @returns `true` if `value` was strictly equals to `killBehavior`, otherwise `false`.
210 export const isKillBehavior
= <KB
extends KillBehavior
>(
214 return value
=== killBehavior
218 * Detects whether the given value is an asynchronous function or not.
220 * @param fn - Any value.
221 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
223 export const isAsyncFunction
= (
225 ): fn
is (...args
: unknown
[]) => Promise
<unknown
> => {
226 return typeof fn
=== 'function' && fn
.constructor
.name
=== 'AsyncFunction'
230 * Updates the given measurement statistics.
232 * @param measurementStatistics - The measurement statistics to update.
233 * @param measurementRequirements - The measurement statistics requirements.
234 * @param measurementValue - The measurement value.
235 * @param numberOfMeasurements - The number of measurements.
238 export const updateMeasurementStatistics
= (
239 measurementStatistics
: MeasurementStatistics
,
240 measurementRequirements
: MeasurementStatisticsRequirements
,
241 measurementValue
: number
243 if (measurementRequirements
.aggregate
) {
244 measurementStatistics
.aggregate
=
245 (measurementStatistics
.aggregate
?? 0) + measurementValue
246 measurementStatistics
.minimum
= Math.min(
248 measurementStatistics
.minimum
?? Infinity
250 measurementStatistics
.maximum
= Math.max(
252 measurementStatistics
.maximum
?? -Infinity
255 (measurementRequirements
.average
|| measurementRequirements
.median
) &&
256 measurementValue
!= null
258 measurementStatistics
.history
.push(measurementValue
)
259 if (measurementRequirements
.average
) {
260 measurementStatistics
.average
= average(measurementStatistics
.history
)
262 if (measurementRequirements
.median
) {
263 measurementStatistics
.median
= median(measurementStatistics
.history
)
270 * Executes a function once at a time.
272 * @param fn - The function to execute.
273 * @param context - The context to bind the function to.
274 * @returns The function to execute.
276 export const once
= (
277 // eslint-disable-next-line @typescript-eslint/no-explicit-any
278 fn
: (...args
: any[]) => void,
280 // eslint-disable-next-line @typescript-eslint/no-explicit-any
281 ): ((...args
: any[]) => void) => {
283 // eslint-disable-next-line @typescript-eslint/no-explicit-any
284 return function (...args
: any[]): void {
287 fn
.apply(context
, args
)
294 * Generate a cryptographically secure random number in the [0,1[ range
296 * @returns A number in the [0,1[ range
298 export const secureRandom
= (): number => {
299 return webcrypto
.getRandomValues(new Uint32Array(1))[0] / 0x100000000