mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
7a25a02f6d
* fix(record): replay the finished export from a retried record stop A remote record stop can outlive its client window while the daemon is still exporting. The finished manifest was then read as no active recording, and its metadata carried no client output path, so the caller had no way to collect the file. A repeated record stop now serves the completed export and says so in the timeout hint. * refactor(record): declare each completion codec once A mapped codec per completion property drives encoding and decoding from one declaration, and the declaration fails to typecheck if a property has no codec. * fix(record): keep manifest encoding inside the session resource module Session teardown reaches the recording resource definition while it loads, and that eager closure takes no new module. Writing a completion is property reads only, so the field map and writers now live with the resource definition; reading one back needs the recording vocabulary and stays behind the stop path. * test(client): give the request timeout hint its own mirror file The hint assertions had outgrown the aggregate client test past its size ratchet; they mirror src/daemon-client/daemon-client-timeout.ts, so they move rather than shrink. * refactor(record): store the finished stop response under one manifest key The manifest now holds the completion as the one object record stop returned, so a replay cannot lose a field between an encoder and a decoder, and the reader lives with the stop path that needs it. Recovery still refuses a response whose served path or caller-side paths are not whole. * refactor(record): reuse the scope guard and record path their owners declare A stored scope is checked by isRecordingScope next to the vocabulary it validates, and a session's durable record path comes from the factory that names it instead of being re-derived at each read. * refactor(client): hand the timed-out request to its timeout handler Command, session, and action all come from the same request, so they are passed as one request instead of three more positional arguments. * refactor(client): name the timed-out request fields the handler reads The client timeout handler stays off the daemon request shape: R10 daemon-modularity holds external importers of that module at the merge-base count, so the fields arrive as named properties instead of the request object. * refactor(client): read a timed-out request's fields once for both transports A socket timeout and an HTTP timeout described the same request with two copies of the same mapping.
113 lines
4.4 KiB
TypeScript
113 lines
4.4 KiB
TypeScript
import path from 'node:path';
|
|
import {
|
|
adoptStartedDurableCapture,
|
|
finishLiveDurableCapture,
|
|
finishRecoveredDurableCapture,
|
|
forceCleanupLiveDurableCapture,
|
|
recoverDurableCaptureResource,
|
|
recoverDurableCaptureResourcesAfterDaemonLock,
|
|
type AdoptStartedDurableCaptureParams,
|
|
type DurableCaptureRecoveryParams,
|
|
type DurableCaptureResourceDefinition,
|
|
type DurableCaptureSessionStore,
|
|
type FinishRecoveredDurableCaptureParams,
|
|
} from '@agent-device/capture-kit/durable-capture';
|
|
import type { LiveResourceHandle } from '@agent-device/contracts/durable-resource';
|
|
import type { ResourceOwnershipFence } from '@agent-device/contracts/platform-runtime';
|
|
import type { DeviceInfo } from '@agent-device/kernel/device';
|
|
import type { DurableCaptureAdmissionLedger } from './durable-capture-admission-ledger.ts';
|
|
import { createNextDurableCaptureFence } from './durable-capture-start-preflight.ts';
|
|
import { safeSessionName } from './session-paths.ts';
|
|
import type { SessionStore } from './session-store.ts';
|
|
import type { SessionState } from './session-state.ts';
|
|
import type { DurableSessionResourceKind } from './durable-session-resource-kinds.ts';
|
|
|
|
type AdoptStartedSessionCaptureParams<K extends string, H extends AsyncDisposable> = Omit<
|
|
AdoptStartedDurableCaptureParams<K, H, SessionState>,
|
|
'reportUndurableCleanup'
|
|
> &
|
|
Readonly<{ admissionLedger: DurableCaptureAdmissionLedger }>;
|
|
|
|
type SessionCaptureRecoveryParams<K extends string, H extends LiveResourceHandle<C>, C> = Omit<
|
|
DurableCaptureRecoveryParams<K, H, C>,
|
|
'definition' | 'resolveSessionDir'
|
|
>;
|
|
|
|
/**
|
|
* Where the shared durable-capture mechanics meet the two authorities that stay daemon policy:
|
|
* the admission ledger, which decides whether a failed adoption blocks a replacement start, and
|
|
* the session store, whose naming rule turns a session id into the one directory its records
|
|
* may occupy.
|
|
*/
|
|
export function createDurableCaptureResource<
|
|
K extends DurableSessionResourceKind,
|
|
H extends LiveResourceHandle<C>,
|
|
C,
|
|
>(definition: DurableCaptureResourceDefinition<K, H, C, SessionState>) {
|
|
const sessionResourcePath = (
|
|
sessionStore: DurableCaptureSessionStore<SessionState>,
|
|
sessionName: string,
|
|
): string => definition.store.resolvePath(sessionStore.resolveSessionDir(sessionName));
|
|
const recoveryParams = (
|
|
params: SessionCaptureRecoveryParams<K, H, C>,
|
|
): DurableCaptureRecoveryParams<K, H, C> => ({
|
|
definition,
|
|
resolveSessionDir: (sessionId) => path.join(params.sessionsDir, safeSessionName(sessionId)),
|
|
...params,
|
|
});
|
|
|
|
return Object.freeze({
|
|
store: definition.store,
|
|
/** Where this session's record for this resource lives. */
|
|
resourcePath: sessionResourcePath,
|
|
createNextFence(params: {
|
|
admissionLedger: DurableCaptureAdmissionLedger;
|
|
resourcePath: string;
|
|
device: DeviceInfo;
|
|
}): ResourceOwnershipFence {
|
|
return createNextDurableCaptureFence(definition, params);
|
|
},
|
|
adoptStarted(params: AdoptStartedSessionCaptureParams<K, H>): Promise<void> {
|
|
return adoptStartedDurableCapture(
|
|
definition,
|
|
{
|
|
...params,
|
|
reportUndurableCleanup: (device, outcome) => {
|
|
if (outcome.confirmed) params.admissionLedger.clearUndurableCleanup(device);
|
|
else params.admissionLedger.blockUndurableCleanup(device, outcome.reason);
|
|
},
|
|
},
|
|
sessionResourcePath(params.sessionStore, params.sessionName),
|
|
);
|
|
},
|
|
finishLive(params: {
|
|
session: SessionState;
|
|
sessionName: string;
|
|
sessionStore: SessionStore;
|
|
}): Promise<C> {
|
|
return finishLiveDurableCapture(
|
|
definition,
|
|
params,
|
|
sessionResourcePath(params.sessionStore, params.sessionName),
|
|
);
|
|
},
|
|
finishRecovered(params: FinishRecoveredDurableCaptureParams<K, H, C>): Promise<C> {
|
|
return finishRecoveredDurableCapture(definition, params);
|
|
},
|
|
forceCleanupLive(params: {
|
|
session: SessionState;
|
|
sessionName?: string;
|
|
sessionStore?: SessionStore;
|
|
resourcePath: string;
|
|
}): Promise<void> {
|
|
return forceCleanupLiveDurableCapture(definition, params);
|
|
},
|
|
recoverAll(params: SessionCaptureRecoveryParams<K, H, C>) {
|
|
return recoverDurableCaptureResourcesAfterDaemonLock(recoveryParams(params));
|
|
},
|
|
recoverOne(params: SessionCaptureRecoveryParams<K, H, C>, resourcePath: string) {
|
|
return recoverDurableCaptureResource(recoveryParams(params), resourcePath);
|
|
},
|
|
});
|
|
}
|