or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

async-operations.mdauthentication.mdcache.mderrors.mdindex.mdlogging.mdtransport.mduser-agent.md
tile.json

async-operations.mddocs/

Async Operations

Promise-based retry mechanisms with validation, aggregation, and timeout handling. Enables building resilient async operations that can retry until success conditions are met.

Capabilities

Create Iterable Promise

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;
  }
});

Advanced Patterns

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`);

Error Handling Best Practices

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`;
  }
}

Performance Considerations

  • Keep timeout values reasonable to avoid excessive waiting
  • Use aggregator for progress tracking and logging, not heavy computation
  • Consider memory usage for operations that accumulate data over iterations
  • Implement proper error conditions to avoid infinite loops
  • Use appropriate validation logic to minimize unnecessary iterations

Integration with Transport Layer

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
  });
};