measure-fn 3.11.1 → 3.11.2

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.
Files changed (2) hide show
  1. package/index.ts +692 -198
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -3,10 +3,12 @@
3
3
  const toAlpha = (num: number): string => {
4
4
  let result = '';
5
5
  let n = num;
6
+
6
7
  do {
7
8
  result = String.fromCharCode(97 + (n % 26)) + result;
8
9
  n = Math.floor(n / 26) - 1;
9
10
  } while (n >= 0);
11
+
10
12
  return result;
11
13
  };
12
14
 
@@ -16,29 +18,65 @@ let maxResultLen = 0;
16
18
 
17
19
  export const safeStringify = (value: unknown, limit?: number): string => {
18
20
  const cap = limit ?? maxResultLen;
21
+
19
22
  if (value === undefined) return '';
20
23
  if (value === null) return 'null';
21
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
22
- if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`;
23
- if (typeof value === 'symbol') return value.toString();
24
+
25
+ if (typeof value === 'number' || typeof value === 'boolean') {
26
+ return String(value);
27
+ }
28
+
29
+ if (typeof value === 'bigint') {
30
+ return `${value}n`;
31
+ }
32
+
33
+ if (typeof value === 'function') {
34
+ return `[Function: ${value.name || 'anonymous'}]`;
35
+ }
36
+
37
+ if (typeof value === 'symbol') {
38
+ return value.toString();
39
+ }
40
+
24
41
  if (typeof value === 'string') {
25
- const q = JSON.stringify(value);
26
- if (cap === 0) return q;
27
- return q.length > cap ? q.slice(0, cap - 1) + '…"' : q;
42
+ const quoted = JSON.stringify(value);
43
+
44
+ if (cap === 0) {
45
+ return quoted;
46
+ }
47
+
48
+ return quoted.length > cap ? quoted.slice(0, Math.max(0, cap - 1)) + '…"' : quoted;
28
49
  }
50
+
29
51
  try {
30
- const seen = new WeakSet();
52
+ const seen = new WeakSet<object>();
53
+
31
54
  const str = JSON.stringify(value, (_key, val) => {
55
+ if (typeof val === 'bigint') {
56
+ return `${val}n`;
57
+ }
58
+
59
+ if (typeof val === 'function') {
60
+ return `[Function: ${val.name || 'anonymous'}]`;
61
+ }
62
+
63
+ if (typeof val === 'symbol') {
64
+ return val.toString();
65
+ }
66
+
32
67
  if (typeof val === 'object' && val !== null) {
33
68
  if (seen.has(val)) return '[Circular]';
34
69
  seen.add(val);
35
70
  }
36
- if (typeof val === 'function') return `[Function: ${val.name || 'anonymous'}]`;
37
- if (typeof val === 'bigint') return `${val}n`;
71
+
38
72
  return val;
39
73
  });
40
- if (cap === 0) return str;
41
- return str.length > cap ? str.slice(0, cap) + '…' : str;
74
+
75
+ if (cap === 0) {
76
+ return str;
77
+ }
78
+
79
+ return str.length > cap ? str.slice(0, Math.max(0, cap)) + '…' : str;
42
80
  } catch {
43
81
  return String(value);
44
82
  }
@@ -46,26 +84,32 @@ export const safeStringify = (value: unknown, limit?: number): string => {
46
84
 
47
85
  // ─── Duration Formatting ─────────────────────────────────────────────
48
86
 
49
- const formatDuration = (ms: number): string => {
87
+ export const formatDuration = (ms: number): string => {
50
88
  if (ms < 1000) return `${ms.toFixed(2)}ms`;
51
89
  if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
90
+
52
91
  const mins = Math.floor(ms / 60000);
53
92
  const secs = Math.round((ms % 60000) / 1000);
93
+
54
94
  return `${mins}m ${secs}s`;
55
95
  };
56
96
 
57
97
  // ─── Timestamps ──────────────────────────────────────────────────────
58
98
 
59
99
  let timestamps =
60
- typeof process !== 'undefined' && (process.env.MEASURE_TIMESTAMPS === '1' || process.env.MEASURE_TIMESTAMPS === 'true');
100
+ typeof process !== 'undefined' &&
101
+ (process.env.MEASURE_TIMESTAMPS === '1' ||
102
+ process.env.MEASURE_TIMESTAMPS === 'true');
61
103
 
62
104
  const ts = (): string => {
63
105
  if (!timestamps) return '';
106
+
64
107
  const now = new Date();
65
108
  const h = String(now.getHours()).padStart(2, '0');
66
109
  const m = String(now.getMinutes()).padStart(2, '0');
67
110
  const s = String(now.getSeconds()).padStart(2, '0');
68
111
  const ms = String(now.getMilliseconds()).padStart(3, '0');
112
+
69
113
  return `[${h}:${m}:${s}.${ms}] `;
70
114
  };
71
115
 
@@ -84,10 +128,24 @@ export type MeasureEvent = {
84
128
  maxResultLength?: number;
85
129
  };
86
130
 
131
+ export type MeasureAction<T = unknown> =
132
+ | string
133
+ | {
134
+ label: string;
135
+ budget?: number;
136
+ timeout?: number;
137
+ maxResultLength?: number;
138
+ result?: (value: T) => unknown;
139
+ meta?: Record<string, unknown>;
140
+ [key: string]: unknown;
141
+ };
142
+
87
143
  // ─── Configuration ───────────────────────────────────────────────────
88
144
 
89
145
  export let silent =
90
- typeof process !== 'undefined' && (process.env.MEASURE_SILENT === '1' || process.env.MEASURE_SILENT === 'true');
146
+ typeof process !== 'undefined' &&
147
+ (process.env.MEASURE_SILENT === '1' ||
148
+ process.env.MEASURE_SILENT === 'true');
91
149
 
92
150
  let dotEndLabel = true;
93
151
  let dotChar = '·';
@@ -114,118 +172,233 @@ export const configure = (opts: ConfigureOpts) => {
114
172
 
115
173
  // ─── Shared Helpers ──────────────────────────────────────────────────
116
174
 
117
- const buildActionLabel = (actionInternal: string | object): string => {
118
- return typeof actionInternal === 'object' && actionInternal !== null && 'label' in actionInternal
119
- ? String(actionInternal.label)
120
- : String(actionInternal);
175
+ const isActionObject = (value: unknown): value is Exclude<MeasureAction, string> => {
176
+ return typeof value === 'object' && value !== null;
121
177
  };
122
178
 
123
- const extractBudget = (actionInternal: string | object): number | undefined => {
124
- if (typeof actionInternal !== 'object' || actionInternal === null) return undefined;
125
- if ('budget' in actionInternal) return Number((actionInternal as any).budget);
126
- return undefined;
179
+ const buildActionLabel = (actionInternal: MeasureAction): string => {
180
+ if (isActionObject(actionInternal) && 'label' in actionInternal) {
181
+ return String(actionInternal.label);
182
+ }
183
+
184
+ return String(actionInternal);
127
185
  };
128
186
 
129
- const extractTimeout = (actionInternal: string | object): number | undefined => {
130
- if (typeof actionInternal !== 'object' || actionInternal === null) return undefined;
131
- if ('timeout' in actionInternal) return Number((actionInternal as any).timeout);
132
- return undefined;
187
+ const extractBudget = (actionInternal: MeasureAction): number | undefined => {
188
+ if (!isActionObject(actionInternal)) return undefined;
189
+ if (!('budget' in actionInternal)) return undefined;
190
+ if (actionInternal.budget === undefined) return undefined;
191
+
192
+ return Number(actionInternal.budget);
133
193
  };
134
194
 
135
- const extractMaxResultLength = (actionInternal: string | object): number | undefined => {
136
- if (typeof actionInternal !== 'object' || actionInternal === null) return undefined;
137
- if ('maxResultLength' in actionInternal) return Number((actionInternal as any).maxResultLength);
138
- return undefined;
195
+ const extractTimeout = (actionInternal: MeasureAction): number | undefined => {
196
+ if (!isActionObject(actionInternal)) return undefined;
197
+ if (!('timeout' in actionInternal)) return undefined;
198
+ if (actionInternal.timeout === undefined) return undefined;
199
+
200
+ return Number(actionInternal.timeout);
139
201
  };
140
202
 
141
- const extractMeta = (actionInternal: string | object): Record<string, unknown> | undefined => {
142
- if (typeof actionInternal !== 'object' || actionInternal === null) return undefined;
143
- const details = { ...actionInternal };
144
- if ('label' in details) delete (details as any).label;
145
- if ('budget' in details) delete (details as any).budget;
146
- if ('maxResultLength' in details) delete (details as any).maxResultLength;
147
- if (Object.keys(details).length === 0) return undefined;
148
- return details as Record<string, unknown>;
203
+ const extractMaxResultLength = (actionInternal: MeasureAction): number | undefined => {
204
+ if (!isActionObject(actionInternal)) return undefined;
205
+ if (!('maxResultLength' in actionInternal)) return undefined;
206
+ if (actionInternal.maxResultLength === undefined) return undefined;
207
+
208
+ return Number(actionInternal.maxResultLength);
209
+ };
210
+
211
+ const extractResultMapper = <T>(
212
+ actionInternal: MeasureAction<T>,
213
+ ): ((value: T) => unknown) | undefined => {
214
+ if (!isActionObject(actionInternal)) return undefined;
215
+ if (typeof actionInternal.result !== 'function') return undefined;
216
+
217
+ return actionInternal.result;
218
+ };
219
+
220
+ const extractMeta = (actionInternal: MeasureAction): Record<string, unknown> | undefined => {
221
+ if (!isActionObject(actionInternal)) return undefined;
222
+
223
+ const details: Record<string, unknown> = { ...actionInternal };
224
+
225
+ delete details.label;
226
+ delete details.budget;
227
+ delete details.timeout;
228
+ delete details.maxResultLength;
229
+ delete details.result;
230
+
231
+ const explicitMeta =
232
+ typeof details.meta === 'object' && details.meta !== null
233
+ ? (details.meta as Record<string, unknown>)
234
+ : undefined;
235
+
236
+ delete details.meta;
237
+
238
+ const merged = {
239
+ ...details,
240
+ ...(explicitMeta ?? {}),
241
+ };
242
+
243
+ if (Object.keys(merged).length === 0) {
244
+ return undefined;
245
+ }
246
+
247
+ return merged;
149
248
  };
150
249
 
151
250
  const formatMeta = (meta?: Record<string, unknown>): string => {
152
251
  if (!meta) return '';
252
+
153
253
  const params = Object.entries(meta)
154
- .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
254
+ .map(([key, value]) => `${key}=${safeStringify(value, 0)}`)
155
255
  .join(' ');
256
+
156
257
  return ` (${params})`;
157
258
  };
158
259
 
260
+ const mapResultForLog = <T>(actionInternal: MeasureAction<T>, result: T): unknown => {
261
+ const mapper = extractResultMapper(actionInternal);
262
+
263
+ if (!mapper) {
264
+ return result;
265
+ }
266
+
267
+ try {
268
+ return mapper(result);
269
+ } catch (error) {
270
+ return {
271
+ resultMapperError:
272
+ error instanceof Error ? error.message : String(error),
273
+ };
274
+ }
275
+ };
276
+
159
277
  const emit = (event: MeasureEvent, prefix?: string) => {
160
278
  if (silent) return;
279
+
161
280
  if (logger) {
162
281
  logger(event);
163
282
  return;
164
283
  }
284
+
165
285
  defaultLogger(event, prefix);
166
286
  };
167
287
 
168
-
169
288
  const defaultLogger = (event: MeasureEvent, prefix?: string) => {
170
289
  const pfx = prefix ? `${prefix}:` : '';
171
290
  const id = `[${pfx}${event.id}]`;
172
291
  const t = ts();
173
292
 
174
293
  switch (event.type) {
175
- case 'start':
294
+ case 'start': {
176
295
  console.log(`${t}${id} ... ${event.label}${formatMeta(event.meta)}`);
177
296
  break;
297
+ }
298
+
178
299
  case 'success': {
179
- const endLabel = dotEndLabel ? dotChar.repeat(event.label.length + 5) : ` ${event.label}`;
180
- const resultStr = event.result !== undefined ? safeStringify(event.result, event.maxResultLength) : '';
300
+ const endLabel = dotEndLabel
301
+ ? dotChar.repeat(event.label.length + 5)
302
+ : ` ${event.label}`;
303
+
304
+ const resultStr =
305
+ event.result !== undefined
306
+ ? safeStringify(event.result, event.maxResultLength)
307
+ : '';
308
+
181
309
  const arrow = resultStr ? ` → ${resultStr}` : '';
182
- const budgetWarn = event.budget && event.duration! > event.budget
183
- ? ` ⚠ OVER BUDGET (${formatDuration(event.budget)})`
184
- : '';
185
- console.log(`${t}${id} ${endLabel} ${formatDuration(event.duration!)}${arrow}${budgetWarn}`);
310
+
311
+ const budgetWarn =
312
+ event.budget && event.duration !== undefined && event.duration > event.budget
313
+ ? ` ⚠ OVER BUDGET (${formatDuration(event.budget)})`
314
+ : '';
315
+
316
+ console.log(
317
+ `${t}${id} ${endLabel} ${formatDuration(event.duration ?? 0)}${arrow}${budgetWarn}`,
318
+ );
319
+
186
320
  break;
187
321
  }
322
+
188
323
  case 'error': {
189
- // Added + 3 to the repeat count because ' ✗ ' takes up 3 characters, needing 5 total to match ' ... '
190
- const endLabel = dotEndLabel ? dotChar.repeat(event.label.length + 3) : ` ${event.label}`;
191
- const errorMsg = event.error instanceof Error ? event.error.message : String(event.error);
192
- const budgetWarn = event.budget && event.duration! > event.budget
193
- ? ` ⚠ OVER BUDGET (${formatDuration(event.budget)})`
194
- : '';
195
- console.log(`${t}${id} ✗ ${endLabel} ${formatDuration(event.duration!)} (${errorMsg})${budgetWarn}`);
324
+ const endLabel = dotEndLabel
325
+ ? dotChar.repeat(event.label.length + 3)
326
+ : ` ${event.label}`;
327
+
328
+ const errorMsg =
329
+ event.error instanceof Error ? event.error.message : String(event.error);
330
+
331
+ const budgetWarn =
332
+ event.budget && event.duration !== undefined && event.duration > event.budget
333
+ ? ` ⚠ OVER BUDGET (${formatDuration(event.budget)})`
334
+ : '';
335
+
336
+ console.log(
337
+ `${t}${id} ✗ ${endLabel} ${formatDuration(event.duration ?? 0)} (${errorMsg})${budgetWarn}`,
338
+ );
339
+
196
340
  if (event.error instanceof Error) {
197
341
  console.error(`${id}`, event.error.stack ?? event.error.message);
342
+
198
343
  if (event.error.cause) {
199
344
  console.error(`${id} Cause:`, event.error.cause);
200
345
  }
201
346
  } else {
202
347
  console.error(`${id}`, event.error);
203
348
  }
349
+
204
350
  break;
205
351
  }
206
- case 'annotation':
352
+
353
+ case 'annotation': {
207
354
  console.log(`${t}${id} = ${event.label}${formatMeta(event.meta)}`);
208
355
  break;
356
+ }
209
357
  }
210
358
  };
211
359
 
212
360
  // ─── Types ───────────────────────────────────────────────────────────
213
361
 
214
-
215
362
  export type MeasureFn = {
216
- <U>(label: string | object, fn: () => Promise<U>): Promise<U | null>;
217
- <U>(label: string | object, fn: (m: MeasureFn, ms: MeasureSyncFn) => Promise<U>): Promise<U | null>;
218
- <U>(label: string | object, fn: (m: MeasureFn) => Promise<U>): Promise<U | null>;
219
- (label: string | object): Promise<null>;
363
+ <U>(label: MeasureAction<U>, fn: () => Promise<U>): Promise<U | null>;
364
+
365
+ <U>(
366
+ label: MeasureAction<U>,
367
+ fn: (m: MeasureFn, ms: MeasureSyncFn) => Promise<U>,
368
+ ): Promise<U | null>;
369
+
370
+ <U>(
371
+ label: MeasureAction<U>,
372
+ fn: (m: MeasureFn) => Promise<U>,
373
+ ): Promise<U | null>;
374
+
375
+ <U>(
376
+ label: MeasureAction<U>,
377
+ fn: () => Promise<U>,
378
+ onError: (error: unknown) => U | null | Promise<U | null>,
379
+ ): Promise<U | null>;
380
+
381
+ (label: MeasureAction): Promise<null>;
220
382
  };
221
383
 
222
384
  export type MeasureSyncFn = {
223
- <U>(label: string | object, fn: () => U): U | null;
224
- <U>(label: string | object, fn: (m: MeasureSyncFn) => U): U | null;
225
- (label: string | object): null;
385
+ <U>(label: MeasureAction<U>, fn: () => U): U | null;
386
+
387
+ <U>(label: MeasureAction<U>, fn: (m: MeasureSyncFn) => U): U | null;
388
+
389
+ <U>(
390
+ label: MeasureAction<U>,
391
+ fn: () => U,
392
+ onError: (error: unknown) => U | null,
393
+ ): U | null;
394
+
395
+ (label: MeasureAction): null;
226
396
  };
227
397
 
228
- export type TimedResult<T> = { result: T | null; duration: number };
398
+ export type TimedResult<T> = {
399
+ result: T | null;
400
+ duration: number;
401
+ };
229
402
 
230
403
  export type RetryOpts = {
231
404
  attempts?: number;
@@ -244,28 +417,47 @@ const createNestedResolver = (
244
417
  fullIdChain: string[],
245
418
  childCounterRef: { value: number },
246
419
  depth: number,
247
- resolver: <U>(fn: any, action: any, chain: (string | number)[], depth: number, onError?: (error: unknown) => any, inheritedMaxLen?: number) => Promise<U | null> | (U | null),
420
+ resolver: <U>(
421
+ fn: any,
422
+ action: MeasureAction<U>,
423
+ chain: (string | number)[],
424
+ depth: number,
425
+ onError?: (error: unknown) => any,
426
+ inheritedMaxLen?: number,
427
+ ) => Promise<U | null> | (U | null),
248
428
  prefix?: string,
249
- inheritedMaxLen?: number
429
+ inheritedMaxLen?: number,
250
430
  ) => {
251
431
  return (...args: any[]) => {
252
- const label = args[0];
432
+ const label = args[0] as MeasureAction;
253
433
  const fn = args[1];
254
434
  const onError = args[2];
255
435
 
256
436
  if (typeof fn === 'function') {
257
437
  const childParentChain = [...fullIdChain, childCounterRef.value++];
258
- return resolver(fn, label, childParentChain, depth + 1, typeof onError === 'function' ? onError : undefined, inheritedMaxLen);
259
- } else {
260
- emit({
438
+
439
+ return resolver(
440
+ fn,
441
+ label,
442
+ childParentChain,
443
+ depth + 1,
444
+ typeof onError === 'function' ? onError : undefined,
445
+ inheritedMaxLen,
446
+ );
447
+ }
448
+
449
+ emit(
450
+ {
261
451
  type: 'annotation',
262
452
  id: fullIdChain.join('-'),
263
453
  label: buildActionLabel(label),
264
454
  depth: depth + 1,
265
455
  meta: extractMeta(label),
266
- }, prefix);
267
- return isAsync ? Promise.resolve(null) : null;
268
- }
456
+ },
457
+ prefix,
458
+ );
459
+
460
+ return isAsync ? Promise.resolve(null) : null;
269
461
  };
270
462
  };
271
463
 
@@ -281,18 +473,32 @@ export type ScopeOpts = {
281
473
  maxResultLength?: number;
282
474
  };
283
475
 
284
- const createMeasureImpl = (prefix?: string, counterRef?: { value: number }, scopeOpts?: ScopeOpts) => {
285
- const counter = counterRef ?? { get value() { return globalRootCounter; }, set value(v) { globalRootCounter = v; } };
476
+ const createMeasureImpl = (
477
+ prefix?: string,
478
+ counterRef?: { value: number },
479
+ scopeOpts?: ScopeOpts,
480
+ ) => {
481
+ const counter =
482
+ counterRef ??
483
+ {
484
+ get value() {
485
+ return globalRootCounter;
486
+ },
487
+ set value(value: number) {
488
+ globalRootCounter = value;
489
+ },
490
+ };
491
+
286
492
  const scopeMaxLen = scopeOpts?.maxResultLength;
287
493
  let _lastError: unknown = null;
288
494
 
289
495
  const _measureInternal = async <U>(
290
496
  fnInternal: (measure: MeasureFn, measureSync: MeasureSyncFn) => Promise<U>,
291
- actionInternal: string | object,
497
+ actionInternal: MeasureAction<U>,
292
498
  parentIdChain: (string | number)[],
293
499
  depth: number,
294
- onError?: (error: unknown) => any,
295
- inheritedMaxLen?: number
500
+ onError?: (error: unknown) => U | null | Promise<U | null>,
501
+ inheritedMaxLen?: number,
296
502
  ): Promise<U | null> => {
297
503
  const start = performance.now();
298
504
  const childCounterRef = { value: 0 };
@@ -302,316 +508,604 @@ const createMeasureImpl = (prefix?: string, counterRef?: { value: number }, scop
302
508
  const localMaxLen = extractMaxResultLength(actionInternal);
303
509
  const effectiveMaxLen = localMaxLen ?? inheritedMaxLen;
304
510
 
305
- const currentId = toAlpha(Number(parentIdChain.pop() ?? 0) as number);
306
- const fullIdChain: string[] = [...parentIdChain.map(v => String(v)) as string[], currentId];
511
+ const currentId = toAlpha(Number(parentIdChain.pop() ?? 0));
512
+ const fullIdChain: string[] = [
513
+ ...parentIdChain.map((value) => String(value)),
514
+ currentId,
515
+ ];
307
516
  const idStr = fullIdChain.join('-');
308
517
 
309
- emit({
310
- type: 'start',
311
- id: idStr,
312
- label,
518
+ emit(
519
+ {
520
+ type: 'start',
521
+ id: idStr,
522
+ label,
523
+ depth,
524
+ meta: extractMeta(actionInternal),
525
+ },
526
+ prefix,
527
+ );
528
+
529
+ const measureForNextLevel = createNestedResolver(
530
+ true,
531
+ fullIdChain,
532
+ childCounterRef,
313
533
  depth,
314
- meta: extractMeta(actionInternal),
315
- }, prefix);
316
-
317
- const measureForNextLevel = createNestedResolver(true, fullIdChain, childCounterRef, depth, _measureInternal, prefix, effectiveMaxLen);
318
- const measureSyncForNextLevel = createNestedResolver(false, fullIdChain, childCounterRef, depth, _measureInternalSync, prefix, effectiveMaxLen);
534
+ _measureInternal,
535
+ prefix,
536
+ effectiveMaxLen,
537
+ );
538
+
539
+ const measureSyncForNextLevel = createNestedResolver(
540
+ false,
541
+ fullIdChain,
542
+ childCounterRef,
543
+ depth,
544
+ _measureInternalSync,
545
+ prefix,
546
+ effectiveMaxLen,
547
+ );
319
548
 
320
549
  try {
321
550
  let result: U;
551
+
322
552
  if (timeout && timeout > 0) {
323
553
  result = await Promise.race([
324
- fnInternal(measureForNextLevel as MeasureFn, measureSyncForNextLevel as MeasureSyncFn),
325
- new Promise<never>((_, reject) =>
326
- setTimeout(() => reject(new Error(`Timeout (${formatDuration(timeout)})`)), timeout)
554
+ fnInternal(
555
+ measureForNextLevel as MeasureFn,
556
+ measureSyncForNextLevel as MeasureSyncFn,
327
557
  ),
558
+ new Promise<never>((_resolve, reject) => {
559
+ setTimeout(
560
+ () => reject(new Error(`Timeout (${formatDuration(timeout)})`)),
561
+ timeout,
562
+ );
563
+ }),
328
564
  ]);
329
565
  } else {
330
- result = await fnInternal(measureForNextLevel as MeasureFn, measureSyncForNextLevel as MeasureSyncFn);
566
+ result = await fnInternal(
567
+ measureForNextLevel as MeasureFn,
568
+ measureSyncForNextLevel as MeasureSyncFn,
569
+ );
331
570
  }
571
+
332
572
  const duration = performance.now() - start;
333
- emit({ type: 'success', id: idStr, label, depth, duration, result, budget, maxResultLength: effectiveMaxLen }, prefix);
573
+
574
+ emit(
575
+ {
576
+ type: 'success',
577
+ id: idStr,
578
+ label,
579
+ depth,
580
+ duration,
581
+ result: mapResultForLog(actionInternal, result),
582
+ budget,
583
+ maxResultLength: effectiveMaxLen,
584
+ },
585
+ prefix,
586
+ );
587
+
334
588
  return result;
335
589
  } catch (error) {
336
590
  const duration = performance.now() - start;
337
- emit({ type: 'error', id: idStr, label, depth, duration, error, budget, maxResultLength: effectiveMaxLen }, prefix);
591
+
592
+ emit(
593
+ {
594
+ type: 'error',
595
+ id: idStr,
596
+ label,
597
+ depth,
598
+ duration,
599
+ error,
600
+ budget,
601
+ maxResultLength: effectiveMaxLen,
602
+ },
603
+ prefix,
604
+ );
605
+
338
606
  _lastError = error;
607
+
339
608
  if (onError) {
340
609
  try {
341
- return onError(error);
610
+ return await onError(error);
342
611
  } catch (onErrorError) {
343
- emit({ type: 'error', id: idStr, label: `${label} (onError)`, depth, duration: performance.now() - start, error: onErrorError, budget, maxResultLength: effectiveMaxLen }, prefix);
612
+ emit(
613
+ {
614
+ type: 'error',
615
+ id: idStr,
616
+ label: `${label} (onError)`,
617
+ depth,
618
+ duration: performance.now() - start,
619
+ error: onErrorError,
620
+ budget,
621
+ maxResultLength: effectiveMaxLen,
622
+ },
623
+ prefix,
624
+ );
625
+
344
626
  _lastError = onErrorError;
345
627
  return null;
346
628
  }
347
629
  }
630
+
348
631
  return null;
349
632
  }
350
633
  };
351
634
 
352
635
  const _measureInternalSync = <U>(
353
636
  fnInternal: (measure: MeasureSyncFn) => U,
354
- actionInternal: string | object,
637
+ actionInternal: MeasureAction<U>,
355
638
  parentIdChain: (string | number)[],
356
639
  depth: number,
357
- _onError?: ((error: unknown) => any) | undefined,
358
- inheritedMaxLen?: number
640
+ onError?: (error: unknown) => U | null,
641
+ inheritedMaxLen?: number,
359
642
  ): U | null => {
360
643
  const start = performance.now();
361
644
  const childCounterRef = { value: 0 };
362
645
  const label = buildActionLabel(actionInternal);
363
- const hasNested = fnInternal.length > 0;
364
646
  const budget = extractBudget(actionInternal);
365
647
  const localMaxLen = extractMaxResultLength(actionInternal);
366
648
  const effectiveMaxLen = localMaxLen ?? inheritedMaxLen;
367
649
 
368
- const currentId = toAlpha(Number(parentIdChain.pop() ?? 0) as number);
369
- const fullIdChain: string[] = [...parentIdChain.map(v => String(v)) as string[], currentId];
650
+ const currentId = toAlpha(Number(parentIdChain.pop() ?? 0));
651
+ const fullIdChain: string[] = [
652
+ ...parentIdChain.map((value) => String(value)),
653
+ currentId,
654
+ ];
370
655
  const idStr = fullIdChain.join('-');
371
656
 
372
- if (hasNested) {
373
- emit({
657
+ emit(
658
+ {
374
659
  type: 'start',
375
660
  id: idStr,
376
661
  label,
377
662
  depth,
378
663
  meta: extractMeta(actionInternal),
379
- }, prefix);
380
- }
381
-
382
- const measureForNextLevel = createNestedResolver(false, fullIdChain, childCounterRef, depth, _measureInternalSync, prefix, effectiveMaxLen);
664
+ },
665
+ prefix,
666
+ );
667
+
668
+ const measureForNextLevel = createNestedResolver(
669
+ false,
670
+ fullIdChain,
671
+ childCounterRef,
672
+ depth,
673
+ _measureInternalSync,
674
+ prefix,
675
+ effectiveMaxLen,
676
+ );
383
677
 
384
678
  try {
385
679
  const result = fnInternal(measureForNextLevel as MeasureSyncFn);
386
680
  const duration = performance.now() - start;
387
- emit({ type: 'success', id: idStr, label, depth, duration, result, budget, maxResultLength: effectiveMaxLen }, prefix);
681
+
682
+ emit(
683
+ {
684
+ type: 'success',
685
+ id: idStr,
686
+ label,
687
+ depth,
688
+ duration,
689
+ result: mapResultForLog(actionInternal, result),
690
+ budget,
691
+ maxResultLength: effectiveMaxLen,
692
+ },
693
+ prefix,
694
+ );
695
+
388
696
  return result;
389
697
  } catch (error) {
390
698
  const duration = performance.now() - start;
391
- emit({ type: 'error', id: idStr, label, depth, duration, error, budget, maxResultLength: effectiveMaxLen }, prefix);
699
+
700
+ emit(
701
+ {
702
+ type: 'error',
703
+ id: idStr,
704
+ label,
705
+ depth,
706
+ duration,
707
+ error,
708
+ budget,
709
+ maxResultLength: effectiveMaxLen,
710
+ },
711
+ prefix,
712
+ );
713
+
392
714
  _lastError = error;
715
+
716
+ if (onError) {
717
+ try {
718
+ return onError(error);
719
+ } catch (onErrorError) {
720
+ emit(
721
+ {
722
+ type: 'error',
723
+ id: idStr,
724
+ label: `${label} (onError)`,
725
+ depth,
726
+ duration: performance.now() - start,
727
+ error: onErrorError,
728
+ budget,
729
+ maxResultLength: effectiveMaxLen,
730
+ },
731
+ prefix,
732
+ );
733
+
734
+ _lastError = onErrorError;
735
+ return null;
736
+ }
737
+ }
738
+
393
739
  return null;
394
740
  }
395
741
  };
396
742
 
397
- // ─── measure (async) ───────────────────────────────────────────
743
+ // ─── measure Async ─────────────────────────────────────────────
398
744
 
399
745
  const measureFn = async <T = null>(
400
- arg1: string | object,
401
- arg2?: ((measure: MeasureFn, measureSync: MeasureSyncFn) => Promise<T>) | ((measure: MeasureFn) => T),
402
- arg3?: (error: unknown) => any
746
+ arg1: MeasureAction<T>,
747
+ arg2?:
748
+ | ((measure: MeasureFn, measureSync: MeasureSyncFn) => Promise<T>)
749
+ | ((measure: MeasureFn) => Promise<T>)
750
+ | (() => Promise<T>),
751
+ arg3?: (error: unknown) => T | null | Promise<T | null>,
403
752
  ): Promise<T | null> => {
404
753
  if (typeof arg2 === 'function') {
405
- return _measureInternal(arg2 as any, arg1, [counter.value++], 0, arg3, scopeMaxLen) as Promise<T | null>;
406
- } else {
407
- const currentId = toAlpha(counter.value++);
408
- emit({
754
+ return _measureInternal(
755
+ arg2 as any,
756
+ arg1,
757
+ [counter.value++],
758
+ 0,
759
+ arg3,
760
+ scopeMaxLen,
761
+ ) as Promise<T | null>;
762
+ }
763
+
764
+ const currentId = toAlpha(counter.value++);
765
+
766
+ emit(
767
+ {
409
768
  type: 'annotation',
410
769
  id: currentId,
411
770
  label: buildActionLabel(arg1),
412
771
  depth: 0,
413
772
  meta: extractMeta(arg1),
414
- }, prefix);
415
- return Promise.resolve(null);
416
- }
773
+ },
774
+ prefix,
775
+ );
776
+
777
+ return Promise.resolve(null);
417
778
  };
418
779
 
419
780
  measureFn.timed = async <T = null>(
420
- arg1: string | object,
421
- arg2?: ((measure: MeasureFn) => Promise<T>)
781
+ arg1: MeasureAction<T>,
782
+ arg2?: ((measure: MeasureFn) => Promise<T>) | (() => Promise<T>),
422
783
  ): Promise<TimedResult<T>> => {
423
784
  const start = performance.now();
424
- const result = await measureFn(arg1, arg2);
785
+ const result = await measureFn(arg1, arg2 as any);
425
786
  const duration = performance.now() - start;
426
- return { result, duration };
787
+
788
+ return {
789
+ result,
790
+ duration,
791
+ };
427
792
  };
428
793
 
429
794
  measureFn.retry = async <T = null>(
430
- label: string | object,
795
+ label: MeasureAction<T>,
431
796
  opts: RetryOpts,
432
- fn: () => Promise<T>
797
+ fn: () => Promise<T>,
433
798
  ): Promise<T | null> => {
434
799
  const attempts = opts.attempts ?? 3;
435
800
  const delay = opts.delay ?? 1000;
436
801
  const backoff = opts.backoff ?? 1;
437
802
  const lbl = buildActionLabel(label);
438
803
  const budget = extractBudget(label);
804
+ const effectiveMaxLen = extractMaxResultLength(label) ?? scopeMaxLen;
439
805
 
440
- for (let i = 0; i < attempts; i++) {
806
+ for (let i = 0; i < attempts; i += 1) {
441
807
  const attempt = i + 1;
442
808
  const attemptLabel = `${lbl} [${attempt}/${attempts}]`;
443
809
  const start = performance.now();
444
810
  const currentId = toAlpha(counter.value++);
445
811
 
446
- emit({
447
- type: 'start',
448
- id: currentId,
449
- label: attemptLabel,
450
- depth: 0,
451
- meta: extractMeta(label),
452
- }, prefix);
812
+ emit(
813
+ {
814
+ type: 'start',
815
+ id: currentId,
816
+ label: attemptLabel,
817
+ depth: 0,
818
+ meta: extractMeta(label),
819
+ },
820
+ prefix,
821
+ );
453
822
 
454
823
  try {
455
824
  const result = await fn();
456
825
  const duration = performance.now() - start;
457
- emit({ type: 'success', id: currentId, label: attemptLabel, depth: 0, duration, result, budget }, prefix);
826
+
827
+ emit(
828
+ {
829
+ type: 'success',
830
+ id: currentId,
831
+ label: attemptLabel,
832
+ depth: 0,
833
+ duration,
834
+ result: mapResultForLog(label, result),
835
+ budget,
836
+ maxResultLength: effectiveMaxLen,
837
+ },
838
+ prefix,
839
+ );
840
+
458
841
  return result;
459
842
  } catch (error) {
460
843
  const duration = performance.now() - start;
461
- emit({ type: 'error', id: currentId, label: attemptLabel, depth: 0, duration, error, budget }, prefix);
844
+
845
+ emit(
846
+ {
847
+ type: 'error',
848
+ id: currentId,
849
+ label: attemptLabel,
850
+ depth: 0,
851
+ duration,
852
+ error,
853
+ budget,
854
+ maxResultLength: effectiveMaxLen,
855
+ },
856
+ prefix,
857
+ );
858
+
859
+ _lastError = error;
860
+
462
861
  if (attempt < attempts) {
463
- await new Promise(r => setTimeout(r, delay * Math.pow(backoff, i)));
862
+ await new Promise((resolve) => {
863
+ setTimeout(resolve, delay * Math.pow(backoff, i));
864
+ });
464
865
  }
465
866
  }
466
867
  }
868
+
467
869
  return null;
468
870
  };
469
871
 
470
872
  measureFn.assert = async <T>(
471
- arg1: string | object,
472
- arg2: ((measure: MeasureFn) => Promise<T>) | (() => Promise<T>)
873
+ arg1: MeasureAction<T>,
874
+ arg2: ((measure: MeasureFn) => Promise<T>) | (() => Promise<T>),
473
875
  ): Promise<T> => {
474
876
  const result = await measureFn(arg1, arg2 as any);
877
+
475
878
  if (result === null) {
476
879
  const cause = _lastError;
477
880
  _lastError = null;
478
- throw new Error(`measure.assert: "${buildActionLabel(arg1)}" failed`, { cause });
881
+
882
+ throw new Error(`measure.assert: "${buildActionLabel(arg1)}" failed`, {
883
+ cause,
884
+ });
479
885
  }
886
+
480
887
  return result;
481
888
  };
482
889
 
483
890
  measureFn.wrap = <A extends any[], R>(
484
- label: string | object,
485
- fn: (...args: A) => Promise<R>
891
+ label: MeasureAction<R>,
892
+ fn: (...args: A) => Promise<R>,
486
893
  ): ((...args: A) => Promise<R | null>) => {
487
894
  return (...args: A) => measureFn(label, () => fn(...args));
488
895
  };
489
896
 
490
897
  measureFn.batch = async <T, R>(
491
- label: string | object,
898
+ label: MeasureAction<R>,
492
899
  items: T[],
493
900
  fn: (item: T, index: number) => Promise<R>,
494
- opts?: BatchOpts
901
+ opts?: BatchOpts,
495
902
  ): Promise<(R | null)[]> => {
496
903
  const lbl = buildActionLabel(label);
497
904
  const total = items.length;
498
905
  const every = opts?.every ?? Math.max(1, Math.ceil(total / 5));
499
906
  const currentId = toAlpha(counter.value++);
500
907
  const startTime = performance.now();
908
+ const budget = extractBudget(label);
501
909
 
502
- emit({
503
- type: 'start',
504
- id: currentId,
505
- label: `${lbl} (${total} items)`,
506
- depth: 0,
507
- meta: extractMeta(label),
508
- }, prefix);
910
+ emit(
911
+ {
912
+ type: 'start',
913
+ id: currentId,
914
+ label: `${lbl} (${total} items)`,
915
+ depth: 0,
916
+ meta: extractMeta(label),
917
+ },
918
+ prefix,
919
+ );
509
920
 
510
921
  const results: (R | null)[] = [];
511
- for (let i = 0; i < items.length; i++) {
922
+
923
+ for (let i = 0; i < items.length; i += 1) {
512
924
  try {
513
925
  results.push(await fn(items[i]!, i));
514
- } catch {
926
+ } catch (error) {
927
+ _lastError = error;
515
928
  results.push(null);
516
929
  }
930
+
517
931
  if ((i + 1) % every === 0 && i + 1 < total) {
518
932
  const elapsed = (performance.now() - startTime) / 1000;
519
933
  const rate = ((i + 1) / elapsed).toFixed(0);
520
- emit({
521
- type: 'annotation',
522
- id: currentId,
523
- label: `${i + 1}/${total} (${elapsed.toFixed(1)}s, ${rate}/s)`,
524
- depth: 0,
525
- }, prefix);
934
+
935
+ emit(
936
+ {
937
+ type: 'annotation',
938
+ id: currentId,
939
+ label: `${i + 1}/${total} (${elapsed.toFixed(1)}s, ${rate}/s)`,
940
+ depth: 0,
941
+ },
942
+ prefix,
943
+ );
526
944
  }
527
945
  }
528
946
 
529
947
  const duration = performance.now() - startTime;
530
- const budget = extractBudget(label);
531
- emit({
532
- type: 'success',
533
- id: currentId,
534
- label: `${lbl} (${total} items)`,
535
- depth: 0,
536
- duration,
537
- result: `${results.filter(r => r !== null).length}/${total} ok`,
538
- budget,
539
- }, prefix);
948
+ const okCount = results.filter((result) => result !== null).length;
949
+
950
+ emit(
951
+ {
952
+ type: 'success',
953
+ id: currentId,
954
+ label: `${lbl} (${total} items)`,
955
+ depth: 0,
956
+ duration,
957
+ result: `${okCount}/${total} ok`,
958
+ budget,
959
+ },
960
+ prefix,
961
+ );
962
+
540
963
  return results;
541
964
  };
542
965
 
543
966
  // ─── measureSync ───────────────────────────────────────────────
544
967
 
545
968
  const measureSyncFn = <T = null>(
546
- arg1: string | object,
547
- arg2?: ((measure: MeasureSyncFn) => T)
969
+ arg1: MeasureAction<T>,
970
+ arg2?: ((measure: MeasureSyncFn) => T) | (() => T),
971
+ arg3?: (error: unknown) => T | null,
548
972
  ): T | null => {
549
973
  if (typeof arg2 === 'function') {
550
- return _measureInternalSync(arg2, arg1, [counter.value++], 0, undefined, scopeMaxLen) as T | null;
551
- } else {
552
- const currentId = toAlpha(counter.value++);
553
- emit({
974
+ return _measureInternalSync(
975
+ arg2 as any,
976
+ arg1,
977
+ [counter.value++],
978
+ 0,
979
+ arg3,
980
+ scopeMaxLen,
981
+ ) as T | null;
982
+ }
983
+
984
+ const currentId = toAlpha(counter.value++);
985
+
986
+ emit(
987
+ {
554
988
  type: 'annotation',
555
989
  id: currentId,
556
990
  label: buildActionLabel(arg1),
557
991
  depth: 0,
558
992
  meta: extractMeta(arg1),
559
- }, prefix);
560
- return null;
561
- }
993
+ },
994
+ prefix,
995
+ );
996
+
997
+ return null;
562
998
  };
563
999
 
564
1000
  measureSyncFn.timed = <T = null>(
565
- arg1: string | object,
566
- arg2?: ((measure: MeasureSyncFn) => T)
1001
+ arg1: MeasureAction<T>,
1002
+ arg2?: ((measure: MeasureSyncFn) => T) | (() => T),
567
1003
  ): TimedResult<T> => {
568
1004
  const start = performance.now();
569
- const result = measureSyncFn(arg1, arg2);
1005
+ const result = measureSyncFn(arg1, arg2 as any);
570
1006
  const duration = performance.now() - start;
571
- return { result, duration };
1007
+
1008
+ return {
1009
+ result,
1010
+ duration,
1011
+ };
572
1012
  };
573
1013
 
574
1014
  measureSyncFn.assert = <T>(
575
- arg1: string | object,
576
- arg2: ((measure: MeasureSyncFn) => T) | (() => T)
1015
+ arg1: MeasureAction<T>,
1016
+ arg2: ((measure: MeasureSyncFn) => T) | (() => T),
577
1017
  ): T => {
578
1018
  const result = measureSyncFn(arg1, arg2 as any);
1019
+
579
1020
  if (result === null) {
580
1021
  const cause = _lastError;
581
1022
  _lastError = null;
582
- throw new Error(`measureSync.assert: "${buildActionLabel(arg1)}" failed`, { cause });
1023
+
1024
+ throw new Error(`measureSync.assert: "${buildActionLabel(arg1)}" failed`, {
1025
+ cause,
1026
+ });
583
1027
  }
1028
+
584
1029
  return result;
585
1030
  };
586
1031
 
587
1032
  measureSyncFn.wrap = <A extends any[], R>(
588
- label: string | object,
589
- fn: (...args: A) => R
1033
+ label: MeasureAction<R>,
1034
+ fn: (...args: A) => R,
590
1035
  ): ((...args: A) => R | null) => {
591
1036
  return (...args: A) => measureSyncFn(label, () => fn(...args));
592
1037
  };
593
1038
 
594
- return { measure: measureFn, measureSync: measureSyncFn };
1039
+ return {
1040
+ measure: measureFn as MeasureFn & {
1041
+ timed: <T = null>(
1042
+ arg1: MeasureAction<T>,
1043
+ arg2?: ((measure: MeasureFn) => Promise<T>) | (() => Promise<T>),
1044
+ ) => Promise<TimedResult<T>>;
1045
+
1046
+ retry: <T = null>(
1047
+ label: MeasureAction<T>,
1048
+ opts: RetryOpts,
1049
+ fn: () => Promise<T>,
1050
+ ) => Promise<T | null>;
1051
+
1052
+ assert: <T>(
1053
+ arg1: MeasureAction<T>,
1054
+ arg2: ((measure: MeasureFn) => Promise<T>) | (() => Promise<T>),
1055
+ ) => Promise<T>;
1056
+
1057
+ wrap: <A extends any[], R>(
1058
+ label: MeasureAction<R>,
1059
+ fn: (...args: A) => Promise<R>,
1060
+ ) => (...args: A) => Promise<R | null>;
1061
+
1062
+ batch: <T, R>(
1063
+ label: MeasureAction<R>,
1064
+ items: T[],
1065
+ fn: (item: T, index: number) => Promise<R>,
1066
+ opts?: BatchOpts,
1067
+ ) => Promise<(R | null)[]>;
1068
+ },
1069
+
1070
+ measureSync: measureSyncFn as MeasureSyncFn & {
1071
+ timed: <T = null>(
1072
+ arg1: MeasureAction<T>,
1073
+ arg2?: ((measure: MeasureSyncFn) => T) | (() => T),
1074
+ ) => TimedResult<T>;
1075
+
1076
+ assert: <T>(
1077
+ arg1: MeasureAction<T>,
1078
+ arg2: ((measure: MeasureSyncFn) => T) | (() => T),
1079
+ ) => T;
1080
+
1081
+ wrap: <A extends any[], R>(
1082
+ label: MeasureAction<R>,
1083
+ fn: (...args: A) => R,
1084
+ ) => (...args: A) => R | null;
1085
+ },
1086
+ };
595
1087
  };
596
1088
 
597
- // ─── Default (global) instance ───────────────────────────────────────
1089
+ // ─── Default Global Instance ─────────────────────────────────────────
598
1090
 
599
1091
  const globalInstance = createMeasureImpl();
600
1092
 
601
1093
  export const measure = globalInstance.measure;
602
1094
  export const measureSync = globalInstance.measureSync;
603
1095
 
604
- // ─── Scoped instances ────────────────────────────────────────────────
1096
+ // ─── Scoped Instances ────────────────────────────────────────────────
605
1097
 
606
1098
  export const createMeasure = (scopePrefix: string, opts?: ScopeOpts) => {
607
- const scopeCounter = { value: 0 };
1099
+ const scopeCounter = {
1100
+ value: 0,
1101
+ };
1102
+
608
1103
  const scoped = createMeasureImpl(scopePrefix, scopeCounter, opts);
1104
+
609
1105
  return {
610
1106
  ...scoped,
611
- resetCounter: () => { scopeCounter.value = 0; },
1107
+ resetCounter: () => {
1108
+ scopeCounter.value = 0;
1109
+ },
612
1110
  };
613
1111
  };
614
-
615
- // ─── Utility exports ─────────────────────────────────────────────────
616
-
617
- export { formatDuration };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "index.ts",
4
4
  "main": "./index.ts",
5
5
  "types": "./index.ts",
6
- "version": "3.11.1",
6
+ "version": "3.11.2",
7
7
  "type": "module",
8
8
  "private": false,
9
9
  "description": "Zero-dependency function performance measurement with hierarchical logging",