Skip to content

Commit 219beff

Browse files
committed
chore: synchronize repository state
1 parent 91c4516 commit 219beff

14 files changed

Lines changed: 356 additions & 65 deletions

‎RELEASE_NOTES.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- Hardened rollback and cleanup behavior for listener, logger, signal, and
88
one-shot handler failures.
99
- Added focused module-level tests while retaining public API coverage.
10-
- Updated `@eliware/log` to 2.1.0 and `@eliware/test` to 2.3.1.
10+
- Updated `@eliware/log` to 2.4.0 and `@eliware/test` to 2.4.0.
1111

1212
## 2.0.0 — 2026-08-25
1313

‎index.d.ts‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import type logger from '@eliware/log';
22

33
export type ProcessEvent = 'uncaughtException' | 'unhandledRejection' | 'warning';
4-
export interface ProcessLike {
5-
on(event: string, listener: (...args: unknown[]) => void): unknown;
6-
once?(event: string, listener: (...args: unknown[]) => void): unknown;
7-
off?(event: string, listener: (...args: unknown[]) => void): unknown;
8-
removeListener?(event: string, listener: (...args: unknown[]) => void): unknown;
4+
export type ProcessListener = (...args: unknown[]) => void;
5+
export interface Logger {
6+
error(message: string, metadata: unknown): unknown;
7+
warn(message: string, metadata: unknown): unknown;
8+
debug(message: string): unknown;
99
}
10+
export type ProcessLike = {
11+
on(event: ProcessEvent, listener: ProcessListener): unknown;
12+
once?(event: ProcessEvent, listener: ProcessListener): unknown;
13+
} & ({
14+
off(event: ProcessEvent, listener: ProcessListener): unknown;
15+
} | {
16+
removeListener(event: ProcessEvent, listener: ProcessListener): unknown;
17+
});
1018
export interface RegisterHandlersOptions {
1119
processObj?: ProcessLike;
12-
log?: typeof logger;
20+
log?: Logger;
1321
events?: ProcessEvent[];
1422
once?: boolean;
1523
signal?: AbortSignal;

‎index.mjs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
export { registerHandlers, registerHandlers as default } from './src/registration.mjs';
1+
import { registerHandlers } from './src/index.mjs';
2+
3+
const publicRegisterHandlers = registerHandlers;
4+
5+
export { publicRegisterHandlers as registerHandlers, publicRegisterHandlers as default };

‎package-lock.json‎

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@
6060
},
6161
"homepage": "https://github.com/eliware/errors#readme",
6262
"devDependencies": {
63-
"@eliware/test": "^2.3.1",
63+
"@eliware/test": "^5.0.0",
6464
"typescript": "^7.0.2"
6565
},
6666
"dependencies": {
67-
"@eliware/log": "^2.1.0"
67+
"@eliware/log": "^2.4.0"
6868
},
6969
"engines": {
7070
"node": ">=26"

‎src/cleanup.mjs‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
1-
export function createCleanup({ processObj, remove, selected, handlers, signal, registrations, registration }) {
1+
export function createCleanup({ processObj, remove, selected, handlers, signal, registrations, registration, detachedEvents }) {
22
let abortListener;
3+
const removedEvents = new Set();
34
const removeHandlers = () => {
45
if (registration.removed) return;
56
registration.removed = true;
67
let removalError;
78
for (const event of selected) {
9+
if (removedEvents.has(event)) continue;
10+
if (detachedEvents.has(event)) {
11+
removedEvents.add(event);
12+
continue;
13+
}
814
try {
915
processObj[remove](event, handlers[event]);
16+
removedEvents.add(event);
1017
} catch (error) {
1118
removalError ??= error;
1219
}
1320
}
14-
if (removalError) {
15-
registration.removed = false;
16-
throw removalError;
17-
}
21+
if (removalError) { registration.removed = false; throw removalError; }
1822
let abortError;
1923
if (abortListener && typeof signal?.removeEventListener === 'function') {
2024
try {
@@ -23,8 +27,12 @@ export function createCleanup({ processObj, remove, selected, handlers, signal,
2327
abortError = error;
2428
}
2529
}
30+
if (abortError) {
31+
registration.removed = false;
32+
registrations.set(processObj, registration);
33+
throw abortError;
34+
}
2635
registrations.delete(processObj);
27-
if (abortError) throw abortError;
2836
};
2937
abortListener = removeHandlers;
3038
return { removeHandlers, setAbortListener: listener => { abortListener = listener; } };

‎src/index.mjs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { registerHandlers } from './registration.mjs';
2+
3+
const publicRegisterHandlers = registerHandlers;
4+
5+
export { publicRegisterHandlers as registerHandlers, publicRegisterHandlers as default };

‎src/logging.mjs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
function diagnostic(value) {
2+
if (value instanceof Error) return { name: 'Error', type: 'Error' };
3+
return { type: typeof value };
4+
}
5+
16
export function eventLog(log, event, ...args) {
2-
if (event === 'warning') return log.warn('Warning', { warning: args[0] });
3-
if (event === 'unhandledRejection') return log.error('Unhandled Rejection', { reason: args[0], promise: args[1] });
4-
return log.error('Uncaught Exception', { error: args[0] });
7+
if (event === 'warning') return log.warn('Warning', { warning: diagnostic(args[0]) });
8+
if (event === 'unhandledRejection') return log.error('Unhandled Rejection', { reasonType: typeof args[0] });
9+
return log.error('Uncaught Exception', { error: diagnostic(args[0]) });
510
}

‎src/registration.mjs‎

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,116 @@
11
import logger from '@eliware/log';
22
import { defaultEvents } from './constants.mjs';
3-
import { normalizeEvents, validateProcessObject } from './validation.mjs';
3+
import { normalizeEvents, validateLogger, validateProcessObject } from './validation.mjs';
44
import { eventLog } from './logging.mjs';
55
import { createCleanup } from './cleanup.mjs';
66

77
const registrations = new WeakMap();
88

9+
function attachFailure(error, key, secondary) {
10+
if ((typeof error !== 'object' && typeof error !== 'function') || error === null) return;
11+
try {
12+
if (Object.isExtensible(error)) error[key] = secondary;
13+
} catch {
14+
// Preserve the primary failure when secondary-error reporting is unavailable.
15+
}
16+
}
17+
918
export function registerHandlers({ processObj = process, log = logger, events, once = false, signal } = {}) {
1019
const selected = normalizeEvents(events);
11-
const existing = registrations.get(processObj);
12-
if (existing) return existing;
1320
const remove = validateProcessObject(processObj);
21+
validateLogger(log);
22+
const existing = registrations.get(processObj);
23+
if (existing) {
24+
if (signal && typeof signal.addEventListener !== 'function') throw new TypeError('signal must provide addEventListener');
25+
if (!existing.ready) throw new Error('registration is still being set up');
26+
return existing;
27+
}
1428
const add = once && typeof processObj.once === 'function' ? 'once' : 'on';
15-
const oneShot = add === 'once';
29+
const oneShot = once;
1630
const fired = new Set();
17-
const registration = { removed: false };
18-
const handlers = Object.fromEntries(selected.map(event => [event, (...args) => {
19-
const result = eventLog(log, event, ...args);
20-
if (oneShot) {
21-
fired.add(event);
22-
if (fired.size === selected.length && registration.removeHandlers) {
23-
registration.removeHandlers();
31+
const detachedEvents = new Set();
32+
const addedEvents = new Set();
33+
const attachedEvents = new Set();
34+
let setupComplete = false;
35+
let cleanupPending = false;
36+
const registration = { removed: false, ready: false };
37+
const handlers = {};
38+
for (const event of selected) {
39+
handlers[event] = (...args) => {
40+
if (registration.removed || fired.has(event)) return undefined;
41+
try {
42+
const result = eventLog(log, event, ...args);
43+
if (oneShot) {
44+
fired.add(event);
45+
if (add === 'once' && addedEvents.has(event)) detachedEvents.add(event);
46+
}
47+
if (oneShot && fired.size === selected.length) {
48+
if (setupComplete) registration.removeHandlers();
49+
else cleanupPending = true;
50+
}
51+
return result;
52+
} catch (error) {
53+
if (oneShot) {
54+
fired.add(event);
55+
if (setupComplete) {
56+
try { registration.removeHandlers(); } catch (cleanupError) { attachFailure(error, 'cleanupError', cleanupError); }
57+
} else cleanupPending = true;
58+
}
59+
throw error;
2460
}
25-
}
26-
return result;
27-
}]));
28-
const added = [];
61+
};
62+
}
63+
registrations.set(processObj, registration);
64+
const cleanup = createCleanup({ processObj, remove, selected, handlers, signal, registrations, registration, detachedEvents });
65+
registration.removeHandlers = cleanup.removeHandlers;
66+
cleanup.setAbortListener(registration.removeHandlers);
2967
try {
3068
for (const event of selected) {
69+
attachedEvents.add(event);
3170
processObj[add](event, handlers[event]);
32-
added.push(event);
71+
addedEvents.add(event);
3372
}
73+
setupComplete = true;
74+
if (cleanupPending || (oneShot && fired.size === selected.length)) registration.removeHandlers();
75+
registration.ready = true;
3476
} catch (error) {
35-
for (const event of added) {
77+
let rollbackError;
78+
for (const event of attachedEvents) {
3679
try {
3780
processObj[remove](event, handlers[event]);
38-
} catch {
39-
// Preserve the original registration failure while attempting all rollback removals.
81+
} catch (removalError) {
82+
rollbackError ??= removalError;
4083
}
4184
}
85+
if (rollbackError) {
86+
registration.ready = true;
87+
registrations.set(processObj, registration);
88+
attachFailure(error, 'rollbackError', rollbackError);
89+
attachFailure(error, 'registration', registration);
90+
} else registrations.delete(processObj);
4291
throw error;
4392
}
44-
const cleanup = createCleanup({ processObj, remove, selected, handlers, signal, registrations, registration });
45-
registration.removeHandlers = cleanup.removeHandlers;
46-
cleanup.setAbortListener(registration.removeHandlers);
47-
registrations.set(processObj, registration);
4893
if (oneShot && fired.size === selected.length) registration.removeHandlers();
4994
try {
50-
if (signal) {
95+
if (signal && !registration.removed) {
5196
if (typeof signal.addEventListener !== 'function') throw new TypeError('signal must provide addEventListener');
97+
signal.addEventListener('abort', registration.removeHandlers, { once: true });
5298
if (signal.aborted) registration.removeHandlers();
53-
else signal.addEventListener('abort', registration.removeHandlers, { once: true });
5499
}
55100
log.debug('Exception handlers registered');
56101
} catch (error) {
57102
let cleanupFailed = false;
58103
try {
59104
registration.removeHandlers();
60-
} catch {
105+
} catch (cleanupError) {
61106
// Preserve the original setup or logger failure.
62107
cleanupFailed = true;
108+
attachFailure(error, 'cleanupError', cleanupError);
109+
}
110+
if (cleanupFailed) {
111+
registration.ready = true;
112+
registrations.set(processObj, registration);
63113
}
64-
if (cleanupFailed) registrations.set(processObj, registration);
65114
else registrations.delete(processObj);
66115
throw error;
67116
}

‎src/validation.mjs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ export function validateProcessObject(processObj) {
1717
if (typeof processObj[remove] !== 'function') throw new TypeError('processObj must provide an off or removeListener method');
1818
return remove;
1919
}
20+
21+
export function validateLogger(log) {
22+
for (const method of ['error', 'warn', 'debug']) {
23+
if (!log || typeof log[method] !== 'function') throw new TypeError(`log must provide a ${method} method`);
24+
}
25+
}

0 commit comments

Comments
 (0)