@k-msg/webhook 0.1.0 → 0.1.2
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/README.md +1 -1
- package/dist/dispatcher/batch.dispatcher.d.ts +68 -0
- package/dist/dispatcher/index.d.ts +9 -0
- package/dist/dispatcher/load-balancer.d.ts +104 -0
- package/dist/dispatcher/queue.manager.d.ts +80 -0
- package/dist/dispatcher/types.d.ts +49 -0
- package/dist/index.d.ts +18 -1134
- package/dist/index.js +43 -3016
- package/dist/index.js.map +89 -1
- package/dist/index.mjs +47 -0
- package/dist/index.mjs.map +89 -0
- package/dist/registry/delivery.store.d.ts +121 -0
- package/dist/registry/endpoint.manager.d.ts +92 -0
- package/dist/registry/event.store.d.ts +125 -0
- package/dist/registry/index.d.ts +9 -0
- package/dist/registry/types.d.ts +62 -0
- package/dist/retry/retry.manager.d.ts +61 -0
- package/dist/security/security.manager.d.ts +50 -0
- package/dist/services/webhook.dispatcher.d.ts +26 -0
- package/dist/services/webhook.registry.d.ts +16 -0
- package/dist/services/webhook.service.d.ts +78 -0
- package/dist/types/webhook.types.d.ts +219 -0
- package/package.json +18 -13
- package/dist/index.cjs +0 -3068
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -1135
package/dist/index.cjs
DELETED
|
@@ -1,3068 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/index.ts
|
|
31
|
-
var index_exports = {};
|
|
32
|
-
__export(index_exports, {
|
|
33
|
-
BatchDispatcher: () => BatchDispatcher,
|
|
34
|
-
DeliveryStore: () => DeliveryStore,
|
|
35
|
-
EndpointManager: () => EndpointManager,
|
|
36
|
-
EventStore: () => EventStore,
|
|
37
|
-
LoadBalancer: () => LoadBalancer,
|
|
38
|
-
QueueManager: () => QueueManager,
|
|
39
|
-
RetryManager: () => RetryManager,
|
|
40
|
-
SecurityManager: () => SecurityManager,
|
|
41
|
-
WebhookDispatcher: () => WebhookDispatcher,
|
|
42
|
-
WebhookEventType: () => WebhookEventType,
|
|
43
|
-
WebhookRegistry: () => WebhookRegistry,
|
|
44
|
-
WebhookService: () => WebhookService
|
|
45
|
-
});
|
|
46
|
-
module.exports = __toCommonJS(index_exports);
|
|
47
|
-
|
|
48
|
-
// src/types/webhook.types.ts
|
|
49
|
-
var import_zod = require("zod");
|
|
50
|
-
var WebhookEventType = /* @__PURE__ */ ((WebhookEventType2) => {
|
|
51
|
-
WebhookEventType2["MESSAGE_SENT"] = "message.sent";
|
|
52
|
-
WebhookEventType2["MESSAGE_DELIVERED"] = "message.delivered";
|
|
53
|
-
WebhookEventType2["MESSAGE_FAILED"] = "message.failed";
|
|
54
|
-
WebhookEventType2["MESSAGE_CLICKED"] = "message.clicked";
|
|
55
|
-
WebhookEventType2["MESSAGE_READ"] = "message.read";
|
|
56
|
-
WebhookEventType2["TEMPLATE_CREATED"] = "template.created";
|
|
57
|
-
WebhookEventType2["TEMPLATE_APPROVED"] = "template.approved";
|
|
58
|
-
WebhookEventType2["TEMPLATE_REJECTED"] = "template.rejected";
|
|
59
|
-
WebhookEventType2["TEMPLATE_UPDATED"] = "template.updated";
|
|
60
|
-
WebhookEventType2["TEMPLATE_DELETED"] = "template.deleted";
|
|
61
|
-
WebhookEventType2["CHANNEL_CREATED"] = "channel.created";
|
|
62
|
-
WebhookEventType2["CHANNEL_VERIFIED"] = "channel.verified";
|
|
63
|
-
WebhookEventType2["SENDER_NUMBER_ADDED"] = "sender_number.added";
|
|
64
|
-
WebhookEventType2["SENDER_NUMBER_VERIFIED"] = "sender_number.verified";
|
|
65
|
-
WebhookEventType2["QUOTA_WARNING"] = "system.quota_warning";
|
|
66
|
-
WebhookEventType2["QUOTA_EXCEEDED"] = "system.quota_exceeded";
|
|
67
|
-
WebhookEventType2["PROVIDER_ERROR"] = "system.provider_error";
|
|
68
|
-
WebhookEventType2["SYSTEM_MAINTENANCE"] = "system.maintenance";
|
|
69
|
-
WebhookEventType2["ANOMALY_DETECTED"] = "analytics.anomaly_detected";
|
|
70
|
-
WebhookEventType2["THRESHOLD_EXCEEDED"] = "analytics.threshold_exceeded";
|
|
71
|
-
return WebhookEventType2;
|
|
72
|
-
})(WebhookEventType || {});
|
|
73
|
-
var WebhookEventSchema = import_zod.z.object({
|
|
74
|
-
id: import_zod.z.string(),
|
|
75
|
-
type: import_zod.z.string(),
|
|
76
|
-
timestamp: import_zod.z.date(),
|
|
77
|
-
data: import_zod.z.any(),
|
|
78
|
-
metadata: import_zod.z.object({
|
|
79
|
-
providerId: import_zod.z.string().optional(),
|
|
80
|
-
channelId: import_zod.z.string().optional(),
|
|
81
|
-
templateId: import_zod.z.string().optional(),
|
|
82
|
-
messageId: import_zod.z.string().optional(),
|
|
83
|
-
userId: import_zod.z.string().optional(),
|
|
84
|
-
organizationId: import_zod.z.string().optional(),
|
|
85
|
-
correlationId: import_zod.z.string().optional(),
|
|
86
|
-
retryCount: import_zod.z.number().optional()
|
|
87
|
-
}),
|
|
88
|
-
version: import_zod.z.string()
|
|
89
|
-
});
|
|
90
|
-
var WebhookEndpointSchema = import_zod.z.object({
|
|
91
|
-
id: import_zod.z.string(),
|
|
92
|
-
url: import_zod.z.string().url(),
|
|
93
|
-
name: import_zod.z.string().optional(),
|
|
94
|
-
description: import_zod.z.string().optional(),
|
|
95
|
-
active: import_zod.z.boolean(),
|
|
96
|
-
events: import_zod.z.array(import_zod.z.string()),
|
|
97
|
-
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
|
|
98
|
-
secret: import_zod.z.string().optional(),
|
|
99
|
-
retryConfig: import_zod.z.object({
|
|
100
|
-
maxRetries: import_zod.z.number().min(0).max(10),
|
|
101
|
-
retryDelayMs: import_zod.z.number().min(1e3),
|
|
102
|
-
backoffMultiplier: import_zod.z.number().min(1).max(5)
|
|
103
|
-
}).optional(),
|
|
104
|
-
filters: import_zod.z.object({
|
|
105
|
-
providerId: import_zod.z.array(import_zod.z.string()).optional(),
|
|
106
|
-
channelId: import_zod.z.array(import_zod.z.string()).optional(),
|
|
107
|
-
templateId: import_zod.z.array(import_zod.z.string()).optional()
|
|
108
|
-
}).optional(),
|
|
109
|
-
createdAt: import_zod.z.date(),
|
|
110
|
-
updatedAt: import_zod.z.date(),
|
|
111
|
-
lastTriggeredAt: import_zod.z.date().optional(),
|
|
112
|
-
status: import_zod.z.enum(["active", "inactive", "error", "suspended"])
|
|
113
|
-
});
|
|
114
|
-
var WebhookDeliverySchema = import_zod.z.object({
|
|
115
|
-
id: import_zod.z.string(),
|
|
116
|
-
endpointId: import_zod.z.string(),
|
|
117
|
-
eventId: import_zod.z.string(),
|
|
118
|
-
url: import_zod.z.string().url(),
|
|
119
|
-
httpMethod: import_zod.z.enum(["POST", "PUT", "PATCH"]),
|
|
120
|
-
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()),
|
|
121
|
-
payload: import_zod.z.string(),
|
|
122
|
-
attempts: import_zod.z.array(import_zod.z.object({
|
|
123
|
-
attemptNumber: import_zod.z.number(),
|
|
124
|
-
timestamp: import_zod.z.date(),
|
|
125
|
-
httpStatus: import_zod.z.number().optional(),
|
|
126
|
-
responseBody: import_zod.z.string().optional(),
|
|
127
|
-
responseHeaders: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
|
|
128
|
-
error: import_zod.z.string().optional(),
|
|
129
|
-
latencyMs: import_zod.z.number()
|
|
130
|
-
})),
|
|
131
|
-
status: import_zod.z.enum(["pending", "success", "failed", "exhausted"]),
|
|
132
|
-
createdAt: import_zod.z.date(),
|
|
133
|
-
completedAt: import_zod.z.date().optional(),
|
|
134
|
-
nextRetryAt: import_zod.z.date().optional()
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// src/services/webhook.dispatcher.ts
|
|
138
|
-
var WebhookDispatcher = class {
|
|
139
|
-
config;
|
|
140
|
-
constructor(config) {
|
|
141
|
-
this.config = config;
|
|
142
|
-
}
|
|
143
|
-
async dispatch(event, endpoint) {
|
|
144
|
-
const delivery = {
|
|
145
|
-
id: this.generateDeliveryId(),
|
|
146
|
-
endpointId: endpoint.id,
|
|
147
|
-
eventId: event.id,
|
|
148
|
-
url: endpoint.url,
|
|
149
|
-
httpMethod: "POST",
|
|
150
|
-
headers: this.buildHeaders(endpoint, event),
|
|
151
|
-
payload: JSON.stringify(event),
|
|
152
|
-
attempts: [],
|
|
153
|
-
status: "pending",
|
|
154
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
155
|
-
};
|
|
156
|
-
await this.executeDelivery(delivery, endpoint);
|
|
157
|
-
return delivery;
|
|
158
|
-
}
|
|
159
|
-
async executeDelivery(delivery, endpoint) {
|
|
160
|
-
const maxRetries = endpoint.retryConfig?.maxRetries || this.config.maxRetries;
|
|
161
|
-
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
162
|
-
const attemptResult = await this.makeHttpRequest(delivery, endpoint, attempt);
|
|
163
|
-
delivery.attempts.push(attemptResult);
|
|
164
|
-
if (attemptResult.httpStatus && attemptResult.httpStatus >= 200 && attemptResult.httpStatus < 300) {
|
|
165
|
-
delivery.status = "success";
|
|
166
|
-
delivery.completedAt = /* @__PURE__ */ new Date();
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (attempt <= maxRetries) {
|
|
170
|
-
const delay = this.calculateRetryDelay(attempt, endpoint);
|
|
171
|
-
await this.sleep(delay);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
delivery.status = "exhausted";
|
|
175
|
-
delivery.completedAt = /* @__PURE__ */ new Date();
|
|
176
|
-
}
|
|
177
|
-
async makeHttpRequest(delivery, endpoint, attemptNumber) {
|
|
178
|
-
const startTime = Date.now();
|
|
179
|
-
const attempt = {
|
|
180
|
-
attemptNumber,
|
|
181
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
182
|
-
latencyMs: 0
|
|
183
|
-
};
|
|
184
|
-
try {
|
|
185
|
-
const response = await fetch(delivery.url, {
|
|
186
|
-
method: delivery.httpMethod,
|
|
187
|
-
headers: delivery.headers,
|
|
188
|
-
body: delivery.payload,
|
|
189
|
-
signal: AbortSignal.timeout(this.config.timeoutMs)
|
|
190
|
-
});
|
|
191
|
-
attempt.httpStatus = response.status;
|
|
192
|
-
attempt.responseBody = await response.text();
|
|
193
|
-
const responseHeaders = {};
|
|
194
|
-
response.headers.forEach((value, key) => {
|
|
195
|
-
responseHeaders[key] = value;
|
|
196
|
-
});
|
|
197
|
-
attempt.responseHeaders = responseHeaders;
|
|
198
|
-
attempt.latencyMs = Date.now() - startTime;
|
|
199
|
-
if (!response.ok) {
|
|
200
|
-
attempt.error = `HTTP ${response.status}: ${response.statusText}`;
|
|
201
|
-
}
|
|
202
|
-
} catch (error) {
|
|
203
|
-
attempt.latencyMs = Date.now() - startTime;
|
|
204
|
-
attempt.error = error instanceof Error ? error.message : "Unknown error";
|
|
205
|
-
}
|
|
206
|
-
return attempt;
|
|
207
|
-
}
|
|
208
|
-
buildHeaders(endpoint, event) {
|
|
209
|
-
const headers = {
|
|
210
|
-
"Content-Type": "application/json",
|
|
211
|
-
"X-Webhook-ID": event.id,
|
|
212
|
-
"X-Webhook-Event": event.type,
|
|
213
|
-
"X-Webhook-Timestamp": event.timestamp.toISOString(),
|
|
214
|
-
"User-Agent": "K-Message-Webhook/1.0"
|
|
215
|
-
};
|
|
216
|
-
if (endpoint.headers) {
|
|
217
|
-
Object.assign(headers, endpoint.headers);
|
|
218
|
-
}
|
|
219
|
-
if (endpoint.secret) {
|
|
220
|
-
const signature = this.generateSignature(JSON.stringify(event), endpoint.secret);
|
|
221
|
-
headers["X-Webhook-Signature"] = signature;
|
|
222
|
-
}
|
|
223
|
-
return headers;
|
|
224
|
-
}
|
|
225
|
-
generateSignature(payload, secret) {
|
|
226
|
-
return `sha256=${Buffer.from(payload + secret).toString("base64")}`;
|
|
227
|
-
}
|
|
228
|
-
calculateRetryDelay(attempt, endpoint) {
|
|
229
|
-
const baseDelay = endpoint.retryConfig?.retryDelayMs || this.config.retryDelayMs;
|
|
230
|
-
const multiplier = endpoint.retryConfig?.backoffMultiplier || 2;
|
|
231
|
-
return baseDelay * Math.pow(multiplier, attempt - 1);
|
|
232
|
-
}
|
|
233
|
-
sleep(ms) {
|
|
234
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
235
|
-
}
|
|
236
|
-
generateDeliveryId() {
|
|
237
|
-
return `delivery_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
238
|
-
}
|
|
239
|
-
async shutdown() {
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
// src/services/webhook.registry.ts
|
|
244
|
-
var WebhookRegistry = class {
|
|
245
|
-
endpoints = /* @__PURE__ */ new Map();
|
|
246
|
-
deliveries = /* @__PURE__ */ new Map();
|
|
247
|
-
async addEndpoint(endpoint) {
|
|
248
|
-
this.endpoints.set(endpoint.id, endpoint);
|
|
249
|
-
}
|
|
250
|
-
async updateEndpoint(endpointId, endpoint) {
|
|
251
|
-
if (!this.endpoints.has(endpointId)) {
|
|
252
|
-
throw new Error(`Endpoint ${endpointId} not found`);
|
|
253
|
-
}
|
|
254
|
-
this.endpoints.set(endpointId, endpoint);
|
|
255
|
-
}
|
|
256
|
-
async removeEndpoint(endpointId) {
|
|
257
|
-
this.endpoints.delete(endpointId);
|
|
258
|
-
}
|
|
259
|
-
async getEndpoint(endpointId) {
|
|
260
|
-
return this.endpoints.get(endpointId) || null;
|
|
261
|
-
}
|
|
262
|
-
async listEndpoints() {
|
|
263
|
-
return Array.from(this.endpoints.values());
|
|
264
|
-
}
|
|
265
|
-
async addDelivery(delivery) {
|
|
266
|
-
this.deliveries.set(delivery.id, delivery);
|
|
267
|
-
}
|
|
268
|
-
async getDeliveries(endpointId, timeRange, eventType, status, limit = 100) {
|
|
269
|
-
let deliveries = Array.from(this.deliveries.values());
|
|
270
|
-
if (endpointId) {
|
|
271
|
-
deliveries = deliveries.filter((d) => d.endpointId === endpointId);
|
|
272
|
-
}
|
|
273
|
-
if (timeRange) {
|
|
274
|
-
deliveries = deliveries.filter(
|
|
275
|
-
(d) => d.createdAt >= timeRange.start && d.createdAt <= timeRange.end
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
if (status) {
|
|
279
|
-
deliveries = deliveries.filter((d) => d.status === status);
|
|
280
|
-
}
|
|
281
|
-
return deliveries.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()).slice(0, limit);
|
|
282
|
-
}
|
|
283
|
-
async getFailedDeliveries(endpointId, eventType) {
|
|
284
|
-
return this.getDeliveries(endpointId, void 0, eventType, "failed");
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
// src/security/security.manager.ts
|
|
289
|
-
var crypto = __toESM(require("crypto"), 1);
|
|
290
|
-
var SecurityManager = class {
|
|
291
|
-
config;
|
|
292
|
-
constructor(webhookConfig) {
|
|
293
|
-
this.config = {
|
|
294
|
-
algorithm: webhookConfig.algorithm || "sha256",
|
|
295
|
-
header: webhookConfig.signatureHeader || "X-Webhook-Signature",
|
|
296
|
-
prefix: webhookConfig.signaturePrefix || "sha256="
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Webhook 페이로드에 대한 서명 생성
|
|
301
|
-
*/
|
|
302
|
-
generateSignature(payload, secret) {
|
|
303
|
-
const hmac = crypto.createHmac(this.config.algorithm, secret);
|
|
304
|
-
hmac.update(payload, "utf8");
|
|
305
|
-
const signature = hmac.digest("hex");
|
|
306
|
-
return this.config.prefix ? `${this.config.prefix}${signature}` : signature;
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Webhook 서명 검증
|
|
310
|
-
*/
|
|
311
|
-
verifySignature(payload, signature, secret) {
|
|
312
|
-
try {
|
|
313
|
-
const expectedSignature = this.generateSignature(payload, secret);
|
|
314
|
-
return this.constantTimeCompare(signature, expectedSignature);
|
|
315
|
-
} catch (error) {
|
|
316
|
-
console.error("Signature verification failed:", error);
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* HTTP 헤더에서 서명 추출
|
|
322
|
-
*/
|
|
323
|
-
extractSignature(headers) {
|
|
324
|
-
const headerName = this.config.header.toLowerCase();
|
|
325
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
326
|
-
if (key.toLowerCase() === headerName) {
|
|
327
|
-
return value;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Webhook 전송을 위한 보안 헤더 생성
|
|
334
|
-
*/
|
|
335
|
-
createSecurityHeaders(payload, secret) {
|
|
336
|
-
const signature = this.generateSignature(payload, secret);
|
|
337
|
-
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
338
|
-
return {
|
|
339
|
-
[this.config.header]: signature,
|
|
340
|
-
"X-Webhook-Timestamp": timestamp,
|
|
341
|
-
"X-Webhook-ID": this.generateWebhookId(),
|
|
342
|
-
"User-Agent": "K-Message-Webhook/1.0"
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* 타임스탬프 기반 재생 공격 방지 검증
|
|
347
|
-
*/
|
|
348
|
-
verifyTimestamp(timestamp, toleranceSeconds = 300) {
|
|
349
|
-
try {
|
|
350
|
-
const webhookTime = parseInt(timestamp, 10);
|
|
351
|
-
const currentTime = Math.floor(Date.now() / 1e3);
|
|
352
|
-
const timeDiff = Math.abs(currentTime - webhookTime);
|
|
353
|
-
return timeDiff <= toleranceSeconds;
|
|
354
|
-
} catch (error) {
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Webhook ID 생성 (추적용)
|
|
360
|
-
*/
|
|
361
|
-
generateWebhookId() {
|
|
362
|
-
return `wh_${crypto.randomBytes(16).toString("hex")}`;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Constant-time 문자열 비교 (타이밍 공격 방지)
|
|
366
|
-
*/
|
|
367
|
-
constantTimeCompare(a, b) {
|
|
368
|
-
if (a.length !== b.length) {
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
let result = 0;
|
|
372
|
-
for (let i = 0; i < a.length; i++) {
|
|
373
|
-
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
374
|
-
}
|
|
375
|
-
return result === 0;
|
|
376
|
-
}
|
|
377
|
-
/**
|
|
378
|
-
* 보안 설정 업데이트
|
|
379
|
-
*/
|
|
380
|
-
updateConfig(config) {
|
|
381
|
-
this.config = { ...this.config, ...config };
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* 현재 보안 설정 반환
|
|
385
|
-
*/
|
|
386
|
-
getConfig() {
|
|
387
|
-
return { ...this.config };
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
// src/retry/retry.manager.ts
|
|
392
|
-
var RetryManager = class {
|
|
393
|
-
config;
|
|
394
|
-
constructor(webhookConfig) {
|
|
395
|
-
this.config = {
|
|
396
|
-
maxRetries: webhookConfig.maxRetries,
|
|
397
|
-
baseDelayMs: webhookConfig.retryDelayMs,
|
|
398
|
-
maxDelayMs: webhookConfig.maxDelayMs || 3e5,
|
|
399
|
-
// 5분
|
|
400
|
-
backoffMultiplier: webhookConfig.backoffMultiplier || 2,
|
|
401
|
-
jitter: webhookConfig.jitter !== false
|
|
402
|
-
// 기본값 true
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* 다음 재시도 시간 계산
|
|
407
|
-
*/
|
|
408
|
-
calculateNextRetry(attemptNumber) {
|
|
409
|
-
if (attemptNumber >= this.config.maxRetries) {
|
|
410
|
-
throw new Error(`Maximum retry attempts (${this.config.maxRetries}) exceeded`);
|
|
411
|
-
}
|
|
412
|
-
let delay = this.config.baseDelayMs * Math.pow(this.config.backoffMultiplier, attemptNumber);
|
|
413
|
-
delay = Math.min(delay, this.config.maxDelayMs);
|
|
414
|
-
if (this.config.jitter) {
|
|
415
|
-
delay = delay * (0.5 + Math.random() * 0.5);
|
|
416
|
-
}
|
|
417
|
-
return new Date(Date.now() + delay);
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* 재시도 가능 여부 확인
|
|
421
|
-
*/
|
|
422
|
-
shouldRetry(attemptNumber, error) {
|
|
423
|
-
if (attemptNumber >= this.config.maxRetries) {
|
|
424
|
-
return false;
|
|
425
|
-
}
|
|
426
|
-
if (error) {
|
|
427
|
-
return this.isRetryableError(error);
|
|
428
|
-
}
|
|
429
|
-
return true;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* 재시도 가능한 에러인지 판단
|
|
433
|
-
*/
|
|
434
|
-
isRetryableError(error) {
|
|
435
|
-
const message = error.message.toLowerCase();
|
|
436
|
-
const retryableErrors = [
|
|
437
|
-
"timeout",
|
|
438
|
-
"network",
|
|
439
|
-
"connection",
|
|
440
|
-
"econnreset",
|
|
441
|
-
"enotfound",
|
|
442
|
-
"econnrefused",
|
|
443
|
-
"socket hang up"
|
|
444
|
-
];
|
|
445
|
-
return retryableErrors.some((keyword) => message.includes(keyword));
|
|
446
|
-
}
|
|
447
|
-
/**
|
|
448
|
-
* HTTP 상태 코드별 재시도 정책
|
|
449
|
-
*/
|
|
450
|
-
shouldRetryStatus(statusCode) {
|
|
451
|
-
if (statusCode >= 400 && statusCode < 500) {
|
|
452
|
-
const retryable4xx = [408, 429];
|
|
453
|
-
return retryable4xx.includes(statusCode);
|
|
454
|
-
}
|
|
455
|
-
if (statusCode >= 500) {
|
|
456
|
-
return true;
|
|
457
|
-
}
|
|
458
|
-
return false;
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* 재시도 통계 계산
|
|
462
|
-
*/
|
|
463
|
-
calculateRetryStats(attempts) {
|
|
464
|
-
if (attempts.length === 0) {
|
|
465
|
-
return {
|
|
466
|
-
totalAttempts: 0,
|
|
467
|
-
successfulAttempts: 0,
|
|
468
|
-
failedAttempts: 0,
|
|
469
|
-
averageDelayMs: 0,
|
|
470
|
-
totalTimeMs: 0
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
const successful = attempts.filter((a) => a.success).length;
|
|
474
|
-
const failed = attempts.length - successful;
|
|
475
|
-
let totalDelay = 0;
|
|
476
|
-
for (let i = 1; i < attempts.length; i++) {
|
|
477
|
-
totalDelay += attempts[i].timestamp.getTime() - attempts[i - 1].timestamp.getTime();
|
|
478
|
-
}
|
|
479
|
-
const averageDelay = attempts.length > 1 ? totalDelay / (attempts.length - 1) : 0;
|
|
480
|
-
const totalTime = attempts.length > 0 ? attempts[attempts.length - 1].timestamp.getTime() - attempts[0].timestamp.getTime() : 0;
|
|
481
|
-
return {
|
|
482
|
-
totalAttempts: attempts.length,
|
|
483
|
-
successfulAttempts: successful,
|
|
484
|
-
failedAttempts: failed,
|
|
485
|
-
averageDelayMs: averageDelay,
|
|
486
|
-
totalTimeMs: totalTime
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* 재시도 설정 업데이트
|
|
491
|
-
*/
|
|
492
|
-
updateConfig(config) {
|
|
493
|
-
this.config = { ...this.config, ...config };
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* 현재 재시도 설정 반환
|
|
497
|
-
*/
|
|
498
|
-
getConfig() {
|
|
499
|
-
return { ...this.config };
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* 백오프 지연 시간 계산 (테스트용)
|
|
503
|
-
*/
|
|
504
|
-
getBackoffDelay(attemptNumber) {
|
|
505
|
-
let delay = this.config.baseDelayMs * Math.pow(this.config.backoffMultiplier, attemptNumber);
|
|
506
|
-
return Math.min(delay, this.config.maxDelayMs);
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
// src/services/webhook.service.ts
|
|
511
|
-
var WebhookService = class {
|
|
512
|
-
config;
|
|
513
|
-
dispatcher;
|
|
514
|
-
registry;
|
|
515
|
-
securityManager;
|
|
516
|
-
retryManager;
|
|
517
|
-
eventQueue = [];
|
|
518
|
-
batchProcessor = null;
|
|
519
|
-
constructor(config) {
|
|
520
|
-
this.config = config;
|
|
521
|
-
this.dispatcher = new WebhookDispatcher(config);
|
|
522
|
-
this.registry = new WebhookRegistry();
|
|
523
|
-
this.securityManager = new SecurityManager(config);
|
|
524
|
-
this.retryManager = new RetryManager(config);
|
|
525
|
-
this.startBatchProcessor();
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* 웹훅 엔드포인트 등록
|
|
529
|
-
*/
|
|
530
|
-
async registerEndpoint(endpoint) {
|
|
531
|
-
await this.validateEndpointUrl(endpoint.url);
|
|
532
|
-
const newEndpoint = {
|
|
533
|
-
...endpoint,
|
|
534
|
-
id: this.generateEndpointId(),
|
|
535
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
536
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
537
|
-
status: "active"
|
|
538
|
-
};
|
|
539
|
-
await this.registry.addEndpoint(newEndpoint);
|
|
540
|
-
await this.testEndpoint(newEndpoint.id);
|
|
541
|
-
return newEndpoint;
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* 웹훅 엔드포인트 수정
|
|
545
|
-
*/
|
|
546
|
-
async updateEndpoint(endpointId, updates) {
|
|
547
|
-
const endpoint = await this.registry.getEndpoint(endpointId);
|
|
548
|
-
if (!endpoint) {
|
|
549
|
-
throw new Error(`Webhook endpoint ${endpointId} not found`);
|
|
550
|
-
}
|
|
551
|
-
if (updates.url && updates.url !== endpoint.url) {
|
|
552
|
-
await this.validateEndpointUrl(updates.url);
|
|
553
|
-
}
|
|
554
|
-
const updatedEndpoint = {
|
|
555
|
-
...endpoint,
|
|
556
|
-
...updates,
|
|
557
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
558
|
-
};
|
|
559
|
-
await this.registry.updateEndpoint(endpointId, updatedEndpoint);
|
|
560
|
-
return updatedEndpoint;
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* 웹훅 엔드포인트 삭제
|
|
564
|
-
*/
|
|
565
|
-
async deleteEndpoint(endpointId) {
|
|
566
|
-
await this.registry.removeEndpoint(endpointId);
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* 웹훅 엔드포인트 조회
|
|
570
|
-
*/
|
|
571
|
-
async getEndpoint(endpointId) {
|
|
572
|
-
return this.registry.getEndpoint(endpointId);
|
|
573
|
-
}
|
|
574
|
-
/**
|
|
575
|
-
* 모든 웹훅 엔드포인트 조회
|
|
576
|
-
*/
|
|
577
|
-
async listEndpoints() {
|
|
578
|
-
return this.registry.listEndpoints();
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* 이벤트 발생 (비동기 처리)
|
|
582
|
-
*/
|
|
583
|
-
async emit(event) {
|
|
584
|
-
if (!this.config.enabledEvents.includes(event.type)) {
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
this.validateEvent(event);
|
|
588
|
-
this.eventQueue.push(event);
|
|
589
|
-
if (this.eventQueue.length >= this.config.batchSize) {
|
|
590
|
-
await this.processBatch();
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
/**
|
|
594
|
-
* 이벤트 발생 (동기 처리)
|
|
595
|
-
*/
|
|
596
|
-
async emitSync(event) {
|
|
597
|
-
this.validateEvent(event);
|
|
598
|
-
const endpoints = await this.getMatchingEndpoints(event);
|
|
599
|
-
const deliveries = [];
|
|
600
|
-
for (const endpoint of endpoints) {
|
|
601
|
-
const delivery = await this.dispatcher.dispatch(event, endpoint);
|
|
602
|
-
deliveries.push(delivery);
|
|
603
|
-
}
|
|
604
|
-
return deliveries;
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* 웹훅 엔드포인트 테스트
|
|
608
|
-
*/
|
|
609
|
-
async testEndpoint(endpointId) {
|
|
610
|
-
const endpoint = await this.registry.getEndpoint(endpointId);
|
|
611
|
-
if (!endpoint) {
|
|
612
|
-
throw new Error(`Webhook endpoint ${endpointId} not found`);
|
|
613
|
-
}
|
|
614
|
-
const testEvent = {
|
|
615
|
-
id: `test_${Date.now()}`,
|
|
616
|
-
type: "system.maintenance" /* SYSTEM_MAINTENANCE */,
|
|
617
|
-
// 테스트용 이벤트
|
|
618
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
619
|
-
data: {
|
|
620
|
-
test: true,
|
|
621
|
-
message: "This is a test webhook"
|
|
622
|
-
},
|
|
623
|
-
metadata: {
|
|
624
|
-
correlationId: `test_${endpointId}`
|
|
625
|
-
},
|
|
626
|
-
version: "1.0"
|
|
627
|
-
};
|
|
628
|
-
const startTime = Date.now();
|
|
629
|
-
try {
|
|
630
|
-
const delivery = await this.dispatcher.dispatch(testEvent, endpoint);
|
|
631
|
-
const endTime = Date.now();
|
|
632
|
-
return {
|
|
633
|
-
endpointId,
|
|
634
|
-
url: endpoint.url,
|
|
635
|
-
success: delivery.status === "success",
|
|
636
|
-
httpStatus: delivery.attempts[0]?.httpStatus,
|
|
637
|
-
responseTime: endTime - startTime,
|
|
638
|
-
testedAt: /* @__PURE__ */ new Date()
|
|
639
|
-
};
|
|
640
|
-
} catch (error) {
|
|
641
|
-
const endTime = Date.now();
|
|
642
|
-
return {
|
|
643
|
-
endpointId,
|
|
644
|
-
url: endpoint.url,
|
|
645
|
-
success: false,
|
|
646
|
-
responseTime: endTime - startTime,
|
|
647
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
648
|
-
testedAt: /* @__PURE__ */ new Date()
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* 웹훅 통계 조회
|
|
654
|
-
*/
|
|
655
|
-
async getStats(endpointId, timeRange) {
|
|
656
|
-
const deliveries = await this.registry.getDeliveries(endpointId, timeRange);
|
|
657
|
-
const successful = deliveries.filter((d) => d.status === "success");
|
|
658
|
-
const failed = deliveries.filter((d) => d.status === "failed" || d.status === "exhausted");
|
|
659
|
-
const totalLatency = deliveries.reduce((sum, d) => {
|
|
660
|
-
const lastAttempt = d.attempts[d.attempts.length - 1];
|
|
661
|
-
return sum + (lastAttempt?.latencyMs || 0);
|
|
662
|
-
}, 0);
|
|
663
|
-
const eventBreakdown = {};
|
|
664
|
-
const errorBreakdown = {};
|
|
665
|
-
for (const delivery of deliveries) {
|
|
666
|
-
if (delivery.status === "failed" || delivery.status === "exhausted") {
|
|
667
|
-
const lastAttempt = delivery.attempts[delivery.attempts.length - 1];
|
|
668
|
-
const errorKey = lastAttempt?.error || `HTTP ${lastAttempt?.httpStatus || "Unknown"}`;
|
|
669
|
-
errorBreakdown[errorKey] = (errorBreakdown[errorKey] || 0) + 1;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
return {
|
|
673
|
-
endpointId,
|
|
674
|
-
timeRange,
|
|
675
|
-
totalDeliveries: deliveries.length,
|
|
676
|
-
successfulDeliveries: successful.length,
|
|
677
|
-
failedDeliveries: failed.length,
|
|
678
|
-
averageLatencyMs: deliveries.length > 0 ? totalLatency / deliveries.length : 0,
|
|
679
|
-
successRate: deliveries.length > 0 ? successful.length / deliveries.length * 100 : 0,
|
|
680
|
-
eventBreakdown,
|
|
681
|
-
errorBreakdown
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* 실패한 웹훅 재시도
|
|
686
|
-
*/
|
|
687
|
-
async retryFailed(endpointId, eventType) {
|
|
688
|
-
const failedDeliveries = await this.registry.getFailedDeliveries(endpointId, eventType);
|
|
689
|
-
let retriedCount = 0;
|
|
690
|
-
for (const delivery of failedDeliveries) {
|
|
691
|
-
const attemptCount = delivery.attempts.length;
|
|
692
|
-
if (this.retryManager.shouldRetry(attemptCount)) {
|
|
693
|
-
setTimeout(async () => {
|
|
694
|
-
const endpoint = await this.registry.getEndpoint(delivery.endpointId);
|
|
695
|
-
if (endpoint) {
|
|
696
|
-
await this.dispatcher.dispatch(
|
|
697
|
-
JSON.parse(delivery.payload),
|
|
698
|
-
endpoint
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
}, this.retryManager.getBackoffDelay(attemptCount));
|
|
702
|
-
retriedCount++;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
return retriedCount;
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* 웹훅 일시 중단
|
|
709
|
-
*/
|
|
710
|
-
async pauseEndpoint(endpointId) {
|
|
711
|
-
await this.updateEndpoint(endpointId, { status: "suspended" });
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* 웹훅 재개
|
|
715
|
-
*/
|
|
716
|
-
async resumeEndpoint(endpointId) {
|
|
717
|
-
await this.updateEndpoint(endpointId, { status: "active" });
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* 웹훅 전달 내역 조회
|
|
721
|
-
*/
|
|
722
|
-
async getDeliveries(endpointId, eventType, status, limit = 100) {
|
|
723
|
-
return this.registry.getDeliveries(endpointId, void 0, eventType, status, limit);
|
|
724
|
-
}
|
|
725
|
-
async processBatch() {
|
|
726
|
-
if (this.eventQueue.length === 0) {
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
const batch = this.eventQueue.splice(0, this.config.batchSize);
|
|
730
|
-
try {
|
|
731
|
-
for (const event of batch) {
|
|
732
|
-
const endpoints = await this.getMatchingEndpoints(event);
|
|
733
|
-
for (const endpoint of endpoints) {
|
|
734
|
-
this.dispatcher.dispatch(event, endpoint).catch((error) => {
|
|
735
|
-
console.error(`Failed to dispatch webhook to ${endpoint.url}:`, error);
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
} catch (error) {
|
|
740
|
-
console.error("Batch processing failed:", error);
|
|
741
|
-
this.eventQueue.unshift(...batch);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
async getMatchingEndpoints(event) {
|
|
745
|
-
const allEndpoints = await this.registry.listEndpoints();
|
|
746
|
-
return allEndpoints.filter((endpoint) => {
|
|
747
|
-
if (endpoint.status !== "active") {
|
|
748
|
-
return false;
|
|
749
|
-
}
|
|
750
|
-
if (!endpoint.events.includes(event.type)) {
|
|
751
|
-
return false;
|
|
752
|
-
}
|
|
753
|
-
if (endpoint.filters) {
|
|
754
|
-
if (endpoint.filters.providerId && event.metadata.providerId) {
|
|
755
|
-
if (!endpoint.filters.providerId.includes(event.metadata.providerId)) {
|
|
756
|
-
return false;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
if (endpoint.filters.channelId && event.metadata.channelId) {
|
|
760
|
-
if (!endpoint.filters.channelId.includes(event.metadata.channelId)) {
|
|
761
|
-
return false;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
if (endpoint.filters.templateId && event.metadata.templateId) {
|
|
765
|
-
if (!endpoint.filters.templateId.includes(event.metadata.templateId)) {
|
|
766
|
-
return false;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
return true;
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
validateEvent(event) {
|
|
774
|
-
if (!event.id) {
|
|
775
|
-
throw new Error("Event ID is required");
|
|
776
|
-
}
|
|
777
|
-
if (!event.type) {
|
|
778
|
-
throw new Error("Event type is required");
|
|
779
|
-
}
|
|
780
|
-
if (!event.timestamp) {
|
|
781
|
-
throw new Error("Event timestamp is required");
|
|
782
|
-
}
|
|
783
|
-
if (!event.version) {
|
|
784
|
-
throw new Error("Event version is required");
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
async validateEndpointUrl(url) {
|
|
788
|
-
try {
|
|
789
|
-
const parsedUrl = new URL(url);
|
|
790
|
-
if (parsedUrl.protocol !== "https:" && !url.includes("localhost") && !url.includes("127.0.0.1")) {
|
|
791
|
-
throw new Error("Webhook URL must use HTTPS");
|
|
792
|
-
}
|
|
793
|
-
if (process.env.NODE_ENV === "production") {
|
|
794
|
-
const hostname = parsedUrl.hostname;
|
|
795
|
-
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname.startsWith("192.168.") || hostname.startsWith("10.")) {
|
|
796
|
-
throw new Error("Private IP addresses are not allowed in production");
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
} catch (error) {
|
|
800
|
-
if (error instanceof Error) {
|
|
801
|
-
throw error;
|
|
802
|
-
}
|
|
803
|
-
throw new Error("Invalid webhook URL");
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
generateEndpointId() {
|
|
807
|
-
return `webhook_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
808
|
-
}
|
|
809
|
-
startBatchProcessor() {
|
|
810
|
-
this.batchProcessor = setInterval(async () => {
|
|
811
|
-
try {
|
|
812
|
-
await this.processBatch();
|
|
813
|
-
} catch (error) {
|
|
814
|
-
console.error("Batch processor error:", error);
|
|
815
|
-
}
|
|
816
|
-
}, this.config.batchTimeoutMs);
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* 서비스 종료 시 정리
|
|
820
|
-
*/
|
|
821
|
-
async shutdown() {
|
|
822
|
-
if (this.batchProcessor) {
|
|
823
|
-
clearInterval(this.batchProcessor);
|
|
824
|
-
this.batchProcessor = null;
|
|
825
|
-
}
|
|
826
|
-
if (this.eventQueue.length > 0) {
|
|
827
|
-
await this.processBatch();
|
|
828
|
-
}
|
|
829
|
-
await this.dispatcher.shutdown();
|
|
830
|
-
}
|
|
831
|
-
};
|
|
832
|
-
|
|
833
|
-
// src/dispatcher/batch.dispatcher.ts
|
|
834
|
-
var import_events = require("events");
|
|
835
|
-
var BatchDispatcher = class extends import_events.EventEmitter {
|
|
836
|
-
config;
|
|
837
|
-
pendingJobs = /* @__PURE__ */ new Map();
|
|
838
|
-
// endpointId -> jobs
|
|
839
|
-
activeBatches = /* @__PURE__ */ new Map();
|
|
840
|
-
batchProcessor = null;
|
|
841
|
-
defaultConfig = {
|
|
842
|
-
maxBatchSize: 100,
|
|
843
|
-
batchTimeoutMs: 5e3,
|
|
844
|
-
maxConcurrentBatches: 10,
|
|
845
|
-
enablePrioritization: true,
|
|
846
|
-
priorityLevels: 3
|
|
847
|
-
};
|
|
848
|
-
constructor(config = {}) {
|
|
849
|
-
super();
|
|
850
|
-
this.config = { ...this.defaultConfig, ...config };
|
|
851
|
-
this.startBatchProcessor();
|
|
852
|
-
}
|
|
853
|
-
/**
|
|
854
|
-
* 배치 작업 추가
|
|
855
|
-
*/
|
|
856
|
-
async addJob(job) {
|
|
857
|
-
const endpointId = job.endpoint.id;
|
|
858
|
-
if (!this.pendingJobs.has(endpointId)) {
|
|
859
|
-
this.pendingJobs.set(endpointId, []);
|
|
860
|
-
}
|
|
861
|
-
const jobs = this.pendingJobs.get(endpointId);
|
|
862
|
-
if (this.config.enablePrioritization) {
|
|
863
|
-
this.insertJobByPriority(jobs, job);
|
|
864
|
-
} else {
|
|
865
|
-
jobs.push(job);
|
|
866
|
-
}
|
|
867
|
-
if (jobs.length >= this.config.maxBatchSize) {
|
|
868
|
-
await this.processBatchForEndpoint(endpointId);
|
|
869
|
-
}
|
|
870
|
-
this.emit("jobAdded", { endpointId, jobId: job.id, queueSize: jobs.length });
|
|
871
|
-
}
|
|
872
|
-
/**
|
|
873
|
-
* 특정 엔드포인트의 배치 처리
|
|
874
|
-
*/
|
|
875
|
-
async processBatchForEndpoint(endpointId) {
|
|
876
|
-
const jobs = this.pendingJobs.get(endpointId);
|
|
877
|
-
if (!jobs || jobs.length === 0) {
|
|
878
|
-
return null;
|
|
879
|
-
}
|
|
880
|
-
if (this.activeBatches.size >= this.config.maxConcurrentBatches) {
|
|
881
|
-
this.emit("batchSkipped", { endpointId, reason: "max_concurrent_batches" });
|
|
882
|
-
return null;
|
|
883
|
-
}
|
|
884
|
-
const batchJobs = jobs.splice(0, this.config.maxBatchSize);
|
|
885
|
-
const batch = this.createBatch(endpointId, batchJobs);
|
|
886
|
-
this.activeBatches.set(batch.id, batch);
|
|
887
|
-
try {
|
|
888
|
-
this.emit("batchStarted", { batchId: batch.id, endpointId, jobCount: batchJobs.length });
|
|
889
|
-
await this.executeBatch(batch, batchJobs);
|
|
890
|
-
batch.status = "completed";
|
|
891
|
-
this.emit("batchCompleted", { batchId: batch.id, endpointId, success: true });
|
|
892
|
-
} catch (error) {
|
|
893
|
-
batch.status = "failed";
|
|
894
|
-
this.emit("batchFailed", { batchId: batch.id, endpointId, error: error instanceof Error ? error.message : "Unknown error" });
|
|
895
|
-
this.requeueFailedJobs(batchJobs);
|
|
896
|
-
} finally {
|
|
897
|
-
this.activeBatches.delete(batch.id);
|
|
898
|
-
}
|
|
899
|
-
return batch;
|
|
900
|
-
}
|
|
901
|
-
/**
|
|
902
|
-
* 모든 대기 중인 배치 처리
|
|
903
|
-
*/
|
|
904
|
-
async processAllBatches() {
|
|
905
|
-
const processedBatches = [];
|
|
906
|
-
const endpointIds = Array.from(this.pendingJobs.keys());
|
|
907
|
-
for (const endpointId of endpointIds) {
|
|
908
|
-
const batch = await this.processBatchForEndpoint(endpointId);
|
|
909
|
-
if (batch) {
|
|
910
|
-
processedBatches.push(batch);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
return processedBatches;
|
|
914
|
-
}
|
|
915
|
-
/**
|
|
916
|
-
* 배치 통계 조회
|
|
917
|
-
*/
|
|
918
|
-
getBatchStats() {
|
|
919
|
-
const endpointIds = Array.from(this.pendingJobs.keys());
|
|
920
|
-
const totalPendingJobs = endpointIds.reduce((sum, id) => {
|
|
921
|
-
return sum + (this.pendingJobs.get(id)?.length || 0);
|
|
922
|
-
}, 0);
|
|
923
|
-
return {
|
|
924
|
-
pendingJobsCount: totalPendingJobs,
|
|
925
|
-
activeBatchesCount: this.activeBatches.size,
|
|
926
|
-
endpointsWithPendingJobs: endpointIds.length,
|
|
927
|
-
averageQueueSize: endpointIds.length > 0 ? totalPendingJobs / endpointIds.length : 0
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* 특정 엔드포인트의 대기 중인 작업 수 조회
|
|
932
|
-
*/
|
|
933
|
-
getPendingJobCount(endpointId) {
|
|
934
|
-
return this.pendingJobs.get(endpointId)?.length || 0;
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* 배치 처리기 시작
|
|
938
|
-
*/
|
|
939
|
-
startBatchProcessor() {
|
|
940
|
-
this.batchProcessor = setInterval(async () => {
|
|
941
|
-
try {
|
|
942
|
-
await this.processAllBatches();
|
|
943
|
-
} catch (error) {
|
|
944
|
-
this.emit("processorError", error);
|
|
945
|
-
}
|
|
946
|
-
}, this.config.batchTimeoutMs);
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* 우선순위 기반 작업 삽입
|
|
950
|
-
*/
|
|
951
|
-
insertJobByPriority(jobs, newJob) {
|
|
952
|
-
let insertIndex = 0;
|
|
953
|
-
for (let i = 0; i < jobs.length; i++) {
|
|
954
|
-
if (jobs[i].priority <= newJob.priority) {
|
|
955
|
-
insertIndex = i;
|
|
956
|
-
break;
|
|
957
|
-
}
|
|
958
|
-
insertIndex = i + 1;
|
|
959
|
-
}
|
|
960
|
-
jobs.splice(insertIndex, 0, newJob);
|
|
961
|
-
}
|
|
962
|
-
/**
|
|
963
|
-
* 배치 생성
|
|
964
|
-
*/
|
|
965
|
-
createBatch(endpointId, jobs) {
|
|
966
|
-
return {
|
|
967
|
-
id: `batch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
968
|
-
endpointId,
|
|
969
|
-
events: jobs.map((job) => job.event),
|
|
970
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
971
|
-
scheduledAt: /* @__PURE__ */ new Date(),
|
|
972
|
-
status: "processing"
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
/**
|
|
976
|
-
* 배치 실행
|
|
977
|
-
*/
|
|
978
|
-
async executeBatch(batch, jobs) {
|
|
979
|
-
const endpoint = jobs[0]?.endpoint;
|
|
980
|
-
if (!endpoint) {
|
|
981
|
-
throw new Error("No endpoint found for batch");
|
|
982
|
-
}
|
|
983
|
-
const deliveryPromises = jobs.map((job) => this.executeJob(job));
|
|
984
|
-
try {
|
|
985
|
-
const deliveries = await Promise.allSettled(deliveryPromises);
|
|
986
|
-
const successful = deliveries.filter((result) => result.status === "fulfilled").length;
|
|
987
|
-
const failed = deliveries.length - successful;
|
|
988
|
-
this.emit("batchExecuted", {
|
|
989
|
-
batchId: batch.id,
|
|
990
|
-
endpointId: batch.endpointId,
|
|
991
|
-
total: deliveries.length,
|
|
992
|
-
successful,
|
|
993
|
-
failed
|
|
994
|
-
});
|
|
995
|
-
if (failed > 0) {
|
|
996
|
-
throw new Error(`Batch partially failed: ${failed}/${deliveries.length} jobs failed`);
|
|
997
|
-
}
|
|
998
|
-
} catch (error) {
|
|
999
|
-
this.emit("batchExecutionError", {
|
|
1000
|
-
batchId: batch.id,
|
|
1001
|
-
endpointId: batch.endpointId,
|
|
1002
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1003
|
-
});
|
|
1004
|
-
throw error;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
/**
|
|
1008
|
-
* 개별 작업 실행 (실제로는 WebhookDispatcher 사용)
|
|
1009
|
-
*/
|
|
1010
|
-
async executeJob(job) {
|
|
1011
|
-
const delivery = {
|
|
1012
|
-
id: `delivery_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1013
|
-
endpointId: job.endpoint.id,
|
|
1014
|
-
eventId: job.event.id,
|
|
1015
|
-
url: job.endpoint.url,
|
|
1016
|
-
httpMethod: "POST",
|
|
1017
|
-
headers: { "Content-Type": "application/json" },
|
|
1018
|
-
payload: JSON.stringify(job.event),
|
|
1019
|
-
attempts: [],
|
|
1020
|
-
status: "pending",
|
|
1021
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
1022
|
-
};
|
|
1023
|
-
const success = Math.random() > 0.1;
|
|
1024
|
-
delivery.attempts.push({
|
|
1025
|
-
attemptNumber: 1,
|
|
1026
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1027
|
-
httpStatus: success ? 200 : 500,
|
|
1028
|
-
responseBody: success ? "OK" : "Internal Server Error",
|
|
1029
|
-
error: success ? void 0 : "Server error",
|
|
1030
|
-
latencyMs: Math.floor(Math.random() * 1e3) + 100
|
|
1031
|
-
});
|
|
1032
|
-
delivery.status = success ? "success" : "failed";
|
|
1033
|
-
delivery.completedAt = /* @__PURE__ */ new Date();
|
|
1034
|
-
return delivery;
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* 실패한 작업들을 다시 큐에 추가
|
|
1038
|
-
*/
|
|
1039
|
-
requeueFailedJobs(jobs) {
|
|
1040
|
-
for (const job of jobs) {
|
|
1041
|
-
job.attempts++;
|
|
1042
|
-
if (job.attempts < job.maxAttempts) {
|
|
1043
|
-
const baseDelay = 1e3;
|
|
1044
|
-
const backoffMultiplier = 2;
|
|
1045
|
-
const delay = baseDelay * Math.pow(backoffMultiplier, job.attempts - 1);
|
|
1046
|
-
job.nextRetryAt = new Date(Date.now() + delay);
|
|
1047
|
-
job.scheduledAt = job.nextRetryAt;
|
|
1048
|
-
setTimeout(() => {
|
|
1049
|
-
this.addJob(job).catch((error) => {
|
|
1050
|
-
this.emit("requeueError", { jobId: job.id, error: error instanceof Error ? error.message : "Unknown error" });
|
|
1051
|
-
});
|
|
1052
|
-
}, delay);
|
|
1053
|
-
} else {
|
|
1054
|
-
this.emit("jobExhausted", { jobId: job.id, endpointId: job.endpoint.id, attempts: job.attempts });
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
/**
|
|
1059
|
-
* 배치 처리기 정지
|
|
1060
|
-
*/
|
|
1061
|
-
async shutdown() {
|
|
1062
|
-
if (this.batchProcessor) {
|
|
1063
|
-
clearInterval(this.batchProcessor);
|
|
1064
|
-
this.batchProcessor = null;
|
|
1065
|
-
}
|
|
1066
|
-
const maxWaitTime = 3e4;
|
|
1067
|
-
const startTime = Date.now();
|
|
1068
|
-
while (this.activeBatches.size > 0 && Date.now() - startTime < maxWaitTime) {
|
|
1069
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1070
|
-
}
|
|
1071
|
-
this.emit("shutdown", {
|
|
1072
|
-
pendingJobs: this.getBatchStats().pendingJobsCount,
|
|
1073
|
-
activeBatches: this.activeBatches.size
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
};
|
|
1077
|
-
|
|
1078
|
-
// src/dispatcher/queue.manager.ts
|
|
1079
|
-
var import_events2 = require("events");
|
|
1080
|
-
var fs = __toESM(require("fs/promises"), 1);
|
|
1081
|
-
var path = __toESM(require("path"), 1);
|
|
1082
|
-
var QueueManager = class extends import_events2.EventEmitter {
|
|
1083
|
-
config;
|
|
1084
|
-
queues = /* @__PURE__ */ new Map();
|
|
1085
|
-
// priority level -> jobs
|
|
1086
|
-
highPriorityQueue = [];
|
|
1087
|
-
mediumPriorityQueue = [];
|
|
1088
|
-
lowPriorityQueue = [];
|
|
1089
|
-
delayedJobs = /* @__PURE__ */ new Map();
|
|
1090
|
-
totalJobs = 0;
|
|
1091
|
-
defaultConfig = {
|
|
1092
|
-
maxQueueSize: 1e4,
|
|
1093
|
-
persistToDisk: false,
|
|
1094
|
-
compressionEnabled: false,
|
|
1095
|
-
ttlMs: 24 * 60 * 60 * 1e3
|
|
1096
|
-
// 24시간
|
|
1097
|
-
};
|
|
1098
|
-
constructor(config = {}) {
|
|
1099
|
-
super();
|
|
1100
|
-
this.config = { ...this.defaultConfig, ...config };
|
|
1101
|
-
this.queues.set("high", this.highPriorityQueue);
|
|
1102
|
-
this.queues.set("medium", this.mediumPriorityQueue);
|
|
1103
|
-
this.queues.set("low", this.lowPriorityQueue);
|
|
1104
|
-
if (this.config.persistToDisk && this.config.diskPath) {
|
|
1105
|
-
this.loadFromDisk().catch((error) => {
|
|
1106
|
-
this.emit("diskLoadError", error);
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
this.startTTLCleanup();
|
|
1110
|
-
}
|
|
1111
|
-
/**
|
|
1112
|
-
* 작업을 큐에 추가
|
|
1113
|
-
*/
|
|
1114
|
-
async enqueue(job) {
|
|
1115
|
-
if (this.totalJobs >= this.config.maxQueueSize) {
|
|
1116
|
-
this.emit("queueFull", { totalJobs: this.totalJobs, maxSize: this.config.maxQueueSize });
|
|
1117
|
-
return false;
|
|
1118
|
-
}
|
|
1119
|
-
if (job.scheduledAt > /* @__PURE__ */ new Date()) {
|
|
1120
|
-
await this.scheduleDelayedJob(job);
|
|
1121
|
-
return true;
|
|
1122
|
-
}
|
|
1123
|
-
const queueName = this.getQueueName(job.priority);
|
|
1124
|
-
const queue = this.queues.get(queueName);
|
|
1125
|
-
if (!queue) {
|
|
1126
|
-
throw new Error(`Invalid queue name: ${queueName}`);
|
|
1127
|
-
}
|
|
1128
|
-
queue.push(job);
|
|
1129
|
-
this.totalJobs++;
|
|
1130
|
-
this.emit("jobEnqueued", {
|
|
1131
|
-
jobId: job.id,
|
|
1132
|
-
priority: job.priority,
|
|
1133
|
-
queueName,
|
|
1134
|
-
totalJobs: this.totalJobs
|
|
1135
|
-
});
|
|
1136
|
-
if (this.config.persistToDisk) {
|
|
1137
|
-
await this.saveToDisk().catch((error) => {
|
|
1138
|
-
this.emit("diskSaveError", error);
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
return true;
|
|
1142
|
-
}
|
|
1143
|
-
/**
|
|
1144
|
-
* 우선순위에 따라 작업 추출
|
|
1145
|
-
*/
|
|
1146
|
-
async dequeue() {
|
|
1147
|
-
for (const [queueName, queue] of this.queues.entries()) {
|
|
1148
|
-
if (queue.length > 0) {
|
|
1149
|
-
const job = queue.shift();
|
|
1150
|
-
this.totalJobs--;
|
|
1151
|
-
this.emit("jobDequeued", {
|
|
1152
|
-
jobId: job.id,
|
|
1153
|
-
queueName,
|
|
1154
|
-
totalJobs: this.totalJobs
|
|
1155
|
-
});
|
|
1156
|
-
if (this.config.persistToDisk) {
|
|
1157
|
-
await this.saveToDisk().catch((error) => {
|
|
1158
|
-
this.emit("diskSaveError", error);
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
return job;
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
return null;
|
|
1165
|
-
}
|
|
1166
|
-
/**
|
|
1167
|
-
* 특정 우선순위 큐에서 작업 추출
|
|
1168
|
-
*/
|
|
1169
|
-
async dequeueFromPriority(priority) {
|
|
1170
|
-
const queueName = this.getQueueName(priority);
|
|
1171
|
-
const queue = this.queues.get(queueName);
|
|
1172
|
-
if (!queue || queue.length === 0) {
|
|
1173
|
-
return null;
|
|
1174
|
-
}
|
|
1175
|
-
const job = queue.shift();
|
|
1176
|
-
this.totalJobs--;
|
|
1177
|
-
this.emit("jobDequeued", {
|
|
1178
|
-
jobId: job.id,
|
|
1179
|
-
queueName,
|
|
1180
|
-
totalJobs: this.totalJobs
|
|
1181
|
-
});
|
|
1182
|
-
return job;
|
|
1183
|
-
}
|
|
1184
|
-
/**
|
|
1185
|
-
* 작업 상태 확인
|
|
1186
|
-
*/
|
|
1187
|
-
peek() {
|
|
1188
|
-
for (const queue of this.queues.values()) {
|
|
1189
|
-
if (queue.length > 0) {
|
|
1190
|
-
return queue[0];
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
return null;
|
|
1194
|
-
}
|
|
1195
|
-
/**
|
|
1196
|
-
* 특정 작업 제거
|
|
1197
|
-
*/
|
|
1198
|
-
async removeJob(jobId) {
|
|
1199
|
-
for (const [queueName, queue] of this.queues.entries()) {
|
|
1200
|
-
const index = queue.findIndex((job) => job.id === jobId);
|
|
1201
|
-
if (index !== -1) {
|
|
1202
|
-
queue.splice(index, 1);
|
|
1203
|
-
this.totalJobs--;
|
|
1204
|
-
this.emit("jobRemoved", {
|
|
1205
|
-
jobId,
|
|
1206
|
-
queueName,
|
|
1207
|
-
totalJobs: this.totalJobs
|
|
1208
|
-
});
|
|
1209
|
-
if (this.config.persistToDisk) {
|
|
1210
|
-
await this.saveToDisk().catch((error) => {
|
|
1211
|
-
this.emit("diskSaveError", error);
|
|
1212
|
-
});
|
|
1213
|
-
}
|
|
1214
|
-
return true;
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
const delayedTimeout = this.delayedJobs.get(jobId);
|
|
1218
|
-
if (delayedTimeout) {
|
|
1219
|
-
clearTimeout(delayedTimeout);
|
|
1220
|
-
this.delayedJobs.delete(jobId);
|
|
1221
|
-
this.emit("delayedJobCanceled", { jobId });
|
|
1222
|
-
return true;
|
|
1223
|
-
}
|
|
1224
|
-
return false;
|
|
1225
|
-
}
|
|
1226
|
-
/**
|
|
1227
|
-
* 큐 통계 조회
|
|
1228
|
-
*/
|
|
1229
|
-
getStats() {
|
|
1230
|
-
return {
|
|
1231
|
-
totalJobs: this.totalJobs,
|
|
1232
|
-
highPriorityJobs: this.highPriorityQueue.length,
|
|
1233
|
-
mediumPriorityJobs: this.mediumPriorityQueue.length,
|
|
1234
|
-
lowPriorityJobs: this.lowPriorityQueue.length,
|
|
1235
|
-
delayedJobs: this.delayedJobs.size,
|
|
1236
|
-
queueUtilization: this.totalJobs / this.config.maxQueueSize * 100
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
/**
|
|
1240
|
-
* 큐 비우기
|
|
1241
|
-
*/
|
|
1242
|
-
async clear() {
|
|
1243
|
-
for (const queue of this.queues.values()) {
|
|
1244
|
-
queue.length = 0;
|
|
1245
|
-
}
|
|
1246
|
-
for (const timeout of this.delayedJobs.values()) {
|
|
1247
|
-
clearTimeout(timeout);
|
|
1248
|
-
}
|
|
1249
|
-
this.delayedJobs.clear();
|
|
1250
|
-
this.totalJobs = 0;
|
|
1251
|
-
this.emit("queueCleared");
|
|
1252
|
-
if (this.config.persistToDisk) {
|
|
1253
|
-
await this.saveToDisk().catch((error) => {
|
|
1254
|
-
this.emit("diskSaveError", error);
|
|
1255
|
-
});
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
/**
|
|
1259
|
-
* 만료된 작업 정리
|
|
1260
|
-
*/
|
|
1261
|
-
async cleanupExpiredJobs() {
|
|
1262
|
-
const now = /* @__PURE__ */ new Date();
|
|
1263
|
-
let removedCount = 0;
|
|
1264
|
-
for (const [queueName, queue] of this.queues.entries()) {
|
|
1265
|
-
const initialLength = queue.length;
|
|
1266
|
-
for (let i = queue.length - 1; i >= 0; i--) {
|
|
1267
|
-
const job = queue[i];
|
|
1268
|
-
const age = now.getTime() - job.createdAt.getTime();
|
|
1269
|
-
if (age > this.config.ttlMs) {
|
|
1270
|
-
queue.splice(i, 1);
|
|
1271
|
-
this.totalJobs--;
|
|
1272
|
-
removedCount++;
|
|
1273
|
-
this.emit("jobExpired", {
|
|
1274
|
-
jobId: job.id,
|
|
1275
|
-
queueName,
|
|
1276
|
-
age
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
if (removedCount > 0) {
|
|
1282
|
-
this.emit("expiredJobsCleanup", { removedCount, totalJobs: this.totalJobs });
|
|
1283
|
-
if (this.config.persistToDisk) {
|
|
1284
|
-
await this.saveToDisk().catch((error) => {
|
|
1285
|
-
this.emit("diskSaveError", error);
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
return removedCount;
|
|
1290
|
-
}
|
|
1291
|
-
/**
|
|
1292
|
-
* 우선순위 숫자를 큐 이름으로 변환
|
|
1293
|
-
*/
|
|
1294
|
-
getQueueName(priority) {
|
|
1295
|
-
if (priority >= 8) return "high";
|
|
1296
|
-
if (priority >= 5) return "medium";
|
|
1297
|
-
return "low";
|
|
1298
|
-
}
|
|
1299
|
-
/**
|
|
1300
|
-
* 지연된 작업 스케줄링
|
|
1301
|
-
*/
|
|
1302
|
-
async scheduleDelayedJob(job) {
|
|
1303
|
-
const delay = job.scheduledAt.getTime() - Date.now();
|
|
1304
|
-
const timeout = setTimeout(async () => {
|
|
1305
|
-
this.delayedJobs.delete(job.id);
|
|
1306
|
-
const success = await this.enqueue({
|
|
1307
|
-
...job,
|
|
1308
|
-
scheduledAt: /* @__PURE__ */ new Date()
|
|
1309
|
-
// 즉시 처리 가능하도록 변경
|
|
1310
|
-
});
|
|
1311
|
-
if (success) {
|
|
1312
|
-
this.emit("delayedJobActivated", { jobId: job.id });
|
|
1313
|
-
}
|
|
1314
|
-
}, delay);
|
|
1315
|
-
this.delayedJobs.set(job.id, timeout);
|
|
1316
|
-
this.emit("jobScheduled", {
|
|
1317
|
-
jobId: job.id,
|
|
1318
|
-
scheduledAt: job.scheduledAt,
|
|
1319
|
-
delay
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
/**
|
|
1323
|
-
* TTL 정리 작업 시작
|
|
1324
|
-
*/
|
|
1325
|
-
startTTLCleanup() {
|
|
1326
|
-
setInterval(async () => {
|
|
1327
|
-
try {
|
|
1328
|
-
await this.cleanupExpiredJobs();
|
|
1329
|
-
} catch (error) {
|
|
1330
|
-
this.emit("cleanupError", error);
|
|
1331
|
-
}
|
|
1332
|
-
}, 5 * 60 * 1e3);
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* 디스크에 큐 상태 저장
|
|
1336
|
-
*/
|
|
1337
|
-
async saveToDisk() {
|
|
1338
|
-
if (!this.config.diskPath) return;
|
|
1339
|
-
try {
|
|
1340
|
-
const data = {
|
|
1341
|
-
queues: {
|
|
1342
|
-
high: this.highPriorityQueue,
|
|
1343
|
-
medium: this.mediumPriorityQueue,
|
|
1344
|
-
low: this.lowPriorityQueue
|
|
1345
|
-
},
|
|
1346
|
-
totalJobs: this.totalJobs,
|
|
1347
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1348
|
-
};
|
|
1349
|
-
const json = JSON.stringify(data, null, 2);
|
|
1350
|
-
const filePath = path.join(this.config.diskPath, "webhook-queue.json");
|
|
1351
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1352
|
-
await fs.writeFile(filePath, json, "utf8");
|
|
1353
|
-
this.emit("diskSaved", { filePath, totalJobs: this.totalJobs });
|
|
1354
|
-
} catch (error) {
|
|
1355
|
-
this.emit("diskSaveError", error);
|
|
1356
|
-
throw error;
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* 디스크에서 큐 상태 로드
|
|
1361
|
-
*/
|
|
1362
|
-
async loadFromDisk() {
|
|
1363
|
-
if (!this.config.diskPath) return;
|
|
1364
|
-
try {
|
|
1365
|
-
const filePath = path.join(this.config.diskPath, "webhook-queue.json");
|
|
1366
|
-
const json = await fs.readFile(filePath, "utf8");
|
|
1367
|
-
const data = JSON.parse(json);
|
|
1368
|
-
this.highPriorityQueue.length = 0;
|
|
1369
|
-
this.mediumPriorityQueue.length = 0;
|
|
1370
|
-
this.lowPriorityQueue.length = 0;
|
|
1371
|
-
this.highPriorityQueue.push(...data.queues.high || []);
|
|
1372
|
-
this.mediumPriorityQueue.push(...data.queues.medium || []);
|
|
1373
|
-
this.lowPriorityQueue.push(...data.queues.low || []);
|
|
1374
|
-
this.totalJobs = data.totalJobs || 0;
|
|
1375
|
-
this.emit("diskLoaded", {
|
|
1376
|
-
filePath,
|
|
1377
|
-
totalJobs: this.totalJobs,
|
|
1378
|
-
timestamp: data.timestamp
|
|
1379
|
-
});
|
|
1380
|
-
} catch (error) {
|
|
1381
|
-
if (error.code !== "ENOENT") {
|
|
1382
|
-
this.emit("diskLoadError", error);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
/**
|
|
1387
|
-
* 큐 관리자 종료
|
|
1388
|
-
*/
|
|
1389
|
-
async shutdown() {
|
|
1390
|
-
for (const timeout of this.delayedJobs.values()) {
|
|
1391
|
-
clearTimeout(timeout);
|
|
1392
|
-
}
|
|
1393
|
-
this.delayedJobs.clear();
|
|
1394
|
-
if (this.config.persistToDisk) {
|
|
1395
|
-
await this.saveToDisk().catch((error) => {
|
|
1396
|
-
this.emit("diskSaveError", error);
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1399
|
-
this.emit("shutdown", { totalJobs: this.totalJobs });
|
|
1400
|
-
}
|
|
1401
|
-
};
|
|
1402
|
-
|
|
1403
|
-
// src/dispatcher/load-balancer.ts
|
|
1404
|
-
var import_events3 = require("events");
|
|
1405
|
-
var LoadBalancer = class extends import_events3.EventEmitter {
|
|
1406
|
-
config;
|
|
1407
|
-
endpointHealth = /* @__PURE__ */ new Map();
|
|
1408
|
-
circuitBreakers = /* @__PURE__ */ new Map();
|
|
1409
|
-
connectionCounts = /* @__PURE__ */ new Map();
|
|
1410
|
-
roundRobinIndex = 0;
|
|
1411
|
-
healthCheckInterval = null;
|
|
1412
|
-
defaultConfig = {
|
|
1413
|
-
strategy: "round-robin",
|
|
1414
|
-
healthCheckInterval: 3e4,
|
|
1415
|
-
// 30초
|
|
1416
|
-
healthCheckTimeoutMs: 5e3,
|
|
1417
|
-
weights: {}
|
|
1418
|
-
};
|
|
1419
|
-
constructor(config = {}) {
|
|
1420
|
-
super();
|
|
1421
|
-
this.config = { ...this.defaultConfig, ...config };
|
|
1422
|
-
this.startHealthChecks();
|
|
1423
|
-
}
|
|
1424
|
-
/**
|
|
1425
|
-
* 엔드포인트 등록
|
|
1426
|
-
*/
|
|
1427
|
-
async registerEndpoint(endpoint) {
|
|
1428
|
-
const health = {
|
|
1429
|
-
endpointId: endpoint.id,
|
|
1430
|
-
isHealthy: true,
|
|
1431
|
-
consecutiveFailures: 0,
|
|
1432
|
-
lastHealthCheckAt: /* @__PURE__ */ new Date(),
|
|
1433
|
-
averageResponseTime: 0,
|
|
1434
|
-
activeConnections: 0
|
|
1435
|
-
};
|
|
1436
|
-
this.endpointHealth.set(endpoint.id, health);
|
|
1437
|
-
this.connectionCounts.set(endpoint.id, 0);
|
|
1438
|
-
await this.checkEndpointHealth(endpoint);
|
|
1439
|
-
this.emit("endpointRegistered", { endpointId: endpoint.id, isHealthy: health.isHealthy });
|
|
1440
|
-
}
|
|
1441
|
-
/**
|
|
1442
|
-
* 엔드포인트 등록 해제
|
|
1443
|
-
*/
|
|
1444
|
-
async unregisterEndpoint(endpointId) {
|
|
1445
|
-
this.endpointHealth.delete(endpointId);
|
|
1446
|
-
this.circuitBreakers.delete(endpointId);
|
|
1447
|
-
this.connectionCounts.delete(endpointId);
|
|
1448
|
-
this.emit("endpointUnregistered", { endpointId });
|
|
1449
|
-
}
|
|
1450
|
-
/**
|
|
1451
|
-
* 로드 밸런싱을 통한 엔드포인트 선택
|
|
1452
|
-
*/
|
|
1453
|
-
async selectEndpoint(endpoints) {
|
|
1454
|
-
const healthyEndpoints = endpoints.filter((endpoint) => {
|
|
1455
|
-
const health = this.endpointHealth.get(endpoint.id);
|
|
1456
|
-
const circuitBreaker = this.circuitBreakers.get(endpoint.id);
|
|
1457
|
-
return health?.isHealthy && endpoint.status === "active" && circuitBreaker?.state !== "open";
|
|
1458
|
-
});
|
|
1459
|
-
if (healthyEndpoints.length === 0) {
|
|
1460
|
-
const halfOpenEndpoint = this.tryHalfOpenEndpoint(endpoints);
|
|
1461
|
-
if (halfOpenEndpoint) {
|
|
1462
|
-
return halfOpenEndpoint;
|
|
1463
|
-
}
|
|
1464
|
-
this.emit("noHealthyEndpoints", { totalEndpoints: endpoints.length });
|
|
1465
|
-
return null;
|
|
1466
|
-
}
|
|
1467
|
-
let selectedEndpoint;
|
|
1468
|
-
switch (this.config.strategy) {
|
|
1469
|
-
case "round-robin":
|
|
1470
|
-
selectedEndpoint = this.selectRoundRobin(healthyEndpoints);
|
|
1471
|
-
break;
|
|
1472
|
-
case "least-connections":
|
|
1473
|
-
selectedEndpoint = this.selectLeastConnections(healthyEndpoints);
|
|
1474
|
-
break;
|
|
1475
|
-
case "weighted":
|
|
1476
|
-
selectedEndpoint = this.selectWeighted(healthyEndpoints);
|
|
1477
|
-
break;
|
|
1478
|
-
case "random":
|
|
1479
|
-
selectedEndpoint = this.selectRandom(healthyEndpoints);
|
|
1480
|
-
break;
|
|
1481
|
-
default:
|
|
1482
|
-
selectedEndpoint = healthyEndpoints[0];
|
|
1483
|
-
}
|
|
1484
|
-
this.incrementConnections(selectedEndpoint.id);
|
|
1485
|
-
this.emit("endpointSelected", {
|
|
1486
|
-
endpointId: selectedEndpoint.id,
|
|
1487
|
-
strategy: this.config.strategy,
|
|
1488
|
-
availableEndpoints: healthyEndpoints.length
|
|
1489
|
-
});
|
|
1490
|
-
return selectedEndpoint;
|
|
1491
|
-
}
|
|
1492
|
-
/**
|
|
1493
|
-
* 요청 완료 시 호출 (연결 수 감소 및 통계 업데이트)
|
|
1494
|
-
*/
|
|
1495
|
-
async onRequestComplete(endpointId, success, responseTime) {
|
|
1496
|
-
this.decrementConnections(endpointId);
|
|
1497
|
-
const health = this.endpointHealth.get(endpointId);
|
|
1498
|
-
if (health) {
|
|
1499
|
-
if (health.averageResponseTime === 0) {
|
|
1500
|
-
health.averageResponseTime = responseTime;
|
|
1501
|
-
} else {
|
|
1502
|
-
health.averageResponseTime = health.averageResponseTime * 0.8 + responseTime * 0.2;
|
|
1503
|
-
}
|
|
1504
|
-
if (success) {
|
|
1505
|
-
health.consecutiveFailures = 0;
|
|
1506
|
-
health.isHealthy = true;
|
|
1507
|
-
const circuitBreaker = this.circuitBreakers.get(endpointId);
|
|
1508
|
-
if (circuitBreaker) {
|
|
1509
|
-
if (circuitBreaker.state === "half-open") {
|
|
1510
|
-
circuitBreaker.state = "closed";
|
|
1511
|
-
circuitBreaker.failureCount = 0;
|
|
1512
|
-
this.emit("circuitBreakerClosed", { endpointId });
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
} else {
|
|
1516
|
-
health.consecutiveFailures++;
|
|
1517
|
-
if (health.consecutiveFailures >= 3) {
|
|
1518
|
-
health.isHealthy = false;
|
|
1519
|
-
this.emit("endpointUnhealthy", { endpointId, consecutiveFailures: health.consecutiveFailures });
|
|
1520
|
-
}
|
|
1521
|
-
this.updateCircuitBreaker(endpointId, false);
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
this.emit("requestCompleted", {
|
|
1525
|
-
endpointId,
|
|
1526
|
-
success,
|
|
1527
|
-
responseTime,
|
|
1528
|
-
averageResponseTime: health?.averageResponseTime
|
|
1529
|
-
});
|
|
1530
|
-
}
|
|
1531
|
-
/**
|
|
1532
|
-
* 엔드포인트 건강 상태 조회
|
|
1533
|
-
*/
|
|
1534
|
-
getEndpointHealth(endpointId) {
|
|
1535
|
-
return this.endpointHealth.get(endpointId) || null;
|
|
1536
|
-
}
|
|
1537
|
-
/**
|
|
1538
|
-
* 모든 엔드포인트 건강 상태 조회
|
|
1539
|
-
*/
|
|
1540
|
-
getAllEndpointHealth() {
|
|
1541
|
-
return Array.from(this.endpointHealth.values());
|
|
1542
|
-
}
|
|
1543
|
-
/**
|
|
1544
|
-
* 로드 밸런서 통계 조회
|
|
1545
|
-
*/
|
|
1546
|
-
getStats() {
|
|
1547
|
-
const healths = Array.from(this.endpointHealth.values());
|
|
1548
|
-
const totalConnections = Array.from(this.connectionCounts.values()).reduce((sum, count) => sum + count, 0);
|
|
1549
|
-
const circuitBreakersOpen = Array.from(this.circuitBreakers.values()).filter((cb) => cb.state === "open").length;
|
|
1550
|
-
const avgResponseTime = healths.length > 0 ? healths.reduce((sum, h) => sum + h.averageResponseTime, 0) / healths.length : 0;
|
|
1551
|
-
return {
|
|
1552
|
-
totalEndpoints: healths.length,
|
|
1553
|
-
healthyEndpoints: healths.filter((h) => h.isHealthy).length,
|
|
1554
|
-
activeConnections: totalConnections,
|
|
1555
|
-
circuitBreakersOpen,
|
|
1556
|
-
averageResponseTime: avgResponseTime
|
|
1557
|
-
};
|
|
1558
|
-
}
|
|
1559
|
-
/**
|
|
1560
|
-
* Round Robin 전략
|
|
1561
|
-
*/
|
|
1562
|
-
selectRoundRobin(endpoints) {
|
|
1563
|
-
const endpoint = endpoints[this.roundRobinIndex % endpoints.length];
|
|
1564
|
-
this.roundRobinIndex = (this.roundRobinIndex + 1) % endpoints.length;
|
|
1565
|
-
return endpoint;
|
|
1566
|
-
}
|
|
1567
|
-
/**
|
|
1568
|
-
* Least Connections 전략
|
|
1569
|
-
*/
|
|
1570
|
-
selectLeastConnections(endpoints) {
|
|
1571
|
-
return endpoints.reduce((least, current) => {
|
|
1572
|
-
const leastConnections = this.connectionCounts.get(least.id) || 0;
|
|
1573
|
-
const currentConnections = this.connectionCounts.get(current.id) || 0;
|
|
1574
|
-
return currentConnections < leastConnections ? current : least;
|
|
1575
|
-
});
|
|
1576
|
-
}
|
|
1577
|
-
/**
|
|
1578
|
-
* Weighted 전략
|
|
1579
|
-
*/
|
|
1580
|
-
selectWeighted(endpoints) {
|
|
1581
|
-
const weights = this.config.weights || {};
|
|
1582
|
-
const totalWeight = endpoints.reduce((sum, endpoint) => {
|
|
1583
|
-
return sum + (weights[endpoint.id] || 1);
|
|
1584
|
-
}, 0);
|
|
1585
|
-
let random = Math.random() * totalWeight;
|
|
1586
|
-
for (const endpoint of endpoints) {
|
|
1587
|
-
const weight = weights[endpoint.id] || 1;
|
|
1588
|
-
random -= weight;
|
|
1589
|
-
if (random <= 0) {
|
|
1590
|
-
return endpoint;
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
return endpoints[0];
|
|
1594
|
-
}
|
|
1595
|
-
/**
|
|
1596
|
-
* Random 전략
|
|
1597
|
-
*/
|
|
1598
|
-
selectRandom(endpoints) {
|
|
1599
|
-
const randomIndex = Math.floor(Math.random() * endpoints.length);
|
|
1600
|
-
return endpoints[randomIndex];
|
|
1601
|
-
}
|
|
1602
|
-
/**
|
|
1603
|
-
* Half-open Circuit Breaker 엔드포인트 시도
|
|
1604
|
-
*/
|
|
1605
|
-
tryHalfOpenEndpoint(endpoints) {
|
|
1606
|
-
const now = /* @__PURE__ */ new Date();
|
|
1607
|
-
for (const endpoint of endpoints) {
|
|
1608
|
-
const circuitBreaker = this.circuitBreakers.get(endpoint.id);
|
|
1609
|
-
if (circuitBreaker?.state === "open" && circuitBreaker.nextRetryTime && now >= circuitBreaker.nextRetryTime) {
|
|
1610
|
-
circuitBreaker.state = "half-open";
|
|
1611
|
-
this.emit("circuitBreakerHalfOpen", { endpointId: endpoint.id });
|
|
1612
|
-
return endpoint;
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
return null;
|
|
1616
|
-
}
|
|
1617
|
-
/**
|
|
1618
|
-
* Circuit Breaker 상태 업데이트
|
|
1619
|
-
*/
|
|
1620
|
-
updateCircuitBreaker(endpointId, success) {
|
|
1621
|
-
let circuitBreaker = this.circuitBreakers.get(endpointId);
|
|
1622
|
-
if (!circuitBreaker) {
|
|
1623
|
-
circuitBreaker = {
|
|
1624
|
-
endpointId,
|
|
1625
|
-
state: "closed",
|
|
1626
|
-
failureCount: 0
|
|
1627
|
-
};
|
|
1628
|
-
this.circuitBreakers.set(endpointId, circuitBreaker);
|
|
1629
|
-
}
|
|
1630
|
-
if (!success) {
|
|
1631
|
-
circuitBreaker.failureCount++;
|
|
1632
|
-
circuitBreaker.lastFailureTime = /* @__PURE__ */ new Date();
|
|
1633
|
-
if (circuitBreaker.failureCount >= 5 && circuitBreaker.state === "closed") {
|
|
1634
|
-
circuitBreaker.state = "open";
|
|
1635
|
-
circuitBreaker.nextRetryTime = new Date(Date.now() + 6e4);
|
|
1636
|
-
this.emit("circuitBreakerOpened", {
|
|
1637
|
-
endpointId,
|
|
1638
|
-
failureCount: circuitBreaker.failureCount,
|
|
1639
|
-
nextRetryTime: circuitBreaker.nextRetryTime
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
/**
|
|
1645
|
-
* 연결 수 증가
|
|
1646
|
-
*/
|
|
1647
|
-
incrementConnections(endpointId) {
|
|
1648
|
-
const currentCount = this.connectionCounts.get(endpointId) || 0;
|
|
1649
|
-
this.connectionCounts.set(endpointId, currentCount + 1);
|
|
1650
|
-
const health = this.endpointHealth.get(endpointId);
|
|
1651
|
-
if (health) {
|
|
1652
|
-
health.activeConnections = currentCount + 1;
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
/**
|
|
1656
|
-
* 연결 수 감소
|
|
1657
|
-
*/
|
|
1658
|
-
decrementConnections(endpointId) {
|
|
1659
|
-
const currentCount = this.connectionCounts.get(endpointId) || 0;
|
|
1660
|
-
const newCount = Math.max(0, currentCount - 1);
|
|
1661
|
-
this.connectionCounts.set(endpointId, newCount);
|
|
1662
|
-
const health = this.endpointHealth.get(endpointId);
|
|
1663
|
-
if (health) {
|
|
1664
|
-
health.activeConnections = newCount;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
/**
|
|
1668
|
-
* 엔드포인트 건강 상태 확인
|
|
1669
|
-
*/
|
|
1670
|
-
async checkEndpointHealth(endpoint) {
|
|
1671
|
-
const startTime = Date.now();
|
|
1672
|
-
try {
|
|
1673
|
-
const response = await fetch(endpoint.url, {
|
|
1674
|
-
method: "HEAD",
|
|
1675
|
-
signal: AbortSignal.timeout(this.config.healthCheckTimeoutMs)
|
|
1676
|
-
});
|
|
1677
|
-
const responseTime = Date.now() - startTime;
|
|
1678
|
-
const success = response.ok;
|
|
1679
|
-
await this.onRequestComplete(endpoint.id, success, responseTime);
|
|
1680
|
-
this.emit("healthCheckCompleted", {
|
|
1681
|
-
endpointId: endpoint.id,
|
|
1682
|
-
success,
|
|
1683
|
-
responseTime,
|
|
1684
|
-
httpStatus: response.status
|
|
1685
|
-
});
|
|
1686
|
-
} catch (error) {
|
|
1687
|
-
const responseTime = Date.now() - startTime;
|
|
1688
|
-
await this.onRequestComplete(endpoint.id, false, responseTime);
|
|
1689
|
-
this.emit("healthCheckFailed", {
|
|
1690
|
-
endpointId: endpoint.id,
|
|
1691
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
1692
|
-
responseTime
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
/**
|
|
1697
|
-
* 건강 상태 확인 시작
|
|
1698
|
-
*/
|
|
1699
|
-
startHealthChecks() {
|
|
1700
|
-
this.healthCheckInterval = setInterval(async () => {
|
|
1701
|
-
const endpoints = Array.from(this.endpointHealth.keys());
|
|
1702
|
-
for (const endpointId of endpoints) {
|
|
1703
|
-
const health = this.endpointHealth.get(endpointId);
|
|
1704
|
-
if (health) {
|
|
1705
|
-
const mockEndpoint = {
|
|
1706
|
-
id: endpointId,
|
|
1707
|
-
url: `https://webhook.example.com/${endpointId}`,
|
|
1708
|
-
active: true,
|
|
1709
|
-
events: [],
|
|
1710
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1711
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
1712
|
-
status: "active"
|
|
1713
|
-
};
|
|
1714
|
-
await this.checkEndpointHealth(mockEndpoint);
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
}, this.config.healthCheckInterval);
|
|
1718
|
-
}
|
|
1719
|
-
/**
|
|
1720
|
-
* 로드 밸런서 종료
|
|
1721
|
-
*/
|
|
1722
|
-
async shutdown() {
|
|
1723
|
-
if (this.healthCheckInterval) {
|
|
1724
|
-
clearInterval(this.healthCheckInterval);
|
|
1725
|
-
this.healthCheckInterval = null;
|
|
1726
|
-
}
|
|
1727
|
-
this.emit("shutdown", {
|
|
1728
|
-
totalEndpoints: this.endpointHealth.size,
|
|
1729
|
-
activeConnections: Array.from(this.connectionCounts.values()).reduce((sum, count) => sum + count, 0)
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
};
|
|
1733
|
-
|
|
1734
|
-
// src/registry/endpoint.manager.ts
|
|
1735
|
-
var import_events4 = require("events");
|
|
1736
|
-
var fs2 = __toESM(require("fs/promises"), 1);
|
|
1737
|
-
var path2 = __toESM(require("path"), 1);
|
|
1738
|
-
var EndpointManager = class extends import_events4.EventEmitter {
|
|
1739
|
-
config;
|
|
1740
|
-
endpoints = /* @__PURE__ */ new Map();
|
|
1741
|
-
indexByUrl = /* @__PURE__ */ new Map();
|
|
1742
|
-
// url -> id
|
|
1743
|
-
indexByEvent = /* @__PURE__ */ new Map();
|
|
1744
|
-
// event -> endpoint ids
|
|
1745
|
-
indexByStatus = /* @__PURE__ */ new Map();
|
|
1746
|
-
// status -> endpoint ids
|
|
1747
|
-
defaultConfig = {
|
|
1748
|
-
type: "memory",
|
|
1749
|
-
retentionDays: 90,
|
|
1750
|
-
enableEncryption: false
|
|
1751
|
-
};
|
|
1752
|
-
constructor(config = {}) {
|
|
1753
|
-
super();
|
|
1754
|
-
this.config = { ...this.defaultConfig, ...config };
|
|
1755
|
-
this.initializeIndexes();
|
|
1756
|
-
if (this.config.type === "file" && this.config.filePath) {
|
|
1757
|
-
this.loadFromFile().catch((error) => {
|
|
1758
|
-
this.emit("loadError", error);
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
/**
|
|
1763
|
-
* 엔드포인트 추가
|
|
1764
|
-
*/
|
|
1765
|
-
async addEndpoint(endpoint) {
|
|
1766
|
-
if (this.indexByUrl.has(endpoint.url)) {
|
|
1767
|
-
const existingId = this.indexByUrl.get(endpoint.url);
|
|
1768
|
-
if (existingId !== endpoint.id) {
|
|
1769
|
-
throw new Error(`Endpoint with URL ${endpoint.url} already exists with different ID`);
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
const existingEndpoint = this.endpoints.get(endpoint.id);
|
|
1773
|
-
if (existingEndpoint) {
|
|
1774
|
-
this.removeFromIndexes(existingEndpoint);
|
|
1775
|
-
}
|
|
1776
|
-
this.endpoints.set(endpoint.id, endpoint);
|
|
1777
|
-
this.addToIndexes(endpoint);
|
|
1778
|
-
if (this.config.type === "file") {
|
|
1779
|
-
await this.saveToFile();
|
|
1780
|
-
}
|
|
1781
|
-
this.emit("endpointAdded", { endpointId: endpoint.id, url: endpoint.url });
|
|
1782
|
-
}
|
|
1783
|
-
/**
|
|
1784
|
-
* 엔드포인트 업데이트
|
|
1785
|
-
*/
|
|
1786
|
-
async updateEndpoint(endpointId, updates) {
|
|
1787
|
-
const existingEndpoint = this.endpoints.get(endpointId);
|
|
1788
|
-
if (!existingEndpoint) {
|
|
1789
|
-
throw new Error(`Endpoint ${endpointId} not found`);
|
|
1790
|
-
}
|
|
1791
|
-
if (updates.url && updates.url !== existingEndpoint.url) {
|
|
1792
|
-
if (this.indexByUrl.has(updates.url)) {
|
|
1793
|
-
const existingId = this.indexByUrl.get(updates.url);
|
|
1794
|
-
if (existingId !== endpointId) {
|
|
1795
|
-
throw new Error(`Endpoint with URL ${updates.url} already exists`);
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
this.removeFromIndexes(existingEndpoint);
|
|
1800
|
-
const updatedEndpoint = {
|
|
1801
|
-
...existingEndpoint,
|
|
1802
|
-
...updates,
|
|
1803
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1804
|
-
};
|
|
1805
|
-
this.endpoints.set(endpointId, updatedEndpoint);
|
|
1806
|
-
this.addToIndexes(updatedEndpoint);
|
|
1807
|
-
if (this.config.type === "file") {
|
|
1808
|
-
await this.saveToFile();
|
|
1809
|
-
}
|
|
1810
|
-
this.emit("endpointUpdated", {
|
|
1811
|
-
endpointId,
|
|
1812
|
-
changes: Object.keys(updates),
|
|
1813
|
-
oldUrl: existingEndpoint.url,
|
|
1814
|
-
newUrl: updatedEndpoint.url
|
|
1815
|
-
});
|
|
1816
|
-
return updatedEndpoint;
|
|
1817
|
-
}
|
|
1818
|
-
/**
|
|
1819
|
-
* 엔드포인트 제거
|
|
1820
|
-
*/
|
|
1821
|
-
async removeEndpoint(endpointId) {
|
|
1822
|
-
const endpoint = this.endpoints.get(endpointId);
|
|
1823
|
-
if (!endpoint) {
|
|
1824
|
-
return false;
|
|
1825
|
-
}
|
|
1826
|
-
this.removeFromIndexes(endpoint);
|
|
1827
|
-
this.endpoints.delete(endpointId);
|
|
1828
|
-
if (this.config.type === "file") {
|
|
1829
|
-
await this.saveToFile();
|
|
1830
|
-
}
|
|
1831
|
-
this.emit("endpointRemoved", { endpointId, url: endpoint.url });
|
|
1832
|
-
return true;
|
|
1833
|
-
}
|
|
1834
|
-
/**
|
|
1835
|
-
* 엔드포인트 조회
|
|
1836
|
-
*/
|
|
1837
|
-
async getEndpoint(endpointId) {
|
|
1838
|
-
return this.endpoints.get(endpointId) || null;
|
|
1839
|
-
}
|
|
1840
|
-
/**
|
|
1841
|
-
* URL로 엔드포인트 조회
|
|
1842
|
-
*/
|
|
1843
|
-
async getEndpointByUrl(url) {
|
|
1844
|
-
const endpointId = this.indexByUrl.get(url);
|
|
1845
|
-
return endpointId ? this.endpoints.get(endpointId) || null : null;
|
|
1846
|
-
}
|
|
1847
|
-
/**
|
|
1848
|
-
* 필터 조건에 맞는 엔드포인트 검색
|
|
1849
|
-
*/
|
|
1850
|
-
async searchEndpoints(filter = {}, pagination = { page: 1, limit: 100 }) {
|
|
1851
|
-
let candidateIds = null;
|
|
1852
|
-
if (filter.status) {
|
|
1853
|
-
const statusIds = this.indexByStatus.get(filter.status);
|
|
1854
|
-
candidateIds = statusIds ? new Set(statusIds) : /* @__PURE__ */ new Set();
|
|
1855
|
-
}
|
|
1856
|
-
if (filter.events && filter.events.length > 0) {
|
|
1857
|
-
const eventIds = /* @__PURE__ */ new Set();
|
|
1858
|
-
for (const eventType of filter.events) {
|
|
1859
|
-
const ids = this.indexByEvent.get(eventType);
|
|
1860
|
-
if (ids) {
|
|
1861
|
-
ids.forEach((id) => eventIds.add(id));
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
if (candidateIds) {
|
|
1865
|
-
candidateIds = new Set(Array.from(candidateIds).filter((id) => eventIds.has(id)));
|
|
1866
|
-
} else {
|
|
1867
|
-
candidateIds = eventIds;
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
if (!candidateIds) {
|
|
1871
|
-
candidateIds = new Set(this.endpoints.keys());
|
|
1872
|
-
}
|
|
1873
|
-
const filteredEndpoints = Array.from(candidateIds).map((id) => this.endpoints.get(id)).filter((endpoint) => this.matchesFilter(endpoint, filter));
|
|
1874
|
-
if (pagination.sortBy) {
|
|
1875
|
-
filteredEndpoints.sort((a, b) => {
|
|
1876
|
-
const aValue = this.getFieldValue(a, pagination.sortBy);
|
|
1877
|
-
const bValue = this.getFieldValue(b, pagination.sortBy);
|
|
1878
|
-
let comparison = 0;
|
|
1879
|
-
if (aValue < bValue) comparison = -1;
|
|
1880
|
-
else if (aValue > bValue) comparison = 1;
|
|
1881
|
-
return pagination.sortOrder === "desc" ? -comparison : comparison;
|
|
1882
|
-
});
|
|
1883
|
-
}
|
|
1884
|
-
const totalCount = filteredEndpoints.length;
|
|
1885
|
-
const totalPages = Math.ceil(totalCount / pagination.limit);
|
|
1886
|
-
const startIndex = (pagination.page - 1) * pagination.limit;
|
|
1887
|
-
const endIndex = startIndex + pagination.limit;
|
|
1888
|
-
const items = filteredEndpoints.slice(startIndex, endIndex);
|
|
1889
|
-
return {
|
|
1890
|
-
items,
|
|
1891
|
-
totalCount,
|
|
1892
|
-
page: pagination.page,
|
|
1893
|
-
totalPages,
|
|
1894
|
-
hasNext: pagination.page < totalPages,
|
|
1895
|
-
hasPrevious: pagination.page > 1
|
|
1896
|
-
};
|
|
1897
|
-
}
|
|
1898
|
-
/**
|
|
1899
|
-
* 특정 이벤트 타입을 구독하는 활성 엔드포인트 조회
|
|
1900
|
-
*/
|
|
1901
|
-
async getActiveEndpointsForEvent(eventType) {
|
|
1902
|
-
const endpointIds = this.indexByEvent.get(eventType);
|
|
1903
|
-
if (!endpointIds) {
|
|
1904
|
-
return [];
|
|
1905
|
-
}
|
|
1906
|
-
return Array.from(endpointIds).map((id) => this.endpoints.get(id)).filter((endpoint) => endpoint.status === "active");
|
|
1907
|
-
}
|
|
1908
|
-
/**
|
|
1909
|
-
* 엔드포인트 통계 조회
|
|
1910
|
-
*/
|
|
1911
|
-
getStats() {
|
|
1912
|
-
const total = this.endpoints.size;
|
|
1913
|
-
const active = this.indexByStatus.get("active")?.size || 0;
|
|
1914
|
-
const inactive = this.indexByStatus.get("inactive")?.size || 0;
|
|
1915
|
-
const error = this.indexByStatus.get("error")?.size || 0;
|
|
1916
|
-
const suspended = this.indexByStatus.get("suspended")?.size || 0;
|
|
1917
|
-
const eventSubscriptions = {};
|
|
1918
|
-
for (const [eventType, endpointIds] of this.indexByEvent.entries()) {
|
|
1919
|
-
eventSubscriptions[eventType] = endpointIds.size;
|
|
1920
|
-
}
|
|
1921
|
-
return {
|
|
1922
|
-
totalEndpoints: total,
|
|
1923
|
-
activeEndpoints: active,
|
|
1924
|
-
inactiveEndpoints: inactive,
|
|
1925
|
-
errorEndpoints: error,
|
|
1926
|
-
suspendedEndpoints: suspended,
|
|
1927
|
-
eventSubscriptions
|
|
1928
|
-
};
|
|
1929
|
-
}
|
|
1930
|
-
/**
|
|
1931
|
-
* 만료된 엔드포인트 정리
|
|
1932
|
-
*/
|
|
1933
|
-
async cleanupExpiredEndpoints() {
|
|
1934
|
-
if (!this.config.retentionDays) {
|
|
1935
|
-
return 0;
|
|
1936
|
-
}
|
|
1937
|
-
const cutoffDate = /* @__PURE__ */ new Date();
|
|
1938
|
-
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
|
|
1939
|
-
const expiredEndpoints = Array.from(this.endpoints.values()).filter((endpoint) => {
|
|
1940
|
-
return endpoint.status === "inactive" && (!endpoint.lastTriggeredAt || endpoint.lastTriggeredAt < cutoffDate);
|
|
1941
|
-
});
|
|
1942
|
-
for (const endpoint of expiredEndpoints) {
|
|
1943
|
-
await this.removeEndpoint(endpoint.id);
|
|
1944
|
-
}
|
|
1945
|
-
if (expiredEndpoints.length > 0) {
|
|
1946
|
-
this.emit("expiredEndpointsCleanup", {
|
|
1947
|
-
removedCount: expiredEndpoints.length,
|
|
1948
|
-
cutoffDate
|
|
1949
|
-
});
|
|
1950
|
-
}
|
|
1951
|
-
return expiredEndpoints.length;
|
|
1952
|
-
}
|
|
1953
|
-
/**
|
|
1954
|
-
* 인덱스 초기화
|
|
1955
|
-
*/
|
|
1956
|
-
initializeIndexes() {
|
|
1957
|
-
const eventTypes = Object.values(WebhookEventType);
|
|
1958
|
-
for (const eventType of eventTypes) {
|
|
1959
|
-
this.indexByEvent.set(eventType, /* @__PURE__ */ new Set());
|
|
1960
|
-
}
|
|
1961
|
-
const statuses = ["active", "inactive", "error", "suspended"];
|
|
1962
|
-
for (const status of statuses) {
|
|
1963
|
-
this.indexByStatus.set(status, /* @__PURE__ */ new Set());
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
/**
|
|
1967
|
-
* 인덱스에 엔드포인트 추가
|
|
1968
|
-
*/
|
|
1969
|
-
addToIndexes(endpoint) {
|
|
1970
|
-
this.indexByUrl.set(endpoint.url, endpoint.id);
|
|
1971
|
-
const statusSet = this.indexByStatus.get(endpoint.status);
|
|
1972
|
-
if (statusSet) {
|
|
1973
|
-
statusSet.add(endpoint.id);
|
|
1974
|
-
}
|
|
1975
|
-
for (const eventType of endpoint.events) {
|
|
1976
|
-
const eventSet = this.indexByEvent.get(eventType);
|
|
1977
|
-
if (eventSet) {
|
|
1978
|
-
eventSet.add(endpoint.id);
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
/**
|
|
1983
|
-
* 인덱스에서 엔드포인트 제거
|
|
1984
|
-
*/
|
|
1985
|
-
removeFromIndexes(endpoint) {
|
|
1986
|
-
this.indexByUrl.delete(endpoint.url);
|
|
1987
|
-
const statusSet = this.indexByStatus.get(endpoint.status);
|
|
1988
|
-
if (statusSet) {
|
|
1989
|
-
statusSet.delete(endpoint.id);
|
|
1990
|
-
}
|
|
1991
|
-
for (const eventType of endpoint.events) {
|
|
1992
|
-
const eventSet = this.indexByEvent.get(eventType);
|
|
1993
|
-
if (eventSet) {
|
|
1994
|
-
eventSet.delete(endpoint.id);
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
/**
|
|
1999
|
-
* 필터 조건 매칭 확인
|
|
2000
|
-
*/
|
|
2001
|
-
matchesFilter(endpoint, filter) {
|
|
2002
|
-
if (filter.providerId && filter.providerId.length > 0) {
|
|
2003
|
-
const hasMatchingProvider = filter.providerId.some(
|
|
2004
|
-
(providerId) => endpoint.filters?.providerId?.includes(providerId)
|
|
2005
|
-
);
|
|
2006
|
-
if (!hasMatchingProvider) return false;
|
|
2007
|
-
}
|
|
2008
|
-
if (filter.channelId && filter.channelId.length > 0) {
|
|
2009
|
-
const hasMatchingChannel = filter.channelId.some(
|
|
2010
|
-
(channelId) => endpoint.filters?.channelId?.includes(channelId)
|
|
2011
|
-
);
|
|
2012
|
-
if (!hasMatchingChannel) return false;
|
|
2013
|
-
}
|
|
2014
|
-
if (filter.createdAfter && endpoint.createdAt < filter.createdAfter) {
|
|
2015
|
-
return false;
|
|
2016
|
-
}
|
|
2017
|
-
if (filter.createdBefore && endpoint.createdAt > filter.createdBefore) {
|
|
2018
|
-
return false;
|
|
2019
|
-
}
|
|
2020
|
-
if (filter.lastTriggeredAfter && (!endpoint.lastTriggeredAt || endpoint.lastTriggeredAt < filter.lastTriggeredAfter)) {
|
|
2021
|
-
return false;
|
|
2022
|
-
}
|
|
2023
|
-
if (filter.lastTriggeredBefore && (!endpoint.lastTriggeredAt || endpoint.lastTriggeredAt > filter.lastTriggeredBefore)) {
|
|
2024
|
-
return false;
|
|
2025
|
-
}
|
|
2026
|
-
return true;
|
|
2027
|
-
}
|
|
2028
|
-
/**
|
|
2029
|
-
* 객체 필드 값 가져오기 (정렬용)
|
|
2030
|
-
*/
|
|
2031
|
-
getFieldValue(obj, fieldPath) {
|
|
2032
|
-
return fieldPath.split(".").reduce((value, key) => value?.[key], obj);
|
|
2033
|
-
}
|
|
2034
|
-
/**
|
|
2035
|
-
* 파일에서 데이터 로드
|
|
2036
|
-
*/
|
|
2037
|
-
async loadFromFile() {
|
|
2038
|
-
if (!this.config.filePath) return;
|
|
2039
|
-
try {
|
|
2040
|
-
const data = await fs2.readFile(this.config.filePath, "utf8");
|
|
2041
|
-
const parsed = JSON.parse(data);
|
|
2042
|
-
for (const endpointData of parsed.endpoints || []) {
|
|
2043
|
-
const endpoint = {
|
|
2044
|
-
...endpointData,
|
|
2045
|
-
createdAt: new Date(endpointData.createdAt),
|
|
2046
|
-
updatedAt: new Date(endpointData.updatedAt),
|
|
2047
|
-
lastTriggeredAt: endpointData.lastTriggeredAt ? new Date(endpointData.lastTriggeredAt) : void 0
|
|
2048
|
-
};
|
|
2049
|
-
this.endpoints.set(endpoint.id, endpoint);
|
|
2050
|
-
this.addToIndexes(endpoint);
|
|
2051
|
-
}
|
|
2052
|
-
this.emit("dataLoaded", {
|
|
2053
|
-
filePath: this.config.filePath,
|
|
2054
|
-
endpointCount: this.endpoints.size
|
|
2055
|
-
});
|
|
2056
|
-
} catch (error) {
|
|
2057
|
-
if (error.code !== "ENOENT") {
|
|
2058
|
-
this.emit("loadError", error);
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
/**
|
|
2063
|
-
* 파일에 데이터 저장
|
|
2064
|
-
*/
|
|
2065
|
-
async saveToFile() {
|
|
2066
|
-
if (!this.config.filePath) return;
|
|
2067
|
-
try {
|
|
2068
|
-
const data = {
|
|
2069
|
-
endpoints: Array.from(this.endpoints.values()),
|
|
2070
|
-
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2071
|
-
};
|
|
2072
|
-
const json = JSON.stringify(data, null, 2);
|
|
2073
|
-
await fs2.mkdir(path2.dirname(this.config.filePath), { recursive: true });
|
|
2074
|
-
await fs2.writeFile(this.config.filePath, json, "utf8");
|
|
2075
|
-
this.emit("dataSaved", {
|
|
2076
|
-
filePath: this.config.filePath,
|
|
2077
|
-
endpointCount: this.endpoints.size
|
|
2078
|
-
});
|
|
2079
|
-
} catch (error) {
|
|
2080
|
-
this.emit("saveError", error);
|
|
2081
|
-
throw error;
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
/**
|
|
2085
|
-
* 엔드포인트 관리자 종료
|
|
2086
|
-
*/
|
|
2087
|
-
async shutdown() {
|
|
2088
|
-
if (this.config.type === "file") {
|
|
2089
|
-
await this.saveToFile().catch((error) => {
|
|
2090
|
-
this.emit("saveError", error);
|
|
2091
|
-
});
|
|
2092
|
-
}
|
|
2093
|
-
this.emit("shutdown", { endpointCount: this.endpoints.size });
|
|
2094
|
-
}
|
|
2095
|
-
};
|
|
2096
|
-
|
|
2097
|
-
// src/registry/delivery.store.ts
|
|
2098
|
-
var import_events5 = require("events");
|
|
2099
|
-
var fs3 = __toESM(require("fs/promises"), 1);
|
|
2100
|
-
var path3 = __toESM(require("path"), 1);
|
|
2101
|
-
var DeliveryStore = class extends import_events5.EventEmitter {
|
|
2102
|
-
config;
|
|
2103
|
-
deliveries = /* @__PURE__ */ new Map();
|
|
2104
|
-
indexByEndpoint = /* @__PURE__ */ new Map();
|
|
2105
|
-
// endpointId -> delivery ids
|
|
2106
|
-
indexByStatus = /* @__PURE__ */ new Map();
|
|
2107
|
-
// status -> delivery ids
|
|
2108
|
-
indexByDate = /* @__PURE__ */ new Map();
|
|
2109
|
-
// YYYY-MM-DD -> delivery ids
|
|
2110
|
-
cleanupInterval = null;
|
|
2111
|
-
defaultConfig = {
|
|
2112
|
-
type: "memory",
|
|
2113
|
-
retentionDays: 30,
|
|
2114
|
-
enableCompression: false,
|
|
2115
|
-
maxMemoryUsage: 100 * 1024 * 1024
|
|
2116
|
-
// 100MB
|
|
2117
|
-
};
|
|
2118
|
-
constructor(config = {}) {
|
|
2119
|
-
super();
|
|
2120
|
-
this.config = { ...this.defaultConfig, ...config };
|
|
2121
|
-
this.initializeIndexes();
|
|
2122
|
-
this.startCleanupTask();
|
|
2123
|
-
if (this.config.type === "file" && this.config.filePath) {
|
|
2124
|
-
this.loadFromFile().catch((error) => {
|
|
2125
|
-
this.emit("loadError", error);
|
|
2126
|
-
});
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
/**
|
|
2130
|
-
* 전달 기록 저장
|
|
2131
|
-
*/
|
|
2132
|
-
async saveDelivery(delivery) {
|
|
2133
|
-
const existingDelivery = this.deliveries.get(delivery.id);
|
|
2134
|
-
if (existingDelivery) {
|
|
2135
|
-
this.removeFromIndexes(existingDelivery);
|
|
2136
|
-
}
|
|
2137
|
-
if (this.config.type === "memory" && this.config.maxMemoryUsage) {
|
|
2138
|
-
await this.checkMemoryUsage();
|
|
2139
|
-
}
|
|
2140
|
-
this.deliveries.set(delivery.id, delivery);
|
|
2141
|
-
this.addToIndexes(delivery);
|
|
2142
|
-
if (this.config.type === "file") {
|
|
2143
|
-
await this.appendToFile(delivery);
|
|
2144
|
-
}
|
|
2145
|
-
this.emit("deliverySaved", {
|
|
2146
|
-
deliveryId: delivery.id,
|
|
2147
|
-
endpointId: delivery.endpointId,
|
|
2148
|
-
status: delivery.status
|
|
2149
|
-
});
|
|
2150
|
-
}
|
|
2151
|
-
/**
|
|
2152
|
-
* 전달 기록 조회
|
|
2153
|
-
*/
|
|
2154
|
-
async getDelivery(deliveryId) {
|
|
2155
|
-
return this.deliveries.get(deliveryId) || null;
|
|
2156
|
-
}
|
|
2157
|
-
/**
|
|
2158
|
-
* 필터 조건에 맞는 전달 기록 검색
|
|
2159
|
-
*/
|
|
2160
|
-
async searchDeliveries(filter = {}, pagination = { page: 1, limit: 100 }) {
|
|
2161
|
-
let candidateIds = null;
|
|
2162
|
-
if (filter.endpointId) {
|
|
2163
|
-
const endpointIds = this.indexByEndpoint.get(filter.endpointId);
|
|
2164
|
-
candidateIds = endpointIds ? new Set(endpointIds) : /* @__PURE__ */ new Set();
|
|
2165
|
-
}
|
|
2166
|
-
if (filter.status) {
|
|
2167
|
-
const statusIds = this.indexByStatus.get(filter.status);
|
|
2168
|
-
if (candidateIds) {
|
|
2169
|
-
candidateIds = new Set(Array.from(candidateIds).filter((id) => statusIds?.has(id)));
|
|
2170
|
-
} else {
|
|
2171
|
-
candidateIds = statusIds ? new Set(statusIds) : /* @__PURE__ */ new Set();
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
if (filter.createdAfter || filter.createdBefore) {
|
|
2175
|
-
const dateIds = this.getDeliveryIdsByDateRange(filter.createdAfter, filter.createdBefore);
|
|
2176
|
-
if (candidateIds) {
|
|
2177
|
-
candidateIds = new Set(Array.from(candidateIds).filter((id) => dateIds.has(id)));
|
|
2178
|
-
} else {
|
|
2179
|
-
candidateIds = dateIds;
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
if (!candidateIds) {
|
|
2183
|
-
candidateIds = new Set(this.deliveries.keys());
|
|
2184
|
-
}
|
|
2185
|
-
const filteredDeliveries = Array.from(candidateIds).map((id) => this.deliveries.get(id)).filter((delivery) => this.matchesFilter(delivery, filter));
|
|
2186
|
-
filteredDeliveries.sort((a, b) => {
|
|
2187
|
-
if (pagination.sortBy === "createdAt" || !pagination.sortBy) {
|
|
2188
|
-
const comparison2 = b.createdAt.getTime() - a.createdAt.getTime();
|
|
2189
|
-
return pagination.sortOrder === "asc" ? -comparison2 : comparison2;
|
|
2190
|
-
}
|
|
2191
|
-
const aValue = this.getFieldValue(a, pagination.sortBy);
|
|
2192
|
-
const bValue = this.getFieldValue(b, pagination.sortBy);
|
|
2193
|
-
let comparison = 0;
|
|
2194
|
-
if (aValue < bValue) comparison = -1;
|
|
2195
|
-
else if (aValue > bValue) comparison = 1;
|
|
2196
|
-
return pagination.sortOrder === "desc" ? -comparison : comparison;
|
|
2197
|
-
});
|
|
2198
|
-
const totalCount = filteredDeliveries.length;
|
|
2199
|
-
const totalPages = Math.ceil(totalCount / pagination.limit);
|
|
2200
|
-
const startIndex = (pagination.page - 1) * pagination.limit;
|
|
2201
|
-
const endIndex = startIndex + pagination.limit;
|
|
2202
|
-
const items = filteredDeliveries.slice(startIndex, endIndex);
|
|
2203
|
-
return {
|
|
2204
|
-
items,
|
|
2205
|
-
totalCount,
|
|
2206
|
-
page: pagination.page,
|
|
2207
|
-
totalPages,
|
|
2208
|
-
hasNext: pagination.page < totalPages,
|
|
2209
|
-
hasPrevious: pagination.page > 1
|
|
2210
|
-
};
|
|
2211
|
-
}
|
|
2212
|
-
/**
|
|
2213
|
-
* 엔드포인트별 전달 기록 조회
|
|
2214
|
-
*/
|
|
2215
|
-
async getDeliveriesByEndpoint(endpointId, limit = 100) {
|
|
2216
|
-
const deliveryIds = this.indexByEndpoint.get(endpointId);
|
|
2217
|
-
if (!deliveryIds) {
|
|
2218
|
-
return [];
|
|
2219
|
-
}
|
|
2220
|
-
return Array.from(deliveryIds).map((id) => this.deliveries.get(id)).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()).slice(0, limit);
|
|
2221
|
-
}
|
|
2222
|
-
/**
|
|
2223
|
-
* 실패한 전달 기록 조회
|
|
2224
|
-
*/
|
|
2225
|
-
async getFailedDeliveries(endpointId, limit = 100) {
|
|
2226
|
-
const filter = {
|
|
2227
|
-
status: "failed",
|
|
2228
|
-
endpointId
|
|
2229
|
-
};
|
|
2230
|
-
const result = await this.searchDeliveries(filter, { page: 1, limit });
|
|
2231
|
-
return result.items;
|
|
2232
|
-
}
|
|
2233
|
-
/**
|
|
2234
|
-
* 전달 통계 조회
|
|
2235
|
-
*/
|
|
2236
|
-
async getDeliveryStats(endpointId, timeRange) {
|
|
2237
|
-
const filter = {
|
|
2238
|
-
endpointId,
|
|
2239
|
-
createdAfter: timeRange?.start,
|
|
2240
|
-
createdBefore: timeRange?.end
|
|
2241
|
-
};
|
|
2242
|
-
const result = await this.searchDeliveries(filter, { page: 1, limit: 1e4 });
|
|
2243
|
-
const deliveries = result.items;
|
|
2244
|
-
const successful = deliveries.filter((d) => d.status === "success");
|
|
2245
|
-
const failed = deliveries.filter((d) => d.status === "failed");
|
|
2246
|
-
const pending = deliveries.filter((d) => d.status === "pending");
|
|
2247
|
-
const exhausted = deliveries.filter((d) => d.status === "exhausted");
|
|
2248
|
-
const completedDeliveries = deliveries.filter((d) => d.completedAt);
|
|
2249
|
-
const totalLatency = completedDeliveries.reduce((sum, delivery) => {
|
|
2250
|
-
const lastAttempt = delivery.attempts[delivery.attempts.length - 1];
|
|
2251
|
-
return sum + (lastAttempt?.latencyMs || 0);
|
|
2252
|
-
}, 0);
|
|
2253
|
-
const averageLatency = completedDeliveries.length > 0 ? totalLatency / completedDeliveries.length : 0;
|
|
2254
|
-
const errorBreakdown = {};
|
|
2255
|
-
for (const delivery of [...failed, ...exhausted]) {
|
|
2256
|
-
const lastAttempt = delivery.attempts[delivery.attempts.length - 1];
|
|
2257
|
-
if (lastAttempt?.error) {
|
|
2258
|
-
errorBreakdown[lastAttempt.error] = (errorBreakdown[lastAttempt.error] || 0) + 1;
|
|
2259
|
-
} else if (lastAttempt?.httpStatus) {
|
|
2260
|
-
const errorKey = `HTTP ${lastAttempt.httpStatus}`;
|
|
2261
|
-
errorBreakdown[errorKey] = (errorBreakdown[errorKey] || 0) + 1;
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
return {
|
|
2265
|
-
totalDeliveries: deliveries.length,
|
|
2266
|
-
successfulDeliveries: successful.length,
|
|
2267
|
-
failedDeliveries: failed.length,
|
|
2268
|
-
pendingDeliveries: pending.length,
|
|
2269
|
-
exhaustedDeliveries: exhausted.length,
|
|
2270
|
-
averageLatency,
|
|
2271
|
-
successRate: deliveries.length > 0 ? successful.length / deliveries.length * 100 : 0,
|
|
2272
|
-
errorBreakdown
|
|
2273
|
-
};
|
|
2274
|
-
}
|
|
2275
|
-
/**
|
|
2276
|
-
* 오래된 전달 기록 정리
|
|
2277
|
-
*/
|
|
2278
|
-
async cleanupOldDeliveries() {
|
|
2279
|
-
if (!this.config.retentionDays) {
|
|
2280
|
-
return 0;
|
|
2281
|
-
}
|
|
2282
|
-
const cutoffDate = /* @__PURE__ */ new Date();
|
|
2283
|
-
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
|
|
2284
|
-
const oldDeliveries = Array.from(this.deliveries.values()).filter(
|
|
2285
|
-
(delivery) => delivery.createdAt < cutoffDate
|
|
2286
|
-
);
|
|
2287
|
-
for (const delivery of oldDeliveries) {
|
|
2288
|
-
this.removeFromIndexes(delivery);
|
|
2289
|
-
this.deliveries.delete(delivery.id);
|
|
2290
|
-
}
|
|
2291
|
-
if (oldDeliveries.length > 0) {
|
|
2292
|
-
this.emit("oldDeliveriesCleanup", {
|
|
2293
|
-
removedCount: oldDeliveries.length,
|
|
2294
|
-
cutoffDate
|
|
2295
|
-
});
|
|
2296
|
-
if (this.config.type === "file") {
|
|
2297
|
-
await this.saveToFile();
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
return oldDeliveries.length;
|
|
2301
|
-
}
|
|
2302
|
-
/**
|
|
2303
|
-
* 저장소 통계 조회
|
|
2304
|
-
*/
|
|
2305
|
-
getStorageStats() {
|
|
2306
|
-
const memoryUsage = this.estimateMemoryUsage();
|
|
2307
|
-
return {
|
|
2308
|
-
totalDeliveries: this.deliveries.size,
|
|
2309
|
-
memoryUsage,
|
|
2310
|
-
indexSizes: {
|
|
2311
|
-
byEndpoint: this.indexByEndpoint.size,
|
|
2312
|
-
byStatus: this.indexByStatus.size,
|
|
2313
|
-
byDate: this.indexByDate.size
|
|
2314
|
-
}
|
|
2315
|
-
};
|
|
2316
|
-
}
|
|
2317
|
-
/**
|
|
2318
|
-
* 인덱스 초기화
|
|
2319
|
-
*/
|
|
2320
|
-
initializeIndexes() {
|
|
2321
|
-
const statuses = ["pending", "success", "failed", "exhausted"];
|
|
2322
|
-
for (const status of statuses) {
|
|
2323
|
-
this.indexByStatus.set(status, /* @__PURE__ */ new Set());
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
/**
|
|
2327
|
-
* 인덱스에 전달 기록 추가
|
|
2328
|
-
*/
|
|
2329
|
-
addToIndexes(delivery) {
|
|
2330
|
-
if (!this.indexByEndpoint.has(delivery.endpointId)) {
|
|
2331
|
-
this.indexByEndpoint.set(delivery.endpointId, /* @__PURE__ */ new Set());
|
|
2332
|
-
}
|
|
2333
|
-
this.indexByEndpoint.get(delivery.endpointId).add(delivery.id);
|
|
2334
|
-
const statusSet = this.indexByStatus.get(delivery.status);
|
|
2335
|
-
if (statusSet) {
|
|
2336
|
-
statusSet.add(delivery.id);
|
|
2337
|
-
}
|
|
2338
|
-
const dateKey = delivery.createdAt.toISOString().split("T")[0];
|
|
2339
|
-
if (!this.indexByDate.has(dateKey)) {
|
|
2340
|
-
this.indexByDate.set(dateKey, /* @__PURE__ */ new Set());
|
|
2341
|
-
}
|
|
2342
|
-
this.indexByDate.get(dateKey).add(delivery.id);
|
|
2343
|
-
}
|
|
2344
|
-
/**
|
|
2345
|
-
* 인덱스에서 전달 기록 제거
|
|
2346
|
-
*/
|
|
2347
|
-
removeFromIndexes(delivery) {
|
|
2348
|
-
const endpointSet = this.indexByEndpoint.get(delivery.endpointId);
|
|
2349
|
-
if (endpointSet) {
|
|
2350
|
-
endpointSet.delete(delivery.id);
|
|
2351
|
-
if (endpointSet.size === 0) {
|
|
2352
|
-
this.indexByEndpoint.delete(delivery.endpointId);
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
const statusSet = this.indexByStatus.get(delivery.status);
|
|
2356
|
-
if (statusSet) {
|
|
2357
|
-
statusSet.delete(delivery.id);
|
|
2358
|
-
}
|
|
2359
|
-
const dateKey = delivery.createdAt.toISOString().split("T")[0];
|
|
2360
|
-
const dateSet = this.indexByDate.get(dateKey);
|
|
2361
|
-
if (dateSet) {
|
|
2362
|
-
dateSet.delete(delivery.id);
|
|
2363
|
-
if (dateSet.size === 0) {
|
|
2364
|
-
this.indexByDate.delete(dateKey);
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
|
-
/**
|
|
2369
|
-
* 날짜 범위로 전달 기록 ID 조회
|
|
2370
|
-
*/
|
|
2371
|
-
getDeliveryIdsByDateRange(startDate, endDate) {
|
|
2372
|
-
const ids = /* @__PURE__ */ new Set();
|
|
2373
|
-
for (const [dateKey, deliveryIds] of this.indexByDate.entries()) {
|
|
2374
|
-
const date = new Date(dateKey);
|
|
2375
|
-
if (startDate && date < startDate) continue;
|
|
2376
|
-
if (endDate && date > endDate) continue;
|
|
2377
|
-
deliveryIds.forEach((id) => ids.add(id));
|
|
2378
|
-
}
|
|
2379
|
-
return ids;
|
|
2380
|
-
}
|
|
2381
|
-
/**
|
|
2382
|
-
* 필터 조건 매칭 확인
|
|
2383
|
-
*/
|
|
2384
|
-
matchesFilter(delivery, filter) {
|
|
2385
|
-
if (filter.eventId && delivery.eventId !== filter.eventId) {
|
|
2386
|
-
return false;
|
|
2387
|
-
}
|
|
2388
|
-
if (filter.httpStatusCode && filter.httpStatusCode.length > 0) {
|
|
2389
|
-
const lastAttempt = delivery.attempts[delivery.attempts.length - 1];
|
|
2390
|
-
if (!lastAttempt?.httpStatus || !filter.httpStatusCode.includes(lastAttempt.httpStatus)) {
|
|
2391
|
-
return false;
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
if (filter.hasError !== void 0) {
|
|
2395
|
-
const hasError = delivery.attempts.some((attempt) => attempt.error);
|
|
2396
|
-
if (filter.hasError !== hasError) {
|
|
2397
|
-
return false;
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
if (filter.completedAfter && (!delivery.completedAt || delivery.completedAt < filter.completedAfter)) {
|
|
2401
|
-
return false;
|
|
2402
|
-
}
|
|
2403
|
-
if (filter.completedBefore && (!delivery.completedAt || delivery.completedAt > filter.completedBefore)) {
|
|
2404
|
-
return false;
|
|
2405
|
-
}
|
|
2406
|
-
return true;
|
|
2407
|
-
}
|
|
2408
|
-
/**
|
|
2409
|
-
* 객체 필드 값 가져오기
|
|
2410
|
-
*/
|
|
2411
|
-
getFieldValue(obj, fieldPath) {
|
|
2412
|
-
return fieldPath.split(".").reduce((value, key) => value?.[key], obj);
|
|
2413
|
-
}
|
|
2414
|
-
/**
|
|
2415
|
-
* 메모리 사용량 추정
|
|
2416
|
-
*/
|
|
2417
|
-
estimateMemoryUsage() {
|
|
2418
|
-
let totalSize = 0;
|
|
2419
|
-
for (const delivery of this.deliveries.values()) {
|
|
2420
|
-
totalSize += JSON.stringify(delivery).length * 2;
|
|
2421
|
-
}
|
|
2422
|
-
return totalSize;
|
|
2423
|
-
}
|
|
2424
|
-
/**
|
|
2425
|
-
* 메모리 사용량 확인 및 정리
|
|
2426
|
-
*/
|
|
2427
|
-
async checkMemoryUsage() {
|
|
2428
|
-
if (!this.config.maxMemoryUsage) return;
|
|
2429
|
-
const currentUsage = this.estimateMemoryUsage();
|
|
2430
|
-
if (currentUsage > this.config.maxMemoryUsage) {
|
|
2431
|
-
const deliveries = Array.from(this.deliveries.values()).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
2432
|
-
let removedCount = 0;
|
|
2433
|
-
const targetUsage = this.config.maxMemoryUsage * 0.8;
|
|
2434
|
-
for (const delivery of deliveries) {
|
|
2435
|
-
if (this.estimateMemoryUsage() <= targetUsage) break;
|
|
2436
|
-
this.removeFromIndexes(delivery);
|
|
2437
|
-
this.deliveries.delete(delivery.id);
|
|
2438
|
-
removedCount++;
|
|
2439
|
-
}
|
|
2440
|
-
if (removedCount > 0) {
|
|
2441
|
-
this.emit("memoryCleanup", {
|
|
2442
|
-
removedCount,
|
|
2443
|
-
previousUsage: currentUsage,
|
|
2444
|
-
currentUsage: this.estimateMemoryUsage()
|
|
2445
|
-
});
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
/**
|
|
2450
|
-
* 정리 작업 시작
|
|
2451
|
-
*/
|
|
2452
|
-
startCleanupTask() {
|
|
2453
|
-
this.cleanupInterval = setInterval(async () => {
|
|
2454
|
-
try {
|
|
2455
|
-
await this.cleanupOldDeliveries();
|
|
2456
|
-
await this.checkMemoryUsage();
|
|
2457
|
-
} catch (error) {
|
|
2458
|
-
this.emit("cleanupError", error);
|
|
2459
|
-
}
|
|
2460
|
-
}, 60 * 60 * 1e3);
|
|
2461
|
-
}
|
|
2462
|
-
/**
|
|
2463
|
-
* 파일에 전달 기록 추가
|
|
2464
|
-
*/
|
|
2465
|
-
async appendToFile(delivery) {
|
|
2466
|
-
if (!this.config.filePath) return;
|
|
2467
|
-
try {
|
|
2468
|
-
const line = JSON.stringify(delivery) + "\n";
|
|
2469
|
-
await fs3.appendFile(this.config.filePath, line, "utf8");
|
|
2470
|
-
} catch (error) {
|
|
2471
|
-
this.emit("appendError", error);
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
/**
|
|
2475
|
-
* 파일에서 데이터 로드
|
|
2476
|
-
*/
|
|
2477
|
-
async loadFromFile() {
|
|
2478
|
-
if (!this.config.filePath) return;
|
|
2479
|
-
try {
|
|
2480
|
-
const data = await fs3.readFile(this.config.filePath, "utf8");
|
|
2481
|
-
const lines = data.trim().split("\n").filter((line) => line.trim());
|
|
2482
|
-
for (const line of lines) {
|
|
2483
|
-
try {
|
|
2484
|
-
const deliveryData = JSON.parse(line);
|
|
2485
|
-
const delivery = {
|
|
2486
|
-
...deliveryData,
|
|
2487
|
-
createdAt: new Date(deliveryData.createdAt),
|
|
2488
|
-
completedAt: deliveryData.completedAt ? new Date(deliveryData.completedAt) : void 0,
|
|
2489
|
-
nextRetryAt: deliveryData.nextRetryAt ? new Date(deliveryData.nextRetryAt) : void 0,
|
|
2490
|
-
attempts: deliveryData.attempts.map((attempt) => ({
|
|
2491
|
-
...attempt,
|
|
2492
|
-
timestamp: new Date(attempt.timestamp)
|
|
2493
|
-
}))
|
|
2494
|
-
};
|
|
2495
|
-
this.deliveries.set(delivery.id, delivery);
|
|
2496
|
-
this.addToIndexes(delivery);
|
|
2497
|
-
} catch (parseError) {
|
|
2498
|
-
this.emit("parseError", { line, error: parseError });
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
this.emit("dataLoaded", {
|
|
2502
|
-
filePath: this.config.filePath,
|
|
2503
|
-
deliveryCount: this.deliveries.size
|
|
2504
|
-
});
|
|
2505
|
-
} catch (error) {
|
|
2506
|
-
if (error.code !== "ENOENT") {
|
|
2507
|
-
this.emit("loadError", error);
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
/**
|
|
2512
|
-
* 파일에 데이터 저장
|
|
2513
|
-
*/
|
|
2514
|
-
async saveToFile() {
|
|
2515
|
-
if (!this.config.filePath) return;
|
|
2516
|
-
try {
|
|
2517
|
-
const lines = Array.from(this.deliveries.values()).map((delivery) => JSON.stringify(delivery)).join("\n");
|
|
2518
|
-
await fs3.mkdir(path3.dirname(this.config.filePath), { recursive: true });
|
|
2519
|
-
await fs3.writeFile(this.config.filePath, lines + "\n", "utf8");
|
|
2520
|
-
this.emit("dataSaved", {
|
|
2521
|
-
filePath: this.config.filePath,
|
|
2522
|
-
deliveryCount: this.deliveries.size
|
|
2523
|
-
});
|
|
2524
|
-
} catch (error) {
|
|
2525
|
-
this.emit("saveError", error);
|
|
2526
|
-
throw error;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
/**
|
|
2530
|
-
* 전달 저장소 종료
|
|
2531
|
-
*/
|
|
2532
|
-
async shutdown() {
|
|
2533
|
-
if (this.cleanupInterval) {
|
|
2534
|
-
clearInterval(this.cleanupInterval);
|
|
2535
|
-
this.cleanupInterval = null;
|
|
2536
|
-
}
|
|
2537
|
-
if (this.config.type === "file") {
|
|
2538
|
-
await this.saveToFile().catch((error) => {
|
|
2539
|
-
this.emit("saveError", error);
|
|
2540
|
-
});
|
|
2541
|
-
}
|
|
2542
|
-
this.emit("shutdown", { deliveryCount: this.deliveries.size });
|
|
2543
|
-
}
|
|
2544
|
-
};
|
|
2545
|
-
|
|
2546
|
-
// src/registry/event.store.ts
|
|
2547
|
-
var import_events6 = require("events");
|
|
2548
|
-
var fs4 = __toESM(require("fs/promises"), 1);
|
|
2549
|
-
var path4 = __toESM(require("path"), 1);
|
|
2550
|
-
var EventStore = class extends import_events6.EventEmitter {
|
|
2551
|
-
config;
|
|
2552
|
-
events = /* @__PURE__ */ new Map();
|
|
2553
|
-
indexByType = /* @__PURE__ */ new Map();
|
|
2554
|
-
indexByDate = /* @__PURE__ */ new Map();
|
|
2555
|
-
// YYYY-MM-DD -> event ids
|
|
2556
|
-
indexByProvider = /* @__PURE__ */ new Map();
|
|
2557
|
-
indexByChannel = /* @__PURE__ */ new Map();
|
|
2558
|
-
cleanupInterval = null;
|
|
2559
|
-
defaultConfig = {
|
|
2560
|
-
type: "memory",
|
|
2561
|
-
retentionDays: 7,
|
|
2562
|
-
enableCompression: false,
|
|
2563
|
-
maxMemoryUsage: 50 * 1024 * 1024
|
|
2564
|
-
// 50MB
|
|
2565
|
-
};
|
|
2566
|
-
constructor(config = {}) {
|
|
2567
|
-
super();
|
|
2568
|
-
this.config = { ...this.defaultConfig, ...config };
|
|
2569
|
-
this.initializeIndexes();
|
|
2570
|
-
this.startCleanupTask();
|
|
2571
|
-
if (this.config.type === "file" && this.config.filePath) {
|
|
2572
|
-
this.loadFromFile().catch((error) => {
|
|
2573
|
-
this.emit("loadError", error);
|
|
2574
|
-
});
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
/**
|
|
2578
|
-
* 이벤트 저장
|
|
2579
|
-
*/
|
|
2580
|
-
async saveEvent(event) {
|
|
2581
|
-
if (this.events.has(event.id)) {
|
|
2582
|
-
this.emit("duplicateEvent", { eventId: event.id });
|
|
2583
|
-
return;
|
|
2584
|
-
}
|
|
2585
|
-
if (this.config.type === "memory" && this.config.maxMemoryUsage) {
|
|
2586
|
-
await this.checkMemoryUsage();
|
|
2587
|
-
}
|
|
2588
|
-
this.events.set(event.id, event);
|
|
2589
|
-
this.addToIndexes(event);
|
|
2590
|
-
if (this.config.type === "file") {
|
|
2591
|
-
await this.appendToFile(event);
|
|
2592
|
-
}
|
|
2593
|
-
this.emit("eventSaved", {
|
|
2594
|
-
eventId: event.id,
|
|
2595
|
-
type: event.type,
|
|
2596
|
-
providerId: event.metadata.providerId
|
|
2597
|
-
});
|
|
2598
|
-
}
|
|
2599
|
-
/**
|
|
2600
|
-
* 이벤트 조회
|
|
2601
|
-
*/
|
|
2602
|
-
async getEvent(eventId) {
|
|
2603
|
-
return this.events.get(eventId) || null;
|
|
2604
|
-
}
|
|
2605
|
-
/**
|
|
2606
|
-
* 필터 조건에 맞는 이벤트 검색
|
|
2607
|
-
*/
|
|
2608
|
-
async searchEvents(filter = {}, pagination = { page: 1, limit: 100 }) {
|
|
2609
|
-
let candidateIds = null;
|
|
2610
|
-
if (filter.type && filter.type.length > 0) {
|
|
2611
|
-
const typeIds = /* @__PURE__ */ new Set();
|
|
2612
|
-
for (const eventType of filter.type) {
|
|
2613
|
-
const ids = this.indexByType.get(eventType);
|
|
2614
|
-
if (ids) {
|
|
2615
|
-
ids.forEach((id) => typeIds.add(id));
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
candidateIds = typeIds;
|
|
2619
|
-
}
|
|
2620
|
-
if (filter.providerId && filter.providerId.length > 0) {
|
|
2621
|
-
const providerIds = /* @__PURE__ */ new Set();
|
|
2622
|
-
for (const providerId of filter.providerId) {
|
|
2623
|
-
const ids = this.indexByProvider.get(providerId);
|
|
2624
|
-
if (ids) {
|
|
2625
|
-
ids.forEach((id) => providerIds.add(id));
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
if (candidateIds) {
|
|
2629
|
-
candidateIds = new Set(Array.from(candidateIds).filter((id) => providerIds.has(id)));
|
|
2630
|
-
} else {
|
|
2631
|
-
candidateIds = providerIds;
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
if (filter.channelId && filter.channelId.length > 0) {
|
|
2635
|
-
const channelIds = /* @__PURE__ */ new Set();
|
|
2636
|
-
for (const channelId of filter.channelId) {
|
|
2637
|
-
const ids = this.indexByChannel.get(channelId);
|
|
2638
|
-
if (ids) {
|
|
2639
|
-
ids.forEach((id) => channelIds.add(id));
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
if (candidateIds) {
|
|
2643
|
-
candidateIds = new Set(Array.from(candidateIds).filter((id) => channelIds.has(id)));
|
|
2644
|
-
} else {
|
|
2645
|
-
candidateIds = channelIds;
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
if (filter.createdAfter || filter.createdBefore) {
|
|
2649
|
-
const dateIds = this.getEventIdsByDateRange(filter.createdAfter, filter.createdBefore);
|
|
2650
|
-
if (candidateIds) {
|
|
2651
|
-
candidateIds = new Set(Array.from(candidateIds).filter((id) => dateIds.has(id)));
|
|
2652
|
-
} else {
|
|
2653
|
-
candidateIds = dateIds;
|
|
2654
|
-
}
|
|
2655
|
-
}
|
|
2656
|
-
if (!candidateIds) {
|
|
2657
|
-
candidateIds = new Set(this.events.keys());
|
|
2658
|
-
}
|
|
2659
|
-
const filteredEvents = Array.from(candidateIds).map((id) => this.events.get(id)).filter((event) => this.matchesFilter(event, filter));
|
|
2660
|
-
filteredEvents.sort((a, b) => {
|
|
2661
|
-
if (pagination.sortBy === "timestamp" || !pagination.sortBy) {
|
|
2662
|
-
const comparison2 = b.timestamp.getTime() - a.timestamp.getTime();
|
|
2663
|
-
return pagination.sortOrder === "asc" ? -comparison2 : comparison2;
|
|
2664
|
-
}
|
|
2665
|
-
const aValue = this.getFieldValue(a, pagination.sortBy);
|
|
2666
|
-
const bValue = this.getFieldValue(b, pagination.sortBy);
|
|
2667
|
-
let comparison = 0;
|
|
2668
|
-
if (aValue < bValue) comparison = -1;
|
|
2669
|
-
else if (aValue > bValue) comparison = 1;
|
|
2670
|
-
return pagination.sortOrder === "desc" ? -comparison : comparison;
|
|
2671
|
-
});
|
|
2672
|
-
const totalCount = filteredEvents.length;
|
|
2673
|
-
const totalPages = Math.ceil(totalCount / pagination.limit);
|
|
2674
|
-
const startIndex = (pagination.page - 1) * pagination.limit;
|
|
2675
|
-
const endIndex = startIndex + pagination.limit;
|
|
2676
|
-
const items = filteredEvents.slice(startIndex, endIndex);
|
|
2677
|
-
return {
|
|
2678
|
-
items,
|
|
2679
|
-
totalCount,
|
|
2680
|
-
page: pagination.page,
|
|
2681
|
-
totalPages,
|
|
2682
|
-
hasNext: pagination.page < totalPages,
|
|
2683
|
-
hasPrevious: pagination.page > 1
|
|
2684
|
-
};
|
|
2685
|
-
}
|
|
2686
|
-
/**
|
|
2687
|
-
* 이벤트 타입별 조회
|
|
2688
|
-
*/
|
|
2689
|
-
async getEventsByType(eventType, limit = 100) {
|
|
2690
|
-
const eventIds = this.indexByType.get(eventType);
|
|
2691
|
-
if (!eventIds) {
|
|
2692
|
-
return [];
|
|
2693
|
-
}
|
|
2694
|
-
return Array.from(eventIds).map((id) => this.events.get(id)).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).slice(0, limit);
|
|
2695
|
-
}
|
|
2696
|
-
/**
|
|
2697
|
-
* 이벤트 통계 조회
|
|
2698
|
-
*/
|
|
2699
|
-
async getEventStats(timeRange) {
|
|
2700
|
-
const filter = {
|
|
2701
|
-
createdAfter: timeRange?.start,
|
|
2702
|
-
createdBefore: timeRange?.end
|
|
2703
|
-
};
|
|
2704
|
-
const result = await this.searchEvents(filter, { page: 1, limit: 1e4 });
|
|
2705
|
-
const events = result.items;
|
|
2706
|
-
const eventsByType = {};
|
|
2707
|
-
for (const eventType of Object.values(WebhookEventType)) {
|
|
2708
|
-
eventsByType[eventType] = 0;
|
|
2709
|
-
}
|
|
2710
|
-
const eventsByProvider = {};
|
|
2711
|
-
const eventsByChannel = {};
|
|
2712
|
-
const eventsPerHour = {};
|
|
2713
|
-
for (const event of events) {
|
|
2714
|
-
eventsByType[event.type]++;
|
|
2715
|
-
if (event.metadata.providerId) {
|
|
2716
|
-
eventsByProvider[event.metadata.providerId] = (eventsByProvider[event.metadata.providerId] || 0) + 1;
|
|
2717
|
-
}
|
|
2718
|
-
if (event.metadata.channelId) {
|
|
2719
|
-
eventsByChannel[event.metadata.channelId] = (eventsByChannel[event.metadata.channelId] || 0) + 1;
|
|
2720
|
-
}
|
|
2721
|
-
const hourKey = event.timestamp.toISOString().substring(0, 13);
|
|
2722
|
-
eventsPerHour[hourKey] = (eventsPerHour[hourKey] || 0) + 1;
|
|
2723
|
-
}
|
|
2724
|
-
return {
|
|
2725
|
-
totalEvents: events.length,
|
|
2726
|
-
eventsByType,
|
|
2727
|
-
eventsByProvider,
|
|
2728
|
-
eventsByChannel,
|
|
2729
|
-
eventsPerHour
|
|
2730
|
-
};
|
|
2731
|
-
}
|
|
2732
|
-
/**
|
|
2733
|
-
* 오래된 이벤트 정리
|
|
2734
|
-
*/
|
|
2735
|
-
async cleanupOldEvents() {
|
|
2736
|
-
if (!this.config.retentionDays) {
|
|
2737
|
-
return 0;
|
|
2738
|
-
}
|
|
2739
|
-
const cutoffDate = /* @__PURE__ */ new Date();
|
|
2740
|
-
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
|
|
2741
|
-
const oldEvents = Array.from(this.events.values()).filter(
|
|
2742
|
-
(event) => event.timestamp < cutoffDate
|
|
2743
|
-
);
|
|
2744
|
-
for (const event of oldEvents) {
|
|
2745
|
-
this.removeFromIndexes(event);
|
|
2746
|
-
this.events.delete(event.id);
|
|
2747
|
-
}
|
|
2748
|
-
if (oldEvents.length > 0) {
|
|
2749
|
-
this.emit("oldEventsCleanup", {
|
|
2750
|
-
removedCount: oldEvents.length,
|
|
2751
|
-
cutoffDate
|
|
2752
|
-
});
|
|
2753
|
-
if (this.config.type === "file") {
|
|
2754
|
-
await this.saveToFile();
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2757
|
-
return oldEvents.length;
|
|
2758
|
-
}
|
|
2759
|
-
/**
|
|
2760
|
-
* 중복 이벤트 정리
|
|
2761
|
-
*/
|
|
2762
|
-
async cleanupDuplicateEvents() {
|
|
2763
|
-
const eventsByContent = /* @__PURE__ */ new Map();
|
|
2764
|
-
for (const event of this.events.values()) {
|
|
2765
|
-
const contentKey = this.generateContentKey(event);
|
|
2766
|
-
if (!eventsByContent.has(contentKey)) {
|
|
2767
|
-
eventsByContent.set(contentKey, []);
|
|
2768
|
-
}
|
|
2769
|
-
eventsByContent.get(contentKey).push(event);
|
|
2770
|
-
}
|
|
2771
|
-
let removedCount = 0;
|
|
2772
|
-
for (const [contentKey, duplicateEvents] of eventsByContent.entries()) {
|
|
2773
|
-
if (duplicateEvents.length > 1) {
|
|
2774
|
-
duplicateEvents.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
2775
|
-
for (let i = 1; i < duplicateEvents.length; i++) {
|
|
2776
|
-
const eventToRemove = duplicateEvents[i];
|
|
2777
|
-
this.removeFromIndexes(eventToRemove);
|
|
2778
|
-
this.events.delete(eventToRemove.id);
|
|
2779
|
-
removedCount++;
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
|
-
if (removedCount > 0) {
|
|
2784
|
-
this.emit("duplicateEventsCleanup", { removedCount });
|
|
2785
|
-
if (this.config.type === "file") {
|
|
2786
|
-
await this.saveToFile();
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
return removedCount;
|
|
2790
|
-
}
|
|
2791
|
-
/**
|
|
2792
|
-
* 저장소 통계 조회
|
|
2793
|
-
*/
|
|
2794
|
-
getStorageStats() {
|
|
2795
|
-
const memoryUsage = this.estimateMemoryUsage();
|
|
2796
|
-
return {
|
|
2797
|
-
totalEvents: this.events.size,
|
|
2798
|
-
memoryUsage,
|
|
2799
|
-
indexSizes: {
|
|
2800
|
-
byType: this.indexByType.size,
|
|
2801
|
-
byDate: this.indexByDate.size,
|
|
2802
|
-
byProvider: this.indexByProvider.size,
|
|
2803
|
-
byChannel: this.indexByChannel.size
|
|
2804
|
-
}
|
|
2805
|
-
};
|
|
2806
|
-
}
|
|
2807
|
-
/**
|
|
2808
|
-
* 인덱스 초기화
|
|
2809
|
-
*/
|
|
2810
|
-
initializeIndexes() {
|
|
2811
|
-
const eventTypes = Object.values(WebhookEventType);
|
|
2812
|
-
for (const eventType of eventTypes) {
|
|
2813
|
-
this.indexByType.set(eventType, /* @__PURE__ */ new Set());
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
|
-
/**
|
|
2817
|
-
* 인덱스에 이벤트 추가
|
|
2818
|
-
*/
|
|
2819
|
-
addToIndexes(event) {
|
|
2820
|
-
const typeSet = this.indexByType.get(event.type);
|
|
2821
|
-
if (typeSet) {
|
|
2822
|
-
typeSet.add(event.id);
|
|
2823
|
-
}
|
|
2824
|
-
const dateKey = event.timestamp.toISOString().split("T")[0];
|
|
2825
|
-
if (!this.indexByDate.has(dateKey)) {
|
|
2826
|
-
this.indexByDate.set(dateKey, /* @__PURE__ */ new Set());
|
|
2827
|
-
}
|
|
2828
|
-
this.indexByDate.get(dateKey).add(event.id);
|
|
2829
|
-
if (event.metadata.providerId) {
|
|
2830
|
-
if (!this.indexByProvider.has(event.metadata.providerId)) {
|
|
2831
|
-
this.indexByProvider.set(event.metadata.providerId, /* @__PURE__ */ new Set());
|
|
2832
|
-
}
|
|
2833
|
-
this.indexByProvider.get(event.metadata.providerId).add(event.id);
|
|
2834
|
-
}
|
|
2835
|
-
if (event.metadata.channelId) {
|
|
2836
|
-
if (!this.indexByChannel.has(event.metadata.channelId)) {
|
|
2837
|
-
this.indexByChannel.set(event.metadata.channelId, /* @__PURE__ */ new Set());
|
|
2838
|
-
}
|
|
2839
|
-
this.indexByChannel.get(event.metadata.channelId).add(event.id);
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
/**
|
|
2843
|
-
* 인덱스에서 이벤트 제거
|
|
2844
|
-
*/
|
|
2845
|
-
removeFromIndexes(event) {
|
|
2846
|
-
const typeSet = this.indexByType.get(event.type);
|
|
2847
|
-
if (typeSet) {
|
|
2848
|
-
typeSet.delete(event.id);
|
|
2849
|
-
}
|
|
2850
|
-
const dateKey = event.timestamp.toISOString().split("T")[0];
|
|
2851
|
-
const dateSet = this.indexByDate.get(dateKey);
|
|
2852
|
-
if (dateSet) {
|
|
2853
|
-
dateSet.delete(event.id);
|
|
2854
|
-
if (dateSet.size === 0) {
|
|
2855
|
-
this.indexByDate.delete(dateKey);
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
if (event.metadata.providerId) {
|
|
2859
|
-
const providerSet = this.indexByProvider.get(event.metadata.providerId);
|
|
2860
|
-
if (providerSet) {
|
|
2861
|
-
providerSet.delete(event.id);
|
|
2862
|
-
if (providerSet.size === 0) {
|
|
2863
|
-
this.indexByProvider.delete(event.metadata.providerId);
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
if (event.metadata.channelId) {
|
|
2868
|
-
const channelSet = this.indexByChannel.get(event.metadata.channelId);
|
|
2869
|
-
if (channelSet) {
|
|
2870
|
-
channelSet.delete(event.id);
|
|
2871
|
-
if (channelSet.size === 0) {
|
|
2872
|
-
this.indexByChannel.delete(event.metadata.channelId);
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
}
|
|
2877
|
-
/**
|
|
2878
|
-
* 날짜 범위로 이벤트 ID 조회
|
|
2879
|
-
*/
|
|
2880
|
-
getEventIdsByDateRange(startDate, endDate) {
|
|
2881
|
-
const ids = /* @__PURE__ */ new Set();
|
|
2882
|
-
for (const [dateKey, eventIds] of this.indexByDate.entries()) {
|
|
2883
|
-
const date = new Date(dateKey);
|
|
2884
|
-
if (startDate && date < startDate) continue;
|
|
2885
|
-
if (endDate && date > endDate) continue;
|
|
2886
|
-
eventIds.forEach((id) => ids.add(id));
|
|
2887
|
-
}
|
|
2888
|
-
return ids;
|
|
2889
|
-
}
|
|
2890
|
-
/**
|
|
2891
|
-
* 필터 조건 매칭 확인
|
|
2892
|
-
*/
|
|
2893
|
-
matchesFilter(event, filter) {
|
|
2894
|
-
if (filter.templateId && filter.templateId.length > 0) {
|
|
2895
|
-
if (!event.metadata.templateId || !filter.templateId.includes(event.metadata.templateId)) {
|
|
2896
|
-
return false;
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
|
-
if (filter.messageId && filter.messageId.length > 0) {
|
|
2900
|
-
if (!event.metadata.messageId || !filter.messageId.includes(event.metadata.messageId)) {
|
|
2901
|
-
return false;
|
|
2902
|
-
}
|
|
2903
|
-
}
|
|
2904
|
-
if (filter.userId && filter.userId.length > 0) {
|
|
2905
|
-
if (!event.metadata.userId || !filter.userId.includes(event.metadata.userId)) {
|
|
2906
|
-
return false;
|
|
2907
|
-
}
|
|
2908
|
-
}
|
|
2909
|
-
if (filter.organizationId && filter.organizationId.length > 0) {
|
|
2910
|
-
if (!event.metadata.organizationId || !filter.organizationId.includes(event.metadata.organizationId)) {
|
|
2911
|
-
return false;
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
return true;
|
|
2915
|
-
}
|
|
2916
|
-
/**
|
|
2917
|
-
* 객체 필드 값 가져오기
|
|
2918
|
-
*/
|
|
2919
|
-
getFieldValue(obj, fieldPath) {
|
|
2920
|
-
return fieldPath.split(".").reduce((value, key) => value?.[key], obj);
|
|
2921
|
-
}
|
|
2922
|
-
/**
|
|
2923
|
-
* 메모리 사용량 추정
|
|
2924
|
-
*/
|
|
2925
|
-
estimateMemoryUsage() {
|
|
2926
|
-
let totalSize = 0;
|
|
2927
|
-
for (const event of this.events.values()) {
|
|
2928
|
-
totalSize += JSON.stringify(event).length * 2;
|
|
2929
|
-
}
|
|
2930
|
-
return totalSize;
|
|
2931
|
-
}
|
|
2932
|
-
/**
|
|
2933
|
-
* 메모리 사용량 확인 및 정리
|
|
2934
|
-
*/
|
|
2935
|
-
async checkMemoryUsage() {
|
|
2936
|
-
if (!this.config.maxMemoryUsage) return;
|
|
2937
|
-
const currentUsage = this.estimateMemoryUsage();
|
|
2938
|
-
if (currentUsage > this.config.maxMemoryUsage) {
|
|
2939
|
-
const events = Array.from(this.events.values()).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
2940
|
-
let removedCount = 0;
|
|
2941
|
-
const targetUsage = this.config.maxMemoryUsage * 0.8;
|
|
2942
|
-
for (const event of events) {
|
|
2943
|
-
if (this.estimateMemoryUsage() <= targetUsage) break;
|
|
2944
|
-
this.removeFromIndexes(event);
|
|
2945
|
-
this.events.delete(event.id);
|
|
2946
|
-
removedCount++;
|
|
2947
|
-
}
|
|
2948
|
-
if (removedCount > 0) {
|
|
2949
|
-
this.emit("memoryCleanup", {
|
|
2950
|
-
removedCount,
|
|
2951
|
-
previousUsage: currentUsage,
|
|
2952
|
-
currentUsage: this.estimateMemoryUsage()
|
|
2953
|
-
});
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
/**
|
|
2958
|
-
* 이벤트 내용 키 생성 (중복 검사용)
|
|
2959
|
-
*/
|
|
2960
|
-
generateContentKey(event) {
|
|
2961
|
-
return `${event.type}_${event.metadata.messageId || ""}_${event.metadata.templateId || ""}_${JSON.stringify(event.data)}`;
|
|
2962
|
-
}
|
|
2963
|
-
/**
|
|
2964
|
-
* 정리 작업 시작
|
|
2965
|
-
*/
|
|
2966
|
-
startCleanupTask() {
|
|
2967
|
-
this.cleanupInterval = setInterval(async () => {
|
|
2968
|
-
try {
|
|
2969
|
-
await this.cleanupOldEvents();
|
|
2970
|
-
await this.cleanupDuplicateEvents();
|
|
2971
|
-
} catch (error) {
|
|
2972
|
-
this.emit("cleanupError", error);
|
|
2973
|
-
}
|
|
2974
|
-
}, 60 * 60 * 1e3);
|
|
2975
|
-
}
|
|
2976
|
-
/**
|
|
2977
|
-
* 파일에 이벤트 추가
|
|
2978
|
-
*/
|
|
2979
|
-
async appendToFile(event) {
|
|
2980
|
-
if (!this.config.filePath) return;
|
|
2981
|
-
try {
|
|
2982
|
-
const line = JSON.stringify(event) + "\n";
|
|
2983
|
-
await fs4.appendFile(this.config.filePath, line, "utf8");
|
|
2984
|
-
} catch (error) {
|
|
2985
|
-
this.emit("appendError", error);
|
|
2986
|
-
}
|
|
2987
|
-
}
|
|
2988
|
-
/**
|
|
2989
|
-
* 파일에서 데이터 로드
|
|
2990
|
-
*/
|
|
2991
|
-
async loadFromFile() {
|
|
2992
|
-
if (!this.config.filePath) return;
|
|
2993
|
-
try {
|
|
2994
|
-
const data = await fs4.readFile(this.config.filePath, "utf8");
|
|
2995
|
-
const lines = data.trim().split("\n").filter((line) => line.trim());
|
|
2996
|
-
for (const line of lines) {
|
|
2997
|
-
try {
|
|
2998
|
-
const eventData = JSON.parse(line);
|
|
2999
|
-
const event = {
|
|
3000
|
-
...eventData,
|
|
3001
|
-
timestamp: new Date(eventData.timestamp)
|
|
3002
|
-
};
|
|
3003
|
-
this.events.set(event.id, event);
|
|
3004
|
-
this.addToIndexes(event);
|
|
3005
|
-
} catch (parseError) {
|
|
3006
|
-
this.emit("parseError", { line, error: parseError });
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
this.emit("dataLoaded", {
|
|
3010
|
-
filePath: this.config.filePath,
|
|
3011
|
-
eventCount: this.events.size
|
|
3012
|
-
});
|
|
3013
|
-
} catch (error) {
|
|
3014
|
-
if (error.code !== "ENOENT") {
|
|
3015
|
-
this.emit("loadError", error);
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
/**
|
|
3020
|
-
* 파일에 데이터 저장
|
|
3021
|
-
*/
|
|
3022
|
-
async saveToFile() {
|
|
3023
|
-
if (!this.config.filePath) return;
|
|
3024
|
-
try {
|
|
3025
|
-
const lines = Array.from(this.events.values()).map((event) => JSON.stringify(event)).join("\n");
|
|
3026
|
-
await fs4.mkdir(path4.dirname(this.config.filePath), { recursive: true });
|
|
3027
|
-
await fs4.writeFile(this.config.filePath, lines + "\n", "utf8");
|
|
3028
|
-
this.emit("dataSaved", {
|
|
3029
|
-
filePath: this.config.filePath,
|
|
3030
|
-
eventCount: this.events.size
|
|
3031
|
-
});
|
|
3032
|
-
} catch (error) {
|
|
3033
|
-
this.emit("saveError", error);
|
|
3034
|
-
throw error;
|
|
3035
|
-
}
|
|
3036
|
-
}
|
|
3037
|
-
/**
|
|
3038
|
-
* 이벤트 저장소 종료
|
|
3039
|
-
*/
|
|
3040
|
-
async shutdown() {
|
|
3041
|
-
if (this.cleanupInterval) {
|
|
3042
|
-
clearInterval(this.cleanupInterval);
|
|
3043
|
-
this.cleanupInterval = null;
|
|
3044
|
-
}
|
|
3045
|
-
if (this.config.type === "file") {
|
|
3046
|
-
await this.saveToFile().catch((error) => {
|
|
3047
|
-
this.emit("saveError", error);
|
|
3048
|
-
});
|
|
3049
|
-
}
|
|
3050
|
-
this.emit("shutdown", { eventCount: this.events.size });
|
|
3051
|
-
}
|
|
3052
|
-
};
|
|
3053
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
3054
|
-
0 && (module.exports = {
|
|
3055
|
-
BatchDispatcher,
|
|
3056
|
-
DeliveryStore,
|
|
3057
|
-
EndpointManager,
|
|
3058
|
-
EventStore,
|
|
3059
|
-
LoadBalancer,
|
|
3060
|
-
QueueManager,
|
|
3061
|
-
RetryManager,
|
|
3062
|
-
SecurityManager,
|
|
3063
|
-
WebhookDispatcher,
|
|
3064
|
-
WebhookEventType,
|
|
3065
|
-
WebhookRegistry,
|
|
3066
|
-
WebhookService
|
|
3067
|
-
});
|
|
3068
|
-
//# sourceMappingURL=index.cjs.map
|