@johntalton/http-util 7.0.1 → 7.0.2

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.
Files changed (47) hide show
  1. package/package.json +1 -1
  2. package/src/body.js +11 -6
  3. package/src/defs.js +13 -4
  4. package/src/headers/accept-encoding.js +28 -5
  5. package/src/headers/accept-language.js +29 -5
  6. package/src/headers/accept.js +77 -32
  7. package/src/headers/cache-control.js +6 -3
  8. package/src/headers/client-hints.js +4 -3
  9. package/src/headers/conditional.js +17 -17
  10. package/src/headers/content-type.js +1 -1
  11. package/src/headers/link.js +8 -3
  12. package/src/headers/multipart.js +14 -6
  13. package/src/headers/range.js +2 -2
  14. package/src/headers/rate-limit.js +19 -3
  15. package/src/headers/server-timing.js +5 -3
  16. package/src/headers/util/kvp.js +2 -1
  17. package/src/headers/util/mime.js +16 -0
  18. package/src/headers/util/quote.js +1 -1
  19. package/src/headers/www-authenticate.js +35 -10
  20. package/src/response/2xx/accepted.js +2 -2
  21. package/src/response/2xx/created.js +3 -3
  22. package/src/response/2xx/no-content.js +3 -3
  23. package/src/response/2xx/preflight.js +9 -9
  24. package/src/response/3xx/found.js +3 -3
  25. package/src/response/3xx/moved-permanently.js +3 -3
  26. package/src/response/3xx/multiple-choices.js +3 -3
  27. package/src/response/3xx/not-modified.js +12 -5
  28. package/src/response/3xx/permanent-redirect.js +3 -3
  29. package/src/response/3xx/see-other.js +3 -3
  30. package/src/response/3xx/temporary-redirect.js +3 -3
  31. package/src/response/4xx/bad-request.js +1 -2
  32. package/src/response/4xx/conflict.js +2 -2
  33. package/src/response/4xx/content-too-large.js +2 -2
  34. package/src/response/4xx/forbidden.js +3 -3
  35. package/src/response/4xx/gone.js +2 -2
  36. package/src/response/4xx/im-a-teapot.js +2 -2
  37. package/src/response/4xx/not-allowed.js +5 -4
  38. package/src/response/4xx/payment-required.js +3 -3
  39. package/src/response/4xx/precondition-failed.js +3 -3
  40. package/src/response/4xx/range-not-satisfiable.js +3 -3
  41. package/src/response/4xx/timeout.js +3 -3
  42. package/src/response/4xx/too-many-requests.js +2 -5
  43. package/src/response/4xx/unauthorized.js +4 -4
  44. package/src/response/4xx/unprocessable.js +2 -2
  45. package/src/response/4xx/unsupported-media.js +15 -9
  46. package/src/response/header-util.js +8 -4
  47. package/src/response/send-util.js +77 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johntalton/http-util",
