UNPKG

@arizeai/phoenix-client

Version:

A client for the Phoenix API

385 lines 15.2 kB
"use strict"; /** * 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 * ``` */ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var g = generator.apply(thisArg, _arguments || []), i, q = []; return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i; function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; } function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } } function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } function fulfill(value) { resume("next", value); } function reject(value) { resume("throw", value); } function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } }; var _Channel_buffer, _Channel_sendQueue, _Channel_receiveQueue, _Channel_closed, _Channel_capacity; Object.defineProperty(exports, "__esModule", { value: true }); exports.CLOSED = exports.Channel = exports.ChannelError = void 0; exports.isClosed = isClosed; /** * Custom error class for channel operations */ class ChannelError extends Error { constructor(message, options) { super(message, options); this.name = "ChannelError"; } } exports.ChannelError = ChannelError; /** * Error messages for channel operations */ const ERRORS = { SEND_TO_CLOSED: "Cannot send to closed channel", CLOSED_WHILE_BLOCKED: "Channel closed while send was blocked", NEGATIVE_CAPACITY: "Channel capacity must be non-negative", }; class Channel { /** * 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 = 10) { _Channel_buffer.set(this, []); _Channel_sendQueue.set(this, []); _Channel_receiveQueue.set(this, []); _Channel_closed.set(this, false); _Channel_capacity.set(this, void 0); if (capacity < 0) { throw new ChannelError(ERRORS.NEGATIVE_CAPACITY); } __classPrivateFieldSet(this, _Channel_capacity, capacity, "f"); } /** * 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 */ async send(value) { if (__classPrivateFieldGet(this, _Channel_closed, "f")) { throw new ChannelError(ERRORS.SEND_TO_CLOSED); } // Direct delivery to waiting receiver const receiver = __classPrivateFieldGet(this, _Channel_receiveQueue, "f").shift(); if (receiver) { receiver.resolve(value); return; } // Add to buffer if space available if (__classPrivateFieldGet(this, _Channel_buffer, "f").length < __classPrivateFieldGet(this, _Channel_capacity, "f")) { __classPrivateFieldGet(this, _Channel_buffer, "f").push(value); return; } // Block until space available return new Promise((resolve, reject) => { __classPrivateFieldGet(this, _Channel_sendQueue, "f").push({ value, resolve, reject }); }); } /** * 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 */ async receive() { // Drain buffer first if (__classPrivateFieldGet(this, _Channel_buffer, "f").length > 0) { const value = __classPrivateFieldGet(this, _Channel_buffer, "f").shift(); // Unblock a waiting sender const sender = __classPrivateFieldGet(this, _Channel_sendQueue, "f").shift(); if (sender) { __classPrivateFieldGet(this, _Channel_buffer, "f").push(sender.value); sender.resolve(); } return value; } // Direct handoff from waiting sender (critical for unbuffered channels) const sender = __classPrivateFieldGet(this, _Channel_sendQueue, "f").shift(); if (sender) { sender.resolve(); return sender.value; } // Channel closed and empty if (__classPrivateFieldGet(this, _Channel_closed, "f")) { return exports.CLOSED; } // Block until value available return new Promise((resolve, reject) => { __classPrivateFieldGet(this, _Channel_receiveQueue, "f").push({ resolve, reject }); }); } /** * 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() { // Drain buffer first if (__classPrivateFieldGet(this, _Channel_buffer, "f").length > 0) { const value = __classPrivateFieldGet(this, _Channel_buffer, "f").shift(); // Unblock a waiting sender const sender = __classPrivateFieldGet(this, _Channel_sendQueue, "f").shift(); if (sender) { __classPrivateFieldGet(this, _Channel_buffer, "f").push(sender.value); sender.resolve(); } return value; } // Direct handoff from waiting sender const sender = __classPrivateFieldGet(this, _Channel_sendQueue, "f").shift(); if (sender) { sender.resolve(); return sender.value; } // Channel closed and empty if (__classPrivateFieldGet(this, _Channel_closed, "f")) { return exports.CLOSED; } // Channel empty but not closed return undefined; } /** * Close the channel * No more sends allowed, but remaining values can be received */ close() { if (__classPrivateFieldGet(this, _Channel_closed, "f")) return; __classPrivateFieldSet(this, _Channel_closed, true, "f"); // Resolve all blocked receivers for (const receiver of __classPrivateFieldGet(this, _Channel_receiveQueue, "f")) { receiver.resolve(exports.CLOSED); } __classPrivateFieldSet(this, _Channel_receiveQueue, [], "f"); // Reject all blocked senders const error = new ChannelError(ERRORS.CLOSED_WHILE_BLOCKED); for (const sender of __classPrivateFieldGet(this, _Channel_sendQueue, "f")) { sender.reject(error); } __classPrivateFieldSet(this, _Channel_sendQueue, [], "f"); } /** * Check if channel is closed */ get isClosed() { return __classPrivateFieldGet(this, _Channel_closed, "f"); } /** * Get current buffer length */ get length() { return __classPrivateFieldGet(this, _Channel_buffer, "f").length; } /** * Get the channel's capacity */ get capacity() { return __classPrivateFieldGet(this, _Channel_capacity, "f"); } /** * Get the number of blocked senders waiting */ get pendingSends() { return __classPrivateFieldGet(this, _Channel_sendQueue, "f").length; } /** * Get the number of blocked receivers waiting */ get pendingReceives() { return __classPrivateFieldGet(this, _Channel_receiveQueue, "f").length; } /** * Async iterator support for for-await-of loops */ [(_Channel_buffer = new WeakMap(), _Channel_sendQueue = new WeakMap(), _Channel_receiveQueue = new WeakMap(), _Channel_closed = new WeakMap(), _Channel_capacity = new WeakMap(), Symbol.asyncIterator)]() { return __asyncGenerator(this, arguments, function* _a() { while (true) { const value = yield __await(this.receive()); if (value === exports.CLOSED) return yield __await(void 0); yield yield __await(value); } }); } } exports.Channel = Channel; /** * Special symbol to indicate channel is closed */ exports.CLOSED = Symbol("CLOSED"); /** * 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); * } * ``` */ function isClosed(value) { return value === exports.CLOSED; } //# sourceMappingURL=channel.js.map