weifuwu 0.30.0 → 0.30.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/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  Web-standard HTTP microframework for Node.js — `(req, ctx) => Response`.
4
4
 
5
- ## Usage
6
-
7
5
  ```bash
8
6
  npm install weifuwu
9
7
  ```
@@ -18,35 +16,24 @@ serve(app, { port: 3000 })
18
16
 
19
17
  ## Exports
20
18
 
21
- Everything is exported from `'weifuwu'`:
22
-
23
19
  ### Core
24
20
 
25
- | Export | Kind | Description |
26
- |---|---|---|
27
- | `serve(app, opts?)` | function | Start HTTP server. Takes a `Router`. Returns `Server`. |
28
- | `Router` | class | Trie-based HTTP router with WebSocket support. |
29
- | `loadEnv(path?)` | function | Load `.env` file into `process.env`. |
30
- | `isDev()` | function | `true` unless `NODE_ENV` is `production` or `test`. |
31
- | `isProd()` | function | `true` if `NODE_ENV` is `production`. |
32
- | `isBundled()` | function | `true` when running the esbuild bundle (checks `__WFW_BUNDLED__`). |
33
- | `getPublicEnv()` | function | Returns all `process.env` keys prefixed with `WEIFUWU_PUBLIC_`. |
34
- | `DEFAULT_MAX_BODY` | const | `10 * 1024 * 1024` (10MB). |
21
+ | Export | Description |
22
+ |---|---|
23
+ | `serve(app, opts?)` | Start HTTP server. Returns `Server` with `port`, `hostname`, `ready`, `close()`. |
24
+ | `Router` | Trie-based HTTP router with WebSocket support. |
25
+ | `HttpError` | `new HttpError(message, status)`. Throw to return that status code. |
26
+ | `DEFAULT_MAX_BODY` | `10 * 1024 * 1024` (10MB). |
35
27
 
36
- ### Router
28
+ ### Router API
37
29
 
38
30
  ```ts
39
31
  const app = new Router()
40
32
 
41
33
  // HTTP methods
42
34
  app.get(path, ...handlers)
43
- app.post(path, ...handlers)
44
- app.put(path, ...handlers)
45
- app.delete(path, ...handlers)
46
- app.patch(path, ...handlers)
47
- app.head(path, ...handlers)
48
- app.options(path, ...handlers)
49
- app.all(path, ...handlers) // match any method
35
+ app.post / put / delete / patch / head / options(path, ...handlers)
36
+ app.all(path, ...handlers) // any method
50
37
 
51
38
  // WebSocket
52
39
  app.ws(path, ...middlewares, handler)
@@ -54,183 +41,145 @@ app.ws(path, ...middlewares, handler)
54
41
 
55
42
  // Middleware & mounting
56
43
  app.use(middleware) // global middleware
57
- app.mount(prefix, router) // mount sub-router at prefix
58
- app.onError(handler) // global error handler
59
- app.routes() // debug: string[] of all registered routes
44
+ app.mount(prefix, router) // sub-router at prefix
45
+ app.onError(handler) // error handler: (error, req, ctx) => Response
46
+ app.routes() // debug: list all registered routes
60
47
 
61
48
  // Path params
62
49
  app.get('/users/:id', (req, ctx) => {
63
- const id = ctx.params.id // string
64
- const q = ctx.query.search // from ?search=...
50
+ ctx.params.id // string
51
+ ctx.query.search // from ?search=...
65
52
  })
66
53
  ```
67
54
 
68
55
  ### Middleware
69
56
 
