@graphql-mesh/transform-cache
Version: 
153 lines (148 loc) • 6.52 kB
JavaScript
;
const resolversComposition = require('@graphql-tools/resolvers-composition');
const stringInterpolation = require('@graphql-mesh/string-interpolation');
const crossHelpers = require('@graphql-mesh/cross-helpers');
const utils = require('@graphql-mesh/utils');
const schema = require('@graphql-tools/schema');
function computeCacheKey(options) {
    const argsHash = options.args ? stringInterpolation.hashObject(options.args) : '';
    const fieldNamesHash = stringInterpolation.hashObject(options.info.fieldNodes);
    if (!options.keyStr) {
        return `${options.info.parentType.name}-${options.info.fieldName}-${argsHash}-${fieldNamesHash}`;
    }
    const templateData = {
        typeName: options.info.parentType.name,
        fieldName: options.info.fieldName,
        args: options.args,
        argsHash,
        fieldNamesHash,
        info: options.info || null,
        env: crossHelpers.process.env,
    };
    return stringInterpolation.stringInterpolator.parse(options.keyStr, templateData);
}
class CacheTransform {
    constructor(options) {
        this.options = options;
        this.noWrap = true;
        this.shouldWaitLocal = {};
    }
    transformSchema(schema$1) {
        var _a;
        const { config, cache } = this.options;
        const sourceResolvers = utils.extractResolvers(schema$1);
        const compositions = {};
        for (const cacheItem of config) {
            const effectingOperations = ((_a = cacheItem.invalidate) === null || _a === void 0 ? void 0 : _a.effectingOperations) || [];
            for (const { operation, matchKey } of effectingOperations) {
                compositions[operation] = originalResolver => async (root, args, context, info) => {
                    const result = await originalResolver(root, args, context, info);
                    const cacheKey = computeCacheKey({
                        keyStr: matchKey,
                        args,
                        info,
                    });
                    await cache.delete(cacheKey);
                    return result;
                };
            }
            compositions[cacheItem.field] = originalResolver => async (root, args, context, info) => {
                var _a, _b;
                const cacheKey = computeCacheKey({
                    keyStr: cacheItem.cacheKey,
                    args,
                    info,
                });
                const cachedValue = await cache.get(cacheKey);
                if (cachedValue) {
                    return cachedValue;
                }
                const shouldWaitCacheKey = `${cacheKey}_shouldWait`;
                const pubsubTopic = `${cacheKey}_resolved`;
                const shouldWait = await this.shouldWait(shouldWaitCacheKey);
                if (shouldWait) {
                    return this.waitAndReturn(pubsubTopic);
                }
                this.setShouldWait(shouldWaitCacheKey);
                try {
                    const result = await originalResolver(root, args, context, info);
                    await cache.set(cacheKey, result, {
                        ttl: (_a = cacheItem.invalidate) === null || _a === void 0 ? void 0 : _a.ttl,
                    });
                    // do not await setting the cache here, otherwise we would delay returnig the result unnecessarily
                    // instead await as part of shouldWait cleanup
                    const setCachePromise = this.options.cache.set(cacheKey, result, {
                        ttl: (_b = cacheItem.invalidate) === null || _b === void 0 ? void 0 : _b.ttl,
                    });
                    // do not wait for cleanup to complete
                    this.cleanupShouldWait({
                        shouldWaitCacheKey,
                        pubsubTopic,
                        data: { result },
                        setCachePromise,
                    });
                    return result;
                }
                catch (error) {
                    this.cleanupShouldWait({
                        shouldWaitCacheKey,
                        pubsubTopic,
                        data: { error },
                    });
                    throw error;
                }
            };
        }
        const wrappedResolvers = resolversComposition.composeResolvers(sourceResolvers, compositions);
        return schema.addResolversToSchema({
            schema: schema$1,
            resolvers: wrappedResolvers,
            updateResolversInPlace: true,
        });
    }
    async shouldWait(shouldWaitCacheKey) {
        // this is to prevent a call to a the cache (which might be distributed)
        // when the should wait was set from current instance
        const shouldWaitLocal = this.shouldWaitLocal[shouldWaitCacheKey];
        if (shouldWaitLocal) {
            return true;
        }
        const shouldWaitGlobal = await this.options.cache.get(shouldWaitCacheKey);
        if (shouldWaitGlobal) {
            return true;
        }
        // requried to be called after async check to eliminate local race condition
        return this.shouldWaitLocal[shouldWaitCacheKey];
    }
    setShouldWait(shouldWaitCacheKey) {
        this.options.cache.set(shouldWaitCacheKey, true);
        this.shouldWaitLocal[shouldWaitCacheKey] = true;
    }
    async cleanupShouldWait({ shouldWaitCacheKey, pubsubTopic, data, setCachePromise, }) {
        if (setCachePromise) {
            // we need to wait for cache to be filled before removing the shouldWait
            await setCachePromise;
        }
        // the below order is deliberate and important
        // we need to delete the shouldWait keys first
        // this ensures that no new subscriptions for topic are created after publish is called
        // since the cache is async we need to await the delete
        await this.options.cache.delete(shouldWaitCacheKey);
        delete this.shouldWaitLocal[shouldWaitCacheKey];
        this.options.pubsub.publish(pubsubTopic, data);
    }
    waitAndReturn(pubsubTopic) {
        return new Promise((resolve, reject) => {
            const subId = this.options.pubsub.subscribe(pubsubTopic, ({ result, error }) => {
                this.options.pubsub.unsubscribe(subId);
                if (error) {
                    reject(error);
                }
                if (result) {
                    resolve(result);
                }
            });
        });
    }
}
module.exports = CacheTransform;