Skip to content

Commit a9ca1cd

Browse files
authored
fix: add subscription manager to prevent GraphQL subscription leaks (#265)
Co-authored-by: Flegma <Flegma@users.noreply.github.com>
1 parent dbeb5c1 commit a9ca1cd

5 files changed

Lines changed: 322 additions & 209 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const subscriptions = new Map<string, { unsubscribe: () => void }>();
2+
3+
export function useSubscriptionManager() {
4+
function subscribe(key: string, sub: { unsubscribe: () => void }) {
5+
const existing = subscriptions.get(key);
6+
if (existing) {
7+
existing.unsubscribe();
8+
}
9+
subscriptions.set(key, sub);
10+
}
11+
12+
function unsubscribe(key: string) {
13+
const existing = subscriptions.get(key);
14+
if (existing) {
15+
existing.unsubscribe();
16+
subscriptions.delete(key);
17+
}
18+
}
19+
20+
function unsubscribeAll() {
21+
for (const [, sub] of subscriptions) {
22+
sub.unsubscribe();
23+
}
24+
subscriptions.clear();
25+
}
26+
27+
return { subscribe, unsubscribe, unsubscribeAll };
28+
}

stores/ApplicationSettings.ts

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { generateSubscription } from "~/graphql/graphqlGen";
66
import { useMatchmakingStore } from "./MatchmakingStore";
77
import { useAuthStore } from "./AuthStore";
88
import { order_by } from "@/generated/zeus";
9+
import { useSubscriptionManager } from "~/composables/useSubscriptionManager";
910

1011
interface Region {
1112
value: string;
@@ -36,6 +37,7 @@ export const useApplicationSettingsStore = defineStore(
3637
ref<Array<{ name: string; value: string }>>(loadCachedSettings());
3738

3839
const subscribeToSettings = async () => {
40+
const { subscribe } = useSubscriptionManager();
3941
const subscription = getGraphqlClient().subscribe({
4042
query: generateSubscription({
4143
settings: [
@@ -48,24 +50,28 @@ export const useApplicationSettingsStore = defineStore(
4850
}),
4951
});
5052

51-
subscription.subscribe({
52-
next: ({ data }) => {
53-
settings.value = data.settings;
54-
try {
55-
localStorage.setItem(
56-
SETTINGS_CACHE_KEY,
57-
JSON.stringify(data.settings),
58-
);
59-
} catch {}
60-
},
61-
});
53+
subscribe(
54+
"settings:settings",
55+
subscription.subscribe({
56+
next: ({ data }) => {
57+
settings.value = data.settings;
58+
try {
59+
localStorage.setItem(
60+
SETTINGS_CACHE_KEY,
61+
JSON.stringify(data.settings),
62+
);
63+
} catch {}
64+
},
65+
}),
66+
);
6267
};
6368

6469
subscribeToSettings();
6570

6671
const currentPluginVersion = ref<string | null>(null);
6772

6873
const subscribeToPluginVersion = async () => {
74+
const { subscribe } = useSubscriptionManager();
6975
const authStore = useAuthStore();
7076
if (
7177
!authStore.me ||
@@ -92,11 +98,14 @@ export const useApplicationSettingsStore = defineStore(
9298
}),
9399
});
94100

95-
subscription.subscribe({
96-
next: ({ data }) => {
97-
currentPluginVersion.value = data.plugin_versions.at(0).version;
98-
},
99-
});
101+
subscribe(
102+
"settings:plugin_version",
103+
subscription.subscribe({
104+
next: ({ data }) => {
105+
currentPluginVersion.value = data.plugin_versions.at(0).version;
106+
},
107+
}),
108+
);
100109
};
101110

102111
// Watch for user authentication before subscribing
@@ -208,7 +217,10 @@ export const useApplicationSettingsStore = defineStore(
208217

209218
const availableRegions = ref<Region[]>([]);
210219

220+
let latencyCheckInterval: ReturnType<typeof setInterval> | null = null;
221+
211222
const subscribeToAvailableRegions = async () => {
223+
const { subscribe } = useSubscriptionManager();
212224
const subscription = getGraphqlClient().subscribe({
213225
query: generateSubscription({
214226
server_regions: [
@@ -230,19 +242,24 @@ export const useApplicationSettingsStore = defineStore(
230242
}),
231243
});
232244

233-
subscription.subscribe({
234-
next: ({ data }) => {
235-
availableRegions.value = data.server_regions;
236-
useMatchmakingStore().checkLatenies();
237-
238-
setInterval(
239-
() => {
240-
useMatchmakingStore().checkLatenies();
241-
},
242-
50 * 60 * 1000,
243-
);
244-
},
245-
});
245+
subscribe(
246+
"settings:available_regions",
247+
subscription.subscribe({
248+
next: ({ data }) => {
249+
availableRegions.value = data.server_regions;
250+
useMatchmakingStore().checkLatenies();
251+
252+
if (!latencyCheckInterval) {
253+
latencyCheckInterval = setInterval(
254+
() => {
255+
useMatchmakingStore().checkLatenies();
256+
},
257+
50 * 60 * 1000,
258+
);
259+
}
260+
},
261+
}),
262+
);
246263
};
247264

248265
subscribeToAvailableRegions();

stores/MatchLobbyStore.ts

Lines changed: 108 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ref, computed } from "vue";
22
import { defineStore, acceptHMRUpdate } from "pinia";
3+
import { useSubscriptionManager } from "~/composables/useSubscriptionManager";
34
import { simpleMatchFields } from "~/graphql/simpleMatchFields";
45
import {
56
$,
@@ -51,14 +52,18 @@ export const useMatchLobbyStore = defineStore("matchLobby", () => {
5152
}),
5253
});
5354

54-
subscription.subscribe({
55-
next: ({ data }) => {
56-
liveMatchesCount.value = data?.matches_aggregate?.aggregate?.count || 0;
57-
},
58-
error: (error) => {
59-
console.error("Error in live matches subscription:", error);
60-
},
61-
});
55+
const { subscribe } = useSubscriptionManager();
56+
subscribe(
57+
"matchLobby:liveMatches",
58+
subscription.subscribe({
59+
next: ({ data }) => {
60+
liveMatchesCount.value = data?.matches_aggregate?.aggregate?.count || 0;
61+
},
62+
error: (error) => {
63+
console.error("Error in live matches subscription:", error);
64+
},
65+
}),
66+
);
6267
};
6368

6469
const subscribeToLiveTournaments = async () => {
@@ -81,15 +86,19 @@ export const useMatchLobbyStore = defineStore("matchLobby", () => {
8186
}),
8287
});
8388

84-
subscription.subscribe({
85-
next: ({ data }) => {
86-
liveTournamentsCount.value =
87-
data?.tournaments_aggregate?.aggregate?.count || 0;
88-
},
89-
error: (error) => {
90-
console.error("Error in live tournaments subscription:", error);
91-
},
92-
});
89+
const { subscribe } = useSubscriptionManager();
90+
subscribe(
91+
"matchLobby:liveTournaments",
92+
subscription.subscribe({
93+
next: ({ data }) => {
94+
liveTournamentsCount.value =
95+
data?.tournaments_aggregate?.aggregate?.count || 0;
96+
},
97+
error: (error) => {
98+
console.error("Error in live tournaments subscription:", error);
99+
},
100+
}),
101+
);
93102
};
94103

95104
const subscribeToOpenRegistrationTournaments = async () => {
@@ -112,18 +121,22 @@ export const useMatchLobbyStore = defineStore("matchLobby", () => {
112121
}),
113122
});
114123

