blob: e483dfa81c98e786d2527da55b262518324e856c [file]
import { decodeSessionCatalogItem, type SessionCatalogItem } from './session-catalog.js';
import { requireEntityId, requireExactRecord, requireRecord } from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { defineOperation } from './operation-spec.js';
const LIFECYCLE_ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'not_found',
'session_busy',
'operation_conflict',
'persistence_failed',
'commit_outcome_unknown',
'internal_failure',
] as const;
export type SessionLifecycleState = 'active' | 'archived';
export interface SessionLifecycleSetInput {
readonly sessionId: string;
readonly state: SessionLifecycleState;
}
export interface SessionRemoveInput {
readonly sessionId: string;
readonly expectedRevision: number;
}
export type SessionRemoveResult =
| { readonly kind: 'removed'; readonly sessionId: string }
| {
readonly kind: 'revision_conflict';
readonly expectedRevision: number;
readonly actualRevision: number;
};
export const SESSION_RETIREMENT_OPERATION_SPECS = {
'session.lifecycle.set': defineOperation<
SessionLifecycleSetInput,
SessionCatalogItem,
(typeof LIFECYCLE_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: LIFECYCLE_ERRORS,
decodeInput: decodeSessionLifecycleSetInput,
decodeOutput: decodeSessionCatalogItem,
assertOutputForInput: (input, output) => {
if (output.id !== input.sessionId) {
throw invalidProtocolFrame('Session lifecycle result belongs to another Session');
}
if ('kind' in output) return;
const archived = input.state === 'archived';
if (output.isArchived !== archived || (output.status === 'archived') !== archived) {
throw invalidProtocolFrame('Session lifecycle result does not match the requested state');
}
},
}),
'session.remove': defineOperation<
SessionRemoveInput,
SessionRemoveResult,
(typeof LIFECYCLE_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: LIFECYCLE_ERRORS,
decodeInput: decodeSessionRemoveInput,
decodeOutput: decodeSessionRemoveResult,
assertOutputForInput: (input, output) => {
if (output.kind === 'removed' && output.sessionId !== input.sessionId) {
throw invalidProtocolFrame('Session remove result belongs to another Session');
}
if (
output.kind === 'revision_conflict' &&
output.expectedRevision !== input.expectedRevision
) {
throw invalidProtocolFrame('Session remove conflict changed the expected revision');
}
},
}),
} as const;
export function decodeSessionLifecycleSetInput(value: unknown): SessionLifecycleSetInput {
const input = requireExactRecord(value, 'Session lifecycle input', ['sessionId', 'state']);
if (input.state !== 'active' && input.state !== 'archived') {
throw invalidProtocolFrame('Invalid Session lifecycle state');
}
return {
sessionId: requireEntityId(input.sessionId, 'sessionId'),
state: input.state,
};
}
export function decodeSessionRemoveInput(value: unknown): SessionRemoveInput {
const input = requireExactRecord(value, 'Session remove input', [
'sessionId',
'expectedRevision',
]);
return {
sessionId: requireEntityId(input.sessionId, 'sessionId'),
expectedRevision: positiveRevision(input.expectedRevision),
};
}
export function decodeSessionRemoveResult(value: unknown): SessionRemoveResult {
const result = requireRecord(value, 'Session remove result');
if (result.kind === 'removed') {
const exact = requireExactRecord(result, 'Removed Session result', ['kind', 'sessionId']);
return { kind: 'removed', sessionId: requireEntityId(exact.sessionId, 'sessionId') };
}
if (result.kind !== 'revision_conflict') {
throw invalidProtocolFrame('Invalid Session remove result kind');
}
const exact = requireExactRecord(result, 'Session remove revision conflict', [
'kind',
'expectedRevision',
'actualRevision',
]);
return {
kind: 'revision_conflict',
expectedRevision: positiveRevision(exact.expectedRevision),
actualRevision: positiveRevision(exact.actualRevision),
};
}
function positiveRevision(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) < 1) {
throw invalidProtocolFrame('Session revision must be a positive safe integer');
}
return value as number;
}