@johntalton/http-util 5.0.0 → 5.1.0

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 (51) hide show
  1. package/package.json +1 -1
  2. package/src/accept-encoding.js +1 -1
  3. package/src/accept-language.js +1 -1
  4. package/src/accept-util.js +1 -1
  5. package/src/accept.js +2 -5
  6. package/src/body.js +6 -5
  7. package/src/clear-site-data.js +59 -0
  8. package/src/conditional.js +11 -8
  9. package/src/content-disposition.js +12 -7
  10. package/src/content-type.js +18 -14
  11. package/src/forwarded.js +1 -1
  12. package/src/index.js +3 -1
  13. package/src/multipart.js +5 -5
  14. package/src/preference.js +205 -0
  15. package/src/range.js +5 -10
  16. package/src/response/accepted.js +1 -0
  17. package/src/response/bytes.js +1 -0
  18. package/src/response/conflict.js +1 -0
  19. package/src/response/content-too-large.js +1 -0
  20. package/src/response/created.js +2 -1
  21. package/src/response/defs.js +2 -0
  22. package/src/response/error.js +1 -0
  23. package/src/response/gone.js +1 -0
  24. package/src/response/header-util.js +2 -1
  25. package/src/response/im-a-teapot.js +1 -0
  26. package/src/response/index.js +3 -0
  27. package/src/response/insufficient-storage.js +1 -0
  28. package/src/response/json.js +1 -1
  29. package/src/response/moved-permanently.js +1 -0
  30. package/src/response/multiple-choices.js +1 -0
  31. package/src/response/no-content.js +2 -1
  32. package/src/response/not-acceptable.js +2 -1
  33. package/src/response/not-allowed.js +1 -0
  34. package/src/response/not-found.js +1 -0
  35. package/src/response/not-implemented.js +1 -0
  36. package/src/response/not-modified.js +3 -2
  37. package/src/response/partial-content.js +3 -3
  38. package/src/response/precondition-failed.js +1 -0
  39. package/src/response/preflight.js +2 -1
  40. package/src/response/range-not-satisfiable.js +2 -1
  41. package/src/response/see-other.js +1 -0
  42. package/src/response/send-util.js +14 -8
  43. package/src/response/sse.js +4 -3
  44. package/src/response/temporary-redirect.js +1 -0
  45. package/src/response/timeout.js +1 -0
  46. package/src/response/too-many-requests.js +1 -0
  47. package/src/response/trace.js +8 -4
  48. package/src/response/unauthorized.js +1 -0
  49. package/src/response/unavailable.js +1 -0
  50. package/src/response/unprocessable.js +1 -0
  51. package/src/response/unsupported-media.js +10 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johntalton/http-util",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,7 +21,7 @@ export class AcceptEncoding {
21
21
  */
22
22
  static select(acceptEncodingHeader, supportedTypes) {
23
23
  const accepts = AcceptEncoding.parse(acceptEncodingHeader)
24
- return this.selectFrom(accepts, supportedTypes)
24
+ return AcceptEncoding.selectFrom(accepts, supportedTypes)
25
25
  }
26
26
 
27
27
  /**
@@ -23,7 +23,7 @@ export class AcceptLanguage {
23
23
  */
24
24
  static select(acceptLanguageHeader, supportedTypes) {
25
25
  const accepts = AcceptLanguage.parse(acceptLanguageHeader)
26
- return this.selectFrom(accepts, supportedTypes)
26
+ return AcceptLanguage.selectFrom(accepts, supportedTypes)
27
27
  }
28
28
 
29
29
  /**
@@ -39,7 +39,7 @@ export function parseAcceptStyleHeader(header, wellKnown) {
39
39
  }))
40
40
 
41
41
  if(!parameters.has(QUALITY)) { parameters.set(QUALITY, DEFAULT_QUALITY_STRING) }
42
- const quality = parseFloat(parameters.get(QUALITY) ?? DEFAULT_QUALITY_STRING)
42
+ const quality = Number.parseFloat(parameters.get(QUALITY) ?? DEFAULT_QUALITY_STRING)
43
43
 
44
44
  return {
45
45
  name,
package/src/accept.js CHANGED
@@ -54,7 +54,6 @@ export class Accept {
54
54
  const qualityB = entryB.quality ?? 0
55
55
  const qualityA = entryA.quality ?? 0
56
56
  return qualityB - qualityA
57
- // return entryB.quality - entryA.quality
58
57
  })
59
58
  }
60
59
 
@@ -64,7 +63,7 @@ export class Accept {
64
63
  */
65
64
  static select(acceptHeader, supportedTypes) {
66
65
  const accepts = Accept.parse(acceptHeader)
67
- return this.selectFrom(accepts, supportedTypes)
66
+ return Accept.selectFrom(accepts, supportedTypes)
68
67
  }
69
68
 
70
69
  /**
@@ -84,9 +83,7 @@ export class Accept {
84
83
  quality
85
84
  }
86
85
  })
87
- .filter(best => {
88
- return best.supportedTypes.length > 0
89
- })
86
+ .filter(best => best.supportedTypes.length > 0)
90
87
 
91
88
  if(bests.length === 0) { return undefined }
92
89
  const [ first ] = bests
package/src/body.js CHANGED
@@ -5,7 +5,8 @@ import {
5
5
  } from './content-type.js'
6
6
  import { Multipart } from './multipart.js'
7
7
 
8
- export const DEFAULT_BYTE_LIMIT = 1024 * 1024 //
8
+ export const BYTE_PER_K = 1024
9
+ export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
9
10
 
10
11
  /** @import { Readable } from 'node:stream' */
