ingenium 0.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.
- package/LICENSE +21 -0
- package/README.md +943 -0
- package/dist/index.cjs +7078 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4262 -0
- package/dist/index.d.ts +4262 -0
- package/dist/index.js +6963 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/src/api-key/middleware.ts +157 -0
- package/src/api-key/types.ts +37 -0
- package/src/app/scope.ts +392 -0
- package/src/app.ts +1752 -0
- package/src/body/limit.ts +21 -0
- package/src/body/middleware.ts +30 -0
- package/src/body/multipart-types.ts +40 -0
- package/src/body/multipart.ts +254 -0
- package/src/context/body.ts +324 -0
- package/src/context/context.ts +650 -0
- package/src/context/cookies.ts +282 -0
- package/src/context/pool.ts +32 -0
- package/src/cors/middleware.ts +182 -0
- package/src/cors/types.ts +79 -0
- package/src/cron/parser.ts +311 -0
- package/src/cron/registry.ts +49 -0
- package/src/cron/scheduler.ts +153 -0
- package/src/csrf/middleware.ts +224 -0
- package/src/csrf/types.ts +65 -0
- package/src/errors.ts +148 -0
- package/src/idempotency/middleware.ts +197 -0
- package/src/idempotency/store.ts +70 -0
- package/src/idempotency/types.ts +87 -0
- package/src/index.ts +328 -0
- package/src/jobs/queue.ts +306 -0
- package/src/jobs/registry.ts +82 -0
- package/src/jobs/store-memory.ts +113 -0
- package/src/jobs/types.ts +135 -0
- package/src/jwt/jwks.ts +143 -0
- package/src/jwt/middleware.ts +313 -0
- package/src/jwt/types.ts +137 -0
- package/src/jwt/verify.ts +370 -0
- package/src/middleware/compose.ts +94 -0
- package/src/middleware/types.ts +37 -0
- package/src/negotiation/accept.ts +159 -0
- package/src/negotiation/etag.ts +30 -0
- package/src/negotiation/format.ts +88 -0
- package/src/negotiation/fresh.ts +89 -0
- package/src/negotiation/json-etag.ts +122 -0
- package/src/negotiation/negotiate.ts +97 -0
- package/src/openapi/describe.ts +79 -0
- package/src/openapi/extract-params.ts +62 -0
- package/src/openapi/generate.ts +251 -0
- package/src/openapi/handler.ts +73 -0
- package/src/openapi/types.ts +145 -0
- package/src/plugin/decorators.ts +100 -0
- package/src/plugin/hooks.ts +114 -0
- package/src/plugin/types.ts +189 -0
- package/src/problem/middleware.ts +55 -0
- package/src/problem/serialize.ts +121 -0
- package/src/problem/types.ts +68 -0
- package/src/proxy/trust.ts +247 -0
- package/src/rate-limit/middleware.ts +72 -0
- package/src/rate-limit/store.ts +129 -0
- package/src/rate-limit/types.ts +60 -0
- package/src/response/reflect.ts +93 -0
- package/src/router/router.ts +284 -0
- package/src/router/trie.ts +309 -0
- package/src/router/types.ts +54 -0
- package/src/schema/standard.ts +67 -0
- package/src/session/middleware.ts +379 -0
- package/src/session/store-memory.ts +79 -0
- package/src/session/types.ts +95 -0
- package/src/sinatra/filters.ts +129 -0
- package/src/sinatra/top-level.ts +151 -0
- package/src/sse/keep-alive.ts +52 -0
- package/src/sse/sse.ts +115 -0
- package/src/static/middleware.ts +254 -0
- package/src/static/types.ts +31 -0
- package/src/transport/http2-helpers.ts +242 -0
- package/src/transport/http2.ts +316 -0
- package/src/transport/node.ts +261 -0
- package/src/transport/shutdown.ts +86 -0
- package/src/transport/types.ts +72 -0
- package/src/util/safe-json.ts +66 -0
- package/src/ws/index.ts +164 -0
- package/src/ws/middleware.ts +178 -0
- package/src/ws/types.ts +52 -0
- package/src/ws/ws-node-adapter.ts +162 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket registrar — the small piece of state that holds path → handler
|
|
3
|
+
* mappings and knows how to wire `'upgrade'` on a Node `http.Server`.
|
|
4
|
+
*
|
|
5
|
+
* Design: the `ws` package is loaded lazily via dynamic `import('ws')` so
|
|
6
|
+
* apps that never use WebSockets pay no cost (no module load, no peer-dep
|
|
7
|
+
* requirement). The first call to `attach()` resolves the import.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Server as HttpServer, IncomingMessage } from 'node:http'
|
|
11
|
+
import type { Socket } from 'node:net'
|
|
12
|
+
import { IngeniumContext } from '../context/context.ts'
|
|
13
|
+
import type { HttpMethod } from '../router/types.ts'
|
|
14
|
+
import type {
|
|
15
|
+
WebSocketHandler,
|
|
16
|
+
WebSocketHandlerOptions,
|
|
17
|
+
WsRegistrar,
|
|
18
|
+
WsRoute,
|
|
19
|
+
} from './types.ts'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Attempt to detect whether `ws` is installed. Used by the test suite to
|
|
23
|
+
* `describe.skipIf` the WS suite when the optional peer dep is missing.
|
|
24
|
+
*/
|
|
25
|
+
export async function peerHasWs(): Promise<boolean> {
|
|
26
|
+
try {
|
|
27
|
+
await import('ws')
|
|
28
|
+
return true
|
|
29
|
+
} catch {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a registrar bound to an app. The registrar is intentionally
|
|
36
|
+
* decoupled from `IngeniumApp` — the app calls `add()` from `app.ws()`, and
|
|
37
|
+
* `enableWebSockets()` (or the app's `listen()` integration) calls `attach()`
|
|
38
|
+
* once the underlying `http.Server` is created.
|
|
39
|
+
*/
|
|
40
|
+
export function createWebSocketRegistrar(): WsRegistrar {
|
|
41
|
+
const routes: Map<string, WsRoute> = new Map()
|
|
42
|
+
let attachedServer: HttpServer | null = null
|
|
43
|
+
// The `ws` `WebSocketServer` instance, lazy-initialized on first upgrade.
|
|
44
|
+
// We use one server per registered path so per-handler options apply.
|
|
45
|
+
const wssByPath: Map<string, unknown> = new Map()
|
|
46
|
+
// We keep a reference to the `ws` module after the first dynamic import.
|
|
47
|
+
let wsModule: typeof import('ws') | null = null
|
|
48
|
+
|
|
49
|
+
// Single shared upgrade listener — installed exactly once.
|
|
50
|
+
let upgradeListener: ((req: IncomingMessage, socket: Socket, head: Buffer) => void) | null = null
|
|
51
|
+
|
|
52
|
+
function add(path: string, handler: WebSocketHandler, options: WebSocketHandlerOptions = {}): void {
|
|
53
|
+
if (routes.has(path)) {
|
|
54
|
+
throw new Error(`ingenium.ws: path "${path}" already has a WebSocket handler`)
|
|
55
|
+
}
|
|
56
|
+
routes.set(path, { path, handler, options })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function attach(httpServer: HttpServer): void {
|
|
60
|
+
if (attachedServer === httpServer) return // idempotent
|
|
61
|
+
if (attachedServer !== null) {
|
|
62
|
+
throw new Error('ingenium.ws: registrar already attached to a different http.Server')
|
|
63
|
+
}
|
|
64
|
+
attachedServer = httpServer
|
|
65
|
+
|
|
66
|
+
upgradeListener = (req, socket, head) => {
|
|
67
|
+
// Parse the path from the upgrade request URL. We only look at the
|
|
68
|
+
// pathname — query strings are exposed via `ctx.rawQuery` for handlers
|
|
69
|
+
// that care.
|
|
70
|
+
const url = req.url ?? '/'
|
|
71
|
+
const qIdx = url.indexOf('?')
|
|
72
|
+
const path = qIdx >= 0 ? url.slice(0, qIdx) : url
|
|
73
|
+
|
|
74
|
+
const route = routes.get(path)
|
|
75
|
+
if (!route) {
|
|
76
|
+
// No handler for this path — close the socket cleanly. The
|
|
77
|
+
// 404-equivalent for WebSockets is just refusing the upgrade.
|
|
78
|
+
socket.destroy()
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Lazy-load `ws`. On the first upgrade, dynamically import. If `ws`
|
|
83
|
+
// isn't installed, give a clear actionable error and tear the socket
|
|
84
|
+
// down — apps that wired `app.ws(...)` without installing the peer
|
|
85
|
+
// dep should learn about it the moment a client tries to connect.
|
|
86
|
+
void (async () => {
|
|
87
|
+
try {
|
|
88
|
+
if (wsModule === null) wsModule = await import('ws')
|
|
89
|
+
} catch (err) {
|
|
90
|
+
process.emitWarning(
|
|
91
|
+
'ingenium: app.ws() was called but the `ws` package is not installed. ' +
|
|
92
|
+
'Install it with `npm install ws` (and `@types/ws` for TypeScript).',
|
|
93
|
+
)
|
|
94
|
+
socket.destroy()
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let wss = wssByPath.get(route.path) as
|
|
99
|
+
| InstanceType<typeof import('ws').WebSocketServer>
|
|
100
|
+
| undefined
|
|
101
|
+
if (!wss) {
|
|
102
|
+
wss = new wsModule.WebSocketServer({
|
|
103
|
+
noServer: true,
|
|
104
|
+
maxPayload: route.options.maxPayload,
|
|
105
|
+
perMessageDeflate: route.options.perMessageDeflate ?? false,
|
|
106
|
+
})
|
|
107
|
+
wssByPath.set(route.path, wss)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
111
|
+
const ctx = buildMinimalContext(req, path)
|
|
112
|
+
try {
|
|
113
|
+
const ret = route.handler(ws, ctx)
|
|
114
|
+
if (ret && typeof (ret as Promise<unknown>).then === 'function') {
|
|
115
|
+
;(ret as Promise<unknown>).catch((err) => {
|
|
116
|
+
process.emitWarning(
|
|
117
|
+
`ingenium.ws: handler for ${path} rejected: ${(err as Error)?.message ?? String(err)}`,
|
|
118
|
+
)
|
|
119
|
+
try { ws.close(1011, 'handler error') } catch { /* socket may already be dead */ }
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
process.emitWarning(
|
|
124
|
+
`ingenium.ws: handler for ${path} threw: ${(err as Error)?.message ?? String(err)}`,
|
|
125
|
+
)
|
|
126
|
+
try { ws.close(1011, 'handler error') } catch { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
})()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
httpServer.on('upgrade', upgradeListener)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function close(): Promise<void> {
|
|
136
|
+
// Detach the upgrade listener so a re-listen on the same server doesn't
|
|
137
|
+
// double-up handlers.
|
|
138
|
+
if (attachedServer && upgradeListener) {
|
|
139
|
+
attachedServer.off('upgrade', upgradeListener)
|
|
140
|
+
}
|
|
141
|
+
upgradeListener = null
|
|
142
|
+
attachedServer = null
|
|
143
|
+
|
|
144
|
+
// Close every per-path WebSocketServer. `ws.WebSocketServer.close(cb)`
|
|
145
|
+
// fires once all clients have disconnected; we await each in parallel.
|
|
146
|
+
const closes: Promise<void>[] = []
|
|
147
|
+
for (const wss of wssByPath.values()) {
|
|
148
|
+
const server = wss as InstanceType<typeof import('ws').WebSocketServer>
|
|
149
|
+
// Forcibly terminate any still-open clients so close() resolves
|
|
150
|
+
// promptly during test teardown.
|
|
151
|
+
for (const client of server.clients) {
|
|
152
|
+
try { client.terminate() } catch { /* ignore */ }
|
|
153
|
+
}
|
|
154
|
+
closes.push(new Promise<void>((resolve) => server.close(() => resolve())))
|
|
155
|
+
}
|
|
156
|
+
wssByPath.clear()
|
|
157
|
+
await Promise.all(closes)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { add, attach, close }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Build a minimal `IngeniumContext` for a WebSocket handler. We don't run the
|
|
165
|
+
* full request pipeline (no middleware, no decorators) because the upgrade
|
|
166
|
+
* has already taken place — the handler owns the socket from here.
|
|
167
|
+
*/
|
|
168
|
+
function buildMinimalContext(req: IncomingMessage, path: string): IngeniumContext {
|
|
169
|
+
const ctx = new IngeniumContext()
|
|
170
|
+
ctx.method = (req.method ?? 'GET') as HttpMethod
|
|
171
|
+
ctx.url = req.url ?? '/'
|
|
172
|
+
ctx.path = path
|
|
173
|
+
const url = ctx.url
|
|
174
|
+
const qIdx = url.indexOf('?')
|
|
175
|
+
ctx.rawQuery = qIdx >= 0 ? url.slice(qIdx + 1) : ''
|
|
176
|
+
ctx.headers = req.headers
|
|
177
|
+
return ctx
|
|
178
|
+
}
|
package/src/ws/types.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the optional WebSocket adapter. The `ws` package is an
|
|
3
|
+
* OPTIONAL peer dependency — these types are erased at runtime, so this file
|
|
4
|
+
* compiles even when `ws` is not installed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { IncomingMessage } from 'node:http'
|
|
8
|
+
// Type-only — TypeScript erases this; safe even without `ws` installed.
|
|
9
|
+
import type { WebSocket as WsWebSocket } from 'ws'
|
|
10
|
+
import type { IngeniumContext } from '../context/context.ts'
|
|
11
|
+
|
|
12
|
+
/** Re-export the underlying `ws` `WebSocket` type for convenience. */
|
|
13
|
+
export type WebSocket = WsWebSocket
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Handler invoked when a client successfully upgrades to a WebSocket.
|
|
17
|
+
*
|
|
18
|
+
* `socket` is the `ws.WebSocket` instance. `ctx` is a minimal `IngeniumContext`
|
|
19
|
+
* populated from the upgrade `IncomingMessage` — the body / response writers
|
|
20
|
+
* are not meaningful for WS handlers (the upgrade has already happened).
|
|
21
|
+
*/
|
|
22
|
+
export type WebSocketHandler = (socket: WsWebSocket, ctx: IngeniumContext) => void | Promise<void>
|
|
23
|
+
|
|
24
|
+
/** Per-handler options forwarded to `WebSocketServer({ noServer: true, ... })`. */
|
|
25
|
+
export interface WebSocketHandlerOptions {
|
|
26
|
+
/** Max payload size (bytes) for incoming frames. */
|
|
27
|
+
maxPayload?: number
|
|
28
|
+
/** Enable permessage-deflate. Defaults to false (matches `ws` default). */
|
|
29
|
+
perMessageDeflate?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Internal: a registered handler entry. */
|
|
33
|
+
export interface WsRoute {
|
|
34
|
+
path: string
|
|
35
|
+
handler: WebSocketHandler
|
|
36
|
+
options: WebSocketHandlerOptions
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Bag passed to integrators (advanced). */
|
|
40
|
+
export interface WsIntegrator {
|
|
41
|
+
(httpServer: import('node:http').Server): void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Shape of the per-app registrar exposed to `enableWebSockets`. */
|
|
45
|
+
export interface WsRegistrar {
|
|
46
|
+
add(path: string, handler: WebSocketHandler, options?: WebSocketHandlerOptions): void
|
|
47
|
+
attach(httpServer: import('node:http').Server): void
|
|
48
|
+
close(): Promise<void>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Re-export so consumers can build minimal contexts in tests. */
|
|
52
|
+
export type { IncomingMessage }
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket-aware variant of `NodeAdapter`. Mirrors the behavior of
|
|
3
|
+
* `transport/node.ts` (request handling, socket tracking, graceful close)
|
|
4
|
+
* but exposes the underlying `http.Server` via an `onServerReady` callback
|
|
5
|
+
* so the WS registrar can `.on('upgrade', …)` it.
|
|
6
|
+
*
|
|
7
|
+
* We did not modify the core `NodeAdapter` because the core has no awareness
|
|
8
|
+
* of WebSockets; this adapter is opt-in via `enableWebSockets()`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from 'node:http'
|
|
12
|
+
import type { Socket } from 'node:net'
|
|
13
|
+
import { Buffer } from 'node:buffer'
|
|
14
|
+
import type { IngeniumContext } from '../context/context.ts'
|
|
15
|
+
import type { HttpMethod } from '../router/types.ts'
|
|
16
|
+
import type {
|
|
17
|
+
CloseOptions,
|
|
18
|
+
ListeningServer,
|
|
19
|
+
Transport,
|
|
20
|
+
TransportHooks,
|
|
21
|
+
} from '../transport/types.ts'
|
|
22
|
+
|
|
23
|
+
export type OnServerReady = (httpServer: HttpServer) => void
|
|
24
|
+
|
|
25
|
+
export class WsNodeAdapter implements Transport {
|
|
26
|
+
private hooks: TransportHooks | null = null
|
|
27
|
+
private readonly onServerReady: OnServerReady
|
|
28
|
+
|
|
29
|
+
constructor(onServerReady: OnServerReady) {
|
|
30
|
+
this.onServerReady = onServerReady
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
attach(hooks: TransportHooks): void {
|
|
34
|
+
this.hooks = hooks
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async listen(port: number, host = '127.0.0.1'): Promise<ListeningServer> {
|
|
38
|
+
if (!this.hooks) throw new Error('WsNodeAdapter.listen() called before attach()')
|
|
39
|
+
const hooks = this.hooks
|
|
40
|
+
|
|
41
|
+
const server = createServer((req, res) => {
|
|
42
|
+
handleRequest(req, res, hooks).catch((err) => {
|
|
43
|
+
if (!res.headersSent) {
|
|
44
|
+
res.statusCode = 500
|
|
45
|
+
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
46
|
+
res.end(JSON.stringify({ error: 'Internal Server Error', code: 'INTERNAL_ERROR' }))
|
|
47
|
+
} else {
|
|
48
|
+
res.end()
|
|
49
|
+
}
|
|
50
|
+
process.emitWarning(`ingenium: dispatch leaked: ${(err as Error).message ?? String(err)}`)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Hand the http.Server to the WS registrar BEFORE listen() resolves —
|
|
55
|
+
// this guarantees upgrade listeners are wired before any client can
|
|
56
|
+
// connect.
|
|
57
|
+
this.onServerReady(server)
|
|
58
|
+
|
|
59
|
+
const sockets = new Set<Socket>()
|
|
60
|
+
server.on('connection', (socket) => {
|
|
61
|
+
sockets.add(socket)
|
|
62
|
+
socket.on('close', () => sockets.delete(socket))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return new Promise<ListeningServer>((resolve, reject) => {
|
|
66
|
+
server.once('error', reject)
|
|
67
|
+
server.listen(port, host, () => {
|
|
68
|
+
const addr = server.address()
|
|
69
|
+
if (!addr || typeof addr === 'string') {
|
|
70
|
+
reject(new Error('Failed to determine bound address'))
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
resolve({
|
|
74
|
+
port: addr.port,
|
|
75
|
+
host: addr.address,
|
|
76
|
+
close: (opts?: CloseOptions) =>
|
|
77
|
+
new Promise<void>((res, rej) => {
|
|
78
|
+
let settled = false
|
|
79
|
+
let timer: NodeJS.Timeout | null = null
|
|
80
|
+
|
|
81
|
+
server.close((err) => {
|
|
82
|
+
if (timer) clearTimeout(timer)
|
|
83
|
+
if (settled) return
|
|
84
|
+
settled = true
|
|
85
|
+
err ? rej(err) : res()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const timeoutMs = opts?.gracefulTimeoutMs
|
|
89
|
+
if (typeof timeoutMs === 'number' && Number.isFinite(timeoutMs)) {
|
|
90
|
+
timer = setTimeout(() => {
|
|
91
|
+
for (const socket of sockets) socket.destroy()
|
|
92
|
+
}, Math.max(0, timeoutMs))
|
|
93
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function handleRequest(req: IncomingMessage, res: ServerResponse, hooks: TransportHooks): Promise<void> {
|
|
103
|
+
const ctx = hooks.acquire()
|
|
104
|
+
try {
|
|
105
|
+
populateContext(ctx, req)
|
|
106
|
+
await hooks.dispatch(ctx)
|
|
107
|
+
writeResponse(ctx, res)
|
|
108
|
+
} finally {
|
|
109
|
+
hooks.release(ctx)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function populateContext(ctx: IngeniumContext, req: IncomingMessage): void {
|
|
114
|
+
ctx.method = (req.method ?? 'GET') as HttpMethod
|
|
115
|
+
ctx.url = req.url ?? '/'
|
|
116
|
+
const url = ctx.url
|
|
117
|
+
const qIdx = url.indexOf('?')
|
|
118
|
+
if (qIdx >= 0) {
|
|
119
|
+
ctx.path = url.slice(0, qIdx)
|
|
120
|
+
ctx.rawQuery = url.slice(qIdx + 1)
|
|
121
|
+
} else {
|
|
122
|
+
ctx.path = url
|
|
123
|
+
ctx.rawQuery = ''
|
|
124
|
+
}
|
|
125
|
+
ctx.headers = req.headers
|
|
126
|
+
|
|
127
|
+
const cl = req.headers['content-length']
|
|
128
|
+
const contentLength = cl ? Number(cl) : undefined
|
|
129
|
+
const ct = req.headers['content-type']
|
|
130
|
+
ctx.body._attach(req, ct, Number.isFinite(contentLength) ? contentLength : undefined)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function writeResponse(ctx: IngeniumContext, res: ServerResponse): void {
|
|
134
|
+
res.statusCode = ctx._statusCode
|
|
135
|
+
|
|
136
|
+
for (const name in ctx._headers) {
|
|
137
|
+
const value = ctx._headers[name]
|
|
138
|
+
if (value !== undefined) res.setHeader(name, value)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const body = ctx._body
|
|
142
|
+
switch (body.kind) {
|
|
143
|
+
case 'none':
|
|
144
|
+
res.end()
|
|
145
|
+
break
|
|
146
|
+
case 'string':
|
|
147
|
+
if (!res.hasHeader('content-length')) {
|
|
148
|
+
res.setHeader('content-length', Buffer.byteLength(body.data))
|
|
149
|
+
}
|
|
150
|
+
res.end(body.data)
|
|
151
|
+
break
|
|
152
|
+
case 'buffer':
|
|
153
|
+
if (!res.hasHeader('content-length')) {
|
|
154
|
+
res.setHeader('content-length', body.data.length)
|
|
155
|
+
}
|
|
156
|
+
res.end(body.data)
|
|
157
|
+
break
|
|
158
|
+
case 'stream':
|
|
159
|
+
body.data.pipe(res)
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
}
|