Skip to content

Commit 9e8a11e

Browse files
committed
fix(session): gate cookie-watcher probes by session value and interval
1 parent 76c1c58 commit 9e8a11e

2 files changed

Lines changed: 74 additions & 2 deletions

File tree

features/session/session.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,47 @@ export function isLoginPage(document: Document): boolean {
7777
);
7878
}
7979

80+
81+
// Probe only when the session id actually changed, and never more often than this
82+
const COOKIE_PROBE_MIN_INTERVAL_MS = 60_000;
83+
84+
// Decides whether a session-cookie set event warrants a network probe.
85+
export function createCookieProbeGate(
86+
minIntervalMs = COOKIE_PROBE_MIN_INTERVAL_MS,
87+
) {
88+
let lastValue: string | undefined;
89+
let lastProbeAt = -Infinity;
90+
return {
91+
shouldProbe(value: string, now: number): boolean {
92+
if (value === lastValue) return false;
93+
lastValue = value;
94+
if (now - lastProbeAt < minIntervalMs) return false;
95+
lastProbeAt = now;
96+
return true;
97+
},
98+
reset(): void {
99+
lastValue = undefined;
100+
},
101+
};
102+
}
103+
80104
// Event-driven cache updates from the background service worker: react the
81105
// moment the session cookie is removed or (re)created, instead of waiting for
82106
// the next popup open. Requires the "cookies" permission.
83107
export function registerSessionCookieWatcher(): void {
84108
if (!browser.cookies?.onChanged) return;
85109

110+
const gate = createCookieProbeGate();
86111
browser.cookies.onChanged.addListener(({ cookie, removed, cause }) => {
87112
if (cookie.name !== SESSION_COOKIE) return;
88113
if (removed) {
89114
// "overwrite" removals are immediately followed by a set event for the
90115
// replacement cookie — not a logout.
91-
if (cause !== "overwrite") void saveLoginState(false);
92-
} else {
116+
if (cause !== "overwrite") {
117+
gate.reset();
118+
void saveLoginState(false);
119+
}
120+
} else if (gate.shouldProbe(cookie.value, Date.now())) {
93121
void refreshLoginState();
94122
}
95123
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { createCookieProbeGate } from "../../features/session/session";
3+
4+
// UT re-sets the session cookie on every response, including the login probe's
5+
// own; the gate keeps the cookie watcher from probing in a loop.
6+
describe("cookie probe gate", () => {
7+
test("probes once for a new session id, then ignores identical re-sets", () => {
8+
const gate = createCookieProbeGate(60_000);
9+
expect(gate.shouldProbe("abc", 0)).toBe(true);
10+
expect(gate.shouldProbe("abc", 100)).toBe(false);
11+
expect(gate.shouldProbe("abc", 200)).toBe(false);
12+
});
13+
14+
test("throttles probes for changing ids to one per interval", () => {
15+
const gate = createCookieProbeGate(60_000);
16+
expect(gate.shouldProbe("a", 0)).toBe(true);
17+
expect(gate.shouldProbe("b", 1_000)).toBe(false);
18+
expect(gate.shouldProbe("c", 59_999)).toBe(false);
19+
expect(gate.shouldProbe("d", 60_000)).toBe(true);
20+
expect(gate.shouldProbe("e", 60_001)).toBe(false);
21+
});
22+
23+
test("a rotated id inside the interval is not re-probed later just for repeating", () => {
24+
const gate = createCookieProbeGate(60_000);
25+
expect(gate.shouldProbe("a", 0)).toBe(true);
26+
expect(gate.shouldProbe("b", 1_000)).toBe(false);
27+
// Same value again after the interval — nothing new to learn.
28+
expect(gate.shouldProbe("b", 70_000)).toBe(false);
29+
});
30+
31+
test("reset() treats the next set as a new session even with the same id", () => {
32+
const gate = createCookieProbeGate(60_000);
33+
expect(gate.shouldProbe("abc", 0)).toBe(true);
34+
gate.reset();
35+
expect(gate.shouldProbe("abc", 60_000)).toBe(true);
36+
});
37+
38+
test("reset() does not bypass the interval throttle", () => {
39+
const gate = createCookieProbeGate(60_000);
40+
expect(gate.shouldProbe("abc", 0)).toBe(true);
41+
gate.reset();
42+
expect(gate.shouldProbe("abc", 1_000)).toBe(false);
43+
});
44+
});

0 commit comments

Comments
 (0)