Skip to content

Commit

Permalink
Update approach to be cache first
Browse files Browse the repository at this point in the history
  • Loading branch information
bloodyowl committed Apr 6, 2024
1 parent e1e067d commit f1cae14
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 86 deletions.
108 changes: 104 additions & 4 deletions src/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -217,12 +218,14 @@ export class ClientCache {
update<A>(
cacheKey: symbol,
path: (symbol | string)[],
updater: (value: A) => Partial<A>,
updater: (value: A) => A,
) {
this.get(cacheKey).map((cachedAncestor) => {
const value = path.reduce<Option<unknown>>(
// @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),
);

Expand All @@ -246,4 +249,101 @@ export class ClientCache {
});
});
}

updateConnection<A, T extends Connection<A>>(
connection: T,
config:
| { prepend: Edge<A>[] }
| { append: Edge<A>[] }
| { 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(() => {});
}
}
76 changes: 1 addition & 75 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,81 +225,7 @@ export class Client {
| { append: Edge<A>[] }
| { 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();
});
Expand Down
16 changes: 16 additions & 0 deletions test/__snapshots__/cache.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
86 changes: 79 additions & 7 deletions test/cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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";
import { addTypenames, inlineFragments } from "../src/graphql/ast";
import { print } from "../src/graphql/print";
import {
OnboardingInfo,
addMembership,
appQuery,
appQueryWithExtraArrayInfo,
bindAccountMembershipMutation,
Expand Down Expand Up @@ -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",
Expand All @@ -215,7 +216,7 @@ test("Write & read in cache", () => {
},
);

const read = readOperationFromCache(cache, preparedAppQuery, {
const read = readOperationFromCache(cache3, preparedAppQuery, {
id: "1",
});

Expand All @@ -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",
Expand All @@ -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();
Expand Down
20 changes: 20 additions & 0 deletions test/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!) {
Expand Down

0 comments on commit f1cae14

Please sign in to comment.