Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 95 additions & 15 deletions lib/handler/cache-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,50 @@ const { parseHttpDate } = require('../util/date.js')

function noop () {}

class CacheController {
#blocked = false
#downstreamPaused = false

constructor (target) {
this.target = target
}

block () {
this.#blocked = true
}

release () {
this.#blocked = false
if (!this.#downstreamPaused) {
this.target.resume()
}
}

pause () {
this.#downstreamPaused = true
this.target.pause()
}

resume () {
this.#downstreamPaused = false
if (!this.#blocked) {
this.target.resume()
}
}

abort (reason) {
this.target.abort(reason)
}

get paused () { return this.#downstreamPaused }
get aborted () { return this.target.aborted }
get reason () { return this.target.reason }
get rawHeaders () { return this.target.rawHeaders }
set rawHeaders (value) { this.target.rawHeaders = value }
get rawTrailers () { return this.target.rawTrailers }
set rawTrailers (value) { this.target.rawTrailers = value }
}

// Status codes that we can use some heuristics on to cache
const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
Expand Down Expand Up @@ -149,6 +193,11 @@ class CacheHandler {
*/
#handler

/**
* @type {CacheController | undefined}
*/
#downstreamController

/**
* @type {import('node:stream').Writable | undefined}
*/
Expand All @@ -170,7 +219,8 @@ class CacheHandler {
onRequestStart (controller, context) {
this.#writeStream?.destroy()
this.#writeStream = undefined
this.#handler.onRequestStart?.(controller, context)
this.#downstreamController = new CacheController(controller)
this.#handler.onRequestStart?.(this.#downstreamController, context)
}

onBodySent (chunk) {
Expand All @@ -181,8 +231,8 @@ class CacheHandler {
this.#handler.onRequestSent?.()
}

onRequestUpgrade (controller, statusCode, headers, socket) {
this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
onRequestUpgrade (_controller, statusCode, headers, socket) {
this.#handler.onRequestUpgrade?.(this.#downstreamController, statusCode, headers, socket)
}

/**
Expand All @@ -199,7 +249,7 @@ class CacheHandler {
) {
const downstreamOnHeaders = () =>
this.#handler.onResponseStart?.(
controller,
this.#downstreamController,
statusCode,
resHeaders,
statusMessage
Expand Down Expand Up @@ -323,10 +373,21 @@ class CacheHandler {
// Not modified, re-use the cached value
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified
if (statusCode === 304) {
let done = false
const finish304 = () => {
if (done) {
return
}
done = true
this.#downstreamController.release()
}

const handle304 = (cachedValue) => {
if (!cachedValue) {
// Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
return downstreamOnHeaders()
downstreamOnHeaders()
finish304()
return
}

// Re-use the cached value: statuscode, statusmessage, headers and body
Expand All @@ -341,34 +402,43 @@ class CacheHandler {
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)

if (!this.#writeStream || !cachedValue?.body) {
finish304()
return
}

if (typeof cachedValue.body.values === 'function') {
const bodyIterator = cachedValue.body.values()

const streamCachedBody = () => {
if (done) {
return
}
for (const chunk of bodyIterator) {
const full = this.#writeStream.write(chunk) === false
this.#handler.onResponseData?.(controller, chunk)
const full = this.#writeStream?.write(chunk) === false
this.#handler.onResponseData?.(this.#downstreamController, chunk)
// when stream is full stop writing until we get a 'drain' event
if (full) {
break
return
}
}
finish304()
}

this.#writeStream
.on('error', function () {
handler.#writeStream = undefined
handler.#store.delete(handler.#cacheKey)
// Keep replaying downstream without the store
streamCachedBody()
})
.on('drain', () => {
streamCachedBody()
})
.on('close', function () {
if (handler.#writeStream === this) {
handler.#writeStream = undefined
// Keep replaying downstream without the store
streamCachedBody()
}
})

Expand All @@ -377,15 +447,17 @@ class CacheHandler {
// Readable stream body (e.g. from async/remote cache stores)
cachedValue.body
.on('data', (chunk) => {
this.#writeStream.write(chunk)
this.#handler.onResponseData?.(controller, chunk)
this.#writeStream?.write(chunk)
this.#handler.onResponseData?.(this.#downstreamController, chunk)
})
.on('end', () => {
this.#writeStream.end()
this.#writeStream?.end()
finish304()
})
.on('error', () => {
this.#writeStream = undefined
this.#store.delete(this.#cacheKey)
finish304()
})

this.#writeStream
Expand All @@ -398,15 +470,23 @@ class CacheHandler {
handler.#writeStream = undefined
}
})
} else {
finish304()
}
}

/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const result = this.#store.get(this.#cacheKey)
// A 304 ends before the cached body is replayed. Keep the origin
// response paused until replay is complete.
this.#downstreamController.block()
controller.pause()
if (result && typeof result.then === 'function') {
result.then(handle304)
result.then(handle304, () => handle304(undefined)).catch((err) => {
controller.abort(err)
})
} else {
handle304(result)
}
Expand Down Expand Up @@ -449,18 +529,18 @@ class CacheHandler {
controller.pause()
}

this.#handler.onResponseData?.(controller, chunk)
this.#handler.onResponseData?.(this.#downstreamController, chunk)
}

onResponseEnd (controller, trailers) {
this.#writeStream?.end()
this.#handler.onResponseEnd?.(controller, trailers)
this.#handler.onResponseEnd?.(this.#downstreamController, trailers)
}

onResponseError (controller, err) {
this.#writeStream?.destroy(err)
this.#writeStream = undefined
this.#handler.onResponseError?.(controller, err)
this.#handler.onResponseError?.(this.#downstreamController, err)
}
}

Expand Down
Loading
Loading