Skip to content

Commit 28b10fa

Browse files
authored
fix: cache fixes (#3830)
1 parent 51836bb commit 28b10fa

File tree

8 files changed

+121
-151
lines changed

8 files changed

+121
-151
lines changed

lib/cache/memory-cache-store.js

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
const { Writable, Readable } = require('node:stream')
3+
const { Writable } = require('node:stream')
44

55
/**
66
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
@@ -81,24 +81,7 @@ class MemoryCacheStore {
8181
return undefined
8282
}
8383

84-
/**
85-
* @type {Readable | undefined}
86-
*/
87-
let readable
88-
if (value.body) {
89-
readable = new Readable()
90-
91-
for (const chunk of value.body) {
92-
readable.push(chunk)
93-
}
94-
95-
readable.push(null)
96-
}
97-
98-
return {
99-
response: value.opts,
100-
body: readable
101-
}
84+
return { ...value.opts, body: value.body }
10285
}
10386

10487
/**
@@ -242,7 +225,7 @@ class MemoryCacheStore {
242225
/**
243226
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
244227
*/
245-
deleteByKey (key) {
228+
delete (key) {
246229
this.#data.delete(`${key.origin}:${key.path}`)
247230
}
248231

lib/handler/cache-handler.js

Lines changed: 25 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class CacheHandler extends DecoratorHandler {
9494
) {
9595
// https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
9696
try {
97-
this.#store.deleteByKey(this.#cacheKey).catch?.(noop)
97+
this.#store.delete(this.#cacheKey).catch?.(noop)
9898
} catch {
9999
// Fail silently
100100
}
@@ -135,43 +135,31 @@ class CacheHandler extends DecoratorHandler {
135135
cacheControlDirectives
136136
)
137137

138-
if (this.#cacheKey.method === 'HEAD') {
139-
this.#store.createWriteStream(this.#cacheKey, {
140-
statusCode,
141-
statusMessage,
142-
rawHeaders: strippedHeaders,
143-
vary: varyDirectives,
144-
cachedAt: now,
145-
staleAt,
146-
deleteAt
147-
})
148-
} else {
149-
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, {
150-
statusCode,
151-
statusMessage,
152-
rawHeaders: strippedHeaders,
153-
vary: varyDirectives,
154-
cachedAt: now,
155-
staleAt,
156-
deleteAt
157-
})
158-
159-
if (this.#writeStream) {
160-
const handler = this
161-
this.#writeStream
162-
.on('drain', resume)
163-
.on('error', function () {
138+
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, {
139+
statusCode,
140+
statusMessage,
141+
rawHeaders: strippedHeaders,
142+
vary: varyDirectives,
143+
cachedAt: now,
144+
staleAt,
145+
deleteAt
146+
})
147+
148+
if (this.#writeStream) {
149+
const handler = this
150+
this.#writeStream
151+
.on('drain', resume)
152+
.on('error', function () {
164153
// TODO (fix): Make error somehow observable?
165-
})
166-
.on('close', function () {
167-
if (handler.#writeStream === this) {
168-
handler.#writeStream = undefined
169-
}
170-
171-
// TODO (fix): Should we resume even if was paused downstream?
172-
resume()
173-
})
174-
}
154+
})
155+
.on('close', function () {
156+
if (handler.#writeStream === this) {
157+
handler.#writeStream = undefined
158+
}
159+
160+
// TODO (fix): Should we resume even if was paused downstream?
161+
resume()
162+
})
175163
}
176164
}
177165

lib/interceptor/cache.js

