@xrystal/dto 26.6.27-v13 → 26.6.27-v14

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.
Files changed (74) hide show
  1. package/README.md +273 -38
  2. package/dist/adapters/elysia.cjs +251 -225
  3. package/dist/adapters/elysia.js +250 -226
  4. package/dist/adapters/express.cjs +237 -223
  5. package/dist/adapters/express.js +237 -224
  6. package/dist/adapters/fetch.cjs +832 -0
  7. package/dist/adapters/fetch.js +826 -0
  8. package/dist/adapters/node.cjs +257 -224
  9. package/dist/adapters/node.js +257 -225
  10. package/dist/adapters/socketio.cjs +204 -193
  11. package/dist/adapters/socketio.js +204 -194
  12. package/dist/adapters/websocket.cjs +207 -193
  13. package/dist/adapters/websocket.js +207 -194
  14. package/dist/dto/adapters/bind.d.ts +40 -0
  15. package/dist/dto/adapters/elysia.d.ts +1 -0
  16. package/dist/dto/adapters/express.d.ts +1 -0
  17. package/dist/dto/adapters/fetch.d.ts +1 -0
  18. package/dist/dto/adapters/http.d.ts +231 -0
  19. package/dist/dto/adapters/node.d.ts +1 -0
  20. package/dist/dto/adapters/realtime.d.ts +53 -0
  21. package/dist/dto/adapters/socketio.d.ts +1 -0
  22. package/dist/dto/adapters/websocket.d.ts +1 -0
  23. package/dist/dto/factories.d.ts +11 -0
  24. package/dist/dto/index.cjs +551 -499
  25. package/dist/dto/index.d.ts +4 -5
  26. package/dist/dto/index.js +536 -500
  27. package/dist/dto/log/index.cjs +20 -39
  28. package/dist/dto/log/index.js +20 -39
  29. package/dist/dto/queue/index.cjs +20 -33
  30. package/dist/dto/queue/index.js +20 -33
  31. package/dist/dto/response/index.cjs +232 -78
  32. package/dist/dto/response/index.js +218 -79
  33. package/dist/dto/runtime/index.cjs +523 -486
  34. package/dist/dto/runtime/index.d.ts +71 -4
  35. package/dist/dto/runtime/index.js +523 -487
  36. package/dist/dto/{response → send}/builder.d.ts +3 -2
  37. package/dist/dto/send/index.d.ts +7 -0
  38. package/dist/dto/{response → send}/presets.d.ts +23 -19
  39. package/dist/dto/send/transport.d.ts +50 -0
  40. package/dist/dto/{log/builder.d.ts → shapes/log.d.ts} +28 -12
  41. package/dist/dto/{queue/builder.d.ts → shapes/queue.d.ts} +36 -9
  42. package/dist/dto/{response/dto.d.ts → shapes/response.d.ts} +8 -6
  43. package/dist/dto/shared.d.ts +15 -0
  44. package/dist/index.cjs +529 -420
  45. package/dist/index.d.ts +12 -12
  46. package/dist/index.js +512 -421
  47. package/dist/types/builder.d.ts +2 -1
  48. package/dist/types/log.d.ts +1 -1
  49. package/dist/types/queue.d.ts +1 -1
  50. package/package.json +6 -1
  51. package/dist/adapters/elysia.d.ts +0 -138
  52. package/dist/adapters/express.d.ts +0 -49
  53. package/dist/adapters/index.d.ts +0 -1
  54. package/dist/adapters/node.d.ts +0 -27
  55. package/dist/adapters/shared.d.ts +0 -11
  56. package/dist/adapters/socketio.d.ts +0 -31
  57. package/dist/adapters/websocket.d.ts +0 -31
  58. package/dist/dto/log/assemble.d.ts +0 -3
  59. package/dist/dto/log/index.d.ts +0 -3
  60. package/dist/dto/log/types.d.ts +0 -26
  61. package/dist/dto/queue/assemble.d.ts +0 -2
  62. package/dist/dto/queue/index.d.ts +0 -3
  63. package/dist/dto/queue/types.d.ts +0 -34
  64. package/dist/dto/response/assemble.d.ts +0 -9
  65. package/dist/dto/response/config.d.ts +0 -10
  66. package/dist/dto/response/index.d.ts +0 -7
  67. package/dist/dto/runtime/context.d.ts +0 -5
  68. package/dist/dto/runtime/dto.d.ts +0 -64
  69. package/dist/dto/runtime/hooks.d.ts +0 -8
  70. package/dist/dto/runtime/metadata.d.ts +0 -6
  71. package/dist/dto/shared/base-dto.d.ts +0 -12
  72. package/dist/dto/shared/schema-utils.d.ts +0 -1
  73. /package/dist/dto/{response → send}/plugins.d.ts +0 -0
  74. /package/dist/{enums/node-env.d.ts → utils/constants/enums.d.ts} +0 -0
