diff --git a/.changeset/cool-chairs-remain.md b/.changeset/cool-chairs-remain.md new file mode 100644 index 00000000000..2de1f0cc872 --- /dev/null +++ b/.changeset/cool-chairs-remain.md @@ -0,0 +1,11 @@ +--- +"@effect/platform": patch +--- + +add HttpLayerRouter module + +The experimental HttpLayerRouter module provides a simplified way to create HTTP servers. + +You can read more in the /platform README: + +https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md#httplayerrouter diff --git a/packages/ai/ai/src/McpServer.ts b/packages/ai/ai/src/McpServer.ts index 325cbf74ff8..9f2edde6577 100644 --- a/packages/ai/ai/src/McpServer.ts +++ b/packages/ai/ai/src/McpServer.ts @@ -2,6 +2,7 @@ * @since 1.0.0 */ import * as Headers from "@effect/platform/Headers" +import type * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" import type * as HttpRouter from "@effect/platform/HttpRouter" import type { RpcMessage } from "@effect/rpc" import type * as Rpc from "@effect/rpc/Rpc" @@ -574,6 +575,24 @@ export const layerHttp = (options: { Layer.provide(RpcSerialization.layerJsonRpc()) ) +/** + * Run the McpServer, using HTTP for input and output. + * + * Uses a `HttpLayerRouter` to register the McpServer routes. + * + * @since 1.0.0 + * @category Layers + */ +export const layerHttpRouter = (options: { + readonly name: string + readonly version: string + readonly path: HttpRouter.PathInput +}): Layer.Layer => + layer(options).pipe( + Layer.provide(RpcServer.layerProtocolHttpRouter(options)), + Layer.provide(RpcSerialization.layerJsonRpc()) + ) + /** * Register an AiToolkit with the McpServer. * diff --git a/packages/platform-node/test/HttpServer.test.ts b/packages/platform-node/test/HttpServer.test.ts index 5ae3117b219..8418a55a638 100644 --- a/packages/platform-node/test/HttpServer.test.ts +++ b/packages/platform-node/test/HttpServer.test.ts @@ -4,6 +4,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse, + HttpLayerRouter, HttpMultiplex, HttpPlatform, HttpRouter, @@ -52,6 +53,27 @@ describe("HttpServer", () => { expect(todo).toEqual({ id: 1, title: "test" }) }).pipe(Effect.provide(NodeHttpServer.layerTest))) + it.scoped("schema HttpLayerRouter", () => + Effect.gen(function*() { + const handler = yield* HttpLayerRouter.toHttpEffect(HttpLayerRouter.use(Effect.fnUntraced(function*(router) { + yield* router.add( + "GET", + "/todos/:id", + Effect.flatMap( + HttpLayerRouter.schemaParams(IdParams), + ({ id }) => todoResponse({ id, title: "test" }) + ) + ) + }))) + + yield* HttpServer.serveEffect(handler) + + const todo = yield* HttpClient.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + expect(todo).toEqual({ id: 1, title: "test" }) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + it.scoped("formData", () => Effect.gen(function*() { yield* HttpRouter.empty.pipe( @@ -696,4 +718,33 @@ describe("HttpServer", () => { maxParamLength: 5 }) )) + + it.scoped("HttpLayerRouter prefixed", () => + Effect.gen(function*() { + const handler = yield* HttpLayerRouter.toHttpEffect(HttpLayerRouter.use(Effect.fnUntraced(function*(router_) { + const router = router_.prefixed("/todos") + yield* router.add( + "GET", + "/:id", + Effect.flatMap( + HttpLayerRouter.schemaParams(IdParams), + ({ id }) => todoResponse({ id, title: "test" }) + ) + ) + yield* router.addAll([ + HttpLayerRouter.route("GET", "/", Effect.succeed(HttpServerResponse.text("root"))) + ]) + }))) + + yield* HttpServer.serveEffect(handler) + + const todo = yield* HttpClient.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + expect(todo).toEqual({ id: 1, title: "test" }) + const root = yield* HttpClient.get("/todos").pipe( + Effect.flatMap((r) => r.text) + ) + expect(root).toEqual("root") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) }) diff --git a/packages/platform/README.md b/packages/platform/README.md index a5377f5ffac..d96a8105c11 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -4828,3 +4828,274 @@ Output: } */ ``` + +# HttpLayerRouter + +The experimental `HttpLayerRouter` module provides a simplified way to create HTTP servers. +It aims to simplify the process of defining routes and registering other HTTP +services like `HttpApi` or `RpcServer`'s. + +## Registering routes + +```ts +import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer" +import * as NodeRuntime from "@effect/platform-node/NodeRuntime" +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { createServer } from "http" + +// Here is how you can register a simple GET route +const HelloRoute = Layer.effectDiscard( + Effect.gen(function* () { + // First, we need to access the `HttpRouter` service + const router = yield* HttpLayerRouter.HttpRouter + + // Then, we can add a new route to the router + yield* router.add("GET", "/hello", HttpServerResponse.text("Hello, World!")) + }) +) + +// You can also use the `HttpLayerRouter.use` function to register a route +const GoodbyeRoute = HttpLayerRouter.use( + Effect.fn(function* (router) { + // The `router` parameter is the `HttpRouter` service + yield* router.add( + "GET", + "/goodbye", + HttpServerResponse.text("Goodbye, World!") + ) + }) +) + +const AllRoutes = Layer.mergeAll(HelloRoute, GoodbyeRoute) + +// To start the server, we use `HttpLayerRouter.serve` with the routes layer +HttpLayerRouter.serve(AllRoutes).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + Layer.launch, + NodeRuntime.runMain +) +``` + +## Applying middleware + +```ts +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import * as HttpMiddleware from "@effect/platform/HttpMiddleware" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" + +// Here is a service that we want to provide to every HTTP request +class CurrentSession extends Context.Tag("CurrentSession")< + CurrentSession, + { + readonly token: string + } +>() {} + +// Using the `HttpLayerRouter.middleware` function, we can create a middleware +// that provides the `CurrentSession` service to every HTTP request. +const SessionMiddleware = HttpLayerRouter.middleware<{ + provides: CurrentSession +}>()( + Effect.gen(function* () { + yield* Effect.log("SessionMiddleware initialized") + + return (httpEffect) => + Effect.provideService(httpEffect, CurrentSession, { + token: "dummy-token" + }) + }) +) + +// And here is a CORS middleware that modifies the HTTP response +const CorsMiddleware = HttpLayerRouter.middleware(HttpMiddleware.cors()) +// You can also use `HttpLayerRouter.cors()` to create a CORS middleware + +const HelloRoute = Layer.effectDiscard( + Effect.gen(function* () { + const router = yield* HttpLayerRouter.HttpRouter + yield* router.add( + "GET", + "/hello", + Effect.gen(function* () { + // We can now access the `CurrentSession` service in our route handler + const session = yield* CurrentSession + return HttpServerResponse.text( + `Hello, World! Your session token is: ${session.token}` + ) + }) + ) + }) +).pipe( + // We can provide the `SessionMiddleware.layer` to the `HelloRoute` layer + Layer.provide(SessionMiddleware.layer), + // And we can also provide the `CorsMiddleware` layer to handle CORS + Layer.provide(CorsMiddleware.layer) +) +``` + +## Interdependent middleware + +If middleware depends on another middleware, you can use the `.combine` api to +combine them. + +```ts +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" + +class CurrentSession extends Context.Tag("CurrentSession")< + CurrentSession, + { + readonly token: string + } +>() {} + +const SessionMiddleware = HttpLayerRouter.middleware<{ + provides: CurrentSession +}>()( + Effect.gen(function* () { + yield* Effect.log("SessionMiddleware initialized") + + return (httpEffect) => + Effect.provideService(httpEffect, CurrentSession, { + token: "dummy-token" + }) + }) +) + +// Here is a middleware that uses the `CurrentSession` service +const LogMiddleware = HttpLayerRouter.middleware( + Effect.gen(function* () { + yield* Effect.log("LogMiddleware initialized") + + return Effect.fn(function* (httpEffect) { + const session = yield* CurrentSession + yield* Effect.log(`Current session token: ${session.token}`) + return yield* httpEffect + }) + }) +) + +// We can then use the .combine method to combine the middlewares +const LogAndSessionMiddleware = LogMiddleware.combine(SessionMiddleware) + +const HelloRoute = Layer.effectDiscard( + Effect.gen(function* () { + const router = yield* HttpLayerRouter.HttpRouter + yield* router.add( + "GET", + "/hello", + Effect.gen(function* () { + const session = yield* CurrentSession + return HttpServerResponse.text( + `Hello, World! Your session token is: ${session.token}` + ) + }) + ) + }) +).pipe(Layer.provide(LogAndSessionMiddleware.layer)) +``` + +## Registering a HttpApi + +```ts +import { + HttpApi, + HttpApiBuilder, + HttpApiEndpoint, + HttpApiGroup, + HttpApiScalar, + HttpLayerRouter +} from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer } from "effect" +import { createServer } from "http" + +// First, we define our HttpApi +class MyApi extends HttpApi.make("api").add( + HttpApiGroup.make("users") + .add(HttpApiEndpoint.get("me", "/me")) + .prefix("/users") +) {} + +// Implement the handlers for the API +const UsersApiLayer = HttpApiBuilder.group(MyApi, "users", (handers) => + handers.handle("me", () => Effect.void) +) + +// Use `HttpLayerRouter.addHttpApi` to register the API with the router +const HttpApiRoutes = HttpLayerRouter.addHttpApi(MyApi, { + openapiPath: "/docs/openapi.json" +}).pipe( + // Provide the api handlers layer + Layer.provide(UsersApiLayer) +) + +// Create a /docs route for the API documentation +const DocsRoute = HttpApiScalar.layerHttpLayerRouter({ + api: MyApi, + path: "/docs" +}) + +// Finally, we merge all routes and serve them using the Node HTTP server +const AllRoutes = Layer.mergeAll(HttpApiRoutes, DocsRoute).pipe( + Layer.provide(HttpLayerRouter.cors()) +) + +HttpLayerRouter.serve(AllRoutes).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + Layer.launch, + NodeRuntime.runMain +) +``` + +## Registering a RpcServer + +```ts +import { HttpLayerRouter } from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Rpc, RpcGroup, RpcSerialization, RpcServer } from "@effect/rpc" +import { Effect, Layer, Schema } from "effect" +import { createServer } from "http" + +export class User extends Schema.Class("User")({ + id: Schema.String, + name: Schema.String +}) {} + +// Define a group of RPCs +export class UserRpcs extends RpcGroup.make( + Rpc.make("UserById", { + success: User, + error: Schema.String, // Indicates that errors, if any, will be returned as strings + payload: { + id: Schema.String + } + }) +) {} + +const UserHandlers = UserRpcs.toLayer({ + UserById: ({ id }) => Effect.succeed(new User({ id, name: "John Doe" })) +}) + +// Use `HttpLayerRouter` to register the rpc server +const RpcRoute = RpcServer.layerHttpRouter({ + group: UserRpcs, + path: "/rpc" +}).pipe(Layer.provide(UserHandlers), Layer.provide(RpcSerialization.layerJson)) + +// Start the HTTP server with the RPC route +HttpLayerRouter.serve(RpcRoute).pipe( + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + Layer.launch, + NodeRuntime.runMain +) +``` diff --git a/packages/platform/src/HttpApiBuilder.ts b/packages/platform/src/HttpApiBuilder.ts index 77d81cc1cf8..017d322c47b 100644 --- a/packages/platform/src/HttpApiBuilder.ts +++ b/packages/platform/src/HttpApiBuilder.ts @@ -123,6 +123,36 @@ export const httpApp: Effect.Effect< ) as any }) +/** + * @since 1.0.0 + * @category constructors + */ +export const buildMiddleware: ( + api: HttpApi.HttpApi +) => Effect.Effect< + ( + effect: Effect.Effect + ) => Effect.Effect +> = Effect.fnUntraced( + function*( + api: HttpApi.HttpApi + ) { + const context = yield* Effect.context() + const middlewareMap = makeMiddlewareMap(api.middlewares, context) + const errorSchema = makeErrorSchema(api as any) + const encodeError = Schema.encodeUnknown(errorSchema) + return (effect: Effect.Effect) => + Effect.catchAllCause( + applyMiddleware(middlewareMap, effect), + (cause) => + Effect.matchEffect(Effect.provide(encodeError(Cause.squash(cause)), context), { + onFailure: () => Effect.failCause(cause), + onSuccess: Effect.succeed + }) + ) + } +) + /** * Construct an http web handler from an `HttpApi` instance. * diff --git a/packages/platform/src/HttpApiScalar.ts b/packages/platform/src/HttpApiScalar.ts index 65bd469f01c..5e6ea884555 100644 --- a/packages/platform/src/HttpApiScalar.ts +++ b/packages/platform/src/HttpApiScalar.ts @@ -2,9 +2,11 @@ * @since 1.0.0 */ import * as Effect from "effect/Effect" -import type { Layer } from "effect/Layer" +import * as Layer from "effect/Layer" import { Api } from "./HttpApi.js" +import type * as HttpApi from "./HttpApi.js" import { Router } from "./HttpApiBuilder.js" +import * as HttpLayerRouter from "./HttpLayerRouter.js" import * as HttpServerResponse from "./HttpServerResponse.js" import * as Html from "./internal/html.js" import * as internal from "./internal/httpApiScalar.js" @@ -120,52 +122,45 @@ export type ScalarConfig = { defaultOpenAllTags?: boolean } -/** - * @since 1.0.0 - * @category layers - */ -export const layer = (options?: { - readonly path?: `/${string}` | undefined +const makeHandler = (options: { + readonly api: HttpApi.HttpApi.Any readonly source?: ScalarScriptSource readonly scalar?: ScalarConfig -}): Layer => - Router.use((router) => - Effect.gen(function*() { - const { api } = yield* Api - const spec = OpenApi.fromApi(api) +}) => { + const spec = OpenApi.fromApi(options.api as any) - const source = options?.source - const defaultScript = internal.javascript - const src: string | null = source - ? typeof source === "string" - ? source - : source.type === "cdn" - ? `https://cdn.jsdelivr.net/npm/@scalar/api-reference@${ - source.version ?? "latest" - }/dist/browser/standalone.min.js` - : null - : null + const source = options?.source + const defaultScript = internal.javascript + const src: string | null = source + ? typeof source === "string" + ? source + : source.type === "cdn" + ? `https://cdn.jsdelivr.net/npm/@scalar/api-reference@${ + source.version ?? "latest" + }/dist/browser/standalone.min.js` + : null + : null - const scalarConfig = { - _integration: "http", - ...options?.scalar - } + const scalarConfig = { + _integration: "http", + ...options?.scalar + } - const response = HttpServerResponse.html(` + const response = HttpServerResponse.html(` ${Html.escape(spec.info.title)} ${ - !spec.info.description - ? "" - : `` - } + !spec.info.description + ? "" + : `` + } ${ - !spec.info.description - ? "" - : `` - } + !spec.info.description + ? "" + : `` + } @@ -178,12 +173,55 @@ export const layer = (options?: { document.getElementById('api-reference').dataset.configuration = JSON.stringify(${Html.escapeJson(scalarConfig)}) ${ - src - ? `` - : `` - } + src + ? `` + : `` + } `) - yield* router.get(options?.path ?? "/docs", Effect.succeed(response)) + + return Effect.succeed(response) +} + +/** + * @since 1.0.0 + * @category layers + */ +export const layer = (options?: { + readonly path?: `/${string}` | undefined + readonly source?: ScalarScriptSource + readonly scalar?: ScalarConfig +}): Layer.Layer => + Router.use((router) => + Effect.gen(function*() { + const { api } = yield* Api + const handler = makeHandler({ ...options, api }) + yield* router.get(options?.path ?? "/docs", handler) }) ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerHttpLayerRouter: ( + options: { + readonly api: HttpApi.HttpApi.Any + readonly path: `/${string}` + readonly source?: ScalarScriptSource + readonly scalar?: ScalarConfig + } +) => Layer.Layer< + never, + never, + HttpLayerRouter.HttpRouter +> = Effect.fnUntraced(function*(options: { + readonly api: HttpApi.HttpApi.Any + readonly path: `/${string}` + readonly source?: ScalarScriptSource + readonly scalar?: ScalarConfig +}) { + const router = yield* HttpLayerRouter.HttpRouter + const handler = makeHandler(options) + yield* router.add("GET", options.path, handler) +}, Layer.effectDiscard) diff --git a/packages/platform/src/HttpApiSwagger.ts b/packages/platform/src/HttpApiSwagger.ts index 468767f6ae0..6f00d060c2a 100644 --- a/packages/platform/src/HttpApiSwagger.ts +++ b/packages/platform/src/HttpApiSwagger.ts @@ -2,30 +2,21 @@ * @since 1.0.0 */ import * as Effect from "effect/Effect" -import type { Layer } from "effect/Layer" +import * as Layer from "effect/Layer" import { Api } from "./HttpApi.js" +import type * as HttpApi from "./HttpApi.js" import { Router } from "./HttpApiBuilder.js" +import * as HttpLayerRouter from "./HttpLayerRouter.js" import * as HttpServerResponse from "./HttpServerResponse.js" import * as Html from "./internal/html.js" import * as internal from "./internal/httpApiSwagger.js" import * as OpenApi from "./OpenApi.js" -/** - * Exported layer mounting Swagger/OpenAPI documentation UI. - * - * @param options.path Optional mount path (default "/docs"). - * - * @since 1.0.0 - * @category layers - */ -export const layer = (options?: { - readonly path?: `/${string}` | undefined -}): Layer => - Router.use((router) => - Effect.gen(function*() { - const { api } = yield* Api - const spec = OpenApi.fromApi(api) - const response = HttpServerResponse.html(` +const makeHandler = (options: { + readonly api: HttpApi.HttpApi.Any +}) => { + const spec = OpenApi.fromApi(options.api as any) + const response = HttpServerResponse.html(` @@ -49,6 +40,46 @@ export const layer = (options?: { `) - yield* router.get(options?.path ?? "/docs", Effect.succeed(response)) + return Effect.succeed(response) +} + +/** + * Exported layer mounting Swagger/OpenAPI documentation UI. + * + * @param options.path Optional mount path (default "/docs"). + * + * @since 1.0.0 + * @category layers + */ +export const layer = (options?: { + readonly path?: `/${string}` | undefined +}): Layer.Layer => + Router.use((router) => + Effect.gen(function*() { + const { api } = yield* Api + const handler = makeHandler({ api }) + yield* router.get(options?.path ?? "/docs", handler) }) ) + +/** + * @since 1.0.0 + * @category layers + */ +export const layerHttpLayerRouter: ( + options: { + readonly api: HttpApi.HttpApi.Any + readonly path: `/${string}` + } +) => Layer.Layer< + never, + never, + HttpLayerRouter.HttpRouter +> = Effect.fnUntraced(function*(options: { + readonly api: HttpApi.HttpApi.Any + readonly path: `/${string}` +}) { + const router = yield* HttpLayerRouter.HttpRouter + const handler = makeHandler(options) + yield* router.add("GET", options.path, handler) +}, Layer.effectDiscard) diff --git a/packages/platform/src/HttpLayerRouter.ts b/packages/platform/src/HttpLayerRouter.ts new file mode 100644 index 00000000000..ac5bcae251d --- /dev/null +++ b/packages/platform/src/HttpLayerRouter.ts @@ -0,0 +1,904 @@ +/** + * @since 1.0.0 + */ +import * as HttpServerRequest from "@effect/platform/HttpServerRequest" +import * as HttpServerResponse from "@effect/platform/HttpServerResponse" +import * as Arr from "effect/Array" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" +import { compose, constant, dual, identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Scope from "effect/Scope" +import * as Tracer from "effect/Tracer" +import * as FindMyWay from "find-my-way-ts" +import * as HttpApi from "./HttpApi.js" +import * as HttpApiBuilder from "./HttpApiBuilder.js" +import type * as HttpApiGroup from "./HttpApiGroup.js" +import type * as HttpMethod from "./HttpMethod.js" +import * as HttpMiddleware from "./HttpMiddleware.js" +import { RouteContext, RouteContextTypeId } from "./HttpRouter.js" +import * as HttpServer from "./HttpServer.js" +import * as HttpServerError from "./HttpServerError.js" +import * as OpenApi from "./OpenApi.js" + +/** + * @since 1.0.0 + * @category Re-exports + */ +export * as FindMyWay from "find-my-way-ts" + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export const TypeId: unique symbol = Symbol.for("@effect/platform/HttpLayerRouter/HttpRouter") + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export interface HttpRouter { + readonly [TypeId]: TypeId + + readonly prefixed: (prefix: string) => HttpRouter + + readonly add: ( + method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS", + path: PathInput, + handler: + | Effect.Effect + | ((request: HttpServerRequest.HttpServerRequest) => Effect.Effect), + options?: { readonly uninterruptible?: boolean | undefined } | undefined + ) => Effect.Effect< + void, + never, + Type.From<"Requires", Exclude> | Type.From<"Error", E> + > + + readonly addAll: >>( + routes: Routes + ) => Effect.Effect< + void, + never, + | Type.From<"Requires", Exclude, Provided>> + | Type.From<"Error", Route.Error> + > + + readonly asHttpEffect: () => Effect.Effect< + HttpServerResponse.HttpServerResponse, + unknown, + HttpServerRequest.HttpServerRequest | Scope.Scope + > +} + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export const HttpRouter: Context.Tag = Context.GenericTag( + "@effect/platform/HttpLayerRouter" +) + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export const make = Effect.gen(function*() { + const router = FindMyWay.make>(yield* RouterConfig) + + const addAll = >>( + routes: Routes + ): Effect.Effect< + void, + never, + | Type.From<"Requires", Exclude, Provided>> + | Type.From<"Error", Route.Error> + > => + Effect.contextWith((context: Context.Context) => { + const middleware = getMiddleware(context) + const applyMiddleware = (effect: Effect.Effect) => { + for (let i = 0; i < middleware.length; i++) { + effect = middleware[i](effect) + } + return effect + } + for (let i = 0; i < routes.length; i++) { + const route = middleware.length === 0 ? routes[i] : makeRoute({ + ...routes[i], + handler: applyMiddleware(routes[i].handler as Effect.Effect) + }) + if (route.method === "*") { + router.all(route.path, route as any) + } else { + router.on(route.method, route.path, route as any) + } + } + }) + + return HttpRouter.of({ + [TypeId]: TypeId, + prefixed(this: HttpRouter, prefix: string) { + return HttpRouter.of({ + [TypeId]: TypeId, + asHttpEffect: this.asHttpEffect, + prefixed: (newPrefix: string) => this.prefixed(prefixPath(prefix, newPrefix)), + addAll: (routes) => addAll(routes.map(prefixRoute(prefix))) as any, + add: (method, path, handler, options) => + addAll([ + makeRoute({ + method, + path: prefixPath(path, prefix) as PathInput, + handler: Effect.isEffect(handler) + ? handler + : Effect.flatMap(HttpServerRequest.HttpServerRequest, handler), + uninterruptible: options?.uninterruptible ?? false, + prefix: Option.some(prefix) + }) + ]) + }) + }, + addAll, + add: (method, path, handler, options) => addAll([route(method, path, handler, options)]), + asHttpEffect() { + return Effect.withFiberRuntime((fiber) => { + const contextMap = new Map(fiber.currentContext.unsafeMap) + const request = contextMap.get(HttpServerRequest.HttpServerRequest.key) as HttpServerRequest.HttpServerRequest + let result = router.find(request.method, request.url) + if (result === undefined && request.method === "HEAD") { + result = router.find("GET", request.url) + } + if (result === undefined) { + return Effect.fail(new HttpServerError.RouteNotFound({ request })) + } + const route = result.handler + if (route.prefix._tag === "Some") { + contextMap.set(HttpServerRequest.HttpServerRequest.key, sliceRequestUrl(request, route.prefix.value)) + } + contextMap.set(HttpServerRequest.ParsedSearchParams.key, result.searchParams) + contextMap.set(RouteContext.key, { + [RouteContextTypeId]: RouteContextTypeId, + route, + params: result.params + }) + + const span = contextMap.get(Tracer.ParentSpan.key) as Tracer.Span | undefined + if (span && span._tag === "Span") { + span.attribute("http.route", route.path) + } + return Effect.locally( + (route.uninterruptible ? + route.handler : + Effect.interruptible(route.handler)) as Effect.Effect< + HttpServerResponse.HttpServerResponse, + unknown + >, + FiberRef.currentContext, + Context.unsafeMake(contextMap) + ) + }) + } + }) +}) + +function sliceRequestUrl(request: HttpServerRequest.HttpServerRequest, prefix: string) { + const prefexLen = prefix.length + return request.modify({ url: request.url.length <= prefexLen ? "/" : request.url.slice(prefexLen) }) +} + +/** + * @since 1.0.0 + * @category Configuration + */ +export class RouterConfig extends Context.Reference()("@effect/platform/HttpLayerRouter/RouterConfig", { + defaultValue: constant>({}) +}) {} + +export { + /** + * @since 1.0.0 + * @category Route context + */ + params, + /** + * @since 1.0.0 + * @category Route context + */ + RouteContext, + /** + * @since 1.0.0 + * @category Route context + */ + schemaJson, + /** + * @since 1.0.0 + * @category Route context + */ + schemaNoBody, + /** + * @since 1.0.0 + * @category Route context + */ + schemaParams, + /** + * @since 1.0.0 + * @category Route context + */ + schemaPathParams +} from "./HttpRouter.js" + +/** + * A helper function that is the equivalent of: + * + * ```ts + * import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" + * import * as Effect from "effect/Effect" + * import * as Layer from "effect/Layer" + * + * const MyRoute = Layer.scopedDiscard(Effect.gen(function*() { + * const router = yield* HttpLayerRouter.HttpRouter + * + * // then use `router.add` to add a route + * })) + * ``` + * + * @since 1.0.0 + * @category HttpRouter + */ +export const use = ( + f: (router: HttpRouter) => Effect.Effect +): Layer.Layer> => Layer.scopedDiscard(Effect.flatMap(HttpRouter, f)) + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export const layer: Layer.Layer = Layer.effect(HttpRouter, make) + +/** + * @since 1.0.0 + * @category HttpRouter + */ +export const toHttpEffect = ( + appLayer: Layer.Layer +): Effect.Effect< + Effect.Effect< + HttpServerResponse.HttpServerResponse, + Type.Only<"Error", R> | HttpServerError.RouteNotFound, + Scope.Scope | HttpServerRequest.HttpServerRequest | Type.Only<"Requires", R> + >, + E, + Exclude, HttpRouter> | Scope.Scope +> => + Effect.gen(function*() { + const scope = yield* Effect.scope + const memoMap = yield* Layer.CurrentMemoMap + const context = yield* Layer.buildWithMemoMap( + Layer.provideMerge(appLayer, layer), + memoMap, + scope + ) + const router = Context.get(context, HttpRouter) + return router.asHttpEffect() + }) as any + +/** + * @since 1.0.0 + * @category Route + */ +export const RouteTypeId: unique symbol = Symbol.for("@effect/platform/HttpLayerRouter/Route") + +/** + * @since 1.0.0 + * @category Route + */ +export type RouteTypeId = typeof RouteTypeId + +/** + * @since 1.0.0 + * @category Route + */ +export interface Route { + readonly [RouteTypeId]: RouteTypeId + readonly method: HttpMethod.HttpMethod | "*" + readonly path: PathInput + readonly handler: Effect.Effect + readonly uninterruptible: boolean + readonly prefix: Option.Option +} + +/** + * @since 1.0.0 + * @category Route + */ +export declare namespace Route { + /** + * @since 1.0.0 + * @category Route + */ + export type Error> = R extends Route ? E : never + + /** + * @since 1.0.0 + * @category Route + */ + export type Context> = T extends Route ? R : never +} + +const makeRoute = (options: { + readonly method: HttpMethod.HttpMethod | "*" + readonly path: PathInput + readonly handler: Effect.Effect + readonly uninterruptible?: boolean | undefined + readonly prefix?: Option.Option | undefined +}): Route> => + ({ + ...options, + uninterruptible: options.uninterruptible ?? false, + prefix: options.prefix ?? Option.none(), + [RouteTypeId]: RouteTypeId + }) as Route> + +/** + * @since 1.0.0 + * @category Route + */ +export const route = ( + method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS", + path: PathInput, + handler: + | Effect.Effect + | ((request: HttpServerRequest.HttpServerRequest) => Effect.Effect), + options?: { + readonly uninterruptible?: boolean | undefined + } +): Route> => + makeRoute({ + ...options, + method, + path, + handler: Effect.isEffect(handler) ? handler : Effect.flatMap(HttpServerRequest.HttpServerRequest, handler), + uninterruptible: options?.uninterruptible ?? false + }) + +/** + * @since 1.0.0 + * @category PathInput + */ +export type PathInput = `/${string}` | "*" + +const removeTrailingSlash = ( + path: PathInput +): PathInput => (path.endsWith("/") ? path.slice(0, -1) : path) as any + +/** + * @since 1.0.0 + * @category PathInput + */ +export const prefixPath: { + (prefix: string): (self: string) => string + (self: string, prefix: string): string +} = dual(2, (self: string, prefix: string) => { + prefix = removeTrailingSlash(prefix as PathInput) + return self === "/" ? prefix : prefix + self +}) + +/** + * @since 1.0.0 + * @category Route + */ +export const prefixRoute: { + (prefix: string): (self: Route) => Route + (self: Route, prefix: string): Route +} = dual(2, (self: Route, prefix: string): Route => + makeRoute({ + ...self, + path: prefixPath(self.path, prefix) as PathInput, + prefix: Option.match(self.prefix, { + onNone: () => Option.some(prefix as string), + onSome: (existingPrefix) => Option.some(prefixPath(existingPrefix, prefix) as string) + }) + })) + +/** + * Represents a request-level dependency, that needs to be provided by + * middleware. + * + * @since 1.0.0 + * @category Request types + */ +export interface Type { + readonly _: unique symbol + readonly kind: Kind + readonly type: T +} + +/** + * @since 1.0.0 + * @category Request types + */ +export declare namespace Type { + /** + * @since 1.0.0 + * @category Request types + */ + export type From = R extends infer T ? Type : never + + /** + * @since 1.0.0 + * @category Request types + */ + export type Only = A extends Type ? T : never + + /** + * @since 1.0.0 + * @category Request types + */ + export type Without = A extends Type ? never : A +} + +/** + * Services provided by the HTTP router, which are available in the + * request context. + * + * @since 1.0.0 + * @category Request types + */ +export type Provided = + | HttpServerRequest.HttpServerRequest + | Scope.Scope + | HttpServerRequest.ParsedSearchParams + | RouteContext + +/** + * @since 1.0.0 + * @category Middleware + */ +export const MiddlewareTypeId: unique symbol = Symbol.for("@effect/platform/HttpLayerRouter/Middleware") + +/** + * @since 1.0.0 + * @category Middleware + */ +export type MiddlewareTypeId = typeof MiddlewareTypeId + +/** + * @since 1.0.0 + * @category Middleware + */ +export interface Middleware< + Config extends { + provides: any + handles: any + error: any + requires: any + layerError: any + layerRequires: any + } +> { + readonly [MiddlewareTypeId]: Config + + readonly layer: [Config["requires"]] extends [never] ? Layer.Layer< + Type.From<"Requires", Config["provides"]>, + Config["layerError"], + Config["layerRequires"] | Type.From<"Requires", Config["requires"]> + > + : "Need to .provide(middleware) that satisfy the missing request dependencies" + + readonly combine: < + Config2 extends { + provides: any + handles: any + error: any + requires: any + layerError: any + layerRequires: any + } + >(other: Middleware) => Middleware<{ + provides: Config2["provides"] | Config["provides"] + handles: Config2["handles"] | Config["handles"] + error: Config2["error"] | Exclude + requires: Exclude | Config2["requires"] + layerError: Config["layerError"] | Config2["layerError"] + layerRequires: Config["layerRequires"] | Config2["layerRequires"] + }> +} + +/** + * Create a middleware layer that can be used to modify requests and responses. + * + * ```ts + * import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" + * import * as HttpMiddleware from "@effect/platform/HttpMiddleware" + * import * as HttpServerResponse from "@effect/platform/HttpServerResponse" + * import * as Context from "effect/Context" + * import * as Effect from "effect/Effect" + * import * as Layer from "effect/Layer" + * + * // Here we are defining a CORS middleware + * const CorsMiddleware = HttpLayerRouter.middleware(HttpMiddleware.cors()).layer + * // You can also use HttpLayerRouter.cors() to create a CORS middleware + * + * class CurrentSession extends Context.Tag("CurrentSession")() {} + * + * // You can create middleware that provides a service to the HTTP requests. + * const SessionMiddleware = HttpLayerRouter.middleware<{ + * provides: CurrentSession + * }>()( + * Effect.gen(function*() { + * yield* Effect.log("SessionMiddleware initialized") + * + * return (httpEffect) => + * Effect.provideService(httpEffect, CurrentSession, { + * token: "dummy-token" + * }) + * }) + * ).layer + * + * Effect.gen(function*() { + * const router = yield* HttpLayerRouter.HttpRouter + * yield* router.add( + * "GET", + * "/hello", + * Effect.gen(function*() { + * // Requests can now access the current session + * const session = yield* CurrentSession + * return HttpServerResponse.text(`Hello, World! Your token is ${session.token}`) + * }) + * ) + * }).pipe( + * Layer.effectDiscard, + * // Provide the SessionMiddleware & CorsMiddleware to some routes + * Layer.provide([SessionMiddleware, CorsMiddleware]) + * ) + * ``` + * + * @since 1.0.0 + * @category Middleware + */ +export const middleware: + & middleware.Make + & (< + Config extends { + provides?: any + handles?: any + } = {} + >() => middleware.Make< + Config extends { provides: infer R } ? R : never, + Config extends { handles: infer E } ? E : never + >) = function() { + if (arguments.length === 0) { + return makeMiddleware as any + } + return makeMiddleware(arguments[0]) + } + +const makeMiddleware = (middleware: any) => + new MiddlewareImpl( + Effect.isEffect(middleware) ? + Layer.scopedContext(Effect.map(middleware, (fn) => Context.unsafeMake(new Map([[fnContextKey, fn]])))) : + Layer.succeedContext(Context.unsafeMake(new Map([[fnContextKey, middleware]]))) as any + ) + +let middlewareId = 0 +const fnContextKey = "@effect/platform/HttpLayerRouter/MiddlewareFn" + +class MiddlewareImpl< + Config extends { + provides: any + handles: any + error: any + requires: any + layerError: any + layerRequires: any + } +> implements Middleware { + readonly [MiddlewareTypeId]: Config = {} as any + + constructor( + readonly layerFn: Layer.Layer, + readonly dependencies?: Layer.Layer + ) { + const contextKey = `@effect/platform/HttpLayerRouter/Middleware-${++middlewareId}` as const + this.layer = Layer.scopedContext(Effect.gen(this, function*() { + const context = yield* Effect.context() + const stack = [context.unsafeMap.get(fnContextKey)] + if (this.dependencies) { + const memoMap = yield* Layer.CurrentMemoMap + const scope = Context.get(context, Scope.Scope) + const depsContext = yield* Layer.buildWithMemoMap(this.dependencies, memoMap, scope) + // eslint-disable-next-line no-restricted-syntax + stack.push(...getMiddleware(depsContext)) + } + return Context.unsafeMake(new Map([[contextKey, stack]])) + })).pipe(Layer.provide(this.layerFn)) + } + + layer: any + + combine< + Config2 extends { + provides: any + handles: any + error: any + requires: any + layerError: any + layerRequires: any + } + >(other: Middleware): Middleware { + return new MiddlewareImpl( + this.layerFn, + this.dependencies ? Layer.provideMerge(this.dependencies, other.layer as any) : other.layer as any + ) as any + } +} + +const middlewareCache = new WeakMap, any>() +const getMiddleware = (context: Context.Context): Array => { + let arr = middlewareCache.get(context) + if (arr) return arr + const topLevel = Arr.empty>() + let maxLength = 0 + for (const [key, value] of context.unsafeMap) { + if (key.startsWith("@effect/platform/HttpLayerRouter/Middleware-")) { + topLevel.push(value) + if (value.length > maxLength) { + maxLength = value.length + } + } + } + if (topLevel.length === 0) { + arr = [] + } else { + const middleware = new Set() + for (let i = maxLength - 1; i >= 0; i--) { + for (const arr of topLevel) { + if (i < arr.length) { + middleware.add(arr[i]) + } + } + } + arr = Arr.fromIterable(middleware).reverse() + } + middlewareCache.set(context, arr) + return arr +} + +/** + * @since 1.0.0 + * @category Middleware + */ +export declare namespace middleware { + /** + * @since 1.0.0 + * @category Middleware + */ + export type Make = { + ( + middleware: Effect.Effect< + ( + effect: Effect.Effect< + HttpServerResponse.HttpServerResponse, + Handles, + Provides + > + ) => Effect.Effect< + HttpServerResponse.HttpServerResponse, + E, + R + >, + EX, + RX + > + ): Middleware<{ + provides: Provides + handles: Handles + error: E + requires: Exclude + layerError: EX + layerRequires: Exclude + }> + ( + middleware: ( + effect: Effect.Effect< + HttpServerResponse.HttpServerResponse, + Handles, + Provides + > + ) => Effect.Effect< + HttpServerResponse.HttpServerResponse, + E, + R + > + ): Middleware<{ + provides: Provides + handles: Handles + error: E + requires: Exclude + layerError: never + layerRequires: never + }> + } + + /** + * @since 1.0.0 + * @category Middleware + */ + export type Fn = ( + effect: Effect.Effect + ) => Effect.Effect +} + +/** + * A middleware that applies CORS headers to the HTTP response. + * + * @since 1.0.0 + * @category Middleware + */ +export const cors = ( + options?: { + readonly allowedOrigins?: ReadonlyArray | undefined + readonly allowedMethods?: ReadonlyArray | undefined + readonly allowedHeaders?: ReadonlyArray | undefined + readonly exposedHeaders?: ReadonlyArray | undefined + readonly maxAge?: number | undefined + readonly credentials?: boolean | undefined + } | undefined +): Layer.Layer => middleware(HttpMiddleware.cors(options)).layer + +/** + * ```ts + * import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer" + * import * as NodeRuntime from "@effect/platform-node/NodeRuntime" + * import * as HttpApi from "@effect/platform/HttpApi" + * import * as HttpApiBuilder from "@effect/platform/HttpApiBuilder" + * import * as HttpApiEndpoint from "@effect/platform/HttpApiEndpoint" + * import * as HttpApiGroup from "@effect/platform/HttpApiGroup" + * import * as HttpApiScalar from "@effect/platform/HttpApiScalar" + * import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" + * import * as HttpMiddleware from "@effect/platform/HttpMiddleware" + * import * as Effect from "effect/Effect" + * import * as Layer from "effect/Layer" + * import { createServer } from "http" + * + * // First, we define our HttpApi + * class MyApi extends HttpApi.make("api").add( + * HttpApiGroup.make("users").add( + * HttpApiEndpoint.get("me", "/me") + * ).prefix("/users") + * ) {} + * + * // Implement the handlers for the API + * const UsersApiLayer = HttpApiBuilder.group(MyApi, "users", (handers) => handers.handle("me", () => Effect.void)) + * + * // Use `HttpLayerRouter.addHttpApi` to register the API with the router + * const HttpApiRoutes = HttpLayerRouter.addHttpApi(MyApi, { + * openapiPath: "/docs/openapi.json" + * }).pipe( + * // Provide the api handlers layer + * Layer.provide(UsersApiLayer) + * ) + * + * // Create a /docs route for the API documentation + * const DocsRoute = HttpApiScalar.layerHttpLayerRouter({ + * api: MyApi, + * path: "/docs" + * }) + * + * const CorsMiddleware = HttpLayerRouter.middleware(HttpMiddleware.cors()) + * // You can also use HttpLayerRouter.cors() to create a CORS middleware + * + * // Finally, we merge all routes and serve them using the Node HTTP server + * const AllRoutes = Layer.mergeAll( + * HttpApiRoutes, + * DocsRoute + * ).pipe( + * Layer.provide(CorsMiddleware.layer) + * ) + * + * HttpLayerRouter.serve(AllRoutes).pipe( + * Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })), + * Layer.launch, + * NodeRuntime.runMain + * ) + * ``` + * + * @since 1.0.0 + * @category HttpApi + */ +export const addHttpApi = ( + api: HttpApi.HttpApi, + options?: { + readonly openapiPath?: `/${string}` | undefined + } +): Layer.Layer< + HttpApi.Api, + never, + HttpRouter | HttpApiGroup.HttpApiGroup.ToService | R | HttpApiGroup.HttpApiGroup.ErrorContext +> => { + const ApiMiddleware = middleware(HttpApiBuilder.buildMiddleware(api)).layer + + return HttpApiBuilder.Router.unwrap(Effect.fnUntraced(function*(router_) { + const contextMap = new Map() + const router = yield* HttpRouter + const routes = Arr.empty>() + const context = yield* Effect.context() + + contextMap.set(HttpApi.Api.key, { api, context }) + + for (const route of router_.routes) { + routes.push(makeRoute(route as any)) + } + + yield* (router.addAll(routes) as Effect.Effect) + + if (options?.openapiPath) { + const spec = OpenApi.fromApi(api) + yield* router.add("GET", options.openapiPath, Effect.succeed(HttpServerResponse.unsafeJson(spec))) + } + + return Context.unsafeMake(contextMap) + }, Layer.effectContext)).pipe( + Layer.provide(ApiMiddleware) + ) +} + +/** + * Serves the provided application layer as an HTTP server. + * + * @since 1.0.0 + * @category Server + */ +export const serve = >( + appLayer: Layer.Layer, + options?: { + readonly routerConfig?: Partial | undefined + readonly disableLogger?: boolean | undefined + readonly disableListenLog?: boolean + /** + * Middleware to apply to the HTTP server. + * + * NOTE: This middleware is applied to the entire HTTP server chain, + * including the sending of the response. This means that modifications + * to the response **WILL NOT** be reflected in the final response sent to the + * client. + * + * Use HttpLayerRouter.middleware to create middleware that can modify the + * response. + */ + readonly middleware?: ( + effect: Effect.Effect< + HttpServerResponse.HttpServerResponse, + Type.Only<"Error", R> | HttpServerError.RouteNotFound, + Scope.Scope | HttpServerRequest.HttpServerRequest | Type.Only<"Requires", R> + > + ) => Effect.Effect + } +): Layer.Layer | Exclude, HttpRouter>> => { + let middleware: any = options?.middleware + if (options?.disableLogger !== true) { + middleware = middleware ? compose(middleware, HttpMiddleware.logger) : HttpMiddleware.logger + } + const RouterLayer = options?.routerConfig + ? Layer.provide(layer, Layer.succeed(RouterConfig, options.routerConfig)) + : layer + return Effect.gen(function*() { + const router = yield* HttpRouter + const handler = router.asHttpEffect() + return middleware ? HttpServer.serve(handler, middleware) : HttpServer.serve(handler) + }).pipe( + Layer.unwrapScoped, + options?.disableListenLog ? identity : HttpServer.withLogAddress, + Layer.provide(appLayer), + Layer.provide(RouterLayer) + ) as any +} diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index 40497125aed..06f07dafbf7 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -138,6 +138,11 @@ export * as HttpClientResponse from "./HttpClientResponse.js" */ export * as HttpIncomingMessage from "./HttpIncomingMessage.js" +/** + * @since 1.0.0 + */ +export * as HttpLayerRouter from "./HttpLayerRouter.js" + /** * @since 1.0.0 * @category models diff --git a/packages/rpc/src/RpcServer.ts b/packages/rpc/src/RpcServer.ts index bd032fa0d15..7f192623361 100644 --- a/packages/rpc/src/RpcServer.ts +++ b/packages/rpc/src/RpcServer.ts @@ -3,6 +3,7 @@ */ import * as Headers from "@effect/platform/Headers" import * as HttpApp from "@effect/platform/HttpApp" +import * as HttpLayerRouter from "@effect/platform/HttpLayerRouter" import * as HttpRouter from "@effect/platform/HttpRouter" import * as HttpServerRequest from "@effect/platform/HttpServerRequest" import * as HttpServerResponse from "@effect/platform/HttpServerResponse" @@ -722,6 +723,39 @@ export const layer = ( | Rpc.Middleware > => Layer.scopedDiscard(Effect.forkScoped(Effect.interruptible(make(group, options)))) +/** + * Create a RPC server that registers a HTTP route with a `HttpLayerRouter`. + * + * It defaults to using websockets for communication, but can be configured to + * use HTTP. + * + * @since 1.0.0 + * @category protocol + */ +export const layerHttpRouter = (options: { + readonly group: RpcGroup.RpcGroup + readonly path: HttpRouter.PathInput + readonly protocol?: "http" | "websocket" | undefined + readonly disableTracing?: boolean | undefined + readonly spanPrefix?: string | undefined + readonly spanAttributes?: Record | undefined + readonly concurrency?: number | "unbounded" | undefined +}): Layer.Layer< + never, + never, + | RpcSerialization.RpcSerialization + | HttpLayerRouter.HttpRouter + | Rpc.ToHandler + | Rpc.Middleware +> => + layer(options.group, options).pipe( + Layer.provide( + options.protocol === "http" + ? layerProtocolHttpRouter(options) + : layerProtocolWebsocketRouter(options) + ) + ) + /** * @since 1.0.0 * @category protocol @@ -822,6 +856,27 @@ export const makeProtocolWebsocket: ( return protocol }) +/** + * @since 1.0.0 + * @category protocol + */ +export const makeProtocolWebsocketRouter: ( + options: { + readonly path: HttpRouter.PathInput + } +) => Effect.Effect< + Protocol["Type"], + never, + RpcSerialization.RpcSerialization | HttpLayerRouter.HttpRouter +> = Effect.fnUntraced(function*(options: { + readonly path: HttpRouter.PathInput +}) { + const router = yield* HttpLayerRouter.HttpRouter + const { httpApp, protocol } = yield* makeProtocolWithHttpAppWebsocket + yield* router.add("GET", options.path, httpApp) + return protocol +}) + /** * A rpc protocol that uses websockets for communication. * @@ -839,6 +894,19 @@ export const layerProtocolWebsocket = (options: { ) } +/** + * A rpc protocol that uses websockets for communication. + * + * Uses a `HttpLayerRouter` to provide the websocket endpoint. + * + * @since 1.0.0 + * @category protocol + */ +export const layerProtocolWebsocketRouter = (options: { + readonly path: HttpLayerRouter.PathInput +}): Layer.Layer => + Layer.effect(Protocol, makeProtocolWebsocketRouter(options)) + /** * @since 1.0.0 * @category protocol @@ -985,6 +1053,19 @@ export const makeProtocolHttp = Effect.fnUntraced(function*(options: { ) } +/** + * A rpc protocol that uses streaming http for communication. + * + * Uses a `HttpLayerRouter` to provide the http endpoint. + * + * @since 1.0.0 + * @category protocol + */ +export const layerProtocolHttpRouter = (options: { + readonly path: HttpRouter.PathInput +}): Layer.Layer => + Layer.effect(Protocol, makeProtocolHttpRouter(options)) + /** * @since 1.0.0 * @category http app