undici 7.0.0-alpha.7 → 7.0.0-alpha.9

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.
@@ -10,45 +10,15 @@ const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHea
10
10
  const { AbortError } = require('../core/errors.js')
11
11
 
12
12
  /**
13
- * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
13
+ * @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn
14
14
  */
15
- function sendGatewayTimeout (handler) {
16
- let aborted = false
17
- try {
18
- if (typeof handler.onConnect === 'function') {
19
- handler.onConnect(() => {
20
- aborted = true
21
- })
22
-
23
- if (aborted) {
24
- return
25
- }
26
- }
27
-
28
- if (typeof handler.onHeaders === 'function') {
29
- handler.onHeaders(504, [], () => {}, 'Gateway Timeout')
30
- if (aborted) {
31
- return
32
- }
33
- }
34
-
35
- if (typeof handler.onComplete === 'function') {
36
- handler.onComplete([])
37
- }
38
- } catch (err) {
39
- if (typeof handler.onError === 'function') {
40
- handler.onError(err)
41
- }
42
- }
43
- }
44
15
 
45
16
  /**
46
17
  * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
47
- * @param {number} age
48
- * @param {import('../util/cache.js').CacheControlDirectives | undefined} cacheControlDirectives
18
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
49
19
  * @returns {boolean}
50
20
  */