70
- | Export | Kind | Description |
71
- |---|---|---|
72
- | `cors(opts?)` | middleware | CORS headers. `origin`, `methods`, `headers`, `credentials`, `maxAge`. |
73
- | `helmet(opts?)` | middleware | Security headers (CSP, HSTS, X-Frame-Options, etc). |
74
- | `compress(opts?)` | middleware | gzip / brotli / deflate response compression. |
75
- | `rateLimit(opts?)` | middleware | Sliding-window rate limiter. `windowMs`, `max`, `keyGenerator`. |
76
- | `requestId(opts?)` | middleware | Generates `X-Request-ID` header. `header`, `generator`. |
77
- | `logger(opts?)` | middleware | Structured request logging. `format`, `level`, `stream`. |
78
- | `validate(schemas?)` | middleware | Zod-based request validation. `{ body?, query?, params?, headers? }`. |
79
- | `upload(opts?)` | middleware | Multipart file upload. `maxSize`, `dest`, `preservePath`. |
80
- | `health(opts?)` | function→Router | Health check router. Mount at `/_health` or custom path. |
81
- | `env()` | middleware | Injects `WEIFUWU_PUBLIC_*` env vars into `ctx.env`. |
82
- | `trace(opts?)` | middleware | Injects trace context into `ctx.trace` (requestId, traceId, startTime, elapsed). |
83
-
84
- ### Route-level tools
85
-
86
- | Export | Kind | Description |
87
- |---|---|---|
88
- | `serveStatic(root, opts?)` | handler | Serve static files. `cacheControl`, `index`, `dotfiles`. |
89
- | `graphql(options)` | function→Router | GraphQL endpoint. Pass a `GraphQLHandler` function `({ query, variables, ctx })`. |
90
-
91
- ### HTML rendering
92
-
93
- | Export | Kind | Description |
94
- |---|---|---|
95
- | `html(template, ...values)` | tagged template | Auto-escaped HTML builder. Signal/Computed-aware. |
96
- | `raw(content)` | function | Raw unescaped HTML string (use with caution). |
97
-
98
- ### SSE (Server-Sent Events)
99
-
100
- | Export | Kind | Description |
101
- |---|---|---|
102
- | `createSSEStream()` | function | Create an SSE stream controller for `text/event-stream` responses. |
103
- | `formatSSE(event, data)` | function | Format a single SSE event string. |
104
- | `formatSSEData(data)` | function | Format SSE data string. |
105
-
106
- ### Cookies
107
-
108
- | Export | Kind | Description |
109
- |---|---|---|
110
- | `getCookies(req)` | function | Parse cookies from `Request`. Returns `Record<string, string>`. |
111
- | `setCookie(headers, name, value, opts?)` | function | Set `Set-Cookie` header. `path`, `domain`, `maxAge`, `httpOnly`, `secure`, `sameSite`, `partitioned`. |
112
- | `deleteCookie(headers, name, opts?)` | function | Delete a cookie (Max-Age=0). |
57
+ | Export | Description |
58
+ |---|---|
59
+ | `cors(opts?)` | CORS headers. `origin`, `methods`, `headers`, `credentials`, `maxAge`. |
60
+ | `helmet(opts?)` | Security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc. |
61
+ | `compress(opts?)` | gzip / brotli / deflate response compression. |
62
+ | `rateLimit(opts?)` | Sliding-window rate limiter. `windowMs`, `max`, `keyGenerator`, Redis backend. |
63
+ | `logger(opts?)` | Request logging. `format: 'short' | 'combined' | 'json'`. |
64
+ | `upload(opts?)` | Multipart file upload via `req.formData()`. Injects `ctx.parsed`. |
65
+ | `serveStatic(root, opts?)` | Static file handler. `cacheControl`, `index`. |
66
+ | `trace(opts?)` | Injects `ctx.trace = { requestId, traceId, elapsed(), startTime }`. |
113
67
 
114
- ### Postgres
68
+ ### Tracing
115
69
 
116
- | Export | Kind | Description |
117
- |---|---|---|
118
- | `postgres(opts?)` | middleware+client | Create a Postgres client. `url`, `host`, `port`, `database`, `user`, `password`, `max`. |
119
- | `MIGRATIONS_TABLE` | const | Migration table name: `_weifuwu_migrations`. |
70
+ | Export | Description |
71
+ |---|---|
72
+ | `currentTraceId()` | Current request's trace ID (AsyncLocalStorage). |
73
+ | `currentTrace()` | Current trace context `{ traceId, startTime }`. |
74
+ | `runWithTrace(id, fn)` | Run `fn` in a trace context. Auto-generates UUID if id is null. |
75
+ | `traceElapsed()` | Milliseconds since trace start. |
76
+ | `trace(opts?)` | Middleware that injects `ctx.trace`. |
77
+
78
+ ### Postgres
120
79
 
