Promise-based retry mechanisms with validation, aggregation, and timeout handling. Enables building resilient async operations that can retry until success conditions are met.
Creates a retryable promise that executes until a validation condition is met, with optional aggregation and error handling.
/**
* Creates a retryable promise with validation and timeout logic
* @param options - Configuration for the iterable promise behavior
* @returns Promise that resolves when validation passes or error condition is met
*/
function createIterablePromise<TResponse>(
options: CreateIterablePromise<TResponse>
): Promise<TResponse>;
interface CreateIterablePromise<TResponse> extends IterableOptions<TResponse> {
/**
* The function to execute on each iteration
* @param previousResponse - Result from previous iteration (undefined on first call)
* @returns Promise with the response to validate
*/
func: (previousResponse?: TResponse) => Promise<TResponse>;
/**
* Validation function to determine if iteration should continue
* @param response - Response from func to validate
* @returns True if iteration should stop (success), false to continue
*/
validate: (response: TResponse) => boolean | PromiseLike<boolean>;
}
interface IterableOptions<TResponse> {
/**
* Optional aggregation function called after each func execution
* @param response - Response from func
* @returns Any value or promise (result is ignored)
*/
aggregator?: (response: TResponse) => unknown | PromiseLike<unknown>;
/**
* Optional error condition configuration
*/
error?: {
/**
* Function to check if response indicates an error condition
* @param response - Response to check
* @returns True if this is an error condition that should stop iteration
*/
validate: (response: TResponse) => boolean | PromiseLike<boolean>;
/**
* Function to generate error message
* @param response - Response that triggered the error
* @returns Error message string
*/
message: (response: TResponse) => string | PromiseLike<string>;
};
/**
* Function to determine wait time between iterations
* @returns Wait time in milliseconds
*/
timeout?: () => number | PromiseLike<number>;
}Usage Examples:
import { createIterablePromise } from "@algolia/client-common";
// Polling until task completes
interface TaskStatus {
taskID: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
progress?: number;
}
const result = await createIterablePromise<TaskStatus>({
func: async (previousResponse) => {
// Use taskID from previous response if available
const taskID = previousResponse?.taskID || "new-task-id";
return await checkTaskStatus(taskID);
},
validate: async (response) => {
// Stop when task is completed
return response.status === 'completed';
},
error: {
validate: async (response) => {
// Error condition: task failed
return response.status === 'failed';
},
message: async (response) => {
return `Task ${response.taskID} failed`;
}
},
aggregator: async (response) => {
// Log progress updates
if (response.progress !== undefined) {
console.log(`Task progress: ${response.progress}%`);
}
},
timeout: () => {
// Wait 1 second between polls
return 1000;
}
});
console.log(`Task completed: ${result.taskID}`);Paginated Data Fetching:
import { createIterablePromise } from "@algolia/client-common";
interface PagedResponse {
data: any[];
nextPage?: string;
totalItems: number;
}
const allData: any[] = [];
await createIterablePromise<PagedResponse>({
func: async (previousResponse) => {
const page = previousResponse?.nextPage || 'first';
return await fetchPage(page);
},
validate: async (response) => {
// Stop when no more pages
return !response.nextPage;
},
aggregator: async (response) => {
// Collect data from each page
allData.push(...response.data);
console.log(`Fetched ${allData.length}/${response.totalItems} items`);
},
timeout: () => 500 // 500ms between page requests
});
console.log(`Fetched ${allData.length} total items`);Exponential Backoff Retry:
import { createIterablePromise } from "@algolia/client-common";
let attempt = 0;
const result = await createIterablePromise({
func: async () => {
attempt++;
console.log(`Attempt ${attempt}`);
return await unreliableApiCall();
},
validate: async (response) => {
// Success condition
return response.success === true;
},
error: {
validate: async (response) => {
// Stop after 5 attempts
return attempt >= 5;
},
message: async () => {
return `Failed after ${attempt} attempts`;
}
},
timeout: () => {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
return Math.pow(2, attempt - 1) * 1000;
}
});Search Indexing Status:
import { createIterablePromise } from "@algolia/client-common";
interface IndexingStatus {
taskID: number;
status: 'notFound' | 'published';
pendingTask: boolean;
}
// Wait for search index to be updated
const indexingComplete = await createIterablePromise<IndexingStatus>({
func: async () => {
return await getTaskStatus(taskID);
},
validate: async (response) => {
// Task is complete when published and no pending tasks
return response.status === 'published' && !response.pendingTask;
},
error: {
validate: async (response) => {
// Error if task not found after initial attempts
return response.status === 'notFound';
},
message: async (response) => {
return `Task ${response.taskID} not found`;
}
},
timeout: () => {
// Check every 100ms for indexing completion
return 100;
}
});State Machine with Iterable Promise:
interface StateMachineResponse {
state: 'init' | 'processing' | 'validation' | 'complete' | 'error';
data?: any;
errorMessage?: string;
}
const finalState = await createIterablePromise<StateMachineResponse>({
func: async (previousResponse) => {
const currentState = previousResponse?.state || 'init';
switch (currentState) {
case 'init':
await initializeProcess();
return { state: 'processing' };
case 'processing':
const result = await processData();
return { state: 'validation', data: result };
case 'validation':
const isValid = await validateResult(previousResponse!.data);
if (isValid) {
return { state: 'complete', data: previousResponse!.data };
} else {
return { state: 'processing' }; // Retry processing
}
default:
return { state: 'error', errorMessage: 'Invalid state' };
}
},
validate: async (response) => {
return response.state === 'complete';
},
error: {
validate: async (response) => {
return response.state === 'error';
},
message: async (response) => {
return response.errorMessage || 'Unknown error';
}
},
timeout: () => 1000
});Batch Operation with Progress Tracking:
interface BatchProgress {
completed: number;
total: number;
currentBatch: any[];
allResults: any[];
}
const BATCH_SIZE = 10;
const items = [...]; // Large array of items to process
const batchResult = await createIterablePromise<BatchProgress>({
func: async (previousResponse) => {
const progress = previousResponse || {
completed: 0,
total: items.length,
currentBatch: [],
allResults: []
};
// Get next batch
const startIndex = progress.completed;
const endIndex = Math.min(startIndex + BATCH_SIZE, items.length);
const batch = items.slice(startIndex, endIndex);
// Process batch
const batchResults = await Promise.all(
batch.map(item => processItem(item))
);
return {
completed: endIndex,
total: items.length,
currentBatch: batchResults,
allResults: [...progress.allResults, ...batchResults]
};
},
validate: async (response) => {
return response.completed >= response.total;
},
aggregator: async (response) => {
const percentage = Math.round((response.completed / response.total) * 100);
console.log(`Progress: ${percentage}% (${response.completed}/${response.total})`);
},
timeout: () => 500 // Brief pause between batches
});
console.log(`Processed ${batchResult.allResults.length} items`);Timeout Configuration:
// Linear backoff
timeout: () => 1000 // Always wait 1 second
// Exponential backoff
let retryCount = 0;
timeout: () => {
retryCount++;
return Math.min(Math.pow(2, retryCount) * 1000, 30000); // Cap at 30 seconds
}
// Jittered backoff (prevents thundering herd)
timeout: () => {
const baseDelay = Math.pow(2, retryCount) * 1000;
const jitter = Math.random() * 0.1 * baseDelay;
return baseDelay + jitter;
}Error Condition Design:
// Time-based timeout
const startTime = Date.now();
const TIMEOUT_MS = 30000; // 30 seconds
error: {
validate: async () => {
return Date.now() - startTime > TIMEOUT_MS;
},
message: async () => {
return `Operation timed out after ${TIMEOUT_MS}ms`;
}
}
// Attempt-based timeout
let attempts = 0;
const MAX_ATTEMPTS = 10;
error: {
validate: async () => {
attempts++;
return attempts >= MAX_ATTEMPTS;
},
message: async () => {
return `Failed after ${attempts} attempts`;
}
}The iterable promise pattern works well with transport operations:
import { createIterablePromise, createTransporter } from "@algolia/client-common";
// Wait for indexing task completion
const waitForTask = async (transporter: Transporter, taskID: number) => {
return createIterablePromise({
func: async () => {
return await transporter.request({
method: 'GET',
path: `/1/task/${taskID}`,
queryParameters: {},
headers: {}
});
},
validate: async (response: any) => {
return response.status === 'published';
},
timeout: () => 1000
});
};