115-
subscription.subscribe({
116-
next: ({ data }) => {
117-
openRegistrationTournamentsCount.value =
118-
data?.tournaments_aggregate?.aggregate?.count || 0;
119-
},
120-
error: (error) => {
121-
console.error(
122-
"Error in open registration tournaments subscription:",
123-
error,
124-
);
125-
},
126-
});
124+
const { subscribe } = useSubscriptionManager();
125+
subscribe(
126+
"matchLobby:openRegistrationTournaments",
127+
subscription.subscribe({
128+
next: ({ data }) => {
129+
openRegistrationTournamentsCount.value =
130+
data?.tournaments_aggregate?.aggregate?.count || 0;
131+
},
132+
error: (error) => {
133+
console.error(
134+
"Error in open registration tournaments subscription:",
135+
error,
136+
);
137+
},
138+
}),
139+
);
127140
};
128141

129142
const subscribeToOpenMatches = async () => {
@@ -151,14 +164,18 @@ export const useMatchLobbyStore = defineStore("matchLobby", () => {
151164
}),
152165
});
153166

154-
subscription.subscribe({
155-
next: ({ data }) => {
156-
openMatchesCount.value = data?.matches_aggregate?.aggregate?.count || 0;
157-
},
158-
error: (error) => {
159-
console.error("Error in open matches subscription:", error);
160-
},
161-
});
167+
const { subscribe } = useSubscriptionManager();
168+
subscribe(
169+
"matchLobby:openMatches",
170+
subscription.subscribe({
171+
next: ({ data }) => {
172+
openMatchesCount.value = data?.matches_aggregate?.aggregate?.count || 0;
173+
},
174+
error: (error) => {
175+
console.error("Error in open matches subscription:", error);
176+
},
177+
}),
178+
);
162179
};
163180

