Skip to content

Commit 98a9464

Browse files
committed
fix: channel cache in slack and teams
1 parent bb0d5f6 commit 98a9464

File tree

6 files changed

+796
-161
lines changed

6 files changed

+796
-161
lines changed

packages/react-core/src/modules/ms-teams/hooks/useMsTeamsChannels.ts

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { GetMsTeamsChannelsResponse, MsTeamsChannel } from "@knocklabs/client";
2+
import { useCallback, useEffect, useRef } from "react";
23
import useSWR from "swr";
34

45
import { useKnockClient } from "../../core";
@@ -23,26 +24,60 @@ function useMsTeamsChannels({
2324
queryOptions,
2425
}: UseMsTeamsChannelsProps): UseMsTeamsChannelsOutput {
2526
const knock = useKnockClient();
26-
const { knockMsTeamsChannelId, tenantId } = useKnockMsTeamsClient();
27-
28-
const fetchChannels = () =>
29-
knock.msTeams.getChannels({
30-
knockChannelId: knockMsTeamsChannelId,
31-
tenant: tenantId,
32-
teamId: teamId!,
33-
queryOptions: {
34-
$filter: queryOptions?.filter,
35-
$select: queryOptions?.select,
36-
},
37-
});
27+
const { knockMsTeamsChannelId, tenantId, connectionStatus } =
28+
useKnockMsTeamsClient();
3829

30+
// Track previous tenant/channel/connectionStatus to detect changes and clear cache
31+
const prevTenantRef = useRef(tenantId);
32+
const prevChannelRef = useRef(knockMsTeamsChannelId);
33+
const prevConnectionStatusRef = useRef(connectionStatus);
34+
35+
const fetchChannels = useCallback(
36+
() =>
37+
knock.msTeams.getChannels({
38+
knockChannelId: knockMsTeamsChannelId,
39+
tenant: tenantId,
40+
teamId: teamId!,
41+
queryOptions: {
42+
$filter: queryOptions?.filter,
43+
$select: queryOptions?.select,
44+
},
45+
}),
46+
[knock.msTeams, knockMsTeamsChannelId, tenantId, teamId, queryOptions],
47+
);
48+
49+
// Include tenantId and knockMsTeamsChannelId in the cache key so that
50+
// SWR treats different tenants as different cache entries
3951
const { data, isLoading, isValidating, mutate } =
4052
useSWR<GetMsTeamsChannelsResponse>(
41-
teamId ? [QUERY_KEY, teamId] : null,
53+
teamId && connectionStatus === "connected"
54+
? [QUERY_KEY, tenantId, knockMsTeamsChannelId, teamId]
55+
: null,
4256
fetchChannels,
4357
{ revalidateOnFocus: false },
4458
);
4559

60+
// Clear cache when tenant, channel, or connection status changes
61+
// This ensures that when the user disconnects and reconnects (possibly to a different
62+
// MS Teams workspace), or when the access token is revoked, the cached channels are cleared
63+
useEffect(() => {
64+
const tenantChanged = prevTenantRef.current !== tenantId;
65+
const channelChanged = prevChannelRef.current !== knockMsTeamsChannelId;
66+
// Detect when connection is re-established (was not connected, now is connected)
67+
const wasConnected = prevConnectionStatusRef.current === "connected";
68+
const isConnected = connectionStatus === "connected";
69+
const connectionReestablished = !wasConnected && isConnected;
70+
71+
if (tenantChanged || channelChanged || connectionReestablished) {
72+
// Reset the SWR state to clear cached data
73+
mutate(undefined, { revalidate: false });
74+
}
75+
76+
prevTenantRef.current = tenantId;
77+
prevChannelRef.current = knockMsTeamsChannelId;
78+
prevConnectionStatusRef.current = connectionStatus;
79+
}, [tenantId, knockMsTeamsChannelId, connectionStatus, mutate]);
80+
4681
return {
4782
data: data?.ms_teams_channels ?? [],
4883
isLoading: isLoading || isValidating,

packages/react-core/src/modules/ms-teams/hooks/useMsTeamsTeams.ts

Lines changed: 81 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GetMsTeamsTeamsResponse, MsTeamsTeam } from "@knocklabs/client";
2-
import { useEffect, useMemo } from "react";
2+
import { useCallback, useEffect, useMemo, useRef } from "react";
33
import useSWRInfinite from "swr/infinite";
44

55
import { useKnockClient } from "../../core";
@@ -20,25 +20,9 @@ type UseMsTeamsTeamsOutput = {
2020
refetch: () => void;
2121
};
2222

23-
type QueryKey = [key: string, skiptoken: string] | null;
24-
25-
function getQueryKey(
26-
pageIndex: number,
27-
previousPageData: GetMsTeamsTeamsResponse,
28-
): QueryKey {
29-
// First page so just pass empty
30-
if (pageIndex === 0) {
31-
return [QUERY_KEY, ""];
32-
}
33-
34-
// If there's no more data then return an empty next skiptoken
35-
if (previousPageData && ["", null].includes(previousPageData.skip_token)) {
36-
return null;
37-
}
38-
39-
// Next skiptoken exists so pass it
40-
return [QUERY_KEY, previousPageData.skip_token ?? ""];
41-
}
23+
type QueryKey =
24+
| [key: string, tenantId: string, channelId: string, skiptoken: string]
25+
| null;
4226

4327
function useMsTeamsTeams({
4428
queryOptions = {},
@@ -47,17 +31,61 @@ function useMsTeamsTeams({
4731
const { knockMsTeamsChannelId, tenantId, connectionStatus } =
4832
useKnockMsTeamsClient();
4933

50-
const fetchTeams = (queryKey: QueryKey) =>
51-
knock.msTeams.getTeams({
52-
knockChannelId: knockMsTeamsChannelId,
53-
tenant: tenantId,
54-
queryOptions: {
55-
$skiptoken: queryKey?.[1],
56-
$top: queryOptions?.limitPerPage,
57-
$filter: queryOptions?.filter,
58-
$select: queryOptions?.select,
59-
},
60-
});
34+
// Track previous tenant/channel/connectionStatus to detect changes and clear cache
35+
const prevTenantRef = useRef(tenantId);
36+
const prevChannelRef = useRef(knockMsTeamsChannelId);
37+
const prevConnectionStatusRef = useRef(connectionStatus);
38+
39+
// Create a getQueryKey function that includes tenantId and knockMsTeamsChannelId
40+
// so that SWR treats different tenants as different cache entries
41+
const getQueryKey = useCallback(
42+
(
43+
pageIndex: number,
44+
previousPageData: GetMsTeamsTeamsResponse | null,
45+
): QueryKey => {
46+
// Don't fetch if not connected
47+
if (connectionStatus !== "connected") {
48+
return null;
49+
}
50+
51+
// First page so just pass empty
52+
if (pageIndex === 0) {
53+
return [QUERY_KEY, tenantId, knockMsTeamsChannelId, ""];
54+
}
55+
56+
// If there's no more data then return an empty next skiptoken
57+
if (
58+
previousPageData &&
59+
["", null].includes(previousPageData.skip_token)
60+
) {
61+
return null;
62+
}
63+
64+
// Next skiptoken exists so pass it
65+
return [
66+
QUERY_KEY,
67+
tenantId,
68+
knockMsTeamsChannelId,
69+
previousPageData?.skip_token ?? "",
70+
];
71+
},
72+
[tenantId, knockMsTeamsChannelId, connectionStatus],
73+
);
74+
75+
const fetchTeams = useCallback(
76+
(queryKey: QueryKey) =>
77+
knock.msTeams.getTeams({
78+
knockChannelId: knockMsTeamsChannelId,
79+
tenant: tenantId,
80+
queryOptions: {
81+
$skiptoken: queryKey?.[3],
82+
$top: queryOptions?.limitPerPage,
83+
$filter: queryOptions?.filter,
84+
$select: queryOptions?.select,
85+
},
86+
}),
87+
[knock.msTeams, knockMsTeamsChannelId, tenantId, queryOptions],
88+
);
6189

6290
const { data, error, isLoading, isValidating, setSize, mutate } =
6391
useSWRInfinite<GetMsTeamsTeamsResponse>(getQueryKey, fetchTeams, {
@@ -66,6 +94,28 @@ function useMsTeamsTeams({
6694
revalidateFirstPage: false,
6795
});
6896

97+
// Clear cache when tenant, channel, or connection status changes
98+
// This ensures that when the user disconnects and reconnects (possibly to a different
99+
// MS Teams workspace), or when the access token is revoked, the cached teams are cleared
100+
useEffect(() => {
101+
const tenantChanged = prevTenantRef.current !== tenantId;
102+
const channelChanged = prevChannelRef.current !== knockMsTeamsChannelId;
103+
// Detect when connection is re-established (was not connected, now is connected)
104+
const wasConnected = prevConnectionStatusRef.current === "connected";
105+
const isConnected = connectionStatus === "connected";
106+
const connectionReestablished = !wasConnected && isConnected;
107+
108+
if (tenantChanged || channelChanged || connectionReestablished) {
109+
// Reset the SWR state to clear cached data
110+
mutate(undefined, { revalidate: false });
111+
setSize(0);
112+
}
113+
114+
prevTenantRef.current = tenantId;
115+
prevChannelRef.current = knockMsTeamsChannelId;
116+
prevConnectionStatusRef.current = connectionStatus;
117+
}, [tenantId, knockMsTeamsChannelId, connectionStatus, mutate, setSize]);
118+
69119
const lastPage = data?.at(-1);
70120
const hasNextPage = lastPage === undefined || !!lastPage.skip_token;
71121

packages/react-core/src/modules/slack/hooks/useSlackChannels.ts

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { SlackChannelQueryOptions, useKnockSlackClient } from "..";
22
import { GetSlackChannelsResponse, SlackChannel } from "@knocklabs/client";
3-
import { useEffect, useMemo } from "react";
3+
import { useCallback, useEffect, useMemo, useRef } from "react";
44
import useSWRInfinite from "swr/infinite";
55

66
import { useKnockClient } from "../../core";
@@ -21,25 +21,9 @@ type UseSlackChannelOutput = {
2121
refetch: () => void;
2222
};
2323

24-
type QueryKey = [key: string, cursor: string] | null;
25-
26-
function getQueryKey(
27-
pageIndex: number,
28-
previousPageData: GetSlackChannelsResponse,
29-
): QueryKey {
30-
// First page so just pass empty
31-
if (pageIndex === 0) {
32-
return [QUERY_KEY, ""];
33-
}
34-
35-
// If there's no more data then return an empty next cursor
36-
if (previousPageData && ["", null].includes(previousPageData.next_cursor)) {
37-
return null;
38-
}
39-
40-
// Next cursor exists so pass it
41-
return [QUERY_KEY, previousPageData.next_cursor ?? ""];
42-
}
24+
type QueryKey =
25+
| [key: string, tenantId: string, channelId: string, cursor: string]
26+
| null;
4327

4428
function useSlackChannels({
4529
queryOptions,
@@ -48,25 +32,91 @@ function useSlackChannels({
4832
const { knockSlackChannelId, tenantId, connectionStatus } =
4933
useKnockSlackClient();
5034

51-
const fetchChannels = (queryKey: QueryKey) => {
52-
return knock.slack.getChannels({
53-
tenant: tenantId,
54-
knockChannelId: knockSlackChannelId,
55-
queryOptions: {
56-
...queryOptions,
57-
cursor: queryKey?.[1],
58-
limit: queryOptions?.limitPerPage || LIMIT_PER_PAGE,
59-
types: queryOptions?.types || CHANNEL_TYPES,
60-
},
61-
});
62-
};
35+
// Track previous tenant/channel/connectionStatus to detect changes and clear cache
36+
const prevTenantRef = useRef(tenantId);
37+
const prevChannelRef = useRef(knockSlackChannelId);
38+
const prevConnectionStatusRef = useRef(connectionStatus);
39+
40+
// Create a getQueryKey function that includes tenantId and knockSlackChannelId
41+
// so that SWR treats different tenants as different cache entries
42+
const getQueryKey = useCallback(
43+
(
44+
pageIndex: number,
45+
previousPageData: GetSlackChannelsResponse | null,
46+
): QueryKey => {
47+
// Don't fetch if not connected
48+
if (connectionStatus !== "connected") {
49+
return null;
50+
}
51+
52+
// First page so just pass empty
53+
if (pageIndex === 0) {
54+
return [QUERY_KEY, tenantId, knockSlackChannelId, ""];
55+
}
56+
57+
// If there's no more data then return an empty next cursor
58+
if (
59+
previousPageData &&
60+
["", null].includes(previousPageData.next_cursor)
61+
) {
62+
return null;
63+
}
64+
65+
// Next cursor exists so pass it
66+
return [
67+
QUERY_KEY,
68+
tenantId,
69+
knockSlackChannelId,
70+
previousPageData?.next_cursor ?? "",
71+
];
72+
},
73+
[tenantId, knockSlackChannelId, connectionStatus],
74+
);
75+
76+
const fetchChannels = useCallback(
77+
(queryKey: QueryKey) => {
78+
return knock.slack.getChannels({
79+
tenant: tenantId,
80+
knockChannelId: knockSlackChannelId,
81+
queryOptions: {
82+
...queryOptions,
83+
cursor: queryKey?.[3],
84+
limit: queryOptions?.limitPerPage || LIMIT_PER_PAGE,
85+
types: queryOptions?.types || CHANNEL_TYPES,
86+
},
87+
});
88+
},
89+
[knock.slack, tenantId, knockSlackChannelId, queryOptions],
90+
);
6391

6492
const { data, error, isLoading, isValidating, setSize, mutate } =
6593
useSWRInfinite<GetSlackChannelsResponse>(getQueryKey, fetchChannels, {
6694
initialSize: 0,
6795
revalidateFirstPage: false,
6896
});
6997

98+
// Clear cache when tenant, channel, or connection status changes
99+
// This ensures that when the user disconnects and reconnects (possibly to a different
100+
// Slack workspace), or when the access token is revoked, the cached channels are cleared
101+
useEffect(() => {
102+
const tenantChanged = prevTenantRef.current !== tenantId;
103+
const channelChanged = prevChannelRef.current !== knockSlackChannelId;
104+
// Detect when connection is re-established (was not connected, now is connected)
105+
const wasConnected = prevConnectionStatusRef.current === "connected";
106+
const isConnected = connectionStatus === "connected";
107+
const connectionReestablished = !wasConnected && isConnected;
108+
109+
if (tenantChanged || channelChanged || connectionReestablished) {
110+
// Reset the SWR state to clear cached data
111+
mutate(undefined, { revalidate: false });
112+
setSize(0);
113+
}
114+
115+
prevTenantRef.current = tenantId;
116+
prevChannelRef.current = knockSlackChannelId;
117+
prevConnectionStatusRef.current = connectionStatus;
118+
}, [tenantId, knockSlackChannelId, connectionStatus, mutate, setSize]);
119+
70120
const lastPage = data?.at(-1);
71121
const hasNextPage = lastPage === undefined || !!lastPage.next_cursor;
72122

0 commit comments

Comments
 (0)