@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
150 lines (149 loc) • 4.94 kB
JavaScript
//#region src/utilities/activity-abort.ts
/**
* Shared abort/timeout composition for media (and summarize) activities.
*
* Callers pass optional `timeout` and/or `abortSignal` on activity options.
* Core composes them into one effective signal, races the adapter call so a
* hung provider still rejects, clears timeout resources on settle, and
* classifies aborts so lifecycle middleware gets `onAbort` rather than
* `onError`.
*/
var ABORT_ERROR_NAMES = /* @__PURE__ */ new Set([
"AbortError",
"TimeoutError",
"APIUserAbortError",
"RequestAbortedError"
]);
/**
* Combine two optional AbortSignals into one that aborts when either does.
* Returns the other signal directly when one is absent or already aborted.
* First abort wins and preserves its reason.
*
* Manual implementation — `AbortSignal.any` requires Node >= 20.3.
*/
function combineAbortSignals(a, b) {
if (!a) return b;
if (!b) return a;
if (a.aborted) return a;
if (b.aborted) return b;
const controller = new AbortController();
const onAbort = (source) => () => {
controller.abort(source.reason);
};
a.addEventListener("abort", onAbort(a), { once: true });
b.addEventListener("abort", onAbort(b), { once: true });
return controller.signal;
}
function createTimeoutReason(ms) {
if (typeof DOMException !== "undefined") return new DOMException(`Activity timed out after ${ms}ms`, "TimeoutError");
const err = /* @__PURE__ */ new Error(`Activity timed out after ${ms}ms`);
err.name = "TimeoutError";
return err;
}
/** Normalize an abort reason into an Error the activity can reject with. */
function toAbortError(reason) {
if (reason instanceof Error) return reason;
if (typeof reason === "string" && reason.length > 0) {
const err = new Error(reason);
err.name = "AbortError";
return err;
}
const err = /* @__PURE__ */ new Error("The operation was aborted");
err.name = "AbortError";
return err;
}
/**
* Compose an activity-level timeout with a caller AbortSignal.
*
* - No SDK-wide default timeout; omit both for unlimited wait.
* - First of caller cancellation or timeout wins and keeps its reason.
* - Call `clear()` when the activity settles (success or failure) so timers
* do not leak.
*/
function createActivityAbortControls(options) {
let timeoutId;
let timeoutSignal;
if (options.timeout !== void 0) {
if (!Number.isFinite(options.timeout) || options.timeout < 0) throw new Error(`Invalid activity timeout: expected a non-negative finite number, got ${String(options.timeout)}`);
const controller = new AbortController();
timeoutSignal = controller.signal;
const ms = options.timeout;
timeoutId = setTimeout(() => {
controller.abort(createTimeoutReason(ms));
}, ms);
}
return {
signal: combineAbortSignals(options.abortSignal, timeoutSignal),
clear: () => {
if (timeoutId !== void 0) {
clearTimeout(timeoutId);
timeoutId = void 0;
}
}
};
}
/**
* Reject when `signal` aborts, even if the underlying promise ignores it.
* Ensures activity-level timeouts work for adapters that do not yet forward
* the signal to the provider SDK.
*
* When the signal wins, the adapter promise is observed with an empty handler
* so a later settle cannot surface as an unhandled rejection.
*/
function raceWithAbort(promise, signal) {
if (!signal) return promise;
const swallow = () => {
promise.then(() => void 0, () => void 0);
};
if (signal.aborted) {
swallow();
return Promise.reject(toAbortError(signal.reason));
}
return new Promise((resolve, reject) => {
let settled = false;
const onAbort = () => {
if (settled) return;
settled = true;
cleanup();
swallow();
reject(toAbortError(signal.reason));
};
const cleanup = () => {
signal.removeEventListener("abort", onAbort);
};
signal.addEventListener("abort", onAbort, { once: true });
promise.then((value) => {
if (settled) return;
settled = true;
cleanup();
resolve(value);
}, (error) => {
if (settled) return;
settled = true;
cleanup();
reject(error);
});
});
}
/**
* Whether a thrown value (and optional effective signal) should route to
* middleware `onAbort` instead of `onError`.
*/
function isActivityAbortError(error, signal) {
if (signal?.aborted) return true;
if (!error || typeof error !== "object") return false;
const name = error.name;
return typeof name === "string" && ABORT_ERROR_NAMES.has(name);
}
/** Best-effort string reason for {@link GenerationAbortInfo}. */
function abortReasonMessage(error, signal) {
if (signal?.reason !== void 0) {
if (typeof signal.reason === "string") return signal.reason;
if (signal.reason instanceof Error) return signal.reason.message;
}
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
}
//#endregion
export { abortReasonMessage, combineAbortSignals, createActivityAbortControls, isActivityAbortError, raceWithAbort, toAbortError };
//# sourceMappingURL=activity-abort.js.map