@oh-my-pi/pi-utils 6.8.0 → 6.8.1
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/package.json +1 -1
- package/src/abortable.ts +20 -6
- package/src/postmortem.ts +58 -28
- package/src/ptree.ts +9 -2
package/package.json
CHANGED
package/src/abortable.ts
CHANGED
|
@@ -5,9 +5,8 @@ export class AbortError extends Error {
|
|
|
5
5
|
assert(signal.aborted, "Abort signal must be aborted");
|
|
6
6
|
|
|
7
7
|
const message = signal.reason instanceof Error ? signal.reason.message : "Cancelled";
|
|
8
|
-
super(`Aborted: ${message}`, { cause:
|
|
8
|
+
super(`Aborted: ${message}`, { cause: signal.reason });
|
|
9
9
|
this.name = "AbortError";
|
|
10
|
-
this.cause = signal.reason;
|
|
11
10
|
}
|
|
12
11
|
}
|
|
13
12
|
|
|
@@ -42,9 +41,11 @@ export function createAbortablePromise<T>(signal?: AbortSignal): {
|
|
|
42
41
|
reject(new AbortError(signal));
|
|
43
42
|
};
|
|
44
43
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
45
|
-
promise
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
promise
|
|
45
|
+
.finally(() => {
|
|
46
|
+
signal.removeEventListener("abort", abortHandler);
|
|
47
|
+
})
|
|
48
|
+
.catch(() => {});
|
|
48
49
|
return { promise, resolve, reject };
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -63,7 +64,20 @@ export function untilAborted<T>(signal: AbortSignal | undefined | null, pr: () =
|
|
|
63
64
|
return Promise.reject(new AbortError(signal));
|
|
64
65
|
}
|
|
65
66
|
const { promise, resolve, reject } = createAbortablePromise<T>(signal);
|
|
66
|
-
|
|
67
|
+
let settled = false;
|
|
68
|
+
const wrappedResolve = (value: T | PromiseLike<T>) => {
|
|
69
|
+
if (!settled) {
|
|
70
|
+
settled = true;
|
|
71
|
+
resolve(value);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const wrappedReject = (reason?: unknown) => {
|
|
75
|
+
if (!settled) {
|
|
76
|
+
settled = true;
|
|
77
|
+
reject(reason);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
pr().then(wrappedResolve, wrappedReject);
|
|
67
81
|
return promise;
|
|
68
82
|
}
|
|
69
83
|
|
package/src/postmortem.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* allow reliably releasing resources or shutting down subprocesses, files, sockets, etc.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { isMainThread } from "node:worker_threads";
|
|
9
10
|
import { logger } from ".";
|
|
10
11
|
|
|
11
12
|
// Cleanup reasons, in order of priority/meaning.
|
|
@@ -45,7 +46,8 @@ function runCleanup(reason: Reason): Promise<void> {
|
|
|
45
46
|
|
|
46
47
|
// Call .cleanup() for each callback that is still "armed".
|
|
47
48
|
// Use Promise.try to handle sync/async, but only those armed.
|
|
48
|
-
|
|
49
|
+
// Create a copy to avoid mutating the original array with reverse()
|
|
50
|
+
const promises = [...callbackList].reverse().map((callback) => {
|
|
49
51
|
return Promise.try(() => callback(reason));
|
|
50
52
|
});
|
|
51
53
|
|
|
@@ -61,33 +63,45 @@ function runCleanup(reason: Reason): Promise<void> {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
// Register signal and error event handlers to trigger cleanup before exit.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
66
|
+
// Main thread: full signal handling (SIGINT, SIGTERM, SIGHUP) + exceptions + exit
|
|
67
|
+
// Worker thread: exit only (workers use self.addEventListener for exceptions)
|
|
68
|
+
if (isMainThread) {
|
|
69
|
+
process
|
|
70
|
+
.on("SIGINT", async () => {
|
|
71
|
+
await runCleanup(Reason.SIGINT);
|
|
72
|
+
process.exit(130); // 128 + SIGINT (2)
|
|
73
|
+
})
|
|
74
|
+
.on("uncaughtException", async (err) => {
|
|
75
|
+
logger.error("Uncaught exception", { err, stack: err.stack });
|
|
76
|
+
await runCleanup(Reason.UNCAUGHT_EXCEPTION);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
})
|
|
79
|
+
.on("unhandledRejection", async (reason) => {
|
|
80
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
81
|
+
logger.error("Unhandled rejection", { err, stack: err.stack });
|
|
82
|
+
await runCleanup(Reason.UNHANDLED_REJECTION);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
})
|
|
85
|
+
.on("exit", async () => {
|
|
86
|
+
void runCleanup(Reason.EXIT); // fire and forget (exit imminent)
|
|
87
|
+
})
|
|
88
|
+
.on("SIGTERM", async () => {
|
|
89
|
+
await runCleanup(Reason.SIGTERM);
|
|
90
|
+
process.exit(143); // 128 + SIGTERM (15)
|
|
91
|
+
})
|
|
92
|
+
.on("SIGHUP", async () => {
|
|
93
|
+
await runCleanup(Reason.SIGHUP);
|
|
94
|
+
process.exit(129); // 128 + SIGHUP (1)
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
// Worker thread: only register exit handler for cleanup.
|
|
98
|
+
// DO NOT register uncaughtException/unhandledRejection handlers here -
|
|
99
|
+
// they would swallow errors before the worker's own handlers (self.addEventListener)
|
|
100
|
+
// can report failures back to the parent thread.
|
|
101
|
+
process.on("exit", () => {
|
|
102
|
+
void runCleanup(Reason.EXIT);
|
|
90
103
|
});
|
|
104
|
+
}
|
|
91
105
|
|
|
92
106
|
/**
|
|
93
107
|
* Register a process cleanup callback, to be run on shutdown, signal, or fatal error.
|
|
@@ -134,10 +148,26 @@ export function register(id: string, callback: (reason: Reason) => void | Promis
|
|
|
134
148
|
}
|
|
135
149
|
|
|
136
150
|
/**
|
|
137
|
-
* Runs all cleanup callbacks
|
|
151
|
+
* Runs all cleanup callbacks without exiting.
|
|
152
|
+
* Use this in workers or when you need to clean up but continue execution.
|
|
153
|
+
*/
|
|
154
|
+
export function cleanup(): Promise<void> {
|
|
155
|
+
return runCleanup(Reason.MANUAL);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Runs all cleanup callbacks and exits.
|
|
160
|
+
*
|
|
161
|
+
* In main thread: waits for stdout drain, then calls process.exit().
|
|
162
|
+
* In workers: runs cleanup only (process.exit would kill entire process).
|
|
138
163
|
*/
|
|
139
164
|
export async function quit(code: number = 0): Promise<void> {
|
|
140
165
|
await runCleanup(Reason.MANUAL);
|
|
166
|
+
|
|
167
|
+
if (!isMainThread) {
|
|
168
|
+
return; // Workers: cleanup done, let worker exit naturally
|
|
169
|
+
}
|
|
170
|
+
|
|
141
171
|
if (process.stdout.writableLength > 0) {
|
|
142
172
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
143
173
|
process.stdout.once("drain", resolve);
|
package/src/ptree.ts
CHANGED
|
@@ -201,7 +201,14 @@ export class ChildProcess {
|
|
|
201
201
|
async killAndWait(): Promise<void> {
|
|
202
202
|
// Try killing with SIGTERM, then SIGKILL if it doesn't exit within 1 second
|
|
203
203
|
this.kill("SIGTERM");
|
|
204
|
-
await Promise.race([
|
|
204
|
+
const exitedOrTimeout = await Promise.race([
|
|
205
|
+
this.exited.then(() => "exited" as const),
|
|
206
|
+
Bun.sleep(1000).then(() => "timeout" as const),
|
|
207
|
+
]);
|
|
208
|
+
if (exitedOrTimeout === "timeout") {
|
|
209
|
+
this.kill("SIGKILL");
|
|
210
|
+
await this.exited.catch(() => {});
|
|
211
|
+
}
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
// Output utilities (aliases for easy chaining)
|
|
@@ -329,7 +336,7 @@ export class AbortError extends Exception {
|
|
|
329
336
|
*/
|
|
330
337
|
export class TimeoutError extends AbortError {
|
|
331
338
|
constructor(timeout: number, stderr: string) {
|
|
332
|
-
super(new Error(`
|
|
339
|
+
super(new Error(`Timed out after ${Math.round(timeout / 1000)}s`), stderr);
|
|
333
340
|
}
|
|
334
341
|
}
|
|
335
342
|
|