mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
ea3813d3b3
* fix(daemon): isolate disconnect cancellation and resource teardown Cancel HTTP requests that lose their client before response headers, and scope disconnect cancellation to the affected request/device/session instead of a global Apple runner abort. Make session resource teardown failure-isolated so one rejected step no longer skips later cleanup, while preserving lease release and session deletion. Closes #1220 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(daemon,runner): request-scoped prep cancellation and platform-close error preservation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(daemon): split session-close teardown to satisfy complexity gate Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(daemon,runner): require pre-close runner stop and add integration prep-cancellation coverage Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(apple): preserve request cancellation during runner build Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(daemon): close request cancellation isolation gaps Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(request): keep cancellation cleanup owned Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
import { AppError } from '../kernel/errors.ts';
|
|
|
|
const canceledRequestIds = new Set<string>();
|
|
const requestAbortControllers = new Map<string, AbortController>();
|
|
const REQUEST_CANCELED_REASON = 'request_canceled';
|
|
const REQUEST_CANCELED_MESSAGE = 'request canceled';
|
|
const REQUEST_CANCELED_HINT =
|
|
'The request was canceled intentionally (explicit cancel or client disconnect) — no retry is needed unless the cancellation was unintended.';
|
|
|
|
export type RequestAbortRegistration = {
|
|
requestId: string;
|
|
controller: AbortController;
|
|
};
|
|
|
|
export function resolveRequestTrackingId(
|
|
requestId: string | undefined,
|
|
fallbackSeed?: unknown,
|
|
): string {
|
|
if (typeof requestId === 'string' && requestId.length > 0) return requestId;
|
|
const rawSeed =
|
|
typeof fallbackSeed === 'string'
|
|
? fallbackSeed
|
|
: typeof fallbackSeed === 'number' && Number.isFinite(fallbackSeed)
|
|
? String(fallbackSeed)
|
|
: 'generated';
|
|
const normalizedSeed =
|
|
rawSeed
|
|
.trim()
|
|
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
.slice(0, 32) || 'generated';
|
|
const nonce = Math.random().toString(36).slice(2, 10);
|
|
return `req:${normalizedSeed}:${process.pid}:${Date.now()}:${nonce}`;
|
|
}
|
|
|
|
const COLLECTION_HIGH_WATERMARK = 50_000;
|
|
const COLLECTION_EVICT_COUNT = 10_000;
|
|
|
|
function evictOldestEntries<K, V>(map: Map<K, V>): void {
|
|
if (map.size <= COLLECTION_HIGH_WATERMARK) return;
|
|
let removed = 0;
|
|
for (const key of map.keys()) {
|
|
if (removed >= COLLECTION_EVICT_COUNT) break;
|
|
map.delete(key);
|
|
removed++;
|
|
}
|
|
}
|
|
|
|
function evictOldestSetEntries<V>(set: Set<V>): void {
|
|
if (set.size <= COLLECTION_HIGH_WATERMARK) return;
|
|
let removed = 0;
|
|
for (const value of set) {
|
|
if (removed >= COLLECTION_EVICT_COUNT) break;
|
|
set.delete(value);
|
|
removed++;
|
|
}
|
|
}
|
|
|
|
export function registerRequestAbort(
|
|
requestId: string | undefined,
|
|
): RequestAbortRegistration | undefined {
|
|
if (!requestId) return undefined;
|
|
if (requestAbortControllers.has(requestId)) {
|
|
throw new AppError('INVALID_ARGS', `Request ID is already in flight: ${requestId}`, {
|
|
requestId,
|
|
reason: 'duplicate_request_id',
|
|
});
|
|
}
|
|
evictOldestEntries(requestAbortControllers);
|
|
const controller = new AbortController();
|
|
requestAbortControllers.set(requestId, controller);
|
|
if (canceledRequestIds.has(requestId)) {
|
|
controller.abort();
|
|
}
|
|
return { requestId, controller };
|
|
}
|
|
|
|
export function markRequestCanceled(requestId: string | undefined): void {
|
|
if (!requestId) return;
|
|
evictOldestSetEntries(canceledRequestIds);
|
|
canceledRequestIds.add(requestId);
|
|
requestAbortControllers.get(requestId)?.abort();
|
|
}
|
|
|
|
export function clearRequestCanceled(
|
|
requestId: string | undefined,
|
|
registration?: RequestAbortRegistration,
|
|
): void {
|
|
if (!requestId) return;
|
|
if (
|
|
registration &&
|
|
(registration.requestId !== requestId ||
|
|
requestAbortControllers.get(requestId) !== registration.controller)
|
|
) {
|
|
return;
|
|
}
|
|
canceledRequestIds.delete(requestId);
|
|
requestAbortControllers.delete(requestId);
|
|
}
|
|
|
|
export function clearRequestAbortRegistration(
|
|
registration: RequestAbortRegistration | undefined,
|
|
): void {
|
|
if (!registration) return;
|
|
clearRequestCanceled(registration.requestId, registration);
|
|
}
|
|
|
|
export function isRequestCanceled(requestId: string | undefined): boolean {
|
|
if (!requestId) return false;
|
|
return canceledRequestIds.has(requestId);
|
|
}
|
|
|
|
export function getRequestSignal(requestId: string | undefined): AbortSignal | undefined {
|
|
if (!requestId) return undefined;
|
|
return requestAbortControllers.get(requestId)?.signal;
|
|
}
|
|
|
|
export function createRequestCanceledError(): AppError {
|
|
return new AppError('COMMAND_FAILED', REQUEST_CANCELED_MESSAGE, {
|
|
reason: REQUEST_CANCELED_REASON,
|
|
hint: REQUEST_CANCELED_HINT,
|
|
});
|
|
}
|
|
|
|
export function throwIfRequestCanceled(requestId: string | undefined): void {
|
|
if (isRequestCanceled(requestId)) {
|
|
throw createRequestCanceledError();
|
|
}
|
|
}
|
|
|
|
export function isRequestCanceledError(error: unknown): boolean {
|
|
if (!(error instanceof AppError)) return false;
|
|
if (error.code !== 'COMMAND_FAILED') return false;
|
|
if (error.details?.reason === REQUEST_CANCELED_REASON) return true;
|
|
return error.message === REQUEST_CANCELED_MESSAGE;
|
|
}
|