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:
parent
a71e6d2012
commit
320daf2f67
2 changed files with 1656 additions and 90 deletions
|
|
@ -236,4 +236,71 @@ describe("ResourceSubscriptionsState", () => {
|
|||
state.destroy();
|
||||
expect(() => state.destroy()).not.toThrow();
|
||||
});
|
||||
|
||||
describe("modern listen-stream state (#1630)", () => {
|
||||
it("starts inactive and seeds from the client", () => {
|
||||
const state = new ResourceSubscriptionsState(client);
|
||||
expect(state.getStreamState()).toEqual({
|
||||
active: false,
|
||||
status: "ended",
|
||||
honoredUris: [],
|
||||
});
|
||||
|
||||
client.resourceSubscriptionStreamState = {
|
||||
active: true,
|
||||
status: "acknowledged",
|
||||
honoredUris: ["file:///a"],
|
||||
};
|
||||
const seeded = new ResourceSubscriptionsState(client);
|
||||
expect(seeded.getStreamState().active).toBe(true);
|
||||
expect(seeded.getStreamState().honoredUris).toEqual(["file:///a"]);
|
||||
});
|
||||
|
||||
it("forwards resourceSubscriptionStreamChange as streamStateChange", async () => {
|
||||
const state = new ResourceSubscriptionsState(client);
|
||||
const next = await new Promise((resolve) => {
|
||||
state.addEventListener("streamStateChange", (e) => resolve(e.detail), {
|
||||
once: true,
|
||||
});
|
||||
client.dispatchTypedEvent("resourceSubscriptionStreamChange", {
|
||||
active: true,
|
||||
status: "reconnecting",
|
||||
honoredUris: [],
|
||||
});
|
||||
});
|
||||
expect(next).toEqual({
|
||||
active: true,
|
||||
status: "reconnecting",
|
||||
honoredUris: [],
|
||||
});
|
||||
expect(state.getStreamState().status).toBe("reconnecting");
|
||||
});
|
||||
|
||||
it("resets stream state to inactive on a terminal status change", () => {
|
||||
const state = new ResourceSubscriptionsState(client);
|
||||
client.dispatchTypedEvent("resourceSubscriptionStreamChange", {
|
||||
active: true,
|
||||
status: "acknowledged",
|
||||
honoredUris: ["file:///a"],
|
||||
});
|
||||
expect(state.getStreamState().active).toBe(true);
|
||||
|
||||
const handler = vi.fn();
|
||||
state.addEventListener("streamStateChange", handler);
|
||||
client.setStatus("disconnected");
|
||||
expect(state.getStreamState().active).toBe(false);
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resets stream state on destroy", () => {
|
||||
const state = new ResourceSubscriptionsState(client);
|
||||
client.dispatchTypedEvent("resourceSubscriptionStreamChange", {
|
||||
active: true,
|
||||
status: "acknowledged",
|
||||
honoredUris: ["file:///a"],
|
||||
});
|
||||
state.destroy();
|
||||
expect(state.getStreamState().active).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -66,4 +66,57 @@ describe("useResourceSubscriptions", () => {
|
|||
client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]);
|
||||
expect(result.current.subscriptions).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an inactive stream state by default and when null", () => {
|
||||
const { result: nullResult } = renderHook(() =>
|
||||
useResourceSubscriptions(null),
|
||||
);
|
||||
expect(nullResult.current.streamState.active).toBe(false);
|
||||
|
||||
const { result } = renderHook(() => useResourceSubscriptions(state));
|
||||
expect(result.current.streamState.active).toBe(false);
|
||||
});
|
||||
|
||||
it("updates streamState when the store dispatches streamStateChange", async () => {
|
||||
const { result } = renderHook(() => useResourceSubscriptions(state));
|
||||
|
||||
act(() => {
|
||||
client.dispatchTypedEvent("resourceSubscriptionStreamChange", {
|
||||
active: true,
|
||||
status: "acknowledged",
|
||||
honoredUris: ["file:///a"],
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.streamState).toEqual({
|
||||
active: true,
|
||||
status: "acknowledged",
|
||||
honoredUris: ["file:///a"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("resets streamState when the state prop becomes null", async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ s }: { s: ResourceSubscriptionsState | null }) =>
|
||||
useResourceSubscriptions(s),
|
||||
{ initialProps: { s: state as ResourceSubscriptionsState | null } },
|
||||
);
|
||||
act(() => {
|
||||
client.dispatchTypedEvent("resourceSubscriptionStreamChange", {
|
||||
active: true,
|
||||
status: "acknowledged",
|
||||
honoredUris: [],
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.streamState.active).toBe(true);
|
||||
});
|
||||
|
||||
rerender({ s: null });
|
||||
await waitFor(() => {
|
||||
expect(result.current.streamState.active).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue