effect-start 0.9.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/LICENSE +21 -0
- package/README.md +109 -0
- package/package.json +57 -0
- package/src/Bundle.ts +167 -0
- package/src/BundleFiles.ts +174 -0
- package/src/BundleHttp.test.ts +160 -0
- package/src/BundleHttp.ts +259 -0
- package/src/Commander.test.ts +1378 -0
- package/src/Commander.ts +672 -0
- package/src/Datastar.test.ts +267 -0
- package/src/Datastar.ts +68 -0
- package/src/Effect_HttpRouter.test.ts +570 -0
- package/src/EncryptedCookies.test.ts +427 -0
- package/src/EncryptedCookies.ts +451 -0
- package/src/FileHttpRouter.test.ts +207 -0
- package/src/FileHttpRouter.ts +122 -0
- package/src/FileRouter.ts +405 -0
- package/src/FileRouterCodegen.test.ts +598 -0
- package/src/FileRouterCodegen.ts +251 -0
- package/src/FileRouter_files.test.ts +64 -0
- package/src/FileRouter_path.test.ts +132 -0
- package/src/FileRouter_tree.test.ts +126 -0
- package/src/FileSystemExtra.ts +102 -0
- package/src/HttpAppExtra.ts +127 -0
- package/src/Hyper.ts +194 -0
- package/src/HyperHtml.test.ts +90 -0
- package/src/HyperHtml.ts +139 -0
- package/src/HyperNode.ts +37 -0
- package/src/JsModule.test.ts +14 -0
- package/src/JsModule.ts +116 -0
- package/src/PublicDirectory.test.ts +280 -0
- package/src/PublicDirectory.ts +108 -0
- package/src/Route.test.ts +873 -0
- package/src/Route.ts +992 -0
- package/src/Router.ts +80 -0
- package/src/SseHttpResponse.ts +55 -0
- package/src/Start.ts +133 -0
- package/src/StartApp.ts +43 -0
- package/src/StartHttp.ts +42 -0
- package/src/StreamExtra.ts +146 -0
- package/src/TestHttpClient.test.ts +54 -0
- package/src/TestHttpClient.ts +100 -0
- package/src/bun/BunBundle.test.ts +277 -0
- package/src/bun/BunBundle.ts +309 -0
- package/src/bun/BunBundle_imports.test.ts +50 -0
- package/src/bun/BunFullstackServer.ts +45 -0
- package/src/bun/BunFullstackServer_httpServer.ts +541 -0
- package/src/bun/BunImportTrackerPlugin.test.ts +77 -0
- package/src/bun/BunImportTrackerPlugin.ts +97 -0
- package/src/bun/BunTailwindPlugin.test.ts +335 -0
- package/src/bun/BunTailwindPlugin.ts +322 -0
- package/src/bun/BunVirtualFilesPlugin.ts +59 -0
- package/src/bun/index.ts +4 -0
- package/src/client/Overlay.ts +34 -0
- package/src/client/ScrollState.ts +120 -0
- package/src/client/index.ts +101 -0
- package/src/index.ts +24 -0
- package/src/jsx-datastar.d.ts +63 -0
- package/src/jsx-runtime.ts +23 -0
- package/src/jsx.d.ts +4402 -0
- package/src/testing.ts +55 -0
- package/src/x/cloudflare/CloudflareTunnel.ts +110 -0
- package/src/x/cloudflare/index.ts +1 -0
- package/src/x/datastar/Datastar.test.ts +267 -0
- package/src/x/datastar/Datastar.ts +68 -0
- package/src/x/datastar/index.ts +4 -0
- package/src/x/datastar/jsx-datastar.d.ts +63 -0
package/src/Router.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as HttpApp from "@effect/platform/HttpApp"
|
|
2
|
+
import * as HttpRouter from "@effect/platform/HttpRouter"
|
|
3
|
+
import * as Context from "effect/Context"
|
|
4
|
+
import * as Effect from "effect/Effect"
|
|
5
|
+
import * as Function from "effect/Function"
|
|
6
|
+
import * as Layer from "effect/Layer"
|
|
7
|
+
import * as FileHttpRouter from "./FileHttpRouter.ts"
|
|
8
|
+
import * as FileRouter from "./FileRouter.ts"
|
|
9
|
+
import * as Route from "./Route"
|
|
10
|
+
|
|
11
|
+
export const ServerMethods = [
|
|
12
|
+
"GET",
|
|
13
|
+
"POST",
|
|
14
|
+
"PUT",
|
|
15
|
+
"PATCH",
|
|
16
|
+
"DELETE",
|
|
17
|
+
"OPTIONS",
|
|
18
|
+
"HEAD",
|
|
19
|
+
] as const
|
|
20
|
+
|
|
21
|
+
export type ServerMethod = (typeof ServerMethods)[number]
|
|
22
|
+
|
|
23
|
+
export type ServerModule = {
|
|
24
|
+
default: Route.Route | Route.RouteSet.Default
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ServerRoute = {
|
|
28
|
+
path: `/${string}`
|
|
29
|
+
segments: readonly FileRouter.Segment[]
|
|
30
|
+
load: () => Promise<ServerModule>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type RouteManifest = {
|
|
34
|
+
modules: readonly FileRouter.RouteModule[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type RouterContext =
|
|
38
|
+
& RouteManifest
|
|
39
|
+
& {
|
|
40
|
+
httpRouter: HttpRouter.HttpRouter
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class Router extends Context.Tag("effect-start/Router")<
|
|
44
|
+
Router,
|
|
45
|
+
RouterContext
|
|
46
|
+
>() {}
|
|
47
|
+
|
|
48
|
+
export function layer(
|
|
49
|
+
manifest: RouteManifest,
|
|
50
|
+
): Layer.Layer<Router, never, never> {
|
|
51
|
+
return Layer.effect(
|
|
52
|
+
Router,
|
|
53
|
+
Effect.gen(function*() {
|
|
54
|
+
const serverRoutes = manifest.modules.map((mod) => ({
|
|
55
|
+
path: mod.path,
|
|
56
|
+
load: mod.load,
|
|
57
|
+
}))
|
|
58
|
+
const httpRouter = yield* FileHttpRouter.make(serverRoutes)
|
|
59
|
+
return {
|
|
60
|
+
...manifest,
|
|
61
|
+
httpRouter,
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function layerPromise(
|
|
68
|
+
load: () => Promise<RouteManifest>,
|
|
69
|
+
): Layer.Layer<Router, never, never> {
|
|
70
|
+
return Layer.unwrapEffect(
|
|
71
|
+
Effect.gen(function*() {
|
|
72
|
+
const importedModule = yield* Function.pipe(
|
|
73
|
+
Effect.promise(() => load()),
|
|
74
|
+
Effect.orDie,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return layer(importedModule)
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
2
|
+
import * as Duration from "effect/Duration"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Function from "effect/Function"
|
|
5
|
+
import * as Schedule from "effect/Schedule"
|
|
6
|
+
import * as Stream from "effect/Stream"
|
|
7
|
+
import * as StreamExtra from "./StreamExtra.ts"
|
|
8
|
+
|
|
9
|
+
const DefaultHeartbeatInterval = Duration.seconds(5)
|
|
10
|
+
|
|
11
|
+
export const make = <T = any>(stream: Stream.Stream<T, any>, options?: {
|
|
12
|
+
heartbeatInterval?: Duration.DurationInput
|
|
13
|
+
}) =>
|
|
14
|
+
Effect.gen(function*() {
|
|
15
|
+
const heartbeat = Stream.repeat(
|
|
16
|
+
Stream.succeed(null),
|
|
17
|
+
Schedule.spaced(options?.heartbeatInterval ?? DefaultHeartbeatInterval),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const encoder = new TextEncoder()
|
|
21
|
+
|
|
22
|
+
const events = Function.pipe(
|
|
23
|
+
Stream.merge(
|
|
24
|
+
heartbeat.pipe(
|
|
25
|
+
Stream.map(() => ":\n\n"),
|
|
26
|
+
),
|
|
27
|
+
stream.pipe(
|
|
28
|
+
Stream.map(event => `data: ${JSON.stringify(event)}\n\n`),
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
// without Stream.tap, only two events are sent
|
|
32
|
+
// Effect.fork(Stream.runDrain) doesn't seem to work.
|
|
33
|
+
// Asked for help here: [2025-04-09]
|
|
34
|
+
// https://discord.com/channels/795981131316985866/1359523331400929341
|
|
35
|
+
Stream.tap(v => Effect.gen(function*() {})),
|
|
36
|
+
Stream.map(str => encoder.encode(str)),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const toStream = StreamExtra.toReadableStreamRuntimePatched(
|
|
40
|
+
yield* Effect.runtime(),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// see toStream to understand why we're not using
|
|
44
|
+
// HttpServerResponse.stream here.
|
|
45
|
+
return HttpServerResponse.raw(
|
|
46
|
+
toStream(events),
|
|
47
|
+
{
|
|
48
|
+
headers: {
|
|
49
|
+
"Content-Type": "text/event-stream",
|
|
50
|
+
"Cache-Control": "no-cache",
|
|
51
|
+
"Connection": "keep-alive",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
})
|
package/src/Start.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as BunContext from "@effect/platform-bun/BunContext"
|
|
2
|
+
import * as BunHttpServer from "@effect/platform-bun/BunHttpServer"
|
|
3
|
+
import * as BunRuntime from "@effect/platform-bun/BunRuntime"
|
|
4
|
+
import * as FetchHttpClient from "@effect/platform/FetchHttpClient"
|
|
5
|
+
import * as HttpClient from "@effect/platform/HttpClient"
|
|
6
|
+
import * as HttpRouter from "@effect/platform/HttpRouter"
|
|
7
|
+
import * as HttpServer from "@effect/platform/HttpServer"
|
|
8
|
+
import * as Effect from "effect/Effect"
|
|
9
|
+
import * as Function from "effect/Function"
|
|
10
|
+
import * as Layer from "effect/Layer"
|
|
11
|
+
import * as BunBundle from "./bun/BunBundle.ts"
|
|
12
|
+
import * as Bundle from "./Bundle.ts"
|
|
13
|
+
import * as BundleHttp from "./BundleHttp.ts"
|
|
14
|
+
import * as FileRouter from "./FileRouter.ts"
|
|
15
|
+
import * as HttpAppExtra from "./HttpAppExtra.ts"
|
|
16
|
+
import * as Router from "./Router.ts"
|
|
17
|
+
import * as StartApp from "./StartApp.ts"
|
|
18
|
+
|
|
19
|
+
// TODO: we probably want to remove this API to avoid
|
|
20
|
+
// multiple entrypoints for routers and bundles.
|
|
21
|
+
// We could handle endpoints routing in {@link layer}
|
|
22
|
+
// or {@link serve}.
|
|
23
|
+
// Serve probably makes more sense because it's an entrypoint
|
|
24
|
+
// for serving an HTTP server
|
|
25
|
+
export function router(options: {
|
|
26
|
+
load: () => Promise<Router.RouteManifest>
|
|
27
|
+
path: string
|
|
28
|
+
}) {
|
|
29
|
+
return Layer.provideMerge(
|
|
30
|
+
// add it to BundleHttp
|
|
31
|
+
Layer.effectDiscard(
|
|
32
|
+
Effect.gen(function*() {
|
|
33
|
+
const httpRouter = yield* HttpRouter.Default
|
|
34
|
+
const startRouter = yield* Router.Router
|
|
35
|
+
|
|
36
|
+
yield* httpRouter.concat(startRouter.httpRouter)
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
Layer.merge(
|
|
40
|
+
Router.layerPromise(options.load),
|
|
41
|
+
FileRouter.layer(options),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function bundleClient(config: BunBundle.BuildOptions | string) {
|
|
47
|
+
const clientLayer = Layer.effect(
|
|
48
|
+
Bundle.ClientBundle,
|
|
49
|
+
Function.pipe(
|
|
50
|
+
BunBundle.buildClient(config),
|
|
51
|
+
Bundle.handleBundleErrorSilently,
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
const assetsLayer = Layer.effectDiscard(Effect.gen(function*() {
|
|
55
|
+
const router = yield* HttpRouter.Default
|
|
56
|
+
const app = BundleHttp.toHttpApp(Bundle.ClientBundle)
|
|
57
|
+
|
|
58
|
+
yield* router.mountApp(
|
|
59
|
+
"/_bundle",
|
|
60
|
+
// we need to use as any here because HttpRouter.Default
|
|
61
|
+
// only accepts default services.
|
|
62
|
+
app as any,
|
|
63
|
+
)
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
return Layer.mergeAll(
|
|
67
|
+
clientLayer,
|
|
68
|
+
assetsLayer,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function layer<
|
|
73
|
+
Layers extends [
|
|
74
|
+
Layer.Layer<never, any, any>,
|
|
75
|
+
...Array<Layer.Layer<never, any, any>>,
|
|
76
|
+
],
|
|
77
|
+
>(...layers: Layers): Layer.Layer<
|
|
78
|
+
{ [k in keyof Layers]: Layer.Layer.Success<Layers[k]> }[number],
|
|
79
|
+
{ [k in keyof Layers]: Layer.Layer.Error<Layers[k]> }[number],
|
|
80
|
+
{ [k in keyof Layers]: Layer.Layer.Context<Layers[k]> }[number]
|
|
81
|
+
> {
|
|
82
|
+
return Layer.mergeAll(...layers)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function serve<ROut, E>(
|
|
86
|
+
load: () => Promise<{
|
|
87
|
+
default: Layer.Layer<
|
|
88
|
+
ROut,
|
|
89
|
+
E,
|
|
90
|
+
| HttpServer.HttpServer
|
|
91
|
+
| HttpRouter.Default
|
|
92
|
+
| HttpClient.HttpClient
|
|
93
|
+
| BunContext.BunContext
|
|
94
|
+
>
|
|
95
|
+
}>,
|
|
96
|
+
) {
|
|
97
|
+
const appLayer = Function.pipe(
|
|
98
|
+
Effect.tryPromise(load),
|
|
99
|
+
Effect.map(v => v.default),
|
|
100
|
+
Effect.orDie,
|
|
101
|
+
Layer.unwrapEffect,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return Function.pipe(
|
|
105
|
+
Layer.unwrapEffect(Effect.gen(function*() {
|
|
106
|
+
const middlewareService = yield* StartApp.StartApp
|
|
107
|
+
const middleware = yield* middlewareService.middleware
|
|
108
|
+
|
|
109
|
+
const finalMiddleware = Function.flow(
|
|
110
|
+
HttpAppExtra.handleErrors,
|
|
111
|
+
middleware,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return Function.pipe(
|
|
115
|
+
HttpRouter
|
|
116
|
+
.Default
|
|
117
|
+
.serve(finalMiddleware),
|
|
118
|
+
HttpServer.withLogAddress,
|
|
119
|
+
)
|
|
120
|
+
})),
|
|
121
|
+
Layer.provide(appLayer),
|
|
122
|
+
Layer.provide([
|
|
123
|
+
FetchHttpClient.layer,
|
|
124
|
+
HttpRouter.Default.Live,
|
|
125
|
+
BunHttpServer.layer({
|
|
126
|
+
port: 3000,
|
|
127
|
+
}),
|
|
128
|
+
StartApp.layer(),
|
|
129
|
+
]),
|
|
130
|
+
Layer.launch,
|
|
131
|
+
BunRuntime.runMain,
|
|
132
|
+
)
|
|
133
|
+
}
|
package/src/StartApp.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as HttpApp from "@effect/platform/HttpApp"
|
|
2
|
+
import * as Context from "effect/Context"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Function from "effect/Function"
|
|
5
|
+
import * as Layer from "effect/Layer"
|
|
6
|
+
import * as Ref from "effect/Ref"
|
|
7
|
+
|
|
8
|
+
type NewType = HttpApp.Default<never, never>
|
|
9
|
+
|
|
10
|
+
type StartMiddleware = <E, R>(
|
|
11
|
+
self: HttpApp.Default<E, R>,
|
|
12
|
+
) => NewType
|
|
13
|
+
|
|
14
|
+
export class StartApp extends Context.Tag("effect-start/StartApp")<
|
|
15
|
+
StartApp,
|
|
16
|
+
{
|
|
17
|
+
readonly env: "development" | "production" | string
|
|
18
|
+
readonly relativeUrlRoot?: string
|
|
19
|
+
readonly addMiddleware: (
|
|
20
|
+
middleware: StartMiddleware,
|
|
21
|
+
) => Effect.Effect<void>
|
|
22
|
+
readonly middleware: Ref.Ref<StartMiddleware>
|
|
23
|
+
}
|
|
24
|
+
>() {
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function layer(options?: {
|
|
28
|
+
env?: string
|
|
29
|
+
}) {
|
|
30
|
+
return Layer.sync(StartApp, () => {
|
|
31
|
+
const env = options?.env ?? process.env.NODE_ENV ?? "development"
|
|
32
|
+
const middleware = Ref.unsafeMake(
|
|
33
|
+
Function.identity as StartMiddleware,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return StartApp.of({
|
|
37
|
+
env,
|
|
38
|
+
middleware,
|
|
39
|
+
addMiddleware: (f) =>
|
|
40
|
+
Ref.update(middleware, (prev) => (app) => f(prev(app))),
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}
|
package/src/StartHttp.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as HttpApp from "@effect/platform/HttpApp"
|
|
2
|
+
import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
|
|
3
|
+
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
4
|
+
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
5
|
+
import * as Effect from "effect/Effect"
|
|
6
|
+
import {
|
|
7
|
+
Bundle,
|
|
8
|
+
BundleHttp,
|
|
9
|
+
} from "."
|
|
10
|
+
|
|
11
|
+
type SsrRenderer = (req: Request) => PromiseLike<Response>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Attempts to render SSR page. If the renderer returns 404,
|
|
15
|
+
* we fall back to app.
|
|
16
|
+
*/
|
|
17
|
+
export function ssr(renderer: SsrRenderer) {
|
|
18
|
+
return Effect.gen(function*() {
|
|
19
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
20
|
+
const webRequest = request.source as Request
|
|
21
|
+
const ssrRes = yield* Effect.tryPromise(() => renderer(webRequest))
|
|
22
|
+
|
|
23
|
+
return HttpServerResponse.raw(ssrRes.body, {
|
|
24
|
+
status: ssrRes.status,
|
|
25
|
+
headers: ssrRes.headers,
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function withBundleAssets(opts?: {
|
|
31
|
+
path?: string
|
|
32
|
+
}) {
|
|
33
|
+
return HttpMiddleware.make(app =>
|
|
34
|
+
Effect.gen(function*() {
|
|
35
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
36
|
+
const bundleResponse = yield* BundleHttp.httpApp()
|
|
37
|
+
|
|
38
|
+
// Fallback to original app
|
|
39
|
+
return yield* app
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import * as Cause from "effect/Cause"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Fiber from "effect/Fiber"
|
|
4
|
+
import { dual } from "effect/Function"
|
|
5
|
+
import * as Predicate from "effect/Predicate"
|
|
6
|
+
import * as Runtime from "effect/Runtime"
|
|
7
|
+
import * as Stream from "effect/Stream"
|
|
8
|
+
import {
|
|
9
|
+
runForEachChunk,
|
|
10
|
+
StreamTypeId,
|
|
11
|
+
} from "effect/Stream"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Patched version of original Stream.toReadableStreamRuntime (v3.14.4) to
|
|
15
|
+
* fix an issue in Bun when native stream controller stops working when request
|
|
16
|
+
* is terminated by the client:
|
|
17
|
+
*
|
|
18
|
+
* TypeError: Value of "this" must be of type ReadableStreamDefaultController
|
|
19
|
+
*
|
|
20
|
+
* See related issues:
|
|
21
|
+
* https://github.com/Effect-TS/effect/issues/4538
|
|
22
|
+
* https://github.com/oven-sh/bun/issues/17837
|
|
23
|
+
*/
|
|
24
|
+
export const toReadableStreamRuntimePatched = dual<
|
|
25
|
+
<A, XR>(
|
|
26
|
+
runtime: Runtime.Runtime<XR>,
|
|
27
|
+
options?: { readonly strategy?: QueuingStrategy<A> | undefined },
|
|
28
|
+
) => <E, R extends XR>(self: Stream.Stream<A, E, R>) => ReadableStream<A>,
|
|
29
|
+
<A, E, XR, R extends XR>(
|
|
30
|
+
self: Stream.Stream<A, E, R>,
|
|
31
|
+
runtime: Runtime.Runtime<XR>,
|
|
32
|
+
options?: { readonly strategy?: QueuingStrategy<A> | undefined },
|
|
33
|
+
) => ReadableStream<A>
|
|
34
|
+
>(
|
|
35
|
+
(args) =>
|
|
36
|
+
Predicate.hasProperty(args[0], StreamTypeId) || Effect.isEffect(args[0]),
|
|
37
|
+
<A, E, XR, R extends XR>(
|
|
38
|
+
self: Stream.Stream<A, E, R>,
|
|
39
|
+
runtime: Runtime.Runtime<XR>,
|
|
40
|
+
options?: { readonly strategy?: QueuingStrategy<A> | undefined },
|
|
41
|
+
): ReadableStream<A> => {
|
|
42
|
+
const runFork = Runtime.runFork(runtime)
|
|
43
|
+
let currentResolve: (() => void) | undefined = undefined
|
|
44
|
+
let fiber: Fiber.RuntimeFiber<void, E> | undefined = undefined
|
|
45
|
+
const latch = Effect.unsafeMakeLatch(false)
|
|
46
|
+
|
|
47
|
+
return new ReadableStream<A>({
|
|
48
|
+
start(controller) {
|
|
49
|
+
fiber = runFork(
|
|
50
|
+
runForEachChunk(self, (chunk) =>
|
|
51
|
+
latch.whenOpen(Effect.sync(() => {
|
|
52
|
+
latch.unsafeClose()
|
|
53
|
+
try {
|
|
54
|
+
for (const item of chunk) {
|
|
55
|
+
controller.enqueue(item)
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (
|
|
59
|
+
(e as Error).message
|
|
60
|
+
=== `Value of "this" must be of type ReadableStreamDefaultController`
|
|
61
|
+
) {
|
|
62
|
+
// Do nothing when this happens in Bun.
|
|
63
|
+
} else {
|
|
64
|
+
throw e
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
currentResolve!()
|
|
68
|
+
currentResolve = undefined
|
|
69
|
+
}))),
|
|
70
|
+
)
|
|
71
|
+
// --- CHANGES HERE ---
|
|
72
|
+
// In original code, we had fiber.addObserver here that called
|
|
73
|
+
// error() or close() on controller. This patched version removes it.
|
|
74
|
+
},
|
|
75
|
+
pull() {
|
|
76
|
+
return new Promise<void>((resolve) => {
|
|
77
|
+
currentResolve = resolve
|
|
78
|
+
Effect.runSync(latch.open)
|
|
79
|
+
})
|
|
80
|
+
},
|
|
81
|
+
cancel() {
|
|
82
|
+
if (!fiber) return
|
|
83
|
+
return Effect.runPromise(Effect.asVoid(Fiber.interrupt(fiber)))
|
|
84
|
+
},
|
|
85
|
+
}, options?.strategy)
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
export const toReadableStreamRuntimePatched2 = dual<
|
|
90
|
+
<A, XR>(
|
|
91
|
+
runtime: Runtime.Runtime<XR>,
|
|
92
|
+
options?: { readonly strategy?: QueuingStrategy<A> | undefined },
|
|
93
|
+
) => <E, R extends XR>(self: Stream.Stream<A, E, R>) => ReadableStream<A>,
|
|
94
|
+
<A, E, XR, R extends XR>(
|
|
95
|
+
self: Stream.Stream<A, E, R>,
|
|
96
|
+
runtime: Runtime.Runtime<XR>,
|
|
97
|
+
options?: { readonly strategy?: QueuingStrategy<A> | undefined },
|
|
98
|
+
) => ReadableStream<A>
|
|
99
|
+
>(
|
|
100
|
+
(args) =>
|
|
101
|
+
Predicate.hasProperty(args[0], StreamTypeId) || Effect.isEffect(args[0]),
|
|
102
|
+
<A, E, XR, R extends XR>(
|
|
103
|
+
self: Stream.Stream<A, E, R>,
|
|
104
|
+
runtime: Runtime.Runtime<XR>,
|
|
105
|
+
options?: { readonly strategy?: QueuingStrategy<A> | undefined },
|
|
106
|
+
): ReadableStream<A> => {
|
|
107
|
+
const runSync = Runtime.runSync(runtime)
|
|
108
|
+
const runFork = Runtime.runFork(runtime)
|
|
109
|
+
let currentResolve: (() => void) | undefined = undefined
|
|
110
|
+
let fiber: Fiber.RuntimeFiber<void, E> | undefined = undefined
|
|
111
|
+
const latch = Effect.unsafeMakeLatch(false)
|
|
112
|
+
|
|
113
|
+
return new ReadableStream<A>({
|
|
114
|
+
start(controller) {
|
|
115
|
+
fiber = runFork(
|
|
116
|
+
runForEachChunk(self, (chunk) =>
|
|
117
|
+
latch.whenOpen(Effect.sync(() => {
|
|
118
|
+
latch.unsafeClose()
|
|
119
|
+
for (const item of chunk) {
|
|
120
|
+
controller.enqueue(item)
|
|
121
|
+
}
|
|
122
|
+
currentResolve!()
|
|
123
|
+
currentResolve = undefined
|
|
124
|
+
}))),
|
|
125
|
+
)
|
|
126
|
+
fiber.addObserver((exit) => {
|
|
127
|
+
if (exit._tag === "Failure") {
|
|
128
|
+
controller.error(Cause.squash(exit.cause))
|
|
129
|
+
} else {
|
|
130
|
+
controller.close()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
},
|
|
134
|
+
pull() {
|
|
135
|
+
return new Promise<void>((resolve) => {
|
|
136
|
+
currentResolve = resolve
|
|
137
|
+
Effect.runSync(latch.open)
|
|
138
|
+
})
|
|
139
|
+
},
|
|
140
|
+
cancel() {
|
|
141
|
+
if (!fiber) return
|
|
142
|
+
return Effect.runPromise(Effect.asVoid(Fiber.interrupt(fiber)))
|
|
143
|
+
},
|
|
144
|
+
}, options?.strategy)
|
|
145
|
+
},
|
|
146
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
2
|
+
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
3
|
+
import * as t from "bun:test"
|
|
4
|
+
import * as Effect from "effect/Effect"
|
|
5
|
+
import * as TestHttpClient from "./TestHttpClient.ts"
|
|
6
|
+
import { effectFn } from "./testing.ts"
|
|
7
|
+
|
|
8
|
+
const App = Effect.gen(function*() {
|
|
9
|
+
const req = yield* HttpServerRequest.HttpServerRequest
|
|
10
|
+
|
|
11
|
+
if (req.url == "/") {
|
|
12
|
+
return HttpServerResponse.text("Hello, World!")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return HttpServerResponse.text("Not Found", {
|
|
16
|
+
status: 404,
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const AppClient = TestHttpClient.make(App)
|
|
21
|
+
|
|
22
|
+
const effect = effectFn()
|
|
23
|
+
|
|
24
|
+
t.it("ok", () =>
|
|
25
|
+
effect(function*() {
|
|
26
|
+
const res = yield* AppClient.get("/")
|
|
27
|
+
|
|
28
|
+
t
|
|
29
|
+
.expect(
|
|
30
|
+
res.status,
|
|
31
|
+
)
|
|
32
|
+
.toEqual(200)
|
|
33
|
+
t
|
|
34
|
+
.expect(
|
|
35
|
+
yield* res.text,
|
|
36
|
+
)
|
|
37
|
+
.toEqual("Hello, World!")
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
t.it("not found", () =>
|
|
41
|
+
effect(function*() {
|
|
42
|
+
const res = yield* AppClient.get("/nope")
|
|
43
|
+
|
|
44
|
+
t
|
|
45
|
+
.expect(
|
|
46
|
+
res.status,
|
|
47
|
+
)
|
|
48
|
+
.toEqual(404)
|
|
49
|
+
t
|
|
50
|
+
.expect(
|
|
51
|
+
yield* res.text,
|
|
52
|
+
)
|
|
53
|
+
.toEqual("Not Found")
|
|
54
|
+
}))
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import * as HttpApp from "@effect/platform/HttpApp"
|
|
3
|
+
import * as HttpClient from "@effect/platform/HttpClient"
|
|
4
|
+
import * as HttpClientError from "@effect/platform/HttpClientError"
|
|
5
|
+
import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
|
|
6
|
+
import * as HttpClientResponse from "@effect/platform/HttpClientResponse"
|
|
7
|
+
import { RouteNotFound } from "@effect/platform/HttpServerError"
|
|
8
|
+
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
9
|
+
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
10
|
+
import * as Effect from "effect/Effect"
|
|
11
|
+
import * as Function from "effect/Function"
|
|
12
|
+
import * as Scope from "effect/Scope"
|
|
13
|
+
import * as Stream from "effect/Stream"
|
|
14
|
+
|
|
15
|
+
const WebHeaders = globalThis.Headers
|
|
16
|
+
|
|
17
|
+
export const make = <E = any, R = any>(
|
|
18
|
+
httpApp: HttpApp.Default<E, R>,
|
|
19
|
+
opts?: {
|
|
20
|
+
baseUrl?: string | null
|
|
21
|
+
handleRouteNotFound?: (
|
|
22
|
+
e: RouteNotFound,
|
|
23
|
+
) => Effect.Effect<HttpClientResponse.HttpClientResponse> | null
|
|
24
|
+
},
|
|
25
|
+
): HttpClient.HttpClient.With<
|
|
26
|
+
HttpClientError.HttpClientError | E,
|
|
27
|
+
Exclude<
|
|
28
|
+
Scope.Scope | R,
|
|
29
|
+
HttpServerRequest.HttpServerRequest
|
|
30
|
+
>
|
|
31
|
+
> =>
|
|
32
|
+
HttpClient
|
|
33
|
+
.make(
|
|
34
|
+
(request, url, signal) => {
|
|
35
|
+
const send = (
|
|
36
|
+
body: BodyInit | undefined,
|
|
37
|
+
) => {
|
|
38
|
+
const app = httpApp
|
|
39
|
+
const serverRequest = HttpServerRequest.fromWeb(
|
|
40
|
+
new Request(url.toString(), {
|
|
41
|
+
method: request.method,
|
|
42
|
+
headers: new WebHeaders(request.headers),
|
|
43
|
+
body,
|
|
44
|
+
duplex: request.body._tag === "Stream" ? "half" : undefined,
|
|
45
|
+
signal,
|
|
46
|
+
} as any),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return Function.pipe(
|
|
50
|
+
app,
|
|
51
|
+
Effect.provideService(
|
|
52
|
+
HttpServerRequest.HttpServerRequest,
|
|
53
|
+
serverRequest,
|
|
54
|
+
),
|
|
55
|
+
Effect.andThen(HttpServerResponse.toWeb),
|
|
56
|
+
Effect.andThen(res => HttpClientResponse.fromWeb(request, res)),
|
|
57
|
+
opts?.handleRouteNotFound === null
|
|
58
|
+
? Function.identity
|
|
59
|
+
: Effect.catchTag("RouteNotFound", e =>
|
|
60
|
+
Effect
|
|
61
|
+
.succeed(HttpClientResponse.fromWeb(
|
|
62
|
+
e.request,
|
|
63
|
+
new Response("Failed with RouteNotFound", {
|
|
64
|
+
status: 404,
|
|
65
|
+
}),
|
|
66
|
+
))),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
switch (
|
|
71
|
+
request
|
|
72
|
+
.body
|
|
73
|
+
._tag
|
|
74
|
+
) {
|
|
75
|
+
case "Raw":
|
|
76
|
+
case "Uint8Array":
|
|
77
|
+
return send(
|
|
78
|
+
request
|
|
79
|
+
.body
|
|
80
|
+
.body as any,
|
|
81
|
+
)
|
|
82
|
+
case "FormData":
|
|
83
|
+
return send(request.body.formData)
|
|
84
|
+
case "Stream":
|
|
85
|
+
return Effect.flatMap(
|
|
86
|
+
Stream.toReadableStreamEffect(request.body.stream),
|
|
87
|
+
send,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return send(undefined)
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
.pipe(
|
|
95
|
+
opts?.baseUrl === null
|
|
96
|
+
? Function.identity
|
|
97
|
+
: HttpClient.mapRequest(
|
|
98
|
+
HttpClientRequest.prependUrl(opts?.baseUrl ?? "http://localhost"),
|
|
99
|
+
),
|
|
100
|
+
)
|