3
- "version": "7.0.1",
3
+ "version": "7.0.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
package/src/body.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { Readable } from 'node:stream'
2
+
1
3
  import {
2
4
  CHARSET_UTF8,
3
5
  MIME_TYPE_MULTIPART_FORM_DATA,
@@ -8,7 +10,6 @@ import { Multipart } from './headers/multipart.js'
8
10
  export const BYTE_PER_K = 1024
9
11
  export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
10
12
 
11
- /** @import { Readable } from 'node:stream' */
12
13
  /** @import { ContentTypeItem } from './headers/content-type.js' */
13
14
 
14
15
  /**
@@ -24,7 +25,7 @@ export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
24
25
  * @property {number} duration
25
26
  * @property {ReadableStream} body
26
27
  * @property {ContentTypeItem|undefined} contentType
27
- * @property { (mimetype: string) => Promise<Blob> } blob
28
+ * @property { (mimetype?: string | undefined) => Promise<Blob> } blob
28
29
  * @property { () => Promise<ArrayBufferLike> } arrayBuffer
29
30
  * @property { () => Promise<Uint8Array> } bytes
30
31
  * @property { () => Promise<string> } text
@@ -38,6 +39,8 @@ export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
38
39
  * @returns {BodyFuture}
39
40
  */
40
41
  export function requestBody(stream, options) {
42
+ if(!(stream instanceof Readable)) { throw new Error('stream is not Readable') }
43
+
41
44
  const signal = options?.signal
42
45
  const byteLimit = options?.byteLimit ?? DEFAULT_BYTE_LIMIT
43
46
  const contentLength = options?.contentLength
@@ -93,7 +96,7 @@ export function requestBody(stream, options) {
93
96
 
94
97
  const listener = () => {
95
98
  stats.closed = true
96
- controller.error(new Error('Abort Signal'))
99
+ controller.error(new Error(`Abort Signal (${signal?.reason})`))
97
100
  }
98
101
 
99
102
  signal?.addEventListener('abort', listener, { once: true })
@@ -101,7 +104,7 @@ export function requestBody(stream, options) {
101
104
  stream.on('data', chunk => {
102
105
  if(signal?.aborted) {
103
106
  console.log('body reader aborted')
104
- controller.error(new Error('Chunk read Abort Signal Timed out'))
107
+ controller.error(new Error(`Chunk read Abort Signal (${signal.reason})`))
105
108
  stats.closed = true
106
109
  return
107
110
  }
@@ -114,8 +117,9 @@ export function requestBody(stream, options) {
114
117
 
115
118
  // chunk is a node Buffer (which is a TypedArray)
116
119
  if(!ArrayBuffer.isView(chunk)) {
117
- controller.error('invalid chunk type')
120
+ controller.error(new Error('invalid chunk type'))
118
121
  stats.closed = true
122
+ return
119
123
  }
120
124
 
121
125
  stats.byteLength += chunk.byteLength
@@ -157,6 +161,7 @@ export function requestBody(stream, options) {
157
161
 
158
162
  cancel(reason) {
159
163
  console.log('body reader canceled', reason)
164
+ // super.cancel(reason)
160
165
  }
161
166
  }
162
167
 
@@ -190,7 +195,7 @@ export function requestBody(stream, options) {
190
195
  get body() { return makeReader() },
191
196
  get contentType() { return contentType },
192
197
 
193
- blob: (/** @type {string | undefined} */ mimetype) => wrap(reader => bodyBlob(reader, mimetype ?? contentType?.mimetype)),
198
+ blob: (/** @type {string | undefined} */ mimetype = undefined) => wrap(reader => bodyBlob(reader, mimetype ?? contentType?.mimetype)),
194
199
  arrayBuffer: () => wrap(reader => bodyArrayBuffer(reader)),
195
200
  bytes: () => wrap(reader => bodyUint8Array(reader)),
196
201
  text: () => wrap(reader => bodyText(reader, charset)),
package/src/defs.js CHANGED
@@ -1,3 +1,5 @@
1
+ /** @import { Readable } from 'node:stream' */
2
+ /** @import { ReadableStream } from 'node:stream/web' */
1
3
 
2
4
  export const HTTP_HEADER_ORIGIN = 'origin'
3
5
  export const HTTP_HEADER_USER_AGENT = 'user-agent'
@@ -26,6 +28,11 @@ export const RANGE_UNITS_BYTES = 'bytes'
26
28
  /** @type {'none'} */
27
29
  export const RANGE_UNITS_NONE = 'none'
28
30
 
31
+ /** @description joiner used for concatenating multiple of the same headers into one */
32
+ export const COMMON_LIST_HEADER_JOINER_COMMA = ', '
33
+
34
+ /** @description joiner used for concatenating multiple values of single header */
35
+ export const COMMON_LIST_VALUE_JOINER_COMMA = ','
29
36
 
30
37
  /** @import { TimingsInfo } from './headers/server-timing.js' */
31
38
  /** @import { EtagItem, IMFFixDateInput } from './headers/conditional.js' */
@@ -44,7 +51,7 @@ export const RANGE_UNITS_NONE = 'none'
44
51
  * @property {Array<TimingsInfo>} performance
45
52
  * @property {string|undefined} servername
46
53
  * @property {string|undefined} origin
47
- * @property {Array<[ CustomHeaderKey, string ]>|undefined} [customHeaders]
54
+ * @property {Array<[ CustomHeaderKey, string | Array<string> ]>|undefined} [customHeaders]
48
55
  */
49
56
 
50
57
  /**
@@ -53,7 +60,9 @@ export const RANGE_UNITS_NONE = 'none'
53
60
  * @property {boolean} [bom]
54
61
  */
55
62
 
56
- /** @typedef {ArrayBufferLike|ArrayBufferView|ReadableStream|string} SendBody */
63
+ /** @typedef {NodeJS.TypedArray<ArrayBuffer>} TypedArray */
64
+ /** @typedef {ArrayBuffer | TypedArray | string} SendBodyTypes */
65
+ /** @typedef { SendBodyTypes | ReadableStream<SendBodyTypes> | Readable } SendBody */
57
66
 
58
67
  /**
59
68
  * @typedef {Object} SendContent
@@ -70,8 +79,8 @@ export const RANGE_UNITS_NONE = 'none'
70
79
  /**
71
80
  * @typedef {Object} SendInfo
72
81
  * @property {Array<string>} supportedMethods
73
- * @property {Array<string>|string} supportedTypes
74
- * @property {Array<string>|string} acceptableMediaType
82
+ * @property {Array<string>|string} supportedTypes for content negotiation failures
83
+ * @property {Array<string>|string} acceptableMediaType for incoming post/put unsupported types
75
84
  * @property {AcceptRangeUnits|undefined} acceptRanges
76
85
  * @property {Array<string>|undefined} supportedQueryTypes
77
86
  * @property {RateLimitInfo} limitInfo
@@ -18,6 +18,8 @@ export class AcceptEncoding {
18
18
  }
19
19
 
20
20
  /**
21
+ * @deprecated
22
+ * @see {@link AcceptEncoding.selectItemFrom}
21
23
  * @param {string|undefined} acceptEncodingHeader
22
24
  * @param {Array<string>} supportedTypes
23
25
  */
@@ -27,24 +29,45 @@ export class AcceptEncoding {
27
29
  }
28
30
 
29
31
  /**
30
- * @param {Array<AcceptStyleItem>} acceptEncodings
31
- * @param {Array<string>} supportedTypes
32
+ * @param {Array<AcceptStyleItem>} acceptEncodings (descending quality order)
33
+ * @param {Array<string>} supportedTypes (descending preferred order)
34
+ * @returns {AcceptStyleItem | undefined}
32
35
  */
33
- static selectFrom(acceptEncodings, supportedTypes) {
36
+ static selectItemFrom(acceptEncodings, supportedTypes) {
37
+ if(acceptEncodings === undefined) { return undefined }
38
+ if(!Array.isArray(acceptEncodings)) { return undefined }
39
+ if(acceptEncodings.length === 0) { return undefined }
40
+
34
41
  if(supportedTypes === undefined) { return undefined }
42
+ if(!Array.isArray(supportedTypes)) { return undefined }
43
+ if(supportedTypes.length === 0) { return undefined }
35
44
 
36
45
  for(const acceptEncoding of acceptEncodings) {
37
46
  const { name } = acceptEncoding
38
47
  if(supportedTypes.includes(name)) {
39
- return name
48
+ return acceptEncoding
40
49
  }
41
50
  }
42
51
 
43
52
  //
44
53
  if(acceptEncodings.some(item => item.name === ENCODING_ANY)) {
45
- return supportedTypes.at(0)
54
+ const [ name ] = supportedTypes
55
+ if(name === undefined) { return undefined }
56
+ return { name }
46
57
  }
47
58
 
48
59
  return undefined
49
60
  }
61
+
62
+ /**
63
+ * @deprecated
64
+ * @see {@link AcceptEncoding.selectItemFrom}
65
+ * @param {Array<AcceptStyleItem>} acceptEncodings
66
+ * @param {Array<string>} supportedTypes
67
+ * @returns {string | undefined}
68
+ */
69
+ static selectFrom(acceptEncodings, supportedTypes) {
70
+ const item = AcceptEncoding.selectItemFrom(acceptEncodings, supportedTypes)
71
+ return item?.name
72
+ }
50
73
  }
@@ -19,6 +19,8 @@ export class AcceptLanguage {
19
19
  }
20
20
 
21
21
  /**
22
+ * @deprecated
23
+ * @see {@link AcceptLanguage.selectItemFrom}
22
24
  * @param {string|undefined} acceptLanguageHeader
23
25
  * @param {Array<string>} supportedTypes
24
26
  */
@@ -28,24 +30,46 @@ export class AcceptLanguage {
28
30
  }
29
31
 
30
32
  /**
31
- * @param {Array<AcceptStyleItem>} acceptLanguages
32
- * @param {Array<string>} supportedTypes
33
+ * @param {Array<AcceptStyleItem>} acceptLanguages (descending quality order)
34
+ * @param {Array<string>} supportedTypes (descending preferred order)
35
+ * @returns {AcceptStyleItem | undefined}
33
36
  */
34
- static selectFrom(acceptLanguages, supportedTypes) {
37
+ static selectItemFrom(acceptLanguages, supportedTypes) {
38
+ if(acceptLanguages === undefined) { return undefined }
39
+ if(!Array.isArray(acceptLanguages)) { return undefined }
40
+ if(acceptLanguages.length === 0) { return undefined }
41
+
35
42
  if(supportedTypes === undefined) { return undefined }
43
+ if(!Array.isArray(supportedTypes)) { return undefined }
44
+ if(supportedTypes.length === 0) { return undefined }
36
45
 
46
+ // this assume acceptLangues is quality sorted descending order
37
47
  for(const acceptLanguage of acceptLanguages) {
38
48
  const { name } = acceptLanguage
39
49
  if(supportedTypes.includes(name)) {
40
- return name
50
+ return acceptLanguage
41
51
  }
42
52
  }
43
53
 
44
54
  //
45
55
  if(acceptLanguages.some(item => item.name === LANGUAGE_ANY)) {
46
- return supportedTypes.at(0)
56
+ const name = supportedTypes.at(0)
57
+ if(name === undefined) { return undefined }
58
+ return { name }
47
59
  }
48
60
 
49
61
  return undefined
50
62
  }
63
+
64
+ /**
65
+ * @deprecated
66
+ * @see {@link AcceptLanguage.selectItemFrom}
67
+ * @param {Array<AcceptStyleItem>} acceptLanguages
68
+ * @param {Array<string>} supportedTypes
69
+ * @returns {string | undefined}
70
+ */
71
+ static selectFrom(acceptLanguages, supportedTypes) {
72
+ const item = AcceptLanguage.selectItemFrom(acceptLanguages, supportedTypes)
73
+ return item?.name
74
+ }
51
75
  }
@@ -1,5 +1,5 @@
1
1
  import { parseAcceptStyleHeader } from './util/accept-util.js'
2
- import { MIME_ANY, Mime } from './util/mime.js'
2
+ import { MIME_ANY, MIME_SEPARATOR, Mime } from './util/mime.js'
3
3
 
4
4
  /** @import { AcceptStyleItem } from './util/accept-util.js' */
5
5
  /** @import { MimeItem } from './util/mime.js' */
@@ -11,7 +11,30 @@ export const WELL_KNOWN = new Map([
11
11
  [ 'application/json', [ { name: 'application/json', quality: 1 } ] ]
12
12
  ])
13
13
 
14
+ export const UNSPECIFIED_QUALITY = 1
15
+
14
16
  export class Accept {
17
+ /**
18
+ * Descending order based on quality and higher specificity.
19
+ *
20
+ * Returns negative is the first item (a) is of higher quality then second (b).
21
+ * @param {AcceptItem} a
22
+ * @param {AcceptItem} b
23
+ */
24
+ static compare(a, b) {
25
+ if(a.quality === b.quality) {
26
+ // prefer things with less ANY
27
+ const specificityA = (a.type === MIME_ANY ? 1 : 0) + (a.subtype === MIME_ANY ? 1 : 0)
28
+ const specificityB = (b.type === MIME_ANY ? 1 : 0) + (b.subtype === MIME_ANY ? 1 : 0)
29
+ return Math.sign(specificityA - specificityB)
30
+ }
31
+
32
+ // B - A descending order
33
+ const qualityB = b.quality ?? UNSPECIFIED_QUALITY
34
+ const qualityA = a.quality ?? UNSPECIFIED_QUALITY
35
+ return Math.sign(qualityB - qualityA)
36
+ }
37
+
15
38
  /**
16
39
  * @param {string|undefined} acceptHeader
17
40
  * @returns {Array<AcceptItem>}
@@ -31,22 +54,12 @@ export class Accept {
31
54
  }
32
55
  })
33
56
  .filter(entry => entry !== undefined)
34
- .sort((entryA, entryB) => {
35
- if(entryA.quality === entryB.quality) {
36
- // prefer things with less ANY
37
- const specificityA = (entryA.type === MIME_ANY ? 1 : 0) + (entryA.subtype === MIME_ANY ? 1 : 0)
38
- const specificityB = (entryB.type === MIME_ANY ? 1 : 0) + (entryB.subtype === MIME_ANY ? 1 : 0)
39
- return specificityA - specificityB
40
- }
41
-
42
- // B - A descending order
43
- const qualityB = entryB.quality ?? 0
44
- const qualityA = entryA.quality ?? 0
45
- return qualityB - qualityA
46
- })
57
+ .sort(Accept.compare)
47
58
  }
48
59
 
49
60
  /**
61
+ * @deprecated
62
+ * @see {@link Accept.selectItemFrom}
50
63
  * @param {string|undefined} acceptHeader
51
64
  * @param {Array<string>} supportedTypes
52
65
  */
@@ -58,29 +71,61 @@ export class Accept {
58
71
  /**
59
72
  * @param {Array<AcceptItem>} accepts
60
73
  * @param {Array<string>} supportedTypes
74
+ * @returns {AcceptItem | undefined}
61
75
  */
62
- static selectFrom(accepts, supportedTypes) {
63
- const bests = accepts.map(accept => {
64
- const { type, subtype, quality } = accept
65
- const st = supportedTypes.filter(supportedType => {
66
- const supportedMime = Mime.parse(supportedType)
67
- if(supportedMime === undefined) { return false }
68
- return ((supportedMime.type === type || type === MIME_ANY) && (supportedMime.subtype === subtype || subtype === MIME_ANY))
69
- })
76
+ static selectItemFrom(accepts, supportedTypes) {
77
+ if(accepts === undefined) { return undefined }
78
+ if(!Array.isArray(accepts)) { return undefined }
79
+ if(accepts.length === 0) { return undefined }
80
+
81
+ if(supportedTypes === undefined) { return undefined }
82
+ if(!Array.isArray(supportedTypes)) { return undefined }
83
+ if(supportedTypes.length === 0) { return undefined }
84
+
85
+ const supportedMimeTypes = supportedTypes
86
+ .map(Mime.parse)
87
+ .filter(m => m !== undefined)
88
+
89
+ // todo if supportedMimeType has ANY show warning?
70
90
 
91
+ if(supportedMimeTypes.length === 0) { return undefined }
92
+
93
+ const matches = accepts.map(accept => {
94
+ const matchSupportedMimeTypes = supportedMimeTypes.filter(mt => Mime.matches(accept, mt))
95
+ const best = matchSupportedMimeTypes.at(0)
96
+ if(best === undefined) { return undefined }
97
+
98
+ // resolve mime type and subtype
99
+ const type = best.type === MIME_ANY ? accept.type : best.type
100
+ const subtype = best.subtype === MIME_ANY ? accept.subtype : best.subtype
101
+
102
+ // preserve name and parameters of source
71
103
  return {
72
- supportedTypes: st,
73
- quality
104
+ ...accept,
105
+ mimetype: `${type}${MIME_SEPARATOR.SUBTYPE}${subtype}`,
106
+ type,
107
+ subtype
74
108
  }
75
109
  })
76
- .filter(best => best.supportedTypes.length > 0)
110
+ .filter(m => m !== undefined)
77
111
 
78
- if(bests.length === 0) { return undefined }
79
- const [ first ] = bests
80
- if(first === undefined) { return undefined }
81
- const [ firstSt ] = first.supportedTypes
82
- return firstSt
83
- }
84
- }
112
+ if(matches.length === 0) { return undefined }
113
+
114
+ // sort is in-place
115
+ matches.sort(Accept.compare)
85
116
 
117
+ return matches.at(0)
118
+ }
86
119
 
120
+ /**
121
+ * @deprecated
122
+ * @see {@link Accept.selectItemFrom}
123
+ * @param {Array<AcceptItem>} accepts
124
+ * @param {Array<string>} supportedTypes
125
+ * @returns {string | undefined}
126
+ */
127
+ static selectFrom(accepts, supportedTypes) {
128
+ const item = Accept.selectItemFrom(accepts, supportedTypes)
129
+ return item?.mimetype
130
+ }
131
+ }
@@ -1,3 +1,5 @@
1
+ import { COMMON_LIST_HEADER_JOINER_COMMA } from "../defs.js"
2
+
1
3
  /** @typedef {'no-cache'|'no-store'|'no-transform'|'must-revalidate'|'immutable'|'must-understand'} Directives */
2
4
 
3
5
  /**
@@ -13,9 +15,10 @@
13
15
  export class CacheControl {
14
16
  /**
15
17
  * @param {CacheControlOptions|undefined} options
16
- * @returns {string|undefined}
18
+ * @param {boolean} [asArray = false]
19
+ * @returns {Array<string>|string|undefined}
17
20
  */
18
- static encode(options) {
21
+ static encode(options, asArray = false) {
19
22
  if(options === undefined) { return undefined }
20
23
 
21
24
  const {
@@ -51,6 +54,6 @@ export class CacheControl {
51
54
  //
52
55
  if(result.length === 0) { return undefined }
53
56
 
54
- return result.join(', ')
57
+ return asArray ? result : result.join(COMMON_LIST_HEADER_JOINER_COMMA)
55
58
  }
56
59
  }
@@ -1,4 +1,4 @@
1
-
1
+ import { COMMON_LIST_HEADER_JOINER_COMMA } from "../defs.js"
2
2
 
3
3
  export const CLIENT_HINT_USER_AGENT = 'Sec-CH-UA'
4
4
  export const CLIENT_HINT_ARCHITECTURE = 'Sec-CH-UA-Arch'
@@ -69,8 +69,9 @@ export class ClientHints {
69
69
 
70
70
  /**
71
71
  * @param {Array<String>} hints
72
+ * @param {boolean} [asArray = false]
72
73
  */
73
- static encode(hints) {
74
+ static encode(hints, asArray = false) {
74
75
  if(hints === undefined) { return undefined }
75
76
  if(!Array.isArray(hints)) { return undefined }
76
77
  if(hints.length === 0) { return undefined }
@@ -80,7 +81,7 @@ export class ClientHints {
80
81
 
81
82
  if(remaining.length === 0) { return undefined }
82
83
 
83
- return remaining.join(', ')
84
+ return asArray ? remaining : remaining.join(COMMON_LIST_HEADER_JOINER_COMMA)
84
85
  }
85
86
  }
86
87
 
@@ -143,7 +143,7 @@ export class ETag {
143
143
 
144
144
  const rawEtag = raw.trim()
145
145
  const weak = rawEtag.startsWith(CONDITION_ETAG_WEAK_PREFIX)
146
- const quotedEtag = weak ? rawEtag.substring(CONDITION_ETAG_WEAK_PREFIX.length) : rawEtag
146
+ const quotedEtag = weak ? rawEtag.slice(CONDITION_ETAG_WEAK_PREFIX.length) : rawEtag
147
147
 
148
148
  if(quotedEtag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
149
149
 
@@ -350,18 +350,18 @@ export class Conditional {
350
350
 
351
351
  //
352
352
  const spaces = [
353
- matchHeader.substring(4, 5),
354
- matchHeader.substring(7, 8),
355
- matchHeader.substring(11, 12),
356
- matchHeader.substring(16, 17),
357
- matchHeader.substring(25, 26)
353
+ matchHeader.slice(4, 5),
354
+ matchHeader.slice(7, 8),
355
+ matchHeader.slice(11, 12),
356
+ matchHeader.slice(16, 17),
357
+ matchHeader.slice(25, 26)
358
358
  ]
359
- const comma = matchHeader.substring(3, 4)
359
+ const comma = matchHeader.slice(3, 4)
360
360
  const timeSeparators = [
361
- matchHeader.substring(19, 20),
362
- matchHeader.substring(22, 23)
361
+ matchHeader.slice(19, 20),
362
+ matchHeader.slice(22, 23)
363
363
  ]
364
- const gmt = matchHeader.substring(26)
364
+ const gmt = matchHeader.slice(26)
365
365
 
366
366
  //
367
367
  if(comma !== DATE_SEPARATOR) { return undefined }
@@ -374,13 +374,13 @@ export class Conditional {
374
374
  }
375
375
 
376
376
  //
377
- const dayName = matchHeader.substring(0, 3)
378
- const day = Number.parseInt(matchHeader.substring(5, 7))
379
- const month = matchHeader.substring(8, 11)
380
- const year = Number.parseInt(matchHeader.substring(12, 16))
381
- const hour = Number.parseInt(matchHeader.substring(17, 19))
382
- const minute = Number.parseInt(matchHeader.substring(20, 22))
383
- const second = Number.parseInt(matchHeader.substring(23, 25))
377
+ const dayName = matchHeader.slice(0, 3)
378
+ const day = Number.parseInt(matchHeader.slice(5, 7))
379
+ const month = matchHeader.slice(8, 11)
380
+ const year = Number.parseInt(matchHeader.slice(12, 16))
381
+ const hour = Number.parseInt(matchHeader.slice(17, 19))
382
+ const minute = Number.parseInt(matchHeader.slice(20, 22))
383
+ const second = Number.parseInt(matchHeader.slice(23, 25))
384
384
 
385
385
  //
386
386
  if(!DATE_DAYS.includes(dayName)) { return undefined }
@@ -16,7 +16,7 @@ export const MIME_TYPE_MESSAGE_HTTP = 'message/http'
16
16
 
17
17
  export const KNOWN_CONTENT_TYPES = [
18
18
  'application', 'audio', 'image', 'message',
19
- 'multipart','text', 'video'
19
+ 'multipart', 'text', 'video'
20
20
  ]
21
21
 
22
22
  export const TYPE_X_TOKEN_PREFIX = 'X-'
@@ -1,3 +1,5 @@
1
+ import { COMMON_LIST_HEADER_JOINER_COMMA } from "../defs.js"
2
+
1
3
  /**
2
4
  * @typedef {Object} LinkItem
3
5
  * @property {URL|string} url
@@ -22,13 +24,16 @@ export class Link {
22
24
 
23
25
  /**
24
26
  * @param {Array<LinkItem>|LinkItem|undefined} links
27
+ * @param {boolean} [asArray = false]
25
28
  */
26
- static encode(links) {
29
+ static encode(links, asArray = false) {
27
30
  if(links === undefined) { return undefined }
28
31
  const linkAry = Array.isArray(links) ? links : [ links ]
29
32
  if(linkAry.length === 0) { return undefined }
30
- return linkAry
33
+
34
+ const ary = linkAry
31
35
  .map(link => [ ...Link.#encode(link) ].join('; '))
32
- .join(', ')
36
+
37
+ return asArray ? ary : ary.join(COMMON_LIST_HEADER_JOINER_COMMA)
33
38
  }
34
39
  }
@@ -2,7 +2,7 @@ import { ReadableStream } from 'node:stream/web'
2
2
 
3
3
  import { ContentDisposition } from './content-disposition.js'
4
4
  import { ContentRange } from './content-range.js'
5
- import { ContentType } from './content-type.js'
5
+ // import { ContentType } from './content-type.js'
6
6
 
7
7
  /** @import { ContentRangeDirective } from './content-range.js' */
8
8
  /** @import { SendBody } from '../defs.js' */
@@ -92,7 +92,7 @@ export class Multipart {
92
92
  const name = rawName?.toLowerCase()
93
93
  // console.log('header', name, value)
94
94
  if(name === MULTIPART_HEADER.CONTENT_TYPE) {
95
- const contentType = ContentType.parse(value)
95
+ //const contentType = ContentType.parse(value)
96
96
  //console.log({ contentType })
97
97
  }
98
98
  else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
@@ -103,6 +103,9 @@ export class Multipart {
103
103
 
104
104
  partName = disposition.name
105
105
  }
106
+ else if(name === MULTIPART_HEADER.CONTENT_RANGE) {
107
+ // todo
108
+ }
106
109
  else {
107
110
  // unsupported part header - ignore
108
111
  console.log('unsupported part header', name)
@@ -138,7 +141,7 @@ export class Multipart {
138
141
  * @param {Array<MultipartBytePart>} parts
139
142
  * @param {number|undefined} contentLength
140
143
  * @param {string} boundary
141
- * @returns {ReadableStream<Uint8Array>}
144
+ * @returns {ReadableStream<Uint8Array<ArrayBuffer>>}
142
145
  */
143
146
  static encode_Bytes(contentType, parts, contentLength, boundary) {
144
147
  const boundaryBegin = `${BOUNDARY_MARK}${boundary}`
@@ -159,16 +162,21 @@ export class Multipart {
159
162
  if(part.obj instanceof ReadableStream) {
160
163
  // biome-ignore lint/performance/noAwaitInLoops: readable
161
164
  for await (const chunk of part.obj) {
162
- if(chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk)) {
165
+ if(chunk instanceof ArrayBuffer) {
166
+ controller.enqueue(new Uint8Array(chunk))
167
+ }
168
+ else if(ArrayBuffer.isView(chunk)) {
163
169
  controller.enqueue(chunk)
164
170
  }
165
171
  else if(typeof chunk === 'string'){
166
172
  controller.enqueue(encoder.encode(chunk))
167
173
  }
168
- else {
169
- // console.log('chunk type', typeof chunk)
174
+ else if(typeof chunk === 'number') {
170
175
  controller.enqueue(Uint8Array.from([ chunk ]))
171
176
  }
177
+ else {
178
+ controller.error(new Error('unknown stream chunk type'))
179
+ }
172
180
  }
173
181
  }
174
182
  else if(part.obj instanceof ArrayBuffer) {
@@ -53,8 +53,8 @@ export class Range {
53
53
  static parse(rangeHeader) {
54
54
  if(rangeHeader === undefined) { return undefined }
55
55
  if(!rangeHeader.startsWith(RANGE_UNITS_BYTES)) { return undefined }
56
- if(!(rangeHeader.substring(RANGE_UNITS_BYTES.length, RANGE_UNITS_BYTES.length + 1) === RANGE_EQUAL)) { return undefined }
57
- const rangeStr = rangeHeader.substring(RANGE_UNITS_BYTES.length + RANGE_EQUAL.length).trim()
56
+ if(!(rangeHeader.slice(RANGE_UNITS_BYTES.length, RANGE_UNITS_BYTES.length + 1) === RANGE_EQUAL)) { return undefined }
57
+ const rangeStr = rangeHeader.slice(RANGE_UNITS_BYTES.length + RANGE_EQUAL.length).trim()
58
58
  if(rangeStr === '') { return undefined }
59
59
 
60
60
  const ranges = rangeStr.split(RANGE_LIST_SEPARATOR)
@@ -1,3 +1,5 @@
1
+ import { COMMON_LIST_HEADER_JOINER_COMMA } from "../defs.js"
2
+
1
3
  // https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-10.html
2
4
 
3
5
  export const HTTP_HEADER_RATE_LIMIT = 'RateLimit'
@@ -41,10 +43,17 @@ export const QUOTA_UNIT = {
41
43
  }
42
44
 
43
45
  export class RateLimit {
46
+ /**
47
+ * @deprecated
48
+ * @see {@link RateLimit.encode}
49
+ * @param {RateLimitInfo} limitInfo
50
+ */
51
+ static from(limitInfo) { return RateLimit.encode(limitInfo) }
52
+
44
53
  /**
45
54
  * @param {RateLimitInfo} limitInfo
46
55
  */
47
- static from(limitInfo) {
56
+ static encode(limitInfo) {
48
57
  if(limitInfo === undefined) { return undefined }
49
58
  const { name, remaining, resetSeconds, partitionKey } = limitInfo
50
59
 
@@ -57,10 +66,17 @@ export class RateLimit {
57
66
  }
58
67
 
59
68
  export class RateLimitPolicy {
69
+ /**
70
+ * @deprecated
71
+ * @see {@link RateLimitPolicy.encode}
72
+ * @param {...RateLimitPolicyInfo} policies
73
+ */
74
+ static from(...policies) { return RateLimitPolicy.encode(...policies) }
75
+
60
76
  /**
61
77
  * @param {...RateLimitPolicyInfo} policies
62
78
  */
63
- static from(...policies) {
79
+ static encode(...policies) { // todo AsArray
64
80
  if(policies === undefined) { return undefined }
65
81
  if(policies.length === 0) { return undefined }
66
82
 
@@ -86,6 +102,6 @@ export class RateLimitPolicy {
86
102
  .filter(item => item !== undefined)
87
103
  .join(';')
88
104
  })
89
- .join(',')
105
+ .join(COMMON_LIST_HEADER_JOINER_COMMA)
90
106
  }
91
107
  }