Skip to content

Commit

Permalink
Experimental: connections update
Browse files Browse the repository at this point in the history
  • Loading branch information
bloodyowl committed Apr 6, 2024
1 parent 5048ddf commit 52e9ae1
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 19 deletions.
39 changes: 39 additions & 0 deletions src/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ export class ClientCache {
typeof value.__typename === "string" &&
value.__typename.endsWith("Connection")
) {
value.__connectionCacheKey = cacheKey.description;
value.__connectionCachePath = [
[...writePath, fieldNameWithArguments].map((item) =>
typeof item === "symbol" ? { symbol: item.description } : item,
),
];
value.__connectionArguments = variables;
}

Expand Down Expand Up @@ -207,4 +213,37 @@ export class ClientCache {
writePath.push(pathCopy.pop() as PropertyKey);
}
}

update<A>(
cacheKey: symbol,
path: (symbol | string)[],
updater: (value: A) => Partial<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])),
Option.fromNullable(cachedAncestor.value),
);

value.map((item) => {
const deepUpdate = path.reduce<unknown>(
(acc, key) => {
return {
[key]: acc,
};
},
updater(item as A),
);

this.set(
cacheKey,
mergeCacheEntries(cachedAncestor, {
requestedKeys: new Set(),
value: deepUpdate,
}),
);
});
});
}
}
89 changes: 88 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
inlineFragments,
} from "./graphql/ast";
import { print } from "./graphql/print";
import { TypedDocumentNode } from "./types";
import { Connection, Edge, TypedDocumentNode } from "./types";

export type RequestConfig = {
url: string;
Expand Down Expand Up @@ -217,4 +217,91 @@ export class Client {
) {
return this.request(document, variables, requestOptions);
}

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 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.subscribers.forEach((func) => {
func();
});
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from "./react/useDeferredQuery";
export * from "./react/useMutation";
export * from "./react/usePagination";
export * from "./react/useQuery";
export { Connection, Edge } from "./types";
49 changes: 31 additions & 18 deletions src/react/usePagination.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,8 @@
import { useRef } from "react";
import { match } from "ts-pattern";
import { Connection } from "../types";
import { isRecord } from "../utils";

export type Edge<T> = {
cursor?: string | null;
node?: T | null | undefined;
};

export type Connection<T> =
| {
edges?: (Edge<T> | null | undefined)[] | null | undefined;
pageInfo: {
hasPreviousPage?: boolean | null | undefined;
hasNextPage?: boolean | null | undefined;
endCursor?: string | null | undefined;
startCursor?: string | null | undefined;
};
}
| null
| undefined;

type mode = "before" | "after";

const mergeConnection = <A, T extends Connection<A>>(
Expand Down Expand Up @@ -53,6 +36,36 @@ const mergeConnection = <A, T extends Connection<A>>(

return {
...next,
// TODO: keep track on connection arguments in order here
/**
* mutate().tapOk(data => {
* client.updateConnection(connection, {remove: [{node: {id}}]})
* client.updateConnection(connection, {prepend: [data.edge]})
* client.updateConnection(connection, {append: [data.edge]})
* })
*/
__connectionArguments: match(mode)
.with("before", () => [
...("__connectionCachePath" in next &&
Array.isArray(next.__connectionCachePath)
? next.__connectionCachePath
: []),
...("__connectionCachePath" in previous &&
Array.isArray(previous.__connectionCachePath)
? previous.__connectionCachePath
: []),
])
.with("after", () => [
...("__connectionCachePath" in previous &&
Array.isArray(previous.__connectionCachePath)
? previous.__connectionCachePath
: []),
...("__connectionCachePath" in next &&
Array.isArray(next.__connectionCachePath)
? next.__connectionCachePath
: []),
])
.exhaustive(),
edges: match(mode)
.with("before", () => [...(next.edges ?? []), ...(previous.edges ?? [])])
.with("after", () => [...(previous.edges ?? []), ...(next.edges ?? [])])
Expand Down
18 changes: 18 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,21 @@ export interface TypedDocumentNode<
},
> extends DocumentNode,
DocumentTypeDecoration<TResult, TVariables> {}

export type Edge<T> = {
cursor?: string | null;
node?: T | null | undefined;
};

export type Connection<T> =
| {
edges?: (Edge<T> | null | undefined)[] | null | undefined;
pageInfo: {
hasPreviousPage?: boolean | null | undefined;
hasNextPage?: boolean | null | undefined;
endCursor?: string | null | undefined;
startCursor?: string | null | undefined;
};
}
| null
| undefined;
106 changes: 106 additions & 0 deletions test/__snapshots__/cache.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ Map {
"__connectionArguments": {
"first": "2",
},
"__connectionCacheKey": "Query",
"__connectionCachePath": [
[
{
"symbol": "accountMemberships({"first":"2"})",
},
],
],
Symbol(__typename): "AccountMembershipConnection",
Symbol(edges): [
{
Expand Down Expand Up @@ -140,6 +148,14 @@ Map {
"__connectionArguments": {
"first": "2",
},
"__connectionCacheKey": "Query",
"__connectionCachePath": [
[
{
"symbol": "accountMemberships({"first":"2"})",
},
],
],
Symbol(__typename): "AccountMembershipConnection",
Symbol(edges): [
{
Expand Down Expand Up @@ -289,6 +305,14 @@ Map {
"__connectionArguments": {
"first": "2",
},
"__connectionCacheKey": "Query",
"__connectionCachePath": [
[
{
"symbol": "accountMemberships({"first":"2"})",
},
],
],
Symbol(__typename): "AccountMembershipConnection",
Symbol(edges): [
{
Expand Down Expand Up @@ -590,3 +614,85 @@ Map {
},
}
`;
exports[`Write & read in cache 7`] = `
t {
"tag": "Some",
"value": t {
"tag": "Ok",
"value": {
"__typename": "Query",
"accountMembership": {
"__typename": "AccountMembership",
"id": "account-membership-1",
"user": {
"__typename": "User",
"firstName": "Matthias",
"id": "user-1",
"identificationLevels": null,
"lastName": "Le Brun",
},
},
"accountMemberships": {
"__connectionArguments": {
"first": "2",
},
"__connectionCacheKey": "Query",
"__connectionCachePath": [
[
{
"symbol": "accountMemberships({"first":"2"})",
},
],
],
"__typename": "AccountMembershipConnection",
"edges": [
{
"__typename": "AccountMembershipEdge",
"node": {
"__typename": "AccountMembership",
"account": {
"__typename": "Account",
"name": "Second",
},
"id": "account-membership-2",
"membershipUser": {
"__typename": "User",
"id": "user-2",
"lastName": "Last",
},
},
},
{
"__typename": "AccountMembershipEdge",
"node": {
"__typename": "AccountMembership",
"account": {
"__typename": "Account",
"name": "First",
},
"id": "account-membership-3",
"membershipUser": {
"__typename": "User",
"id": "user-3",
"lastName": "Le Brun",
},
},
},
],
},
"supportingDocumentCollection": {
"__typename": "SupportingDocumentCollection",
"id": "supporting-document-collection-1",
"supportingDocuments": [
{
"__typename": "SupportingDocument",
"createdAt": "2024-03-14T12:06:10.857Z",
"id": "supporting-document-1",
},
],
},
},
},
}
`;
Loading

0 comments on commit 52e9ae1

Please sign in to comment.