package/README.md CHANGED
@@ -41,13 +41,15 @@ This attaches `req.ctx` and `res.builder` on every request.
41
41
 
42
42
  ### 2. Respond in a handler
43
43
 
44
+ Every chain ends with a **verb** — `.send()` (go to client) or `.build()` (schema only):
45
+
44
46
  ```typescript
45
47
  app.get('/users/:id', (req, res) => {
46
48
  return res.builder.as.ok(
47
49
  { id: req.params.id },
48
50
  'User fetched',
49
51
  { meta: { requestId: req.ctx.requestId } },
50
- )
52
+ ).send()
51
53
  })
52
54
  ```
53
55
 
@@ -62,7 +64,12 @@ Sent JSON (fixed order: `status → message → payload? → code`):
62
64
  }
63
65
  ```
64
66
 
65
- Presets (`.as.ok`, `.as.notFound`, …) dispatch automatically — **no `.build()`**.
67
+ | Verb | What it does |
68
+ |------|----------------|
69
+ | `.send()` | Assembles + **pushes** via send sink (HTTP write, WebSocket emit, …) |
70
+ | `.build()` | Assembles **schema only** — no network side-effect |
71
+
72
+ Presets (`.as.ok`, `.as.notFound`, …) configure the body and return a terminal — you still call `.send()` or `.build()`.
66
73
 
67
74
  ### 3. Static shape DTOs (no request)
68
75
 
@@ -102,52 +109,226 @@ const runtime = new DTO()
102
109
  new Elysia()
103
110
  .use(runtime.elysia())
104
111
  .get('/users/:id', ({ builder, requestId, params }) =>
105
- builder.as.ok({ id: params.id }, 'User fetched', { meta: { requestId } }),
112
+ builder.as.ok({ id: params.id }, 'User fetched', { meta: { requestId } }).send(),
106
113
  )
107
114
  ```
108
115
 
116
+ ### Fetch API — Workers, Hono, Bun.serve
117
+
118
+ ```typescript
119
+ import { DTO } from '@xrystal/dto'
120
+
121
+ const runtime = new DTO({ withDefaultHeaders: true })
122
+
123
+ export async function GET(request: Request) {
124
+ const { builder, requestId } = runtime.forRequest(request)
125
+ return builder.as.ok({ ok: true }, 'Healthy', { meta: { requestId } }).send()
126
+ // → Web Response
127
+ }
128
+ ```
129
+
109
130
  ### When to use what
110
131
 
111
132
  | Situation | Use |
112
133
  |-----------|-----|
113
- | Express / Node / Elysia handler | `res.builder` / `ctx.builder` |
114
- | WebSocket / Socket.IO | `ws.builder` / `socket.builder` |
134
+ | Express / Node / Elysia / Fetch handler | `builder…send()` |
135
+ | WebSocket / Socket.IO | `socket.builder…send()` |
136
+ | Schema only (log, queue, test, transform) | `builder…build()` or `DTO.*.build()` |
115
137
  | Cron, worker, logger, queue publish | `DTO.Log`, `DTO.Queue`, `DTO.Response` |
