@johntalton/http-util 6.0.0 → 6.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 (43) hide show
  1. package/package.json +16 -2
  2. package/src/defs.js +34 -1
  3. package/src/headers/accept-encoding.js +9 -12
  4. package/src/headers/accept-language.js +10 -4
  5. package/src/headers/accept.js +0 -28
  6. package/src/headers/cache-control.js +0 -6
  7. package/src/headers/clear-site-data.js +4 -10
  8. package/src/headers/client-hints.js +9 -2
  9. package/src/headers/conditional.js +169 -105
  10. package/src/headers/content-disposition.js +0 -14
  11. package/src/headers/content-range.js +0 -17
  12. package/src/headers/content-type.js +1 -1
  13. package/src/headers/forwarded.js +2 -33
  14. package/src/headers/link.js +10 -11
  15. package/src/headers/multipart.js +17 -9
  16. package/src/headers/preference.js +0 -43
  17. package/src/headers/range.js +3 -31
  18. package/src/headers/rate-limit.js +6 -1
  19. package/src/headers/server-timing.js +1 -14
  20. package/src/headers/strict-transport-security.js +1 -0
  21. package/src/headers/util/mime.js +2 -2
  22. package/src/headers/util/whitespace.js +3 -1
  23. package/src/response/2xx/bytes.js +41 -6
  24. package/src/response/2xx/created.js +26 -5
  25. package/src/response/2xx/json.js +36 -4
  26. package/src/response/2xx/no-content.js +25 -5
  27. package/src/response/2xx/partial-content.js +38 -8
  28. package/src/response/2xx/preflight.js +26 -7
  29. package/src/response/3xx/found.js +23 -0
  30. package/src/response/3xx/not-modified.js +27 -3
  31. package/src/response/4xx/bad-request.js +19 -0
  32. package/src/response/4xx/not-acceptable.js +12 -1
  33. package/src/response/4xx/not-allowed.js +15 -4
  34. package/src/response/4xx/payment-required.js +17 -0
  35. package/src/response/4xx/precondition-failed.js +32 -3
  36. package/src/response/4xx/range-not-satisfiable.js +12 -1
  37. package/src/response/4xx/too-many-requests.js +18 -1
  38. package/src/response/4xx/unauthorized.js +1 -1
  39. package/src/response/4xx/unsupported-media.js +19 -2
  40. package/src/response/5xx/unavailable.js +13 -1
  41. package/src/response/index.js +4 -0
  42. package/src/response/response.js +6 -0
  43. package/src/response/send-util.js +23 -10
@@ -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
  */
