import { __awaiter, __extends, __generator } from "tslib";
import { random } from "lodash";
import { BLOCK_NUMBER_TTL, PROVIDER_CACHE_TTL, PROVIDER_CACHE_TTL_MODIFIER as ttl_modifier } from "./constants";
import { RateLimitedProvider } from "./rateLimitedProvider";
import { CacheType } from "./utils";
var CacheProvider = /** @class */ (function (_super) {
    __extends(CacheProvider, _super);
    function CacheProvider(providerCacheNamespace, redisClient, 
    // Note: if not provided, this is set to POSITIVE_INFINITY, meaning no cache entries are set with the standard TTL.
    standardTtlBlockDistance, 
    // Note: if not provided, this is set to POSITIVE_INFINITY, meaning no cache entries are set with no TTL.
    noTtlBlockDistance, providerCacheTtl) {
        if (standardTtlBlockDistance === void 0) { standardTtlBlockDistance = Number.POSITIVE_INFINITY; }
        if (noTtlBlockDistance === void 0) { noTtlBlockDistance = Number.POSITIVE_INFINITY; }
        if (providerCacheTtl === void 0) { providerCacheTtl = PROVIDER_CACHE_TTL; }
        var jsonRpcConstructorParams = [];
        for (var _i = 5; _i < arguments.length; _i++) {
            jsonRpcConstructorParams[_i - 5] = arguments[_i];
        }
        var _this = _super.apply(this, jsonRpcConstructorParams) || this;
        _this.redisClient = redisClient;
        _this.standardTtlBlockDistance = standardTtlBlockDistance;
        _this.noTtlBlockDistance = noTtlBlockDistance;
        _this.providerCacheTtl = providerCacheTtl;
        var chainId = _this.network.chainId;
        // Pre-compute as much of the redis key as possible.
        var cachePrefix = "".concat(providerCacheNamespace, ",").concat(new URL(_this.connection.url).hostname, ",").concat(chainId);
        _this.getBlockByNumberPrefix = "".concat(cachePrefix, ":getBlockByNumber,");
        _this.getLogsCachePrefix = "".concat(cachePrefix, ":eth_getLogs,");
        _this.callCachePrefix = "".concat(cachePrefix, ":eth_call,");
        _this.getTransactionReceiptPrefix = "".concat(cachePrefix, ":eth_getTransactionReceipt,");
        var _ttlVar = providerCacheTtl;
        var _ttl = Number(_ttlVar);
        if (isNaN(_ttl) || _ttl <= 0) {
            throw new Error("PROVIDER_CACHE_TTL (".concat(_ttlVar, ") must be numeric and > 0"));
        }
        _this.baseTTL = _ttl;
        return _this;
    }
    CacheProvider.prototype.send = function (method, params) {
        var _a, _b, _c;
        return __awaiter(this, void 0, void 0, function () {
            var cacheType, _d, redisKey, redisResult, result, blockNumber, _e, ttl;
            return __generator(this, function (_f) {
                switch (_f.label) {
                    case 0:
                        if (!this.redisClient) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.cacheType(method, params)];
                    case 1:
                        _d = _f.sent();
                        return [3 /*break*/, 3];
                    case 2:
                        _d = CacheType.NONE;
                        _f.label = 3;
                    case 3:
                        cacheType = _d;
                        if (!(cacheType === CacheType.NONE)) return [3 /*break*/, 5];
                        return [4 /*yield*/, _super.prototype.send.call(this, method, params)];
                    case 4: return [2 /*return*/, _f.sent()];
                    case 5:
                        redisKey = this.buildRedisKey(method, params);
                        return [4 /*yield*/, ((_a = this.redisClient) === null || _a === void 0 ? void 0 : _a.get(redisKey))];
                    case 6:
                        redisResult = _f.sent();
                        // If cache has the result, parse the json and return it.
                        if (redisResult) {
                            return [2 /*return*/, JSON.parse(redisResult)];
                        }
                        return [4 /*yield*/, _super.prototype.send.call(this, method, params)];
                    case 7:
                        result = _f.sent();
                        if (!(cacheType === CacheType.DECIDE_TTL_POST_SEND)) return [3 /*break*/, 9];
                        blockNumber = this.getBlockNumberFromRpcResponse(method, result);
                        return [4 /*yield*/, this.cacheTypeForBlock(blockNumber)];
                    case 8:
                        cacheType = _f.sent();
                        if (cacheType === CacheType.NONE) {
                            return [2 /*return*/, result];
                        }
                        _f.label = 9;
                    case 9:
                        _e = cacheType;
                        switch (_e) {
                            case CacheType.WITH_TTL: return [3 /*break*/, 10];
                            case CacheType.NO_TTL: return [3 /*break*/, 12];
                        }
                        return [3 /*break*/, 14];
                    case 10:
                        ttl = this.baseTTL + Math.ceil(random(-ttl_modifier, ttl_modifier, true) * this.baseTTL);
                        return [4 /*yield*/, ((_b = this.redisClient) === null || _b === void 0 ? void 0 : _b.set(redisKey, JSON.stringify(result), ttl))];
                    case 11:
                        _f.sent();
                        return [3 /*break*/, 15];
                    case 12: return [4 /*yield*/, ((_c = this.redisClient) === null || _c === void 0 ? void 0 : _c.set(redisKey, JSON.stringify(result), Number.POSITIVE_INFINITY))];
                    case 13:
                        _f.sent();
                        return [3 /*break*/, 15];
                    case 14: throw new Error("Unexpected Cache type: ".concat(cacheType));
                    case 15: 
                    // Return the cached result.
                    return [2 /*return*/, result];
                }
            });
        });
    };
    CacheProvider.prototype.buildRedisKey = function (method, params) {
        switch (method) {
            case "eth_getBlockByNumber":
                return this.getBlockByNumberPrefix + JSON.stringify(params);
            case "eth_getLogs":
                return this.getLogsCachePrefix + JSON.stringify(params);
            case "eth_call":
                return this.callCachePrefix + JSON.stringify(params);
            case "eth_getTransactionReceipt":
                return this.getTransactionReceiptPrefix + JSON.stringify(params);
            default:
                throw new Error("CacheProvider::buildRedisKey: invalid JSON-RPC method ".concat(method));
        }
    };
    CacheProvider.prototype.cacheType = function (method, params) {
        // Today, we only cache eth_getLogs and eth_call.
        if (method === "eth_getLogs") {
            var _a = params[0], fromBlock = _a.fromBlock, toBlock = _a.toBlock;
            // Handle odd cases where the ordering is flipped, etc.
            // toBlock/fromBlock is in hex, so it must be parsed before being compared to the first unsafe block.
            var fromBlockNumber = parseInt(String(fromBlock), 16);
            var toBlockNumber = parseInt(String(toBlock), 16);
            // Handle cases where the input block numbers are not hex values ("latest", "pending", etc).
            // This would result in the result of the above being NaN.
            if (Number.isNaN(fromBlockNumber) || Number.isNaN(toBlockNumber)) {
                return Promise.resolve(CacheType.NONE);
            }
            if (toBlockNumber < fromBlockNumber) {
                throw new Error("CacheProvider::shouldCache toBlock cannot be smaller than fromBlock.");
            }
            return this.cacheTypeForBlock(toBlock);
        }
        else if ("eth_call" === method || "eth_getBlockByNumber" === method) {
            // Pull out the block tag from params. Its position in params is dependent on the method.
            // We are only interested in numeric block tags, which would be hex-encoded strings.
            var idx = method === "eth_getBlockByNumber" ? 0 : 1;
            var blockNumber = parseInt(String(params[idx]), 16);
            // If the block number isn't present or is a text string, this will be NaN and we return false.
            if (Number.isNaN(blockNumber)) {
                return Promise.resolve(CacheType.NONE);
            }
            // If the block is old enough to cache, cache the call.
            return this.cacheTypeForBlock(blockNumber);
        }
        else if ("eth_getTransactionReceipt" === method) {
            // The only param to this request is "hash" so we can't determine how old the data we want is until after
            // we receive the RPC result. Therefore we'll defer the TTL decision.
            return Promise.resolve(CacheType.DECIDE_TTL_POST_SEND);
        }
        else {
            return Promise.resolve(CacheType.NONE);
        }
    };
    CacheProvider.prototype.getBlockNumberFromRpcResponse = function (method, result) {
        if (method === "eth_getTransactionReceipt") {
            var receipt = result;
            return Number(receipt.blockNumber);
        }
        else {
            throw new Error("CacheProvider::getBlockNumberFromRpcResponse: unsupported JSON-RPC method ".concat(method));
        }
    };
    CacheProvider.prototype.cacheTypeForBlock = function (blockNumber) {
        return __awaiter(this, void 0, void 0, function () {
            var currentBlockNumber, headDistance;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, _super.prototype._getInternalBlockNumber.call(this, BLOCK_NUMBER_TTL * 1000)];
                    case 1:
                        currentBlockNumber = _a.sent();
                        headDistance = currentBlockNumber - blockNumber;
                        // If the distance from head is large enough, set with no TTL.
                        if (headDistance > this.noTtlBlockDistance) {
                            return [2 /*return*/, CacheType.NO_TTL];
                        }
                        // If the distance is <= noTtlBlockDistance, but > standardTtlBlockDistance, use standard TTL.
                        if (headDistance > this.standardTtlBlockDistance) {
                            return [2 /*return*/, CacheType.WITH_TTL];
                        }
                        // Too close to HEAD, no cache.
                        return [2 /*return*/, CacheType.NONE];
                }
            });
        });
    };
    return CacheProvider;
}(RateLimitedProvider));
export { CacheProvider };
//# sourceMappingURL=cachedProvider.js.map