116
- | Custom HTTP status + headers | `res.builder.httpStatus(429).header(...).schema({...}).build()` |
138
+ | Custom HTTP status + headers | `builder.httpStatus(429).header(...).schema({...}).send()` |
117
139
 
118
140
  Plugins and custom `codes` on `new DTO({ … })` apply **only to `builder`** — not to static `DTO.Log` / `DTO.Queue`.
119
141
 
120
142
  ---
121
143
 
122
- ## Architecture
144
+ ## Adapters & send sinks
145
+
146
+ Each runtime has an **adapter** (wires `builder` per request) and a **send sink** (writes `FinalResult` to the transport on `.send()`).
147
+
148
+ | Piece | Role |
149
+ |-------|------|
150
+ | **Adapter** | Framework middleware / derive — creates `requestId`, attaches `builder`, applies default headers |
151
+ | **Send sink** | `{ dispatch(result) }` object — one line per runtime, delegates to `send/transport.ts` |
152
+
153
+ ```
154
+ Handler chain Core Transport
155
+ ───────────── ───── ─────────
156
+ builder.as.ok() → .build() → FinalResult
157
+ → .send() → dispatchResult() → sink.dispatch()
158
+ → send/transport.ts
159
+ → res.json / Response / ws.send / …
160
+ ```
161
+
162
+ Core dispatch lives in `dto/send/transport.ts`. Adapters pass a sink into `createBuilderRuntime()` — no separate dispatcher classes needed for normal use. `ExpressDispatcher` and friends remain as deprecated aliases for custom wiring.
163
+
164
+ ### Runtime map
165
+
166
+ | Runtime | Adapter | Sink | Attaches | `.send()` output |
167
+ |---------|---------|------|----------|------------------|
168
+ | **Express** | `ExpressAdapter` | `expressSink(res)` | `req.ctx`, `res.builder` | writes `res.status().json()` |
169
+ | **Node http** | `NodeAdapter` | `nodeSink(res)` | `req.ctx`, `res.builder` | writes `res.end(JSON)` + Set-Cookie |
170
+ | **Fetch API** | `FetchAdapter` | `fetchSink` | `builder`, `requestId` per `Request` | returns `Response` |
171
+ | **Elysia** | `ElysiaAdapter` | `elysiaSink(set)` | `builder`, `requestId`, `startTime` on ctx | mutates `set`, returns body |
172
+ | **WebSocket** | `WebSocketAdapter` | `websocketSink(socket)` | `socket.ctx`, `socket.builder` | `socket.send(wire envelope JSON)` |
173
+ | **Socket.IO** | `SocketIoAdapter` | `socketIoSink(socket, event)` | `socket.ctx`, `socket.builder` | `socket.emit(event, wire envelope)` |
174
+
175
+ WebSocket and Socket.IO send the full transport envelope (`statusCode`, `headers`, `cookies`, `body`) so `.httpStatus()` and `.header()` are not lost on realtime transports.
176
+
177
+ ### `new DTO()` shortcuts
178
+
179
+ ```typescript
180
+ const runtime = new DTO({ withDefaultHeaders: true, socket: { event: 'api:response' } })
181
+
182
+ runtime.express() // → Express middleware
183
+ runtime.node() // → Node http middleware
184
+ runtime.elysia() // → Elysia plugin
185
+ runtime.fetch() // → (request: Request) => { builder, requestId, … }
186
+ runtime.forRequest(request) // → one-shot Fetch context
187
+
188
+ runtime.websocket() // → (socket, next?) => void
189
+ runtime.attachWebSocket(ws) // → attach builder to one socket
190
+
191
+ runtime.socketIo() // → (socket, next?) => void
192
+ runtime.attachSocketIo(socket) // → attach builder to one socket
193
+ ```
194
+
195
+ ### When to import adapters directly
196
+
197
+ Most apps only need `new DTO().express()` (etc.). Import adapters / sinks when you:
198
+
199
+ - Integrate a **custom framework** — `createBuilderRuntime(expressSink(res), options, context)`
200
+ - **Unit-test** transport without full middleware
201
+ - Build an **internal wrapper** (e.g. Fastify plugin)
202
+
203
+ ```typescript
204
+ import { createBuilderRuntime, expressSink, ContextFactory } from '@xrystal/dto'
205
+
206
+ // Custom wiring example
207
+ const context = ContextFactory.create(req)
208
+ const { builder } = createBuilderRuntime(expressSink(res), options, context)
209
+ ```
210
+
211
+ Subpath imports (tree-shake friendly):
212
+
213
+ | Import | Contents |
214
+ |--------|----------|
215
+ | `@xrystal/dto/express` | `ExpressAdapter`, `ExpressDispatcher`, `expressMiddleware` |
216
+ | `@xrystal/dto/node` | `NodeAdapter`, `NodeDispatcher`, `nodeMiddleware` |
217
+ | `@xrystal/dto/fetch` | `FetchAdapter`, `FetchDispatcher`, `createFetchDerive` |
218
+ | `@xrystal/dto/elysia` | `ElysiaAdapter`, `ElysiaDispatcher`, `createElysiaDerive` |
219
+ | `@xrystal/dto/websocket` | `WebSocketAdapter`, `WebSocketDispatcher` |
220
+ | `@xrystal/dto/socketio` | `SocketIoAdapter`, `SocketIoDispatcher` |
221
+
222
+ ---
223
+
224
+ ## Core vs DTO — tek adapter yeter mi?
225
+
226
+ Kütüphanenin **iki işi** var; mimari buna göre bölündü:
123
227
 
