@linxiraos/pi-utils 1.4.6 → 1.4.7
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 +11 -1
- 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/dist/types/ports.d.ts +4 -2
- package/package.json +4 -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/src/ports.ts +4 -2
- package/dist/types/fs-open.d.ts +0 -17
- package/src/fs-open.ts +0 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.4.7] - 2026-10-30
|
|
4
|
+
|
|
5
|
+
- Synced with upstream OMP v18.14.0; no package-specific user-visible changes.
|
|
6
|
+
|
|
3
7
|
## [1.4.6] - 2026-10-28
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed `untilAborted` so promises passed to an already-aborted signal are still observed, preventing later rejections from being reported as unhandled.
|
|
6
16
|
|
|
7
17
|
## [1.4.5] - 2026-10-28
|
|
8
18
|
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Import from the root barrel or per-module subpaths (`@linxiraos/pi-utils/<module
|
|
|
29
29
|
bun add @linxiraos/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/dist/types/ports.d.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* OMP installs on the same machine use 30141/30142/3847, and common developer
|
|
5
5
|
* tools squat on 3000/8080/8787/3847. Zeta therefore owns a dedicated block:
|
|
6
|
-
*
|
|
7
|
-
* unrelated services cannot take the gateway
|
|
6
|
+
* uncommon ports, at least two apart, so a side-by-side OMP install and
|
|
7
|
+
* unrelated services cannot take the gateway, stats dashboard, or browser
|
|
8
|
+
* relay away.
|
|
8
9
|
*
|
|
9
10
|
* Every default in the repository derives from these constants. The standalone
|
|
10
11
|
* Electron shell (`desktop/src/main.ts`, not a Bun workspace package) carries
|
|
@@ -13,3 +14,4 @@
|
|
|
13
14
|
export declare const ZETA_WEB_PORT = 43141;
|
|
14
15
|
export declare const ZETA_GATEWAY_PORT = 43143;
|
|
15
16
|
export declare const ZETA_STATS_PORT = 43147;
|
|
17
|
+
export declare const ZETA_BROWSER_RELAY_PORT = 43153;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxiraos/pi-utils",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.7",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"utilities",
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@linxiraos/pi-natives": "1.4.
|
|
57
|
+
"@linxiraos/pi-natives": "1.4.7"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@types/bun": "1.4.
|
|
60
|
+
"@types/bun": "1.4.4"
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|
|
63
|
-
"bun": ">=1.
|
|
63
|
+
"bun": ">=1.4.4"
|
|
64
64
|
}
|
|
65
65
|
}
|
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 zeta 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. `ZETA_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];
|
package/src/ports.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* OMP installs on the same machine use 30141/30142/3847, and common developer
|
|
5
5
|
* tools squat on 3000/8080/8787/3847. Zeta therefore owns a dedicated block:
|
|
6
|
-
*
|
|
7
|
-
* unrelated services cannot take the gateway
|
|
6
|
+
* uncommon ports, at least two apart, so a side-by-side OMP install and
|
|
7
|
+
* unrelated services cannot take the gateway, stats dashboard, or browser
|
|
8
|
+
* relay away.
|
|
8
9
|
*
|
|
9
10
|
* Every default in the repository derives from these constants. The standalone
|
|
10
11
|
* Electron shell (`desktop/src/main.ts`, not a Bun workspace package) carries
|
|
@@ -13,3 +14,4 @@
|
|
|
13
14
|
export const ZETA_WEB_PORT = 43141;
|
|
14
15
|
export const ZETA_GATEWAY_PORT = 43143;
|
|
15
16
|
export const ZETA_STATS_PORT = 43147;
|
|
17
|
+
export const ZETA_BROWSER_RELAY_PORT = 43153;
|
package/dist/types/fs-open.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `fs.openSync` with close-on-exec, matching Node's default.
|
|
3
|
-
*
|
|
4
|
-
* libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
|
|
5
|
-
* dies at `exec`. Bun's `fs.open*` did not before 1.4.3 (still allowed by the
|
|
6
|
-
* `bun@>=1.4` pin), and descriptors it returns survive
|
|
7
|
-
* into any child spawned through a path that does not close strays itself — the
|
|
8
|
-
* bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
|
|
9
|
-
* this for every descriptor kept open past such a spawn (session transcripts,
|
|
10
|
-
* logs, spools); a child must never hold a writable handle to them (#13224).
|
|
11
|
-
*
|
|
12
|
-
* Flags are numeric only: the bit cannot be OR-ed into `"a"`/`"w+"` strings.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* const fd = openCloexecSync(logPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
|
|
16
|
-
*/
|
|
17
|
-
export declare function openCloexecSync(filePath: string, flags: number, mode?: number): number;
|
package/src/fs-open.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/** Close-on-exec file opens for descriptors held past a child spawn. */
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* `O_CLOEXEC`, which neither Node nor Bun exposes in `fs.constants`.
|
|
6
|
-
*
|
|
7
|
-
* Kernel ABI values: `0o2000000` on Linux, `0x0100_0000` on Darwin. Windows has
|
|
8
|
-
* no `exec` and its CRT handles are inherited only when a spawn explicitly asks
|
|
9
|
-
* for it, so the bit is 0 there — as it is on any other platform, where adding
|
|
10
|
-
* an unknown flag bit would be worse than leaving the descriptor inheritable.
|
|
11
|
-
*/
|
|
12
|
-
const O_CLOEXEC = process.platform === "linux" ? 0o2000000 : process.platform === "darwin" ? 0x0100_0000 : 0;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* `fs.openSync` with close-on-exec, matching Node's default.
|
|
16
|
-
*
|
|
17
|
-
* libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
|
|
18
|
-
* dies at `exec`. Bun's `fs.open*` did not before 1.4.3 (still allowed by the
|
|
19
|
-
* `bun@>=1.4` pin), and descriptors it returns survive
|
|
20
|
-
* into any child spawned through a path that does not close strays itself — the
|
|
21
|
-
* bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
|
|
22
|
-
* this for every descriptor kept open past such a spawn (session transcripts,
|
|
23
|
-
* logs, spools); a child must never hold a writable handle to them (#13224).
|
|
24
|
-
*
|
|
25
|
-
* Flags are numeric only: the bit cannot be OR-ed into `"a"`/`"w+"` strings.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* const fd = openCloexecSync(logPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
|
|
29
|
-
*/
|
|
30
|
-
export function openCloexecSync(filePath: string, flags: number, mode?: number): number {
|
|
31
|
-
return fs.openSync(filePath, flags | O_CLOEXEC, mode);
|
|
32
|
-
}
|