@ulthon/ul-opencode-event 0.1.4 → 0.1.5
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/dist/email.d.ts +1 -0
- package/dist/email.js +50 -28
- package/dist/handler.d.ts +2 -0
- package/dist/handler.js +41 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -0
- package/package.json +1 -1
package/dist/email.d.ts
CHANGED
package/dist/email.js
CHANGED
|
@@ -40,6 +40,30 @@ async function resolveHostViaLookup(host) {
|
|
|
40
40
|
catch { }
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates nodemailer transport options
|
|
45
|
+
*/
|
|
46
|
+
function buildTransportOptions(config, hostOverride) {
|
|
47
|
+
const host = hostOverride || config.host;
|
|
48
|
+
const transportOptions = {
|
|
49
|
+
host,
|
|
50
|
+
port: config.port,
|
|
51
|
+
secure: config.secure,
|
|
52
|
+
auth: {
|
|
53
|
+
user: config.auth.user,
|
|
54
|
+
pass: config.auth.pass,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
if (config.localAddress) {
|
|
58
|
+
transportOptions.localAddress = config.localAddress;
|
|
59
|
+
}
|
|
60
|
+
if (hostOverride && hostOverride !== config.host) {
|
|
61
|
+
transportOptions.tls = {
|
|
62
|
+
servername: config.host,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return transportOptions;
|
|
66
|
+
}
|
|
43
67
|
/**
|
|
44
68
|
* Creates an email sender for the given SMTP channel
|
|
45
69
|
* @param channel - The channel configuration (must be SMTP type)
|
|
@@ -60,30 +84,10 @@ export function createEmailSender(channel) {
|
|
|
60
84
|
}
|
|
61
85
|
const config = channel.config;
|
|
62
86
|
const fromName = channel.name || 'Notification';
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
port: config.port,
|
|
68
|
-
secure: config.secure,
|
|
69
|
-
auth: {
|
|
70
|
-
user: config.auth.user,
|
|
71
|
-
pass: config.auth.pass,
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
if (config.localAddress) {
|
|
75
|
-
transportOptions.localAddress = config.localAddress;
|
|
76
|
-
}
|
|
77
|
-
if (hostOverride && hostOverride !== config.host) {
|
|
78
|
-
transportOptions.tls = {
|
|
79
|
-
servername: config.host,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
const transport = nodemailer.createTransport(transportOptions);
|
|
83
|
-
transport.set('disableFileAccess', true);
|
|
84
|
-
transport.set('disableUrlAccess', true);
|
|
85
|
-
return transport;
|
|
86
|
-
};
|
|
87
|
+
// Create and cache the primary transporter (connection reuse)
|
|
88
|
+
const primaryTransport = nodemailer.createTransport(buildTransportOptions(config));
|
|
89
|
+
primaryTransport.set('disableFileAccess', true);
|
|
90
|
+
primaryTransport.set('disableUrlAccess', true);
|
|
87
91
|
return {
|
|
88
92
|
async send(subject, body) {
|
|
89
93
|
const mailOptions = {
|
|
@@ -93,30 +97,45 @@ export function createEmailSender(channel) {
|
|
|
93
97
|
html: body,
|
|
94
98
|
text: body,
|
|
95
99
|
};
|
|
100
|
+
// Try pre-resolved host first if DNS resolution succeeded
|
|
96
101
|
const preResolvedHost = await resolveHostViaLookup(config.host);
|
|
97
102
|
if (preResolvedHost && preResolvedHost !== config.host) {
|
|
103
|
+
let preResolvedTransport = null;
|
|
98
104
|
try {
|
|
99
|
-
|
|
105
|
+
// Create temporary transport for resolved IP (can't reuse for different host)
|
|
106
|
+
preResolvedTransport = nodemailer.createTransport(buildTransportOptions(config, preResolvedHost));
|
|
107
|
+
preResolvedTransport.set('disableFileAccess', true);
|
|
108
|
+
preResolvedTransport.set('disableUrlAccess', true);
|
|
100
109
|
await preResolvedTransport.sendMail(mailOptions);
|
|
101
110
|
return;
|
|
102
111
|
}
|
|
103
112
|
catch { }
|
|
113
|
+
finally {
|
|
114
|
+
preResolvedTransport?.close();
|
|
115
|
+
}
|
|
104
116
|
}
|
|
105
117
|
try {
|
|
106
|
-
|
|
107
|
-
await
|
|
118
|
+
// Use cached primary transporter (connection reuse)
|
|
119
|
+
await primaryTransport.sendMail(mailOptions);
|
|
108
120
|
}
|
|
109
121
|
catch (error) {
|
|
110
122
|
const message = error instanceof Error ? error.message : String(error);
|
|
111
123
|
if (shouldRetryWithLookup(message)) {
|
|
112
124
|
const resolvedHost = await resolveHostViaLookup(config.host);
|
|
113
125
|
if (resolvedHost) {
|
|
126
|
+
let fallbackTransport = null;
|
|
114
127
|
try {
|
|
115
|
-
|
|
128
|
+
// Create temporary transport for fallback (different host)
|
|
129
|
+
fallbackTransport = nodemailer.createTransport(buildTransportOptions(config, resolvedHost));
|
|
130
|
+
fallbackTransport.set('disableFileAccess', true);
|
|
131
|
+
fallbackTransport.set('disableUrlAccess', true);
|
|
116
132
|
await fallbackTransport.sendMail(mailOptions);
|
|
117
133
|
return;
|
|
118
134
|
}
|
|
119
135
|
catch { }
|
|
136
|
+
finally {
|
|
137
|
+
fallbackTransport?.close();
|
|
138
|
+
}
|
|
120
139
|
}
|
|
121
140
|
}
|
|
122
141
|
// Silent failure: log error but don't throw
|
|
@@ -124,5 +143,8 @@ export function createEmailSender(channel) {
|
|
|
124
143
|
console.error(`[${timestamp}] [${fromName}] Failed to send email:`, error);
|
|
125
144
|
}
|
|
126
145
|
},
|
|
146
|
+
async close() {
|
|
147
|
+
primaryTransport.close();
|
|
148
|
+
},
|
|
127
149
|
};
|
|
128
150
|
}
|
package/dist/handler.d.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
import type { NotificationConfig, EventPayload, EventType } from './types.js';
|
|
5
5
|
export interface EventHandler {
|
|
6
6
|
handle: (eventType: EventType, payload: EventPayload) => Promise<void>;
|
|
7
|
+
close: () => Promise<void>;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* Creates an event handler that dispatches notifications to all enabled channels
|
|
11
|
+
* Senders are cached at initialization for connection reuse)
|
|
10
12
|
*/
|
|
11
13
|
export declare function createEventHandler(config: NotificationConfig): EventHandler;
|
package/dist/handler.js
CHANGED
|
@@ -17,16 +17,27 @@ const defaultTemplates = {
|
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
19
|
* Creates an event handler that dispatches notifications to all enabled channels
|
|
20
|
+
* Senders are cached at initialization for connection reuse)
|
|
20
21
|
*/
|
|
21
22
|
export function createEventHandler(config) {
|
|
23
|
+
// Create and cache all senders at initialization (connection reuse)
|
|
24
|
+
const senders = new Map();
|
|
25
|
+
// map channel index to sender key for same order
|
|
26
|
+
const channelIndexMap = new Map();
|
|
27
|
+
for (let i = 0; i < config.channels.length; i++) {
|
|
28
|
+
const channel = config.channels[i];
|
|
29
|
+
const sender = createChannelSender(channel);
|
|
30
|
+
if (sender) {
|
|
31
|
+
const key = channel.name || `channel-${i}`;
|
|
32
|
+
senders.set(key, sender);
|
|
33
|
+
channelIndexMap.set(i, key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
22
36
|
return {
|
|
23
37
|
handle: async (eventType, payload) => {
|
|
24
|
-
// Process each channel
|
|
25
|
-
for (
|
|
26
|
-
|
|
27
|
-
if (channel.enabled === false) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
38
|
+
// Process each channel using cached senders
|
|
39
|
+
for (let i = 0; i < config.channels.length; i++) {
|
|
40
|
+
const channel = config.channels[i];
|
|
30
41
|
// Check if this event type is enabled for the channel
|
|
31
42
|
const eventEnabled = channel.events[eventType];
|
|
32
43
|
if (eventEnabled === false) {
|
|
@@ -34,7 +45,7 @@ export function createEventHandler(config) {
|
|
|
34
45
|
}
|
|
35
46
|
// Get template (channel-specific or default)
|
|
36
47
|
const template = channel.templates?.[eventType] ?? defaultTemplates[eventType];
|
|
37
|
-
//
|
|
48
|
+
// prepare template variables from payload
|
|
38
49
|
const variables = {
|
|
39
50
|
eventType: payload.eventType,
|
|
40
51
|
timestamp: payload.timestamp,
|
|
@@ -44,35 +55,42 @@ export function createEventHandler(config) {
|
|
|
44
55
|
error: payload.error,
|
|
45
56
|
duration: payload.duration,
|
|
46
57
|
};
|
|
47
|
-
//
|
|
58
|
+
// render templates
|
|
48
59
|
const subject = template.subject ? renderTemplate(template.subject, variables) : '';
|
|
49
60
|
const body = template.body ? renderTemplate(template.body, variables) : '';
|
|
50
|
-
//
|
|
51
|
-
|
|
61
|
+
// dispatch using cached sender
|
|
62
|
+
const key = channel.name || `channel-${i}`;
|
|
63
|
+
const sender = senders.get(key);
|
|
64
|
+
if (sender) {
|
|
65
|
+
await sender.send(subject, body);
|
|
66
|
+
}
|
|
52
67
|
}
|
|
53
68
|
},
|
|
69
|
+
close: async () => {
|
|
70
|
+
// close all cached senders (cleanup SMTP connections)
|
|
71
|
+
const closePromises = Array.from(senders.values()).map(sender => sender.close());
|
|
72
|
+
await Promise.all(closePromises);
|
|
73
|
+
senders.clear();
|
|
74
|
+
channelIndexMap.clear();
|
|
75
|
+
},
|
|
54
76
|
};
|
|
55
77
|
}
|
|
56
78
|
/**
|
|
57
|
-
*
|
|
79
|
+
* create a sender for the given channel type
|
|
58
80
|
*/
|
|
59
|
-
|
|
81
|
+
function createChannelSender(channel) {
|
|
60
82
|
switch (channel.type) {
|
|
61
|
-
case 'smtp':
|
|
62
|
-
|
|
63
|
-
if (sender) {
|
|
64
|
-
await sender.send(subject, body);
|
|
65
|
-
}
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
83
|
+
case 'smtp':
|
|
84
|
+
return createEmailSender(channel);
|
|
68
85
|
case 'feishu':
|
|
69
86
|
case 'dingtalk':
|
|
70
|
-
//
|
|
71
|
-
|
|
87
|
+
// skip unsupported channel types silently
|
|
88
|
+
return null;
|
|
72
89
|
default: {
|
|
73
|
-
//
|
|
90
|
+
// log warning for unknown channel types
|
|
74
91
|
const _exhaustiveCheck = channel.type;
|
|
75
|
-
console.warn(`[EventHandler]
|
|
92
|
+
console.warn(`[EventHandler] unsupported channel type: ${_exhaustiveCheck}`);
|
|
93
|
+
return null;
|
|
76
94
|
}
|
|
77
95
|
}
|
|
78
96
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -88,6 +88,21 @@ export const NotificationPlugin = async (ctx) => {
|
|
|
88
88
|
if (DEBUG_ENABLED) {
|
|
89
89
|
console.info(`${LOG_PREFIX} Plugin initialized for ${ctx.directory} with ${config.channels.length} channels`);
|
|
90
90
|
}
|
|
91
|
+
// Register cleanup handlers for graceful shutdown
|
|
92
|
+
const cleanup = async () => {
|
|
93
|
+
await eventHandler.close();
|
|
94
|
+
if (DEBUG_ENABLED) {
|
|
95
|
+
console.info(`${LOG_PREFIX} Closed all SMTP connections`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
// Handle process exit (sync handler, cannot be async)
|
|
99
|
+
process.on('exit', () => {
|
|
100
|
+
// Note: async cleanup won't work in 'exit' handler, but we try anyway
|
|
101
|
+
eventHandler.close().catch(() => { });
|
|
102
|
+
});
|
|
103
|
+
// Handle termination signals (async cleanup possible)
|
|
104
|
+
process.on('SIGINT', cleanup);
|
|
105
|
+
process.on('SIGTERM', cleanup);
|
|
91
106
|
return {
|
|
92
107
|
event: async ({ event }) => {
|
|
93
108
|
if (event.type === 'session.created') {
|