@effect/platform-bun 4.0.0-beta.7 → 4.0.0-beta.71

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 (108) hide show
  1. package/dist/BunChildProcessSpawner.d.ts +1 -1
  2. package/dist/BunChildProcessSpawner.js +1 -1
  3. package/dist/BunClusterHttp.d.ts +70 -7
  4. package/dist/BunClusterHttp.d.ts.map +1 -1
  5. package/dist/BunClusterHttp.js +41 -10
  6. package/dist/BunClusterHttp.js.map +1 -1
  7. package/dist/BunClusterSocket.d.ts +54 -9
  8. package/dist/BunClusterSocket.d.ts.map +1 -1
  9. package/dist/BunClusterSocket.js +54 -9
  10. package/dist/BunClusterSocket.js.map +1 -1
  11. package/dist/BunCrypto.d.ts +10 -0
  12. package/dist/BunCrypto.d.ts.map +1 -0
  13. package/dist/BunCrypto.js +22 -0
  14. package/dist/BunCrypto.js.map +1 -0
  15. package/dist/BunFileSystem.d.ts +4 -2
  16. package/dist/BunFileSystem.d.ts.map +1 -1
  17. package/dist/BunFileSystem.js +27 -3
  18. package/dist/BunFileSystem.js.map +1 -1
  19. package/dist/BunHttpClient.d.ts +2 -2
  20. package/dist/BunHttpClient.js +2 -2
  21. package/dist/BunHttpPlatform.d.ts +4 -2
  22. package/dist/BunHttpPlatform.d.ts.map +1 -1
  23. package/dist/BunHttpPlatform.js +5 -3
  24. package/dist/BunHttpPlatform.js.map +1 -1
  25. package/dist/BunHttpServer.d.ts +77 -19
  26. package/dist/BunHttpServer.d.ts.map +1 -1
  27. package/dist/BunHttpServer.js +63 -36
  28. package/dist/BunHttpServer.js.map +1 -1
  29. package/dist/BunHttpServerRequest.d.ts +26 -2
  30. package/dist/BunHttpServerRequest.d.ts.map +1 -1
  31. package/dist/BunHttpServerRequest.js +3 -1
  32. package/dist/BunHttpServerRequest.js.map +1 -1
  33. package/dist/BunMultipart.d.ts +28 -5
  34. package/dist/BunMultipart.d.ts.map +1 -1
  35. package/dist/BunMultipart.js +15 -5
  36. package/dist/BunMultipart.js.map +1 -1
  37. package/dist/BunPath.d.ts +12 -6
  38. package/dist/BunPath.d.ts.map +1 -1
  39. package/dist/BunPath.js +42 -7
  40. package/dist/BunPath.js.map +1 -1
  41. package/dist/BunRedis.d.ts +48 -9
  42. package/dist/BunRedis.d.ts.map +1 -1
  43. package/dist/BunRedis.js +51 -12
  44. package/dist/BunRedis.js.map +1 -1
  45. package/dist/BunRuntime.d.ts +24 -30
  46. package/dist/BunRuntime.d.ts.map +1 -1
  47. package/dist/BunRuntime.js +38 -11
  48. package/dist/BunRuntime.js.map +1 -1
  49. package/dist/BunServices.d.ts +32 -5
  50. package/dist/BunServices.d.ts.map +1 -1
  51. package/dist/BunServices.js +7 -3
  52. package/dist/BunServices.js.map +1 -1
  53. package/dist/BunSink.d.ts +2 -2
  54. package/dist/BunSink.js +2 -2
  55. package/dist/BunSocket.d.ts +30 -4
  56. package/dist/BunSocket.d.ts.map +1 -1
  57. package/dist/BunSocket.js +10 -3
  58. package/dist/BunSocket.js.map +1 -1
  59. package/dist/BunSocketServer.d.ts +2 -2
  60. package/dist/BunSocketServer.js +2 -2
  61. package/dist/BunStdio.d.ts +5 -2
  62. package/dist/BunStdio.d.ts.map +1 -1
  63. package/dist/BunStdio.js +36 -3
  64. package/dist/BunStdio.js.map +1 -1
  65. package/dist/BunStream.d.ts +3 -2
  66. package/dist/BunStream.d.ts.map +1 -1
  67. package/dist/BunStream.js +34 -3
  68. package/dist/BunStream.js.map +1 -1
  69. package/dist/BunTerminal.d.ts +8 -2
  70. package/dist/BunTerminal.d.ts.map +1 -1
  71. package/dist/BunTerminal.js +41 -3
  72. package/dist/BunTerminal.js.map +1 -1
  73. package/dist/BunWorker.d.ts +9 -2
  74. package/dist/BunWorker.d.ts.map +1 -1
  75. package/dist/BunWorker.js +46 -4
  76. package/dist/BunWorker.js.map +1 -1
  77. package/dist/BunWorkerRunner.d.ts +5 -1
  78. package/dist/BunWorkerRunner.d.ts.map +1 -1
  79. package/dist/BunWorkerRunner.js +41 -5
  80. package/dist/BunWorkerRunner.js.map +1 -1
  81. package/dist/index.d.ts +25 -23
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +25 -23
  84. package/dist/index.js.map +1 -1
  85. package/package.json +5 -5
  86. package/src/BunChildProcessSpawner.ts +1 -1
  87. package/src/BunClusterHttp.ts +75 -11
  88. package/src/BunClusterSocket.ts +54 -9
  89. package/src/BunCrypto.ts +24 -0
  90. package/src/BunFileSystem.ts +27 -3
  91. package/src/BunHttpClient.ts +2 -2
  92. package/src/BunHttpPlatform.ts +28 -4
  93. package/src/BunHttpServer.ts +128 -56
  94. package/src/BunHttpServerRequest.ts +26 -2
  95. package/src/BunMultipart.ts +36 -6
  96. package/src/BunPath.ts +42 -7
  97. package/src/BunRedis.ts +53 -14
  98. package/src/BunRuntime.ts +54 -31
  99. package/src/BunServices.ts +34 -5
  100. package/src/BunSink.ts +2 -2
  101. package/src/BunSocket.ts +30 -4
  102. package/src/BunSocketServer.ts +2 -2
  103. package/src/BunStdio.ts +36 -3
  104. package/src/BunStream.ts +34 -3
  105. package/src/BunTerminal.ts +41 -3
  106. package/src/BunWorker.ts +46 -4
  107. package/src/BunWorkerRunner.ts +41 -5
  108. package/src/index.ts +26 -23
@@ -1,10 +1,43 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Bun implementation of the Effect `HttpServer`.
3
+ *
4
+ * This module builds an Effect HTTP server from `Bun.serve`, translating Bun's
5
+ * Web `Request` objects into `HttpServerRequest` values and Effect
6
+ * `HttpServerResponse` values back into Web `Response` objects. It is the Bun
7
+ * runtime entry point for serving `HttpApp`s, streaming responses, file
8
+ * responses through `BunHttpPlatform`, multipart requests, and websocket
9
+ * endpoints through `HttpServerRequest.upgrade`.
10
+ *
11
+ * Common use cases include using {@link layer} or {@link layerConfig} to serve
12
+ * an application from Bun configuration, {@link layerServer} when only the
13
+ * `HttpServer` service is needed, and {@link layerTest} for tests that need an
14
+ * ephemeral Bun listener and fetch-compatible client.
15
+ *
16
+ * Bun supplies absolute request URLs and Web-standard request bodies. This
17
+ * adapter stores the normalized path-and-query URL on `HttpServerRequest.url`,
18
+ * while the underlying `Request` still follows Web body rules: pick the
19
+ * streamed, text, JSON, URL-encoded, or multipart view that matches the route
20
+ * instead of consuming the same body in incompatible ways. Because `Bun.serve`
21
+ * has a single active `fetch` handler, each `serve` call reloads that handler
22
+ * and restores the previous one when the serve scope finalizes.
23
+ *
24
+ * WebSocket upgrades must happen from the Bun request handler. The
25
+ * `HttpServerRequest.upgrade` effect calls `server.upgrade`, fails when Bun says
26
+ * the request is not upgradeable, buffers messages that arrive before the
27
+ * Effect socket handler is installed, and maps non-normal close codes into
28
+ * `Socket` errors. The server is stopped with `server.stop()` when its
29
+ * acquisition scope closes; unless preemptive shutdown is disabled, finalizing
30
+ * a serve scope also starts that stop with the configured graceful shutdown
31
+ * timeout or the default timeout.
32
+ *
33
+ * @since 4.0.0
3
34
  */
4
35
  import type { Server as BunServer, ServerWebSocket } from "bun"
5
36
  import * as Config from "effect/Config"
6
37
  import type { ConfigError } from "effect/Config"
38
+ import * as Context from "effect/Context"
7
39
  import * as Deferred from "effect/Deferred"
40
+ import * as Duration from "effect/Duration"
8
41
  import * as Effect from "effect/Effect"
9
42
  import * as Exit from "effect/Exit"
10
43
  import * as Fiber from "effect/Fiber"
@@ -13,11 +46,12 @@ import type * as FileSystem from "effect/FileSystem"
13
46
  import { flow } from "effect/Function"
