239 lines
6.7 KiB
JavaScript
239 lines
6.7 KiB
JavaScript
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
|
|
import { ChaosMonkey } from "./chaos-monkey.js";
|
|
import {
|
|
writeTurnCloseoutGitRecord,
|
|
writeTurnGitTransaction,
|
|
} from "./gitops.js";
|
|
import {
|
|
acquireWriterToken,
|
|
nextWriteRecord,
|
|
releaseWriterToken,
|
|
} from "./writer.js";
|
|
|
|
const GITOPS_TIMEOUT_MS = 10_000;
|
|
|
|
function writeGitTransactionWithTimeout(args) {
|
|
return Promise.race([
|
|
writeTurnGitTransaction(args),
|
|
new Promise((_, reject) =>
|
|
setTimeout(
|
|
() => reject(new Error("Git transaction timed out")),
|
|
GITOPS_TIMEOUT_MS,
|
|
),
|
|
),
|
|
]);
|
|
}
|
|
export function createTurnObserver(options) {
|
|
let current = null;
|
|
let writerToken = null;
|
|
const phaseResults = [];
|
|
const chaosMonkey = options.enableChaosMonkey ? new ChaosMonkey() : null;
|
|
|
|
/**
|
|
* Enrich metadata with write sequence info when a writer token is active.
|
|
*
|
|
* Purpose: Provide audit/traceability by attaching sequence numbers to
|
|
* gitops and audit metadata. When no token is active (e.g., early in
|
|
* turn setup), returns metadata unchanged.
|
|
*
|
|
* @param {string} category — e.g., "gitops", "audit"
|
|
* @param {string} operation — e.g., "insert", "update"
|
|
* @param {object} [metadata] — caller-provided metadata
|
|
* @returns {object} metadata with optional writeSequence and writerTokenId
|
|
*/
|
|
function nextSequenceMetadata(category, operation, metadata) {
|
|
if (!writerToken) return metadata ?? {};
|
|
const record = nextWriteRecord({
|
|
basePath: options.basePath,
|
|
token: writerToken,
|
|
category,
|
|
operation,
|
|
metadata,
|
|
});
|
|
return {
|
|
...(metadata ?? {}),
|
|
writeSequence: record.sequence.sequence,
|
|
writerTokenId: record.writerToken.tokenId,
|
|
};
|
|
}
|
|
return {
|
|
onTurnStart(contract) {
|
|
if (chaosMonkey) chaosMonkey.strike("turn-start");
|
|
current = {
|
|
...contract,
|
|
runControl: options.runControl,
|
|
permissionProfile: options.permissionProfile,
|
|
};
|
|
phaseResults.length = 0;
|
|
writerToken = acquireWriterToken({
|
|
basePath: options.basePath,
|
|
traceId: current.traceId,
|
|
turnId: current.turnId,
|
|
});
|
|
if (options.enableGitops) {
|
|
writeGitTransactionWithTimeout({
|
|
basePath: options.basePath,
|
|
traceId: current.traceId,
|
|
turnId: current.turnId,
|
|
unitType: current.unitType,
|
|
unitId: current.unitId,
|
|
stage: "turn-start",
|
|
action: options.gitAction,
|
|
push: options.gitPush,
|
|
status: "ok",
|
|
metadata: nextSequenceMetadata("gitops", "insert", {
|
|
iteration: current.iteration,
|
|
sidecarKind: current.sidecarKind,
|
|
runControl: current.runControl,
|
|
permissionProfile: current.permissionProfile,
|
|
}),
|
|
}).catch((err) => {
|
|
console.error(`[loop-adapter] Git transaction failed: ${err.message}`);
|
|
});
|
|
}
|
|
if (options.enableAudit) {
|
|
emitUokAuditEvent(
|
|
options.basePath,
|
|
buildAuditEnvelope({
|
|
traceId: current.traceId,
|
|
turnId: current.turnId,
|
|
category: "orchestration",
|
|
type: "turn-start",
|
|
payload: nextSequenceMetadata("audit", "append", {
|
|
iteration: current.iteration,
|
|
unitType: current.unitType,
|
|
unitId: current.unitId,
|
|
sidecarKind: current.sidecarKind,
|
|
runControl: current.runControl,
|
|
permissionProfile: current.permissionProfile,
|
|
}),
|
|
}),
|
|
);
|
|
}
|
|
},
|
|
onPhaseResult(phase, action, data) {
|
|
if (chaosMonkey) chaosMonkey.strike(`after-${phase}`);
|
|
phaseResults.push({
|
|
phase,
|
|
action,
|
|
ts: new Date().toISOString(),
|
|
data,
|
|
});
|
|
if (!current || !options.enableGitops) return;
|
|
if (phase === "dispatch") {
|
|
writeGitTransactionWithTimeout({
|
|
basePath: options.basePath,
|
|
traceId: current.traceId,
|
|
turnId: current.turnId,
|
|
unitType: data?.unitType,
|
|
unitId: data?.unitId,
|
|
stage: "stage",
|
|
action: options.gitAction,
|
|
push: options.gitPush,
|
|
status: "ok",
|
|
metadata: nextSequenceMetadata("gitops", "update", { action }),
|
|
}).catch((err) => {
|
|
console.error(`[loop-adapter] Git transaction failed: ${err.message}`);
|
|
});
|
|
}
|
|
if (phase === "unit") {
|
|
writeGitTransactionWithTimeout({
|
|
basePath: options.basePath,
|
|
traceId: current.traceId,
|
|
turnId: current.turnId,
|
|
unitType: data?.unitType,
|
|
unitId: data?.unitId,
|
|
stage: "checkpoint",
|
|
action: options.gitAction,
|
|
push: options.gitPush,
|
|
status: "ok",
|
|
metadata: nextSequenceMetadata("gitops", "update", { action }),
|
|
}).catch((err) => {
|
|
console.error(`[loop-adapter] Git transaction failed: ${err.message}`);
|
|
});
|
|
}
|
|
if (phase === "finalize") {
|
|
writeGitTransactionWithTimeout({
|
|
basePath: options.basePath,
|
|
traceId: current.traceId,
|
|
turnId: current.turnId,
|
|
unitType: data?.unitType,
|
|
unitId: data?.unitId,
|
|
stage: "publish",
|
|
action: options.gitAction,
|
|
push: options.gitPush,
|
|
status: "ok",
|
|
metadata: nextSequenceMetadata("gitops", "update", { action }),
|
|
}).catch((err) => {
|
|
console.error(`[loop-adapter] Git transaction failed: ${err.message}`);
|
|
});
|
|
}
|
|
},
|
|
onTurnResult(result) {
|
|
const merged = {
|
|
runControl: options.runControl,
|
|
permissionProfile: options.permissionProfile,
|
|
...result,
|
|
phaseResults:
|
|
result.phaseResults.length > 0
|
|
? result.phaseResults
|
|
: [...phaseResults],
|
|
};
|
|
if (options.enableAudit) {
|
|
emitUokAuditEvent(
|
|
options.basePath,
|
|
buildAuditEnvelope({
|
|
traceId: merged.traceId,
|
|
turnId: merged.turnId,
|
|
category: "orchestration",
|
|
type: "turn-result",
|
|
payload: nextSequenceMetadata("audit", "append", {
|
|
unitType: merged.unitType,
|
|
unitId: merged.unitId,
|
|
status: merged.status,
|
|
failureClass: merged.failureClass,
|
|
error: merged.error,
|
|
phaseCount: merged.phaseResults.length,
|
|
runControl: merged.runControl,
|
|
permissionProfile: merged.permissionProfile,
|
|
}),
|
|
}),
|
|
);
|
|
}
|
|
if (options.enableGitops) {
|
|
const closeout = merged.closeout ?? {
|
|
traceId: merged.traceId,
|
|
turnId: merged.turnId,
|
|
unitType: merged.unitType,
|
|
unitId: merged.unitId,
|
|
status: merged.status,
|
|
failureClass: merged.failureClass,
|
|
gitAction: options.gitAction,
|
|
gitPushed: options.gitPush,
|
|
finishedAt: merged.finishedAt,
|
|
};
|
|
Promise.race([
|
|
writeTurnCloseoutGitRecord(
|
|
options.basePath,
|
|
closeout,
|
|
nextSequenceMetadata("gitops", "update", { action: "record" }),
|
|
),
|
|
new Promise((_, reject) =>
|
|
setTimeout(
|
|
() => reject(new Error("Git closeout timed out")),
|
|
GITOPS_TIMEOUT_MS,
|
|
),
|
|
),
|
|
]).catch((err) => {
|
|
console.error(`[loop-adapter] Git closeout failed: ${err.message}`);
|
|
});
|
|
}
|
|
if (writerToken) {
|
|
releaseWriterToken(options.basePath, writerToken);
|
|
}
|
|
writerToken = null;
|
|
current = null;
|
|
phaseResults.length = 0;
|
|
},
|
|
};
|
|
}
|