@johntalton/http-util 6.0.0 → 7.0.1

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 (56) hide show
  1. package/README.md +1 -1
  2. package/package.json +17 -2
  3. package/src/defs.js +34 -1
  4. package/src/headers/accept-encoding.js +9 -12
  5. package/src/headers/accept-language.js +10 -4
  6. package/src/headers/accept.js +0 -28
  7. package/src/headers/cache-control.js +0 -6
  8. package/src/headers/clear-site-data.js +4 -10
  9. package/src/headers/client-hints.js +9 -2
  10. package/src/headers/conditional.js +170 -106
  11. package/src/headers/content-disposition.js +0 -14
  12. package/src/headers/content-range.js +0 -17
  13. package/src/headers/content-type.js +1 -1
  14. package/src/headers/forwarded.js +2 -33
  15. package/src/headers/link.js +11 -12
  16. package/src/headers/multipart.js +19 -18
  17. package/src/headers/preference.js +0 -43
  18. package/src/headers/range.js +5 -31
  19. package/src/headers/rate-limit.js +7 -2
  20. package/src/headers/server-timing.js +1 -14
  21. package/src/headers/strict-transport-security.js +1 -0
  22. package/src/headers/util/mime.js +3 -3
  23. package/src/headers/util/whitespace.js +3 -1
  24. package/src/headers/www-authenticate.js +0 -1
  25. package/src/response/2xx/bytes.js +19 -13
  26. package/src/response/2xx/created.js +16 -8
  27. package/src/response/2xx/json.js +29 -10
  28. package/src/response/2xx/no-content.js +12 -6
  29. package/src/response/2xx/partial-content.js +17 -14
  30. package/src/response/2xx/preflight.js +22 -10
  31. package/src/response/3xx/found.js +25 -0
  32. package/src/response/3xx/moved-permanently.js +7 -5
  33. package/src/response/3xx/not-modified.js +13 -8
  34. package/src/response/3xx/permanent-redirect.js +4 -2
  35. package/src/response/3xx/see-other.js +7 -5
  36. package/src/response/3xx/temporary-redirect.js +4 -2
  37. package/src/response/4xx/bad-request.js +19 -0
  38. package/src/response/4xx/content-too-large.js +1 -1
  39. package/src/response/4xx/gone.js +1 -1
  40. package/src/response/4xx/im-a-teapot.js +1 -1
  41. package/src/response/4xx/not-acceptable.js +6 -4
  42. package/src/response/4xx/not-allowed.js +6 -4
  43. package/src/response/4xx/payment-required.js +17 -0
  44. package/src/response/4xx/precondition-failed.js +18 -3
  45. package/src/response/4xx/range-not-satisfiable.js +5 -3
  46. package/src/response/4xx/too-many-requests.js +8 -5
  47. package/src/response/4xx/unauthorized.js +1 -1
  48. package/src/response/4xx/unsupported-media.js +9 -5
  49. package/src/response/5xx/error.js +2 -3
  50. package/src/response/5xx/insufficient-storage.js +2 -2
  51. package/src/response/5xx/not-implemented.js +2 -3
  52. package/src/response/5xx/unavailable.js +7 -12
  53. package/src/response/header-util.js +1 -1
  54. package/src/response/index.js +4 -0
  55. package/src/response/response.js +8 -2
  56. package/src/response/send-util.js +46 -14
@@ -41,6 +41,8 @@ export class Forwarded {
41
41
  * @returns {Map<string, string>|undefined}
42
42
  */
43
43
  static selectRightMost(forwardedList, skipList = []) {
44
+ if(forwardedList === undefined) { return undefined }
45
+
44
46
  const iter = skipList[Symbol.iterator]()
45
47
 
46
48
  for(const forwarded of forwardedList.toReversed()) {
@@ -54,39 +56,6 @@ export class Forwarded {
54
56
  }
55
57
  }
56
58
 
57
-
58
- /*
59
- const examples = [
60
- { f: [], s: [], ef: undefined },
61
-
62
- { f: [], s: [ '1.1.1.1' ] , ef: undefined },
63
- { f: [], s: [ '*' ] , ef: undefined },
64
-
65
- { f: [ { for: '1.1.1.1' } ], s: [], ef: '1.1.1.1' },
66
- { f: [ { for: '1.1.1.1' } ], s: [ '*' ], ef: undefined },
67
- { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' } ], s: [], ef: '2.2.2.2' },
68
- { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' } ], s: [ '2.2.2.2' ], ef: '1.1.1.1' },
69
- { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '3.3.3.3', '2.2.2.2' ], ef: '1.1.1.1' },
70
- { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '3.3.3.3', '*' ], ef: '1.1.1.1' },
71
- { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '*', '*' ], ef: '1.1.1.1' },
72
- { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '*', '*', '*' ], ef: undefined },
73
-
74
- { f: [ { for: '1.1.1.1' } ], s: [ '*', '*' ], ef: undefined },
75
-
76
- { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '3.3.3.3'], ef: '2.2.2.2' },
77
- { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '*'], ef: '2.2.2.2' },
78
- ]
79
-
80
- for(const { f, s, ef } of examples) {
81
- const result = Forwarded.selectRightMost(f.map(i => new Map(Object.entries(i))), s)
82
- const resultFor = result?.get('for')
83
- if(resultFor !== ef) {
84
- console.log(`mismatch ${ef} !== ${resultFor}`)
85
- }
86
- }
87
- */
88
-
89
-
90
59
  /*
91
60
  const examples = [
92
61
  null,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @typedef {Object} LinkItem
3
- * @property {string} url
3
+ * @property {URL|string} url
4
4
  * @property {string|undefined} [relation]
5
5
  * @property {Map<string, string>|undefined} [parameters]
6
6
  */
@@ -9,8 +9,8 @@ export class Link {
9
9
  /**
10
10
  * @param {LinkItem} link
11
11
  */
12
- static *#encode(link) {
13
- const encodedUri = encodeURI(link.url)
12
+ static *#encode(link) {
13
+ const encodedUri = (link.url instanceof URL) ? link.url : encodeURI(link.url)
14
14
 
15
15
  yield `<${encodedUri}>`
16
16
  if(link.relation !== undefined) { yield `rel="${link.relation}"` }
@@ -20,16 +20,15 @@ export class Link {
20
20
  }
21
21
  }
22
22
 
23
-
24
23
  /**
25
- * @param {LinkItem} link
24
+ * @param {Array<LinkItem>|LinkItem|undefined} links
26
25
  */
27
- static encode(link) {
28
- return [ ...Link.#encode(link) ].join('; ')
26
+ static encode(links) {
27
+ if(links === undefined) { return undefined }
28
+ const linkAry = Array.isArray(links) ? links : [ links ]
29
+ if(linkAry.length === 0) { return undefined }
30
+ return linkAry
31
+ .map(link => [ ...Link.#encode(link) ].join('; '))
32
+ .join(', ')
29
33
  }
30
34
  }
31
-
32
- // console.log(Link.encode({ url: '/index.html', parameters: new Map([ [ 'as', 'style' ], [ 'fetchpriority', 'high' ] ]) }))
33
- // console.log(Link.encode({ url: '/index.html', relation: 'next', parameters: new Map([ [ 'fetchpriority', 'high' ] ]) }))
34
- // console.log(Link.encode({ url: '/index.html', relation: 'next' }))
35
- // console.log(Link.encode({ url: 'https://example.com/苗条', relation: 'preconnect' }))
@@ -49,11 +49,14 @@ export class Multipart {
49
49
  * @param {string} text
50
50
  * @param {string} boundary
51
51
  * @param {string} [_charset='utf8']
52
+ * @returns {FormData}
52
53
  */
53
54
  static parse_FormData(text, boundary, _charset = 'utf8') {
54
- // console.log({ boundary, text })
55
55
  const formData = new FormData()
56
56
 
57
+ if(text === undefined) { return formData }
58
+ if(boundary === undefined) { return formData }
59
+
57
60
  if(text === '') {
58
61
  // empty body
59
62
  return formData
@@ -61,11 +64,6 @@ export class Multipart {
61
64
 
62
65
  const lines = text.split(MULTIPART_SEPARATOR)
63
66
 
64
- if(lines.length === 0) {
65
- // missing body?
66
- return formData
67
- }
68
-
69
67
  const boundaryBegin = `${BOUNDARY_MARK}${boundary}`
70
68
  const boundaryEnd = `${BOUNDARY_MARK}${boundary}${BOUNDARY_MARK}`
71
69
 
@@ -94,8 +92,8 @@ export class Multipart {
94
92
  const name = rawName?.toLowerCase()
95
93
  // console.log('header', name, value)
96
94
  if(name === MULTIPART_HEADER.CONTENT_TYPE) {
97
- const _contentType = ContentType.parse(value)
98
- // console.log({ contentType })
95
+ const contentType = ContentType.parse(value)
96
+ //console.log({ contentType })
99
97
  }
100
98
  else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
101
99
  const disposition = ContentDisposition.parse(value)
@@ -103,8 +101,7 @@ export class Multipart {
103
101
  throw new Error('disposition not form-data')
104
102
  }
105
103
 
106
- // todo: are names always quoted?
107
- partName = disposition.name?.slice(1, -1)
104
+ partName = disposition.name
108
105
  }
109
106
  else {
110
107
  // unsupported part header - ignore
@@ -128,10 +125,9 @@ export class Multipart {
128
125
  }
129
126
  state = MULTIPART_STATE.HEADERS
130
127
  }
131
- else {
132
- throw new Error('unknown state')
133
- }
134
-
128
+ // else {
129
+ // throw new Error('unknown state')
130
+ // }
135
131
  }
136
132
 
137
133
  return formData
@@ -150,6 +146,7 @@ export class Multipart {
150
146
 
151
147
  return new ReadableStream({
152
148
  type: 'bytes',
149
+ // async pull(controller) {},
153
150
  async start(controller) {
154
151
  const encoder = new TextEncoder()
155
152
 
@@ -158,7 +155,6 @@ export class Multipart {
158
155
  controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_TYPE}: ${contentType}${MULTIPART_SEPARATOR}`))
159
156
  controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_RANGE}: ${ContentRange.encode({ ...part.range, size: contentLength })}${MULTIPART_SEPARATOR}`))
160
157
  controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
161
- // controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
162
158
 
163
159
  if(part.obj instanceof ReadableStream) {
164
160
  // biome-ignore lint/performance/noAwaitInLoops: readable
@@ -175,7 +171,10 @@ export class Multipart {
175
171
  }
176
172
  }
177
173
  }
178
- else if(part.obj instanceof ArrayBuffer || ArrayBuffer.isView(part.obj)) {
174
+ else if(part.obj instanceof ArrayBuffer) {
175
+ controller.enqueue(new Uint8Array(part.obj))
176
+ }
177
+ else if(ArrayBuffer.isView(part.obj)) {
179
178
  controller.enqueue(part.obj)
180
179
  }
181
180
  else if(typeof part.obj === 'string'){
@@ -183,7 +182,7 @@ export class Multipart {
183
182
  }
184
183
  else {
185
184
  // console.log('error', typeof part.obj, part.obj)
186
- throw new Error('unknown part type')
185
+ controller.error(new Error('unknown part type'))
187
186
  }
188
187
 
189
188
  controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
@@ -191,7 +190,9 @@ export class Multipart {
191
190
 
192
191
  controller.enqueue(encoder.encode(boundaryEnd))
193
192
 
194
- controller.close()
193
+ // controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
194
+
195
+ controller.close()
195
196
  }
196
197
  })
197
198
  }
@@ -128,46 +128,3 @@ export class AppliedPreferences {
128
128
  return AppliedPreferences.#encode_Map(applied)
129
129
  }
130
130
  }
131
-
132
-
133
- // console.log(AppliedPreferences.encode(undefined))
134
- // console.log(AppliedPreferences.encode({ }))
135
- // console.log(AppliedPreferences.encode({ wait: 10 }))
136
- // console.log(AppliedPreferences.encode({ asynchronous: undefined }))
137
- // console.log(AppliedPreferences.encode({ asynchronous: false }))
138
- // console.log(AppliedPreferences.encode({ asynchronous: true }))
139
- // console.log(AppliedPreferences.encode({ preferences: new Map([
140
- // [ 'respond-async', { value: undefined } ]
141
- // ]) }))
142
- // console.log(AppliedPreferences.encode({
143
- // asynchronous: false,
144
- // preferences: new Map([
145
- // [ 'respond-async', { value: 'fake' } ]
146
- // ]) }))
147
- // console.log(AppliedPreferences.encode({ asynchronous: true, wait: 100 }))
148
- // console.log(AppliedPreferences.encode({
149
- // representation: DIRECTIVE_REPRESENTATION_MINIMAL,
150
- // preferences: new Map([
151
- // ['foo', { value: 'bar', parameters: new Map([ [ 'biz', 'bang' ] ]) } ],
152
- // [ 'fake', undefined ]
153
- // ])
154
- // }))
155
-
156
- // console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
157
- // console.log(Preferences.parse(' foo; bar')?.preferences)
158
- // console.log(Preferences.parse(' foo; bar=""')?.preferences)
159
- // console.log(Preferences.parse(' foo=""; bar')?.preferences)
160
- // console.log(Preferences.parse(' foo =""; bar;biz; bang ')?.preferences)
161
- // console.log(Preferences.parse('return=minimal; foo="some parameter"')?.preferences)
162
-
163
- // console.log(Preferences.parse('timezone=America/Los_Angeles'))
164
- // console.log(Preferences.parse('return=headers-only'))
165
- // console.log(Preferences.parse('return=minimal'))
166
- // console.log(Preferences.parse('return=representation'))
167
- // console.log(Preferences.parse('respond-async, wait=10`'))
168
- // console.log(Preferences.parse('priority=5'))
169
- // console.log(Preferences.parse('foo; bar'))
170
- // console.log(Preferences.parse('foo; bar=""'))
171
- // console.log(Preferences.parse('foo=""; bar'))
172
- // console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
173
- // console.log(Preferences.parse('return=minimal; foo="some parameter"'))
@@ -1,5 +1,7 @@
1
1
  import { RANGE_UNITS_BYTES } from '../defs.js'
2
2
 
3
+ /** @import { AcceptRangeUnits } from '../defs.js' */
4
+
3
5
  export const RANGE_EQUAL = '='
4
6
  export const RANGE_SEPARATOR = '-'
5
7
  export const RANGE_LIST_SEPARATOR = ','
@@ -32,7 +34,7 @@ export const RANGE_EMPTY = ''
32
34
  /**
33
35
  * @template RV
34
36
  * @typedef {Object} RangeDirective
35
- * @property {'bytes'|'none'|undefined} units
37
+ * @property {AcceptRangeUnits|undefined} units
36
38
  * @property {Array<RV>} ranges
37
39
  */
38
40
 
@@ -97,6 +99,8 @@ export class Range {
97
99
  */
98
100
  static normalize(directive, contentLength) {
99
101
  if(directive === undefined) { return undefined }
102
+ if(!Number.isInteger(contentLength)) { return undefined }
103
+ if(contentLength <= 0) { return undefined }
100
104
 
101
105
  /** @type {Array<NormalizedRangeValue>} */
102
106
  const normalizedRanges = directive.ranges.map(({ start, end }) => {
@@ -122,33 +126,3 @@ export class Range {
122
126
  }
123
127
  }
124
128
  }
125
-
126
- // console.log(Range.parse(''))
127
- // console.log(Range.parse('='))
128
- // console.log(Range.parse('foo'))
129
- // console.log(Range.parse('bytes'))
130
- // console.log(Range.parse('bytes='))
131
- // console.log(Range.parse('bytes=-'))
132
- // console.log(Range.parse('bytes=foo'))
133
- // console.log(Range.parse('bytes=0-foo'))
134
- // console.log(Range.parse('bytes=0-0xff'))
135
- // console.log()
136
- // console.log(Range.parse('bytes=1024-'))
137
- // console.log(Range.parse('bytes=-1024'))
138
- // console.log(Range.parse('bytes=0-1024'))
139
- // console.log()
140
- // console.log(Range.parse('bytes=0-0,-1'))
141
- // console.log(Range.parse('bytes=0-1024, -1024'))
142
- // console.log(Range.parse('bytes= 0-999, 4500-5499, -1000'))
143
- // console.log(Range.parse('bytes=500-600,601-999'))
144
-
145
- // console.log('------')
146
- // console.log(Range.normalize(Range.parse('bytes=1024-'), 5000))
147
- // console.log(Range.normalize(Range.parse('bytes=-1024'), 5000))
148
- // console.log(Range.normalize(Range.parse('bytes=0-1024'), 5000))
149
- // console.log(Range.normalize(Range.parse('bytes=0-0,-1'), 10000)) // 0 and 9999
150
- // console.log(Range.normalize(Range.parse('bytes=0-1024, -1024'), 5000))
151
- // console.log(Range.normalize(Range.parse('bytes= 0-999, 4500-5499, -1000'), 5000))
152
- // console.log(Range.normalize(Range.parse('bytes=500-600,601-999'), 5000))
153
-
154
- // console.log(Range.normalize(Range.parse('bytes=-500'), 10000)) // 9500-9999
@@ -1,6 +1,6 @@
1
1
  // https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-10.html
2
2
 
3
- export const HTTP_HEADER_RATE_LIMIT = 'RateLimit'
3
+ export const HTTP_HEADER_RATE_LIMIT = 'RateLimit'
4
4
  export const HTTP_HEADER_RATE_LIMIT_POLICY = 'RateLimit-Policy'
5
5
 
6
6
  /**
@@ -45,6 +45,7 @@ export class RateLimit {
45
45
  * @param {RateLimitInfo} limitInfo
46
46
  */
47
47
  static from(limitInfo) {
48
+ if(limitInfo === undefined) { return undefined }
48
49
  const { name, remaining, resetSeconds, partitionKey } = limitInfo
49
50
 
50
51
  if(name === undefined || remaining === undefined) { return undefined }
@@ -61,8 +62,12 @@ export class RateLimitPolicy {
61
62
  */
62
63
  static from(...policies) {
63
64
  if(policies === undefined) { return undefined }
65
+ if(policies.length === 0) { return undefined }
64
66
 
65
- return policies
67
+ const remainingPolicies = policies.filter(pol => pol !== undefined)
68
+ if(remainingPolicies.length === 0) { return undefined }
69
+
70
+ return remainingPolicies
66
71
  .filter(policy => policy.name !== undefined && policy.quota !== undefined)
67
72
  .map(policy => {
68
73
  const {
@@ -19,7 +19,7 @@ export const SERVER_TIMING_SEPARATOR = {
19
19
 
20
20
  export class ServerTiming {
21
21
  /**
22
- * @param {Array<TimingsInfo>} timings
22
+ * @param {Array<TimingsInfo>|undefined} timings
23
23
  */
24
24
  static encode(timings) {
25
25
  if(timings === undefined) { return undefined }
@@ -36,16 +36,3 @@ export class ServerTiming {
36
36
  .join(SERVER_TIMING_SEPARATOR.METRIC)
37
37
  }
38
38
  }
39
-
40
-
41
- // console.log(ServerTiming.encode([{ name: 'missedCache' }]))
42
- // console.log(ServerTiming.encode([{ name: 'cpu', duration: 2.4 }]))
43
-
44
- // // cache;desc="Cache Read";dur=23.2
45
- // console.log(ServerTiming.encode([{ name: 'cache', duration: 23.2, description: "Cache Read" }]))
46
-
47
- // // db;dur=53, app;dur=47.2
48
- // console.log(ServerTiming.encode([
49
- // { name: 'db', duration: 54 },
50
- // { name: 'app', duration: 47.2 }
51
- // ]))
@@ -33,6 +33,7 @@ export class StrictTransportSecurity {
33
33
  * @param {StrictTransportSecurityOptions} sts
34
34
  */
35
35
  static encode(sts) {
36
+ if(sts === undefined) { return undefined }
36
37
  return [ ...StrictTransportSecurity.#encode(sts) ].join('; ')
37
38
  }
38
39
  }
@@ -19,7 +19,7 @@ export const SPECIAL_CHARS = [
19
19
  '{', '}',
20
20
  '@', ',', ';', ':',
21
21
  '\\', '"', '/', '?', '=', // '.',
22
- // '%', // '!', '$', '&', // # ^ * | ~ `
22
+ // '%', // '!', '$', '&', // # ^ * | ~ `
23
23
  // space
24
24
  ' ', '\u000B', '\u000C',
25
25
  // control
@@ -66,10 +66,10 @@ export class Mime {
66
66
  // if(candidateSubtype === '') { return undefined }
67
67
  if(hasSpecialChar(candidateSubtype)) { return undefined }
68
68
 
69
- const subtype = (candidateSubtype === '') ? MIME_ANY : candidateSubtype ?? MIME_ANY
69
+ const subtype = (candidateSubtype === '') ? MIME_ANY : (candidateSubtype ?? MIME_ANY)
70
70
 
71
71
  return {
72
- mimetype: `${type}${MIME_SEPARATOR.SUBTYPE}${subtype ?? MIME_ANY}`,
72
+ mimetype: `${type}${MIME_SEPARATOR.SUBTYPE}${subtype}`,
73
73
  type,
74
74
  subtype
75
75
  }
@@ -3,4 +3,6 @@ export const WHITESPACE_REGEX = /\s/
3
3
  /**
4
4
  * @param {string} c
5
5
  */
6
- export function isWhitespace(c){ return WHITESPACE_REGEX.test(c) }
6
+ export function isWhitespace(c){
7
+ return WHITESPACE_REGEX.test(c)
8
+ }
@@ -31,7 +31,6 @@ export function paramNeedQuotes(paramName) {
31
31
  return PARAMETERS_THAT_NEED_QUOTES.includes(paramName.toLowerCase())
32
32
  }
33
33
 
34
-
35
34
  export class Challenge {
36
35
  /**
37
36
  * @param {string} realm
@@ -3,25 +3,31 @@ import http2 from 'node:http2'
3
3
  import { send_bytes } from '../send-util.js'
4
4
 
5
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
6
- /** @import { Metadata } from '../../defs.js' */
7
- /** @import { EtagItem } from '../../headers/conditional.js' */
8
- /** @import { CacheControlOptions } from '../../headers/cache-control.js' */
9
- /** @import { SendBody } from '../send-util.js' */
6
+ /** @import { SendContent, SendInfo, Metadata, SendBody } from '../../defs.js' */
10
7
 
11
8
  const { HTTP_STATUS_OK } = http2.constants
12
9
 
13
10
  /**
14
11
  * @param {ServerHttp2Stream} stream
15
12
  * @param {SendBody|undefined} obj
16
- * @param {string|undefined} contentType
17
- * @param {number|undefined} contentLength
18
- * @param {string|undefined} encoding
19
- * @param {EtagItem|undefined} etag
20
- * @param {number|undefined} age
21
- * @param {CacheControlOptions} cacheControl
22
- * @param {'bytes'|'none'|undefined} acceptRanges
13
+ * @param {Omit<SendContent, 'rangeDirective'>} content
14
+ * @param {Pick<SendInfo, 'acceptRanges'>} info
23
15
  * @param {Metadata} meta
24
16
  */
25
- export function sendBytes(stream, contentType, obj, contentLength, encoding, etag, age, cacheControl, acceptRanges, meta) {
26
- send_bytes(stream, HTTP_STATUS_OK, contentType, obj, undefined, contentLength, encoding, etag, age, cacheControl, acceptRanges, undefined, meta)
17
+ export function sendBytes(stream, obj, content, info, meta) {
18
+ const {
19
+ contentType,
20
+ contentLength,
21
+ encoding,
22
+ etag,
23
+ lastModified,
24
+ age,
25
+ cacheControl,
26
+ } = content
27
+
28
+ const { acceptRanges } = info
29
+
30
+ const supportedQueryType = undefined
31
+ const range = undefined
32
+ send_bytes(stream, HTTP_STATUS_OK, contentType, obj, range, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryType, meta)
27
33
  }
@@ -4,25 +4,33 @@ import { Conditional } from '../../headers/conditional.js'
4
4
  import { send } from '../send-util.js'
5
5
 
6
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
7
- /** @import { Metadata } from '../../defs.js' */
8
- /** @import { EtagItem } from '../../headers/conditional.js' */
7
+ /** @import { SendContent, Metadata } from '../../defs.js' */
9
8
 
10
9
  const {
11
10
  HTTP2_HEADER_LOCATION,
12
- HTTP2_HEADER_ETAG
11
+ HTTP2_HEADER_ETAG,
12
+ HTTP2_HEADER_LAST_MODIFIED
13
13
  } = http2.constants
14
14
 
15
15
  const { HTTP_STATUS_CREATED } = http2.constants
16
16
 
17
17
  /**
18
18
  * @param {ServerHttp2Stream} stream
19
- * @param {URL} location
20
- * @param {EtagItem|undefined} etag
19
+ * @param {URL|string} location
20
+ * @param {Pick<SendContent, 'etag' | 'lastModified'>} content
21
21
  * @param {Metadata} meta
22
22
  */
23
- export function sendCreated(stream, location, etag, meta) {
23
+ export function sendCreated(stream, location, content, meta) {
24
+ const {
25
+ etag,
26
+ lastModified
27
+ } = content
28
+
29
+ const loc = (location instanceof URL) ? location.href : location
30
+
24
31
  send(stream, HTTP_STATUS_CREATED, {
25
- [HTTP2_HEADER_LOCATION]: location.href,
26
- [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
32
+ [HTTP2_HEADER_LOCATION]: loc,
33
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
34
+ [HTTP2_HEADER_LAST_MODIFIED]: Conditional.encodeFixDate(lastModified)
27
35
  }, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
28
36
  }
@@ -4,25 +4,44 @@ import { CONTENT_TYPE_JSON } from '../../headers/content-type.js'
4
4
  import { send_encoded } from '../send-util.js'
5
5
 
6
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
7
- /** @import { Metadata } from '../../defs.js' */
8
- /** @import { EtagItem } from '../../headers/conditional.js' */
9
- /** @import { CacheControlOptions } from '../../headers/cache-control.js' */
7
+ /** @import { SendContent, SendInfo, Metadata } from '../../defs.js' */
10
8
 
11
9
  const { HTTP_STATUS_OK } = http2.constants
12
10
 
13
11
  /**
12
+ * @deprecated
14
13
  * @param {ServerHttp2Stream} stream
15
14
  * @param {Object} obj
16
- * @param {string|undefined} encoding
17
- * @param {EtagItem|undefined} etag
18
- * @param {number|undefined} age
19
- * @param {CacheControlOptions} cacheControl
20
- * @param {Array<string>|undefined} supportedQueryTypes
15
+ * @param {Omit<SendContent, 'contentType' | 'contentLength' | 'rangeDirective'>} content
16
+ * @param {Pick<SendInfo, 'supportedQueryTypes'>} info
21
17
  * @param {Metadata} meta
22
18
  */
23
- export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl, supportedQueryTypes, meta) {
19
+ export function sendJSON_Encoded(stream, obj, content, info, meta) {
20
+ sendJSON(stream, obj, content, info, meta)
21
+ }
22
+
23
+ /**
24
+ * @param {ServerHttp2Stream} stream
25
+ * @param {Object} obj
26
+ * @param {Omit<SendContent, 'contentType' | 'contentLength' | 'rangeDirective'>} content
27
+ * @param {Pick<SendInfo, 'supportedQueryTypes'>} info
28
+ * @param {Metadata} meta
29
+ */
30
+ export function sendJSON(stream, obj, content, info, meta) {
31
+ const {
32
+ encoding,
33
+ etag,
34
+ lastModified,
35
+ age,
36
+ cacheControl
37
+ } = content
38
+
39
+ const {
40
+ supportedQueryTypes
41
+ } = info
42
+
24
43
  if(stream.closed) { return }
25
44
 
26
45
  const json = JSON.stringify(obj)
27
- send_encoded(stream, HTTP_STATUS_OK, CONTENT_TYPE_JSON, json, encoding, etag, age, cacheControl, undefined, supportedQueryTypes, meta)
46
+ send_encoded(stream, HTTP_STATUS_OK, CONTENT_TYPE_JSON, json, encoding, etag, lastModified, age, cacheControl, undefined, supportedQueryTypes, meta)
28
47
  }
@@ -4,22 +4,28 @@ import { Conditional } from '../../headers/conditional.js'
4
4
  import { send } from '../send-util.js'
5
5
 
6
6
  /** @import { ServerHttp2Stream } from 'node:http2' */
7
- /** @import { Metadata } from '../../defs.js' */
8
- /** @import { EtagItem } from '../../headers/conditional.js' */
7
+ /** @import { SendContent, Metadata } from '../../defs.js' */
9
8
 
10
9
  const {
11
- HTTP2_HEADER_ETAG
10
+ HTTP2_HEADER_ETAG,
11
+ HTTP2_HEADER_LAST_MODIFIED
12
12
  } = http2.constants
13
13
 
14
14
  const { HTTP_STATUS_NO_CONTENT } = http2.constants
15
15
 
16
16
  /**
17
17
  * @param {ServerHttp2Stream} stream
18
- * @param {EtagItem|undefined} etag
18
+ * @param {Pick<SendContent, 'etag' | 'lastModified'>} content
19
19
  * @param {Metadata} meta
20
20
  */
21
- export function sendNoContent(stream, etag, meta) {
21
+ export function sendNoContent(stream, content, meta) {
22
+ const {
23
+ etag,
24
+ lastModified
25
+ } = content
26
+
22
27
  send(stream, HTTP_STATUS_NO_CONTENT, {
23
- [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
28
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
29
+ [HTTP2_HEADER_LAST_MODIFIED]: Conditional.encodeFixDate(lastModified)
24
30
  }, [], undefined, undefined, meta)
25
31
  }
@@ -1,16 +1,13 @@
1
1
  import http2 from 'node:http2'
2
2
 
3
3
  import { RANGE_UNITS_BYTES } from '../../defs.js'
4
- import { MIME_TYPE_MULTIPART_RANGE } from '../../headers/content-type.js'
4
+ import { MIME_TYPE_MULTIPART_RANGE, MIME_TYPE_OCTET_STREAM } from '../../headers/content-type.js'
5
5
  import { Multipart } from '../../headers/multipart.js'
6
6
  import { send_bytes } from '../send-util.js'
7
7
 
8
8
  /** @import { ServerHttp2Stream } from 'node:http2' */
9
- /** @import { Metadata } from '../../defs.js' */
10
- /** @import { EtagItem } from '../../headers/conditional.js' */
11
- /** @import { CacheControlOptions } from '../../headers/cache-control.js' */
9
+ /** @import { SendContent, Metadata, SendBody } from '../../defs.js' */
12
10
  /** @import { ContentRangeDirective } from '../../headers/content-range.js' */
13
- /** @import { SendBody } from '../send-util.js' */
14
11
 
15
12
  const { HTTP_STATUS_PARTIAL_CONTENT } = http2.constants
16
13
 
@@ -27,23 +24,28 @@ const { HTTP_STATUS_PARTIAL_CONTENT } = http2.constants
27
24
 
28
25
  /**
29
26
  * @param {ServerHttp2Stream} stream
30
- * @param {string} contentType
31
27
  * @param {NonEmptyArray<PartialBytes>|PartialBytes} objs
32
- * @param {number|undefined} contentLength
33
- * @param {string|undefined} encoding
34
- * @param {EtagItem|undefined} etag
35
- * @param {number|undefined} age
36
- * @param {CacheControlOptions} cacheControl
28
+ * @param {Omit<SendContent, 'rangeDirective'>} content
37
29
  * @param {Metadata} meta
38
30
  */
39
- export function sendPartialContent(stream, contentType, objs, contentLength, encoding, etag, age, cacheControl, meta) {
31
+ export function sendPartialContent(stream, objs, content, meta) {
32
+ const {
33
+ contentType,
34
+ contentLength,
35
+ encoding,
36
+ etag,
37
+ lastModified,
38
+ age,
39
+ cacheControl
40
+ } = content
41
+
40
42
  const acceptRanges = RANGE_UNITS_BYTES
41
43
  const supportedQueryTypes = undefined
42
44
 
43
45
  if(Array.isArray(objs) && objs.length > 1) {
44
46
  // send using multipart bytes
45
47
  const boundary = 'PARTIAL_CONTENT_BOUNDARY' // todo make unique for content
46
- const obj = Multipart.encode_Bytes(contentType, objs, contentLength, boundary)
48
+ const obj = Multipart.encode_Bytes(contentType ?? MIME_TYPE_OCTET_STREAM, objs, contentLength, boundary)
47
49
 
48
50
  const multipartContentType = `${MIME_TYPE_MULTIPART_RANGE}; boundary=${boundary}`
49
51
 
@@ -56,6 +58,7 @@ export function sendPartialContent(stream, contentType, objs, contentLength, enc
56
58
  undefined,
57
59
  encoding,
58
60
  etag,
61
+ lastModified,
59
62
  age,
60
63
  cacheControl,
61
64
  acceptRanges,
@@ -67,5 +70,5 @@ export function sendPartialContent(stream, contentType, objs, contentLength, enc
67
70
 
68
71
  // single range, send as regular object
69
72
  const obj = Array.isArray(objs) ? objs[0] : objs
70
- send_bytes(stream, HTTP_STATUS_PARTIAL_CONTENT, contentType, obj.obj, obj.range, undefined, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta)
73
+ send_bytes(stream, HTTP_STATUS_PARTIAL_CONTENT, contentType, obj.obj, obj.range, undefined, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta)
71
74
  }