Lines changed: 67 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
const assert = require('node:assert')
4+
const { Readable } = require('node:stream')
45
const util = require('../core/util')
56
const CacheHandler = require('../handler/cache-handler')
67
const MemoryCacheStore = require('../cache/memory-cache-store')
@@ -57,95 +58,88 @@ module.exports = (opts = {}) => {
5758
// Where body can be a Buffer, string, stream or blob?
5859
const result = store.get(cacheKey)
5960
if (!result) {
60-
// Request isn't cached
6161
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
6262
}
6363

6464
/**
65-
* @param {import('node:stream').Readable | undefined} stream
66-
* @param {import('../../types/cache-interceptor.d.ts').default.CachedResponse} value
65+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
6766
*/
68-
const respondWithCachedValue = (stream, value) => {
69-
assert(!stream || !stream.destroyed, 'stream should not be destroyed')
70-
assert(!stream || !stream.readableDidRead, 'stream should not be readableDidRead')
71-
try {
72-
stream
73-
?.on('error', function (err) {
74-
if (!this.readableEnded) {
75-
if (typeof handler.onError === 'function') {
76-
handler.onError(err)
77-
} else {
78-
process.nextTick(() => {
79-
throw err
80-
})
81-
}
82-
}
83-
})
84-
.on('close', function () {
85-
if (!this.errored && typeof handler.onComplete === 'function') {
86-
handler.onComplete([])
67+
const respondWithCachedValue = ({ cachedAt, rawHeaders, statusCode, statusMessage, body }) => {
68+
const stream = util.isStream(body)
69+
? body
70+
: Readable.from(body ?? [])
71+
72+
assert(!stream.destroyed, 'stream should not be destroyed')
73+
assert(!stream.readableDidRead, 'stream should not be readableDidRead')
74+
75+
stream
76+
.on('error', function (err) {
77+
if (!this.readableEnded) {
78+
if (typeof handler.onError === 'function') {
79+
handler.onError(err)
80+
} else {
81+
throw err
8782
}
88-
})
83+
}
84+
})
85+
.on('close', function () {
86+
if (!this.errored && typeof handler.onComplete === 'function') {
87+
handler.onComplete([])
88+
}
89+
})
8990

90-
if (typeof handler.onConnect === 'function') {
91-
handler.onConnect((err) => {
92-
stream?.destroy(err)
93-
})
91+
if (typeof handler.onConnect === 'function') {
92+
handler.onConnect((err) => {
93+
stream.destroy(err)
94+
})
9495

95-
if (stream?.destroyed) {
96-
return
97-
}
96+
if (stream.destroyed) {
97+
return
9898
}
99+
}
99100

100-
if (typeof handler.onHeaders === 'function') {
101-
// Add the age header
102-
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
103-
const age = Math.round((Date.now() - value.cachedAt) / 1000)
101+
if (typeof handler.onHeaders === 'function') {
102+
// Add the age header
103+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
104+
const age = Math.round((Date.now() - cachedAt) / 1000)
104105

105-
// TODO (fix): What if rawHeaders already contains age header?
106-
const rawHeaders = [...value.rawHeaders, AGE_HEADER, Buffer.from(`${age}`)]
106+
// TODO (fix): What if rawHeaders already contains age header?
107+
rawHeaders = [...rawHeaders, AGE_HEADER, Buffer.from(`${age}`)]
107108

108-
if (handler.onHeaders(value.statusCode, rawHeaders, () => stream?.resume(), value.statusMessage) === false) {
109-
stream?.pause()
110-
}
109+
if (handler.onHeaders(statusCode, rawHeaders, () => stream?.resume(), statusMessage) === false) {
110+
stream.pause()
111111
}
112+
}
112113

113-
if (opts.method === 'HEAD') {
114-
if (typeof handler.onComplete === 'function') {
115-
handler.onComplete([])
114+
if (opts.method === 'HEAD') {
115+
stream.destroy()
116+
} else {
117+
stream.on('data', function (chunk) {
118+
if (typeof handler.onData === 'function' && !handler.onData(chunk)) {
119+
stream.pause()
116120
}
117-
118-
stream?.destroy()
119-
} else {
120-
stream.on('data', function (chunk) {
121-
if (typeof handler.onData === 'function' && !handler.onData(chunk)) {
122-
stream.pause()
123-
}
124-
})
125-
}
126-
} catch (err) {
127-
stream?.destroy(err)
121+
})
128122
}
129123
}
130124

131125
/**
132126
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
133127
*/
134-
const handleStream = (result) => {
135-
const { response: value, body: stream } = result
128+
const handleResult = (result) => {
129+
// TODO (perf): Readable.from path can be optimized...
136130

137-
if (!stream && opts.method !== 'HEAD') {
131+
if (!result.body && opts.method !== 'HEAD') {
138132
throw new Error('stream is undefined but method isn\'t HEAD')
139133
}
140134

141135
// Check if the response is stale
142136
const now = Date.now()
143-
if (now < value.staleAt) {
137+
if (now < result.staleAt) {
144138
// Dump request body.
145139
if (util.isStream(opts.body)) {
146140
opts.body.on('error', () => {}).destroy()
147141
}
148-
respondWithCachedValue(stream, value)
142+
respondWithCachedValue(result)
149143
} else if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
150144
// If body is is stream we can't revalidate...
151145
// TODO (fix): This could be less strict...
@@ -157,15 +151,15 @@ module.exports = (opts = {}) => {
157151
...opts,
158152
headers: {
159153
...opts.headers,
160-
'if-modified-since': new Date(value.cachedAt).toUTCString()
154+
'if-modified-since': new Date(result.cachedAt).toUTCString()
161155
}
162156
},
163157
new CacheRevalidationHandler(
164158
(success) => {
165159
if (success) {
166-
respondWithCachedValue(stream, value)
167-
} else {
168-
stream.on('error', () => {}).destroy()
160+
respondWithCachedValue(result)
161+
} else if (util.isStream(result.body)) {
162+
result.body.on('error', () => {}).destroy()
169163
}
170164
},
171165
new CacheHandler(globalOpts, cacheKey, handler)
@@ -177,14 +171,19 @@ module.exports = (opts = {}) => {
177171
if (typeof result.then === 'function') {
178172
result.then((result) => {
179173
if (!result) {
180-
// Request isn't cached
181-
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
174+
dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
175+
} else {
176+
handleResult(result)
182177
}
183-
184-
handleStream(result)
185-
}).catch(err => handler.onError(err))
178+
}, err => {
179+
if (typeof handler.onError === 'function') {
180+
handler.onError(err)
181+
} else {
182+
throw err
183+
}
184+
})
186185
} else {
187-
handleStream(result)
186+
handleResult(result)
188187
}
189188

190189
return true

lib/util/cache.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ function assertCacheStore (store, name = 'CacheStore') {
210210
throw new TypeError(`expected type of ${name} to be a CacheStore, got ${store === null ? 'null' : typeof store}`)
211211
}
212212

213-
for (const fn of ['get', 'createWriteStream', 'deleteByKey']) {
213+
for (const fn of ['get', 'createWriteStream', 'delete']) {
214214
if (typeof store[fn] !== 'function') {
215215
throw new TypeError(`${name} needs to have a \`${fn}()\` function`)
216216
}

test/cache-interceptor/cache-stores.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const { describe, test } = require('node:test')
44
const { deepStrictEqual, notEqual, equal } = require('node:assert')
5+
const { Readable } = require('node:stream')
56
const { once } = require('node:events')
67
const MemoryCacheStore = require('../../lib/cache/memory-cache-store')
78

@@ -17,7 +18,7 @@ function cacheStoreTests (CacheStore) {
1718
equal(typeof store.isFull, 'boolean')
1819
equal(typeof store.get, 'function')
1920
equal(typeof store.createWriteStream, 'function')
20-
equal(typeof store.deleteByKey, 'function')
21+
equal(typeof store.delete, 'function')
2122
})
2223

2324
// Checks that it can store & fetch different responses
@@ -268,9 +269,11 @@ function writeResponse (stream, body) {
268269
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
269270
* @returns {Promise<import('../../types/cache-interceptor.d.ts').default.GetResult | { body: Buffer[] }>}
270271
*/
271-
async function readResponse ({ response, body: stream }) {
272+
async function readResponse ({ body: src, ...response }) {
272273
notEqual(response, undefined)
273-
notEqual(stream, undefined)
274+
notEqual(src, undefined)
275+
276+
const stream = Readable.from(src ?? [])
274277

275278
/**
276279
* @type {Buffer[]}

0 commit comments

Comments
 (0)