瀏覽代碼

feat: add persistent cache and optimistic updates

Andrew Bastin 4 年之前
父節點
當前提交
d5123c793a

+ 2 - 0
packages/hoppscotch-app/components/teams/index.vue

@@ -87,6 +87,7 @@ const myTeams = useGQLQuery<
       myRole: TeamMemberRole
       ownersCount: number
       members: Array<{
+        membershipID: string
         user: {
           photoURL: string | null
           displayName: string
@@ -107,6 +108,7 @@ const myTeams = useGQLQuery<
         myRole
         ownersCount
         members {
+          membershipID
           user {
             photoURL
             displayName

+ 107 - 7
packages/hoppscotch-app/helpers/backend/GQLClient.ts

@@ -13,17 +13,19 @@ import {
   OperationResult,
   dedupExchange,
   OperationContext,
-  cacheExchange,
   fetchExchange,
   makeOperation,
 } from "@urql/core"
 import { authExchange } from "@urql/exchange-auth"
+import { offlineExchange } from "@urql/exchange-graphcache"
+import { makeDefaultStorage } from "@urql/exchange-graphcache/default-storage"
 import { devtoolsExchange } from "@urql/devtools"
 import * as E from "fp-ts/Either"
 import * as TE from "fp-ts/TaskEither"
 import { pipe, constVoid } from "fp-ts/function"
 import { subscribe } from "wonka"
 import clone from "lodash/clone"
+import gql from "graphql-tag"
 import {
   getAuthIDToken,
   probableUser$,
@@ -35,12 +37,105 @@ const BACKEND_GQL_URL =
     ? "https://api.hoppscotch.io/graphql"
     : "https://api.hoppscotch.io/graphql"
 
+const storage = makeDefaultStorage({
+  idbName: "hoppcache-v1",
+  maxAge: 7,
+})
+
 const client = createClient({
   url: BACKEND_GQL_URL,
   exchanges: [
     devtoolsExchange,
     dedupExchange,
-    cacheExchange,
+    // TODO: Extract this outttttttt
+    offlineExchange({
+      keys: {
+        User: (data) => (data as any).uid,
+        TeamMember: (data) => (data as any).membershipID,
+        Team: (data) => data.id as any,
+      },
+      optimistic: {
+        deleteTeam: () => true,
+        leaveTeam: () => true,
+      },
+      updates: {
+        Mutation: {
+          deleteTeam: (_r, { teamID }, cache, _info) => {
+            cache.updateQuery(
+              {
+                query: gql`
+                  query {
+                    myTeams {
+                      id
+                    }
+                  }
+                `,
+              },
+              (data: any) => {
+                console.log(data)
+                data.myTeams = (data as any).myTeams.filter(
+                  (x: any) => x.id !== teamID
+                )
+
+                return data
+              }
+            )
+
+            cache.invalidate({
+              __typename: "Team",
+              id: teamID as any,
+            })
+          },
+          leaveTeam: (_r, { teamID }, cache, _info) => {
+            cache.updateQuery(
+              {
+                query: gql`
+                  query {
+                    myTeams {
+                      id
+                    }
+                  }
+                `,
+              },
+              (data: any) => {
+                console.log(data)
+                data.myTeams = (data as any).myTeams.filter(
+                  (x: any) => x.id !== teamID
+                )
+
+                return data
+              }
+            )
+
+            cache.invalidate({
+              __typename: "Team",
+              id: teamID as any,
+            })
+          },
+          createTeam: (result, _args, cache, _info) => {
+            cache.updateQuery(
+              {
+                query: gql`
+                  {
+                    myTeams {
+                      id
+                    }
+                  }
+                `,
+              },
+              (data: any) => {
+                console.log(result)
+                console.log(data)
+
+                data.myTeams.push(result.createTeam)
+                return data
+              }
+            )
+          },
+        },
+      },
+      storage,
+    }),
     authExchange({
       addAuthToOperation({ authState, operation }) {
         if (!authState || !authState.authToken) {
@@ -145,7 +240,9 @@ export function useGQLQuery<
   let subscription: { unsubscribe(): void } | null = null
 
   onMounted(() => {
-    const gqlQuery = client.query<any, QueryVariables>(query, variables)
+    const gqlQuery = client.query<any, QueryVariables>(query, variables, {
+      requestPolicy: "cache-and-network",
+    })
 
     const processResult = (result: OperationResult<any, QueryVariables>) =>
       pipe(
@@ -218,18 +315,21 @@ export const runMutation = <
     TE.tryCatch(
       () =>
         client
-          .mutation<MutationReturnType>(mutation, variables, additionalConfig)
+          .mutation<MutationReturnType>(mutation, variables, {
+            requestPolicy: "cache-and-network",
+            ...additionalConfig,
+          })
           .toPromise(),
       () => constVoid() as never // The mutation function can never fail, so this will never be called ;)
     ),
     TE.chainEitherK((result) =>
       pipe(
-        result.data as MutationReturnType, // If we have the result, then okay
+        result.data as MutationReturnType,
         E.fromNullable(
           // Result is null
           pipe(
-            result.error?.networkError, // Check for network error
-            E.fromNullable(result.error?.name), // If it is null, then it is a GQL error
+            result.error?.networkError,
+            E.fromNullable(result.error?.name),
             E.match(
               // The left case (network error was null)
               (gqlErr) =>

+ 13 - 0
packages/hoppscotch-app/helpers/backend/mutations/Team.ts

@@ -3,6 +3,7 @@ import { pipe } from "fp-ts/function"
 import * as TE from "fp-ts/TaskEither"
 import { runMutation } from "../GQLClient"
 import { TeamName } from "../types/TeamName"
+import { TeamMemberRole } from "../types/TeamMemberRole"
 
 type DeleteTeamErrors =
   | "team/not_required_role"
@@ -24,6 +25,11 @@ export const createTeam = (name: TeamName) =>
         createTeam: {
           id: string
           name: string
+          members: Array<{ membershipID: string }>
+          myRole: TeamMemberRole
+          ownersCount: number
+          editorsCount: number
+          viewersCount: number
         }
       },
       CreateTeamErrors
@@ -33,6 +39,13 @@ export const createTeam = (name: TeamName) =>
           createTeam(name: $name) {
             id
             name
+            members {
+              membershipID
+            }
+            myRole
+            ownersCount
+            editorsCount
+            viewersCount
           }
         }
       `,

+ 1 - 0
packages/hoppscotch-app/package.json

@@ -37,6 +37,7 @@
     "@nuxtjs/toast": "^3.3.1",
     "@urql/core": "^2.3.3",
     "@urql/exchange-auth": "^0.1.6",
+    "@urql/exchange-graphcache": "^4.3.5",
     "acorn": "^8.5.0",
     "acorn-walk": "^8.2.0",
     "axios": "^0.22.0",

+ 15 - 0
pnpm-lock.yaml

@@ -85,6 +85,7 @@ importers:
       '@urql/core': ^2.3.3
       '@urql/devtools': ^2.0.3
       '@urql/exchange-auth': ^0.1.6
+      '@urql/exchange-graphcache': ^4.3.5
       '@vue/runtime-dom': ^3.2.19
       '@vue/test-utils': ^1.2.2
 >>>>>>> ecdc7919 (fix: queries not waiting for authentication)
@@ -170,7 +171,11 @@ importers:
       '@nuxtjs/toast': 3.3.1
       '@urql/core': [email protected]
       '@urql/exchange-auth': [email protected]
+<<<<<<< HEAD
 >>>>>>> ecdc7919 (fix: queries not waiting for authentication)
+=======
+      '@urql/exchange-graphcache': [email protected]
+>>>>>>> 96cf7746 (feat: add persistent cache and optimistic updates)
       acorn: 8.5.0
       acorn-walk: 8.2.0
       axios: 0.22.0
@@ -5995,6 +6000,16 @@ packages:
       wonka: 4.0.15
     dev: false
 
+  /@urql/exchange-graphcache/[email protected]:
+    resolution: {integrity: sha512-q5/CzNtSxd5fW/YZ94KABmtQ34XliyS+KKKhyJ+/y66D7mYrN/ZEiuKTlnB7FTt9GLZ0yRtgIfXjwoGicB/Tlw==}
+    peerDependencies:
+      graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0
+    dependencies:
+      '@urql/core': [email protected]
+      graphql: 15.6.0
+      wonka: 4.0.15
+    dev: false
+
   /@vue/babel-helper-vue-jsx-merge-props/1.2.1:
     resolution:
       {