1 /* eslint-disable security/detect-object-injection */
5 * Assorted utility functions.
8 const { common
} = require('@squeep/api-dingus');
10 const { randomBytes
, getHashes
} = require('crypto');
11 const { promisify
} = require('util');
15 * Wrap this in a promise, as crypto.randomBytes is capable of blocking.
17 const randomBytesAsync
= promisify(randomBytes
);
21 * The HMAC hashes we are willing to support.
22 * @param {string} algorithm potential sha* algorithm
23 * @returns {boolean} is supported
25 const validHash
= (algorithm
) => getHashes()
26 .filter((h
) => h
.match(/^sha\d+$/))
31 * Return an array containing x if x is not an array.
32 * @param {*} x possibly an array
33 * @returns {Array} x or [x]
35 const ensureArray
= (x
) => {
36 if (x
=== undefined) {
39 if (!Array
.isArray(x
)) {
47 * Recursively freeze an object.
48 * @param {object} o object
49 * @returns {object} frozen object
51 const freezeDeep
= (o
) => {
53 Object
.getOwnPropertyNames(o
).forEach((prop
) => {
54 if (Object
.hasOwn(o
, prop
)
55 && ['object', 'function'].includes(typeof o
[prop
])
56 && !Object
.isFrozen(o
[prop
])) {
57 return freezeDeep(o
[prop
]);
65 * Pick out useful got response fields.
66 * @param {*} res response
67 * @returns {object} winnowed response
69 const gotResponseLogData
= (res
) => {
70 const data
= common
.pick(res
, [
77 if (typeof res
.body
=== 'string') {
78 data
.body
= logTruncate(data
.body
, 100);
79 } else if (res
.body
instanceof Buffer
) {
80 data
.body
= `<Buffer ${res.body.byteLength} bytes>`;
82 if (res
?.timings
?.phases
?.total
) {
83 data
.elapsedTimeMs
= res
.timings
.phases
.total
;
85 if (res
?.redirectUrls
?.length
) {
86 data
.redirectUrls
= res
.redirectUrls
;
88 if (res
?.retryCount
) {
89 data
.retryCount
= res
.retryCount
;
96 * Fallback values, if not configured.
97 * @returns {object} object
99 const topicLeaseDefaults
= () => {
100 return Object
.freeze({
101 leaseSecondsPreferred: 86400 * 10,
102 leaseSecondsMin: 86400 * 1,
103 leaseSecondsMax: 86400 * 365,
109 * Pick from a range, constrained, with some fuzziness.
110 * @param {number} attempt attempt number
111 * @param {number[]=} retryBackoffSeconds array of indexed delays
112 * @param {number=} jitter vary backoff by up to this fraction additional
113 * @returns {number} seconds to delay retry
115 const attemptRetrySeconds
= (attempt
, retryBackoffSeconds
= [60, 120, 360, 1440, 7200, 43200, 86400], jitter
= 0.618) => {
116 const maxAttempt
= retryBackoffSeconds
.length
- 1;
117 if (typeof attempt
!== 'number' || attempt
< 0) {
119 } else if (attempt
> maxAttempt
) {
120 attempt
= maxAttempt
;
123 let seconds
= retryBackoffSeconds
[attempt
];
124 seconds
+= Math
.floor(Math
.random() * seconds
* jitter
);
130 * Return array items split as an array of arrays of no more than per items each.
131 * @param {Array} array items
132 * @param {number} per chunk size
133 * @returns {Array[]} array of chunks
135 const arrayChunk
= (array
, per
= 1) => {
136 const nChunks
= Math
.ceil(array
.length
/ per
);
137 return Array
.from(Array(nChunks
), (_
, i
) => array
.slice(i
* per
, (i
+ 1) * per
));
142 * Be paranoid about blowing the stack when pushing to an array.
143 * @param {Array} dst destination array
144 * @param {Array} src source array
146 const stackSafePush
= (dst
, src
) => {
147 const jsEngineMaxArguments
= 2**16; // Current as of Node 12
148 arrayChunk(src
, jsEngineMaxArguments
).forEach((items
) => {
149 Array
.prototype.push
.apply(dst
, items
);
155 * Limit length of string to keep logs sane
156 * @param {string} str string
157 * @param {number} len max length
158 * @returns {string} truncated string
160 const logTruncate
= (str
, len
) => {
161 if (typeof str
!== 'string' || str
.toString().length
<= len
) {
164 return str
.toString().slice(0, len
) + `... (${str.toString().length} bytes)`;