measure-fn 3.11.0 → 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 (3) hide show
  1. package/index.ts +692 -195
  2. package/package.json +2 -2
  3. package/bun.lock +0 -26
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,54 +172,116 @@ 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
 
@@ -171,58 +291,114 @@ const defaultLogger = (event: MeasureEvent, prefix?: string) => {
171
291
  const t = ts();
172
292
 
173
293
  switch (event.type) {
174
- case 'start':
294
+ case 'start': {
175
295
  console.log(`${t}${id} ... ${event.label}${formatMeta(event.meta)}`);
176
296
  break;
297
+ }
298
+
177
299
  case 'success': {
178
- const endLabel = dotEndLabel ? dotChar.repeat(event.label.length) : event.label;
179
- 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
+
180
309
  const arrow = resultStr ? ` → ${resultStr}` : '';
181
- const budgetWarn = event.budget && event.duration! > event.budget
182
- ? ` ⚠ OVER BUDGET (${formatDuration(event.budget)})`
183
- : '';
184
- 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
+
185
320
  break;
186
321
  }
322
+
187
323
  case 'error': {
188
- const endLabel = dotEndLabel ? dotChar.repeat(event.label.length) : event.label;
189
- const errorMsg = event.error instanceof Error ? event.error.message : String(event.error);
190
- const budgetWarn = event.budget && event.duration! > event.budget
191
- ? ` ⚠ OVER BUDGET (${formatDuration(event.budget)})`
192
- : '';
193
- 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
+
194
340
  if (event.error instanceof Error) {
195
341
  console.error(`${id}`, event.error.stack ?? event.error.message);
342
+
196
343
  if (event.error.cause) {
197
344
  console.error(`${id} Cause:`, event.error.cause);
198
345
  }
199
346
  } else {
200
347
  console.error(`${id}`, event.error);
201
348
  }
349
+
202
350
  break;
203
351
  }
204
- case 'annotation':
352
+
353
+ case 'annotation': {
205
354
  console.log(`${t}${id} = ${event.label}${formatMeta(event.meta)}`);
206
355
  break;
356
+ }
207
357
  }
208
358
  };
209
359
 
210
360
  // ─── Types ───────────────────────────────────────────────────────────
211
361
 
212
362
  export type MeasureFn = {
213
- <U>(label: string | object, fn: () => Promise<U>): Promise<U | null>;
214
- <U>(label: string | object, fn: (m: MeasureFn, ms: MeasureSyncFn) => Promise<U>): Promise<U | null>;
215
- <U>(label: string | object, fn: (m: MeasureFn) => Promise<U>): Promise<U | null>;
216
- (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>;
217
382
  };
218
383
 
219
384
  export type MeasureSyncFn = {
220
- <U>(label: string | object, fn: () => U): U | null;
221
- <U>(label: string | object, fn: (m: MeasureSyncFn) => U): U | null;
222
- (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;
223
396
  };
224
397
 
225
- export type TimedResult<T> = { result: T | null; duration: number };
398
+ export type TimedResult<T> = {
399
+ result: T | null;
400
+ duration: number;
401
+ };
226
402
 
227
403
  export type RetryOpts = {
228
404
  attempts?: number;
@@ -241,28 +417,47 @@ const createNestedResolver = (
241
417
  fullIdChain: string[],
242
418
  childCounterRef: { value: number },
243
419
  depth: number,
244
- 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),
245
428
  prefix?: string,
246
- inheritedMaxLen?: number
429
+ inheritedMaxLen?: number,
247
430
  ) => {
248
431
  return (...args: any[]) => {
249
- const label = args[0];
432
+ const label = args[0] as MeasureAction;
250
433
  const fn = args[1];
251
434
  const onError = args[2];
252
435
 
253
436
  if (typeof fn === 'function') {
254
437
  const childParentChain = [...fullIdChain, childCounterRef.value++];
255
- return resolver(fn, label, childParentChain, depth + 1, typeof onError === 'function' ? onError : undefined, inheritedMaxLen);
256
- } else {
257
- 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
+ {
258
451
  type: 'annotation',
259
452
  id: fullIdChain.join('-'),
260
453
  label: buildActionLabel(label),
261
454
  depth: depth + 1,
262
455
  meta: extractMeta(label),
263
- }, prefix);
264
- return isAsync ? Promise.resolve(null) : null;
265
- }
456
+ },
457
+ prefix,
458
+ );
459
+
460
+ return isAsync ? Promise.resolve(null) : null;
266
461
  };
267
462
  };
268
463
 
