const storage = {};
const pendingCalls = {};

/**
 * Conditionally console.log() the given args, if environment variable DEBUG is set.
 */
function log(args) {
    if (!process.env.DEBUG) {
        return;
    }
    console.log.apply(args);
}

/**
 * Generate a hashcode from string.
 *
 * @param {string} str Subject string
 */
function hashCode(str) {
    /* eslint-disable no-bitwise */
    return str.split("").reduce((prevHash, currVal) =>
        ((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0);
    /* eslint-enable */
}

/**
 * Get cache key specifically for the invocation fn(args).
 *
 * @param {Function} fn Function to determine cache key for
 * @param {*} args Invocation argument list to include in cache key
 */
function getCacheKey(fn, args = []) {
    const fnName = fn.name || hashCode(fn.toString());
    return JSON.stringify([fnName, ...args], (key, value) => {
        if (typeof value === "function") {
            return value.toString();
        }
        return value;
    });
}

/**
 * Wrap a given function so that the result is cached and only refreshed
 * after a given period of time has passed since the last result.
 *
 * @param {Function} fn Function to wrap
 * @param {*} [ttlSeconds] Number of seconds the result is valid
 */
function wrapFunction(fn, ttlSeconds) {
    if (!fn || !(fn instanceof Function)) {
        throw new Error("First argument to wrapFunction() should be a function");
    }

    const TTL_SECONDS = ttlSeconds || 300;
    log(`Caching calls to ${getCacheKey(fn)} (${TTL_SECONDS} seconds)`);

    return function returnCachedResultAsync(...args) {
        log(`Function ${fn.name} called`);

        // Take arguments into account -> cache key might differ between invocations
        const cacheKey = getCacheKey(fn, args);
        if (!cacheKey) {
            throw new Error(`Couldn't determine cache key for function to be cached`);
        }

        const TIME_ID = `lastTime_${cacheKey}`;
        const RESULT_ID = `lastResult_${cacheKey}`;

        const lastFetchTime = storage[TIME_ID];
        const lastFetchAge = (Date.now() - lastFetchTime) / 1000;
        const previousFetchFinished = storage[RESULT_ID] !== undefined;

        const canUseResultFromCache = !!lastFetchTime &&
            previousFetchFinished && lastFetchAge < TTL_SECONDS;

        if (canUseResultFromCache) {
            log("Previous result still valid, returning from cache");

            return new Promise((resolve, reject) => {
                try {
                    const result = storage[RESULT_ID];
                    resolve(result);
                } catch (err) {
                    reject(err);
                }
            });
        }

        if (pendingCalls[cacheKey]) {
            log("Pending result found, returning it...");
            return pendingCalls[cacheKey];
        }

        log("Refreshing...");
        storage[TIME_ID] = Date.now();

        const currentCall = Promise.resolve(fn.apply(this, args))
            .then(result => {
                log("Caching result...");

                // Cache results for next use
                storage[RESULT_ID] = result;

                // Erase pending call from bookkeeping
                pendingCalls[cacheKey] = null;
                delete pendingCalls[cacheKey];

                return result;
            });

        pendingCalls[cacheKey] = currentCall;
        return currentCall;
    };
}

module.exports = wrapFunction;

@@ -1,15 +1,22 @@
 const 
-s
+defaultS
 torage =
@@ -16,18 +16,25 @@
 orage = 
-%7B%7D
+new Map()
 ;%0Aconst 
@@ -844,48 +844,24 @@
     
-return JSON.stringify(%5BfnName, ...args%5D,
+const replacer =
  (ke
@@ -992,16 +992,71 @@
 e;%0A    %7D
+;%0A    return JSON.stringify(%5BfnName, ...args%5D, replacer
 );%0A%7D%0A%0A/*
@@ -1195,24 +1195,234 @@
  result.%0A *%0A
+ * Possible keys for the %60options%60 object:%0A * - ttl: Number of seconds the result is valid (defaults to 300)%0A * - get: custom storage get function (optional)%0A * - set: custom storage set function (optional)%0A *%0A
  * @param %7BF
@@ -1465,61 +1465,25 @@
 am %7B
-*%7D %5BttlSec
+Object%7D %5Bopti
 on
-d
 s%5D
- Number of seconds the result is valid
 %0A */
@@ -1509,26 +1509,28 @@
 ion(fn, 
-ttlSeconds
+options = %7B%7D
 ) %7B%0A    
@@ -1654,24 +1654,228 @@
 n%22);%0A    %7D%0A%0A
+    // Use cache getter and setter provided in options, or fallback to default%0A    const storage = (options.get && options.set) ?%0A        %7B get: options.get, set: options.set %7D :%0A        defaultStorage;%0A%0A
     const TT
@@ -1890,18 +1890,19 @@
 S = 
-ttlSeconds
+options.ttl
  %7C%7C 
@@ -2473,25 +2473,29 @@
  storage
-%5B
+.get(
 TIME_ID
-%5D
+)
 ;%0A      
@@ -2591,33 +2591,37 @@
 nished = storage
-%5B
+.get(
 RESULT_ID%5D !== u
@@ -2613,17 +2613,17 @@
 ESULT_ID
-%5D
+)
  !== und
@@ -2972,33 +2972,37 @@
 result = storage
-%5B
+.get(
 RESULT_ID%5D;%0A    
@@ -2994,17 +2994,17 @@
 ESULT_ID
-%5D
+)
 ;%0A      
@@ -3343,19 +3343,21 @@
 rage
-%5B
+.set(
 TIME_ID
-%5D =
+,
  Dat
@@ -3363,16 +3363,17 @@
 te.now()
+)
 ;%0A%0A     
@@ -3575,17 +3575,21 @@
  storage
-%5B
+.set(
 RESULT_I
@@ -3593,18 +3593,17 @@
 T_ID
-%5D =
+,
  result
+)
 ;%0A%0A 

Use Map() as a default cache, allow passing custom one