@@ -10,7 +10,7 @@ export class Link {
10
10
  * @param {LinkItem} link
11
11
  */
12
12
  static *#encode(link) {
13
- const encodedUri = encodeURI(link.url)
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' }))
@@ -1,5 +1,6 @@
1
1
  import { ReadableStream } from 'node:stream/web'
2
2
 
3
+ import { isQuoted, stripQuotes } from '../headers/util/quote.js'
3
4
  import { ContentDisposition } from './content-disposition.js'
4
5
  import { ContentRange } from './content-range.js'
5
6
  import { ContentType } from './content-type.js'
@@ -49,11 +50,14 @@ export class Multipart {
49
50
  * @param {string} text
50
51
  * @param {string} boundary
51
52
  * @param {string} [_charset='utf8']
53
+ * @returns {FormData}
52
54
  */
53
55
  static parse_FormData(text, boundary, _charset = 'utf8') {
54
- // console.log({ boundary, text })
55
56
  const formData = new FormData()
56
57
 
58
+ if(text === undefined) { return formData }
59
+ if(boundary === undefined) { return formData }
60
+
57
61
  if(text === '') {
58
62
  // empty body
59
63
  return formData
@@ -94,8 +98,8 @@ export class Multipart {
94
98
  const name = rawName?.toLowerCase()
95
99
  // console.log('header', name, value)
96
100
  if(name === MULTIPART_HEADER.CONTENT_TYPE) {
97
- const _contentType = ContentType.parse(value)
98
- // console.log({ contentType })
101
+ const contentType = ContentType.parse(value)
102
+ //console.log({ contentType })
99
103
  }
100
104
  else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
101
105
  const disposition = ContentDisposition.parse(value)
@@ -103,8 +107,7 @@ export class Multipart {
103
107
  throw new Error('disposition not form-data')
104
108
  }
105
109
 
106
- // todo: are names always quoted?
107
- partName = disposition.name?.slice(1, -1)
110
+ partName = isQuoted(disposition.name) ? stripQuotes(disposition.name) : disposition.name
108
111
  }
109
112
  else {
110
113
  // unsupported part header - ignore
@@ -150,6 +153,7 @@ export class Multipart {
150
153
 
151
154
  return new ReadableStream({
152
155
  type: 'bytes',
156
+ // async pull(controller) {},
153
157
  async start(controller) {
154
158
  const encoder = new TextEncoder()
155
159
 
@@ -158,7 +162,6 @@ export class Multipart {
158
162
  controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_TYPE}: ${contentType}${MULTIPART_SEPARATOR}`))
159
163
  controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_RANGE}: ${ContentRange.encode({ ...part.range, size: contentLength })}${MULTIPART_SEPARATOR}`))
160
164
  controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
161
- // controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
162
165
 
163
166
  if(part.obj instanceof ReadableStream) {
164
167
  // biome-ignore lint/performance/noAwaitInLoops: readable
@@ -175,7 +178,10 @@ export class Multipart {
175
178
  }
176
179
  }
177
180
  }
178
- else if(part.obj instanceof ArrayBuffer || ArrayBuffer.isView(part.obj)) {
181
+ else if(part.obj instanceof ArrayBuffer) {
182
+ controller.enqueue(new Uint8Array(part.obj))
183
+ }
184
+ else if(ArrayBuffer.isView(part.obj)) {
179
185
  controller.enqueue(part.obj)
180
186
  }
181
187
  else if(typeof part.obj === 'string'){
@@ -183,7 +189,7 @@ export class Multipart {
183
189
  }
184
190
  else {
185
191
  // console.log('error', typeof part.obj, part.obj)
186
- throw new Error('unknown part type')
192
+ controller.error(new Error('unknown part type'))
187
193
  }
188
194
 
189
195
  controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
@@ -191,7 +197,9 @@ export class Multipart {
191
197
 
192
198
  controller.enqueue(encoder.encode(boundaryEnd))
193
199
 
194
- controller.close()
200
+ // controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
201
+
202
+ controller.close()
195
203
  }
196
204
  })
197
205
  }
@@ -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
 
@@ -122,33 +124,3 @@ export class Range {
122
124
  }
123
125
  }
124
126
  }
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
@@ -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
  }
@@ -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
+ }
@@ -3,10 +3,9 @@ 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' */
6
+ /** @import { AcceptRangeUnits, SendContent, SendInfo, Metadata, SendBody } from '../../defs.js' */
7
+ /** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
8
8
  /** @import { CacheControlOptions } from '../../headers/cache-control.js' */
9
- /** @import { SendBody } from '../send-util.js' */
10
9
 
11
10
  const { HTTP_STATUS_OK } = http2.constants
12
11
 
@@ -17,11 +16,47 @@ const { HTTP_STATUS_OK } = http2.constants
17
16
  * @param {number|undefined} contentLength
18
17
  * @param {string|undefined} encoding
19
18
  * @param {EtagItem|undefined} etag
19
+ * @param {IMFFixDateInput|string|undefined} lastModified
20
20
  * @param {number|undefined} age
21
21
  * @param {CacheControlOptions} cacheControl
22
- * @param {'bytes'|'none'|undefined} acceptRanges
22
+ * @param {AcceptRangeUnits|undefined} acceptRanges
23
23
  * @param {Metadata} meta
24
24
  */
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)
25
+ export function sendBytes(stream, contentType, obj, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, meta) {
26
+ _sendBytes(stream, obj, {
27
+ contentType,
28
+ contentLength,
29
+ encoding,
30
+ etag,
31
+ lastModified,
32
+ age,
33
+ cacheControl
34
+ }, {
35
+ acceptRanges
36
+ }, meta)
37
+ }
38
+
39
+ /**
40
+ * @param {ServerHttp2Stream} stream
41
+ * @param {SendBody|undefined} obj
42
+ * @param {Omit<SendContent, 'rangeDirective'>} content
43
+ * @param {Pick<SendInfo, 'acceptRanges'>} info
44
+ * @param {Metadata} meta
45
+ */
46
+ export function _sendBytes(stream, obj, content, info, meta) {
47
+ const {
48
+ contentType,
49
+ contentLength,
50
+ encoding,
51
+ etag,
52
+ lastModified,
53
+ age,
54
+ cacheControl,
55
+ } = content
56
+
57
+ const { acceptRanges } = info
58
+
59
+ const supportedQueryType = undefined
60
+ const range = undefined
61
+ send_bytes(stream, HTTP_STATUS_OK, contentType, obj, range, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryType, meta)
27
62
  }