14
47
  import * as Inspectable from "effect/Inspectable"
15
48
  import * as Layer from "effect/Layer"
49
+ import * as Option from "effect/Option"
16
50
  import type * as Path from "effect/Path"
17
51
  import type * as Record from "effect/Record"
18
- import type * as Scope from "effect/Scope"
52
+ import type * as Schema from "effect/Schema"
53
+ import * as Scope from "effect/Scope"
19
54
  import * as Semaphore from "effect/Semaphore"
20
- import * as ServiceMap from "effect/ServiceMap"
21
55
  import * as Stream from "effect/Stream"
22
56
  import * as Cookies from "effect/unstable/http/Cookies"
23
57
  import * as Etag from "effect/unstable/http/Etag"
@@ -41,8 +75,10 @@ import * as BunServices from "./BunServices.ts"
41
75
  import * as BunStream from "./BunStream.ts"
42
76
 
43
77
  /**
44
- * @since 1.0.0
78
+ * Bun serve options accepted by the HTTP server, extended with typed route definitions.
79
+ *
45
80
  * @category Options
81
+ * @since 4.0.0
46
82
  */
47
83
  export type ServeOptions<R extends string> =
48
84
  & (
@@ -52,13 +88,19 @@ export type ServeOptions<R extends string> =
52
88
  & { readonly routes?: Bun.Serve.Routes<WebSocketContext, R> }
53
89
 
54
90
  /**
55
- * @since 1.0.0
56
- * @category Constructors
91
+ * Creates a scoped Bun `HttpServer` from `Bun.serve` options, stopping the server on scope finalization with optional graceful shutdown settings.
92
+ *
93
+ * @category constructors
94
+ * @since 4.0.0
57
95
  */
58
96
  export const make = Effect.fnUntraced(
59
97
  function*<R extends string>(
60
- options: ServeOptions<R>
98
+ options: ServeOptions<R> & {
99
+ readonly disablePreemptiveShutdown?: boolean | undefined
100
+ readonly gracefulShutdownTimeout?: Duration.Input | undefined
101
+ }
61
102
  ) {
103
+ const scope = yield* Effect.scope
62
104
  const handlerStack: Array<(request: Request, server: BunServer<WebSocketContext>) => Response | Promise<Response>> =
63
105
  [
64
106
  function(_request, _server) {
@@ -76,6 +118,7 @@ export const make = Effect.fnUntraced(
76
118
  ws.data.run(message)
77
119
  },
78
120
  close(ws, code, closeReason) {
121
+ code = typeof code === "number" ? code : 1001
79
122
  Deferred.doneUnsafe(
80
123
  ws.data.closeDeferred,
81
124
  Socket.defaultCloseCodeIsError(code)
@@ -90,13 +133,24 @@ export const make = Effect.fnUntraced(
90
133
  }
91
134
  })
92
135
 
93
- yield* Effect.addFinalizer(() => Effect.promise(() => server.stop()))
136
+ const shutdown = yield* Effect.promise(() => server.stop()).pipe(
137
+ Effect.cached
138
+ )
139
+ const preemptiveShutdown = options.disablePreemptiveShutdown ? Effect.void : Effect.timeoutOrElse(shutdown, {
140
+ duration: options.gracefulShutdownTimeout ?? Duration.seconds(20),
141
+ orElse: () => Effect.void
142
+ })
143
+
144
+ yield* Scope.addFinalizer(scope, shutdown)
94
145
 
95
146
  return Server.make({
96
147
  address: { _tag: "TcpAddress", port: server.port!, hostname: server.hostname! },
97
148
  serve: Effect.fnUntraced(function*(httpApp, middleware) {
98
- const scope = yield* Effect.scope
99
- const services = yield* Effect.services<never>()
149
+ const parent = yield* Effect.fiber
150
+ const services = parent.context
151
+ const serveScope = Context.getUnsafe(services, Scope.Scope)
152
+ const scope = Scope.forkUnsafe(serveScope, "parallel")
153
+
100
154
  const httpEffect = HttpEffect.toHandled(httpApp, (request, response) =>
101
155
  Effect.sync(() => {
102
156
  ;(request as BunServerRequest).resolve(makeResponse(request, response, services, scope))
@@ -109,24 +163,20 @@ export const make = Effect.fnUntraced(
109
163
  ServerRequest.HttpServerRequest.key,
110
164
  new BunServerRequest(request, resolve, removeHost(request.url), server)
111
165
  )
112
- const fiber = Fiber.runIn(Effect.runForkWith(ServiceMap.makeUnsafe<any>(map))(httpEffect), scope)
166
+ const fiber = Fiber.runIn(Effect.runForkWith(Context.makeUnsafe<any>(map))(httpEffect), scope)
113
167
  request.signal.addEventListener("abort", () => {
114
- fiber.interruptUnsafe(Error.clientAbortFiberId)
168
+ fiber.interruptUnsafe(parent.id, Error.ClientAbort.annotation)
115
169
  }, { once: true })
116
170
  })
117
171
  }
118
172
 
119
- yield* Effect.acquireRelease(
120
- Effect.sync(() => {
121
- handlerStack.push(handler)
122
- server.reload({ fetch: handler })
123
- }),
124
- () =>
125
- Effect.sync(() => {
126
- handlerStack.pop()
127
- server.reload({ fetch: handlerStack[handlerStack.length - 1] })
128
- })
129
- )
173
+ yield* Scope.addFinalizerExit(serveScope, () => {
174
+ handlerStack.pop()
175
+ server.reload({ fetch: handlerStack[handlerStack.length - 1] })
176
+ return preemptiveShutdown
177
+ })
178
+ handlerStack.push(handler)
179
+ server.reload({ fetch: handler })
130
180
  })
131
181
  })
132
182
  }
@@ -135,7 +185,7 @@ export const make = Effect.fnUntraced(
135
185
  const makeResponse = (
136
186
  request: ServerRequest.HttpServerRequest,
137
187
  response: ServerResponse.HttpServerResponse,
138
- services: ServiceMap.ServiceMap<never>,
188
+ context: Context.Context<never>,
139
189
  scope: Scope.Scope
140
190
  ): Response => {
141
191
  const fields: {
@@ -186,7 +236,7 @@ const makeResponse = (
186
236
  Fiber.runIn(fiber, scope)
187
237
  return Effect.succeed(body.stream)
188
238
  })),
189
- services
239
+ context
190
240
  ),
191
241
  fields
192
242
  )
@@ -195,16 +245,23 @@ const makeResponse = (
195
245
  }
196
246
 
197
247
  /**
198
- * @since 1.0.0
199
- * @category Layers
248
+ * Layer that provides only `HttpServer` by constructing a scoped Bun server from the supplied serve options.
249
+ *
250
+ * @category layers
251
+ * @since 4.0.0
200
252
  */
201
253
  export const layerServer: <R extends string>(
202
- options: ServeOptions<R>
254
+ options: ServeOptions<R> & {
255
+ readonly disablePreemptiveShutdown?: boolean | undefined
256
+ readonly gracefulShutdownTimeout?: Duration.Input | undefined
257
+ }
203
258
  ) => Layer.Layer<Server.HttpServer> = flow(make, Layer.effect(Server.HttpServer)) as any
204
259
 
205
260
  /**
206
- * @since 1.0.0
207
- * @category Layers
261
+ * Layer that provides Bun HTTP support services: `HttpPlatform`, weak ETag generation, and `BunServices`.
262
+ *
263
+ * @category layers
264
+ * @since 4.0.0
208
265
  */
209
266
  export const layerHttpServices: Layer.Layer<
210
267
  | HttpPlatform
@@ -217,11 +274,16 @@ export const layerHttpServices: Layer.Layer<
217
274
  )
218
275
 
219
276
  /**
220
- * @since 1.0.0
221
- * @category Layers
277
+ * Layer that provides a Bun `HttpServer` together with the Bun HTTP platform, ETag generator, and Bun services.
278
+ *
279
+ * @category layers
280
+ * @since 4.0.0
222
281
  */
223
282
  export const layer = <R extends string>(
224
- options: ServeOptions<R>
283
+ options: ServeOptions<R> & {
284
+ readonly disablePreemptiveShutdown?: boolean | undefined
285
+ readonly gracefulShutdownTimeout?: Duration.Input | undefined
286
+ }
225
287
  ): Layer.Layer<
226
288
  | Server.HttpServer
227
289
  | HttpPlatform
@@ -230,8 +292,10 @@ export const layer = <R extends string>(
230
292
  > => Layer.mergeAll(layerServer(options), layerHttpServices)
231
293
 
232
294
  /**
233
- * @since 1.0.0
234
- * @category Layers
295
+ * Test layer that starts a Bun HTTP server on an ephemeral port and provides the HTTP test client dependencies.
296
+ *
297
+ * @category layers
298
+ * @since 4.0.0
235
299
  */
236
300
  export const layerTest: Layer.Layer<
237
301
  Server.HttpServer | HttpPlatform | FileSystem.FileSystem | Etag.Generator | Path.Path | HttpClient
@@ -243,17 +307,24 @@ export const layerTest: Layer.Layer<
243
307
  )
244
308
 
245
309
  /**
246
- * @since 1.0.0
247
- * @category Layers
310
+ * Creates the Bun HTTP server and support-services layer from configurable serve options.
311
+ *
312
+ * @category layers
313
+ * @since 4.0.0
248
314
  */
249
315
  export const layerConfig = <R extends string>(
250
- options: Config.Wrap<ServeOptions<R>>
316
+ options: Config.Wrap<
317
+ ServeOptions<R> & {
318
+ readonly disablePreemptiveShutdown?: boolean | undefined
319
+ readonly gracefulShutdownTimeout?: Duration.Input | undefined
320
+ }
321
+ >
251
322
  ): Layer.Layer<
252
323
  Server.HttpServer | HttpPlatform | FileSystem.FileSystem | Etag.Generator | Path.Path,
253
324
  ConfigError
254
325
  > =>
255
326
  Layer.mergeAll(
256
- Layer.effect(Server.HttpServer)(Effect.flatMap(Config.unwrap(options).asEffect(), make)),
327
+ Layer.effect(Server.HttpServer)(Effect.flatMap(Config.unwrap(options), make)),
257
328
  layerHttpServices
258
329
  )
259
330
 
@@ -280,7 +351,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
280
351
  readonly url: string
281
352
  private bunServer: BunServer<WebSocketContext>
282
353
  public headersOverride?: Headers.Headers | undefined
283
- private remoteAddressOverride?: string | undefined
354
+ private remoteAddressOverride?: Option.Option<string> | undefined
284
355
 
285
356
  constructor(
286
357
  source: Request,
@@ -288,7 +359,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
288
359
  url: string,
289
360
  bunServer: BunServer<WebSocketContext>,
290
361
  headersOverride?: Headers.Headers,
291
- remoteAddressOverride?: string
362
+ remoteAddressOverride?: Option.Option<string>
292
363
  ) {
293
364
  super()
294
365
  this[ServerRequest.TypeId] = ServerRequest.TypeId
@@ -311,7 +382,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
311
382
  options: {
312
383
  readonly url?: string | undefined
313
384
  readonly headers?: Headers.Headers | undefined
314
- readonly remoteAddress?: string | undefined
385
+ readonly remoteAddress?: Option.Option<string> | undefined
315
386
  }
316
387
  ) {
317
388
  return new BunServerRequest(
@@ -320,7 +391,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
320
391
  options.url ?? this.url,
321
392
  this.bunServer,
322
393
  options.headers ?? this.headersOverride,
323
- options.remoteAddress ?? this.remoteAddressOverride
394
+ "remoteAddress" in options ? options.remoteAddress : this.remoteAddressOverride
324
395
  )
325
396
  }
326
397
  get method(): HttpMethod {
@@ -329,8 +400,8 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
329
400
  get originalUrl() {
330
401
  return this.source.url
331
402
  }
332
- get remoteAddress(): string | undefined {
333
- return this.remoteAddressOverride ?? this.bunServer.requestIP(this.source)?.address
403
+ get remoteAddress(): Option.Option<string> {
404
+ return this.remoteAddressOverride ?? Option.fromNullishOr(this.bunServer.requestIP(this.source)?.address)
334
405
  }
335
406
  get headers(): Headers.Headers {
336
407
  this.headersOverride ??= Headers.fromInput(this.source.headers)
@@ -348,7 +419,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
348
419
  get stream(): Stream.Stream<Uint8Array, Error.HttpServerError> {
349
420
  return this.source.body
350
421
  ? BunStream.fromReadableStream({
351
- evaluate: () => this.source.body as any,
422
+ evaluate: () => this.source.body ?? emptyReadbleStream,
352
423
  onError: (cause) =>
353
424
  new Error.HttpServerError({
354
425
  reason: new Error.RequestParseError({
@@ -387,10 +458,10 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
387
458
  return this.textEffect
388
459
  }
389
460
 
390
- get json(): Effect.Effect<unknown, Error.HttpServerError> {
461
+ get json(): Effect.Effect<Schema.Json, Error.HttpServerError> {
391
462
  return Effect.flatMap(this.text, (_) =>
392
463
  Effect.try({
393
- try: () => JSON.parse(_) as unknown,
464
+ try: () => JSON.parse(_) as Schema.Json,
394
465
  catch: (cause) =>
395
466
  new Error.HttpServerError({
396
467
  reason: new Error.RequestParseError({
@@ -457,6 +528,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
457
528
  })
458
529
  })
459
530
  ))
531
+ this.textEffect = Effect.map(this.arrayBufferEffect, (_) => new TextDecoder().decode(_))
460
532
  return this.arrayBufferEffect
461
533
  }
462
534
 
@@ -524,14 +596,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
524
596
  semaphore.withPermits(1)
525
597
  )
526
598
 
527
- const encoder = new TextEncoder()
528
- const run = <R, E, _>(handler: (_: Uint8Array) => Effect.Effect<_, E, R> | void, opts?: {
529
- readonly onOpen?: Effect.Effect<void> | undefined
530
- }) => runRaw((data) => typeof data === "string" ? handler(encoder.encode(data)) : handler(data), opts)
531
-
532
- return Socket.Socket.of({
533
- [Socket.TypeId]: Socket.TypeId as typeof Socket.TypeId,
534
- run,
599
+ return Socket.make({
535
600
  runRaw,
536
601
  writer
537
602
  })
@@ -540,6 +605,13 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
540
605
  }
541
606
  }
542
607
 
608
+ const emptyReadbleStream = new ReadableStream({
609
+ start(controller) {
610
+ controller.enqueue(new Uint8Array())
611
+ controller.close()
612
+ }
613
+ })
614
+
543
615
  const removeHost = (url: string) => {
544
616
  if (url[0] === "/") {
545
617
  return url
@@ -1,11 +1,35 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Accessors for the Bun `Request` object backing a platform Bun
3
+ * `HttpServerRequest`.
4
+ *
5
+ * Use this module at interop boundaries when an Effect HTTP handler needs the
6
+ * original `Bun.BunRequest`, for example to read Bun route parameters, pass the
7
+ * request to Bun-specific APIs, inspect Web `Request` fields that are not
8
+ * exposed by the portable `HttpServerRequest` interface, or coordinate with code
9
+ * that already works directly with Bun's server request type.
10
+ *
11
+ * The returned request is the original Web request supplied by `Bun.serve`. It
12
+ * does not reflect Effect request overrides made by middleware, such as a
13
+ * rewritten URL, adjusted headers, or a substituted remote address. Its body is
14
+ * the same one-shot Web `ReadableStream` used by the Effect body helpers, so
15
+ * calling `text`, `json`, `formData`, `arrayBuffer`, or reading `body` directly
16
+ * can disturb the request and conflict with Effect body, multipart, or stream
17
+ * helpers unless ownership of the body is clear.
18
+ *
19
+ * Bun stores client IP information on the server rather than on the request
20
+ * object. Prefer `HttpServerRequest.remoteAddress` when you need the address
21
+ * seen by Effect or middleware; the raw request returned here will not expose
22
+ * middleware-provided remote address overrides.
23
+ *
24
+ * @since 4.0.0
3
25
  */
4
26
  import type { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"
5
27
 
6
28
  /**
7
- * @since 1.0.0
29
+ * Returns the underlying `Bun.BunRequest` from an Effect `HttpServerRequest`.
30
+ *
8
31
  * @category Accessors
32
+ * @since 4.0.0
9
33
  */
10
34
  export const toBunServerRequest = <T extends string = string>(self: HttpServerRequest): Bun.BunRequest<T> =>
11
35
  (self as any).source
@@ -1,5 +1,24 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Bun-specific helpers for parsing HTTP `multipart/form-data` request bodies.
3
+ *
4
+ * This module adapts a Bun `Request` body and headers into the shared
5
+ * `Multipart` model. Use `stream` from Bun HTTP handlers when form fields and
6
+ * uploaded files should be consumed incrementally, for example validating text
7
+ * fields while piping large file parts to storage. Use `persisted` when the
8
+ * whole form should be collected into a record and file parts should be written
9
+ * to scoped temporary files through the current `FileSystem`, `Path`, and
10
+ * `Scope` services.
11
+ *
12
+ * Bun requests expose one-shot web streams, so choose one body reader and do
13
+ * not call `formData`, `text`, `json`, or `arrayBuffer` before using this
14
+ * module. Incoming `FormData` uploads must include a `multipart/form-data`
15
+ * content type with the boundary generated by the client; when constructing
16
+ * `FormData` requests, let the runtime set that header. Stream file content for
17
+ * large uploads, reserve `contentEffect` for small files, and treat client
18
+ * filenames as metadata rather than trusted filesystem paths. Persisted file
19
+ * paths remain valid only for the surrounding scope.
20
+ *
21
+ * @since 4.0.0
3
22
  */
4
23
  import type * as Effect from "effect/Effect"
5
24
  import type { FileSystem } from "effect/FileSystem"
@@ -10,20 +29,31 @@ import * as Multipart from "effect/unstable/http/Multipart"
10
29
  import * as BunStream from "./BunStream.ts"
11
30
 
12
31
  /**
13
- * @since 1.0.0
14
- * @category Constructors
32
+ * Parses a Bun `Request` body as multipart data and returns a stream of multipart parts.
33
+ *
34
+ * @category constructors
35
+ * @since 4.0.0
15
36
  */
16
37
  export const stream = (source: Request): Stream.Stream<Multipart.Part, Multipart.MultipartError> =>
17
38
  BunStream.fromReadableStream({
18
- evaluate: () => source.body!,
39
+ evaluate: () => source.body ?? emptyReadbleStream,
19
40
  onError: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
20
41
  }).pipe(
21
42
  Stream.pipeThroughChannel(Multipart.makeChannel(Object.fromEntries(source.headers)))
22
43
  )
23
44
 
45
+ const emptyReadbleStream = new ReadableStream({
46
+ start(controller) {
47
+ controller.enqueue(new Uint8Array())
48
+ controller.close()
49
+ }
50
+ })
51
+
24
52
  /**
25
- * @since 1.0.0
26
- * @category Constructors
53
+ * Parses and persists multipart data from a Bun `Request`, requiring file-system, path, and scope services.
54
+ *
55
+ * @category constructors
56
+ * @since 4.0.0
27
57
  */
28
58
  export const persisted = (
29
59
  source: Request
package/src/BunPath.ts CHANGED
@@ -1,24 +1,59 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Bun-backed layers for Effect's {@link Path} service.
3
+ *
4
+ * This module provides the `Path` service for Bun programs by reusing the
5
+ * shared Node-compatible path implementation. Provide one of these layers when
6
+ * Bun code should receive path operations from the Effect environment instead
7
+ * of importing runtime path helpers directly.
8
+ *
9
+ * **Mental model**
10
+ *
11
+ * Bun exposes Node-compatible path and file URL behavior, so the default
12
+ * {@link layer} follows the host operating system's rules. The
13
+ * {@link layerPosix} and {@link layerWin32} variants pin parsing and formatting
14
+ * to POSIX or Windows semantics for portable tests, generated paths, and
15
+ * cross-platform tooling.
16
+ *
17
+ * **Common tasks**
18
+ *
19
+ * Use {@link layer} for normal Bun applications and CLIs. Use
20
+ * {@link layerPosix} or {@link layerWin32} when code must produce stable path
21
+ * syntax regardless of the machine running Bun. `BunServices.layer` already
22
+ * includes {@link layer}, so import this module directly when only `Path` or a
23
+ * fixed platform variant is needed.
24
+ *
25
+ * **Gotchas**
26
+ *
27
+ * Path operations are syntactic. They normalize and convert strings and
28
+ * `file:` URLs, but they do not read the filesystem, check permissions, confirm
29
+ * that a path exists, or make request URLs safe to use as local paths.
30
+ *
31
+ * @since 4.0.0
3
32
  */
4
33
  import * as NodePath from "@effect/platform-node-shared/NodePath"
5
34
  import type * as Layer from "effect/Layer"
6
35
  import type { Path } from "effect/Path"
7
36
 
8
37
  /**
9
- * @since 1.0.0
10
- * @category layer
38
+ * Layer that provides the default `Path` service for Bun using the shared Node path implementation.
39
+ *
40
+ * @category layers
41
+ * @since 4.0.0
11
42
  */
12
43
  export const layer: Layer.Layer<Path> = NodePath.layer
13
44
 
14
45
  /**
15
- * @since 1.0.0
16
- * @category layer
46
+ * Layer that provides the POSIX `Path` service for Bun using the shared Node path implementation.
47
+ *
48
+ * @category layers
49
+ * @since 4.0.0
17
50
  */
18
51
  export const layerPosix: Layer.Layer<Path> = NodePath.layerPosix
19
52
 
20
53
  /**
21
- * @since 1.0.0
22
- * @category layer
54
+ * Layer that provides the Win32 `Path` service for Bun using the shared Node path implementation.
55
+ *
56
+ * @category layers
57
+ * @since 4.0.0
23
58
  */
24
59
  export const layerWin32: Layer.Layer<Path> = NodePath.layerWin32
package/src/BunRedis.ts CHANGED
@@ -1,20 +1,55 @@
1
1
  /**
2
- * @since 1.0.0
2
+ * Bun Redis integration backed by Bun's built-in `RedisClient`.
3
+ *
4
+ * This module creates scoped Bun `RedisClient` connections and exposes them as
5
+ * both the portable `Redis` service used by Effect persistence modules and the
6
+ * Bun-specific `BunRedis` service for direct access to the raw client. Use it in
7
+ * Bun applications that need Redis-backed persistence, persisted queues,
8
+ * distributed rate limiting, custom Redis commands, or Bun Redis features such
9
+ * as pub/sub.
10
+ *
11
+ * **Mental model**
12
+ *
13
+ * `layer` and `layerConfig` acquire one `RedisClient` for the layer scope. The
14
+ * same client backs the portable `Redis.Redis` command interface and the
15
+ * `BunRedis` service, and it is closed with `close` when the scope finalizes.
16
+ * Install the layer at the lifetime you want for that connection.
17
+ *
18
+ * **Common tasks**
19
+ *
20
+ * Pass a Redis URL or Bun `RedisOptions` to `layer` for direct configuration,
21
+ * or use `layerConfig` when connection settings should come from Effect
22
+ * configuration. Depend on `Redis.Redis` for persistence and rate limiter
23
+ * stores. Depend on `BunRedis` when you need `RedisClient` features that are
24
+ * not exposed by the portable service, including custom commands and pub/sub.
25
+ *
26
+ * **Gotchas**
27
+ *
28
+ * Pub/sub should normally use a separately scoped client so a subscription does
29
+ * not interfere with ordinary command traffic. Persistence and rate limiter
30
+ * stores build keys and Lua scripts on top of this service, so choose stable
31
+ * prefixes and store ids, account for persisted values that may fail to decode
32
+ * after schema changes, and avoid unbounded high-cardinality rate-limit keys
33
+ * without a cleanup or bounding strategy.
34
+ *
35
+ * @since 4.0.0
3
36
  */
4
37
  import { RedisClient, type RedisOptions } from "bun"
5
38
  import * as Config from "effect/Config"
39
+ import * as Context from "effect/Context"
6
40
  import * as Effect from "effect/Effect"
7
41
  import * as Fn from "effect/Function"
8
42
  import * as Layer from "effect/Layer"
9
43
  import * as Scope from "effect/Scope"
10
- import * as ServiceMap from "effect/ServiceMap"
11
44
  import * as Redis from "effect/unstable/persistence/Redis"
12
45
 
13
46
  /**
14
- * @since 1.0.0
15
- * @category Service
47
+ * Service tag for Bun Redis integration, exposing the raw `RedisClient` and a `use` helper that maps client promise failures to `RedisError`.
48
+ *
49
+ * @category services
50
+ * @since 4.0.0
16
51
  */
17
- export class BunRedis extends ServiceMap.Service<BunRedis, {
52
+ export class BunRedis extends Context.Service<BunRedis, {
18
53
  readonly client: RedisClient
19
54
  readonly use: <A>(f: (client: RedisClient) => Promise<A>) => Effect.Effect<A, Redis.RedisError>
20
55
  }>()("@effect/platform-bun/BunRedis") {}
@@ -47,28 +82,32 @@ const make = Effect.fnUntraced(function*(
47
82
  use
48
83
  })
49
84
 
50
- return ServiceMap.make(BunRedis, bunRedis).pipe(
51
- ServiceMap.add(Redis.Redis, redis)
85
+ return Context.make(BunRedis, bunRedis).pipe(
86
+ Context.add(Redis.Redis, redis)
52
87
  )
53
88
  })
54
89
 
55
90
  /**
56
- * @since 1.0.0
57
- * @category Layers
91
+ * Creates scoped Bun Redis layers for `Redis.Redis` and `BunRedis`, closing the underlying client when the scope finalizes.
92
+ *
93
+ * @category layers
94
+ * @since 4.0.0
58
95
  */
59
96
  export const layer = (
60
97
  options?: ({ readonly url?: string } & RedisOptions) | undefined
61
- ): Layer.Layer<Redis.Redis | BunRedis> => Layer.effectServices(make(options))
98
+ ): Layer.Layer<Redis.Redis | BunRedis> => Layer.effectContext(make(options))
62
99
 
63
100
  /**
64
- * @since 1.0.0
65
- * @category Layers
101
+ * Creates scoped Bun Redis layers from configurable Redis options, closing the underlying client when the scope finalizes.
102
+ *
103
+ * @category layers
104
+ * @since 4.0.0
66
105
  */
67
106
  export const layerConfig = (
68
107
  options: Config.Wrap<{ readonly url?: string } & RedisOptions>
69
108
  ): Layer.Layer<Redis.Redis | BunRedis, Config.ConfigError> =>
70
- Layer.effectServices(
71
- Config.unwrap(options).asEffect().pipe(
109
+ Layer.effectContext(
110
+ Config.unwrap(options).pipe(
72
111
  Effect.flatMap(make)
73
112
  )
74
113
  )