@arizeai/phoenix-client
Version:
A client for the Phoenix API
385 lines • 15.2 kB
JavaScript
"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