effect-start 0.9.0 → 0.10.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/package.json +12 -13
- package/src/BundleHttp.test.ts +1 -1
- package/src/Commander.test.ts +15 -15
- package/src/Commander.ts +58 -88
- package/src/EncryptedCookies.test.ts +4 -4
- package/src/FileHttpRouter.test.ts +81 -12
- package/src/FileHttpRouter.ts +115 -26
- package/src/FileRouter.ts +60 -162
- package/src/FileRouterCodegen.test.ts +250 -64
- package/src/FileRouterCodegen.ts +13 -56
- package/src/FileRouterPattern.test.ts +116 -0
- package/src/FileRouterPattern.ts +59 -0
- package/src/FileRouter_path.test.ts +63 -102
- package/src/FileSystemExtra.test.ts +226 -0
- package/src/FileSystemExtra.ts +24 -60
- package/src/HttpUtils.test.ts +68 -0
- package/src/HttpUtils.ts +15 -0
- package/src/HyperHtml.ts +24 -5
- package/src/JsModule.test.ts +1 -1
- package/src/NodeFileSystem.ts +764 -0
- package/src/Random.ts +59 -0
- package/src/Route.test.ts +471 -0
- package/src/Route.ts +298 -153
- package/src/RouteRender.ts +38 -0
- package/src/Router.ts +11 -33
- package/src/RouterPattern.test.ts +629 -0
- package/src/RouterPattern.ts +391 -0
- package/src/Start.ts +14 -52
- package/src/bun/BunBundle.test.ts +0 -3
- package/src/bun/BunHttpServer.ts +246 -0
- package/src/bun/BunHttpServer_web.ts +384 -0
- package/src/bun/BunRoute.test.ts +341 -0
- package/src/bun/BunRoute.ts +326 -0
- package/src/bun/BunRoute_bundles.test.ts +218 -0
- package/src/bun/BunRuntime.ts +33 -0
- package/src/bun/BunTailwindPlugin.test.ts +1 -1
- package/src/bun/_empty.html +1 -0
- package/src/bun/index.ts +2 -1
- package/src/testing.ts +12 -3
- package/src/Datastar.test.ts +0 -267
- package/src/Datastar.ts +0 -68
- package/src/bun/BunFullstackServer.ts +0 -45
- package/src/bun/BunFullstackServer_httpServer.ts +0 -541
- package/src/jsx-datastar.d.ts +0 -63
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import * as HttpApp from "@effect/platform/HttpApp"
|
|
2
|
+
import * as HttpServer from "@effect/platform/HttpServer"
|
|
3
|
+
import * as HttpServerError from "@effect/platform/HttpServerError"
|
|
4
|
+
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
5
|
+
import * as Socket from "@effect/platform/Socket"
|
|
6
|
+
import * as Bun from "bun"
|
|
7
|
+
import * as Config from "effect/Config"
|
|
8
|
+
import * as Context from "effect/Context"
|
|
9
|
+
import * as Deferred from "effect/Deferred"
|
|
10
|
+
import * as Effect from "effect/Effect"
|
|
11
|
+
import * as Exit from "effect/Exit"
|
|
12
|
+
import * as FiberSet from "effect/FiberSet"
|
|
13
|
+
import * as Layer from "effect/Layer"
|
|
14
|
+
import * as Option from "effect/Option"
|
|
15
|
+
import type * as Scope from "effect/Scope"
|
|
16
|
+
import * as Random from "../Random.ts"
|
|
17
|
+
import * as Router from "../Router.ts"
|
|
18
|
+
import EmptyHTML from "./_empty.html"
|
|
19
|
+
import {
|
|
20
|
+
makeResponse,
|
|
21
|
+
ServerRequestImpl,
|
|
22
|
+
WebSocketContext,
|
|
23
|
+
} from "./BunHttpServer_web.ts"
|
|
24
|
+
import * as BunRoute from "./BunRoute.ts"
|
|
25
|
+
|
|
26
|
+
type FetchHandler = (
|
|
27
|
+
request: Request,
|
|
28
|
+
server: Bun.Server<WebSocketContext>,
|
|
29
|
+
) => Response | Promise<Response>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Basically `Omit<Bun.Serve.Options, "fetch" | "error" | "websocket">`
|
|
33
|
+
* TypeScript 5.9 cannot verify discriminated union types used in
|
|
34
|
+
* {@link Bun.serve} so we need to define them explicitly.
|
|
35
|
+
*/
|
|
36
|
+
interface ServeOptions {
|
|
37
|
+
readonly port?: number
|
|
38
|
+
readonly hostname?: string
|
|
39
|
+
readonly reusePort?: boolean
|
|
40
|
+
readonly ipv6Only?: boolean
|
|
41
|
+
readonly idleTimeout?: number
|
|
42
|
+
readonly development?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type BunServer = {
|
|
46
|
+
readonly server: Bun.Server<WebSocketContext>
|
|
47
|
+
readonly addRoutes: (routes: BunRoute.BunRoutes) => void
|
|
48
|
+
// TODO: we probably don't want to expose these methods publicly
|
|
49
|
+
readonly pushHandler: (fetch: FetchHandler) => void
|
|
50
|
+
readonly popHandler: () => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const BunServer = Context.GenericTag<BunServer>(
|
|
54
|
+
"effect-start/BunServer",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
export const make = (
|
|
58
|
+
options: ServeOptions,
|
|
59
|
+
): Effect.Effect<
|
|
60
|
+
BunServer,
|
|
61
|
+
never,
|
|
62
|
+
Scope.Scope
|
|
63
|
+
> =>
|
|
64
|
+
Effect.gen(function*() {
|
|
65
|
+
const port = yield* Config.number("PORT").pipe(
|
|
66
|
+
Effect.catchTag("ConfigError", () => Effect.succeed(3000)),
|
|
67
|
+
)
|
|
68
|
+
const hostname = yield* Config.string("HOSTNAME").pipe(
|
|
69
|
+
Effect.catchTag("ConfigError", () => Effect.succeed(undefined)),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const handlerStack: Array<FetchHandler> = [
|
|
73
|
+
function(_request, _server) {
|
|
74
|
+
return new Response("not found", { status: 404 })
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
let currentRoutes: BunRoute.BunRoutes = {}
|
|
79
|
+
|
|
80
|
+
// Bun HMR doesn't work on successive calls to `server.reload` if there are no routes
|
|
81
|
+
// on server start. We workaround that by passing a dummy HTMLBundle [2025-11-26]
|
|
82
|
+
// see: https://github.com/oven-sh/bun/issues/23564
|
|
83
|
+
currentRoutes[`/.BunEmptyHtml-${Random.token(6)}`] = EmptyHTML
|
|
84
|
+
|
|
85
|
+
const websocket: Bun.WebSocketHandler<WebSocketContext> = {
|
|
86
|
+
open(ws) {
|
|
87
|
+
Deferred.unsafeDone(ws.data.deferred, Exit.succeed(ws))
|
|
88
|
+
},
|
|
89
|
+
message(ws, message) {
|
|
90
|
+
ws.data.run(message)
|
|
91
|
+
},
|
|
92
|
+
close(ws, code, closeReason) {
|
|
93
|
+
Deferred.unsafeDone(
|
|
94
|
+
ws.data.closeDeferred,
|
|
95
|
+
Socket.defaultCloseCodeIsError(code)
|
|
96
|
+
? Exit.fail(
|
|
97
|
+
new Socket.SocketCloseError({
|
|
98
|
+
reason: "Close",
|
|
99
|
+
code,
|
|
100
|
+
closeReason,
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
: Exit.void,
|
|
104
|
+
)
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const server = Bun.serve({
|
|
109
|
+
port,
|
|
110
|
+
hostname,
|
|
111
|
+
...options,
|
|
112
|
+
routes: currentRoutes,
|
|
113
|
+
fetch: handlerStack[0],
|
|
114
|
+
websocket,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
yield* Effect.addFinalizer(() =>
|
|
118
|
+
Effect.sync(() => {
|
|
119
|
+
server.stop()
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const reload = () => {
|
|
124
|
+
server.reload({
|
|
125
|
+
fetch: handlerStack[handlerStack.length - 1],
|
|
126
|
+
routes: currentRoutes,
|
|
127
|
+
websocket,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return BunServer.of({
|
|
132
|
+
server,
|
|
133
|
+
pushHandler(fetch) {
|
|
134
|
+
handlerStack.push(fetch)
|
|
135
|
+
reload()
|
|
136
|
+
},
|
|
137
|
+
popHandler() {
|
|
138
|
+
handlerStack.pop()
|
|
139
|
+
reload()
|
|
140
|
+
},
|
|
141
|
+
addRoutes(routes) {
|
|
142
|
+
currentRoutes = {
|
|
143
|
+
...currentRoutes,
|
|
144
|
+
...routes,
|
|
145
|
+
}
|
|
146
|
+
reload()
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
export const layer = (
|
|
152
|
+
options?: ServeOptions,
|
|
153
|
+
): Layer.Layer<BunServer> => Layer.scoped(BunServer, make(options ?? {}))
|
|
154
|
+
|
|
155
|
+
export const makeHttpServer: Effect.Effect<
|
|
156
|
+
HttpServer.HttpServer,
|
|
157
|
+
never,
|
|
158
|
+
Scope.Scope | BunServer
|
|
159
|
+
> = Effect.gen(function*() {
|
|
160
|
+
const bunServer = yield* BunServer
|
|
161
|
+
|
|
162
|
+
return HttpServer.make({
|
|
163
|
+
address: {
|
|
164
|
+
_tag: "TcpAddress",
|
|
165
|
+
port: bunServer.server.port!,
|
|
166
|
+
hostname: bunServer.server.hostname!,
|
|
167
|
+
},
|
|
168
|
+
serve(httpApp, middleware) {
|
|
169
|
+
return Effect.gen(function*() {
|
|
170
|
+
const runFork = yield* FiberSet.makeRuntime<never>()
|
|
171
|
+
const runtime = yield* Effect.runtime<never>()
|
|
172
|
+
const app = HttpApp.toHandled(
|
|
173
|
+
httpApp,
|
|
174
|
+
(request, response) =>
|
|
175
|
+
Effect.sync(() => {
|
|
176
|
+
;(request as ServerRequestImpl).resolve(
|
|
177
|
+
makeResponse(request, response, runtime),
|
|
178
|
+
)
|
|
179
|
+
}),
|
|
180
|
+
middleware,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
function handler(
|
|
184
|
+
request: Request,
|
|
185
|
+
server: Bun.Server<WebSocketContext>,
|
|
186
|
+
) {
|
|
187
|
+
return new Promise<Response>((resolve, _reject) => {
|
|
188
|
+
const fiber = runFork(Effect.provideService(
|
|
189
|
+
app,
|
|
190
|
+
HttpServerRequest.HttpServerRequest,
|
|
191
|
+
new ServerRequestImpl(
|
|
192
|
+
request,
|
|
193
|
+
resolve,
|
|
194
|
+
removeHost(request.url),
|
|
195
|
+
server,
|
|
196
|
+
),
|
|
197
|
+
))
|
|
198
|
+
request.signal.addEventListener("abort", () => {
|
|
199
|
+
runFork(
|
|
200
|
+
fiber.interruptAsFork(HttpServerError.clientAbortFiberId),
|
|
201
|
+
)
|
|
202
|
+
}, { once: true })
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
yield* Effect.acquireRelease(
|
|
207
|
+
Effect.sync(() => {
|
|
208
|
+
bunServer.pushHandler(handler)
|
|
209
|
+
}),
|
|
210
|
+
() =>
|
|
211
|
+
Effect.sync(() => {
|
|
212
|
+
bunServer.popHandler()
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
})
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
export const layerServer = (
|
|
221
|
+
options?: ServeOptions,
|
|
222
|
+
): Layer.Layer<HttpServer.HttpServer | BunServer> =>
|
|
223
|
+
Layer.provideMerge(
|
|
224
|
+
Layer.scoped(HttpServer.HttpServer, makeHttpServer),
|
|
225
|
+
layer(options ?? {}),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
export function layerRoutes() {
|
|
229
|
+
return Layer.effectDiscard(Effect.gen(function*() {
|
|
230
|
+
const bunServer = yield* BunServer
|
|
231
|
+
const router = yield* Effect.serviceOption(Router.Router)
|
|
232
|
+
|
|
233
|
+
if (Option.isSome(router)) {
|
|
234
|
+
const bunRoutes = yield* BunRoute.routesFromRouter(router.value)
|
|
235
|
+
bunServer.addRoutes(bunRoutes)
|
|
236
|
+
}
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const removeHost = (url: string) => {
|
|
241
|
+
if (url[0] === "/") {
|
|
242
|
+
return url
|
|
243
|
+
}
|
|
244
|
+
const index = url.indexOf("/", url.indexOf("//") + 2)
|
|
245
|
+
return index === -1 ? "/" : url.slice(index)
|
|
246
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import * as Cookies from "@effect/platform/Cookies"
|
|
2
|
+
import type * as FileSystem from "@effect/platform/FileSystem"
|
|
3
|
+
import * as Headers from "@effect/platform/Headers"
|
|
4
|
+
import * as HttpApp from "@effect/platform/HttpApp"
|
|
5
|
+
import * as HttpIncomingMessage from "@effect/platform/HttpIncomingMessage"
|
|
6
|
+
import type { HttpMethod } from "@effect/platform/HttpMethod"
|
|
7
|
+
import * as HttpServerError from "@effect/platform/HttpServerError"
|
|
8
|
+
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
9
|
+
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
10
|
+
import type * as Multipart from "@effect/platform/Multipart"
|
|
11
|
+
import type * as Path from "@effect/platform/Path"
|
|
12
|
+
import * as Socket from "@effect/platform/Socket"
|
|
13
|
+
import * as UrlParams from "@effect/platform/UrlParams"
|
|
14
|
+
import type {
|
|
15
|
+
Server as BunServerInstance,
|
|
16
|
+
ServerWebSocket,
|
|
17
|
+
} from "bun"
|
|
18
|
+
import * as Deferred from "effect/Deferred"
|
|
19
|
+
import * as Effect from "effect/Effect"
|
|
20
|
+
import * as FiberSet from "effect/FiberSet"
|
|
21
|
+
import * as Inspectable from "effect/Inspectable"
|
|
22
|
+
import * as Option from "effect/Option"
|
|
23
|
+
import type { ReadonlyRecord } from "effect/Record"
|
|
24
|
+
import * as Runtime from "effect/Runtime"
|
|
25
|
+
import type * as Scope from "effect/Scope"
|
|
26
|
+
import * as Stream from "effect/Stream"
|
|
27
|
+
|
|
28
|
+
export interface WebSocketContext {
|
|
29
|
+
readonly deferred: Deferred.Deferred<ServerWebSocket<WebSocketContext>>
|
|
30
|
+
readonly closeDeferred: Deferred.Deferred<void, Socket.SocketError>
|
|
31
|
+
readonly buffer: Array<Uint8Array | string>
|
|
32
|
+
run: (_: Uint8Array | string) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ServerRequestImpl extends Inspectable.Class
|
|
36
|
+
implements HttpServerRequest.HttpServerRequest
|
|
37
|
+
{
|
|
38
|
+
readonly [HttpServerRequest.TypeId]: HttpServerRequest.TypeId
|
|
39
|
+
readonly [HttpIncomingMessage.TypeId]: HttpIncomingMessage.TypeId
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
readonly source: Request,
|
|
43
|
+
public resolve: (response: Response) => void,
|
|
44
|
+
readonly url: string,
|
|
45
|
+
private bunServer: BunServerInstance<WebSocketContext>,
|
|
46
|
+
public headersOverride?: Headers.Headers,
|
|
47
|
+
private remoteAddressOverride?: string,
|
|
48
|
+
) {
|
|
49
|
+
super()
|
|
50
|
+
this[HttpServerRequest.TypeId] = HttpServerRequest.TypeId
|
|
51
|
+
this[HttpIncomingMessage.TypeId] = HttpIncomingMessage.TypeId
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toJSON(): unknown {
|
|
55
|
+
return HttpIncomingMessage.inspect(this, {
|
|
56
|
+
_id: "@effect/platform/HttpServerRequest",
|
|
57
|
+
method: this.method,
|
|
58
|
+
url: this.originalUrl,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
modify(
|
|
63
|
+
options: {
|
|
64
|
+
readonly url?: string | undefined
|
|
65
|
+
readonly headers?: Headers.Headers | undefined
|
|
66
|
+
readonly remoteAddress?: string | undefined
|
|
67
|
+
},
|
|
68
|
+
) {
|
|
69
|
+
return new ServerRequestImpl(
|
|
70
|
+
this.source,
|
|
71
|
+
this.resolve,
|
|
72
|
+
options.url ?? this.url,
|
|
73
|
+
this.bunServer,
|
|
74
|
+
options.headers ?? this.headersOverride,
|
|
75
|
+
options.remoteAddress ?? this.remoteAddressOverride,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get method(): HttpMethod {
|
|
80
|
+
return this.source.method.toUpperCase() as HttpMethod
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get originalUrl() {
|
|
84
|
+
return this.source.url
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get remoteAddress(): Option.Option<string> {
|
|
88
|
+
return this.remoteAddressOverride
|
|
89
|
+
? Option.some(this.remoteAddressOverride)
|
|
90
|
+
: Option.fromNullable(this.bunServer.requestIP(this.source)?.address)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get headers(): Headers.Headers {
|
|
94
|
+
this.headersOverride ??= Headers.fromInput(this.source.headers)
|
|
95
|
+
return this.headersOverride
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private cachedCookies: ReadonlyRecord<string, string> | undefined
|
|
99
|
+
get cookies() {
|
|
100
|
+
if (this.cachedCookies) {
|
|
101
|
+
return this.cachedCookies
|
|
102
|
+
}
|
|
103
|
+
return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get stream(): Stream.Stream<Uint8Array, HttpServerError.RequestError> {
|
|
107
|
+
return this.source.body
|
|
108
|
+
? Stream.fromReadableStream(
|
|
109
|
+
() => this.source.body as ReadableStream<Uint8Array>,
|
|
110
|
+
(cause) =>
|
|
111
|
+
new HttpServerError.RequestError({
|
|
112
|
+
request: this,
|
|
113
|
+
reason: "Decode",
|
|
114
|
+
cause,
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
: Stream.fail(
|
|
118
|
+
new HttpServerError.RequestError({
|
|
119
|
+
request: this,
|
|
120
|
+
reason: "Decode",
|
|
121
|
+
description: "can not create stream from empty body",
|
|
122
|
+
}),
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private textEffect:
|
|
127
|
+
| Effect.Effect<string, HttpServerError.RequestError>
|
|
128
|
+
| undefined
|
|
129
|
+
|
|
130
|
+
get text(): Effect.Effect<string, HttpServerError.RequestError> {
|
|
131
|
+
if (this.textEffect) {
|
|
132
|
+
return this.textEffect
|
|
133
|
+
}
|
|
134
|
+
this.textEffect = Effect.runSync(Effect.cached(
|
|
135
|
+
Effect.tryPromise({
|
|
136
|
+
try: () => this.source.text(),
|
|
137
|
+
catch: (cause) =>
|
|
138
|
+
new HttpServerError.RequestError({
|
|
139
|
+
request: this,
|
|
140
|
+
reason: "Decode",
|
|
141
|
+
cause,
|
|
142
|
+
}),
|
|
143
|
+
}),
|
|
144
|
+
))
|
|
145
|
+
return this.textEffect
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get json(): Effect.Effect<unknown, HttpServerError.RequestError> {
|
|
149
|
+
return Effect.tryMap(this.text, {
|
|
150
|
+
try: (_) => JSON.parse(_) as unknown,
|
|
151
|
+
catch: (cause) =>
|
|
152
|
+
new HttpServerError.RequestError({
|
|
153
|
+
request: this,
|
|
154
|
+
reason: "Decode",
|
|
155
|
+
cause,
|
|
156
|
+
}),
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get urlParamsBody(): Effect.Effect<
|
|
161
|
+
UrlParams.UrlParams,
|
|
162
|
+
HttpServerError.RequestError
|
|
163
|
+
> {
|
|
164
|
+
return Effect.flatMap(this.text, (_) =>
|
|
165
|
+
Effect.try({
|
|
166
|
+
try: () => UrlParams.fromInput(new URLSearchParams(_)),
|
|
167
|
+
catch: (cause) =>
|
|
168
|
+
new HttpServerError.RequestError({
|
|
169
|
+
request: this,
|
|
170
|
+
reason: "Decode",
|
|
171
|
+
cause,
|
|
172
|
+
}),
|
|
173
|
+
}))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private multipartEffect:
|
|
177
|
+
| Effect.Effect<
|
|
178
|
+
Multipart.Persisted,
|
|
179
|
+
Multipart.MultipartError,
|
|
180
|
+
Scope.Scope | FileSystem.FileSystem | Path.Path
|
|
181
|
+
>
|
|
182
|
+
| undefined
|
|
183
|
+
|
|
184
|
+
get multipart(): Effect.Effect<
|
|
185
|
+
Multipart.Persisted,
|
|
186
|
+
Multipart.MultipartError,
|
|
187
|
+
Scope.Scope | FileSystem.FileSystem | Path.Path
|
|
188
|
+
> {
|
|
189
|
+
if (this.multipartEffect) {
|
|
190
|
+
return this.multipartEffect
|
|
191
|
+
}
|
|
192
|
+
this.multipartEffect = Effect.runSync(Effect.cached(
|
|
193
|
+
Effect.die("Multipart not implemented"),
|
|
194
|
+
))
|
|
195
|
+
return this.multipartEffect
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get multipartStream(): Stream.Stream<
|
|
199
|
+
Multipart.Part,
|
|
200
|
+
Multipart.MultipartError
|
|
201
|
+
> {
|
|
202
|
+
return Stream.die("Multipart stream not implemented")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private arrayBufferEffect:
|
|
206
|
+
| Effect.Effect<ArrayBuffer, HttpServerError.RequestError>
|
|
207
|
+
| undefined
|
|
208
|
+
get arrayBuffer(): Effect.Effect<ArrayBuffer, HttpServerError.RequestError> {
|
|
209
|
+
if (this.arrayBufferEffect) {
|
|
210
|
+
return this.arrayBufferEffect
|
|
211
|
+
}
|
|
212
|
+
this.arrayBufferEffect = Effect.runSync(Effect.cached(
|
|
213
|
+
Effect.tryPromise({
|
|
214
|
+
try: () => this.source.arrayBuffer(),
|
|
215
|
+
catch: (cause) =>
|
|
216
|
+
new HttpServerError.RequestError({
|
|
217
|
+
request: this,
|
|
218
|
+
reason: "Decode",
|
|
219
|
+
cause,
|
|
220
|
+
}),
|
|
221
|
+
}),
|
|
222
|
+
))
|
|
223
|
+
return this.arrayBufferEffect
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
get upgrade(): Effect.Effect<Socket.Socket, HttpServerError.RequestError> {
|
|
227
|
+
return Effect.flatMap(
|
|
228
|
+
Effect.all([
|
|
229
|
+
Deferred.make<ServerWebSocket<WebSocketContext>>(),
|
|
230
|
+
Deferred.make<void, Socket.SocketError>(),
|
|
231
|
+
Effect.makeSemaphore(1),
|
|
232
|
+
]),
|
|
233
|
+
([deferred, closeDeferred, semaphore]) =>
|
|
234
|
+
Effect.async<Socket.Socket, HttpServerError.RequestError>((resume) => {
|
|
235
|
+
const success = this.bunServer.upgrade(
|
|
236
|
+
this.source,
|
|
237
|
+
{
|
|
238
|
+
data: {
|
|
239
|
+
deferred,
|
|
240
|
+
closeDeferred,
|
|
241
|
+
buffer: [],
|
|
242
|
+
run: wsDefaultRun,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
if (!success) {
|
|
247
|
+
resume(Effect.fail(
|
|
248
|
+
new HttpServerError.RequestError({
|
|
249
|
+
request: this,
|
|
250
|
+
reason: "Decode",
|
|
251
|
+
description: "Not an upgradeable ServerRequest",
|
|
252
|
+
}),
|
|
253
|
+
))
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
resume(Effect.map(Deferred.await(deferred), (ws) => {
|
|
257
|
+
const write = (chunk: Uint8Array | string | Socket.CloseEvent) =>
|
|
258
|
+
Effect.sync(() => {
|
|
259
|
+
if (typeof chunk === "string") {
|
|
260
|
+
ws.sendText(chunk)
|
|
261
|
+
} else if (Socket.isCloseEvent(chunk)) {
|
|
262
|
+
ws.close(chunk.code, chunk.reason)
|
|
263
|
+
} else {
|
|
264
|
+
ws.sendBinary(chunk)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return true
|
|
268
|
+
})
|
|
269
|
+
const writer = Effect.succeed(write)
|
|
270
|
+
const runRaw = Effect.fnUntraced(
|
|
271
|
+
function*<RR, EE, _>(
|
|
272
|
+
handler: (
|
|
273
|
+
_: Uint8Array | string,
|
|
274
|
+
) => Effect.Effect<_, EE, RR> | void,
|
|
275
|
+
opts?: { readonly onOpen?: Effect.Effect<void> | undefined },
|
|
276
|
+
) {
|
|
277
|
+
const set = yield* FiberSet.make<unknown, EE>()
|
|
278
|
+
const run = yield* FiberSet.runtime(set)<RR>()
|
|
279
|
+
function runRawInner(data: Uint8Array | string) {
|
|
280
|
+
const result = handler(data)
|
|
281
|
+
if (Effect.isEffect(result)) {
|
|
282
|
+
run(result)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
ws.data.run = runRawInner
|
|
286
|
+
ws.data.buffer.forEach(runRawInner)
|
|
287
|
+
ws.data.buffer.length = 0
|
|
288
|
+
if (opts?.onOpen) yield* opts.onOpen
|
|
289
|
+
return yield* FiberSet.join(set)
|
|
290
|
+
},
|
|
291
|
+
Effect.scoped,
|
|
292
|
+
Effect.onExit((exit) => {
|
|
293
|
+
ws.close(exit._tag === "Success" ? 1000 : 1011)
|
|
294
|
+
return Effect.void
|
|
295
|
+
}),
|
|
296
|
+
Effect.raceFirst(Deferred.await(closeDeferred)),
|
|
297
|
+
semaphore.withPermits(1),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
const encoder = new TextEncoder()
|
|
301
|
+
const run = <RR, EE, _>(
|
|
302
|
+
handler: (_: Uint8Array) => Effect.Effect<_, EE, RR> | void,
|
|
303
|
+
opts?: {
|
|
304
|
+
readonly onOpen?: Effect.Effect<void> | undefined
|
|
305
|
+
},
|
|
306
|
+
) =>
|
|
307
|
+
runRaw(
|
|
308
|
+
(data) =>
|
|
309
|
+
typeof data === "string"
|
|
310
|
+
? handler(encoder.encode(data))
|
|
311
|
+
: handler(data),
|
|
312
|
+
opts,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return Socket.Socket.of({
|
|
316
|
+
[Socket.TypeId]: Socket.TypeId,
|
|
317
|
+
run,
|
|
318
|
+
runRaw,
|
|
319
|
+
writer,
|
|
320
|
+
})
|
|
321
|
+
}))
|
|
322
|
+
}),
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function wsDefaultRun(this: WebSocketContext, _: Uint8Array | string) {
|
|
328
|
+
this.buffer.push(_)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function makeResponse(
|
|
332
|
+
request: HttpServerRequest.HttpServerRequest,
|
|
333
|
+
response: HttpServerResponse.HttpServerResponse,
|
|
334
|
+
runtime: Runtime.Runtime<never>,
|
|
335
|
+
): Response {
|
|
336
|
+
const fields: {
|
|
337
|
+
headers: globalThis.Headers
|
|
338
|
+
status?: number
|
|
339
|
+
statusText?: string
|
|
340
|
+
} = {
|
|
341
|
+
headers: new globalThis.Headers(response.headers),
|
|
342
|
+
status: response.status,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!Cookies.isEmpty(response.cookies)) {
|
|
346
|
+
for (const header of Cookies.toSetCookieHeaders(response.cookies)) {
|
|
347
|
+
fields.headers.append("set-cookie", header)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (response.statusText !== undefined) {
|
|
352
|
+
fields.statusText = response.statusText
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (request.method === "HEAD") {
|
|
356
|
+
return new Response(undefined, fields)
|
|
357
|
+
}
|
|
358
|
+
const ejectedResponse = HttpApp.unsafeEjectStreamScope(response)
|
|
359
|
+
const body = ejectedResponse.body
|
|
360
|
+
switch (body._tag) {
|
|
361
|
+
case "Empty": {
|
|
362
|
+
return new Response(undefined, fields)
|
|
363
|
+
}
|
|
364
|
+
case "Uint8Array":
|
|
365
|
+
case "Raw": {
|
|
366
|
+
if (body.body instanceof Response) {
|
|
367
|
+
for (const [key, value] of fields.headers.entries()) {
|
|
368
|
+
body.body.headers.set(key, value)
|
|
369
|
+
}
|
|
370
|
+
return body.body
|
|
371
|
+
}
|
|
372
|
+
return new Response(body.body as BodyInit, fields)
|
|
373
|
+
}
|
|
374
|
+
case "FormData": {
|
|
375
|
+
return new Response(body.formData as FormData, fields)
|
|
376
|
+
}
|
|
377
|
+
case "Stream": {
|
|
378
|
+
return new Response(
|
|
379
|
+
Stream.toReadableStreamRuntime(body.stream, runtime),
|
|
380
|
+
fields,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|