| 1 | import { useMemo } from "react"; |
| 2 | import { CommandCancelled, executeCapturedCommand, type CommandAuthority, type CommandOutcome } from "./commandOutcome"; |
| 3 | import { useCommittedSlot, type CommittedSlot } from "./useCommittedSlot"; |
| 4 | |
| 5 | type CommandDefinition<Args extends unknown[], Input, Result> = { |
| 6 | capture: (...args: Args) => Input; |
| 7 | execute: (input: Input, authority: CommandAuthority) => Result; |
| 8 | }; |
| 9 | |
| 10 | function captureAuthority(slot: CommittedSlot<unknown>): CommandAuthority { |
| 11 | const epoch = slot.epoch; |
| 12 | const requestId = ++slot.requestId; |
| 13 | return { |
| 14 | checkpoint() { |
| 15 | if (slot.phase !== "ready" || slot.epoch !== epoch) throw new CommandCancelled("disposed"); |
| 16 | if (slot.requestId !== requestId) throw new CommandCancelled("superseded"); |
| 17 | }, |
| 18 | }; |
| 19 | } |
| 20 | |
| 21 | function bindCommittedAsync<Args extends unknown[], Input, Result>( |
| 22 | slot: CommittedSlot<CommandDefinition<Args, Input, Result>>, |
| 23 | ) { |
| 24 | return (...args: Args): Promise<CommandOutcome<Awaited<Result>>> => { |
| 25 | if (!slot.value || slot.phase !== "ready") { |
| 26 | return Promise.resolve({ status: "cancelled", reason: slot.phase === "disposed" ? "disposed" : "not-ready" }); |
| 27 | } |
| 28 | const authority = captureAuthority(slot); |
| 29 | try { |
| 30 | const { capture, execute } = slot.value; |
| 31 | return executeCapturedCommand(capture(...args), execute, authority); |
| 32 | } catch (error) { |
| 33 | return Promise.resolve(error instanceof CommandCancelled |
| 34 | ? { status: "cancelled", reason: error.reason } |
| 35 | : { status: "failed", error }); |
| 36 | } |
| 37 | }; |
| 38 | } |
| 39 | |
| 40 | /** One command lane. Use independent owners for unrelated resource operations. */ |
| 41 | export function useCommittedAsyncCommand<Args extends unknown[], Input, Result>( |
| 42 | capture: (...args: Args) => Input, |
| 43 | execute: (input: Input, authority: CommandAuthority) => Result, |
| 44 | ) { |
| 45 | const slot = useCommittedSlot({ capture, execute }); |
| 46 | return useMemo(() => bindCommittedAsync(slot), [slot]); |
| 47 | } |
| 48 |