Skip to content

Commit c4e6d45

Browse files
committed
Centralize attached handler cleanup
1 parent a166658 commit c4e6d45

6 files changed

Lines changed: 113 additions & 51 deletions

File tree

‎src/cleanup/attached-handlers.mjs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { removeProcessListener } from "./listener.mjs";
2+
3+
export function removeAttachedHandlers({
4+
processObj,
5+
remove,
6+
events,
7+
handlers,
8+
shouldSkip,
9+
markRemoved,
10+
}) {
11+
let removalError;
12+
let hasRemovalError = false;
13+
for (const event of events) {
14+
if (shouldSkip(event)) {
15+
markRemoved(event);
16+
continue;
17+
}
18+
try {
19+
removeProcessListener({ processObj, remove, event, handler: handlers[event] });
20+
markRemoved(event);
21+
} catch (error) {
22+
if (!hasRemovalError) {
23+
removalError = error;
24+
hasRemovalError = true;
25+
}
26+
}
27+
}
28+
return { removalError, hasRemovalError };
29+
}

‎src/handler-removal.mjs‎

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
1-
import { removeProcessListener } from "./cleanup/listener.mjs";
1+
import { removeAttachedHandlers } from "./cleanup/attached-handlers.mjs";
22

33
export function createHandlerRemoval({ processObj, remove, selected, handlers, detachedEvents }) {
44
const removedEvents = new Set();
55
return () => {
6-
let removalError;
7-
let hasRemovalError = false;
8-
for (const event of selected) {
9-
if (removedEvents.has(event)) continue;
10-
if (detachedEvents.has(event)) {
11-
removedEvents.add(event);
12-
continue;
13-
}
14-
try {
15-
removeProcessListener({ processObj, remove, event, handler: handlers[event] });
16-
removedEvents.add(event);
17-
} catch (error) {
18-
if (!hasRemovalError) {
19-
removalError = error;
20-
hasRemovalError = true;
21-
}
22-
}
23-
}
6+
const { removalError, hasRemovalError } = removeAttachedHandlers({
7+
processObj,
8+
remove,
9+
events: selected,
10+
handlers,
11+
shouldSkip: (event) => removedEvents.has(event) || detachedEvents.has(event),
12+
markRemoved: (event) => removedEvents.add(event),
13+
});
2414
if (hasRemovalError) throw removalError;
2515
};
2616
}

‎src/setup/rollback.mjs‎

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { attachFailure } from "../failure.mjs";
2-
import { removeProcessListener } from "../cleanup/listener.mjs";
2+
import { removeAttachedHandlers } from "../cleanup/attached-handlers.mjs";
33

