(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 }; });