51
- function needsRevalidation (result, age, cacheControlDirectives) {
21
+ function needsRevalidation (result, cacheControlDirectives) {
52
22
  if (cacheControlDirectives?.['no-cache']) {
53
23
  // Always revalidate requests with the no-cache directive
54
24
  return true
@@ -81,6 +51,219 @@ function needsRevalidation (result, age, cacheControlDirectives) {
81
51
  return false
82
52
  }
83
53
 
54
+ /**
55
+ * @param {DispatchFn} dispatch
56
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
57
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
58
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
59
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
60
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
61
+ */
62
+ function handleUncachedResponse (
63
+ dispatch,
64
+ globalOpts,
65
+ cacheKey,
66
+ handler,
67
+ opts,
68
+ reqCacheControl
69
+ ) {
70
+ if (reqCacheControl?.['only-if-cached']) {
71
+ let aborted = false
72
+ try {
73
+ if (typeof handler.onConnect === 'function') {
74
+ handler.onConnect(() => {
75
+ aborted = true
76
+ })
77
+
78
+ if (aborted) {
79
+ return
80
+ }
81
+ }
82
+
83
+ if (typeof handler.onHeaders === 'function') {
84
+ handler.onHeaders(504, [], () => {}, 'Gateway Timeout')
85
+ if (aborted) {
86
+ return
87
+ }
88
+ }
89
+
90
+ if (typeof handler.onComplete === 'function') {
91
+ handler.onComplete([])
92
+ }
93
+ } catch (err) {
94
+ if (typeof handler.onError === 'function') {
95
+ handler.onError(err)
96
+ }
97
+ }
98
+
99
+ return true
100
+ }
101
+
102
+ return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
103
+ }
104
+
105
+ /**
106
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
107
+ * @param {number} age
108
+ */
109
+ function sendCachedValue (handler, opts, result, age, context) {
110
+ // TODO (perf): Readable.from path can be optimized...
111
+ const stream = util.isStream(result.body)
112
+ ? result.body
113
+ : Readable.from(result.body ?? [])
114
+
115
+ assert(!stream.destroyed, 'stream should not be destroyed')
116
+ assert(!stream.readableDidRead, 'stream should not be readableDidRead')
117
+
118
+ const controller = {
119
+ resume () {
120
+ stream.resume()
121
+ },
122
+ pause () {
123
+ stream.pause()
124
+ },
125
+ get paused () {
126
+ return stream.isPaused()
127
+ },
128
+ get aborted () {
129
+ return stream.destroyed
130
+ },
131
+ get reason () {
132
+ return stream.errored
133
+ },
134
+ abort (reason) {
135
+ stream.destroy(reason ?? new AbortError())
136
+ }
137
+ }
138
+
139
+ stream
140
+ .on('error', function (err) {
141
+ if (!this.readableEnded) {
142
+ if (typeof handler.onResponseError === 'function') {
143
+ handler.onResponseError(controller, err)
144
+ } else {
145
+ throw err
146
+ }
147
+ }
148
+ })
149
+ .on('close', function () {
150
+ if (!this.errored) {
151
+ handler.onResponseEnd?.(controller, {})
152
+ }
153
+ })
154
+
155
+ handler.onRequestStart?.(controller, context)
156
+
157
+ if (stream.destroyed) {
158
+ return
159
+ }
160
+
161
+ // Add the age header
162
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-age
163
+ // TODO (fix): What if headers.age already exists?
164
+ const headers = age != null ? { ...result.headers, age: String(age) } : result.headers
165
+
166
+ handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
167
+
168
+ if (opts.method === 'HEAD') {
169
+ stream.destroy()
170
+ } else {
171
+ stream.on('data', function (chunk) {
172
+ handler.onResponseData?.(controller, chunk)
173
+ })
174
+ }
175
+ }
176
+
177
+ /**
178
+ * @param {DispatchFn} dispatch
179
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} globalOpts
180
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
181
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
182
+ * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} opts
183
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} reqCacheControl
184
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined} result
185
+ */
186
+ function handleResult (
187
+ dispatch,
188
+ globalOpts,
189
+ cacheKey,
190
+ handler,
191
+ opts,
192
+ reqCacheControl,
193
+ result
194
+ ) {
195
+ if (!result) {
196
+ return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
197
+ }
198
+
199
+ const now = Date.now()
200
+ if (now > result.deleteAt) {
201
+ // Response is expired, cache store shouldn't have given this to us
202
+ return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
203
+ }
204
+
205
+ const age = Math.round((now - result.cachedAt) / 1000)
206
+ if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) {
207
+ // Response is considered expired for this specific request
208
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
209
+ return dispatch(opts, handler)
210
+ }
211
+
212
+ // Check if the response is stale
213
+ if (needsRevalidation(result, reqCacheControl)) {
214
+ if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
215
+ // If body is is stream we can't revalidate...
216
+ // TODO (fix): This could be less strict...
217
+ return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
218
+ }
219
+
220
+ let withinStaleIfErrorThreshold = false
221
+ const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
222
+ if (staleIfErrorExpiry) {
223
+ withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
224
+ }
225
+
226
+ let headers = {
227
+ ...opts.headers,
228
+ 'if-modified-since': new Date(result.cachedAt).toUTCString(),
229
+ 'if-none-match': result.etag
230
+ }
231
+
232
+ if (result.vary) {
233
+ headers = {
234
+ ...headers,
235
+ ...result.vary
236
+ }
237
+ }
238
+
239
+ // We need to revalidate the response
240
+ return dispatch(
241
+ {
242
+ ...opts,
243
+ headers
244
+ },
245
+ new CacheRevalidationHandler(
246
+ (success, context) => {
247
+ if (success) {
248
+ sendCachedValue(handler, opts, result, age, context)
249
+ } else if (util.isStream(result.body)) {
250
+ result.body.on('error', () => {}).destroy()
251
+ }
252
+ },
253
+ new CacheHandler(globalOpts, cacheKey, handler),
254
+ withinStaleIfErrorThreshold
255
+ )
256
+ )
257
+ }
258
+
259
+ // Dump request body.
260
+ if (util.isStream(opts.body)) {
261
+ opts.body.on('error', () => {}).destroy()
262
+ }
263
+
264
+ sendCachedValue(handler, opts, result, age, null)
265
+ }
266
+
84
267
  /**
85
268
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
86
269
  * @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
@@ -88,7 +271,9 @@ function needsRevalidation (result, age, cacheControlDirectives) {
88
271
  module.exports = (opts = {}) => {
89
272
  const {
90
273
  store = new MemoryCacheStore(),
91
- methods = ['GET']
274
+ methods = ['GET'],
275
+ cacheByDefault = undefined,
276
+ type = 'shared'
92
277
  } = opts
93
278
 
94
279
  if (typeof opts !== 'object' || opts === null) {
@@ -98,28 +283,35 @@ module.exports = (opts = {}) => {
98
283
  assertCacheStore(store, 'opts.store')
99
284
  assertCacheMethods(methods, 'opts.methods')
100
285
 
286
+ if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') {
287
+ throw new TypeError(`exepcted opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
288
+ }
289
+
290
+ if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') {
291
+ throw new TypeError(`exepcted opts.type to be shared, private, or undefined, got ${typeof type}`)
292
+ }
293
+
101
294
  const globalOpts = {
102
295
  store,
103
- methods
296
+ methods,
297
+ cacheByDefault,
298
+ type
104
299
  }
105
300
 
106
301
  const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
107
302
 
108
303
  return dispatch => {
109
304
  return (opts, handler) => {
110
- // TODO (fix): What if e.g. opts.headers has if-modified-since header? Or other headers
111
- // that make things ambigious?
112
-
113
305
  if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
114
306
  // Not a method we want to cache or we don't have the origin, skip
115
307
  return dispatch(opts, handler)
116
308
  }
117
309
 
118
- const requestCacheControl = opts.headers?.['cache-control']
310
+ const reqCacheControl = opts.headers?.['cache-control']
119
311
  ? parseCacheControlHeader(opts.headers['cache-control'])
120
312
  : undefined
121
313
 
122
- if (requestCacheControl?.['no-store']) {
314
+ if (reqCacheControl?.['no-store']) {
123
315
  return dispatch(opts, handler)
124
316
  }
125
317
 
@@ -127,172 +319,29 @@ module.exports = (opts = {}) => {
127
319
  * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
128
320
  */
129
321
  const cacheKey = makeCacheKey(opts)
130
-
131
- // TODO (perf): For small entries support returning a Buffer instead of a stream.
132
- // Maybe store should return { staleAt, headers, body, etc... } instead of a stream + stream.value?
133
- // Where body can be a Buffer, string, stream or blob?
134
322
  const result = store.get(cacheKey)
135
- if (!result) {
136
- if (requestCacheControl?.['only-if-cached']) {
137
- // We only want cached responses
138
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
139
- sendGatewayTimeout(handler)
140
- return true
141
- }
142
-
143
- return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
144
- }
145
-
146
- /**
147
- * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
148
- * @param {number} age
149
- */
150
- const respondWithCachedValue = ({ headers, statusCode, statusMessage, body }, age, context) => {
151
- const stream = util.isStream(body)
152
- ? body
153
- : Readable.from(body ?? [])
154
-
155
- assert(!stream.destroyed, 'stream should not be destroyed')
156
- assert(!stream.readableDidRead, 'stream should not be readableDidRead')
157
-
158
- const controller = {
159
- resume () {
160
- stream.resume()
161
- },
162
- pause () {
163
- stream.pause()
164
- },
165
- get paused () {
166
- return stream.isPaused()
167
- },
168
- get aborted () {
169
- return stream.destroyed
170
- },
171
- get reason () {
172
- return stream.errored
173
- },
174
- abort (reason) {
175
- stream.destroy(reason ?? new AbortError())
176
- }
177
- }
178
-
179
- stream
180
- .on('error', function (err) {
181
- if (!this.readableEnded) {
182
- if (typeof handler.onResponseError === 'function') {
183
- handler.onResponseError(controller, err)
184
- } else {
185
- throw err
186
- }
187
- }
188
- })
189
- .on('close', function () {
190
- if (!this.errored) {
191
- handler.onResponseEnd?.(controller, {})
192
- }
193
- })
194
-
195
- handler.onRequestStart?.(controller, context)
196
-
197
- if (stream.destroyed) {
198
- return
199
- }
200
-
201
- // Add the age header
202
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-age
203
- // TODO (fix): What if headers.age already exists?
204
- headers = age != null ? { ...headers, age: String(age) } : headers
205
-
206
- handler.onResponseStart?.(controller, statusCode, statusMessage, headers)
207
-
208
- if (opts.method === 'HEAD') {
209
- stream.destroy()
210
- } else {
211
- stream.on('data', function (chunk) {
212
- handler.onResponseData?.(controller, chunk)
213
- })
214
- }
215
- }
216
-
217
- /**
218
- * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
219
- */
220
- const handleResult = (result) => {
221
- // TODO (perf): Readable.from path can be optimized...
222
-
223
- if (!result.body && opts.method !== 'HEAD') {
224
- throw new Error('stream is undefined but method isn\'t HEAD')
225
- }
226
-
227
- const age = Math.round((Date.now() - result.cachedAt) / 1000)
228
- if (requestCacheControl?.['max-age'] && age >= requestCacheControl['max-age']) {
229
- // Response is considered expired for this specific request
230
- // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
231
- return dispatch(opts, handler)
232
- }
233
-
234
- // Check if the response is stale
235
- if (needsRevalidation(result, age, requestCacheControl)) {
236
- if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
237
- // If body is is stream we can't revalidate...
238
- // TODO (fix): This could be less strict...
239
- return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
240
- }
241
323
 
242
- // We need to revalidate the response
243
- return dispatch(
244
- {
245
- ...opts,
246
- headers: {
247
- ...opts.headers,
248
- 'if-modified-since': new Date(result.cachedAt).toUTCString(),
249
- etag: result.etag
250
- }
251
- },
252
- new CacheRevalidationHandler(
253
- (success, context) => {
254
- if (success) {
255
- respondWithCachedValue(result, age, context)
256
- } else if (util.isStream(result.body)) {
257
- result.body.on('error', () => {}).destroy()
258
- }
259
- },
260
- new CacheHandler(globalOpts, cacheKey, handler)
261
- )
324
+ if (result && typeof result.then === 'function') {
325
+ result.then(result => {
326
+ handleResult(dispatch,
327
+ globalOpts,
328
+ cacheKey,
329
+ handler,
330
+ opts,
331
+ reqCacheControl,
332
+ result
262
333
  )
263
- }
264
-
265
- // Dump request body.
266
- if (util.isStream(opts.body)) {
267
- opts.body.on('error', () => {}).destroy()
268
- }
269
-
270
- respondWithCachedValue(result, age, null)
271
- }
272
-
273
- if (typeof result.then === 'function') {
274
- result.then((result) => {
275
- if (!result) {
276
- if (requestCacheControl?.['only-if-cached']) {
277
- // We only want cached responses
278
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
279
- sendGatewayTimeout(handler)
280
- return true
281
- }
282
-
283
- dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
284
- } else {
285
- handleResult(result)
286
- }
287
- }, err => {
288
- if (typeof handler.onError === 'function') {
289
- handler.onError(err)
290
- } else {
291
- throw err
292
- }
293
334
  })
294
335
  } else {
295
- handleResult(result)
336
+ handleResult(
337
+ dispatch,
338
+ globalOpts,
339
+ cacheKey,
340
+ handler,
341
+ opts,
342
+ reqCacheControl,
343
+ result
344
+ )
296
345
  }
297
346
 
298
347
  return true
@@ -4,7 +4,7 @@ const { parseHeaders } = require('../core/util')
4
4
  const DecoratorHandler = require('../handler/decorator-handler')
5
5
  const { ResponseError } = require('../core/errors')
6
6
 
7
- class Handler extends DecoratorHandler {
7
+ class ResponseErrorHandler extends DecoratorHandler {
8
8
  #handler
9
9
  #statusCode
10
10
  #contentType
@@ -66,7 +66,7 @@ class Handler extends DecoratorHandler {
66
66
  Error.stackTraceLimit = 0
67
67
  try {
68
68
  err = new ResponseError('Response Error', this.#statusCode, {
69
- data: this.#body,
69
+ body: this.#body,
70
70
  headers: this.#headers
71
71
  })
72
72
  } finally {
@@ -84,6 +84,10 @@ class Handler extends DecoratorHandler {
84
84
  }
85
85
  }
86
86
 
87
- module.exports = (dispatch) => (opts, handler) => opts.throwOnError
88
- ? dispatch(opts, new Handler(opts, { handler }))
89
- : dispatch(opts, handler)
87
+ module.exports = () => {
88
+ return (dispatch) => {
89
+ return function Intercept (opts, handler) {
90
+ return dispatch(opts, new ResponseErrorHandler(opts, { handler }))
91
+ }
92
+ }
93
+ }
package/lib/util/cache.js CHANGED
@@ -96,36 +96,27 @@ function assertCacheValue (value) {
96
96
  /**
97
97
  * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-cache-control
98
98
  * @see https://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml
99
- *
100
- * @typedef {{
101
- * 'max-stale'?: number;
102
- * 'min-fresh'?: number;
103
- * 'max-age'?: number;
104
- * 's-maxage'?: number;
105
- * 'stale-while-revalidate'?: number;
106
- * 'stale-if-error'?: number;
107
- * public?: true;
108
- * private?: true | string[];
109
- * 'no-store'?: true;
110
- * 'no-cache'?: true | string[];
111
- * 'must-revalidate'?: true;
112
- * 'proxy-revalidate'?: true;
113
- * immutable?: true;
114
- * 'no-transform'?: true;
115
- * 'must-understand'?: true;
116
- * 'only-if-cached'?: true;
117
- * }} CacheControlDirectives
118
- *
99
+
119
100
  * @param {string | string[]} header
120
- * @returns {CacheControlDirectives}
101
+ * @returns {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives}
121
102
  */
122
103
  function parseCacheControlHeader (header) {
123
104
  /**
124
- * @type {import('../util/cache.js').CacheControlDirectives}
105
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives}
125
106
  */
126
107
  const output = {}
127
108
 
128
- const directives = Array.isArray(header) ? header : header.split(',')
109
+ let directives
110
+ if (Array.isArray(header)) {
111
+ directives = []
112
+
113
+ for (const directive of header) {
114
+ directives.push(...directive.split(','))
115
+ }
116
+ } else {
117
+ directives = header.split(',')
118
+ }
119
+
129
120
  for (let i = 0; i < directives.length; i++) {
130
121
  const directive = directives[i].toLowerCase()
131
122
  const keyValueDelimiter = directive.indexOf('=')
@@ -133,10 +124,8 @@ function parseCacheControlHeader (header) {
133
124
  let key
134
125
  let value
135
126
  if (keyValueDelimiter !== -1) {
136
- key = directive.substring(0, keyValueDelimiter).trim()
137
- value = directive
138
- .substring(keyValueDelimiter + 1)
139
- .trim()
127
+ key = directive.substring(0, keyValueDelimiter).trimStart()
128
+ value = directive.substring(keyValueDelimiter + 1)
140
129
  } else {
141
130
  key = directive.trim()
142
131
  }
@@ -148,16 +137,28 @@ function parseCacheControlHeader (header) {
148
137
  case 's-maxage':
149
138
  case 'stale-while-revalidate':
150
139
  case 'stale-if-error': {
151
- if (value === undefined) {
140
+ if (value === undefined || value[0] === ' ') {
152
141
  continue
153
142
  }
154
143
 
144
+ if (
145
+ value.length >= 2 &&
146
+ value[0] === '"' &&
147
+ value[value.length - 1] === '"'
148
+ ) {
149
+ value = value.substring(1, value.length - 1)
150
+ }
151
+
155
152
  const parsedValue = parseInt(value, 10)
156
153
  // eslint-disable-next-line no-self-compare
157
154
  if (parsedValue !== parsedValue) {
158
155
  continue
159
156
  }
160
157
 
158
+ if (key === 'max-age' && key in output && output[key] >= parsedValue) {
159
+ continue
160
+ }
161
+
161
162
  output[key] = parsedValue
162
163
 
163
164
  break
@@ -206,11 +207,19 @@ function parseCacheControlHeader (header) {
206
207
  headers[headers.length - 1] = lastHeader
207
208
  }
208
209
 
209
- output[key] = headers
210
+ if (key in output) {
211
+ output[key] = output[key].concat(headers)
212
+ } else {
213
+ output[key] = headers
214
+ }
210
215
  }
211
216
  } else {
212
217
  // Something like `no-cache=some-header`
213
- output[key] = [value]
218
+ if (key in output) {
219
+ output[key] = output[key].concat(value)
220
+ } else {
221
+ output[key] = [value]
222
+ }
214
223
  }
215
224
 
216
225
  break
@@ -248,7 +257,7 @@ function parseCacheControlHeader (header) {
248
257
  * @returns {Record<string, string | string[]>}
249
258
  */
250
259
  function parseVaryHeader (varyHeader, headers) {
251
- if (typeof varyHeader === 'string' && varyHeader === '*') {
260
+ if (typeof varyHeader === 'string' && varyHeader.includes('*')) {
252
261
  return headers
253
262
  }
254
263
 
@@ -262,6 +271,8 @@ function parseVaryHeader (varyHeader, headers) {
262
271
 
263
272
  if (headers[trimmedHeader]) {
264
273
  output[trimmedHeader] = headers[trimmedHeader]
274
+ } else {
275
+ return undefined
265
276
  }
266
277
  }
267
278