Skip to content

Chore/docs retry and modal race fixes - #1289

Merged
github-actions[bot] merged 5 commits into
leojay-net:mainfrom
nonso7:chore/docs-retry-and-modal-race-fixes
Jul 26, 2026
Merged

Chore/docs retry and modal race fixes#1289
github-actions[bot] merged 5 commits into
leojay-net:mainfrom
nonso7:chore/docs-retry-and-modal-race-fixes

Conversation

@nonso7

@nonso7 nonso7 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

#1239 — get_escrow_record docs (lib.rs:3243)
Intent, parameter, all three distinct reasons it returns None and how to distinguish them, the fact it cannot fail or panic, TTL/version caveats, and a worked deposit→migrate→read example. Cross-refs use FiatBridge::… rather than Self::… — #[contractimpl] copies docs onto generated spec items where Self doesn't resolve, which is why the file's existing Self:: links all emit rustdoc warnings. Mine resolve cleanly.

#1247 — filterTelemetry docs (filterTelemetry.ts)
File header, every exported type/field, the emit helper's three guards, and each public method with a worked example. Documented explicitly that it never throws, so delivery failures are silent.

#1210 — useAccessibleModal race (useAccessibleModal.ts)
Root cause: onClose was an effect dependency and every consumer passes an inline callback, so the whole modal setup tore down and rebuilt on each parent re-render. Fallout: stacked modals clobbered each other's scroll-lock snapshot (page permanently unscrollable), focus was yanked back to the first control mid-form, and restoring focus to an unmounted opener silently stranded it. Fixed with a ref for onClose, a module-scope reference-counted scroll lock, connected-node-checked focus restore, and an error toast when the trap can't take hold. 14 regression tests — 6 fail against the old implementation, all 14 pass after.

#1043 — resend failed message (useMessageRetry.ts, Message.tsx:250)
3 automatic retries at 1s/2s/4s with a live countdown, plus a manual button that doesn't spend the automatic budget. Content comes from originalPayload.content — the text the user actually typed, which survives content being overwritten with the failure placeholder. onRetry now carries it, wired through ChatMessages to StellarChatInterface. 20 new tests; status copy added to en/fr/es.

Two things to flag
The contract WASM size guard drove a scope cut. soroban-sdk embeds doc strings in the on-chain spec, and CI's limit (92160 bytes) had only ~2.2 KB of headroom over main's 89929. I'd initially also documented migrate_escrow, get_migration_cursor, get_escrow_storage_version and EscrowRecord's fields — that came to 94589 bytes, over the limit. I cut back to get_escrow_record plus a one-line EscrowRecord doc, landing at 91173 bytes. Documenting the rest of the escrow area needs MAX_WASM_BYTES raised in .github/workflows/contracts.yml; I didn't touch it, since weakening a size guard as a side effect of a docs PR is your call.

Pre-existing failures on main, not from these changes. pnpm lint and pnpm build fail on apiSchemas.ts, OfflineStatusBanner.tsx, and a parse error in aiAssistant.test.ts; pnpm test:unit fails 19 tests across those same files plus CCIPBridgeModal. I verified identical failures on a stashed baseline. My files are lint-clean, pnpm typecheck passes, next build compiles successfully (it's the lint gate that fails), and contracts pass cargo test (282), cargo clippy -D warnings, and the WASM build.

Push only — I didn't open a PR against leojay-net/Stellar-Dex-Chat. Say the word and I'll open it, or four separate ones if you'd rather they track the issues individually.

Closes #1043
Closes #1210
Closes #1239
Closes #1247

nonso7 added 4 commits July 26, 2026 22:12
Expand the rustdoc on `FiatBridge::get_escrow_record` from nothing to a
full entry: what the function is for (the read side of the receipt→escrow
migration, and the dense-id path indexers use instead of receipt hashes),
its parameter, all three distinct reasons it can return `None` and how to
tell them apart, the fact that it cannot fail or panic, TTL and version
caveats, and a worked deposit→migrate→read example.

Also add a one-line doc to `EscrowRecord`, the type it returns.

Cross-references use intra-doc links spelled `FiatBridge::…` rather than
`Self::…`; `#[contractimpl]` copies doc comments onto generated spec items
where `Self` no longer resolves, so the surrounding `Self::` links in this
file all emit `broken_intra_doc_links` warnings. The links added here
resolve cleanly.

Docs are deliberately limited to these two items: soroban-sdk embeds doc
strings in the on-chain contract spec, and the CI WASM size guard
(92160 bytes) only had ~2.2 KB of headroom. Documenting the neighbouring
escrow-migration entry points as well pushed the artifact over the limit.
The build now sits at 91173 bytes.

No behavioural changes.
Add TSDoc across `src/lib/filterTelemetry.ts`, which had one comment on a
private helper and nothing on its public surface.

- A file header explaining why filter events ride the shared
  `chat:telemetry` channel instead of widening `ChatEventName`.
- `FilterEventName` and each payload interface, field by field, including
  the `nextValue`/`isCleared` relationship.
- `emit`: its three guards (consent, SSR, deferred dispatch), and an
  explicit note that it never throws — so delivery failures are silent and
  the channel must not be used for anything the app depends on.
- `filterTelemetry` and each method, with a worked example covering a chip
  toggle, a keyboard cycle, a wrap-around clear, and a bulk reset.
- Cross-references via `{@link …}` to the sibling `chatTelemetry` module.

No behavioural changes.
…et#1210)

Root cause: the effect listed `onClose` in its dependency array. Every
consumer passes an inline callback, so any parent re-render tore the whole
modal setup down and built it again while the modal was still open. Two
things raced as a result, and a third failure was silent.

1. Body scroll lock. Each setup snapshotted `document.body.style.overflow`
   into a per-effect local. With two modals stacked (BottomSheet over
   BankDetailsModal) the inner one snapshotted the outer one's 'hidden'.
   Closing the outer modal first unlocked the page while the inner one was
   still up; closing the inner one then wrote 'hidden' back to a page with
   no modals on it, leaving it permanently unscrollable.

2. Focus. Every re-setup re-applied initial focus, yanking the user back to
   the first control mid-form, and every teardown bounced focus through the
   opener on the way past.

3. Focus restore. `previousActive?.focus()` on a node that had since been
   unmounted is a no-op, silently stranding focus on the closing modal's
   own controls.

Fix:

- `onClose` is read through a ref, so the effect keys on `isOpen` alone and
  runs exactly once per open/close. The latest callback is still the one
  invoked on Escape.
- The scroll lock is reference-counted at module scope: only the first
  acquire records the pre-modal overflow, only the last release restores it.
- Focus is restored only to an opener still connected to the document;
  otherwise the active element is blurred so focus falls back to the body.
- Error paths surface a message instead of failing silently. If the
  container never mounts (retried once on the next frame, for animated
  sheets) or holds nothing focusable and cannot take focus itself, the hook
  raises an error toast — overridable via a new `onError` option — telling
  the user the dialog is not keyboard-navigable. Reported at most once per
  open so a held Tab key cannot spam it.

Regression tests: 14 cases in `useAccessibleModal.test.tsx`. Six of them
fail against the previous implementation — the stacked-modal lock, focus
stolen on re-render, focus restore to a detached opener, and the three
error-path cases — and all 14 pass after the fix.

No `any` casts; `pnpm typecheck` and `pnpm test:unit` are unchanged.
…1043)

