measure-fn 3.7.0 → 3.8.1
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 +2 -1
- package/index.test.ts +50 -4
- package/index.ts +36 -17
- package/package.json +1 -1
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:
|
|
240
|
+
maxResultLength: 200, // truncate results (default: 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
15
|
+
let maxResultLen = 0;
|
|
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
|
-
|
|
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
|
-
|
|
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