121
80
  ```ts
81
+ import { postgres, MIGRATIONS_TABLE } from 'weifuwu'
82
+
122
83
  const sql = postgres({ url: process.env.DATABASE_URL })
84
+ app.use(sql) // injects ctx.sql
123
85
 
124
- // As middleware injects ctx.sql
125
- app.use(sql)
126
- // As query client
127
- const rows = await sql.client`SELECT * FROM users WHERE id = ${id}`
86
+ const rows = await sql.sql`SELECT * FROM users WHERE id = ${id}`
128
87
 
129
88
  // Migrations
130
89
  await sql.migrate({
131
- '001_init': async (s) => {
132
- await s`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
133
- },
90
+ '001_init': async (s) => await s`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`,
134
91
  })
135
92
  ```
136
93
 
137
94
  ### Redis
138
95
 
139
- | Export | Kind | Description |
140
- |---|---|---|
141
- | `redis(opts?)` | middleware+client | Create a Redis client (ioredis). `url`, `host`, `port`, `password`, `db`. |
142
-
143
96
  ```ts
97
+ import { redis } from 'weifuwu'
98
+
144
99
  const r = redis({ url: process.env.REDIS_URL })
145
100
  app.use(r) // injects ctx.redis
146
- await r.client.set('key', 'value')
101
+ await r.redis.set('key', 'value')
147
102
  ```
148
103
 
149
104
  ### Queue & Cron
150
105
 
151
- | Export | Kind | Description |
152
- |---|---|---|
153
- | `queue(opts?)` | middleware+instance | In-process job queue with cron support. `concurrency`. |
154
-
155
106
  ```ts
156
- const q = queue({ concurrency: 3 })
107
+ import { queue } from 'weifuwu'
108
+
109
+ const q = queue()
157
110
  app.use(q) // injects ctx.queue
158
111
 
