@oh-my-pi/pi-utils 15.12.3 → 15.13.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 +30 -60
- package/dist/types/abortable.d.ts +5 -0
- package/dist/types/fetch-retry.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/loop-phase.d.ts +10 -0
- package/dist/types/runtime-install.d.ts +3 -0
- package/package.json +2 -2
- package/src/abortable.ts +86 -1
- package/src/fetch-retry.ts +8 -0
- package/src/index.ts +1 -0
- package/src/loop-phase.ts +49 -0
- package/src/runtime-install.ts +4 -1
- package/src/temp.ts +56 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,47 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [15.12.0] - 2026-06-12
|
|
6
|
-
|
|
7
5
|
### Added
|
|
8
6
|
|
|
7
|
+
- Added support for a runtime `overrides` map in `RuntimeInstallSpec`, which is now written into generated runtime `package.json` manifests to force dependency pins (including transitive ones) across the runtime tree
|
|
8
|
+
- Added a lightweight loop-phase breadcrumb stack (`pushLoopPhase`/`popLoopPhase`/`currentLoopPhase`, plus `takeRecentLoopPhase` which returns the live phase or the most recently popped one and clears it) so the TUI event-loop watchdog can attribute a main-thread block to the phase that caused it — including a synchronous phase already popped before the watchdog's delayed tick runs ([#2485](https://github.com/can1357/oh-my-pi/issues/2485))
|
|
9
|
+
- Added `FetchWithRetryOptions.timeout` (forwarded to the underlying `fetch` call). `false` disables Bun's native ~300s pre-response timeout; a positive number overrides the ceiling. Bare browser/Node fetch ignores it ([#2422](https://github.com/can1357/oh-my-pi/issues/2422))
|
|
9
10
|
- Added `runtime-install`: shared on-demand runtime dependency support — `ensureRuntimeInstalled()` (locked, idempotent `bun install` of a pinned dependency set into a cache dir) and a multi-root `installRuntimeModuleResolver()`/`resolveRuntimeModule()` for loading those graphs inside compiled binaries (Bun #1763). Extracted from the coding-agent tiny-model worker; now also backs Mnemopi's on-demand fastembed runtime ([#2389](https://github.com/can1357/oh-my-pi/issues/2389))
|
|
10
11
|
- Added `getFastembedRuntimeDir()` (~/.omp/cache/fastembed-runtime) alongside `getFastembedCacheDir()`
|
|
11
|
-
|
|
12
|
-
## [15.11.4] - 2026-06-12
|
|
13
|
-
|
|
14
|
-
### Added
|
|
15
|
-
|
|
16
12
|
- Added `getEditorConfigFormatting(file)`: returns the `.editorconfig`-pinned `tabSize`/`insertSpaces` (both optional, no fallback) so LSP-format callers can layer per-file defaults under it without paving over silence with the renderer's display tab width ([#2329](https://github.com/can1357/oh-my-pi/issues/2329)).
|
|
17
|
-
|
|
18
|
-
## [15.11.3] - 2026-06-11
|
|
19
|
-
|
|
20
|
-
### Added
|
|
21
|
-
|
|
22
|
-
- Added `getEditorConfigFormatting(file)`: returns the `.editorconfig`-pinned `tabSize`/`insertSpaces` (both optional, no fallback) so LSP-format callers can layer per-file defaults under it without paving over silence with the renderer's display tab width ([#2329](https://github.com/can1357/oh-my-pi/issues/2329)).
|
|
23
|
-
|
|
24
|
-
## [15.11.1] - 2026-06-11
|
|
25
|
-
|
|
26
|
-
### Fixed
|
|
27
|
-
|
|
28
|
-
- Fixed cleanup reentry noise during fatal shutdown: recursive cleanup requests now no-op idempotently instead of logging repeated `Cleanup invoked recursively` errors ([#2284](https://github.com/can1357/oh-my-pi/issues/2284)).
|
|
29
|
-
|
|
30
|
-
## [15.11.0] - 2026-06-10
|
|
31
|
-
|
|
32
|
-
### Added
|
|
33
|
-
|
|
34
13
|
- Added the `path-tree` module (`buildPathTree`, `walkPathTree`, `formatGroupedPaths`, `isUrlLikePath`), moved from the coding agent's grouped file output so compaction file lists can share the same prefix-folded directory-tree rendering; `formatGroupedPaths` gains an optional `annotate` callback for per-file suffixes
|
|
35
|
-
|
|
36
|
-
### Fixed
|
|
37
|
-
|
|
38
|
-
- Fixed the `{{join}}` prompt helper joining with a literal two-character `\n` when templates pass `"\n"` as the separator — Handlebars string literals carry no escape processing. The separator now unescapes `\n`/`\t`, matching the `{{#list}}` helper's documented convention (visible as literal `\n` between paths in compaction `<read-files>` lists).
|
|
39
|
-
|
|
40
|
-
## [15.10.11] - 2026-06-10
|
|
41
|
-
### Added
|
|
42
|
-
|
|
43
14
|
- Restored `PI_DEBUG_STARTUP` streaming startup markers: `logger.time` now writes a synchronous `[startup] <op>:start` / `:done` / `:fail` stderr line per phase (independent of `PI_TIMING`), so a startup that hangs hard still names the phase it is stuck in — the `PI_TIMING` tree only prints after startup completes and is structurally unable to diagnose a hang. The CLI runner emits `cli:load:<name>` markers around each lazily-imported command module for the same reason.
|
|
44
15
|
- Added `logger.openSpanPath()`: ops of the currently-open timing-span chain (root → deepest), used by the coding agent's startup watchdog to name the in-flight phase of a stalled startup.
|
|
45
16
|
- Added `declareWorkerHostEntry()` / `workerHostEntry()` (env): self-dispatching CLI entrypoints declare `Bun.main` as the worker host so worker spawn sites can re-enter the single entry module with `WorkerOptions.argv` selectors across source, npm-bundle, and compiled distributions
|
|
17
|
+
- Added `getAuthBrokerSnapshotCachePath()` with `OMP_AUTH_BROKER_SNAPSHOT_CACHE` override support for isolating the encrypted broker snapshot cache.
|
|
18
|
+
- Added color helpers `colorLuma` (perceptual luma), `relativeLuminance` (WCAG, linearized sRGB), and `hslToHex` to the color utilities. The luminance helpers parse `#rgb`/`#rrggbb` hex and 256-color palette indices, returning `undefined` for unparseable values.
|
|
19
|
+
- Added `peekFileEnds`, a single-open head-and-tail file peek helper that reuses the head bytes for the tail when the file fits the head window.
|
|
20
|
+
- Added `peekFileTail`, the tail mirror of `peekFile`: reads up to the last `maxBytes` of a file ending at EOF, reusing the same pooled-buffer strategy (no per-call allocation for small reads).
|
|
21
|
+
- Added `getFastembedCacheDir` to return the FastEmbed model cache directory under ~/.omp/cache/fastembed
|
|
22
|
+
- Added an XDG-aware tiny-title model cache directory helper for coding-agent local title models.
|
|
46
23
|
|
|
47
24
|
### Changed
|
|
48
25
|
|
|
@@ -50,59 +27,52 @@
|
|
|
50
27
|
- `Snowflake.formatParts` packs the id as a single 64-bit BigInt hex format instead of stitching four 16-bit segments (simpler and ~1.7x faster), and `getTimestamp` extracts via exact double arithmetic instead of a BigInt round-trip. Output is bit-identical.
|
|
51
28
|
- Logger initialization is lazy: the winston logger, file transport, and log-directory creation now happen on first log emission instead of at module import (the import previously cost ~8ms of fs work on the CLI startup path); the in-memory timing infrastructure never touches winston
|
|
52
29
|
- `prompt.format()` post-processing got cheap per-line guards and a single-pass ASCII-symbol replacement (was 7 chained regex passes per line), roughly halving render post-processing cost; output is byte-identical
|
|
30
|
+
- `logger.printTimings()` (the `PI_TIMING` startup tree) now surfaces two previously-invisible regions: a `(before instrumentation)` line for runtime init / uncaptured pre-marker work, and an `(unattributed self)` line for the root span's own untimed work so the gap between visible top-level spans and `Total` is no longer swallowed. `Total` is now labelled `(since first marker)` to make the window explicit. The restored `module-timer.ts` preload can feed module spans into the report: each module records `onLoad` → final top-level marker as `total`, a prepended body marker → final marker as `body/TLA`, and resolved static imports as a bounded dependency tree so the report separates graph wait from actual top-level module work.
|
|
53
31
|
|
|
54
32
|
### Fixed
|
|
55
33
|
|
|
34
|
+
- Made `TempDir` cleanup retry transient Windows `EBUSY`/`EPERM`/`ENOTEMPTY` removal failures so tests are less likely to fail when deleting just-used temp directories.
|
|
35
|
+
- Fixed abortable stream wrappers to cancel the source stream on abort, so timeout watchdogs release upstream HTTP bodies instead of only stopping the local reader.
|
|
36
|
+
- Fixed cleanup reentry noise during fatal shutdown: recursive cleanup requests now no-op idempotently instead of logging repeated `Cleanup invoked recursively` errors ([#2284](https://github.com/can1357/oh-my-pi/issues/2284)).
|
|
37
|
+
- Fixed the `{{join}}` prompt helper joining with a literal two-character `\n` when templates pass `"\n"` as the separator — Handlebars string literals carry no escape processing. The separator now unescapes `\n`/`\t`, matching the `{{#list}}` helper's documented convention (visible as literal `\n` between paths in compaction `<read-files>` lists).
|
|
56
38
|
- Fixed `prompt.format()` so ASCII symbol replacements such as `-->` and `!=` still run on lines containing a closing HTML comment token when not inside a comment
|
|
57
39
|
- `isCompiledBinary()` now also honors a define-folded `process.env.PI_COMPILED` (only `Bun.env` was checked), so builds that constant-fold `process.env` keep compiled-binary detection without relying on `import.meta.url` bunfs markers
|
|
58
40
|
- `omp <cmd> --help` now loads only the requested command module instead of the entire command table, so an unrelated command whose import graph hangs or crashes can no longer take down every per-command help invocation.
|
|
41
|
+
- Hardened `getIndentation` against malformed paths: any filesystem error from the `.editorconfig` probe (e.g. `ENAMETOOLONG` on oversized garbage path segments) is now swallowed and cached as a miss instead of escaping and crashing the TUI mid-render ([#1871](https://github.com/can1357/oh-my-pi/issues/1871)).
|
|
42
|
+
- Fixed `getIndentation` (and the edit renderer's `replaceTabs` callers) crashing with `ENAMETOOLONG`/`ENOTDIR`/etc. when handed a path with an overlong component or a non-directory in its parent chain. Editorconfig discovery now short-circuits to the default tab width on any path component above `NAME_MAX` (255 bytes) and absorbs any `FsError` while walking the editorconfig chain — best-effort discovery must never escape as an uncaught exception ([#1872](https://github.com/can1357/oh-my-pi/issues/1872)).
|
|
43
|
+
- Fixed `$flag` environment parsing to accept lowercase truthy values such as `y`, `true`, `yes`, and `on`
|
|
59
44
|
|
|
60
|
-
## [15.10.8] - 2026-06-09
|
|
61
45
|
### Removed
|
|
62
46
|
|
|
63
47
|
- Removed the exported `hookFetch` API, which previously intercepted `globalThis.fetch` via middleware handlers
|
|
64
48
|
- Removed `hookFetch` from the package entrypoint, so imports from `@.../utils` no longer provide this fetch interception helper
|
|
65
49
|
|
|
66
|
-
## [15.
|
|
50
|
+
## [15.13.0] - 2026-06-14
|
|
67
51
|
|
|
68
|
-
|
|
52
|
+
## [15.12.4] - 2026-06-13
|
|
69
53
|
|
|
70
|
-
|
|
54
|
+
## [15.12.0] - 2026-06-12
|
|
71
55
|
|
|
72
|
-
## [15.
|
|
56
|
+
## [15.11.4] - 2026-06-12
|
|
73
57
|
|
|
74
|
-
|
|
58
|
+
## [15.11.3] - 2026-06-11
|
|
75
59
|
|
|
76
|
-
|
|
60
|
+
## [15.11.1] - 2026-06-11
|
|
77
61
|
|
|
78
|
-
## [15.
|
|
62
|
+
## [15.11.0] - 2026-06-10
|
|
79
63
|
|
|
80
|
-
|
|
64
|
+
## [15.10.11] - 2026-06-10
|
|
81
65
|
|
|
82
|
-
|
|
83
|
-
- Fixed `getIndentation` (and the edit renderer's `replaceTabs` callers) crashing with `ENAMETOOLONG`/`ENOTDIR`/etc. when handed a path with an overlong component or a non-directory in its parent chain. Editorconfig discovery now short-circuits to the default tab width on any path component above `NAME_MAX` (255 bytes) and absorbs any `FsError` while walking the editorconfig chain — best-effort discovery must never escape as an uncaught exception ([#1872](https://github.com/can1357/oh-my-pi/issues/1872)).
|
|
66
|
+
## [15.10.8] - 2026-06-09
|
|
84
67
|
|
|
85
|
-
## [15.
|
|
68
|
+
## [15.10.0] - 2026-06-06
|
|
86
69
|
|
|
87
|
-
|
|
70
|
+
## [15.9.2] - 2026-06-05
|
|
88
71
|
|
|
89
|
-
|
|
90
|
-
- Added `peekFileEnds`, a single-open head-and-tail file peek helper that reuses the head bytes for the tail when the file fits the head window.
|
|
72
|
+
## [15.9.1] - 2026-06-04
|
|
91
73
|
|
|
92
|
-
|
|
74
|
+
## [15.9.0] - 2026-06-04
|
|
93
75
|
|
|
94
76
|
## [15.7.3] - 2026-05-31
|
|
95
77
|
|
|
96
|
-
### Added
|
|
97
|
-
|
|
98
|
-
- Added `getFastembedCacheDir` to return the FastEmbed model cache directory under ~/.omp/cache/fastembed
|
|
99
|
-
|
|
100
|
-
### Fixed
|
|
101
|
-
|
|
102
|
-
- Fixed `$flag` environment parsing to accept lowercase truthy values such as `y`, `true`, `yes`, and `on`
|
|
103
|
-
|
|
104
78
|
## [15.6.0] - 2026-05-30
|
|
105
|
-
|
|
106
|
-
### Added
|
|
107
|
-
|
|
108
|
-
- Added an XDG-aware tiny-title model cache directory helper for coding-agent local title models.
|
|
@@ -4,6 +4,11 @@ export declare class AbortError extends Error {
|
|
|
4
4
|
/**
|
|
5
5
|
* Creates an abortable stream from a given stream and signal.
|
|
6
6
|
*
|
|
7
|
+
* Unlike `stream.pipeThrough(..., { signal })`, this explicitly cancels the
|
|
8
|
+
* source reader when the signal aborts. That propagates HTTP-client disconnects
|
|
9
|
+
* and stream watchdog timeouts all the way to the backend request instead of
|
|
10
|
+
* only stopping the local consumer.
|
|
11
|
+
*
|
|
7
12
|
* @param stream - The stream to make abortable
|
|
8
13
|
* @param signal - The signal to abort the stream
|
|
9
14
|
* @returns The abortable stream
|
|
@@ -43,6 +43,14 @@ export interface FetchWithRetryOptions extends RequestInit {
|
|
|
43
43
|
* mock during tests.
|
|
44
44
|
*/
|
|
45
45
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
46
|
+
/**
|
|
47
|
+
* Bun extension forwarded verbatim to the underlying `fetch` call. `false`
|
|
48
|
+
* disables Bun's native ~300s pre-response timeout (callers that own a
|
|
49
|
+
* configurable first-event/idle watchdog or an external `AbortSignal`
|
|
50
|
+
* supply this so the runtime ceiling cannot pre-empt them); a positive
|
|
51
|
+
* number sets a custom ceiling in ms. Bare browser/Node fetch ignores it.
|
|
52
|
+
*/
|
|
53
|
+
timeout?: number | false;
|
|
46
54
|
}
|
|
47
55
|
/**
|
|
48
56
|
* Fetch with bounded retries and sensible defaults. Retries on any
|
package/dist/types/index.d.ts
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function pushLoopPhase(label: string): void;
|
|
2
|
+
export declare function popLoopPhase(): void;
|
|
3
|
+
export declare function currentLoopPhase(): string | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Phase to blame for a just-detected loop block: the live top phase if one is
|
|
6
|
+
* still held, else the most recent phase pushed since the last call. Clears the
|
|
7
|
+
* recent slot so a block in a later, phase-less interval is not misattributed
|
|
8
|
+
* to a phase that already finished.
|
|
9
|
+
*/
|
|
10
|
+
export declare function takeRecentLoopPhase(): string | undefined;
|
|
@@ -43,6 +43,8 @@ export declare function installRuntimeModuleResolver({ runtimeNodeModules, stubs
|
|
|
43
43
|
/** Pinned dependency set materialized into a runtime cache directory. */
|
|
44
44
|
export interface RuntimeInstallSpec {
|
|
45
45
|
dependencies: Record<string, string>;
|
|
46
|
+
/** Version pins forced across the whole runtime tree (bun `overrides`), e.g. dislodging a transitive dep. */
|
|
47
|
+
overrides?: Record<string, string>;
|
|
46
48
|
/** Packages whose lifecycle scripts bun may run during the install. */
|
|
47
49
|
trustedDependencies?: string[];
|
|
48
50
|
}
|
|
@@ -58,6 +60,7 @@ export interface EnsureRuntimeInstalledOptions {
|
|
|
58
60
|
lockAttempts?: number;
|
|
59
61
|
lockSleepMs?: number;
|
|
60
62
|
}
|
|
63
|
+
export declare function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void>;
|
|
61
64
|
/**
|
|
62
65
|
* Materialize a pinned dependency set into `runtimeDir` (idempotent,
|
|
63
66
|
* cross-process safe via a lock directory). Returns `runtimeDir`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.13.0",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "15.
|
|
34
|
+
"@oh-my-pi/pi-natives": "15.13.0",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
package/src/abortable.ts
CHANGED
|
@@ -10,16 +10,101 @@ export class AbortError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
type AbortableStreamReadResult<T> = { done: true; value?: T } | { done: false; value: T };
|
|
14
|
+
|
|
15
|
+
interface AbortableStreamReader<T> {
|
|
16
|
+
read(): Promise<AbortableStreamReadResult<T>>;
|
|
17
|
+
cancel(reason?: unknown): Promise<void>;
|
|
18
|
+
releaseLock(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
/**
|
|
14
22
|
* Creates an abortable stream from a given stream and signal.
|
|
15
23
|
*
|
|
24
|
+
* Unlike `stream.pipeThrough(..., { signal })`, this explicitly cancels the
|
|
25
|
+
* source reader when the signal aborts. That propagates HTTP-client disconnects
|
|
26
|
+
* and stream watchdog timeouts all the way to the backend request instead of
|
|
27
|
+
* only stopping the local consumer.
|
|
28
|
+
*
|
|
16
29
|
* @param stream - The stream to make abortable
|
|
17
30
|
* @param signal - The signal to abort the stream
|
|
18
31
|
* @returns The abortable stream
|
|
19
32
|
*/
|
|
20
33
|
export function createAbortableStream<T>(stream: ReadableStream<T>, signal?: AbortSignal): ReadableStream<T> {
|
|
21
34
|
if (!signal) return stream;
|
|
22
|
-
|
|
35
|
+
let reader: AbortableStreamReader<T> | undefined;
|
|
36
|
+
let closed = false;
|
|
37
|
+
let onAbort: (() => void) | undefined;
|
|
38
|
+
|
|
39
|
+
const cleanup = () => {
|
|
40
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
41
|
+
onAbort = undefined;
|
|
42
|
+
const currentReader = reader;
|
|
43
|
+
reader = undefined;
|
|
44
|
+
try {
|
|
45
|
+
currentReader?.releaseLock();
|
|
46
|
+
} catch {}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const cancelReader = (reason: unknown): Promise<void> => {
|
|
50
|
+
if (closed) return Promise.resolve();
|
|
51
|
+
closed = true;
|
|
52
|
+
const currentReader = reader;
|
|
53
|
+
reader = undefined;
|
|
54
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
55
|
+
onAbort = undefined;
|
|
56
|
+
if (!currentReader) return Promise.resolve();
|
|
57
|
+
return currentReader
|
|
58
|
+
.cancel(reason)
|
|
59
|
+
.catch(() => {})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
try {
|
|
62
|
+
currentReader.releaseLock();
|
|
63
|
+
} catch {}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return new ReadableStream<T>({
|
|
68
|
+
start(controller) {
|
|
69
|
+
reader = stream.getReader();
|
|
70
|
+
onAbort = () => {
|
|
71
|
+
void cancelReader(signal.reason);
|
|
72
|
+
try {
|
|
73
|
+
controller.error(new AbortError(signal));
|
|
74
|
+
} catch {}
|
|
75
|
+
};
|
|
76
|
+
if (signal.aborted) {
|
|
77
|
+
onAbort();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
81
|
+
void (async () => {
|
|
82
|
+
try {
|
|
83
|
+
for (;;) {
|
|
84
|
+
const currentReader = reader;
|
|
85
|
+
if (!currentReader) return;
|
|
86
|
+
const { value, done } = await currentReader.read();
|
|
87
|
+
if (closed) return;
|
|
88
|
+
if (done) {
|
|
89
|
+
closed = true;
|
|
90
|
+
cleanup();
|
|
91
|
+
controller.close();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
controller.enqueue(value);
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (closed) return;
|
|
98
|
+
closed = true;
|
|
99
|
+
cleanup();
|
|
100
|
+
controller.error(signal.aborted ? new AbortError(signal) : error);
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
},
|
|
104
|
+
cancel(reason) {
|
|
105
|
+
return cancelReader(reason);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
23
108
|
}
|
|
24
109
|
|
|
25
110
|
/**
|
package/src/fetch-retry.ts
CHANGED
|
@@ -129,6 +129,14 @@ export interface FetchWithRetryOptions extends RequestInit {
|
|
|
129
129
|
* mock during tests.
|
|
130
130
|
*/
|
|
131
131
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
132
|
+
/**
|
|
133
|
+
* Bun extension forwarded verbatim to the underlying `fetch` call. `false`
|
|
134
|
+
* disables Bun's native ~300s pre-response timeout (callers that own a
|
|
135
|
+
* configurable first-event/idle watchdog or an external `AbortSignal`
|
|
136
|
+
* supply this so the runtime ceiling cannot pre-empt them); a positive
|
|
137
|
+
* number sets a custom ceiling in ms. Bare browser/Node fetch ignores it.
|
|
138
|
+
*/
|
|
139
|
+
timeout?: number | false;
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
const DEFAULT_MAX_DELAY_MS = 60_000;
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live event-loop phase breadcrumb. Hot synchronous paths push a short label
|
|
3
|
+
* before running and pop it after (via `try`/`finally`); the loop watchdog
|
|
4
|
+
* reads {@link takeRecentLoopPhase} when it detects a block, so a stall is
|
|
5
|
+
* logged with the work that caused it instead of an opaque "unknown".
|
|
6
|
+
*
|
|
7
|
+
* This is deliberately a process-global stack and not part of the logger span
|
|
8
|
+
* machinery: `main.ts` ends timing spans before the interactive TUI starts, so
|
|
9
|
+
* `logger.openSpanPath()` is empty in a live session.
|
|
10
|
+
*
|
|
11
|
+
* Correctness constraint: each `pushLoopPhase` must be balanced by a
|
|
12
|
+
* `popLoopPhase` within the SAME synchronous execution (always via `try`/
|
|
13
|
+
* `finally`). The stack is global and shared, so a label held across an
|
|
14
|
+
* `await`/async boundary — or interleaved between concurrent tasks — would
|
|
15
|
+
* misattribute or leak phases. Instrument only synchronous spans; for async
|
|
16
|
+
* work, push/pop around each synchronous chunk, not across the await.
|
|
17
|
+
*/
|
|
18
|
+
const stack: string[] = [];
|
|
19
|
+
// The most recent label pushed, retained after it is popped. A hot path pushes
|
|
20
|
+
// and pops a phase entirely within one synchronous macrotask, so by the time
|
|
21
|
+
// the watchdog's delayed tick runs the stack is already empty; this slot keeps
|
|
22
|
+
// the culprit available for that one tick. Consumed (cleared) on read so it
|
|
23
|
+
// only attributes the just-elapsed interval.
|
|
24
|
+
let recentPhase: string | undefined;
|
|
25
|
+
|
|
26
|
+
export function pushLoopPhase(label: string): void {
|
|
27
|
+
stack.push(label);
|
|
28
|
+
recentPhase = label;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function popLoopPhase(): void {
|
|
32
|
+
stack.pop();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function currentLoopPhase(): string | undefined {
|
|
36
|
+
return stack[stack.length - 1];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Phase to blame for a just-detected loop block: the live top phase if one is
|
|
41
|
+
* still held, else the most recent phase pushed since the last call. Clears the
|
|
42
|
+
* recent slot so a block in a later, phase-less interval is not misattributed
|
|
43
|
+
* to a phase that already finished.
|
|
44
|
+
*/
|
|
45
|
+
export function takeRecentLoopPhase(): string | undefined {
|
|
46
|
+
const phase = stack[stack.length - 1] ?? recentPhase;
|
|
47
|
+
recentPhase = undefined;
|
|
48
|
+
return phase;
|
|
49
|
+
}
|
package/src/runtime-install.ts
CHANGED
|
@@ -242,6 +242,8 @@ export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }:
|
|
|
242
242
|
/** Pinned dependency set materialized into a runtime cache directory. */
|
|
243
243
|
export interface RuntimeInstallSpec {
|
|
244
244
|
dependencies: Record<string, string>;
|
|
245
|
+
/** Version pins forced across the whole runtime tree (bun `overrides`), e.g. dislodging a transitive dep. */
|
|
246
|
+
overrides?: Record<string, string>;
|
|
245
247
|
/** Packages whose lifecycle scripts bun may run during the install. */
|
|
246
248
|
trustedDependencies?: string[];
|
|
247
249
|
}
|
|
@@ -281,13 +283,14 @@ async function acquireInstallLock(runtimeDir: string, attempts: number, sleepMs:
|
|
|
281
283
|
throw new Error(`Timed out waiting for runtime install lock: ${lockDir}`);
|
|
282
284
|
}
|
|
283
285
|
|
|
284
|
-
async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
|
|
286
|
+
export async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
|
|
285
287
|
await fsp.mkdir(runtimeDir, { recursive: true });
|
|
286
288
|
const manifest: Record<string, unknown> = {
|
|
287
289
|
private: true,
|
|
288
290
|
type: "module",
|
|
289
291
|
dependencies: install.dependencies,
|
|
290
292
|
};
|
|
293
|
+
if (install.overrides && Object.keys(install.overrides).length) manifest.overrides = install.overrides;
|
|
291
294
|
if (install.trustedDependencies?.length) manifest.trustedDependencies = install.trustedDependencies;
|
|
292
295
|
await Bun.write(path.join(runtimeDir, "package.json"), `${JSON.stringify(manifest, null, "\t")}\n`);
|
|
293
296
|
}
|
package/src/temp.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as fsPromises from "node:fs/promises";
|
|
2
3
|
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
|
|
@@ -13,7 +14,7 @@ export class TempDir {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
static async create(prefix?: string): Promise<TempDir> {
|
|
16
|
-
return new TempDir(await
|
|
17
|
+
return new TempDir(await fsPromises.mkdtemp(normalizePrefix(prefix)));
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
#removePromise: Promise<void> | null = null;
|
|
@@ -30,13 +31,13 @@ export class TempDir {
|
|
|
30
31
|
if (this.#removePromise) {
|
|
31
32
|
return this.#removePromise;
|
|
32
33
|
}
|
|
33
|
-
const removePromise =
|
|
34
|
+
const removePromise = removeWithRetries(this.#path);
|
|
34
35
|
this.#removePromise = removePromise;
|
|
35
36
|
return removePromise;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
removeSync(): void {
|
|
39
|
-
|
|
40
|
+
removeSyncWithRetries(this.#path);
|
|
40
41
|
this.#removePromise = Promise.resolve();
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -75,3 +76,55 @@ function normalizePrefix(prefix?: string): string {
|
|
|
75
76
|
}
|
|
76
77
|
return prefix;
|
|
77
78
|
}
|
|
79
|
+
|
|
80
|
+
const kRemoveOptions = { recursive: true, force: true } as const;
|
|
81
|
+
const kRemoveRetries = 4;
|
|
82
|
+
const kRemoveRetryDelayMs = 10;
|
|
83
|
+
const kRetryableRemoveErrorCodes = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]);
|
|
84
|
+
const kSleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
85
|
+
|
|
86
|
+
async function removeWithRetries(target: string): Promise<void> {
|
|
87
|
+
for (let attempt = 0; ; attempt++) {
|
|
88
|
+
try {
|
|
89
|
+
await fsPromises.rm(target, kRemoveOptions);
|
|
90
|
+
return;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (!shouldRetryRemove(err, attempt)) throw err;
|
|
93
|
+
await Bun.sleep(kRemoveRetryDelayMs);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function removeSyncWithRetries(target: string): void {
|
|
99
|
+
for (let attempt = 0; ; attempt++) {
|
|
100
|
+
try {
|
|
101
|
+
fs.rmSync(target, kRemoveOptions);
|
|
102
|
+
return;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (!shouldRetryRemove(err, attempt)) throw err;
|
|
105
|
+
sleepSync(kRemoveRetryDelayMs);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function shouldRetryRemove(err: unknown, attempt: number): boolean {
|
|
111
|
+
return attempt < kRemoveRetries && process.platform === "win32" && isRetryableRemoveError(err);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isRetryableRemoveError(err: unknown): boolean {
|
|
115
|
+
return (
|
|
116
|
+
typeof err === "object" &&
|
|
117
|
+
err !== null &&
|
|
118
|
+
"code" in err &&
|
|
119
|
+
typeof err.code === "string" &&
|
|
120
|
+
kRetryableRemoveErrorCodes.has(err.code)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sleepSync(ms: number): void {
|
|
125
|
+
if ("sleepSync" in Bun && typeof Bun.sleepSync === "function") {
|
|
126
|
+
Bun.sleepSync(ms);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
Atomics.wait(kSleepBuffer, 0, 0, ms);
|
|
130
|
+
}
|