measure-fn 3.7.0 → 3.8.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.
package/README.md CHANGED
@@ -125,6 +125,7 @@ The first argument can be a label string, or an options object:
125
125
  | `label` | `string` | Display name (required if object) |
126
126
  | `timeout` | `number` | Aborts after N ms (returns `null`) |
127
127
  | `budget` | `number` | Warns if slower than N ms (doesn't abort) |
128
+ | `maxResultLength` | `number` | Override result truncation (0 = unlimited, inherits to children) |
128
129
  | any other | `any` | Logged inline as context metadata |
129
130
 
130
131
  **Timeout** (enforce):
@@ -236,7 +237,7 @@ import { configure } from 'measure-fn';
236
237
  configure({
237
238
  silent: true, // suppress all output
238
239
  timestamps: true, // prepend [HH:MM:SS.mmm]
239
- maxResultLength: 200, // truncate results (default: 80)
240
+ maxResultLength: 200, // truncate results (default: 200, 0 = unlimited)
240
241
  dotEndLabel: false, // show full label on end lines (default: true = dots)
241
242
  dotChar: '.', // character for dot fill (default: '·')
242
243
  logger: (event) => { // custom event handler
package/index.test.ts CHANGED
@@ -21,7 +21,7 @@ function captureConsole() {
21
21
  describe("measure (async)", () => {
22
22
  beforeEach(() => {
23
23
  resetCounter();
24
- configure({ silent: false, logger: null, timestamps: false, maxResultLength: 80 });
24
+ configure({ silent: false, logger: null, timestamps: false, maxResultLength: 200 });
25
25
  });
26
26
 
27
27
  test("runs and returns result", async () => {
@@ -115,7 +115,7 @@ describe("measure (async)", () => {
115
115
  describe("measureSync", () => {
116
116
  beforeEach(() => {
117
117
  resetCounter();
118
- configure({ silent: false, logger: null, timestamps: false, maxResultLength: 80 });
118
+ configure({ silent: false, logger: null, timestamps: false, maxResultLength: 200 });
119
119
  });
120
120
 
121
121
  test("leaf = single line with result", () => {
@@ -178,7 +178,7 @@ describe("formatDuration", () => {
178
178
  describe("safeStringify", () => {
179
179
  beforeEach(() => {
180
180
  resetCounter();
181
- configure({ silent: false, logger: null, timestamps: false, maxResultLength: 80 });
181
+ configure({ silent: false, logger: null, timestamps: false, maxResultLength: 200 });
182
182
  });
183
183
 
184
184
  test("circular handled", () => {
@@ -247,7 +247,7 @@ describe("timestamps", () => {
247
247
  describe("configurable truncation", () => {
248
248
  beforeEach(() => {
249
249
  resetCounter();
250
- configure({ silent: false, logger: null, timestamps: false, maxResultLength: 80 });
250
+ configure({ silent: false, logger: null, timestamps: false, maxResultLength: 200 });
251
251
  });
252
252
 
253
253
  test("shorter truncation", () => {
@@ -265,6 +265,52 @@ describe("configurable truncation", () => {
265
265
  out.restore();
266
266
  expect(out.logs[0]).not.toContain("…");
267
267
  });
268
+
269
+ test("per-label maxResultLength overrides global", () => {
270
+ configure({ maxResultLength: 500 });
271
+ const out = captureConsole();
272
+ measureSync({ label: "op", maxResultLength: 15 }, () => ({ d: "x".repeat(50) }));
273
+ out.restore();
274
+ expect(out.logs[0]).toContain("…");
275
+ });
276
+
277
+ test("per-label maxResultLength inherits to children", () => {
278
+ const out = captureConsole();
279
+ measureSync({ label: "parent", maxResultLength: 15 }, (m) => {
280
+ m("child", () => ({ d: "x".repeat(50) }));
281
+ return 1;
282
+ });
283
+ out.restore();
284
+ const childLine = out.logs[1]; // [a-a] line
285
+ expect(childLine).toContain("…");
286
+ });
287
+
288
+ test("child can override inherited maxResultLength", () => {
289
+ const out = captureConsole();
290
+ measureSync({ label: "parent", maxResultLength: 15 }, (m) => {
291
+ m({ label: "child", maxResultLength: 500 }, () => ({ d: "x".repeat(50) }));
292
+ return 1;
293
+ });
294
+ out.restore();
295
+ const childLine = out.logs[1]; // child line
296
+ expect(childLine).not.toContain("…");
297
+ });
298
+
299
+ test("maxResultLength: 0 means unlimited", () => {
300
+ const out = captureConsole();
301
+ measureSync({ label: "op", maxResultLength: 0 }, () => ({ d: "x".repeat(500) }));
302
+ out.restore();
303
+ expect(out.logs[0]).not.toContain("…");
304
+ expect(out.logs[0]).toContain("x".repeat(500));
305
+ });
306
+
307
+ test("maxResultLength not shown in meta", () => {
308
+ const out = captureConsole();
309
+ measureSync({ label: "op", maxResultLength: 50 }, (m) => { return 1; });
310
+ out.restore();
311
+ expect(out.logs[0]).toBe("[a] ... op");
312
+ expect(out.logs[0]).not.toContain("maxResultLength");
313
+ });
268
314
  });
269
315
 
270
316
  // ─── Budget ──────────────────────────────────────────────────────────
package/index.ts CHANGED
@@ -12,9 +12,10 @@ const toAlpha = (num: number): string => {
12
12
 
13
13
  // ─── Safe Stringify ──────────────────────────────────────────────────
14
14
 
15
- let maxResultLen = 80;
15
+ let maxResultLen = 200;
16
16
 
17
- export const safeStringify = (value: unknown): string => {
17
+ export const safeStringify = (value: unknown, limit?: number): string => {
18
+ const cap = limit ?? maxResultLen;
18
19
  if (value === undefined) return '';
19
20
  if (value === null) return 'null';
20
21
  if (typeof value === 'number' || typeof value === 'boolean') return String(value);
@@ -22,7 +23,8 @@ export const safeStringify = (value: unknown): string => {
22
23
  if (typeof value === 'symbol') return value.toString();
23
24
  if (typeof value === 'string') {
24
25
  const q = JSON.stringify(value);
25
- return q.length > maxResultLen ? q.slice(0, maxResultLen - 1) + '…"' : q;
26
+ if (cap === 0) return q;
27
+ return q.length > cap ? q.slice(0, cap - 1) + '…"' : q;
26
28
  }
27
29
  try {
28
30
  const seen = new WeakSet();
@@ -35,7 +37,8 @@ export const safeStringify = (value: unknown): string => {
35
37
  if (typeof val === 'bigint') return `${val}n`;
36
38
  return val;
37
39
  });
38
- return str.length > maxResultLen ? str.slice(0, maxResultLen) + '…' : str;
40
+ if (cap === 0) return str;
41
+ return str.length > cap ? str.slice(0, cap) + '…' : str;
39
42
  } catch {
40
43
  return String(value);
41
44
  }
@@ -78,6 +81,7 @@ export type MeasureEvent = {
78
81
  error?: unknown;
79
82
  meta?: Record<string, unknown>;
80
83
  budget?: number;
84
+ maxResultLength?: number;
81
85
  };
82
86
 
83
87
  // ─── Configuration ───────────────────────────────────────────────────
@@ -128,11 +132,18 @@ const extractTimeout = (actionInternal: string | object): number | undefined =>
128
132
  return undefined;
129
133
  };
130
134
 
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;
139
+ };
140
+
131
141
  const extractMeta = (actionInternal: string | object): Record<string, unknown> | undefined => {
132
142
  if (typeof actionInternal !== 'object' || actionInternal === null) return undefined;
133
143
  const details = { ...actionInternal };
134
144
  if ('label' in details) delete (details as any).label;
135
145
  if ('budget' in details) delete (details as any).budget;
146
+ if ('maxResultLength' in details) delete (details as any).maxResultLength;
136
147
  if (Object.keys(details).length === 0) return undefined;
137
148
  return details as Record<string, unknown>;
138
149
  };
@@ -165,7 +176,7 @@ const defaultLogger = (event: MeasureEvent, prefix?: string) => {
165
176
  break;
166
177
  case 'success': {
167
178
  const endLabel = dotEndLabel ? dotChar.repeat(event.label.length) : event.label;
168
- const resultStr = event.result !== undefined ? safeStringify(event.result) : '';
179
+ const resultStr = event.result !== undefined ? safeStringify(event.result, event.maxResultLength) : '';
169
180
  const arrow = resultStr ? ` → ${resultStr}` : '';
170
181
  const budgetWarn = event.budget && event.duration! > event.budget
171
182
  ? ` ⚠ OVER BUDGET (${formatDuration(event.budget)})`
@@ -229,8 +240,9 @@ const createNestedResolver = (
229
240
  fullIdChain: string[],
230
241
  childCounterRef: { value: number },
231
242
  depth: number,
232
- resolver: <U>(fn: any, action: any, chain: (string | number)[], depth: number, onError?: (error: unknown) => any) => Promise<U | null> | (U | null),
233
- prefix?: string
243
+ resolver: <U>(fn: any, action: any, chain: (string | number)[], depth: number, onError?: (error: unknown) => any, inheritedMaxLen?: number) => Promise<U | null> | (U | null),
244
+ prefix?: string,
245
+ inheritedMaxLen?: number
234
246
  ) => {
235
247
  return (...args: any[]) => {
236
248
  const label = args[0];
@@ -239,7 +251,7 @@ const createNestedResolver = (
239
251
 
240
252
  if (typeof fn === 'function') {
241
253
  const childParentChain = [...fullIdChain, childCounterRef.value++];
242
- return resolver(fn, label, childParentChain, depth + 1, typeof onError === 'function' ? onError : undefined);
254
+ return resolver(fn, label, childParentChain, depth + 1, typeof onError === 'function' ? onError : undefined, inheritedMaxLen);
243
255
  } else {
244
256
  emit({
245
257
  type: 'annotation',
@@ -272,13 +284,16 @@ const createMeasureImpl = (prefix?: string, counterRef?: { value: number }) => {
272
284
  actionInternal: string | object,
273
285
  parentIdChain: (string | number)[],
274
286
  depth: number,
275
- onError?: (error: unknown) => any
287
+ onError?: (error: unknown) => any,
288
+ inheritedMaxLen?: number
276
289
  ): Promise<U | null> => {
277
290
  const start = performance.now();
278
291
  const childCounterRef = { value: 0 };
279
292
  const label = buildActionLabel(actionInternal);
280
293
  const budget = extractBudget(actionInternal);
281
294
  const timeout = extractTimeout(actionInternal);
295
+ const localMaxLen = extractMaxResultLength(actionInternal);
296
+ const effectiveMaxLen = localMaxLen ?? inheritedMaxLen;
282
297
 
283
298
  const currentId = toAlpha(parentIdChain.pop() ?? 0);
284
299
  const fullIdChain = [...parentIdChain, currentId];
@@ -292,7 +307,7 @@ const createMeasureImpl = (prefix?: string, counterRef?: { value: number }) => {
292
307
  meta: extractMeta(actionInternal),
293
308
  }, prefix);
294
309
 
295
- const measureForNextLevel = createNestedResolver(true, fullIdChain, childCounterRef, depth, _measureInternal, prefix);
310
+ const measureForNextLevel = createNestedResolver(true, fullIdChain, childCounterRef, depth, _measureInternal, prefix, effectiveMaxLen);
296
311
 
297
312
  try {
298
313
  let result: U;
@@ -307,17 +322,17 @@ const createMeasureImpl = (prefix?: string, counterRef?: { value: number }) => {
307
322
  result = await fnInternal(measureForNextLevel as MeasureFn);
308
323
  }
309
324
  const duration = performance.now() - start;
310
- emit({ type: 'success', id: idStr, label, depth, duration, result, budget }, prefix);
325
+ emit({ type: 'success', id: idStr, label, depth, duration, result, budget, maxResultLength: effectiveMaxLen }, prefix);
311
326
  return result;
312
327
  } catch (error) {
313
328
  const duration = performance.now() - start;
314
- emit({ type: 'error', id: idStr, label, depth, duration, error, budget }, prefix);
329
+ emit({ type: 'error', id: idStr, label, depth, duration, error, budget, maxResultLength: effectiveMaxLen }, prefix);
315
330
  _lastError = error;
316
331
  if (onError) {
317
332
  try {
318
333
  return onError(error);
319
334
  } catch (onErrorError) {
320
- emit({ type: 'error', id: idStr, label: `${label} (onError)`, depth, duration: performance.now() - start, error: onErrorError, budget }, prefix);
335
+ emit({ type: 'error', id: idStr, label: `${label} (onError)`, depth, duration: performance.now() - start, error: onErrorError, budget, maxResultLength: effectiveMaxLen }, prefix);
321
336
  _lastError = onErrorError;
322
337
  return null;
323
338
  }
@@ -330,13 +345,17 @@ const createMeasureImpl = (prefix?: string, counterRef?: { value: number }) => {
330
345
  fnInternal: (measure: MeasureSyncFn) => U,
331
346
  actionInternal: string | object,
332
347
  parentIdChain: (string | number)[],
333
- depth: number
348
+ depth: number,
349
+ _onError?: undefined,
350
+ inheritedMaxLen?: number
334
351
  ): U | null => {
335
352
  const start = performance.now();
336
353
  const childCounterRef = { value: 0 };
337
354
  const label = buildActionLabel(actionInternal);
338
355
  const hasNested = fnInternal.length > 0;
339
356
  const budget = extractBudget(actionInternal);
357
+ const localMaxLen = extractMaxResultLength(actionInternal);
358
+ const effectiveMaxLen = localMaxLen ?? inheritedMaxLen;
340
359
 
341
360
  const currentId = toAlpha(parentIdChain.pop() ?? 0);
342
361
  const fullIdChain = [...parentIdChain, currentId];
@@ -352,16 +371,16 @@ const createMeasureImpl = (prefix?: string, counterRef?: { value: number }) => {
352
371
  }, prefix);
353
372
  }
354
373
 
355
- const measureForNextLevel = createNestedResolver(false, fullIdChain, childCounterRef, depth, _measureInternalSync, prefix);
374
+ const measureForNextLevel = createNestedResolver(false, fullIdChain, childCounterRef, depth, _measureInternalSync, prefix, effectiveMaxLen);
356
375
 
357
376
  try {
358
377
  const result = fnInternal(measureForNextLevel as MeasureSyncFn);
359
378
  const duration = performance.now() - start;
360
- emit({ type: 'success', id: idStr, label, depth, duration, result, budget }, prefix);
379
+ emit({ type: 'success', id: idStr, label, depth, duration, result, budget, maxResultLength: effectiveMaxLen }, prefix);
361
380
  return result;
362
381
  } catch (error) {
363
382
  const duration = performance.now() - start;
364
- emit({ type: 'error', id: idStr, label, depth, duration, error, budget }, prefix);
383
+ emit({ type: 'error', id: idStr, label, depth, duration, error, budget, maxResultLength: effectiveMaxLen }, prefix);
365
384
  _lastError = error;
366
385
  return null;
367
386
  }
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.7.0",
6
+ "version": "3.8.0",
7
7
  "type": "module",
8
8
  "private": false,
9
9
  "description": "Zero-dependency function performance measurement with hierarchical logging",