@oh-my-pi/pi-utils 16.1.0 → 16.1.2
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 +10 -0
- package/dist/types/abortable.d.ts +9 -9
- package/dist/types/dirs.d.ts +7 -0
- package/dist/types/index.d.ts +1 -1
- package/package.json +2 -2
- package/src/abortable.ts +39 -88
- package/src/dirs.ts +14 -0
- package/src/index.ts +1 -1
- package/src/stream.ts +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.2] - 2026-06-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `directoryExists(dir)` to `dirs`: resolves whether a path is an existing directory, returning `false` on any stat failure (ENOENT, permission, non-directory). Lets callers check a directory is safe to `chdir` into before `setProjectDir` throws.
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- Removed the public `createAbortableStream` API from `@oh-my-pi/pi-utils`. Consumers should use the lighter, direct-reader `abortableSource` async generator inside `@oh-my-pi/pi-utils/stream` to avoid the extra ReadableStream wrapper layer and per-chunk enqueue overhead.
|
|
14
|
+
|
|
5
15
|
## [16.0.11] - 2026-06-19
|
|
6
16
|
|
|
7
17
|
### Removed
|
|
@@ -2,18 +2,18 @@ export declare class AbortError extends Error {
|
|
|
2
2
|
constructor(signal: AbortSignal);
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Abortable async iteration over a {@link ReadableStream}. Reads the source
|
|
6
|
+
* reader directly and yields each chunk, so the consumer's `for await` drives a
|
|
7
|
+
* single read loop with no intermediate stream or per-chunk enqueue.
|
|
6
8
|
*
|
|
7
9
|
* Unlike `stream.pipeThrough(..., { signal })`, this explicitly cancels the
|
|
8
|
-
* source reader
|
|
9
|
-
* and
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @param signal - The signal to abort the stream
|
|
14
|
-
* @returns The abortable stream
|
|
10
|
+
* source reader on abort or early `break`, propagating HTTP-client disconnects
|
|
11
|
+
* and watchdog timeouts to the backend request instead of only stopping the
|
|
12
|
+
* local consumer. On abort it throws {@link AbortError}; the lock is released
|
|
13
|
+
* on completion, abort, throw, or early exit. The source is cancelled only on
|
|
14
|
+
* abort or early exit — never on natural EOF.
|
|
15
15
|
*/
|
|
16
|
-
export declare function
|
|
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
19
|
* execution, the promise is rejected with a standard error.
|
package/dist/types/dirs.d.ts
CHANGED
|
@@ -44,6 +44,13 @@ export declare function relativePathWithinRoot(root: string, candidate: string):
|
|
|
44
44
|
export declare function getProjectDir(): string;
|
|
45
45
|
/** Set the project directory. */
|
|
46
46
|
export declare function setProjectDir(dir: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Whether `dir` resolves to an existing directory. Any stat failure — a deleted
|
|
49
|
+
* path (ENOENT), permission error, or a non-directory — returns `false`, so
|
|
50
|
+
* callers can decide whether a directory is safe to `chdir` into or adopt as a
|
|
51
|
+
* working directory before {@link setProjectDir} throws on it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function directoryExists(dir: string): Promise<boolean>;
|
|
47
54
|
/** Get the config directory name relative to home (e.g. ".omp" or PI_CONFIG_DIR override). */
|
|
48
55
|
export declare function getConfigDirName(): string;
|
|
49
56
|
/** Get the config agent directory name relative to home (e.g. ".omp/agent" or PI_CONFIG_DIR + "/agent"). */
|
package/dist/types/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.2",
|
|
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": "16.1.
|
|
34
|
+
"@oh-my-pi/pi-natives": "16.1.2",
|
|
35
35
|
"handlebars": "^4.7.9",
|
|
36
36
|
"winston": "^3.19.0",
|
|
37
37
|
"winston-daily-rotate-file": "^5.0.0"
|
package/src/abortable.ts
CHANGED
|
@@ -10,101 +10,52 @@ 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
|
-
|
|
21
13
|
/**
|
|
22
|
-
*
|
|
14
|
+
* Abortable async iteration over a {@link ReadableStream}. Reads the source
|
|
15
|
+
* reader directly and yields each chunk, so the consumer's `for await` drives a
|
|
16
|
+
* single read loop with no intermediate stream or per-chunk enqueue.
|
|
23
17
|
*
|
|
24
18
|
* Unlike `stream.pipeThrough(..., { signal })`, this explicitly cancels the
|
|
25
|
-
* source reader
|
|
26
|
-
* and
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @param signal - The signal to abort the stream
|
|
31
|
-
* @returns The abortable stream
|
|
19
|
+
* source reader on abort or early `break`, propagating HTTP-client disconnects
|
|
20
|
+
* and watchdog timeouts to the backend request instead of only stopping the
|
|
21
|
+
* local consumer. On abort it throws {@link AbortError}; the lock is released
|
|
22
|
+
* on completion, abort, throw, or early exit. The source is cancelled only on
|
|
23
|
+
* abort or early exit — never on natural EOF.
|
|
32
24
|
*/
|
|
33
|
-
export function
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
let closed = false;
|
|
25
|
+
export async function* abortableSource<T>(stream: ReadableStream<T>, signal?: AbortSignal): AsyncGenerator<T> {
|
|
26
|
+
if (signal?.aborted) throw new AbortError(signal);
|
|
27
|
+
const reader = stream.getReader();
|
|
37
28
|
let onAbort: (() => void) | undefined;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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();
|
|
29
|
+
if (signal) {
|
|
30
|
+
onAbort = () => {
|
|
31
|
+
void reader.cancel(signal.reason).catch(() => {});
|
|
32
|
+
};
|
|
33
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
34
|
+
}
|
|
35
|
+
let completed = false;
|
|
36
|
+
try {
|
|
37
|
+
for (;;) {
|
|
38
|
+
const result = await reader.read();
|
|
39
|
+
if (signal?.aborted) throw new AbortError(signal);
|
|
40
|
+
if (result.done) {
|
|
41
|
+
completed = true;
|
|
78
42
|
return;
|
|
79
43
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
});
|
|
44
|
+
yield result.value;
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
48
|
+
// Propagate early-exit (`break`/`return`) and abort to the backend; skip
|
|
49
|
+
// on natural EOF where the stream already closed itself.
|
|
50
|
+
if (!completed) {
|
|
51
|
+
try {
|
|
52
|
+
await reader.cancel();
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
reader.releaseLock();
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
108
59
|
}
|
|
109
60
|
|
|
110
61
|
/**
|
package/src/dirs.ts
CHANGED
|
@@ -185,6 +185,20 @@ export function setProjectDir(dir: string): void {
|
|
|
185
185
|
process.chdir(projectDir);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Whether `dir` resolves to an existing directory. Any stat failure — a deleted
|
|
190
|
+
* path (ENOENT), permission error, or a non-directory — returns `false`, so
|
|
191
|
+
* callers can decide whether a directory is safe to `chdir` into or adopt as a
|
|
192
|
+
* working directory before {@link setProjectDir} throws on it.
|
|
193
|
+
*/
|
|
194
|
+
export async function directoryExists(dir: string): Promise<boolean> {
|
|
195
|
+
try {
|
|
196
|
+
return (await fs.promises.stat(dir)).isDirectory();
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
188
202
|
/** Get the config directory name relative to home (e.g. ".omp" or PI_CONFIG_DIR override). */
|
|
189
203
|
export function getConfigDirName(): string {
|
|
190
204
|
return process.env.PI_CONFIG_DIR || CONFIG_DIR_NAME;
|
package/src/index.ts
CHANGED
package/src/stream.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { abortableSource } from "./abortable";
|
|
2
2
|
|
|
3
3
|
const LF = 0x0a;
|
|
4
4
|
type JsonlChunkResult = {
|
|
@@ -23,7 +23,7 @@ function parseJsonlChunkCompat(input: Uint8Array | string, beg?: number, end?: n
|
|
|
23
23
|
|
|
24
24
|
export async function* readLines(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<Uint8Array> {
|
|
25
25
|
const buffer = new ConcatSink();
|
|
26
|
-
const source =
|
|
26
|
+
const source = abortableSource(stream, signal);
|
|
27
27
|
try {
|
|
28
28
|
for await (const chunk of source) {
|
|
29
29
|
for (const line of buffer.appendAndFlushLines(chunk)) {
|
|
@@ -46,7 +46,7 @@ export async function* readLines(stream: ReadableStream<Uint8Array>, signal?: Ab
|
|
|
46
46
|
|
|
47
47
|
export async function* readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<T> {
|
|
48
48
|
const buffer = new ConcatSink();
|
|
49
|
-
const source =
|
|
49
|
+
const source = abortableSource(stream, signal);
|
|
50
50
|
try {
|
|
51
51
|
for await (const chunk of source) {
|
|
52
52
|
yield* buffer.pullJSONL<T>(chunk, 0, chunk.length);
|
|
@@ -339,7 +339,7 @@ export async function* readSseEvents(
|
|
|
339
339
|
): AsyncGenerator<ServerSentEvent> {
|
|
340
340
|
const lineBuffer = new ConcatSink();
|
|
341
341
|
const state: SseEventState = { event: null, data: null, raw: [] };
|
|
342
|
-
const source =
|
|
342
|
+
const source = abortableSource(stream, signal);
|
|
343
343
|
try {
|
|
344
344
|
for await (const chunk of source) {
|
|
345
345
|
for (const line of lineBuffer.appendAndFlushLines(chunk)) {
|