alepha
Version: 
Alepha is a convention-driven TypeScript framework for building robust, end-to-end type-safe applications, from serverless APIs to full-stack React apps.
552 lines (551 loc) • 20.6 kB
TypeScript
import * as _alepha_core1 from "alepha";
import { AsyncFn, Descriptor, KIND, Static } from "alepha";
import * as _alepha_topic0 from "alepha/topic";
import { TopicProvider } from "alepha/topic";
import { DateTime, DateTimeProvider, DurationLike, Timeout } from "alepha/datetime";
import * as _alepha_logger0 from "alepha/logger";
import * as dayjs_plugin_duration0 from "dayjs/plugin/duration";
//#region src/providers/LockProvider.d.ts
/**
 * Store Provider Interface
 */
declare abstract class LockProvider {
  /**
   * Set the string value of a key.
   *
   * @param key The key of the value to set.
   * @param value The value to set.
   * @param nx If set to true, the key will only be set if it does not already exist.
   * @param px Set the specified expire time, in milliseconds.
   */
  abstract set(key: string, value: string, nx?: boolean, px?: number): Promise<string>;
  /**
   * Remove the specified keys.
   *
   * @param keys The keys to delete.
   */
  abstract del(...keys: string[]): Promise<void>;
}
//#endregion
//#region src/descriptors/$lock.d.ts
/**
 * Creates a distributed lock descriptor for ensuring single-instance execution across processes.
 *
 * This descriptor provides a powerful distributed locking mechanism that prevents multiple instances
 * of the same operation from running simultaneously. It's essential for maintaining data consistency
 * and preventing race conditions in distributed applications, scheduled tasks, and critical sections
 * that must execute atomically.
 *
 * **Key Features**
 *
 * - **Distributed Coordination**: Works across multiple processes, servers, and containers
 * - **Automatic Expiration**: Locks expire automatically to prevent deadlocks
 * - **Graceful Handling**: Configurable wait behavior for different use cases
 * - **Grace Periods**: Optional lock extension after completion for additional safety
 * - **Topic Integration**: Uses pub/sub for efficient lock release notifications
 * - **Unique Instance IDs**: Prevents lock conflicts between different instances
 * - **Timeout Management**: Configurable durations with intelligent retry logic
 *
 * **Use Cases**
 *
 * Perfect for ensuring single execution in distributed environments:
 * - Database migrations and schema updates
 * - Scheduled job execution (cron-like tasks)
 * - File processing and batch operations
 * - Critical section protection
 * - Resource initialization and cleanup
 * - Singleton service operations
 * - Cache warming and maintenance tasks
 *
 * @example
 * **Basic lock for scheduled tasks:**
 * ```ts
 * import { $lock } from "alepha/lock";
 *
 * class ScheduledTaskService {
 *   dailyReport = $lock({
 *     handler: async () => {
 *       // This will only run on one server even if multiple servers
 *       // trigger the task simultaneously
 *       console.log('Generating daily report...');
 *
 *       const report = await this.generateDailyReport();
 *       await this.sendReportToManagement(report);
 *
 *       console.log('Daily report completed');
 *     }
 *   });
 *
 *   async runDailyReport() {
 *     // Multiple servers can call this, but only one will execute
 *     await this.dailyReport.run();
 *   }
 * }
 * ```
 *
 * @example
 * **Migration lock with wait behavior:**
 * ```ts
 * class DatabaseService {
 *   migration = $lock({
 *     wait: true,  // Wait for other instances to complete migration
 *     maxDuration: [10, "minutes"],  // Migration timeout
 *     handler: async (version: string) => {
 *       console.log(`Running migration to version ${version}`);
 *
 *       const currentVersion = await this.getCurrentSchemaVersion();
 *       if (currentVersion >= version) {
 *         console.log(`Already at version ${version}, skipping`);
 *         return;
 *       }
 *
 *       await this.runMigrationScripts(version);
 *       await this.updateSchemaVersion(version);
 *
 *       console.log(`Migration to ${version} completed`);
 *     }
 *   });
 *
 *   async migrateToVersion(version: string) {
 *     // All instances will wait for the first one to complete
 *     // before continuing with their startup process
 *     await this.migration.run(version);
 *   }
 * }
 * ```
 *
 * @example
 * **Dynamic lock keys with grace periods:**
 * ```ts
 * class FileProcessor {
 *   processFile = $lock({
 *     name: (filePath: string) => `file-processing:${filePath}`,
 *     wait: false,  // Don't wait, skip if already processing
 *     maxDuration: [30, "minutes"],
 *     gracePeriod: [5, "minutes"],  // Keep lock for 5min after completion
 *     handler: async (filePath: string) => {
 *       console.log(`Processing file: ${filePath}`);
 *
 *       try {
 *         const fileData = await this.readFile(filePath);
 *         const processedData = await this.processData(fileData);
 *         await this.saveProcessedData(filePath, processedData);
 *         await this.moveToCompleted(filePath);
 *
 *         console.log(`File processing completed: ${filePath}`);
 *       } catch (error) {
 *         console.error(`File processing failed: ${filePath}`, error);
 *         await this.moveToError(filePath, error.message);
 *         throw error;
 *       }
 *     }
 *   });
 *
 *   async processUploadedFile(filePath: string) {
 *     // Each file gets its own lock, preventing duplicate processing
 *     // Grace period prevents immediate reprocessing of the same file
 *     await this.processFile.run(filePath);
 *   }
 * }
 * ```
 *
 * @example
 * **Resource initialization with conditional grace periods:**
 * ```ts
 * class CacheService {
 *   warmCache = $lock({
 *     name: (cacheKey: string) => `cache-warming:${cacheKey}`,
 *     wait: true,  // Wait for cache to be warmed before continuing
 *     maxDuration: [15, "minutes"],
 *     gracePeriod: (cacheKey: string) => {
 *       // Dynamic grace period based on cache importance
 *       const criticalCaches = ['user-sessions', 'product-catalog'];
 *       return criticalCaches.includes(cacheKey)
 *         ? [30, "minutes"]  // Longer grace for critical caches
 *         : [5, "minutes"];   // Shorter grace for regular caches
 *     },
 *     handler: async (cacheKey: string, force: boolean = false) => {
 *       console.log(`Warming cache: ${cacheKey}`);
 *
 *       if (!force && await this.isCacheWarm(cacheKey)) {
 *         console.log(`Cache ${cacheKey} is already warm`);
 *         return;
 *       }
 *
 *       const startTime = Date.now();
 *
 *       switch (cacheKey) {
 *         case 'user-sessions':
 *           await this.warmUserSessionsCache();
 *           break;
 *         case 'product-catalog':
 *           await this.warmProductCatalogCache();
 *           break;
 *         case 'configuration':
 *           await this.warmConfigurationCache();
 *           break;
 *         default:
 *           throw new Error(`Unknown cache key: ${cacheKey}`);
 *       }
 *
 *       const duration = Date.now() - startTime;
 *       console.log(`Cache warming completed for ${cacheKey} in ${duration}ms`);
 *
 *       await this.markCacheAsWarm(cacheKey);
 *     }
 *   });
 *
 *   async ensureCacheWarmed(cacheKey: string, force: boolean = false) {
 *     // Multiple instances can call this, but cache warming happens only once
 *     // All instances wait for completion before proceeding
 *     await this.warmCache.run(cacheKey, force);
 *   }
 * }
 * ```
 *
 * @example
 * **Critical section protection with custom timeout handling:**
 * ```ts
 * class InventoryService {
 *   updateInventory = $lock({
 *     name: (productId: string) => `inventory-update:${productId}`,
 *     wait: true,  // Ensure all inventory updates are sequential
 *     maxDuration: [2, "minutes"],
 *     gracePeriod: [30, "seconds"],  // Brief grace to prevent immediate conflicts
 *     handler: async (productId: string, quantity: number, operation: 'add' | 'subtract') => {
 *       console.log(`Updating inventory for product ${productId}: ${operation} ${quantity}`);
 *
 *       try {
 *         // Start transaction for inventory update
 *         await this.db.transaction(async (tx) => {
 *           const currentInventory = await tx.getInventory(productId);
 *
 *           if (operation === 'subtract' && currentInventory.quantity < quantity) {
 *             throw new Error(`Insufficient inventory for product ${productId}. Available: ${currentInventory.quantity}, Requested: ${quantity}`);
 *           }
 *
 *           const newQuantity = operation === 'add'
 *             ? currentInventory.quantity + quantity
 *             : currentInventory.quantity - quantity;
 *
 *           await tx.updateInventory(productId, newQuantity);
 *
 *           // Log inventory change for audit
 *           await tx.logInventoryChange({
 *             productId,
 *             operation,
 *             quantity,
 *             previousQuantity: currentInventory.quantity,
 *             newQuantity,
 *             timestamp: new Date()
 *           });
 *
 *           console.log(`Inventory updated for product ${productId}: ${currentInventory.quantity} -> ${newQuantity}`);
 *         });
 *
 *         // Notify other services about inventory change
 *         await this.inventoryChangeNotifier.notify({
 *           productId,
 *           operation,
 *           quantity,
 *           timestamp: new Date()
 *         });
 *
 *       } catch (error) {
 *         console.error(`Inventory update failed for product ${productId}`, error);
 *         throw error;
 *       }
 *     }
 *   });
 *
 *   async addInventory(productId: string, quantity: number) {
 *     await this.updateInventory.run(productId, quantity, 'add');
 *   }
 *
 *   async subtractInventory(productId: string, quantity: number) {
 *     await this.updateInventory.run(productId, quantity, 'subtract');
 *   }
 * }
 * ```
 */
