weifuwu 0.27.2 → 0.27.3
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/dist/ai/provider.d.ts +45 -0
- package/dist/ai/stream.d.ts +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +131 -0
- package/dist/core/cookie.d.ts +36 -0
- package/dist/core/env.d.ts +69 -0
- package/dist/core/logger.d.ts +16 -0
- package/dist/core/router.d.ts +72 -0
- package/dist/core/serve.d.ts +38 -0
- package/dist/core/sse.d.ts +47 -0
- package/dist/core/trace.d.ts +95 -0
- package/dist/graphql.d.ts +16 -0
- package/dist/hub.d.ts +36 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +3963 -0
- package/dist/mailer.d.ts +51 -0
- package/dist/middleware/compress.d.ts +20 -0
- package/dist/middleware/cors.d.ts +25 -0
- package/dist/middleware/csrf.d.ts +47 -0
- package/dist/middleware/flash.d.ts +90 -0
- package/dist/middleware/health.d.ts +24 -0
- package/dist/middleware/helmet.d.ts +33 -0
- package/dist/middleware/i18n.d.ts +39 -0
- package/dist/middleware/rate-limit.d.ts +44 -0
- package/dist/middleware/request-id.d.ts +40 -0
- package/dist/middleware/static.d.ts +23 -0
- package/dist/middleware/theme.d.ts +31 -0
- package/dist/middleware/upload.d.ts +55 -0
- package/dist/middleware/validate.d.ts +32 -0
- package/dist/postgres/client.d.ts +4 -0
- package/dist/postgres/index.d.ts +4 -0
- package/dist/postgres/module.d.ts +16 -0
- package/dist/postgres/schema/columns.d.ts +99 -0
- package/dist/postgres/schema/index.d.ts +6 -0
- package/dist/postgres/schema/sql.d.ts +22 -0
- package/dist/postgres/schema/table.d.ts +141 -0
- package/dist/postgres/schema/where.d.ts +29 -0
- package/dist/postgres/types.d.ts +49 -0
- package/dist/queue/cron.d.ts +9 -0
- package/dist/queue/index.d.ts +2 -0
- package/dist/queue/types.d.ts +61 -0
- package/dist/redis/client.d.ts +2 -0
- package/{redis/index.ts → dist/redis/index.d.ts} +2 -2
- package/dist/redis/types.d.ts +17 -0
- package/dist/test/test-utils.d.ts +193 -0
- package/dist/types.d.ts +50 -0
- package/package.json +10 -10
- package/ai/provider.ts +0 -129
- package/ai/stream.ts +0 -63
- package/cli.ts +0 -147
- package/core/cookie.ts +0 -114
- package/core/env.ts +0 -142
- package/core/logger.ts +0 -72
- package/core/router.ts +0 -795
- package/core/serve.ts +0 -294
- package/core/sse.ts +0 -85
- package/core/trace.ts +0 -146
- package/graphql.ts +0 -267
- package/hub.ts +0 -133
- package/index.ts +0 -71
- package/mailer.ts +0 -81
- package/middleware/compress.ts +0 -103
- package/middleware/cors.ts +0 -81
- package/middleware/csrf.ts +0 -112
- package/middleware/flash.ts +0 -144
- package/middleware/health.ts +0 -44
- package/middleware/helmet.ts +0 -98
- package/middleware/i18n.ts +0 -175
- package/middleware/rate-limit.ts +0 -167
- package/middleware/request-id.ts +0 -60
- package/middleware/static.ts +0 -149
- package/middleware/theme.ts +0 -84
- package/middleware/upload.ts +0 -168
- package/middleware/validate.ts +0 -186
- package/postgres/client.ts +0 -132
- package/postgres/index.ts +0 -4
- package/postgres/module.ts +0 -37
- package/postgres/schema/columns.ts +0 -186
- package/postgres/schema/index.ts +0 -36
- package/postgres/schema/sql.ts +0 -39
- package/postgres/schema/table.ts +0 -548
- package/postgres/schema/where.ts +0 -99
- package/postgres/types.ts +0 -48
- package/queue/cron.ts +0 -90
- package/queue/index.ts +0 -654
- package/queue/types.ts +0 -60
- package/redis/client.ts +0 -24
- package/redis/types.ts +0 -28
- package/types.ts +0 -78
package/core/router.ts
DELETED
|
@@ -1,795 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
import { WebSocketServer } from 'ws'
|
|
3
|
-
import type {
|
|
4
|
-
WebSocket,
|
|
5
|
-
Context,
|
|
6
|
-
Handler,
|
|
7
|
-
Middleware,
|
|
8
|
-
MiddlewareMeta,
|
|
9
|
-
ErrorHandler,
|
|
10
|
-
} from '../types.ts'
|
|
11
|
-
import http, { type IncomingMessage } from 'node:http'
|
|
12
|
-
import type { Duplex } from 'node:stream'
|
|
13
|
-
import { createHub, type Hub } from '../hub.ts'
|
|
14
|
-
|
|
15
|
-
import { isProd } from './env.ts'
|
|
16
|
-
|
|
17
|
-
export type WebSocketHandler = {
|
|
18
|
-
open?: (ws: WebSocket, ctx: Context) => void | Promise<void>
|
|
19
|
-
message?: (ws: WebSocket, ctx: Context, data: string | Buffer) => void | Promise<void>
|
|
20
|
-
close?: (ws: WebSocket, ctx: Context) => void | Promise<void>
|
|
21
|
-
error?: (ws: WebSocket, ctx: Context, error: Error) => void | Promise<void>
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type TrieNode = {
|
|
25
|
-
children: Map<string, TrieNode>
|
|
26
|
-
handlers: Map<string, Handler>
|
|
27
|
-
middlewares: Map<string, Middleware[]>
|
|
28
|
-
param?: string
|
|
29
|
-
wildcard?: boolean
|
|
30
|
-
pathMws: Middleware[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
type WsTrieNode = {
|
|
34
|
-
children: Map<string, WsTrieNode>
|
|
35
|
-
handler?: WebSocketHandler
|
|
36
|
-
middlewares: Middleware[]
|
|
37
|
-
param?: string
|
|
38
|
-
wildcard?: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const createTrieNode = (): TrieNode => ({
|
|
42
|
-
children: new Map(),
|
|
43
|
-
handlers: new Map(),
|
|
44
|
-
middlewares: new Map(),
|
|
45
|
-
pathMws: [],
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
const createWsNode = (): WsTrieNode => ({
|
|
49
|
-
children: new Map(),
|
|
50
|
-
middlewares: [],
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
interface TrieNodeBase<T> {
|
|
54
|
-
children: Map<string, T>
|
|
55
|
-
param?: string
|
|
56
|
-
wildcard?: boolean
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function createParamChild<T extends TrieNodeBase<T>>(
|
|
60
|
-
node: T,
|
|
61
|
-
segment: string,
|
|
62
|
-
createNode: () => T,
|
|
63
|
-
): T {
|
|
64
|
-
const paramName = segment.slice(1)
|
|
65
|
-
if (!node.children.has(':')) {
|
|
66
|
-
const child = createNode()
|
|
67
|
-
child.param = paramName
|
|
68
|
-
node.children.set(':', child)
|
|
69
|
-
}
|
|
70
|
-
const child = node.children.get(':')!
|
|
71
|
-
if (child.param !== paramName) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
`Param name conflict: ":${child.param}" already registered, cannot register ":"${paramName}"`,
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
return child
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function getOrCreateChild<T extends TrieNodeBase<T>>(
|
|
80
|
-
node: T,
|
|
81
|
-
segment: string,
|
|
82
|
-
createNode: () => T,
|
|
83
|
-
allowWildcard: boolean,
|
|
84
|
-
): T {
|
|
85
|
-
if (allowWildcard && segment === '*') {
|
|
86
|
-
node.wildcard = true
|
|
87
|
-
return node
|
|
88
|
-
}
|
|
89
|
-
if (segment.startsWith(':')) return createParamChild(node, segment, createNode)
|
|
90
|
-
if (!node.children.has(segment)) node.children.set(segment, createNode())
|
|
91
|
-
return node.children.get(segment)!
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function matchChild<T extends TrieNodeBase<T>>(
|
|
95
|
-
node: T,
|
|
96
|
-
segment: string,
|
|
97
|
-
params: Record<string, string>,
|
|
98
|
-
allowWildcard = false,
|
|
99
|
-
): T | null {
|
|
100
|
-
if (node.children.has(segment)) return node.children.get(segment)!
|
|
101
|
-
if (node.children.has(':')) {
|
|
102
|
-
const child = node.children.get(':')!
|
|
103
|
-
if (child.param) params[child.param] = segment
|
|
104
|
-
return child
|
|
105
|
-
}
|
|
106
|
-
if (allowWildcard && node.wildcard) return node
|
|
107
|
-
return null
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
type WsMatchResult = {
|
|
111
|
-
handler: WebSocketHandler
|
|
112
|
-
middlewares: Middleware[]
|
|
113
|
-
params: Record<string, string>
|
|
114
|
-
} | null
|
|
115
|
-
|
|
116
|
-
type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void
|
|
117
|
-
|
|
118
|
-
// Router<T> — T accumulates types from global middleware calls via use(mw).
|
|
119
|
-
// Route-level middleware does not change the Router's type parameter.
|
|
120
|
-
export class Router<T extends Context = Context> {
|
|
121
|
-
private root: TrieNode = createTrieNode()
|
|
122
|
-
private wsRoot: WsTrieNode = createWsNode()
|
|
123
|
-
private globalMws: Middleware[] = []
|
|
124
|
-
private errorHandler?: ErrorHandler<T>
|
|
125
|
-
private _hasWildcard = false
|
|
126
|
-
private _hub?: Hub
|
|
127
|
-
private _wss?: WebSocketServer
|
|
128
|
-
/** Track which ctx fields have been injected so far (for dependency checking). */
|
|
129
|
-
private _ctxFields = new Set<string>()
|
|
130
|
-
private get wss(): WebSocketServer {
|
|
131
|
-
if (!this._wss) this._wss = new WebSocketServer({ noServer: true })
|
|
132
|
-
return this._wss
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private get hub(): Hub {
|
|
136
|
-
if (!this._hub) this._hub = createHub()
|
|
137
|
-
return this._hub
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Inject a custom hub (e.g. with Redis for cross-process broadcast). */
|
|
141
|
-
wsHub(hub: Hub): this {
|
|
142
|
-
this._hub = hub
|
|
143
|
-
return this
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Global middleware — accumulates types into Router<T>.
|
|
147
|
-
// The middleware's In type is Context (base); Out is what it injects.
|
|
148
|
-
// Router accumulates via intersection: Router<T & Out>
|
|
149
|
-
use<Out extends Context>(mw: Middleware<Context, Out>): Router<T & Out>
|
|
150
|
-
// Path-scoped middleware — does not accumulate
|
|
151
|
-
use(path: string, mw: Middleware<T, T>): Router<T>
|
|
152
|
-
// Mount sub-router — flattens into parent, does not accumulate
|
|
153
|
-
use(path: string, router: Router<Context>): Router<T>
|
|
154
|
-
// Module with .middleware() — auto-register middleware + mount at /
|
|
155
|
-
use(mod: Router & { middleware: () => Middleware }): Router<T>
|
|
156
|
-
use(
|
|
157
|
-
arg1: string | Middleware<Context, Context> | (Router & { middleware: () => Middleware }),
|
|
158
|
-
arg2?: Router<Context> | Middleware<T, T>,
|
|
159
|
-
): Router<T> {
|
|
160
|
-
if (typeof arg1 === 'string') {
|
|
161
|
-
if (arg2 instanceof Router) {
|
|
162
|
-
this._mountRouter(arg1, arg2)
|
|
163
|
-
} else if (typeof arg2 === 'function') {
|
|
164
|
-
let node = this.root
|
|
165
|
-
for (const segment of this.splitPath(arg1)) {
|
|
166
|
-
node = getOrCreateChild(node, segment, createTrieNode, false)
|
|
167
|
-
}
|
|
168
|
-
node.pathMws.push(arg2 as unknown as Middleware)
|
|
169
|
-
this._checkMiddlewareMeta(arg2, `${arg1}`)
|
|
170
|
-
}
|
|
171
|
-
} else if (typeof arg1 === 'function') {
|
|
172
|
-
this.globalMws.push(arg1 as unknown as Middleware)
|
|
173
|
-
this._checkMiddlewareMeta(arg1, 'global')
|
|
174
|
-
} else if (
|
|
175
|
-
typeof arg1 === 'object' &&
|
|
176
|
-
arg1 !== null &&
|
|
177
|
-
'middleware' in arg1 &&
|
|
178
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
179
|
-
typeof (arg1 as any).middleware === 'function' &&
|
|
180
|
-
arg1 instanceof Router
|
|
181
|
-
) {
|
|
182
|
-
// Auto-register modules with .middleware() — e.g. theme(), i18n(), analytics()
|
|
183
|
-
// Registers both the middleware and mounts routes at /
|
|
184
|
-
const mod = arg1 as Router & { middleware: () => Middleware }
|
|
185
|
-
const mw = mod.middleware()
|
|
186
|
-
this.globalMws.push(mw as unknown as Middleware)
|
|
187
|
-
this._checkMiddlewareMeta(mw, 'global (auto-registered)')
|
|
188
|
-
this._mountRouter('/', mod as Router)
|
|
189
|
-
}
|
|
190
|
-
return this
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Check a middleware's dependency metadata and emit warnings if
|
|
195
|
-
* required fields haven't been injected yet.
|
|
196
|
-
* Attach __meta to a middleware function:
|
|
197
|
-
*
|
|
198
|
-
* ```ts
|
|
199
|
-
* mw.__meta = { injects: ['sql'], depends: ['session'] }
|
|
200
|
-
* ```
|
|
201
|
-
*/
|
|
202
|
-
private _checkMiddlewareMeta(mw: unknown, location: string): void {
|
|
203
|
-
const meta: MiddlewareMeta | undefined =
|
|
204
|
-
(mw as Middleware).__meta ??
|
|
205
|
-
(typeof mw === 'object' && mw && 'middleware' in mw
|
|
206
|
-
? (mw as { middleware(): Middleware }).middleware().__meta
|
|
207
|
-
: undefined)
|
|
208
|
-
if (!meta) return
|
|
209
|
-
|
|
210
|
-
for (const dep of meta.depends) {
|
|
211
|
-
if (!this._ctxFields.has(dep)) {
|
|
212
|
-
console.warn(
|
|
213
|
-
`[weifuwu] Middleware at "${location}" depends on ctx.${dep} but it hasn't been registered yet.` +
|
|
214
|
-
`\n Register the provider before this middleware:` +
|
|
215
|
-
`\n app.use(${dep}()) // add before this middleware` +
|
|
216
|
-
`\n Current ctx fields: [${[...this._ctxFields].join(', ')}]`,
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
for (const field of meta.injects) {
|
|
222
|
-
this._ctxFields.add(field)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Route registration — returns Router<T> unchanged.
|
|
227
|
-
// Route-level middleware and handlers get Context<T>.
|
|
228
|
-
get(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
|
|
229
|
-
return this._route('GET', path, ...args)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
post(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
|
|
233
|
-
return this._route('POST', path, ...args)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
put(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
|
|
237
|
-
return this._route('PUT', path, ...args)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
delete(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
|
|
241
|
-
return this._route('DELETE', path, ...args)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
patch(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
|
|
245
|
-
return this._route('PATCH', path, ...args)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
head(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
|
|
249
|
-
return this._route('HEAD', path, ...args)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
options(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
|
|
253
|
-
return this._route('OPTIONS', path, ...args)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
all(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
|
|
257
|
-
return this._route('*', path, ...args)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
onError(handler: ErrorHandler<T>): Router<T> {
|
|
261
|
-
this.errorHandler = handler
|
|
262
|
-
return this
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
private _route(
|
|
266
|
-
method: string,
|
|
267
|
-
path: string,
|
|
268
|
-
...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]
|
|
269
|
-
): Router<T> {
|
|
270
|
-
return this._routeImpl(method, path, args)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** Internal route registration — no type constraints (used by _mountRouter). */
|
|
274
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
275
|
-
private _routeImpl(method: string, path: string, args: any[]): Router<T> {
|
|
276
|
-
const last = args[args.length - 1]
|
|
277
|
-
if (last instanceof Router) {
|
|
278
|
-
this._mountRouter(path, last, args.slice(0, -1))
|
|
279
|
-
return this
|
|
280
|
-
}
|
|
281
|
-
const handler = args.pop()
|
|
282
|
-
const middlewares: Middleware[] = args
|
|
283
|
-
const segments = this.splitPath(path)
|
|
284
|
-
let node = this.root
|
|
285
|
-
|
|
286
|
-
for (const segment of segments) {
|
|
287
|
-
if (segment === '*') {
|
|
288
|
-
this._hasWildcard = true
|
|
289
|
-
const remaining = segments.indexOf('*') < segments.length - 1
|
|
290
|
-
if (remaining) {
|
|
291
|
-
console.warn(`Route "${path}": segments after "*" are ignored`)
|
|
292
|
-
}
|
|
293
|
-
node.wildcard = true
|
|
294
|
-
node.handlers.set(method, handler)
|
|
295
|
-
if (middlewares.length > 0) node.middlewares.set(method, middlewares)
|
|
296
|
-
return this
|
|
297
|
-
}
|
|
298
|
-
node = getOrCreateChild(node, segment, createTrieNode, false)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (!isProd() && node.handlers.has(method)) {
|
|
302
|
-
console.warn(`[router] route conflict: ${method} ${path} overwrites existing handler`)
|
|
303
|
-
}
|
|
304
|
-
node.handlers.set(method, handler)
|
|
305
|
-
if (middlewares.length > 0) node.middlewares.set(method, middlewares)
|
|
306
|
-
return this
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
ws(path: string, ...args: [...Middleware<T, T>[], WebSocketHandler]): Router<T> {
|
|
310
|
-
const handler = args.pop()! as WebSocketHandler
|
|
311
|
-
const middlewares = args as unknown as Middleware[]
|
|
312
|
-
const segments = this.splitPath(path)
|
|
313
|
-
let node = this.wsRoot
|
|
314
|
-
|
|
315
|
-
for (const segment of segments) {
|
|
316
|
-
node = getOrCreateChild(node, segment, createWsNode, true)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
node.handler = handler
|
|
320
|
-
node.middlewares = middlewares
|
|
321
|
-
return this
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
handler(): Handler<T> {
|
|
325
|
-
return (req, ctx) => {
|
|
326
|
-
const url = new URL(req.url)
|
|
327
|
-
return this.handle(req, ctx as Context, this.splitPath(url.pathname))
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** Returns a human-readable list of all registered routes. Useful for debugging. */
|
|
332
|
-
routes(): string[] {
|
|
333
|
-
const result: string[] = []
|
|
334
|
-
if (this.globalMws.length > 0) {
|
|
335
|
-
result.push(`MIDDLEWARE [${this.globalMws.length} global]`)
|
|
336
|
-
}
|
|
337
|
-
this._collectRoutes(this.root, '', result)
|
|
338
|
-
this._collectWsRoutes(this.wsRoot, '', result)
|
|
339
|
-
return result
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
private _collectRoutes(node: TrieNode, prefix: string, result: string[]): void {
|
|
343
|
-
for (const [method] of node.handlers) {
|
|
344
|
-
const m = method === '*' ? 'ANY' : method
|
|
345
|
-
const path = (prefix || '/') + (node.wildcard ? '/*' : '')
|
|
346
|
-
const middlewares = node.middlewares.get(method)
|
|
347
|
-
const mwCount = middlewares ? ` (+${middlewares.length} mw)` : ''
|
|
348
|
-
result.push(`${m.padEnd(7)} ${path}${mwCount}`)
|
|
349
|
-
}
|
|
350
|
-
for (const [seg, child] of node.children) {
|
|
351
|
-
const segment = seg === ':' ? `:${child.param}` : seg
|
|
352
|
-
this._collectRoutes(child, prefix + '/' + segment, result)
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private _collectWsRoutes(node: WsTrieNode, prefix: string, result: string[]): void {
|
|
357
|
-
if (node.handler) {
|
|
358
|
-
const path = prefix || '/'
|
|
359
|
-
const mwCount = node.middlewares.length ? ` (+${node.middlewares.length} mw)` : ''
|
|
360
|
-
result.push(`WS ${path}${mwCount}`)
|
|
361
|
-
}
|
|
362
|
-
for (const [seg, child] of node.children) {
|
|
363
|
-
const segment = seg === ':' ? `:${child.param}` : seg
|
|
364
|
-
this._collectWsRoutes(child, prefix + '/' + segment, result)
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
websocketHandler(): WsUpgradeHandler {
|
|
369
|
-
const wsRoot = this.wsRoot
|
|
370
|
-
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
371
|
-
const router = this
|
|
372
|
-
|
|
373
|
-
return (req, socket, head) => {
|
|
374
|
-
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
375
|
-
const segments = url.pathname.split('/').filter(Boolean)
|
|
376
|
-
|
|
377
|
-
const match = router.matchWsTrie(wsRoot, segments)
|
|
378
|
-
if (!match) {
|
|
379
|
-
socket.destroy()
|
|
380
|
-
return
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const query = Object.fromEntries(url.searchParams)
|
|
384
|
-
const ctx = { params: match.params, query } as Context
|
|
385
|
-
|
|
386
|
-
const allMws =
|
|
387
|
-
router.globalMws.length === 0 && match.middlewares.length === 0
|
|
388
|
-
? ([] as Middleware[])
|
|
389
|
-
: [...router.globalMws, ...match.middlewares]
|
|
390
|
-
|
|
391
|
-
if (allMws.length === 0) {
|
|
392
|
-
upgradeSocket(router.wss, req, socket, head, match.handler, ctx, router.hub)
|
|
393
|
-
return
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const finalHandler: Handler = () => {
|
|
397
|
-
try {
|
|
398
|
-
upgradeSocket(router.wss, req, socket, head, match.handler, ctx, router.hub)
|
|
399
|
-
} catch {
|
|
400
|
-
socket.destroy()
|
|
401
|
-
return new Response('WebSocket upgrade failed', { status: 500 })
|
|
402
|
-
}
|
|
403
|
-
return new Response(null, { status: 200 })
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const webReq = new Request(url.href, {
|
|
407
|
-
method: req.method ?? 'GET',
|
|
408
|
-
headers: nodeReqHeadersToRecord(req.headers),
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
void router
|
|
412
|
-
.runChain(allMws, finalHandler, webReq, ctx)
|
|
413
|
-
.then((result) => {
|
|
414
|
-
if (result.status >= 400) {
|
|
415
|
-
sendHttpResponseOnSocket(socket, result)
|
|
416
|
-
}
|
|
417
|
-
})
|
|
418
|
-
.catch(() => {
|
|
419
|
-
socket.destroy()
|
|
420
|
-
})
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
private _mountRouter(prefix: string, sub: Router<Context>, extraMws: Middleware[] = []): void {
|
|
425
|
-
const base = prefix === '/' ? '' : prefix.replace(/\/$/, '')
|
|
426
|
-
|
|
427
|
-
const mountMw: Middleware = (req, ctx, next) => {
|
|
428
|
-
ctx.mountPath = (ctx.mountPath || '') + base
|
|
429
|
-
return next(req, ctx)
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const allExtra =
|
|
433
|
-
extraMws.length === 0 && sub.globalMws.length === 0
|
|
434
|
-
? [mountMw]
|
|
435
|
-
: [mountMw, ...extraMws, ...sub.globalMws]
|
|
436
|
-
|
|
437
|
-
const routes: Array<{
|
|
438
|
-
method: string
|
|
439
|
-
path: string
|
|
440
|
-
handler: Handler
|
|
441
|
-
middlewares: Middleware[]
|
|
442
|
-
}> = []
|
|
443
|
-
this._collect(sub.root, '', routes, [])
|
|
444
|
-
for (const { method, path, handler, middlewares } of routes) {
|
|
445
|
-
this._routeImpl(method, base + path, [...allExtra, ...middlewares, handler])
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const wsRoutes: Array<{ path: string; handler: WebSocketHandler; middlewares: Middleware[] }> =
|
|
449
|
-
[]
|
|
450
|
-
this._collectWs(sub.wsRoot, '', wsRoutes)
|
|
451
|
-
for (const { path, handler, middlewares } of wsRoutes) {
|
|
452
|
-
this.ws(
|
|
453
|
-
base + path,
|
|
454
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
455
|
-
...(allExtra as Middleware<any, any>[]),
|
|
456
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
457
|
-
...(middlewares as Middleware<any, any>[]),
|
|
458
|
-
handler,
|
|
459
|
-
)
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
private mergeMws(base: Middleware[], extra: Middleware[]): Middleware[] {
|
|
464
|
-
if (base.length === 0) return extra.length === 0 ? base : extra
|
|
465
|
-
if (extra.length === 0) return base
|
|
466
|
-
return [...base, ...extra]
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
private _collect(
|
|
470
|
-
node: TrieNode,
|
|
471
|
-
prefix: string,
|
|
472
|
-
result: Array<{ method: string; path: string; handler: Handler; middlewares: Middleware[] }>,
|
|
473
|
-
pathMwsAcc: Middleware[],
|
|
474
|
-
): void {
|
|
475
|
-
const mws = this.mergeMws(pathMwsAcc, node.pathMws)
|
|
476
|
-
for (const [method, handler] of node.handlers) {
|
|
477
|
-
const rmws = node.middlewares.get(method) || []
|
|
478
|
-
const suffix = node.wildcard ? '/*' : ''
|
|
479
|
-
result.push({
|
|
480
|
-
method,
|
|
481
|
-
path: (prefix || '/') + suffix,
|
|
482
|
-
handler,
|
|
483
|
-
middlewares: this.mergeMws(mws, rmws),
|
|
484
|
-
})
|
|
485
|
-
}
|
|
486
|
-
for (const [seg, child] of node.children) {
|
|
487
|
-
const next = seg === ':' ? `/:${child.param}` : `/${seg}`
|
|
488
|
-
this._collect(child, prefix + next, result, mws)
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
private _collectWs(
|
|
493
|
-
node: WsTrieNode,
|
|
494
|
-
prefix: string,
|
|
495
|
-
result: Array<{ path: string; handler: WebSocketHandler; middlewares: Middleware[] }>,
|
|
496
|
-
pathMwsAcc: Middleware[] = [],
|
|
497
|
-
): void {
|
|
498
|
-
const mws = this.mergeMws(pathMwsAcc, node.middlewares)
|
|
499
|
-
if (node.handler) result.push({ path: prefix || '/', handler: node.handler, middlewares: mws })
|
|
500
|
-
for (const [seg, child] of node.children) {
|
|
501
|
-
const next = seg === ':' ? `/:${child.param}` : `/${seg}`
|
|
502
|
-
this._collectWs(child, prefix + next, result, mws)
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
private splitPath(path: string): string[] {
|
|
507
|
-
return path.split('/').filter(Boolean)
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
private matchTrie(
|
|
511
|
-
method: string,
|
|
512
|
-
segments: string[],
|
|
513
|
-
): {
|
|
514
|
-
handler?: Handler
|
|
515
|
-
middlewares: Middleware[]
|
|
516
|
-
pathMws: Middleware[]
|
|
517
|
-
params: Record<string, string>
|
|
518
|
-
allowedMethods?: string[]
|
|
519
|
-
} | null {
|
|
520
|
-
let node = this.root
|
|
521
|
-
const params: Record<string, string> = {}
|
|
522
|
-
const pathMws: Middleware[] = []
|
|
523
|
-
let wildcardHandler: Handler | null = null
|
|
524
|
-
let wildcardMws: Middleware[] = []
|
|
525
|
-
let wildcardIdx = -1
|
|
526
|
-
|
|
527
|
-
for (let i = 0; i < segments.length; i++) {
|
|
528
|
-
pathMws.push(...node.pathMws)
|
|
529
|
-
|
|
530
|
-
if (this._hasWildcard && node.wildcard) {
|
|
531
|
-
const h = node.handlers.get('*') || node.handlers.get(method)
|
|
532
|
-
if (h) {
|
|
533
|
-
wildcardHandler = h
|
|
534
|
-
wildcardMws = node.middlewares.get(method) || node.middlewares.get('*') || []
|
|
535
|
-
wildcardIdx = i
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const segment = segments[i]
|
|
540
|
-
|
|
541
|
-
const next = matchChild(node, segment, params, false)
|
|
542
|
-
if (!next) {
|
|
543
|
-
if (wildcardHandler) {
|
|
544
|
-
params['*'] = segments.slice(wildcardIdx).join('/')
|
|
545
|
-
return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params }
|
|
546
|
-
}
|
|
547
|
-
return null
|
|
548
|
-
}
|
|
549
|
-
node = next
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
pathMws.push(...node.pathMws)
|
|
553
|
-
|
|
554
|
-
const handler = node.handlers.get(method) || node.handlers.get('*')
|
|
555
|
-
if (handler) {
|
|
556
|
-
if (node.wildcard) params['*'] = segments.slice(segments.length).join('/')
|
|
557
|
-
return {
|
|
558
|
-
handler,
|
|
559
|
-
middlewares: node.middlewares.get(method) || node.middlewares.get('*') || [],
|
|
560
|
-
pathMws,
|
|
561
|
-
params,
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (wildcardHandler) {
|
|
566
|
-
params['*'] = segments.slice(wildcardIdx).join('/')
|
|
567
|
-
return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params }
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (node.handlers.size > 0) {
|
|
571
|
-
return {
|
|
572
|
-
middlewares: [],
|
|
573
|
-
pathMws,
|
|
574
|
-
params,
|
|
575
|
-
allowedMethods: [...node.handlers.keys()].filter((k) => k !== '*'),
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
return null
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
private matchWsTrie(root: WsTrieNode, segments: string[]): WsMatchResult {
|
|
583
|
-
let node = root
|
|
584
|
-
const params: Record<string, string> = {}
|
|
585
|
-
|
|
586
|
-
for (const segment of segments) {
|
|
587
|
-
const next = matchChild(node, segment, params, true)
|
|
588
|
-
if (!next) return null
|
|
589
|
-
node = next
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
return node.handler ? { handler: node.handler, middlewares: node.middlewares, params } : null
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private async handleError(e: unknown, req: Request, ctx: Context): Promise<Response> {
|
|
596
|
-
const err = e instanceof Error ? e : new Error(String(e))
|
|
597
|
-
console.error(err)
|
|
598
|
-
return this.errorHandler
|
|
599
|
-
? await this.errorHandler(err, req, ctx as T)
|
|
600
|
-
: new Response('Internal Server Error', { status: 500 })
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
private async handle(req: Request, ctx: Context, segments: string[]): Promise<Response> {
|
|
604
|
-
const match = this.matchTrie(req.method, segments)
|
|
605
|
-
|
|
606
|
-
if (match) {
|
|
607
|
-
Object.assign(ctx.params, match.params)
|
|
608
|
-
|
|
609
|
-
if (match.handler) {
|
|
610
|
-
const { handler, middlewares: routeMws, pathMws } = match
|
|
611
|
-
const mws = this.mergeMws(this.mergeMws(this.globalMws, pathMws), routeMws)
|
|
612
|
-
try {
|
|
613
|
-
return await this.runChain(mws, handler, req, ctx)
|
|
614
|
-
} catch (e) {
|
|
615
|
-
return this.handleError(e, req, ctx)
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
if (match.allowedMethods && match.allowedMethods.length > 0) {
|
|
620
|
-
if (this.globalMws.length > 0) {
|
|
621
|
-
try {
|
|
622
|
-
return await this.runChain(
|
|
623
|
-
this.globalMws,
|
|
624
|
-
() =>
|
|
625
|
-
new Response('Method Not Allowed', {
|
|
626
|
-
status: 405,
|
|
627
|
-
headers: { Allow: match.allowedMethods!.join(', ') },
|
|
628
|
-
}),
|
|
629
|
-
req,
|
|
630
|
-
ctx,
|
|
631
|
-
)
|
|
632
|
-
} catch (e) {
|
|
633
|
-
return this.handleError(e, req, ctx)
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
return new Response('Method Not Allowed', {
|
|
637
|
-
status: 405,
|
|
638
|
-
headers: { Allow: match.allowedMethods.join(', ') },
|
|
639
|
-
})
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
if (this.globalMws.length > 0) {
|
|
644
|
-
try {
|
|
645
|
-
return await this.runChain(
|
|
646
|
-
this.globalMws,
|
|
647
|
-
() => {
|
|
648
|
-
if (!isProd()) {
|
|
649
|
-
return Response.json(
|
|
650
|
-
{ error: 'Not Found', path: '/' + segments.join('/'), method: req.method },
|
|
651
|
-
{ status: 404 },
|
|
652
|
-
)
|
|
653
|
-
}
|
|
654
|
-
return new Response('Not Found', { status: 404 })
|
|
655
|
-
},
|
|
656
|
-
req,
|
|
657
|
-
ctx,
|
|
658
|
-
)
|
|
659
|
-
} catch (e) {
|
|
660
|
-
return this.handleError(e, req, ctx)
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (!isProd()) {
|
|
665
|
-
return Response.json(
|
|
666
|
-
{
|
|
667
|
-
error: 'Not Found',
|
|
668
|
-
path: '/' + segments.join('/'),
|
|
669
|
-
method: req.method,
|
|
670
|
-
},
|
|
671
|
-
{ status: 404 },
|
|
672
|
-
)
|
|
673
|
-
}
|
|
674
|
-
return new Response('Not Found', { status: 404 })
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
private async runChain(
|
|
678
|
-
middlewares: Middleware[],
|
|
679
|
-
finalHandler: Handler,
|
|
680
|
-
req: Request,
|
|
681
|
-
ctx: Context,
|
|
682
|
-
): Promise<Response> {
|
|
683
|
-
if (middlewares.length === 0) return await finalHandler(req, ctx)
|
|
684
|
-
return await runChainLoop(middlewares, 0, finalHandler, req, ctx)
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function runChainLoop(
|
|
689
|
-
middlewares: Middleware[],
|
|
690
|
-
index: number,
|
|
691
|
-
finalHandler: Handler,
|
|
692
|
-
req: Request,
|
|
693
|
-
ctx: Context,
|
|
694
|
-
): Promise<Response> {
|
|
695
|
-
if (index < middlewares.length) {
|
|
696
|
-
const mw = middlewares[index]
|
|
697
|
-
let called = false
|
|
698
|
-
const dispatch: Handler = (r, c) => {
|
|
699
|
-
if (called) {
|
|
700
|
-
console.warn(
|
|
701
|
-
'[router] next() called more than once in middleware — ignoring duplicate call',
|
|
702
|
-
)
|
|
703
|
-
return Promise.resolve(new Response('Internal Server Error', { status: 500 }))
|
|
704
|
-
}
|
|
705
|
-
called = true
|
|
706
|
-
return runChainLoop(middlewares, index + 1, finalHandler, r, c)
|
|
707
|
-
}
|
|
708
|
-
return Promise.resolve(mw(req, ctx, dispatch as unknown as Parameters<typeof mw>[2]))
|
|
709
|
-
}
|
|
710
|
-
return Promise.resolve(finalHandler(req, ctx))
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function upgradeSocket(
|
|
714
|
-
wss: WebSocketServer,
|
|
715
|
-
req: IncomingMessage,
|
|
716
|
-
socket: Duplex,
|
|
717
|
-
head: Buffer,
|
|
718
|
-
handler: WebSocketHandler,
|
|
719
|
-
ctx: Context,
|
|
720
|
-
hub: Hub,
|
|
721
|
-
): void {
|
|
722
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
723
|
-
// ── Per-connection ctx — cloned from upgrade ctx ─────────
|
|
724
|
-
// Each connection gets its own ctx, inheriting params/query/user/etc.
|
|
725
|
-
const connCtx: Context = { ...ctx, params: { ...ctx.params }, query: { ...ctx.query } }
|
|
726
|
-
|
|
727
|
-
// ── ctx.ws — per-connection WS helpers ───────────────────
|
|
728
|
-
const wsState: Record<string, unknown> = {}
|
|
729
|
-
connCtx.ws = {
|
|
730
|
-
get state() {
|
|
731
|
-
return wsState
|
|
732
|
-
},
|
|
733
|
-
json(data: unknown) {
|
|
734
|
-
ws.send(JSON.stringify(data))
|
|
735
|
-
},
|
|
736
|
-
join(room: string) {
|
|
737
|
-
hub.join(room, ws)
|
|
738
|
-
},
|
|
739
|
-
leave(_room: string) {
|
|
740
|
-
hub.leave(ws)
|
|
741
|
-
},
|
|
742
|
-
sendRoom(room: string, data: unknown) {
|
|
743
|
-
hub.broadcast(room, JSON.stringify(data))
|
|
744
|
-
},
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if (handler.open) {
|
|
748
|
-
handler.open(ws, connCtx)
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
ws.on('message', (data) => {
|
|
752
|
-
handler.message?.(ws, connCtx, data as string | Buffer)
|
|
753
|
-
})
|
|
754
|
-
|
|
755
|
-
ws.on('close', () => {
|
|
756
|
-
hub.leave(ws)
|
|
757
|
-
handler.close?.(ws, connCtx)
|
|
758
|
-
})
|
|
759
|
-
|
|
760
|
-
ws.on('error', (err) => {
|
|
761
|
-
handler.error?.(ws, connCtx, err)
|
|
762
|
-
})
|
|
763
|
-
})
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function nodeReqHeadersToRecord(headers: http.IncomingHttpHeaders): Record<string, string> {
|
|
767
|
-
const result: Record<string, string> = {}
|
|
768
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
769
|
-
if (v !== undefined) result[k] = Array.isArray(v) ? v.join(', ') : v
|
|
770
|
-
}
|
|
771
|
-
return result
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function sendHttpResponseOnSocket(socket: Duplex, response: Response): void {
|
|
775
|
-
const statusLine = `HTTP/1.1 ${response.status} ${response.statusText}`
|
|
776
|
-
const headerLines: string[] = [statusLine]
|
|
777
|
-
response.headers.forEach((value, key) => {
|
|
778
|
-
headerLines.push(`${key}: ${value}`)
|
|
779
|
-
})
|
|
780
|
-
headerLines.push('Connection: close')
|
|
781
|
-
headerLines.push('')
|
|
782
|
-
const headerStr = headerLines.join('\r\n')
|
|
783
|
-
|
|
784
|
-
response
|
|
785
|
-
.arrayBuffer()
|
|
786
|
-
.then((buf) => {
|
|
787
|
-
socket.write(headerStr + '\r\n')
|
|
788
|
-
if (buf.byteLength > 0) socket.write(Buffer.from(buf))
|
|
789
|
-
socket.end()
|
|
790
|
-
})
|
|
791
|
-
.catch(() => {
|
|
792
|
-
socket.write(headerStr + '\r\n')
|
|
793
|
-
socket.end()
|
|
794
|
-
})
|
|
795
|
-
}
|