A message that failed to send showed an error indicator with a Retry
button that no caller ever wired up, so users had to retype the message.

- `useMessageRetry` (new) owns the retry policy: up to 3 automatic resends
  on exponential backoff (1s, 2s, 4s), a countdown to the next attempt, and
  a `retryNow` escape hatch. Manual retries cancel the pending timer and
  resend immediately without spending the automatic budget, so the
  remaining attempts still follow if the resend fails again. A message that
  recovers gets a fresh budget. Rejected and synchronously-throwing
  handlers are treated as ordinary failures rather than stalling the loop.

- `Message` renders the retry action with a live-region status line
  ("Retrying automatically in Ns (attempt 1 of 3)" → "Resending…" →
  exhausted), disables the button while a resend is in flight, and counts
  attempts already recorded on the message alongside its own.

- Retry content comes from `message.originalPayload.content`, falling back
  to `message.content`. That is what pre-populates the resend: the original
  text the user typed survives even though `content` is overwritten with a
  failure placeholder when a send fails.

- `onRetry` now carries that content — `(messageId, content)` — so callers
  do not have to reconstruct it. Wired through `ChatMessages` to
  `StellarChatInterface`, which resends via `sendMessage`.

- Status copy added to the en/fr/es locale files.

Tests: 12 cases for the hook, 8 for the component.
@drips-wave

drips-wave Bot commented Jul 26, 2026

Copy link
Copy Markdown

@nonso7 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

`pnpm lint` (and therefore `pnpm build`) failed on main, and 19 unit tests
failed alongside it. All of these predate the current branch.

Lint:

- `aiAssistant.test.ts` did not parse: the last `it()` in the abort-signal
  suite was never closed, so its `});` closed the `it` and left the
  `describe` open. Added the missing brace.
- `apiSchemas.ts` stapled `status` and `response` onto a plain `Error`
  through two `any` casts. Replaced with an `HttpResponseError` class that
  declares both as typed readonly fields.
- `apiSchemas.test.ts` had an unused `startTime` and an `as any` on a
  `setTimeout` spy.
- `OfflineStatusBanner.tsx` kept a `pendingCount` state nothing read and two
  optimistic-update callbacks nothing called. The queued count is owned by
  `offlineMessageQueue` and published by `useChat`; the banner only mirrors
  it, so the local copies were redundant. Removed, along with the imports
  they were the only users of.
- `OfflineStatusBanner.test.tsx` imported `renderHook` without using it.

Two real bugs surfaced while fixing the above:

- `withRetry` decided status retries with `error instanceof Response`. The
  value thrown by `fetchWithRetry` is an `Error`, never a `Response`, so
  that arm was dead and 500/503/429 responses were never retried — the
  `any` casts were what hid the status from the check. It now tests
  `HttpResponseError` (keeping the `Response` arm for callers that throw one
  directly). An explicit `AbortError` guard was also added, so an aborted
  request is never retried regardless of a caller's `retryableErrors`.

- `OfflineStatusBanner` ignored `wasOffline`, announcing a reconnect only
  when it observed the offline render itself. Mounting after the connection
  was already back skipped the toast and never called `resetWasOffline`,
  leaving the hook's latch set forever. The reconnect branch now also fires
  on a latched `wasOffline` and consumes the latch immediately.

Tests: the failures were fake-timer deadlocks. Suites installed
`vi.useFakeTimers()` and then awaited code that sleeps between retries, or
polled with `waitFor` while the clock was frozen — so the promise never
settled and the test hit its 15s timeout. Fixed by draining timers with
`vi.runAllTimersAsync()`, attaching rejection handlers before draining (an
unhandled rejection otherwise escapes), and replacing frozen-clock `waitFor`
calls with assertions made directly after an explicit `advanceTimersByTime`.

`OfflineStatusBanner`'s pending-count test also pulled from
`getQueuedMessageCount`, which the component never reads; it now drives the
subscription the component actually listens to.

pnpm typecheck, pnpm lint, pnpm build and pnpm test:unit (805 tests) all
pass.
@github-actions
github-actions Bot merged commit 6bce56a into leojay-net:main Jul 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant