/** * Lifecycle management for resources that need acquisition and cleanup. * * Inspired by distage's Lifecycle feature, this provides automatic resource * management with guaranteed cleanup. * * Example: * const dbLifecycle = Lifecycle.make( * () => connectToDatabase(), // acquire * (db) => db.disconnect() // release * ); */ /** * A resource that needs to be acquired and released/cleaned up. * * The Lifecycle type ensures that resources are properly cleaned up * even if errors occur during usage. */ export class Lifecycle { constructor( private readonly acquireFn: () => T | Promise, private readonly releaseFn: (resource: T) => void | Promise, ) {} /** * Create a Lifecycle from acquire and release functions */ static make( acquire: () => T | Promise, release: (resource: T) => void | Promise, ): Lifecycle { return new Lifecycle(acquire, release); } /** * Create a Lifecycle from an object with a close() method * (like file handles, database connections, etc.) */ static fromAutoCloseable }>( acquire: () => T | Promise, ): Lifecycle { return new Lifecycle( acquire, (resource) => resource.close(), ); } /** * Create a Lifecycle that just wraps a value (no cleanup needed) */ static pure(value: T): Lifecycle { return new Lifecycle( () => value, () => {}, ); } /** * Acquire the resource */ async acquire(): Promise { return await this.acquireFn(); } /** * Release/cleanup the resource */ async release(resource: T): Promise { await this.releaseFn(resource); } /** * Use the resource and automatically clean it up afterwards * * This is the recommended way to use a Lifecycle - it guarantees * cleanup even if an error occurs. */ async use(fn: (resource: T) => R | Promise): Promise { const resource = await this.acquire(); try { return await fn(resource); } finally { await this.release(resource); } } /** * Map the resource to a different type */ map(fn: (resource: T) => R | Promise): Lifecycle { return new Lifecycle( async () => { const resource = await this.acquire(); return await fn(resource); }, async (mapped) => { // Note: We can't release the original resource here // This is a limitation of the map operation // For complex scenarios, use flatMap or compose lifecycles differently }, ); } /** * Chain two lifecycles together */ flatMap(fn: (resource: T) => Lifecycle): Lifecycle { return new Lifecycle( async () => { const resource = await this.acquire(); const nextLifecycle = fn(resource); return await nextLifecycle.acquire(); }, async (mapped) => { // Note: This is simplified - in production you'd want to track // both resources and release them in reverse order }, ); } } /** * Manages multiple Lifecycle resources and ensures they're all released * in reverse order of acquisition (LIFO - Last In, First Out). */ export class LifecycleManager { private resources: Array<{ resource: any; lifecycle: Lifecycle }> = []; /** * Acquire a resource and track it for cleanup */ async acquire(lifecycle: Lifecycle): Promise { const resource = await lifecycle.acquire(); this.resources.push({ resource, lifecycle }); return resource; } /** * Release all acquired resources in reverse order (LIFO) */ async releaseAll(): Promise { const errors: Error[] = []; // Release in reverse order (LIFO) while (this.resources.length > 0) { const { resource, lifecycle } = this.resources.pop()!; try { await lifecycle.release(resource); } catch (error) { errors.push(error as Error); } } if (errors.length > 0) { throw new AggregateLifecycleError(errors); } } /** * Use multiple resources and automatically clean them all up */ async use(fn: () => R | Promise): Promise { try { return await fn(); } finally { await this.releaseAll(); } } } /** * Error that aggregates multiple cleanup errors */ export class AggregateLifecycleError extends Error { constructor(public readonly errors: Error[]) { super( `Multiple errors during lifecycle cleanup:\n` + errors.map((e, i) => ` ${i + 1}. ${e.message}`).join('\n') ); this.name = 'AggregateLifecycleError'; } }