Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEVPROD-12696: Support caching for waterfall pagination #535

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 140 additions & 4 deletions apps/spruce/src/gql/client/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { InMemoryCache } from "@apollo/client";
import {
WaterfallBuild,
WaterfallPagination,
WaterfallVersionFragment,
} from "gql/generated/types";
import { IMAGE_EVENT_LIMIT } from "pages/image/tabs/EventLogTab/useImageEvents";
import { mergeVersions, mergeBuilds } from "./mergeFunctions";
import { readBuilds, readVersions } from "./readFunctions";

export const cache = new InMemoryCache({
typePolicies: {
Expand All @@ -9,14 +16,146 @@ export const cache = new InMemoryCache({
keyArgs: ["$distroId"],
},
projectEvents: {
keyArgs: ["$identifier"],
keyArgs: ["$projectIdentifier"],
},
repoEvents: {
keyArgs: ["$id"],
},
hasVersion: {
keyArgs: ["$patchId"],
},
waterfall: {
keyArgs: ["options", ["projectIdentifier"]],
read(existing, { args, readField, variables }) {
// A read function should always return undefined if existing is
// undefined. Returning undefined signals that the field is
// missing from the cache, which instructs Apollo Client to
// fetch its value from your GraphQL server.
if (!existing) {
return undefined;
}

const minOrder = args?.options?.minOrder ?? 0;
const maxOrder = args?.options?.maxOrder ?? 0;
const limit = variables?.options?.limit ?? 0;

const existingVersions =
readField<WaterfallVersionFragment[]>(
"flattenedVersions",
existing,
) ?? [];

const flattenedVersions = readVersions({
maxOrder,
minOrder,
limit,
versions: existingVersions,
readField,
});

const allVersionIds: string[] = [];
const activeVersionIds: string[] = [];

flattenedVersions.forEach((v) => {
const activated = readField<boolean>("activated", v) ?? false;
const versionId = readField<string>("id", v) ?? "";
if (activated) {
activeVersionIds.push(versionId);
}
allVersionIds.push(versionId);
});

if (activeVersionIds.length < limit) {
console.log("Number of active versions is below limit: ", {
flattenedVersions,
activeVersionIds,
allVersionIds,
});
return undefined;
}

const existingBuilds =
readField<WaterfallBuild[]>("flattenedBuilds", existing) ?? [];

const builds = readBuilds({
versionIds: allVersionIds,
builds: existingBuilds,
readField,
});

const prevOrderNumber =
readField<number>("order", flattenedVersions[0]) ?? 0;
const nextOrderNumber =
readField<number>(
"order",
flattenedVersions[flattenedVersions.length - 1],
) ?? 0;

console.log("Read result: ", {
builds,
flattenedVersions,
prevOrderNumber,
nextOrderNumber,
});

return {
flattenedBuilds: builds,
flattenedVersions,
pagination: {
prevPageOrder: prevOrderNumber,
nextPageOrder: nextOrderNumber,
hasNextPage: nextOrderNumber > 0,
hasPrevPage: prevOrderNumber > 0,
},
};
},
merge(existing, incoming, { readField }) {
const existingVersions =
readField<WaterfallVersionFragment[]>(
"flattenedVersions",
existing,
) ?? [];
const incomingVersions =
readField<WaterfallVersionFragment[]>(
"flattenedVersions",
incoming,
) ?? [];
const versions = mergeVersions({
existingVersions,
incomingVersions,
readField,
});

const existingBuilds =
readField<WaterfallBuild[]>("flattenedBuilds", existing) ?? [];
const incomingBuilds =
readField<WaterfallBuild[]>("flattenedBuilds", incoming) ?? [];
const flattenedBuilds = mergeBuilds({
existingBuilds,
incomingBuilds,
readField,
});

const pagination = readField<WaterfallPagination>(
"pagination",
incoming,
);

// To help verify that this is working, inspect these variables and
// check that they do not keep increasing in length as you page
// backwards (since those results were already seen and merged).
console.log("Merge result: ", {
versions,
flattenedBuilds,
});

return {
flattenedBuilds,
flattenedVersions: versions,
pagination,
};
},
},
},
},
GeneralSubscription: {
Expand Down Expand Up @@ -105,8 +244,5 @@ export const cache = new InMemoryCache({
},
},
},
WaterfallBuildVariant: {
keyFields: ["version", "id"],
},
},
});
125 changes: 125 additions & 0 deletions apps/spruce/src/gql/client/mergeFunctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { ReadFieldFunction } from "@apollo/client/cache/core/types/common";
import {
WaterfallVersionFragment,
WaterfallBuild,
WaterfallBuildVariant,
} from "gql/generated/types";

