EdgeAIG's picture
download
raw
21.8 kB
/**
* Builds server-side routers for Effect HTTP applications.
*
* `HttpRouter` collects routes and middleware while an application layer is
* being built. Once the router is complete, it handles each
* `HttpServerRequest` by finding a matching route and producing an
* `HttpServerResponse`. The module also includes helpers for route definitions,
* prefixes, parameters, request decoding, CORS, and running the router.
*
* @since 4.0.0
*/
import * as Arr from "../../Array.js";
import * as Context from "../../Context.js";
import * as Effect from "../../Effect.js";
import { compose, dual, identity } from "../../Function.js";
import * as Layer from "../../Layer.js";
import * as Option from "../../Option.js";
import * as Schema from "../../Schema.js";
import * as Scope from "../../Scope.js";
import * as Tracer from "../../Tracer.js";
import * as FindMyWay from "./FindMyWay.js";
import * as HttpEffect from "./HttpEffect.js";
import * as HttpMiddleware from "./HttpMiddleware.js";
import * as HttpServer from "./HttpServer.js";
import * as HttpServerError from "./HttpServerError.js";
import * as HttpServerRequest from "./HttpServerRequest.js";
import * as HttpServerResponse from "./HttpServerResponse.js";
const TypeId = "~effect/http/HttpRouter";
/**
* Service tag for the HTTP router used while constructing an HTTP application.
* Route and middleware layers require this service to register themselves with
* the router.
*
* @category HttpRouter
* @since 4.0.0
*/
export const HttpRouter = /*#__PURE__*/Context.Service("effect/http/HttpRouter");
/**
* Constructs an empty `HttpRouter` service.
*
* **Details**
*
* The returned router accepts route and middleware registrations and later routes
* the current `HttpServerRequest` to the matching `HttpServerResponse`.
*
* @category HttpRouter
* @since 4.0.0
*/
export const make = /*#__PURE__*/Effect.gen(function* () {
const router = FindMyWay.make(yield* RouterConfig);
const middleware = new Set();
const addAll = routes => Effect.contextWith(context => {
const middleware = getMiddleware(context);
const applyMiddleware = 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)
});
if (route.method === "*") {
if (route.path.endsWith("/*")) {
router.all(route.path, route);
router.all(route.path.slice(0, -2), route);
} else {
router.all(route.path, route);
}
} else {
if (route.path.endsWith("/*")) {
router.on(route.method, route.path, route);
router.on(route.method, route.path.slice(0, -2), route);
} else {
router.on(route.method, route.path, route);
}
}
}
return Effect.void;
});
return HttpRouter.of({
[TypeId]: TypeId,
prefixed(prefix) {
return HttpRouter.of({
...this,
prefixed: newPrefix => this.prefixed(prefixPath(prefix, newPrefix)),
addAll: routes => addAll(routes.map(prefixRoute(prefix))),
add: (method, path, handler, options) => addAll([makeRoute({
method,
path: prefixPath(path, prefix),
handler: HttpServerResponse.isHttpServerResponse(handler) ? Effect.succeed(handler) : Effect.isEffect(handler) ? handler : Effect.flatMap(HttpServerRequest.HttpServerRequest, handler),
uninterruptible: options?.uninterruptible ?? false,
prefix
})])
});
},
addAll,
add: (method, path, handler, options) => addAll([route(method, path, handler, options)]),
addGlobalMiddleware: middleware_ => Effect.sync(() => {
middleware.add(middleware_);
}),
asHttpEffect() {
let handler = Effect.withFiber(fiber => {
const contextMap = new Map(fiber.context.mapUnsafe);
const request = contextMap.get(HttpServerRequest.HttpServerRequest.key);
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.HttpServerError({
reason: new HttpServerError.RouteNotFound({
request
})
}));
}
const route = result.handler;
if (Option.isSome(route.prefix)) {
contextMap.set(HttpServerRequest.HttpServerRequest.key, sliceRequestUrl(request, route.prefix.value));
}
contextMap.set(HttpServerRequest.ParsedSearchParams.key, result.searchParams);
contextMap.set(RouteContext.key, {
route,
params: result.params
});
const span = contextMap.get(Tracer.ParentSpan.key);
if (span && span._tag === "Span") {
span.attribute("http.route", route.path);
}
return Effect.provideContext(route.uninterruptible ? route.handler : Effect.interruptible(route.handler), Context.makeUnsafe(contextMap));
});
if (middleware.size === 0) return handler;
for (const fn of Arr.reverse(middleware)) {
handler = fn(handler);
}
return handler;
}
});
});
function sliceRequestUrl(request, prefix) {
const prefexLen = prefix.length;
return request.modify({
url: request.url.length <= prefexLen ? "/" : request.url.slice(prefexLen)
});
}
/**
* Context reference for low-level router configuration.
*
* **Details**
*
* The value is passed to the route matcher when an `HttpRouter` is created and
* defaults to an empty configuration.
*
* @category configuration
* @since 4.0.0
*/
export const RouterConfig = /*#__PURE__*/Context.Reference("effect/http/HttpRouter/RouterConfig", {
defaultValue: () => ({})
});
/**
* Service for the matched HTTP route in the current request.
*
* **When to use**
*
* Use to read captured path parameters and route metadata while handling a
* request matched by the router.
*
* **Details**
*
* It provides the route definition and the path parameters captured by the route
* matcher.
*
* @category services
* @since 4.0.0
*/
export class RouteContext extends /*#__PURE__*/Context.Service()("effect/http/HttpRouter/RouteContext") {}
/**
* Effect that returns the path parameters captured for the current matched route.
*
* @category getters
* @since 4.0.0
*/
export const params = /*#__PURE__*/Effect.map(RouteContext, _ => _.params);
/**
* Decodes a schema from the current request and its JSON body.
*
* **Details**
*
* The input passed to the schema includes the request method, URL, headers,
* cookies, path parameters, search parameters, and parsed JSON body. The effect
* fails if the body cannot be parsed or the schema decode fails.
*
* @category schemas
* @since 4.0.0
*/
export const schemaJson = (schema, options) => {
const parse = Schema.decodeUnknownEffect(schema);
return Effect.contextWith(context => {
const request = Context.get(context, HttpServerRequest.HttpServerRequest);
const searchParams = Context.get(context, HttpServerRequest.ParsedSearchParams);
const routeContext = Context.get(context, RouteContext);
return Effect.flatMap(request.json, body => parse({
method: request.method,
url: request.url,
headers: request.headers,
cookies: request.cookies,
pathParams: routeContext.params,
searchParams,
body
}, options));
});
};
/**
* Decodes a schema from the current request without reading the request body.
*
* **Details**
*
* The input passed to the schema includes the request method, URL, headers,
* cookies, path parameters, and search parameters.
*
* @category schemas
* @since 4.0.0
*/
export const schemaNoBody = (schema, options) => {
const parse = Schema.decodeUnknownEffect(schema);
return Effect.contextWith(context => {
const request = Context.get(context, HttpServerRequest.HttpServerRequest);
const searchParams = Context.get(context, HttpServerRequest.ParsedSearchParams);
const routeContext = Context.get(context, RouteContext);
return parse({
method: request.method,
url: request.url,
headers: request.headers,
cookies: request.cookies,
pathParams: routeContext.params,
searchParams
}, options);
});
};
/**
* Decodes a schema from the current route path parameters and search parameters.
*
* **Details**
*
* When the same key appears in both sources, the path parameter value is used.
*
* @category schemas
* @since 4.0.0
*/
export const schemaParams = (schema, options) => {
const parse = Schema.decodeUnknownEffect(schema);
return Effect.contextWith(context => {
const searchParams = Context.get(context, HttpServerRequest.ParsedSearchParams);
const routeContext = Context.get(context, RouteContext);
return parse({
...searchParams,
...routeContext.params
}, options);
});
};
/**
* Decodes a schema from the path parameters captured for the current matched
* route.
*
* @category schemas
* @since 4.0.0
*/
export const schemaPathParams = (schema, options) => {
const parse = Schema.decodeUnknownEffect(schema);
return Effect.flatMap(params, _ => parse(_, options));
};
/**
* Creates a layer that accesses the current `HttpRouter` service and runs the
* supplied effect.
*
* **When to use**
*
* Use when you need to register routes or middleware with the router during layer
* construction.
*
* **Example** (Registering routes during layer construction)
*
* ```ts
* import { Effect, Layer } from "effect"
* import { HttpRouter } from "effect/unstable/http"
*
* const MyRoute = Layer.effectDiscard(Effect.gen(function*() {
* const router = yield* HttpRouter.HttpRouter
*
* // then use `yield* router.add(...)` to add a route
* }))
* ```
*
* @category HttpRouter
* @since 4.0.0
*/
export const use = f => Layer.effectDiscard(Effect.flatMap(HttpRouter, f));
/**
* Create a layer that adds a single route to the HTTP router.
*
* **Example** (Adding a GET route)
*
* ```ts
* import { Effect } from "effect"
* import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
*
* const Route = HttpRouter.add(
* "GET",
* "/hello",
* Effect.succeed(HttpServerResponse.text("Hello, World!"))
* )
* ```
*
* @category HttpRouter
* @since 4.0.0
*/
export const add = (method, path, handler, options) => use(router => router.add(method, path, handler, options));
/**
* Create a layer that adds multiple routes to the HTTP router.
*
* **Example** (Adding multiple routes)
*
* ```ts
* import { Effect } from "effect"
* import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
*
* const Routes = HttpRouter.addAll([
* HttpRouter.route(
* "GET",
* "/hello",
* Effect.succeed(HttpServerResponse.text("Hello, World!"))
* )
* ])
* ```
*
* @category HttpRouter
* @since 4.0.0
*/
export const addAll = (routes, options) => Layer.effectDiscard(Effect.gen(function* () {
const toAdd = Effect.isEffect(routes) ? yield* routes : routes;
let router = yield* HttpRouter;
if (options?.prefix) {
router = router.prefixed(options.prefix);
}
yield* router.addAll(toAdd);
}));
/**
* Layer that provides a newly constructed `HttpRouter`.
*
* @category HttpRouter
* @since 4.0.0
*/
export const layer = /*#__PURE__*/Layer.effect(HttpRouter)(make);
/**
* Builds an application layer with a router and returns the router as an HTTP
* handler effect.
*
* **Details**
*
* The returned effect handles the current `HttpServerRequest` in the current
* `Scope`; route request markers are converted into the ordinary requirements of
* the returned handler.
*
* @category HttpRouter
* @since 4.0.0
*/
export const toHttpEffect = appLayer => Effect.gen(function* () {
const context = yield* Layer.build(Layer.provideMerge(appLayer, layer));
const router = Context.get(context, HttpRouter);
// @effect-diagnostics effect/returnEffectInGen:off
return router.asHttpEffect();
});
const RouteTypeId = "~effect/http/HttpRouter/Route";
const makeRoute = options => ({
...options,
uninterruptible: options.uninterruptible ?? false,
prefix: typeof options.prefix === "string" ? Option.some(options.prefix) : options.prefix ?? Option.none(),
[RouteTypeId]: RouteTypeId
});
/**
* Constructs a `Route` from an HTTP method, path, and handler.
*
* **Details**
*
* The handler may be a static response, an effect that produces a response, or a
* function from the current request to a response effect. Set `uninterruptible` to
* prevent the route handler from being made interruptible while it runs.
*
* @category Route
* @since 4.0.0
*/
export const route = (method, path, handler, options) => makeRoute({
...options,
method,
path,
handler: HttpServerResponse.isHttpServerResponse(handler) ? Effect.succeed(handler) : Effect.isEffect(handler) ? handler : Effect.flatMap(HttpServerRequest.HttpServerRequest, handler),
uninterruptible: options?.uninterruptible ?? false
});
const removeTrailingSlash = path => path.endsWith("/") ? path.slice(0, -1) : path;
/**
* Adds a path prefix to a route path.
*
* **Details**
*
* Trailing slashes are removed from the prefix; `/` becomes the prefix itself and
* `*` becomes a wildcard route under the prefix.
*
* @category PathInput
* @since 4.0.0
*/
export const prefixPath = /*#__PURE__*/dual(2, (self, prefix) => {
prefix = removeTrailingSlash(prefix);
if (self === "*") return `${prefix}/*`;else if (self === "/") return prefix;
return prefix + self;
});
/**
* Returns a copy of a route with its path prefixed.
*
* **Details**
*
* The prefix is also tracked on the route so that, when the route handles a
* request, the matched prefix can be removed from the request URL seen by the
* handler.
*
* @category Route
* @since 4.0.0
*/
export const prefixRoute = /*#__PURE__*/dual(2, (self, prefix) => makeRoute({
...self,
path: prefixPath(self.path, prefix),
prefix: Option.match(self.prefix, {
onNone: () => prefix,
onSome: existingPrefix => prefixPath(existingPrefix, prefix)
})
}));
const MiddlewareTypeId = "~effect/http/HttpRouter/Middleware";
/**
* Create a middleware layer that can be used to modify requests and responses.
*
* **Details**
*
* By default, the middleware only affects the routes that it is provided to.
*
* If you want to create a middleware that applies globally to all routes, pass
* the `global` option as `true`.
*
* **Example** (Applying route and global middleware)
*
* ```ts
* import { Context, Effect, Layer } from "effect"
* import { HttpMiddleware, HttpRouter, HttpServerResponse } from "effect/unstable/http"
*
* // Here we are defining a CORS middleware
* const CorsMiddleware = HttpRouter.middleware(HttpMiddleware.cors()).layer
* // You can also use HttpRouter.cors() to create a CORS middleware
*
* class CurrentSession extends Context.Service<CurrentSession, {
* readonly token: string
* }>()("CurrentSession") {}
*
* // You can create middleware that provides a service to the HTTP requests.
* const SessionMiddleware = HttpRouter.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* HttpRouter.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])
* )
* ```
*
* @category middleware
* @since 4.0.0
*/
export const middleware = function () {
if (arguments.length === 0) {
return makeMiddleware;
}
return makeMiddleware(arguments[0], arguments[1]);
};
const makeMiddleware = (middleware, options) => options?.global ? Layer.effectDiscard(Effect.gen(function* () {
const router = yield* HttpRouter;
const fn = Effect.isEffect(middleware) ? yield* middleware : middleware;
yield* router.addGlobalMiddleware(fn);
})) : new MiddlewareImpl(Effect.isEffect(middleware) ? Layer.effectContext(Effect.map(middleware, fn => Context.makeUnsafe(new Map([[fnContextKey, fn]])))) : Layer.succeedContext(Context.makeUnsafe(new Map([[fnContextKey, middleware]]))));
let middlewareId = 0;
const fnContextKey = "effect/http/HttpRouter/MiddlewareFn";
class MiddlewareImpl {
[MiddlewareTypeId] = {};
layerFn;
dependencies;
constructor(layerFn, dependencies) {
this.layerFn = layerFn;
this.dependencies = dependencies;
const contextKey = `effect/http/HttpRouter/Middleware-${++middlewareId}`;
this.layer = Layer.effectContext(Effect.gen({
self: this
}, function* () {
const context = yield* Effect.context();
const stack = [context.mapUnsafe.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);
stack.push(...getMiddleware(depsContext));
}
return Context.makeUnsafe(new Map([[contextKey, stack]]));
})).pipe(Layer.provide(this.layerFn));
}
layer;
combine(other) {
return new MiddlewareImpl(this.layerFn, this.dependencies ? Layer.provideMerge(this.dependencies, other.layer) : other.layer);
}
}
const middlewareCache = /*#__PURE__*/new WeakMap();
const getMiddleware = context => {
let arr = middlewareCache.get(context);
if (arr) return arr;
const topLevel = Arr.empty();
let maxLength = 0;
for (const [key, value] of context.mapUnsafe) {
if (key.startsWith("effect/http/HttpRouter/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;
};
/**
* Middleware that applies CORS headers to the HTTP response.
*
* @category middleware
* @since 4.0.0
*/
export const cors = options => middleware(HttpMiddleware.cors(options), {
global: true
});
/**
* Middleware that disables the logger for some routes.
*
* **Example** (Disabling route logging)
*
* ```ts
* import { Effect, Layer } from "effect"
* import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
*
* const Route = HttpRouter.add(
* "GET",
* "/hello",
* Effect.succeed(HttpServerResponse.text("Hello, World!"))
* ).pipe(
* // disable the logger for this route
* Layer.provide(HttpRouter.disableLogger)
* )
* ```
*
* @category middleware
* @since 4.0.0
*/
export const disableLogger = /*#__PURE__*/middleware(HttpMiddleware.withLoggerDisabled).layer;
/**
* Provides request-level dependencies to some routes.
*
* @category middleware
* @since 4.0.0
*/
export const provideRequest = layer => self => Layer.provide(self, middleware()(Effect.gen(function* () {
const services = yield* Layer.build(layer);
return effect => Effect.provideContext(effect, services);
})).layer);
/**
* Runs the provided application layer as an HTTP server.
*
* @category server
* @since 4.0.0
*/
export const serve = (appLayer, options) => {
let middleware = 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.unwrap, Layer.provideMerge(appLayer), Layer.provide(RouterLayer), options?.disableListenLog ? identity : HttpServer.withLogAddress);
};
/**
* Builds a Fetch-compatible request handler from an HTTP router application
* layer.
*
* **Details**
*
* The result contains a `handler` function that converts Web `Request` values to
* Web `Response` values and a `dispose` function for releasing the layer
* resources.
*
* @category server
* @since 4.0.0
*/
export const toWebHandler = (appLayer, options) => {
let middleware = 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 HttpEffect.toWebHandlerLayerWith(Layer.provideMerge(appLayer, RouterLayer), {
toHandler: s => Effect.succeed(Context.get(s, HttpRouter).asHttpEffect()),
middleware,
memoMap: options?.memoMap
});
};
//# sourceMappingURL=HttpRouter.js.map

Xet Storage Details

Size:
21.8 kB
·
Xet hash:
5ee3811f86607f2adfbd8f4c36eac8f06a77b68e4ad06f4bb61c49a68788084d

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.