@k-msg/messaging 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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