shoc-frontend-new/docs/adr/0001-query-broadcast-client.md
Arthur Bassi 4af826a1e0
feat(work-orders): query broadcast, remove auth bypass, dispatch polish (#100)
* fix(work-orders): address wizard create review findings

[recover] remove malicious eslint payload (was 166c63e4)

* fix(work-orders): remove legacy completedDate EditWorkorder path

[recover] remove malicious eslint payload (was f4e6132b)

* fix(work-orders): use local calendar day for completedDate

Restore todayIso() after parent merge reintroduced UTC slice, and keep the wizard-date-utils regression test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(work-orders): send wizard status and clear POC notes on site change

Map draft.status to lifecycleStatus on board create, and reset pocNotes with POC fields when the wizard site changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(query): scope broadcast channel per account and clear on auth

Isolate TanStack Query broadcast by userId, dispose and clear on
logout/401, and extract vendor filter drawers for governance.

* feat(work-orders): board wizard create with service notes

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(work-orders): enforce single-service wizard create per SH-118

Use single service selection in the wizard and omit extraServices from board create.

* fix(vendors): restore filter drawer labels for e2e and visual CI

Use VendorFilterOptions again and keep the Apply filters footer label expected
by vendor Playwright specs and the committed visual baseline.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 11:52:48 -03:00

4.8 KiB

0001. Cross-tab QueryClient sync via @tanstack/query-broadcast-client-experimental

Status

Accepted

Context

The SeaHaven admin SPA is frequently used with multiple browser tabs open against the same work-order board (e.g. a dispatcher triaging the board in one tab while editing a work order in another). Each tab owns its own TanStack Query QueryClient cache, so a mutation performed in one tab (status change, dispatch creation, comment, vendor patch, etc.) does not invalidate or update the cache in sibling tabs. Users were seeing stale board/detail data until a manual refresh or the next background refetch.

We need a way to keep QueryClient caches roughly in sync across tabs of the same origin, without introducing a new state-management layer (Redux is disallowed by this repo's conventions) or a server-push mechanism.

Decision

Use TanStack's own experimental broadcast client, @tanstack/query-broadcast-client-experimental, wired up in a single dedicated setup module, src/lib/query/setup-query-broadcast.ts. The module:

  • Exposes startQueryBroadcast(queryClient, userId) and stopQueryBroadcast(queryClient).
  • Keys broadcastQueryClient to an account-scoped channel (seahaven-admin-query:${userId}), so tabs belonging to different accounts on the same origin do not share cache traffic.
  • Disposes the prior subscription (the unsubscribe returned by broadcastQueryClient) and clears the QueryClient when the authenticated user changes or the session ends.
  • No-ops the BroadcastChannel outside the browser (SSR/build) and under Vitest (import.meta.env.MODE === "test").
  • Is started after authentication (AuthProvider session restore + login success) and stopped on logout and HTTP 401 session clear — not at QueryClient module load.

This piggybacks on the query cache we already have (no parallel store), uses the library that owns the QueryClient we already depend on, and keeps the integration isolated to auth/session boundaries.

Alternatives considered

  • Custom BroadcastChannel + manual queryClient.invalidateQueries calls — full control over payloads, but requires hand-rolling serialization/versioning of query keys and mutation results, and keeping every future mutation hook wired to broadcast. More code to own and more surface area for subtle cache-desync bugs.
  • Constant channel from module load — simplest wiring, but shares one channel across all sessions on the origin; after logout/login or multi-account use, cached work-order/vendor data can leak into the next session. Rejected.
  • No cross-tab sync — simplest option, but leaves the stale-tab UX problem unresolved; users would need to manually refresh or wait for refetchOnWindowFocus/staleTime to catch up, which is not reliable enough for a live dispatch board.
  • Full WebSocket-based real-time sync — solves both cross-tab and cross-user staleness, but is a materially larger investment (server-side push infra, connection lifecycle, auth over the socket) that is out of scope for the current admin SPA and not justified by the actual problem (same browser, same user, same origin).

Consequences

  • Positive: sibling tabs for the same authenticated account reflect mutations (status changes, dispatch actions, comments, patches) without a manual refresh; logout/401 tear down the broadcaster and clear sensitive cache so the next account cannot inherit prior data.
  • Risk — package lifecycle: the dependency is explicitly "experimental" in the TanStack ecosystem; its API may change or be deprecated between minor versions. @tanstack/query-broadcast-client-experimental, @tanstack/react-query, and @tanstack/react-query-devtools are pinned to the exact same version in package.json (no ^ range). All three resolve to the identical @tanstack/query-core version at that pin, which keeps the QueryClient type used by the broadcast helpers structurally identical to the one constructed in query-client.ts — a caret range lets npm resolve the broadcast client and React Query against two different query-core versions independently, which breaks QueryClient type identity (TS2322) even though both packages build fine in isolation. Bump all three together and re-evaluate on every TanStack Query upgrade.
  • Risk — same-origin only: BroadcastChannel only syncs tabs on the same origin; it does not sync across different users/sessions or devices. Account scoping further limits sync to tabs of the same userId.
  • Rollback plan: remove the @tanstack/query-broadcast-client-experimental dependency from package.json, delete setup-query-broadcast.ts, and remove the startQueryBroadcast / stopQueryBroadcast call sites in auth and api.ts. No other code depends on it, so rollback is isolated with no data migration.