@dunx/http 0.1.0 → 0.2.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 CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  `Bun.serve` adapter for [dunx](https://github.com/petarzarkov/dunx). Class-based
4
4
  controllers **and WebSocket gateways**, standard decorators, and no JavaScript
5
- router Bun's native `routes` does path params and per-method dispatch in Zig.
5
+ router - Bun's native `routes` does path params and per-method dispatch in Zig.
6
6
 
7
7
  `Bun.serve` takes `routes` and `websocket` in one call, so both live here: one
8
- `listen()`, one server, one port. Zero dependencies beyond `@dunx/core` no
8
+ `listen()`, one server, one port. Zero dependencies beyond `@dunx/core` - no
9
9
  `express`, no `ws`, no `socket.io`.
10
10
 
11
11
  ## Install
@@ -69,14 +69,14 @@ create(input: Input<typeof createNote>): Note {
69
69
 
70
70
  `Input<typeof opts>` has to be written out. A standard method decorator can
71
71
  **check** a handler's parameter type but cannot contextually type an unannotated
72
- one, so the annotation is required and it is a type-level function over the
72
+ one, so the annotation is required - and it is a type-level function over the
73
73
  options object, so each type is still declared exactly once. A wrong annotation is
74
74
  a compile error naming the mismatched property; an unannotated parameter is
75
75
  `TS7006`.
76
76
 
77
77
  | Field | Source | Declared by |
78
78
  | -------------- | ------------------------------------- | ----------- |
79
- | `input.req` | the `BunRequest` always present | always |
79
+ | `input.req` | the `BunRequest` - always present | always |
80
80
  | `input.body` | parsed by `content-type`, then validated | `body` |
81
81
  | `input.query` | `new URL(req.url).searchParams` | `query` |
82
82
  | `input.params` | `req.params` | `params` |
@@ -85,7 +85,7 @@ With no options at all, annotate `Input<RouteSchemas>` for the request, or take
85
85
  parameter. Path params without a `params` schema stay on `input.req.params`.
86
86
 
87
87
  Validation is the **Standard Schema** spec (`~standard.validate`, sync or async),
88
- restated in this package's own types so Zod 4, Valibot and ArkType all work and
88
+ restated in this package's own types - so Zod 4, Valibot and ArkType all work and
89
89
  `@dunx/http` still has zero dependencies. Anything with a `~standard` property
90
90
  qualifies, including a hand-written object; see `examples/full`.
91
91
 
@@ -98,7 +98,7 @@ Parsed only when `body` is declared, by media type:
98
98
  | `application/json`, `*+json`, none | `req.json()` |
99
99
  | `application/x-www-form-urlencoded` | fields; a repeated key becomes an array |
100
100
  | `multipart/form-data` | fields and `File`s, same repeat rule |
101
- | `text/*` | `req.text()` a string |
101
+ | `text/*` | `req.text()` - a string |
102
102
  | anything else | **415**, nothing read |
103
103
 
104
104
  A body the caller mangled is a **400** (`Malformed application/json body`), never a
@@ -109,16 +109,16 @@ the schema error that is about to be more useful.
109
109
 
110
110
  | Handler returns | Response |
111
111
  | ----------------- | -------------------------------------------- |
112
- | a `Response` | passed through untouched the escape hatch |
112
+ | a `Response` | passed through untouched - the escape hatch |
113
113
  | `undefined`/`null`| `204`, no body |
114
114
  | anything else | `Response.json(value)` at the status below |
115
115
 
116
- Status precedence: `options.status`, else **201 for POST**, else **200** Nest's
116
+ Status precedence: `options.status`, else **201 for POST**, else **200** - Nest's
117
117
  rule. A thrown `HttpError` still goes through the error mapper.
118
118
 
119
119
  ### Validation failures
120
120
 
121
- A rejected schema is a `ValidationError` a `400` whose body carries every issue,
121
+ A rejected schema is a `ValidationError` - a `400` whose body carries every issue,
122
122
  with the path flattened to dots (both `['a', 0]` and `[{ key: 'a' }, { key: 0 }]`
123
123
  render as `a.0`):
124
124
 
@@ -132,7 +132,7 @@ render as `a.0`):
132
132
 
133
133
  ### Which validator to use
134
134
 
135
- Any of them. This is measured rather than asserted `bun run validation` in
135
+ Any of them. This is measured rather than asserted - `bun run validation` in
136
136
  `tools/bench` runs the same dunx app and the same schema shape with only the library
137
137
  behind `~standard` changed, and reports what each one costs per request:
138
138
 
@@ -146,13 +146,13 @@ behind `~standard` changed, and reports what each one costs per request:
146
146
 
147
147
  **`await req.json()` on the same request costs 3.10 µs**, which is more than all of
148
148
  them put together. So validation is not where a slow endpoint's time goes, and
149
- swapping zod for a compiled validator buys about 7% of a small request worth having
149
+ swapping zod for a compiled validator buys about 7% of a small request - worth having
150
150
  if a profile points at it, not worth restructuring for. zod is what `@dunx/openapi`
151
151
  reads schemas from (via `z.toJSONSchema`), and it is the default for that reason
152
152
  rather than a performance one. Three fields, though: a deeply nested schema would very
153
153
  likely separate these engines much further.
154
154
 
155
- Two of the five ship no `~standard` property. Bridging one is small enough to inline
155
+ Two of the five ship no `~standard` property. Bridging one is small enough to inline -
156
156
  this is the whole of it:
157
157
 
158
158
  ```ts
@@ -181,7 +181,7 @@ Full numbers, methodology and the ajv version:
181
181
  ## Route metadata and scoped middleware
182
182
 
183
183
  A decorator annotates a route; a guard reads the annotation back. Metadata on its
184
- own enforces nothing which is why `@Roles` needs a guard that looks at it, and
184
+ own enforces nothing - which is why `@Roles` needs a guard that looks at it, and
185
185
  why one global guard plus `@Public()` is the combination worth learning.
186
186
 
187
187
  ```ts
@@ -251,7 +251,7 @@ const app = await HttpFactory.create(AppModule, { middleware: [AuthGuard] });
251
251
  ### `RouteContext`
252
252
 
253
253
  The second argument to `handle`. Built **once per route at boot** and closed over
254
- by the chain, so `get` is a `Map` lookup over an already-merged record not a
254
+ by the chain, so `get` is a `Map` lookup over an already-merged record - not a
255
255
  prototype walk, and nothing is resolved per request.
256
256
 
257
257
  | Member | Is |
@@ -263,7 +263,7 @@ prototype walk, and nothing is resolved per request.
263
263
  | `get(key)` | The metadata value, or `undefined` |
264
264
 
265
265
  `get` resolves the **handler's** metadata first and the **controller class's**
266
- second the same override direction as Nest's `Reflector.getAllAndOverride`.
266
+ second - the same override direction as Nest's `Reflector.getAllAndOverride`.
267
267
 
268
268
  ### Your own keys
269
269
 
@@ -326,7 +326,7 @@ logs at `warn`, a 5xx at `error`.
326
326
  It needs no configuration: `Logger` and `RequestContext` are `@dunx/core`
327
327
  contracts with default bindings, so this works in an app that imported no logging
328
328
  module. Import `@dunx/infra/logger` and the same entries go through
329
- `@arkv/logger` sanitized, masked, optionally to a rotating file with nothing
329
+ `@arkv/logger` - sanitized, masked, optionally to a rotating file - with nothing
330
330
  here changing.
331
331
 
332
332
  Everything the **handler** logs in between carries the same `requestId`, `method`,
@@ -337,7 +337,7 @@ one is minted and returned on the response.
337
337
  ### Bodies are off by default, and what that costs
338
338
 
339
339
  `requestBody` and `responseBody` default to **`false`**. Turning either on means a
340
- `clone().text()` a second copy of every payload, buffered and parsed, on the hot
340
+ `clone().text()` - a second copy of every payload, buffered and parsed, on the hot
341
341
  path. Measured in `tools/bench`, both on cost roughly two thirds of the throughput
342
342
  on the `validate` scenario. The response body is also the field most likely to
343
343
  carry a secret, so this is the right default twice over.
@@ -345,7 +345,7 @@ carry a secret, so this is the right default twice over.
345
345
  Turn them on in development, where seeing the payload is the point:
346
346
 
347
347
  ```ts
348
- // Off entirely what the benchmark's primary `dunx` subject uses, since no other
348
+ // Off entirely - what the benchmark's primary `dunx` subject uses, since no other
349
349
  // framework in that suite logs anything.
350
350
  HttpFactory.create(AppModule, { requestLogging: false });
351
351
 
@@ -362,15 +362,15 @@ HttpFactory.create(AppModule, {
362
362
 
363
363
  **Even at its cheapest, a log line is not free.** `tools/bench` carries `dunx` and
364
364
  `dunx-logging` as separate subjects for exactly this reason: with logging off dunx
365
- runs at 81100% of raw `Bun.serve` depending on the scenario, and with it on, 4045%.
365
+ runs at 81-100% of raw `Bun.serve` depending on the scenario, and with it on, 40-45%.
366
366
  The remainder is `JSON.stringify` plus a `write` per request inside an
367
367
  `AsyncLocalStorage` scope. If you need the last of the throughput, turn it off and
368
- sample at the edge instead but know what you gave up.
368
+ sample at the edge instead - but know what you gave up.
369
369
 
370
370
  ### Unmatched paths are logged too
371
371
 
372
372
  `Bun.serve({ routes })` answers a miss itself, so nothing in the middleware chain
373
- would ever see a 404 invisible to logging, metrics and tracing. `listen()`
373
+ would ever see a 404 - invisible to logging, metrics and tracing. `listen()`
374
374
  installs one `fetch` fallback that runs the global middleware and returns
375
375
  `{"error":"NOT_FOUND","status":404}`.
376
376
 
@@ -381,7 +381,7 @@ fallback runs only after it has decided nothing matched.
381
381
 
382
382
  A route with **no middleware and no CORS** is dispatched by a handler in which
383
383
  nothing is `async`. It returns a `Response` rather than a `Promise<Response>`
384
- wherever it has nothing to wait for Bun accepts either. The general path awaits the
384
+ wherever it has nothing to wait for - Bun accepts either. The general path awaits the
385
385
  input reader, the handler and the response coercion, and for most shapes those awaits
386
386
  are on values that were never thenable, each costing an async frame and a microtask
387
387
  tick for nothing.
@@ -389,16 +389,16 @@ tick for nothing.
389
389
  | Route shape | What it costs |
390
390
  | ---------------------------------------- | ----------------------------------------- |
391
391
  | no schemas | no promise at all |
392
- | `query` and/or `params`, sync validator | no promise at all read and validated inline |
392
+ | `query` and/or `params`, sync validator | no promise at all - read and validated inline |
393
393
  | `body` declared | one promise link, for `req.json()` |
394
394
 
395
395
  Measured in `tools/bench`: `plaintext` 89.5% -> 97.2% of raw `Bun.serve` when this
396
396
  covered only schema-less routes, and `validate` 84.0% -> 92.3% once it was extended
397
397
  to routes that read input. A handler that *does* return a promise, or a validator
398
- that does, is adopted rather than wrapped nothing about this is conditional on
398
+ that does, is adopted rather than wrapped - nothing about this is conditional on
399
399
  writing sync code.
400
400
 
401
- Adding middleware including `requestLogging` opts a route back into the async
401
+ Adding middleware - including `requestLogging` - opts a route back into the async
402
402
  path, because middleware is `async` by contract.
403
403
 
404
404
  ## App-level configuration
@@ -417,13 +417,13 @@ await app.listen(3000);
417
417
 
418
418
  Calling any of them **after** `listen()` throws. The route table and the middleware
419
419
  chain are folded into one closure per route when the server binds, so a late call
420
- could only ever be a silent no-op the failure mode worth trading for an error.
420
+ could only ever be a silent no-op - the failure mode worth trading for an error.
421
421
 
422
422
  | Hook | Effect |
423
423
  | ------------------------ | ----------------------------------------------------------------------------- |
424
424
  | `setGlobalPrefix(p)` | Prefixes every discovered route. Slashes normalised; last call wins |
425
425
  | `use(...middleware)` | Appends container-resolved `Ctor<Middleware>`, so it can inject |
426
- | `set(key, value)` | Typed settings a key must exist on `AppSettings`, so a typo is a type error |
426
+ | `set(key, value)` | Typed settings - a key must exist on `AppSettings`, so a typo is a type error |
427
427
  | `setting(key)` | Reads one back |
428
428
  | `enableCors(options?)` | Response headers plus an `OPTIONS` preflight per path. Last call wins |
429
429
  | `clientIp(req)` | The `inject(ClientAddress)` singleton, honouring `'trust proxy'` |
@@ -433,11 +433,11 @@ could only ever be a silent no-op — the failure mode worth trading for an erro
433
433
 
434
434
  - **Middleware order**: `HttpOptions.middleware` first (outermost), then each
435
435
  `use()` call in the order it was made, then a controller's `@UseGuards`, then a
436
- method's innermost. Outermost sees the request first and the response last.
436
+ method's - innermost. Outermost sees the request first and the response last.
437
437
  - **Port**: the `listen(port)` argument, else `HttpOptions.port`, else `3000`.
438
438
  - **Error mapper**: `HttpOptions.onError`; there is no imperative equivalent.
439
439
  - **Overrides**: `HttpOptions.overrides` is core's `AppOptions.overrides`, passed
440
- straight through bindings replaced in place, which is what `@dunx/testing`'s
440
+ straight through - bindings replaced in place, which is what `@dunx/testing`'s
441
441
  `createTestServer` uses.
442
442
  - **Repeated calls**: `setGlobalPrefix`, `set` and `enableCors` all replace, so the
443
443
  last call wins. `use()` appends.
@@ -448,7 +448,7 @@ could only ever be a silent no-op — the failure mode worth trading for an erro
448
448
  ### CORS and preflight
449
449
 
450
450
  `Bun.serve({ routes })` answers a method miss with `404`, so a preflight can never
451
- be inferred `enableCors()` mounts an explicit `OPTIONS` handler on every path,
451
+ be inferred - `enableCors()` mounts an explicit `OPTIONS` handler on every path,
452
452
  built at boot from the methods that path actually declares. `origin` takes a
453
453
  string, a list, or a predicate; anything not allowed gets **no** CORS headers at
454
454
  all, which is what makes the browser block it. `'*'` is the default, and because a
@@ -459,7 +459,7 @@ a mapped `500` still carries them.
459
459
 
460
460
  ### Client IP
461
461
 
462
- `ClientAddress` needs no registration every class is injectable, and `listen()`
462
+ `ClientAddress` needs no registration - every class is injectable, and `listen()`
463
463
  hands the resolved singleton the live server:
464
464
 
465
465
  ```ts
@@ -480,7 +480,7 @@ send whatever it likes.
480
480
 
481
481
  ## WebSocket gateways
482
482
 
483
- A gateway is a normal injectable class declared in `@Module({ providers })` there
483
+ A gateway is a normal injectable class declared in `@Module({ providers })` - there
484
484
  is no second list and no module to configure. `HttpFactory` finds it by its
485
485
  `@Gateway` marker, and `listen()` mounts it on the same server as the routes:
486
486
 
@@ -539,7 +539,7 @@ path that upgrades.
539
539
 
540
540
  | Decorator | Signature | Notes |
541
541
  | ------------------- | ------------------------------------- | -------------------------------------------------------------- |
542
- | `@Gateway(path)` | class | Required it is what marks the provider as a gateway |
542
+ | `@Gateway(path)` | class | Required - it is what marks the provider as a gateway |
543
543
  | `@OnUpgrade()` | `(req: BunRequest)` | Return a `Response` to refuse; anything else becomes `context` |
544
544
  | `@OnOpen()` | `(socket)` | |
545
545
  | `@OnMessage(event)` | `(data, socket)` | Routed by envelope event name |
@@ -549,7 +549,7 @@ path that upgrades.
549
549
  | `@OnPing()` | `(data, socket)` | Bun still answers with a pong |
550
550
  | `@OnPong()` | `(data, socket)` | |
551
551
 
552
- Handlers may be `async`. A returned value is sent to the sender under the same
552
+ Handlers may be `async`. A returned value is sent to the sender - under the same
553
553
  event name for `@OnMessage(event)`, verbatim (or JSON) for the raw handler, and
554
554
  never for a lifecycle handler. Return `undefined` to send nothing.
555
555
 
@@ -569,7 +569,7 @@ needed** for a socket to connect. Consequences, all measured:
569
569
  and can be returned as the connection's `context`.
570
570
  - A plain `GET` on a gateway path is **426**; any other method is Bun's native
571
571
  **404**, because the upgrade is mounted as a `GET`. A path no gateway and no
572
- controller serves is the same native 404 there is nothing to fall through to.
572
+ controller serves is the same native 404 - there is nothing to fall through to.
573
573
  - A path claimed by both a gateway and a controller route is a **boot error**
574
574
  naming both, since one of the two would otherwise be dropped from the table.
575
575
  - `setGlobalPrefix()` moves routes, **not** gateways. A gateway path is the exact
@@ -588,7 +588,7 @@ These throw at boot rather than picking a winner:
588
588
  - two handlers claiming one event or one lifecycle slot, named individually
589
589
  - two gateways on one path, named individually
590
590
  - a `@Gateway` class with no handlers at all
591
- - a handler-declaring provider that is **not** a `@Gateway` it could never
591
+ - a handler-declaring provider that is **not** a `@Gateway` - it could never
592
592
  receive a frame, so it is an error instead of a silent no-op
593
593
 
594
594
  ### The envelope
@@ -603,7 +603,7 @@ smallest one that works:
603
603
  It is **opt-in**: a frame is only parsed for a gateway that declares at least one
604
604
  `@OnMessage(event)` handler. A gateway with only a raw `@OnMessage()` never sees
605
605
  JSON it did not ask for. Binary frames, invalid JSON, a non-object, a missing
606
- `event`, and an event no handler claims all fall through to the raw handler and
606
+ `event`, and an event no handler claims all fall through to the raw handler - and
607
607
  are ignored if there is none. Nothing is ever replied to the sender that a handler
608
608
  did not return.
609
609
 
@@ -618,7 +618,7 @@ Topics live in Bun, not in a JavaScript map. A socket joins one with
618
618
  native methods on the socket you already hold.
619
619
 
620
620
  `PubSub` is the injectable side, for publishing without a socket. `HttpFactory`
621
- binds it around your root module, so nothing has to be imported or registered
621
+ binds it around your root module, so nothing has to be imported or registered -
622
622
  listing it in `providers` as well is the container's duplicate-binding error:
623
623
 
624
624
  ```ts
@@ -634,7 +634,7 @@ class Notifier {
634
634
  ```
635
635
 
636
636
  `publish` returns the bytes sent, `0` if the message was dropped, `-1` under
637
- backpressure Bun's own status. It goes through `server.publish`, which reaches
637
+ backpressure - Bun's own status. It goes through `server.publish`, which reaches
638
638
  **every** subscriber including the socket whose handler triggered it (unlike
639
639
  `socket.publish`, which honours `publishToSelf`). Publishing before the server is
640
640
  listening throws saying so.
@@ -665,7 +665,7 @@ the default logs.
665
665
  ### Shutdown with a live socket
666
666
 
667
667
  Measured: a graceful `server.stop()` waits for open connections, and a WebSocket
668
- does not close on its own so it **never resolves** while a socket is open. An app
668
+ does not close on its own - so it **never resolves** while a socket is open. An app
669
669
  with at least one gateway therefore force-stops (`stop(true)`) in `shutdown()`, and
670
670
  those clients see a `1006` close. An app with no gateways still stops gracefully.
671
671
  Bun also delivers an empty close `reason` to `@OnClose` once a socket has exchanged
@@ -686,7 +686,7 @@ const app = await HttpFactory.create(AppModule, {
686
686
  });
687
687
  ```
688
688
 
689
- That is the whole opt-in. `RedisRelay` is `Bun.RedisClient` a Bun global so this
689
+ That is the whole opt-in. `RedisRelay` is `Bun.RedisClient` - a Bun global - so this
690
690
  adds **no dependency**, and with no `relay` configured nothing here runs at all.
691
691
 
692
692
  Nothing else changes. `socket.subscribe(topic)` is still Bun's, and a topic no
@@ -726,11 +726,11 @@ await app.get(PubSub).relayThrough(app.get(RedisConnection), {
726
726
  await app.listen(3000);
727
727
  ```
728
728
 
729
- Only one relay per `PubSub` a second `relayThrough` throws, because two
729
+ Only one relay per `PubSub` - a second `relayThrough` throws, because two
730
730
  subscriptions on one channel is the other way to deliver everything twice.
731
731
 
732
732
  `socket.publish(topic, data)` is Bun's own method and stays local; anything that must
733
- cross nodes goes through `PubSub`. `subscriberCount` is local too Bun cannot count
733
+ cross nodes goes through `PubSub`. `subscriberCount` is local too - Bun cannot count
734
734
  another node's sockets.
735
735
 
736
736
  `maxRetries` on `RedisRelay` defaults to `0`, and that is deliberate: a
@@ -740,7 +740,7 @@ Bun's reconnection.
740
740
 
741
741
  ## Status codes
742
742
 
743
- `HttpStatusCode` is a frozen object, not an `enum` one name serving as both the
743
+ `HttpStatusCode` is a frozen object, not an `enum` - one name serving as both the
744
744
  value and the type, so it reads like an enum and erases like a constant:
745
745
 
746
746
  ```ts
@@ -769,11 +769,11 @@ still works.
769
769
  - Schemas, parsers and the status are resolved in `buildRoutes` at boot, into the
770
770
  same closure the middleware chain folds into. Per request the framework parses
771
771
  and validates what was declared, calls the method, wraps the return, and maps a
772
- throw no metadata read, no lookup, no DI.
772
+ throw - no metadata read, no lookup, no DI.
773
773
  - Gateways use the same marker-plus-prototype-scan discovery as routes, and go into
774
774
  the same route table. `withUpgradeRoutes` and `buildWebSocket` are exported for
775
775
  anyone assembling `Bun.serve` themselves.
776
776
 
777
777
  ## License
778
778
 
779
- MIT
779
+ Apache-2.0
package/dist/index.js CHANGED
@@ -603,7 +603,7 @@ class PubSub {
603
603
  }
604
604
  async relayThrough(relay, options = {}) {
605
605
  if (this.#relay) {
606
- throw new AppError5("PubSub already relays. Two subscriptions on one channel would deliver " + "every relayed message twice \u2014 pass HttpOptions.relay or call " + "relayThrough(), not both.");
606
+ throw new AppError5("PubSub already relays. Two subscriptions on one channel would deliver " + "every relayed message twice - pass HttpOptions.relay or call " + "relayThrough(), not both.");
607
607
  }
608
608
  this.#relay = relay;
609
609
  this.#channel = options.channel ?? DEFAULT_RELAY_CHANNEL;
@@ -1423,5 +1423,5 @@ export {
1423
1423
  ClientAddress
1424
1424
  };
1425
1425
 
1426
- //# debugId=B2F007DAD14DC76D64756E2164756E21
1426
+ //# debugId=1EC09309FBB1CF1964756E2164756E21
1427
1427
  //# sourceMappingURL=index.js.map