Skip to content

Commit e18fb36

Browse files
committed
[Experiment] unstable_cache: Teams and Projects
1 parent c86e13b commit e18fb36

File tree

7 files changed

+200
-31
lines changed

7 files changed

+200
-31
lines changed

apps/dashboard/src/@/actions/proxies.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use server";
2-
2+
import { revalidateTag } from "next/cache";
33
import { getAuthToken } from "../../app/api/lib/getAuthToken";
4+
import { projectCacheTag, teamCacheTag } from "../constants/cacheTags";
45
import { API_SERVER_URL } from "../constants/env";
56

67
type ProxyActionParams = {
@@ -9,6 +10,12 @@ type ProxyActionParams = {
910
method: "GET" | "POST" | "PUT" | "DELETE";
1011
body?: string;
1112
headers?: Record<string, string>;
13+
revalidateCacheTags?: RevalidateCacheTagOptions;
14+
};
15+
16+
type RevalidateCacheTagOptions = {
17+
teamTag?: true;
18+
projectTag?: true;
1219
};
1320

1421
type ProxyActionResult<T extends object> =
@@ -28,6 +35,15 @@ async function proxy<T extends object>(
2835
params: ProxyActionParams,
2936
): Promise<ProxyActionResult<T>> {
3037
const authToken = await getAuthToken();
38+
const { revalidateCacheTags: revalidateCacheTag } = params;
39+
40+
if (!authToken) {
41+
return {
42+
status: 401,
43+
ok: false,
44+
error: "Unauthorized",
45+
};
46+
}
3147

3248
// build URL
3349
const url = new URL(baseUrl);
@@ -64,6 +80,16 @@ async function proxy<T extends object>(
6480
}
6581
}
6682

83+
if (revalidateCacheTag) {
84+
if (revalidateCacheTag.teamTag) {
85+
revalidateTag(teamCacheTag(authToken));
86+
}
87+
88+
if (revalidateCacheTag.projectTag) {
89+
revalidateTag(projectCacheTag(authToken));
90+
}
91+
}
92+
6793
return {
6894
status: res.status,
6995
ok: true,

apps/dashboard/src/@/api/projects.ts

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import "server-only";
22
import { API_SERVER_URL } from "@/constants/env";
3+
import { unstable_cache } from "next/cache";
34
import { getAuthToken } from "../../app/api/lib/getAuthToken";
5+
import { projectCacheTag } from "../constants/cacheTags";
46

57
export type Project = {
68
id: string;
@@ -26,37 +28,74 @@ export async function getProjects(teamSlug: string) {
2628
return [];
2729
}
2830

29-
const teamsRes = await fetch(
30-
`${API_SERVER_URL}/v1/teams/${teamSlug}/projects`,
31+
const getCachedProjects = unstable_cache(
32+
getProjectsForAuthToken,
33+
["getProjects"],
3134
{
32-
headers: {
33-
Authorization: `Bearer ${token}`,
34-
},
35+
tags: [projectCacheTag(token)],
36+
revalidate: 3600, // 1 hour
3537
},
3638
);
37-
if (teamsRes.ok) {
38-
return (await teamsRes.json())?.result as Project[];
39+
40+
return getCachedProjects(token, teamSlug);
41+
}
42+
43+
export async function getProjectsForAuthToken(
44+
authToken: string,
45+
teamSlug: string,
46+
) {
47+
console.log("FETCHING PROJECTS ------------------------");
48+
const res = await fetch(`${API_SERVER_URL}/v1/teams/${teamSlug}/projects`, {
49+
headers: {
50+
Authorization: `Bearer ${authToken}`,
51+
},
52+
});
53+
if (res.ok) {
54+
return (await res.json())?.result as Project[];
3955
}
4056
return [];
4157
}
4258

4359
export async function getProject(teamSlug: string, projectSlug: string) {
44-
const token = await getAuthToken();
60+
const authToken = await getAuthToken();
4561

46-
if (!token) {
62+
if (!authToken) {
4763
return null;
4864
}
4965

50-
const teamsRes = await fetch(
66+
const getCachedProject = unstable_cache(
67+
getProjectForAuthToken,
68+
["getProject"],
69+
{
70+
tags: [projectCacheTag(authToken)],
71+
revalidate: 3600, // 1 hour
72+
},
73+
);
74+
75+
return getCachedProject(authToken, teamSlug, projectSlug);
76+
}
77+
78+
async function getProjectForAuthToken(
79+
authToken: string,
80+
teamSlug: string,
81+
projectSlug: string,
82+
) {
83+
console.log(
84+
"FETCHING PROJECT ------------------------",
85+
teamSlug,
86+
projectSlug,
87+
);
88+
const res = await fetch(
5189
`${API_SERVER_URL}/v1/teams/${teamSlug}/projects/${projectSlug}`,
5290
{
5391
headers: {
54-
Authorization: `Bearer ${token}`,
92+
Authorization: `Bearer ${authToken}`,
5593
},
5694
},
5795
);
58-
if (teamsRes.ok) {
59-
return (await teamsRes.json())?.result as Project;
96+
97+
if (res.ok) {
98+
return (await res.json())?.result as Project;
6099
}
61100
return null;
62101
}

apps/dashboard/src/@/api/team.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import "server-only";
22
import { API_SERVER_URL } from "@/constants/env";
3+
import { unstable_cache } from "next/cache";
34
import { getAuthToken } from "../../app/api/lib/getAuthToken";
5+
import { teamCacheTag } from "../constants/cacheTags";
46

57
type EnabledTeamScope =
68
| "pay"
@@ -29,16 +31,31 @@ export type Team = {
2931
enabledScopes: EnabledTeamScope[];
3032
};
3133

32-
export async function getTeamBySlug(slug: string) {
33-
const token = await getAuthToken();
34+
export async function getTeamBySlug(teamSlug: string) {
35+
const authToken = await getAuthToken();
3436

35-
if (!token) {
37+
if (!authToken) {
3638
return null;
3739
}
3840

39-
const teamRes = await fetch(`${API_SERVER_URL}/v1/teams/${slug}`, {
41+
const getCachedTeam = unstable_cache(
42+
getTeamBySlugAndAuthToken,
43+
["getTeamBySlug"],
44+
{
45+
tags: [teamCacheTag(authToken)],
46+
revalidate: 3600, // 1 hour
47+
},
48+
);
49+
50+
return getCachedTeam(teamSlug, authToken);
51+
}
52+
53+
async function getTeamBySlugAndAuthToken(teamSlug: string, authToken: string) {
54+
console.log("FETCHING TEAM ------------------------", teamSlug);
55+
56+
const teamRes = await fetch(`${API_SERVER_URL}/v1/teams/${teamSlug}`, {
4057
headers: {
41-
Authorization: `Bearer ${token}`,
58+
Authorization: `Bearer ${authToken}`,
4259
},
4360
});
4461
if (teamRes.ok) {
@@ -48,14 +65,26 @@ export async function getTeamBySlug(slug: string) {
4865
}
4966

5067
export async function getTeams() {
51-
const token = await getAuthToken();
52-
if (!token) {
68+
const authToken = await getAuthToken();
69+
70+
if (!authToken) {
5371
return null;
5472
}
5573

74+
const getCachedTeams = unstable_cache(getTeamsForAuthToken, ["getTeams"], {
75+
tags: [teamCacheTag(authToken)],
76+
revalidate: 3600, // 1 hour
77+
});
78+
79+
return getCachedTeams(authToken);
80+
}
81+
82+
async function getTeamsForAuthToken(authToken: string) {
83+
console.log("FETCHING ALL TEAMs ------------------------");
84+
5685
const teamsRes = await fetch(`${API_SERVER_URL}/v1/teams`, {
5786
headers: {
58-
Authorization: `Bearer ${token}`,
87+
Authorization: `Bearer ${authToken}`,
5988
},
6089
});
6190
if (teamsRes.ok) {
@@ -70,24 +99,39 @@ type TeamNebulaWaitList = {
7099
};
71100

72101
export async function getTeamNebulaWaitList(teamSlug: string) {
73-
const token = await getAuthToken();
102+
const authToken = await getAuthToken();
74103

75-
if (!token) {
104+
if (!authToken) {
76105
return null;
77106
}
78107

108+
const getCachedNebulaWaitlist = unstable_cache(
109+
getTeamNebulaWaitListForAuthToken,
110+
["getTeamNebulaWaitList"],
111+
{
112+
tags: [teamCacheTag(authToken)],
113+
revalidate: 3600, // 1 hour
114+
},
115+
);
116+
117+
return getCachedNebulaWaitlist(teamSlug, authToken);
118+
}
119+
120+
async function getTeamNebulaWaitListForAuthToken(
121+
teamSlug: string,
122+
authToken: string,
123+
) {
79124
const res = await fetch(
80125
`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist?scope=nebula`,
81126
{
82127
headers: {
83-
Authorization: `Bearer ${token}`,
128+
Authorization: `Bearer ${authToken}`,
84129
},
85130
},
86131
);
87132

88133
if (res.ok) {
89134
return (await res.json()).result as TeamNebulaWaitList;
90135
}
91-
92136
return null;
93137
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { keccak256 } from "thirdweb";
2+
3+
// TODO: technically should be using account.id instead of authToken
4+
// but that's gonna require a lot of code changes, so authToken it is for now
5+
6+
export function teamCacheTag(authToken: string) {
7+
return `team-${shortenAuthToken(authToken)}`;
8+
}
9+
10+
export function projectCacheTag(authToken: string) {
11+
return `project-${shortenAuthToken(authToken)}`;
12+
}
13+
14+
function shortenAuthToken(authToken: string) {
15+
// authToken is too long for the next.js cache tag, we have to shorten it
16+
// shorten auth token by creating a hash of it
17+
18+
const authTokenHash = keccak256(new TextEncoder().encode(authToken));
19+
return authTokenHash;
20+
}

apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,9 @@ export function useCreateApiKey() {
567567
"Content-Type": "application/json",
568568
},
569569
body: JSON.stringify(input),
570+
revalidateCacheTags: {
571+
projectTag: true,
572+
},
570573
});
571574

572575
if (!res.ok) {
@@ -607,6 +610,9 @@ export function useUpdateApiKey() {
607610
"Content-Type": "application/json",
608611
},
609612
body: JSON.stringify(input),
613+
revalidateCacheTags: {
614+
projectTag: true,
615+
},
610616
});
611617

612618
if (!res.ok) {
@@ -647,6 +653,9 @@ export function useRevokeApiKey() {
647653
"Content-Type": "application/json",
648654
},
649655
body: JSON.stringify({}),
656+
revalidateCacheTags: {
657+
projectTag: true,
658+
},
650659
});
651660

652661
if (!res.ok) {

apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { getWalletConnections } from "@/api/analytics";
2-
import { type Project, getProjects } from "@/api/projects";
2+
import { getProjectsForAuthToken } from "@/api/projects";
33
import { getTeamBySlug } from "@/api/team";
44
import { Changelog } from "components/dashboard/Changelog";
55
import { subDays } from "date-fns";
6+
import { unstable_cache } from "next/cache";
67
import { redirect } from "next/navigation";
8+
import { projectCacheTag } from "../../../../@/constants/cacheTags";
9+
import { getAuthToken } from "../../../api/lib/getAuthToken";
10+
import { loginRedirect } from "../../../login/loginRedirect";
711
import {
812
type ProjectWithAnalytics,
913
TeamProjectsPage,
@@ -13,14 +17,33 @@ export default async function Page(props: {
1317
params: Promise<{ team_slug: string }>;
1418
}) {
1519
const params = await props.params;
16-
const team = await getTeamBySlug(params.team_slug);
20+
21+
const [team, authToken] = await Promise.all([
22+
getTeamBySlug(params.team_slug),
23+
getAuthToken(),
24+
]);
25+
26+
if (!authToken) {
27+
loginRedirect(`/team/${params.team_slug}`);
28+
}
1729

1830
if (!team) {
1931
redirect("/team");
2032
}
2133

22-
const projects = await getProjects(params.team_slug);
23-
const projectsWithTotalWallets = await getProjectsWithAnalytics(projects);
34+
const getCachedProjectsWithAnalytics = unstable_cache(
35+
getProjectsWithAnalytics,
36+
["getProjectsWithAnalytics"],
37+
{
38+
revalidate: 3600, // 1 hour,
39+
tags: [projectCacheTag(authToken)],
40+
},
41+
);
42+
43+
const projectsWithTotalWallets = await getCachedProjectsWithAnalytics(
44+
authToken,
45+
params.team_slug,
46+
);
2447

2548
return (
2649
<div className="container flex grow flex-col gap-12 py-8 lg:flex-row">
@@ -39,8 +62,12 @@ export default async function Page(props: {
3962
}
4063

4164
async function getProjectsWithAnalytics(
42-
projects: Project[],
65+
authToken: string,
66+
teamSlug: string,
4367
): Promise<Array<ProjectWithAnalytics>> {
68+
console.log("FETCHING PROJECTS WITH ANALYTICS -------------", teamSlug);
69+
const projects = await getProjectsForAuthToken(authToken, teamSlug);
70+
4471
return Promise.all(
4572
projects.map(async (p) => {
4673
try {

0 commit comments

Comments
 (0)