class Async::HTTP::Cache::General

Implements a general shared cache according to www.rfc-editor.org/rfc/rfc9111

def cacheable_request?(request)

def cacheable_request?(request)
	# We don't support caching requests which have a request body:
	if request.body
		return false
	end
	
	# We can't cache upgraded requests:
	if request.protocol
		return false
	end
	
	# We only support caching GET and HEAD requests:
	unless request.method == "GET" || request.method == "HEAD"
		return false
	end
	
	if request.headers[AUTHORIZATION]
		return false
	end
	
	if request.headers[COOKIE]
		return false
	end
	
	# Otherwise, we can cache it:
	return true
end

def cacheable_response?(response)

def cacheable_response?(response)
	# At this point, we know response.status and response.headers.
	# But we don't know response.body or response.headers.trailer.
	unless CACHEABLE_RESPONSE_CODES.include?(response.status)
		Console.logger.debug(self, status: response.status) {"Cannot cache response with status code!"}
		return false
	end
	
	unless cacheable_response_headers?(response.headers)
		Console.logger.debug(self) {"Cannot cache response with uncacheable headers!"}
		return false
	end
	
	return true
end

def cacheable_response_headers?(headers)

def cacheable_response_headers?(headers)
	if cache_control = headers[CACHE_CONTROL]
		if cache_control.no_store? || cache_control.private?
			Console.logger.debug(self, cache_control: cache_control) {"Cannot cache response with cache-control header!"}
			return false
		end
	end
	
	if set_cookie = headers[SET_COOKIE]
		Console.logger.debug(self) {"Cannot cache response with set-cookie header!"}
		return false
	end
				
	return true
end

def call(request)

def call(request)
	cache_control = request.headers[CACHE_CONTROL]
	
	unless cache_control&.no_cache?
		key = self.key(request)
		
		if response = @store.lookup(key, request)
			Console.logger.debug(self, key: key) {"Cache hit!"}
			@count += 1
			
			# Return the cached response:
			return response
		end
	end
	
	unless cache_control&.no_store?
		return wrap(key, request, super)
	end
	
	return super
end

def close

def close
	@store.close
ensure
	super
end

def initialize(app, store: Store.default)

def initialize(app, store: Store.default)
	super(app)
	
	@count = 0
	
	@store = store
end

def key(request)

def key(request)
	@store.normalize(request)
	
	[request.authority, request.method, request.path]
end

def proceed_with_response_cache?(response)

Semantically speaking, it is possible for trailers to result in an uncacheable response, so we need to check for that.
def proceed_with_response_cache?(response)
	if response.headers.trailer?
		unless cacheable_response_headers?(response.headers)
			Console.logger.debug(self, trailer: trailer.keys) {"Cannot cache response with trailer header!"}
			return false
		end
	end
	
	return true
end

def wrap(key, request, response)

Potentially wrap the response so that it updates the cache, if caching is possible.
def wrap(key, request, response)
	if request.head? and body = response.body
		unless body.empty?
			Console.logger.warn(self) {"HEAD request resulted in non-empty body!"}
			
			return response
		end
	end
	
	unless cacheable_request?(request)
		Console.logger.debug(self) {"Cannot cache request!"}
		return response
	end
	
	unless cacheable_response?(response)
		Console.logger.debug(self) {"Cannot cache response!"}
		return response
	end
	
	return Body.wrap(response) do |response, body|
		if proceed_with_response_cache?(response)
			key ||= self.key(request)
			
			Console.logger.debug(self, key: key) {"Updating miss!"}
			@store.insert(key, request, Response.new(response, body))
		end
	end
end