124
- Symmetric modules under `src/dto/`:
228
+ | İş | Katman | Adapter gerekir mi? |
229
+ |----|--------|---------------------|
230
+ | **Şema üret** | `shapes/` + `shared.ts` | **Hayır** — `DTO.Log.build()`, `DTO.Response.build()`, testler |
231
+ | **Client'a gönder** | `send/` + `adapters/` | **Evet** — `.send()` bir transport'a yazmalı |
125
232
 
126
233
  ```
127
- dto/
128
- ├── runtime/ ← new DTO(), middleware, ContextFactory, Hooks
129
- │ ├── dto.ts
130
- │ ├── context.ts
131
- │ ├── metadata.ts
132
- │ └── hooks.ts
133
- ├── response/ ← ResponseDTO, Builder, presets, plugins
134
- ├── log/
135
- ├── queue/
136
- └── shared/
137
- ├── base-dto.ts
138
- └── schema-utils.ts
234
+ ┌─────────────────────────────────────────────┐
235
+ │ new DTO() — tek giriş noktası │
236
+ │ runtime.express() / .fetch() / … │
237
+ └──────────────────┬──────────────────────────┘
238
+ │
239
+ ┌──────────────────▼──────────────────────────┐
240
+ │ adapters/ — sadece runtime wiring │
241
+ │ middleware → res.builder + send sink │
242
+ └──────────────────┬──────────────────────────┘
243
+ │
244
+ ┌──────────────────▼──────────────────────────┐
245
+ │ send/ — Builder, presets, transport │
246
+ │ .build() şema | .send() client'a yazar │
247
+ └──────────────────┬──────────────────────────┘
248
+ │
249
+ ┌──────────────────▼──────────────────────────┐
250
+ │ shapes/ — ResponseDTO, LogDTO, QueueDTO │
251
+ │ field sırası, .schema() ezme kuralları │
252
+ └─────────────────────────────────────────────┘
139
253
  ```
140
254
 
