167 lines
7.0 KiB
JavaScript
167 lines
7.0 KiB
JavaScript
(function initExecutionGuard(root, factory) {
|
|
const api = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
root.LTJTExecutionGuard = api;
|
|
})(typeof self !== 'undefined' ? self : globalThis, function createExecutionGuard() {
|
|
'use strict';
|
|
|
|
const TERMINAL_STATES = new Set(['completed', 'blocked', 'cancelled', 'uncertain']);
|
|
const WRITE_STATES = new Set(['write_started', 'submitted', 'uncertain', 'completed']);
|
|
const RESULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
const HISTORY_MAINTENANCE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
const LOCAL_TERMINAL_ROOT_LIMIT = 12;
|
|
const ACTIVE_EXECUTION_STATES = new Set(['running', 'write_started', 'submitted', 'uncertain']);
|
|
const RECONCILABLE_RESULT_STATUSES = new Set(['saved_unverified', 'execution_uncertain', 'reconciliation_pending', 'uncertain']);
|
|
|
|
function rootTaskId(taskId) {
|
|
return String(taskId || '').split('#')[0];
|
|
}
|
|
|
|
function acceptanceDecision(existing, executionId) {
|
|
const normalizedExecutionId = String(executionId || '').trim();
|
|
if (!normalizedExecutionId) {
|
|
return { accepted: false, status: 'missing_execution_id', reason: '缺少服务端执行编号。' };
|
|
}
|
|
if (!existing) return { accepted: true, status: 'accepted', reason: '' };
|
|
if (String(existing.execution_id || '') !== normalizedExecutionId) {
|
|
return { accepted: false, status: 'duplicate_blocked', reason: '同一任务出现了不同执行编号。' };
|
|
}
|
|
return {
|
|
accepted: false,
|
|
status: TERMINAL_STATES.has(existing.state) ? 'already_finished' : 'already_started',
|
|
reason: '该执行编号已经被插件接收,重复消息不会再次执行。'
|
|
};
|
|
}
|
|
|
|
function failureState(existing) {
|
|
return existing && WRITE_STATES.has(existing.state) ? 'uncertain' : 'blocked';
|
|
}
|
|
|
|
function confirmedPrewriteNoErpWrite(report) {
|
|
if (!report || typeof report !== 'object' || Array.isArray(report)) return false;
|
|
const submitSafety = report.submit_safety;
|
|
const ajaxRecordsValid = report.ajax_records == null || Array.isArray(report.ajax_records);
|
|
const sideEffectsValid = report.side_effects == null || Array.isArray(report.side_effects);
|
|
const ajaxRecords = Array.isArray(report.ajax_records) ? report.ajax_records : [];
|
|
const sideEffects = Array.isArray(report.side_effects) ? report.side_effects : [];
|
|
const ajaxStayedBeforeNetwork = ajaxRecords.every((record) => Boolean(
|
|
record
|
|
&& typeof record === 'object'
|
|
&& (record.prevented_from_network === true || record.network_request_attempted === false)
|
|
));
|
|
return report.status === 'live_submit_blocked'
|
|
&& report.execution_phase === 'prewrite_blocked'
|
|
&& report.no_erp_write === true
|
|
&& report.write_attempted === false
|
|
&& submitSafety
|
|
&& typeof submitSafety === 'object'
|
|
&& !Array.isArray(submitSafety)
|
|
&& submitSafety.live_submit_attempted === false
|
|
&& !report.native_request
|
|
&& !report.server_response
|
|
&& !report.requery
|
|
&& ajaxRecordsValid
|
|
&& sideEffectsValid
|
|
&& sideEffects.length === 0
|
|
&& ajaxStayedBeforeNetwork;
|
|
}
|
|
|
|
function timeoutDisposition(existing) {
|
|
const writeStarted = Boolean(existing && WRITE_STATES.has(existing.state));
|
|
return {
|
|
state: writeStarted ? 'uncertain' : 'blocked',
|
|
result_status: writeStarted ? 'execution_uncertain' : 'failed',
|
|
write_attempted: writeStarted,
|
|
no_erp_write: !writeStarted,
|
|
reconciliation_required: writeStarted
|
|
};
|
|
}
|
|
|
|
function canTransition(existing, executionId) {
|
|
return Boolean(existing) && String(existing.execution_id || '') === String(executionId || '');
|
|
}
|
|
|
|
function recordTimestamp(value) {
|
|
for (const candidate of [value?.updated_at, value?.accepted_at, value?.received_at, value?.created_at]) {
|
|
const timestamp = Date.parse(String(candidate || ''));
|
|
if (Number.isFinite(timestamp)) return timestamp;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function pruneLocalTaskHistory(state = {}, options = {}) {
|
|
const tasks = { ...(state.tasks || {}) };
|
|
const results = { ...(state.results || {}) };
|
|
const executions = { ...(state.executions || {}) };
|
|
const timingStates = { ...(state.timingStates || {}) };
|
|
const currentRoot = rootTaskId(options.currentTaskId || '');
|
|
const terminalRootLimit = Math.max(0, Math.trunc(Number(options.terminalRootLimit ?? LOCAL_TERMINAL_ROOT_LIMIT)));
|
|
const roots = new Set([
|
|
...Object.keys(tasks),
|
|
...Object.keys(results),
|
|
...Object.keys(executions),
|
|
...Object.keys(timingStates)
|
|
].map(rootTaskId).filter(Boolean));
|
|
const protectedRoots = new Set(currentRoot ? [currentRoot] : []);
|
|
const lastUpdatedByRoot = new Map();
|
|
const collect = (entries) => {
|
|
for (const [taskId, value] of Object.entries(entries)) {
|
|
const rootId = rootTaskId(taskId);
|
|
if (!rootId) continue;
|
|
lastUpdatedByRoot.set(rootId, Math.max(lastUpdatedByRoot.get(rootId) || 0, recordTimestamp(value)));
|
|
}
|
|
};
|
|
collect(tasks);
|
|
collect(results);
|
|
collect(executions);
|
|
for (const [taskId, execution] of Object.entries(executions)) {
|
|
if (ACTIVE_EXECUTION_STATES.has(String(execution?.state || ''))) protectedRoots.add(rootTaskId(taskId));
|
|
}
|
|
for (const [taskId, result] of Object.entries(results)) {
|
|
if (RECONCILABLE_RESULT_STATUSES.has(String(result?.status || ''))) protectedRoots.add(rootTaskId(taskId));
|
|
}
|
|
for (const taskId of Object.keys(timingStates)) protectedRoots.add(rootTaskId(taskId));
|
|
|
|
const terminalRoots = [...roots]
|
|
.filter((rootId) => !protectedRoots.has(rootId))
|
|
.sort((left, right) => (lastUpdatedByRoot.get(right) || 0) - (lastUpdatedByRoot.get(left) || 0));
|
|
const retainedRoots = new Set([...protectedRoots, ...terminalRoots.slice(0, terminalRootLimit)]);
|
|
const filter = (entries) => Object.fromEntries(
|
|
Object.entries(entries).filter(([taskId]) => retainedRoots.has(rootTaskId(taskId)))
|
|
);
|
|
const next = {
|
|
tasks: filter(tasks),
|
|
results: filter(results),
|
|
executions: filter(executions),
|
|
timingStates: filter(timingStates)
|
|
};
|
|
return {
|
|
...next,
|
|
counters: {
|
|
retained_root_count: retainedRoots.size,
|
|
protected_root_count: protectedRoots.size,
|
|
removed_root_count: Math.max(0, roots.size - retainedRoots.size),
|
|
removed_task_count: Math.max(0, Object.keys(tasks).length - Object.keys(next.tasks).length),
|
|
removed_result_count: Math.max(0, Object.keys(results).length - Object.keys(next.results).length),
|
|
removed_execution_count: Math.max(0, Object.keys(executions).length - Object.keys(next.executions).length),
|
|
removed_timing_count: Math.max(0, Object.keys(timingStates).length - Object.keys(next.timingStates).length)
|
|
}
|
|
};
|
|
}
|
|
|
|
return {
|
|
TERMINAL_STATES,
|
|
WRITE_STATES,
|
|
RESULT_TIMEOUT_MS,
|
|
HISTORY_MAINTENANCE_TIMEOUT_MS,
|
|
LOCAL_TERMINAL_ROOT_LIMIT,
|
|
rootTaskId,
|
|
acceptanceDecision,
|
|
failureState,
|
|
confirmedPrewriteNoErpWrite,
|
|
timeoutDisposition,
|
|
canTransition,
|
|
pruneLocalTaskHistory
|
|
};
|
|
});
|