weifuwu 0.25.2 → 0.27.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.
- package/README.md +291 -2489
- package/ai/provider.ts +129 -0
- package/ai/stream.ts +63 -0
- package/cli.ts +55 -257
- package/core/cookie.ts +114 -0
- package/core/env.ts +142 -0
- package/core/logger.ts +72 -0
- package/core/router.ts +795 -0
- package/core/serve.ts +294 -0
- package/core/sse.ts +85 -0
- package/core/trace.ts +146 -0
- package/graphql.ts +267 -0
- package/hub.ts +133 -0
- package/index.ts +71 -0
- package/mailer.ts +81 -0
- package/middleware/compress.ts +103 -0
- package/middleware/cors.ts +81 -0
- package/middleware/csrf.ts +112 -0
- package/middleware/flash.ts +144 -0
- package/middleware/health.ts +44 -0
- package/middleware/helmet.ts +98 -0
- package/middleware/i18n.ts +175 -0
- package/middleware/rate-limit.ts +167 -0
- package/middleware/request-id.ts +60 -0
- package/middleware/static.ts +149 -0
- package/middleware/theme.ts +84 -0
- package/middleware/upload.ts +168 -0
- package/middleware/validate.ts +186 -0
- package/package.json +14 -36
- package/postgres/client.ts +132 -0
- package/postgres/index.ts +4 -0
- package/postgres/module.ts +37 -0
- package/postgres/schema/columns.ts +186 -0
- package/postgres/schema/index.ts +36 -0
- package/postgres/schema/sql.ts +39 -0
- package/postgres/schema/table.ts +548 -0
- package/postgres/schema/where.ts +99 -0
- package/postgres/types.ts +48 -0
- package/queue/cron.ts +90 -0
- package/queue/index.ts +654 -0
- package/queue/types.ts +60 -0
- package/redis/client.ts +24 -0
- package/{dist/redis/index.d.ts → redis/index.ts} +2 -2
- package/redis/types.ts +28 -0
- package/types.ts +78 -0
- package/cli/template/app.ts +0 -22
- package/cli/template/index.ts +0 -10
- package/cli/template/locales/en.json +0 -13
- package/cli/template/locales/zh-CN.json +0 -13
- package/cli/template/locales/zh-TW.json +0 -13
- package/cli/template/locales/zh.json +0 -13
- package/cli/template/ui/app/globals.css +0 -2
- package/cli/template/ui/app/layout.tsx +0 -15
- package/cli/template/ui/app/page.tsx +0 -124
- package/cli/template/ui/components/Greeting.tsx +0 -3
- package/dist/agent/client.d.ts +0 -2
- package/dist/agent/index.d.ts +0 -2
- package/dist/agent/rest.d.ts +0 -14
- package/dist/agent/run.d.ts +0 -19
- package/dist/agent/types.d.ts +0 -55
- package/dist/ai/provider.d.ts +0 -45
- package/dist/ai/utils.d.ts +0 -5
- package/dist/ai/workflow.d.ts +0 -17
- package/dist/ai-sdk.d.ts +0 -2
- package/dist/ai.d.ts +0 -13
- package/dist/analytics.d.ts +0 -45
- package/dist/auth.d.ts +0 -22
- package/dist/cache.d.ts +0 -74
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -302
- package/dist/client-locale.d.ts +0 -25
- package/dist/client-pref.d.ts +0 -3
- package/dist/client-router.d.ts +0 -300
- package/dist/client-state.d.ts +0 -22
- package/dist/client-theme.d.ts +0 -36
- package/dist/compile.d.ts +0 -15
- package/dist/compress.d.ts +0 -20
- package/dist/cookie.d.ts +0 -36
- package/dist/cors.d.ts +0 -25
- package/dist/cron-utils.d.ts +0 -73
- package/dist/csrf.d.ts +0 -47
- package/dist/deploy/config.d.ts +0 -2
- package/dist/deploy/gateway.d.ts +0 -2
- package/dist/deploy/index.d.ts +0 -4
- package/dist/deploy/manager.d.ts +0 -16
- package/dist/deploy/process.d.ts +0 -14
- package/dist/deploy/types.d.ts +0 -53
- package/dist/env.d.ts +0 -69
- package/dist/error-boundary.d.ts +0 -2
- package/dist/flash.d.ts +0 -90
- package/dist/fts.d.ts +0 -36
- package/dist/graphql.d.ts +0 -16
- package/dist/head.d.ts +0 -6
- package/dist/health.d.ts +0 -24
- package/dist/helmet.d.ts +0 -33
- package/dist/html-shell.d.ts +0 -1
- package/dist/hub.d.ts +0 -37
- package/dist/i18n.d.ts +0 -39
- package/dist/iii/client.d.ts +0 -2
- package/dist/iii/index.d.ts +0 -4
- package/dist/iii/register-worker.d.ts +0 -9
- package/dist/iii/rest.d.ts +0 -3
- package/dist/iii/stream.d.ts +0 -82
- package/dist/iii/types.d.ts +0 -121
- package/dist/iii/worker.d.ts +0 -2
- package/dist/iii/ws.d.ts +0 -22
- package/dist/index.d.ts +0 -101
- package/dist/index.js +0 -12752
- package/dist/kb/index.d.ts +0 -3
- package/dist/kb/types.d.ts +0 -72
- package/dist/layout.d.ts +0 -2
- package/dist/live.d.ts +0 -7
- package/dist/logdb/client.d.ts +0 -2
- package/dist/logdb/index.d.ts +0 -2
- package/dist/logdb/rest.d.ts +0 -5
- package/dist/logdb/types.d.ts +0 -27
- package/dist/logger.d.ts +0 -16
- package/dist/mailer.d.ts +0 -51
- package/dist/mcp.d.ts +0 -34
- package/dist/messager/agent.d.ts +0 -11
- package/dist/messager/client.d.ts +0 -2
- package/dist/messager/index.d.ts +0 -2
- package/dist/messager/rest.d.ts +0 -15
- package/dist/messager/types.d.ts +0 -57
- package/dist/messager/ws.d.ts +0 -14
- package/dist/module-server.d.ts +0 -9
- package/dist/not-found.d.ts +0 -2
- package/dist/notifier/client.d.ts +0 -2
- package/dist/notifier/index.d.ts +0 -2
- package/dist/notifier/types.d.ts +0 -105
- package/dist/opencode/client.d.ts +0 -2
- package/dist/opencode/index.d.ts +0 -2
- package/dist/opencode/permissions.d.ts +0 -5
- package/dist/opencode/prompt.d.ts +0 -8
- package/dist/opencode/rest.d.ts +0 -16
- package/dist/opencode/run.d.ts +0 -13
- package/dist/opencode/session.d.ts +0 -26
- package/dist/opencode/skills.d.ts +0 -4
- package/dist/opencode/tools/bash.d.ts +0 -6
- package/dist/opencode/tools/edit.d.ts +0 -19
- package/dist/opencode/tools/glob.d.ts +0 -9
- package/dist/opencode/tools/grep.d.ts +0 -17
- package/dist/opencode/tools/index.d.ts +0 -12
- package/dist/opencode/tools/question.d.ts +0 -5
- package/dist/opencode/tools/read.d.ts +0 -16
- package/dist/opencode/tools/skill.d.ts +0 -18
- package/dist/opencode/tools/web.d.ts +0 -18
- package/dist/opencode/tools/write.d.ts +0 -13
- package/dist/opencode/types.d.ts +0 -90
- package/dist/opencode/ws.d.ts +0 -21
- package/dist/permissions.d.ts +0 -51
- package/dist/postgres/client.d.ts +0 -4
- package/dist/postgres/index.d.ts +0 -4
- package/dist/postgres/module.d.ts +0 -17
- package/dist/postgres/schema/columns.d.ts +0 -99
- package/dist/postgres/schema/index.d.ts +0 -6
- package/dist/postgres/schema/sql.d.ts +0 -22
- package/dist/postgres/schema/table.d.ts +0 -141
- package/dist/postgres/schema/where.d.ts +0 -29
- package/dist/postgres/types.d.ts +0 -50
- package/dist/queue/index.d.ts +0 -2
- package/dist/queue/types.d.ts +0 -62
- package/dist/rate-limit.d.ts +0 -45
- package/dist/react.d.ts +0 -14
- package/dist/react.js +0 -751
- package/dist/redis/client.d.ts +0 -2
- package/dist/redis/types.d.ts +0 -18
- package/dist/request-id.d.ts +0 -40
- package/dist/router.d.ts +0 -73
- package/dist/s3.d.ts +0 -68
- package/dist/seo.d.ts +0 -104
- package/dist/serve.d.ts +0 -38
- package/dist/server-registry.d.ts +0 -10
- package/dist/session.d.ts +0 -117
- package/dist/sse.d.ts +0 -47
- package/dist/ssr-entries.d.ts +0 -4
- package/dist/ssr.d.ts +0 -11
- package/dist/static.d.ts +0 -23
- package/dist/stream.d.ts +0 -24
- package/dist/tailwind.d.ts +0 -15
- package/dist/tenant/client.d.ts +0 -2
- package/dist/tenant/graphql.d.ts +0 -3
- package/dist/tenant/index.d.ts +0 -2
- package/dist/tenant/rest.d.ts +0 -3
- package/dist/tenant/schema.d.ts +0 -5
- package/dist/tenant/types.d.ts +0 -48
- package/dist/tenant/utils.d.ts +0 -9
- package/dist/test-utils.d.ts +0 -194
- package/dist/theme.d.ts +0 -31
- package/dist/trace.d.ts +0 -95
- package/dist/tsx-context.d.ts +0 -32
- package/dist/types.d.ts +0 -47
- package/dist/upload.d.ts +0 -55
- package/dist/use-action.d.ts +0 -42
- package/dist/use-agent-stream.d.ts +0 -49
- package/dist/use-flash-message.d.ts +0 -17
- package/dist/use-websocket.d.ts +0 -42
- package/dist/user/client.d.ts +0 -30
- package/dist/user/index.d.ts +0 -2
- package/dist/user/oauth-login.d.ts +0 -21
- package/dist/user/oauth2.d.ts +0 -31
- package/dist/user/types.d.ts +0 -178
- package/dist/validate.d.ts +0 -32
- package/dist/vendor.d.ts +0 -7
- package/dist/webhook.d.ts +0 -79
- package/opencode/ui/app/globals.css +0 -1
- package/opencode/ui/app/layout.tsx +0 -13
- package/opencode/ui/app/page.tsx +0 -523
package/core/serve.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import http, { type IncomingMessage, type ServerResponse } from 'node:http'
|
|
3
|
+
import type { Duplex } from 'node:stream'
|
|
4
|
+
import { HttpError, type Context, type Handler } from '../types.ts'
|
|
5
|
+
import { runWithTrace, currentTraceId } from './trace.ts'
|
|
6
|
+
|
|
7
|
+
export interface ServeOptions {
|
|
8
|
+
port?: number
|
|
9
|
+
hostname?: string
|
|
10
|
+
signal?: AbortSignal
|
|
11
|
+
websocket?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void
|
|
12
|
+
/** Max request body size in bytes. Default: 10MB. Set to 0 for unlimited. */
|
|
13
|
+
maxBodySize?: number
|
|
14
|
+
/** Socket timeout in ms (inactivity). Default: 30_000. */
|
|
15
|
+
timeout?: number
|
|
16
|
+
/** Keep-Alive idle timeout in ms. Default: 5_000. */
|
|
17
|
+
keepAliveTimeout?: number
|
|
18
|
+
/** Headers timeout in ms (must be > keepAliveTimeout). Default: 6_000. */
|
|
19
|
+
headersTimeout?: number
|
|
20
|
+
shutdown?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Server {
|
|
24
|
+
stop: (timeoutMs?: number) => Promise<void>
|
|
25
|
+
/** Alias for `stop()`. Prefer this for consistency with other modules. */
|
|
26
|
+
close: (timeoutMs?: number) => Promise<void>
|
|
27
|
+
readonly port: number
|
|
28
|
+
readonly hostname: string
|
|
29
|
+
ready: Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Default max body size: 10MB. Set maxBodySize: 0 for unlimited. */
|
|
33
|
+
export const DEFAULT_MAX_BODY = 10 * 1024 * 1024
|
|
34
|
+
|
|
35
|
+
export async function readBody(req: IncomingMessage, maxSize?: number): Promise<Buffer> {
|
|
36
|
+
const limit = maxSize ?? DEFAULT_MAX_BODY
|
|
37
|
+
|
|
38
|
+
if (limit > 0) {
|
|
39
|
+
const cl = parseInt(req.headers['content-length'] ?? '0', 10)
|
|
40
|
+
if (cl > limit) throw new HttpError('Request body too large', 413)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const chunks: Buffer[] = []
|
|
44
|
+
let total = 0
|
|
45
|
+
for await (const chunk of req) {
|
|
46
|
+
total += (chunk as Buffer).byteLength
|
|
47
|
+
if (limit > 0 && total > limit) throw new HttpError('Request body too large', 413)
|
|
48
|
+
chunks.push(chunk as Buffer)
|
|
49
|
+
}
|
|
50
|
+
return Buffer.concat(chunks)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createRequest(
|
|
54
|
+
req: IncomingMessage,
|
|
55
|
+
body: Buffer,
|
|
56
|
+
): [Request, Record<string, string>] {
|
|
57
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
58
|
+
const query = Object.fromEntries(url.searchParams)
|
|
59
|
+
|
|
60
|
+
const headers: Record<string, string> = {}
|
|
61
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
62
|
+
if (value !== undefined) {
|
|
63
|
+
headers[key] = Array.isArray(value) ? value.join(', ') : value
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const request = new Request(url.href, {
|
|
68
|
+
method: req.method?.toUpperCase() ?? 'GET',
|
|
69
|
+
headers,
|
|
70
|
+
body:
|
|
71
|
+
req.method !== 'GET' && req.method !== 'HEAD' && body.length > 0 ? (body as BodyInit) : null,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return [request, query]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function sendResponse(
|
|
78
|
+
res: ServerResponse,
|
|
79
|
+
response: Response,
|
|
80
|
+
opts?: { traceId?: string | null },
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const headers: Record<string, string | string[]> = {}
|
|
83
|
+
response.headers.forEach((value, key) => {
|
|
84
|
+
if (key.toLowerCase() === 'set-cookie') {
|
|
85
|
+
const existing = headers[key]
|
|
86
|
+
headers[key] = existing
|
|
87
|
+
? Array.isArray(existing)
|
|
88
|
+
? [...existing, value]
|
|
89
|
+
: [existing, value]
|
|
90
|
+
: value
|
|
91
|
+
} else {
|
|
92
|
+
headers[key] = value
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Inject trace header — zero allocation, no Response re-wrapping
|
|
97
|
+
if (opts?.traceId && !headers['x-trace-id']) {
|
|
98
|
+
headers['x-trace-id'] = opts.traceId
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
res.writeHead(response.status, response.statusText, headers)
|
|
102
|
+
|
|
103
|
+
if (response.body) {
|
|
104
|
+
const reader = response.body.getReader()
|
|
105
|
+
try {
|
|
106
|
+
while (true) {
|
|
107
|
+
const { done, value } = await reader.read()
|
|
108
|
+
if (done) break
|
|
109
|
+
res.write(value)
|
|
110
|
+
}
|
|
111
|
+
res.end()
|
|
112
|
+
} catch (err) {
|
|
113
|
+
// Client disconnected or write failed — destroy socket cleanly
|
|
114
|
+
if (!res.destroyed) {
|
|
115
|
+
res.destroy(err instanceof Error ? err : undefined)
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
reader.releaseLock()
|
|
119
|
+
}
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
res.end()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function createTestServer(
|
|
127
|
+
handler: Handler,
|
|
128
|
+
options?: ServeOptions,
|
|
129
|
+
): Promise<{ server: Server; url: string }> {
|
|
130
|
+
const server = serve(handler, { ...options, port: options?.port ?? 0, shutdown: false })
|
|
131
|
+
await server.ready
|
|
132
|
+
return { server, url: `http://localhost:${server.port}` }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function serve(handler: Handler, options?: ServeOptions): Server {
|
|
136
|
+
const port = options?.port ?? 0
|
|
137
|
+
const hostname = options?.hostname ?? '0.0.0.0'
|
|
138
|
+
|
|
139
|
+
const server = http.createServer(async (req, res) => {
|
|
140
|
+
const incomingTrace =
|
|
141
|
+
(req.headers['x-trace-id'] as string) ||
|
|
142
|
+
(req.headers['traceparent'] as string)?.split('-')[1] ||
|
|
143
|
+
null
|
|
144
|
+
|
|
145
|
+
await runWithTrace(incomingTrace, async () => {
|
|
146
|
+
try {
|
|
147
|
+
const body = await readBody(req, options?.maxBodySize)
|
|
148
|
+
const [request, query] = createRequest(req, body)
|
|
149
|
+
const response = await handler(request, { params: {}, query } as Context)
|
|
150
|
+
await sendResponse(res, response, { traceId: currentTraceId() })
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (err instanceof HttpError && err.status === 413) {
|
|
153
|
+
res.writeHead(413, { 'Content-Type': 'text/plain' })
|
|
154
|
+
res.end('Request Body Too Large')
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
158
|
+
console.error(`[${currentTraceId()}] unhandled error: ${msg}`)
|
|
159
|
+
if (err instanceof Error && err.stack) console.error(err.stack)
|
|
160
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
|
161
|
+
res.end('Internal Server Error')
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// Connection timeouts — prevent slowloris and idle connection leaks
|
|
167
|
+
server.timeout = options?.timeout ?? 30_000
|
|
168
|
+
server.keepAliveTimeout = options?.keepAliveTimeout ?? 5_000
|
|
169
|
+
server.headersTimeout = options?.headersTimeout ?? 6_000
|
|
170
|
+
|
|
171
|
+
if (options?.websocket) {
|
|
172
|
+
server.on('upgrade', options.websocket)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let resolveReady!: () => void
|
|
176
|
+
const ready = new Promise<void>((r) => {
|
|
177
|
+
resolveReady = r
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
let shutdownHandler: (() => void) | null = null
|
|
181
|
+
|
|
182
|
+
if (options?.shutdown !== false) {
|
|
183
|
+
let shuttingDown = false
|
|
184
|
+
const shutdown = () => {
|
|
185
|
+
if (shuttingDown) return
|
|
186
|
+
shuttingDown = true
|
|
187
|
+
server.close()
|
|
188
|
+
// Give in-flight requests a chance to complete
|
|
189
|
+
const timer = setTimeout(() => {
|
|
190
|
+
server.closeAllConnections()
|
|
191
|
+
process.exit(0)
|
|
192
|
+
}, 10_000)
|
|
193
|
+
server.on('close', () => {
|
|
194
|
+
clearTimeout(timer)
|
|
195
|
+
process.exit(0)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
shutdownHandler = shutdown
|
|
199
|
+
process.on('SIGTERM', shutdown)
|
|
200
|
+
process.on('SIGINT', shutdown)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let _cachedPort = 0
|
|
204
|
+
let _cachedHostname = ''
|
|
205
|
+
|
|
206
|
+
if (options?.signal) {
|
|
207
|
+
if (options.signal.aborted) {
|
|
208
|
+
_cachedPort = 0
|
|
209
|
+
_cachedHostname = ''
|
|
210
|
+
server.close()
|
|
211
|
+
resolveReady()
|
|
212
|
+
return {
|
|
213
|
+
stop: () => Promise.resolve(),
|
|
214
|
+
close: () => Promise.resolve(),
|
|
215
|
+
ready,
|
|
216
|
+
get port() {
|
|
217
|
+
return 0
|
|
218
|
+
},
|
|
219
|
+
get hostname() {
|
|
220
|
+
return hostname
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
options.signal.addEventListener(
|
|
225
|
+
'abort',
|
|
226
|
+
() => {
|
|
227
|
+
server.close()
|
|
228
|
+
},
|
|
229
|
+
{ once: true },
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
server.on('error', (err) => {
|
|
234
|
+
console.error('Failed to start server:', err.message)
|
|
235
|
+
server.close()
|
|
236
|
+
_cachedPort = 0
|
|
237
|
+
resolveReady()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
server.listen(port, hostname, () => {
|
|
241
|
+
const addr = server.address()
|
|
242
|
+
if (addr && typeof addr !== 'string') {
|
|
243
|
+
_cachedPort = addr.port
|
|
244
|
+
_cachedHostname = addr.address
|
|
245
|
+
}
|
|
246
|
+
resolveReady()
|
|
247
|
+
|
|
248
|
+
// Startup message — automatic in all environments
|
|
249
|
+
const displayHost = _cachedHostname === '0.0.0.0' ? 'localhost' : _cachedHostname || 'localhost'
|
|
250
|
+
console.log(`weifuwu listening on http://${displayHost}:${_cachedPort}`)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
async function stop(timeoutMs = 10_000): Promise<void> {
|
|
254
|
+
if (shutdownHandler) {
|
|
255
|
+
process.off('SIGTERM', shutdownHandler)
|
|
256
|
+
process.off('SIGINT', shutdownHandler)
|
|
257
|
+
shutdownHandler = null
|
|
258
|
+
}
|
|
259
|
+
if (!server.listening) return
|
|
260
|
+
|
|
261
|
+
// 1. Stop accepting new connections
|
|
262
|
+
server.close()
|
|
263
|
+
|
|
264
|
+
// 2. Close idle keep-alive connections
|
|
265
|
+
server.closeIdleConnections()
|
|
266
|
+
|
|
267
|
+
// 3. Wait for in-flight requests to finish, or force-close after timeout
|
|
268
|
+
return new Promise<void>((resolve) => {
|
|
269
|
+
const timer = setTimeout(() => {
|
|
270
|
+
server.closeAllConnections()
|
|
271
|
+
resolve()
|
|
272
|
+
}, timeoutMs)
|
|
273
|
+
|
|
274
|
+
server.on('close', () => {
|
|
275
|
+
clearTimeout(timer)
|
|
276
|
+
resolve()
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
close: stop,
|
|
283
|
+
stop,
|
|
284
|
+
ready,
|
|
285
|
+
get port() {
|
|
286
|
+
if (!server.listening) return 0
|
|
287
|
+
return _cachedPort
|
|
288
|
+
},
|
|
289
|
+
get hostname() {
|
|
290
|
+
if (!server.listening) return hostname
|
|
291
|
+
return _cachedHostname || hostname
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
}
|
package/core/sse.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const encoder = new TextEncoder()
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format an SSE message string with a named event type.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* formatSSE('ping', { ts: Date.now() })
|
|
8
|
+
* // "event: ping\ndata: {"ts":...}\n\n"
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export function formatSSE(event: string, data: unknown): string {
|
|
12
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Format an SSE message string with only a data line (no event type).
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* formatSSEData({ message: 'hello' })
|
|
20
|
+
* // "data: {"message":"hello"}\n\n"
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function formatSSEData(data: unknown): string {
|
|
24
|
+
return `data: ${JSON.stringify(data)}\n\n`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** An SSE event to be sent via {@link createSSEStream}. */
|
|
28
|
+
export interface SSEEvent {
|
|
29
|
+
/** Event type (maps to `event:` field). */
|
|
30
|
+
event: string
|
|
31
|
+
/** Event payload (serialized as JSON in `data:` field). */
|
|
32
|
+
data: unknown
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a Server-Sent Events (SSE) `Response` from an async iterable.
|
|
37
|
+
*
|
|
38
|
+
* Each item in the iterable is serialized as an SSE message:
|
|
39
|
+
* - If the item has a `.type` property → `event: {type}` + `data: {item}`
|
|
40
|
+
* - Otherwise → `data: {item}`
|
|
41
|
+
*
|
|
42
|
+
* Errors are sent as `event: error` messages. `AbortError` is silently ignored.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* app.get('/events', () => {
|
|
46
|
+
* async function* generate() {
|
|
47
|
+
* yield { type: 'ping', data: { ts: Date.now() } }
|
|
48
|
+
* }
|
|
49
|
+
* return createSSEStream(generate())
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function createSSEStream(
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
iterable: AsyncIterable<any>,
|
|
56
|
+
opts?: { headers?: Record<string, string>; status?: number },
|
|
57
|
+
): Response {
|
|
58
|
+
return new Response(
|
|
59
|
+
new ReadableStream<Uint8Array>({
|
|
60
|
+
async start(controller) {
|
|
61
|
+
try {
|
|
62
|
+
for await (const event of iterable) {
|
|
63
|
+
const text = event.type ? formatSSE(event.type, event) : formatSSEData(event)
|
|
64
|
+
controller.enqueue(encoder.encode(text))
|
|
65
|
+
}
|
|
66
|
+
} catch (e: unknown) {
|
|
67
|
+
if (e instanceof Error && e.name !== 'AbortError') {
|
|
68
|
+
controller.enqueue(encoder.encode(formatSSE('error', { error: e.message })))
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
controller.close()
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
{
|
|
76
|
+
status: opts?.status ?? 200,
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'text/event-stream',
|
|
79
|
+
'Cache-Control': 'no-cache',
|
|
80
|
+
Connection: 'keep-alive',
|
|
81
|
+
...opts?.headers,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
}
|
package/core/trace.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
3
|
+
import type { Context, Middleware } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
// Augment Context with trace property
|
|
6
|
+
declare module '../types.ts' {
|
|
7
|
+
interface Context {
|
|
8
|
+
trace: TraceInjected
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TraceInjected {
|
|
13
|
+
/** Unique request identifier (from X-Request-ID header or auto-generated). */
|
|
14
|
+
requestId: string
|
|
15
|
+
/** Unique trace identifier for the request. */
|
|
16
|
+
traceId: string
|
|
17
|
+
/** Milliseconds elapsed since the trace started. */
|
|
18
|
+
elapsed: () => number
|
|
19
|
+
/** Timestamp (ms) when the trace started. */
|
|
20
|
+
startTime: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TraceContext {
|
|
24
|
+
/** Unique identifier for the current request trace. */
|
|
25
|
+
traceId: string
|
|
26
|
+
/** Timestamp (ms since epoch) when the trace started. */
|
|
27
|
+
startTime: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const als = new AsyncLocalStorage<TraceContext>()
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the current request's trace ID.
|
|
34
|
+
* Returns `undefined` when called outside a request context (e.g. at startup).
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const traceId = currentTraceId()
|
|
38
|
+
* log.info({ traceId }, 'request started')
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function currentTraceId(): string | undefined {
|
|
42
|
+
return als.getStore()?.traceId
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the full current trace context ({ traceId, startTime }).
|
|
47
|
+
* Returns `undefined` outside a request.
|
|
48
|
+
*/
|
|
49
|
+
export function currentTrace(): TraceContext | undefined {
|
|
50
|
+
return als.getStore()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run a function inside a trace context.
|
|
55
|
+
* Used internally by `serve()` for every incoming request.
|
|
56
|
+
* If `incomingTraceId` is provided (e.g. from an `X-Trace-Id` header) it is reused;
|
|
57
|
+
* otherwise a new UUID is generated.
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* const result = runWithTrace(req.headers.get('x-trace-id'), () => {
|
|
61
|
+
* return handleRequest(req)
|
|
62
|
+
* })
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @param incomingTraceId - Optional trace ID from upstream. Pass `null` to auto-generate.
|
|
66
|
+
* @param fn - Function to execute within the trace scope.
|
|
67
|
+
* @returns The return value of `fn`.
|
|
68
|
+
*/
|
|
69
|
+
export function runWithTrace<T>(incomingTraceId: string | null, fn: () => T): T {
|
|
70
|
+
const traceId = incomingTraceId || crypto.randomUUID()
|
|
71
|
+
const startTime = Date.now()
|
|
72
|
+
return als.run({ traceId, startTime }, fn)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Milliseconds elapsed since the current trace started.
|
|
77
|
+
* Returns `0` if called outside a request context.
|
|
78
|
+
*
|
|
79
|
+
* ```ts
|
|
80
|
+
* app.use(async (req, ctx, next) => {
|
|
81
|
+
* const res = await next(req, ctx)
|
|
82
|
+
* console.log('handled in', traceElapsed(), 'ms')
|
|
83
|
+
* return res
|
|
84
|
+
* })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function traceElapsed(): number {
|
|
88
|
+
const ctx = als.getStore()
|
|
89
|
+
if (!ctx) return 0
|
|
90
|
+
return Date.now() - ctx.startTime
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Options for {@link trace}. */
|
|
94
|
+
export interface TraceOptions {
|
|
95
|
+
/** Header name for request ID (default: `'X-Request-ID'`). */
|
|
96
|
+
header?: string
|
|
97
|
+
/** Custom ID generator (default: `crypto.randomUUID`). */
|
|
98
|
+
generator?: () => string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Request tracing middleware.
|
|
103
|
+
*
|
|
104
|
+
* Injects `ctx.trace = { requestId, traceId, elapsed, startTime }`.
|
|
105
|
+
* Reads/writes `X-Request-ID` header. Combines the functionality of `requestId()`
|
|
106
|
+
* with the per-request tracing from `AsyncLocalStorage`.
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* import { trace } from 'weifuwu'
|
|
110
|
+
* app.use(trace())
|
|
111
|
+
*
|
|
112
|
+
* app.get('/', (req, ctx) => {
|
|
113
|
+
* console.log(ctx.trace.requestId) // 550e8400-e29b-...
|
|
114
|
+
* console.log(ctx.trace.traceId) // same as currentTraceId()
|
|
115
|
+
* console.log(ctx.trace.elapsed()) // ms since request start
|
|
116
|
+
* })
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export function trace(
|
|
120
|
+
options?: TraceOptions,
|
|
121
|
+
): Middleware<Context, Context & { trace: TraceInjected }> {
|
|
122
|
+
const header = options?.header ?? 'X-Request-ID'
|
|
123
|
+
const gen = options?.generator ?? (() => crypto.randomUUID())
|
|
124
|
+
|
|
125
|
+
return async (req, ctx, next) => {
|
|
126
|
+
const existing = req.headers.get(header)
|
|
127
|
+
const requestId = existing ?? gen()
|
|
128
|
+
const tc = als.getStore()
|
|
129
|
+
|
|
130
|
+
;(ctx as Context & { trace: TraceInjected }).trace = {
|
|
131
|
+
requestId,
|
|
132
|
+
traceId: tc?.traceId ?? requestId,
|
|
133
|
+
startTime: tc?.startTime ?? Date.now(),
|
|
134
|
+
elapsed: () => {
|
|
135
|
+
const t = als.getStore()
|
|
136
|
+
return t ? Date.now() - t.startTime : 0
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const res = await next(req, ctx as Context & { trace: TraceInjected })
|
|
141
|
+
if (res.headers.has(header)) return res
|
|
142
|
+
const h = new Headers(res.headers)
|
|
143
|
+
h.set(header, requestId)
|
|
144
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h })
|
|
145
|
+
}
|
|
146
|
+
}
|