@effect/platform-bun 4.0.0-rc.111 → 4.0.0-rc.113

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 (49) hide show
  1. package/AGENTS.md +24 -9
  2. package/CLAUDE.md +24 -9
  3. package/ai-docs/package.json +2 -2
  4. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +18 -5
  5. package/ai-docs/src/01_effect/01_basics/index.md +5 -3
  6. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +1 -1
  7. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +2 -2
  8. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +2 -2
  9. package/ai-docs/src/03_stream/30_encoding.ts +5 -7
  10. package/ai-docs/src/08_observability/10_logging.ts +1 -1
  11. package/ai-docs/src/70_cli/10_basics.ts +7 -7
  12. package/ai-docs/src/71_ai/10_language-model.ts +2 -2
  13. package/ai-docs/src/71_ai/20_tools.ts +1 -1
  14. package/ai-docs/src/71_ai/30_chat.ts +1 -1
  15. package/dist/BunClusterHttp.d.ts +2 -2
  16. package/dist/BunClusterHttp.d.ts.map +1 -1
  17. package/dist/BunClusterHttp.js +3 -3
  18. package/dist/BunClusterHttp.js.map +1 -1
  19. package/dist/BunClusterSocket.d.ts +1 -1
  20. package/dist/BunClusterSocket.d.ts.map +1 -1
  21. package/dist/BunClusterSocket.js +2 -2
  22. package/dist/BunClusterSocket.js.map +1 -1
  23. package/dist/BunHttpPlatform.d.ts.map +1 -1
  24. package/dist/BunHttpPlatform.js +7 -4
  25. package/dist/BunHttpPlatform.js.map +1 -1
  26. package/dist/BunHttpServer.d.ts +22 -7
  27. package/dist/BunHttpServer.d.ts.map +1 -1
  28. package/dist/BunHttpServer.js +149 -39
  29. package/dist/BunHttpServer.js.map +1 -1
  30. package/dist/BunRedis.d.ts +1 -1
  31. package/dist/BunRedis.d.ts.map +1 -1
  32. package/dist/BunRedis.js +3 -12
  33. package/dist/BunRedis.js.map +1 -1
  34. package/dist/BunSocket.d.ts +3 -3
  35. package/dist/BunSocket.d.ts.map +1 -1
  36. package/dist/BunSocket.js +6 -3
  37. package/dist/BunSocket.js.map +1 -1
  38. package/dist/BunStream.d.ts +1 -1
  39. package/dist/BunStream.d.ts.map +1 -1
  40. package/dist/BunStream.js +8 -2
  41. package/dist/BunStream.js.map +1 -1
  42. package/package.json +5 -5
  43. package/src/BunClusterHttp.ts +3 -3
  44. package/src/BunClusterSocket.ts +4 -2
  45. package/src/BunHttpPlatform.ts +8 -4
  46. package/src/BunHttpServer.ts +180 -62
  47. package/src/BunRedis.ts +2 -1
  48. package/src/BunSocket.ts +7 -4
  49. package/src/BunStream.ts +12 -3
@@ -61,7 +61,7 @@ export const layer = <
61
61
  const Storage extends "local" | "sql" | "byo" = never
62
62
  >(
63
63
  options?: {
64
- readonly serialization?: "msgpack" | "ndjson" | undefined
64
+ readonly serialization?: "binary" | "ndjson" | undefined
65
65
  readonly serializationMaxBufferSize?: number | "unbounded" | undefined
66
66
  readonly clientOnly?: ClientOnly | undefined
67
67
  readonly storage?: Storage | undefined
@@ -124,7 +124,9 @@ export const layer = <
124
124
  Layer.provide(
125
125
  options?.serialization === "ndjson"
126
126
  ? RpcSerialization.layerNdjsonWith({ maxBufferSize: options?.serializationMaxBufferSize })
127
- : RpcSerialization.layerMsgPackWith({ maxBufferSize: options?.serializationMaxBufferSize })
127
+ : RpcSerialization.layerSchemaBinary({
128
+ maxFrameSize: options?.serializationMaxBufferSize
129
+ })
128
130
  )
129
131
  ) as any
130
132
  }
