UNPKG

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
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