Loading files
import Headers from 'headers'
import { URL, URLSearchParams } from 'url'
class Request {
raw
constructor(request) {
this.raw = request
}
get method() {
return this.raw.method.toLowerCase()
}
get path() {
return this.raw.url.pathname
}
get url() {
return this.raw.url.href
}
header(key) {
return this.raw.headers.get(key.toLowerCase())
}
query(key) {
return key ? this.raw.url.searchParams.get(key) : Object.fromEntries(this.raw.url.searchParams.entries())
}
async text() {
return await this.raw.text()
}
async json() {
return await this.raw.json()
}
async formData() {
const queryString = await this.text()
return Object.fromEntries(new URLSearchParams(queryString))
}
}
class Response {
#body
#headers
#status = 200
/** Encode the response body and supply Content-Length unless explicitly provided. */
constructor(body, options = {}) {
this.#body = body instanceof ArrayBuffer ? body : ArrayBuffer.fromString(body.toString())
const headers = new Headers()
if (options.headers) {
for (const [key, value] of options.headers.entries
? options.headers.entries()
: Object.entries(options.headers)) {
headers.set(key, value)
}
}
if (headers.get('content-length') === undefined) {
headers.set('content-length', this.#body.byteLength)
}
this.#headers = headers
this.#status = options.status ?? 200
}
get body() {
return this.#body
}
get headers() {
return this.#headers
}
get status() {
return this.#status
}
async arrayBuffer() {
let body = this.#body
if (body) {
this.#body = undefined
body = await body
}
return body
}
async json() {
let body = this.#body
if (body) {
this.#body = undefined
body = await body
body = String.fromArrayBuffer(body)
return JSON.parse(body)
}
return body
}
async text() {
let body = this.#body
if (body) {
this.#body = undefined
body = await body
body = String.fromArrayBuffer(body)
}
return body
}
}
class Context {
#req
#status
#headers = new Headers()
constructor(request) {
this.#req = new Request(request)
}
get req() {
return this.#req
}
status(status) {
this.#status = status
}
header(key, value) {
this.#headers.set(key, value)
}
text(text, status) {
this.#headers.set('Content-type', 'text/plain')
return new Response(text, {
status: status ?? this.#status,
headers: Object.fromEntries(this.#headers.entries()),
})
}
json(json, status) {
this.#headers.set('Content-type', 'application/json')
return new Response(JSON.stringify(json), {
status: status ?? this.#status,
headers: Object.fromEntries(this.#headers.entries()),
})
}
}
class HttpServerService {
#routes = {
get: new Map(),
post: new Map(),
put: new Map(),
patch: new Map(),
delete: new Map(),
}
get = (path, handler) => this.#routes.get.set(path, handler)
post = (path, handler) => this.#routes.post.set(path, handler)
put = (path, handler) => this.#routes.put.set(path, handler)
patch = (path, handler) => this.#routes.patch.set(path, handler)
delete = (path, handler) => this.#routes.delete.set(path, handler)
#server
#closed = false
#notFound
/** Create a server with a bounded request body (16 KiB by default). */
constructor(options = {}) {
const maxRequestBodyBytes = options.maxRequestBodyBytes ?? 16 * 1024
if (!Number.isSafeInteger(maxRequestBodyBytes) || maxRequestBodyBytes < 0) {
throw new RangeError('maxRequestBodyBytes must be a non-negative safe integer')
}
this.#notFound = options.onNotFound
const service = this
this.#server = new device.network.http.server.io({
...device.network.http.server,
port: options.port ?? 8080,
/** Each connection owns one current request until its response finishes. */
onConnect(connection) {
let current
connection.accept({
/** Replace all body and response state when a keep-alive request begins. */
onRequest(request) {
current = { request, chunks: [], length: 0, body: undefined, offset: 0 }
},
/** Buffer incoming bytes only while the cumulative request limit permits. */
onReadable(count) {
// Reject before allocating another chunk, including chunked requests.
if (!current || count > maxRequestBodyBytes - current.length) {
current = undefined
this.close()
return
}
const chunk = this.read(count)
current.chunks.push(chunk)
current.length += chunk.byteLength
},
/** Route a complete request while guarding against connection closure. */
onResponse(response) {
const state = current
service.#respond(this, state, response, () => !service.#closed && current === state)
},
/** Send only the response bytes that fit the current socket capacity. */
onWritable(count) {
const state = current
if (!state?.body) return
const length = Math.min(count, state.body.byteLength - state.offset)
if (length <= 0) return
const chunk = new DataView(state.body, state.offset, length)
state.offset += length
this.write(chunk)
},
/** Release the completed response before another request is accepted. */
onDone() {
current = undefined
},
/** Invalidate pending asynchronous replies when the connection fails. */
onError() {
current = undefined
},
})
},
})
}
/** Return the bound listener port for endpoint construction. */
get port() {
return this.#server.port
}
/** Close the listener and prevent pending handlers from sending replies. */
close() {
this.#closed = true
this.#server.close()
}
/** Dispatch one complete request; route failures become 500 responses. */
async #respond(connection, state, rawResponse, isCurrent) {
try {
const bytes = new Uint8Array(state.length)
let offset = 0
for (const chunk of state.chunks) {
bytes.set(new Uint8Array(chunk), offset)
offset += chunk.byteLength
}
state.chunks = undefined
let body = bytes.buffer
const request = state.request
const query = request.query ? `?${request.query}` : ''
const rawRequest = {
method: request.method,
headers: request.headers,
url: new URL(`${request.path}${query}`, `http://localhost:${this.#server.port}`),
/** Consume the buffered request body once as UTF-8 text. */
async text() {
const value = body
body = undefined
return value === undefined ? undefined : String.fromArrayBuffer(value)
},
/** Consume and decode the request body as JSON. */
async json() {
return JSON.parse(await this.text())
},
}
const context = new Context(rawRequest)
let response
try {
const handler = this.#routes[context.req.method]?.get(context.req.path)
response = handler
? await handler(context)
: this.#notFound
? await this.#notFound(context)
: context.text('Resource Not Found', 404)
} catch {
response = context.text('Internal Server Error', 500)
}
state.body = await response.arrayBuffer()
if (!isCurrent()) return
rawResponse.status = response.status
rawResponse.headers = response.headers
connection.respond(rawResponse)
} catch {
if (isCurrent()) connection.close()
}
}
}
export { HttpServerService, Response }
c44ec7454This file is pinned to the selected release. Download the original to open it in your design tool.