255
+ ### Tek adapter bağlamak yeter mi?
256
+
257
+ | Senaryo | Ne yaparsın |
258
+ |---------|-------------|
259
+ | **Normal HTTP app (Express vb.)** | `new DTO().express()` — başka adapter import etmezsin |
260
+ | **Sadece JSON üret (log, queue, test)** | `DTO.Log` / `DTO.Queue` / `builder.build()` — adapter yok |
261
+ | **Custom framework (Fastify vb.)** | `createBuilderRuntime(expressSink(res), …)` veya kendi sink'in |
262
+ | **WS + HTTP aynı app'te** | Her transport için bir kez wire: `.express()` + `.websocket()` |
263
+
264
+ **Özet:** Handler'dan client'a cevap gönderiyorsan bir adapter (veya sink) şart — ama çoğu app'te `new DTO().express()` tek satır yeter; `ExpressAdapter` import etmezsin. Dispatcher sınıfına gerek yok; adapter içinde `expressSink(res)` gibi tek satırlık `{ dispatch }` objesi var.
265
+
266
+ | Layer | Import eder misin? | Ne yapar |
267
+ |-------|-------------------|----------|
268
+ | **shapes** | Nadiren (`@xrystal/dto/log`) | Saf JSON şema |
269
+ | **send** | Nadiren (`@xrystal/dto/response`) | Builder, transport, presets |
270
+ | **adapters** | Custom entegrasyon / subpath | `builder`'ı request'e takar |
271
+ | **DTO** | Her zaman | Config + `runtime.express()` shortcut |
272
+
273
+ ### Request lifecycle (Express)
274
+
275
+ ```
276
+ 1. Request gelir
277
+ 2. ExpressAdapter middleware:
278
+ ContextFactory → requestId
279
+ createBuilderRuntime(expressSink(res), options, ctx)
280
+ res.builder = builder
281
+ 3. Handler:
282
+ res.builder.as.ok(data).send()
283
+ → .build() → FinalResult
284
+ → .send() → transport.ts → expressSink → res.json()
285
+ ```
286
+
287
+ Şema step 2'de oluşmaz — `.build()` veya `.send()` çağrıldığında assemble edilir.
288
+
289
+ ---
290
+
291
+ ## Architecture
292
+
293
+ ```
294
+ src/dto/
295
+ ├── shared.ts BaseDTO, field rules, builder config
296
+ ├── factories.ts ContextFactory, MetadataFactory
297
+ ├── shapes/ ← görev 1: şema
298
+ │ ├── response.ts
299
+ │ ├── log.ts
300
+ │ └── queue.ts
301
+ ├── send/ ← görev 2: build + push
302
+ │ ├── builder.ts
303
+ │ ├── presets.ts
304
+ │ ├── transport.ts
305
+ │ └── plugins.ts
306
+ ├── adapters/ runtime wiring
307
+ │ ├── http.ts Express, Node, Fetch, Elysia + *Sink helpers
308
+ │ ├── realtime.ts WebSocket, Socket.IO
309
+ │ ├── bind.ts DTO.express() shortcuts
310
+ │ └── express.ts … @xrystal/dto/express subpath shims
311
+ └── runtime/
312
+ └── index.ts DTO class, Hooks, createBuilderRuntime
313
+ ```
314
+
315
+ Subpath exports (`@xrystal/dto/express`, `/response`, …) unchanged — shims map to the folders above.
316
+
141
317
  ### Package exports
142
318
 
143
319
  | Import | Contents |
144
320
  |--------|----------|
145
- | `@xrystal/dto` | Everything |
321
+ | `@xrystal/dto` | Everything (including all adapters & dispatchers) |
146
322
  | `@xrystal/dto/dto` | `DTO`, `createDTO`, runtime helpers |
147
- | `@xrystal/dto/response` | `ResponseDTO`, `Builder`, presets |
323
+ | `@xrystal/dto/response` | `ResponseDTO`, `Builder`, `PresetTerminal`, transport helpers |
148
324
  | `@xrystal/dto/log` | `LogDTO` |
149
325
  | `@xrystal/dto/queue` | `QueueDTO` |
