brainy/src/utils/callerLocation.ts

54 lines
2.7 KiB
TypeScript

/**
* @module utils/callerLocation
* @description Stack-frame extraction utility used by error/warning messages
* across Brainy. The first non-Brainy frame on the stack is the consumer's
* actual call site — the one that's useful to point at in diagnostics.
*
* Used by:
* - Subtype enforcement errors in `brainy.ts` (7.30.1+) — guides consumers
* from `requireSubtype()` rejections back to the offending call site.
* - Query-limit enforcement in `paramValidation.ts` (7.30.2+) — pairs with
* the recalibrated limit cap so consumers see which `find({ limit })`
* call triggered the warn-or-throw.
*
* No runtime dependencies. Pure stack walking + string matching.
*/
/**
* Extract the first non-Brainy frame from the current stack so error and
* warning messages can point at the consumer's call site instead of Brainy
* internals.
*
* The function walks `new Error().stack`, skips any frame inside Brainy's own
* source or compiled output, and returns the first remaining frame stripped of
* the leading `at ` token so the caller can compose the rendered line however
* it likes.
*
* @param extraSkipPatterns Optional list of substrings — frames matching ANY
* of these are also skipped. Use for in-module helpers that show up between
* the public API and the consumer (e.g. the formatter that builds the
* error message itself).
* @returns The caller's location string (e.g. `"OrderService.create (/app/src/orders/service.ts:42:23)"`)
* or `null` when the stack isn't available or only contains Brainy frames.
*/
export function findCallerLocation(extraSkipPatterns: string[] = []): string | null {
const stack = new Error().stack
if (!stack) return null
const lines = stack.split('\n').slice(1) // drop the `Error` line
for (const raw of lines) {
const line = raw.trim()
// Always skip Brainy's own source + compiled output. Consumers need their
// own call site, not a line inside `brainy.ts` or `dist/brainy.js`.
if (line.includes('/src/brainy.ts') || line.includes('/dist/brainy.js')) continue
// Skip the validation + diagnostic helpers regardless of which file they
// live in — they're plumbing between the public API and the consumer.
if (line.includes('findCallerLocation')) continue
if (line.includes('paramValidation') && line.includes('validate')) continue
if (line.includes('enforceSubtypeOn') || line.includes('formatSubtypeError')) continue
// Caller-supplied patterns (e.g. specific formatter names)
if (extraSkipPatterns.some(p => line.includes(p))) continue
// Strip leading `at ` if present so the caller can format consistently.
return line.replace(/^at /, '')
}
return null
}