|
1 | 1 | import logger from '@eliware/log'; |
2 | 2 | import { defaultEvents } from './constants.mjs'; |
3 | | -import { normalizeEvents, validateProcessObject } from './validation.mjs'; |
| 3 | +import { normalizeEvents, validateLogger, validateProcessObject } from './validation.mjs'; |
4 | 4 | import { eventLog } from './logging.mjs'; |
5 | 5 | import { createCleanup } from './cleanup.mjs'; |
6 | 6 |
|
7 | 7 | const registrations = new WeakMap(); |
8 | 8 |
|
| 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 | + |
9 | 18 | export function registerHandlers({ processObj = process, log = logger, events, once = false, signal } = {}) { |
10 | 19 | const selected = normalizeEvents(events); |
11 | | - const existing = registrations.get(processObj); |
12 | | - if (existing) return existing; |
13 | 20 | 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 | + } |
14 | 28 | const add = once && typeof processObj.once === 'function' ? 'once' : 'on'; |
15 | | - const oneShot = add === 'once'; |
| 29 | + const oneShot = once; |
16 | 30 | 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; |
24 | 60 | } |
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); |
29 | 67 | try { |
30 | 68 | for (const event of selected) { |
| 69 | + attachedEvents.add(event); |
31 | 70 | processObj[add](event, handlers[event]); |
32 | | - added.push(event); |
| 71 | + addedEvents.add(event); |
33 | 72 | } |
| 73 | + setupComplete = true; |
| 74 | + if (cleanupPending || (oneShot && fired.size === selected.length)) registration.removeHandlers(); |
| 75 | + registration.ready = true; |
34 | 76 | } catch (error) { |
35 | | - for (const event of added) { |
| 77 | + let rollbackError; |
| 78 | + for (const event of attachedEvents) { |
36 | 79 | try { |
37 | 80 | processObj[remove](event, handlers[event]); |
38 | | - } catch { |
39 | | - // Preserve the original registration failure while attempting all rollback removals. |
| 81 | + } catch (removalError) { |
| 82 | + rollbackError ??= removalError; |
40 | 83 | } |
41 | 84 | } |
| 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); |
42 | 91 | throw error; |
43 | 92 | } |
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); |
48 | 93 | if (oneShot && fired.size === selected.length) registration.removeHandlers(); |
49 | 94 | try { |
50 | | - if (signal) { |
| 95 | + if (signal && !registration.removed) { |
51 | 96 | if (typeof signal.addEventListener !== 'function') throw new TypeError('signal must provide addEventListener'); |
| 97 | + signal.addEventListener('abort', registration.removeHandlers, { once: true }); |
52 | 98 | if (signal.aborted) registration.removeHandlers(); |
53 | | - else signal.addEventListener('abort', registration.removeHandlers, { once: true }); |
54 | 99 | } |
55 | 100 | log.debug('Exception handlers registered'); |
56 | 101 | } catch (error) { |
57 | 102 | let cleanupFailed = false; |
58 | 103 | try { |
59 | 104 | registration.removeHandlers(); |
60 | | - } catch { |
| 105 | + } catch (cleanupError) { |
61 | 106 | // Preserve the original setup or logger failure. |
62 | 107 | cleanupFailed = true; |
| 108 | + attachFailure(error, 'cleanupError', cleanupError); |
| 109 | + } |
| 110 | + if (cleanupFailed) { |
| 111 | + registration.ready = true; |
| 112 | + registrations.set(processObj, registration); |
63 | 113 | } |
64 | | - if (cleanupFailed) registrations.set(processObj, registration); |
65 | 114 | else registrations.delete(processObj); |
66 | 115 | throw error; |
67 | 116 | } |
|
0 commit comments