150
- | `@xrystal/dto/express` | Express middleware |
326
+ | `@xrystal/dto/express` | Express adapter |
327
+ | `@xrystal/dto/node` | Node http adapter |
328
+ | `@xrystal/dto/fetch` | Fetch API adapter |
329
+ | `@xrystal/dto/elysia` | Elysia adapter |
330
+ | `@xrystal/dto/websocket` | WebSocket adapter |
331
+ | `@xrystal/dto/socketio` | Socket.IO adapter |
151
332
 
152
333
  ---
153
334
 
@@ -168,17 +349,20 @@ Transport (HTTP status, headers, cookies) lives on **`builder`**, not on `DTO.Re
168
349
 
169
350
  ### Presets (recommended)
170
351
 
352
+ Presets return a terminal — always end with `.send()` or `.build()`:
353
+
171
354
  ```typescript
172
- res.builder.as.ok(data, 'Done')
173
- res.builder.as.fail('Not found')
174
- res.builder.as.notFound('User missing')
175
- res.builder.as.tooManyRequests('Slow down')
355
+ return res.builder.as.ok(data, 'Done').send()
356
+ return res.builder.as.fail('Not found').send()
357
+ return res.builder.as.notFound('User missing').send()
358
+ return res.builder.as.tooManyRequests('Slow down').send()
359
+
360
+ // Headers before preset
361
+ return res.builder.header('X-Trace', traceId).as.ok(data).send()
176
362
  ```
177
363
 
178
364
  ### Manual chain
179
365
 
180
- End with `.build()` when you need headers, cookies, or fine control:
181
-
182
366
  ```typescript
183
367
  return res.builder
184
368
  .httpStatus(429)
@@ -189,7 +373,14 @@ return res.builder
189
373
  code: 4,
190
374
  payload: { data: { retryAfter: 60 } },
191
375
  })
192
- .build()
376
+ .send()
377
+ ```
378
+
379
+ Schema only (no dispatch):
380
+
381
+ ```typescript
382
+ const result = res.builder.schema({ payload: { data: user } }).build()
383
+ await auditLog.write(result.body)
193
384
  ```
194
385
 
195
386
  | Chain rule | Detail |
@@ -197,6 +388,7 @@ return res.builder
197
388
  | Last call wins | Later `.schema()` or `.status()` overrides earlier values |
198
389
  | `.schema()` is body-only | HTTP status and headers stay on the chain |
199
390
  | `payload` in `.schema()` | Replaces entire payload — no merge |
391
+ | Preset terminal | `.as.*()` exposes only `.build()` and `.send()` |
200
392
 
201
393
  ---
202
394
 
@@ -272,10 +464,10 @@ io.use(runtime.socketIo())
272
464
 
273
465
  | Option | Affects |
274
466
  |--------|---------|
275
- | `plugins` | `builder` pipeline only |
467
+ | `plugins` | `builder` pipeline only (`beforeDispatch` runs on `.send()`, not `.build()`) |
276
468
  | `codes` | `builder` presets and `.code()` |
277
- | `withDefaultHeaders` | HTTP middleware |
278
- | `socket.event` | WebSocket / Socket.IO emit name |
469
+ | `withDefaultHeaders` | HTTP / Fetch middleware |
470
+ | `socket.event` | Socket.IO emit name |
279
471
 
280
472
  ---
281
473
 
@@ -285,6 +477,7 @@ io.use(runtime.socketIo())
285
477
  runtime.express()
286
478
  runtime.node()
287
479
  runtime.elysia()
480
+ runtime.fetch() / runtime.forRequest(request)
288
481
  runtime.websocket() / runtime.attachWebSocket(ws)
289
482
  runtime.socketIo() / runtime.attachSocketIo(socket)
290
483
  ```
@@ -294,9 +487,9 @@ runtime.socketIo() / runtime.attachSocketIo(socket)
294
487
  ```typescript
295
488
  try {
296
489
  const user = await fetchUser(id)
297
- return res.builder.as.ok(user, 'User fetched')
490
+ return res.builder.as.ok(user, 'User fetched').send()
298
491
  } catch (err) {
299
- return res.builder.fromError(err).build()
492
+ return res.builder.fromError(err).send()
300
493
  }
301
494
  ```