44
export function rollbackRegistration({
55
error,
@@ -10,18 +10,14 @@ export function rollbackRegistration({
1010
registration,
1111
registrations,
1212
}) {
13-
let rollbackError;
14-
let rollbackFailed = false;
15-
for (const event of attachedEvents) {
16-
try {
17-
removeProcessListener({ processObj, remove, event, handler: handlers[event] });
18-
} catch (removalError) {
19-
if (!rollbackFailed) {
20-
rollbackError = removalError;
21-
rollbackFailed = true;
22-
}
23-
}
24-
}
13+
const { removalError: rollbackError, hasRemovalError: rollbackFailed } = removeAttachedHandlers({
14+
processObj,
15+
remove,
16+
events: attachedEvents,
17+
handlers,
18+
shouldSkip: () => false,
19+
markRemoved: () => {},
20+
});
2521
if (rollbackFailed) {
2622
registration.ready = true;
2723
registrations.set(processObj, registration);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { jest } from "@jest/globals";
2+
import { removeAttachedHandlers } from "../../src/cleanup/attached-handlers.mjs";
3+
4+
test("skips selected handlers and records removed events", () => {
5+
const handler = jest.fn();
6+
const processObj = { off: jest.fn() };
7+
const removedEvents = new Set();
8+
const result = removeAttachedHandlers({
9+
processObj,
10+
remove: "off",
11+
events: ["warning", "uncaughtException"],
12+
handlers: { warning: handler, uncaughtException: jest.fn() },
13+
shouldSkip: (event) => event === "uncaughtException",
14+
markRemoved: (event) => removedEvents.add(event),
15+
});
16+
17+
expect(result).toEqual({ removalError: undefined, hasRemovalError: false });
18+
expect(processObj.off).toHaveBeenCalledTimes(1);
19+
expect(processObj.off).toHaveBeenCalledWith("warning", handler);
20+
expect(removedEvents).toEqual(new Set(["warning", "uncaughtException"]));
21+
});
22+
23+
test("attempts all events and returns the first failure", () => {
24+
const first = Error("first removal");
25+
const processObj = {
26+
off: jest
27+
.fn()
28+
.mockImplementationOnce(() => {
29+
throw first;
30+
})
31+
.mockImplementationOnce(() => {
32+
throw Error("second removal");
33+
}),
34+
};
35+
const result = removeAttachedHandlers({
36+
processObj,
37+
remove: "off",
38+
events: ["warning", "uncaughtException"],
39+
handlers: { warning: jest.fn(), uncaughtException: jest.fn() },
40+
shouldSkip: () => false,
41+
markRemoved: jest.fn(),
42+
});
43+
44+
expect(result).toEqual({ removalError: first, hasRemovalError: true });
45+
expect(processObj.off).toHaveBeenCalledTimes(2);
46+
});

‎tests/event/handler.test.mjs‎

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,6 @@ test("logs events and ignores removed or fired handlers", () => {
3131
expect(handler("ignored")).toBeUndefined();
3232
});
3333

34-
test("propagates logger failures from warning and rejection callbacks", () => {
35-
const processObj = { on: jest.fn(), off: jest.fn() };
36-
const log = {
37-
error: jest.fn(() => {
38-
throw Error("rejection log");
39-
}),
40-
warn: jest.fn(() => {
41-
throw Error("warning log");
42-
}),
43-
debug: jest.fn(),
44-
};
45-
registerHandlers({ processObj, log, events: ["warning", "unhandledRejection"] });
46-
const handlers = Object.fromEntries(
47-
processObj.on.mock.calls.map(([event, handler]) => [event, handler]),
48-
);
49-
expect(() => handlers.warning("warning")).toThrow("warning log");
50-
expect(() => handlers.unhandledRejection("reason")).toThrow("rejection log");
51-
});
52-
5334
test("propagates synchronous one-shot logger failures during setup", () => {
5435
const processObj = {
5536
on: jest.fn(),

‎tests/logging.test.mjs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ test("routes warning and rejection events to the logger", () => {
2727
expect(log.error).toHaveBeenCalledWith("Unhandled promise error", { reasonType: "string" });
2828
});
2929

30+
test("propagates logger failures from warning and rejection callbacks", () => {
31+
const processObj = { on: jest.fn(), off: jest.fn() };
32+
const log = {
33+
error: jest.fn(() => {
34+
throw Error("rejection log");
35+
}),
36+
warn: jest.fn(() => {
37+
throw Error("warning log");
38+
}),
39+
debug: jest.fn(),
40+
};
41+
registerHandlers({ processObj, log, events: ["warning", "unhandledRejection"] });
42+
const handlers = Object.fromEntries(
43+
processObj.on.mock.calls.map(([event, handler]) => [event, handler]),
44+
);
45+
46+
expect(() => handlers.warning("warning")).toThrow("warning log");
47+
expect(() => handlers.unhandledRejection("reason")).toThrow("rejection log");
48+
});
49+
3050
test("logs uncaught exceptions", () => {
3151
const processObj = { on: jest.fn(), off: jest.fn() };
3252
const log = { debug: jest.fn(), error: jest.fn(), warn: jest.fn() };

0 commit comments

Comments
 (0)