From dc6cbe3f46b03561384247fde4686d03982a0afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= <48084558+servusrene@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:02:34 +0100 Subject: [PATCH 1/3] fix: update subscriber role cache in real-time via EventSub events The subscriber cache was only populated at startup and conditionally on chat reconnect, causing $userRoles to not reflect new or expired subs until restart. Now the cache is updated immediately on sub/resub/gift sub/upgrade events, refreshed on stream online and chat reconnect, and subscribers are removed via onChannelSubscriptionEnd. --- src/backend/chat/twitch-chat.ts | 5 +-- src/backend/roles/twitch-roles-manager.ts | 27 +++++++++++++++ .../twitch/api/eventsub/eventsub-client.ts | 34 ++++++++++++++++++- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/backend/chat/twitch-chat.ts b/src/backend/chat/twitch-chat.ts index e5c06fb04..04b6b824e 100644 --- a/src/backend/chat/twitch-chat.ts +++ b/src/backend/chat/twitch-chat.ts @@ -123,10 +123,7 @@ class TwitchChat extends EventEmitter { // While connected, we can just react to changes via chat messages/EventSub events await twitchRolesManager.loadVips(); await twitchRolesManager.loadModerators(); - - if (!twitchRolesManager.getSubscribers().length) { - await twitchRolesManager.loadSubscribers(); - } + await twitchRolesManager.loadSubscribers(); // Load the current Shared Chat session await SharedChatCache.loadSessionFromTwitch(); diff --git a/src/backend/roles/twitch-roles-manager.ts b/src/backend/roles/twitch-roles-manager.ts index 3bcb7245f..1a1b746e7 100644 --- a/src/backend/roles/twitch-roles-manager.ts +++ b/src/backend/roles/twitch-roles-manager.ts @@ -98,6 +98,33 @@ class TwitchRolesManager extends TypedEmitter { return this._subscribers; } + addSubscriberToSubscribersList(userId: string, username: string, displayName: string, tier: string): void { + const subTier = this.getRoleForSubTier(tier); + const existing = this._subscribers.find(s => s.id === userId); + + if (existing == null) { + this._subscribers.push({ id: userId, username, displayName, subTier }); + this.emit("viewer-role-updated", userId, "sub", "added"); + this.emit("viewer-role-updated", userId, subTier, "added"); + return; + } + + const previousTier = existing.subTier; + existing.subTier = subTier; + + if (previousTier === subTier) { + return; + } + + this.emit("viewer-role-updated", userId, previousTier, "removed"); + this.emit("viewer-role-updated", userId, subTier, "added"); + } + + removeSubscriberFromSubscribersList(userId: string): void { + this._subscribers = this._subscribers.filter(s => s.id !== userId); + this.emit("viewer-role-updated", userId, "sub", "removed"); + } + private getRoleForSubTier(tier: string): string { let role = ""; switch (tier) { diff --git a/src/backend/streaming-platforms/twitch/api/eventsub/eventsub-client.ts b/src/backend/streaming-platforms/twitch/api/eventsub/eventsub-client.ts index 657ee7c5b..6f58b8125 100644 --- a/src/backend/streaming-platforms/twitch/api/eventsub/eventsub-client.ts +++ b/src/backend/streaming-platforms/twitch/api/eventsub/eventsub-client.ts @@ -29,12 +29,13 @@ class TwitchEventSubClient { const streamer = AccountAccess.getAccounts().streamer; // Stream online - const onlineSubscription = this._eventSubListener.onStreamOnline(streamer.userId, (event) => { + const onlineSubscription = this._eventSubListener.onStreamOnline(streamer.userId, async (event) => { TwitchEventHandlers.stream.triggerStreamOnline( event.broadcasterName, event.broadcasterId, event.broadcasterDisplayName ); + await twitchRolesManager.loadSubscribers(); }); this._subscriptions.push(onlineSubscription); @@ -815,6 +816,12 @@ class TwitchEventSubClient { event.isPrime, event.type === "resub" ); + twitchRolesManager.addSubscriberToSubscribersList( + event.chatterId, + event.chatterName, + event.chatterDisplayName, + event.tier + ); break; case "community_sub_gift": @@ -837,6 +844,12 @@ class TwitchEventSubClient { event.cumulativeAmount, event.communityGiftId ); + twitchRolesManager.addSubscriberToSubscribersList( + event.recipientId, + event.recipientName, + event.recipientDisplayName, + event.tier + ); await viewerDatabase.calculateAutoRanks(event.recipientId); break; @@ -852,6 +865,13 @@ class TwitchEventSubClient { event.gifterDisplayName, upgradeTier ); + + twitchRolesManager.addSubscriberToSubscribersList( + event.chatterId, + event.chatterName, + event.chatterDisplayName, + upgradeTier + ); } await viewerDatabase.calculateAutoRanks(event.chatterId); break; @@ -863,6 +883,12 @@ class TwitchEventSubClient { event.chatterDisplayName, event.tier ); + twitchRolesManager.addSubscriberToSubscribersList( + event.chatterId, + event.chatterName, + event.chatterDisplayName, + event.tier + ); await viewerDatabase.calculateAutoRanks(event.chatterId); break; @@ -872,6 +898,12 @@ class TwitchEventSubClient { } }); this._subscriptions.push(chatNotificationSubscription); + + // Subscription ended + const subscriptionEndSubscription = this._eventSubListener.onChannelSubscriptionEnd(streamer.userId, (event) => { + twitchRolesManager.removeSubscriberFromSubscribersList(event.userId); + }); + this._subscriptions.push(subscriptionEndSubscription); } createClient(): void { From 53f0a776ed254706c1da659190f0731e43ddeff4 Mon Sep 17 00:00:00 2001 From: Zack Williamson Date: Tue, 23 Jun 2026 23:42:25 -0400 Subject: [PATCH 2/3] fix merge --- .../twitch/api/eventsub/eventsub-client.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/backend/streaming-platforms/twitch/api/eventsub/eventsub-client.ts b/src/backend/streaming-platforms/twitch/api/eventsub/eventsub-client.ts index 5dd8b6859..65b683454 100644 --- a/src/backend/streaming-platforms/twitch/api/eventsub/eventsub-client.ts +++ b/src/backend/streaming-platforms/twitch/api/eventsub/eventsub-client.ts @@ -941,6 +941,12 @@ class TwitchEventSubClient { event.isPrime, event.type === "resub" ); + twitchRolesManager.addSubscriberToSubscribersList( + event.chatterId, + event.chatterName, + event.chatterDisplayName, + event.tier + ); break; case "community_sub_gift": @@ -963,6 +969,12 @@ class TwitchEventSubClient { event.cumulativeAmount, event.communityGiftId ); + twitchRolesManager.addSubscriberToSubscribersList( + event.recipientId, + event.recipientName, + event.recipientDisplayName, + event.tier + ); await viewerDatabase.calculateAutoRanks(event.recipientId); break; @@ -979,6 +991,13 @@ class TwitchEventSubClient { event.gifterDisplayName, upgradeTier ); + + twitchRolesManager.addSubscriberToSubscribersList( + event.chatterId, + event.chatterName, + event.chatterDisplayName, + upgradeTier + ); } await viewerDatabase.calculateAutoRanks(event.chatterId); break; @@ -990,6 +1009,12 @@ class TwitchEventSubClient { event.chatterDisplayName, event.tier ); + twitchRolesManager.addSubscriberToSubscribersList( + event.chatterId, + event.chatterName, + event.chatterDisplayName, + event.tier + ); await viewerDatabase.calculateAutoRanks(event.chatterId); break; From 68b801edf35f9a2321aa9682a5ee906f65914ba3 Mon Sep 17 00:00:00 2001 From: Zack Williamson Date: Tue, 23 Jun 2026 23:47:22 -0400 Subject: [PATCH 3/3] add subscriber refresh interval check --- src/backend/roles/twitch-roles-manager.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/backend/roles/twitch-roles-manager.ts b/src/backend/roles/twitch-roles-manager.ts index 51fd4277a..2d5beee75 100644 --- a/src/backend/roles/twitch-roles-manager.ts +++ b/src/backend/roles/twitch-roles-manager.ts @@ -20,6 +20,7 @@ class TwitchRolesManager extends TypedEmitter { private _vips: BasicViewer[] = []; private _moderators: BasicViewer[] = []; private _subscribers: Subscriber[] = []; + private _subscribersLastLoadedAt: number | null = null; constructor() { super(); @@ -86,6 +87,11 @@ class TwitchRolesManager extends TypedEmitter { } async loadSubscribers(): Promise { + // Only reload subscribers if it's been more than 30 minutes since they were last loaded + if (this.getMinutesSinceSubscribersLoaded() < 30) { + return; + } + this._subscribers = (await TwitchApi.subscriptions.getSubscriptions()) .map(m => ({ id: m.userId, @@ -93,6 +99,15 @@ class TwitchRolesManager extends TypedEmitter { displayName: m.userDisplayName, subTier: this.getRoleForSubTier(m.tier) })); + + this._subscribersLastLoadedAt = Date.now(); + } + + private getMinutesSinceSubscribersLoaded(): number { + if (this._subscribersLastLoadedAt == null) { + return Infinity; + } + return (Date.now() - this._subscribersLastLoadedAt) / 1000 / 60; } getSubscribers(): Subscriber[] {