makeCacheableWithContext = fn => {
	/** @type {WeakMap<AssociatedObjectForCache, Map<string, Map<string, string>>>} */
	const cache = new WeakMap();

	/** @type {MakeCacheableWithContextResult & { bindCache: BindCacheForContext, bindContextCache: BindContextCacheForContext }} */
	const cachedFn = (context, identifier, associatedObjectForCache) => {
		if (!associatedObjectForCache) return fn(context, identifier);

		let innerCache = cache.get(associatedObjectForCache);
		if (innerCache === undefined) {
			innerCache = new Map();
			cache.set(associatedObjectForCache, innerCache);
		}

		let cachedResult;
		let innerSubCache = innerCache.get(context);
		if (innerSubCache === undefined) {
			innerCache.set(context, (innerSubCache = new Map()));
		} else {
			cachedResult = innerSubCache.get(identifier);
		}

		if (cachedResult !== undefined) {
			return cachedResult;
		}
		const result = fn(context, identifier);
		innerSubCache.set(identifier, result);
		return result;
	};

	/** @type {BindCacheForContext} */
	cachedFn.bindCache = associatedObjectForCache => {
		let innerCache;
		if (associatedObjectForCache) {
			innerCache = cache.get(associatedObjectForCache);
			if (innerCache === undefined) {
				innerCache = new Map();
				cache.set(associatedObjectForCache, innerCache);
			}
		} else {
			innerCache = new Map();
		}

		/**
		 * @param {string} context context used to create relative path
		 * @param {string} identifier identifier used to create relative path
		 * @returns {string} the returned relative path
		 */
		const boundFn = (context, identifier) => {
			let cachedResult;
			let innerSubCache = innerCache.get(context);
			if (innerSubCache === undefined) {
				innerCache.set(context, (innerSubCache = new Map()));
			} else {
				cachedResult = innerSubCache.get(identifier);
			}

			if (cachedResult !== undefined) {
				return cachedResult;
			}
			const result = fn(context, identifier);
			innerSubCache.set(identifier, result);
			return result;
		};

		return boundFn;
	};

	/** @type {BindContextCacheForContext} */
	cachedFn.bindContextCache = (context, associatedObjectForCache) => {
		let innerSubCache;
		if (associatedObjectForCache) {
			let innerCache = cache.get(associatedObjectForCache);
			if (innerCache === undefined) {
				innerCache = new Map();
				cache.set(associatedObjectForCache, innerCache);
			}

			innerSubCache = innerCache.get(context);
			if (innerSubCache === undefined) {
				innerCache.set(context, (innerSubCache = new Map()));
			}
		} else {
			innerSubCache = new Map();
		}

		/**
		 * @param {string} identifier identifier used to create relative path
		 * @returns {string} the returned relative path
		 */
		const boundFn = identifier => {
			const cachedResult = innerSubCache.get(identifier);
			if (cachedResult !== undefined) {
				return cachedResult;
			}
			const result = fn(context, identifier);
			innerSubCache.set(identifier, result);
			return result;
		};

		return boundFn;
	};

	return cachedFn;
}
@param {(context: string, identifier: string) => string} fn function
@returns {MakeCacheableWithContextResult & { bindCache: BindCacheForContext, bindContextCache: BindContextCacheForContext }} cacheable function with context