@k-msg/core
Version:
Core types and interfaces for K-Message platform
968 lines (965 loc) • 31 kB
JavaScript
// src/errors.ts
var KMessageErrorCode = /* @__PURE__ */ ((KMessageErrorCode2) => {
KMessageErrorCode2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
KMessageErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
KMessageErrorCode2["CONFIGURATION_ERROR"] = "CONFIGURATION_ERROR";
KMessageErrorCode2["PROVIDER_NOT_FOUND"] = "PROVIDER_NOT_FOUND";
KMessageErrorCode2["PROVIDER_NOT_AVAILABLE"] = "PROVIDER_NOT_AVAILABLE";
KMessageErrorCode2["PROVIDER_AUTHENTICATION_FAILED"] = "PROVIDER_AUTHENTICATION_FAILED";
KMessageErrorCode2["PROVIDER_CONNECTION_FAILED"] = "PROVIDER_CONNECTION_FAILED";
KMessageErrorCode2["PROVIDER_RATE_LIMITED"] = "PROVIDER_RATE_LIMITED";
KMessageErrorCode2["PROVIDER_INSUFFICIENT_BALANCE"] = "PROVIDER_INSUFFICIENT_BALANCE";
KMessageErrorCode2["TEMPLATE_NOT_FOUND"] = "TEMPLATE_NOT_FOUND";
KMessageErrorCode2["TEMPLATE_VALIDATION_FAILED"] = "TEMPLATE_VALIDATION_FAILED";
KMessageErrorCode2["TEMPLATE_CREATION_FAILED"] = "TEMPLATE_CREATION_FAILED";
KMessageErrorCode2["TEMPLATE_MODIFICATION_FAILED"] = "TEMPLATE_MODIFICATION_FAILED";
KMessageErrorCode2["TEMPLATE_DELETION_FAILED"] = "TEMPLATE_DELETION_FAILED";
KMessageErrorCode2["MESSAGE_SEND_FAILED"] = "MESSAGE_SEND_FAILED";
KMessageErrorCode2["MESSAGE_INVALID_PHONE_NUMBER"] = "MESSAGE_INVALID_PHONE_NUMBER";
KMessageErrorCode2["MESSAGE_INVALID_VARIABLES"] = "MESSAGE_INVALID_VARIABLES";
KMessageErrorCode2["MESSAGE_QUOTA_EXCEEDED"] = "MESSAGE_QUOTA_EXCEEDED";
KMessageErrorCode2["MESSAGE_RESERVATION_FAILED"] = "MESSAGE_RESERVATION_FAILED";
KMessageErrorCode2["MESSAGE_CANCELLATION_FAILED"] = "MESSAGE_CANCELLATION_FAILED";
KMessageErrorCode2["NETWORK_TIMEOUT"] = "NETWORK_TIMEOUT";
KMessageErrorCode2["NETWORK_CONNECTION_FAILED"] = "NETWORK_CONNECTION_FAILED";
KMessageErrorCode2["NETWORK_SERVICE_UNAVAILABLE"] = "NETWORK_SERVICE_UNAVAILABLE";
KMessageErrorCode2["API_INVALID_REQUEST"] = "API_INVALID_REQUEST";
KMessageErrorCode2["API_UNAUTHORIZED"] = "API_UNAUTHORIZED";
KMessageErrorCode2["API_FORBIDDEN"] = "API_FORBIDDEN";
KMessageErrorCode2["API_NOT_FOUND"] = "API_NOT_FOUND";
KMessageErrorCode2["API_TOO_MANY_REQUESTS"] = "API_TOO_MANY_REQUESTS";
KMessageErrorCode2["API_INTERNAL_SERVER_ERROR"] = "API_INTERNAL_SERVER_ERROR";
return KMessageErrorCode2;
})(KMessageErrorCode || {});
var KMessageError = class _KMessageError extends Error {
code;
context;
retryable;
statusCode;
cause;
constructor(code, message, context = {}, options = {}) {
super(message);
this.name = "KMessageError";
this.code = code;
this.context = {
...context,
timestamp: context.timestamp || /* @__PURE__ */ new Date()
};
this.retryable = options.retryable ?? this.isRetryableByDefault(code);
this.statusCode = options.statusCode;
this.cause = options.cause;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, _KMessageError);
}
}
isRetryableByDefault(code) {
const retryableCodes = [
"NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */,
"NETWORK_CONNECTION_FAILED" /* NETWORK_CONNECTION_FAILED */,
"NETWORK_SERVICE_UNAVAILABLE" /* NETWORK_SERVICE_UNAVAILABLE */,
"PROVIDER_CONNECTION_FAILED" /* PROVIDER_CONNECTION_FAILED */,
"PROVIDER_RATE_LIMITED" /* PROVIDER_RATE_LIMITED */,
"API_TOO_MANY_REQUESTS" /* API_TOO_MANY_REQUESTS */,
"API_INTERNAL_SERVER_ERROR" /* API_INTERNAL_SERVER_ERROR */
];
return retryableCodes.includes(code);
}
toJSON() {
return {
name: this.name,
code: this.code,
message: this.message,
context: this.context,
retryable: this.retryable,
statusCode: this.statusCode,
stack: this.stack
};
}
};
var ProviderError = class extends KMessageError {
constructor(providerId, code, message, context = {}, options = {}) {
super(code, message, { ...context, providerId }, options);
this.name = "ProviderError";
}
};
var TemplateError = class extends KMessageError {
constructor(templateCode, code, message, context = {}, options = {}) {
super(code, message, { ...context, templateCode }, options);
this.name = "TemplateError";
}
};
var MessageError = class extends KMessageError {
constructor(phoneNumber, code, message, context = {}, options = {}) {
super(code, message, { ...context, phoneNumber }, options);
this.name = "MessageError";
}
};
var ErrorFactory = {
providerNotFound: (providerId) => new ProviderError(
providerId,
"PROVIDER_NOT_FOUND" /* PROVIDER_NOT_FOUND */,
`Provider '${providerId}' not found`,
{},
{ retryable: false, statusCode: 404 }
),
providerNotAvailable: (providerId, reason) => new ProviderError(
providerId,
"PROVIDER_NOT_AVAILABLE" /* PROVIDER_NOT_AVAILABLE */,
`Provider '${providerId}' is not available${reason ? `: ${reason}` : ""}`,
{},
{ retryable: true, statusCode: 503 }
),
authenticationFailed: (providerId, details) => new ProviderError(
providerId,
"PROVIDER_AUTHENTICATION_FAILED" /* PROVIDER_AUTHENTICATION_FAILED */,
`Authentication failed for provider '${providerId}'${details ? `: ${details}` : ""}`,
{},
{ retryable: false, statusCode: 401 }
),
templateNotFound: (templateCode) => new TemplateError(
templateCode,
"TEMPLATE_NOT_FOUND" /* TEMPLATE_NOT_FOUND */,
`Template '${templateCode}' not found`,
{},
{ retryable: false, statusCode: 404 }
),
invalidPhoneNumber: (phoneNumber) => new MessageError(
phoneNumber,
"MESSAGE_INVALID_PHONE_NUMBER" /* MESSAGE_INVALID_PHONE_NUMBER */,
`Invalid phone number format: ${phoneNumber}`,
{},
{ retryable: false, statusCode: 400 }
),
networkTimeout: (providerId, timeout) => new KMessageError(
"NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */,
`Network request timed out${timeout ? ` after ${timeout}ms` : ""}`,
{ providerId },
{ retryable: true, statusCode: 408 }
),
rateLimited: (providerId, retryAfter) => new ProviderError(
providerId,
"PROVIDER_RATE_LIMITED" /* PROVIDER_RATE_LIMITED */,
`Rate limit exceeded for provider '${providerId}'${retryAfter ? `. Retry after ${retryAfter}s` : ""}`,
{ retryAfter },
{ retryable: true, statusCode: 429 }
),
insufficientBalance: (providerId, balance) => new ProviderError(
providerId,
"PROVIDER_INSUFFICIENT_BALANCE" /* PROVIDER_INSUFFICIENT_BALANCE */,
`Insufficient balance for provider '${providerId}'${balance ? `. Current balance: ${balance}` : ""}`,
{ balance },
{ retryable: false, statusCode: 402 }
),
fromHttpStatus: (statusCode, message, context = {}) => {
let code;
let retryable = false;
switch (statusCode) {
case 400:
code = "API_INVALID_REQUEST" /* API_INVALID_REQUEST */;
break;
case 401:
code = "API_UNAUTHORIZED" /* API_UNAUTHORIZED */;
break;
case 403:
code = "API_FORBIDDEN" /* API_FORBIDDEN */;
break;
case 404:
code = "API_NOT_FOUND" /* API_NOT_FOUND */;
break;
case 408:
code = "NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */;
retryable = true;
break;
case 429:
code = "API_TOO_MANY_REQUESTS" /* API_TOO_MANY_REQUESTS */;
retryable = true;
break;
case 500:
case 502:
case 503:
case 504:
code = "API_INTERNAL_SERVER_ERROR" /* API_INTERNAL_SERVER_ERROR */;
retryable = true;
break;
default:
code = "UNKNOWN_ERROR" /* UNKNOWN_ERROR */;
retryable = statusCode >= 500 && statusCode < 600;
}
return new KMessageError(code, message, context, { retryable, statusCode });
}
};
var Result = {
success: (data) => ({ success: true, data }),
failure: (error) => ({ success: false, error }),
fromPromise: async (promise) => {
try {
const data = await promise;
return Result.success(data);
} catch (error) {
if (error instanceof KMessageError) {
return Result.failure(error);
}
return Result.failure(new KMessageError(
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
error instanceof Error ? error.message : "Unknown error occurred",
{},
{ cause: error instanceof Error ? error : void 0 }
));
}
}
};
var ErrorUtils = {
isRetryable: (error) => {
if (error instanceof KMessageError) {
return error.retryable;
}
const retryableMessages = ["timeout", "ECONNRESET", "ECONNREFUSED", "ETIMEDOUT"];
return retryableMessages.some(
(msg) => error.message.toLowerCase().includes(msg.toLowerCase())
);
},
getStatusCode: (error) => {
if (error instanceof KMessageError && error.statusCode) {
return error.statusCode;
}
return 500;
},
formatErrorForClient: (error) => {
if (error instanceof KMessageError) {
return {
code: error.code,
message: error.message,
retryable: error.retryable,
context: error.context
};
}
return {
code: "UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
message: error.message || "Unknown error occurred",
retryable: false,
context: {}
};
},
formatErrorForLogging: (error) => {
const baseInfo = {
name: error.name,
message: error.message,
stack: error.stack
};
if (error instanceof KMessageError) {
return {
...baseInfo,
code: error.code,
context: error.context,
retryable: error.retryable,
statusCode: error.statusCode
};
}
return baseInfo;
}
};
// src/test-utils.ts
import { expect } from "bun:test";
var MockProvider = class {
id;
name;
_healthy = true;
_issues = [];
_balance = "1000";
_templates = [];
_history = [];
constructor(id = "mock", name = "Mock Provider") {
this.id = id;
this.name = name;
}
// Health check simulation
async healthCheck() {
return {
healthy: this._healthy,
issues: [...this._issues],
data: {
balance: this._balance,
status: this._healthy ? "connected" : "disconnected",
code: this._healthy ? 200 : 500,
message: this._healthy ? "OK" : "Service unavailable"
}
};
}
// Test helpers
setHealthy(healthy, issues = []) {
this._healthy = healthy;
this._issues = issues;
}
setBalance(balance) {
this._balance = balance;
}
setTemplates(templates) {
this._templates = templates;
}
setHistory(history) {
this._history = history;
}
// Provider methods with mock implementations
async sendMessage(templateCode, phoneNumber, variables, options) {
if (!this._healthy) {
throw new MessageError(phoneNumber, "MESSAGE_SEND_FAILED" /* MESSAGE_SEND_FAILED */, "Provider is unhealthy");
}
if (phoneNumber === "01000000000") {
throw new MessageError(phoneNumber, "MESSAGE_INVALID_PHONE_NUMBER" /* MESSAGE_INVALID_PHONE_NUMBER */, "Invalid phone number");
}
return {
success: true,
messageId: `mock_${Date.now()}`,
status: "sent",
error: null
};
}
async getTemplates(page = 1, size = 15, filters) {
if (!this._healthy) {
throw new ProviderError(this.id, "PROVIDER_NOT_AVAILABLE" /* PROVIDER_NOT_AVAILABLE */, "Provider is unhealthy");
}
const start = (page - 1) * size;
const end = start + size;
const list = this._templates.slice(start, end);
return {
code: 200,
message: "Success",
totalCount: this._templates.length,
list
};
}
async createTemplate(name, content, category, buttons) {
if (!this._healthy) {
throw new TemplateError(name, "TEMPLATE_CREATION_FAILED" /* TEMPLATE_CREATION_FAILED */, "Provider is unhealthy");
}
if (name === "invalid_template") {
throw new TemplateError(name, "TEMPLATE_VALIDATION_FAILED" /* TEMPLATE_VALIDATION_FAILED */, "Template validation failed");
}
const templateCode = `mock_${name}_${Date.now()}`;
this._templates.push({
templateCode,
templateName: name,
templateContent: content,
status: "Y",
createDate: (/* @__PURE__ */ new Date()).toISOString()
});
return {
success: true,
templateCode,
status: "created",
error: null
};
}
async modifyTemplate(templateCode, name, content, buttons) {
if (!this._healthy) {
throw new TemplateError(templateCode, "TEMPLATE_MODIFICATION_FAILED" /* TEMPLATE_MODIFICATION_FAILED */, "Provider is unhealthy");
}
const template = this._templates.find((t) => t.templateCode === templateCode);
if (!template) {
throw new TemplateError(templateCode, "TEMPLATE_NOT_FOUND" /* TEMPLATE_NOT_FOUND */, "Template not found");
}
template.templateName = name;
template.templateContent = content;
return {
success: true,
templateCode,
status: "modified",
error: null
};
}
async deleteTemplate(templateCode) {
if (!this._healthy) {
throw new TemplateError(templateCode, "TEMPLATE_DELETION_FAILED" /* TEMPLATE_DELETION_FAILED */, "Provider is unhealthy");
}
const index = this._templates.findIndex((t) => t.templateCode === templateCode);
if (index === -1) {
return {
code: 404,
message: "Template not found"
};
}
this._templates.splice(index, 1);
return {
code: 200,
message: "Template deleted successfully"
};
}
async getHistory(page = 1, size = 15, filters) {
if (!this._healthy) {
throw new ProviderError(this.id, "PROVIDER_NOT_AVAILABLE" /* PROVIDER_NOT_AVAILABLE */, "Provider is unhealthy");
}
const start = (page - 1) * size;
const end = start + size;
const list = this._history.slice(start, end);
return {
code: 200,
message: "Success",
totalCount: this._history.length,
list
};
}
async cancelReservation(messageId) {
if (!this._healthy) {
throw new KMessageError("MESSAGE_CANCELLATION_FAILED" /* MESSAGE_CANCELLATION_FAILED */, "Provider is unhealthy");
}
return {
code: 200,
message: "Reservation cancelled successfully"
};
}
};
var TestAssertions = {
/**
* Assert that an error is a KMessageError with specific code
*/
assertKMessageError: (error, expectedCode, expectedMessage) => {
expect(error).toBeInstanceOf(KMessageError);
const kError = error;
expect(kError.code).toBe(expectedCode);
if (expectedMessage) {
expect(kError.message).toContain(expectedMessage);
}
},
/**
* Assert that an error is retryable
*/
assertRetryable: (error, expected = true) => {
expect(error.retryable).toBe(expected);
},
/**
* Assert that a health status is healthy
*/
assertHealthy: (health, expected = true) => {
expect(health.healthy).toBe(expected);
if (!expected) {
expect(health.issues.length).toBeGreaterThan(0);
}
},
/**
* Assert that a provider result has expected structure
*/
assertProviderResult: (result, expectSuccess = true) => {
expect(result).toHaveProperty("success");
expect(result.success).toBe(expectSuccess);
if (expectSuccess) {
expect(result.error).toBeNull();
} else {
expect(result.error).toBeDefined();
}
},
/**
* Assert that API response has expected structure
*/
assertApiResponse: (response, expectedCode = 200) => {
expect(response).toHaveProperty("code");
expect(response).toHaveProperty("message");
expect(response.code).toBe(expectedCode);
}
};
var TestData = {
createMockTemplate: (overrides = {}) => ({
templateCode: "mock_template_001",
templateName: "Mock Template",
templateContent: "[#{\uC11C\uBE44\uC2A4\uBA85}] \uC548\uB155\uD558\uC138\uC694, #{\uACE0\uAC1D\uBA85}\uB2D8!",
status: "Y",
createDate: "2024-01-01 12:00:00",
...overrides
}),
createMockMessage: (overrides = {}) => ({
seqNo: 12345,
phone: "01012345678",
templateCode: "mock_template_001",
statusCode: "OK",
statusCodeName: "\uC131\uACF5",
requestDate: "2024-01-01 12:00:00",
sendDate: "2024-01-01 12:01:00",
receiveDate: "2024-01-01 12:01:30",
sendMessage: "[MyApp] \uC548\uB155\uD558\uC138\uC694, \uD64D\uAE38\uB3D9\uB2D8!",
...overrides
}),
createMockVariables: (overrides = {}) => ({
\uC11C\uBE44\uC2A4\uBA85: "MyApp",
\uACE0\uAC1D\uBA85: "\uD64D\uAE38\uB3D9",
\uC778\uC99D\uCF54\uB4DC: "123456",
...overrides
}),
generatePhoneNumber: (valid = true) => {
if (valid) {
const numbers = ["010", "011", "016", "017", "018", "019"];
const prefix = numbers[Math.floor(Math.random() * numbers.length)];
const suffix = Math.floor(Math.random() * 1e8).toString().padStart(8, "0");
return prefix + suffix;
} else {
return "01000000000";
}
}
};
var TestSetup = {
/**
* Create a test environment with mock providers
*/
createTestEnvironment: () => {
const mockProviders = {
healthy: new MockProvider("healthy", "Healthy Provider"),
unhealthy: new MockProvider("unhealthy", "Unhealthy Provider"),
rateLimited: new MockProvider("ratelimited", "Rate Limited Provider")
};
mockProviders.unhealthy.setHealthy(false, ["Connection failed", "Authentication error"]);
mockProviders.rateLimited.setHealthy(true);
return {
providers: mockProviders,
cleanup: () => {
}
};
},
/**
* Create test data for various scenarios
*/
createTestScenarios: () => ({
validMessage: {
templateCode: "valid_template",
phoneNumber: TestData.generatePhoneNumber(true),
variables: TestData.createMockVariables()
},
invalidMessage: {
templateCode: "invalid_template",
phoneNumber: TestData.generatePhoneNumber(false),
variables: {}
},
templates: [
TestData.createMockTemplate({ templateCode: "template_001", templateName: "Welcome Message" }),
TestData.createMockTemplate({ templateCode: "template_002", templateName: "OTP Message" }),
TestData.createMockTemplate({ templateCode: "template_003", templateName: "Notification" })
],
history: [
TestData.createMockMessage({ seqNo: 1, templateCode: "template_001" }),
TestData.createMockMessage({ seqNo: 2, templateCode: "template_002" }),
TestData.createMockMessage({ seqNo: 3, templateCode: "template_003" })
]
})
};
var PerformanceTest = {
/**
* Measure execution time of a function
*/
measureTime: async (fn) => {
const start = performance.now();
const result = await fn();
const duration = performance.now() - start;
return { result, duration };
},
/**
* Run a function multiple times and get statistics
*/
benchmark: async (fn, iterations = 10) => {
const results = [];
const durations = [];
for (let i = 0; i < iterations; i++) {
const { result, duration } = await PerformanceTest.measureTime(fn);
results.push(result);
durations.push(duration);
}
durations.sort((a, b) => a - b);
const statistics = {
min: durations[0],
max: durations[durations.length - 1],
average: durations.reduce((sum, d) => sum + d, 0) / durations.length,
median: durations[Math.floor(durations.length / 2)]
};
return { results, statistics };
}
};
// src/retry.ts
var RetryHandler = class {
static defaultOptions = {
maxAttempts: 3,
initialDelay: 1e3,
maxDelay: 3e4,
backoffMultiplier: 2,
jitter: true,
retryCondition: (error) => ErrorUtils.isRetryable(error)
};
static async execute(operation, options = {}) {
const opts = { ...this.defaultOptions, ...options };
let lastError;
let delay = opts.initialDelay;
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === opts.maxAttempts || !opts.retryCondition(lastError, attempt)) {
throw lastError;
}
const actualDelay = opts.jitter ? delay + Math.random() * delay * 0.1 : delay;
opts.onRetry?.(lastError, attempt);
await new Promise((resolve) => setTimeout(resolve, actualDelay));
delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelay);
}
}
throw lastError;
}
static createRetryableFunction(func, options = {}) {
return async (...args) => {
return this.execute(() => func(...args), options);
};
}
};
var CircuitBreaker = class {
constructor(options) {
this.options = options;
}
state = "CLOSED";
failureCount = 0;
lastFailureTime = 0;
nextAttemptTime = 0;
async execute(operation) {
const now = Date.now();
switch (this.state) {
case "OPEN":
if (now < this.nextAttemptTime) {
throw new KMessageError(
"NETWORK_SERVICE_UNAVAILABLE" /* NETWORK_SERVICE_UNAVAILABLE */,
"Circuit breaker is OPEN",
{ state: this.state, nextAttemptTime: this.nextAttemptTime }
);
}
this.state = "HALF_OPEN";
this.options.onHalfOpen?.();
break;
case "HALF_OPEN":
break;
case "CLOSED":
break;
}
try {
const result = await Promise.race([
operation(),
new Promise(
(_, reject) => setTimeout(() => reject(
new KMessageError(
"NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */,
"Circuit breaker timeout",
{ timeout: this.options.timeout }
)
), this.options.timeout)
)
]);
if (this.state === "HALF_OPEN") {
this.state = "CLOSED";
this.failureCount = 0;
this.options.onClose?.();
}
return result;
} catch (error) {
this.recordFailure();
throw error;
}
}
recordFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.options.failureThreshold) {
this.state = "OPEN";
this.nextAttemptTime = this.lastFailureTime + this.options.resetTimeout;
this.options.onOpen?.();
}
}
getState() {
return this.state;
}
getFailureCount() {
return this.failureCount;
}
reset() {
this.state = "CLOSED";
this.failureCount = 0;
this.lastFailureTime = 0;
this.nextAttemptTime = 0;
}
};
var BulkOperationHandler = class {
static async execute(items, operation, options = {}) {
const opts = {
concurrency: 5,
retryOptions: {
maxAttempts: 3,
initialDelay: 1e3,
maxDelay: 1e4,
backoffMultiplier: 2,
jitter: true
},
failFast: false,
...options
};
const startTime = Date.now();
const successful = [];
const failed = [];
let completed = 0;
const retryableOperation = RetryHandler.createRetryableFunction(
operation,
opts.retryOptions
);
const batches = this.createBatches(items, opts.concurrency);
for (const batch of batches) {
const batchPromises = batch.map(async (item) => {
try {
const result = await retryableOperation(item);
successful.push({ item, result });
} catch (error) {
failed.push({ item, error });
if (opts.failFast) {
throw new KMessageError(
"MESSAGE_SEND_FAILED" /* MESSAGE_SEND_FAILED */,
`Bulk operation failed fast after ${failed.length} failures`,
{ totalItems: items.length, failedCount: failed.length }
);
}
} finally {
completed++;
opts.onProgress?.(completed, items.length, failed.length);
}
});
await Promise.allSettled(batchPromises);
if (opts.failFast && failed.length > 0) {
break;
}
}
const duration = Date.now() - startTime;
return {
successful,
failed,
summary: {
total: items.length,
successful: successful.length,
failed: failed.length,
duration
}
};
}
static createBatches(items, batchSize) {
const batches = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
return batches;
}
};
var RateLimiter = class {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
}
requests = [];
async acquire() {
const now = Date.now();
this.requests = this.requests.filter((time) => now - time < this.windowMs);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = Math.min(...this.requests);
const waitTime = this.windowMs - (now - oldestRequest);
if (waitTime > 0) {
await new Promise((resolve) => setTimeout(resolve, waitTime));
return this.acquire();
}
}
this.requests.push(now);
}
canMakeRequest() {
const now = Date.now();
this.requests = this.requests.filter((time) => now - time < this.windowMs);
return this.requests.length < this.maxRequests;
}
getRemainingRequests() {
const now = Date.now();
this.requests = this.requests.filter((time) => now - time < this.windowMs);
return Math.max(0, this.maxRequests - this.requests.length);
}
};
var HealthMonitor = class {
constructor(services, checkIntervalMs = 3e4) {
this.services = services;
this.checkInterval = checkIntervalMs;
}
healthStatus = /* @__PURE__ */ new Map();
lastCheck = /* @__PURE__ */ new Map();
checkInterval;
intervalId;
start() {
this.intervalId = setInterval(() => {
this.checkAllServices();
}, this.checkInterval);
this.checkAllServices();
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = void 0;
}
}
async checkAllServices() {
const checks = Array.from(this.services.entries()).map(
async ([serviceName, healthCheck]) => {
try {
const isHealthy = await healthCheck();
const wasHealthy = this.healthStatus.get(serviceName);
this.healthStatus.set(serviceName, isHealthy);
this.lastCheck.set(serviceName, Date.now());
if (wasHealthy !== void 0 && wasHealthy !== isHealthy) {
console.log(`Service ${serviceName} status changed: ${wasHealthy ? "healthy" : "unhealthy"} -> ${isHealthy ? "healthy" : "unhealthy"}`);
}
} catch (error) {
this.healthStatus.set(serviceName, false);
this.lastCheck.set(serviceName, Date.now());
console.error(`Health check failed for ${serviceName}:`, error);
}
}
);
await Promise.allSettled(checks);
}
getServiceHealth(serviceName) {
return this.healthStatus.get(serviceName);
}
getAllHealth() {
return Object.fromEntries(this.healthStatus);
}
isServiceHealthy(serviceName) {
return this.healthStatus.get(serviceName) === true;
}
getLastCheckTime(serviceName) {
return this.lastCheck.get(serviceName);
}
};
var GracefulDegradation = class {
fallbackStrategies = /* @__PURE__ */ new Map();
registerFallback(operationName, fallbackFunction) {
this.fallbackStrategies.set(operationName, fallbackFunction);
}
async executeWithFallback(operationName, primaryOperation, options = {}) {
const timeout = options.timeout || 1e4;
try {
const result = await Promise.race([
RetryHandler.execute(primaryOperation, options.retryOptions),
new Promise(
(_, reject) => setTimeout(() => reject(
new KMessageError(
"NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */,
`Operation ${operationName} timed out`,
{ operationName, timeout }
)
), timeout)
)
]);
return result;
} catch (error) {
console.warn(`Primary operation ${operationName} failed, attempting fallback:`, error);
const fallback = this.fallbackStrategies.get(operationName);
if (fallback) {
try {
return await fallback();
} catch (fallbackError) {
throw new KMessageError(
"UNKNOWN_ERROR" /* UNKNOWN_ERROR */,
`Both primary and fallback operations failed for ${operationName}`,
{
operationName,
primaryError: error.message,
fallbackError: fallbackError.message
},
{ cause: error }
);
}
}
throw error;
}
}
};
var ErrorRecovery = {
/**
* Create a resilient function that combines multiple recovery patterns
*/
createResilientFunction(func, options = {}) {
let circuitBreaker;
let rateLimiter;
if (options.circuitBreaker) {
circuitBreaker = new CircuitBreaker(options.circuitBreaker);
}
if (options.rateLimiter) {
rateLimiter = new RateLimiter(
options.rateLimiter.maxRequests,
options.rateLimiter.windowMs
);
}
return async (...args) => {
if (rateLimiter) {
await rateLimiter.acquire();
}
const operation = async () => {
const wrappedFunc = async () => {
if (options.timeout) {
return Promise.race([
func(...args),
new Promise(
(_, reject) => setTimeout(() => reject(
new KMessageError(
"NETWORK_TIMEOUT" /* NETWORK_TIMEOUT */,
"Operation timed out",
{ timeout: options.timeout }
)
), options.timeout)
)
]);
}
return func(...args);
};
if (circuitBreaker) {
return circuitBreaker.execute(wrappedFunc);
}
return wrappedFunc();
};
try {
return await RetryHandler.execute(operation, options.retryOptions);
} catch (error) {
if (options.fallback) {
console.warn("Primary operation failed, using fallback:", error);
return options.fallback(...args);
}
throw error;
}
};
}
};
// src/index.ts
var TemplateCategory = /* @__PURE__ */ ((TemplateCategory2) => {
TemplateCategory2["AUTHENTICATION"] = "AUTHENTICATION";
TemplateCategory2["NOTIFICATION"] = "NOTIFICATION";
TemplateCategory2["PROMOTION"] = "PROMOTION";
TemplateCategory2["INFORMATION"] = "INFORMATION";
TemplateCategory2["RESERVATION"] = "RESERVATION";
TemplateCategory2["SHIPPING"] = "SHIPPING";
TemplateCategory2["PAYMENT"] = "PAYMENT";
return TemplateCategory2;
})(TemplateCategory || {});
export {
BulkOperationHandler,
CircuitBreaker,
ErrorFactory,
ErrorRecovery,
ErrorUtils,
GracefulDegradation,
HealthMonitor,
KMessageError,
KMessageErrorCode,
MessageError,
MockProvider,
PerformanceTest,
ProviderError,
RateLimiter,
Result,
RetryHandler,
TemplateCategory,
TemplateError,
TestAssertions,
TestData,
TestSetup
};
//# sourceMappingURL=index.js.map