@k-msg/messaging 0.1.0

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