Skip to content

Commit

Permalink
Merge pull request #9 from swan-io/keep-paginated-data-in-sync
Browse files Browse the repository at this point in the history
Keep paginated data up to date with the cache
  • Loading branch information
bloodyowl authored Jul 22, 2024
2 parents c0353ef + eb4132e commit a08fb3e
Show file tree
Hide file tree
Showing 4 changed files with 2,167 additions and 10 deletions.
10 changes: 10 additions & 0 deletions src/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,19 +165,25 @@ export class ClientCache {
fieldNameWithArguments,
value,
path,
jsonPath,
ancestors,
variables,
queryVariables,
rootTypename,
selectedKeys,
documentNode,
}: {
originalFieldName: string;
fieldNameWithArguments: symbol | string;
value: unknown;
path: PropertyKey[];
jsonPath: PropertyKey[];
ancestors: unknown[];
variables: Record<string, unknown>;
queryVariables: Record<string, unknown>;
rootTypename: string;
selectedKeys: Set<symbol>;
documentNode: DocumentNode;
}) {
const ancestorsCopy = ancestors.concat();
const pathCopy = path.concat();
Expand Down Expand Up @@ -207,6 +213,10 @@ export class ClientCache {
),
];
value.__connectionArguments = variables;
// used to retrieve up to date data from pagination hook
value.__connectionQueryArguments = queryVariables;
value.__connectionDocumentNode = documentNode;
value.__connectionJsonPath = [...jsonPath, originalFieldName];
}
value[REQUESTED_KEYS] = selectedKeys;
}
Expand Down
28 changes: 26 additions & 2 deletions src/cache/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const writeOperationToCache = (
selections: SelectionSetNode,
data: unknown[],
path: PropertyKey[] = [],
jsonPath: PropertyKey[] = [],
rootTypename: string,
) => {
selections.selections.forEach((selection) => {
Expand All @@ -47,10 +48,13 @@ export const writeOperationToCache = (
fieldNameWithArguments,
value: fieldValue,
path,
jsonPath,
ancestors: data,
variables: fieldArguments,
queryVariables: variables,
rootTypename,
selectedKeys,
documentNode: document,
});

const nextValue = Array(fieldValue.length);
Expand All @@ -60,10 +64,13 @@ export const writeOperationToCache = (
fieldNameWithArguments,
value: nextValue,
path: [...path, fieldNameWithArguments],
jsonPath: [...jsonPath, originalFieldName],
ancestors: [...data, fieldValue],
variables: fieldArguments,
queryVariables: variables,
rootTypename,
selectedKeys,
documentNode: document,
});

fieldValue.forEach((item: unknown, index: number) => {
Expand All @@ -74,17 +81,21 @@ export const writeOperationToCache = (
fieldNameWithArguments: index.toString(),
value,
path: [...path, fieldNameWithArguments],
jsonPath: [...jsonPath, originalFieldName],
ancestors: [...data, fieldValue],
variables: fieldArguments,
queryVariables: variables,
rootTypename,
selectedKeys,
documentNode: document,
});

if (isRecord(item)) {
traverse(
fieldNode.selectionSet!,
[...data, fieldValue, item],
[...path, fieldNameWithArguments, index.toString()],
[...jsonPath, originalFieldName, index.toString()],
rootTypename,
);
}
Expand All @@ -97,17 +108,21 @@ export const writeOperationToCache = (
fieldNameWithArguments,
value,
path,
jsonPath,
ancestors: data,
variables: fieldArguments,
queryVariables: variables,
rootTypename,
selectedKeys,
documentNode: document,
});

if (isRecord(fieldValue) && fieldNode.selectionSet != undefined) {
traverse(
fieldNode.selectionSet,
[...data, fieldValue],
[...path, fieldNameWithArguments],
[...jsonPath, originalFieldName],
rootTypename,
);
}
Expand All @@ -119,16 +134,25 @@ export const writeOperationToCache = (
fieldNameWithArguments,
value: fieldValue,
path,
jsonPath,
ancestors: data,
variables: fieldArguments,
queryVariables: variables,
rootTypename,
selectedKeys,
documentNode: document,
});
}
}
})
.with({ kind: Kind.INLINE_FRAGMENT }, (inlineFragmentNode) => {
traverse(inlineFragmentNode.selectionSet, data, path, rootTypename);
traverse(
inlineFragmentNode.selectionSet,
data,
path,
jsonPath,
rootTypename,
);
})
.with({ kind: Kind.FRAGMENT_SPREAD }, () => {
// ignore, those are stripped
Expand All @@ -155,7 +179,7 @@ export const writeOperationToCache = (
: response,
getSelectedKeys(definition, variables),
);
traverse(definition.selectionSet, [response], [], rootTypename);
traverse(definition.selectionSet, [response], [], [], rootTypename);
}
});

