Files
Jyotisha/frontend/tests/birth-time-dynamic-job-memory-store.ts
T
2026-07-19 09:12:45 +08:00

114 lines
4.9 KiB
TypeScript

import { isDeepStrictEqual } from "node:util";
import type {
BirthTimeJourneyStore,
DynamicStoredRectificationCase,
} from "../src/lib/birth-time-journey-service.ts";
import { samePersistedDynamicReceipt } from "../src/lib/birth-time-dynamic-action-replay.ts";
import type { DynamicScoringJobSpec } from "../src/lib/birth-time-scoring-job.ts";
import { BirthTimeScoringJobError, scoringJobDurationMs, scoringProcessingLeaseMs } from "../src/lib/birth-time-scoring-job.ts";
import { StaleJourneyTurnError } from "../src/lib/birth-time-journey-turn-persistence.ts";
type DynamicMemoryJob = DynamicScoringJobSpec & {
readonly caseId: string;
readonly userId: string;
readonly status: "pending" | "processing" | "completed" | "failed";
readonly updatedAt: string;
readonly result: NonNullable<DynamicStoredRectificationCase["candidateResult"]> | null;
};
function completedMatches(
job: DynamicMemoryJob,
current: DynamicStoredRectificationCase,
): boolean {
const result = job.result;
const action = current.dynamicTurnState.nextAction;
if (result === null || !isDeepStrictEqual(current.candidateResult, result)) return false;
if (result.confidence === "high") {
return action.kind === "request_candidate_confirmation" && action.resultId === result.resultId;
}
if (action.kind === "generate_dynamic_question") return true;
return result.confidence === "medium"
? action.kind === "present_medium_result" && action.resultId === result.resultId
: action.kind === "present_low_result" && action.resultId === result.resultId;
}
export function dynamicJobStore(
base: BirthTimeJourneyStore,
read: () => DynamicStoredRectificationCase | null,
) {
const jobs = new Map<string, DynamicMemoryJob>();
const store: BirthTimeJourneyStore = {
...base,
async createDynamicScoringJob(value, expectedVersion, actionId, _questionId, spec) {
const current = read();
if (!current) throw new BirthTimeScoringJobError("unavailable");
const receipt = actionId.toLowerCase();
if (current.processedActionIds.includes(receipt)) {
if (samePersistedDynamicReceipt(value, current, receipt, expectedVersion)) return current;
throw new StaleJourneyTurnError(value.id, expectedVersion, current.turnVersion);
}
if (current.turnVersion !== expectedVersion) {
throw new StaleJourneyTurnError(value.id, expectedVersion, current.turnVersion);
}
jobs.set(spec.jobId, {
...spec,
caseId: value.id,
userId: value.userId,
status: "pending",
updatedAt: new Date(Date.parse(spec.expiresAt) - scoringJobDurationMs).toISOString(),
result: null,
});
return base.saveDynamicTurn(value, expectedVersion, receipt);
},
async claimDynamicScoringJob(identity) {
const job = jobs.get(identity.jobId);
if (!job || job.caseId !== identity.caseId || job.userId !== identity.userId
|| job.evidenceFingerprint !== identity.evidenceFingerprint) {
throw new BirthTimeScoringJobError("unavailable");
}
if (job.algorithmVersion !== identity.algorithmVersion) {
throw new BirthTimeScoringJobError("algorithm_mismatch");
}
const current = read();
if (!current) throw new BirthTimeScoringJobError("unavailable");
if (job.status === "completed") {
if (!completedMatches(job, current)) throw new BirthTimeScoringJobError("invalid_turn");
return { kind: "completed", algorithmVersion: job.algorithmVersion };
}
const action = current.dynamicTurnState.nextAction;
if ((action.kind !== "score_pending" && action.kind !== "retry_scoring")
|| action.jobId !== identity.jobId) throw new BirthTimeScoringJobError("invalid_turn");
if (job.status === "processing") {
const leaseEnds = Date.parse(job.updatedAt) + scoringProcessingLeaseMs;
if (Date.parse(identity.now) < leaseEnds) {
return { kind: "processing", algorithmVersion: job.algorithmVersion };
}
}
jobs.set(job.jobId, { ...job, status: "processing", updatedAt: identity.now });
return { kind: "claimed", algorithmVersion: job.algorithmVersion };
},
async completeDynamicScoringJob(value, command) {
const job = jobs.get(command.jobId);
if (!job || job.status !== "processing") {
throw new BirthTimeScoringJobError("invalid_turn");
}
const saved = await base.completeDynamicScoringJob(value, command);
jobs.set(job.jobId, { ...job, status: "completed", result: value.candidateResult ?? null });
return saved;
},
async failDynamicScoringJob(value, command) {
const job = jobs.get(command.jobId);
if (!job || job.status !== "processing") {
throw new BirthTimeScoringJobError("invalid_turn");
}
const saved = await base.failDynamicScoringJob(value, command);
jobs.set(job.jobId, { ...job, status: "failed" });
return saved;
},
};
return {
store,
count: () => jobs.size,
};
}