mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 10:05:41 +02:00
perf(issues): make the URL rewrite to the identifier cost no extra request
Opening an issue from an in-app link fetched it twice. In-app links still carry the UUID, so the route lands on the UUID URL, loads the issue, then rewrites the address bar to the identifier. That rewrite is a navigation: the route re-renders with the identifier as its segment, `useCanonicalIssue` sees a non-UUID, and its resolution query misses on an identifier-keyed cache entry nothing has filled — so it re-fetches an issue already in hand. Measured across the rewrite: 2 requests for one issue open. Mirror the loaded row into its identifier-keyed entry, the reverse of the `initialData` hop that already covers the identifier-first direction. Both directions now hold at one request. The `??` keeps a realtime-patched entry intact, and only `.id` is ever read back out of that entry, which never changes. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -91,6 +91,28 @@ describe("useCanonicalIssue", () => {
|
||||
// The whole point of `initialData` over a post-resolve cache seed: the
|
||||
// canonical query must never observe an empty cache and start its own fetch.
|
||||
// A seed from an effect runs after that decision.
|
||||
// The common path: an in-app link carries the UUID, the route rewrites the
|
||||
// address bar to the identifier, and that rewrite re-renders this hook with
|
||||
// the identifier as its segment. Without the identifier-keyed seed the
|
||||
// resolution query missed and re-fetched an issue already in hand, so one
|
||||
// issue open cost two requests.
|
||||
it("costs no extra request when the URL is rewritten to the identifier", async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ routeId }: { routeId: string }) => useCanonicalIssue("ws-1", routeId),
|
||||
{ wrapper: createWrapper(qc), initialProps: { routeId: ISSUE_UUID } },
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.issue).toEqual(issue));
|
||||
expect(getIssue).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender({ routeId: "TRS-134" });
|
||||
|
||||
await waitFor(() => expect(result.current.canonicalId).toBe(ISSUE_UUID));
|
||||
expect(result.current.issue).toEqual(issue);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(getIssue).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("opens an identifier URL with exactly one request", async () => {
|
||||
const { result } = renderHook(() => useCanonicalIssue("ws-1", "TRS-134"), {
|
||||
wrapper: createWrapper(qc),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { Issue } from "../types";
|
||||
import { issueDetailOptions } from "./queries";
|
||||
import { issueDetailOptions, issueKeys } from "./queries";
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
@@ -51,6 +52,7 @@ export interface CanonicalIssue {
|
||||
* and leave the single-request property resting on hook ordering.
|
||||
*/
|
||||
export function useCanonicalIssue(wsId: string, routeId: string): CanonicalIssue {
|
||||
const qc = useQueryClient();
|
||||
const isUuid = isIssueUuid(routeId);
|
||||
const resolveEnabled = !isUuid && !!wsId && !!routeId;
|
||||
|
||||
@@ -69,6 +71,26 @@ export function useCanonicalIssue(wsId: string, routeId: string): CanonicalIssue
|
||||
initialData: isUuid ? undefined : resolve.data,
|
||||
});
|
||||
|
||||
// Mirror the loaded row into its identifier-keyed entry, the reverse of the
|
||||
// `initialData` hop above.
|
||||
//
|
||||
// In-app links still carry the UUID, so opening one lands on the UUID URL and
|
||||
// the route then rewrites the address bar to the identifier. That rewrite is
|
||||
// a navigation: the route re-renders with the identifier as its segment, and
|
||||
// without this seed the resolution query above would miss on a cache entry
|
||||
// nothing has filled and re-fetch an issue already in hand — a second request
|
||||
// for one issue open. The `??` keeps a realtime-patched entry intact, and
|
||||
// only `.id` is ever read back out, which never changes.
|
||||
const loadedIdentifier = detail.data?.identifier;
|
||||
const loadedIssue = detail.data;
|
||||
useEffect(() => {
|
||||
if (!loadedIssue || !loadedIdentifier || loadedIdentifier === routeId) return;
|
||||
qc.setQueryData<Issue>(
|
||||
issueKeys.detail(wsId, loadedIdentifier),
|
||||
(old) => old ?? loadedIssue,
|
||||
);
|
||||
}, [qc, wsId, loadedIdentifier, loadedIssue, routeId]);
|
||||
|
||||
// Read the resolution query's own settled state rather than "pending and no
|
||||
// data": a failed resolution must present as terminally not-found, never as
|
||||
// still-resolving, or the caller flips back to a loading frame and the
|
||||
|
||||
Reference in New Issue
Block a user