mnotify-ts-sdk
Version:
Modern, zero-dependency TypeScript SDK for mNotify BMS API - Type-safe SMS, contacts, and account management with Railway-Oriented Programming
173 lines • 7.41 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpClient = void 0;
const MNotifyError_1 = require("../errors/MNotifyError");
const Result_1 = require("../types/Result");
/**
* Core HTTP client for mNotify API communication using native fetch
*
* @remarks
* This class handles all low-level HTTP requests to the mNotify API,
* including authentication, retries, and error handling.
*/
class HttpClient {
/**
* Creates a new HttpClient instance
* @param config - Configuration object
* @param config.apiKey - Your mNotify API key
* @param config.baseUrl - Base API URL (default: 'https://api.mnotify.com/api')
* @param config.timeout - Request timeout in ms (default: 10000)
* @param config.maxRetries - Maximum retry attempts for failed requests (default: 3)
*/
constructor(config) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || "https://api.mnotify.com/api";
this.timeout = config.timeout || 10000;
this.maxRetries = config.maxRetries || 3;
}
/**
* Makes an HTTP request to the mNotify API with Result type (railway-oriented programming)
* @param config - Request configuration
* @param retryCount - Current retry attempt (used internally)
* @returns Promise with Result containing either the response data or an error
*
* @example
* ```typescript
* const client = new HttpClient({ apiKey: 'your-key' });
* const result = await client.requestSafe<T>({
* method: 'GET',
* url: '/account/balance'
* });
*
* if (result.isOk()) {
* console.log(result.value);
* } else {
* console.error(result.error);
* }
* ```
*/
requestSafe(config_1) {
return __awaiter(this, arguments, void 0, function* (config, retryCount = 0) {
const url = this.buildUrl(config.url, config.params);
const requestContext = {
service: "HttpClient",
operation: "requestSafe",
stage: "request",
method: config.method,
path: config.url,
url: this.redactUrl(url),
retryCount,
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = yield fetch(url, {
method: config.method,
headers: {
Authorization: this.apiKey,
"Content-Type": "application/json",
Accept: "application/json",
},
body: config.data ? JSON.stringify(config.data) : undefined,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
// Handle rate limiting with retry
if (response.status === 429 && retryCount < this.maxRetries) {
const retryAfter = parseInt(response.headers.get("retry-after") || "1") * 1000;
yield this.sleep(retryAfter);
return this.requestSafe(config, retryCount + 1);
}
const errorData = yield response.json().catch(() => ({}));
return (0, Result_1.err)(new MNotifyError_1.MNotifyError(errorData.message || response.statusText, response.status, errorData, Object.assign(Object.assign({}, requestContext), { stage: "response" })));
}
const data = (yield response.json());
return (0, Result_1.ok)(data);
}
catch (error) {
clearTimeout(timeoutId);
if (error instanceof MNotifyError_1.MNotifyError) {
return (0, Result_1.err)(error.withContext(Object.assign(Object.assign({}, requestContext), { stage: "network" })));
}
// Handle timeout
if (error instanceof Error && error.name === "AbortError") {
return (0, Result_1.err)(MNotifyError_1.MNotifyError.fromUnknown(error, "Request timeout", 408, Object.assign(Object.assign({}, requestContext), { stage: "network" })));
}
// Handle network errors
return (0, Result_1.err)(MNotifyError_1.MNotifyError.fromUnknown(error, "Network error", 0, Object.assign(Object.assign({}, requestContext), { stage: "network" })));
}
});
}
/**
* Makes an HTTP request to the mNotify API (throws on error - legacy API)
* @param config - Request configuration
* @param retryCount - Current retry attempt (used internally)
* @returns Promise with the parsed response data
* @throws {MNotifyError} When API returns an error response
*
* @example
* ```typescript
* const client = new HttpClient({ apiKey: 'your-key' });
* const response = await client.request<T>({
* method: 'GET',
* url: '/account/balance'
* });
* ```
*/
request(config_1) {
return __awaiter(this, arguments, void 0, function* (config, retryCount = 0) {
const result = yield this.requestSafe(config, retryCount);
return result.unwrap();
});
}
/**
* Builds the full URL with query parameters
*/
buildUrl(path, params) {
// Ensure API base paths like ".../api" are preserved even when path starts with "/"
const normalizedBase = this.baseUrl.endsWith("/")
? this.baseUrl
: `${this.baseUrl}/`;
const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
const url = new URL(normalizedPath, normalizedBase);
// Always add API key as query param
url.searchParams.set("key", this.apiKey);
// Add additional params if provided
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
}
return url.toString();
}
redactUrl(rawUrl) {
try {
const url = new URL(rawUrl);
if (url.searchParams.has("key")) {
url.searchParams.set("key", "***");
}
return url.toString();
}
catch (_a) {
return rawUrl;
}
}
/**
* Sleep utility for retry logic
*/
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
exports.HttpClient = HttpClient;
//# sourceMappingURL=HttpClient.js.map