UNPKG

@arizeai/phoenix-client

Version:

A client for the Phoenix API

229 lines 7.47 kB
/** * A bounded, buffered CSP channel implementation for TypeScript. * * Implements the Producer-Consumer pattern with automatic backpressure via * blocking send/receive semantics. Based on Communicating Sequential Processes (Hoare, 1978). * * Properties: * - Bounded buffer: O(capacity) memory usage * - Blocking send: Blocks when buffer is full * - Blocking receive: Blocks when buffer is empty * - Graceful shutdown: Close drains buffer before terminating * * Performance Characteristics: * - send(): O(R) where R = pending receivers (typically 0-10) * - receive(): O(B + S) where B = buffer size, S = pending senders * - Uses Array.shift() which is O(n) but acceptable for small queues * - Same complexity trade-off as async.queue, p-limit, and similar libraries * - For typical usage (buffer < 100, queues < 10), overhead is negligible (<10ms per 5000 operations) * * Note: Could be optimized to O(1) with linked list or circular buffer, but current * implementation prioritizes simplicity and is comparable to standard JS libraries. * * Deadlock Prevention: * JavaScript channels use cooperative blocking via Promises, not true thread blocking. * Deadlocks are rare but possible in certain patterns: * * ❌ AVOID: Sequential operations on unbuffered channels * ```typescript * const ch = new Channel<number>(0); * await ch.send(1); // Blocks forever - no receiver started * await ch.receive(); // Never reached * ``` * * ❌ AVOID: Circular dependencies between channels * ```typescript * const ch1 = new Channel(0); * const ch2 = new Channel(0); * // Task 1: await ch1.send() → await ch2.receive() * // Task 2: await ch2.send() → await ch1.receive() * // Both block on send, never reach receive * ``` * * ✅ SAFE: Concurrent start with buffered channels (recommended pattern) * ```typescript * const ch = new Channel<number>(); // Default (10) is safe * * // Start producer immediately * const producer = (async () => { * for (let i = 0; i < 100; i++) { * await ch.send(i); * } * ch.close(); // Always close in finally block * })(); * * // Start consumers immediately * const consumers = Array.from({ length: 5 }, async () => { * for await (const value of ch) { * await processValue(value); * } * }); * * // Wait for all to complete * await Promise.all([producer, ...consumers]); * ``` * * Best Practices: * 1. Use default capacity or higher (10+) for production - provides safety and throughput * 2. Always close() channels in a finally block to prevent hanging operations * 3. Start producers and consumers concurrently, not sequentially * 4. Use for-await loops for automatic cleanup on close * 5. Avoid circular dependencies between channels * 6. Handle errors in workers so they don't crash and leave channel blocked * 7. Only use unbuffered (capacity=0) when you need strict happens-before guarantees * * @see https://en.wikipedia.org/wiki/Communicating_sequential_processes * * @template T The type of values sent through the channel * * @example Safe Producer-Consumer Pattern * ```typescript * // Default capacity (10) is safe for most cases * const ch = new Channel<number>(); // or explicit: new Channel<number>(50) * * // Producer with proper cleanup * const producer = (async () => { * try { * for (let i = 0; i < 100; i++) { * await ch.send(i); // Blocks if buffer full (backpressure) * } * } finally { * ch.close(); // Guaranteed cleanup * } * })(); * * // Multiple consumers * const consumers = Array.from({ length: 3 }, async () => { * for await (const value of ch) { * console.log(value); * } * }); * * await Promise.all([producer, ...consumers]); * ``` * * @example Unbuffered Channel (Rendezvous) * ```typescript * const ch = new Channel<number>(0); // Unbuffered - use with care! * * // Must start both operations before awaiting * const sendPromise = ch.send(42); // Starts but doesn't block caller yet * const value = await ch.receive(); // Unblocks the sender * await sendPromise; // Now safe to await * ``` */ /** * Custom error class for channel operations */ export declare class ChannelError extends Error { constructor(message: string, options?: ErrorOptions); } export declare class Channel<T> { #private; /** * Create a new channel with the specified buffer capacity. * * @param capacity - Buffer size (default: 10) * - 0: Unbuffered/rendezvous channel - strict synchronization, higher deadlock risk. * Use only when you need guaranteed happens-before ordering. * - 1-100: Buffered channel - recommended for production use. * - Higher values: Better throughput but more memory usage. * * @example * ```typescript * // Default buffered (safe for most cases) * const ch1 = new Channel<number>(); * * // Explicit buffer size (production pattern) * const ch2 = new Channel<number>(50); * * // Unbuffered (advanced - strict synchronization) * const ch3 = new Channel<number>(0); * ``` */ constructor(capacity?: number); /** * Send a value to the channel * Blocks if the buffer is full until space is available * * @param value - The value to send * @throws {ChannelError} If channel is closed */ send(value: T): Promise<void>; /** * Receive a value from the channel * Blocks if no value is available until one arrives * * @returns The received value, or CLOSED symbol if channel is closed and empty */ receive(): Promise<T | typeof CLOSED>; /** * Try to receive a value without blocking * Returns immediately with value or undefined if channel is empty * * @returns The received value, CLOSED if channel is closed, or undefined if empty * * @example * ```typescript * const ch = new Channel<number>(10); * await ch.send(42); * * const value = ch.tryReceive(); * if (value !== undefined && value !== CLOSED) { * console.log("Got:", value); * } * ``` */ tryReceive(): T | typeof CLOSED | undefined; /** * Close the channel * No more sends allowed, but remaining values can be received */ close(): void; /** * Check if channel is closed */ get isClosed(): boolean; /** * Get current buffer length */ get length(): number; /** * Get the channel's capacity */ get capacity(): number; /** * Get the number of blocked senders waiting */ get pendingSends(): number; /** * Get the number of blocked receivers waiting */ get pendingReceives(): number; /** * Async iterator support for for-await-of loops */ [Symbol.asyncIterator](): AsyncIterableIterator<T>; } /** * Special symbol to indicate channel is closed */ export declare const CLOSED: unique symbol; /** * Type guard to check if a value is the CLOSED symbol * * @param value - Value to check * @returns true if value is CLOSED symbol * * @example * ```typescript * const value = await ch.receive(); * if (isClosed(value)) { * console.log("Channel is closed"); * } else { * console.log("Got value:", value); * } * ``` */ export declare function isClosed<T>(value: T | typeof CLOSED): value is typeof CLOSED; //# sourceMappingURL=channel.d.ts.map