159
- q.process('send-email', async (job) => {
160
- await sendEmail(job.data.to)
112
+ q.process('email', async (job) => {
113
+ await sendEmail(job.payload.to)
161
114
  })
162
115
 
163
116
  q.cron('cleanup', '0 3 * * *', async () => {
164
117
  await cleanupOldSessions()
165
118
  })
166
119
 
167
- await q.add('send-email', { to: 'user@example.com' })
120
+ await q.add('email', { to: 'user@example.com' })
121
+ await q.add('remind', {}, { delay: 60_000 })
122
+ await q.add('report', {}, { schedule: '0 9 * * 1' })
123
+
124
+ q.run() // start processing
168
125
  ```
169
126
 
170
127
  ### WebSocket Hub
171
128
 
172
- | Export | Kind | Description |
173
- |---|---|---|
174
- | `createHub(opts?)` | function | Pub/Sub hub for cross-connection broadcast. Supports Redis adapter for multi-process. |
175
-
176
129
  ```ts
130
+ import { createHub } from 'weifuwu'
131
+
177
132
  const hub = createHub()
178
133
  app.ws('/chat', {
179
134
  open(ws, ctx) {
180
135
  ctx.ws.join('lobby')
181
- ctx.ws.json({ type: 'join', room: 'lobby' })
136
+ ctx.ws.json({ type: 'join' })
182
137
  },
183
138
  message(ws, ctx, data) {
184
- ctx.ws.sendRoom('lobby', { type: 'chat', text: data.toString() })
139
+ ctx.ws.sendRoom('lobby', { text: data.toString() })
185
140
  },
186
141
  })
187
142
  ```
188
143
 
189
- ### Theme & I18n
190
-
191
- | Export | Kind | Description |
192
- |---|---|---|
193
- | `theme(opts?)` | middleware+Router | Theme middleware. Injects `ctx.theme` with `get()`, `set(theme)`, `toggle()`. Routes: `/__theme/:theme`. |
194
- | `i18n(opts?)` | middleware+Router | I18n middleware. Injects `ctx.i18n` with `locale`, `t(key, params?)`, `set(locale)`. `dir` for locale JSON files. Routes: `/__lang/:locale`. |
195
- | `flash()` | middleware | Flash messages. Injects `ctx.flash` with `set(msg)`, `get()`. One-time messages across redirects. |
196
- | `csrf()` | middleware | CSRF protection. Injects `ctx.csrf` with `token`. Auto-validates on `POST`/`PUT`/`PATCH`/`DELETE`. |
144
+ ### GraphQL
197
145
 
198
- ### Tracing
199
-
200
- | Export | Kind | Description |
201
- |---|---|---|
202
- | `currentTraceId()` | function | Get current request's trace ID (from AsyncLocalStorage). |
203
- | `currentTrace()` | function | Get current trace context `{ traceId, startTime }`. |
204
- | `runWithTrace(id, fn)` | function | Run `fn` with a trace context. Auto-generates ID if null. |
205
- | `traceElapsed()` | function | Milliseconds since trace start. |
206
-
207
- ### Error handling
208
-
209
- | Export | Kind | Description |
210
- |---|---|---|
211
- | `HttpError` | class | `new HttpError(message, status)`. Throw to return that status. |
212
- | `app.onError(handler)` | Router method | Global error handler: `(error, req, ctx) => Response`. |
213
-
214
- ### Testing
146
+ ```ts
147
+ import { graphql } from 'weifuwu'
148
+
149
+ const gql = graphql((req, ctx) => ({
150
+ schema: `
151
+ type Query {
152
+ hello: String
153
+ user(id: ID!): User
154
+ }
155
+ type User {
156
+ id: ID
157
+ name: String
158
+ }
159
+ `,
160
+ resolvers: {
161
+ Query: {
162
+ hello: () => 'world',
163
+ user: (_, { id }) => ctx.sql`SELECT * FROM users WHERE id = ${id}`.then(r => r[0]),
164
+ },
165
+ },
166
+ graphiql: true,
167
+ }))
215
168
 
216
- | Export | Kind | Description |
217
- |---|---|---|
218
- | `testApp(app)` | function | Create a test client for a Router. Returns `{ get, post, put, delete, patch }` for `(path, opts?)`. |
219
- | `createTestServer(app, opts?)` | function | Start a test server on a random port. Returns `{ server, url }`. |
220
- | `createTestDb()` | function | Create an ephemeral Postgres test database. |
221
- | `withTestDb(fn)` | function | Run `fn(sql)` with a temp database, auto-cleanup. |
169
+ app.mount('/graphql', gql)
170
+ ```
222
171
 
223
172
  ### Types
224
173
 
225
174
  | Export | Description |
226
175
  |---|---|
227
- | `Context` | `{ params, query, mountPath?, user?, loaderData?, env?, [key: string] }` |
176
+ | `Context` | `{ params, query, mountPath?, [key: string] }` |
228
177
  | `Handler<T>` | `(req: Request, ctx: T) => Response \| Promise<Response>` |
229
- | `Middleware<In, Out>` | `(req, ctx, next) => Response \| Promise<Response>` |
178
+ | `Middleware` | `(req, ctx, next) => Response \| Promise<Response>` |
230
179
  | `WebSocketHandler` | `{ open?, message?, close?, error? }` |
231
180
  | `HttpError` | `Error` subclass with `.status: number` |
232
181
  | `Closeable` | `{ close(): Promise<void> }` |
233
- | `Server` | `{ stop, close, port, hostname, ready }` |
182
+ | `Server` | `{ close, port, hostname, ready }` |
234
183
  | `ServeOptions` | `{ port, hostname, signal, maxBodySize, timeout, keepAliveTimeout, headersTimeout, shutdown }` |
235
184
 
236
185
  ## Handler / Middleware patterns
@@ -238,14 +187,13 @@ app.ws('/chat', {
238
187
  ```ts
239
188
  // Handler — standard Web API
240
189
  app.get('/api/data', (req, ctx) => {
241
- const { q } = ctx.query
242
- const id = ctx.params.id
243
- return Response.json({ q, id })
190
+ const q = ctx.query.q
191
+ return Response.json({ q })
244
192
  })
245
193
 
246
194
  // Async
247
195
  app.get('/db/users', async (req, ctx) => {
248
- const rows = await sql`SELECT * FROM users`
196
+ const rows = await ctx.sql`SELECT * FROM users`
249
197
  return Response.json(rows)
250
198
  })
251
199
 
@@ -264,15 +212,11 @@ app.use(async (req, ctx, next) => {
264
212
  return res
265
213
  })
266
214
 
267
- // Route-level middleware (before handler)
268
- app.get('/admin', validate(bodySchema), authCheck, adminHandler)
269
-
270
215
  // Error handler
271
216
  app.onError((err, req, ctx) => {
272
217
  if (err instanceof HttpError) {
273
218
  return Response.json({ error: err.message }, { status: err.status })
274
219
  }
275
- console.error(err)
276
220
  return new Response('Internal error', { status: 500 })
277
221
  })
278
222
  ```
@@ -280,85 +224,52 @@ app.onError((err, req, ctx) => {
280
224
  ## Complete example
281
225
 
282
226
  ```ts
283
- import { serve, Router, loadEnv, cors, helmet, compress, logger, requestId, rateLimit, postgres, redis, queue, HttpError, html } from 'weifuwu'
284
-
285
- loadEnv()
227
+ import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, queue, HttpError } from 'weifuwu'
286
228
 
287
229
  const app = new Router()
288
230
 
289
231
  // Global middleware
290
- app.use(requestId())
232
+ app.use(trace())
291
233
  app.use(logger())
292
234
  app.use(cors())
293
235
  app.use(helmet())
294
236
  app.use(compress())
295
237
  app.use(rateLimit({ windowMs: 60_000, max: 100 }))
296
238
 
297
- // Database & services
298
- const sql = postgres({ url: process.env.DATABASE_URL })
299
- const cache = redis({ url: process.env.REDIS_URL })
300
- const jobs = queue()
301
-
239
+ // Database
240
+ const sql = postgres()
302
241
  app.use(sql)
303
- app.use(cache)
304
- app.use(jobs)
305
242
 
306
243
  // Routes
307
- app.get('/', () => html`<h1>weifuwu</h1><p>Running on ${process.version}</p>`)
308
-
309
244
  app.get('/api/users', async (req, ctx) => {
310
- const users = await ctx.sql`SELECT id, name, email FROM users ORDER BY id`
245
+ const users = await ctx.sql`SELECT id, name FROM users ORDER BY id`
311
246
  return Response.json(users)
312
247
  })
313
248
 
314
- app.get('/api/users/:id', async (req, ctx) => {
315
- const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
316
- if (!user) throw new HttpError('User not found', 404)
317
- return Response.json(user)
318
- })
319
-
320
249
  app.post('/api/users', async (req, ctx) => {
321
250
  const { name, email } = await req.json()
322
- const [user] = await ctx.sql`INSERT INTO users (name, email) VALUES (${name}, ${email}) RETURNING *`
251
+ const [user] = await ctx.sql`INSERT INTO users (name,email) VALUES (${name},${email}) RETURNING *`
323
252
  return Response.json(user, { status: 201 })
324
253
  })
325
254
 
326
- // Background job
327
- jobs.process('welcome-email', async (job) => {
328
- const { userId } = job.data
329
- const [user] = await sql.client`SELECT * FROM users WHERE id = ${userId}`
330
- await sendWelcomeEmail(user.email)
331
- })
332
-
333
- app.post('/api/register', async (req, ctx) => {
334
- const { name, email } = await req.json()
335
- const [user] = await ctx.sql`INSERT INTO users (name, email) VALUES (${name}, ${email}) RETURNING *`
336
- await ctx.queue.add('welcome-email', { userId: user.id })
337
- return Response.json(user, { status: 201 })
255
+ app.get('/api/users/:id', async (req, ctx) => {
256
+ const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
257
+ if (!user) throw new HttpError('Not found', 404)
258
+ return Response.json(user)
338
259
  })
339
260
 
340
- // WebSocket chat
261
+ // WebSocket
341
262
  app.ws('/chat', {
342
- open(ws, ctx) {
343
- ctx.ws.join('lobby')
344
- },
345
- message(ws, ctx, data) {
346
- ctx.ws.sendRoom('lobby', { text: data.toString() })
347
- },
263
+ open(ws, ctx) { ctx.ws.join('lobby') },
264
+ message(ws, ctx, data) { ctx.ws.sendRoom('lobby', { text: data.toString() }) },
348
265
  })
349
266
 
350
267
  // Error handling
351
- app.onError((err, req, ctx) => {
352
- if (err instanceof HttpError) {
353
- return Response.json({ error: err.message }, { status: err.status })
354
- }
268
+ app.onError((err) => {
269
+ if (err instanceof HttpError) return Response.json({ error: err.message }, { status: err.status })
355
270
  return Response.json({ error: 'Internal error' }, { status: 500 })
356
271
  })
357
272
 
358
- // Health check
359
- import { health } from 'weifuwu'
360
- app.mount('/_health', health())
361
-
362
273
  serve(app, { port: 3000 })
363
274
  ```
364
275
 
@@ -368,27 +279,28 @@ serve(app, { port: 3000 })
368
279
  weifuwu/
369
280
  ├── package.json
370
281
  ├── tsconfig.json
282
+ ├── docker-compose.yml ← postgres + redis for tests
371
283
  ├── scripts/
372
284
  │ ├── build.mjs
373
285
  │ └── release.mjs
374
286
  ├── src/
375
- │ ├── index.ts ← all exports
376
- │ ├── types.ts ← Context, Handler, Middleware, HttpError
377
- ├── core/ ← serve, router, trace, env, logger, cookie, sse, html
378
- │ ├── middleware/ ← cors, compress, helmet, rate-limit, static, validate, upload, health, theme, i18n, flash, csrf, request-id
379
- ├── postgres/ ← postgres client, migrations, module injection
380
- │ ├── redis/ ← redis client
381
- │ ├── queue/ job queue, cron
382
- │ ├── graphql.ts ← schema-first GraphQL handler
383
- │ ├── hub.ts ← pub/sub hub (WebSocket rooms)
384
- │ └── test/ 324 tests
287
+ │ ├── index.ts
288
+ │ ├── types.ts
289
+ ├── core/ ← serve, router, ws, trace, logger
290
+ │ ├── middleware/ ← cors, helmet, compress, rate-limit, static, upload
291
+ ├── postgres/
292
+ │ ├── redis/
293
+ │ ├── queue/ cron, index, types
294
+ │ ├── graphql.ts
295
+ │ ├── hub.ts
296
+ │ └── test/ 122 tests (18 files)
385
297
  └── dist/
386
298
  ```
387
299
 
388
300
  ## Development
389
301
 
390
302
  ```bash
391
- npm run build # esbuild bundle → dist/index.js
303
+ npm run build # esbuild → dist/index.js
392
304
  npm run typecheck # tsc --noEmit
393
- npm test # node --test (324 tests, 0 failures)
305
+ npm test # 122 tests (requires docker compose)
394
306
  ```
@@ -1,30 +1,17 @@
1
- import type { WebSocket, Context, Handler, Middleware, ErrorHandler } from '../types.ts';
2
- import { type IncomingMessage } from 'node:http';
3
- import type { Duplex } from 'node:stream';
1
+ import type { Context, Handler, Middleware, ErrorHandler } from '../types.ts';
4
2
  import { type Hub } from '../hub.ts';
3
+ import { type WebSocketHandler, type WsUpgradeHandler } from './ws.ts';
5
4
  declare module '../types.ts' {
6
5
  interface Context {
7
6
  ws: {
8
- /** Per-connection state object */
9
7
  state: Record<string, unknown>;
10
- /** Send JSON to this connection */
11
8
  json(data: unknown): void;
12
- /** Join a room */
13
9
  join(room: string): void;
14
- /** Leave a room */
15
10
  leave(room: string): void;
16
- /** Broadcast to a room */
17
11
  sendRoom(room: string, data: unknown): void;
18
12
  };
19
13
  }
20
14
  }
21
- export type WebSocketHandler = {
22
- open?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
23
- message?: (ws: WebSocket, ctx: Context, data: string | Buffer) => void | Promise<void>;
24
- close?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
25
- error?: (ws: WebSocket, ctx: Context, error: Error) => void | Promise<void>;
26
- };
27
- type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
28
15
  export declare class Router<T extends Context = Context> {
29
16
  private root;
30
17
  private wsRoot;
@@ -33,63 +20,39 @@ export declare class Router<T extends Context = Context> {
33
20
  private _hasWildcard;
34
21
  private _hub?;
35
22
  private _wss?;
36
- /** Track which ctx fields have been injected so far (for dependency checking). */
37
23
  private _ctxFields;
38
24
  private get wss();
39
25
  private get hub();
40
- /** Inject a custom hub (e.g. with Redis for cross-process broadcast). */
41
26
  wsHub(hub: Hub): this;
42
- /** Global middleware — accumulates types into Router<T>. */
43
- use<Out extends Context>(mw: Middleware<Context, Out>): Router<T & Out>;
44
- /**
45
- * Mount a sub-router at the given path prefix.
46
- * All routes from the sub-router are registered with the prefix.
47
- *
48
- * ```ts
49
- * const admin = new Router()
50
- * admin.get('/dashboard', handler)
51
- * app.mount('/admin', admin) // → GET /admin/dashboard
52
- * ```
53
- */
27
+ use(mw: Middleware<Context, Context>): Router<T>;
54
28
  mount(path: string, router: Router<Context>): Router<T>;
55
- /**
56
- * Check a middleware's dependency metadata and emit warnings if
57
- * required fields haven't been injected yet.
58
- * Attach __meta to a middleware function:
59
- *
60
- * ```ts
61
- * mw.__meta = { injects: ['sql'], depends: ['session'] }
62
- * ```
63
- */
64
- private _checkMiddlewareMeta;
65
- get(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
66
- post(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
67
- put(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
68
- delete(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
69
- patch(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
70
- head(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
71
- options(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
72
- all(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
73
29
  onError(handler: ErrorHandler<T>): Router<T>;
74
- private _route;
75
- /** Internal route registration no type constraints (used by _mountRouter). */
76
- private _routeImpl;
77
- ws(path: string, ...args: [...Middleware<T, T>[], WebSocketHandler]): Router<T>;
30
+ get(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
31
+ post(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
32
+ put(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
33
+ delete(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
34
+ patch(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
35
+ head(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
36
+ options(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
37
+ all(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
38
+ ws(path: string, ...args: [...Middleware[], WebSocketHandler]): Router<T>;
78
39
  handler(): Handler<T>;
79
- /** Returns a human-readable list of all registered routes. Useful for debugging. */
80
- routes(): string[];
81
- private _collectRoutes;
82
- private _collectWsRoutes;
83
40
  websocketHandler(): WsUpgradeHandler;
41
+ routes(): string[];
42
+ private _route;
43
+ private _routeImpl;
84
44
  private _mountRouter;
85
45
  private _collect;
86
46
  private _collectWs;
87
47
  private splitPath;
48
+ private _collectRoutes;
49
+ private _collectWsRoutes;
88
50
  private matchTrie;
51
+ private _wildcardMatch;
52
+ private _resolveMatch;
89
53
  private matchWsTrie;
90
- private handleError;
91
- private _notFoundResponse;
92
54
  private handle;
55
+ private handleError;
93
56
  private runChain;
57
+ private _checkMiddlewareMeta;
94
58
  }
95
- export {};
@@ -29,8 +29,4 @@ export declare function createRequest(req: IncomingMessage, body: Buffer): [Requ
29
29
  export declare function sendResponse(res: ServerResponse, response: Response, opts?: {
30
30
  traceId?: string | null;
31
31
  }): Promise<void>;
32
- export declare function createTestServer(router: Router, options?: ServeOptions): Promise<{
33
- server: Server;
34
- url: string;
35
- }>;
36
32
  export declare function serve(router: Router, options?: ServeOptions): Server;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * WebSocket upgrade + connection lifecycle.
3
+ *
4
+ * Handles the HTTP-to-WS upgrade and per-connection state.
5
+ * Used internally by Router — not exported to end users.
6
+ */
7
+ import { WebSocketServer } from 'ws';
8
+ import type { WebSocket, Context, Handler, Middleware } from '../types.ts';
9
+ import type { IncomingMessage } from 'node:http';
10
+ import type { Duplex } from 'node:stream';
11
+ import { type Hub } from '../hub.ts';
12
+ /** WebSocket lifecycle handler. */
13
+ export type WebSocketHandler = {
14
+ open?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
15
+ message?: (ws: WebSocket, ctx: Context, data: string | Buffer) => void | Promise<void>;
16
+ close?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
17
+ error?: (ws: WebSocket, ctx: Context, error: Error) => void | Promise<void>;
18
+ };
19
+ export type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
20
+ export declare function nodeReqHeadersToRecord(headers: IncomingMessage['headers']): Record<string, string>;
21
+ export declare function upgradeSocket(wss: WebSocketServer, req: IncomingMessage, socket: Duplex, head: Buffer, handler: WebSocketHandler, ctx: Context, hub: Hub): void;
22
+ type WsMatch = {
23
+ handler: WebSocketHandler;
24
+ middlewares: Middleware[];
25
+ params: Record<string, string>;
26
+ };
27
+ export declare function createWsUpgradeHandler(wss: WebSocketServer, hub: Hub, matchWsFn: (segments: string[]) => WsMatch | null, globalMws: Middleware[], runChainFn: (mws: Middleware[], h: Handler, req: Request, ctx: Context) => Promise<Response>): WsUpgradeHandler;
28
+ export {};