feat(FN-4370): complete Step 3 — retain badge snapshots across unsubscribes

Fusion-Task-Id: FN-4370
Fusion-Task-Lineage: c18188d5-c478-4233-943f-943b0125b271
This commit is contained in:
Fusion
2026-05-13 14:59:53 -07:00
committed by gsxdsm
parent 89ef3704ba
commit bcad65e84e
2 changed files with 154 additions and 2 deletions

View File

@@ -60,6 +60,7 @@ class BadgeWebSocketStore {
private ws: WebSocket | null = null;
private listeners = new Set<() => void>();
private badgeUpdates = new Map<string, BadgeSnapshot>();
private retainedBadgeUpdates = new Map<string, BadgeSnapshot>();
private subscriptionsByTask = new Map<string, Set<string>>();
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
private reconnectDelayMs = 1_000;
@@ -76,6 +77,7 @@ class BadgeWebSocketStore {
private previousProjectIdRef: string | null = null;
private projectContextVersionRef = 0;
private contextVersionAtStart = 0;
private static readonly RETAINED_BADGE_CAP = 200;
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
@@ -127,6 +129,13 @@ class BadgeWebSocketStore {
subscribeTask(hookId: string, taskId: string): void {
const scopedKey = toScopedKey(this.projectId, taskId);
const retainedSnapshot = this.retainedBadgeUpdates.get(scopedKey);
if (retainedSnapshot && !this.badgeUpdates.has(scopedKey)) {
this.badgeUpdates.set(scopedKey, retainedSnapshot);
this.retainedBadgeUpdates.delete(scopedKey);
this.emit();
}
const subscribers = this.subscriptionsByTask.get(scopedKey) ?? new Set<string>();
const isNewSubscription = !subscribers.has(hookId);
subscribers.add(hookId);
@@ -150,6 +159,18 @@ class BadgeWebSocketStore {
subscribers.delete(hookId);
if (subscribers.size === 0) {
this.subscriptionsByTask.delete(scopedKey);
const snapshot = this.badgeUpdates.get(scopedKey);
if (snapshot) {
// Keep a bounded cache of last-known snapshots so cards can restore badge state
// immediately after transient viewport unsubscriptions. Cap prevents unbounded growth.
this.retainedBadgeUpdates.delete(scopedKey);
this.retainedBadgeUpdates.set(scopedKey, snapshot);
while (this.retainedBadgeUpdates.size > BadgeWebSocketStore.RETAINED_BADGE_CAP) {
const oldestKey = this.retainedBadgeUpdates.keys().next().value;
if (!oldestKey) break;
this.retainedBadgeUpdates.delete(oldestKey);
}
}
this.badgeUpdates.delete(scopedKey);
this.send({ type: "unsubscribe", taskId, projectId: this.projectId });
this.emit();
@@ -172,6 +193,7 @@ class BadgeWebSocketStore {
reset(): void {
this.disconnect();
this.badgeUpdates.clear();
this.retainedBadgeUpdates.clear();
this.subscriptionsByTask.clear();
this.shouldReconnect = false;
this.emit();