mirror of
https://github.com/Kong/insomnia.git
synced 2026-11-03 19:45:33 -05:00
refactor(landing): startup sync as a query
"Once per organization and session" is what a query keyed by organization already does, so the startup sync no longer needs to emulate it on top of a mutation (looking the run up in the mutation cache from an effect, keeping runs with `gcTime: Infinity`, reading the status through `useMutationState` to survive StrictMode). It is now `startupSyncQuery(orgId)`: every reader shares one run, `refetch` is the retry, and `retryOnMount: false` keeps a failed run from retrying by itself when the layout mounts again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d16ead978c
commit
df74b47fa6
2 files changed
+38
-41
No files matched your search
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { QueryClient, useMutation } from '@tanstack/react-query';
|
||||
import { QueryClient, useMutation, useQuery } from '@tanstack/react-query';
|
||||
import React, { act, StrictMode } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
@@ -18,9 +18,9 @@ vi.mock('~/ui/organization-utils', () => ({
|
||||
findMigrationTargetSpaceId: vi.fn(() => 'org_target'),
|
||||
migrateProjectsUnderOrganization: vi.fn(),
|
||||
}));
|
||||
vi.mock('~/ui/context/app/server-data-context', () => ({ useServerDataQueryClient: () => queryClient }));
|
||||
vi.mock('~/ui/hooks/use-query', () => ({
|
||||
useServerMutation: (options: Parameters<typeof useMutation>[0]) => useMutation(options, queryClient),
|
||||
useServerQuery: (options: Parameters<typeof useQuery>[0]) => useQuery(options, queryClient),
|
||||
}));
|
||||
|
||||
import { services } from 'insomnia-data';
|
||||
@@ -50,7 +50,7 @@ const render = async (organizationId: string) => {
|
||||
await flush();
|
||||
};
|
||||
|
||||
// TanStack Query delivers mutation results through its batched (setTimeout) notifier.
|
||||
// TanStack Query delivers query results through its batched (setTimeout) notifier.
|
||||
const flush = () => act(() => new Promise(resolve => setTimeout(resolve, 0)));
|
||||
|
||||
describe('useStartupSync', () => {
|
||||
@@ -121,14 +121,18 @@ describe('useStartupSync', () => {
|
||||
expect(result.current?.status).toBe('idle');
|
||||
});
|
||||
|
||||
it('reports an error without retrying by itself, and a retry runs the full task set again', async () => {
|
||||
it('reports an error without retrying by itself, even on remount, and a retry runs the full task set again', async () => {
|
||||
vi.mocked(syncProjects).mockRejectedValueOnce(new Error('network'));
|
||||
|
||||
await render('org_1');
|
||||
expect(result.current?.status).toBe('error');
|
||||
|
||||
await render('org_1');
|
||||
act(() => root.unmount());
|
||||
root = createRoot(container);
|
||||
await render('org_1');
|
||||
expect(syncProjects).toHaveBeenCalledTimes(1);
|
||||
expect(result.current?.status).toBe('error');
|
||||
|
||||
await act(async () => result.current?.retry());
|
||||
await flush();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useMutationState } from '@tanstack/react-query';
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import type { Organization } from 'insomnia-api';
|
||||
import { services } from 'insomnia-data';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { syncProjects } from '~/common/project';
|
||||
import { invariant } from '~/common/utils/invariant';
|
||||
import { useServerDataQueryClient } from '~/ui/context/app/server-data-context';
|
||||
import { useServerMutation } from '~/ui/hooks/use-query';
|
||||
import { useServerMutation, useServerQuery } from '~/ui/hooks/use-query';
|
||||
import { findMigrationTargetSpaceId, migrateProjectsUnderOrganization } from '~/ui/organization-utils';
|
||||
|
||||
// Migrates legacy projects into the account's organizations and syncs the team projects of the
|
||||
@@ -29,7 +28,30 @@ async function runStartupSync(organizationId: string) {
|
||||
await syncProjects(organizationId);
|
||||
}
|
||||
|
||||
const startupSyncMutationKey = (organizationId: string) => ['startup-sync', organizationId];
|
||||
/**
|
||||
* The startup sync of one organization as a query: one run per organization and session, shared by
|
||||
* every component that reads it. Its data is only a "done" marker, so it never goes stale and is
|
||||
* kept for the session; signing out clears the cache, so the next session syncs again. A failed run
|
||||
* is not retried by itself, not even when a component mounts again: the sync indicator offers the
|
||||
* retry.
|
||||
*/
|
||||
export const startupSyncQuery = (organizationId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['startup-sync', organizationId],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
await runStartupSync(organizationId);
|
||||
} catch (error) {
|
||||
console.log('Failed to run async task', error);
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
staleTime: Infinity,
|
||||
gcTime: Infinity,
|
||||
retry: false,
|
||||
retryOnMount: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Runs the startup sync in the background the first time an organization is entered in this
|
||||
@@ -37,40 +59,11 @@ const startupSyncMutationKey = (organizationId: string) => ['startup-sync', orga
|
||||
* organizations). A retry always runs the full task set again.
|
||||
*/
|
||||
export function useStartupSync(organizationId: string) {
|
||||
const queryClient = useServerDataQueryClient();
|
||||
const mutationKey = startupSyncMutationKey(organizationId);
|
||||
const { mutate } = useServerMutation({
|
||||
mutationKey,
|
||||
mutationFn: runStartupSync,
|
||||
onError: error => console.log('Failed to run async task', error),
|
||||
// Keep every run for the session: it marks the organization as synced, and a failed one keeps
|
||||
// the indicator offering a retry. Signing out clears the cache, so the next session syncs again.
|
||||
gcTime: Infinity,
|
||||
});
|
||||
// Read the status from the mutation cache rather than from `useMutation`: under StrictMode the
|
||||
// simulated unmount detaches its observer from the in-flight run, and as the effect below skips
|
||||
// an organization that already has a run, nothing would re-attach it.
|
||||
const statuses = useMutationState(
|
||||
{ filters: { mutationKey, exact: true }, select: mutation => mutation.state.status },
|
||||
queryClient,
|
||||
);
|
||||
const status = statuses.at(-1) ?? 'idle';
|
||||
|
||||
useEffect(() => {
|
||||
// Once per organization: a run in the cache, in flight or settled, means it was synced already.
|
||||
// This also absorbs the StrictMode double effect.
|
||||
const hasRun = queryClient
|
||||
.getMutationCache()
|
||||
.find({ mutationKey: startupSyncMutationKey(organizationId), exact: true });
|
||||
if (!hasRun) {
|
||||
mutate(organizationId);
|
||||
}
|
||||
}, [mutate, organizationId, queryClient]);
|
||||
|
||||
const retry = useCallback(() => mutate(organizationId), [mutate, organizationId]);
|
||||
const { isFetching, isError, refetch } = useServerQuery(startupSyncQuery(organizationId));
|
||||
const retry = useCallback(() => refetch(), [refetch]);
|
||||
|
||||
return {
|
||||
status: status === 'success' ? 'idle' : status,
|
||||
status: isFetching ? 'pending' : isError ? 'error' : 'idle',
|
||||
retry,
|
||||
} as const;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user