declare const $lock: {
  <TFunc extends AsyncFn>(options: LockDescriptorOptions<TFunc>): LockDescriptor<TFunc>;
  [KIND]: typeof LockDescriptor;
};
interface LockDescriptorOptions<TFunc extends AsyncFn> {
  /**
   * The function to execute when the lock is successfully acquired.
   *
   * This function:
   * - Only executes on the instance that successfully acquires the lock
   * - Has exclusive access to the protected resource during execution
   * - Should contain the critical section logic that must not run concurrently
   * - Can be async and perform any operations needed
   * - Will automatically release the lock upon completion or error
   * - Has access to the full Alepha dependency injection container
   *
   * **Handler Design Guidelines**:
   * - Keep critical sections as short as possible to minimize lock contention
   * - Include proper error handling to ensure locks are released
   * - Use timeouts for external operations to prevent deadlocks
   * - Log important operations for debugging and monitoring
   * - Consider idempotency for handlers that might be retried
   *
   * @param ...args - The arguments passed to the lock execution
   * @returns Promise that resolves when the protected operation is complete
   *
   * @example
   * ```ts
   * handler: async (batchId: string) => {
   *   console.log(`Processing batch ${batchId} - only one instance will run this`);
   *
   *   const batch = await this.getBatchData(batchId);
   *   const results = await this.processBatchItems(batch.items);
   *   await this.saveBatchResults(batchId, results);
   *
   *   console.log(`Batch ${batchId} completed successfully`);
   * }
   * ```
   */
  handler: TFunc;
  /**
   * Whether the lock should wait for other instances to complete before giving up.
   *
   * **wait = false (default)**:
   * - Non-blocking behavior - if lock is held, immediately return without executing
   * - Perfect for scheduled tasks where you only want one execution per trigger
   * - Use when multiple triggers are acceptable but concurrent execution is not
   * - Examples: periodic cleanup, cron jobs, background maintenance
   *
   * **wait = true**:
   * - Blocking behavior - wait for the current lock holder to finish
   * - All instances will eventually execute (one after another)
   * - Perfect for initialization tasks where all instances need the work completed
   * - Examples: database migrations, cache warming, resource initialization
   *
   * **Trade-offs**:
   * - Non-waiting: Better performance, may miss executions if timing is off
   * - Waiting: Guaranteed execution order, slower overall throughput
   *
   * @default false
   *
   * @example
   * ```ts
   * // Scheduled task - don't wait, just skip if already running
   * scheduledCleanup = $lock({
   *   wait: false,  // Skip if cleanup already running
   *   handler: async () => { } // perform cleanup
   * });
   *
   * // Migration - wait for completion before proceeding
   * migration = $lock({
   *   wait: true,   // All instances wait for migration to complete
   *   handler: async () => { } // perform migration
   * });
   * ```
   */
  wait?: boolean;
  /**
   * The unique identifier for the lock.
   *
   * Can be either:
   * - **Static string**: A fixed identifier for the lock
   * - **Dynamic function**: A function that generates the lock key based on arguments
   *
   * **Dynamic Lock Keys**:
   * - Enable per-resource locking (e.g., per-user, per-file, per-product)
   * - Allow fine-grained concurrency control
   * - Prevent unnecessary blocking between unrelated operations
   *
   * **Key Design Guidelines**:
   * - Use descriptive names that indicate the protected resource
   * - Include relevant identifiers for dynamic keys
   * - Keep keys reasonably short but unique
   * - Consider using hierarchical naming (e.g., "service:operation:resource")
   *
   * If not provided, defaults to `{serviceName}:{propertyKey}`.
   *
   * @example "user-migration"
   * @example "daily-report-generation"
   * @example (userId: string) => `user-profile-update:${userId}`
   * @example (fileId: string, operation: string) => `file-${operation}:${fileId}`
   *
   * @example
   * ```ts
   * // Static lock key - all instances compete for the same lock
   * globalCleanup = $lock({
   *   name: "system-cleanup",
   *   handler: async () => { } // perform cleanup
   * });
   *
   * // Dynamic lock key - per-user locks, users don't block each other
   * updateUserProfile = $lock({
   *   name: (userId: string) => `user-update:${userId}`,
   *   handler: async (userId: string, data: UserData) => {
   *     // Only one update per user at a time, but different users can update concurrently
   *   }
   * });
   * ```
   */
  name?: string | ((...args: Parameters<TFunc>) => string);
  /**
   * Maximum duration the lock can be held before it expires automatically.
   *
   * This prevents deadlocks when a process dies while holding a lock or when
   * operations take longer than expected. The lock will be automatically released
   * after this duration, allowing other instances to proceed.
   *
   * **Duration Guidelines**:
   * - Set based on expected operation duration plus safety margin
   * - Too short: Operations may be interrupted by early expiration
   * - Too long: Failed processes block others for extended periods
   * - Consider worst-case scenarios and external dependency timeouts
   *
   * **Typical Values**:
   * - Quick operations: 30 seconds - 2 minutes
   * - Database operations: 5 - 15 minutes
   * - File processing: 10 - 30 minutes
   * - Large migrations: 30 minutes - 2 hours
   *
   * @default [5, "minutes"]
   *
   * @example [30, "seconds"]    // Quick operations
   * @example [10, "minutes"]    // Database migrations
   * @example [1, "hour"]        // Long-running batch jobs
   *
   * @example
   * ```ts
   * quickTask = $lock({
   *   maxDuration: [2, "minutes"],  // Quick timeout for fast operations
   *   handler: async () => { } // perform quick task
   * });
   *
   * heavyProcessing = $lock({
   *   maxDuration: [30, "minutes"], // Longer timeout for heavy work
   *   handler: async () => { } // perform heavy processing
   * });
   * ```
   */
  maxDuration?: DurationLike;
  /**
   * Additional time to keep the lock active after the handler completes successfully.
   *
   * This provides a "cooling off" period that can be useful for:
   * - Preventing immediate re-execution of the same operation
   * - Giving time for related systems to process the results
   * - Avoiding race conditions with dependent operations
   * - Providing a buffer for cleanup operations
   *
   * Can be either:
   * - **Static duration**: Fixed grace period for all executions
   * - **Dynamic function**: Grace period determined by execution arguments
   * - **undefined**: No grace period, lock released immediately after completion
   *
   * **Grace Period Use Cases**:
   * - File processing: Prevent immediate reprocessing of uploaded files
   * - Cache updates: Allow time for cache propagation
   * - Batch operations: Prevent overlapping batch processing
   * - External API calls: Respect rate limiting requirements
   *
   * @default undefined (no grace period)
   *
   * @example [5, "minutes"]     // Fixed 5-minute grace period
   * @example [30, "seconds"]    // Short grace for quick operations
   * @example (userId: string) => userId.startsWith("premium") ? [10, "minutes"] : [2, "minutes"]
   *
   * @example
   * ```ts
   * fileProcessor = $lock({
   *   gracePeriod: [10, "minutes"],  // Prevent reprocessing same file immediately
   *   handler: async (filePath: string) => {
   *     await this.processFile(filePath);
   *   }
   * });
   *
   * userOperation = $lock({
   *   gracePeriod: (userId: string, operation: string) => {
   *     // Dynamic grace based on operation type
   *     return operation === 'migration' ? [30, "minutes"] : [5, "minutes"];
   *   },
   *   handler: async (userId: string, operation: string) => {
   *     await this.performUserOperation(userId, operation);
   *   }
   * });
   * ```
   */
  gracePeriod?: ((...args: Parameters<TFunc>) => DurationLike | undefined) | DurationLike;
}
declare const envSchema: _alepha_core1.TObject<{
  LOCK_PREFIX_KEY: _alepha_core1.TString;
}>;
declare module "alepha" {
  interface Env extends Partial<Static<typeof envSchema>> {}
}
declare class LockDescriptor<TFunc extends AsyncFn> extends Descriptor<LockDescriptorOptions<TFunc>> {
  protected readonly log: _alepha_logger0.Logger;
  protected readonly provider: LockProvider;
  protected readonly env: {
    LOCK_PREFIX_KEY: string;
  };
  protected readonly dateTimeProvider: DateTimeProvider;
  protected readonly id: `${string}-${string}-${string}-${string}-${string}`;
  readonly maxDuration: dayjs_plugin_duration0.Duration;
  protected readonly topicLockEnd: _alepha_topic0.TopicDescriptor<{
    payload: _alepha_core1.TObject<{
      name: _alepha_core1.TString;
    }>;
  }>;
  run(...args: Parameters<TFunc>): Promise<void>;
  /**
   * Set the lock for the given key.
   */
  protected lock(key: string): Promise<LockResult>;
  protected setGracePeriod(key: string, lock: LockResult, ...args: Parameters<TFunc>): Promise<void>;
  protected wait(key: string, maxDuration: DurationLike): Promise<void>;
  protected key(...args: Parameters<TFunc>): string;
  protected parse(value: string): LockResult;
}
interface LockResult {
  id: string;
  createdAt: DateTime;
  endedAt?: DateTime;
  response?: string;
}
//#endregion
//#region src/providers/LockTopicProvider.d.ts
declare abstract class LockTopicProvider extends TopicProvider {}
//#endregion
//#region src/providers/MemoryLockProvider.d.ts
/**
 * A simple in-memory store provider.
 */
declare class MemoryLockProvider implements LockProvider {
  protected readonly dateTimeProvider: DateTimeProvider;
  protected readonly log: _alepha_logger0.Logger;
  /**
   * The in-memory store.
   */
  protected store: Record<string, string>;
  /**
   * Timeouts used to expire keys.
   */
  protected storeTimeout: Record<string, Timeout>;
  set(key: string, value: string, nx?: boolean, px?: number): Promise<string>;
  del(...keys: string[]): Promise<void>;
  private ttl;
}
//#endregion
//#region src/index.d.ts
/**
 * Lock a resource for a certain period of time.
 *
 * This module provides a memory implementation of the lock provider.
 * You probably want to use an implementation like RedisLockProvider for distributed systems.
 *
 * @see {@link $lock}
 * @module alepha.lock
 */
declare const AlephaLock: _alepha_core1.Service<_alepha_core1.Module>;
//#endregion
export { $lock, AlephaLock, LockDescriptor, LockDescriptorOptions, LockProvider, LockResult, LockTopicProvider, MemoryLockProvider };
//# sourceMappingURL=index.d.ts.map