Files
Stirling-PDF/frontend/editor
ConnorYohandAnthony Stirling ead8a536d2 feat(editor): move signing sessions onto TanStack Query (#7436)
# Description of Changes

Step 4 of the TanStack Query rollout, and the first of the polling
hooks. Follows #7264, #7283, #7285.

## The problem

`useSigningSessions` hand-rolled its own fetch, loading state and
`setInterval`. Two consequences:

- **A raw `setInterval` keeps polling a hidden tab.** Browsers throttle
background timers, they do not stop them, so a backgrounded editor with
Shared Sign open keeps hitting both endpoints for as long as it is open.
- **No tests.** The hook had none, and its quietest behaviour (below) is
the easiest thing to break without noticing.

## End state

One query behind `qk.signingSessions()`, with the polling lifecycle
handed to the library:

- Polling stops while the tab is hidden, and refetches on return rather
than leaving data up to a full interval stale.
- Mounts render from cache while they revalidate, so moving between the
tool picker and the signing tool no longer flashes an empty list.
- 12 tests where there were none.

Same return shape, so no consumer files change.

### What this is not

This is not a deduplication win. The three consumers are never mounted
at the same time: `ToolPanel` renders the tool picker or the active tool
and never both, so the badge cannot be on screen with either of the
others, and `SharedSigningLauncher` and `useSigningSessionController`
sit inside two different tools. The shared key earns its keep on cache
reuse across those transitions, not on concurrent fetches.

## The bit worth reviewing

The hand-rolled `{ silent: true }` flag encoded three states, and no
single Query flag reproduces them:

| | Spinner | Toast on failure |
|---|---|---|
| First load | yes | yes |
| Background poll | no | no |
| Explicit refetch | **yes** | **yes** |

`isLoading` is false during an explicit refetch when data is already on
screen; `isFetching` is true during a background poll. Neither matches,
so the user-initiated case is tracked with a small flag and the failure
toast is gated on `isLoadingError` plus the explicit path.

## Testing

Twelve tests. Rather than trust them, each claim was checked by breaking
the implementation and confirming the relevant test fails:

| Mutation | Caught by |
|---|---|
| `refetchIntervalInBackground: true` | hidden-tab test |
| Drop `refetchOnWindowFocus` | returns-to-view test |
| Drop the user-initiated spinner flag | manual-refresh test |
| Toast on every error | background-failure-is-silent test |
| Give each observer its own key | dedupe test |

Three things worth knowing for the next conversion:

- **`waitFor` flushes renders.** Recording an index *after*
`waitFor(callCount === 2)` skips past the in-flight render, so a "did
the spinner flip on" assertion passes vacuously. The marker has to go
before the poll.
- **Fake timers hide in-flight state.** The fetch settles inside the
same `act()`, so the intermediate render never happens. That test uses
real timers and a held-open promise.
- **`visibilitychange` has to bubble.** query-core listens for it on
`window`, and the real event bubbles from `document`. A test helper
dispatching a non-bubbling event never reaches the focus manager, and
the pause behaviour still appears to work because `refetchInterval`
reads `document.visibilityState` directly at tick time rather than
through the event.

**One claim is deliberately unguarded.** `isLoading` vs `isFetching` for
a background poll produces no re-render at all, so there is nothing
observable for a test to assert and no user-visible difference to
protect.

## Pre-existing failures

`task frontend:check` passes typecheck, lint and oxfmt, and 2363 of 2365
editor tests. The two failures, `workbenchSession.test.ts` and
`notificationActions.test.tsx`, fail identically with this branch's
changes reverted and are untouched by it.

## Scope

This is one of five pollers. The remaining four, `useLocalFolderPoller`,
`WatchedFolderWorkbenchView`, `SessionDetailPanel` and cloud
`TeamSection`, are separate files with their own consumers and follow
separately, now that the silent-refresh pattern has a worked example.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-08-29 10:35:40 +00:00
..
2026-08-24 20:45:54 +00:00