feat: add persistent cache and optimistic updates

This commit is contained in:
Andrew Bastin
2021-10-05 02:09:39 +05:30
committed by liyasthomas
parent e6707c1e4a
commit d5123c793a
5 changed files with 138 additions and 7 deletions

View File

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

View File

@@ -13,17 +13,19 @@ import {
OperationResult, OperationResult,
dedupExchange, dedupExchange,
OperationContext, OperationContext,
cacheExchange,
fetchExchange, fetchExchange,
makeOperation, makeOperation,
} from "@urql/core" } from "@urql/core"
import { authExchange } from "@urql/exchange-auth" 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 { devtoolsExchange } from "@urql/devtools"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { pipe, constVoid } from "fp-ts/function" import { pipe, constVoid } from "fp-ts/function"
import { subscribe } from "wonka" import { subscribe } from "wonka"
import clone from "lodash/clone" import clone from "lodash/clone"
import gql from "graphql-tag"
import { import {
getAuthIDToken, getAuthIDToken,
probableUser$, probableUser$,
@@ -35,12 +37,105 @@ const BACKEND_GQL_URL =
? "https://api.hoppscotch.io/graphql" ? "https://api.hoppscotch.io/graphql"
: "https://api.hoppscotch.io/graphql" : "https://api.hoppscotch.io/graphql"
const storage = makeDefaultStorage({
idbName: "hoppcache-v1",
maxAge: 7,
})
const client = createClient({ const client = createClient({
url: BACKEND_GQL_URL, url: BACKEND_GQL_URL,
exchanges: [ exchanges: [
devtoolsExchange, devtoolsExchange,
dedupExchange, 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({ authExchange({
addAuthToOperation({ authState, operation }) { addAuthToOperation({ authState, operation }) {
if (!authState || !authState.authToken) { if (!authState || !authState.authToken) {
@@ -145,7 +240,9 @@ export function useGQLQuery<
let subscription: { unsubscribe(): void } | null = null let subscription: { unsubscribe(): void } | null = null
onMounted(() => { 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>) => const processResult = (result: OperationResult<any, QueryVariables>) =>
pipe( pipe(
@@ -218,18 +315,21 @@ export const runMutation = <
TE.tryCatch( TE.tryCatch(
() => () =>
client client
.mutation<MutationReturnType>(mutation, variables, additionalConfig) .mutation<MutationReturnType>(mutation, variables, {
requestPolicy: "cache-and-network",
...additionalConfig,
})
.toPromise(), .toPromise(),
() => constVoid() as never // The mutation function can never fail, so this will never be called ;) () => constVoid() as never // The mutation function can never fail, so this will never be called ;)
), ),
TE.chainEitherK((result) => TE.chainEitherK((result) =>
pipe( pipe(
result.data as MutationReturnType, // If we have the result, then okay result.data as MutationReturnType,
E.fromNullable( E.fromNullable(
// Result is null // Result is null
pipe( pipe(
result.error?.networkError, // Check for network error result.error?.networkError,
E.fromNullable(result.error?.name), // If it is null, then it is a GQL error E.fromNullable(result.error?.name),
E.match( E.match(
// The left case (network error was null) // The left case (network error was null)
(gqlErr) => (gqlErr) =>

View File

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

View File

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

15
pnpm-lock.yaml generated
View File

@@ -85,6 +85,7 @@ importers:
'@urql/core': ^2.3.3 '@urql/core': ^2.3.3
'@urql/devtools': ^2.0.3 '@urql/devtools': ^2.0.3
'@urql/exchange-auth': ^0.1.6 '@urql/exchange-auth': ^0.1.6
'@urql/exchange-graphcache': ^4.3.5
'@vue/runtime-dom': ^3.2.19 '@vue/runtime-dom': ^3.2.19
'@vue/test-utils': ^1.2.2 '@vue/test-utils': ^1.2.2
>>>>>>> ecdc7919 (fix: queries not waiting for authentication) >>>>>>> ecdc7919 (fix: queries not waiting for authentication)
@@ -170,7 +171,11 @@ importers:
'@nuxtjs/toast': 3.3.1 '@nuxtjs/toast': 3.3.1
'@urql/core': 2.3.3_graphql@15.6.0 '@urql/core': 2.3.3_graphql@15.6.0
'@urql/exchange-auth': 0.1.6_graphql@15.6.0 '@urql/exchange-auth': 0.1.6_graphql@15.6.0
<<<<<<< HEAD
>>>>>>> ecdc7919 (fix: queries not waiting for authentication) >>>>>>> ecdc7919 (fix: queries not waiting for authentication)
=======
'@urql/exchange-graphcache': 4.3.5_graphql@15.6.0
>>>>>>> 96cf7746 (feat: add persistent cache and optimistic updates)
acorn: 8.5.0 acorn: 8.5.0
acorn-walk: 8.2.0 acorn-walk: 8.2.0
axios: 0.22.0 axios: 0.22.0
@@ -5995,6 +6000,16 @@ packages:
wonka: 4.0.15 wonka: 4.0.15
dev: false dev: false
/@urql/exchange-graphcache/4.3.5_graphql@15.6.0:
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': 2.3.3_graphql@15.6.0
graphql: 15.6.0
wonka: 4.0.15
dev: false
/@vue/babel-helper-vue-jsx-merge-props/1.2.1: /@vue/babel-helper-vue-jsx-merge-props/1.2.1:
resolution: resolution:
{ {