Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-01-19 06:07:38 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-01-19 06:07:38 +0300
commit0cee6f1577cd31cae7dc0e82f65dcad462a4d18a (patch)
tree180a48cc0b5b15e6f2ac489a6c828ebab591d4cf /app/assets/javascripts/lib
parentdcd01617a750c41fd082cc3383fc7ad2f2afd026 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/lib')
-rw-r--r--app/assets/javascripts/lib/apollo/persist_link.js141
-rw-r--r--app/assets/javascripts/lib/apollo/persistence_mapper.js67
-rw-r--r--app/assets/javascripts/lib/graphql.js41
3 files changed, 238 insertions, 11 deletions
diff --git a/app/assets/javascripts/lib/apollo/persist_link.js b/app/assets/javascripts/lib/apollo/persist_link.js
new file mode 100644
index 00000000000..9d95409d96c
--- /dev/null
+++ b/app/assets/javascripts/lib/apollo/persist_link.js
@@ -0,0 +1,141 @@
+// this file is based on https://github.com/apollographql/apollo-cache-persist/blob/master/examples/react-native/src/utils/persistence/persistLink.ts
+// with some heavy refactororing
+
+/* eslint-disable consistent-return */
+/* eslint-disable @gitlab/require-i18n-strings */
+/* eslint-disable no-param-reassign */
+import { visit } from 'graphql';
+import { ApolloLink } from '@apollo/client/core';
+import traverse from 'traverse';
+
+const extractPersistDirectivePaths = (originalQuery, directive = 'persist') => {
+ const paths = [];
+ const fragmentPaths = {};
+ const fragmentPersistPaths = {};
+
+ const query = visit(originalQuery, {
+ FragmentSpread: ({ name: { value: name } }, _key, _parent, _path, ancestors) => {
+ const root = ancestors.find(
+ ({ kind }) => kind === 'OperationDefinition' || kind === 'FragmentDefinition',
+ );
+
+ const rootKey = root.kind === 'FragmentDefinition' ? root.name.value : '$ROOT';
+
+ const fieldPath = ancestors
+ .filter(({ kind }) => kind === 'Field')
+ .map(({ name: { value } }) => value);
+
+ fragmentPaths[name] = [rootKey].concat(fieldPath);
+ },
+ Directive: ({ name: { value: name } }, _key, _parent, _path, ancestors) => {
+ if (name === directive) {
+ const fieldPath = ancestors
+ .filter(({ kind }) => kind === 'Field')
+ .map(({ name: { value } }) => value);
+
+ const fragmentDefinition = ancestors.find(({ kind }) => kind === 'FragmentDefinition');
+
+ // If we are inside a fragment, we must save the reference.
+ if (fragmentDefinition) {
+ fragmentPersistPaths[fragmentDefinition.name.value] = fieldPath;
+ } else if (fieldPath.length) {
+ paths.push(fieldPath);
+ }
+ return null;
+ }
+ },
+ });
+
+ // In case there are any FragmentDefinition items, we need to combine paths.
+ if (Object.keys(fragmentPersistPaths).length) {
+ visit(originalQuery, {
+ FragmentSpread: ({ name: { value: name } }, _key, _parent, _path, ancestors) => {
+ if (fragmentPersistPaths[name]) {
+ let fieldPath = ancestors
+ .filter(({ kind }) => kind === 'Field')
+ .map(({ name: { value } }) => value);
+
+ fieldPath = fieldPath.concat(fragmentPersistPaths[name]);
+
+ const fragment = name;
+ let parent = fragmentPaths[fragment][0];
+
+ while (parent && parent !== '$ROOT' && fragmentPaths[parent]) {
+ fieldPath = fragmentPaths[parent].slice(1).concat(fieldPath);
+ // eslint-disable-next-line prefer-destructuring
+ parent = fragmentPaths[parent][0];
+ }
+
+ paths.push(fieldPath);
+ }
+ },
+ });
+ }
+
+ return { query, paths };
+};
+
+/**
+ * Given a data result object path, return the equivalent query selection path.
+ *
+ * @param {Array} path The data result object path. i.e.: ["a", 0, "b"]
+ * @return {String} the query selection path. i.e.: "a.b"
+ */
+const toQueryPath = (path) => path.filter((key) => Number.isNaN(Number(key))).join('.');
+
+const attachPersists = (paths, object) => {
+ const queryPaths = paths.map(toQueryPath);
+ function mapperFunction() {
+ if (
+ !this.isRoot &&
+ this.node &&
+ typeof this.node === 'object' &&
+ Object.keys(this.node).length &&
+ !Array.isArray(this.node)
+ ) {
+ const path = toQueryPath(this.path);
+
+ this.update({
+ __persist: Boolean(
+ queryPaths.find(
+ (queryPath) => queryPath.indexOf(path) === 0 || path.indexOf(queryPath) === 0,
+ ),
+ ),
+ ...this.node,
+ });
+ }
+ }
+
+ return traverse(object).map(mapperFunction);
+};
+
+export const getPersistLink = () => {
+ return new ApolloLink((operation, forward) => {
+ const { query, paths } = extractPersistDirectivePaths(operation.query);
+
+ // Noop if not a persist query
+ if (!paths.length) {
+ return forward(operation);
+ }
+
+ // Replace query with one without @persist directives.
+ operation.query = query;
+
+ // Remove requesting __persist fields.
+ operation.query = visit(operation.query, {
+ Field: ({ name: { value: name } }) => {
+ if (name === '__persist') {
+ return null;
+ }
+ },
+ });
+
+ return forward(operation).map((result) => {
+ if (result.data) {
+ result.data = attachPersists(paths, result.data);
+ }
+
+ return result;
+ });
+ });
+};
diff --git a/app/assets/javascripts/lib/apollo/persistence_mapper.js b/app/assets/javascripts/lib/apollo/persistence_mapper.js
new file mode 100644
index 00000000000..8fc7c69c79d
--- /dev/null
+++ b/app/assets/javascripts/lib/apollo/persistence_mapper.js
@@ -0,0 +1,67 @@
+// this file is based on https://github.com/apollographql/apollo-cache-persist/blob/master/examples/react-native/src/utils/persistence/persistenceMapper.ts
+// with some heavy refactororing
+
+/* eslint-disable @gitlab/require-i18n-strings */
+/* eslint-disable no-underscore-dangle */
+/* eslint-disable no-param-reassign */
+/* eslint-disable dot-notation */
+export const persistenceMapper = async (data) => {
+ const parsed = JSON.parse(data);
+
+ const mapped = {};
+ const persistEntities = [];
+ const rootQuery = parsed['ROOT_QUERY'];
+
+ // cache entities that have `__persist: true`
+ Object.keys(parsed).forEach((key) => {
+ if (parsed[key]['__persist']) {
+ persistEntities.push(key);
+ }
+ });
+
+ // cache root queries that have `@persist` directive
+ mapped['ROOT_QUERY'] = Object.keys(rootQuery).reduce(
+ (obj, key) => {
+ if (key === '__typename') return obj;
+
+ if (/@persist$/.test(key)) {
+ obj[key] = rootQuery[key];
+
+ if (Array.isArray(rootQuery[key])) {
+ const entities = rootQuery[key].map((item) => item.__ref);
+ persistEntities.push(...entities);
+ } else {
+ const entity = rootQuery[key].__ref;
+ persistEntities.push(entity);
+ }
+ }
+
+ return obj;
+ },
+ { __typename: 'Query' },
+ );
+
+ persistEntities.reduce((obj, key) => {
+ const parsedEntity = parsed[key];
+
+ // check for root queries and only cache root query properties that have `__persist: true`
+ // we need this to prevent overcaching when we fetch the same entity (e.g. project) more than once
+ // with different set of fields
+
+ if (Object.values(rootQuery).some((value) => value.__ref === key)) {
+ const mappedEntity = {};
+ Object.entries(parsedEntity).forEach(([parsedKey, parsedValue]) => {
+ if (!parsedValue || typeof parsedValue !== 'object' || parsedValue['__persist']) {
+ mappedEntity[parsedKey] = parsedValue;
+ }
+ });
+ obj[key] = mappedEntity;
+ } else {
+ obj[key] = parsed[key];
+ }
+
+ return obj;
+ }, mapped);
+
+ return JSON.stringify(mapped);
+};
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 98e45f95b38..c0e923b2670 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -1,6 +1,7 @@
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client/core';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { createUploadLink } from 'apollo-upload-client';
+import { persistCacheSync, LocalStorageWrapper } from 'apollo3-cache-persist';
import ActionCableLink from '~/actioncable_link';
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import possibleTypes from '~/graphql_shared/possible_types.json';
@@ -10,6 +11,8 @@ import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
import { getInstrumentationLink } from './apollo/instrumentation_link';
import { getSuppressNetworkErrorsDuringNavigationLink } from './apollo/suppress_network_errors_during_navigation_link';
+import { getPersistLink } from './apollo/persist_link';
+import { persistenceMapper } from './apollo/persistence_mapper';
export const fetchPolicies = {
CACHE_FIRST: 'cache-first',
@@ -110,6 +113,7 @@ export default (resolvers = {}, config = {}) => {
typeDefs,
path = '/api/graphql',
useGet = false,
+ localCacheKey = null,
} = config;
let ac = null;
let uri = `${gon.relative_url_root || ''}${path}`;
@@ -201,6 +205,8 @@ export default (resolvers = {}, config = {}) => {
});
});
+ const persistLink = getPersistLink();
+
const appLink = ApolloLink.split(
hasSubscriptionOperation,
new ActionCableLink(),
@@ -212,27 +218,40 @@ export default (resolvers = {}, config = {}) => {
performanceBarLink,
new StartupJSLink(),
apolloCaptchaLink,
+ persistLink,
uploadsLink,
requestLink,
].filter(Boolean),
),
);
+ const newCache = new InMemoryCache({
+ ...cacheConfig,
+ typePolicies: {
+ ...typePolicies,
+ ...cacheConfig.typePolicies,
+ },
+ possibleTypes: {
+ ...possibleTypes,
+ ...cacheConfig.possibleTypes,
+ },
+ });
+
+ if (localCacheKey) {
+ persistCacheSync({
+ cache: newCache,
+ // we leave NODE_ENV here temporarily for visibility so developers can easily see caching happening in dev mode
+ debug: process.env.NODE_ENV === 'development',
+ storage: new LocalStorageWrapper(window.localStorage),
+ persistenceMapper,
+ });
+ }
+
ac = new ApolloClient({
typeDefs,
link: appLink,
connectToDevTools: process.env.NODE_ENV !== 'production',
- cache: new InMemoryCache({
- ...cacheConfig,
- typePolicies: {
- ...typePolicies,
- ...cacheConfig.typePolicies,
- },
- possibleTypes: {
- ...possibleTypes,
- ...cacheConfig.possibleTypes,
- },
- }),
+ cache: newCache,
resolvers,
defaultOptions: {
query: {