Advanced Features (v2.0.0)
Learn about polling, progress tracking, timeout features, memory management, and cleanup in hmm-api v2.0.0.
Advanced Features in hmm-api v2.0.0
Polling - Automatic Data Fetching
Easily implement polling without manual interval management. Perfect for real-time data updates.
Basic Polling
import ApiClient from "hmm-api";
const api = new ApiClient({
baseUrl: "https://api.example.com",
});
// Poll for job status every 2 seconds using GET
const result = await api.get("/job/status/123", {
poll: {
interval: 2000, // Poll every 2 seconds
maxAttempts: 10, // Stop after 10 attempts
stopCondition: (response) => {
// Stop when job is complete
return response.success && response.data.status === "completed";
},
onPollSuccess: (response, attempt) => {
console.log(`Attempt ${attempt}: ${response.data.status}`);
},
},
});
// TypeScript knows this is a PollingResult<T> because poll option is present
console.log(`Job completed after ${result.attempts} attempts`);
console.log(`Poll ID: ${result.pollId}`); // Auto-generated poll ID
console.log("Final data:", result.finalResponse.data);
// For regular requests without polling
const regularResult = await api.get("/users/123");
// TypeScript knows this is ApiResponse<T>
if (regularResult.success) {
console.log("User data:", regularResult.data);
}
Custom Poll ID Management
Assign custom IDs to polling operations for better control and management:
const api = new ApiClient({
baseUrl: "https://api.example.com",
});
// Start polling with custom ID
const result = await api.get("/job/status/123", {
poll: {
interval: 2000,
maxAttempts: 10,
pollId: "job-status-123", // Custom poll ID
stopCondition: (response) => {
return response.success && response.data.status === "completed";
},
onPollSuccess: (response, attempt) => {
console.log(`Job 123 - Attempt ${attempt}: ${response.data.status}`);
},
},
});
// Access the custom poll ID
if ("pollId" in result) {
console.log(`Custom Poll ID: ${result.pollId}`); // "job-status-123"
}
// Stop specific polling operation by ID
const stopped = api.stopPolling("job-status-123");
console.log(`Polling stopped: ${stopped}`); // true if successful
// Check active polling count
console.log(`Active polls: ${api.getActivePollingCount()}`);
Real-World Polling Examples
File Processing Status with Custom ID
const checkProcessingStatus = async (fileId: string) => {
const customPollId = `file-processing-${fileId}`;
const result = await api.get(`/files/${fileId}/status`, {
poll: {
interval: 3000,
maxAttempts: 20,
pollId: customPollId, // Custom ID for this specific file
stopCondition: (response) => {
const status = response.data?.status;
return status === "completed" || status === "failed";
},
onPollSuccess: (response, attempt) => {
updateProgressBar(response.data.progress);
console.log(`File ${fileId} - Processing: ${response.data.progress}%`);
},
},
});
if ("finalResponse" in result) {
console.log(`File processing completed with poll ID: ${result.pollId}`);
if (result.finalResponse.data.status === "completed") {
showSuccessMessage("File processed successfully!");
}
}
};
// Stop specific file processing if needed
const cancelFileProcessing = (fileId: string) => {
const pollId = `file-processing-${fileId}`;
const stopped = api.stopPolling(pollId);
if (stopped) {
console.log(`Cancelled processing for file ${fileId}`);
}
};
Order Status Tracking with Management
const trackOrder = async (orderId: string) => {
const orderPollId = `order-tracking-${orderId}`;
const result = await api.get(`/orders/${orderId}`, {
poll: {
interval: 5000, // Check every 5 seconds
pollId: orderPollId,
stopCondition: (response) => {
return response.data?.status === "delivered";
},
onPollSuccess: (response, attempt) => {
updateOrderStatus(response.data.status);
console.log(`Order ${orderId} - Status: ${response.data.status}`);
// Stop polling if user navigates away
if (shouldStopPolling()) {
api.stopPolling(orderPollId);
}
},
},
});
if ("finalResponse" in result) {
console.log(`Order tracking completed: ${result.pollId}`);
}
};
// Stop order tracking from anywhere in your app
const stopOrderTracking = (orderId: string) => {
const orderPollId = `order-tracking-${orderId}`;
const stopped = api.stopPolling(orderPollId);
if (stopped) {
console.log(`Stopped tracking order ${orderId}`);
updateUI(`Tracking stopped for order ${orderId}`);
}
};
Upload/Download Progress Tracking
Track file upload and download progress like Axios.
Upload Progress
const uploadFile = async (file: File) => {
const formData = new FormData();
formData.append("file", file);
const response = await api.post("/upload", formData, {
onUploadProgress: (progress) => {
console.log(`Upload: ${progress.percentage}%`);
updateProgressBar(progress.percentage);
// Show detailed progress
console.log(`${progress.loaded} / ${progress.total} bytes`);
},
onSuccess: (response) => {
showSuccessMessage("File uploaded successfully!");
},
});
};
Download Progress
const downloadFile = async (fileId: string) => {
const response = await api.get(`/files/${fileId}/download`, {
onDownloadProgress: (progress) => {
console.log(`Download: ${progress.percentage}%`);
updateDownloadProgress(progress.percentage);
},
onSuccess: (response) => {
// Handle downloaded file
saveFile(response.data);
},
});
};
Complete File Upload Example
const FileUploader = () => {
const [uploadProgress, setUploadProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const handleFileUpload = async (file: File) => {
setUploading(true);
setUploadProgress(0);
const formData = new FormData();
formData.append("file", file);
formData.append("category", "documents");
const response = await api.post("/files/upload", formData, {
onUploadProgress: (progress) => {
setUploadProgress(progress.percentage);
},
onSuccess: (response) => {
toast.success("File uploaded successfully!");
addToFileList(response.data);
},
finally: () => {
setUploading(false);
},
});
};
return (
<div>
<input
type="file"
onChange={(e) => handleFileUpload(e.target.files[0])}
disabled={uploading}
/>
{uploading && (
<div>
<ProgressBar value={uploadProgress} />
<span>{uploadProgress}% uploaded</span>
</div>
)}
</div>
);
};
Timeout Configuration
Set timeouts globally or per request to handle slow connections.
Global Timeout with Safety Limits
const api = new ApiClient({
baseUrl: "https://api.example.com",
timeout: 600000, // Requested 10 minutes, but automatically capped at 5 minutes (300000ms)
onError: (response) => {
if (response.error?.name === "AbortError") {
toast.error("Request timed out. Please try again.");
}
},
});
// Timeout validation prevents invalid values
try {
const invalidApi = new ApiClient({
timeout: -5000, // Error: Timeout must be a positive number
});
} catch (error) {
console.error("Invalid timeout:", error.message);
}
Per-Request Timeout with Enhanced Abort Handling
// Short timeout for quick operations
const quickResponse = await api.get("/health", {
timeout: 3000, // 3 seconds
});
// Longer timeout for heavy operations
const heavyResponse = await api.post("/process-large-file", data, {
timeout: 60000, // 1 minute
});
// Combined timeout and manual abort control
const controller = new AbortController();
// Set up manual cancellation after 30 seconds
setTimeout(() => controller.abort(), 30000);
const response = await api.get("/long-operation", {
timeout: 60000, // 1 minute timeout
signal: controller.signal, // Manual abort control
});
// The API automatically combines both timeout and manual abort signals
Enhanced Abort Signal Handling
const api = new ApiClient({
baseUrl: "https://api.example.com",
});
// Multiple abort signals are automatically combined
const userController = new AbortController();
const timeoutController = new AbortController();
// User can cancel manually
document.getElementById("cancel-btn").onclick = () => {
userController.abort();
};
// Automatic timeout cancellation
setTimeout(() => timeoutController.abort(), 10000);
const response = await api.get("/data", {
signal: userController.signal, // User control
timeout: 15000, // Automatic timeout (creates internal abort signal)
// Both signals are automatically combined using AbortSignal.any() or fallback
});
Timeout with Retry Logic
const fetchWithRetry = async (url: string, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const response = await api.get(url, {
timeout: 5000,
showError: false, // Don't show error toast for retries
});
if (response.success) {
return response;
}
if (attempt < maxRetries) {
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
// All retries failed
toast.error("Request failed after multiple attempts");
return null;
};
Combining Features
Use multiple advanced features together for powerful functionality.
File Upload with Polling and Progress
const uploadAndProcess = async (file: File) => {
// Step 1: Upload file with progress
const uploadResponse = await api.post("/files/upload", formData, {
onUploadProgress: (progress) => {
setUploadProgress(progress.percentage);
},
timeout: 30000, // 30 second upload timeout
});
if (!uploadResponse.success) return;
const fileId = uploadResponse.data.id;
// Step 2: Poll for processing status
const { finalResponse } = await api.poll(`/files/${fileId}/process`, {
interval: 2000,
maxAttempts: 30,
stopCondition: (response) => {
return response.data?.status === "completed";
},
onPollSuccess: (response, attempt) => {
setProcessingProgress(response.data.progress);
},
});
if (finalResponse.success) {
toast.success("File processed successfully!");
}
};
Real-time Dashboard with Polling
const Dashboard = () => {
const [data, setData] = useState(null);
const [polling, setPolling] = useState(null);
const startPolling = async () => {
const { stop } = await api.poll("/dashboard/stats", {
interval: 5000, // Update every 5 seconds
onPollSuccess: (response) => {
setData(response.data);
},
onPollError: (response, attempt) => {
console.warn(`Poll attempt ${attempt} failed`);
},
});
setPolling({ stop });
};
const stopPolling = () => {
if (polling) {
polling.stop();
setPolling(null);
}
};
useEffect(() => {
startPolling();
return () => stopPolling(); // Cleanup on unmount
}, []);
return (
<div>
<button onClick={polling ? stopPolling : startPolling}>
{polling ? "Stop Live Updates" : "Start Live Updates"}
</button>
<DashboardStats data={data} />
</div>
);
};
Best Practices
1. Use Appropriate Timeouts
// Quick health checks
const health = await api.get("/health", { timeout: 3000 });
// File uploads
const upload = await api.post("/upload", data, { timeout: 60000 });
// Heavy processing
const process = await api.post("/process", data, { timeout: 300000 });
2. Handle Progress Gracefully
const handleUpload = async (file: File) => {
let lastProgress = 0;
await api.post("/upload", formData, {
onUploadProgress: (progress) => {
// Only update UI if progress changed significantly
if (progress.percentage - lastProgress >= 5) {
updateUI(progress.percentage);
lastProgress = progress.percentage;
}
},
});
};
3. Smart Polling
// Adaptive polling - slow down as time passes
let pollInterval = 1000; // Start with 1 second
const { finalResponse } = await api.poll("/status", {
interval: pollInterval,
onPollSuccess: (response, attempt) => {
// Gradually increase interval
if (attempt > 5) pollInterval = 5000; // 5 seconds after 5 attempts
if (attempt > 10) pollInterval = 10000; // 10 seconds after 10 attempts
},
});
Multiple Independent Polling Operations
One of the most powerful features is the ability to run multiple polling operations simultaneously without interference.
Real-time Dashboard Example with Custom Poll IDs
const Dashboard = () => {
const [orderData, setOrderData] = useState(null);
const [inventoryData, setInventoryData] = useState(null);
const [systemData, setSystemData] = useState(null);
// Define custom poll IDs for better management
const POLL_IDS = {
orders: "dashboard-orders",
inventory: "dashboard-inventory",
system: "dashboard-system",
};
const startMultiplePolling = async () => {
// Start 3 independent polling operations with custom IDs
const [orderResult, inventoryResult, systemResult] = await Promise.all([
// Orders polling - every 3 seconds
api.get("/dashboard/orders", {
poll: {
interval: 3000,
maxAttempts: 100,
pollId: POLL_IDS.orders, // Custom poll ID
onPollSuccess: (response, attempt) => {
setOrderData(response.data);
console.log(
`Orders (${POLL_IDS.orders}) updated - attempt ${attempt}`
);
},
stopCondition: () => false, // Run indefinitely
},
}),
// Inventory polling - every 10 seconds
api.get("/dashboard/inventory", {
poll: {
interval: 10000,
maxAttempts: 100,
pollId: POLL_IDS.inventory, // Custom poll ID
onPollSuccess: (response, attempt) => {
setInventoryData(response.data);
console.log(
`Inventory (${POLL_IDS.inventory}) updated - attempt ${attempt}`
);
},
stopCondition: () => false, // Run indefinitely
},
}),
// System health polling - every 5 seconds
api.get("/dashboard/system", {
poll: {
interval: 5000,
maxAttempts: 100,
pollId: POLL_IDS.system, // Custom poll ID
onPollSuccess: (response, attempt) => {
setSystemData(response.data);
console.log(
`System (${POLL_IDS.system}) updated - attempt ${attempt}`
);
},
stopCondition: () => false, // Run indefinitely
},
}),
]);
// Verify poll IDs match our custom IDs
if (
"pollId" in orderResult &&
"pollId" in inventoryResult &&
"pollId" in systemResult
) {
console.log(`Started polling with IDs:`, {
orders: orderResult.pollId,
inventory: inventoryResult.pollId,
system: systemResult.pollId,
});
}
};
// Stop specific polling operations by ID
const stopSpecificPolling = (type: keyof typeof POLL_IDS) => {
const pollId = POLL_IDS[type];
const stopped = api.stopPolling(pollId);
if (stopped) {
console.log(`Stopped ${type} polling (${pollId})`);
} else {
console.warn(`Failed to stop ${type} polling (${pollId})`);
}
};
useEffect(() => {
startMultiplePolling();
return () => {
// Cleanup all polling on unmount
Object.values(pollingControls).forEach((stop) => {
if (typeof stop === "function") stop();
});
};
}, []);
return (
<div>
<div className="controls">
<button onClick={() => stopSpecificPolling("orders")}>
Stop Orders Polling
</button>
<button onClick={() => stopSpecificPolling("inventory")}>
Stop Inventory Polling
</button>
<button onClick={() => stopSpecificPolling("system")}>
Stop System Polling
</button>
</div>
<div className="dashboard-grid">
<OrdersWidget data={orderData} />
<InventoryWidget data={inventoryData} />
<SystemHealthWidget data={systemData} />
</div>
</div>
);
};
Key Benefits of Independent Polling:
- 🔄 No Interference: Each polling operation runs on its own schedule
- ⏱️ Different Intervals: Orders (3s), Inventory (10s), System (5s) - all independent
- 🎛️ Individual Control: Stop/start each polling operation separately
- ⚡ Parallel Execution: All polling happens simultaneously using Promise.all
- 💾 Resource Efficient: Each poll only makes requests when needed
- 🛑 Manual Stop: Can stop any polling operation at any time
- 🔧 Flexible Conditions: Each poll can have different stop conditions
Testing Multiple Polling Operations
// Test that multiple polling operations don't interfere
const testIndependentPolling = async () => {
let poll1Count = 0;
let poll2Count = 0;
let poll3Count = 0;
const [result1, result2, result3] = await Promise.all([
api.get("/endpoint1", {
poll: {
interval: 500,
maxAttempts: 3,
onPollSuccess: () => poll1Count++,
stopCondition: () => poll1Count >= 2,
},
}),
api.get("/endpoint2", {
poll: {
interval: 800,
maxAttempts: 4,
onPollSuccess: () => poll2Count++,
stopCondition: () => poll2Count >= 3,
},
}),
api.get("/endpoint3", {
poll: {
interval: 300,
maxAttempts: 5,
onPollSuccess: () => poll3Count++,
stopCondition: () => poll3Count >= 2,
},
}),
]);
// Each polling operation completes independently
console.log(`Poll 1: ${result1.attempts} attempts`);
console.log(`Poll 2: ${result2.attempts} attempts`);
console.log(`Poll 3: ${result3.attempts} attempts`);
};
Poll ID Management
Control and manage individual polling operations with custom IDs for precise control.
Custom Poll IDs
Assign meaningful IDs to your polling operations for better organization and control:
const api = new ApiClient({
baseUrl: "https://api.example.com",
});
// Start polling with custom ID
const result = await api.get("/user/notifications", {
poll: {
interval: 5000,
pollId: "user-notifications", // Custom meaningful ID
onPollSuccess: (response, attempt) => {
updateNotifications(response.data);
},
},
});
console.log(`Started polling with ID: ${result.pollId}`); // "user-notifications"
Stop Specific Polling Operations
Stop individual polling operations without affecting others:
// Stop specific polling operation
const stopped = api.stopPolling("user-notifications");
if (stopped) {
console.log("Successfully stopped user notifications polling");
} else {
console.log("Polling operation not found or already stopped");
}
// Check remaining active polls
console.log(`Active polls remaining: ${api.getActivePollingCount()}`);
Prevent Duplicate Poll IDs
The API prevents duplicate poll IDs to avoid conflicts:
try {
// Start first polling operation
const poll1 = api.get("/data", {
poll: {
interval: 1000,
pollId: "my-poll",
},
});
// Try to start another with same ID - this will throw an error
const poll2 = api.get("/other-data", {
poll: {
interval: 2000,
pollId: "my-poll", // Duplicate ID!
},
});
} catch (error) {
console.error(error.message); // "Polling operation with ID 'my-poll' already exists"
}
Dynamic Poll ID Generation
Create dynamic poll IDs based on your application logic:
const startUserSpecificPolling = (userId: string, dataType: string) => {
const pollId = `user-${userId}-${dataType}`;
return api.get(`/users/${userId}/${dataType}`, {
poll: {
interval: 3000,
pollId: pollId,
onPollSuccess: (response, attempt) => {
console.log(`User ${userId} ${dataType} updated (attempt ${attempt})`);
updateUserData(userId, dataType, response.data);
},
},
});
};
// Start polling for different users and data types
startUserSpecificPolling("123", "profile");
startUserSpecificPolling("123", "settings");
startUserSpecificPolling("456", "profile");
// Stop specific user's profile polling
api.stopPolling("user-123-profile");
Poll ID Best Practices
// ✅ Good - Descriptive and unique IDs
const POLL_IDS = {
USER_NOTIFICATIONS: "user-notifications",
SYSTEM_HEALTH: "system-health-check",
ORDER_STATUS: (orderId: string) => `order-status-${orderId}`,
FILE_PROCESSING: (fileId: string) => `file-processing-${fileId}`,
};
// ✅ Good - Namespace your poll IDs
const createPollId = (module: string, feature: string, id?: string) => {
return id ? `${module}-${feature}-${id}` : `${module}-${feature}`;
};
const pollId = createPollId("dashboard", "orders", "live-updates");
// Result: "dashboard-orders-live-updates"
// ✅ Good - Check if poll exists before starting
const startPollingIfNotExists = async (pollId: string, endpoint: string) => {
// Check if already polling
if (api.getActivePollingCount() > 0) {
// You could implement a method to check specific poll ID exists
console.log("Polling may already be active");
}
try {
const result = await api.get(endpoint, {
poll: {
interval: 2000,
pollId: pollId,
onPollSuccess: (response) => {
console.log(`Poll ${pollId} success:`, response.data);
},
},
});
return result;
} catch (error) {
if (error.message.includes("already exists")) {
console.log(`Poll ${pollId} is already running`);
return null;
}
throw error;
}
};
Memory Management & Cleanup
Prevent memory leaks and properly manage resources with built-in cleanup methods.
Destroying API Client
import ApiClient from "hmm-api";
const api = new ApiClient({
baseUrl: "https://api.example.com",
});
// Start some polling operations
const pollingResult = api.get("/status", {
poll: {
interval: 2000,
maxAttempts: 100,
},
});
// Later, when component unmounts or app closes
api.destroy(); // Stops all polling, clears resources, prevents memory leaks
// Any further API calls will throw an error
try {
await api.get("/test"); // This will throw
} catch (error) {
console.log(error.message); // "ApiClient has been destroyed and cannot be used"
}
Managing Multiple Polling Operations
const api = new ApiClient({
baseUrl: "https://api.example.com",
});
// Start multiple polling operations with custom IDs
const dashboard = async () => {
const [orders, inventory, system] = await Promise.all([
api.get("/orders", {
poll: {
interval: 3000,
pollId: "dashboard-orders",
},
}),
api.get("/inventory", {
poll: {
interval: 5000,
pollId: "dashboard-inventory",
},
}),
api.get("/system", {
poll: {
interval: 2000,
pollId: "dashboard-system",
},
}),
]);
};
// Check how many polling operations are active
console.log(`Active polls: ${api.getActivePollingCount()}`); // e.g., "Active polls: 3"
// Get all active poll IDs
const activeIds = api.getActivePollingIds();
console.log("Active poll IDs:", activeIds); // ["dashboard-orders", "dashboard-inventory", "dashboard-system"]
// Stop specific polling operation
const stopped = api.stopPolling("dashboard-orders");
console.log(`Orders polling stopped: ${stopped}`); // true
// Stop all polling operations at once
api.stopAllPolling();
console.log(`Active polls: ${api.getActivePollingCount()}`); // "Active polls: 0"
// Individual polling operations can still be stopped from their result
if ("stop" in result) {
result.stop(); // Stop this specific polling operation
}
React Component Cleanup Example
import { useEffect, useRef } from "react";
import ApiClient from "hmm-api";
const Dashboard = () => {
const apiRef = useRef(null);
const [data, setData] = useState(null);
useEffect(() => {
// Create API client
apiRef.current = new ApiClient({
baseUrl: "https://api.example.com",
});
// Start polling
const startPolling = async () => {
const result = await apiRef.current.get("/dashboard", {
poll: {
interval: 5000,
onPollSuccess: (response) => {
setData(response.data);
},
},
});
};
startPolling();
// Cleanup function
return () => {
if (apiRef.current) {
apiRef.current.destroy(); // Proper cleanup
apiRef.current = null;
}
};
}, []);
return <div>{/* Your dashboard UI */}</div>;
};
Enhanced Error Handling & Validation
Better error management with automatic cleanup, input validation, and prevention of operations on destroyed clients.
Input Validation
The API client now includes comprehensive input validation to prevent common errors:
const api = new ApiClient({
baseUrl: "https://api.example.com",
timeout: 30000, // Automatically capped at 5 minutes maximum
});
// These will throw validation errors:
try {
api.setAuthToken(123); // Error: Auth token must be a string or null
api.setGlobalHeaders("invalid"); // Error: Headers must be an object
api.setShowGlobalError("true"); // Error: showGlobalError must be a boolean
// Invalid polling configuration
await api.get("/test", {
poll: {
interval: -1000, // Error: Polling interval must be greater than 0
maxAttempts: 0, // Error: Max attempts must be greater than 0
pollId: "", // Error: Poll ID must be a non-empty string
},
});
} catch (error) {
console.error("Validation error:", error.message);
}
Enhanced Error Parsing
Improved error message parsing handles multiple error formats:
const api = new ApiClient({
returnParsedError: true,
onError: (response) => {
// Now handles various error formats:
// - { message: "Error message" }
// - { error: { message: "Nested error" } }
// - { detail: "Detail message" }
// - { msg: "Message field" }
// - Plain strings
// - Complex objects (JSON stringified)
console.error("Clean error:", response.error);
},
});
Automatic Cleanup on Errors
const api = new ApiClient({
baseUrl: "https://api.example.com",
onError: (response) => {
console.error("Request failed:", response.error);
// Automatic cleanup happens internally
// No memory leaks from failed requests
},
});
// Even if requests fail, resources are properly cleaned up
const failedRequest = await api.get("/nonexistent-endpoint");
Input Validation & Safe Operations
const api = new ApiClient({
timeout: 600000, // Automatically capped at 5 minutes (300000ms)
});
// Input validation prevents common errors
try {
api.setAuthToken(123); // Error: Auth token must be a string or null
api.setGlobalHeaders("invalid"); // Error: Headers must be an object
await api.get("/test", {
poll: {
interval: -1000, // Error: Polling interval must be greater than 0
pollId: "", // Error: Poll ID must be a non-empty string
},
});
} catch (error) {
console.error("Validation prevented invalid operation:", error.message);
}
Safe Callback Execution
const api = new ApiClient({
onError: (response) => {
// Even if this callback throws, it won't break the client
throw new Error("Callback error!");
},
onSuccess: (response) => {
// Safe execution prevents client corruption
someUndefinedFunction(); // Won't crash the client
},
});
// Client continues to work even if callbacks fail
const response = await api.get("/test");
Destroyed Client Protection
const api = new ApiClient();
// Start some operations
const polling = api.get("/status", {
poll: { interval: 1000 },
});
// Destroy the client
api.destroy();
// All subsequent operations are safely prevented
try {
api.setAuthToken("new-token"); // Throws error
api.get("/test"); // Throws error
api.stopAllPolling(); // Safe to call, but no-op
} catch (error) {
console.log("Protected from using destroyed client");
}
// Destroying multiple times is safe
api.destroy(); // No error, already destroyed
Enhanced Progress Tracking
Improved progress tracking with better cleanup and error handling.
Upload Progress with Cleanup
const uploadWithCleanup = async (file: File) => {
const api = new ApiClient({
baseUrl: "https://api.example.com",
});
let progressInterval = null;
try {
const response = await api.post("/upload", formData, {
onUploadProgress: (progress) => {
// Progress tracking with automatic cleanup
console.log(`Upload: ${progress.percentage}%`);
updateProgressBar(progress.percentage);
},
onDownloadProgress: (progress) => {
// Download progress also has automatic cleanup
console.log(`Download: ${progress.percentage}%`);
},
timeout: 30000,
});
return response;
} finally {
// Cleanup happens automatically, but you can also destroy if needed
// api.destroy(); // Optional explicit cleanup
}
};
Progress with Abort Handling
const uploadWithAbort = async (file: File) => {
const controller = new AbortController();
// Set up abort after 10 seconds
setTimeout(() => controller.abort(), 10000);
const response = await api.post("/upload", formData, {
signal: controller.signal, // Pass abort signal
onUploadProgress: (progress) => {
console.log(`Upload: ${progress.percentage}%`);
// Progress tracking automatically stops when aborted
if (controller.signal.aborted) {
console.log("Upload aborted, progress stopped");
}
},
});
};
Best Practices for Memory Management
1. Always Cleanup in React
// ✅ Good - Proper cleanup
const useApiClient = () => {
const apiRef = useRef(null);
useEffect(() => {
apiRef.current = new ApiClient({ baseUrl: "/api" });
return () => {
apiRef.current?.destroy();
};
}, []);
return apiRef.current;
};
// ❌ Bad - No cleanup, potential memory leaks
const useApiClient = () => {
const [api] = useState(() => new ApiClient({ baseUrl: "/api" }));
return api; // No cleanup!
};
2. Monitor Active Polling
const api = new ApiClient();
// Periodically check active polling operations
const monitorPolling = () => {
const activeCount = api.getActivePollingCount();
if (activeCount > 10) {
console.warn(`High number of active polls: ${activeCount}`);
// Consider stopping some polls
api.stopAllPolling();
}
};
setInterval(monitorPolling, 30000); // Check every 30 seconds
3. Graceful Shutdown
// Application shutdown handler
const gracefulShutdown = () => {
console.log("Shutting down application...");
// Stop all API operations
api.stopAllPolling();
// Destroy API client
api.destroy();
console.log("Cleanup completed");
};
// Register shutdown handlers
process.on("SIGTERM", gracefulShutdown);
process.on("SIGINT", gracefulShutdown);
window.addEventListener("beforeunload", gracefulShutdown);
4. Error Recovery
const createResilientApiClient = () => {
let api = new ApiClient({ baseUrl: "/api" });
const recreateClient = () => {
if (api) {
api.destroy(); // Clean up old client
}
api = new ApiClient({ baseUrl: "/api" });
};
const makeRequest = async (url, options = {}) => {
try {
return await api.get(url, options);
} catch (error) {
if (error.message.includes("destroyed")) {
console.log("Client was destroyed, recreating...");
recreateClient();
return await api.get(url, options); // Retry with new client
}
throw error;
}
};
return { makeRequest, destroy: () => api.destroy() };
};
Configuration Reference
Complete Configuration Example
import ApiClient from "hmm-api";
const api = new ApiClient({
// Basic configuration
baseUrl: "https://api.example.com",
timeout: 30000, // 30 seconds (automatically capped at 5 minutes)
credentials: "include",
// Error and success handling
showGlobalError: true, // Enable global error callbacks
showGlobalSuccess: false, // Disable global success callbacks
returnParsedError: true, // Get clean error messages
// Global headers (immutable after setting)
globalHeaders: {
"X-API-Version": "v2",
"X-Client-ID": "web-app",
},
// Global callbacks (with safe execution)
onSuccess: (response) => {
console.log("✅ Request succeeded:", response.status);
// Even if this throws an error, it won't break the client
},
onError: (response) => {
console.error("❌ Request failed:", response.error);
// Safe callback execution prevents client corruption
if (response.status === 401) {
redirectToLogin();
}
},
});
// Input validation prevents invalid configurations
try {
const invalidApi = new ApiClient({
timeout: -1000, // Error: Timeout must be a positive number
baseUrl: 123, // Error: BaseUrl must be a string or null
});
} catch (error) {
console.error("Configuration error:", error.message);
}
Method Reference
// Instance methods for configuration
api.setAuthToken("Bearer your-token");
api.setGlobalHeaders({ "X-Custom": "value" });
api.setShowGlobalError(false);
api.setShowGlobalSuccess(true);
api.setOnSuccess(newSuccessCallback);
api.setOnError(newErrorCallback);
// Memory management methods
api.getActivePollingCount(); // Returns number of active polls
api.getActivePollingIds(); // Returns array of active poll IDs
api.stopAllPolling(); // Stop all polling operations
api.stopPolling("custom-poll-id"); // Stop specific polling operation by ID
api.destroy(); // Complete cleanup and destroy client
// Request methods with all options
const response = await api.get("/endpoint", {
// Override global settings
showError: false,
showSuccess: true,
timeout: 10000,
// Progress tracking
onDownloadProgress: (progress) => {
console.log(`Download: ${progress.percentage}%`);
},
// Polling configuration
poll: {
interval: 2000,
maxAttempts: 10,
pollId: "my-custom-poll-id", // Optional custom poll ID
stopCondition: (response) => response.data.ready,
onPollSuccess: (response, attempt) => {
console.log(`Poll ${attempt}: ${response.data.status}`);
},
onPollError: (response, attempt) => {
console.warn(`Poll ${attempt} failed: ${response.error}`);
},
},
// Callbacks
onSuccess: (response) => {
console.log("Request-specific success");
},
onError: (response) => {
console.error("Request-specific error");
},
finally: () => {
console.log("Cleanup after request");
},
});
Migration from v1.x
If you're upgrading from v1.x, here are the key changes:
Property Name Updates
// Old (v1.x) - Still works but deprecated
const api = new ApiClient({
showGlobalErrorToast: true,
showGlobalSuccessToast: false,
});
// New (v2.0.0) - Recommended
const api = new ApiClient({
showGlobalError: true,
showGlobalSuccess: false,
});
Enhanced Capabilities
// v1.x - Basic functionality
const response = await api.get("/data");
// v2.0.0 - Enhanced with new features
const response = await api.get("/data", {
timeout: 5000,
onDownloadProgress: (progress) => updateProgress(progress.percentage),
poll: {
interval: 1000,
stopCondition: (response) => response.data.ready,
},
finally: () => hideLoader(),
});
TypeScript Support & Type Safety
Automatic Type Inference
The API client provides excellent TypeScript support with automatic type inference:
import ApiClient from "hmm-api";
const api = new ApiClient({
baseUrl: "https://api.example.com",
});
// Regular request - TypeScript knows this is ApiResponse<User>
interface User {
id: number;
name: string;
email: string;
}
const userResponse = await api.get<User>("/users/123");
// TypeScript knows userResponse is ApiResponse<User>
if (userResponse.success) {
console.log(userResponse.data.name); // ✅ TypeScript knows data is User
}
// Polling request - TypeScript knows this is PollingResult<User>
const pollingResult = await api.get<User>("/users/123", {
poll: {
interval: 1000,
stopCondition: (response) => response.success,
},
});
// TypeScript knows pollingResult is PollingResult<User>
console.log(pollingResult.attempts); // ✅ TypeScript knows this exists
console.log(pollingResult.finalResponse.data.name); // ✅ TypeScript knows the structure
Type Guards for Mixed Usage
When you need to handle both polling and non-polling results dynamically:
type ApiResult<T> = ApiResponse<T> | PollingResult<T>;
function handleResult<T>(result: ApiResult<T>): T | null {
// Type guard to check if it's a polling result
if ("finalResponse" in result) {
// TypeScript knows this is PollingResult<T>
return result.finalResponse.success ? result.finalResponse.data : null;
} else {
// TypeScript knows this is ApiResponse<T>
return result.success ? result.data : null;
}
}
// Usage
const result = await api.get<User>("/users/123", someOptions);
const userData = handleResult(result); // userData is User | null
Generic Helper Functions
// Helper to always get ApiResponse regardless of polling
function getResponse<T>(
result: ApiResponse<T> | PollingResult<T>
): ApiResponse<T> {
return "finalResponse" in result ? result.finalResponse : result;
}
// Helper to extract data safely
function extractData<T>(result: ApiResponse<T> | PollingResult<T>): T | null {
const response = getResponse(result);
return response.success ? response.data : null;
}
// Usage
const result = await api.get<User>("/users/123", options);
const user = extractData(result); // user is User | null
Interface Definitions
// Define your API response types
interface ApiUser {
id: number;
name: string;
email: string;
createdAt: string;
}
interface ApiError {
message: string;
code: string;
details?: Record<string, any>;
}
// Use with the API client
const api = new ApiClient({
baseUrl: "https://api.example.com",
returnParsedError: true, // Get clean error messages
});
// TypeScript will enforce the types
const userResult = await api.get<ApiUser>("/users/123");
const usersResult = await api.get<ApiUser[]>("/users");
// Polling with types
const pollingResult = await api.get<ApiUser>("/users/123", {
poll: {
interval: 1000,
stopCondition: (response) => {
// TypeScript knows response is ApiResponse<ApiUser>
return response.success && response.data.id > 0;
},
onPollSuccess: (response, attempt) => {
// TypeScript knows response.data is ApiUser
console.log(`User ${response.data.name} found on attempt ${attempt}`);
},
},
});
Enhanced Reliability & Compatibility
Browser Compatibility & Fallbacks
The API client includes automatic fallbacks for older browsers:
const api = new ApiClient({
baseUrl: "https://api.example.com",
});
// Automatic fallbacks for:
// - AbortSignal.any() (falls back to manual signal combination)
// - Modern fetch features (graceful degradation)
// - Console methods (safe execution in environments without console)
const controller1 = new AbortController();
const controller2 = new AbortController();
// Works in all browsers with automatic fallback
const response = await api.get("/data", {
signal: controller1.signal,
timeout: 10000, // Creates internal timeout signal
// Both signals are combined automatically
});
Immutable Configuration
Global headers and other configurations are immutable to prevent accidental modifications:
const api = new ApiClient({
globalHeaders: {
"X-API-Version": "v2",
Authorization: "Bearer token",
},
});
// Headers are frozen - this won't affect the original
const headers = api.globalHeaders;
headers["X-API-Version"] = "v3"; // This won't change the actual headers
// Use setGlobalHeaders to properly update
api.setGlobalHeaders({
"X-API-Version": "v3", // This will work correctly
});
Robust Error Recovery
The client continues to work even when callbacks or other operations fail:
const api = new ApiClient({
onSuccess: (response) => {
// Even if this throws an error, the client continues to work
throw new Error("Callback failed!");
},
onError: (response) => {
// Safe execution prevents client corruption
undefinedFunction(); // Won't crash the client
},
});
// Client remains functional despite callback errors
const response1 = await api.get("/test1"); // Works
const response2 = await api.get("/test2"); // Still works
Memory-Safe Operations
All operations are designed to prevent memory leaks:
const api = new ApiClient();
// Start multiple operations
const polling1 = api.get("/status", { poll: { interval: 1000 } });
const polling2 = api.get("/health", { poll: { interval: 2000 } });
// Automatic cleanup on destroy
api.destroy();
// All resources are properly cleaned up:
// - Polling intervals cleared
// - Event listeners removed
// - References nullified
// - No memory leaks
console.log(api.getActivePollingCount()); // 0
Enhanced Error Messages
Comprehensive error messages help with debugging:
const api = new ApiClient();
try {
// Various validation errors provide clear messages
api.setAuthToken(123); // "Auth token must be a string or null"
api.setGlobalHeaders("invalid"); // "Headers must be an object"
await api.get("/test", {
poll: {
interval: -1000, // "Polling interval must be greater than 0"
maxAttempts: 0, // "Max attempts must be greater than 0"
pollId: "", // "Poll ID must be a non-empty string"
},
});
await api.get("invalid-url"); // "Invalid URL: invalid-url"
} catch (error) {
console.error("Clear error message:", error.message);
}
These enhanced features make hmm-api v2.0.0 perfect for modern web applications that need real-time updates, file handling, robust error management, proper memory management, and enterprise-grade reliability!