83 lines
5.2 KiB
TypeScript
83 lines
5.2 KiB
TypeScript
import { ref } from 'vue'
|
|
import { ApiResponseError } from '@/shared/api/http'
|
|
import type { CommandResult, DispatchAction } from './dispatchContracts'
|
|
import { requireVersion } from './dispatchContracts'
|
|
|
|
export interface PendingDispatchCommand { dispatchNo: string; action: DispatchAction; body: Record<string, unknown>; attempted: boolean; result: CommandResult | null; rejected: boolean; operation?: 'ROUTE'; itemId?: string }
|
|
const storageKey = (scope: string) => `dispatch-command:${scope}`
|
|
const snapshot = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T
|
|
const sessions = new Map<string, { pending: ReturnType<typeof ref<PendingDispatchCommand | null>>; busy: ReturnType<typeof ref<boolean>> }>()
|
|
export function isDefiniteDispatchRejection(error: unknown) {
|
|
return error instanceof ApiResponseError && /^(?:A0|HTTP_)?(?:400|401|403|404|409|410|422)$/.test(String(error.code))
|
|
}
|
|
export function validateCommandResult(result: CommandResult, pending: PendingDispatchCommand) {
|
|
if (!result || result.dispatchNo !== pending.dispatchNo && pending.action !== 'CREATE'
|
|
|| result.action !== pending.action || result.idempotencyKey !== pending.body.idempotencyKey
|
|
|| !result.dispatchNo || !result.result) throw new Error('操作结果与原排车请求不一致,请保留原请求核对')
|
|
requireVersion(result.recordVersion)
|
|
if (pending.action === 'DEPART' && result.stage !== 'DELIVERING') throw new Error('发车结果未确认派送中,请核对原请求')
|
|
if (pending.action === 'LOADING_COMPLETE' && result.stage !== 'AFTER_LOADING') throw new Error('装车结果未确认装车后,请核对原请求')
|
|
}
|
|
export function validateCurrentDispatch(value: unknown, result: CommandResult) {
|
|
const current = value as { dispatchNo?: string; recordVersion?: unknown } | null
|
|
if (current?.dispatchNo !== result.dispatchNo) throw new Error('恢复详情与原排车单不一致')
|
|
const latest = String(requireVersion(current.recordVersion)).replace(/^0+(?=\d)/, '')
|
|
const confirmed = String(requireVersion(result.recordVersion)).replace(/^0+(?=\d)/, '')
|
|
if (latest.length < confirmed.length || latest.length === confirmed.length && latest < confirmed) throw new Error('详情版本早于已确认操作,请保留原请求重读')
|
|
}
|
|
|
|
/** A response proves the original command; a subsequent GET supplies current UI facts. */
|
|
export function useDispatchCommandRecovery(scope: () => string) {
|
|
const owner = scope()
|
|
const saved = owner ? sessionStorage.getItem(storageKey(owner)) : null
|
|
const existing = sessions.get(owner)
|
|
const pending = existing?.pending || ref<PendingDispatchCommand | null>(null)
|
|
const busy = existing?.busy || ref(false)
|
|
if (!existing && saved) {
|
|
try { pending.value = JSON.parse(saved) as PendingDispatchCommand }
|
|
catch { throw new Error('本次排车请求记录损坏,请保留现场核对') }
|
|
}
|
|
if (!existing) sessions.set(owner, { pending, busy })
|
|
function persist() {
|
|
if (!owner || owner !== scope()) throw new Error('账号已变化,请重新进入排车列表')
|
|
if (pending.value) sessionStorage.setItem(storageKey(owner), JSON.stringify(pending.value))
|
|
else sessionStorage.removeItem(storageKey(owner))
|
|
}
|
|
function begin(dispatchNo: string, action: DispatchAction, body: object, target: Pick<PendingDispatchCommand, 'operation' | 'itemId'> = {}) {
|
|
if (pending.value) throw new Error('上次排车操作结果尚未核对,请先恢复原请求')
|
|
pending.value = { dispatchNo, action, body: snapshot(body) as Record<string, unknown>, attempted: false, result: null, rejected: false, ...target }
|
|
persist()
|
|
}
|
|
async function execute(send: (command: PendingDispatchCommand) => Promise<CommandResult>, read: (dispatchNo: string) => Promise<unknown>, find: (dispatchNo: string, key: string) => Promise<CommandResult>) {
|
|
if (busy.value || !pending.value) return
|
|
const command = pending.value
|
|
const previouslyAttempted = command.attempted
|
|
busy.value = true
|
|
try {
|
|
persist()
|
|
if (!command.result && previouslyAttempted && command.dispatchNo) {
|
|
try { const found = await find(command.dispatchNo, String(command.body.idempotencyKey)); validateCommandResult(found, command); command.result = found }
|
|
catch (error) { if (!(error instanceof ApiResponseError) || !['404', 'A0404', 'HTTP_404'].includes(String(error.code))) throw error }
|
|
}
|
|
if (!command.result) {
|
|
command.attempted = true; persist()
|
|
try { const sent = await send(snapshot(command)); validateCommandResult(sent, command); command.result = sent }
|
|
catch (error) {
|
|
// A later rejection cannot erase an earlier unknown write result.
|
|
if (!previouslyAttempted && isDefiniteDispatchRejection(error)) command.rejected = true
|
|
persist(); throw error
|
|
}
|
|
}
|
|
validateCommandResult(command.result, command); persist()
|
|
validateCurrentDispatch(await read(command.result.dispatchNo), command.result)
|
|
pending.value = null; persist()
|
|
return command.result
|
|
} finally { busy.value = false }
|
|
}
|
|
function discardRejected() {
|
|
if (busy.value || !pending.value?.rejected) return
|
|
pending.value = null; persist()
|
|
}
|
|
return { pending, busy, begin, execute, discardRejected }
|
|
}
|