@@ -278,18 +473,32 @@ export type ScopeOpts = {
278
473
  maxResultLength?: number;
279
474
  };
280
475
 
281
- const createMeasureImpl = (prefix?: string, counterRef?: { value: number }, scopeOpts?: ScopeOpts) => {
282
- 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
+
283
492
  const scopeMaxLen = scopeOpts?.maxResultLength;
284
493
  let _lastError: unknown = null;
285
494
 
286
495
  const _measureInternal = async <U>(
287
496
  fnInternal: (measure: MeasureFn, measureSync: MeasureSyncFn) => Promise<U>,
288
- actionInternal: string | object,
497
+ actionInternal: MeasureAction<U>,
289
498
  parentIdChain: (string | number)[],
290
499
  depth: number,
291
- onError?: (error: unknown) => any,
292
- inheritedMaxLen?: number
500
+ onError?: (error: unknown) => U | null | Promise<U | null>,
501
+ inheritedMaxLen?: number,
293
502
  ): Promise<U | null> => {
294
503
  const start = performance.now();
295
504
  const childCounterRef = { value: 0 };
@@ -299,316 +508,604 @@ const createMeasureImpl = (prefix?: string, counterRef?: { value: number }, scop
299
508
  const localMaxLen = extractMaxResultLength(actionInternal);
300
509
  const effectiveMaxLen = localMaxLen ?? inheritedMaxLen;
301
510
 
302
- const currentId = toAlpha(Number(parentIdChain.pop() ?? 0) as number);
303
- 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
+ ];
304
516
  const idStr = fullIdChain.join('-');
305
517
 
306
- emit({
307
- type: 'start',
308
- id: idStr,
309
- 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,
310
533
  depth,
311
- meta: extractMeta(actionInternal),
312
- }, prefix);
313
-
314
- const measureForNextLevel = createNestedResolver(true, fullIdChain, childCounterRef, depth, _measureInternal, prefix, effectiveMaxLen);
315
- 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
+ );
316
548
 
317
549
  try {
318
550
  let result: U;
551
+
319
552
  if (timeout && timeout > 0) {
320
553
  result = await Promise.race([
321
- fnInternal(measureForNextLevel as MeasureFn, measureSyncForNextLevel as MeasureSyncFn),
322
- new Promise<never>((_, reject) =>
323
- setTimeout(() => reject(new Error(`Timeout (${formatDuration(timeout)})`)), timeout)
554
+ fnInternal(
555
+ measureForNextLevel as MeasureFn,
556
+ measureSyncForNextLevel as MeasureSyncFn,
324
557
  ),
558
+ new Promise<never>((_resolve, reject) => {
559
+ setTimeout(
560
+ () => reject(new Error(`Timeout (${formatDuration(timeout)})`)),
561
+ timeout,
562
+ );
563
+ }),
325
564
  ]);
326
565
  } else {
327
- result = await fnInternal(measureForNextLevel as MeasureFn, measureSyncForNextLevel as MeasureSyncFn);
566
+ result = await fnInternal(
567
+ measureForNextLevel as MeasureFn,
568
+ measureSyncForNextLevel as MeasureSyncFn,
569
+ );
328
570
  }
571
+
329
572
  const duration = performance.now() - start;
330
- 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
+
331
588
  return result;
332
589
  } catch (error) {
333
590
  const duration = performance.now() - start;
334
- 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
+
335
606
  _lastError = error;
607
+
336
608
  if (onError) {
337
609
  try {
338
- return onError(error);
610
+ return await onError(error);
339
611
  } catch (onErrorError) {
340
- 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
+
341
626
  _lastError = onErrorError;
342
627
  return null;
343
628
  }
344
629
  }
630
+
345
631
  return null;
346
632
  }
347
633
  };
348
634
 
349
635
  const _measureInternalSync = <U>(
350
636
  fnInternal: (measure: MeasureSyncFn) => U,
351
- actionInternal: string | object,
637
+ actionInternal: MeasureAction<U>,
352
638
  parentIdChain: (string | number)[],
353
639
  depth: number,
354
- _onError?: ((error: unknown) => any) | undefined,
355
- inheritedMaxLen?: number
640
+ onError?: (error: unknown) => U | null,
641
+ inheritedMaxLen?: number,
356
642
  ): U | null => {
357
643
  const start = performance.now();
358
644
  const childCounterRef = { value: 0 };
359
645
  const label = buildActionLabel(actionInternal);
360
- const hasNested = fnInternal.length > 0;
361
646
  const budget = extractBudget(actionInternal);
362
647
  const localMaxLen = extractMaxResultLength(actionInternal);
363
648
  const effectiveMaxLen = localMaxLen ?? inheritedMaxLen;
364
649
 
365
- const currentId = toAlpha(Number(parentIdChain.pop() ?? 0) as number);
366
- 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
+ ];
367
655
  const idStr = fullIdChain.join('-');
368
656
 
369
- if (hasNested) {
370
- emit({
657
+ emit(
658
+ {
371
659
  type: 'start',
372
660
  id: idStr,
373
661
  label,
374
662
  depth,
375
663
  meta: extractMeta(actionInternal),
376
- }, prefix);
377
- }
378
-
379
- 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
+ );
380
677
 
381
678
  try {
382
679
  const result = fnInternal(measureForNextLevel as MeasureSyncFn);
383
680
  const duration = performance.now() - start;
384
- 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
+
385
696
  return result;
386
697
  } catch (error) {
387
698
  const duration = performance.now() - start;
388
- 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
+
389
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
+
390
739
  return null;
391
740
  }
392
741
  };
393
742
 
394
- // ─── measure (async) ───────────────────────────────────────────
743
+ // ─── measure Async ─────────────────────────────────────────────
395
744
 
396
745
  const measureFn = async <T = null>(
397
- arg1: string | object,
398
- arg2?: ((measure: MeasureFn, measureSync: MeasureSyncFn) => Promise<T>) | ((measure: MeasureFn) => T),
399
- 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>,
400
752
  ): Promise<T | null> => {
401
753
  if (typeof arg2 === 'function') {
402
- return _measureInternal(arg2 as any, arg1, [counter.value++], 0, arg3, scopeMaxLen) as Promise<T | null>;
403
- } else {
404
- const currentId = toAlpha(counter.value++);
405
- 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
+ {
406
768
  type: 'annotation',
407
769
  id: currentId,
408
770
  label: buildActionLabel(arg1),
409
771
  depth: 0,
410
772
  meta: extractMeta(arg1),
411
- }, prefix);
412
- return Promise.resolve(null);
413
- }
773
+ },
774
+ prefix,
775
+ );
776
+
777
+ return Promise.resolve(null);
414
778
  };
