Skip to content

Instrument bundle refetches on window focus although the bundle for an ID does not change #1538

Description

@gdevenyi

Current Implementation and Limitations

services/react-query.ts does not set staleTime or refetchOnWindowFocus (react-query.ts). With the TanStack Query v5 defaults, a mounted query with stale data refetches when the tab becomes visible again (visibilitychange). staleTime is 0 by default, and useSuspenseQuery raises it to 1 second. retry is already false (#L12).

On the render page, useInstrumentBundle refetches the bundle each time the tab becomes visible again (useInstrumentBundle.ts#L7-L19). In a clinic, users often switch between the app and other programs during a visit. This has two costs:

  1. Network. Each refetch downloads the full bundle again. A bundle that inlines media can be many megabytes.
  2. Lost data. Queries set throwOnError: true (#L13). TanStack Query v5 sends a failed background refetch to the error boundary, even when the query already has data. If the refetch fails, for example with a 401 after the token expires (Session expires after one hour without warning, and in-progress instrument data is lost #1535), the error page replaces the open instrument and the entered data is lost. This is the stronger reason for the change.

The bundle for a given ID does not change:

  • A scalar instrument ID is hash(`${name}-${edition}`) (instruments.service.ts#L456-L458). It is not a hash of the content. But create returns 409 for an ID that already exists (#L112, #L151), repository sync treats that 409 as "already stored", and the only updates to an instrument row change sourceRepoId and sourceRepoName (instrument-repos.service.ts#L443-L470).
  • A series instrument ID is a hash of its content, title and owning group (#L460-L467). Only series can be deleted (#L218-L221). A series that is deleted and created again with the same title and items, but with other details changed, gets the same ID and a new bundle.

The first version of this issue said that all instrument IDs are content hashes. That is correct only for series.

Associated Application Components

Client

Proposed Solution

Set staleTime: Infinity on the bundle query only. useMailSettingsQuery and useSetupStateQuery already do this (useMailSettingsQuery.ts#L14, useSetupStateQuery.ts#L14). Also remove the cached bundles when a series is deleted, so that a series created again with the same ID does not show the old bundle:

// apps/web/src/hooks/useInstrumentBundle.ts
staleTime: Infinity,

// apps/web/src/hooks/useDeleteSeriesInstrumentMutation.ts, onSuccess
queryClient.removeQueries({ queryKey: ['instrument-bundle'] });

Do not change the global defaults (refetchOnWindowFocus: false or a global staleTime). Many mutations do not invalidate the queries they affect, so a refetch when the tab becomes visible is how a user sees changes, including changes that other users make:

  • useCreateSessionMutation does not invalidate the subjects list or the summary.
  • The record submit on the render page (a raw axios.post) and useUploadInstrumentRecordsMutation invalidate nothing.
  • useUpdateGroupMutation invalidates nothing. Its callers update only their own view: group/manage.tsx calls changeGroup, and GroupEmailTemplates sets its own group query. The groups list is not invalidated.
  • useDeleteGroupMutation invalidates the groups, but not the users (useDeleteGroupMutation.ts#L14).

The first version of this issue said that mutations already invalidate the queries they affect. The list above shows that this is not correct.

Separately, a failed background refetch should not replace a page that already has data. One option is throwOnError: (error, query) => query.state.data === undefined. apps/web/AGENTS.md makes throwOnError: true a rule, so this change needs a decision from the maintainers. See #1535.

Tests. A unit test that useInstrumentBundle does not refetch when the window gets focus. A unit test that useDeleteSeriesInstrumentMutation removes the cached bundles. An e2e test that opens an instrument, hides the tab and shows it again, and asserts that no new GET /v1/instruments/bundle/:id request occurs.

Estimated Difficulty

Low

Priority

Low

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions