Resources: subscriptions/listen model on modern servers (era fork) (#1630) (#1730)

Fork resource subscriptions by protocol era: legacy keeps resources/subscribe; modern (2026-07-28) models subscriptions as a filter over one long-lived subscriptions/listen stream with reconnect-by-re-listen (capped backoff), an optimistic Connecting→Listening header badge, and a fix so the browser proxy no longer blocks listen() ~60s. Closes #1630.
This commit is contained in:
Cliff Hall 2026-07-20 19:33:40 -04:00 • committed by GitHub
commit 320daf2f67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -3,17 +3,28 @@ import type {
ResourceSubscriptionsState,
ResourceSubscriptionsStateEventMap,
} from "../mcp/state/resourceSubscriptionsState.js";
import type { InspectorResourceSubscription } from "../mcp/types.js";
import type {
InspectorResourceSubscription,
ResourceSubscriptionStreamState,
} from "../mcp/types.js";
import { INACTIVE_SUBSCRIPTION_STREAM_STATE } from "../mcp/types.js";
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
export interface UseResourceSubscriptionsResult {
subscriptions: InspectorResourceSubscription[];
/**
* Modern-era (2026-07-28) `subscriptions/listen` stream state (#1630).
* `active: false` on the legacy era (and with no active server), so the
* Resources screen renders no stream chrome there.
*/
streamState: ResourceSubscriptionStreamState;
}
/**
* React hook that subscribes to ResourceSubscriptionsState and returns the
* current InspectorResourceSubscription[]. When the state is null (no active
* server), returns an empty array.
* current InspectorResourceSubscription[] plus the modern listen-stream state.
* When the state is null (no active server), returns an empty array and an
* inactive stream state.
*/
export function useResourceSubscriptions(
state: ResourceSubscriptionsState | null,
@ -21,13 +32,19 @@ export function useResourceSubscriptions(
const [subscriptions, setSubscriptions] = useState<
InspectorResourceSubscription[]
>(state?.getSubscriptions() ?? []);
const [streamState, setStreamState] =
useState<ResourceSubscriptionStreamState>(
state?.getStreamState() ?? INACTIVE_SUBSCRIPTION_STREAM_STATE,
);
useEffect(() => {
if (!state) {
setSubscriptions([]);
setStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
return;
}
setSubscriptions(state.getSubscriptions());
setStreamState(state.getStreamState());
const onSubscriptionsChange = (
event: TypedEventGeneric<
ResourceSubscriptionsStateEventMap,
@ -36,11 +53,21 @@ export function useResourceSubscriptions(
) => {
setSubscriptions(event.detail);
};
const onStreamStateChange = (
event: TypedEventGeneric<
ResourceSubscriptionsStateEventMap,
"streamStateChange"
>,
) => {
setStreamState(event.detail);
};
state.addEventListener("subscriptionsChange", onSubscriptionsChange);
state.addEventListener("streamStateChange", onStreamStateChange);
return () => {
state.removeEventListener("subscriptionsChange", onSubscriptionsChange);
state.removeEventListener("streamStateChange", onStreamStateChange);
};
}, [state]);
return { subscriptions };
return { subscriptions, streamState };
}