# `phases/prepare` ## Purpose Implements phase 1 of the 2PC protocol for a single participant. The coordinator iterates participants and calls this module for each; unanimous commit votes trigger phase 2, any abort or prepare failure short-circuits to rollback. This module is the only place the `PREPARE TRANSACTION ''` SQL is emitted, so the gid format (`phases/gid`) and the SQL emission point stay co-located. Phase 1 is the latency-sensitive half of 2PC: every participant holds row-level locks for the duration. The module records `2pc.prepare_duration_ms` so operators can spot creeping lock holds before they cause throughput regressions. ## Features - Runs the participant's `prepare` callback, then the `PREPARE TRANSACTION` SQL when the vote is `commit` AND the participant is a writer. - Skips the SQL for read-only voters — they participate in the vote only. - Refuses a read-only voter that returned `writeIntent: true` with `TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE`. - Honours an `AbortSignal` for the phase-1 deadline; aborted SQL is remapped to `TWOPC_TIMEOUT`. - Emits a `2pc.prepare` span around the whole call and records a `2pc.prepare_duration_ms` observation tagged with the outcome. - Failure cases preserve the underlying cause on the thrown `TwoPcError`. ## Usage: Basic ```ts import { preparePhaseOne } from "@suaveplan/api-2pc/phases"; const outcome = await preparePhaseOne({ hostInstanceId: "host-01", txId, participant, payload: { invoiceId: "inv-1" }, instruments, signal: ac.signal, }); if (outcome.vote === "abort") { // run rollback for every participant that voted commit } ``` ## Usage: Advanced The coordinator runs this in a loop. Read-only voters interleave freely with writers — the outcome's `prepared` flag tells phase 2 whether to send commit/rollback SQL: ```ts import { preparePhaseOne } from "@suaveplan/api-2pc/phases"; const outcomes: ParticipantPrepareOutcome[] = []; for (const p of registry.list()) { const o = await preparePhaseOne({ hostInstanceId, txId, participant: p, payload: payloadFor(p), instruments, signal, }); outcomes.push(o); if (o.vote === "abort") break; // short-circuit } // phase 2: commit only those with prepared === true const writers = outcomes.filter((o) => o.prepared); ``` ## API Reference - `interface ParticipantPrepareOutcome` — `{ participantId, vote, prepared, gid }`. - `interface PreparePhaseInput` — `{ hostInstanceId, txId, participant, payload, instruments, signal }`. - `function preparePhaseOne(input): Promise` - throws `TWOPC_PREPARE_FAILED` when the participant callback or SQL raises (signal not aborted), - throws `TWOPC_TIMEOUT` when the signal aborted around the SQL, - throws `TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE` when a read-only voter returned `writeIntent: true`. ## Implementation Notes The SQL escape strategy is conservative: gids are built from the host instance id, transaction id, and participant id — all three are already validated at host config / id-generator / registry time — but the function still calls `.replace(/'/g, "''")` before interpolating into the `PREPARE TRANSACTION` statement. The runtime cost is zero on the happy path and the defence-in-depth is cheap. The coordinator never accepts user-supplied SQL fragments. The span is started before the prepare callback so the participant's own work shows up in the trace as a child of the `2pc.prepare` span if the participant opens its own spans against the same tracer. The `finally`-block `span.end()` guarantees the span is closed regardless of which branch fires.