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 cpus
= os
.cpus()
63 if (Array.isArray(cpus
) && cpus
.length
> 0) {
64 availableParallelism
= cpus
.length
67 return availableParallelism
71 * Returns the worker type of the given worker.
73 * @param worker - The worker to get the type of.
74 * @returns The worker type of the given worker.
77 export const getWorkerType
= <Worker
extends IWorker
>(
79 ): WorkerType
| undefined => {
80 if (worker
instanceof ThreadWorker
) {
81 return WorkerTypes
.thread
82 } else if (worker
instanceof ClusterWorker
) {
83 return WorkerTypes
.cluster
88 * Returns the worker id of the given worker.
90 * @param worker - The worker to get the id of.
91 * @returns The worker id of the given worker.
94 export const getWorkerId
= <Worker
extends IWorker
>(
96 ): number | undefined => {
97 if (worker
instanceof ThreadWorker
) {
98 return worker
.threadId
99 } else if (worker
instanceof ClusterWorker
) {
105 * Sleeps for the given amount of milliseconds.
107 * @param ms - The amount of milliseconds to sleep.
108 * @returns A promise that resolves after the given amount of milliseconds.
110 export const sleep
= async (ms
: number): Promise
<void> => {
111 await new Promise((resolve
) => {
112 setTimeout(resolve
, ms
)
117 * Computes the retry delay in milliseconds using an exponential back off algorithm.
119 * @param retryNumber - The number of retries that have already been attempted
120 * @param delayFactor - The base delay factor in milliseconds
121 * @returns Delay in milliseconds
124 export const exponentialDelay
= (
128 const delay
= Math.pow(2, retryNumber
) * delayFactor
129 const randomSum
= delay
* 0.2 * secureRandom() // 0-20% of the delay
130 return delay
+ randomSum
134 * Computes the average of the given data set.
136 * @param dataSet - Data set.
137 * @returns The average of the given data set.
140 export const average
= (dataSet
: number[]): number => {
141 if (Array.isArray(dataSet
) && dataSet
.length
=== 0) {
144 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
148 dataSet
.reduce((accumulator
, number) => accumulator
+ number, 0) /
154 * Computes the median of the given data set.
156 * @param dataSet - Data set.
157 * @returns The median of the given data set.
160 export const median
= (dataSet
: number[]): number => {
161 if (Array.isArray(dataSet
) && dataSet
.length
=== 0) {
164 if (Array.isArray(dataSet
) && dataSet
.length
=== 1) {
167 const sortedDataSet
= dataSet
.slice().sort((a
, b
) => a
- b
)
169 (sortedDataSet
[(sortedDataSet
.length
- 1) >> 1] +
170 sortedDataSet
[sortedDataSet
.length
>> 1]) /
176 * Rounds the given number to the given scale.
177 * The rounding is done using the "round half away from zero" method.
179 * @param num - The number to round.
180 * @param scale - The scale to round to.
181 * @returns The rounded number.
183 export const round
= (num
: number, scale
= 2): number => {
184 const rounder
= Math.pow(10, scale
)
185 return Math.round(num
* rounder
* (1 + Number.EPSILON
)) / rounder
189 * Is the given object a plain object?
191 * @param obj - The object to check.
192 * @returns `true` if the given object is a plain object, `false` otherwise.
194 export const isPlainObject
= (obj
: unknown
): boolean =>
195 typeof obj
=== 'object' &&
197 obj
?.constructor
=== Object &&
198 Object.prototype
.toString
.call(obj
) === '[object Object]'
201 * Detects whether the given value is a kill behavior or not.
203 * @typeParam KB - Which specific KillBehavior type to test against.
204 * @param killBehavior - Which kind of kill behavior to detect.
205 * @param value - Any value.
206 * @returns `true` if `value` was strictly equals to `killBehavior`, otherwise `false`.
209 export const isKillBehavior
= <KB
extends KillBehavior
>(
213 return value
=== killBehavior
217 * Detects whether the given value is an asynchronous function or not.
219 * @param fn - Any value.
220 * @returns `true` if `fn` was an asynchronous function, otherwise `false`.
222 export const isAsyncFunction
= (
224 ): fn
is (...args
: unknown
[]) => Promise
<unknown
> => {
225 return typeof fn
=== 'function' && fn
.constructor
.name
=== 'AsyncFunction'
229 * Updates the given measurement statistics.
231 * @param measurementStatistics - The measurement statistics to update.
232 * @param measurementRequirements - The measurement statistics requirements.
233 * @param measurementValue - The measurement value.
234 * @param numberOfMeasurements - The number of measurements.
237 export const updateMeasurementStatistics
= (
238 measurementStatistics
: MeasurementStatistics
,
239 measurementRequirements
: MeasurementStatisticsRequirements
,
240 measurementValue
: number
242 if (measurementRequirements
.aggregate
) {
243 measurementStatistics
.aggregate
=
244 (measurementStatistics
.aggregate
?? 0) + measurementValue
245 measurementStatistics
.minimum
= Math.min(
247 measurementStatistics
.minimum
?? Infinity
249 measurementStatistics
.maximum
= Math.max(
251 measurementStatistics
.maximum
?? -Infinity
254 (measurementRequirements
.average
|| measurementRequirements
.median
) &&
255 measurementValue
!= null
257 measurementStatistics
.history
.push(measurementValue
)
258 if (measurementRequirements
.average
) {
259 measurementStatistics
.average
= average(measurementStatistics
.history
)
260 } else if (measurementStatistics
.average
!= null) {
261 delete measurementStatistics
.average
263 if (measurementRequirements
.median
) {
264 measurementStatistics
.median
= median(measurementStatistics
.history
)
265 } else if (measurementStatistics
.median
!= null) {
266 delete measurementStatistics
.median
273 * Generate a cryptographically secure random number in the [0,1[ range
275 * @returns A number in the [0,1[ range
277 export const secureRandom
= (): number => {
278 return webcrypto
.getRandomValues(new Uint32Array(1))[0] / 0x100000000