@k-msg/messaging 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/delivery/tracker.d.ts +144 -0
- package/dist/hooks.d.ts +11 -0
- package/dist/index.d.ts +10 -854
- package/dist/index.js +112 -2030
- package/dist/index.js.map +106 -1
- package/dist/index.mjs +121 -0
- package/dist/index.mjs.map +106 -0
- package/dist/k-msg.d.ts +8 -0
- package/dist/personalization/variable.replacer.d.ts +139 -0
- package/dist/queue/job-queue.interface.d.ts +39 -0
- package/dist/queue/job.processor.d.ts +133 -0
- package/dist/queue/retry.handler.d.ts +105 -0
- package/dist/queue/sqlite-job-queue.d.ts +28 -0
- package/dist/sender/bulk.sender.d.ts +18 -0
- package/dist/types/message.types.d.ts +280 -0
- package/package.json +20 -13
- package/dist/index.cjs +0 -2084
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -854
package/dist/index.cjs
DELETED
|
@@ -1,2084 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
BulkMessageSender: () => BulkMessageSender,
|
|
24
|
-
DeliveryTracker: () => DeliveryTracker,
|
|
25
|
-
JobProcessor: () => JobProcessor,
|
|
26
|
-
MessageErrorSchema: () => MessageErrorSchema,
|
|
27
|
-
MessageEventType: () => MessageEventType,
|
|
28
|
-
MessageJobProcessor: () => MessageJobProcessor,
|
|
29
|
-
MessageRequestSchema: () => MessageRequestSchema,
|
|
30
|
-
MessageResultSchema: () => MessageResultSchema,
|
|
31
|
-
MessageRetryHandler: () => MessageRetryHandler,
|
|
32
|
-
MessageStatus: () => MessageStatus,
|
|
33
|
-
RecipientResultSchema: () => RecipientResultSchema,
|
|
34
|
-
RecipientSchema: () => RecipientSchema,
|
|
35
|
-
SchedulingOptionsSchema: () => SchedulingOptionsSchema,
|
|
36
|
-
SendingOptionsSchema: () => SendingOptionsSchema,
|
|
37
|
-
SingleMessageSender: () => SingleMessageSender,
|
|
38
|
-
VariableMapSchema: () => VariableMapSchema,
|
|
39
|
-
VariableReplacer: () => VariableReplacer,
|
|
40
|
-
VariableUtils: () => VariableUtils,
|
|
41
|
-
defaultVariableReplacer: () => defaultVariableReplacer
|
|
42
|
-
});
|
|
43
|
-
module.exports = __toCommonJS(index_exports);
|
|
44
|
-
|
|
45
|
-
// src/types/message.types.ts
|
|
46
|
-
var import_zod = require("zod");
|
|
47
|
-
var MessageStatus = /* @__PURE__ */ ((MessageStatus2) => {
|
|
48
|
-
MessageStatus2["QUEUED"] = "QUEUED";
|
|
49
|
-
MessageStatus2["SENDING"] = "SENDING";
|
|
50
|
-
MessageStatus2["SENT"] = "SENT";
|
|
51
|
-
MessageStatus2["DELIVERED"] = "DELIVERED";
|
|
52
|
-
MessageStatus2["FAILED"] = "FAILED";
|
|
53
|
-
MessageStatus2["CLICKED"] = "CLICKED";
|
|
54
|
-
MessageStatus2["CANCELLED"] = "CANCELLED";
|
|
55
|
-
return MessageStatus2;
|
|
56
|
-
})(MessageStatus || {});
|
|
57
|
-
var MessageEventType = /* @__PURE__ */ ((MessageEventType2) => {
|
|
58
|
-
MessageEventType2["TEMPLATE_CREATED"] = "template.created";
|
|
59
|
-
MessageEventType2["TEMPLATE_APPROVED"] = "template.approved";
|
|
60
|
-
MessageEventType2["TEMPLATE_REJECTED"] = "template.rejected";
|
|
61
|
-
MessageEventType2["TEMPLATE_UPDATED"] = "template.updated";
|
|
62
|
-
MessageEventType2["TEMPLATE_DELETED"] = "template.deleted";
|
|
63
|
-
MessageEventType2["MESSAGE_QUEUED"] = "message.queued";
|
|
64
|
-
MessageEventType2["MESSAGE_SENT"] = "message.sent";
|
|
65
|
-
MessageEventType2["MESSAGE_DELIVERED"] = "message.delivered";
|
|
66
|
-
MessageEventType2["MESSAGE_FAILED"] = "message.failed";
|
|
67
|
-
MessageEventType2["MESSAGE_CLICKED"] = "message.clicked";
|
|
68
|
-
MessageEventType2["MESSAGE_CANCELLED"] = "message.cancelled";
|
|
69
|
-
MessageEventType2["CHANNEL_CREATED"] = "channel.created";
|
|
70
|
-
MessageEventType2["CHANNEL_VERIFIED"] = "channel.verified";
|
|
71
|
-
MessageEventType2["SENDER_NUMBER_ADDED"] = "sender_number.added";
|
|
72
|
-
MessageEventType2["QUOTA_WARNING"] = "system.quota_warning";
|
|
73
|
-
MessageEventType2["QUOTA_EXCEEDED"] = "system.quota_exceeded";
|
|
74
|
-
MessageEventType2["PROVIDER_ERROR"] = "system.provider_error";
|
|
75
|
-
return MessageEventType2;
|
|
76
|
-
})(MessageEventType || {});
|
|
77
|
-
var VariableMapSchema = import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.date()]));
|
|
78
|
-
var RecipientSchema = import_zod.z.object({
|
|
79
|
-
phoneNumber: import_zod.z.string().regex(/^[0-9]{10,11}$/),
|
|
80
|
-
variables: VariableMapSchema.optional(),
|
|
81
|
-
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.any()).optional()
|
|
82
|
-
});
|
|
83
|
-
var SchedulingOptionsSchema = import_zod.z.object({
|
|
84
|
-
scheduledAt: import_zod.z.date().min(/* @__PURE__ */ new Date()),
|
|
85
|
-
timezone: import_zod.z.string().optional(),
|
|
86
|
-
retryCount: import_zod.z.number().min(0).max(5).optional().default(3)
|
|
87
|
-
});
|
|
88
|
-
var SendingOptionsSchema = import_zod.z.object({
|
|
89
|
-
priority: import_zod.z.enum(["high", "normal", "low"]).optional().default("normal"),
|
|
90
|
-
ttl: import_zod.z.number().min(0).optional(),
|
|
91
|
-
failover: import_zod.z.object({
|
|
92
|
-
enabled: import_zod.z.boolean(),
|
|
93
|
-
fallbackChannel: import_zod.z.enum(["sms", "lms"]).optional(),
|
|
94
|
-
fallbackContent: import_zod.z.string().optional()
|
|
95
|
-
}).optional(),
|
|
96
|
-
deduplication: import_zod.z.object({
|
|
97
|
-
enabled: import_zod.z.boolean(),
|
|
98
|
-
window: import_zod.z.number().min(0).max(3600)
|
|
99
|
-
}).optional(),
|
|
100
|
-
tracking: import_zod.z.object({
|
|
101
|
-
enabled: import_zod.z.boolean(),
|
|
102
|
-
webhookUrl: import_zod.z.string().url().optional()
|
|
103
|
-
}).optional()
|
|
104
|
-
});
|
|
105
|
-
var MessageRequestSchema = import_zod.z.object({
|
|
106
|
-
templateId: import_zod.z.string().min(1),
|
|
107
|
-
recipients: import_zod.z.array(RecipientSchema).min(1).max(1e4),
|
|
108
|
-
variables: VariableMapSchema,
|
|
109
|
-
scheduling: SchedulingOptionsSchema.optional(),
|
|
110
|
-
options: SendingOptionsSchema.optional()
|
|
111
|
-
});
|
|
112
|
-
var MessageErrorSchema = import_zod.z.object({
|
|
113
|
-
code: import_zod.z.string(),
|
|
114
|
-
message: import_zod.z.string(),
|
|
115
|
-
details: import_zod.z.record(import_zod.z.string(), import_zod.z.any()).optional()
|
|
116
|
-
});
|
|
117
|
-
var RecipientResultSchema = import_zod.z.object({
|
|
118
|
-
phoneNumber: import_zod.z.string(),
|
|
119
|
-
messageId: import_zod.z.string().optional(),
|
|
120
|
-
status: import_zod.z.nativeEnum(MessageStatus),
|
|
121
|
-
error: MessageErrorSchema.optional(),
|
|
122
|
-
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.any()).optional()
|
|
123
|
-
});
|
|
124
|
-
var MessageResultSchema = import_zod.z.object({
|
|
125
|
-
requestId: import_zod.z.string(),
|
|
126
|
-
results: import_zod.z.array(RecipientResultSchema),
|
|
127
|
-
summary: import_zod.z.object({
|
|
128
|
-
total: import_zod.z.number().min(0),
|
|
129
|
-
queued: import_zod.z.number().min(0),
|
|
130
|
-
sent: import_zod.z.number().min(0),
|
|
131
|
-
failed: import_zod.z.number().min(0)
|
|
132
|
-
}),
|
|
133
|
-
metadata: import_zod.z.object({
|
|
134
|
-
createdAt: import_zod.z.date(),
|
|
135
|
-
provider: import_zod.z.string(),
|
|
136
|
-
templateId: import_zod.z.string()
|
|
137
|
-
})
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// src/sender/single.sender.ts
|
|
141
|
-
var SingleMessageSender = class {
|
|
142
|
-
constructor() {
|
|
143
|
-
this.providers = /* @__PURE__ */ new Map();
|
|
144
|
-
this.templates = /* @__PURE__ */ new Map();
|
|
145
|
-
}
|
|
146
|
-
// Template cache
|
|
147
|
-
addProvider(provider) {
|
|
148
|
-
this.providers.set(provider.id, provider);
|
|
149
|
-
}
|
|
150
|
-
removeProvider(providerId) {
|
|
151
|
-
this.providers.delete(providerId);
|
|
152
|
-
}
|
|
153
|
-
async send(request) {
|
|
154
|
-
const requestId = this.generateRequestId();
|
|
155
|
-
const results = [];
|
|
156
|
-
const template = await this.getTemplate(request.templateId);
|
|
157
|
-
if (!template) {
|
|
158
|
-
throw new Error(`Template ${request.templateId} not found`);
|
|
159
|
-
}
|
|
160
|
-
const provider = this.providers.get(template.provider);
|
|
161
|
-
if (!provider) {
|
|
162
|
-
throw new Error(`Provider ${template.provider} not found`);
|
|
163
|
-
}
|
|
164
|
-
for (const recipient of request.recipients) {
|
|
165
|
-
try {
|
|
166
|
-
const result = await this.sendToRecipient(
|
|
167
|
-
provider,
|
|
168
|
-
template,
|
|
169
|
-
recipient,
|
|
170
|
-
request.variables,
|
|
171
|
-
request.options
|
|
172
|
-
);
|
|
173
|
-
results.push(result);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
results.push({
|
|
176
|
-
phoneNumber: recipient.phoneNumber,
|
|
177
|
-
status: "FAILED" /* FAILED */,
|
|
178
|
-
error: {
|
|
179
|
-
code: "SEND_ERROR",
|
|
180
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
181
|
-
},
|
|
182
|
-
metadata: recipient.metadata
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const summary = this.calculateSummary(results);
|
|
187
|
-
return {
|
|
188
|
-
requestId,
|
|
189
|
-
results,
|
|
190
|
-
summary,
|
|
191
|
-
metadata: {
|
|
192
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
193
|
-
provider: template.provider,
|
|
194
|
-
templateId: request.templateId
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
async sendToRecipient(provider, template, recipient, commonVariables, options) {
|
|
199
|
-
const variables = { ...commonVariables, ...recipient.variables };
|
|
200
|
-
const providerRequest = {
|
|
201
|
-
templateCode: template.code,
|
|
202
|
-
phoneNumber: recipient.phoneNumber,
|
|
203
|
-
variables,
|
|
204
|
-
options
|
|
205
|
-
};
|
|
206
|
-
const providerResult = await provider.send(providerRequest);
|
|
207
|
-
return {
|
|
208
|
-
phoneNumber: recipient.phoneNumber,
|
|
209
|
-
messageId: providerResult.messageId,
|
|
210
|
-
status: providerResult.status,
|
|
211
|
-
error: providerResult.error,
|
|
212
|
-
metadata: recipient.metadata
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
async getTemplate(templateId) {
|
|
216
|
-
if (this.templates.has(templateId)) {
|
|
217
|
-
return this.templates.get(templateId);
|
|
218
|
-
}
|
|
219
|
-
const template = {
|
|
220
|
-
id: templateId,
|
|
221
|
-
code: "TEMPLATE_CODE",
|
|
222
|
-
provider: "mock-provider",
|
|
223
|
-
variables: [],
|
|
224
|
-
content: "Mock template content"
|
|
225
|
-
};
|
|
226
|
-
this.templates.set(templateId, template);
|
|
227
|
-
return template;
|
|
228
|
-
}
|
|
229
|
-
calculateSummary(results) {
|
|
230
|
-
return {
|
|
231
|
-
total: results.length,
|
|
232
|
-
queued: results.filter((r) => r.status === "QUEUED" /* QUEUED */).length,
|
|
233
|
-
sent: results.filter((r) => r.status === "SENT" /* SENT */).length,
|
|
234
|
-
failed: results.filter((r) => r.status === "FAILED" /* FAILED */).length
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
generateRequestId() {
|
|
238
|
-
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
239
|
-
}
|
|
240
|
-
async cancelMessage(messageId) {
|
|
241
|
-
throw new Error("Not implemented");
|
|
242
|
-
}
|
|
243
|
-
async getMessageStatus(messageId) {
|
|
244
|
-
throw new Error("Not implemented");
|
|
245
|
-
}
|
|
246
|
-
async resendMessage(messageId, options) {
|
|
247
|
-
throw new Error("Not implemented");
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
// src/sender/bulk.sender.ts
|
|
252
|
-
var BulkMessageSender = class {
|
|
253
|
-
constructor(singleSender) {
|
|
254
|
-
this.activeBulkJobs = /* @__PURE__ */ new Map();
|
|
255
|
-
this.singleSender = singleSender;
|
|
256
|
-
}
|
|
257
|
-
async sendBulk(request) {
|
|
258
|
-
const requestId = this.generateRequestId();
|
|
259
|
-
const batchSize = request.options?.batchSize || 100;
|
|
260
|
-
const batchDelay = request.options?.batchDelay || 1e3;
|
|
261
|
-
const batches = this.createBatches(request.recipients, batchSize);
|
|
262
|
-
const bulkResult = {
|
|
263
|
-
requestId,
|
|
264
|
-
totalRecipients: request.recipients.length,
|
|
265
|
-
batches: [],
|
|
266
|
-
summary: {
|
|
267
|
-
queued: request.recipients.length,
|
|
268
|
-
sent: 0,
|
|
269
|
-
failed: 0,
|
|
270
|
-
processing: 0
|
|
271
|
-
},
|
|
272
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
273
|
-
};
|
|
274
|
-
const bulkJob = {
|
|
275
|
-
id: requestId,
|
|
276
|
-
request,
|
|
277
|
-
result: bulkResult,
|
|
278
|
-
status: "processing",
|
|
279
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
280
|
-
};
|
|
281
|
-
this.activeBulkJobs.set(requestId, bulkJob);
|
|
282
|
-
this.processBatchesAsync(bulkJob, batches, batchDelay);
|
|
283
|
-
return bulkResult;
|
|
284
|
-
}
|
|
285
|
-
async processBatchesAsync(bulkJob, batches, batchDelay) {
|
|
286
|
-
try {
|
|
287
|
-
for (let i = 0; i < batches.length; i++) {
|
|
288
|
-
const batch = batches[i];
|
|
289
|
-
const batchId = `${bulkJob.id}_batch_${i + 1}`;
|
|
290
|
-
const batchResult = {
|
|
291
|
-
batchId,
|
|
292
|
-
batchNumber: i + 1,
|
|
293
|
-
recipients: [],
|
|
294
|
-
status: "processing",
|
|
295
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
296
|
-
};
|
|
297
|
-
bulkJob.result.batches.push(batchResult);
|
|
298
|
-
bulkJob.result.summary.processing += batch.length;
|
|
299
|
-
bulkJob.result.summary.queued -= batch.length;
|
|
300
|
-
try {
|
|
301
|
-
const batchRecipients = await this.processBatch(
|
|
302
|
-
bulkJob.request,
|
|
303
|
-
batch,
|
|
304
|
-
batchId
|
|
305
|
-
);
|
|
306
|
-
batchResult.recipients = batchRecipients;
|
|
307
|
-
batchResult.status = "completed";
|
|
308
|
-
batchResult.completedAt = /* @__PURE__ */ new Date();
|
|
309
|
-
const sent = batchRecipients.filter((r) => r.status === "SENT" /* SENT */).length;
|
|
310
|
-
const failed = batchRecipients.filter((r) => r.status === "FAILED" /* FAILED */).length;
|
|
311
|
-
bulkJob.result.summary.sent += sent;
|
|
312
|
-
bulkJob.result.summary.failed += failed;
|
|
313
|
-
bulkJob.result.summary.processing -= batch.length;
|
|
314
|
-
} catch (error) {
|
|
315
|
-
batchResult.status = "failed";
|
|
316
|
-
batchResult.completedAt = /* @__PURE__ */ new Date();
|
|
317
|
-
batchResult.recipients = batch.map((recipient) => ({
|
|
318
|
-
phoneNumber: recipient.phoneNumber,
|
|
319
|
-
status: "FAILED" /* FAILED */,
|
|
320
|
-
error: {
|
|
321
|
-
code: "BATCH_ERROR",
|
|
322
|
-
message: error instanceof Error ? error.message : "Batch processing failed"
|
|
323
|
-
},
|
|
324
|
-
metadata: recipient.metadata
|
|
325
|
-
}));
|
|
326
|
-
bulkJob.result.summary.failed += batch.length;
|
|
327
|
-
bulkJob.result.summary.processing -= batch.length;
|
|
328
|
-
}
|
|
329
|
-
if (i < batches.length - 1) {
|
|
330
|
-
await this.delay(batchDelay);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
bulkJob.status = "completed";
|
|
334
|
-
bulkJob.result.completedAt = /* @__PURE__ */ new Date();
|
|
335
|
-
} catch (error) {
|
|
336
|
-
bulkJob.status = "failed";
|
|
337
|
-
bulkJob.result.completedAt = /* @__PURE__ */ new Date();
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
async processBatch(request, batchRecipients, batchId) {
|
|
341
|
-
const results = [];
|
|
342
|
-
const maxConcurrency = request.options?.maxConcurrency || 10;
|
|
343
|
-
const promises = [];
|
|
344
|
-
for (let i = 0; i < batchRecipients.length; i += maxConcurrency) {
|
|
345
|
-
const chunk = batchRecipients.slice(i, i + maxConcurrency);
|
|
346
|
-
const chunkPromises = chunk.map(
|
|
347
|
-
(recipient) => this.processRecipient(request, recipient)
|
|
348
|
-
);
|
|
349
|
-
const chunkResults = await Promise.allSettled(chunkPromises);
|
|
350
|
-
for (const result of chunkResults) {
|
|
351
|
-
if (result.status === "fulfilled") {
|
|
352
|
-
results.push(result.value);
|
|
353
|
-
} else {
|
|
354
|
-
results.push({
|
|
355
|
-
phoneNumber: "unknown",
|
|
356
|
-
status: "FAILED" /* FAILED */,
|
|
357
|
-
error: {
|
|
358
|
-
code: "PROCESSING_ERROR",
|
|
359
|
-
message: result.reason?.message || "Unknown processing error"
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
return results;
|
|
366
|
-
}
|
|
367
|
-
async processRecipient(request, recipient) {
|
|
368
|
-
try {
|
|
369
|
-
const variables = { ...request.commonVariables, ...recipient.variables };
|
|
370
|
-
const messageRequest = {
|
|
371
|
-
templateId: request.templateId,
|
|
372
|
-
recipients: [{
|
|
373
|
-
phoneNumber: recipient.phoneNumber,
|
|
374
|
-
variables: {},
|
|
375
|
-
metadata: recipient.metadata
|
|
376
|
-
}],
|
|
377
|
-
variables,
|
|
378
|
-
options: request.options
|
|
379
|
-
};
|
|
380
|
-
const result = await this.singleSender.send(messageRequest);
|
|
381
|
-
return result.results[0];
|
|
382
|
-
} catch (error) {
|
|
383
|
-
return {
|
|
384
|
-
phoneNumber: recipient.phoneNumber,
|
|
385
|
-
status: "FAILED" /* FAILED */,
|
|
386
|
-
error: {
|
|
387
|
-
code: "RECIPIENT_ERROR",
|
|
388
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
389
|
-
},
|
|
390
|
-
metadata: recipient.metadata
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
createBatches(items, batchSize) {
|
|
395
|
-
const batches = [];
|
|
396
|
-
for (let i = 0; i < items.length; i += batchSize) {
|
|
397
|
-
batches.push(items.slice(i, i + batchSize));
|
|
398
|
-
}
|
|
399
|
-
return batches;
|
|
400
|
-
}
|
|
401
|
-
delay(ms) {
|
|
402
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
403
|
-
}
|
|
404
|
-
generateRequestId() {
|
|
405
|
-
return `bulk_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
406
|
-
}
|
|
407
|
-
async getBulkStatus(requestId) {
|
|
408
|
-
const job = this.activeBulkJobs.get(requestId);
|
|
409
|
-
return job ? job.result : null;
|
|
410
|
-
}
|
|
411
|
-
async cancelBulkJob(requestId) {
|
|
412
|
-
const job = this.activeBulkJobs.get(requestId);
|
|
413
|
-
if (!job) {
|
|
414
|
-
return false;
|
|
415
|
-
}
|
|
416
|
-
job.status = "cancelled";
|
|
417
|
-
for (const batch of job.result.batches) {
|
|
418
|
-
if (batch.status === "pending" || batch.status === "processing") {
|
|
419
|
-
batch.status = "failed";
|
|
420
|
-
batch.completedAt = /* @__PURE__ */ new Date();
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return true;
|
|
424
|
-
}
|
|
425
|
-
async retryFailedBatch(requestId, batchId) {
|
|
426
|
-
const job = this.activeBulkJobs.get(requestId);
|
|
427
|
-
if (!job) {
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
const batch = job.result.batches.find((b) => b.batchId === batchId);
|
|
431
|
-
if (!batch || batch.status !== "failed") {
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
batch.status = "processing";
|
|
435
|
-
batch.createdAt = /* @__PURE__ */ new Date();
|
|
436
|
-
delete batch.completedAt;
|
|
437
|
-
try {
|
|
438
|
-
const failedRecipients = batch.recipients.filter((r) => r.status === "FAILED" /* FAILED */).map((r) => ({
|
|
439
|
-
phoneNumber: r.phoneNumber,
|
|
440
|
-
variables: {},
|
|
441
|
-
metadata: r.metadata
|
|
442
|
-
}));
|
|
443
|
-
const retryResults = await this.processBatch(
|
|
444
|
-
job.request,
|
|
445
|
-
failedRecipients,
|
|
446
|
-
batchId
|
|
447
|
-
);
|
|
448
|
-
batch.recipients = retryResults;
|
|
449
|
-
batch.status = "completed";
|
|
450
|
-
batch.completedAt = /* @__PURE__ */ new Date();
|
|
451
|
-
return batch;
|
|
452
|
-
} catch (error) {
|
|
453
|
-
batch.status = "failed";
|
|
454
|
-
batch.completedAt = /* @__PURE__ */ new Date();
|
|
455
|
-
return batch;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
cleanup() {
|
|
459
|
-
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3);
|
|
460
|
-
for (const [id, job] of this.activeBulkJobs) {
|
|
461
|
-
if (job.status === "completed" && job.createdAt < oneDayAgo) {
|
|
462
|
-
this.activeBulkJobs.delete(id);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
// src/queue/job.processor.ts
|
|
469
|
-
var import_events = require("events");
|
|
470
|
-
var import_core = require("@k-msg/core");
|
|
471
|
-
var JobProcessor = class extends import_events.EventEmitter {
|
|
472
|
-
constructor(options) {
|
|
473
|
-
super();
|
|
474
|
-
this.options = options;
|
|
475
|
-
this.handlers = /* @__PURE__ */ new Map();
|
|
476
|
-
this.queue = [];
|
|
477
|
-
this.processing = /* @__PURE__ */ new Set();
|
|
478
|
-
this.isRunning = false;
|
|
479
|
-
this.metrics = {
|
|
480
|
-
processed: 0,
|
|
481
|
-
succeeded: 0,
|
|
482
|
-
failed: 0,
|
|
483
|
-
retried: 0,
|
|
484
|
-
activeJobs: 0,
|
|
485
|
-
queueSize: 0,
|
|
486
|
-
averageProcessingTime: 0
|
|
487
|
-
};
|
|
488
|
-
if (options.rateLimiter) {
|
|
489
|
-
this.rateLimiter = new import_core.RateLimiter(
|
|
490
|
-
options.rateLimiter.maxRequests,
|
|
491
|
-
options.rateLimiter.windowMs
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
if (options.circuitBreaker) {
|
|
495
|
-
this.circuitBreaker = new import_core.CircuitBreaker(options.circuitBreaker);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
/**
|
|
499
|
-
* Register a job handler
|
|
500
|
-
*/
|
|
501
|
-
handle(jobType, handler) {
|
|
502
|
-
this.handlers.set(jobType, handler);
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Add a job to the queue
|
|
506
|
-
*/
|
|
507
|
-
async add(jobType, data, options = {}) {
|
|
508
|
-
const jobId = `${jobType}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
509
|
-
const now = /* @__PURE__ */ new Date();
|
|
510
|
-
const job = {
|
|
511
|
-
id: jobId,
|
|
512
|
-
type: jobType,
|
|
513
|
-
data,
|
|
514
|
-
priority: options.priority || 5,
|
|
515
|
-
attempts: 0,
|
|
516
|
-
maxAttempts: options.maxAttempts || this.options.maxRetries,
|
|
517
|
-
delay: options.delay || 0,
|
|
518
|
-
createdAt: now,
|
|
519
|
-
processAt: new Date(now.getTime() + (options.delay || 0)),
|
|
520
|
-
metadata: options.metadata || {}
|
|
521
|
-
};
|
|
522
|
-
const insertIndex = this.queue.findIndex(
|
|
523
|
-
(existingJob) => existingJob.priority < job.priority
|
|
524
|
-
);
|
|
525
|
-
if (insertIndex === -1) {
|
|
526
|
-
this.queue.push(job);
|
|
527
|
-
} else {
|
|
528
|
-
this.queue.splice(insertIndex, 0, job);
|
|
529
|
-
}
|
|
530
|
-
this.updateMetrics();
|
|
531
|
-
this.emit("job:added", job);
|
|
532
|
-
return jobId;
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Start processing jobs
|
|
536
|
-
*/
|
|
537
|
-
start() {
|
|
538
|
-
if (this.isRunning) {
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
this.isRunning = true;
|
|
542
|
-
this.scheduleNextPoll();
|
|
543
|
-
this.emit("processor:started");
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* Stop processing jobs
|
|
547
|
-
*/
|
|
548
|
-
async stop() {
|
|
549
|
-
this.isRunning = false;
|
|
550
|
-
if (this.pollTimer) {
|
|
551
|
-
clearTimeout(this.pollTimer);
|
|
552
|
-
this.pollTimer = void 0;
|
|
553
|
-
}
|
|
554
|
-
while (this.processing.size > 0) {
|
|
555
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
556
|
-
}
|
|
557
|
-
this.emit("processor:stopped");
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Get current metrics
|
|
561
|
-
*/
|
|
562
|
-
getMetrics() {
|
|
563
|
-
return { ...this.metrics };
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Get queue status
|
|
567
|
-
*/
|
|
568
|
-
getQueueStatus() {
|
|
569
|
-
const failed = this.queue.filter((job) => job.failedAt).length;
|
|
570
|
-
return {
|
|
571
|
-
pending: this.queue.length - failed,
|
|
572
|
-
processing: this.processing.size,
|
|
573
|
-
failed,
|
|
574
|
-
totalProcessed: this.metrics.processed
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Remove completed jobs from queue
|
|
579
|
-
*/
|
|
580
|
-
cleanup() {
|
|
581
|
-
const initialLength = this.queue.length;
|
|
582
|
-
this.queue = this.queue.filter(
|
|
583
|
-
(job) => !job.completedAt && !job.failedAt
|
|
584
|
-
);
|
|
585
|
-
const removed = initialLength - this.queue.length;
|
|
586
|
-
this.updateMetrics();
|
|
587
|
-
return removed;
|
|
588
|
-
}
|
|
589
|
-
/**
|
|
590
|
-
* Get specific job by ID
|
|
591
|
-
*/
|
|
592
|
-
getJob(jobId) {
|
|
593
|
-
return this.queue.find((job) => job.id === jobId);
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Remove job from queue
|
|
597
|
-
*/
|
|
598
|
-
removeJob(jobId) {
|
|
599
|
-
const index = this.queue.findIndex((job) => job.id === jobId);
|
|
600
|
-
if (index !== -1) {
|
|
601
|
-
this.queue.splice(index, 1);
|
|
602
|
-
this.processing.delete(jobId);
|
|
603
|
-
this.updateMetrics();
|
|
604
|
-
return true;
|
|
605
|
-
}
|
|
606
|
-
return false;
|
|
607
|
-
}
|
|
608
|
-
scheduleNextPoll() {
|
|
609
|
-
if (!this.isRunning) {
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
this.pollTimer = setTimeout(() => {
|
|
613
|
-
this.processJobs();
|
|
614
|
-
this.scheduleNextPoll();
|
|
615
|
-
}, this.options.pollInterval);
|
|
616
|
-
}
|
|
617
|
-
async processJobs() {
|
|
618
|
-
const availableSlots = this.options.concurrency - this.processing.size;
|
|
619
|
-
if (availableSlots <= 0) {
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
const now = /* @__PURE__ */ new Date();
|
|
623
|
-
const readyJobs = this.queue.filter(
|
|
624
|
-
(job) => !job.completedAt && !job.failedAt && !this.processing.has(job.id) && job.processAt <= now
|
|
625
|
-
).slice(0, availableSlots);
|
|
626
|
-
for (const job of readyJobs) {
|
|
627
|
-
this.processJob(job);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
async processJob(job) {
|
|
631
|
-
const handler = this.handlers.get(job.type);
|
|
632
|
-
if (!handler) {
|
|
633
|
-
this.failJob(job, `No handler registered for job type: ${job.type}`);
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
this.processing.add(job.id);
|
|
637
|
-
job.attempts++;
|
|
638
|
-
this.metrics.activeJobs++;
|
|
639
|
-
const startTime = Date.now();
|
|
640
|
-
try {
|
|
641
|
-
if (this.rateLimiter) {
|
|
642
|
-
await this.rateLimiter.acquire();
|
|
643
|
-
}
|
|
644
|
-
const executeJob = async () => handler(job);
|
|
645
|
-
const result = this.circuitBreaker ? await this.circuitBreaker.execute(executeJob) : await executeJob();
|
|
646
|
-
job.completedAt = /* @__PURE__ */ new Date();
|
|
647
|
-
this.processing.delete(job.id);
|
|
648
|
-
this.metrics.activeJobs--;
|
|
649
|
-
this.metrics.succeeded++;
|
|
650
|
-
this.metrics.processed++;
|
|
651
|
-
const processingTime = Date.now() - startTime;
|
|
652
|
-
this.updateAverageProcessingTime(processingTime);
|
|
653
|
-
this.emit("job:completed", { job, result, processingTime });
|
|
654
|
-
} catch (error) {
|
|
655
|
-
this.processing.delete(job.id);
|
|
656
|
-
this.metrics.activeJobs--;
|
|
657
|
-
const shouldRetry = job.attempts < job.maxAttempts;
|
|
658
|
-
if (shouldRetry) {
|
|
659
|
-
const retryDelay = this.getRetryDelay(job.attempts);
|
|
660
|
-
job.processAt = new Date(Date.now() + retryDelay);
|
|
661
|
-
job.error = error instanceof Error ? error.message : String(error);
|
|
662
|
-
this.metrics.retried++;
|
|
663
|
-
this.emit("job:retry", { job, error, retryDelay });
|
|
664
|
-
} else {
|
|
665
|
-
this.failJob(job, error instanceof Error ? error.message : String(error));
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
this.updateMetrics();
|
|
669
|
-
}
|
|
670
|
-
failJob(job, error) {
|
|
671
|
-
job.failedAt = /* @__PURE__ */ new Date();
|
|
672
|
-
job.error = error;
|
|
673
|
-
this.metrics.failed++;
|
|
674
|
-
this.metrics.processed++;
|
|
675
|
-
this.emit("job:failed", { job, error });
|
|
676
|
-
}
|
|
677
|
-
getRetryDelay(attempt) {
|
|
678
|
-
const delayIndex = Math.min(attempt - 1, this.options.retryDelays.length - 1);
|
|
679
|
-
return this.options.retryDelays[delayIndex] || this.options.retryDelays[this.options.retryDelays.length - 1];
|
|
680
|
-
}
|
|
681
|
-
updateMetrics() {
|
|
682
|
-
this.metrics.queueSize = this.queue.length;
|
|
683
|
-
this.metrics.lastProcessedAt = /* @__PURE__ */ new Date();
|
|
684
|
-
}
|
|
685
|
-
updateAverageProcessingTime(newTime) {
|
|
686
|
-
const totalProcessed = this.metrics.succeeded + this.metrics.failed;
|
|
687
|
-
if (totalProcessed === 1) {
|
|
688
|
-
this.metrics.averageProcessingTime = newTime;
|
|
689
|
-
} else {
|
|
690
|
-
this.metrics.averageProcessingTime = (this.metrics.averageProcessingTime * (totalProcessed - 1) + newTime) / totalProcessed;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
var MessageJobProcessor = class extends JobProcessor {
|
|
695
|
-
constructor(options = {}) {
|
|
696
|
-
super({
|
|
697
|
-
concurrency: 5,
|
|
698
|
-
retryDelays: [1e3, 5e3, 15e3, 6e4],
|
|
699
|
-
// 1s, 5s, 15s, 1m
|
|
700
|
-
maxRetries: 3,
|
|
701
|
-
pollInterval: 1e3,
|
|
702
|
-
enableMetrics: true,
|
|
703
|
-
...options
|
|
704
|
-
});
|
|
705
|
-
this.setupMessageHandlers();
|
|
706
|
-
}
|
|
707
|
-
setupMessageHandlers() {
|
|
708
|
-
this.handle("send_message", async (job) => {
|
|
709
|
-
return this.processSingleMessage(job);
|
|
710
|
-
});
|
|
711
|
-
this.handle("send_bulk_messages", async (job) => {
|
|
712
|
-
return this.processBulkMessages(job);
|
|
713
|
-
});
|
|
714
|
-
this.handle("update_delivery_status", async (job) => {
|
|
715
|
-
return this.processDeliveryUpdate(job);
|
|
716
|
-
});
|
|
717
|
-
this.handle("send_scheduled_message", async (job) => {
|
|
718
|
-
return this.processScheduledMessage(job);
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
async processSingleMessage(job) {
|
|
722
|
-
const { data: messageRequest } = job;
|
|
723
|
-
this.emit("message:processing", {
|
|
724
|
-
type: "message.queued" /* MESSAGE_QUEUED */,
|
|
725
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
726
|
-
data: { requestId: job.id, messageRequest },
|
|
727
|
-
metadata: job.metadata
|
|
728
|
-
});
|
|
729
|
-
const results = messageRequest.recipients.map((recipient) => ({
|
|
730
|
-
phoneNumber: recipient.phoneNumber,
|
|
731
|
-
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
|
|
732
|
-
status: "QUEUED" /* QUEUED */,
|
|
733
|
-
metadata: recipient.metadata
|
|
734
|
-
}));
|
|
735
|
-
const result = {
|
|
736
|
-
requestId: job.id,
|
|
737
|
-
results,
|
|
738
|
-
summary: {
|
|
739
|
-
total: messageRequest.recipients.length,
|
|
740
|
-
queued: messageRequest.recipients.length,
|
|
741
|
-
sent: 0,
|
|
742
|
-
failed: 0
|
|
743
|
-
},
|
|
744
|
-
metadata: {
|
|
745
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
746
|
-
provider: "default",
|
|
747
|
-
templateId: messageRequest.templateId
|
|
748
|
-
}
|
|
749
|
-
};
|
|
750
|
-
this.emit("message:queued", {
|
|
751
|
-
type: "message.queued" /* MESSAGE_QUEUED */,
|
|
752
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
753
|
-
data: result,
|
|
754
|
-
metadata: job.metadata
|
|
755
|
-
});
|
|
756
|
-
return result;
|
|
757
|
-
}
|
|
758
|
-
async processBulkMessages(job) {
|
|
759
|
-
const { data: messageRequests } = job;
|
|
760
|
-
const results = [];
|
|
761
|
-
for (const messageRequest of messageRequests) {
|
|
762
|
-
const singleJob = {
|
|
763
|
-
...job,
|
|
764
|
-
id: `${job.id}_${results.length}`,
|
|
765
|
-
data: messageRequest
|
|
766
|
-
};
|
|
767
|
-
const result = await this.processSingleMessage(singleJob);
|
|
768
|
-
results.push(result);
|
|
769
|
-
}
|
|
770
|
-
return results;
|
|
771
|
-
}
|
|
772
|
-
async processDeliveryUpdate(job) {
|
|
773
|
-
const { data: deliveryReport } = job;
|
|
774
|
-
this.emit("delivery:updated", {
|
|
775
|
-
type: "message.delivered" /* MESSAGE_DELIVERED */,
|
|
776
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
777
|
-
data: deliveryReport,
|
|
778
|
-
metadata: job.metadata
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
async processScheduledMessage(job) {
|
|
782
|
-
const { data: messageRequest } = job;
|
|
783
|
-
const scheduledAt = messageRequest.scheduling?.scheduledAt;
|
|
784
|
-
if (scheduledAt && scheduledAt > /* @__PURE__ */ new Date()) {
|
|
785
|
-
throw new Error(`Message scheduled for ${scheduledAt.toISOString()}, rescheduling`);
|
|
786
|
-
}
|
|
787
|
-
return this.processSingleMessage(job);
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* Add a message to the processing queue
|
|
791
|
-
*/
|
|
792
|
-
async queueMessage(messageRequest, options = {}) {
|
|
793
|
-
const priority = options.priority || (messageRequest.options?.priority === "high" ? 10 : messageRequest.options?.priority === "low" ? 1 : 5);
|
|
794
|
-
const delay = options.delay || 0;
|
|
795
|
-
return this.add("send_message", messageRequest, {
|
|
796
|
-
priority,
|
|
797
|
-
delay,
|
|
798
|
-
metadata: options.metadata
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
/**
|
|
802
|
-
* Add bulk messages to the processing queue
|
|
803
|
-
*/
|
|
804
|
-
async queueBulkMessages(messageRequests, options = {}) {
|
|
805
|
-
return this.add("send_bulk_messages", messageRequests, {
|
|
806
|
-
priority: options.priority || 3,
|
|
807
|
-
delay: options.delay || 0,
|
|
808
|
-
metadata: options.metadata
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
/**
|
|
812
|
-
* Schedule a message for future delivery
|
|
813
|
-
*/
|
|
814
|
-
async scheduleMessage(messageRequest, scheduledAt, options = {}) {
|
|
815
|
-
const delay = Math.max(0, scheduledAt.getTime() - Date.now());
|
|
816
|
-
return this.add("send_scheduled_message", messageRequest, {
|
|
817
|
-
priority: 5,
|
|
818
|
-
delay,
|
|
819
|
-
metadata: options.metadata
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
};
|
|
823
|
-
|
|
824
|
-
// src/queue/retry.handler.ts
|
|
825
|
-
var import_events2 = require("events");
|
|
826
|
-
var import_core2 = require("@k-msg/core");
|
|
827
|
-
var MessageRetryHandler = class extends import_events2.EventEmitter {
|
|
828
|
-
constructor(options) {
|
|
829
|
-
super();
|
|
830
|
-
this.options = options;
|
|
831
|
-
this.retryQueue = [];
|
|
832
|
-
this.processing = /* @__PURE__ */ new Set();
|
|
833
|
-
this.isRunning = false;
|
|
834
|
-
this.defaultPolicy = {
|
|
835
|
-
maxAttempts: 3,
|
|
836
|
-
backoffMultiplier: 2,
|
|
837
|
-
initialDelay: 5e3,
|
|
838
|
-
// 5 seconds
|
|
839
|
-
maxDelay: 3e5,
|
|
840
|
-
// 5 minutes
|
|
841
|
-
jitter: true,
|
|
842
|
-
retryableStatuses: ["FAILED" /* FAILED */],
|
|
843
|
-
retryableErrorCodes: [
|
|
844
|
-
"NETWORK_TIMEOUT",
|
|
845
|
-
"PROVIDER_CONNECTION_FAILED",
|
|
846
|
-
"PROVIDER_RATE_LIMITED",
|
|
847
|
-
"PROVIDER_SERVICE_UNAVAILABLE"
|
|
848
|
-
]
|
|
849
|
-
};
|
|
850
|
-
this.options.policy = { ...this.defaultPolicy, ...this.options.policy };
|
|
851
|
-
this.metrics = {
|
|
852
|
-
totalRetries: 0,
|
|
853
|
-
successfulRetries: 0,
|
|
854
|
-
failedRetries: 0,
|
|
855
|
-
exhaustedRetries: 0,
|
|
856
|
-
queueSize: 0,
|
|
857
|
-
averageRetryDelay: 0
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
/**
|
|
861
|
-
* Start the retry handler
|
|
862
|
-
*/
|
|
863
|
-
start() {
|
|
864
|
-
if (this.isRunning) {
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
this.isRunning = true;
|
|
868
|
-
this.scheduleNextCheck();
|
|
869
|
-
this.emit("handler:started");
|
|
870
|
-
}
|
|
871
|
-
/**
|
|
872
|
-
* Stop the retry handler
|
|
873
|
-
*/
|
|
874
|
-
async stop() {
|
|
875
|
-
this.isRunning = false;
|
|
876
|
-
if (this.checkTimer) {
|
|
877
|
-
clearTimeout(this.checkTimer);
|
|
878
|
-
this.checkTimer = void 0;
|
|
879
|
-
}
|
|
880
|
-
while (this.processing.size > 0) {
|
|
881
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
882
|
-
}
|
|
883
|
-
this.emit("handler:stopped");
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* Add a failed delivery for retry
|
|
887
|
-
*/
|
|
888
|
-
async addForRetry(deliveryReport) {
|
|
889
|
-
if (!this.shouldRetry(deliveryReport)) {
|
|
890
|
-
return false;
|
|
891
|
-
}
|
|
892
|
-
const existingItem = this.retryQueue.find(
|
|
893
|
-
(item) => item.messageId === deliveryReport.messageId
|
|
894
|
-
);
|
|
895
|
-
if (existingItem) {
|
|
896
|
-
return this.updateRetryItem(existingItem, deliveryReport);
|
|
897
|
-
}
|
|
898
|
-
const retryItem = await this.createRetryItem(deliveryReport);
|
|
899
|
-
if (this.retryQueue.length >= this.options.maxQueueSize) {
|
|
900
|
-
this.cleanupQueue();
|
|
901
|
-
if (this.retryQueue.length >= this.options.maxQueueSize) {
|
|
902
|
-
this.emit("queue:full", { rejected: deliveryReport });
|
|
903
|
-
return false;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
this.retryQueue.push(retryItem);
|
|
907
|
-
this.updateMetrics();
|
|
908
|
-
this.emit("retry:queued", {
|
|
909
|
-
type: "message.queued" /* MESSAGE_QUEUED */,
|
|
910
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
911
|
-
data: retryItem,
|
|
912
|
-
metadata: deliveryReport.metadata
|
|
913
|
-
});
|
|
914
|
-
return true;
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* Cancel retry for a specific message
|
|
918
|
-
*/
|
|
919
|
-
cancelRetry(messageId) {
|
|
920
|
-
const item = this.retryQueue.find((item2) => item2.messageId === messageId);
|
|
921
|
-
if (item) {
|
|
922
|
-
item.status = "cancelled";
|
|
923
|
-
item.updatedAt = /* @__PURE__ */ new Date();
|
|
924
|
-
this.updateMetrics();
|
|
925
|
-
this.emit("retry:cancelled", item);
|
|
926
|
-
return true;
|
|
927
|
-
}
|
|
928
|
-
return false;
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* Get retry status for a message
|
|
932
|
-
*/
|
|
933
|
-
getRetryStatus(messageId) {
|
|
934
|
-
return this.retryQueue.find((item) => item.messageId === messageId);
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Get all retry queue items
|
|
938
|
-
*/
|
|
939
|
-
getRetryQueue() {
|
|
940
|
-
return [...this.retryQueue];
|
|
941
|
-
}
|
|
942
|
-
/**
|
|
943
|
-
* Get metrics
|
|
944
|
-
*/
|
|
945
|
-
getMetrics() {
|
|
946
|
-
return { ...this.metrics };
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* Clean up completed/exhausted retry items
|
|
950
|
-
*/
|
|
951
|
-
cleanup() {
|
|
952
|
-
const initialLength = this.retryQueue.length;
|
|
953
|
-
this.retryQueue = this.retryQueue.filter(
|
|
954
|
-
(item) => item.status === "pending" || item.status === "processing"
|
|
955
|
-
);
|
|
956
|
-
const removed = initialLength - this.retryQueue.length;
|
|
957
|
-
this.updateMetrics();
|
|
958
|
-
return removed;
|
|
959
|
-
}
|
|
960
|
-
scheduleNextCheck() {
|
|
961
|
-
if (!this.isRunning) {
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
this.checkTimer = setTimeout(() => {
|
|
965
|
-
this.processRetryQueue();
|
|
966
|
-
this.scheduleNextCheck();
|
|
967
|
-
}, this.options.checkInterval);
|
|
968
|
-
}
|
|
969
|
-
async processRetryQueue() {
|
|
970
|
-
const now = /* @__PURE__ */ new Date();
|
|
971
|
-
const readyItems = this.retryQueue.filter(
|
|
972
|
-
(item) => item.status === "pending" && item.nextRetryAt <= now && !this.processing.has(item.id)
|
|
973
|
-
);
|
|
974
|
-
for (const item of readyItems) {
|
|
975
|
-
this.processRetryItem(item);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
async processRetryItem(item) {
|
|
979
|
-
this.processing.add(item.id);
|
|
980
|
-
item.status = "processing";
|
|
981
|
-
item.updatedAt = /* @__PURE__ */ new Date();
|
|
982
|
-
try {
|
|
983
|
-
const attempt = {
|
|
984
|
-
messageId: item.messageId,
|
|
985
|
-
phoneNumber: item.phoneNumber,
|
|
986
|
-
attemptNumber: item.attempts.length + 1,
|
|
987
|
-
scheduledAt: /* @__PURE__ */ new Date(),
|
|
988
|
-
provider: item.originalDeliveryReport.attempts[0]?.provider || "unknown",
|
|
989
|
-
templateId: item.originalDeliveryReport.metadata.templateId || "",
|
|
990
|
-
variables: item.originalDeliveryReport.metadata.variables || {},
|
|
991
|
-
metadata: item.originalDeliveryReport.metadata
|
|
992
|
-
};
|
|
993
|
-
item.attempts.push(attempt);
|
|
994
|
-
this.emit("retry:started", {
|
|
995
|
-
type: "message.queued" /* MESSAGE_QUEUED */,
|
|
996
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
997
|
-
data: { item, attempt },
|
|
998
|
-
metadata: item.originalDeliveryReport.metadata
|
|
999
|
-
});
|
|
1000
|
-
const result = await this.executeRetry(attempt);
|
|
1001
|
-
item.status = "exhausted";
|
|
1002
|
-
this.processing.delete(item.id);
|
|
1003
|
-
this.metrics.successfulRetries++;
|
|
1004
|
-
this.metrics.totalRetries++;
|
|
1005
|
-
this.updateMetrics();
|
|
1006
|
-
await this.options.onRetrySuccess?.(item, result);
|
|
1007
|
-
this.emit("retry:success", {
|
|
1008
|
-
type: "message.sent" /* MESSAGE_SENT */,
|
|
1009
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1010
|
-
data: { item, attempt, result },
|
|
1011
|
-
metadata: item.originalDeliveryReport.metadata
|
|
1012
|
-
});
|
|
1013
|
-
} catch (error) {
|
|
1014
|
-
this.processing.delete(item.id);
|
|
1015
|
-
this.metrics.failedRetries++;
|
|
1016
|
-
this.metrics.totalRetries++;
|
|
1017
|
-
const maxAttempts = this.options.policy.maxAttempts;
|
|
1018
|
-
const shouldRetryAgain = item.attempts.length < maxAttempts;
|
|
1019
|
-
if (shouldRetryAgain) {
|
|
1020
|
-
const nextDelay = this.calculateRetryDelay(item.attempts.length);
|
|
1021
|
-
item.nextRetryAt = new Date(Date.now() + nextDelay);
|
|
1022
|
-
item.status = "pending";
|
|
1023
|
-
} else {
|
|
1024
|
-
item.status = "exhausted";
|
|
1025
|
-
this.metrics.exhaustedRetries++;
|
|
1026
|
-
await this.options.onRetryExhausted?.(item);
|
|
1027
|
-
this.emit("retry:exhausted", {
|
|
1028
|
-
type: "message.failed" /* MESSAGE_FAILED */,
|
|
1029
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1030
|
-
data: { item, finalError: error },
|
|
1031
|
-
metadata: item.originalDeliveryReport.metadata
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
item.updatedAt = /* @__PURE__ */ new Date();
|
|
1035
|
-
this.updateMetrics();
|
|
1036
|
-
await this.options.onRetryFailed?.(item, error);
|
|
1037
|
-
this.emit("retry:failed", {
|
|
1038
|
-
type: "message.failed" /* MESSAGE_FAILED */,
|
|
1039
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1040
|
-
data: { item, error, willRetry: shouldRetryAgain },
|
|
1041
|
-
metadata: item.originalDeliveryReport.metadata
|
|
1042
|
-
});
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
async executeRetry(attempt) {
|
|
1046
|
-
return import_core2.RetryHandler.execute(
|
|
1047
|
-
async () => {
|
|
1048
|
-
if (Math.random() < 0.7) {
|
|
1049
|
-
return {
|
|
1050
|
-
messageId: attempt.messageId,
|
|
1051
|
-
status: "sent",
|
|
1052
|
-
sentAt: /* @__PURE__ */ new Date()
|
|
1053
|
-
};
|
|
1054
|
-
} else {
|
|
1055
|
-
throw new Error("Retry failed");
|
|
1056
|
-
}
|
|
1057
|
-
},
|
|
1058
|
-
{
|
|
1059
|
-
maxAttempts: 1,
|
|
1060
|
-
// We handle retries at a higher level
|
|
1061
|
-
initialDelay: 0,
|
|
1062
|
-
retryCondition: () => false
|
|
1063
|
-
// No retries at this level
|
|
1064
|
-
}
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
|
-
shouldRetry(deliveryReport) {
|
|
1068
|
-
const { policy } = this.options;
|
|
1069
|
-
if (!policy.retryableStatuses.includes(deliveryReport.status)) {
|
|
1070
|
-
return false;
|
|
1071
|
-
}
|
|
1072
|
-
let errorToCheck = deliveryReport.error;
|
|
1073
|
-
if (!errorToCheck && deliveryReport.attempts.length > 0) {
|
|
1074
|
-
const latestAttempt = deliveryReport.attempts[deliveryReport.attempts.length - 1];
|
|
1075
|
-
errorToCheck = latestAttempt.error;
|
|
1076
|
-
}
|
|
1077
|
-
if (errorToCheck) {
|
|
1078
|
-
const isRetryableError = policy.retryableErrorCodes.includes(errorToCheck.code);
|
|
1079
|
-
if (!isRetryableError) {
|
|
1080
|
-
return false;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
return deliveryReport.attempts.length < policy.maxAttempts;
|
|
1084
|
-
}
|
|
1085
|
-
async createRetryItem(deliveryReport) {
|
|
1086
|
-
const initialDelay = this.calculateRetryDelay(deliveryReport.attempts.length);
|
|
1087
|
-
return {
|
|
1088
|
-
id: `retry_${deliveryReport.messageId}_${Date.now()}`,
|
|
1089
|
-
messageId: deliveryReport.messageId,
|
|
1090
|
-
phoneNumber: deliveryReport.phoneNumber,
|
|
1091
|
-
originalDeliveryReport: deliveryReport,
|
|
1092
|
-
attempts: [],
|
|
1093
|
-
nextRetryAt: new Date(Date.now() + initialDelay),
|
|
1094
|
-
status: "pending",
|
|
1095
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1096
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
updateRetryItem(item, deliveryReport) {
|
|
1100
|
-
if (item.status === "exhausted" || item.status === "cancelled") {
|
|
1101
|
-
return false;
|
|
1102
|
-
}
|
|
1103
|
-
item.originalDeliveryReport = deliveryReport;
|
|
1104
|
-
item.updatedAt = /* @__PURE__ */ new Date();
|
|
1105
|
-
if (item.status === "pending") {
|
|
1106
|
-
const nextDelay = this.calculateRetryDelay(item.attempts.length);
|
|
1107
|
-
item.nextRetryAt = new Date(Date.now() + nextDelay);
|
|
1108
|
-
}
|
|
1109
|
-
return true;
|
|
1110
|
-
}
|
|
1111
|
-
calculateRetryDelay(attemptNumber) {
|
|
1112
|
-
const { policy } = this.options;
|
|
1113
|
-
let delay = policy.initialDelay * Math.pow(policy.backoffMultiplier, attemptNumber);
|
|
1114
|
-
delay = Math.min(delay, policy.maxDelay);
|
|
1115
|
-
if (policy.jitter) {
|
|
1116
|
-
const jitterAmount = delay * 0.1;
|
|
1117
|
-
delay += (Math.random() - 0.5) * 2 * jitterAmount;
|
|
1118
|
-
}
|
|
1119
|
-
return Math.max(0, delay);
|
|
1120
|
-
}
|
|
1121
|
-
cleanupQueue() {
|
|
1122
|
-
const cutoffTime = new Date(Date.now() - 24 * 60 * 60 * 1e3);
|
|
1123
|
-
this.retryQueue = this.retryQueue.filter(
|
|
1124
|
-
(item) => item.status === "pending" || item.status === "processing" || item.status === "exhausted" && item.updatedAt > cutoffTime
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
updateMetrics() {
|
|
1128
|
-
this.metrics.queueSize = this.retryQueue.length;
|
|
1129
|
-
this.metrics.lastRetryAt = /* @__PURE__ */ new Date();
|
|
1130
|
-
const pendingItems = this.retryQueue.filter((item) => item.status === "pending");
|
|
1131
|
-
if (pendingItems.length > 0) {
|
|
1132
|
-
const totalDelay = pendingItems.reduce((sum, item) => {
|
|
1133
|
-
return sum + Math.max(0, item.nextRetryAt.getTime() - Date.now());
|
|
1134
|
-
}, 0);
|
|
1135
|
-
this.metrics.averageRetryDelay = totalDelay / pendingItems.length;
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
};
|
|
1139
|
-
|
|
1140
|
-
// src/delivery/tracker.ts
|
|
1141
|
-
var import_events3 = require("events");
|
|
1142
|
-
var DeliveryTracker = class extends import_events3.EventEmitter {
|
|
1143
|
-
constructor(options) {
|
|
1144
|
-
super();
|
|
1145
|
-
this.options = options;
|
|
1146
|
-
this.trackingRecords = /* @__PURE__ */ new Map();
|
|
1147
|
-
this.statusIndex = /* @__PURE__ */ new Map();
|
|
1148
|
-
this.webhookQueue = [];
|
|
1149
|
-
this.isRunning = false;
|
|
1150
|
-
this.defaultOptions = {
|
|
1151
|
-
trackingInterval: 5e3,
|
|
1152
|
-
// Check every 5 seconds
|
|
1153
|
-
maxTrackingDuration: 864e5,
|
|
1154
|
-
// 24 hours
|
|
1155
|
-
batchSize: 100,
|
|
1156
|
-
enableWebhooks: true,
|
|
1157
|
-
webhookRetries: 3,
|
|
1158
|
-
webhookTimeout: 5e3,
|
|
1159
|
-
persistence: {
|
|
1160
|
-
enabled: true,
|
|
1161
|
-
retentionDays: 30
|
|
1162
|
-
}
|
|
1163
|
-
};
|
|
1164
|
-
this.options = { ...this.defaultOptions, ...options };
|
|
1165
|
-
this.stats = {
|
|
1166
|
-
totalMessages: 0,
|
|
1167
|
-
byStatus: {},
|
|
1168
|
-
byProvider: {},
|
|
1169
|
-
averageDeliveryTime: 0,
|
|
1170
|
-
deliveryRate: 0,
|
|
1171
|
-
failureRate: 0,
|
|
1172
|
-
lastUpdated: /* @__PURE__ */ new Date()
|
|
1173
|
-
};
|
|
1174
|
-
Object.values(MessageStatus).forEach((status) => {
|
|
1175
|
-
this.stats.byStatus[status] = 0;
|
|
1176
|
-
this.statusIndex.set(status, /* @__PURE__ */ new Set());
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
/**
|
|
1180
|
-
* Start delivery tracking
|
|
1181
|
-
*/
|
|
1182
|
-
start() {
|
|
1183
|
-
if (this.isRunning) {
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
this.isRunning = true;
|
|
1187
|
-
this.scheduleTracking();
|
|
1188
|
-
this.emit("tracker:started");
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Stop delivery tracking
|
|
1192
|
-
*/
|
|
1193
|
-
stop() {
|
|
1194
|
-
this.isRunning = false;
|
|
1195
|
-
if (this.trackingTimer) {
|
|
1196
|
-
clearTimeout(this.trackingTimer);
|
|
1197
|
-
this.trackingTimer = void 0;
|
|
1198
|
-
}
|
|
1199
|
-
this.emit("tracker:stopped");
|
|
1200
|
-
}
|
|
1201
|
-
/**
|
|
1202
|
-
* Start tracking a message
|
|
1203
|
-
*/
|
|
1204
|
-
async trackMessage(messageId, phoneNumber, templateId, provider, options = {}) {
|
|
1205
|
-
const now = /* @__PURE__ */ new Date();
|
|
1206
|
-
const expiresAt = new Date(now.getTime() + this.options.maxTrackingDuration);
|
|
1207
|
-
const initialStatus = options.initialStatus || "QUEUED" /* QUEUED */;
|
|
1208
|
-
const deliveryReport = {
|
|
1209
|
-
messageId,
|
|
1210
|
-
phoneNumber,
|
|
1211
|
-
status: initialStatus,
|
|
1212
|
-
attempts: [{
|
|
1213
|
-
attemptNumber: 1,
|
|
1214
|
-
attemptedAt: now,
|
|
1215
|
-
status: initialStatus,
|
|
1216
|
-
provider
|
|
1217
|
-
}],
|
|
1218
|
-
metadata: options.metadata || {}
|
|
1219
|
-
};
|
|
1220
|
-
const record = {
|
|
1221
|
-
messageId,
|
|
1222
|
-
phoneNumber,
|
|
1223
|
-
templateId,
|
|
1224
|
-
provider,
|
|
1225
|
-
currentStatus: initialStatus,
|
|
1226
|
-
statusHistory: [{
|
|
1227
|
-
status: initialStatus,
|
|
1228
|
-
timestamp: now,
|
|
1229
|
-
provider,
|
|
1230
|
-
source: "system"
|
|
1231
|
-
}],
|
|
1232
|
-
deliveryReport,
|
|
1233
|
-
webhooks: options.webhooks || [],
|
|
1234
|
-
createdAt: now,
|
|
1235
|
-
updatedAt: now,
|
|
1236
|
-
expiresAt,
|
|
1237
|
-
metadata: options.metadata || {}
|
|
1238
|
-
};
|
|
1239
|
-
this.trackingRecords.set(messageId, record);
|
|
1240
|
-
this.statusIndex.get(initialStatus)?.add(messageId);
|
|
1241
|
-
this.updateStats();
|
|
1242
|
-
this.emit("tracking:started", {
|
|
1243
|
-
type: "message.queued" /* MESSAGE_QUEUED */,
|
|
1244
|
-
timestamp: now,
|
|
1245
|
-
data: record,
|
|
1246
|
-
metadata: record.metadata
|
|
1247
|
-
});
|
|
1248
|
-
if (this.options.enableWebhooks && record.webhooks.length > 0) {
|
|
1249
|
-
const event = {
|
|
1250
|
-
id: `evt_${messageId}_${Date.now()}`,
|
|
1251
|
-
type: "message.queued" /* MESSAGE_QUEUED */,
|
|
1252
|
-
timestamp: now,
|
|
1253
|
-
data: deliveryReport,
|
|
1254
|
-
metadata: record.metadata
|
|
1255
|
-
};
|
|
1256
|
-
this.queueWebhook(record, event);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
/**
|
|
1260
|
-
* Update message status
|
|
1261
|
-
*/
|
|
1262
|
-
async updateStatus(messageId, status, details = {}) {
|
|
1263
|
-
const record = this.trackingRecords.get(messageId);
|
|
1264
|
-
if (!record) {
|
|
1265
|
-
return false;
|
|
1266
|
-
}
|
|
1267
|
-
const now = /* @__PURE__ */ new Date();
|
|
1268
|
-
const oldStatus = record.currentStatus;
|
|
1269
|
-
if (!this.isStatusProgression(oldStatus, status)) {
|
|
1270
|
-
return false;
|
|
1271
|
-
}
|
|
1272
|
-
this.statusIndex.get(oldStatus)?.delete(messageId);
|
|
1273
|
-
record.currentStatus = status;
|
|
1274
|
-
record.updatedAt = now;
|
|
1275
|
-
record.statusHistory.push({
|
|
1276
|
-
status,
|
|
1277
|
-
timestamp: now,
|
|
1278
|
-
provider: details.provider || record.provider,
|
|
1279
|
-
details: details.metadata,
|
|
1280
|
-
source: details.source || "system"
|
|
1281
|
-
});
|
|
1282
|
-
record.deliveryReport.status = status;
|
|
1283
|
-
record.deliveryReport.metadata = { ...record.deliveryReport.metadata, ...details.metadata };
|
|
1284
|
-
if (details.sentAt) record.deliveryReport.sentAt = details.sentAt;
|
|
1285
|
-
if (details.deliveredAt) record.deliveryReport.deliveredAt = details.deliveredAt;
|
|
1286
|
-
if (details.clickedAt) record.deliveryReport.clickedAt = details.clickedAt;
|
|
1287
|
-
if (details.failedAt) record.deliveryReport.failedAt = details.failedAt;
|
|
1288
|
-
if (details.error) record.deliveryReport.error = details.error;
|
|
1289
|
-
record.deliveryReport.attempts.push({
|
|
1290
|
-
attemptNumber: record.deliveryReport.attempts.length + 1,
|
|
1291
|
-
attemptedAt: now,
|
|
1292
|
-
status,
|
|
1293
|
-
error: details.error,
|
|
1294
|
-
provider: details.provider || record.provider
|
|
1295
|
-
});
|
|
1296
|
-
this.statusIndex.get(status)?.add(messageId);
|
|
1297
|
-
this.updateStats();
|
|
1298
|
-
const eventType = this.getEventTypeForStatus(status);
|
|
1299
|
-
const event = {
|
|
1300
|
-
id: `evt_${messageId}_${Date.now()}`,
|
|
1301
|
-
type: eventType,
|
|
1302
|
-
timestamp: now,
|
|
1303
|
-
data: {
|
|
1304
|
-
messageId,
|
|
1305
|
-
previousStatus: oldStatus,
|
|
1306
|
-
currentStatus: status,
|
|
1307
|
-
deliveryReport: record.deliveryReport,
|
|
1308
|
-
...details
|
|
1309
|
-
},
|
|
1310
|
-
metadata: record.metadata
|
|
1311
|
-
};
|
|
1312
|
-
this.emit("status:updated", event);
|
|
1313
|
-
if (this.options.enableWebhooks && record.webhooks.length > 0) {
|
|
1314
|
-
this.queueWebhook(record, event);
|
|
1315
|
-
}
|
|
1316
|
-
if (this.isTerminalStatus(status)) {
|
|
1317
|
-
this.emit("tracking:completed", {
|
|
1318
|
-
...event,
|
|
1319
|
-
data: { ...event.data, trackingCompleted: true }
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
return true;
|
|
1323
|
-
}
|
|
1324
|
-
/**
|
|
1325
|
-
* Get delivery report for a message
|
|
1326
|
-
*/
|
|
1327
|
-
getDeliveryReport(messageId) {
|
|
1328
|
-
return this.trackingRecords.get(messageId)?.deliveryReport;
|
|
1329
|
-
}
|
|
1330
|
-
/**
|
|
1331
|
-
* Get tracking record for a message
|
|
1332
|
-
*/
|
|
1333
|
-
getTrackingRecord(messageId) {
|
|
1334
|
-
return this.trackingRecords.get(messageId);
|
|
1335
|
-
}
|
|
1336
|
-
/**
|
|
1337
|
-
* Get messages by status
|
|
1338
|
-
*/
|
|
1339
|
-
getMessagesByStatus(status) {
|
|
1340
|
-
const messageIds = this.statusIndex.get(status) || /* @__PURE__ */ new Set();
|
|
1341
|
-
return Array.from(messageIds).map((id) => this.trackingRecords.get(id)).filter((record) => record !== void 0);
|
|
1342
|
-
}
|
|
1343
|
-
/**
|
|
1344
|
-
* Get delivery statistics
|
|
1345
|
-
*/
|
|
1346
|
-
getStats() {
|
|
1347
|
-
return { ...this.stats };
|
|
1348
|
-
}
|
|
1349
|
-
/**
|
|
1350
|
-
* Get delivery statistics for a specific time range
|
|
1351
|
-
*/
|
|
1352
|
-
getStatsForPeriod(startDate, endDate) {
|
|
1353
|
-
const records = Array.from(this.trackingRecords.values()).filter(
|
|
1354
|
-
(record) => record.createdAt >= startDate && record.createdAt <= endDate
|
|
1355
|
-
);
|
|
1356
|
-
const stats = {
|
|
1357
|
-
totalMessages: records.length,
|
|
1358
|
-
byStatus: {},
|
|
1359
|
-
byProvider: {},
|
|
1360
|
-
averageDeliveryTime: 0,
|
|
1361
|
-
deliveryRate: 0,
|
|
1362
|
-
failureRate: 0,
|
|
1363
|
-
lastUpdated: /* @__PURE__ */ new Date()
|
|
1364
|
-
};
|
|
1365
|
-
Object.values(MessageStatus).forEach((status) => {
|
|
1366
|
-
stats.byStatus[status] = 0;
|
|
1367
|
-
});
|
|
1368
|
-
let totalDeliveryTime = 0;
|
|
1369
|
-
let deliveredCount = 0;
|
|
1370
|
-
let failedCount = 0;
|
|
1371
|
-
records.forEach((record) => {
|
|
1372
|
-
stats.byStatus[record.currentStatus]++;
|
|
1373
|
-
stats.byProvider[record.provider] = (stats.byProvider[record.provider] || 0) + 1;
|
|
1374
|
-
if (record.deliveryReport.deliveredAt && record.deliveryReport.sentAt) {
|
|
1375
|
-
const deliveryTime = record.deliveryReport.deliveredAt.getTime() - record.deliveryReport.sentAt.getTime();
|
|
1376
|
-
totalDeliveryTime += deliveryTime;
|
|
1377
|
-
deliveredCount++;
|
|
1378
|
-
}
|
|
1379
|
-
if (record.currentStatus === "FAILED" /* FAILED */) {
|
|
1380
|
-
failedCount++;
|
|
1381
|
-
}
|
|
1382
|
-
});
|
|
1383
|
-
if (deliveredCount > 0) {
|
|
1384
|
-
stats.averageDeliveryTime = totalDeliveryTime / deliveredCount;
|
|
1385
|
-
}
|
|
1386
|
-
if (records.length > 0) {
|
|
1387
|
-
stats.deliveryRate = stats.byStatus["DELIVERED" /* DELIVERED */] / records.length * 100;
|
|
1388
|
-
stats.failureRate = failedCount / records.length * 100;
|
|
1389
|
-
}
|
|
1390
|
-
return stats;
|
|
1391
|
-
}
|
|
1392
|
-
/**
|
|
1393
|
-
* Clean up expired tracking records
|
|
1394
|
-
*/
|
|
1395
|
-
cleanup() {
|
|
1396
|
-
const now = /* @__PURE__ */ new Date();
|
|
1397
|
-
let removed = 0;
|
|
1398
|
-
for (const [messageId, record] of this.trackingRecords.entries()) {
|
|
1399
|
-
if (record.expiresAt <= now || this.isTerminalStatus(record.currentStatus)) {
|
|
1400
|
-
this.trackingRecords.delete(messageId);
|
|
1401
|
-
this.statusIndex.get(record.currentStatus)?.delete(messageId);
|
|
1402
|
-
removed++;
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
if (removed > 0) {
|
|
1406
|
-
this.updateStats();
|
|
1407
|
-
this.emit("cleanup:completed", { removedCount: removed });
|
|
1408
|
-
}
|
|
1409
|
-
return removed;
|
|
1410
|
-
}
|
|
1411
|
-
/**
|
|
1412
|
-
* Stop tracking a specific message
|
|
1413
|
-
*/
|
|
1414
|
-
stopTracking(messageId) {
|
|
1415
|
-
const record = this.trackingRecords.get(messageId);
|
|
1416
|
-
if (!record) {
|
|
1417
|
-
return false;
|
|
1418
|
-
}
|
|
1419
|
-
this.trackingRecords.delete(messageId);
|
|
1420
|
-
this.statusIndex.get(record.currentStatus)?.delete(messageId);
|
|
1421
|
-
this.updateStats();
|
|
1422
|
-
this.emit("tracking:stopped", {
|
|
1423
|
-
type: "message.cancelled" /* MESSAGE_CANCELLED */,
|
|
1424
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
1425
|
-
data: record,
|
|
1426
|
-
metadata: record.metadata
|
|
1427
|
-
});
|
|
1428
|
-
return true;
|
|
1429
|
-
}
|
|
1430
|
-
scheduleTracking() {
|
|
1431
|
-
if (!this.isRunning) {
|
|
1432
|
-
return;
|
|
1433
|
-
}
|
|
1434
|
-
this.trackingTimer = setTimeout(() => {
|
|
1435
|
-
this.processTracking();
|
|
1436
|
-
this.processWebhookQueue();
|
|
1437
|
-
this.scheduleTracking();
|
|
1438
|
-
}, this.options.trackingInterval);
|
|
1439
|
-
}
|
|
1440
|
-
async processTracking() {
|
|
1441
|
-
await this.processWebhookQueue();
|
|
1442
|
-
const shouldCleanup = Date.now() % (60 * 60 * 1e3) < this.options.trackingInterval;
|
|
1443
|
-
if (shouldCleanup) {
|
|
1444
|
-
this.cleanup();
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
async processWebhookQueue() {
|
|
1448
|
-
if (!this.options.enableWebhooks || this.webhookQueue.length === 0) {
|
|
1449
|
-
return;
|
|
1450
|
-
}
|
|
1451
|
-
const batch = this.webhookQueue.splice(0, this.options.batchSize);
|
|
1452
|
-
for (const { record, event } of batch) {
|
|
1453
|
-
for (const webhook of record.webhooks) {
|
|
1454
|
-
if (webhook.events.includes(event.type)) {
|
|
1455
|
-
this.deliverWebhook(webhook, event);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
async deliverWebhook(webhook, event) {
|
|
1461
|
-
let lastError;
|
|
1462
|
-
for (let attempt = 1; attempt <= webhook.retries + 1; attempt++) {
|
|
1463
|
-
try {
|
|
1464
|
-
const result = await this.sendWebhook(webhook, event, attempt);
|
|
1465
|
-
if (result.success) {
|
|
1466
|
-
this.emit("webhook:delivered", {
|
|
1467
|
-
webhook,
|
|
1468
|
-
event,
|
|
1469
|
-
result,
|
|
1470
|
-
attempt
|
|
1471
|
-
});
|
|
1472
|
-
return;
|
|
1473
|
-
} else {
|
|
1474
|
-
lastError = new Error(`HTTP ${result.statusCode}: ${result.error}`);
|
|
1475
|
-
}
|
|
1476
|
-
} catch (error) {
|
|
1477
|
-
lastError = error;
|
|
1478
|
-
}
|
|
1479
|
-
if (attempt <= webhook.retries) {
|
|
1480
|
-
const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 3e4);
|
|
1481
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
this.emit("webhook:failed", {
|
|
1485
|
-
webhook,
|
|
1486
|
-
event,
|
|
1487
|
-
error: lastError,
|
|
1488
|
-
attempts: webhook.retries + 1
|
|
1489
|
-
});
|
|
1490
|
-
}
|
|
1491
|
-
async sendWebhook(webhook, event, attempt) {
|
|
1492
|
-
const startTime = Date.now();
|
|
1493
|
-
try {
|
|
1494
|
-
const headers = {
|
|
1495
|
-
"Content-Type": "application/json",
|
|
1496
|
-
"User-Agent": "K-Message-Delivery-Tracker/1.0",
|
|
1497
|
-
...webhook.headers
|
|
1498
|
-
};
|
|
1499
|
-
if (webhook.secret) {
|
|
1500
|
-
const payload = JSON.stringify(event);
|
|
1501
|
-
headers["X-Signature"] = `sha256=${webhook.secret}`;
|
|
1502
|
-
}
|
|
1503
|
-
const response = await fetch(webhook.url, {
|
|
1504
|
-
method: "POST",
|
|
1505
|
-
headers,
|
|
1506
|
-
body: JSON.stringify(event),
|
|
1507
|
-
signal: AbortSignal.timeout(webhook.timeout)
|
|
1508
|
-
});
|
|
1509
|
-
const responseTime = Date.now() - startTime;
|
|
1510
|
-
return {
|
|
1511
|
-
success: response.ok,
|
|
1512
|
-
statusCode: response.status,
|
|
1513
|
-
error: response.ok ? void 0 : response.statusText,
|
|
1514
|
-
responseTime,
|
|
1515
|
-
attempt
|
|
1516
|
-
};
|
|
1517
|
-
} catch (error) {
|
|
1518
|
-
const responseTime = Date.now() - startTime;
|
|
1519
|
-
return {
|
|
1520
|
-
success: false,
|
|
1521
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
1522
|
-
responseTime,
|
|
1523
|
-
attempt
|
|
1524
|
-
};
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
queueWebhook(record, event) {
|
|
1528
|
-
this.webhookQueue.push({ record, event });
|
|
1529
|
-
}
|
|
1530
|
-
isStatusProgression(oldStatus, newStatus) {
|
|
1531
|
-
const statusOrder = [
|
|
1532
|
-
"QUEUED" /* QUEUED */,
|
|
1533
|
-
"SENDING" /* SENDING */,
|
|
1534
|
-
"SENT" /* SENT */,
|
|
1535
|
-
"DELIVERED" /* DELIVERED */,
|
|
1536
|
-
"CLICKED" /* CLICKED */
|
|
1537
|
-
];
|
|
1538
|
-
const oldIndex = statusOrder.indexOf(oldStatus);
|
|
1539
|
-
const newIndex = statusOrder.indexOf(newStatus);
|
|
1540
|
-
return newIndex > oldIndex || newStatus === "FAILED" /* FAILED */ || newStatus === "CANCELLED" /* CANCELLED */;
|
|
1541
|
-
}
|
|
1542
|
-
isTerminalStatus(status) {
|
|
1543
|
-
return [
|
|
1544
|
-
"DELIVERED" /* DELIVERED */,
|
|
1545
|
-
"FAILED" /* FAILED */,
|
|
1546
|
-
"CANCELLED" /* CANCELLED */,
|
|
1547
|
-
"CLICKED" /* CLICKED */
|
|
1548
|
-
].includes(status);
|
|
1549
|
-
}
|
|
1550
|
-
getEventTypeForStatus(status) {
|
|
1551
|
-
switch (status) {
|
|
1552
|
-
case "QUEUED" /* QUEUED */:
|
|
1553
|
-
return "message.queued" /* MESSAGE_QUEUED */;
|
|
1554
|
-
case "SENT" /* SENT */:
|
|
1555
|
-
return "message.sent" /* MESSAGE_SENT */;
|
|
1556
|
-
case "DELIVERED" /* DELIVERED */:
|
|
1557
|
-
return "message.delivered" /* MESSAGE_DELIVERED */;
|
|
1558
|
-
case "FAILED" /* FAILED */:
|
|
1559
|
-
return "message.failed" /* MESSAGE_FAILED */;
|
|
1560
|
-
case "CLICKED" /* CLICKED */:
|
|
1561
|
-
return "message.clicked" /* MESSAGE_CLICKED */;
|
|
1562
|
-
default:
|
|
1563
|
-
return "message.queued" /* MESSAGE_QUEUED */;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
updateStats() {
|
|
1567
|
-
this.stats.totalMessages = this.trackingRecords.size;
|
|
1568
|
-
this.stats.lastUpdated = /* @__PURE__ */ new Date();
|
|
1569
|
-
Object.values(MessageStatus).forEach((status) => {
|
|
1570
|
-
this.stats.byStatus[status] = 0;
|
|
1571
|
-
});
|
|
1572
|
-
this.stats.byProvider = {};
|
|
1573
|
-
let totalDeliveryTime = 0;
|
|
1574
|
-
let deliveredCount = 0;
|
|
1575
|
-
let failedCount = 0;
|
|
1576
|
-
for (const record of this.trackingRecords.values()) {
|
|
1577
|
-
this.stats.byStatus[record.currentStatus]++;
|
|
1578
|
-
this.stats.byProvider[record.provider] = (this.stats.byProvider[record.provider] || 0) + 1;
|
|
1579
|
-
if (record.deliveryReport.deliveredAt && record.deliveryReport.sentAt) {
|
|
1580
|
-
const deliveryTime = record.deliveryReport.deliveredAt.getTime() - record.deliveryReport.sentAt.getTime();
|
|
1581
|
-
totalDeliveryTime += deliveryTime;
|
|
1582
|
-
deliveredCount++;
|
|
1583
|
-
}
|
|
1584
|
-
if (record.currentStatus === "FAILED" /* FAILED */) {
|
|
1585
|
-
failedCount++;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
if (deliveredCount > 0) {
|
|
1589
|
-
this.stats.averageDeliveryTime = totalDeliveryTime / deliveredCount;
|
|
1590
|
-
}
|
|
1591
|
-
if (this.stats.totalMessages > 0) {
|
|
1592
|
-
this.stats.deliveryRate = this.stats.byStatus["DELIVERED" /* DELIVERED */] / this.stats.totalMessages * 100;
|
|
1593
|
-
this.stats.failureRate = failedCount / this.stats.totalMessages * 100;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
};
|
|
1597
|
-
|
|
1598
|
-
// src/personalization/variable.replacer.ts
|
|
1599
|
-
var VariableReplacer = class {
|
|
1600
|
-
constructor(options = {}) {
|
|
1601
|
-
this.options = options;
|
|
1602
|
-
this.defaultOptions = {
|
|
1603
|
-
variablePattern: /\#\{([^}]+)\}/g,
|
|
1604
|
-
allowUndefined: false,
|
|
1605
|
-
undefinedReplacement: "",
|
|
1606
|
-
caseSensitive: true,
|
|
1607
|
-
enableFormatting: true,
|
|
1608
|
-
enableConditionals: true,
|
|
1609
|
-
enableLoops: true,
|
|
1610
|
-
maxRecursionDepth: 10
|
|
1611
|
-
};
|
|
1612
|
-
this.options = { ...this.defaultOptions, ...options };
|
|
1613
|
-
}
|
|
1614
|
-
/**
|
|
1615
|
-
* Replace variables in content
|
|
1616
|
-
*/
|
|
1617
|
-
replace(content, variables) {
|
|
1618
|
-
const startTime = Date.now();
|
|
1619
|
-
const originalLength = content.length;
|
|
1620
|
-
const result = {
|
|
1621
|
-
content,
|
|
1622
|
-
variables: [],
|
|
1623
|
-
missingVariables: [],
|
|
1624
|
-
errors: [],
|
|
1625
|
-
metadata: {
|
|
1626
|
-
originalLength,
|
|
1627
|
-
finalLength: 0,
|
|
1628
|
-
variableCount: 0,
|
|
1629
|
-
replacementTime: 0
|
|
1630
|
-
}
|
|
1631
|
-
};
|
|
1632
|
-
try {
|
|
1633
|
-
if (this.options.enableConditionals) {
|
|
1634
|
-
result.content = this.processConditionals(result.content, variables, result);
|
|
1635
|
-
}
|
|
1636
|
-
if (this.options.enableLoops) {
|
|
1637
|
-
result.content = this.processLoops(result.content, variables, result);
|
|
1638
|
-
}
|
|
1639
|
-
result.content = this.replaceSimpleVariables(result.content, variables, result);
|
|
1640
|
-
if (this.hasVariables(result.content)) {
|
|
1641
|
-
result.content = this.replaceRecursive(result.content, variables, result, 0);
|
|
1642
|
-
}
|
|
1643
|
-
} catch (error) {
|
|
1644
|
-
result.errors.push({
|
|
1645
|
-
type: "syntax_error",
|
|
1646
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
1647
|
-
});
|
|
1648
|
-
}
|
|
1649
|
-
result.metadata.finalLength = result.content.length;
|
|
1650
|
-
result.metadata.variableCount = result.variables.length;
|
|
1651
|
-
result.metadata.replacementTime = Date.now() - startTime;
|
|
1652
|
-
return result;
|
|
1653
|
-
}
|
|
1654
|
-
/**
|
|
1655
|
-
* Extract variables from content without replacing
|
|
1656
|
-
*/
|
|
1657
|
-
extractVariables(content) {
|
|
1658
|
-
const variables = /* @__PURE__ */ new Set();
|
|
1659
|
-
const pattern = new RegExp(this.options.variablePattern);
|
|
1660
|
-
let match;
|
|
1661
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
1662
|
-
const variableName = this.parseVariableName(match[1]);
|
|
1663
|
-
variables.add(variableName);
|
|
1664
|
-
}
|
|
1665
|
-
if (this.options.enableConditionals) {
|
|
1666
|
-
const conditionals = this.extractConditionals(content);
|
|
1667
|
-
conditionals.forEach((conditional) => {
|
|
1668
|
-
const conditionVars = this.extractVariablesFromExpression(conditional.condition);
|
|
1669
|
-
conditionVars.forEach((v) => variables.add(v));
|
|
1670
|
-
const contentVars = this.extractVariables(conditional.content);
|
|
1671
|
-
contentVars.forEach((v) => variables.add(v));
|
|
1672
|
-
if (conditional.elseContent) {
|
|
1673
|
-
const elseVars = this.extractVariables(conditional.elseContent);
|
|
1674
|
-
elseVars.forEach((v) => variables.add(v));
|
|
1675
|
-
}
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
if (this.options.enableLoops) {
|
|
1679
|
-
const loops = this.extractLoops(content);
|
|
1680
|
-
loops.forEach((loop) => {
|
|
1681
|
-
variables.add(loop.array);
|
|
1682
|
-
const contentVars = this.extractVariables(loop.content);
|
|
1683
|
-
contentVars.forEach((v) => variables.add(v));
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
return Array.from(variables);
|
|
1687
|
-
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Validate that all required variables are provided
|
|
1690
|
-
*/
|
|
1691
|
-
validate(content, variables) {
|
|
1692
|
-
const requiredVariables = this.extractVariables(content);
|
|
1693
|
-
const providedVariables = Object.keys(variables);
|
|
1694
|
-
const missingVariables = requiredVariables.filter((required) => {
|
|
1695
|
-
const normalizedRequired = this.options.caseSensitive ? required : required.toLowerCase();
|
|
1696
|
-
return !providedVariables.some((provided) => {
|
|
1697
|
-
const normalizedProvided = this.options.caseSensitive ? provided : provided.toLowerCase();
|
|
1698
|
-
return normalizedProvided === normalizedRequired;
|
|
1699
|
-
});
|
|
1700
|
-
});
|
|
1701
|
-
const errors = missingVariables.map((variable) => ({
|
|
1702
|
-
type: "missing_variable",
|
|
1703
|
-
message: `Missing required variable: ${variable}`,
|
|
1704
|
-
variable
|
|
1705
|
-
}));
|
|
1706
|
-
return {
|
|
1707
|
-
isValid: missingVariables.length === 0,
|
|
1708
|
-
missingVariables,
|
|
1709
|
-
errors
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
/**
|
|
1713
|
-
* Preview replacement result without actually replacing
|
|
1714
|
-
*/
|
|
1715
|
-
preview(content, variables) {
|
|
1716
|
-
const result = this.replace(content, variables);
|
|
1717
|
-
const highlights = {};
|
|
1718
|
-
const pattern = new RegExp(this.options.variablePattern);
|
|
1719
|
-
let match;
|
|
1720
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
1721
|
-
const variableName = this.parseVariableName(match[1]);
|
|
1722
|
-
const value = this.getVariableValue(variableName, variables);
|
|
1723
|
-
if (!highlights[variableName]) {
|
|
1724
|
-
highlights[variableName] = { value: String(value), positions: [] };
|
|
1725
|
-
}
|
|
1726
|
-
highlights[variableName].positions.push({
|
|
1727
|
-
start: match.index,
|
|
1728
|
-
end: match.index + match[0].length
|
|
1729
|
-
});
|
|
1730
|
-
}
|
|
1731
|
-
const variableHighlights = Object.entries(highlights).map(([variable, info]) => ({
|
|
1732
|
-
variable,
|
|
1733
|
-
value: info.value,
|
|
1734
|
-
positions: info.positions
|
|
1735
|
-
}));
|
|
1736
|
-
return {
|
|
1737
|
-
originalContent: content,
|
|
1738
|
-
previewContent: result.content,
|
|
1739
|
-
variableHighlights
|
|
1740
|
-
};
|
|
1741
|
-
}
|
|
1742
|
-
replaceSimpleVariables(content, variables, result) {
|
|
1743
|
-
const pattern = new RegExp(this.options.variablePattern, "g");
|
|
1744
|
-
return content.replace(pattern, (match, variableExpression, offset) => {
|
|
1745
|
-
try {
|
|
1746
|
-
const variableName = this.parseVariableName(variableExpression);
|
|
1747
|
-
const value = this.getVariableValue(variableName, variables);
|
|
1748
|
-
if (value === void 0 || value === null) {
|
|
1749
|
-
if (!this.options.allowUndefined) {
|
|
1750
|
-
result.missingVariables.push(variableName);
|
|
1751
|
-
result.errors.push({
|
|
1752
|
-
type: "missing_variable",
|
|
1753
|
-
message: `Variable '${variableName}' is not defined`,
|
|
1754
|
-
variable: variableName,
|
|
1755
|
-
position: { start: offset, end: offset + match.length }
|
|
1756
|
-
});
|
|
1757
|
-
}
|
|
1758
|
-
return this.options.undefinedReplacement;
|
|
1759
|
-
}
|
|
1760
|
-
const formattedValue = this.options.enableFormatting ? this.formatValue(value, variableExpression) : String(value);
|
|
1761
|
-
result.variables.push({
|
|
1762
|
-
name: variableName,
|
|
1763
|
-
value,
|
|
1764
|
-
formatted: formattedValue,
|
|
1765
|
-
type: this.getValueType(value),
|
|
1766
|
-
position: { start: offset, end: offset + match.length }
|
|
1767
|
-
});
|
|
1768
|
-
return formattedValue;
|
|
1769
|
-
} catch (error) {
|
|
1770
|
-
result.errors.push({
|
|
1771
|
-
type: "format_error",
|
|
1772
|
-
message: error instanceof Error ? error.message : "Format error",
|
|
1773
|
-
variable: variableExpression,
|
|
1774
|
-
position: { start: offset, end: offset + match.length }
|
|
1775
|
-
});
|
|
1776
|
-
return match;
|
|
1777
|
-
}
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
1780
|
-
replaceRecursive(content, variables, result, depth) {
|
|
1781
|
-
if (depth >= this.options.maxRecursionDepth) {
|
|
1782
|
-
result.errors.push({
|
|
1783
|
-
type: "recursion_limit",
|
|
1784
|
-
message: `Maximum recursion depth (${this.options.maxRecursionDepth}) exceeded`
|
|
1785
|
-
});
|
|
1786
|
-
return content;
|
|
1787
|
-
}
|
|
1788
|
-
const replaced = this.replaceSimpleVariables(content, variables, result);
|
|
1789
|
-
if (this.hasVariables(replaced) && replaced !== content) {
|
|
1790
|
-
return this.replaceRecursive(replaced, variables, result, depth + 1);
|
|
1791
|
-
}
|
|
1792
|
-
return replaced;
|
|
1793
|
-
}
|
|
1794
|
-
processConditionals(content, variables, result) {
|
|
1795
|
-
const conditionals = this.extractConditionals(content);
|
|
1796
|
-
let processedContent = content;
|
|
1797
|
-
conditionals.forEach((conditional) => {
|
|
1798
|
-
try {
|
|
1799
|
-
const conditionResult = this.evaluateCondition(conditional.condition, variables);
|
|
1800
|
-
const replacementContent = conditionResult ? conditional.content : conditional.elseContent || "";
|
|
1801
|
-
const blockPattern = this.buildConditionalPattern(conditional);
|
|
1802
|
-
processedContent = processedContent.replace(blockPattern, replacementContent);
|
|
1803
|
-
} catch (error) {
|
|
1804
|
-
result.errors.push({
|
|
1805
|
-
type: "syntax_error",
|
|
1806
|
-
message: `Error in conditional: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1807
|
-
});
|
|
1808
|
-
}
|
|
1809
|
-
});
|
|
1810
|
-
return processedContent;
|
|
1811
|
-
}
|
|
1812
|
-
processLoops(content, variables, result) {
|
|
1813
|
-
const loops = this.extractLoops(content);
|
|
1814
|
-
let processedContent = content;
|
|
1815
|
-
loops.forEach((loop) => {
|
|
1816
|
-
try {
|
|
1817
|
-
const arrayValue = this.getVariableValue(loop.array, variables);
|
|
1818
|
-
if (!Array.isArray(arrayValue)) {
|
|
1819
|
-
result.errors.push({
|
|
1820
|
-
type: "syntax_error",
|
|
1821
|
-
message: `Loop variable '${loop.array}' is not an array`
|
|
1822
|
-
});
|
|
1823
|
-
return;
|
|
1824
|
-
}
|
|
1825
|
-
let loopContent = "";
|
|
1826
|
-
arrayValue.forEach((item, index) => {
|
|
1827
|
-
const loopVariables = {
|
|
1828
|
-
...variables,
|
|
1829
|
-
[loop.variable]: item,
|
|
1830
|
-
[`${loop.variable}_index`]: index,
|
|
1831
|
-
[`${loop.variable}_first`]: index === 0,
|
|
1832
|
-
[`${loop.variable}_last`]: index === arrayValue.length - 1
|
|
1833
|
-
};
|
|
1834
|
-
const itemContent = this.replaceSimpleVariables(loop.content, loopVariables, result);
|
|
1835
|
-
loopContent += itemContent;
|
|
1836
|
-
});
|
|
1837
|
-
const blockPattern = this.buildLoopPattern(loop);
|
|
1838
|
-
processedContent = processedContent.replace(blockPattern, loopContent);
|
|
1839
|
-
} catch (error) {
|
|
1840
|
-
result.errors.push({
|
|
1841
|
-
type: "syntax_error",
|
|
1842
|
-
message: `Error in loop: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1843
|
-
});
|
|
1844
|
-
}
|
|
1845
|
-
});
|
|
1846
|
-
return processedContent;
|
|
1847
|
-
}
|
|
1848
|
-
parseVariableName(expression) {
|
|
1849
|
-
const parts = expression.split("|");
|
|
1850
|
-
return parts[0].trim();
|
|
1851
|
-
}
|
|
1852
|
-
getVariableValue(name, variables) {
|
|
1853
|
-
const parts = name.split(".");
|
|
1854
|
-
let value = variables;
|
|
1855
|
-
for (const part of parts) {
|
|
1856
|
-
if (value === null || value === void 0) {
|
|
1857
|
-
return void 0;
|
|
1858
|
-
}
|
|
1859
|
-
if (this.options.caseSensitive) {
|
|
1860
|
-
value = value[part];
|
|
1861
|
-
} else {
|
|
1862
|
-
const key = Object.keys(value).find((k) => k.toLowerCase() === part.toLowerCase());
|
|
1863
|
-
value = key ? value[key] : void 0;
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
return value;
|
|
1867
|
-
}
|
|
1868
|
-
formatValue(value, expression) {
|
|
1869
|
-
if (!this.options.enableFormatting) {
|
|
1870
|
-
return String(value);
|
|
1871
|
-
}
|
|
1872
|
-
const parts = expression.split("|");
|
|
1873
|
-
if (parts.length < 2) {
|
|
1874
|
-
return String(value);
|
|
1875
|
-
}
|
|
1876
|
-
const formatter = parts[1].trim();
|
|
1877
|
-
try {
|
|
1878
|
-
switch (formatter) {
|
|
1879
|
-
case "upper":
|
|
1880
|
-
return String(value).toUpperCase();
|
|
1881
|
-
case "lower":
|
|
1882
|
-
return String(value).toLowerCase();
|
|
1883
|
-
case "capitalize":
|
|
1884
|
-
return String(value).charAt(0).toUpperCase() + String(value).slice(1).toLowerCase();
|
|
1885
|
-
case "number":
|
|
1886
|
-
return Number(value).toLocaleString();
|
|
1887
|
-
case "currency":
|
|
1888
|
-
return new Intl.NumberFormat("ko-KR", {
|
|
1889
|
-
style: "currency",
|
|
1890
|
-
currency: "KRW"
|
|
1891
|
-
}).format(Number(value));
|
|
1892
|
-
case "date":
|
|
1893
|
-
return new Date(value).toLocaleDateString("ko-KR");
|
|
1894
|
-
case "datetime":
|
|
1895
|
-
return new Date(value).toLocaleString("ko-KR");
|
|
1896
|
-
case "time":
|
|
1897
|
-
return new Date(value).toLocaleTimeString("ko-KR");
|
|
1898
|
-
default:
|
|
1899
|
-
if (formatter.startsWith("date:")) {
|
|
1900
|
-
const format = formatter.substring(5);
|
|
1901
|
-
return this.formatDate(new Date(value), format);
|
|
1902
|
-
}
|
|
1903
|
-
if (formatter.startsWith("number:")) {
|
|
1904
|
-
const digits = parseInt(formatter.substring(7));
|
|
1905
|
-
return Number(value).toFixed(digits);
|
|
1906
|
-
}
|
|
1907
|
-
return String(value);
|
|
1908
|
-
}
|
|
1909
|
-
} catch (error) {
|
|
1910
|
-
return String(value);
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
formatDate(date, format) {
|
|
1914
|
-
const year = date.getFullYear();
|
|
1915
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1916
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
1917
|
-
const hours = String(date.getHours()).padStart(2, "0");
|
|
1918
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
1919
|
-
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
1920
|
-
return format.replace("YYYY", String(year)).replace("MM", month).replace("DD", day).replace("HH", hours).replace("mm", minutes).replace("ss", seconds);
|
|
1921
|
-
}
|
|
1922
|
-
getValueType(value) {
|
|
1923
|
-
if (value === void 0 || value === null) return "undefined";
|
|
1924
|
-
if (typeof value === "string") return "string";
|
|
1925
|
-
if (typeof value === "number") return "number";
|
|
1926
|
-
if (typeof value === "boolean") return "boolean";
|
|
1927
|
-
if (value instanceof Date) return "date";
|
|
1928
|
-
if (Array.isArray(value)) return "array";
|
|
1929
|
-
if (typeof value === "object") return "object";
|
|
1930
|
-
return "string";
|
|
1931
|
-
}
|
|
1932
|
-
hasVariables(content) {
|
|
1933
|
-
return this.options.variablePattern.test(content);
|
|
1934
|
-
}
|
|
1935
|
-
extractConditionals(content) {
|
|
1936
|
-
const conditionalPattern = /\{\{if\s+([^}]+)\}\}(.*?)\{\{\/if\}\}/gs;
|
|
1937
|
-
const conditionals = [];
|
|
1938
|
-
let match;
|
|
1939
|
-
while ((match = conditionalPattern.exec(content)) !== null) {
|
|
1940
|
-
const condition = match[1].trim();
|
|
1941
|
-
const fullContent = match[2];
|
|
1942
|
-
const elsePattern = /^(.*?)\{\{else\}\}(.*)$/s;
|
|
1943
|
-
const elseMatch = fullContent.match(elsePattern);
|
|
1944
|
-
if (elseMatch) {
|
|
1945
|
-
conditionals.push({
|
|
1946
|
-
condition,
|
|
1947
|
-
content: elseMatch[1],
|
|
1948
|
-
elseContent: elseMatch[2]
|
|
1949
|
-
});
|
|
1950
|
-
} else {
|
|
1951
|
-
conditionals.push({
|
|
1952
|
-
condition,
|
|
1953
|
-
content: fullContent
|
|
1954
|
-
});
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
return conditionals;
|
|
1958
|
-
}
|
|
1959
|
-
extractLoops(content) {
|
|
1960
|
-
const loopPattern = /\{\{for\s+(\w+)\s+in\s+(\w+)\}\}(.*?)\{\{\/for\}\}/gs;
|
|
1961
|
-
const loops = [];
|
|
1962
|
-
let match;
|
|
1963
|
-
while ((match = loopPattern.exec(content)) !== null) {
|
|
1964
|
-
loops.push({
|
|
1965
|
-
variable: match[1],
|
|
1966
|
-
array: match[2],
|
|
1967
|
-
content: match[3]
|
|
1968
|
-
});
|
|
1969
|
-
}
|
|
1970
|
-
return loops;
|
|
1971
|
-
}
|
|
1972
|
-
extractVariablesFromExpression(expression) {
|
|
1973
|
-
const variables = /* @__PURE__ */ new Set();
|
|
1974
|
-
const variablePattern = /\b([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\b/g;
|
|
1975
|
-
let match;
|
|
1976
|
-
while ((match = variablePattern.exec(expression)) !== null) {
|
|
1977
|
-
variables.add(match[1]);
|
|
1978
|
-
}
|
|
1979
|
-
return Array.from(variables);
|
|
1980
|
-
}
|
|
1981
|
-
evaluateCondition(condition, variables) {
|
|
1982
|
-
try {
|
|
1983
|
-
const normalizedCondition = condition.trim();
|
|
1984
|
-
if (normalizedCondition.startsWith("!")) {
|
|
1985
|
-
const variable = normalizedCondition.substring(1).trim();
|
|
1986
|
-
const value2 = this.getVariableValue(variable, variables);
|
|
1987
|
-
return !value2;
|
|
1988
|
-
}
|
|
1989
|
-
if (normalizedCondition.includes("===")) {
|
|
1990
|
-
const [left, right] = normalizedCondition.split("===").map((s) => s.trim());
|
|
1991
|
-
const leftValue = this.getVariableValue(left, variables);
|
|
1992
|
-
const rightValue = right.startsWith('"') || right.startsWith("'") ? right.slice(1, -1) : this.getVariableValue(right, variables);
|
|
1993
|
-
return leftValue === rightValue;
|
|
1994
|
-
}
|
|
1995
|
-
if (normalizedCondition.includes("!==")) {
|
|
1996
|
-
const [left, right] = normalizedCondition.split("!==").map((s) => s.trim());
|
|
1997
|
-
const leftValue = this.getVariableValue(left, variables);
|
|
1998
|
-
const rightValue = right.startsWith('"') || right.startsWith("'") ? right.slice(1, -1) : this.getVariableValue(right, variables);
|
|
1999
|
-
return leftValue !== rightValue;
|
|
2000
|
-
}
|
|
2001
|
-
const value = this.getVariableValue(normalizedCondition, variables);
|
|
2002
|
-
return Boolean(value);
|
|
2003
|
-
} catch (error) {
|
|
2004
|
-
return false;
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
buildConditionalPattern(conditional) {
|
|
2008
|
-
const escapedCondition = conditional.condition.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2009
|
-
const elsePattern = conditional.elseContent ? ".*?\\{\\{else\\}\\}.*?" : ".*?";
|
|
2010
|
-
return new RegExp(`\\{\\{if\\s+${escapedCondition}\\}\\}${elsePattern}\\{\\{/if\\}\\}`, "gs");
|
|
2011
|
-
}
|
|
2012
|
-
buildLoopPattern(loop) {
|
|
2013
|
-
const escapedVariable = loop.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2014
|
-
const escapedArray = loop.array.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2015
|
-
return new RegExp(`\\{\\{for\\s+${escapedVariable}\\s+in\\s+${escapedArray}\\}\\}.*?\\{\\{/for\\}\\}`, "gs");
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2018
|
-
var defaultVariableReplacer = new VariableReplacer({
|
|
2019
|
-
variablePattern: /\#\{([^}]+)\}/g,
|
|
2020
|
-
allowUndefined: false,
|
|
2021
|
-
undefinedReplacement: "",
|
|
2022
|
-
caseSensitive: false,
|
|
2023
|
-
// More flexible for Korean usage
|
|
2024
|
-
enableFormatting: true,
|
|
2025
|
-
enableConditionals: true,
|
|
2026
|
-
enableLoops: true,
|
|
2027
|
-
maxRecursionDepth: 5
|
|
2028
|
-
});
|
|
2029
|
-
var VariableUtils = {
|
|
2030
|
-
/**
|
|
2031
|
-
* Extract all variables from content
|
|
2032
|
-
*/
|
|
2033
|
-
extractVariables: (content) => {
|
|
2034
|
-
return defaultVariableReplacer.extractVariables(content);
|
|
2035
|
-
},
|
|
2036
|
-
/**
|
|
2037
|
-
* Replace variables in content
|
|
2038
|
-
*/
|
|
2039
|
-
replace: (content, variables) => {
|
|
2040
|
-
return defaultVariableReplacer.replace(content, variables).content;
|
|
2041
|
-
},
|
|
2042
|
-
/**
|
|
2043
|
-
* Validate content has all required variables
|
|
2044
|
-
*/
|
|
2045
|
-
validate: (content, variables) => {
|
|
2046
|
-
return defaultVariableReplacer.validate(content, variables).isValid;
|
|
2047
|
-
},
|
|
2048
|
-
/**
|
|
2049
|
-
* Create personalized content for multiple recipients
|
|
2050
|
-
*/
|
|
2051
|
-
personalize: (content, recipients) => {
|
|
2052
|
-
return recipients.map((recipient) => {
|
|
2053
|
-
const result = defaultVariableReplacer.replace(content, recipient.variables);
|
|
2054
|
-
return {
|
|
2055
|
-
phoneNumber: recipient.phoneNumber,
|
|
2056
|
-
content: result.content,
|
|
2057
|
-
errors: result.errors.length > 0 ? result.errors.map((e) => e.message) : void 0
|
|
2058
|
-
};
|
|
2059
|
-
});
|
|
2060
|
-
}
|
|
2061
|
-
};
|
|
2062
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
2063
|
-
0 && (module.exports = {
|
|
2064
|
-
BulkMessageSender,
|
|
2065
|
-
DeliveryTracker,
|
|
2066
|
-
JobProcessor,
|
|
2067
|
-
MessageErrorSchema,
|
|
2068
|
-
MessageEventType,
|
|
2069
|
-
MessageJobProcessor,
|
|
2070
|
-
MessageRequestSchema,
|
|
2071
|
-
MessageResultSchema,
|
|
2072
|
-
MessageRetryHandler,
|
|
2073
|
-
MessageStatus,
|
|
2074
|
-
RecipientResultSchema,
|
|
2075
|
-
RecipientSchema,
|
|
2076
|
-
SchedulingOptionsSchema,
|
|
2077
|
-
SendingOptionsSchema,
|
|
2078
|
-
SingleMessageSender,
|
|
2079
|
-
VariableMapSchema,
|
|
2080
|
-
VariableReplacer,
|
|
2081
|
-
VariableUtils,
|
|
2082
|
-
defaultVariableReplacer
|
|
2083
|
-
});
|
|
2084
|
-
//# sourceMappingURL=index.cjs.map
|