Skip to content

census(finding): MePermissionsProvider's fetch driver is a useCallback the effect depends on, so a discard costs a redundant /me/permissions round trip #6862

Description

@claude

Found by the domain:ui execution seat while implementing objectui#6813 (both providers' context VALUE). Not fixed there — #6813 is scope-locked to the context value, and this is the other memo in the same file, feeding an effect rather than the context.

The shape

packages/permissions/src/MePermissionsProvider.tsx builds its fetch driver in a useCallback and names it as the fetch effect's dependency:

const fetchPermissions = useCallback(async (token) => { ... },
[endpoint, fetcher, maxRetries, retryBaseDelayMs]);
const retry = useCallback(() => { void fetchPermissions(); }, [fetchPermissions]);
useEffect(() => {
if (initialPermissions) return;
const token = { cancelled: false };
void fetchPermissions(token);
return () => { token.cancelled = true; };
}, [fetchPermissions, initialPermissions]);

useCallback carries no semantic guarantee: React may discard the cache and rebuild even when the four dependencies compare equal. The rebuilt function is a NEW identity, so the effect tears down and re-runs with nothing an author or a caller controls having changed. The observable is a redundant /api/v1/auth/me/permissions round trip — one network request per discard, on the provider that gates the whole console.

It also moves the context value, legitimately: fetchPermissions opens with setLoading(true), which flips isLoaded to false and then back, so consumers see two genuine context changes for a discard nobody asked for. After the first successful load data is non-null, so loading && !data is false and children are NOT unmounted — the fallback does not flash.

Why #6813 does not close it

#6813 makes both providers' context VALUE discard-proof, keyed on the inputs it is derived from. That is the right guarantee for the value and it does not reach here: this defect is at the EFFECT end, and the context change it produces is honest (isLoaded really did flip). Fixing it means the effect must stop keying on a droppable identity, which is a different shape from the one #6813 lands — and a riskier one, since the four dependencies include a fetcher that may be undefined and three primitives, so the input-keyed WeakMap idiom #6813 introduces does not transfer unchanged.

Measured, not inferred — this is LATENT

Same standing as the rest of the family. On React 19.2.8 (this repo's pinned version) the cache is not discarded spontaneously: measured while objectui#6724 landed, 51 re-renders with no provider, 51 with one and 42 under StrictMode each returned ONE identity, and there is no Activity/Offscreen subtree in this repo — the documented case where React does throw memo caches away. So this is a correctness dependency resting on a licence React has not exercised here, NOT a reproduction. Type Task, not Bug.

The instrument exists: the forced-discard proxy in packages/permissions/src/__tests__/providerCtxIdentity.discarded.test.tsx (added by #6813) already patches useCallback as well as useMemo, so a pin here would reuse it directly. A pin that does not force a discard proves nothing, because React will not discard on its own.

Severity

Low, same as the family — a redundant round trip on a discard, not incorrect data.

Family: objectui#6018 · #5976 · #6591 · #6592 · #6697 · #6724 · #6813 — same hazard, the effect end of the provider #6813 hardens.

Dedupe

/search/issues is unreliable for this seat, so this went through the repo-scoped REST list endpoint plus a local grep: 250 open issues collected, zero hits for fetchPermissions, MePermissionsProvider or /me/permissions other than an unrelated CEL-predicate card (#4421), and the only discard hit is #6813 itself. Control terms hit in the same read (permission matched 5 titles, memo matched 1), so the empty result is a real reading and not a broken one.

Unassigned — filed as a finding for triage to route, not claimed.

Generated by Claude Code


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions