refactor!: simplify query and session ownership
Frontend verify / commits (pull_request) Failing after 27s
Frontend verify / security (pull_request) Skipped
Frontend verify / verify (pull_request) Skipped

Use Query cancellation and native mutation results instead of HTTP
registries, access epochs and copied scope state.
Keep local auth ordering and mutation identity fences.
Allow staged generated files to remain excluded from ESLint.

Backend contract: WealthLedger/backend@d698ed9

BREAKING CHANGE: drop investment stateVersion/progress/resources
from the client contract and use data/metadata readiness instead of
rewritten query status. Cross-tab auth may require re-login.
This commit is contained in:
2026-10-03 11:45:04 +03:00
parent 6a89b08ee5
commit 69997075e5
25 changed files with 151 additions and 551 deletions
+31 -33
View File
@@ -2,7 +2,7 @@ import { act, renderHook, waitFor } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { describe, expect, it, vi } from "vitest";
import { apiClient, configureAuthClient } from "@/shared/api";
import { configureAuthClient } from "@/shared/api";
import { createQueryWrapper } from "@/test/query";
import { resource } from "@/test/resource";
import { server } from "@/test/server";
@@ -37,7 +37,7 @@ function auth() {
}
describe("persistent resource query lifecycle", () => {
it("observes into the canonical cache and rejects an old notified state before accepting the current one", async () => {
it("observes once into the canonical cache without another import", async () => {
auth();
const modes: (null | string)[] = [];
let observations = 0;
@@ -51,7 +51,7 @@ describe("persistent resource query lifecycle", () => {
return HttpResponse.json(
resource(snapshot, {
stateVersion: observations > 1 ? "new" : "old",
dataRevision: observations > 0 ? "new" : "old",
}),
);
},
@@ -60,16 +60,16 @@ describe("persistent resource query lifecycle", () => {
const { queryClient, wrapper } = createQueryWrapper();
const { result } = renderHook(() => usePrivateResource("a"), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
await act(() => result.current.observe("new"));
await waitFor(() => expect(result.current.meta?.stateVersion).toBe("new"));
await act(() => result.current.observe());
await waitFor(() => expect(result.current.meta?.dataRevision).toBe("new"));
expect(modes).toEqual([null, "observe", "observe"]);
expect(modes).toEqual([null, "observe"]);
expect(queryClient.getQueryCache().getAll()).toHaveLength(1);
expect(
queryClient.getQueryData(resourceOptions("a").queryKey),
).toMatchObject({ data: snapshot, meta: { stateVersion: "new" } });
).toMatchObject({ data: snapshot, meta: { dataRevision: "new" } });
});
it("does not convert missing or failed source data to empty financial success", async () => {
it("keeps successful HTTP state distinct from missing or failed source data", async () => {
auth();
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
@@ -91,24 +91,17 @@ describe("persistent resource query lifecycle", () => {
await waitFor(() => expect(result.current.meta?.sync.state).toBe("failed"));
expect(result.current.data).toBeUndefined();
expect(result.current.isSuccess).toBe(false);
expect(result.current.status).toBe("pending");
expect(result.current.isPending).toBe(true);
expect(result.current.isSuccess).toBe(true);
expect(result.current.status).toBe("success");
expect(result.current.isPending).toBe(false);
expect(result.current.isLoading).toBe(false);
expect(result.current.transport).toEqual(
{
error: null,
fetchStatus: "idle",
status: "success",
},
);
expect(result.current.meta?.availability).toBe("missing");
});
it("keeps the refetch envelope while exposing the refreshed resource payload", async () => {
auth();
let isReady = false;
const refreshed = resource(snapshot, { stateVersion: "ready" });
const refreshed = resource(snapshot, { dataRevision: "ready" });
server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
HttpResponse.json(isReady ? refreshed : resource(null)),
@@ -120,7 +113,7 @@ describe("persistent resource query lifecycle", () => {
});
await waitFor(() =>
expect(result.current.transport.status).toBe("success"),
expect(result.current.status).toBe("success"),
);
await act(async () => {
const missing = await result.current.refetch();
@@ -140,35 +133,40 @@ describe("persistent resource query lifecycle", () => {
expect(result.current.isSuccess).toBe(true);
expect(result.current.isPending).toBe(false);
});
it("rejects a late ordinary response when an observe response for that resource arrived first", async () => {
it("keeps the observed snapshot when an older query finishes later", async () => {
auth();
const { promise: blocked, resolve: release }
= Promise.withResolvers<void>();
let isStarted = false;
server.use(
http.get(
"*/api/v1/brokerage-accounts/:accountId/portfolio",
async ({ request }) => {
if (!new URL(request.url).searchParams.has("sync")) await blocked;
if (!new URL(request.url).searchParams.has("sync")) {
isStarted = true;
await blocked;
return HttpResponse.json(resource({ ...snapshot, isStale: true }));
}
return HttpResponse.json(resource(snapshot));
},
),
);
const old = apiClient.GET(
"/api/v1/brokerage-accounts/{accountId}/portfolio",
{ params: { path: { accountId: "a" } } },
);
// eslint-disable-next-line vitest/valid-expect -- Attach rejection handling before releasing the request; the assertion is awaited below.
const rejected = expect(old).rejects.toMatchObject({ name: "AbortError" });
await apiClient.GET("/api/v1/brokerage-accounts/{accountId}/portfolio", {
params: { path: { accountId: "a" }, query: { sync: "observe" } },
const { result } = renderHook(() => usePrivateResource("a"), {
wrapper: createQueryWrapper().wrapper,
});
release();
await rejected;
await waitFor(() => expect(isStarted).toBe(true));
await act(() => result.current.observe());
await act(async () => {
release();
await blocked;
});
expect(result.current.data).toEqual(snapshot);
});
it("clears private snapshots on confirmed access loss and fences another pending success", async () => {
auth();