Expand Down
92 changes: 84 additions & 8 deletions src/react/usePagination.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useRef } from "react";
import { DocumentNode } from "@0no-co/graphql.web";
import { Option, Result } from "@swan-io/boxed";
import { useCallback, useContext, useRef, useSyncExternalStore } from "react";
import { match } from "ts-pattern";
import { Connection } from "../types";
import { isRecord } from "../utils";
import { deepEqual, isRecord, serializeVariables } from "../utils";
import { ClientContext } from "./ClientContext";

type mode = "before" | "after";

Expand Down Expand Up @@ -81,13 +84,86 @@ const mergeConnection = <A, T extends Connection<A>>(

const createPaginationHook = (direction: mode) => {
return <A, T extends Connection<A>>(connection: T): T => {
const connectionRef = useRef(connection);
connectionRef.current = mergeConnection(
connectionRef.current,
connection,
direction,
const client = useContext(ClientContext);
const connectionArgumentsRef = useRef<[string, Record<string, unknown>][]>(
[],
);
return connectionRef.current;

if (connection != null && "__connectionQueryArguments" in connection) {
const arg = connection.__connectionQueryArguments as Record<
string,
unknown
>;
const serializedArg = serializeVariables(arg);
if (
!connectionArgumentsRef.current.find(
([serialized]) => serializedArg === serialized,
)
) {
connectionArgumentsRef.current = [
...connectionArgumentsRef.current,
[serializedArg, arg],
];
}
}

const jsonPath = useRef(
connection != null && "__connectionJsonPath" in connection
? (connection.__connectionJsonPath as string[])
: [],
);

const documentNode =
connection != null && "__connectionDocumentNode" in connection
? (connection.__connectionDocumentNode as DocumentNode)
: undefined;

const lastReturnedValueRef = useRef<Option<T[]>>(Option.None());

// Get fresh data from cache
const getSnapshot = useCallback(() => {
if (documentNode == null) {
return Option.None();
}
const value = Option.all(
connectionArgumentsRef.current.map(([, args]) =>
client.readFromCache(documentNode, args, {}),
),
)
.map(Result.all)
.flatMap((x) => x.toOption())
.map((queries) =>
queries.map((query) =>
jsonPath.current.reduce(
(acc, key) =>
acc != null && typeof acc === "object" && key in acc
? // @ts-expect-error indexable
acc[key]
: null,
query,
),
),
) as Option<T[]>;
if (!deepEqual(value, lastReturnedValueRef.current)) {
lastReturnedValueRef.current = value;
return value;
} else {
return lastReturnedValueRef.current;
}
}, [client, documentNode]);

const data = useSyncExternalStore(
(func) => client.subscribe(func),
getSnapshot,
) as Option<T[]>;

return data
.map(([first, ...rest]) =>
rest.reduce((acc, item) => {
return mergeConnection(acc, item, direction);
}, first),
)
.getOr(connection) as T;
};
};

Expand Down
Loading

0 comments on commit a08fb3e

Please sign in to comment.