164181
const subscribeToChatTournaments = async () => {
@@ -198,14 +215,18 @@ export const useMatchLobbyStore = defineStore("matchLobby", () => {
198215
}),
199216
});
200217

201-
subscription.subscribe({
202-
next: ({ data }) => {
203-
chatTournaments.value = data?.tournaments || [];
204-
},
205-
error: (error) => {
206-
console.error("Error in chat tournaments subscription:", error);
207-
},
208-
});
218+
const { subscribe } = useSubscriptionManager();
219+
subscribe(
220+
"matchLobby:chatTournaments",
221+
subscription.subscribe({
222+
next: ({ data }) => {
223+
chatTournaments.value = data?.tournaments || [];
224+
},
225+
error: (error) => {
226+
console.error("Error in chat tournaments subscription:", error);
227+
},
228+
}),
229+
);
209230
};
210231

211232
const subscribeToManagingMatches = async () => {
@@ -240,18 +261,22 @@ export const useMatchLobbyStore = defineStore("matchLobby", () => {
240261
}),
241262
});
242263

243-
subscription.subscribe({
244-
next: ({ data }) => {
245-
if (data?.matches_aggregate?.aggregate?.count !== undefined) {
246-
managingMatchesCount.value = data.matches_aggregate.aggregate.count;
247-
} else {
248-
managingMatchesCount.value = 0;
249-
}
250-
},
251-
error: (error) => {
252-
console.error("Error in managing matches subscription:", error);
253-
},
254-
});
264+
const { subscribe } = useSubscriptionManager();
265+
subscribe(
266+
"matchLobby:managingMatches",
267+
subscription.subscribe({
268+
next: ({ data }) => {
269+
if (data?.matches_aggregate?.aggregate?.count !== undefined) {
270+
managingMatchesCount.value = data.matches_aggregate.aggregate.count;
271+
} else {
272+
managingMatchesCount.value = 0;
273+
}
274+
},
275+
error: (error) => {
276+
console.error("Error in managing matches subscription:", error);
277+
},
278+
}),
279+
);
255280
};
256281

257282
const subscribeToManagingTournaments = async () => {
@@ -284,19 +309,23 @@ export const useMatchLobbyStore = defineStore("matchLobby", () => {
284309
}),
285310
});
286311

287-
subscription.subscribe({
288-
next: ({ data }) => {
289-
if (data?.tournaments_aggregate?.aggregate?.count !== undefined) {
290-
managingTournamentsCount.value =
291-
data.tournaments_aggregate.aggregate.count;
292-
} else {
293-
managingTournamentsCount.value = 0;
294-
}
295-
},
296-
error: (error) => {
297-
console.error("Error in managing tournaments subscription:", error);
298-
},
299-
});
312+
const { subscribe } = useSubscriptionManager();
313+
subscribe(
314+
"matchLobby:managingTournaments",
315+
subscription.subscribe({
316+
next: ({ data }) => {
317+
if (data?.tournaments_aggregate?.aggregate?.count !== undefined) {
318+
managingTournamentsCount.value =
319+
data.tournaments_aggregate.aggregate.count;
320+
} else {
321+
managingTournamentsCount.value = 0;
322+
}
323+
},
324+
error: (error) => {
325+
console.error("Error in managing tournaments subscription:", error);
326+
},
327+
}),
328+
);
300329
};
301330

302331
const subscribeToMyMatches = async () => {
@@ -373,11 +402,15 @@ export const useMatchLobbyStore = defineStore("matchLobby", () => {
373402
},
374403
});
375404

376-
subscription.subscribe({
377-
next: ({ data }) => {
378-
myMatches.value = data?.matches;
379-
},
380-
});
405+
const { subscribe } = useSubscriptionManager();
406+
subscribe(
407+
"matchLobby:myMatches",
408+
subscription.subscribe({
409+
next: ({ data }) => {
410+
myMatches.value = data?.matches;
411+
},
412+
}),
413+
);
381414
};
382415

383416
const add = (

0 commit comments

Comments
 (0)