"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getByIp = exports.LOCATION_ORACLE_URL = void 0;
const conditional_cache_1 = require("../utils/conditional_cache");
const batch_concurrent_1 = __importDefault(require("../utils/batch_concurrent"));
const Ip = __importStar(require("../utils/ip"));
const CACHE_OPTS = {
    // 30 MiB; Worker limit is 128. If each location entry is 256 bytes,
    // this capacity allows for storing 2^16.9 addresses.
    // See https://developers.cloudflare.com/workers/about/limits/#memory
    max: 30 * 2 ** 20,
    // Entry size, in bytes (conservative). Estimated by opening Chrome's JS console,
    // allocating a bunch of EdgeScapeGeolocation-like objects, taking a heap dump,
    // and seeing how much space they took up: under 200 bytes each.
    length: () => 256,
    // TTL: 1 month, in ms
    // When an entry reaches its TTL, it's evicted (lazily, at the time of next access).
    maxAge: 1000 * 60 * 60 * 24 * 30,
};
// URL that, if asked via a GET request bearing an X-Forwarded-For header, will
// respond with a JSON-serialized LocationResponse pertaining to the leftmost IP
// address in the X-Forwarded-For header.
// Exported for testing.
exports.LOCATION_ORACLE_URL = 'https://cdn.optimizely.com/geo4.json';
/**
 * @typedef {Object} LocationResponse
 * @property {String} ip
 *        The IP address whose location was resolved.
 * @property {EdgeScapeGeolocation} location
 *        Geolocation data for `ip`.
 */
/**
 * A record of geolocation data, sourced from Akamai EdgeScape.
 * See https://developer.akamai.com/edgescape
 * @typedef {Object} EdgeScapeGeolocation
 *
 * @property {String} continent
 *        EdgeScape's two-letter continent code
 * @property {String} country
 *        ISO-3166's two-letter country code
 * @property {String} region
 *        ISO-3166's two-letter code for a country subdivision
 *        (e.g., state, province, region)
 * @property {String} city
 *        EdgeScape's name for a nearby city
 */
/**
 * @param {String} ip
 *        The IP address to resolve to a geolocation.
 * @returns {Promise.<EdgeScapeGeolocation, (Error|Response)>}
 *        Rejects if the subrequest to resolve geolocation fails.
 *        Fulfills otherwise.
 */
async function getByIpUncached(ip) {
    const response = await fetch(exports.LOCATION_ORACLE_URL, {
        headers: { 'X-Forwarded-For': ip },
    });
    if (response.status !== 200) {
        return Promise.reject(response);
    }
    const data = await response.json();
    return data.location;
}
const cachedConcurrentGetByIp = (0, conditional_cache_1.cache)(CACHE_OPTS, (0, batch_concurrent_1.default)(getByIpUncached));
/**
 * @param {String} ip
 *        The IP address to resolve to a geolocation.
 * @returns {Promise.<EdgeScapeGeolocation>}
 *        Rejects in the event of a cache miss followed by a failed subrequest to
 *        resolve geolocation. Fulfills otherwise, with either a cached value or a
 *        fresh one.
 *
 *        Also includes a Symbol(CacheAccessId) property, which can be used to look up
 *        information about the underlying cache access.
 */
function getByIp(ip) {
    // Low bits in IP addresses likely don't buy us much more accurate geolocations,
    // so they can be discarded. Aliasing many IP addresses to the same cache key helps
    // get us a higher cache hit rate.
    return cachedConcurrentGetByIp(Ip.anonymize(ip).toLowerCase());
}
exports.getByIp = getByIp;
getByIp[conditional_cache_1.Cache] = cachedConcurrentGetByIp[conditional_cache_1.Cache];
// Surface the underlying cached function's access map on getByIp, for
// use in RUM reporting.
getByIp[conditional_cache_1.CacheAccesses] = cachedConcurrentGetByIp[conditional_cache_1.CacheAccesses];
exports.default = {
    getByIp,
};
