The gauge was maintained by conditional inc on "this sessionId isn't in the in-memory `docs` Map yet" and dec on "last client for the sessionId left." Because `docs` caches entries indefinitely — even after every client disconnects — a reconnect found the doc still cached, skipped the inc, then decremented again on the next disconnect. Every reconnect-after-last-disconnect leaked one decrement. After a day of normal reload / wifi-blip / tab-hibernate patterns the gauge hit -182. Replace the transitions with a `set()` derived from the live `conns` map: count distinct session ids with at least one open WebSocket. Self-correcting, no drift possible. Extract `countActiveSessions()` as a pure helper + unit tests including a regression case that simulates repeated reconnect/ disconnect cycles on one session and asserts the count never goes negative. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { countActiveSessions } from "./yjs-server.ts";
|
|
|
|
describe("countActiveSessions", () => {
|
|
it("is 0 for an empty iterable (regression: prod gauge saw -182 drift)", () => {
|
|
expect(countActiveSessions([])).toBe(0);
|
|
});
|
|
|
|
it("counts a single session with one client as 1", () => {
|
|
expect(countActiveSessions([{ sessionId: "s1" }])).toBe(1);
|
|
});
|
|
|
|
it("deduplicates multiple clients on the same session", () => {
|
|
expect(
|
|
countActiveSessions([
|
|
{ sessionId: "s1" },
|
|
{ sessionId: "s1" },
|
|
{ sessionId: "s1" },
|
|
]),
|
|
).toBe(1);
|
|
});
|
|
|
|
it("counts distinct sessions across clients", () => {
|
|
expect(
|
|
countActiveSessions([
|
|
{ sessionId: "s1" },
|
|
{ sessionId: "s2" },
|
|
{ sessionId: "s1" },
|
|
{ sessionId: "s3" },
|
|
]),
|
|
).toBe(3);
|
|
});
|
|
|
|
it("doesn't go negative under the reconnect pattern that broke the old inc/dec logic", () => {
|
|
// Simulate: client connects, client disconnects, reconnects,
|
|
// disconnects again — five times over. The old code would have
|
|
// decremented once per cycle (because `docs` stayed cached, so
|
|
// the re-connect's `isNewSession` check skipped inc). Here we
|
|
// derive from live state, so each empty snapshot is 0, not -N.
|
|
for (let i = 0; i < 5; i++) {
|
|
expect(countActiveSessions([{ sessionId: "s1" }])).toBe(1); // reconnect
|
|
expect(countActiveSessions([])).toBe(0); // disconnect
|
|
}
|
|
});
|
|
});
|