From 96cf774652b5ad8cd9610e30077a40aaa12f018a Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Tue, 5 Oct 2021 02:09:39 +0530 Subject: [PATCH] feat: add persistent cache and optimistic updates --- .../hoppscotch-app/components/teams/index.vue | 2 + .../helpers/backend/GQLClient.ts | 114 ++++++++++++++++-- .../helpers/backend/mutations/Team.ts | 13 ++ packages/hoppscotch-app/package.json | 1 + pnpm-lock.yaml | 12 ++ 5 files changed, 135 insertions(+), 7 deletions(-) diff --git a/packages/hoppscotch-app/components/teams/index.vue b/packages/hoppscotch-app/components/teams/index.vue index 6f736033d..c170960f5 100644 --- a/packages/hoppscotch-app/components/teams/index.vue +++ b/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 diff --git a/packages/hoppscotch-app/helpers/backend/GQLClient.ts b/packages/hoppscotch-app/helpers/backend/GQLClient.ts index 9aadfe86e..049fb7de9 100644 --- a/packages/hoppscotch-app/helpers/backend/GQLClient.ts +++ b/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(query, variables) + const gqlQuery = client.query(query, variables, { + requestPolicy: "cache-and-network", + }) const processResult = (result: OperationResult) => pipe( @@ -218,18 +315,21 @@ export const runMutation = < TE.tryCatch( () => client - .mutation(mutation, variables, additionalConfig) + .mutation(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) => diff --git a/packages/hoppscotch-app/helpers/backend/mutations/Team.ts b/packages/hoppscotch-app/helpers/backend/mutations/Team.ts index c761cf47d..8d23a33cf 100644 --- a/packages/hoppscotch-app/helpers/backend/mutations/Team.ts +++ b/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 } } `, diff --git a/packages/hoppscotch-app/package.json b/packages/hoppscotch-app/package.json index dab0e98e4..43b326ffc 100644 --- a/packages/hoppscotch-app/package.json +++ b/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.21.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7758007a0..fd58acb60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,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 acorn: ^8.5.0 @@ -122,6 +123,7 @@ importers: '@nuxtjs/toast': 3.3.1 '@urql/core': 2.3.3_graphql@15.6.0 '@urql/exchange-auth': 0.1.6_graphql@15.6.0 + '@urql/exchange-graphcache': 4.3.5_graphql@15.6.0 acorn: 8.5.0 acorn-walk: 8.2.0 axios: 0.21.4 @@ -4635,6 +4637,16 @@ packages: wonka: 4.0.15 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: resolution: {integrity: sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA==} dev: false