@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.
- package/AGENTS.md +24 -9
- package/CLAUDE.md +24 -9
- package/ai-docs/package.json +2 -2
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +18 -5
- package/ai-docs/src/01_effect/01_basics/index.md +5 -3
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +1 -1
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +2 -2
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +2 -2
- package/ai-docs/src/03_stream/30_encoding.ts +5 -7
- package/ai-docs/src/08_observability/10_logging.ts +1 -1
- package/ai-docs/src/70_cli/10_basics.ts +7 -7
- package/ai-docs/src/71_ai/10_language-model.ts +2 -2
- package/ai-docs/src/71_ai/20_tools.ts +1 -1
- package/ai-docs/src/71_ai/30_chat.ts +1 -1
- package/dist/BunClusterHttp.d.ts +2 -2
- package/dist/BunClusterHttp.d.ts.map +1 -1
- package/dist/BunClusterHttp.js +3 -3
- package/dist/BunClusterHttp.js.map +1 -1
- package/dist/BunClusterSocket.d.ts +1 -1
- package/dist/BunClusterSocket.d.ts.map +1 -1
- package/dist/BunClusterSocket.js +2 -2
- package/dist/BunClusterSocket.js.map +1 -1
- package/dist/BunHttpPlatform.d.ts.map +1 -1
- package/dist/BunHttpPlatform.js +7 -4
- package/dist/BunHttpPlatform.js.map +1 -1
- package/dist/BunHttpServer.d.ts +22 -7
- package/dist/BunHttpServer.d.ts.map +1 -1
- package/dist/BunHttpServer.js +149 -39
- package/dist/BunHttpServer.js.map +1 -1
- package/dist/BunRedis.d.ts +1 -1
- package/dist/BunRedis.d.ts.map +1 -1
- package/dist/BunRedis.js +3 -12
- package/dist/BunRedis.js.map +1 -1
- package/dist/BunSocket.d.ts +3 -3
- package/dist/BunSocket.d.ts.map +1 -1
- package/dist/BunSocket.js +6 -3
- package/dist/BunSocket.js.map +1 -1
- package/dist/BunStream.d.ts +1 -1
- package/dist/BunStream.d.ts.map +1 -1
- package/dist/BunStream.js +8 -2
- package/dist/BunStream.js.map +1 -1
- package/package.json +5 -5
- package/src/BunClusterHttp.ts +3 -3
- package/src/BunClusterSocket.ts +4 -2
- package/src/BunHttpPlatform.ts +8 -4
- package/src/BunHttpServer.ts +180 -62
- package/src/BunRedis.ts +2 -1
- package/src/BunSocket.ts +7 -4
- package/src/BunStream.ts +12 -3
package/src/BunClusterSocket.ts
CHANGED
|
@@ -61,7 +61,7 @@ export const layer = <
|
|
|
61
61
|
const Storage extends "local" | "sql" | "byo" = never
|
|
62
62
|
>(
|
|
63
63
|
options?: {
|
|
64
|
-
readonly serialization?: "
|
|
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.
|
|
127
|
+
: RpcSerialization.layerSchemaBinary({
|
|
128
|
+
maxFrameSize: options?.serializationMaxBufferSize
|
|
129
|
+
})
|
|
128
130
|
)
|
|
129
131
|
) as any
|
|
130
132
|
}
|
package/src/BunHttpPlatform.ts
CHANGED
|
@@ -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,
|
|
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, {
|
|
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 =
|
|
48
|
-
const end = options?.bytesToRead !== undefined ? start +
|
|
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
|
package/src/BunHttpServer.ts
CHANGED
|
@@ -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 =
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
...
|
|
144
|
+
...listenOptions as ServeOptions<R>,
|
|
111
145
|
fetch: handlerStack[0],
|
|
112
146
|
websocket: {
|
|
113
|
-
...
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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,
|
|
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,
|
|
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
|
|
38
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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))))
|