@oh-my-pi/pi-utils 18.13.2 → 18.14.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/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/dist/types/abortable.d.ts +2 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/logger.d.ts +14 -0
- package/package.json +3 -4
- package/src/abortable.ts +6 -2
- package/src/index.ts +0 -1
- package/src/logger/rotating-file.ts +1 -2
- package/src/logger.ts +47 -6
- package/THIRD-PARTY-NOTICES.txt +0 -22901
- package/dist/types/fs-open.d.ts +0 -17
- package/src/fs-open.ts +0 -32
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.14.0] - 2026-10-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `logger.withContext(fields, fn)` to attach contextual fields to all records produced during a function and its asynchronous work, plus `logger.withoutContext(fn)` for work that should not inherit caller context.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed `untilAborted` so promises passed to an already-aborted signal are still observed, preventing later rejections from being reported as unhandled.
|
|
14
|
+
|
|
15
|
+
## [18.13.3] - 2026-10-28
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Updated Bun engine requirement to ≥1.4.4
|
|
20
|
+
|
|
5
21
|
## [18.13.2] - 2026-10-27
|
|
6
22
|
|
|
7
23
|
### Added
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Import from the root barrel or per-module subpaths (`@oh-my-pi/pi-utils/<module>
|
|
|
29
29
|
bun add @oh-my-pi/pi-utils
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Ships TypeScript source directly (no build step); requires Bun ≥ 1.
|
|
32
|
+
Ships TypeScript source directly (no build step); requires Bun ≥ 1.4.4.
|
|
33
33
|
|
|
34
34
|
## References
|
|
35
35
|
|
|
@@ -16,7 +16,8 @@ export declare class AbortError extends Error {
|
|
|
16
16
|
export declare function abortableSource<T>(stream: ReadableStream<T>, signal?: AbortSignal): AsyncGenerator<T>;
|
|
17
17
|
/**
|
|
18
18
|
* Runs a promise-returning function (`pr`). If the given AbortSignal is aborted before or during
|
|
19
|
-
* execution, the promise is rejected with a standard error.
|
|
19
|
+
* execution, the promise is rejected with a standard error. A promise passed directly stays
|
|
20
|
+
* observed, so its later rejection is never unhandled.
|
|
20
21
|
*
|
|
21
22
|
* @param signal - Optional AbortSignal to cancel the operation
|
|
22
23
|
* @param pr - Function returning a promise to run
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export * from "./file-lock.js";
|
|
|
10
10
|
export * from "./format.js";
|
|
11
11
|
export * from "./frontmatter.js";
|
|
12
12
|
export * from "./fs-error.js";
|
|
13
|
-
export * from "./fs-open.js";
|
|
14
13
|
export * from "./incoming-json.js";
|
|
15
14
|
export * from "./json.js";
|
|
16
15
|
export * from "./json-parse.js";
|
package/dist/types/logger.d.ts
CHANGED
|
@@ -11,6 +11,20 @@ export interface LogEvent {
|
|
|
11
11
|
export type LogSink = (event: LogEvent) => void;
|
|
12
12
|
/** Register an out-of-band log sink and return a disposer. */
|
|
13
13
|
export declare function registerLogSink(sink: LogSink): () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Run `fn` with `context`'s fields on every record logged while it runs,
|
|
16
|
+
* including from the promises, timers and callbacks it starts. A nested call
|
|
17
|
+
* replaces the enclosing context, as a subagent's session replaces its
|
|
18
|
+
* parent's; a field passed to the log call itself wins. Fields are read as each
|
|
19
|
+
* record is written, so a getter stays current.
|
|
20
|
+
*/
|
|
21
|
+
export declare function withContext<T>(context: Readonly<Record<string, unknown>>, fn: () => T): T;
|
|
22
|
+
/**
|
|
23
|
+
* Run `fn` outside any {@link withContext}: for process-wide work started on
|
|
24
|
+
* behalf of one caller, such as a shared server's read loop, whose later
|
|
25
|
+
* records belong to no single caller.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withoutContext<T>(fn: () => T): T;
|
|
14
28
|
/**
|
|
15
29
|
* Write buffered file records to disk now. The file transport batches records
|
|
16
30
|
* (one write per second or per 64 KiB) and writes `warn`/`error` records
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.14.0",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"README.md",
|
|
28
28
|
"CHANGELOG.md",
|
|
29
29
|
"LICENSE",
|
|
30
|
-
"THIRD-PARTY-NOTICES.txt",
|
|
31
30
|
"dist/types"
|
|
32
31
|
],
|
|
33
32
|
"type": "module",
|
|
@@ -57,12 +56,12 @@
|
|
|
57
56
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
58
57
|
},
|
|
59
58
|
"dependencies": {
|
|
60
|
-
"@oh-my-pi/pi-natives": "18.
|
|
59
|
+
"@oh-my-pi/pi-natives": "18.14.0"
|
|
61
60
|
},
|
|
62
61
|
"devDependencies": {
|
|
63
62
|
"@types/bun": "^1.3.14"
|
|
64
63
|
},
|
|
65
64
|
"engines": {
|
|
66
|
-
"bun": ">=1.
|
|
65
|
+
"bun": ">=1.4.4"
|
|
67
66
|
}
|
|
68
67
|
}
|
package/src/abortable.ts
CHANGED
|
@@ -60,7 +60,8 @@ export async function* abortableSource<T>(stream: ReadableStream<T>, signal?: Ab
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Runs a promise-returning function (`pr`). If the given AbortSignal is aborted before or during
|
|
63
|
-
* execution, the promise is rejected with a standard error.
|
|
63
|
+
* execution, the promise is rejected with a standard error. A promise passed directly stays
|
|
64
|
+
* observed, so its later rejection is never unhandled.
|
|
64
65
|
*
|
|
65
66
|
* @param signal - Optional AbortSignal to cancel the operation
|
|
66
67
|
* @param pr - Function returning a promise to run
|
|
@@ -71,7 +72,10 @@ export function untilAborted<T>(
|
|
|
71
72
|
pr: Promise<T> | (() => Promise<T>),
|
|
72
73
|
): Promise<T> {
|
|
73
74
|
if (!signal) return typeof pr === "function" ? pr() : pr;
|
|
74
|
-
if (signal.aborted)
|
|
75
|
+
if (signal.aborted) {
|
|
76
|
+
if (typeof pr !== "function") void pr.catch(() => {});
|
|
77
|
+
return Promise.reject(new AbortError(signal));
|
|
78
|
+
}
|
|
75
79
|
|
|
76
80
|
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
77
81
|
const onAbort = () => reject(new AbortError(signal));
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,6 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { localDay } from "../dirs";
|
|
6
|
-
import { openCloexecSync } from "../fs-open";
|
|
7
6
|
|
|
8
7
|
/** Write batching for {@link RotatingFileSink}. */
|
|
9
8
|
export interface RotatingFileBatchOptions {
|
|
@@ -87,7 +86,7 @@ export class RotatingFileSink {
|
|
|
87
86
|
|
|
88
87
|
#openFd(filePath: string): void {
|
|
89
88
|
this.#closeFd();
|
|
90
|
-
this.#fd =
|
|
89
|
+
this.#fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
|
|
91
90
|
try {
|
|
92
91
|
this.#onRotate?.(filePath);
|
|
93
92
|
} catch {
|
package/src/logger.ts
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* transport so a process supervisor (pm2, journald, k8s) captures the logs.
|
|
8
8
|
*
|
|
9
9
|
* Each entry includes `process.pid` so concurrent omp instances stay
|
|
10
|
-
* traceable
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* traceable, and the fields of the enclosing {@link withContext} (a session's
|
|
11
|
+
* `sessionId`) so sessions sharing one process stay apart. The file is created
|
|
12
|
+
* on the first record written. Records are batched — one write per second or
|
|
13
|
+
* per 64 KiB — while `warn`/`error` records are written at once together with
|
|
14
|
+
* everything buffered before them; exit, fatal, and signal paths flush the
|
|
15
|
+
* rest. `OMP_LOG_LEVEL` optionally limits the levels written to the file
|
|
16
|
+
* (default: all).
|
|
15
17
|
*/
|
|
16
18
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
19
|
import * as fs from "node:fs";
|
|
@@ -48,7 +50,18 @@ export function registerLogSink(sink: LogSink): () => void {
|
|
|
48
50
|
|
|
49
51
|
function emitToSinks(level: LogLevel, message: string, context: Record<string, unknown> | undefined): void {
|
|
50
52
|
if (logSinks.size === 0) return;
|
|
51
|
-
|
|
53
|
+
// The enclosing {@link withContext} fields reach sinks as they reach the file;
|
|
54
|
+
// call-site fields win, and a record outside any context keeps its own object.
|
|
55
|
+
const scoped = logContextStorage.getStore();
|
|
56
|
+
let merged = context;
|
|
57
|
+
if (scoped) {
|
|
58
|
+
try {
|
|
59
|
+
merged = { ...scoped, ...context };
|
|
60
|
+
} catch {
|
|
61
|
+
// A throwing getter must not turn a log call into a failure; sinks get the call's own fields.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const event: LogEvent = { level, message, context: merged, timestamp: new Date() };
|
|
52
65
|
for (const sink of logSinks) {
|
|
53
66
|
try {
|
|
54
67
|
sink(event);
|
|
@@ -251,6 +264,28 @@ function formatLocalTimestamp(date: Date): string {
|
|
|
251
264
|
|
|
252
265
|
const FORMAT_TOKEN_PATTERN = /%[scdjifoO%]/;
|
|
253
266
|
|
|
267
|
+
const logContextStorage = new AsyncLocalStorage<Readonly<Record<string, unknown>>>();
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Run `fn` with `context`'s fields on every record logged while it runs,
|
|
271
|
+
* including from the promises, timers and callbacks it starts. A nested call
|
|
272
|
+
* replaces the enclosing context, as a subagent's session replaces its
|
|
273
|
+
* parent's; a field passed to the log call itself wins. Fields are read as each
|
|
274
|
+
* record is written, so a getter stays current.
|
|
275
|
+
*/
|
|
276
|
+
export function withContext<T>(context: Readonly<Record<string, unknown>>, fn: () => T): T {
|
|
277
|
+
return logContextStorage.run(context, fn);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Run `fn` outside any {@link withContext}: for process-wide work started on
|
|
282
|
+
* behalf of one caller, such as a shared server's read loop, whose later
|
|
283
|
+
* records belong to no single caller.
|
|
284
|
+
*/
|
|
285
|
+
export function withoutContext<T>(fn: () => T): T {
|
|
286
|
+
return logContextStorage.exit(fn);
|
|
287
|
+
}
|
|
288
|
+
|
|
254
289
|
/** Serialize one log record: timestamp, level, pid, message, then context fields. */
|
|
255
290
|
function formatLogRecord(
|
|
256
291
|
level: LogLevel,
|
|
@@ -266,6 +301,12 @@ function formatLogRecord(
|
|
|
266
301
|
pid: process.pid,
|
|
267
302
|
message: metadata?.message ? `${message} ${metadata.message}` : message,
|
|
268
303
|
};
|
|
304
|
+
const scoped = logContextStorage.getStore();
|
|
305
|
+
if (scoped) {
|
|
306
|
+
for (const key of Object.keys(scoped)) {
|
|
307
|
+
if (key !== "level" && key !== "timestamp" && key !== "message") entry[key] = scoped[key];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
269
310
|
if (metadata) {
|
|
270
311
|
for (const key of Object.keys(metadata)) {
|
|
271
312
|
if (key !== "level" && key !== "timestamp" && key !== "message") entry[key] = metadata[key];
|