Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,97 @@ afterEach(async () => {
);
});

describe("manual size notification cleanup", () => {
for (const pending of ["initial measurement", "resize measurement"]) {
it(`cancels a pending ${pending}`, async () => {
const frames = new Map<number, FrameRequestCallback>();
let nextFrame = 0;
let resize = () => {};
let disconnected = false;
let measurements = 0;
let height = 100;
const html = {
style: { height: "100%" },
getBoundingClientRect: () => {
measurements += 1;
return { height };
},
};
const globals = {
window: { innerWidth: 400 },
document: { documentElement: html, body: {} },
requestAnimationFrame: (callback: FrameRequestCallback) => {
const id = nextFrame++;
frames.set(id, callback);
return id;
},
cancelAnimationFrame: (id: number) => frames.delete(id),
ResizeObserver: class {
constructor(callback: () => void) {
resize = callback;
}
observe() {}
disconnect() {
disconnected = true;
}
},
};
const originals = new Map(
Object.keys(globals).map((key) => [
key,
Object.getOwnPropertyDescriptor(globalThis, key),
]),
);
for (const [key, value] of Object.entries(globals)) {
Object.defineProperty(globalThis, key, { configurable: true, value });
}
const flushFrames = () => {
const callbacks = [...frames.values()];
frames.clear();
for (const callback of callbacks) callback(0);
};
try {
const pair = await connectPair();
connected.push(pair);
const sizeMessages = () =>
pair.sentByApp.filter(
(message) =>
"method" in message &&
message.method === "ui/notifications/size-changed",
);
const cleanup = pair.app.setupSizeChangedNotifications();
if (pending === "resize measurement") {
flushFrames();
expect(sizeMessages()).toHaveLength(1);
height = 200;
resize();
resize();
}
expect(frames.size).toBe(1);
const measurementsBeforeCleanup = measurements;
const messagesBeforeCleanup = sizeMessages().length;
cleanup();
cleanup();
flushFrames();
expect(disconnected).toBe(true);
expect(measurements).toBe(measurementsBeforeCleanup);
expect(sizeMessages()).toHaveLength(messagesBeforeCleanup);
expect(html.style.height).toBe("100%");

const nextCleanup = pair.app.setupSizeChangedNotifications();
flushFrames();
expect(sizeMessages()).toHaveLength(messagesBeforeCleanup + 1);
nextCleanup();
} finally {
for (const [key, descriptor] of originals) {
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
else Reflect.deleteProperty(globalThis, key);
}
}
});
}
});

describe("App base MCP SDK v2 Protocol migration", () => {
it("performs only the Apps handshake — no MCP initialize on the wire", async () => {
const pair = await connectPair();
Expand Down
19 changes: 12 additions & 7 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1896,7 +1896,7 @@ export class App extends Protocol<BaseContext> {
* option is true (default). You typically don't need to call this manually unless
* you disabled autoResize and want to enable it later.
*
* @returns Cleanup function to disconnect the observer
* @returns Cleanup function to disconnect the observer and cancel pending size notifications
*
* @example Manual setup for custom scenarios
* ```ts source="./app.examples.ts#App_setupAutoResize_manual"
Expand All @@ -1915,17 +1915,16 @@ export class App extends Protocol<BaseContext> {
* ```
*/
setupSizeChangedNotifications() {
let scheduled = false;
let frame: number | undefined;
let lastWidth = 0;
let lastHeight = 0;

const sendBodySizeChanged = () => {
if (scheduled) {
if (frame !== undefined) {
return;
}
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
frame = requestAnimationFrame(() => {
frame = undefined;
const html = document.documentElement;

// Measure actual content height by temporarily overriding html sizing.
Expand Down Expand Up @@ -1961,7 +1960,13 @@ export class App extends Protocol<BaseContext> {
resizeObserver.observe(document.documentElement);
resizeObserver.observe(document.body);

return () => resizeObserver.disconnect();
return () => {
resizeObserver.disconnect();
if (frame !== undefined) {
cancelAnimationFrame(frame);
frame = undefined;
}
};
}

/**
Expand Down