@@ -36,16 +36,20 @@ const make: Effect.Effect<
36
36
  > = Platform.make({
37
37
  platform: "bun",
38
38
  compression,
39
- fileResponse(path, status, statusText, headers, start, end, _contentLength) {
39
+ fileResponse(path, status, statusText, headers, start, end, contentLength) {
40
40
  let file = Bun.file(path)
41
41
  if (start > 0 || end !== undefined) {
42
42
  file = file.slice(start, end)
43
43
  }
44
- return Response.raw(file, { headers, status, statusText })
44
+ return Response.raw(file, {
45
+ headers: { ...headers, "content-length": contentLength.toString() },
46
+ status,
47
+ statusText
48
+ })
45
49
  },
46
50
  fileWebResponse(file, status, statusText, headers, options) {
47
- const start = Number(options?.offset ?? 0)
48
- const end = options?.bytesToRead !== undefined ? start + Number(options.bytesToRead) : undefined
51
+ const start = options?.offset ?? 0
52
+ const end = options?.bytesToRead !== undefined ? start + options.bytesToRead : undefined
49
53
  const body = start > 0 || end !== undefined
50
54
  ? (file as File).slice(start, end, file.type)
51
55
  : file
@@ -12,6 +12,7 @@
12
12
  * @since 4.0.0
13
13
  */
14
14
  import type { Server as BunServer, ServerWebSocket } from "bun"
15
+ import type * as Arr from "effect/Array"
15
16
  import * as Config from "effect/Config"
16
17
  import type { ConfigError } from "effect/Config"
17
18
  import * as Context from "effect/Context"
@@ -20,14 +21,15 @@ import * as Duration from "effect/Duration"
20
21
  import * as Effect from "effect/Effect"
21
22
  import * as Exit from "effect/Exit"
22
23
  import * as Fiber from "effect/Fiber"
23
- import * as FiberSet from "effect/FiberSet"
24
24
  import type * as FileSystem from "effect/FileSystem"
25
- import { flow } from "effect/Function"
25
+ import { constVoid, flow } from "effect/Function"
26
26
  import * as Inspectable from "effect/Inspectable"
27
27
  import * as Layer from "effect/Layer"
28
28
  import * as Option from "effect/Option"
29
29
  import type * as Path from "effect/Path"
30
30
  import type * as Record from "effect/Record"
31
+ import * as Result from "effect/Result"
32
+ import * as Scheduler from "effect/Scheduler"
31
33
  import type * as Schema from "effect/Schema"
32
34
  import * as Scope from "effect/Scope"
33
35
  import * as Semaphore from "effect/Semaphore"
@@ -47,6 +49,7 @@ import * as ServerRequest from "effect/unstable/http/HttpServerRequest"
47
49
  import type * as ServerResponse from "effect/unstable/http/HttpServerResponse"
48
50
  import type * as Multipart from "effect/unstable/http/Multipart"
49
51
  import * as UrlParams from "effect/unstable/http/UrlParams"
52
+ import * as NetAddress from "effect/unstable/net/NetAddress"
50
53
  import * as Socket from "effect/unstable/socket/Socket"
51
54
  import * as Platform from "./BunHttpPlatform.ts"
52
55
  import * as BunMultipart from "./BunMultipart.ts"
@@ -77,13 +80,27 @@ export type ServeOptions<R extends string> =
77
80
  * through, e.g.
78
81
  * `BunHttpServer.layer({ port: 3000, websocket: { perMessageDeflate: true } })`.
79
82
  *
83
+ * The `compressionThreshold` option controls the minimum message size in bytes
84
+ * that is compressed when per-message deflate is negotiated. It defaults to
85
+ * 1024, matching the default threshold of Node's `ws` server.
86
+ *
80
87
  * @category options
81
88
  * @since 4.0.0
82
89
  */
83
- export type WebSocketOptions = Omit<
84
- Bun.WebSocketHandler<WebSocketContext>,
85
- "open" | "message" | "close" | "drain" | "ping" | "pong" | "data" | "binaryType"
86
- >
90
+ export type WebSocketOptions =
91
+ & Omit<
92
+ Bun.WebSocketHandler<WebSocketContext>,
93
+ "open" | "message" | "close" | "drain" | "ping" | "pong" | "data" | "binaryType"
94
+ >
95
+ & {
96
+ /**
97
+ * The minimum message size in bytes that is compressed when per-message
98
+ * deflate is negotiated.
99
+ *
100
+ * @default 1024
101
+ */
102
+ readonly compressionThreshold?: number | undefined
103
+ }
87
104
 
88
105
  /**
89
106
  * Creates a scoped Bun `HttpServer` from `Bun.serve` options, stopping the server on scope finalization with optional graceful shutdown settings.
@@ -100,6 +117,23 @@ export const make = Effect.fnUntraced(
100
117
  }
101
118
  ) {
102
119
  const scope = yield* Effect.scope
120
+ let listenOptions = options
121
+ if (!("unix" in options) || options.unix === undefined) {
122
+ const internetOptions = options as Bun.Serve.HostnamePortServeOptions<WebSocketContext>
123
+ const hostname = internetOptions.hostname ?? "0.0.0.0"
124
+ if (Result.isFailure(NetAddress.ipFromString(hostname))) {
125
+ const resolved = yield* Effect.tryPromise({
126
+ try: async () => {
127
+ const result = await Bun.dns.lookup(hostname, { socketType: "tcp" })
128
+ if (result.length === 0) throw new globalThis.Error(`Could not resolve hostname: ${hostname}`)
129
+ return result[0].address
130
+ },
131
+ catch: (cause) => new Error.ServeError({ cause })
132
+ })
133
+ listenOptions = { ...options, hostname: resolved }
134
+ }
135
+ }
136
+ const { compressionThreshold = MIN_COMPRESSIBLE_SIZE, ...websocket } = options.websocket ?? {}
103
137
  const handlerStack: Array<(request: Request, server: BunServer<WebSocketContext>) => Response | Promise<Response>> =
104
138
  [
105
139
  function(_request, _server) {
@@ -107,10 +141,10 @@ export const make = Effect.fnUntraced(
107
141
  }
108
142
  ]
109
143
  const server = Bun.serve<WebSocketContext, R>({
110
- ...options as ServeOptions<R>,
144
+ ...listenOptions as ServeOptions<R>,
111
145
  fetch: handlerStack[0],
112
146
  websocket: {
113
- ...options.websocket,
147
+ ...websocket,
114
148
  open(ws) {
115
149
  Deferred.doneUnsafe(ws.data.deferred, Exit.succeed(ws))
116
150
  },
@@ -119,16 +153,11 @@ export const make = Effect.fnUntraced(
119
153
  },
120
154
  close(ws, code, closeReason) {
121
155
  code = typeof code === "number" ? code : 1001
122
- Deferred.doneUnsafe(
123
- ws.data.closeDeferred,
124
- Socket.defaultCloseCodeIsError(code)
125
- ? Exit.fail(
126
- new Socket.SocketError({
127
- reason: new Socket.SocketCloseError({ code, closeReason })
128
- })
129
- )
130
- : Exit.void
131
- )
156
+ const error = new Socket.SocketError({
157
+ reason: new Socket.SocketCloseError({ code, closeReason })
158
+ })
159
+ ws.data.closeError = error
160
+ ws.data.onClose(error)
132
161
  }
133
162
  }
134
163
  })
@@ -143,8 +172,14 @@ export const make = Effect.fnUntraced(
143
172
 
144
173
  yield* Scope.addFinalizer(scope, shutdown)
145
174
 
175
+ const address = "unix" in options && options.unix !== undefined
176
+ ? NetAddress.unixPathAddress(options.unix)
177
+ : yield* Effect.fromResult(NetAddress.inetAddressFromIpString(server.hostname!, server.port!)).pipe(
178
+ Effect.mapError((cause) => new Error.ServeError({ cause }))
179
+ )
180
+
146
181
  return Server.make({
147
- address: { _tag: "TcpAddress", port: server.port!, hostname: server.hostname! },
182
+ address,
148
183
  serve: Effect.fnUntraced(function*(httpApp, middleware) {
149
184
  const parent = yield* Effect.fiber
150
185
  const services = parent.context
@@ -161,7 +196,7 @@ export const make = Effect.fnUntraced(
161
196
  const context = Context.add(
162
197
  services,
163
198
  ServerRequest.HttpServerRequest,
164
- new BunServerRequest(request, resolve, removeHost(request.url), server)
199
+ new BunServerRequest(request, resolve, removeHost(request.url), server, compressionThreshold)
165
200
  )
166
201
  const fiber = Fiber.runIn(Effect.runForkWith(context)(httpEffect), scope)
167
202
  request.signal.addEventListener("abort", () => {
@@ -173,16 +208,24 @@ export const make = Effect.fnUntraced(
173
208
  yield* Scope.addFinalizerExit(serveScope, () => {
174
209
  const index = handlerStack.indexOf(handler)
175
210
  if (index !== -1) handlerStack.splice(index, 1)
176
- server.reload({ fetch: handlerStack[handlerStack.length - 1] })
211
+ server.reload({
212
+ fetch: handlerStack[handlerStack.length - 1],
213
+ ...(options.routes === undefined ? undefined : { routes: options.routes })
214
+ })
177
215
  return handlerStack.length === 1 ? preemptiveShutdown : Effect.void
178
216
  })
179
217
  handlerStack.push(handler)
180
- server.reload({ fetch: handler })
218
+ server.reload({
219
+ fetch: handler,
220
+ ...(options.routes === undefined ? undefined : { routes: options.routes })
221
+ })
181
222
  })
182
223
  })
183
224
  }
184
225
  )
185
226
 
227
+ const MIN_COMPRESSIBLE_SIZE = 1024
228
+
186
229
  const makeResponse = (
187
230
  request: ServerRequest.HttpServerRequest,
188
231
  response: ServerResponse.HttpServerResponse,
@@ -217,7 +260,9 @@ const makeResponse = (
217
260
  case "Empty": {
218
261
  return new Response(undefined, fields)
219
262
  }
220
- case "Uint8Array":
263
+ case "Uint8Array": {
264
+ return new Response(body.text ?? body.body as any, fields)
265
+ }
221
266
  case "Raw": {
222
267
  if (body.body instanceof Response) {
223
268
  for (const [key, value] of fields.headers.entries()) {
@@ -257,7 +302,7 @@ export const layerServer: <R extends string>(
257
302
  readonly gracefulShutdownTimeout?: Duration.Input | undefined
258
303
  readonly websocket?: WebSocketOptions | undefined
259
304
  }
260
- ) => Layer.Layer<Server.HttpServer> = flow(make, Layer.effect(Server.HttpServer)) as any
305
+ ) => Layer.Layer<Server.HttpServer, Error.ServeError> = flow(make, Layer.effect(Server.HttpServer)) as any
261
306
 
262
307
  /**
263
308
  * Layer that provides Bun HTTP support services: `HttpPlatform`, weak ETag generation, and `BunServices`.
@@ -291,7 +336,8 @@ export const layer = <R extends string>(
291
336
  | Server.HttpServer
292
337
  | HttpPlatform
293
338
  | Etag.Generator
294
- | BunServices.BunServices
339
+ | BunServices.BunServices,
340
+ Error.ServeError
295
341
  > => Layer.mergeAll(layerServer(options), layerHttpServices)
296
342
 
297
343
  /**
@@ -306,7 +352,7 @@ export const layerTest: Layer.Layer<
306
352
  Layer.provide(FetchHttpClient.layer.pipe(
307
353
  Layer.provide(Layer.succeed(FetchHttpClient.RequestInit)({ keepalive: false }))
308
354
  )),
309
- Layer.provideMerge(layer({ port: 0 }))
355
+ Layer.provideMerge(Layer.orDie(layer({ hostname: "127.0.0.1", port: 0 })))
310
356
  )
311
357
 
312
358
  /**
@@ -325,7 +371,7 @@ export const layerConfig = <R extends string>(
325
371
  >
326
372
  ): Layer.Layer<
327
373
  Server.HttpServer | HttpPlatform | FileSystem.FileSystem | Etag.Generator | Path.Path,
328
- ConfigError
374
+ ConfigError | Error.ServeError
329
375
  > =>
330
376
  Layer.mergeAll(
331
377
  Layer.effect(Server.HttpServer)(Effect.flatMap(Config.unwrap(options), make)),
@@ -338,9 +384,10 @@ export const layerConfig = <R extends string>(
338
384
 
339
385
  interface WebSocketContext {
340
386
  readonly deferred: Deferred.Deferred<ServerWebSocket<WebSocketContext>>
341
- readonly closeDeferred: Deferred.Deferred<void, Socket.SocketError>
342
387
  readonly buffer: Array<Uint8Array | string>
388
+ closeError: Socket.SocketError | undefined
343
389
  run: (_: Uint8Array | string) => void
390
+ onClose: (error: Socket.SocketError) => void
344
391
  }
345
392
 
346
393
  function wsDefaultRun(this: WebSocketContext, _: Uint8Array | string) {
@@ -354,6 +401,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
354
401
  public resolve: (response: Response) => void
355
402
  readonly url: string
356
403
  private bunServer: BunServer<WebSocketContext>
404
+ private compressionThreshold: number
357
405
  public headersOverride?: Headers.Headers | undefined
358
406
  private remoteAddressOverride?: Option.Option<string> | undefined
359
407
 
@@ -362,6 +410,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
362
410
  resolve: (response: Response) => void,
363
411
  url: string,
364
412
  bunServer: BunServer<WebSocketContext>,
413
+ compressionThreshold: number,
365
414
  headersOverride?: Headers.Headers,
366
415
  remoteAddressOverride?: Option.Option<string>
367
416
  ) {
@@ -372,6 +421,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
372
421
  this.resolve = resolve
373
422
  this.url = url
374
423
  this.bunServer = bunServer
424
+ this.compressionThreshold = compressionThreshold
375
425
  this.headersOverride = headersOverride
376
426
  this.remoteAddressOverride = remoteAddressOverride
377
427
  }
@@ -394,6 +444,7 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
394
444
  this.resolve,
395
445
  options.url ?? this.url,
396
446
  this.bunServer,
447
+ this.compressionThreshold,
397
448
  options.headers ?? this.headersOverride,
398
449
  "remoteAddress" in options ? options.remoteAddress : this.remoteAddressOverride
399
450
  )
@@ -539,15 +590,15 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
539
590
  get upgrade(): Effect.Effect<Socket.Socket, Error.HttpServerError> {
540
591
  return Effect.callback<Socket.Socket, Error.HttpServerError>((resume) => {
541
592
  const deferred = Deferred.makeUnsafe<ServerWebSocket<WebSocketContext>>()
542
- const closeDeferred = Deferred.makeUnsafe<void, Socket.SocketError>()
543
593
  const semaphore = Semaphore.makeUnsafe(1)
544
594
 
545
595
  const success = this.bunServer.upgrade(this.source, {
546
596
  data: {
547
597
  deferred,
548
- closeDeferred,
549
598
  buffer: [],
550
- run: wsDefaultRun
599
+ closeError: undefined,
600
+ run: wsDefaultRun,
601
+ onClose: constVoid
551
602
  }
552
603
  })
553
604
  if (!success) {
@@ -561,49 +612,116 @@ class BunServerRequest extends Inspectable.Class implements ServerRequest.HttpSe
561
612
  ))
562
613
  return
563
614
  }
615
+ const compressionThreshold = this.compressionThreshold
564
616
  resume(Effect.map(Deferred.await(deferred), (ws) => {
565
617
  const write = (chunk: Uint8Array | string | Socket.CloseEvent) =>
566
618
  Effect.sync(() => {
567
619
  if (typeof chunk === "string") {
568
- ws.sendText(chunk)
620
+ ws.sendText(chunk, chunk.length >= compressionThreshold)
569
621
  } else if (Socket.isCloseEvent(chunk)) {
570
622
  ws.close(chunk.code, chunk.reason)
571
623
  } else {
572
- ws.sendBinary(chunk)
624
+ ws.sendBinary(chunk, chunk.byteLength >= compressionThreshold)
573
625
  }
574
-
575
- return true
576
626
  })
577
- const writer = Effect.succeed(write)
578
- const runRaw = Effect.fnUntraced(
579
- function*<R, E, _>(
580
- handler: (_: Uint8Array | string) => Effect.Effect<_, E, R> | void,
581
- opts?: { readonly onOpen?: Effect.Effect<void> | undefined }
582
- ) {
583
- const set = yield* FiberSet.make<any, E>()
584
- const run = yield* FiberSet.runtime(set)<R>()
585
- function runRaw(data: Uint8Array | string) {
586
- const result = handler(data)
587
- if (Effect.isEffect(result)) {
588
- run(result)
627
+ const writeAll = (chunks: ReadonlyArray<Uint8Array | string>) =>
628
+ Effect.sync(() => {
629
+ for (let i = 0; i < chunks.length; i++) {
630
+ const chunk = chunks[i]
631
+ if (typeof chunk === "string") {
632
+ ws.sendText(chunk, chunk.length >= compressionThreshold)
633
+ } else {
634
+ ws.sendBinary(chunk, chunk.byteLength >= compressionThreshold)
589
635
  }
590
636
  }
591
- ws.data.run = runRaw
592
- ws.data.buffer.forEach(runRaw)
593
- ws.data.buffer.length = 0
594
- if (opts?.onOpen) yield* opts.onOpen
595
- return yield* FiberSet.join(set)
596
- },
597
- Effect.scoped,
598
- Effect.onExit((exit) => Effect.sync(() => ws.close(exit._tag === "Success" ? 1000 : 1011))),
599
- Effect.raceFirst(Deferred.await(closeDeferred)),
600
- semaphore.withPermits(1)
601
- )
602
-
603
- return Socket.make({
604
- runRaw,
605
- writer
637
+ })
638
+ const writer: Socket.Socket["writer"] = Effect.succeed({ write, writeAll })
639
+
640
+ const reader: Socket.Socket["reader"] = Effect.gen(function*() {
641
+ const dispatcher = (yield* Scheduler.Scheduler).makeDispatcher()
642
+ yield* Effect.acquireRelease(semaphore.take(1), () => semaphore.release(1))
643
+ const closeError = ws.data.closeError ?? (ws.readyState >= 2
644
+ ? new Socket.SocketError({
645
+ reason: new Socket.SocketCloseError({ code: 1006 })
646
+ })
647
+ : undefined)
648
+ if (closeError !== undefined && ws.data.buffer.length === 0) {
649
+ return yield* closeError
650
+ }
651
+ const scope = yield* Effect.scope
652
+
653
+ type ReadResume = (
654
+ effect: Effect.Effect<Arr.NonEmptyReadonlyArray<Uint8Array | string>, Socket.SocketError>
655
+ ) => void
656
+
657
+ let buffer: Array<Uint8Array | string> = ws.data.buffer.splice(0)
658
+ let error: Socket.SocketError | undefined = closeError
659
+ let waiter: ReadResume | undefined
660
+ let flushScheduled = false
661
+
662
+ function takeBuffer(): Arr.NonEmptyReadonlyArray<Uint8Array | string> {
663
+ const chunk = buffer
664
+ buffer = []
665
+ return chunk as unknown as Arr.NonEmptyReadonlyArray<Uint8Array | string>
666
+ }
667
+ function deliver() {
668
+ flushScheduled = false
669
+ if (waiter === undefined || buffer.length === 0) return
670
+ const resumeRead = waiter
671
+ waiter = undefined
672
+ resumeRead(Effect.succeed(takeBuffer()))
673
+ }
674
+ function push(data: Uint8Array | string) {
675
+ buffer.push(data)
676
+ if (waiter !== undefined && !flushScheduled) {
677
+ flushScheduled = true
678
+ dispatcher.scheduleTask(deliver, 0)
679
+ }
680
+ }
681
+ function fail(err: Socket.SocketError) {
682
+ if (error === undefined) error = err
683
+ if (waiter !== undefined) {
684
+ const resumeRead = waiter
685
+ waiter = undefined
686
+ resumeRead(buffer.length > 0 ? Effect.succeed(takeBuffer()) : Effect.fail(error))
687
+ }
688
+ }
689
+
690
+ ws.data.run = push
691
+ ws.data.onClose = fail
692
+ yield* Scope.addFinalizer(
693
+ scope,
694
+ Effect.suspend(() => {
695
+ // resume a pull blocked in another fiber before detaching
696
+ fail(
697
+ new Socket.SocketError({
698
+ reason: new Socket.SocketCloseError({ code: 1006 })
699
+ })
700
+ )
701
+ ws.data.run = wsDefaultRun
702
+ ws.data.onClose = constVoid
703
+ ws.close(1000)
704
+ return Effect.void
705
+ })
706
+ )
707
+
708
+ return {
709
+ pull: Effect.callback<
710
+ Arr.NonEmptyReadonlyArray<Uint8Array | string>,
711
+ Socket.SocketError
712
+ >((resumeRead) => {
713
+ if (buffer.length > 0) return resumeRead(Effect.succeed(takeBuffer()))
714
+ if (error !== undefined) return resumeRead(Effect.fail(error))
715
+ waiter = resumeRead
716
+ return Effect.sync(() => {
717
+ if (waiter === resumeRead) waiter = undefined
718
+ })
719
+ }),
720
+ upgrade: Socket.SocketUpgradeError.unsupported
721
+ }
606
722
  })
723
+
724
+ return Socket.make({ reader, writer })
607
725
  }))
608
726
  })
609
727
  }
package/src/BunRedis.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @since 4.0.0
11
11
  */
12
- import { RedisClient, type RedisOptions } from "bun"
12
+ import type { RedisClient, RedisOptions } from "bun"
13
13
  import * as Config from "effect/Config"
14
14
  import * as Context from "effect/Context"
15
15
  import * as Deferred from "effect/Deferred"
@@ -35,6 +35,7 @@ const make = Effect.fnUntraced(function*(
35
35
  readonly url?: string
36
36
  } & RedisOptions
37
37
  ) {
38
+ const { RedisClient } = yield* Effect.promise(() => import("bun"))
38
39
  const scope = yield* Effect.scope
39
40
  yield* Scope.addFinalizer(scope, Effect.sync(() => client.close()))
40
41
  const client = new RedisClient(options?.url, options)
package/src/BunSocket.ts CHANGED
@@ -29,13 +29,16 @@ export * from "@effect/platform-node-shared/NodeSocket"
29
29
  export const layerWebSocketConstructor: Layer.Layer<
30
30
  Socket.WebSocketConstructor
31
31
  > = Layer.succeed(Socket.WebSocketConstructor)(
32
- (url, protocols) => new globalThis.WebSocket(url, protocols)
32
+ (url, options) =>
33
+ // Bun accepts `WebSocketOptions`, but `bun-types` selects the DOM overload
34
+ // when `lib.dom` is loaded and hides those constructor options.
35
+ new globalThis.WebSocket(url, options as string | Array<string> | undefined)
33
36
  )
34
37
 
35
38
  /**
36
39
  * Creates a `Socket.Socket` layer for a WebSocket URL using Bun's global
37
- * `WebSocket` constructor, honoring protocol, open-timeout, and close-code
38
- * error options.
40
+ * `WebSocket` constructor, honoring protocol, open-timeout, and
41
+ * high-water-mark options.
39
42
  *
40
43
  * @category layers
41
44
  * @since 4.0.0
@@ -43,9 +46,9 @@ export const layerWebSocketConstructor: Layer.Layer<
43
46
  export const layerWebSocket: (
44
47
  url: string | Effect<string>,
45
48
  options?: {
46
- readonly closeCodeIsError?: ((code: number) => boolean) | undefined
47
49
  readonly openTimeout?: Duration.Input | undefined
48
50
  readonly protocols?: string | Array<string> | undefined
51
+ readonly highWaterMark?: number | undefined
49
52
  } | undefined
50
53
  ) => Layer.Layer<Socket.Socket, never, never> = flow(
51
54
  Socket.makeWebSocket,
package/src/BunStream.ts CHANGED
@@ -13,7 +13,7 @@ import * as Arr from "effect/Array"
13
13
  import * as Cause from "effect/Cause"
14
14
  import * as Channel from "effect/Channel"
15
15
  import * as Effect from "effect/Effect"
16
- import type { LazyArg } from "effect/Function"
16
+ import { constVoid, type LazyArg } from "effect/Function"
17
17
  import type * as Pull from "effect/Pull"
18
18
  import * as Scope from "effect/Scope"
19
19
  import * as Stream from "effect/Stream"
@@ -41,10 +41,19 @@ export const fromReadableStream = <A, E>(
41
41
  const reader = options.evaluate().getReader()
42
42
  yield* Scope.addFinalizer(
43
43
  scope,
44
- options.releaseLockOnEnd ? Effect.sync(() => reader.releaseLock()) : Effect.promise(() => reader.cancel())
44
+ options.releaseLockOnEnd
45
+ ? Effect.sync(() => reader.releaseLock())
46
+ : Effect.promise(() => reader.cancel().catch(constVoid))
45
47
  )
46
48
  function readMany(): Pull.Pull<Arr.NonEmptyReadonlyArray<A>, E> {
47
- const result = reader.readMany()
49
+ let result:
50
+ | Bun.ReadableStreamDefaultReadManyResult<A>
51
+ | Promise<Bun.ReadableStreamDefaultReadManyResult<A>>
52
+ try {
53
+ result = reader.readMany()
54
+ } catch (error) {
55
+ return Effect.fail(options.onError(error))
56
+ }
48
57
  if ("then" in result) {
49
58
  return Effect.callback<Arr.NonEmptyReadonlyArray<A>, E | Cause.Done>((resume) => {
50
59
  result.then((_) => resume(handleResult(_)), (e) => resume(Effect.fail(options.onError(e))))