Merge origin/main into pr-93985-prep

* origin/main:
  fix(ui): keep canvas lease renewals lifecycle-owned and timer-safe (#117173)
  fix(teams): restore the official square product icon (#117170)
This commit is contained in:
Vincent Koc
2026-08-01 10:55:54 +08:00
3 changed files with 109 additions and 10 deletions
@@ -2,7 +2,7 @@
"id": "teams-meetings",
"name": "Microsoft Teams meetings",
"description": "Join Microsoft Teams meetings as a Chrome browser guest.",
"icon": "https://cdn.simpleicons.org/microsoftteams",
"icon": "https://res.cdn.office.net/files/fabric-cdn-prod_20230815.001/assets/brand-icons/product/svg/teams_48x1.svg",
"enabledByDefault": true,
"commandAliases": [{ "name": "teamsmeetings" }],
"activation": {
+18 -9
View File
@@ -1,4 +1,6 @@
// Loaded after hello so capability renewal does not inflate the startup chunk.
import { resolveSafeTimeoutDelayMs } from "@openclaw/gateway-client/browser";
const RENEWAL_LEAD_MS = 15_000;
const MIN_RENEWAL_DELAY_MS = 1_000;
// Used only when a refresh response omits expiry; an early one-minute refresh
@@ -45,6 +47,7 @@ export function createCanvasSurfaceLease<
let consecutiveFailures = 0;
let generation = 0;
let started = false;
const ownsGeneration = (expected: number) => started && generation === expected;
const clearScheduledRenewal = () => {
if (timer !== null) {
@@ -54,17 +57,24 @@ export function createCanvasSurfaceLease<
};
const schedule = (delayMs: number, expectedGeneration: number) => {
if (!ownsGeneration(expectedGeneration)) {
return;
}
clearScheduledRenewal();
timer = setTimer(() => {
timer = null;
if (started && generation === expectedGeneration) {
timer = setTimer(
() => {
if (!ownsGeneration(expectedGeneration)) {
return;
}
timer = null;
renew(expectedGeneration);
}
}, delayMs);
},
resolveSafeTimeoutDelayMs(delayMs, { minMs: MIN_RENEWAL_DELAY_MS }),
);
};
const handleFailure = (expectedGeneration: number) => {
if (!started || generation !== expectedGeneration) {
if (!ownsGeneration(expectedGeneration)) {
return;
}
consecutiveFailures += 1;
@@ -75,8 +85,7 @@ export function createCanvasSurfaceLease<
const renew = (expectedGeneration: number) => {
if (
inFlight?.generation === expectedGeneration ||
!started ||
generation !== expectedGeneration ||
!ownsGeneration(expectedGeneration) ||
!currentUrl
) {
return;
@@ -85,7 +94,7 @@ export function createCanvasSurfaceLease<
const request = Promise.resolve()
.then(() => params.request("plugin.surface.refresh", { surface: "canvas", observedUrl }))
.then((response) => {
if (!started || generation !== expectedGeneration || currentUrl !== observedUrl) {
if (!ownsGeneration(expectedGeneration) || currentUrl !== observedUrl) {
return;
}
const refreshed = parseCanvasSurfaceRefresh(response);
+90
View File
@@ -185,6 +185,96 @@ describe("createCanvasSurfaceLease", () => {
expect(clock.nextDelayMs).toBe(60_000);
});
it.each(["stops", "reconnects without a canvas"] as const)(
"does not schedule a retired generation when publishing a refreshed URL %s",
async (transition) => {
const clock = new FakeClock();
const changes: Array<string | null> = [];
const request = vi.fn(async () => ({
surface: "canvas",
pluginSurfaceUrls: { canvas: "https://canvas.test/__openclaw__/cap/refreshed" },
expiresAtMs: 120_000,
}));
const lease = createCanvasSurfaceLease({
request,
onChange: (url) => {
changes.push(url);
if (url === "https://canvas.test/__openclaw__/cap/refreshed") {
if (transition === "stops") {
lease.stop();
} else {
lease.start(undefined);
}
}
},
now: clock.now,
setTimer: clock.setTimer,
clearTimer: clock.clearTimer,
});
lease.start("https://canvas.test/__openclaw__/cap/original");
await flushPromises();
expect(request).toHaveBeenCalledOnce();
expect(changes).toEqual([
"https://canvas.test/__openclaw__/cap/original",
"https://canvas.test/__openclaw__/cap/refreshed",
null,
]);
expect(clock.pendingCount).toBe(0);
},
);
it("keeps the reconnect renewal owned when a retired timer callback arrives late", async () => {
const request = vi
.fn<(method: string, params: unknown) => Promise<unknown>>()
.mockResolvedValueOnce({
surface: "canvas",
pluginSurfaceUrls: { canvas: "https://canvas.test/__openclaw__/cap/first" },
expiresAtMs: 120_000,
})
.mockResolvedValueOnce({
surface: "canvas",
pluginSurfaceUrls: { canvas: "https://canvas.test/__openclaw__/cap/reconnected" },
expiresAtMs: 180_000,
});
const { clock, lease } = createLeaseHarness(request);
lease.start("https://canvas.test/__openclaw__/cap/original");
await flushPromises();
const retiredCallback = clock.takeNextCallback();
expect(retiredCallback).toBeDefined();
lease.stop();
lease.start("https://canvas.test/__openclaw__/cap/reconnect");
await flushPromises();
expect(clock.pendingCount).toBe(1);
retiredCallback?.();
expect(request).toHaveBeenCalledTimes(2);
lease.stop();
expect(clock.pendingCount).toBe(0);
});
it.each([Number.MAX_SAFE_INTEGER, 2 ** 32 + 115_000])(
"clamps an advertised canvas expiry of %d to a browser-safe native timer",
async (expiresAtMs) => {
const request = vi.fn(async () => ({
surface: "canvas",
pluginSurfaceUrls: { canvas: "https://canvas.test/__openclaw__/cap/refreshed" },
expiresAtMs,
}));
const { clock, lease } = createLeaseHarness(request);
lease.start("https://canvas.test/__openclaw__/cap/original");
await flushPromises();
expect(clock.nextDelayMs).toBe(2_147_483_647);
lease.stop();
},
);
it("stop clears timers, ignores an in-flight result, and publishes null once", async () => {
const pending = deferred<unknown>();
const { changes, clock, connectionChanges, lease } = createLeaseHarness(() => pending.promise);