415
779
 
416
780
  measureFn.timed = async <T = null>(
417
- arg1: string | object,
418
- arg2?: ((measure: MeasureFn) => Promise<T>)
781
+ arg1: MeasureAction<T>,
782
+ arg2?: ((measure: MeasureFn) => Promise<T>) | (() => Promise<T>),
419
783
  ): Promise<TimedResult<T>> => {
420
784
  const start = performance.now();
421
- const result = await measureFn(arg1, arg2);
785
+ const result = await measureFn(arg1, arg2 as any);
422
786
  const duration = performance.now() - start;
423
- return { result, duration };
787
+
788
+ return {
789
+ result,
790
+ duration,
791
+ };
424
792
  };
425
793
 
426
794
  measureFn.retry = async <T = null>(
427
- label: string | object,
795
+ label: MeasureAction<T>,
428
796
  opts: RetryOpts,
429
- fn: () => Promise<T>
797
+ fn: () => Promise<T>,
430
798
  ): Promise<T | null> => {
431
799
  const attempts = opts.attempts ?? 3;
432
800
  const delay = opts.delay ?? 1000;
433
801
  const backoff = opts.backoff ?? 1;
434
802
  const lbl = buildActionLabel(label);
435
803
  const budget = extractBudget(label);
804
+ const effectiveMaxLen = extractMaxResultLength(label) ?? scopeMaxLen;
436
805
 
437
- for (let i = 0; i < attempts; i++) {
806
+ for (let i = 0; i < attempts; i += 1) {
438
807
  const attempt = i + 1;
439
808
  const attemptLabel = `${lbl} [${attempt}/${attempts}]`;
440
809
  const start = performance.now();
441
810
  const currentId = toAlpha(counter.value++);
442
811
 
443
- emit({
444
- type: 'start',
445
- id: currentId,
446
- label: attemptLabel,
447
- depth: 0,
448
- meta: extractMeta(label),
449
- }, prefix);
812
+ emit(
813
+ {
814
+ type: 'start',
815
+ id: currentId,
816
+ label: attemptLabel,
817
+ depth: 0,
818
+ meta: extractMeta(label),
819
+ },
820
+ prefix,
821
+ );
450
822
 
451
823
  try {
452
824
  const result = await fn();
453
825
  const duration = performance.now() - start;
454
- 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
+
455
841
  return result;
456
842
  } catch (error) {
457
843
  const duration = performance.now() - start;
458
- 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
+
459
861
  if (attempt < attempts) {
460
- 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
+ });
461
865
  }
462
866
  }
463
867
  }
