From f1cae149afa83ad4717cde97ac306d2a1a3d4bd7 Mon Sep 17 00:00:00 2001 From: bloodyowl Date: Sat, 6 Apr 2024 15:20:44 +0200 Subject: [PATCH] Update approach to be cache first --- src/cache/cache.ts | 108 +++++++++++++++++++++++++- src/client.ts | 76 +----------------- test/__snapshots__/cache.test.ts.snap | 16 ++++ test/cache.test.ts | 86 ++++++++++++++++++-- test/data.ts | 20 +++++ 5 files changed, 220 insertions(+), 86 deletions(-) diff --git a/src/cache/cache.ts b/src/cache/cache.ts index 20783d3..39dbc6d 100644 --- a/src/cache/cache.ts +++ b/src/cache/cache.ts @@ -3,8 +3,9 @@ import { OperationDefinitionNode, OperationTypeNode, } from "@0no-co/graphql.web"; -import { Option, Result } from "@swan-io/boxed"; +import { Array, Option, Result } from "@swan-io/boxed"; import { P, match } from "ts-pattern"; +import { Connection, Edge } from "../types"; import { DEEP_MERGE_DELETE, containsAll, @@ -217,12 +218,14 @@ export class ClientCache { update( cacheKey: symbol, path: (symbol | string)[], - updater: (value: A) => Partial, + updater: (value: A) => A, ) { this.get(cacheKey).map((cachedAncestor) => { const value = path.reduce>( - // @ts-expect-error fromNullable makes it safe - (acc, key) => acc.flatMap((acc) => Option.fromNullable(acc[key])), + (acc, key) => + acc.flatMap((acc) => + Option.fromNullable(isRecord(acc) ? acc[key] : null), + ), Option.fromNullable(cachedAncestor.value), ); @@ -246,4 +249,101 @@ export class ClientCache { }); }); } + + updateConnection>( + connection: T, + config: + | { prepend: Edge[] } + | { append: Edge[] } + | { remove: string[] }, + ) { + match(connection as unknown) + .with( + { + __connectionCacheKey: P.string, + __connectionCachePath: P.array( + P.array(P.union({ symbol: P.string }, P.string)), + ), + }, + ({ __connectionCacheKey, __connectionCachePath }) => { + const cacheKey = Symbol.for(__connectionCacheKey); + const cachePath = __connectionCachePath.map((path) => + path.map((item) => + typeof item === "string" ? item : Symbol.for(item.symbol), + ), + ); + const typenameSymbol = Symbol.for("__typename"); + const edgesSymbol = Symbol.for("edges"); + const nodeSymbol = Symbol.for("node"); + match(config) + .with({ prepend: P.select(P.nonNullable) }, (edges) => { + const firstPath = cachePath[0]; + if (firstPath != null) { + this.update(cacheKey, firstPath, (value) => { + if (!isRecord(value) || !Array.isArray(value[edgesSymbol])) { + return value; + } + return { + ...value, + [edgesSymbol]: [ + ...Array.filterMap(edges, ({ node, __typename }) => + getCacheKeyFromJson(node).flatMap((key) => + this.getFromCacheWithoutKey(key).map(() => ({ + [typenameSymbol]: __typename, + [nodeSymbol]: key, + })), + ), + ), + ...value[edgesSymbol], + ], + }; + }); + } + }) + .with({ append: P.select(P.nonNullable) }, (edges) => { + const lastPath = cachePath[cachePath.length - 1]; + if (lastPath != null) { + this.update(cacheKey, lastPath, (value) => { + if (!isRecord(value) || !Array.isArray(value[edgesSymbol])) { + return value; + } + return { + ...value, + [edgesSymbol]: [ + ...value[edgesSymbol], + ...Array.filterMap(edges, ({ node, __typename }) => + getCacheKeyFromJson(node).flatMap((key) => + this.getFromCacheWithoutKey(key).map(() => ({ + [typenameSymbol]: __typename, + [nodeSymbol]: key, + })), + ), + ), + ], + }; + }); + } + }) + .with({ remove: P.select(P.array()) }, (nodeIds) => { + cachePath.forEach((path) => { + this.update(cacheKey, path, (value) => { + return isRecord(value) && Array.isArray(value[edgesSymbol]) + ? { + ...value, + [edgesSymbol]: value[edgesSymbol].filter((edge) => { + const node = edge[nodeSymbol] as symbol; + return !nodeIds.some((nodeId) => { + return node.description?.includes(`<${nodeId}>`); + }); + }), + } + : value; + }); + }); + }) + .exhaustive(); + }, + ) + .otherwise(() => {}); + } } diff --git a/src/client.ts b/src/client.ts index 4d1fc8b..41f059d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -225,81 +225,7 @@ export class Client { | { append: Edge[] } | { remove: string[] }, ) { - match(connection as unknown) - .with( - { - __connectionCacheKey: P.string, - __connectionCachePath: P.array( - P.array(P.union({ symbol: P.string }, P.string)), - ), - }, - ({ __connectionCacheKey, __connectionCachePath }) => { - const cacheKey = Symbol.for(__connectionCacheKey); - const cachePath = __connectionCachePath.map((path) => - path.map((item) => - typeof item === "string" ? item : Symbol.for(item.symbol), - ), - ); - const edgesSymbol = Symbol.for("edges"); - const nodeSymbol = Symbol.for("node"); - match(config) - .with({ prepend: P.select(P.array()) }, (edges) => { - const firstPath = cachePath[0]; - if (firstPath != null) { - this.cache.update(cacheKey, firstPath, (value) => - // @ts-expect-error safe - value[edgesSymbol] != null - ? { - // @ts-expect-error safe - ...value, - // @ts-expect-error safe - [edgesSymbol]: [...edges, ...value[edgesSymbol]], - } - : value, - ); - } - }) - .with({ append: P.select(P.array()) }, (edges) => { - const lastPath = cachePath[cachePath.length - 1]; - if (lastPath != null) { - this.cache.update(cacheKey, lastPath, (value) => - // @ts-expect-error safe - value[edgesSymbol] != null - ? { - // @ts-expect-error safe - ...value, - // @ts-expect-error safe - [edgesSymbol]: [...value[edgesSymbol], ...edges], - } - : value, - ); - } - }) - .with({ remove: P.select(P.array()) }, (nodeIds) => { - cachePath.forEach((path) => { - this.cache.update(cacheKey, path, (value) => { - // @ts-expect-error safe - return value[edgesSymbol] != null - ? { - // @ts-expect-error safe - ...value, - // @ts-expect-error safe - [edgesSymbol]: value[edgesSymbol].filter((edge) => { - const node = edge[nodeSymbol] as symbol; - return !nodeIds.some((nodeId) => { - return node.description?.includes(`<${nodeId}>`); - }); - }), - } - : value; - }); - }); - }) - .exhaustive(); - }, - ) - .otherwise(() => {}); - + this.cache.updateConnection(connection, config); this.subscribers.forEach((func) => { func(); }); diff --git a/test/__snapshots__/cache.test.ts.snap b/test/__snapshots__/cache.test.ts.snap index a00a123..01fffe2 100644 --- a/test/__snapshots__/cache.test.ts.snap +++ b/test/__snapshots__/cache.test.ts.snap @@ -647,6 +647,22 @@ t { ], "__typename": "AccountMembershipConnection", "edges": [ + { + "__typename": "AccountMembershipEdge", + "node": { + "__typename": "AccountMembership", + "account": { + "__typename": "Account", + "name": "First", + }, + "id": "account-membership-0", + "membershipUser": { + "__typename": "User", + "id": "user-0", + "lastName": "Le Brun", + }, + }, + }, { "__typename": "AccountMembershipEdge", "node": { diff --git a/test/cache.test.ts b/test/cache.test.ts index 6f5fb44..4ab8b59 100644 --- a/test/cache.test.ts +++ b/test/cache.test.ts @@ -1,6 +1,6 @@ import { Option, Result } from "@swan-io/boxed"; import { expect, test } from "vitest"; -import { Client, Connection } from "../src"; +import { Connection } from "../src"; import { ClientCache } from "../src/cache/cache"; import { optimizeQuery, readOperationFromCache } from "../src/cache/read"; import { writeOperationToCache } from "../src/cache/write"; @@ -8,6 +8,7 @@ import { addTypenames, inlineFragments } from "../src/graphql/ast"; import { print } from "../src/graphql/print"; import { OnboardingInfo, + addMembership, appQuery, appQueryWithExtraArrayInfo, bindAccountMembershipMutation, @@ -201,10 +202,10 @@ test("Write & read in cache", () => { }), ).toMatchObject(Option.Some(Result.Ok(onboardingInfoResponse))); - const client = new Client({ url: "/" }); + const cache3 = new ClientCache(); writeOperationToCache( - client.cache, + cache3, preparedAppQuery, getAppQueryResponse({ user2LastName: "Last", @@ -215,7 +216,7 @@ test("Write & read in cache", () => { }, ); - const read = readOperationFromCache(cache, preparedAppQuery, { + const read = readOperationFromCache(cache3, preparedAppQuery, { id: "1", }); @@ -237,10 +238,61 @@ test("Write & read in cache", () => { lastName: string; }; }>; - client.updateConnection(accountMemberships, { + cache3.updateConnection(accountMemberships, { remove: ["account-membership-1"], }); - client.updateConnection(accountMemberships, { + + writeOperationToCache( + cache3, + inlineFragments(addTypenames(addMembership)), + { + __typename: "Mutation", + addMembership: { + __typename: "AddMembership", + membership: { + __typename: "AccountMembership", + id: "account-membership-3", + account: { + __typename: "Account", + name: "First", + }, + membershipUser: { + __typename: "User", + id: "user-3", + lastName: "Le Brun", + }, + }, + }, + }, + {}, + ); + + writeOperationToCache( + cache3, + inlineFragments(addTypenames(addMembership)), + { + __typename: "Mutation", + addMembership: { + __typename: "AddMembership", + membership: { + __typename: "AccountMembership", + id: "account-membership-0", + account: { + __typename: "Account", + name: "First", + }, + membershipUser: { + __typename: "User", + id: "user-0", + lastName: "Le Brun", + }, + }, + }, + }, + {}, + ); + + cache3.updateConnection(accountMemberships, { append: [ { __typename: "AccountMembershipEdge", @@ -260,10 +312,30 @@ test("Write & read in cache", () => { }, ], }); + cache3.updateConnection(accountMemberships, { + prepend: [ + { + __typename: "AccountMembershipEdge", + node: { + __typename: "AccountMembership", + id: "account-membership-0", + account: { + __typename: "Account", + name: "First", + }, + membershipUser: { + __typename: "User", + id: "user-0", + lastName: "Le Brun", + }, + }, + }, + ], + }); } expect( - readOperationFromCache(client.cache, preparedAppQuery, { + readOperationFromCache(cache3, preparedAppQuery, { id: "1", }), ).toMatchSnapshot(); diff --git a/test/data.ts b/test/data.ts index 2f016dc..bbc2ccf 100644 --- a/test/data.ts +++ b/test/data.ts @@ -247,6 +247,26 @@ export const bindAccountMembershipMutation = graphql( [UserInfo], ); +export const addMembership = graphql(` + mutation AddMembership { + addMembership { + membership { + id + createdAt + account { + name + bankDetails + } + membershipUser: user { + id + lastName + firstName + } + } + } + } +`); + export const OnboardingInfo = graphql( ` query GetOnboarding($id: ID!, $language: String!) {