@robota-sdk/agent-plugin 3.0.0-beta.64
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/LICENSE +21 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +1724 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +48 -0
- package/src/conversation-history/__tests__/conversation-history-plugin.test.ts +221 -0
- package/src/conversation-history/__tests__/history-storages.test.ts +115 -0
- package/src/conversation-history/conversation-history-helpers.ts +120 -0
- package/src/conversation-history/conversation-history-plugin.ts +294 -0
- package/src/conversation-history/index.ts +11 -0
- package/src/conversation-history/storages/database-storage.ts +96 -0
- package/src/conversation-history/storages/file-storage.ts +95 -0
- package/src/conversation-history/storages/index.ts +3 -0
- package/src/conversation-history/storages/memory-storage.ts +44 -0
- package/src/conversation-history/types.ts +64 -0
- package/src/error-handling/__tests__/error-handling-plugin.test.ts +201 -0
- package/src/error-handling/context-adapter.ts +48 -0
- package/src/error-handling/error-handling-helpers.ts +53 -0
- package/src/error-handling/error-handling-plugin.ts +293 -0
- package/src/error-handling/index.ts +9 -0
- package/src/error-handling/types.ts +82 -0
- package/src/execution-analytics/__tests__/execution-analytics-plugin.test.ts +224 -0
- package/src/execution-analytics/analytics-aggregation.ts +88 -0
- package/src/execution-analytics/execution-analytics-helpers.ts +83 -0
- package/src/execution-analytics/execution-analytics-plugin.ts +315 -0
- package/src/execution-analytics/index.ts +9 -0
- package/src/execution-analytics/types.ts +97 -0
- package/src/index.ts +8 -0
- package/src/limits/__tests__/limits-plugin.test.ts +712 -0
- package/src/limits/index.ts +9 -0
- package/src/limits/limits-helpers.ts +185 -0
- package/src/limits/limits-plugin.ts +196 -0
- package/src/limits/types.ts +73 -0
- package/src/limits/validation.ts +81 -0
- package/src/logging/__tests__/formatters.test.ts +48 -0
- package/src/logging/__tests__/logging-plugin.test.ts +464 -0
- package/src/logging/__tests__/logging-storages.test.ts +95 -0
- package/src/logging/formatters.ts +28 -0
- package/src/logging/index.ts +15 -0
- package/src/logging/logging-helpers.ts +223 -0
- package/src/logging/logging-plugin.ts +288 -0
- package/src/logging/storages/console-storage.ts +44 -0
- package/src/logging/storages/file-storage.ts +44 -0
- package/src/logging/storages/index.ts +4 -0
- package/src/logging/storages/remote-storage.ts +78 -0
- package/src/logging/storages/silent-storage.ts +18 -0
- package/src/logging/types.ts +106 -0
- package/src/performance/__tests__/memory-storage.test.ts +86 -0
- package/src/performance/__tests__/performance-plugin.test.ts +208 -0
- package/src/performance/__tests__/system-metrics-collector.test.ts +33 -0
- package/src/performance/collectors/system-metrics-collector.ts +69 -0
- package/src/performance/index.ts +12 -0
- package/src/performance/performance-helpers.ts +86 -0
- package/src/performance/performance-plugin.ts +274 -0
- package/src/performance/storages/index.ts +1 -0
- package/src/performance/storages/memory-storage.ts +88 -0
- package/src/performance/types.ts +160 -0
- package/src/usage/__tests__/aggregate-usage-stats.test.ts +136 -0
- package/src/usage/__tests__/memory-storage.test.ts +83 -0
- package/src/usage/__tests__/silent-storage.test.ts +44 -0
- package/src/usage/__tests__/usage-plugin-helpers.test.ts +155 -0
- package/src/usage/__tests__/usage-plugin.test.ts +358 -0
- package/src/usage/aggregate-usage-stats.ts +142 -0
- package/src/usage/index.ts +14 -0
- package/src/usage/storages/file-storage.ts +115 -0
- package/src/usage/storages/index.ts +4 -0
- package/src/usage/storages/memory-storage.ts +61 -0
- package/src/usage/storages/remote-storage.ts +143 -0
- package/src/usage/storages/silent-storage.ts +38 -0
- package/src/usage/types.ts +132 -0
- package/src/usage/usage-plugin-helpers.ts +116 -0
- package/src/usage/usage-plugin.ts +296 -0
- package/src/webhook/__tests__/webhook-plugin.test.ts +560 -0
- package/src/webhook/http-client.ts +141 -0
- package/src/webhook/index.ts +9 -0
- package/src/webhook/transformer.ts +209 -0
- package/src/webhook/types.ts +201 -0
- package/src/webhook/webhook-helpers.ts +60 -0
- package/src/webhook/webhook-plugin.ts +298 -0
- package/src/webhook/webhook-queue.ts +148 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Plugin - Facade Pattern Implementation
|
|
3
|
+
* Coordinates webhook functionality through clean, separated components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
AbstractPlugin,
|
|
8
|
+
PluginCategory,
|
|
9
|
+
PluginPriority,
|
|
10
|
+
type IPluginExecutionContext,
|
|
11
|
+
type IPluginExecutionResult,
|
|
12
|
+
type IPluginErrorContext,
|
|
13
|
+
createLogger,
|
|
14
|
+
type ILogger,
|
|
15
|
+
PluginError,
|
|
16
|
+
} from '@robota-sdk/agent-core';
|
|
17
|
+
|
|
18
|
+
import { WebhookTransformer } from './transformer';
|
|
19
|
+
import { WebhookHttpClient } from './http-client';
|
|
20
|
+
import {
|
|
21
|
+
validateWebhookEndpoints,
|
|
22
|
+
WEBHOOK_EXEC_EVENTS,
|
|
23
|
+
WEBHOOK_CONV_EVENTS,
|
|
24
|
+
WEBHOOK_TOOL_EVENTS,
|
|
25
|
+
WEBHOOK_ERROR_EVENTS,
|
|
26
|
+
} from './webhook-helpers';
|
|
27
|
+
import { WebhookQueueManager } from './webhook-queue';
|
|
28
|
+
import type {
|
|
29
|
+
TWebhookEventName,
|
|
30
|
+
IWebhookEventData,
|
|
31
|
+
TWebhookMetadata,
|
|
32
|
+
IWebhookPayload,
|
|
33
|
+
IWebhookEndpoint,
|
|
34
|
+
IWebhookPluginOptions,
|
|
35
|
+
IWebhookPluginStats,
|
|
36
|
+
} from './types';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sends HTTP webhook notifications for agent execution lifecycle events.
|
|
40
|
+
*
|
|
41
|
+
* Routes events to configured {@link IWebhookEndpoint | endpoints} with
|
|
42
|
+
* optional event filtering per endpoint. Supports asynchronous delivery with
|
|
43
|
+
* configurable concurrency, automatic retries via {@link WebhookHttpClient},
|
|
44
|
+
* and optional payload batching.
|
|
45
|
+
*
|
|
46
|
+
* Lifecycle hooks used: {@link AbstractPlugin.afterExecution | afterExecution},
|
|
47
|
+
* {@link AbstractPlugin.afterConversation | afterConversation},
|
|
48
|
+
* {@link AbstractPlugin.afterToolExecution | afterToolExecution},
|
|
49
|
+
* {@link AbstractPlugin.onError | onError}
|
|
50
|
+
*
|
|
51
|
+
* @extends AbstractPlugin
|
|
52
|
+
* @see IWebhookPluginOptions - configuration options
|
|
53
|
+
* @see WebhookTransformer - payload transformation utilities
|
|
54
|
+
* @see WebhookHttpClient - HTTP delivery client
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const plugin = new WebhookPlugin({
|
|
59
|
+
* endpoints: [{ url: 'https://example.com/hook' }],
|
|
60
|
+
* async: true,
|
|
61
|
+
* maxConcurrency: 3,
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export class WebhookPlugin extends AbstractPlugin<IWebhookPluginOptions, IWebhookPluginStats> {
|
|
66
|
+
name = 'WebhookPlugin';
|
|
67
|
+
version = '1.0.0';
|
|
68
|
+
|
|
69
|
+
private pluginOptions: Required<IWebhookPluginOptions>;
|
|
70
|
+
private logger: ILogger;
|
|
71
|
+
private queue: WebhookQueueManager;
|
|
72
|
+
|
|
73
|
+
constructor(options: IWebhookPluginOptions) {
|
|
74
|
+
super();
|
|
75
|
+
|
|
76
|
+
// Set plugin classification
|
|
77
|
+
this.category = PluginCategory.NOTIFICATION;
|
|
78
|
+
this.priority = PluginPriority.LOW;
|
|
79
|
+
|
|
80
|
+
// Validate required options
|
|
81
|
+
if (!options.endpoints || options.endpoints.length === 0) {
|
|
82
|
+
throw new PluginError('At least one webhook endpoint is required', this.name);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Set default options
|
|
86
|
+
this.pluginOptions = {
|
|
87
|
+
enabled: options.enabled ?? true,
|
|
88
|
+
events: [
|
|
89
|
+
WEBHOOK_EXEC_EVENTS.COMPLETE,
|
|
90
|
+
WEBHOOK_CONV_EVENTS.COMPLETE,
|
|
91
|
+
WEBHOOK_TOOL_EVENTS.EXECUTED,
|
|
92
|
+
WEBHOOK_ERROR_EVENTS.OCCURRED,
|
|
93
|
+
],
|
|
94
|
+
defaultTimeout: 5000,
|
|
95
|
+
defaultRetries: 3,
|
|
96
|
+
async: true,
|
|
97
|
+
maxConcurrency: 3,
|
|
98
|
+
batching: {
|
|
99
|
+
enabled: false,
|
|
100
|
+
maxSize: 10,
|
|
101
|
+
flushInterval: 5000,
|
|
102
|
+
},
|
|
103
|
+
payloadTransformer: WebhookTransformer.defaultPayloadTransformer,
|
|
104
|
+
// Add plugin options defaults
|
|
105
|
+
category: options.category ?? PluginCategory.NOTIFICATION,
|
|
106
|
+
priority: options.priority ?? PluginPriority.LOW,
|
|
107
|
+
moduleEvents: options.moduleEvents ?? [],
|
|
108
|
+
subscribeToAllModuleEvents: options.subscribeToAllModuleEvents ?? false,
|
|
109
|
+
...options,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
this.logger = createLogger(`${this.name}`);
|
|
113
|
+
const httpClient = new WebhookHttpClient(this.logger);
|
|
114
|
+
this.queue = new WebhookQueueManager(
|
|
115
|
+
this.logger,
|
|
116
|
+
httpClient,
|
|
117
|
+
this.pluginOptions.maxConcurrency,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
validateWebhookEndpoints(this.pluginOptions.endpoints, this.name, [
|
|
121
|
+
WEBHOOK_EXEC_EVENTS.START,
|
|
122
|
+
WEBHOOK_EXEC_EVENTS.COMPLETE,
|
|
123
|
+
WEBHOOK_EXEC_EVENTS.ERROR,
|
|
124
|
+
WEBHOOK_CONV_EVENTS.COMPLETE,
|
|
125
|
+
WEBHOOK_TOOL_EVENTS.EXECUTED,
|
|
126
|
+
WEBHOOK_ERROR_EVENTS.OCCURRED,
|
|
127
|
+
'custom',
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
if (this.pluginOptions.batching.enabled) {
|
|
131
|
+
this.queue.setupBatching(
|
|
132
|
+
this.pluginOptions.batching.flushInterval,
|
|
133
|
+
this.getEndpointsForEvent.bind(this),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.logger.info('WebhookPlugin initialized', {
|
|
138
|
+
endpointCount: this.pluginOptions.endpoints.length,
|
|
139
|
+
events: this.pluginOptions.events,
|
|
140
|
+
batching: this.pluginOptions.batching.enabled,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Sends an execution-complete webhook after the agent finishes processing.
|
|
146
|
+
*/
|
|
147
|
+
override async afterExecution(
|
|
148
|
+
context: IPluginExecutionContext,
|
|
149
|
+
result: IPluginExecutionResult,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
const webhookContext = WebhookTransformer.contextToWebhook(context);
|
|
152
|
+
const webhookResult = WebhookTransformer.resultToWebhook(result);
|
|
153
|
+
const eventData = WebhookTransformer.createExecutionData(webhookContext, webhookResult);
|
|
154
|
+
await this.sendWebhook(WEBHOOK_EXEC_EVENTS.COMPLETE, eventData);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Sends a conversation-complete webhook after a conversation round finishes.
|
|
159
|
+
*/
|
|
160
|
+
override async afterConversation(
|
|
161
|
+
context: IPluginExecutionContext,
|
|
162
|
+
result: IPluginExecutionResult,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const webhookContext = WebhookTransformer.contextToWebhook(context);
|
|
165
|
+
const webhookResult = WebhookTransformer.resultToWebhook(result);
|
|
166
|
+
const eventData = WebhookTransformer.createConversationData(webhookContext, webhookResult);
|
|
167
|
+
await this.sendWebhook(WEBHOOK_CONV_EVENTS.COMPLETE, eventData);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Sends a tool-executed webhook for each tool call in the result set.
|
|
172
|
+
*/
|
|
173
|
+
override async afterToolExecution(
|
|
174
|
+
context: IPluginExecutionContext,
|
|
175
|
+
toolResults: IPluginExecutionResult,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
const webhookContext = WebhookTransformer.contextToWebhook(context);
|
|
178
|
+
if (toolResults.toolCalls && toolResults.toolCalls.length > 0) {
|
|
179
|
+
for (const toolCall of toolResults.toolCalls) {
|
|
180
|
+
const toolData = {
|
|
181
|
+
toolName: toolCall.name || '',
|
|
182
|
+
toolId: toolCall.id || '',
|
|
183
|
+
result: toolCall.result,
|
|
184
|
+
success: toolCall.result !== null,
|
|
185
|
+
duration: toolResults.duration,
|
|
186
|
+
};
|
|
187
|
+
const eventData = WebhookTransformer.createToolData(webhookContext, toolData);
|
|
188
|
+
await this.sendWebhook(WEBHOOK_TOOL_EVENTS.EXECUTED, eventData);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Sends both an error-occurred and execution-error webhook on failure.
|
|
195
|
+
*/
|
|
196
|
+
override async onError(error: Error, context?: IPluginErrorContext): Promise<void> {
|
|
197
|
+
const webhookContext = context
|
|
198
|
+
? { executionId: context.executionId, sessionId: context.sessionId, userId: context.userId }
|
|
199
|
+
: { executionId: undefined, sessionId: undefined, userId: undefined };
|
|
200
|
+
const errorEventData = WebhookTransformer.createErrorData(webhookContext, error);
|
|
201
|
+
await this.sendWebhook(WEBHOOK_ERROR_EVENTS.OCCURRED, errorEventData);
|
|
202
|
+
await this.sendWebhook(WEBHOOK_EXEC_EVENTS.ERROR, errorEventData);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Builds a webhook payload and delivers it to all matching endpoints. When
|
|
207
|
+
* batching is enabled, payloads are queued and flushed at the configured interval.
|
|
208
|
+
*/
|
|
209
|
+
async sendWebhook(
|
|
210
|
+
event: TWebhookEventName,
|
|
211
|
+
data: IWebhookEventData,
|
|
212
|
+
metadata?: TWebhookMetadata,
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
if (!this.pluginOptions.events.includes(event)) return;
|
|
215
|
+
|
|
216
|
+
const payload: IWebhookPayload = {
|
|
217
|
+
event,
|
|
218
|
+
timestamp: new Date().toISOString(),
|
|
219
|
+
data: this.pluginOptions.payloadTransformer(event, data),
|
|
220
|
+
...(metadata && { metadata }),
|
|
221
|
+
};
|
|
222
|
+
if (data.executionId) payload.executionId = data.executionId;
|
|
223
|
+
if (data.sessionId) payload.sessionId = data.sessionId;
|
|
224
|
+
if (data.userId) payload.userId = data.userId;
|
|
225
|
+
|
|
226
|
+
this.logger.debug('Sending webhook', {
|
|
227
|
+
event,
|
|
228
|
+
endpointCount: this.getEndpointsForEvent(event).length,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (this.pluginOptions.batching.enabled) {
|
|
232
|
+
await this.queue.enqueueBatch(
|
|
233
|
+
payload,
|
|
234
|
+
this.pluginOptions.batching.maxSize,
|
|
235
|
+
this.getEndpointsForEvent.bind(this),
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
await this.queue.sendToEndpoints(
|
|
239
|
+
payload,
|
|
240
|
+
this.getEndpointsForEvent.bind(this),
|
|
241
|
+
this.pluginOptions.async,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Sends a webhook with the `custom` event type.
|
|
248
|
+
*/
|
|
249
|
+
async sendCustomWebhook(data: IWebhookEventData, metadata?: TWebhookMetadata): Promise<void> {
|
|
250
|
+
await this.sendWebhook('custom', data, metadata);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get webhook plugin statistics
|
|
255
|
+
*/
|
|
256
|
+
override getStats(): IWebhookPluginStats {
|
|
257
|
+
const base = super.getStats();
|
|
258
|
+
return {
|
|
259
|
+
...base,
|
|
260
|
+
endpointCount: this.pluginOptions.endpoints.length,
|
|
261
|
+
queueLength: this.queue.queueLength,
|
|
262
|
+
batchQueueLength: this.queue.batchQueueLength,
|
|
263
|
+
activeConcurrency: this.queue.activeConcurrency,
|
|
264
|
+
supportedEvents: this.pluginOptions.events,
|
|
265
|
+
totalSent: this.queue.totalSentCount,
|
|
266
|
+
totalErrors: this.queue.totalErrorCount,
|
|
267
|
+
averageResponseTime:
|
|
268
|
+
this.queue.totalSentCount > 0
|
|
269
|
+
? this.queue.totalResponseTime / this.queue.totalSentCount
|
|
270
|
+
: 0,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Clear webhook queue
|
|
276
|
+
*/
|
|
277
|
+
clearQueue(): void {
|
|
278
|
+
this.queue.clearQueues();
|
|
279
|
+
this.logger.info('Webhook queues cleared');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Flushes pending batches, clears request queues, and stops the batch timer.
|
|
284
|
+
*/
|
|
285
|
+
async destroy(): Promise<void> {
|
|
286
|
+
this.queue.stopBatchTimer();
|
|
287
|
+
await this.queue.drainBatch(this.getEndpointsForEvent.bind(this));
|
|
288
|
+
this.clearQueue();
|
|
289
|
+
this.logger.info('WebhookPlugin destroyed');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private getEndpointsForEvent(event: TWebhookEventName): IWebhookEndpoint[] {
|
|
293
|
+
return this.pluginOptions.endpoints.filter((endpoint) => {
|
|
294
|
+
if (!endpoint.events || endpoint.events.length === 0) return true;
|
|
295
|
+
return endpoint.events.includes(event);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Plugin - Queue and batch management.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from webhook-plugin.ts to keep each file under 300 lines.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type ILogger, type TTimerId } from '@robota-sdk/agent-core';
|
|
9
|
+
import type {
|
|
10
|
+
TWebhookEventName,
|
|
11
|
+
IWebhookPayload,
|
|
12
|
+
IWebhookEndpoint,
|
|
13
|
+
IWebhookRequest,
|
|
14
|
+
} from './types';
|
|
15
|
+
import { WebhookHttpClient } from './http-client';
|
|
16
|
+
|
|
17
|
+
/** Manages the async request queue, batch queue, and concurrency for WebhookPlugin. @internal */
|
|
18
|
+
export class WebhookQueueManager {
|
|
19
|
+
private requestQueue: IWebhookRequest[] = [];
|
|
20
|
+
private batchQueue: IWebhookPayload[] = [];
|
|
21
|
+
private batchTimer?: TTimerId;
|
|
22
|
+
activeConcurrency = 0;
|
|
23
|
+
totalSentCount = 0;
|
|
24
|
+
totalErrorCount = 0;
|
|
25
|
+
totalResponseTime = 0;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly logger: ILogger,
|
|
29
|
+
private readonly httpClient: WebhookHttpClient,
|
|
30
|
+
private readonly maxConcurrency: number,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
get queueLength(): number {
|
|
34
|
+
return this.requestQueue.length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get batchQueueLength(): number {
|
|
38
|
+
return this.batchQueue.length;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setupBatching(
|
|
42
|
+
flushInterval: number,
|
|
43
|
+
getEndpoints: (event: TWebhookEventName) => IWebhookEndpoint[],
|
|
44
|
+
): void {
|
|
45
|
+
this.batchTimer = setInterval(() => {
|
|
46
|
+
this.drainBatch(getEndpoints);
|
|
47
|
+
}, flushInterval);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async enqueueBatch(
|
|
51
|
+
payload: IWebhookPayload,
|
|
52
|
+
maxSize: number,
|
|
53
|
+
getEndpoints: (event: TWebhookEventName) => IWebhookEndpoint[],
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
this.batchQueue.push(payload);
|
|
56
|
+
if (this.batchQueue.length >= maxSize) {
|
|
57
|
+
await this.drainBatch(getEndpoints);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async sendToEndpoints(
|
|
62
|
+
payload: IWebhookPayload,
|
|
63
|
+
getEndpoints: (event: TWebhookEventName) => IWebhookEndpoint[],
|
|
64
|
+
async_: boolean = true,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
const endpoints = getEndpoints(payload.event);
|
|
67
|
+
if (endpoints.length === 0) return;
|
|
68
|
+
|
|
69
|
+
const requests: IWebhookRequest[] = endpoints.map((endpoint) => ({
|
|
70
|
+
endpoint,
|
|
71
|
+
payload,
|
|
72
|
+
attempt: 1,
|
|
73
|
+
timestamp: new Date(),
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
if (async_) {
|
|
77
|
+
this.requestQueue.push(...requests);
|
|
78
|
+
this.processQueue();
|
|
79
|
+
} else {
|
|
80
|
+
await Promise.allSettled(
|
|
81
|
+
requests.map((req) => {
|
|
82
|
+
const startTime = Date.now();
|
|
83
|
+
return this.httpClient
|
|
84
|
+
.sendRequest(req)
|
|
85
|
+
.then(() => {
|
|
86
|
+
this.totalSentCount++;
|
|
87
|
+
this.totalResponseTime += Date.now() - startTime;
|
|
88
|
+
})
|
|
89
|
+
.catch((error: unknown) => {
|
|
90
|
+
this.totalErrorCount++;
|
|
91
|
+
throw error;
|
|
92
|
+
});
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private processQueue(): void {
|
|
99
|
+
while (this.requestQueue.length > 0 && this.activeConcurrency < this.maxConcurrency) {
|
|
100
|
+
const request = this.requestQueue.shift();
|
|
101
|
+
if (!request) break;
|
|
102
|
+
|
|
103
|
+
this.activeConcurrency++;
|
|
104
|
+
const startTime = Date.now();
|
|
105
|
+
this.httpClient
|
|
106
|
+
.sendRequest(request)
|
|
107
|
+
.then(() => {
|
|
108
|
+
this.totalSentCount++;
|
|
109
|
+
this.totalResponseTime += Date.now() - startTime;
|
|
110
|
+
})
|
|
111
|
+
.catch((error: unknown) => {
|
|
112
|
+
this.totalErrorCount++;
|
|
113
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
+
this.logger.error('Webhook request failed', {
|
|
115
|
+
endpoint: request.endpoint.url,
|
|
116
|
+
event: request.payload.event,
|
|
117
|
+
error: message,
|
|
118
|
+
});
|
|
119
|
+
})
|
|
120
|
+
.finally(() => {
|
|
121
|
+
this.activeConcurrency--;
|
|
122
|
+
if (this.requestQueue.length > 0) this.processQueue();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async drainBatch(getEndpoints: (event: TWebhookEventName) => IWebhookEndpoint[]): Promise<void> {
|
|
128
|
+
if (this.batchQueue.length === 0) return;
|
|
129
|
+
const payloads = [...this.batchQueue];
|
|
130
|
+
this.batchQueue = [];
|
|
131
|
+
this.logger.debug('Flushing webhook batch', { payloadCount: payloads.length });
|
|
132
|
+
for (const payload of payloads) {
|
|
133
|
+
await this.sendToEndpoints(payload, getEndpoints);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
clearQueues(): void {
|
|
138
|
+
this.requestQueue = [];
|
|
139
|
+
this.batchQueue = [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
stopBatchTimer(): void {
|
|
143
|
+
if (this.batchTimer) {
|
|
144
|
+
clearInterval(this.batchTimer);
|
|
145
|
+
this.batchTimer = undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|