868
+
464
869
  return null;
465
870
  };
466
871
 
467
872
  measureFn.assert = async <T>(
468
- arg1: string | object,
469
- arg2: ((measure: MeasureFn) => Promise<T>) | (() => Promise<T>)
873
+ arg1: MeasureAction<T>,
874
+ arg2: ((measure: MeasureFn) => Promise<T>) | (() => Promise<T>),
470
875
  ): Promise<T> => {
471
876
  const result = await measureFn(arg1, arg2 as any);
877
+
472
878
  if (result === null) {
473
879
  const cause = _lastError;
474
880
  _lastError = null;
475
- throw new Error(`measure.assert: "${buildActionLabel(arg1)}" failed`, { cause });
881
+
882
+ throw new Error(`measure.assert: "${buildActionLabel(arg1)}" failed`, {
883
+ cause,
884
+ });
476
885
  }
886
+
477
887
  return result;
478
888
  };
479
889
 
480
890
  measureFn.wrap = <A extends any[], R>(
481
- label: string | object,
482
- fn: (...args: A) => Promise<R>
891
+ label: MeasureAction<R>,
892
+ fn: (...args: A) => Promise<R>,
483
893
  ): ((...args: A) => Promise<R | null>) => {
484
894
  return (...args: A) => measureFn(label, () => fn(...args));
485
895
  };
486
896
 
487
897
  measureFn.batch = async <T, R>(
488
- label: string | object,
898
+ label: MeasureAction<R>,
489
899
  items: T[],
490
900
  fn: (item: T, index: number) => Promise<R>,
491
- opts?: BatchOpts
901
+ opts?: BatchOpts,
492
902
  ): Promise<(R | null)[]> => {
493
903
  const lbl = buildActionLabel(label);
494
904
  const total = items.length;
495
905
  const every = opts?.every ?? Math.max(1, Math.ceil(total / 5));
496
906
  const currentId = toAlpha(counter.value++);
497
907
  const startTime = performance.now();
908
+ const budget = extractBudget(label);
498
909
 
499
- emit({
500
- type: 'start',
501
- id: currentId,
502
- label: `${lbl} (${total} items)`,
503
- depth: 0,
504
- meta: extractMeta(label),
505
- }, 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
+ );
506
920
 
507
921
  const results: (R | null)[] = [];
508
- for (let i = 0; i < items.length; i++) {
922
+
923
+ for (let i = 0; i < items.length; i += 1) {
509
924
  try {
510
925
  results.push(await fn(items[i]!, i));
511
- } catch {
926
+ } catch (error) {
927
+ _lastError = error;
512
928
  results.push(null);
513
929
  }
930
+
514
931
  if ((i + 1) % every === 0 && i + 1 < total) {
515
932
  const elapsed = (performance.now() - startTime) / 1000;
516
933
  const rate = ((i + 1) / elapsed).toFixed(0);
517
- emit({
518
- type: 'annotation',
519
- id: currentId,
520
- label: `${i + 1}/${total} (${elapsed.toFixed(1)}s, ${rate}/s)`,
521
- depth: 0,
522
- }, 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
+ );
523
944
  }
524
945
  }
525
946
 
526
947
  const duration = performance.now() - startTime;
527
- const budget = extractBudget(label);
528
- emit({
529
- type: 'success',
530
- id: currentId,
531
- label: `${lbl} (${total} items)`,
532
- depth: 0,
533
- duration,
534
- result: `${results.filter(r => r !== null).length}/${total} ok`,
535
- budget,
536
- }, 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
+
537
963
  return results;
538
964
  };
539
965
 
540
966
  // ─── measureSync ───────────────────────────────────────────────
541
967
 
542
968
  const measureSyncFn = <T = null>(
543
- arg1: string | object,
544
- arg2?: ((measure: MeasureSyncFn) => T)
969
+ arg1: MeasureAction<T>,
970
+ arg2?: ((measure: MeasureSyncFn) => T) | (() => T),
971
+ arg3?: (error: unknown) => T | null,
545
972
  ): T | null => {
546
973
  if (typeof arg2 === 'function') {
547
- return _measureInternalSync(arg2, arg1, [counter.value++], 0, undefined, scopeMaxLen) as T | null;
548
- } else {
549
- const currentId = toAlpha(counter.value++);
550
- 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
+ {
551
988
  type: 'annotation',
552
989
  id: currentId,
553
990
  label: buildActionLabel(arg1),
554
991
  depth: 0,
555
992
  meta: extractMeta(arg1),
556
- }, prefix);
557
- return null;
558
- }
993
+ },
994
+ prefix,
995
+ );
996
+
997
+ return null;
559
998
  };
