@prometheus-ai/utils 0.5.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/dist/types/abortable.d.ts +27 -0
- package/dist/types/async.d.ts +6 -0
- package/dist/types/cli.d.ts +117 -0
- package/dist/types/color.d.ts +102 -0
- package/dist/types/dirs.d.ts +171 -0
- package/dist/types/env.d.ts +55 -0
- package/dist/types/fetch-retry.d.ts +80 -0
- package/dist/types/format.d.ts +37 -0
- package/dist/types/frontmatter.d.ts +25 -0
- package/dist/types/fs-error.d.ts +31 -0
- package/dist/types/glob.d.ts +28 -0
- package/dist/types/hook-fetch.d.ts +16 -0
- package/dist/types/index.d.ts +29 -0
- package/dist/types/json.d.ts +4 -0
- package/dist/types/logger.d.ts +66 -0
- package/dist/types/mermaid-ascii.d.ts +11 -0
- package/dist/types/mime.d.ts +29 -0
- package/dist/types/peek-file.d.ts +29 -0
- package/dist/types/postmortem.d.ts +29 -0
- package/dist/types/procmgr.d.ts +25 -0
- package/dist/types/prompt.d.ts +18 -0
- package/dist/types/ptree.d.ts +108 -0
- package/dist/types/ring.d.ts +93 -0
- package/dist/types/sanitize-text.d.ts +14 -0
- package/dist/types/snowflake.d.ts +25 -0
- package/dist/types/stream.d.ts +68 -0
- package/dist/types/tab-spacing.d.ts +9 -0
- package/dist/types/temp.d.ts +14 -0
- package/dist/types/type-guards.d.ts +3 -0
- package/dist/types/which.d.ts +37 -0
- package/package.json +61 -0
- package/src/abortable.ts +73 -0
- package/src/async.ts +50 -0
- package/src/cli.ts +432 -0
- package/src/color.ts +302 -0
- package/src/dirs.ts +584 -0
- package/src/env.ts +172 -0
- package/src/fetch-retry.ts +325 -0
- package/src/format.ts +113 -0
- package/src/frontmatter.ts +128 -0
- package/src/fs-error.ts +56 -0
- package/src/glob.ts +189 -0
- package/src/hook-fetch.ts +30 -0
- package/src/index.ts +49 -0
- package/src/json.ts +10 -0
- package/src/logger.ts +417 -0
- package/src/mermaid-ascii.ts +31 -0
- package/src/mime.ts +159 -0
- package/src/peek-file.ts +188 -0
- package/src/postmortem.ts +196 -0
- package/src/procmgr.ts +195 -0
- package/src/prompt.ts +471 -0
- package/src/ptree.ts +390 -0
- package/src/ring.ts +169 -0
- package/src/sanitize-text.ts +38 -0
- package/src/snowflake.ts +136 -0
- package/src/stream.ts +403 -0
- package/src/tab-spacing.ts +342 -0
- package/src/temp.ts +77 -0
- package/src/type-guards.ts +11 -0
- package/src/which.ts +232 -0
package/src/logger.ts
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized logger for prometheus.
|
|
3
|
+
*
|
|
4
|
+
* Default: rotating `~/.prometheus/logs/prometheus.<DATE>.log`, no console output (writing
|
|
5
|
+
* to stdout/stderr would corrupt the TUI). Long-running headless services
|
|
6
|
+
* (the auth broker, etc.) call {@link setTransports} to swap in a console
|
|
7
|
+
* transport so a process supervisor (pm2, journald, k8s) captures the logs.
|
|
8
|
+
*
|
|
9
|
+
* Each entry includes `process.pid` so concurrent prometheus instances stay
|
|
10
|
+
* traceable.
|
|
11
|
+
*/
|
|
12
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import { isPromise } from "node:util/types";
|
|
15
|
+
import winston from "winston";
|
|
16
|
+
import DailyRotateFile from "winston-daily-rotate-file";
|
|
17
|
+
import { getLogsDir } from "./dirs";
|
|
18
|
+
|
|
19
|
+
/** Ensure a logs directory exists; return the resolved path. */
|
|
20
|
+
function ensureDir(dir: string): string {
|
|
21
|
+
if (!fs.existsSync(dir)) {
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* JSON.stringify replacer that unwraps {@link Error} instances. Error's own
|
|
29
|
+
* properties are non-enumerable, so a plain `JSON.stringify(err)` produces
|
|
30
|
+
* `"{}"`. Without this, a context like `{ err }` lost every useful field and
|
|
31
|
+
* forensic logs showed only an opaque empty object.
|
|
32
|
+
*/
|
|
33
|
+
function jsonReplacer(_key: string, value: unknown): unknown {
|
|
34
|
+
if (value instanceof Error) {
|
|
35
|
+
const out: Record<string, unknown> = {
|
|
36
|
+
name: value.name,
|
|
37
|
+
message: value.message,
|
|
38
|
+
stack: value.stack,
|
|
39
|
+
};
|
|
40
|
+
// Preserve `.cause` and any custom enumerable fields the caller attached.
|
|
41
|
+
const errAsRecord = value as unknown as Record<string, unknown>;
|
|
42
|
+
for (const k in errAsRecord) out[k] = errAsRecord[k];
|
|
43
|
+
if (value.cause !== undefined) out.cause = value.cause;
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Custom format that includes pid and flattens metadata */
|
|
50
|
+
const logFormat = winston.format.combine(
|
|
51
|
+
winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }),
|
|
52
|
+
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
|
53
|
+
const entry: Record<string, unknown> = {
|
|
54
|
+
timestamp,
|
|
55
|
+
level,
|
|
56
|
+
pid: process.pid,
|
|
57
|
+
message,
|
|
58
|
+
};
|
|
59
|
+
// Flatten metadata into entry
|
|
60
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
61
|
+
if (key !== "level" && key !== "timestamp" && key !== "message") {
|
|
62
|
+
entry[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return JSON.stringify(entry, jsonReplacer);
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/** Build a rotating file transport, materializing the target directory lazily. */
|
|
70
|
+
function makeFileTransport(dir?: string): winston.transport {
|
|
71
|
+
return new DailyRotateFile({
|
|
72
|
+
dirname: ensureDir(dir ?? getLogsDir()),
|
|
73
|
+
filename: "prometheus.%DATE%.log",
|
|
74
|
+
datePattern: "YYYY-MM-DD",
|
|
75
|
+
maxSize: "10m",
|
|
76
|
+
maxFiles: 5,
|
|
77
|
+
zippedArchive: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function makeConsoleTransport(): winston.transport {
|
|
82
|
+
return new winston.transports.Console({ format: logFormat });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The winston logger instance. Default: file ON (TUI-safe), console OFF. */
|
|
86
|
+
const winstonLogger = winston.createLogger({
|
|
87
|
+
level: "debug",
|
|
88
|
+
format: logFormat,
|
|
89
|
+
transports: [makeFileTransport()],
|
|
90
|
+
// Don't exit on error - logging failures shouldn't crash the app
|
|
91
|
+
exitOnError: false,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Replace the active log transports. Pass `console: true, file: false` for
|
|
96
|
+
* long-running services (the auth broker, etc.) that want their structured
|
|
97
|
+
* logs piped into a process supervisor instead of the rotating file.
|
|
98
|
+
*/
|
|
99
|
+
export function setTransports(opts: { console?: boolean; file?: boolean | string }): void {
|
|
100
|
+
winstonLogger.clear();
|
|
101
|
+
if (opts.file) {
|
|
102
|
+
winstonLogger.add(makeFileTransport(typeof opts.file === "string" ? opts.file : undefined));
|
|
103
|
+
}
|
|
104
|
+
if (opts.console) winstonLogger.add(makeConsoleTransport());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Log an error message.
|
|
109
|
+
* @param message - The message to log.
|
|
110
|
+
* @param context - The context to log.
|
|
111
|
+
*/
|
|
112
|
+
export function error(message: string, context?: Record<string, unknown>): void {
|
|
113
|
+
try {
|
|
114
|
+
winstonLogger.error(message, context);
|
|
115
|
+
} catch {
|
|
116
|
+
// Silently ignore logging failures
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Log a warning message.
|
|
122
|
+
* @param message - The message to log.
|
|
123
|
+
* @param context - The context to log.
|
|
124
|
+
*/
|
|
125
|
+
export function warn(message: string, context?: Record<string, unknown>): void {
|
|
126
|
+
try {
|
|
127
|
+
winstonLogger.warn(message, context);
|
|
128
|
+
} catch {
|
|
129
|
+
// Silently ignore logging failures
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Log an informational message.
|
|
135
|
+
* @param message - The message to log.
|
|
136
|
+
* @param context - The context to log.
|
|
137
|
+
*/
|
|
138
|
+
export function info(message: string, context?: Record<string, unknown>): void {
|
|
139
|
+
try {
|
|
140
|
+
winstonLogger.info(message, context);
|
|
141
|
+
} catch {
|
|
142
|
+
// Silently ignore logging failures
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Log a debug message.
|
|
148
|
+
* @param message - The message to log.
|
|
149
|
+
* @param context - The context to log.
|
|
150
|
+
*/
|
|
151
|
+
export function debug(message: string, context?: Record<string, unknown>): void {
|
|
152
|
+
try {
|
|
153
|
+
winstonLogger.debug(message, context);
|
|
154
|
+
} catch {
|
|
155
|
+
// Silently ignore logging failures
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const LOGGED_TIMING_THRESHOLD_MS = 0.5;
|
|
160
|
+
|
|
161
|
+
interface Span {
|
|
162
|
+
op: string;
|
|
163
|
+
start: number;
|
|
164
|
+
end?: number;
|
|
165
|
+
parent?: Span;
|
|
166
|
+
children: Span[];
|
|
167
|
+
/** Marker / point event without a duration. */
|
|
168
|
+
point?: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const spanStorage = new AsyncLocalStorage<Span>();
|
|
172
|
+
let gRootSpan: Span | undefined;
|
|
173
|
+
let gRecordTimings = false;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Print collected timings as an indented tree.
|
|
177
|
+
* Each span shows wall duration; parents with children also show "(self)" for unattributed time.
|
|
178
|
+
* Sibling spans are sorted by start time. Spans whose intervals overlap with siblings ran in parallel.
|
|
179
|
+
*/
|
|
180
|
+
export function printTimings(): void {
|
|
181
|
+
if (!gRecordTimings || !gRootSpan) {
|
|
182
|
+
console.error("\n--- Startup Timings ---\n(no markers)\n");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
gRootSpan.end = performance.now();
|
|
187
|
+
const lines: string[] = [];
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push("--- Startup timings (hierarchical) ---");
|
|
190
|
+
const work: Span[] = [];
|
|
191
|
+
const loads: Span[] = [];
|
|
192
|
+
for (const child of gRootSpan.children) {
|
|
193
|
+
if (isModuleLoadSpan(child)) loads.push(child);
|
|
194
|
+
else work.push(child);
|
|
195
|
+
}
|
|
196
|
+
for (const child of work.sort((a, b) => a.start - b.start)) {
|
|
197
|
+
printSpan(child, 0, lines);
|
|
198
|
+
}
|
|
199
|
+
if (loads.length > 0) {
|
|
200
|
+
printModuleLoadSummary(loads, 0, lines);
|
|
201
|
+
}
|
|
202
|
+
const totalMs = (gRootSpan.end - gRootSpan.start).toFixed(1);
|
|
203
|
+
lines.push(`Total: ${totalMs}ms`);
|
|
204
|
+
lines.push("--------------------------------------");
|
|
205
|
+
lines.push("");
|
|
206
|
+
console.error(lines.join("\n"));
|
|
207
|
+
gRootSpan.end = undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Begin recording startup timings under a new root span.
|
|
212
|
+
* Idempotent: a second call while already recording is a no-op so that side-effect
|
|
213
|
+
* starters (see module-timer.ts) and explicit starters (main.ts) can coexist.
|
|
214
|
+
*/
|
|
215
|
+
export function startTiming(): void {
|
|
216
|
+
if (gRecordTimings) return;
|
|
217
|
+
gRootSpan = {
|
|
218
|
+
op: "(root)",
|
|
219
|
+
start: performance.now(),
|
|
220
|
+
parent: undefined,
|
|
221
|
+
children: [],
|
|
222
|
+
};
|
|
223
|
+
gRecordTimings = true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Record an externally-measured span as a leaf child of the active span (or root
|
|
228
|
+
* when no span is active). Used by the module-load timing plugin to splice load
|
|
229
|
+
* events into the tree retroactively.
|
|
230
|
+
*/
|
|
231
|
+
export function recordModuleLoadSpan(path: string, start: number, durationMs: number): void {
|
|
232
|
+
if (!gRecordTimings || !gRootSpan) return;
|
|
233
|
+
const parent = spanStorage.getStore() ?? gRootSpan;
|
|
234
|
+
const span: Span = {
|
|
235
|
+
op: `load:${shortenLoadPath(path)}`,
|
|
236
|
+
start,
|
|
237
|
+
end: start + durationMs,
|
|
238
|
+
parent,
|
|
239
|
+
children: [],
|
|
240
|
+
};
|
|
241
|
+
parent.children.push(span);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function shortenLoadPath(p: string): string {
|
|
245
|
+
const cwd = process.cwd();
|
|
246
|
+
if (p.startsWith(`${cwd}/`)) return p.slice(cwd.length + 1);
|
|
247
|
+
const home = process.env.HOME;
|
|
248
|
+
if (home && p.startsWith(`${home}/`)) return `~/${p.slice(home.length + 1)}`;
|
|
249
|
+
return p;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* End timing window and clear buffers.
|
|
254
|
+
*/
|
|
255
|
+
export function endTiming(): void {
|
|
256
|
+
gRootSpan = undefined;
|
|
257
|
+
gRecordTimings = false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function durationOf(span: Span): number {
|
|
261
|
+
if (span.point || span.end === undefined) return 0;
|
|
262
|
+
return span.end - span.start;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Self time = total - union of child intervals (handles parallel children correctly). */
|
|
266
|
+
function selfTimeOf(span: Span): number {
|
|
267
|
+
const dur = durationOf(span);
|
|
268
|
+
if (span.children.length === 0 || span.point) return dur;
|
|
269
|
+
const intervals = span.children
|
|
270
|
+
.filter(c => !c.point && c.end !== undefined)
|
|
271
|
+
.map(c => [c.start, c.end as number] as const)
|
|
272
|
+
.sort((a, b) => a[0] - b[0]);
|
|
273
|
+
if (intervals.length === 0) return dur;
|
|
274
|
+
let union = 0;
|
|
275
|
+
let curStart = intervals[0][0];
|
|
276
|
+
let curEnd = intervals[0][1];
|
|
277
|
+
for (let i = 1; i < intervals.length; i++) {
|
|
278
|
+
const [s, e] = intervals[i];
|
|
279
|
+
if (s > curEnd) {
|
|
280
|
+
union += curEnd - curStart;
|
|
281
|
+
curStart = s;
|
|
282
|
+
curEnd = e;
|
|
283
|
+
} else if (e > curEnd) {
|
|
284
|
+
curEnd = e;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
union += curEnd - curStart;
|
|
288
|
+
return Math.max(0, dur - union);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function fmtMs(ms: number): string {
|
|
292
|
+
if (ms < 1) return `${ms.toFixed(2)}ms`;
|
|
293
|
+
if (ms < 100) return `${ms.toFixed(1)}ms`;
|
|
294
|
+
return `${ms.toFixed(0)}ms`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const MODULE_LOAD_PREFIX = "load:";
|
|
298
|
+
const MODULE_LOAD_VERBOSE_TOP = 10;
|
|
299
|
+
|
|
300
|
+
function isModuleLoadSpan(span: Span): boolean {
|
|
301
|
+
return span.op.startsWith(MODULE_LOAD_PREFIX);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function printSpan(span: Span, depth: number, lines: string[]): void {
|
|
305
|
+
const indent = " ".repeat(depth);
|
|
306
|
+
if (span.point) {
|
|
307
|
+
lines.push(`${indent}• ${span.op}`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const dur = durationOf(span);
|
|
311
|
+
if (dur < LOGGED_TIMING_THRESHOLD_MS && span.children.length === 0) return;
|
|
312
|
+
const parallel = isParallel(span);
|
|
313
|
+
const tag = parallel ? " [parallel]" : "";
|
|
314
|
+
const self = selfTimeOf(span);
|
|
315
|
+
const selfStr = span.children.length > 0 && self > LOGGED_TIMING_THRESHOLD_MS ? ` (self ${fmtMs(self)})` : "";
|
|
316
|
+
lines.push(`${indent}${span.op}: ${fmtMs(dur)}${selfStr}${tag}`);
|
|
317
|
+
|
|
318
|
+
// Split children into work spans and module-load spans for summarization.
|
|
319
|
+
const work: Span[] = [];
|
|
320
|
+
const loads: Span[] = [];
|
|
321
|
+
for (const child of span.children) {
|
|
322
|
+
if (isModuleLoadSpan(child)) loads.push(child);
|
|
323
|
+
else work.push(child);
|
|
324
|
+
}
|
|
325
|
+
for (const child of work.sort((a, b) => a.start - b.start)) {
|
|
326
|
+
printSpan(child, depth + 1, lines);
|
|
327
|
+
}
|
|
328
|
+
if (loads.length > 0) {
|
|
329
|
+
printModuleLoadSummary(loads, depth + 1, lines);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Collapse the (typically hundreds of) module-load spans into one summary line. */
|
|
334
|
+
function printModuleLoadSummary(loads: Span[], depth: number, lines: string[]): void {
|
|
335
|
+
const childIndent = " ".repeat(depth);
|
|
336
|
+
const grandIndent = " ".repeat(depth + 1);
|
|
337
|
+
let unionStart = Number.POSITIVE_INFINITY;
|
|
338
|
+
let unionEnd = 0;
|
|
339
|
+
let totalSelf = 0;
|
|
340
|
+
for (const span of loads) {
|
|
341
|
+
if (span.end === undefined) continue;
|
|
342
|
+
if (span.start < unionStart) unionStart = span.start;
|
|
343
|
+
if (span.end > unionEnd) unionEnd = span.end;
|
|
344
|
+
totalSelf += span.end - span.start;
|
|
345
|
+
}
|
|
346
|
+
const wall = unionEnd > unionStart ? unionEnd - unionStart : 0;
|
|
347
|
+
lines.push(`${childIndent}(modules): ${loads.length} loaded, wall ${fmtMs(wall)}, sum ${fmtMs(totalSelf)}`);
|
|
348
|
+
const showAll = process.env.PROMETHEUS_TIMING === "full";
|
|
349
|
+
const sorted = [...loads].sort((a, b) => durationOf(b) - durationOf(a));
|
|
350
|
+
const visible = showAll ? sorted : sorted.slice(0, MODULE_LOAD_VERBOSE_TOP);
|
|
351
|
+
for (const span of visible) {
|
|
352
|
+
const dur = durationOf(span);
|
|
353
|
+
if (dur < LOGGED_TIMING_THRESHOLD_MS) break;
|
|
354
|
+
const tag = isParallel(span) ? " [parallel]" : "";
|
|
355
|
+
lines.push(`${grandIndent}${span.op}: ${fmtMs(dur)}${tag}`);
|
|
356
|
+
}
|
|
357
|
+
if (!showAll && sorted.length > MODULE_LOAD_VERBOSE_TOP) {
|
|
358
|
+
lines.push(
|
|
359
|
+
`${grandIndent}… ${sorted.length - MODULE_LOAD_VERBOSE_TOP} more (PROMETHEUS_TIMING=full to show all)`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** A span is parallel if it overlaps a sibling that started before it. */
|
|
365
|
+
function isParallel(span: Span): boolean {
|
|
366
|
+
const parent = span.parent;
|
|
367
|
+
if (!parent || span.end === undefined) return false;
|
|
368
|
+
for (const sibling of parent.children) {
|
|
369
|
+
if (sibling === span || sibling.end === undefined || sibling.point) continue;
|
|
370
|
+
// Overlap test: A overlaps B iff A.start < B.end && B.start < A.end
|
|
371
|
+
if (sibling.start < span.end && span.start < sibling.end) return true;
|
|
372
|
+
}
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Time a span. Three forms:
|
|
378
|
+
* time(op) — point event (zero-duration breadcrumb)
|
|
379
|
+
* time(op, fn, ...args) — wrap fn in a span; returns fn's return value (sync or Promise)
|
|
380
|
+
*
|
|
381
|
+
* Spans nest hierarchically via AsyncLocalStorage: a child started inside another span's fn
|
|
382
|
+
* (even across awaits) becomes that span's child. Parallel children are recorded as siblings
|
|
383
|
+
* with overlapping intervals.
|
|
384
|
+
*/
|
|
385
|
+
export function time(op: string): void;
|
|
386
|
+
export function time<T, A extends unknown[]>(op: string, fn: (...args: A) => T, ...args: A): T;
|
|
387
|
+
export function time<T, A extends unknown[]>(op: string, fn?: (...args: A) => T, ...args: A): T | undefined {
|
|
388
|
+
if (!gRecordTimings || !gRootSpan) {
|
|
389
|
+
if (fn === undefined) return undefined as T;
|
|
390
|
+
return fn(...args);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const parent = spanStorage.getStore() ?? gRootSpan;
|
|
394
|
+
const span: Span = { op, start: performance.now(), parent, children: [] };
|
|
395
|
+
parent.children.push(span);
|
|
396
|
+
|
|
397
|
+
if (fn === undefined) {
|
|
398
|
+
span.end = span.start;
|
|
399
|
+
span.point = true;
|
|
400
|
+
return undefined as T;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const finish = (): void => {
|
|
404
|
+
span.end = performance.now();
|
|
405
|
+
};
|
|
406
|
+
try {
|
|
407
|
+
const result = spanStorage.run(span, () => fn(...args));
|
|
408
|
+
if (isPromise(result)) {
|
|
409
|
+
return result.finally(finish) as T;
|
|
410
|
+
}
|
|
411
|
+
finish();
|
|
412
|
+
return result;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
finish();
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type AsciiRenderOptions, renderMermaidASCII } from "beautiful-mermaid";
|
|
2
|
+
|
|
3
|
+
export type { AsciiRenderOptions as MermaidAsciiRenderOptions };
|
|
4
|
+
|
|
5
|
+
export function renderMermaidAscii(source: string, options?: AsciiRenderOptions): string {
|
|
6
|
+
return renderMermaidASCII(source, options);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function renderMermaidAsciiSafe(source: string, options?: AsciiRenderOptions): string | null {
|
|
10
|
+
try {
|
|
11
|
+
return renderMermaidASCII(source, options);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract mermaid code blocks from markdown text.
|
|
19
|
+
*/
|
|
20
|
+
export function extractMermaidBlocks(markdown: string): { source: string; hash: bigint | number }[] {
|
|
21
|
+
const blocks: { source: string; hash: bigint | number }[] = [];
|
|
22
|
+
const regex = /```mermaid\s*\n([\s\S]*?)```/g;
|
|
23
|
+
|
|
24
|
+
for (let match = regex.exec(markdown); match !== null; match = regex.exec(markdown)) {
|
|
25
|
+
const source = match[1].trim();
|
|
26
|
+
const hash = Bun.hash(source);
|
|
27
|
+
blocks.push({ source, hash });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return blocks;
|
|
31
|
+
}
|
package/src/mime.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { peekFile, peekFileSync } from "./peek-file";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_IMAGE_METADATA_HEADER_BYTES = 256 * 1024;
|
|
4
|
+
|
|
5
|
+
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
6
|
+
const JPEG_MAGIC = Buffer.from([0xff, 0xd8, 0xff]);
|
|
7
|
+
const WEBP_RIFF_MAGIC = Buffer.from([0x52, 0x49, 0x46, 0x46]);
|
|
8
|
+
const WEBP_MAGIC = Buffer.from([0x57, 0x45, 0x42, 0x50]);
|
|
9
|
+
const PNG_IHDR = Buffer.from("IHDR");
|
|
10
|
+
const GIF87A = Buffer.from("GIF87a");
|
|
11
|
+
const GIF89A = Buffer.from("GIF89a");
|
|
12
|
+
const WEBP_VP8X = Buffer.from("VP8X");
|
|
13
|
+
const WEBP_VP8L = Buffer.from("VP8L");
|
|
14
|
+
const WEBP_VP8 = Buffer.from("VP8 ");
|
|
15
|
+
|
|
16
|
+
export const SUPPORTED_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
17
|
+
|
|
18
|
+
export type ImageMetadata =
|
|
19
|
+
| { mimeType: "image/png"; width?: number; height?: number; channels?: number; hasAlpha?: boolean }
|
|
20
|
+
| { mimeType: "image/jpeg"; width?: number; height?: number; channels?: number; hasAlpha?: false }
|
|
21
|
+
| { mimeType: "image/gif"; width?: number; height?: number; channels?: 3; hasAlpha?: never }
|
|
22
|
+
| { mimeType: "image/webp"; width?: number; height?: number; channels?: number; hasAlpha?: boolean };
|
|
23
|
+
|
|
24
|
+
function magicEquals(header: Uint8Array, offset: number, magic: Buffer): boolean {
|
|
25
|
+
if (header.length < offset + magic.length) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return magic.equals(header.subarray(offset, offset + magic.length));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parsePngMetadata(header: Uint8Array): ImageMetadata | null {
|
|
32
|
+
if (!magicEquals(header, 0, PNG_MAGIC)) return null;
|
|
33
|
+
if (!magicEquals(header, 12, PNG_IHDR)) return { mimeType: "image/png" };
|
|
34
|
+
if (header.length < 26) return { mimeType: "image/png" };
|
|
35
|
+
|
|
36
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
|
|
37
|
+
const width = view.getUint32(16, false);
|
|
38
|
+
const height = view.getUint32(20, false);
|
|
39
|
+
const colorType = view.getUint8(25);
|
|
40
|
+
if (colorType === 0) return { mimeType: "image/png", width, height, channels: 1, hasAlpha: false };
|
|
41
|
+
if (colorType === 2) return { mimeType: "image/png", width, height, channels: 3, hasAlpha: false };
|
|
42
|
+
if (colorType === 3) return { mimeType: "image/png", width, height, channels: 3 };
|
|
43
|
+
if (colorType === 4) return { mimeType: "image/png", width, height, channels: 2, hasAlpha: true };
|
|
44
|
+
if (colorType === 6) return { mimeType: "image/png", width, height, channels: 4, hasAlpha: true };
|
|
45
|
+
return { mimeType: "image/png", width, height };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseJpegMetadata(header: Uint8Array): ImageMetadata | null {
|
|
49
|
+
if (!magicEquals(header, 0, JPEG_MAGIC)) return null;
|
|
50
|
+
if (header.length < 4) return { mimeType: "image/jpeg" };
|
|
51
|
+
|
|
52
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
|
|
53
|
+
let offset = 2;
|
|
54
|
+
while (offset + 9 < header.length) {
|
|
55
|
+
if (header[offset] !== 0xff) {
|
|
56
|
+
offset += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let markerOffset = offset + 1;
|
|
61
|
+
while (markerOffset < header.length && header[markerOffset] === 0xff) {
|
|
62
|
+
markerOffset += 1;
|
|
63
|
+
}
|
|
64
|
+
if (markerOffset >= header.length) break;
|
|
65
|
+
|
|
66
|
+
const marker = header[markerOffset];
|
|
67
|
+
const segmentOffset = markerOffset + 1;
|
|
68
|
+
if (marker === 0xd8 || marker === 0xd9 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
69
|
+
offset = segmentOffset;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (segmentOffset + 1 >= header.length) break;
|
|
73
|
+
|
|
74
|
+
const segmentLength = view.getUint16(segmentOffset, false);
|
|
75
|
+
if (segmentLength < 2) break;
|
|
76
|
+
|
|
77
|
+
const isStartOfFrame = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
|
|
78
|
+
if (isStartOfFrame) {
|
|
79
|
+
if (segmentOffset + 7 >= header.length) break;
|
|
80
|
+
const height = view.getUint16(segmentOffset + 3, false);
|
|
81
|
+
const width = view.getUint16(segmentOffset + 5, false);
|
|
82
|
+
const channels = header[segmentOffset + 7];
|
|
83
|
+
return {
|
|
84
|
+
mimeType: "image/jpeg",
|
|
85
|
+
width,
|
|
86
|
+
height,
|
|
87
|
+
channels: Number.isFinite(channels) ? channels : undefined,
|
|
88
|
+
hasAlpha: false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
offset = segmentOffset + segmentLength;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { mimeType: "image/jpeg" };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseGifMetadata(header: Uint8Array): ImageMetadata | null {
|
|
99
|
+
if (!magicEquals(header, 0, GIF87A) && !magicEquals(header, 0, GIF89A)) return null;
|
|
100
|
+
if (header.length < 10) return { mimeType: "image/gif" };
|
|
101
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
|
|
102
|
+
return {
|
|
103
|
+
mimeType: "image/gif",
|
|
104
|
+
width: view.getUint16(6, true),
|
|
105
|
+
height: view.getUint16(8, true),
|
|
106
|
+
channels: 3,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseWebpMetadata(header: Uint8Array): ImageMetadata | null {
|
|
111
|
+
if (!magicEquals(header, 0, WEBP_RIFF_MAGIC)) return null;
|
|
112
|
+
if (!magicEquals(header, 8, WEBP_MAGIC)) return null;
|
|
113
|
+
if (header.length < 30) return { mimeType: "image/webp" };
|
|
114
|
+
|
|
115
|
+
if (magicEquals(header, 12, WEBP_VP8X)) {
|
|
116
|
+
const hasAlpha = (header[20] & 0x10) !== 0;
|
|
117
|
+
const width = (header[24] | (header[25] << 8) | (header[26] << 16)) + 1;
|
|
118
|
+
const height = (header[27] | (header[28] << 8) | (header[29] << 16)) + 1;
|
|
119
|
+
return { mimeType: "image/webp", width, height, channels: hasAlpha ? 4 : 3, hasAlpha };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
|
|
123
|
+
if (magicEquals(header, 12, WEBP_VP8L)) {
|
|
124
|
+
if (header.length < 25) return { mimeType: "image/webp" };
|
|
125
|
+
const bits = view.getUint32(21, true);
|
|
126
|
+
const width = (bits & 0x3fff) + 1;
|
|
127
|
+
const height = ((bits >> 14) & 0x3fff) + 1;
|
|
128
|
+
const hasAlpha = ((bits >> 28) & 0x1) === 1;
|
|
129
|
+
return { mimeType: "image/webp", width, height, channels: hasAlpha ? 4 : 3, hasAlpha };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (magicEquals(header, 12, WEBP_VP8)) {
|
|
133
|
+
const width = view.getUint16(26, true) & 0x3fff;
|
|
134
|
+
const height = view.getUint16(28, true) & 0x3fff;
|
|
135
|
+
return { mimeType: "image/webp", width, height, channels: 3, hasAlpha: false };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { mimeType: "image/webp" };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function parseImageMetadata(header: Uint8Array): ImageMetadata | null {
|
|
142
|
+
return (
|
|
143
|
+
parsePngMetadata(header) ?? parseJpegMetadata(header) ?? parseGifMetadata(header) ?? parseWebpMetadata(header)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function readImageMetadataSync(
|
|
148
|
+
filePath: string,
|
|
149
|
+
maxBytes = DEFAULT_IMAGE_METADATA_HEADER_BYTES,
|
|
150
|
+
): ImageMetadata | null {
|
|
151
|
+
return peekFileSync(filePath, maxBytes, parseImageMetadata);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function readImageMetadata(
|
|
155
|
+
filePath: string,
|
|
156
|
+
maxBytes = DEFAULT_IMAGE_METADATA_HEADER_BYTES,
|
|
157
|
+
): Promise<ImageMetadata | null> {
|
|
158
|
+
return peekFile(filePath, maxBytes, parseImageMetadata);
|
|
159
|
+
}
|