event-storage 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,132 @@
1
1
  import crypto from 'crypto';
2
- import { assert, assertEqual } from './util.js';
3
- import { BYTE_OPEN_OBJECT, indexOfSameLevel } from './jsonUtil.js';
2
+ import {assert, assertEqual} from './util.js';
3
+ import {
4
+ indexOfSameLevel,
5
+ findJsonValueEnd,
6
+ parseJsonValue,
7
+ matchesAnyValuePattern,
8
+ isOpeningObject,
9
+ compareNumeric
10
+ } from './jsonUtil.js';
4
11
 
12
+ const compiledOperatorMatcherCache = new WeakMap();
13
+
14
+ /**
15
+ * @param {any} value Value to classify.
16
+ * @returns {boolean} True when `value` is a non-null object.
17
+ */
18
+ function isObject(value) {
19
+ return value !== null && typeof value === 'object';
20
+ }
21
+
22
+ /**
23
+ * @param {any} value Value to classify.
24
+ * @returns {boolean} True when `value` is a non-array object.
25
+ */
5
26
  function isPlainObject(value) {
6
27
  return value !== null && typeof value === 'object' && !Array.isArray(value);
7
28
  }
8
29
 
30
+ /**
31
+ * @param {object} obj Candidate matcher object.
32
+ * @returns {boolean} True when all keys are operator keys (`$...`).
33
+ */
34
+ function isOperatorObject(obj) {
35
+ const keys = Object.keys(obj);
36
+ return keys.length > 0 && keys.every(key => key.startsWith('$'));
37
+ }
38
+
39
+ /**
40
+ * Dispatch between array (OR), operator object, nested object, and scalar equality matching
41
+ * so callers don't need to know the shape of `matcherValue`.
42
+ *
43
+ * @param {any} documentValue Value from the document.
44
+ * @param {any} matcherValue Value from the matcher definition.
45
+ * @returns {boolean} True when both values match under matcher semantics.
46
+ */
9
47
  function propertyMatchesValue(documentValue, matcherValue) {
10
- if (Array.isArray(matcherValue)) {
11
- return matcherValue.includes(documentValue);
12
- } else if (matcherValue && typeof matcherValue === 'object') {
48
+ if (isObject(matcherValue)) {
49
+ if (Array.isArray(matcherValue)) {
50
+ return matcherValue.includes(documentValue);
51
+ } else if (isOperatorObject(matcherValue)) {
52
+ const operatorChecks = getCompiledOperatorChecks(matcherValue);
53
+ return matchesCompiledOperators(documentValue, operatorChecks);
54
+ }
13
55
  return matches(documentValue, matcherValue);
14
56
  }
15
57
  return typeof matcherValue === 'undefined' || documentValue === matcherValue;
16
58
  }
17
59
 
60
+ /**
61
+ * Pre-compile an operator object into an array of comparison closures so the hot path avoids
62
+ * repeated `Object.entries` + switch dispatch per matched document.
63
+ *
64
+ * @param {object} operatorObj Object containing operator/value pairs.
65
+ * @returns {Array<function(any): boolean>} Compiled predicate checks in evaluation order.
66
+ */
67
+ function buildOperatorChecks(operatorObj) {
68
+ const checks = [];
69
+ for (const [operator, expectedValue] of Object.entries(operatorObj)) {
70
+ switch (operator) {
71
+ case '$gt':
72
+ checks.push(value => value > expectedValue);
73
+ break;
74
+ case '$gte':
75
+ checks.push(value => value >= expectedValue);
76
+ break;
77
+ case '$lt':
78
+ checks.push(value => value < expectedValue);
79
+ break;
80
+ case '$lte':
81
+ checks.push(value => value <= expectedValue);
82
+ break;
83
+ case '$eq':
84
+ checks.push(value => value === expectedValue);
85
+ break;
86
+ case '$ne':
87
+ checks.push(value => value !== expectedValue);
88
+ break;
89
+ default:
90
+ throw new TypeError(`Unknown operator: ${operator}`);
91
+ }
92
+ }
93
+ return checks;
94
+ }
95
+
96
+ /**
97
+ * Return cached compiled checks for `operatorObj`, compiling and caching on first access.
98
+ * Using WeakMap keeps operator objects GC-eligible and avoids mutating user-supplied objects.
99
+ *
100
+ * @param {object} operatorObj Object containing operator/value pairs.
101
+ * @returns {Array<function(any): boolean>} Cached or newly compiled operator checks.
102
+ */
103
+ function getCompiledOperatorChecks(operatorObj) {
104
+ const cachedChecks = compiledOperatorMatcherCache.get(operatorObj);
105
+ if (cachedChecks) {
106
+ return cachedChecks;
107
+ }
108
+ const checks = buildOperatorChecks(operatorObj);
109
+ compiledOperatorMatcherCache.set(operatorObj, checks);
110
+ return checks;
111
+ }
112
+
113
+ /**
114
+ * @param {any} documentValue Parsed scalar value from the document.
115
+ * @param {Array<function(any): boolean>} checks Compiled operator checks.
116
+ * @returns {boolean} True when all checks pass.
117
+ */
118
+ function matchesCompiledOperators(documentValue, checks) {
119
+ if (documentValue === undefined) {
120
+ return false;
121
+ }
122
+ for (const check of checks) {
123
+ if (!check(documentValue)) {
124
+ return false;
125
+ }
126
+ }
127
+ return true;
128
+ }
129
+
18
130
  /**
19
131
  * Build a buffer containing the file magic header and a JSON stringified metadata block, padded to be a multiple of 16 bytes long.
20
132
  *
@@ -42,10 +154,10 @@ function buildMetadataHeader(magic, metadata) {
42
154
  * @returns {function(string)} A function that calculates the HMAC for a given string
43
155
  */
44
156
  const createHmac = secret => string => {
45
- const hmac = crypto.createHmac('sha256', secret);
46
- hmac.update(string);
47
- return hmac.digest('hex');
48
- };
157
+ const hmac = crypto.createHmac('sha256', secret);
158
+ hmac.update(string);
159
+ return hmac.digest('hex');
160
+ };
49
161
 
