UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

110 lines (109 loc) • 4.6 kB
/** * Out-of-band cancellation for chained async streams. * * `AsyncGenerator.prototype.return()` is the obvious way to tell a stream its * consumer has gone away, and it does not work through a chain of generators. * The request is *queued* behind whatever `next()` is already in flight; it * cannot interrupt an `await`. A provider stream is wrapped several times * (lifecycle in `baseProvider`, MCP and pool wrappers in `neurolink`), and * during a pull every one of those layers is parked inside an `await` on the * layer below. So none of them can unwind, the innermost source is never * closed, and a teardown that waits for that to happen waits forever. * * This module is the side channel that does work. A wrapper registers a * cancel callback on the stream object it returns; the callback closes its own * upstream iterator and forwards the request further down. Because these are * ordinary function calls rather than generator protocol, they run immediately * — no queue, no dependence on any pending `next()` settling. * * Registration is optional and reading is defensive, so a stream that knows * nothing about this behaves exactly as it did before. */ const STREAM_CANCEL = Symbol.for("neurolink.streamCancel"); /** * Swallow a rejection from a thenable teardown result. * * An unhandled rejection **terminates the process** — a caller doing everything * right still dies, with the stack pointing at library internals it never * called. Teardown runs after the consumer has stopped listening, so there is * nobody left to hand the error to; dropping it is the only correct outcome, * and dropping it deliberately is what keeps it from becoming fatal. */ function suppressRejection(result) { if (result !== null && typeof result === "object" && typeof result.then === "function" && typeof result.catch === "function") { void result.catch(() => { }); } } /** * Register `cancel` as `stream`'s teardown hook and return the same object. * * Non-enumerable so the property never shows up in spreads, `JSON.stringify` * or logging of a stream handle. */ export function attachStreamCancel(stream, cancel) { Object.defineProperty(stream, STREAM_CANCEL, { value: cancel, enumerable: false, configurable: true, writable: true, }); return stream; } /** * Invoke `stream`'s cancel hook if it has one. * * Never throws. This runs from `finally` blocks during teardown, where the * consumer has already stopped listening — an exception here would replace * whatever outcome the caller was actually returning with a cleanup error. */ export function cancelStream(stream) { if (stream === null || stream === undefined) { return; } if (typeof stream !== "object" && typeof stream !== "function") { return; } try { // The property read is inside the try, not before it. Reading a symbol off // an arbitrary object can execute user code — a Proxy trap or a throwing // getter — and this function is documented as never throwing because it // runs from teardown `finally` blocks, where an exception would replace // the outcome the caller was actually returning with a cleanup error. const hook = stream[STREAM_CANCEL]; if (typeof hook !== "function") { return; } // `attachStreamCancel` types the hook as `() => void`, but nothing stops a // caller registering an `async` function — whose rejection would otherwise // be unhandled, and therefore fatal to the process. suppressRejection(hook()); } catch { // Teardown is best-effort by definition. } } /** * Close `iterator` without waiting for it and without surfacing a rejection. * * Deliberately not awaited: on a generator that is parked mid-`await` this * promise may never settle, which is the whole failure this module exists to * avoid. The call still propagates whenever the iterator can act on it. */ export function releaseIterator(iterator) { try { // Read the method inside the try, for the same reason `cancelStream` does: // `iterator` is whatever an upstream handed us, and a Proxy trap or a // throwing getter turns a property access into arbitrary user code. const release = iterator.return; if (typeof release !== "function") { return; } suppressRejection(release.call(iterator, undefined)); } catch { // Best-effort, as above. } }