mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
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:
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user