11
12
  /** @import { ContentType } from './content-type.js' */
@@ -43,7 +44,7 @@ export function requestBody(stream, options) {
43
44
  const charset = options?.contentType?.charset ?? CHARSET_UTF8
44
45
  const contentType = options?.contentType
45
46
 
46
- const invalidContentLength = (contentLength === undefined || isNaN(contentLength))
47
+ const invalidContentLength = (contentLength === undefined || Number.isNaN(contentLength))
47
48
  // if(contentLength > byteLimit) {
48
49
  // console.log(contentLength, invalidContentLength)
49
50
  // throw new Error('contentLength exceeds limit')
@@ -278,7 +279,7 @@ export async function bodyJSON(reader, charset) {
278
279
  * @param {ReadableStream} reader
279
280
  * @param {ContentType} contentType
280
281
  */
281
- async function _bodyFormData_Multipart(reader, contentType) {
282
+ export async function _bodyFormData_Multipart(reader, contentType) {
282
283
  const MULTIPART_FORM_DATA_BOUNDARY_PARAMETER = 'boundary'
283
284
 
284
285
  const text = await bodyText(reader, contentType.charset)
@@ -292,7 +293,7 @@ async function _bodyFormData_Multipart(reader, contentType) {
292
293
  * @param {ReadableStream} reader
293
294
  * @param {ContentType} contentType
294
295
  */
295
- async function _bodyFormData_URL(reader, contentType) {
296
+ export async function _bodyFormData_URL(reader, contentType) {
296
297
  const text = await bodyText(reader, contentType.charset)
297
298
  const sp = new URLSearchParams(text)
298
299
  const formData = new FormData()
@@ -320,4 +321,4 @@ export async function bodyFormData(reader, contentType) {
320
321
  }
321
322
 
322
323
  throw new TypeError('unknown mime type for form data')
323
- }
324
+ }
@@ -0,0 +1,59 @@
1
+
2
+ /** @type {'*'} */
3
+ export const WILDCARD = '*'
4
+
5
+ export const CSD_DIRECTIVE_SEPARATOR = ','
6
+ export const CSD_QUOTE = '"'
7
+
8
+ export const CSD_DIRECTIVE_CACHE = 'cache'
9
+ export const CSD_DIRECTIVE_CLIENT_HINTS = 'clientHints'
10
+ export const CSD_DIRECTIVE_COOKIES = 'cookies'
11
+ export const CSD_DIRECTIVE_EXECUTION_CONTEXTS = 'executionContexts'
12
+ export const CSD_DIRECTIVE_PREFETCH_CACHE = 'prefetchCache'
13
+ export const CSD_DIRECTIVE_PRERENDER_CACHE = 'prerenderCache'
14
+ export const CSD_DIRECTIVE_STORAGE = 'storage'
15
+
16
+ /**
17
+ * @typedef {Object} ClearOptions
18
+ * @property {boolean} cache
19
+ * @property {boolean} clientHints
20
+ * @property {boolean} cookies
21
+ * @property {boolean} executionContext
22
+ * @property {boolean} prefetchCache
23
+ * @property {boolean} prerenderCache
24
+ * @property {boolean} storage
25
+ */
26
+
27
+ export class SiteData {
28
+ /**
29
+ * @param {Partial<ClearOptions>|true|'*'|undefined} directives
30
+ */
31
+ static encode(directives) {
32
+ if(directives === undefined) { return undefined }
33
+ if(directives === true) { return WILDCARD }
34
+ if(directives === WILDCARD) { return WILDCARD }
35
+
36
+ const result = []
37
+ if(directives.cache === true) { result.push(CSD_DIRECTIVE_CACHE) }
38
+ if(directives.clientHints === true) { result.push(CSD_DIRECTIVE_CLIENT_HINTS) }
39
+ if(directives.cookies === true) { result.push(CSD_DIRECTIVE_COOKIES) }
40
+ if(directives.executionContext === true) { result.push(CSD_DIRECTIVE_EXECUTION_CONTEXTS) }
41
+ if(directives.prefetchCache === true) { result.push(CSD_DIRECTIVE_PREFETCH_CACHE) }
42
+ if(directives.prerenderCache === true) { result.push(CSD_DIRECTIVE_PRERENDER_CACHE) }
43
+ if(directives.storage === true) { result.push(CSD_DIRECTIVE_STORAGE) }
44
+
45
+ return result
46
+ .map(item => `${CSD_QUOTE}${item}${CSD_QUOTE}`)
47
+ .join(CSD_DIRECTIVE_SEPARATOR)
48
+ }
49
+ }
50
+
51
+ // console.log(SiteData.encode())
52
+ // console.log(SiteData.encode({}))
53
+ // console.log(SiteData.encode(false))
54
+ // console.log(SiteData.encode('true'))
55
+ // console.log(SiteData.encode(true))
56
+ // console.log(SiteData.encode('*'))
57
+ // console.log(SiteData.encode({ storage: false }))
58
+ // console.log(SiteData.encode({ storage: true }))
59
+ // console.log(SiteData.encode({ storage: true, cookies: true }))
@@ -49,6 +49,9 @@ export const DATE_SEPARATOR = ','
49
49
  export const DATE_TIME_SEPARATOR = ':'
50
50
  export const DATE_ZONE = 'GMT'
51
51
 
52
+ export const MINIMUM_YEAR = 1900
53
+ export const MAXIMUM_DAY = 31
54
+
52
55
  /**
53
56
  * @param {string} etag
54
57
  */
@@ -201,7 +204,7 @@ export class Conditional {
201
204
  // <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
202
205
  // day-name "," SP date1 SP time-of-day SP GMT
203
206
 
204
- if(matchHeader.length != 29) { return undefined }
207
+ if(matchHeader.length !== 29) { return undefined }
205
208
 
206
209
  //
207
210
  const spaces = [
@@ -230,12 +233,12 @@ export class Conditional {
230
233
 
231
234
  //
232
235
  const dayName = matchHeader.substring(0, 3)
233
- const day = parseInt(matchHeader.substring(5, 7))
236
+ const day = Number.parseInt(matchHeader.substring(5, 7))
234
237
  const month = matchHeader.substring(8, 11)
235
- const year = parseInt(matchHeader.substring(12, 16))
236
- const hour = parseInt(matchHeader.substring(17, 19))
237
- const minute = parseInt(matchHeader.substring(20, 22))
238
- const second = parseInt(matchHeader.substring(23, 25))
238
+ const year = Number.parseInt(matchHeader.substring(12, 16))
239
+ const hour = Number.parseInt(matchHeader.substring(17, 19))
240
+ const minute = Number.parseInt(matchHeader.substring(20, 22))
241
+ const second = Number.parseInt(matchHeader.substring(23, 25))
239
242
 
240
243
  //
241
244
  if(!DATE_DAYS.includes(dayName)) { return undefined }
@@ -247,8 +250,8 @@ export class Conditional {
247
250
  if(!Number.isInteger(second)) { return undefined }
248
251
 
249
252
  //
250
- if(day > 31 || day <= 0) { return undefined }
251
- if(year < 1900) { return undefined }
253
+ if(day > MAXIMUM_DAY || day <= 0) { return undefined }
254
+ if(year < MINIMUM_YEAR) { return undefined }
252
255
  if(hour > 24 || hour < 0) { return undefined }
253
256
  if(minute > 60 || minute < 0) { return undefined }
254
257
  if(second > 60 || second < 0) { return undefined }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @typedef {Object} Disposition
3
3
  * @property {string} disposition
4
- * @property {Map<string, string>} parameters
5
- * @property {string} [name]
6
- * @property {string} [filename]
4
+ * @property {Map<string, string|undefined>} parameters
5
+ * @property {string|undefined} [name]
6
+ * @property {string|undefined} [filename]
7
7
  */
8
8
 
9
9
  export const DISPOSITION_SEPARATOR = {
@@ -15,17 +15,22 @@ export const DISPOSITION_PARAM_NAME = 'name'
15
15
  export const DISPOSITION_PARAM_FILENAME = 'filename'
16
16
 
17
17
  /**
18
- * @param {string} contentDispositionHeader
18
+ * @param {string|undefined} contentDispositionHeader
19
19
  * @returns {Disposition|undefined}
20
20
  */
21
21
  export function parseContentDisposition(contentDispositionHeader) {
22
22
  if(contentDispositionHeader === undefined) { return undefined }
23
23
 
24
24
  const [ disposition, ...parameterSet ] = contentDispositionHeader.trim().split(DISPOSITION_SEPARATOR.PARAMETER).map(entry => entry.trim())
25
+ if(disposition === undefined) { return undefined }
25
26
  const parameters = new Map(parameterSet.map(parameter => {
26
- const [ key, value ] = parameter.split(DISPOSITION_SEPARATOR.KVP).map(p => p.trim())
27
- return [ key, value ]
28
- }))
27
+ const [ key, value ] = parameter.split(DISPOSITION_SEPARATOR.KVP).map(p => p.trim())
28
+ if(key === undefined) { return undefined }
29
+ return { key, value }
30
+ })
31
+ .filter(item => item !== undefined)
32
+ .map(({ key, value }) => ([ key, value ])))
33
+
29
34
 
30
35
  const name = parameters.get(DISPOSITION_PARAM_NAME)
31
36
  const filename = parameters.get(DISPOSITION_PARAM_FILENAME)
@@ -29,10 +29,12 @@ export const SPECIAL_CHARS = [
29
29
  '\n', '\r', '\t'
30
30
  ]
31
31
 
32
+ export const WHITESPACE_REGEX = /\s/
33
+
32
34
  /**
33
35
  * @param {string} c
34
36
  */
35
- export function isWhitespace(c){ return /\s/.test(c) }
37
+ export function isWhitespace(c){ return WHITESPACE_REGEX.test(c) }
36
38
 
37
39
  /**
38
40
  * @param {string|undefined} value
@@ -116,22 +118,21 @@ export function parseContentType(contentTypeHeader) {
116
118
 
117
119
  const parameters = new Map()
118
120
 
119
- parameterSet
120
- .forEach(parameter => {
121
- const [ key, value ] = parameter.split(CONTENT_TYPE_SEPARATOR.KVP)
122
- if(key === undefined || key === '') { return }
123
- if(value === undefined || value === '') { return }
124
- if(hasSpecialChar(key)) { return }
121
+ for(const parameter of parameterSet) {
122
+ const [ key, value ] = parameter.split(CONTENT_TYPE_SEPARATOR.KVP)
123
+ if(key === undefined || key === '') { continue }
124
+ if(value === undefined || value === '') { continue }
125
125
 
126
- const actualKey = key?.trim().toLowerCase()
126
+ const actualKey = key?.trim().toLowerCase()
127
+ if(hasSpecialChar(actualKey)) { continue }
127
128
 
128
- const quoted = (value.charAt(0) === '"' && value.charAt(value.length - 1) === '"')
129
- const actualValue = quoted ? value.substring(1, value.length - 1) : value
129
+ const quoted = (value.at(0) === '"' && value.at(-1) === '"')
130
+ const actualValue = quoted ? value.substring(1, value.length - 1) : value
130
131
 
131
- if(!parameters.has(actualKey)) {
132
- parameters.set(actualKey, actualValue)
133
- }
134
- })
132
+ if(!parameters.has(actualKey)) {
133
+ parameters.set(actualKey, actualValue)
134
+ }
135
+ }
135
136
 
136
137
  const charset = parameters.get(CHARSET)
137
138
 
@@ -142,3 +143,6 @@ export function parseContentType(contentTypeHeader) {
142
143
  parameters
143
144
  }
144
145
  }
146
+
147
+ //
148
+ // console.log(parseContentType('multipart/form-data; boundary=----WebKitFormBoundaryJZy5maoMBkBMoGjt'))
package/src/forwarded.js CHANGED
@@ -49,7 +49,7 @@ export class Forwarded {
49
49
  })
50
50
  .filter(item => item !== undefined))
51
51
  )
52
- .filter(m => m.size !== 0)
52
+ .filter(m => m.size > 0)
53
53
  }
54
54
 
55
55
  /**
package/src/index.js CHANGED
@@ -1,7 +1,9 @@
1
+ /** biome-ignore-all lint/performance/noBarrelFile: entry point */
2
+ /** biome-ignore-all lint/performance/noReExportAll: entry point */
3
+ export * from './accept.js'
1
4
  export * from './accept-encoding.js'
2
5
  export * from './accept-language.js'
3
6
  export * from './accept-util.js'
4
- export * from './accept.js'
5
7
  export * from './cache-control.js'
6
8
  export * from './conditional.js'
7
9
  export * from './content-disposition.js'
package/src/multipart.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { ReadableStream } from 'node:stream/web'
2
2
 
3
3
  import { parseContentDisposition } from './content-disposition.js'
4
- import { parseContentType } from './content-type.js'
5
4
  import { ContentRange } from './content-range.js'
5
+ import { parseContentType } from './content-type.js'
6
6
 
7
7
  /** @import { ContentRangeDirective } from './content-range.js' */
8
8
  /** @import { SendBody } from './response/send-util.js' */
@@ -48,9 +48,9 @@ export class Multipart {
48
48
  /**
49
49
  * @param {string} text
50
50
  * @param {string} boundary
51
- * @param {string} [charset='utf8']
51
+ * @param {string} [_charset='utf8']
52
52
  */
53
- static parse_FormData(text, boundary, charset = 'utf8') {
53
+ static parse_FormData(text, boundary, _charset = 'utf8') {
54
54
  // console.log({ boundary, text })
55
55
  const formData = new FormData()
56
56
 
@@ -91,10 +91,10 @@ export class Multipart {
91
91
  if(line === EMPTY) { state = MULTIPART_STATE.VALUE }
92
92
  else {
93
93
  const [ rawName, value ] = line.split(HEADER_SEPARATOR)
94
- const name = rawName.toLowerCase()
94
+ const name = rawName?.toLowerCase()
95
95
  // console.log('header', name, value)
96
96
  if(name === MULTIPART_HEADER.CONTENT_TYPE) {
97
- const contentType = parseContentType(value)
97
+ const _contentType = parseContentType(value)
98
98
  // console.log({ contentType })
99
99
  }
100
100
  else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
@@ -0,0 +1,205 @@
1
+ // https://datatracker.ietf.org/doc/html/rfc7240
2
+ // https://www.rfc-editor.org/rfc/rfc7240#section-3
3
+
4
+ export const SEPARATOR = {
5
+ PREFERENCE: ',',
6
+ PARAMS: ';',
7
+ PARAM_KVP: '=',
8
+ KVP: '='
9
+ }
10
+
11
+ export const QUOTE = '"'
12
+
13
+ export const DIRECTIVE_RESPOND_ASYNC = 'respond-async'
14
+ export const DIRECTIVE_WAIT = 'wait'
15
+ export const DIRECTIVE_HANDLING = 'handling'
16
+ export const DIRECTIVE_REPRESENTATION = 'return'
17
+ export const DIRECTIVE_TIMEZONE = 'timezone'
18
+
19
+ export const DIRECTIVE_HANDLING_STRICT = 'strict'
20
+ export const DIRECTIVE_HANDLING_LENIENT = 'lenient'
21
+
22
+ export const DIRECTIVE_REPRESENTATION_MINIMAL = 'minimal'
23
+ export const DIRECTIVE_REPRESENTATION_HEADERS_ONLY = 'headers-only'
24
+ export const DIRECTIVE_REPRESENTATION_FULL = 'representation'
25
+
26
+
27
+ /**
28
+ * @param {string|undefined} value
29
+ */
30
+ export function stripQuotes(value) {
31
+ if(value === undefined) { return undefined }
32
+ return value.substring(1, value.length - 1)
33
+ }
34
+
35
+ /**
36
+ * @param {string|undefined} value
37
+ */
38
+ export function isQuoted(value) {
39
+ if(value === undefined) { return false }
40
+ if(value.length < 2) { return false }
41
+ if(!value.startsWith(QUOTE)) { return false }
42
+ if(!value.endsWith(QUOTE)) { return false }
43
+ return true
44
+ }
45
+
46
+ /**
47
+ * @typedef {Object} Preference
48
+ * @property {string|undefined} value
49
+ * @property {Map<string, string|undefined>} parameters
50
+ */
51
+
52
+ /**
53
+ * @template P
54
+ * @typedef {Object} RequestPreferencesBase
55
+ * @property {boolean|undefined} [asynchronous]
56
+ * @property {string|undefined} [representation]
57
+ * @property {string|undefined} [handling]
58
+ * @property {number|undefined} [wait]
59
+ * @property {string|undefined} [timezone]
60
+ * @property {Map<string, P>} preferences
61
+ */
62
+
63
+ /** @typedef {RequestPreferencesBase<Preference>} RequestPreferences */
64
+ /** @typedef {RequestPreferencesBase<Partial<Omit<Preference,'parameters'>>|undefined>} AppliedRequestPreferences */
65
+
66
+ export class Preferences {
67
+ /**
68
+ * @param {string|undefined} header
69
+ * @returns {RequestPreferences|undefined}
70
+ */
71
+ static parse(header) {
72
+ if(header === undefined) { return undefined }
73
+
74
+ const preferences = new Map(header.split(SEPARATOR.PREFERENCE)
75
+ .map(pref => {
76
+ const [ kvp, ...params ] = pref.trim().split(SEPARATOR.PARAMS)
77
+ const [ key, rawValue ] = kvp?.split(SEPARATOR.KVP) ?? []
78
+
79
+ if(key === undefined) { return {} }
80
+ const valueOrEmpty = isQuoted(rawValue) ? stripQuotes(rawValue) : rawValue
81
+ const value = (valueOrEmpty !== '') ? valueOrEmpty : undefined
82
+
83
+ const parameters = new Map(params
84
+ .map(param => {
85
+ const [ pKey, rawPValue ] = param.split(SEPARATOR.PARAM_KVP)
86
+ if(pKey === undefined) { return {} }
87
+ const trimmedRawPValue = rawPValue?.trim()
88
+ const pValueOrEmpty = isQuoted(trimmedRawPValue) ? stripQuotes(trimmedRawPValue) : trimmedRawPValue
89
+ const pValue = (pValueOrEmpty !== '') ? pValueOrEmpty : undefined
90
+ return { key: pKey.trim(), value: pValue }
91
+ })
92
+ .filter(item => item.key !== undefined)
93
+ .map(item => ([ item.key, item.value ]))
94
+ )
95
+
96
+ return { key, value, parameters }
97
+ })
98
+ .filter(item => item.key !== undefined)
99
+ .map(item => ([
100
+ item.key,
101
+ { value: item.value, parameters: item.parameters }
102
+ ]))
103
+ )
104
+
105
+ //
106
+ const asynchronous = preferences.get(DIRECTIVE_RESPOND_ASYNC) !== undefined
107
+ const representation = preferences.get(DIRECTIVE_REPRESENTATION)?.value
108
+ const handling = preferences.get(DIRECTIVE_HANDLING)?.value
109
+ const wait = Number.parseInt(preferences.get(DIRECTIVE_WAIT)?.value ?? '')
110
+ const timezone = preferences.get(DIRECTIVE_TIMEZONE)?.value
111
+
112
+ return {
113
+ asynchronous,
114
+ representation,
115
+ handling,
116
+ wait: Number.isFinite(wait) ? wait : undefined,
117
+ timezone,
118
+ preferences
119
+ }
120
+ }
121
+ }
122
+
123
+ export class AppliedPreferences {
124
+
125
+ /**
126
+ * @param {Map<string, string|undefined>} preferences
127
+ */
128
+ static #encode_Map(preferences) {
129
+ return [ ...preferences.entries()
130
+ .map(([ key, value ]) => {
131
+ // todo check if value should be quoted
132
+ if(value !== undefined) { return `${key}${SEPARATOR.KVP}${value}` }
133
+ return key
134
+ }) ]
135
+ .join(SEPARATOR.PREFERENCE)
136
+ }
137
+
138
+ /**
139
+ * @param {Partial<AppliedRequestPreferences>|Map<string, string|undefined>|undefined} preferences
140
+ */
141
+ static encode(preferences) {
142
+ if(preferences === undefined) { return undefined}
143
+ if(preferences instanceof Map) {
144
+ return AppliedPreferences.#encode_Map(preferences)
145
+ }
146
+
147
+ const applied = new Map()
148
+
149
+ for(const [ key, pref ] of preferences.preferences?.entries() ?? []) {
150
+ applied.set(key, pref?.value)
151
+ }
152
+
153
+ if(preferences.asynchronous === true) { applied.set(DIRECTIVE_RESPOND_ASYNC, undefined) }
154
+ if(preferences.asynchronous === false) { applied.delete(DIRECTIVE_RESPOND_ASYNC) }
155
+ if(preferences.representation !== undefined) { applied.set(DIRECTIVE_REPRESENTATION, preferences.representation) }
156
+ if(preferences.handling !== undefined) { applied.set(DIRECTIVE_HANDLING, preferences.handling) }
157
+ if(preferences.wait !== undefined) { applied.set(DIRECTIVE_WAIT, preferences.wait) }
158
+ if(preferences.timezone !== undefined) { applied.set(DIRECTIVE_TIMEZONE, preferences.timezone) }
159
+
160
+ return AppliedPreferences.#encode_Map(applied)
161
+ }
162
+ }
163
+
164
+
165
+ // console.log(AppliedPreferences.encode(undefined))
166
+ // console.log(AppliedPreferences.encode({ }))
167
+ // console.log(AppliedPreferences.encode({ wait: 10 }))
168
+ // console.log(AppliedPreferences.encode({ asynchronous: undefined }))
169
+ // console.log(AppliedPreferences.encode({ asynchronous: false }))
170
+ // console.log(AppliedPreferences.encode({ asynchronous: true }))
171
+ // console.log(AppliedPreferences.encode({ preferences: new Map([
172
+ // [ 'respond-async', { value: undefined } ]
173
+ // ]) }))
174
+ // console.log(AppliedPreferences.encode({
175
+ // asynchronous: false,
176
+ // preferences: new Map([
177
+ // [ 'respond-async', { value: 'fake' } ]
178
+ // ]) }))
179
+ // console.log(AppliedPreferences.encode({ asynchronous: true, wait: 100 }))
180
+ // console.log(AppliedPreferences.encode({
181
+ // representation: DIRECTIVE_REPRESENTATION_MINIMAL,
182
+ // preferences: new Map([
183
+ // ['foo', { value: 'bar', parameters: new Map([ [ 'biz', 'bang' ] ]) } ],
184
+ // [ 'fake', undefined ]
185
+ // ])
186
+ // }))
187
+
188
+ // console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
189
+ // console.log(Preferences.parse(' foo; bar')?.preferences)
190
+ // console.log(Preferences.parse(' foo; bar=""')?.preferences)
191
+ // console.log(Preferences.parse(' foo=""; bar')?.preferences)
192
+ // console.log(Preferences.parse(' foo =""; bar;biz; bang ')?.preferences)
193
+ // console.log(Preferences.parse('return=minimal; foo="some parameter"')?.preferences)
194
+
195
+ // console.log(Preferences.parse('timezone=America/Los_Angeles'))
196
+ // console.log(Preferences.parse('return=headers-only'))
197
+ // console.log(Preferences.parse('return=minimal'))
198
+ // console.log(Preferences.parse('return=representation'))
199
+ // console.log(Preferences.parse('respond-async, wait=10`'))
200
+ // console.log(Preferences.parse('priority=5'))
201
+ // console.log(Preferences.parse('foo; bar'))
202
+ // console.log(Preferences.parse('foo; bar=""'))
203
+ // console.log(Preferences.parse('foo=""; bar'))
204
+ // console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
205
+ // console.log(Preferences.parse('return=minimal; foo="some parameter"'))
package/src/range.js CHANGED
@@ -77,7 +77,7 @@ export class Range {
77
77
  return { start, end: RANGE_EMPTY }
78
78
  }
79
79
 
80
- if(!Number.isInteger(start) || !Number.isInteger(end)) { return undefined }
80
+ if(!(Number.isInteger(start) && Number.isInteger(end))) { return undefined }
81
81
  return { start, end }
82
82
  })
83
83
  .filter(range => range !== undefined)
@@ -105,19 +105,14 @@ export class Range {
105
105
  return { start, end }
106
106
  })
107
107
 
108
- const exceeds = normalizedRanges.reduce((acc, value) => {
109
- return acc || (value.start >= contentLength) || (value.end >= contentLength)
110
- }, false)
108
+ const exceeds = normalizedRanges.reduce((acc, value) => (acc || (value.start >= contentLength) || (value.end >= contentLength)), false)
111
109
 
112
- const overlap = normalizedRanges
110
+ const { overlap } = normalizedRanges
113
111
  .toSorted((a, b) => a.start - b.start)
114
- .reduce((acc, item) => {
115
- return {
112
+ .reduce((acc, item) => ({
116
113
  overlap: acc.overlap || acc.end > item.start,
117
114
  end: item.end
118
- }
119
- }, { overlap: false, end: 0 })
120
- .overlap
115
+ }), { overlap: false, end: 0 })
121
116
 
122
117
  return {
123
118
  units: directive.units,
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send_bytes } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,6 +1,7 @@
1
1
  import http2 from 'node:http2'
2
- import { send } from './send-util.js'
2
+
3
3
  import { Conditional } from '../conditional.js'
4
+ import { send } from './send-util.js'
4
5
 
5
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
6
7
  /** @import { Metadata } from './defs.js' */
@@ -10,6 +10,8 @@ export const HTTP_HEADER_SEC_FETCH_MODE = 'sec-fetch-mode'
10
10
  export const HTTP_HEADER_SEC_FETCH_DEST = 'sec-fetch-dest'
11
11
  export const HTTP_HEADER_ACCEPT_POST = 'accept-post'
12
12
  export const HTTP_HEADER_ACCEPT_PATCH = 'accept-patch'
13
+ export const HTTP_HEADER_CLEAR_SITE_DATE = 'clear-site-data'
14
+ export const HTTP_HEADER_PREFERENCE_APPLIED = 'preference-applied'
13
15
 
14
16
  export const HTTP_METHOD_QUERY = 'QUERY'
15
17
  export const HTTP_HEADER_ACCEPT_QUERY = 'accept-query'
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { CONTENT_TYPE_TEXT } from '../content-type.js'
3
4
  import { send } from './send-util.js'
4
5
 
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,7 +1,8 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import {
3
- HTTP_HEADER_TIMING_ALLOW_ORIGIN,
4
4
  HTTP_HEADER_SERVER_TIMING,
5
+ HTTP_HEADER_TIMING_ALLOW_ORIGIN,
5
6
  ServerTiming
6
7
  } from '../server-timing.js'
7
8
 
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,5 +1,8 @@
1
+ /** biome-ignore-all lint/performance/noBarrelFile: entry point */
2
+ /** biome-ignore-all lint/performance/noReExportAll: entry point */
1
3
  export * from './defs.js'
2
4
  export * from './send-util.js'
5
+ // end common headers
3
6
 
4
7
  export * from './accepted.js'
5
8
  export * from './bytes.js'
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,7 +1,7 @@
1
1
  import http2 from 'node:http2'
2
2
 
3
- import { send_encoded } from './send-util.js'
4
3
  import { CONTENT_TYPE_JSON } from '../content-type.js'
4
+ import { send_encoded } from './send-util.js'
5
5
 
6
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
7
7
  /** @import { Metadata } from './defs.js' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,6 +1,7 @@
1
1
  import http2 from 'node:http2'
2
- import { send } from './send-util.js'
2
+
3
3
  import { Conditional } from '../conditional.js'
4
+ import { send } from './send-util.js'
4
5
 
5
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
6
7
  /** @import { Metadata } from './defs.js' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { CONTENT_TYPE_JSON } from '../content-type.js'
3
4
  import { send } from './send-util.js'
4
5
 
@@ -14,7 +15,7 @@ const { HTTP_STATUS_NOT_ACCEPTABLE } = http2.constants
14
15
  */
15
16
  export function sendNotAcceptable(stream, supportedTypes, meta) {
16
17
  const supportedTypesList = Array.isArray(supportedTypes) ? supportedTypes : [ supportedTypes ]
17
- const has = supportedTypesList.length !== 0
18
+ const has = supportedTypesList.length > 0
18
19
 
19
20
  send(stream,
20
21
  HTTP_STATUS_NOT_ACCEPTABLE,
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { CONTENT_TYPE_TEXT } from '../content-type.js'
3
4
  import { send } from './send-util.js'
4
5
 
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { CONTENT_TYPE_TEXT } from '../content-type.js'
3
4
  import { send } from './send-util.js'
4
5
 
@@ -1,7 +1,8 @@
1
1
  import http2 from 'node:http2'
2
- import { send } from './send-util.js'
3
- import { Conditional } from '../conditional.js'
2
+
4
3
  import { CacheControl } from '../cache-control.js'
4
+ import { Conditional } from '../conditional.js'
5
+ import { send } from './send-util.js'
5
6
 
6
7
  /** @import { ServerHttp2Stream } from 'node:http2' */
7
8
  /** @import { Metadata } from './defs.js' */
@@ -1,9 +1,9 @@
1
1
  import http2 from 'node:http2'
2
2
 
3
- import { send_bytes } from './send-util.js'
4
- import { RANGE_UNITS_BYTES } from "./defs.js"
5
- import { Multipart } from '../multipart.js'
6
3
  import { MIME_TYPE_MULTIPART_RANGE } from '../content-type.js'
4
+ import { Multipart } from '../multipart.js'
5
+ import { RANGE_UNITS_BYTES } from "./defs.js"
6
+ import { send_bytes } from './send-util.js'
7
7
 
8
8
  /** @import { ServerHttp2Stream } from 'node:http2' */
9
9
  /** @import { Metadata } from './defs.js' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,8 +1,9 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import {
3
- HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
4
4
  HTTP_HEADER_ACCEPT_QUERY,
5
5
  HTTP_METHOD_QUERY,
6
+ HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
6
7
  PREFLIGHT_AGE_SECONDS
7
8
  } from './defs.js'
8
9
  import { send } from './send-util.js'
@@ -1,6 +1,7 @@
1
1
  import http2 from 'node:http2'
2
- import { send } from './send-util.js'
2
+
3
3
  import { CONTENT_RANGE_UNKNOWN, ContentRange } from '../content-range.js'
4
+ import { send } from './send-util.js'
4
5
 
5
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
6
7
  /** @import { Metadata } from './defs.js' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -8,16 +8,16 @@ import {
8
8
  zstdCompressSync
9
9
  } from 'node:zlib'
10
10
 
11
+ import { CacheControl } from '../cache-control.js'
12
+ import { Conditional } from '../conditional.js'
13
+ import { ContentRange } from '../content-range.js'
14
+ import { CHARSET_UTF8 } from '../content-type.js'
15
+ import { HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
11
16
  import {
12
17
  coreHeaders,
13
18
  customHeaders,
14
19
  performanceHeaders
15
20
  } from './header-util.js'
16
- import { ContentRange } from '../content-range.js'
17
- import { CacheControl } from '../cache-control.js'
18
- import { Conditional } from '../conditional.js'
19
- import { HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
20
- import { CHARSET_UTF8 } from '../content-type.js'
21
21
 
22
22
  /** @import { ServerHttp2Stream } from 'node:http2' */
23
23
  /** @import { IncomingHttpHeaders } from 'node:http2' */
@@ -29,6 +29,12 @@ import { CHARSET_UTF8 } from '../content-type.js'
29
29
 
30
30
  /** @typedef {ArrayBufferLike|ArrayBufferView|ReadableStream|string} SendBody */
31
31
 
32
+ const {
33
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
34
+ HTTP_STATUS_NOT_FOUND,
35
+ HTTP_STATUS_UNAUTHORIZED
36
+ } = http2.constants
37
+
32
38
  const {
33
39
  HTTP2_HEADER_CONTENT_ENCODING,
34
40
  HTTP2_HEADER_VARY,
@@ -138,9 +144,9 @@ export function send_bytes(stream, status, contentType, obj, range, contentLengt
138
144
  */
139
145
  export function send(stream, status, headers, exposedHeaders, contentType, body, meta) {
140
146
  // if(status >= 400) { console.warn(status, body) }
141
- if(status === 401) { console.warn(status, body) }
142
- if(status === 404) { console.warn(status, body) }
143
- if(status >= 500) { console.warn(status, body) }
147
+ if(status === HTTP_STATUS_UNAUTHORIZED) { console.warn(status, body) }
148
+ if(status === HTTP_STATUS_NOT_FOUND) { console.warn(status, body) }
149
+ if(status >= HTTP_STATUS_INTERNAL_SERVER_ERROR) { console.warn(status, body) }
144
150
  // console.log('SEND', status, body?.byteLength)
145
151
 
146
152
  if(stream === undefined) { console.log('send - end stream undef'); return }
@@ -1,9 +1,10 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import {
3
- SSE_MIME,
4
- SSE_INACTIVE_STATUS_CODE,
5
- SSE_BOM,
6
4
  ENDING,
5
+ SSE_BOM,
6
+ SSE_INACTIVE_STATUS_CODE,
7
+ SSE_MIME,
7
8
  } from '@johntalton/sse-util'
8
9
  import { coreHeaders, performanceHeaders } from './header-util.js'
9
10
 
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { CONTENT_TYPE_TEXT } from '../content-type.js'
3
4
  import {
4
5
  HTTP_HEADER_RATE_LIMIT,
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { CONTENT_TYPE_MESSAGE_HTTP } from '../content-type.js'
3
4
  import { send } from './send-util.js'
4
5
 
@@ -8,6 +9,9 @@ import { send } from './send-util.js'
8
9
 
9
10
  const { HTTP_STATUS_OK } = http2.constants
10
11
 
12
+ const LINE_ENDING = '\n'
13
+ const PSEUDO_HEADER_PREFIX = ':'
14
+
11
15
  /**
12
16
  * @param {ServerHttp2Stream} stream
13
17
  * @param {string} method
@@ -27,13 +31,13 @@ export function sendTrace(stream, method, url, headers, meta) {
27
31
  const reconstructed = [
28
32
  `${method} ${url.pathname}${url.search} ${version}`,
29
33
  Object.entries(headers)
30
- .filter(([ key ]) => !key.startsWith(':'))
34
+ .filter(([ key ]) => !key.startsWith(PSEUDO_HEADER_PREFIX))
31
35
  .filter(([ key ]) => !FILTER_KEYS.includes(key))
32
36
  .map(([ key, value ]) => `${key}: ${value}`)
33
- .join('\n'),
34
- '\n'
37
+ .join(LINE_ENDING),
38
+ LINE_ENDING
35
39
  ]
36
- .join('\n')
40
+ .join(LINE_ENDING)
37
41
 
38
42
  send(stream, HTTP_STATUS_OK, {}, [], CONTENT_TYPE_MESSAGE_HTTP, reconstructed, meta)
39
43
  }
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { CONTENT_TYPE_TEXT } from '../content-type.js'
3
4
  import { send } from './send-util.js'
4
5
 
@@ -1,4 +1,5 @@
1
1
  import http2 from 'node:http2'
2
+
2
3
  import { send } from './send-util.js'
3
4
 
4
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -1,10 +1,13 @@
1
1
  import http2 from 'node:http2'
2
- import { HTTP_HEADER_ACCEPT_POST, HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
2
+
3
+ import { HTTP_HEADER_ACCEPT_PATCH, HTTP_HEADER_ACCEPT_POST, HTTP_HEADER_ACCEPT_QUERY } from './defs.js'
3
4
  import { send } from './send-util.js'
4
5
 
5
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
6
7
  /** @import { Metadata } from './defs.js' */
7
8
 
9
+ const { HTTP2_METHOD_POST, HTTP2_METHOD_PATCH } = http2.constants
10
+
8
11
  const { HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE } = http2.constants
9
12
 
10
13
  /**
@@ -14,13 +17,16 @@ const { HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE } = http2.constants
14
17
  * @param {Metadata} meta
15
18
  */
16
19
  export function sendUnsupportedMediaType(stream, acceptableMediaType, supportedQueryTypes, meta) {
17
- const acceptable = Array.isArray(acceptableMediaType) ? acceptableMediaType : [ acceptableMediaType ]
18
-
19
20
  const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
20
21
  const exposedHeaders = supportsQuery ? [ HTTP_HEADER_ACCEPT_QUERY, HTTP_HEADER_ACCEPT_POST ] : [ HTTP_HEADER_ACCEPT_POST ]
21
22
 
23
+ const method = HTTP2_METHOD_POST // todo pass in as parameter or split acceptable to post and patch types
24
+ const acceptable = Array.isArray(acceptableMediaType) ? acceptableMediaType : [ acceptableMediaType ]
25
+ const acceptHeader = (method === HTTP2_METHOD_POST) ? HTTP_HEADER_ACCEPT_POST : HTTP_HEADER_ACCEPT_PATCH
26
+ const acceptValue = ((method === HTTP2_METHOD_POST) || (method === HTTP2_METHOD_PATCH)) ? acceptable.join(',') : undefined
27
+
22
28
  send(stream, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, {
23
- [HTTP_HEADER_ACCEPT_POST]: acceptable.join(','),
29
+ [acceptHeader]: acceptValue,
24
30
  [HTTP_HEADER_ACCEPT_QUERY]: supportedQueryTypes?.join(',')
25
31
  }, exposedHeaders, undefined, undefined, meta)
26
32
  }