fix(tx): write the payload receipt from the request that issued it - #289
Conversation
`loadTransactions()` stamps three receipts for the payload it just loaded (`txTable.loadedPage` / `loadedPageSize` / `loadedQuerySig`), and `renderTransactions()` compares them with the *current* controls to decide whether to re-query. The first two are captured before the `await` — they belong to the request. The signature was not: it was read from live state when the response landed, so the guard compared the receipt against its own projection and was satisfied by construction. Two control changes inside one RTT therefore put two requests with different payloads in flight, and the superseded one carries no identity — if it lands last it is adopted *and certified as current*. The table then keeps the rows of a filter the user has already left, and nothing ever re-checks. Capture the signature before the request goes out (`const reqSig = txQuerySig();`) and stamp that value, like its two sibling receipts. Companion gate in `src/state_gate.rs` (four derivable rules: no receipt RHS may be a call; a bare-identifier RHS must be bound in the same function before its first `await`; the signature receipt must be initialised from the very function the guard compares against; and the guard/roster must stay non-empty). Both instruments were re-run against the landed bytes: the compiled gate is green here and red on exactly the axis test with the fix removed, and the jsdom probe is 5/5 trees as declared (fix + generation-guard green; unfixed, in-flight-skip and cosmetic-sync rejected).
|
Self-review (author, committer with Re-read the diff at Checks re-run locally on the branch tip (
Two extra checks worth recording, because both are claims the diff makes in prose:
The jsdom probe's per-tree readings (5/5 as declared, re-generated from the landed bytes) are in the No further changes requested from myself; merging at CI green. |
Summary
loadTransactions()stamps three receipts for the payload it just loaded(
txTable.loadedPage/loadedPageSize/loadedQuerySig), andrenderTransactions()compares them with the current controls to decide whether tore-query. The first two are captured before the
await— they belong to the request.The signature was not: it was read from live state when the response landed
(
txTable.loadedQuerySig = txQuerySig();), so the guard compared the receipt againstits own projection and was satisfied by construction.
Consequence: two control changes inside one RTT put two requests with different payloads
in flight. The superseded one carries no identity, so if it lands last it is adopted and
certified as current — the table keeps the rows of a filter the user has already left, and
nothing ever re-checks (zero further requests). Reached by any two changes inside one RTT
(the top tabs, the type column filter, the range select, the custom start/end inputs — all
four go through the same trigger
reloadTransactions(); the text filter's 300 ms debouncecan also overlap an in-flight request).
Related Issue
None. (Reported and instrumented inside this repository's own defect-hunting loop.)
Changes
ui/js/app.js: capture the signature before the request goes out(
const reqSig = txQuerySig();) and stamp that value — exactly the shape its two siblingreceipts already had. Net effect: one captured identifier + one changed right-hand side.
src/state_gate.rs: new gatethe_receipt_is_written_by_the_request_that_issued_it(R1 no receipt right-hand side may be a call; R2 a bare-identifier right-hand side must be
bound in the same function before its first
await; R3 the signature receipt must beinitialised from the very function the guard compares against; R4 reverse/non-empty — the
guard must still compare against a call, and the roster/write sites must stay non-empty).
All four expectations are derived from the file (roster from
txTable.loaded*,the compared function from the guard line), not spelled out as this edit's literals.
Companions:
the_r155_rules_have_teeth(four synthetic mutants, each flipping exactly onerule),
the_r155_rules_separate_the_variants, andthe_r155_extractor_lands_on_real_function_bodies.ui/index.html: cache-bust token forapp.js.ui/README.md: the convention, plus an honest scope note.Tests
cargo test— 365 passed, 0 failed (361 before; the four new tests are added)cargo fmt --check— cleancargo clippy --all-targets -- -D warnings— cleanInstrument 1 — compiled gate (A/B over two trees). With the fix removed
(
ui/js/app.jsmd5c6ca93107395…, i.e. the parent commit's bytes) and the landed gatecompiled against it:
69 passed; 1 failedand the one failure is exactlythe_receipt_is_written_by_the_request_that_issued_it(R155 未修:收据在响应落地后读活状态,r1bad=["line 1827: txTable.loadedQuerySig = txQuerySig()"]). On the landed bytes the samegate is green.
Instrument 2 — jsdom probe (real four scripts, real controls, real
fetch, parkedresponses released in a chosen order; the "user's intent" is the click the instrument itself
drove, never an indicator read back out of the page). Re-run against the landed bytes:
5/5 trees as declared.
app.jsmd5c6883a954598…)A1,A2,A3m1ignore the change while in flightP0,P0b,A1,A2,A3,C2m2pull the controls back to what was loadedA1,A2,A3Landed reading:
rows belong to the current filter (consume)=true,#tx-count=3,consume-list requests=2(the app re-asked). Base reading:table says earn=truewhile theclicked filter was
consume,#tx-count=2 expected 3,consume-list requests=1(the appnever re-asked).
Known asymmetry (deliberate, measurable). The two instruments judge the
generation-guard-only variant differently: the probe accepts it (terminal state correct, and
it even saves a request), the gate rejects it (R1) — "who authored the receipt" is one claim
on this axis, and a receipt kept by a second mechanism alone is not the defect fixed. That is
recorded in
ui/README.mdrather than smoothed over.Checklist
fix/…)and one changed right-hand side; the rest is the gate, the prose and the cache-bust)