@@ -4,12 +4,13 @@ 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' */
8
+ /** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
9
9
 
10
10
  const {
11
11
  HTTP2_HEADER_LOCATION,
12
- HTTP2_HEADER_ETAG
12
+ HTTP2_HEADER_ETAG,
13
+ HTTP2_HEADER_LAST_MODIFIED
13
14
  } = http2.constants
14
15
 
15
16
  const { HTTP_STATUS_CREATED } = http2.constants
@@ -18,11 +19,31 @@ const { HTTP_STATUS_CREATED } = http2.constants
18
19
  * @param {ServerHttp2Stream} stream
19
20
  * @param {URL} location
20
21
  * @param {EtagItem|undefined} etag
22
+ * @param {IMFFixDateInput|string|undefined} lastModified
21
23
  * @param {Metadata} meta
22
24
  */
23
- export function sendCreated(stream, location, etag, meta) {
25
+ export function sendCreated(stream, location, etag, lastModified, meta) {
26
+ _sendCreated(stream, location, {
27
+ etag,
28
+ lastModified
29
+ }, meta)
30
+ }
31
+
32
+ /**
33
+ * @param {ServerHttp2Stream} stream
34
+ * @param {URL} location
35
+ * @param {Pick<SendContent, 'etag' | 'lastModified'>} content
36
+ * @param {Metadata} meta
37
+ */
38
+ export function _sendCreated(stream, location, content, meta) {
39
+ const {
40
+ etag,
41
+ lastModified
42
+ } = content
43
+
24
44
  send(stream, HTTP_STATUS_CREATED, {
25
45
  [HTTP2_HEADER_LOCATION]: location.href,
26
- [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
46
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
47
+ [HTTP2_HEADER_LAST_MODIFIED]: Conditional.encodeFixDate(lastModified)
27
48
  }, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
28
49
  }
@@ -4,8 +4,8 @@ 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' */
7
+ /** @import { SendContent, SendInfo, Metadata } from '../../defs.js' */
8
+ /** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
9
9
  /** @import { CacheControlOptions } from '../../headers/cache-control.js' */
10
10
 
11
11
  const { HTTP_STATUS_OK } = http2.constants
@@ -15,14 +15,46 @@ const { HTTP_STATUS_OK } = http2.constants
15
15
  * @param {Object} obj
16
16
  * @param {string|undefined} encoding
17
17
  * @param {EtagItem|undefined} etag
18
+ * @param {IMFFixDateInput|string|undefined} lastModified
18
19
  * @param {number|undefined} age
19
20
  * @param {CacheControlOptions} cacheControl
20
21
  * @param {Array<string>|undefined} supportedQueryTypes
21
22
  * @param {Metadata} meta
22
23
  */
23
- export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl, supportedQueryTypes, meta) {
24
+ export function sendJSON_Encoded(stream, obj, encoding, etag, lastModified, age, cacheControl, supportedQueryTypes, meta) {
25
+ _sendJSON_Encoded(stream, obj, {
26
+ encoding,
27
+ etag,
28
+ lastModified,
29
+ age,
30
+ cacheControl
31
+ }, {
32
+ supportedQueryTypes
33
+ }, meta)
34
+ }
35
+
36
+ /**
37
+ * @param {ServerHttp2Stream} stream
38
+ * @param {Object} obj
39
+ * @param {Omit<SendContent, 'contentType' | 'contentLength' | 'rangeDirective'>} content
40
+ * @param {Pick<SendInfo, 'supportedQueryTypes'>} info
41
+ * @param {Metadata} meta
42
+ */
43
+ export function _sendJSON_Encoded(stream, obj, content, info, meta) {
44
+ const {
45
+ encoding,
46
+ etag,
47
+ lastModified,
48
+ age,
49
+ cacheControl
50
+ } = content
51
+
52
+ const {
53
+ supportedQueryTypes
54
+ } = info
55
+
24
56
  if(stream.closed) { return }
25
57
 
26
58
  const json = JSON.stringify(obj)
27
- send_encoded(stream, HTTP_STATUS_OK, CONTENT_TYPE_JSON, json, encoding, etag, age, cacheControl, undefined, supportedQueryTypes, meta)
59
+ send_encoded(stream, HTTP_STATUS_OK, CONTENT_TYPE_JSON, json, encoding, etag, lastModified, age, cacheControl, undefined, supportedQueryTypes, meta)
28
60
  }
@@ -4,11 +4,12 @@ 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' */
8
+ /** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
9
9
 
10
10
  const {
11
- HTTP2_HEADER_ETAG
11
+ HTTP2_HEADER_ETAG,
12
+ HTTP2_HEADER_LAST_MODIFIED
12
13
  } = http2.constants
13
14
 
14
15
  const { HTTP_STATUS_NO_CONTENT } = http2.constants
@@ -16,10 +17,29 @@ const { HTTP_STATUS_NO_CONTENT } = http2.constants
16
17
  /**
17
18
  * @param {ServerHttp2Stream} stream
18
19
  * @param {EtagItem|undefined} etag
20
+ * @param {IMFFixDateInput|string|undefined} lastModified
19
21
  * @param {Metadata} meta
20
22
  */
21
- export function sendNoContent(stream, etag, meta) {
23
+ export function sendNoContent(stream, etag, lastModified, meta) {
24
+ _sendNoContent(stream, {
25
+ etag,
26
+ lastModified
27
+ }, meta)
28
+ }
29
+
30
+ /**
31
+ * @param {ServerHttp2Stream} stream
32
+ * @param {Pick<SendContent, 'etag' | 'lastModified'>} content
33
+ * @param {Metadata} meta
34
+ */
35
+ export function _sendNoContent(stream, content, meta) {
36
+ const {
37
+ etag,
38
+ lastModified
39
+ } = content
40
+
22
41
  send(stream, HTTP_STATUS_NO_CONTENT, {
23
- [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
42
+ [HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
43
+ [HTTP2_HEADER_LAST_MODIFIED]: Conditional.encodeFixDate(lastModified)
24
44
  }, [], undefined, undefined, meta)
25
45
  }
@@ -1,16 +1,16 @@
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' */
9
+ /** @import { SendContent, Metadata, SendBody } from '../../defs.js' */
10
+ /** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
11
11
  /** @import { CacheControlOptions } from '../../headers/cache-control.js' */
12
12
  /** @import { ContentRangeDirective } from '../../headers/content-range.js' */
13
- /** @import { SendBody } from '../send-util.js' */
13
+
14
14
 
15
15
  const { HTTP_STATUS_PARTIAL_CONTENT } = http2.constants
16
16
 
@@ -27,23 +27,52 @@ const { HTTP_STATUS_PARTIAL_CONTENT } = http2.constants
27
27
 
28
28
  /**
29
29
  * @param {ServerHttp2Stream} stream
30
- * @param {string} contentType
30
+ * @param {string|undefined} contentType
31
31
  * @param {NonEmptyArray<PartialBytes>|PartialBytes} objs
32
32
  * @param {number|undefined} contentLength
33
33
  * @param {string|undefined} encoding
34
34
  * @param {EtagItem|undefined} etag
35
+ * @param {IMFFixDateInput|string|undefined} lastModified
35
36
  * @param {number|undefined} age
36
37
  * @param {CacheControlOptions} cacheControl
37
38
  * @param {Metadata} meta
38
39
  */
39
- export function sendPartialContent(stream, contentType, objs, contentLength, encoding, etag, age, cacheControl, meta) {
40
+ export function sendPartialContent(stream, contentType, objs, contentLength, encoding, etag, lastModified, age, cacheControl, meta) {
41
+ return _sendPartialContent(stream, objs, {
42
+ contentType,
43
+ contentLength,
44
+ encoding,
45
+ etag,
46
+ lastModified,
47
+ age,
48
+ cacheControl
49
+ }, meta)
50
+ }
51
+
52
+ /**
53
+ * @param {ServerHttp2Stream} stream
54
+ * @param {NonEmptyArray<PartialBytes>|PartialBytes} objs
55
+ * @param {Omit<SendContent, 'rangeDirective'>} content
56
+ * @param {Metadata} meta
57
+ */
58
+ export function _sendPartialContent(stream, objs, content, meta) {
59
+ const {
60
+ contentType,
61
+ contentLength,
62
+ encoding,
63
+ etag,
64
+ lastModified,
65
+ age,
66
+ cacheControl
67
+ } = content
68
+
40
69
  const acceptRanges = RANGE_UNITS_BYTES
41
70
  const supportedQueryTypes = undefined
42
71
 
43
72
  if(Array.isArray(objs) && objs.length > 1) {
44
73
  // send using multipart bytes
45
74
  const boundary = 'PARTIAL_CONTENT_BOUNDARY' // todo make unique for content
46
- const obj = Multipart.encode_Bytes(contentType, objs, contentLength, boundary)
75
+ const obj = Multipart.encode_Bytes(contentType ?? MIME_TYPE_OCTET_STREAM, objs, contentLength, boundary)
47
76
 
48
77
  const multipartContentType = `${MIME_TYPE_MULTIPART_RANGE}; boundary=${boundary}`
49
78
 
@@ -56,6 +85,7 @@ export function sendPartialContent(stream, contentType, objs, contentLength, enc
56
85
  undefined,
57
86
  encoding,
58
87
  etag,
88
+ lastModified,
59
89
  age,
60
90
  cacheControl,
61
91
  acceptRanges,
@@ -67,5 +97,5 @@ export function sendPartialContent(stream, contentType, objs, contentLength, enc
67
97
 
68
98
  // single range, send as regular object
69
99
  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)
100
+ send_bytes(stream, HTTP_STATUS_PARTIAL_CONTENT, contentType, obj.obj, obj.range, undefined, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta)
71
101
  }