@nxtedition/lib 15.0.28 → 15.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -74,6 +74,7 @@ const dispatchers = {
74
74
  responseRetry: require('./interceptor/response-retry.js'),
75
75
  signal: require('./interceptor/signal.js'),
76
76
  proxy: require('./interceptor/proxy.js'),
77
+ cache: require('./interceptor/cache.js'),
77
78
  }
78
79
 
79
80
  async function request(urlOrOpts, opts = {}) {
@@ -136,6 +137,7 @@ async function request(urlOrOpts, opts = {}) {
136
137
  dispatch = dispatchers.redirect(dispatch)
137
138
  dispatch = dispatchers.signal(dispatch)
138
139
  dispatch = dispatchers.proxy(dispatch)
140
+ dispatch = dispatchers.cache(dispatch)
139
141
 
140
142
  dispatch(opts, {
141
143
  resolve,
@@ -0,0 +1,197 @@
1
+ import assert from 'node:assert'
2
+ import { LRUCache } from 'lru-cache'
3
+ import cacheControlParser from 'cache-control-parser'
4
+
5
+ class CacheHandler {
6
+ constructor({ key, handler, store }) {
7
+ this.key = key
8
+ this.handler = handler
9
+ this.store = store
10
+ this.value = null
11
+ }
12
+
13
+ onConnect(abort) {
14
+ return this.handler.onConnect(abort)
15
+ }
16
+
17
+ onHeaders(statusCode, rawHeaders, resume, statusMessage) {
18
+ // NOTE: Only cache 307 respones for now...
19
+ if (statusCode !== 307) {
20
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
21
+ }
22
+
23
+ let cacheControl
24
+ for (let n = 0; n < rawHeaders.length; n += 2) {
25
+ if (
26
+ rawHeaders[n].length === 'cache-control'.length &&
27
+ rawHeaders[n].toString().toLowerCase() === 'cache-control'
28
+ ) {
29
+ cacheControl = cacheControlParser.parse(rawHeaders[n + 1].toString())
30
+ break
31
+ }
32
+ }
33
+
34
+ if (
35
+ cacheControl &&
36
+ cacheControl.public &&
37
+ !cacheControl.private &&
38
+ !cacheControl['no-store'] &&
39
+ // TODO (fix): Support all cache control directives...
40
+ // !opts.headers['no-transform'] &&
41
+ !cacheControl['no-cache'] &&
42
+ !cacheControl['must-understand'] &&
43
+ !cacheControl['must-revalidate'] &&
44
+ !cacheControl['proxy-revalidate']
45
+ ) {
46
+ const maxAge = cacheControl['s-max-age'] ?? cacheControl['max-age']
47
+ const ttl = cacheControl.immutable
48
+ ? 31556952 // 1 year
49
+ : Number(maxAge)
50
+
51
+ if (ttl > 0) {
52
+ this.value = {
53
+ statusCode,
54
+ statusMessage,
55
+ rawHeaders,
56
+ rawTrailers: null,
57
+ body: [],
58
+ size: 0,
59
+ ttl: ttl * 1e3,
60
+ }
61
+ }
62
+ }
63
+
64
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
65
+ }
66
+
67
+ onData(chunk) {
68
+ if (this.value) {
69
+ this.value.size += chunk.bodyLength
70
+ if (this.value.size > this.store.maxEntrySize) {
71
+ this.value = null
72
+ } else {
73
+ this.value.body.push(chunk)
74
+ }
75
+ }
76
+ return this.handler.onData(chunk)
77
+ }
78
+
79
+ onComplete(rawTrailers) {
80
+ if (this.value) {
81
+ this.value.rawTrailers = rawTrailers
82
+ this.store.set(this.key, this.value, this.value.ttl)
83
+ }
84
+ return this.handler.onComplete(rawTrailers)
85
+ }
86
+
87
+ onError(err) {
88
+ return this.handler.onError(err)
89
+ }
90
+ }
91
+
92
+ // TODO (fix): Async filesystem cache.
93
+ class CacheStore {
94
+ constructor({ maxSize, maxEntrySize }) {
95
+ this.maxSize = maxSize
96
+ this.maxEntrySize = maxEntrySize
97
+ this.cache = new LRUCache({
98
+ maxSize,
99
+ sizeCalculation: (value) => value.body.byteLength,
100
+ })
101
+ }
102
+
103
+ set(key, value, ttl) {
104
+ this.cache.set(key, value, ttl)
105
+ }
106
+
107
+ get(key) {
108
+ return this.cache.get(key)
109
+ }
110
+ }
111
+
112
+ function makeKey(opts) {
113
+ // NOTE: Ignores headers...
114
+ return `${opts.method}:${opts.path}`
115
+ }
116
+
117
+ const DEFAULT_CACHE_STORE = new CacheStore({ maxSize: 128 * 1024, maxEntrySize: 1024 })
118
+
119
+ module.exports = (dispatch) => (opts, handler) => {
120
+ if (!opts.cache) {
121
+ return dispatch(opts, handler)
122
+ }
123
+
124
+ if (opts.method !== 'GET' && opts.method !== 'HEAD') {
125
+ dispatch(opts, handler)
126
+ return
127
+ }
128
+
129
+ if (opts.headers?.['cache-control'] || opts.headers?.authorization) {
130
+ // TODO (fix): Support all cache control directives...
131
+ // const cacheControl = cacheControlParser.parse(opts.headers['cache-control'])
132
+ // cacheControl['no-cache']
133
+ // cacheControl['no-store']
134
+ // cacheControl['max-age']
135
+ // cacheControl['max-stale']
136
+ // cacheControl['min-fresh']
137
+ // cacheControl['no-transform']
138
+ // cacheControl['only-if-cached']
139
+ dispatch(opts, handler)
140
+ return
141
+ }
142
+
143
+ // TODO (fix): Support body...
144
+ assert(opts.method === 'GET' || opts.method === 'HEAD')
145
+ // Dump body...
146
+ opts.body.on('error', () => {}).resume()
147
+
148
+ const store = opts.cache === true ? DEFAULT_CACHE_STORE : opts.cache
149
+
150
+ if (!store) {
151
+ throw new Error(`Cache store not provided.`)
152
+ }
153
+
154
+ let key = makeKey(opts)
155
+ let value = store.get(key)
156
+
157
+ if (value == null && opts.method === 'HEAD') {
158
+ key = makeKey({ ...opts, method: 'GET' })
159
+ value = store.get(key)
160
+ }
161
+
162
+ if (value) {
163
+ const { statusCode, statusMessage, rawHeaders, rawTrailers, body } = value
164
+ const ac = new AbortController()
165
+ const signal = ac.signal
166
+
167
+ const resume = () => {}
168
+ const abort = () => {
169
+ ac.abort()
170
+ }
171
+
172
+ try {
173
+ handler.onConnect(abort)
174
+ signal.throwIfAborted()
175
+ handler.onHeaders(statusCode, rawHeaders, resume, statusMessage)
176
+ signal.throwIfAborted()
177
+ if (opts.method !== 'HEAD') {
178
+ for (const chunk of body) {
179
+ const ret = handler.onData(chunk)
180
+ signal.throwIfAborted()
181
+ if (ret === false) {
182
+ // TODO (fix): back pressure...
183
+ }
184
+ }
185
+ handler.onComplete(rawTrailers)
186
+ signal.throwIfAborted()
187
+ } else {
188
+ handler.onComplete([])
189
+ signal.throwIfAborted()
190
+ }
191
+ } catch (err) {
192
+ handler.onError(err)
193
+ }
194
+ } else {
195
+ dispatch(opts, new CacheHandler({ handler, store, key: makeKey(opts) }))
196
+ }
197
+ }
@@ -4,32 +4,11 @@ const net = require('net')
4
4
  class Handler {
5
5
  constructor(opts, { handler }) {
6
6
  this.handler = handler
7
- this.opts = {
8
- ...opts,
9
- headers: reduceHeaders(
10
- {
11
- headers: opts.headers ?? {},
12
- httpVersion: opts.proxy.httpVersion ?? opts.proxy.req?.httpVersion,
13
- socket: opts.proxy.socket ?? opts.proxy.req?.socket,
14
- proxyName: opts.proxy.name,
15
- },
16
- (obj, key, val) => {
17
- obj[key] = val
18
- return obj
19
- },
20
- {},
21
- ),
22
- }
23
- this.abort = null
24
- this.aborted = false
7
+ this.opts = opts
25
8
  }
26
9
 
27
10
  onConnect(abort) {
28
- this.abort = abort
29
- this.handler.onConnect?.((reason) => {
30
- this.aborted = true
31
- this.abort(reason)
32
- })
11
+ return this.handler.onConnect?.(abort)
33
12
  }
34
13
 
35
14
  onBodySent(chunk) {
@@ -70,8 +49,30 @@ class Handler {
70
49
  }
71
50
  }
72
51
 
73
- module.exports = (dispatch) => (opts, handler) =>
74
- opts.proxy ? dispatch(opts, new Handler(opts, { handler })) : dispatch(opts, handler)
52
+ module.exports = (dispatch) => (opts, handler) => {
53
+ if (!opts.proxy) {
54
+ return dispatch(opts, handler)
55
+ }
56
+
57
+ opts = {
58
+ ...opts,
59
+ headers: reduceHeaders(
60
+ {
61
+ headers: opts.headers ?? {},
62
+ httpVersion: opts.proxy.httpVersion ?? opts.proxy.req?.httpVersion,
63
+ socket: opts.proxy.socket ?? opts.proxy.req?.socket,
64
+ proxyName: opts.proxy.name,
65
+ },
66
+ (obj, key, val) => {
67
+ obj[key] = val
68
+ return obj
69
+ },
70
+ {},
71
+ ),
72
+ }
73
+
74
+ return dispatch(opts, new Handler(opts, { handler }))
75
+ }
75
76
 
76
77
  // This expression matches hop-by-hop headers.
77
78
  // These headers are meaningful only for a single transport-level connection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "15.0.28",
3
+ "version": "15.0.29",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "files": [