fix(hal): return DispenseResult from IPC dispense handler

The hal:dispense IPC handler in main.ts did not return the result of
dispenseCash(), causing the state machine guard to crash on undefined
output. This left the UI stuck on "Dispensing cash..." after successful
dispense.

- hal-service.ts: return DispenseResult instead of void/throwing
- main.ts: add missing return in IPC handler
- machine.ts: defensive guard (?. instead of .) as safety net

Bug found with the aid of Seoyoung at Trece Cielos.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-06 15:46:52 -05:00
commit d8841f7fe9
3 changed files with 39 additions and 8 deletions

View file

@ -34,13 +34,19 @@ export interface ValidatorCallbacks {
onError: (error: string) => void
}
export interface DispenseResult {
bills: { denomination: number; dispensed: number; rejected: number }[]
dispensed: boolean
error?: string
}
export interface HalInstance {
connectValidator: (callbacks: ValidatorCallbacks) => void
enableValidator: () => void
disableValidator: () => void
stackBill: () => void
rejectBill: () => void
dispenseCash: (amounts: { denomination: number; count: number }[]) => Promise<void>
dispenseCash: (amounts: { denomination: number; count: number }[]) => Promise<DispenseResult>
getInventory: () => Record<number, number>
cleanup: () => Promise<void>
}
@ -139,14 +145,22 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
stackBill: () => validator.stack(),
rejectBill: () => validator.reject(),
dispenseCash: async (amounts) => {
dispenseCash: async (amounts): Promise<DispenseResult> => {
console.log('[HAL] Dispensing:', amounts)
const notes: number[] = new Array(cassetteDenominations.length).fill(0)
for (const { denomination, count } of amounts) {
const idx = cassetteDenominations.indexOf(denomination)
if (idx === -1) {
throw new Error(`No cassette loaded with denomination: ${denomination}`)
return {
bills: amounts.map((a) => ({
denomination: a.denomination,
dispensed: 0,
rejected: 0,
})),
dispensed: false,
error: `No cassette loaded with denomination: ${denomination}`,
}
}
notes[idx] = count
}
@ -161,12 +175,27 @@ export async function initializeHal(config: HalConfig): Promise<HalInstance> {
}
}
// Build per-denomination result
const bills = amounts.map((a, i) => ({
denomination: a.denomination,
dispensed: result.value[i]?.dispensed ?? 0,
rejected: result.value[i]?.rejected ?? 0,
}))
const totalRequested = amounts.reduce((s, a) => s + a.count, 0)
const totalDispensed = bills.reduce((s, b) => s + b.dispensed, 0)
if (result.error) {
throw result.error
return { bills, dispensed: false, error: result.error.message }
}
await dispenser.waitForBillsRemoved()
console.log('[HAL] Bills removed by customer')
// Wait for customer to take bills
if (totalDispensed > 0) {
await dispenser.waitForBillsRemoved()
console.log('[HAL] Bills removed by customer')
}
return { bills, dispensed: totalRequested === totalDispensed }
},
getInventory: () => ({ ...inventory }),