type MergeBuildsProps = {
existingBuilds: readonly WaterfallBuild[];
incomingBuilds: readonly WaterfallBuild[];
readField: ReadFieldFunction;
};

/**
* `mergeBuilds` is used to merge existing and incoming builds.
* @param opts - object containing arguments to this function
* @param opts.existingBuilds - existing builds in the Apollo cache
* @param opts.incomingBuilds - incoming builds as a result of a GraphQL query
* @param opts.readField - function provided by Apollo to access fields from Reference objects
* @returns an array of builds containing no duplicates and in descending sort by the
* `order` field
*/
const mergeBuilds = ({
existingBuilds,
incomingBuilds,
readField,
}: MergeBuildsProps): WaterfallBuild[] => {
const builds = [...existingBuilds, ...incomingBuilds];
const buildsMap = new Map<string, WaterfallBuild>();

builds.forEach((b) => {
const id = readField<string>("id", b) ?? "";
buildsMap.set(id, b);
});

return Array.from(buildsMap.values()).sort((a, b) => {
const aOrder = readField<number>("order", a) ?? 0;
const bOrder = readField<number>("order", b) ?? 0;
return bOrder - aOrder;
});
};

type MergeBuildVariantsProps = {
existingBuildVariants: readonly WaterfallBuildVariant[];
incomingBuildVariants: readonly WaterfallBuildVariant[];
readField: ReadFieldFunction;
};

/**
* `mergeBuildVariants` is used to merge existing and incoming build variants.
* @param opts - object containing arguments to this function
* @param opts.existingBuildVariants - existing build variants in the Apollo cache
* @param opts.incomingBuildVariants - incoming build variants as a result of a GraphQL query
* @param opts.readField - function provided by Apollo to access fields from Reference objects
* @returns an array of build variants containing no duplicates and in descending sort by the
* `order` field
*/
const mergeBuildVariants = ({
existingBuildVariants,
incomingBuildVariants,
readField,
}: MergeBuildVariantsProps): WaterfallBuildVariant[] => {
const buildVariants = [...existingBuildVariants, ...incomingBuildVariants];

const buildVariantsMap = new Map<string, WaterfallBuildVariant>();

buildVariants.forEach((bv) => {
const buildVariantId = readField<string>("id", bv) ?? "";

const mapItem = buildVariantsMap.get(buildVariantId);

if (mapItem) {
const existingBuilds =
readField<WaterfallBuild[]>("builds", mapItem) ?? [];
const incomingBuilds = readField<WaterfallBuild[]>("builds", bv) ?? [];
const mergedBuilds = mergeBuilds({
existingBuilds,
incomingBuilds,
readField,
});
buildVariantsMap.set(buildVariantId, { ...bv, builds: mergedBuilds });
} else {
buildVariantsMap.set(buildVariantId, bv);
}
});
return Array.from(buildVariantsMap.values());
};

type MergeVersionsProps = {
existingVersions: readonly WaterfallVersionFragment[];
incomingVersions: readonly WaterfallVersionFragment[];
readField: ReadFieldFunction;
};

/**
* `mergeVersions` is used to merge existing and incoming versions.
* @param opts - object containing arguments to this function
* @param opts.existingVersions - existing versions in the Apollo cache
* @param opts.incomingVersions - incoming versions as a result of a GraphQL query
* @param opts.readField - function provided by Apollo to access fields from Reference objects
* @returns an array of versions (both active or inactive) that contains no duplicates and in
* descending sort by the `order` field
*/
const mergeVersions = ({
existingVersions,
incomingVersions,
readField,
}: MergeVersionsProps): WaterfallVersionFragment[] => {
const versions = [...existingVersions, ...incomingVersions];

// Use a map to enforce that there are no duplicates.
const versionsMap = new Map<number, WaterfallVersionFragment>();
versions.forEach((v) => {
const order = readField<number>("order", v) ?? 0;
versionsMap.set(order, v);
});

return Array.from(versionsMap.values()).sort((a, b) => {
const aOrder = readField<number>("order", a) ?? 0;
const bOrder = readField<number>("order", b) ?? 0;
return bOrder - aOrder;
});
};

export { mergeBuildVariants, mergeVersions, mergeBuilds };
Loading