Skip to content

Commit 7a346a1

Browse files
committed
fix(web): remove activation heartbeat
1 parent 4e2535b commit 7a346a1

11 files changed

Lines changed: 38 additions & 170 deletions
165 KB
Loading
165 KB
Loading

packages/server/src/__tests__/activation-commands.test.ts

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -131,55 +131,6 @@ describe("activation commands", () => {
131131
});
132132
});
133133

134-
it("does not allow a stale websocket to heartbeat after same-client rebind", async () => {
135-
const request = createMockRequest();
136-
const broadcaster = {
137-
broadcast: vi.fn(),
138-
sendToClient: vi.fn(() => true),
139-
sendBinaryToClient: vi.fn(() => true),
140-
getRequestMetadata: vi.fn(() => request),
141-
} satisfies Broadcaster;
142-
const ctx = createBaseContext({ broadcaster });
143-
144-
await dispatch(
145-
{
146-
kind: "command",
147-
id: "claim-1",
148-
op: "activation.claim",
149-
args: { clientInstanceId: "client-a" },
150-
},
151-
ctx,
152-
"ws-a"
153-
);
154-
155-
const rebound = await dispatch(
156-
{
157-
kind: "command",
158-
id: "claim-2",
159-
op: "activation.claim",
160-
args: { clientInstanceId: "client-a" },
161-
},
162-
ctx,
163-
"ws-b"
164-
);
165-
166-
expect(rebound.ok).toBe(true);
167-
168-
const heartbeat = await dispatch(
169-
{
170-
kind: "command",
171-
id: "heartbeat-stale",
172-
op: "activation.heartbeat",
173-
args: { clientInstanceId: "client-a", generation: 1 },
174-
},
175-
ctx,
176-
"ws-a"
177-
);
178-
179-
expect(heartbeat.ok).toBe(true);
180-
expect(heartbeat.data).toEqual({ ok: false });
181-
});
182-
183134
it("does not allow a stale websocket to release after same-client rebind", async () => {
184135
const request = createMockRequest();
185136
const broadcaster = {

packages/server/src/__tests__/activation-manager.test.ts

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ describe("ActivationManager", () => {
1616
beforeEach(() => {
1717
vi.useRealTimers();
1818
manager = new ActivationManager({
19-
heartbeatMs: 10_000,
20-
leaseExpirationMs: 30_000,
2119
graceMs: 3_000,
2220
});
2321
});
@@ -71,15 +69,6 @@ describe("ActivationManager", () => {
7169
});
7270
});
7371

74-
it("rejects heartbeat for stale generations", () => {
75-
const first = manager.claim("client-a", "ws-a", request);
76-
manager.claim("client-b", "ws-b", request);
77-
78-
const ok = manager.heartbeat("client-a", first.generation);
79-
80-
expect(ok).toBe(false);
81-
});
82-
8372
it("ignores release for stale generations and clears only the current lease", () => {
8473
const first = manager.claim("client-a", "ws-a", request);
8574
const current = manager.claim("client-b", "ws-b", request);
@@ -91,34 +80,18 @@ describe("ActivationManager", () => {
9180
expect(manager.getLease()).toBeNull();
9281
});
9382

94-
it("nulls expired state when getLease is called after expiry", () => {
95-
vi.useFakeTimers();
96-
vi.setSystemTime(new Date("2026-05-12T00:00:00.000Z"));
97-
98-
manager.claim("client-a", "ws-a", request);
99-
100-
vi.advanceTimersByTime(30_001);
101-
102-
expect(manager.getLease()).toBeNull();
103-
expect(manager.getLease()).toBeNull();
104-
});
105-
106-
it("refreshes expiry on heartbeat for the active generation", () => {
83+
it("retains the active lease without idle expiration", () => {
10784
vi.useFakeTimers();
10885
vi.setSystemTime(new Date("2026-05-12T00:00:00.000Z"));
10986

110-
const claim = manager.claim("client-a", "ws-a", request);
111-
const beforeExpiresAt = manager.getLease()?.expiresAt;
87+
const lease = manager.claim("client-a", "ws-a", request);
11288

113-
expect(beforeExpiresAt).toBe(Date.now() + 30_000);
89+
vi.advanceTimersByTime(12 * 60 * 60 * 1000);
11490

115-
vi.advanceTimersByTime(5_000);
116-
117-
const ok = manager.heartbeat("client-a", claim.generation);
118-
const after = manager.getLease();
119-
120-
expect(ok).toBe(true);
121-
expect(after?.expiresAt).toBe(Date.now() + 30_000);
122-
expect(after?.expiresAt).toBeGreaterThan(beforeExpiresAt ?? 0);
91+
expect(manager.getLease()).toMatchObject({
92+
clientInstanceId: "client-a",
93+
wsClientId: "ws-a",
94+
generation: lease.generation,
95+
});
12396
});
12497
});

packages/server/src/__tests__/ws-hub.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ const createMockRequest = (): FastifyRequest =>
6767

6868
const createActivationManager = () =>
6969
new ActivationManager({
70-
heartbeatMs: 10_000,
71-
leaseExpirationMs: 30_000,
7270
graceMs: 3_000,
7371
});
7472

packages/server/src/commands/activation.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,6 @@ registerCommand(
2929
}
3030
);
3131

32-
registerCommand(
33-
"activation.heartbeat",
34-
z.object({ clientInstanceId: z.string(), generation: z.number().int().positive() }),
35-
async (args, ctx, clientId) => {
36-
const lease = ctx.activationMgr.getLease();
37-
if (!clientId || !lease || lease.wsClientId !== clientId) {
38-
return { ok: false };
39-
}
40-
41-
return {
42-
ok: ctx.activationMgr.heartbeat(args.clientInstanceId, args.generation),
43-
};
44-
}
45-
);
46-
4732
registerCommand(
4833
"activation.release",
4934
z.object({ clientInstanceId: z.string(), generation: z.number().int().positive() }),

packages/server/src/supervisor/evaluator.windows.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ describe("SupervisorEvaluator windows child-process options", () => {
113113

114114
await expect(evaluator.evaluate(makeSupervisor(), makeContext())).resolves.toEqual({
115115
message: "Run pnpm vitest to verify",
116+
objectiveComplete: false,
116117
});
117118

118119
expect(spawnMock).toHaveBeenCalledWith(

packages/server/src/ws/activation.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export interface ActivationLease {
55
wsClientId: string;
66
generation: number;
77
issuedAt: number;
8-
expiresAt: number;
98
graceUntil: number | null;
109
ip: string;
1110
userAgent: string;
@@ -19,14 +18,10 @@ export interface ActivationClaimResult {
1918
}
2019

2120
export interface ActivationManagerOptions {
22-
heartbeatMs: number;
23-
leaseExpirationMs: number;
2421
graceMs: number;
2522
}
2623

2724
const DEFAULT_OPTIONS: ActivationManagerOptions = {
28-
heartbeatMs: 10_000,
29-
leaseExpirationMs: 30_000,
3025
graceMs: 3_000,
3126
};
3227

@@ -54,7 +49,6 @@ export class ActivationManager {
5449

5550
activeLease.wsClientId = wsClientId;
5651
activeLease.graceUntil = null;
57-
activeLease.expiresAt = now + this.options.leaseExpirationMs;
5852

5953
return {
6054
active: true,
@@ -76,7 +70,6 @@ export class ActivationManager {
7670
wsClientId,
7771
generation: this.generation,
7872
issuedAt: now,
79-
expiresAt: now + this.options.leaseExpirationMs,
8073
graceUntil: null,
8174
ip: request.ip,
8275
userAgent: request.headers["user-agent"] ?? "",
@@ -90,20 +83,6 @@ export class ActivationManager {
9083
};
9184
}
9285

93-
heartbeat(clientInstanceId: string, generation: number): boolean {
94-
const lease = this.getLease();
95-
if (!lease) {
96-
return false;
97-
}
98-
99-
if (lease.clientInstanceId !== clientInstanceId || lease.generation !== generation) {
100-
return false;
101-
}
102-
103-
lease.expiresAt = Date.now() + this.options.leaseExpirationMs;
104-
return true;
105-
}
106-
10786
release(clientInstanceId: string, generation: number): void {
10887
const lease = this.getLease();
10988
if (!lease) {
@@ -131,11 +110,6 @@ export class ActivationManager {
131110
return null;
132111
}
133112

134-
if (Date.now() > this.lease.expiresAt) {
135-
this.lease = null;
136-
return null;
137-
}
138-
139113
return this.lease;
140114
}
141115
}

packages/server/src/ws/dispatch.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ const handlers = new Map<string, CommandHandler>();
6060
const schemas = new Map<string, CommandSchema>();
6161
const ACTIVATION_ALLOWLIST = new Set([
6262
"activation.claim",
63-
"activation.heartbeat",
6463
"activation.release",
6564
"connection.probe",
6665
]);

packages/web/src/app/providers.lifecycle.test.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ function createWsSendCommandMock(
7676
};
7777
}
7878

79-
if (op === "activation.heartbeat" || op === "activation.release") {
79+
if (op === "activation.release") {
8080
return { ok: true };
8181
}
8282

@@ -513,6 +513,34 @@ describe("AppProviders lifecycle recovery", () => {
513513
});
514514
});
515515

516+
it("does not send activation.heartbeat after the session becomes active", async () => {
517+
const store = createStore();
518+
setVisibilityState("visible");
519+
vi.useFakeTimers();
520+
521+
renderProviders(store);
522+
523+
await vi.waitFor(() => {
524+
expect(wsState.client?.connect).toHaveBeenCalled();
525+
});
526+
527+
act(() => {
528+
wsState.client?.statusHandler?.("connected");
529+
});
530+
531+
await vi.waitFor(() => {
532+
expect(store.get(activationStatusAtom)).toBe("active");
533+
});
534+
535+
act(() => {
536+
vi.advanceTimersByTime(60_000);
537+
});
538+
539+
expect(
540+
wsState.client?.sendCommand?.mock.calls.filter(([op]) => op === "activation.heartbeat") ?? []
541+
).toHaveLength(0);
542+
});
543+
516544
it("disconnects and gates when activation.revoked is received", async () => {
517545
const store = createStore();
518546
seedWorkspaces(store, ["ws-1"], "ws-1");

0 commit comments

Comments
 (0)