560
999
 
561
1000
  measureSyncFn.timed = <T = null>(
562
- arg1: string | object,
563
- arg2?: ((measure: MeasureSyncFn) => T)
1001
+ arg1: MeasureAction<T>,
1002
+ arg2?: ((measure: MeasureSyncFn) => T) | (() => T),
564
1003
  ): TimedResult<T> => {
565
1004
  const start = performance.now();
566
- const result = measureSyncFn(arg1, arg2);
1005
+ const result = measureSyncFn(arg1, arg2 as any);
567
1006
  const duration = performance.now() - start;
568
- return { result, duration };
1007
+
1008
+ return {
1009
+ result,
1010
+ duration,
1011
+ };
569
1012
  };
570
1013
 
571
1014
  measureSyncFn.assert = <T>(
572
- arg1: string | object,
573
- arg2: ((measure: MeasureSyncFn) => T) | (() => T)
1015
+ arg1: MeasureAction<T>,
1016
+ arg2: ((measure: MeasureSyncFn) => T) | (() => T),
574
1017
  ): T => {
575
1018
  const result = measureSyncFn(arg1, arg2 as any);
1019
+
576
1020
  if (result === null) {
577
1021
  const cause = _lastError;
578
1022
  _lastError = null;
579
- throw new Error(`measureSync.assert: "${buildActionLabel(arg1)}" failed`, { cause });
1023
+
1024
+ throw new Error(`measureSync.assert: "${buildActionLabel(arg1)}" failed`, {
1025
+ cause,
1026
+ });
580
1027
  }
1028
+
581
1029
  return result;
582
1030
  };
583
1031
 
584
1032
  measureSyncFn.wrap = <A extends any[], R>(
585
- label: string | object,
586
- fn: (...args: A) => R
1033
+ label: MeasureAction<R>,
1034
+ fn: (...args: A) => R,
587
1035
  ): ((...args: A) => R | null) => {
588
1036
  return (...args: A) => measureSyncFn(label, () => fn(...args));
589
1037
  };
590
1038
 
591
- 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
+ };
592
1087
  };
593
1088
 
594
- // ─── Default (global) instance ───────────────────────────────────────
1089
+ // ─── Default Global Instance ─────────────────────────────────────────
595
1090
 
596
1091
  const globalInstance = createMeasureImpl();
597
1092
 
598
1093
  export const measure = globalInstance.measure;
599
1094
  export const measureSync = globalInstance.measureSync;
600
1095
 
601
- // ─── Scoped instances ────────────────────────────────────────────────
1096
+ // ─── Scoped Instances ────────────────────────────────────────────────
602
1097
 
603
1098
  export const createMeasure = (scopePrefix: string, opts?: ScopeOpts) => {
604
- const scopeCounter = { value: 0 };
1099
+ const scopeCounter = {
1100
+ value: 0,
1101
+ };
1102
+
605
1103
  const scoped = createMeasureImpl(scopePrefix, scopeCounter, opts);
1104
+
606
1105
  return {
607
1106
  ...scoped,
608
- resetCounter: () => { scopeCounter.value = 0; },
1107
+ resetCounter: () => {
1108
+ scopeCounter.value = 0;
1109
+ },
609
1110
  };
610
1111
  };
611
-
612
- // ─── Utility exports ─────────────────────────────────────────────────
613
-
614
- 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.0",
6
+ "version": "3.11.2",
7
7
  "type": "module",
8
8
  "private": false,
9
9
  "description": "Zero-dependency function performance measurement with hierarchical logging",
@@ -30,4 +30,4 @@
30
30
  "test": "bun test",
31
31
  "example": "bun run example.ts"
32
32
  }
33
- }
33
+ }
package/bun.lock DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "lockfileVersion": 1,
3
- "configVersion": 0,
4
- "workspaces": {
5
- "": {
6
- "name": "ments-utils",
7
- "devDependencies": {
8
- "@types/bun": "latest",
9
- },
10
- "peerDependencies": {
11
- "typescript": "^5",
12
- },
13
- },
14
- },
15
- "packages": {
16
- "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
17
-
18
- "@types/node": ["@types/node@24.0.4", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA=="],
19
-
20
- "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
21
-
22
- "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
23
-
24
- "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
25
- }
26
- }