Skip to content

Commit

Permalink
Merge pull request #6 from swan-io/update-connections
Browse files Browse the repository at this point in the history
Experimental: connections update
  • Loading branch information
bloodyowl authored Apr 6, 2024
2 parents 5048ddf + a0d4e1b commit aeb838d
Show file tree
Hide file tree
Showing 10 changed files with 573 additions and 26 deletions.
28 changes: 28 additions & 0 deletions docs/docs/use-mutation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ sidebar_label: useMutation
### Params

- `mutation`: your mutation document node
- `config`:
- `connectionUpdates`: configuration to prepend/append/remove edges from connections on mutation

### Returns

Expand Down Expand Up @@ -74,3 +76,29 @@ const UserPage = ({ userId }: Props) => {
);
};
```

## Handling connections

```ts
useMutation(BlockUser, {
connectionUpdates: [
({ data, append }) =>
Option.fromNullable(data.blockUser).map(({ user }) =>
append(blockedUsers, [user]),
),
({ data, prepend }) =>
Option.fromNullable(data.blockUser).map(({ user }) =>
prepend(lastBlockedUsers, [user]),
),
],
});

useMutation(Unfriend, {
connectionUpdates: [
({ variables, remove }) =>
Option.fromNullable(data.unfriend).map(() =>
remove(friends, [variables.id]),
),
],
});
```
143 changes: 142 additions & 1 deletion 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 @@ -176,6 +177,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 +214,138 @@ export class ClientCache {
writePath.push(pathCopy.pop() as PropertyKey);
}
}

unsafe__update<A>(
cacheKey: symbol,
path: (symbol | string)[],
updater: (value: A) => A,
) {
this.get(cacheKey).map((cachedAncestor) => {
const value = path.reduce<Option<unknown>>(
(acc, key) =>
acc.flatMap((acc) =>
Option.fromNullable(isRecord(acc) ? acc[key] : null),
),
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,
}),
);
});
});
}

updateConnection<A>(
connection: Connection<A>,
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.unsafe__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) =>
// we can omit the requested fields here because the Connection<A> contrains the fields
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.unsafe__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) =>
// we can omit the requested fields here because the Connection<A> contrains the fields
this.getFromCacheWithoutKey(key).map(() => ({
[typenameSymbol]: __typename,
[nodeSymbol]: key,
})),
),
),
],
};
});
}
})
.with({ remove: P.select(P.array()) }, (nodeIds) => {
cachePath.forEach((path) => {
this.unsafe__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(() => {});
}
}
67 changes: 62 additions & 5 deletions 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 @@ -71,7 +71,50 @@ const defaultMakeRequest: MakeRequest = ({
);
};

type RequestOptions = { optimize?: boolean };
type ConnectionUpdate<Node> = [
Connection<Node>,
{ prepend: Edge<Node>[] } | { append: Edge<Node>[] } | { remove: string[] },
];

const prepend = <A>(
connection: Connection<A>,
edges: Edge<A>[],
): ConnectionUpdate<A> => {
return [connection, { prepend: edges }];
};

const append = <A>(
connection: Connection<A>,
edges: Edge<A>[],
): ConnectionUpdate<A> => {
return [connection, { append: edges }];
};

const remove = <A>(
connection: Connection<A>,
ids: string[],
): ConnectionUpdate<A> => {
return [connection, { remove: ids }];
};

export type GetConnectionUpdate<Data, Variables> = (config: {
data: Data;
variables: Variables;
prepend: <A>(
connection: Connection<A>,
edges: Edge<A>[],
) => ConnectionUpdate<A>;
append: <A>(
connection: Connection<A>,
edges: Edge<A>[],
) => ConnectionUpdate<A>;
remove: <A>(connection: Connection<A>, ids: string[]) => ConnectionUpdate<A>;
}) => Option<ConnectionUpdate<unknown>>;

type RequestOptions<Data, Variables> = {
optimize?: boolean;
connectionUpdates?: GetConnectionUpdate<Data, Variables>[] | undefined;
};

export class Client {
url: string;
Expand Down Expand Up @@ -122,7 +165,10 @@ export class Client {
request<Data, Variables>(
document: TypedDocumentNode<Data, Variables>,
variables: Variables,
{ optimize = false }: RequestOptions = {},
{
optimize = false,
connectionUpdates,
}: RequestOptions<Data, Variables> = {},
): Future<Result<Data, ClientError>> {
const transformedDocument = this.getTransformedDocument(document);
const transformedDocumentsForRequest =
Expand Down Expand Up @@ -170,6 +216,17 @@ export class Client {
variablesAsRecord,
);
})
.tapOk((data) => {
if (connectionUpdates !== undefined) {
connectionUpdates.forEach((getUpdate) => {
getUpdate({ data, variables, prepend, append, remove }).map(
([connection, update]) => {
this.cache.updateConnection(connection, update);
},
);
});
}
})
.tap((result) => {
this.cache.setOperationInCache(
transformedDocument,
Expand Down Expand Up @@ -205,15 +262,15 @@ export class Client {
query<Data, Variables>(
document: TypedDocumentNode<Data, Variables>,
variables: Variables,
requestOptions?: RequestOptions,
requestOptions?: RequestOptions<Data, Variables>,
) {
return this.request(document, variables, requestOptions);
}

commitMutation<Data, Variables>(
document: TypedDocumentNode<Data, Variables>,
variables: Variables,
requestOptions?: RequestOptions,
requestOptions?: RequestOptions<Data, Variables>,
) {
return this.request(document, variables, requestOptions);
}
Expand Down
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";
15 changes: 13 additions & 2 deletions src/react/useMutation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AsyncData, Future, Result } from "@swan-io/boxed";
import { useCallback, useContext, useState } from "react";
import { useCallback, useContext, useRef, useState } from "react";
import { GetConnectionUpdate } from "../client";
import { ClientError } from "../errors";
import { TypedDocumentNode } from "../types";
import { ClientContext } from "./ClientContext";
Expand All @@ -9,11 +10,19 @@ export type Mutation<Data, Variables> = readonly [
AsyncData<Result<Data, ClientError>>,
];

export type MutationConfig<Data, Variables> = {
connectionUpdates?: GetConnectionUpdate<Data, Variables>[] | undefined;
};

export const useMutation = <Data, Variables>(
mutation: TypedDocumentNode<Data, Variables>,
config: MutationConfig<Data, Variables> = {},
): Mutation<Data, Variables> => {
const client = useContext(ClientContext);

const connectionUpdatesRef = useRef(config?.connectionUpdates);
connectionUpdatesRef.current = config?.connectionUpdates;

const [stableMutation] =
useState<TypedDocumentNode<Data, Variables>>(mutation);

Expand All @@ -25,7 +34,9 @@ export const useMutation = <Data, Variables>(
(variables: Variables) => {
setData(AsyncData.Loading());
return client
.commitMutation(stableMutation, variables)
.commitMutation(stableMutation, variables, {
connectionUpdates: connectionUpdatesRef.current,
})
.tap((result) => setData(AsyncData.Done(result)));
},
[client, stableMutation],
Expand Down
Loading

0 comments on commit aeb838d

Please sign in to comment.