@k-msg/core 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,968 @@
1
+ // src/errors.ts
2
+ var KMessageErrorCode = /* @__PURE__ */ ((KMessageErrorCode2) => {
3
+ KMessageErrorCode2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
4
+ KMessageErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
5
+ KMessageErrorCode2["CONFIGURATION_ERROR"] = "CONFIGURATION_ERROR";
6
+ KMessageErrorCode2["PROVIDER_NOT_FOUND"] = "PROVIDER_NOT_FOUND";
7
+ KMessageErrorCode2["PROVIDER_NOT_AVAILABLE"] = "PROVIDER_NOT_AVAILABLE";
8
+ KMessageErrorCode2["PROVIDER_AUTHENTICATION_FAILED"] = "PROVIDER_AUTHENTICATION_FAILED";
9
+ KMessageErrorCode2["PROVIDER_CONNECTION_FAILED"] = "PROVIDER_CONNECTION_FAILED";
10
+ KMessageErrorCode2["PROVIDER_RATE_LIMITED"] = "PROVIDER_RATE_LIMITED";
11
+ KMessageErrorCode2["PROVIDER_INSUFFICIENT_BALANCE"] = "PROVIDER_INSUFFICIENT_BALANCE";
12
+ KMessageErrorCode2["TEMPLATE_NOT_FOUND"] = "TEMPLATE_NOT_FOUND";
13
+ KMessageErrorCode2["TEMPLATE_VALIDATION_FAILED"] = "TEMPLATE_VALIDATION_FAILED";
14
+ KMessageErrorCode2["TEMPLATE_CREATION_FAILED"] = "TEMPLATE_CREATION_FAILED";
15
+ KMessageErrorCode2["TEMPLATE_MODIFICATION_FAILED"] = "TEMPLATE_MODIFICATION_FAILED";
16
+ KMessageErrorCode2["TEMPLATE_DELETION_FAILED"] = "TEMPLATE_DELETION_FAILED";
17
+ KMessageErrorCode2["MESSAGE_SEND_FAILED"] = "MESSAGE_SEND_FAILED";
18
+ KMessageErrorCode2["MESSAGE_INVALID_PHONE_NUMBER"] = "MESSAGE_INVALID_PHONE_NUMBER";
19
+ KMessageErrorCode2["MESSAGE_INVALID_VARIABLES"] = "MESSAGE_INVALID_VARIABLES";
20
+ KMessageErrorCode2["MESSAGE_QUOTA_EXCEEDED"] = "MESSAGE_QUOTA_EXCEEDED";
21
+ KMessageErrorCode2["MESSAGE_RESERVATION_FAILED"] = "MESSAGE_RESERVATION_FAILED";
22
+ KMessageErrorCode2["MESSAGE_CANCELLATION_FAILED"] = "MESSAGE_CANCELLATION_FAILED";
23
+ KMessageErrorCode2["NETWORK_TIMEOUT"] = "NETWORK_TIMEOUT";
24
+ KMessageErrorCode2["NETWORK_CONNECTION_FAILED"] = "NETWORK_CONNECTION_FAILED";
25
+ KMessageErrorCode2["NETWORK_SERVICE_UNAVAILABLE"] = "NETWORK_SERVICE_UNAVAILABLE";
26
+ KMessageErrorCode2["API_INVALID_REQUEST"] = "API_INVALID_REQUEST";
27
+ KMessageErrorCode2["API_UNAUTHORIZED"] = "API_UNAUTHORIZED";
28
+ KMessageErrorCode2["API_FORBIDDEN"] = "API_FORBIDDEN";
29
+ KMessageErrorCode2["API_NOT_FOUND"] = "API_NOT_FOUND";
30
+ KMessageErrorCode2["API_TOO_MANY_REQUESTS"] = "API_TOO_MANY_REQUESTS";
31
+ KMessageErrorCode2["API_INTERNAL_SERVER_ERROR"] = "API_INTERNAL_SERVER_ERROR";
32
+ return KMessageErrorCode2;
33
+ })(KMessageErrorCode || {});
34
+ var KMessageError = class _KMessageError extends Error {
35
+ code;
36
+ context;
37
+ retryable;
38
+ statusCode;
39
+ cause;
40
+ constructor(code, message, context = {}, options = {}) {
41
+ super(message);
42
+ this.name = "KMessageError";
43
+ this.code = code;
44
+ this.context = {
45
+ ...context,
46
+ timestamp: context.timestamp || /* @__PURE__ */ new Date()
47
+ };
48
+ this.retryable = options.retryable ?? this.isRetryableByDefault(code);
49
+ this.statusCode = options.statusCode;
50
+ this.cause = options.cause;
51
+ if (Error.captureStackTrace) {
52
+ Error.captureStackTrace(this, _KMessageError);
53
+ }
54
+ }
55
+ isRetryableByDefault(code) {
56
+ const retryableCodes = [
57
+ "NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */,
58
+ "NETWORK_CONNECTION_FAILED" /* NETWORK_CONNECTION_FAILED */,
59
+ "NETWORK_SERVICE_UNAVAILABLE" /* NETWORK_SERVICE_UNAVAILABLE */,
60
+ "PROVIDER_CONNECTION_FAILED" /* PROVIDER_CONNECTION_FAILED */,
61
+ "PROVIDER_RATE_LIMITED" /* PROVIDER_RATE_LIMITED */,
62
+ "API_TOO_MANY_REQUESTS" /* API_TOO_MANY_REQUESTS */,
63
+ "API_INTERNAL_SERVER_ERROR" /* API_INTERNAL_SERVER_ERROR */
64
+ ];
65
+ return retryableCodes.includes(code);
66
+ }
67
+ toJSON() {
68
+ return {
69
+ name: this.name,
70
+ code: this.code,
71
+ message: this.message,
72
+ context: this.context,
73
+ retryable: this.retryable,
74
+ statusCode: this.statusCode,
75
+ stack: this.stack
76
+ };
77
+ }
78
+ };
79
+ var ProviderError = class extends KMessageError {
80
+ constructor(providerId, code, message, context = {}, options = {}) {
81
+ super(code, message, { ...context, providerId }, options);
82
+ this.name = "ProviderError";
83
+ }
84
+ };
85
+ var TemplateError = class extends KMessageError {
86
+ constructor(templateCode, code, message, context = {}, options = {}) {
87
+ super(code, message, { ...context, templateCode }, options);
88
+ this.name = "TemplateError";
89
+ }
90
+ };
91
+ var MessageError = class extends KMessageError {
92
+ constructor(phoneNumber, code, message, context = {}, options = {}) {
93
+ super(code, message, { ...context, phoneNumber }, options);
94
+ this.name = "MessageError";
95
+ }
96
+ };
97
+ var ErrorFactory = {
98
+ providerNotFound: (providerId) => new ProviderError(
99
+ providerId,
100
+ "PROVIDER_NOT_FOUND" /* PROVIDER_NOT_FOUND */,
101
+ `Provider '${providerId}' not found`,
102
+ {},
103
+ { retryable: false, statusCode: 404 }
104
+ ),
105
+ providerNotAvailable: (providerId, reason) => new ProviderError(
106
+ providerId,
107
+ "PROVIDER_NOT_AVAILABLE" /* PROVIDER_NOT_AVAILABLE */,
108
+ `Provider '${providerId}' is not available${reason ? `: ${reason}` : ""}`,
109
+ {},
110
+ { retryable: true, statusCode: 503 }
111
+ ),
112
+ authenticationFailed: (providerId, details) => new ProviderError(
113
+ providerId,
114
+ "PROVIDER_AUTHENTICATION_FAILED" /* PROVIDER_AUTHENTICATION_FAILED */,
115
+ `Authentication failed for provider '${providerId}'${details ? `: ${details}` : ""}`,
116
+ {},
117
+ { retryable: false, statusCode: 401 }
118
+ ),
119
+ templateNotFound: (templateCode) => new TemplateError(
120
+ templateCode,
121
+ "TEMPLATE_NOT_FOUND" /* TEMPLATE_NOT_FOUND */,
122
+ `Template '${templateCode}' not found`,
123
+ {},
124
+ { retryable: false, statusCode: 404 }
125
+ ),
126
+ invalidPhoneNumber: (phoneNumber) => new MessageError(
127
+ phoneNumber,
128
+ "MESSAGE_INVALID_PHONE_NUMBER" /* MESSAGE_INVALID_PHONE_NUMBER */,
129
+ `Invalid phone number format: ${phoneNumber}`,
130
+ {},
131
+ { retryable: false, statusCode: 400 }
132
+ ),
133
+ networkTimeout: (providerId, timeout) => new KMessageError(
134
+ "NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */,
135
+ `Network request timed out${timeout ? ` after ${timeout}ms` : ""}`,
136
+ { providerId },
137
+ { retryable: true, statusCode: 408 }
138
+ ),
139
+ rateLimited: (providerId, retryAfter) => new ProviderError(
140
+ providerId,
141
+ "PROVIDER_RATE_LIMITED" /* PROVIDER_RATE_LIMITED */,
142
+ `Rate limit exceeded for provider '${providerId}'${retryAfter ? `. Retry after ${retryAfter}s` : ""}`,
143
+ { retryAfter },
144
+ { retryable: true, statusCode: 429 }
145
+ ),
146
+ insufficientBalance: (providerId, balance) => new ProviderError(
147
+ providerId,
148
+ "PROVIDER_INSUFFICIENT_BALANCE" /* PROVIDER_INSUFFICIENT_BALANCE */,
149
+ `Insufficient balance for provider '${providerId}'${balance ? `. Current balance: ${balance}` : ""}`,
150
+ { balance },
151
+ { retryable: false, statusCode: 402 }
152
+ ),
153
+ fromHttpStatus: (statusCode, message, context = {}) => {
154
+ let code;
155
+ let retryable = false;
156
+ switch (statusCode) {
157
+ case 400:
158
+ code = "API_INVALID_REQUEST" /* API_INVALID_REQUEST */;
159
+ break;
160
+ case 401:
161
+ code = "API_UNAUTHORIZED" /* API_UNAUTHORIZED */;
162
+ break;
163
+ case 403:
164
+ code = "API_FORBIDDEN" /* API_FORBIDDEN */;
165
+ break;
166
+ case 404:
167
+ code = "API_NOT_FOUND" /* API_NOT_FOUND */;
168
+ break;
169
+ case 408:
170
+ code = "NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */;
171
+ retryable = true;
172
+ break;
173
+ case 429:
174
+ code = "API_TOO_MANY_REQUESTS" /* API_TOO_MANY_REQUESTS */;
175
+ retryable = true;
176
+ break;
177
+ case 500:
178
+ case 502:
179
+ case 503:
180
+ case 504:
181
+ code = "API_INTERNAL_SERVER_ERROR" /* API_INTERNAL_SERVER_ERROR */;
182
+ retryable = true;
183
+ break;
184
+ default:
185
+ code = "UNKNOWN_ERROR" /* UNKNOWN_ERROR */;
186
+ retryable = statusCode >= 500 && statusCode < 600;
187
+ }
188
+ return new KMessageError(code, message, context, { retryable, statusCode });
189
+ }
190
+ };
191
+ var Result = {
192
+ success: (data) => ({ success: true, data }),
193
+ failure: (error) => ({ success: false, error }),
194
+ fromPromise: async (promise) => {
195
+ try {
196
+ const data = await promise;
197
+ return Result.success(data);
198
+ } catch (error) {
199
+ if (error instanceof KMessageError) {
200
+ return Result.failure(error);
201
+ }
202
+ return Result.failure(new KMessageError(
203
+ "UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
204
+ error instanceof Error ? error.message : "Unknown error occurred",
205
+ {},
206
+ { cause: error instanceof Error ? error : void 0 }
207
+ ));
208
+ }
209
+ }
210
+ };
211
+ var ErrorUtils = {
212
+ isRetryable: (error) => {
213
+ if (error instanceof KMessageError) {
214
+ return error.retryable;
215
+ }
216
+ const retryableMessages = ["timeout", "ECONNRESET", "ECONNREFUSED", "ETIMEDOUT"];
217
+ return retryableMessages.some(
218
+ (msg) => error.message.toLowerCase().includes(msg.toLowerCase())
219
+ );
220
+ },
221
+ getStatusCode: (error) => {
222
+ if (error instanceof KMessageError && error.statusCode) {
223
+ return error.statusCode;
224
+ }
225
+ return 500;
226
+ },
227
+ formatErrorForClient: (error) => {
228
+ if (error instanceof KMessageError) {
229
+ return {
230
+ code: error.code,
231
+ message: error.message,
232
+ retryable: error.retryable,
233
+ context: error.context
234
+ };
235
+ }
236
+ return {
237
+ code: "UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
238
+ message: error.message || "Unknown error occurred",
239
+ retryable: false,
240
+ context: {}
241
+ };
242
+ },
243
+ formatErrorForLogging: (error) => {
244
+ const baseInfo = {
245
+ name: error.name,
246
+ message: error.message,
247
+ stack: error.stack
248
+ };
249
+ if (error instanceof KMessageError) {
250
+ return {
251
+ ...baseInfo,
252
+ code: error.code,
253
+ context: error.context,
254
+ retryable: error.retryable,
255
+ statusCode: error.statusCode
256
+ };
257
+ }
258
+ return baseInfo;
259
+ }
260
+ };
261
+
262
+ // src/test-utils.ts
263
+ import { expect } from "bun:test";
264
+ var MockProvider = class {
265
+ id;
266
+ name;
267
+ _healthy = true;
268
+ _issues = [];
269
+ _balance = "1000";
270
+ _templates = [];
271
+ _history = [];
272
+ constructor(id = "mock", name = "Mock Provider") {
273
+ this.id = id;
274
+ this.name = name;
275
+ }
276
+ // Health check simulation
277
+ async healthCheck() {
278
+ return {
279
+ healthy: this._healthy,
280
+ issues: [...this._issues],
281
+ data: {
282
+ balance: this._balance,
283
+ status: this._healthy ? "connected" : "disconnected",
284
+ code: this._healthy ? 200 : 500,
285
+ message: this._healthy ? "OK" : "Service unavailable"
286
+ }
287
+ };
288
+ }
289
+ // Test helpers
290
+ setHealthy(healthy, issues = []) {
291
+ this._healthy = healthy;
292
+ this._issues = issues;
293
+ }
294
+ setBalance(balance) {
295
+ this._balance = balance;
296
+ }
297
+ setTemplates(templates) {
298
+ this._templates = templates;
299
+ }
300
+ setHistory(history) {
301
+ this._history = history;
302
+ }
303
+ // Provider methods with mock implementations
304
+ async sendMessage(templateCode, phoneNumber, variables, options) {
305
+ if (!this._healthy) {
306
+ throw new MessageError(phoneNumber, "MESSAGE_SEND_FAILED" /* MESSAGE_SEND_FAILED */, "Provider is unhealthy");
307
+ }
308
+ if (phoneNumber === "01000000000") {
309
+ throw new MessageError(phoneNumber, "MESSAGE_INVALID_PHONE_NUMBER" /* MESSAGE_INVALID_PHONE_NUMBER */, "Invalid phone number");
310
+ }
311
+ return {
312
+ success: true,
313
+ messageId: `mock_${Date.now()}`,
314
+ status: "sent",
315
+ error: null
316
+ };
317
+ }
318
+ async getTemplates(page = 1, size = 15, filters) {
319
+ if (!this._healthy) {
320
+ throw new ProviderError(this.id, "PROVIDER_NOT_AVAILABLE" /* PROVIDER_NOT_AVAILABLE */, "Provider is unhealthy");
321
+ }
322
+ const start = (page - 1) * size;
323
+ const end = start + size;
324
+ const list = this._templates.slice(start, end);
325
+ return {
326
+ code: 200,
327
+ message: "Success",
328
+ totalCount: this._templates.length,
329
+ list
330
+ };
331
+ }
332
+ async createTemplate(name, content, category, buttons) {
333
+ if (!this._healthy) {
334
+ throw new TemplateError(name, "TEMPLATE_CREATION_FAILED" /* TEMPLATE_CREATION_FAILED */, "Provider is unhealthy");
335
+ }
336
+ if (name === "invalid_template") {
337
+ throw new TemplateError(name, "TEMPLATE_VALIDATION_FAILED" /* TEMPLATE_VALIDATION_FAILED */, "Template validation failed");
338
+ }
339
+ const templateCode = `mock_${name}_${Date.now()}`;
340
+ this._templates.push({
341
+ templateCode,
342
+ templateName: name,
343
+ templateContent: content,
344
+ status: "Y",
345
+ createDate: (/* @__PURE__ */ new Date()).toISOString()
346
+ });
347
+ return {
348
+ success: true,
349
+ templateCode,
350
+ status: "created",
351
+ error: null
352
+ };
353
+ }
354
+ async modifyTemplate(templateCode, name, content, buttons) {
355
+ if (!this._healthy) {
356
+ throw new TemplateError(templateCode, "TEMPLATE_MODIFICATION_FAILED" /* TEMPLATE_MODIFICATION_FAILED */, "Provider is unhealthy");
357
+ }
358
+ const template = this._templates.find((t) => t.templateCode === templateCode);
359
+ if (!template) {
360
+ throw new TemplateError(templateCode, "TEMPLATE_NOT_FOUND" /* TEMPLATE_NOT_FOUND */, "Template not found");
361
+ }
362
+ template.templateName = name;
363
+ template.templateContent = content;
364
+ return {
365
+ success: true,
366
+ templateCode,
367
+ status: "modified",
368
+ error: null
369
+ };
370
+ }
371
+ async deleteTemplate(templateCode) {
372
+ if (!this._healthy) {
373
+ throw new TemplateError(templateCode, "TEMPLATE_DELETION_FAILED" /* TEMPLATE_DELETION_FAILED */, "Provider is unhealthy");
374
+ }
375
+ const index = this._templates.findIndex((t) => t.templateCode === templateCode);
376
+ if (index === -1) {
377
+ return {
378
+ code: 404,
379
+ message: "Template not found"
380
+ };
381
+ }
382
+ this._templates.splice(index, 1);
383
+ return {
384
+ code: 200,
385
+ message: "Template deleted successfully"
386
+ };
387
+ }
388
+ async getHistory(page = 1, size = 15, filters) {
389
+ if (!this._healthy) {
390
+ throw new ProviderError(this.id, "PROVIDER_NOT_AVAILABLE" /* PROVIDER_NOT_AVAILABLE */, "Provider is unhealthy");
391
+ }
392
+ const start = (page - 1) * size;
393
+ const end = start + size;
394
+ const list = this._history.slice(start, end);
395
+ return {
396
+ code: 200,
397
+ message: "Success",
398
+ totalCount: this._history.length,
399
+ list
400
+ };
401
+ }
402
+ async cancelReservation(messageId) {
403
+ if (!this._healthy) {
404
+ throw new KMessageError("MESSAGE_CANCELLATION_FAILED" /* MESSAGE_CANCELLATION_FAILED */, "Provider is unhealthy");
405
+ }
406
+ return {
407
+ code: 200,
408
+ message: "Reservation cancelled successfully"
409
+ };
410
+ }
411
+ };
412
+ var TestAssertions = {
413
+ /**
414
+ * Assert that an error is a KMessageError with specific code
415
+ */
416
+ assertKMessageError: (error, expectedCode, expectedMessage) => {
417
+ expect(error).toBeInstanceOf(KMessageError);
418
+ const kError = error;
419
+ expect(kError.code).toBe(expectedCode);
420
+ if (expectedMessage) {
421
+ expect(kError.message).toContain(expectedMessage);
422
+ }
423
+ },
424
+ /**
425
+ * Assert that an error is retryable
426
+ */
427
+ assertRetryable: (error, expected = true) => {
428
+ expect(error.retryable).toBe(expected);
429
+ },
430
+ /**
431
+ * Assert that a health status is healthy
432
+ */
433
+ assertHealthy: (health, expected = true) => {
434
+ expect(health.healthy).toBe(expected);
435
+ if (!expected) {
436
+ expect(health.issues.length).toBeGreaterThan(0);
437
+ }
438
+ },
439
+ /**
440
+ * Assert that a provider result has expected structure
441
+ */
442
+ assertProviderResult: (result, expectSuccess = true) => {
443
+ expect(result).toHaveProperty("success");
444
+ expect(result.success).toBe(expectSuccess);
445
+ if (expectSuccess) {
446
+ expect(result.error).toBeNull();
447
+ } else {
448
+ expect(result.error).toBeDefined();
449
+ }
450
+ },
451
+ /**
452
+ * Assert that API response has expected structure
453
+ */
454
+ assertApiResponse: (response, expectedCode = 200) => {
455
+ expect(response).toHaveProperty("code");
456
+ expect(response).toHaveProperty("message");
457
+ expect(response.code).toBe(expectedCode);
458
+ }
459
+ };
460
+ var TestData = {
461
+ createMockTemplate: (overrides = {}) => ({
462
+ templateCode: "mock_template_001",
463
+ templateName: "Mock Template",
464
+ templateContent: "[#{\uC11C\uBE44\uC2A4\uBA85}] \uC548\uB155\uD558\uC138\uC694, #{\uACE0\uAC1D\uBA85}\uB2D8!",
465
+ status: "Y",
466
+ createDate: "2024-01-01 12:00:00",
467
+ ...overrides
468
+ }),
469
+ createMockMessage: (overrides = {}) => ({
470
+ seqNo: 12345,
471
+ phone: "01012345678",
472
+ templateCode: "mock_template_001",
473
+ statusCode: "OK",
474
+ statusCodeName: "\uC131\uACF5",
475
+ requestDate: "2024-01-01 12:00:00",
476
+ sendDate: "2024-01-01 12:01:00",
477
+ receiveDate: "2024-01-01 12:01:30",
478
+ sendMessage: "[MyApp] \uC548\uB155\uD558\uC138\uC694, \uD64D\uAE38\uB3D9\uB2D8!",
479
+ ...overrides
480
+ }),
481
+ createMockVariables: (overrides = {}) => ({
482
+ \uC11C\uBE44\uC2A4\uBA85: "MyApp",
483
+ \uACE0\uAC1D\uBA85: "\uD64D\uAE38\uB3D9",
484
+ \uC778\uC99D\uCF54\uB4DC: "123456",
485
+ ...overrides
486
+ }),
487
+ generatePhoneNumber: (valid = true) => {
488
+ if (valid) {
489
+ const numbers = ["010", "011", "016", "017", "018", "019"];
490
+ const prefix = numbers[Math.floor(Math.random() * numbers.length)];
491
+ const suffix = Math.floor(Math.random() * 1e8).toString().padStart(8, "0");
492
+ return prefix + suffix;
493
+ } else {
494
+ return "01000000000";
495
+ }
496
+ }
497
+ };
498
+ var TestSetup = {
499
+ /**
500
+ * Create a test environment with mock providers
501
+ */
502
+ createTestEnvironment: () => {
503
+ const mockProviders = {
504
+ healthy: new MockProvider("healthy", "Healthy Provider"),
505
+ unhealthy: new MockProvider("unhealthy", "Unhealthy Provider"),
506
+ rateLimited: new MockProvider("ratelimited", "Rate Limited Provider")
507
+ };
508
+ mockProviders.unhealthy.setHealthy(false, ["Connection failed", "Authentication error"]);
509
+ mockProviders.rateLimited.setHealthy(true);
510
+ return {
511
+ providers: mockProviders,
512
+ cleanup: () => {
513
+ }
514
+ };
515
+ },
516
+ /**
517
+ * Create test data for various scenarios
518
+ */
519
+ createTestScenarios: () => ({
520
+ validMessage: {
521
+ templateCode: "valid_template",
522
+ phoneNumber: TestData.generatePhoneNumber(true),
523
+ variables: TestData.createMockVariables()
524
+ },
525
+ invalidMessage: {
526
+ templateCode: "invalid_template",
527
+ phoneNumber: TestData.generatePhoneNumber(false),
528
+ variables: {}
529
+ },
530
+ templates: [
531
+ TestData.createMockTemplate({ templateCode: "template_001", templateName: "Welcome Message" }),
532
+ TestData.createMockTemplate({ templateCode: "template_002", templateName: "OTP Message" }),
533
+ TestData.createMockTemplate({ templateCode: "template_003", templateName: "Notification" })
534
+ ],
535
+ history: [
536
+ TestData.createMockMessage({ seqNo: 1, templateCode: "template_001" }),
537
+ TestData.createMockMessage({ seqNo: 2, templateCode: "template_002" }),
538
+ TestData.createMockMessage({ seqNo: 3, templateCode: "template_003" })
539
+ ]
540
+ })
541
+ };
542
+ var PerformanceTest = {
543
+ /**
544
+ * Measure execution time of a function
545
+ */
546
+ measureTime: async (fn) => {
547
+ const start = performance.now();
548
+ const result = await fn();
549
+ const duration = performance.now() - start;
550
+ return { result, duration };
551
+ },
552
+ /**
553
+ * Run a function multiple times and get statistics
554
+ */
555
+ benchmark: async (fn, iterations = 10) => {
556
+ const results = [];
557
+ const durations = [];
558
+ for (let i = 0; i < iterations; i++) {
559
+ const { result, duration } = await PerformanceTest.measureTime(fn);
560
+ results.push(result);
561
+ durations.push(duration);
562
+ }
563
+ durations.sort((a, b) => a - b);
564
+ const statistics = {
565
+ min: durations[0],
566
+ max: durations[durations.length - 1],
567
+ average: durations.reduce((sum, d) => sum + d, 0) / durations.length,
568
+ median: durations[Math.floor(durations.length / 2)]
569
+ };
570
+ return { results, statistics };
571
+ }
572
+ };
573
+
574
+ // src/retry.ts
575
+ var RetryHandler = class {
576
+ static defaultOptions = {
577
+ maxAttempts: 3,
578
+ initialDelay: 1e3,
579
+ maxDelay: 3e4,
580
+ backoffMultiplier: 2,
581
+ jitter: true,
582
+ retryCondition: (error) => ErrorUtils.isRetryable(error)
583
+ };
584
+ static async execute(operation, options = {}) {
585
+ const opts = { ...this.defaultOptions, ...options };
586
+ let lastError;
587
+ let delay = opts.initialDelay;
588
+ for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
589
+ try {
590
+ return await operation();
591
+ } catch (error) {
592
+ lastError = error;
593
+ if (attempt === opts.maxAttempts || !opts.retryCondition(lastError, attempt)) {
594
+ throw lastError;
595
+ }
596
+ const actualDelay = opts.jitter ? delay + Math.random() * delay * 0.1 : delay;
597
+ opts.onRetry?.(lastError, attempt);
598
+ await new Promise((resolve) => setTimeout(resolve, actualDelay));
599
+ delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelay);
600
+ }
601
+ }
602
+ throw lastError;
603
+ }
604
+ static createRetryableFunction(func, options = {}) {
605
+ return async (...args) => {
606
+ return this.execute(() => func(...args), options);
607
+ };
608
+ }
609
+ };
610
+ var CircuitBreaker = class {
611
+ constructor(options) {
612
+ this.options = options;
613
+ }
614
+ state = "CLOSED";
615
+ failureCount = 0;
616
+ lastFailureTime = 0;
617
+ nextAttemptTime = 0;
618
+ async execute(operation) {
619
+ const now = Date.now();
620
+ switch (this.state) {
621
+ case "OPEN":
622
+ if (now < this.nextAttemptTime) {
623
+ throw new KMessageError(
624
+ "NETWORK_SERVICE_UNAVAILABLE" /* NETWORK_SERVICE_UNAVAILABLE */,
625
+ "Circuit breaker is OPEN",
626
+ { state: this.state, nextAttemptTime: this.nextAttemptTime }
627
+ );
628
+ }
629
+ this.state = "HALF_OPEN";
630
+ this.options.onHalfOpen?.();
631
+ break;
632
+ case "HALF_OPEN":
633
+ break;
634
+ case "CLOSED":
635
+ break;
636
+ }
637
+ try {
638
+ const result = await Promise.race([
639
+ operation(),
640
+ new Promise(
641
+ (_, reject) => setTimeout(() => reject(
642
+ new KMessageError(
643
+ "NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */,
644
+ "Circuit breaker timeout",
645
+ { timeout: this.options.timeout }
646
+ )
647
+ ), this.options.timeout)
648
+ )
649
+ ]);
650
+ if (this.state === "HALF_OPEN") {
651
+ this.state = "CLOSED";
652
+ this.failureCount = 0;
653
+ this.options.onClose?.();
654
+ }
655
+ return result;
656
+ } catch (error) {
657
+ this.recordFailure();
658
+ throw error;
659
+ }
660
+ }
661
+ recordFailure() {
662
+ this.failureCount++;
663
+ this.lastFailureTime = Date.now();
664
+ if (this.failureCount >= this.options.failureThreshold) {
665
+ this.state = "OPEN";
666
+ this.nextAttemptTime = this.lastFailureTime + this.options.resetTimeout;
667
+ this.options.onOpen?.();
668
+ }
669
+ }
670
+ getState() {
671
+ return this.state;
672
+ }
673
+ getFailureCount() {
674
+ return this.failureCount;
675
+ }
676
+ reset() {
677
+ this.state = "CLOSED";
678
+ this.failureCount = 0;
679
+ this.lastFailureTime = 0;
680
+ this.nextAttemptTime = 0;
681
+ }
682
+ };
683
+ var BulkOperationHandler = class {
684
+ static async execute(items, operation, options = {}) {
685
+ const opts = {
686
+ concurrency: 5,
687
+ retryOptions: {
688
+ maxAttempts: 3,
689
+ initialDelay: 1e3,
690
+ maxDelay: 1e4,
691
+ backoffMultiplier: 2,
692
+ jitter: true
693
+ },
694
+ failFast: false,
695
+ ...options
696
+ };
697
+ const startTime = Date.now();
698
+ const successful = [];
699
+ const failed = [];
700
+ let completed = 0;
701
+ const retryableOperation = RetryHandler.createRetryableFunction(
702
+ operation,
703
+ opts.retryOptions
704
+ );
705
+ const batches = this.createBatches(items, opts.concurrency);
706
+ for (const batch of batches) {
707
+ const batchPromises = batch.map(async (item) => {
708
+ try {
709
+ const result = await retryableOperation(item);
710
+ successful.push({ item, result });
711
+ } catch (error) {
712
+ failed.push({ item, error });
713
+ if (opts.failFast) {
714
+ throw new KMessageError(
715
+ "MESSAGE_SEND_FAILED" /* MESSAGE_SEND_FAILED */,
716
+ `Bulk operation failed fast after ${failed.length} failures`,
717
+ { totalItems: items.length, failedCount: failed.length }
718
+ );
719
+ }
720
+ } finally {
721
+ completed++;
722
+ opts.onProgress?.(completed, items.length, failed.length);
723
+ }
724
+ });
725
+ await Promise.allSettled(batchPromises);
726
+ if (opts.failFast && failed.length > 0) {
727
+ break;
728
+ }
729
+ }
730
+ const duration = Date.now() - startTime;
731
+ return {
732
+ successful,
733
+ failed,
734
+ summary: {
735
+ total: items.length,
736
+ successful: successful.length,
737
+ failed: failed.length,
738
+ duration
739
+ }
740
+ };
741
+ }
742
+ static createBatches(items, batchSize) {
743
+ const batches = [];
744
+ for (let i = 0; i < items.length; i += batchSize) {
745
+ batches.push(items.slice(i, i + batchSize));
746
+ }
747
+ return batches;
748
+ }
749
+ };
750
+ var RateLimiter = class {
751
+ constructor(maxRequests, windowMs) {
752
+ this.maxRequests = maxRequests;
753
+ this.windowMs = windowMs;
754
+ }
755
+ requests = [];
756
+ async acquire() {
757
+ const now = Date.now();
758
+ this.requests = this.requests.filter((time) => now - time < this.windowMs);
759
+ if (this.requests.length >= this.maxRequests) {
760
+ const oldestRequest = Math.min(...this.requests);
761
+ const waitTime = this.windowMs - (now - oldestRequest);
762
+ if (waitTime > 0) {
763
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
764
+ return this.acquire();
765
+ }
766
+ }
767
+ this.requests.push(now);
768
+ }
769
+ canMakeRequest() {
770
+ const now = Date.now();
771
+ this.requests = this.requests.filter((time) => now - time < this.windowMs);
772
+ return this.requests.length < this.maxRequests;
773
+ }
774
+ getRemainingRequests() {
775
+ const now = Date.now();
776
+ this.requests = this.requests.filter((time) => now - time < this.windowMs);
777
+ return Math.max(0, this.maxRequests - this.requests.length);
778
+ }
779
+ };
780
+ var HealthMonitor = class {
781
+ constructor(services, checkIntervalMs = 3e4) {
782
+ this.services = services;
783
+ this.checkInterval = checkIntervalMs;
784
+ }
785
+ healthStatus = /* @__PURE__ */ new Map();
786
+ lastCheck = /* @__PURE__ */ new Map();
787
+ checkInterval;
788
+ intervalId;
789
+ start() {
790
+ this.intervalId = setInterval(() => {
791
+ this.checkAllServices();
792
+ }, this.checkInterval);
793
+ this.checkAllServices();
794
+ }
795
+ stop() {
796
+ if (this.intervalId) {
797
+ clearInterval(this.intervalId);
798
+ this.intervalId = void 0;
799
+ }
800
+ }
801
+ async checkAllServices() {
802
+ const checks = Array.from(this.services.entries()).map(
803
+ async ([serviceName, healthCheck]) => {
804
+ try {
805
+ const isHealthy = await healthCheck();
806
+ const wasHealthy = this.healthStatus.get(serviceName);
807
+ this.healthStatus.set(serviceName, isHealthy);
808
+ this.lastCheck.set(serviceName, Date.now());
809
+ if (wasHealthy !== void 0 && wasHealthy !== isHealthy) {
810
+ console.log(`Service ${serviceName} status changed: ${wasHealthy ? "healthy" : "unhealthy"} -> ${isHealthy ? "healthy" : "unhealthy"}`);
811
+ }
812
+ } catch (error) {
813
+ this.healthStatus.set(serviceName, false);
814
+ this.lastCheck.set(serviceName, Date.now());
815
+ console.error(`Health check failed for ${serviceName}:`, error);
816
+ }
817
+ }
818
+ );
819
+ await Promise.allSettled(checks);
820
+ }
821
+ getServiceHealth(serviceName) {
822
+ return this.healthStatus.get(serviceName);
823
+ }
824
+ getAllHealth() {
825
+ return Object.fromEntries(this.healthStatus);
826
+ }
827
+ isServiceHealthy(serviceName) {
828
+ return this.healthStatus.get(serviceName) === true;
829
+ }
830
+ getLastCheckTime(serviceName) {
831
+ return this.lastCheck.get(serviceName);
832
+ }
833
+ };
834
+ var GracefulDegradation = class {
835
+ fallbackStrategies = /* @__PURE__ */ new Map();
836
+ registerFallback(operationName, fallbackFunction) {
837
+ this.fallbackStrategies.set(operationName, fallbackFunction);
838
+ }
839
+ async executeWithFallback(operationName, primaryOperation, options = {}) {
840
+ const timeout = options.timeout || 1e4;
841
+ try {
842
+ const result = await Promise.race([
843
+ RetryHandler.execute(primaryOperation, options.retryOptions),
844
+ new Promise(
845
+ (_, reject) => setTimeout(() => reject(
846
+ new KMessageError(
847
+ "NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */,
848
+ `Operation ${operationName} timed out`,
849
+ { operationName, timeout }
850
+ )
851
+ ), timeout)
852
+ )
853
+ ]);
854
+ return result;
855
+ } catch (error) {
856
+ console.warn(`Primary operation ${operationName} failed, attempting fallback:`, error);
857
+ const fallback = this.fallbackStrategies.get(operationName);
858
+ if (fallback) {
859
+ try {
860
+ return await fallback();
861
+ } catch (fallbackError) {
862
+ throw new KMessageError(
863
+ "UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
864
+ `Both primary and fallback operations failed for ${operationName}`,
865
+ {
866
+ operationName,
867
+ primaryError: error.message,
868
+ fallbackError: fallbackError.message
869
+ },
870
+ { cause: error }
871
+ );
872
+ }
873
+ }
874
+ throw error;
875
+ }
876
+ }
877
+ };
878
+ var ErrorRecovery = {
879
+ /**
880
+ * Create a resilient function that combines multiple recovery patterns
881
+ */
882
+ createResilientFunction(func, options = {}) {
883
+ let circuitBreaker;
884
+ let rateLimiter;
885
+ if (options.circuitBreaker) {
886
+ circuitBreaker = new CircuitBreaker(options.circuitBreaker);
887
+ }
888
+ if (options.rateLimiter) {
889
+ rateLimiter = new RateLimiter(
890
+ options.rateLimiter.maxRequests,
891
+ options.rateLimiter.windowMs
892
+ );
893
+ }
894
+ return async (...args) => {
895
+ if (rateLimiter) {
896
+ await rateLimiter.acquire();
897
+ }
898
+ const operation = async () => {
899
+ const wrappedFunc = async () => {
900
+ if (options.timeout) {
901
+ return Promise.race([
902
+ func(...args),
903
+ new Promise(
904
+ (_, reject) => setTimeout(() => reject(
905
+ new KMessageError(
906
+ "NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */,
907
+ "Operation timed out",
908
+ { timeout: options.timeout }
909
+ )
910
+ ), options.timeout)
911
+ )
912
+ ]);
913
+ }
914
+ return func(...args);
915
+ };
916
+ if (circuitBreaker) {
917
+ return circuitBreaker.execute(wrappedFunc);
918
+ }
919
+ return wrappedFunc();
920
+ };
921
+ try {
922
+ return await RetryHandler.execute(operation, options.retryOptions);
923
+ } catch (error) {
924
+ if (options.fallback) {
925
+ console.warn("Primary operation failed, using fallback:", error);
926
+ return options.fallback(...args);
927
+ }
928
+ throw error;
929
+ }
930
+ };
931
+ }
932
+ };
933
+
934
+ // src/index.ts
935
+ var TemplateCategory = /* @__PURE__ */ ((TemplateCategory2) => {
936
+ TemplateCategory2["AUTHENTICATION"] = "AUTHENTICATION";
937
+ TemplateCategory2["NOTIFICATION"] = "NOTIFICATION";
938
+ TemplateCategory2["PROMOTION"] = "PROMOTION";
939
+ TemplateCategory2["INFORMATION"] = "INFORMATION";
940
+ TemplateCategory2["RESERVATION"] = "RESERVATION";
941
+ TemplateCategory2["SHIPPING"] = "SHIPPING";
942
+ TemplateCategory2["PAYMENT"] = "PAYMENT";
943
+ return TemplateCategory2;
944
+ })(TemplateCategory || {});
945
+ export {
946
+ BulkOperationHandler,
947
+ CircuitBreaker,
948
+ ErrorFactory,
949
+ ErrorRecovery,
950
+ ErrorUtils,
951
+ GracefulDegradation,
952
+ HealthMonitor,
953
+ KMessageError,
954
+ KMessageErrorCode,
955
+ MessageError,
956
+ MockProvider,
957
+ PerformanceTest,
958
+ ProviderError,
959
+ RateLimiter,
960
+ Result,
961
+ RetryHandler,
962
+ TemplateCategory,
963
+ TemplateError,
964
+ TestAssertions,
965
+ TestData,
966
+ TestSetup
967
+ };
968
+ //# sourceMappingURL=index.js.map