@znight/http 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/query.ts CHANGED
@@ -23,8 +23,14 @@ export function parseQuery(query: string): QueryRecord {
23
23
  return result
24
24
  }
25
25
 
26
+ /** A request target split into its raw path and raw query string (without `?`). */
27
+ export interface RequestTarget {
28
+ readonly path: string
29
+ readonly query: string
30
+ }
31
+
26
32
  /** Splits a request target (`/users?page=2`) into its raw path and raw query (without `?`). */
27
- export function splitUrl(url: string): { readonly path: string; readonly query: string } {
33
+ export function splitUrl(url: string): RequestTarget {
28
34
  const index = url.indexOf('?')
29
35
  return index === -1 ? { path: url, query: '' } : { path: url.slice(0, index), query: url.slice(index + 1) }
30
36
  }
package/src/serialize.ts CHANGED
@@ -5,6 +5,7 @@ import type { ResolvedFile } from './files.ts'
5
5
  import type { HttpResponse } from './response.ts'
6
6
  import type { SseSession } from './sse.ts'
7
7
 
8
+ import { Buffer } from 'node:buffer'
8
9
  import { Readable } from 'node:stream'
9
10
 
10
11
  import { isFileBody } from './files.ts'
@@ -12,10 +13,16 @@ import { isHttpResponse } from './response.ts'
12
13
  import { isSseBody } from './sse.ts'
13
14
  import { isBodylessStatus } from './status.ts'
14
15
  import { discardStream, isByteStream, toByteStream } from './stream.ts'
15
- import { emptyRecord } from './utils.ts'
16
+ import { emptyRecord, isBun } from './utils.ts'
16
17
 
17
18
  const ENCODER = new TextEncoder()
18
19
 
20
+ /**
21
+ * UTF-8 encoding of a text body: a pooled `Buffer` (a `Uint8Array`) is several times cheaper than
22
+ * `TextEncoder` on Node.js, `TextEncoder` is the fastest on Bun.
23
+ */
24
+ const utf8: (text: string) => Uint8Array = isBun() ? text => ENCODER.encode(text) : text => Buffer.from(text, 'utf8')
25
+
19
26
  /** Content types set by the serialiser. */
20
27
  export const ContentType: {
21
28
  readonly json: string
@@ -48,6 +55,10 @@ export interface SerializedReply {
48
55
  readonly status: number
49
56
  readonly headers: Record<string, HeaderValue>
50
57
  readonly body: ReplyBody
58
+ /** The text `body` encodes, for a body serialised from a string (JSON, text). */
59
+ readonly text?: string
60
+ /** Only the serialiser set the headers (content type and length): they are valid by construction. */
61
+ readonly ownHeaders?: boolean
51
62
  /** Cookies of the `HttpResponse` (`withCookie`), applied by the pipeline over those of the context. */
52
63
  readonly cookies?: readonly ResponseCookie[]
53
64
  /** The event stream of the body (`@Sse`, `HttpResponse.sse()`). */
@@ -87,10 +98,20 @@ export function serializeResult(result: unknown, options: SerializeOptions): Ser
87
98
  const finalStatus = options.status ?? status ?? defaultStatus(options.routeMethod, payload)
88
99
  const headers = emptyRecord<HeaderValue>()
89
100
  const body = encode(payload, headers)
90
- if (options.headers !== undefined) assignHeaders(headers, options.headers)
91
- if (options.context !== undefined) assignHeaders(headers, options.context)
92
- if (extra !== undefined) assignHeaders(headers, extra)
93
- return withCookies(finish(finalStatus, headers, body, options.method), cookies)
101
+ let own = true
102
+ if (options.headers !== undefined) {
103
+ assignHeaders(headers, options.headers)
104
+ own = false
105
+ }
106
+ if (options.context !== undefined) {
107
+ assignHeaders(headers, options.context)
108
+ own = false
109
+ }
110
+ if (extra !== undefined) {
111
+ assignHeaders(headers, extra)
112
+ own = false
113
+ }
114
+ return withCookies(finish(finalStatus, headers, body, options.method, own), cookies)
94
115
  }
95
116
 
96
117
  /**
@@ -184,11 +205,16 @@ export function addMissingHeaders(
184
205
  }
185
206
  }
186
207
 
208
+ /**
209
+ * The reply of a body: a string (see {@link encode}) is sent as its UTF-8 bytes, with its text.
210
+ * `own`: only {@link encode} set `headers`, which are then valid for a text body.
211
+ */
187
212
  function finish(
188
213
  status: number,
189
214
  headers: Record<string, HeaderValue>,
190
- body: ReplyBody,
215
+ body: ReplyBody | string,
191
216
  method: string,
217
+ own = false,
192
218
  ): SerializedReply {
193
219
  if (isBodylessStatus(status)) {
194
220
  discard(body)
@@ -200,16 +226,23 @@ function finish(
200
226
  discard(body)
201
227
  return { status, headers, body: null }
202
228
  }
203
- return { status, headers, body }
229
+ if (typeof body !== 'string') return { status, headers, body }
230
+ return own
231
+ ? { status, headers, body: utf8(body), text: body, ownHeaders: true }
232
+ : { status, headers, body: utf8(body), text: body }
204
233
  }
205
234
 
206
- function discard(body: ReplyBody): void {
235
+ function discard(body: ReplyBody | string): void {
207
236
  if (body instanceof ReadableStream) discardStream(body)
208
237
  }
209
238
 
210
- function encode(payload: unknown, headers: Record<string, HeaderValue>): ReplyBody {
239
+ /**
240
+ * The body of a payload, its `content-type` and, for bytes and text, its `content-length` set in
241
+ * `headers`. Text (a string, JSON) is returned as a string, encoded as UTF-8 by {@link finish}.
242
+ */
243
+ function encode(payload: unknown, headers: Record<string, HeaderValue>): ReplyBody | string {
211
244
  if (payload === undefined) return null
212
- if (typeof payload === 'string') return bytes(ENCODER.encode(payload), headers, ContentType.text)
245
+ if (typeof payload === 'string') return text(payload, headers, ContentType.text)
213
246
  if (payload instanceof Uint8Array) return bytes(payload, headers, ContentType.bytes)
214
247
  if (payload instanceof ArrayBuffer) return bytes(new Uint8Array(payload), headers, ContentType.bytes)
215
248
  if (ArrayBuffer.isView(payload)) {
@@ -233,7 +266,7 @@ function encode(payload: unknown, headers: Record<string, HeaderValue>): ReplyBo
233
266
  if (isSseBody(payload)) throw new TypeError('An event stream (HttpResponse.sse()) cannot be serialised here')
234
267
  const json = JSON.stringify(payload)
235
268
  if (json === undefined) throw new TypeError(`Cannot serialise a ${typeof payload} as a response body`)
236
- return bytes(ENCODER.encode(json), headers, ContentType.json)
269
+ return text(json, headers, ContentType.json)
237
270
  }
238
271
 
239
272
  function bytes(data: Uint8Array, headers: Record<string, HeaderValue>, type: string): Uint8Array {
@@ -242,6 +275,13 @@ function bytes(data: Uint8Array, headers: Record<string, HeaderValue>, type: str
242
275
  return data
243
276
  }
244
277
 
278
+ function text(data: string, headers: Record<string, HeaderValue>, type: string): string {
279
+ headers['content-type'] = type
280
+ // the length of its UTF-8 encoding (a lone surrogate counts as U+FFFD, like the encoders)
281
+ headers['content-length'] = String(Buffer.byteLength(data, 'utf8'))
282
+ return data
283
+ }
284
+
245
285
  function isNodeReadable(value: unknown): value is Readable {
246
286
  return (
247
287
  value instanceof Readable ||