336 lines
12 KiB
JavaScript
336 lines
12 KiB
JavaScript
(function initOperationTiming(root, factory) {
|
|
const api = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
root.LTJTOperationTiming = api;
|
|
})(typeof self !== 'undefined' ? self : globalThis, function createOperationTiming() {
|
|
'use strict';
|
|
|
|
const SCHEMA_VERSION = 'ltjt-operation-timing-v1';
|
|
const MAX_STAGES = 64;
|
|
const MAX_DETAILS_PER_STAGE = 48;
|
|
const MAX_COUNTERS = 24;
|
|
const MAX_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
const CODE_PATTERN = /^[a-z][a-z0-9_.:-]{0,79}$/i;
|
|
const TERMINAL_STATUSES = new Set([
|
|
'completed',
|
|
'dry_run',
|
|
'blocked',
|
|
'failed',
|
|
'cancelled',
|
|
'saved_unverified',
|
|
'execution_uncertain',
|
|
'reconciliation_pending'
|
|
]);
|
|
|
|
function safeCode(value, fallback = '') {
|
|
const normalized = String(value || '').trim();
|
|
return CODE_PATTERN.test(normalized) ? normalized : fallback;
|
|
}
|
|
|
|
function safeDuration(value) {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number) || number <= 0) return 0;
|
|
return Math.min(MAX_DURATION_MS, Math.round(number));
|
|
}
|
|
|
|
function safeTimestamp(value, fallback) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
}
|
|
|
|
function sanitizeCounters(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
const output = {};
|
|
for (const [key, entry] of Object.entries(value).slice(0, MAX_COUNTERS)) {
|
|
const safeKey = safeCode(key);
|
|
const number = Number(entry);
|
|
if (!safeKey || !Number.isFinite(number) || number < 0) continue;
|
|
output[safeKey] = Math.min(Number.MAX_SAFE_INTEGER, Math.round(number));
|
|
}
|
|
return Object.keys(output).length ? output : undefined;
|
|
}
|
|
|
|
function sanitizeDetail(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
const step = safeCode(value.step);
|
|
if (!step) return null;
|
|
const counters = sanitizeCounters(value.counters);
|
|
return {
|
|
step,
|
|
status: safeCode(value.status, 'completed'),
|
|
duration_ms: safeDuration(value.duration_ms),
|
|
...(counters ? { counters } : {})
|
|
};
|
|
}
|
|
|
|
function mergeDetails(existing = [], incoming = []) {
|
|
const output = [];
|
|
let truncated = 0;
|
|
const add = (candidate) => {
|
|
const detail = sanitizeDetail(candidate);
|
|
if (!detail) return;
|
|
const matched = output.find((item) => item.step === detail.step);
|
|
if (matched) {
|
|
matched.duration_ms = safeDuration(matched.duration_ms + detail.duration_ms);
|
|
if (matched.status !== detail.status) matched.status = 'mixed';
|
|
if (detail.counters) {
|
|
matched.counters = matched.counters || {};
|
|
for (const [key, value] of Object.entries(detail.counters)) {
|
|
matched.counters[key] = Math.min(
|
|
Number.MAX_SAFE_INTEGER,
|
|
Math.round(Number(matched.counters[key] || 0) + Number(value || 0))
|
|
);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (output.length >= MAX_DETAILS_PER_STAGE) {
|
|
truncated += 1;
|
|
return;
|
|
}
|
|
output.push(detail);
|
|
};
|
|
for (const detail of Array.isArray(existing) ? existing : []) add(detail);
|
|
for (const detail of Array.isArray(incoming) ? incoming : []) add(detail);
|
|
return { details: output, truncated };
|
|
}
|
|
|
|
function sanitizeStage(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
const stage = safeCode(value.stage);
|
|
if (!stage) return null;
|
|
const merged = mergeDetails([], value.details);
|
|
const truncatedDetailCount = Math.max(
|
|
0,
|
|
Math.round(Number(value.truncated_detail_count || 0)) + merged.truncated
|
|
);
|
|
return {
|
|
stage,
|
|
status: safeCode(value.status, 'unknown'),
|
|
duration_ms: safeDuration(value.duration_ms),
|
|
...(merged.details.length ? { details: merged.details } : {}),
|
|
...(truncatedDetailCount ? { truncated_detail_count: truncatedDetailCount } : {})
|
|
};
|
|
}
|
|
|
|
function sanitizeSnapshot(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
if (value.schema_version !== SCHEMA_VERSION) return null;
|
|
const stages = (Array.isArray(value.stages) ? value.stages : [])
|
|
.map(sanitizeStage)
|
|
.filter(Boolean)
|
|
.slice(0, MAX_STAGES);
|
|
const truncatedStageCount = Math.max(0, Math.round(Number(value.truncated_stage_count || 0)));
|
|
return {
|
|
schema_version: SCHEMA_VERSION,
|
|
status: safeCode(value.status, 'unknown'),
|
|
total_ms: safeDuration(value.total_ms),
|
|
stage_count: Math.max(stages.length, Math.round(Number(value.stage_count || stages.length))),
|
|
stages,
|
|
...(truncatedStageCount ? { truncated_stage_count: truncatedStageCount } : {})
|
|
};
|
|
}
|
|
|
|
function createState({ now_ms, started_at_ms, stage = 'auto_executor', status = 'accepted' } = {}) {
|
|
const now = safeTimestamp(now_ms, Date.now());
|
|
const startedAt = Math.min(now, safeTimestamp(started_at_ms, now));
|
|
return {
|
|
schema_version: SCHEMA_VERSION,
|
|
base_total_ms: 0,
|
|
started_at_ms: startedAt,
|
|
active_stage: safeCode(stage, 'auto_executor'),
|
|
active_stage_started_at_ms: startedAt,
|
|
active_status: safeCode(status, 'accepted'),
|
|
active_details: [],
|
|
active_truncated_detail_count: 0,
|
|
stages: [],
|
|
truncated_stage_count: 0,
|
|
final_status: ''
|
|
};
|
|
}
|
|
|
|
function stateFromSnapshot(snapshotValue, update, now) {
|
|
const snapshot = sanitizeSnapshot(snapshotValue);
|
|
if (!snapshot) return null;
|
|
const updatedAt = safeTimestamp(update.updated_at_ms, now);
|
|
const gap = Math.max(0, now - updatedAt);
|
|
const terminal = TERMINAL_STATUSES.has(snapshot.status);
|
|
if (terminal) {
|
|
return {
|
|
schema_version: SCHEMA_VERSION,
|
|
base_total_ms: snapshot.total_ms,
|
|
started_at_ms: now,
|
|
active_stage: '',
|
|
active_stage_started_at_ms: now,
|
|
active_status: '',
|
|
active_details: [],
|
|
active_truncated_detail_count: 0,
|
|
stages: snapshot.stages,
|
|
truncated_stage_count: snapshot.truncated_stage_count || 0,
|
|
final_status: snapshot.status
|
|
};
|
|
}
|
|
const active = snapshot.stages[snapshot.stages.length - 1] || null;
|
|
const activeDuration = safeDuration(active?.duration_ms) + gap;
|
|
return {
|
|
schema_version: SCHEMA_VERSION,
|
|
base_total_ms: 0,
|
|
started_at_ms: Math.max(0, now - snapshot.total_ms - gap),
|
|
active_stage: safeCode(active?.stage, safeCode(update.stage, 'auto_executor')),
|
|
active_stage_started_at_ms: Math.max(0, now - activeDuration),
|
|
active_status: safeCode(active?.status, safeCode(update.status, 'running')),
|
|
active_details: Array.isArray(active?.details) ? active.details : [],
|
|
active_truncated_detail_count: Math.max(0, Math.round(Number(active?.truncated_detail_count || 0))),
|
|
stages: active ? snapshot.stages.slice(0, -1) : snapshot.stages,
|
|
truncated_stage_count: snapshot.truncated_stage_count || 0,
|
|
final_status: ''
|
|
};
|
|
}
|
|
|
|
function sanitizeState(value, update, now) {
|
|
if (!value || typeof value !== 'object' || value.schema_version !== SCHEMA_VERSION) {
|
|
return stateFromSnapshot(update.existing_timing, update, now)
|
|
|| createState({
|
|
now_ms: now,
|
|
started_at_ms: update.started_at_ms,
|
|
stage: update.stage,
|
|
status: update.status
|
|
});
|
|
}
|
|
const stages = (Array.isArray(value.stages) ? value.stages : [])
|
|
.map(sanitizeStage)
|
|
.filter(Boolean)
|
|
.slice(-MAX_STAGES);
|
|
const merged = mergeDetails([], value.active_details);
|
|
return {
|
|
schema_version: SCHEMA_VERSION,
|
|
base_total_ms: safeDuration(value.base_total_ms),
|
|
started_at_ms: Math.min(now, safeTimestamp(value.started_at_ms, now)),
|
|
active_stage: safeCode(value.active_stage),
|
|
active_stage_started_at_ms: Math.min(now, safeTimestamp(value.active_stage_started_at_ms, now)),
|
|
active_status: safeCode(value.active_status, 'running'),
|
|
active_details: merged.details,
|
|
active_truncated_detail_count: Math.max(
|
|
0,
|
|
Math.round(Number(value.active_truncated_detail_count || 0)) + merged.truncated
|
|
),
|
|
stages,
|
|
truncated_stage_count: Math.max(0, Math.round(Number(value.truncated_stage_count || 0))),
|
|
final_status: safeCode(value.final_status)
|
|
};
|
|
}
|
|
|
|
function appendClosedStage(state, stage) {
|
|
// Reserve one public slot for the currently active stage so the newest
|
|
// work is never hidden by the bounded history.
|
|
if (state.stages.length >= MAX_STAGES - 1) {
|
|
state.stages.shift();
|
|
state.truncated_stage_count += 1;
|
|
}
|
|
state.stages.push(stage);
|
|
}
|
|
|
|
function closeActiveStage(state, now, status) {
|
|
if (!state.active_stage) return;
|
|
appendClosedStage(state, {
|
|
stage: state.active_stage,
|
|
status: safeCode(status, state.active_status || 'completed'),
|
|
duration_ms: safeDuration(now - state.active_stage_started_at_ms),
|
|
...(state.active_details.length ? { details: state.active_details } : {}),
|
|
...(state.active_truncated_detail_count
|
|
? { truncated_detail_count: state.active_truncated_detail_count }
|
|
: {})
|
|
});
|
|
state.active_stage = '';
|
|
state.active_stage_started_at_ms = now;
|
|
state.active_status = '';
|
|
state.active_details = [];
|
|
state.active_truncated_detail_count = 0;
|
|
}
|
|
|
|
function openStage(state, stage, status, now) {
|
|
state.active_stage = safeCode(stage, 'unknown_stage');
|
|
state.active_stage_started_at_ms = now;
|
|
state.active_status = safeCode(status, 'running');
|
|
state.active_details = [];
|
|
state.active_truncated_detail_count = 0;
|
|
state.final_status = '';
|
|
}
|
|
|
|
function snapshot(stateValue, nowValue = Date.now()) {
|
|
const now = safeTimestamp(nowValue, Date.now());
|
|
const state = sanitizeState(stateValue, {}, now);
|
|
const stages = state.stages.map((stage) => ({ ...stage }));
|
|
if (state.active_stage) {
|
|
stages.push({
|
|
stage: state.active_stage,
|
|
status: state.active_status || 'running',
|
|
duration_ms: safeDuration(now - state.active_stage_started_at_ms),
|
|
...(state.active_details.length ? { details: state.active_details } : {}),
|
|
...(state.active_truncated_detail_count
|
|
? { truncated_detail_count: state.active_truncated_detail_count }
|
|
: {})
|
|
});
|
|
}
|
|
const stageCount = stages.length + state.truncated_stage_count;
|
|
return sanitizeSnapshot({
|
|
schema_version: SCHEMA_VERSION,
|
|
status: state.active_stage ? (state.active_status || 'running') : (state.final_status || 'unknown'),
|
|
total_ms: state.base_total_ms + Math.max(0, now - state.started_at_ms),
|
|
stage_count: stageCount,
|
|
stages,
|
|
truncated_stage_count: state.truncated_stage_count
|
|
});
|
|
}
|
|
|
|
function advance(stateValue, update = {}) {
|
|
const now = safeTimestamp(update.now_ms, Date.now());
|
|
const state = sanitizeState(stateValue, update, now);
|
|
const nextStage = safeCode(update.stage, state.active_stage || 'unknown_stage');
|
|
const nextStatus = safeCode(update.status, state.active_status || 'running');
|
|
const incoming = mergeDetails([], update.details);
|
|
|
|
if (state.active_stage) {
|
|
const merged = mergeDetails(state.active_details, incoming.details);
|
|
state.active_details = merged.details;
|
|
state.active_truncated_detail_count += incoming.truncated + merged.truncated;
|
|
if (state.active_stage !== nextStage) {
|
|
closeActiveStage(state, now, 'completed');
|
|
openStage(state, nextStage, nextStatus, now);
|
|
}
|
|
} else {
|
|
openStage(state, nextStage, nextStatus, now);
|
|
const merged = mergeDetails([], incoming.details);
|
|
state.active_details = merged.details;
|
|
state.active_truncated_detail_count += incoming.truncated + merged.truncated;
|
|
}
|
|
|
|
state.active_status = nextStatus;
|
|
if (TERMINAL_STATUSES.has(nextStatus)) {
|
|
closeActiveStage(state, now, nextStatus);
|
|
state.final_status = nextStatus;
|
|
}
|
|
|
|
return {
|
|
state,
|
|
snapshot: snapshot(state, now),
|
|
terminal: TERMINAL_STATUSES.has(nextStatus)
|
|
};
|
|
}
|
|
|
|
return {
|
|
SCHEMA_VERSION,
|
|
MAX_STAGES,
|
|
MAX_DETAILS_PER_STAGE,
|
|
TERMINAL_STATUSES,
|
|
safeCode,
|
|
sanitizeCounters,
|
|
sanitizeDetail,
|
|
mergeDetails,
|
|
sanitizeSnapshot,
|
|
createState,
|
|
snapshot,
|
|
advance
|
|
};
|
|
});
|