302
495
 
@@ -316,7 +509,7 @@ app.use(runtime.express())
316
509
 
317
510
  app.get('/users/:id', async (req, res) => {
318
511
  const user = await db.findUser(req.params.id)
319
- if (!user) return res.builder.as.notFound('User not found')
512
+ if (!user) return res.builder.as.notFound('User not found').send()
320
513
 
321
514
  console.log(
322
515
  DTO.Log.level('info')
@@ -328,7 +521,7 @@ app.get('/users/:id', async (req, res) => {
328
521
 
329
522
  return res.builder.as.ok(user, 'User fetched', {
330
523
  meta: { requestId: req.ctx.requestId },
331
- })
524
+ }).send()
332
525
  })
333
526
  ```
334
527
 
@@ -346,7 +539,49 @@ app.get('/users/:id', async (req, res) => {
346
539
  | `.tooManyRequests()` | 429 | CLIENT |
347
540
  | `.error()` | 500 | SERVER |
348
541
 
349
- See source `dto/response/presets.ts` for the full list.
542
+ See source `dto/send/presets.ts` for the full list.
543
+
544
+ ---
545
+
546
+ ### Breaking / API changes
547
+
548
+ - **Explicit terminal verbs** — every handler chain must end with `.send()` (dispatch to client) or `.build()` (schema only). No automatic dispatch.
549
+ - **Preset terminal** — `.as.ok()`, `.as.fail()`, … return `PresetTerminal` with only `.build()` and `.send()`. No further chaining after presets (put headers **before** preset: `builder.header('X').as.ok(data).send()`).
550
+ - **WebSocket / Socket.IO wire format** — emits full transport envelope (`statusCode`, `headers`, `cookies`, `body`) instead of bare body JSON. Update clients if they parsed the old shape.
551
+
552
+ ### New
553
+
554
+ - **Fetch API adapter** — `runtime.fetch()`, `runtime.forRequest(request)`, `@xrystal/dto/fetch`. Returns Web `Response` from `.send()`. Works with Workers, Hono, Bun.serve, Next.js route handlers.
555
+ - **Core transport layer** — `dto/send/transport.ts` centralises headers, cookies, JSON body, Fetch `Response`, and realtime wire envelope.
556
+ - **Exported adapters & dispatchers** — `ExpressDispatcher`, `FetchDispatcher`, `ElysiaDispatcher`, … available from main entry and subpaths for custom integrations.
557
+
558
+ ### Fixes & consistency
559
+
560
+ - **Elysia** — inline dispatcher replaced with `ElysiaDispatcher` class (same pattern as Express/Node/Fetch).
561
+ - **Node http** — `Set-Cookie` support added (was Express-only via `res.cookie`).
562
+ - **Dispatch hooks** — `beforeDispatch` / `afterDispatch` plugins run on `.send()` only, not on `.build()`.
563
+
564
+ ### Package surface
565
+
566
+ | Runtimes | 6 — Express, Node, Fetch, Elysia, WebSocket, Socket.IO |
567
+ | Tests | 77 passing |
568
+ | Subpaths | `/express`, `/node`, `/fetch`, `/elysia`, `/websocket`, `/socketio`, `/response`, `/log`, `/queue`, `/dto` |
569
+
570
+ ### Migration cheat sheet
571
+
572
+ ```typescript
573
+ // Before
574
+ return res.builder.as.ok(data, 'Done')
575
+
576
+ // After
577
+ return res.builder.as.ok(data, 'Done').send()
578
+
579
+ // Schema only (unchanged intent, now explicit)
580
+ const body = res.builder.as.ok(data).build()
581
+
582
+ // WS client — expect envelope now
583
+ // { statusCode, headers, cookies, body: { status, message, … } }
584
+ ```
350
585
 
351
586
  ---
352
587