Keep peer payment callbacks bound to their original modal operation

This commit is contained in:
archipelago
2026-10-06 21:27:55 -04:00
parent 530c497277
commit 47cd915e40
4 changed files with 415 additions and 155 deletions
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import { usePeerPaymentOperations } from './peerPaymentOperations'
describe('payment operation completion ownership', () => {
it('does not let a duplicated old completion clear the next operation in the same modal', () => {
const ui = usePeerPaymentOperations(() => ({ peer: 'seller', item: 'file', generation: 1 }))
const first = ui.begin('invoice', 'seller', 'file')!
expect(ui.finish(first)).toBe(true)
const second = ui.begin('invoice-status', 'seller', 'file')!
expect(ui.selected(first)).toBe(false)
expect(ui.finish(first)).toBe(false)
expect(ui.current.value?.token).toBe(second.token)
expect(ui.busy.value).toBe(true)
expect(ui.finish(second)).toBe(true)
expect(ui.busy.value).toBe(false)
})
})
@@ -0,0 +1,38 @@
import { computed, reactive } from 'vue'
export type PaymentSelection = { peer: string; item: string; generation: number } | null
export type PaymentOperation = { peer: string; item: string; generation: number; kind: string; token: symbol }
/** In-flight UI ownership complements durable node/browser attempt journals.
* An old callback may persist its own receipt, but cannot own a new modal. */
export function usePeerPaymentOperations(selection: () => PaymentSelection) {
const slots = reactive(new Map<string, PaymentOperation>())
const key = (peer: string, item: string) => JSON.stringify([peer, item])
function begin(kind: string, peer: string, item: string): PaymentOperation | null {
const index = key(peer, item)
if (slots.has(index)) return null
const current = selection()
if (!current || current.peer !== peer || current.item !== item) return null
const operation = { peer, item, generation: current.generation, kind, token: Symbol(kind) }
slots.set(index, operation)
return operation
}
function selected(operation: PaymentOperation): boolean {
const current = selection()
return slots.get(key(operation.peer, operation.item))?.token === operation.token
&& current?.peer === operation.peer && current?.item === operation.item && current?.generation === operation.generation
}
function finish(operation: PaymentOperation): boolean {
const index = key(operation.peer, operation.item)
// Never reset a newer operation's busy flag, even for the same file.
const owned = slots.get(index)?.token === operation.token
const wasSelected = owned && selected(operation)
if (owned) slots.delete(index)
return wasSelected
}
const current = computed(() => {
const value = selection()
return value ? slots.get(key(value.peer, value.item)) : undefined
})
return { begin, selected, finish, current, busy: computed(() => current.value !== undefined) }
}