50
162
  /**
51
163
  * @typedef {object|function(object):boolean} Matcher
@@ -76,14 +188,15 @@ function matches(document, matcher) {
76
188
  * @returns {{matcher: string|object, hmac?: string}}
77
189
  */
78
190
  function buildMetadataForMatcher(matcher, hmac) {
79
- if (!matcher) {
80
- return undefined;
81
- }
191
+ /* c8 ignore next 2 */
192
+ if (!matcher) {
193
+ return undefined;
194
+ }
82
195
  if (typeof matcher === 'object') {
83
- return { matcher };
196
+ return {matcher};
84
197
  }
85
198
  const matcherString = matcher.toString();
86
- return { matcher: matcherString, hmac: hmac(matcherString) };
199
+ return {matcher: matcherString, hmac: hmac(matcherString)};
87
200
  }
88
201
 
89
202
  /**
@@ -92,11 +205,12 @@ function buildMetadataForMatcher(matcher, hmac) {
92
205
  * @returns {Matcher} The matcher object or function.
93
206
  */
94
207
  function buildMatcherFromMetadata(matcherMetadata, hmac) {
95
- let matcher;
96
- if (typeof matcherMetadata.matcher === 'object') {
97
- matcher = matcherMetadata.matcher;
98
- } else {
99
- assert(matcherMetadata.hmac === hmac(matcherMetadata.matcher), 'Invalid HMAC for matcher.');
208
+ let matcher;
209
+ if (typeof matcherMetadata.matcher === 'object') {
210
+ matcher = matcherMetadata.matcher;
211
+ } else {
212
+ /* c8 ignore next 1 */
213
+ assert(matcherMetadata.hmac === hmac(matcherMetadata.matcher), 'Invalid HMAC for matcher.');
100
214
 
101
215
  matcher = eval('(' + matcherMetadata.matcher + ')').bind({}); // jshint ignore:line
102
216
  }
@@ -112,33 +226,35 @@ function buildMatcherFromMetadata(matcherMetadata, hmac) {
112
226
  */
113
227
  function buildTypeMatcherFn(payloadPath) {
114
228
  const parts = payloadPath.split('.');
115
- return function(typeValue) {
229
+ return function (typeValue) {
116
230
  let obj = typeValue;
117
231
  for (let i = parts.length - 1; i >= 0; i--) {
118
- obj = { [parts[i]]: obj };
232
+ obj = {[parts[i]]: obj};
119
233
  }
120
- return { payload: obj };
234
+ return {payload: obj};
121
235
  };
122
236
  }
123
237
 
124
238
  /**
125
- * Builds a raw-buffer matcher.
126
- * It expects the Buffer to contain compact stringified JSON
127
- * and supports matcher objects with sub properties and multi-value matches (OR/any of).
239
+ * Compile an object matcher into a raw-buffer predicate so raw-mode reads can filter compact
240
+ * JSON without parsing every document first.
128
241
  *
129
- * @param {object} matcher Object matcher.
130
- * @returns {function(Buffer): boolean}
242
+ * @param {object} matcher Object matcher to compile.
243
+ * @param {{enableOperatorBufferMatcher?: boolean}} [options] Raw matcher build options.
244
+ * @returns {function(Buffer): boolean} Predicate over compact JSON buffers.
131
245
  */
132
- function buildRawBufferMatcher(matcher = {}) {
133
- assert(matcher && typeof matcher ==='object' && !Array.isArray(matcher), 'Matcher must be an object.', TypeError);
246
+ function buildRawBufferMatcher(matcher = {}, options = {}) {
247
+ assert(isPlainObject(matcher), 'Matcher must be an object.', TypeError);
248
+ const enableOperatorBufferMatcher = options.enableOperatorBufferMatcher !== false;
134
249
 
135
- const root = buildMatcherTree(matcher);
250
+ const root = buildMatcherTree(matcher, enableOperatorBufferMatcher);
251
+ /* c8 ignore next 3 */
136
252
  if (root.children.length === 0) {
137
253
  return () => true;
138
254
  }
139
255
 
140
256
  return function matchesRawBuffer(buffer) {
141
- if (buffer[0] !== BYTE_OPEN_OBJECT) {
257
+ if (!isOpeningObject(buffer[0])) {
142
258
  return false;
143
259
  }
144
260
  if (!preCheck(buffer, 1, root)) {
@@ -149,93 +265,247 @@ function buildRawBufferMatcher(matcher = {}) {
149
265
  }
150
266
 
151
267
  /**
152
- * Optimization pass: check that every required byte pattern is present anywhere in the buffer
153
- * before spending the more expensive per-depth scan in `matchesNode`.
154
- */
155
- function preCheck(buffer, startOffset, node) {
156
- for (const child of node.children) {
157
- if (child.valuePatterns && !child.valuePatterns.some((pattern, i) => {
158
- child.valueMatches[i] = buffer.indexOf(pattern, startOffset);
159
- return child.valueMatches[i] !== -1;
160
- })) {
161
- return false;
162
- }
163
- if (child.objectPattern) {
164
- const objectMatch = buffer.indexOf(child.objectPattern, startOffset);
165
- if (objectMatch === -1) {
166
- return false;
167
- }
168
- child.objMatch = objectMatch;
169
- if (!preCheck(buffer, objectMatch, child.node)) {
170
- return false;
171
- }
172
- }
173
- }
174
- return true;
175
- }
176
-
177
- /**
178
- * Pre-compile a plain object matcher into a tree of byte patterns so `matchesNode` can scan
179
- * raw JSON buffers without deserializing them.
268
+ * Compile a matcher object into a tree whose children each carry one primary byte pattern plus
269
+ * optional follow-up checks for nested objects, operators, or multi-value scalars.
270
+ * Fast scalar-equality children are placed first so preCheck and matchesNode short-circuit early.
271
+ *
272
+ * @param {object} matcher Matcher object for this tree level.
273
+ * @returns {{children: Array<object>}} Compiled child descriptors for this level.
180
274
  */
181
275
  function buildMatcherTree(matcher) {
182
- const node = { children: [] };
276
+ const fast = [];
277
+ const slow = [];
183
278
 
184
279
  for (const [key, value] of Object.entries(matcher)) {
185
- node.children.push(buildMatcherTreeChild(key, value));
280
+ const child = buildMatcherTreeChild(key, value);
281
+ // A child with only one byte pattern and no follow-up matcher cannot be outperformed by any extra matcher logic.
282
+ (!child.matches ? fast : slow).push(child);
186
283
  }
187
284
 
188
- return node;
285
+ return {children: [...fast, ...slow]};
189
286
  }
190
287
 
288
+ /**
289
+ * Normalize one matcher property into the cheapest raw-buffer strategy for that value shape.
290
+ * Children that compile to a plain byte-equality pattern leave `matches` as null.
291
+ *
292
+ * @param {string} key Property name at this matcher level.
293
+ * @param {any} value Matcher value for `key`.
294
+ * @returns {{pattern: Buffer, isKeyPattern: boolean, matches: ((function(Buffer, number): boolean)|null), node: ({children: Array<object>}|null), lastMatch: number}} Compiled descriptor consumed by preCheck/matchesNode.
295
+ */
191
296
  function buildMatcherTreeChild(key, value) {
192
297
  const keyPrefix = Buffer.from(`${JSON.stringify(key)}:`, 'utf8');
193
- const child = { objectPattern: null, valuePatterns: null, node: null, objMatch: null, valueMatches: [] };
194
- if (Array.isArray(value)) {
195
- if (value.some(item => item && typeof item === 'object')) {
196
- throw new TypeError('Array matcher values must be scalars.');
298
+ const child = {
299
+ pattern: null,
300
+ isKeyPattern: false,
301
+ matches: null,
302
+ node: null,
303
+ lastMatch: -1
304
+ };
305
+
306
+ if (isObject(value)) {
307
+ if (Array.isArray(value)) {
308
+ assert(!value.some(isObject), 'Array matcher values must be scalars.', TypeError);
309
+ if (value.length === 1) {
310
+ child.pattern = buildKeyValuePattern(keyPrefix, value[0]);
311
+ } else {
312
+ child.isKeyPattern = true;
313
+ child.pattern = keyPrefix;
314
+ const valuePatterns = value.map(item => Buffer.from(JSON.stringify(item), 'utf8'));
315
+ child.matches = (buffer, startOffset) => matchesAnyValuePattern(buffer, startOffset, valuePatterns);
316
+ }
317
+ } else if ('$eq' in value && Object.keys(value).length === 1) {
318
+ // A lone $eq is semantically identical to a scalar equality check — fold it into a
319
+ // value pattern at compile time so the buffer scan takes the same fast path as { key: value }.
320
+ child.pattern = buildKeyValuePattern(keyPrefix, value['$eq']);
321
+ } else if ('$ne' in value && Object.keys(value).length === 1) {
322
+ // A lone $ne is the logical negation of $eq: confirm the key exists at this level,
323
+ // then reject only when the value byte-matches the excluded pattern.
324
+ child.isKeyPattern = true;
325
+ child.pattern = keyPrefix;
326
+ const nePattern = [Buffer.from(JSON.stringify(value['$ne']), 'utf8')];
327
+ child.matches = (buffer, valueStart) => !matchesAnyValuePattern(buffer, valueStart, nePattern);
328
+ } else if (isOperatorObject(value)) {
329
+ child.isKeyPattern = true;
330
+ child.pattern = keyPrefix;
331
+ child.matches = buildOperatorBufferMatcher(value);
332
+ } else {
333
+ child.pattern = Buffer.concat([keyPrefix, Buffer.from('{', 'utf8')]);
334
+ child.node = buildMatcherTree(value);
335
+ child.matches = (buffer, startOffset) => matchesNode(buffer, startOffset, child.node);
197
336
  }
198
- child.valuePatterns = value.map(item => buildValuePattern(keyPrefix, item));
199
- return child;
200
- } else if (value && typeof value === 'object') {
201
- child.objectPattern = Buffer.concat([keyPrefix, Buffer.from('{', 'utf8')]);
202
- child.node = buildMatcherTree(value);
203
- return child;
337
+ } else {
338
+ child.pattern = buildKeyValuePattern(keyPrefix, value);
204
339
  }
205
- child.valuePatterns = [buildValuePattern(keyPrefix, value)];
206
340
  return child;
207
341
  }
208
342
 
209
- function buildValuePattern(keyPrefix, value) {
343
+ /**
344
+ * @param {Buffer} keyPrefix Serialized key prefix (`"key":`).
345
+ * @param {any} value Scalar value to append.
346
+ * @returns {Buffer} Full serialized `"key":value` pattern.
347
+ */
348
+ function buildKeyValuePattern(keyPrefix, value) {
210
349
  return Buffer.concat([keyPrefix, Buffer.from(JSON.stringify(value), 'utf8')]);
211
350
  }
212
351
 
213
352
  /**
214
- * Verify that each required byte pattern in the tree is present at the correct JSON nesting
215
- * depth so values inside nested objects don't satisfy a top-level match requirement.
353
+ * Cheap pass: confirm each child's primary pattern exists somewhere and cache that position as a
354
+ * hint for the depth-aware pass that follows.
355
+ *
356
+ * @param {Buffer} buffer Compact JSON document buffer.
357
+ * @param {number} startOffset Start offset within `buffer`.
358
+ * @param {{children: Array<object>}} node Compiled matcher node for this level.
359
+ * @returns {boolean} True when every child pattern exists somewhere from `startOffset`.
360
+ */
361
+ function preCheck(buffer, startOffset, node) {
362
+ for (const child of node.children) {
363
+ child.lastMatch = buffer.indexOf(child.pattern, startOffset);
364
+ if (child.lastMatch === -1) {
365
+ return false;
366
+ }
367
+ if (child.node && !preCheck(buffer, child.lastMatch, child.node)) {
368
+ return false;
369
+ }
370
+ }
371
+ return true;
372
+ }
373
+
374
+ /**
375
+ * Confirm that each prechecked child really matches at the requested JSON level and then run the
376
+ * optional value-specific follow-up checks from the compiled tree.
377
+ *
378
+ * @param {Buffer} buffer Compact JSON document buffer.
379
+ * @param {number} startOffset Start offset within `buffer`.
380
+ * @param {{children: Array<object>}} node Compiled matcher node for this level.
381
+ * @returns {boolean} True when all children match at the requested JSON level.
216
382
  */
217
383
  function matchesNode(buffer, startOffset, node) {
218
384
  for (const child of node.children) {
219
- if (child.valuePatterns && !child.valuePatterns.some((pattern, i) => {
220
- return indexOfSameLevel(buffer, pattern, startOffset, child.valueMatches[i]) !== -1;
221
- })) {
385
+ const matchPosition = indexOfSameLevel(buffer, child.pattern, startOffset, child.lastMatch, child.isKeyPattern);
386
+ if (matchPosition === -1) {
222
387
  return false;
223
388
  }
224
389
 
225
- if (child.node) {
226
- const objectIndex = indexOfSameLevel(buffer, child.objectPattern, startOffset, child.objMatch);
227
- if (objectIndex === -1) {
228
- return false;
229
- }
230
- if (!matchesNode(buffer, objectIndex + child.objectPattern.length, child.node)) {
231
- return false;
232
- }
390
+ const valueStart = matchPosition + child.pattern.length;
391
+ if (child.matches && !child.matches(buffer, valueStart)) {
392
+ return false;
233
393
  }
234
394
  }
235
395
 
236
396
  return true;
237
397
  }
238
398
 
399
+ /**
400
+ * Parse the scalar at a matched key position only when operator objects require a real JavaScript
401
+ * comparison instead of a pure byte-pattern check.
402
+ *
403
+ * @param {Buffer} buffer Compact JSON document buffer.
404
+ * @param {number} startOffset Offset of the scalar value to parse.
405
+ * @param {Array<function(any): boolean>} operatorChecks Compiled operator checks.
406
+ * @returns {boolean} True when the parsed scalar satisfies all operators.
407
+ */
408
+ function matchesOperatorInBuffer(buffer, startOffset, operatorChecks) {
409
+ const valueStart = startOffset;
410
+ const valueEnd = findJsonValueEnd(buffer, valueStart);
411
+ /* c8 ignore next 2 */
412
+ if (valueEnd === -1 || valueEnd <= valueStart) {
413
+ return false;
414
+ }
415
+
416
+ const parsedValue = parseJsonValue(buffer, valueStart, valueEnd);
417
+
418
+ return matchesCompiledOperators(parsedValue, operatorChecks);
419
+ }
420
+
421
+ /**
422
+ * Map pre-computed ordering information to operator semantics.
423
+ * ordering: -1 (actual < expected), 0 (equal), 1 (actual > expected)
424
+ *
425
+ * @param {string} operator
426
+ * @param {-1|0|1} ordering
427
+ * @returns {boolean}
428
+ */
429
+ function matchesOrdering(operator, ordering) {
430
+ switch (operator) {
431
+ case '$gt':
432
+ return ordering === 1;
433
+ case '$gte':
434
+ return ordering >= 0;
435
+ case '$lt':
436
+ return ordering === -1;
437
+ case '$lte':
438
+ return ordering <= 0;
439
+ case '$eq':
440
+ return ordering === 0;
441
+ case '$ne':
442
+ return ordering !== 0;
443
+ /* c8 ignore next 1 */
444
+ default:
445
+ return false;
446
+ }
447
+ }
448
+
449
+ /**
450
+ * @param {Array<[string, any]>} entries
451
+ * @returns {Array<{operator: string, expectedNumeric: {isNegative: boolean, integerPart: string, fractionPart: string}}>|null}
452
+ */
453
+ function buildNumericOperatorComparisons(entries) {
454
+ const comparisons = [];
455
+ for (const [operator, expectedValue] of entries) {
456
+ if (typeof expectedValue !== 'number') {
457
+ return null;
458
+ }
459
+ const expectedStr = JSON.stringify(expectedValue);
460
+ const expectedIsNegative = expectedStr[0] === '-';
461
+ const intStart = expectedIsNegative ? 1 : 0;
462
+ const [expectedIntegerPart, expectedFractionPart = ''] = expectedStr.substring(intStart).split('.');
463
+ comparisons.push({
464
+ operator,
465
+ expectedNumeric: {
466
+ isNegative: expectedIsNegative,
467
+ integerPart: expectedIntegerPart,
468
+ fractionPart: expectedFractionPart
469
+ }
470
+ });
471
+ }
472
+ return comparisons;
473
+ }
474
+
475
+ /**
476
+ * Build a specialized buffer-based operator comparator by pre-compiling operator-specific
477
+ * byte shortcuts at matcher build time. This avoids runtime dispatch and enables aggressive
478
+ * short-circuit evaluation for sign mismatches and digit-count differences.
479
+ *
480
+ * Assumes no scientific notation in the JSON buffer.
481
+ *
482
+ * @param {object} operatorObj Operator object (e.g., { $gt: 100 }).
483
+ * @returns {function(Buffer, number): boolean} Predicate that reads the buffer at given offset.
484
+ */
485
+ function buildOperatorBufferMatcher(operatorObj) {
486
+ const entries = Object.entries(operatorObj);
487
+ const numericComparisons = buildNumericOperatorComparisons(entries);
488
+ if (numericComparisons) {
489
+ return (buffer, startOffset) => {
490
+ for (const comparison of numericComparisons) {
491
+ const ordering = compareNumeric(buffer, startOffset, comparison.expectedNumeric);
492
+ /* c8 ignore next 2 */
493
+ if (ordering === null) {
494
+ return false;
495
+ }
496
+ if (!matchesOrdering(comparison.operator, ordering)) {
497
+ return false;
498
+ }
499
+ }
500
+ return true;
501
+ };
502
+ }
503
+
504
+ // Non-numeric expected value: use generic operator checks.
505
+ const operatorChecks = getCompiledOperatorChecks(operatorObj);
506
+ return (buffer, startOffset) => matchesOperatorInBuffer(buffer, startOffset, operatorChecks);
507
+ }
508
+
239
509
  export {
240
510
  createHmac,
241
511
  matches,
package/src/utils/util.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Assert that actual and expected match or throw an Error with the given message appended by information about expected and actual value.
3
3
  *
4
- * @param {*} actual
5
- * @param {*} expected
6
- * @param {string} message
4
+ * @param {*} actual Actual value.
5
+ * @param {*} expected Expected value.
6
+ * @param {string} message Error message prefix.
7
+ * @returns {void}
7
8
  */
8
9
  function assertEqual(actual, expected, message) {
9
10
  if (actual !== expected) {
@@ -14,9 +15,10 @@ function assertEqual(actual, expected, message) {
14
15
  /**
15
16
  * Assert that the condition holds and if not, throw an error with the given message.
16
17
  *
17
- * @param {boolean} condition
18
- * @param {string} message
19
- * @param {typeof Error} ErrorType
18
+ * @param {boolean} condition Condition to verify.
19
+ * @param {string} message Error message when the condition fails.
20
+ * @param {typeof Error} ErrorType Error class to throw.
21
+ * @returns {void}
20
22
  */
21
23
  function assert(condition, message, ErrorType = Error) {
22
24
  if (!condition) {
@@ -27,9 +29,9 @@ function assert(condition, message, ErrorType = Error) {
27
29
  /**
28
30
  * Return the amount required to align value to the given alignment.
29
31
  * It calculates the difference of the alignment and the modulo of value by alignment.
30
- * @param {number} value
31
- * @param {number} alignment
32
- * @returns {number}
32
+ * @param {number} value Source value.
33
+ * @param {number} alignment Target alignment.
34
+ * @returns {number} Additional offset needed to reach the next aligned value.
33
35
  */
34
36
  function alignTo(value, alignment) {
35
37
  return (alignment - (value % alignment)) % alignment;
@@ -38,11 +40,11 @@ function alignTo(value, alignment) {
38
40
  /**
39
41
  * Method for hashing a string (e.g. a partition name) to a 32-bit unsigned integer.
40
42
  *
41
- * @param {string} str
42
- * @returns {number}
43
+ * @param {string} str Input string.
44
+ * @returns {number} 32-bit unsigned hash value.
43
45
  */
44
46
  function hash(str) {
45
- /* istanbul ignore if */
47
+ /* c8 ignore next 3 */
46
48
  if (str.length === 0) {
47
49
  return 0;
48
50
  }
@@ -115,8 +117,9 @@ function wrapAndCheck(index, length) {
115
117
  /**
116
118
  * Iterate an array-like list in forward or reverse order.
117
119
  *
118
- * @param {Iterable} entries
119
- * @param {boolean} forwards
120
+ * @param {Iterable|ArrayLike<any>} entries Entries to iterate.
121
+ * @param {boolean} forwards Iteration direction.
122
+ * @returns {Generator<any>}
120
123
  */
121
124
  function* iterate(entries, forwards) {
122
125
  if (forwards) {
@@ -141,7 +144,7 @@ function* iterate(entries, forwards) {
141
144
  * @param {boolean} [ascending=true] When true, yields items in ascending key order (min-merge).
142
145
  * When false, yields in descending key order (max-merge).
143
146
  * @param {function(*): *} [visit] Optional extractor for the yielded value. Defaults to identity.
144
- * @returns {Generator<*>}
147
+ * @returns {Generator<*>} Merged sequence in key order.
145
148
  */
146
149
  function *kWayMerge(iterables, getSortKey, ascending = true, visit = v => v) {
147
150
  const states = [];
@@ -175,9 +178,9 @@ function *kWayMerge(iterables, getSortKey, ascending = true, visit = v => v) {
175
178
  * Read a scalar value at a dot-notation path from an object.
176
179
  * Returns `undefined` if any path segment is absent or an intermediate value is not an object.
177
180
  *
178
- * @param {object} obj
181
+ * @param {object} obj Source object.
179
182
  * @param {string} dotPath Dot-separated property path, e.g. `'payload.type'`.
180
- * @returns {*}
183
+ * @returns {*} Value at the path or `undefined`.
181
184
  */
182
185
  function getPropertyAtPath(obj, dotPath) {
183
186
  let current = obj;