Skip to content

Commit

Permalink
refactor: cleaner entrypoints
Browse files Browse the repository at this point in the history
  • Loading branch information
TylorS committed May 28, 2024
1 parent 0a1ff96 commit 4c919f1
Show file tree
Hide file tree
Showing 12 changed files with 84 additions and 59 deletions.
2 changes: 2 additions & 0 deletions examples/realworld/.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ VITE_DATABASE_PASSWORD=effect_dev
VITE_DATABASE_NAME=realworld

VITE_JWT_SECRET=replace_me_with_a_real_secret_not_in_an_env_var_pls

VITE_OTEL_TRACE_URL=http://localhost:4318/v1/traces
13 changes: 12 additions & 1 deletion examples/realworld/src/api/infrastructure.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as NodeSdk from "@effect/opentelemetry/NodeSdk"
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"
import { getRandomValues } from "@typed/id"
import { ArticlesLive } from "@typed/realworld/api/articles/infrastructure/ArticlesLive"
import { CommentsLive } from "@typed/realworld/api/comments/infrastructure/CommentsLive"
Expand All @@ -14,7 +17,15 @@ export const Live = Layer.mergeAll(ArticlesLive, CommentsLive, ProfilesLive, Tag
// You probably shouldn't use import.meta.env in a real application
// as it will leak information into your bundle since Vite replaces these values at build time
Layer.provide(Layer.setConfigProvider(ConfigProvider.fromJson(import.meta.env))),
Layer.provideMerge(NodeSwaggerFiles.SwaggerFilesLive)
Layer.provideMerge(NodeSwaggerFiles.SwaggerFilesLive),
Layer.provideMerge(NodeSdk.layer(() => ({
resource: { serviceName: "realworld" },
spanProcessor: new BatchSpanProcessor(
new OTLPTraceExporter({
url: import.meta.env.VITE_OTEL_TRACE_URL
})
)
})))
)

export { CurrentUserLive } from "@typed/realworld/api/users/infrastructure"
9 changes: 7 additions & 2 deletions examples/realworld/src/api/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ServerRouterBuilder } from "@typed/server"
import { ServerResponse, ServerRouterBuilder } from "@typed/server"
import { Effect } from "effect"
import { handleArticles } from "./articles/handlers"
import { handleComments } from "./comments/handlers"
import { handleProfiles } from "./profiles/handlers"
Expand All @@ -12,5 +13,9 @@ export const server = ServerRouterBuilder.make(Spec, { enableDocs: true, docsPat
handleProfiles,
handleGetTags,
handleUsers,
ServerRouterBuilder.build
ServerRouterBuilder.build,
// Handle API errors
Effect.catchTag("RouteDecodeError", (_) => ServerResponse.json(_, { status: 400 })),
Effect.catchTag("Unauthorized", () => ServerResponse.empty({ status: 401 })),
Effect.catchTag("RouteNotMatched", () => ServerResponse.empty({ status: 404 }))
)
7 changes: 3 additions & 4 deletions examples/realworld/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { hydrateFromWindow, hydrateToLayer } from "@typed/core"
import { hydrateFromWindow } from "@typed/core"
import { Storage } from "@typed/dom/Storage"
import "./styles.css"

import { Effect, Layer } from "effect"
import * as Ui from "./ui"
import { UiClient } from "./ui/client"

Ui.main.pipe(
hydrateToLayer,
UiClient.pipe(
Layer.provide(Ui.Live),
Layer.provide(Storage.layer(localStorage)),
Layer.provide(hydrateFromWindow(window, { rootElement: document.getElementById("app")! })),
Expand Down
36 changes: 5 additions & 31 deletions examples/realworld/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,25 @@
import * as NodeSdk from "@effect/opentelemetry/NodeSdk"
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"
import * as Node from "@typed/core/Node"
import { toServerRouter } from "@typed/core/Platform"
import * as Api from "@typed/realworld/api"
import { CurrentUserLive, Live } from "@typed/realworld/api/infrastructure"
import * as Ui from "@typed/realworld/ui"
import { ServerHeaders, ServerResponse, ServerRouter } from "@typed/server"
import { ServerRouter } from "@typed/server"
import { Effect, LogLevel } from "effect"
import sms from "source-map-support"
import { UiServer } from "./ui/server"

// Enable source maps for errors
sms.install()

// Convert our UI router to a ServerRouter and provide a layout to construct a full HTML document.
toServerRouter(Ui.router, { layout: Ui.document }).pipe(
ServerRouter.catchAll((error) =>
ServerResponse.empty({
status: 303,
headers: ServerHeaders.fromInput({
location: error._tag === "RedirectError" ? error.path.toString() : "/login"
})
})
),
// Our server is a composition of our API and UI servers
UiServer.pipe(
// Mount our API
ServerRouter.mountApp(
"/api",
Effect.catchTag(Api.server, "Unauthorized", () => ServerResponse.empty({ status: 401 }))
),
ServerRouter.mountApp("/api", Api.server),
// Provide all resources which change per-request
Effect.provide(CurrentUserLive),
// Start the server. Integrates with our Vite plugin to serve client assets using Vite for development and
// using a static file server, with gzip support, for production.
Node.listen({ port: 3000, serverDirectory: import.meta.dirname, logLevel: LogLevel.Debug }),
// Provide all static resources which do not change per-request
Effect.provide(Live),
// OpenTelemetry tracing
Effect.provide(
NodeSdk.layer(() => ({
resource: { serviceName: "realworld" },
spanProcessor: new BatchSpanProcessor(
new OTLPTraceExporter({
url: "http://localhost:4318/v1/traces"
}) as any // Something weird with the types here "Type 'OTLPTraceExporter' is missing the following properties from type 'SpanExporter': export, shutdown"
)
}))
),
// Kick off the application, capturing SIGINT and SIGTERM to gracefully shutdown the server
// as well as respond to Vite's HMR requests to clean up resources when change occur during development.
Node.run
Expand Down
13 changes: 13 additions & 0 deletions examples/realworld/src/ui/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Fx, hydrateToLayer, Navigation, Router } from "@typed/core"
import { isAuthenticated } from "@typed/realworld/services"
import { Effect } from "effect"
import * as Routes from "./common/routes"
import { layout } from "./layout"
import { router } from "./router"

const onNotFound = Effect.if(Fx.first(isAuthenticated), {
onFalse: () => new Navigation.RedirectError({ path: Routes.login.interpolate({}) }),
onTrue: () => new Navigation.RedirectError({ path: Routes.home.interpolate({}) })
})

export const UiClient = router.pipe(Router.notFoundWith(onNotFound), layout, hydrateToLayer)
15 changes: 8 additions & 7 deletions examples/realworld/src/ui/components/EditArticle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@typed/realworld/model"
import type { Unauthorized, Unprocessable } from "@typed/realworld/services/errors"
import * as Routes from "@typed/realworld/ui/common/routes"
import type { Cause } from "effect"
import { Effect } from "effect"

export type EditArticleFields = Pick<
Expand All @@ -21,14 +22,14 @@ export type EditArticleFields = Pick<
export function useEditArticle<R, R2>(
initial: RefSubject.Computed<
EditArticleFields,
Unprocessable | Unauthorized | ParseError,
Unprocessable | Unauthorized | ParseError | Cause.NoSuchElementException,
R
>,
onSubmit: (
updated: EditArticleFields
) => Effect.Effect<
unknown,
Unprocessable | Unauthorized | ParseError | NavigationError,
Unprocessable | Unauthorized | ParseError | NavigationError | Cause.NoSuchElementException,
R2
>
) {
Expand All @@ -43,9 +44,9 @@ export function useEditArticle<R, R2>(
Effect.flatMap(onSubmit),
Effect.catchTags({
Unprocessable: (error) => RefSubject.set(errors, error.errors),
Unauthorized: () => Router.navigate(Routes.login, { relative: false }),
ParseError: (issue) => RefSubject.set(errors, [issue.message])
})
Unauthorized: () => Router.navigate(Routes.login, { relative: false })
}),
Effect.catchAll((issue) => RefSubject.set(errors, [issue.message]))
)
)

Expand Down Expand Up @@ -206,14 +207,14 @@ export function renderForm<R, R2>({
export function EditArticle<R, R2>(
initial: RefSubject.Computed<
EditArticleFields,
Unprocessable | Unauthorized | ParseError,
Unprocessable | Unauthorized | ParseError | Cause.NoSuchElementException,
R
>,
onSubmit: (
updated: EditArticleFields
) => Effect.Effect<
unknown,
Unprocessable | NavigationError | Unauthorized | ParseError,
Unprocessable | NavigationError | Unauthorized | ParseError | Cause.NoSuchElementException,
R2
>
) {
Expand Down
5 changes: 3 additions & 2 deletions examples/realworld/src/ui/pages/settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ArrayFormatter } from "@effect/schema"
import { AsyncData, Fx, RefAsyncData, RefSubject, Router } from "@typed/core"
import type { EventWithTarget } from "@typed/dom/EventTarget"
import { parseFormData } from "@typed/realworld/lib/Schema"
import { CurrentUser, isAuthenticatedGuard, Users } from "@typed/realworld/services"
import { Unprocessable } from "@typed/realworld/services/errors"
Expand All @@ -11,9 +12,9 @@ import { Effect, Option } from "effect"

export const route = Routes.settings.pipe(isAuthenticatedGuard)

type SubmitEvent = Event & { target: HTMLFormElement }
type SubmitEvent = EventWithTarget<HTMLFormElement>

export const main = Fx.gen(function* (_) {
export const main = Fx.gen(function*(_) {
const userErrors = yield* _(useCurrentUserErrors)
const onSubmit = EventHandler.preventDefault((ev: SubmitEvent) =>
Effect.zipRight(updateUser(ev), userErrors.onSubmit)
Expand Down
12 changes: 1 addition & 11 deletions examples/realworld/src/ui/router.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { Fx, Navigation, Router } from "@typed/core"
import { isAuthenticated } from "@typed/realworld/services"
import { Effect } from "effect"
import { layout } from "./layout"
import { Router } from "@typed/core"
import * as pages from "./pages"

export const router = Router
Expand All @@ -13,10 +10,3 @@ export const router = Router
.match(pages.editArticle.route, pages.editArticle.main)
.match(pages.profile.route, pages.profile.main)
.match(pages.home.route, pages.home.main)

const onNotFound = Effect.if(Fx.first(isAuthenticated), {
onFalse: () => new Navigation.RedirectError({ path: pages.login.route.interpolate({}) }),
onTrue: () => new Navigation.RedirectError({ path: pages.home.route.interpolate({}) })
})

export const main = layout(Router.notFoundWith(router, onNotFound))
12 changes: 12 additions & 0 deletions examples/realworld/src/ui/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { toServerRouter } from "@typed/core/Platform"
import { ServerResponse, ServerRouter } from "@typed/server"
import { document } from "./document"
import { router } from "./router"

// Convert our UI router to a ServerRouter and provide a layout to construct a full HTML document.
export const UiServer = toServerRouter(router, { layout: document }).pipe(
// Handle UI errors
ServerRouter.catchTag("RedirectError", (error) => ServerResponse.seeOther(error.path.toString())),
ServerRouter.catchTag("Unprocessable", (_) => ServerResponse.json(_, { status: 422 })),
ServerRouter.catchAll(() => ServerResponse.seeOther("/login"))
)
7 changes: 6 additions & 1 deletion packages/router/src/Matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import { dual, pipe } from "effect/Function"
import * as Option from "effect/Option"
import { type Pipeable, pipeArguments } from "effect/Pipeable"
import { hasProperty } from "effect/Predicate"
import type * as Scope from "effect/Scope"
import * as Unify from "effect/Unify"
Expand All @@ -33,7 +34,7 @@ export type RouteMatcherTypeId = typeof RouteMatcherTypeId
/**
* @since 1.0.0
*/
export interface RouteMatcher<Matches extends RouteMatch.RouteMatch.Any> {
export interface RouteMatcher<Matches extends RouteMatch.RouteMatch.Any> extends Pipeable {
readonly [RouteMatcherTypeId]: RouteMatcherTypeId

readonly matches: ReadonlyArray<Matches>
Expand Down Expand Up @@ -162,6 +163,10 @@ class RouteMatcherImpl<Matches extends RouteMatch.RouteMatch.Any> implements Rou
) {
return this.match(input, Fx.map(f))
}

pipe() {
return pipeArguments(this, arguments)
}
}

/**
Expand Down
12 changes: 12 additions & 0 deletions packages/server/src/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@
* @since 1.0.0
*/

import * as ServerHeaders from "@effect/platform/Http/Headers"
import * as ServerResponse from "@effect/platform/Http/ServerResponse"

/**
* @since 1.0.0
*/
export * from "@effect/platform/Http/ServerResponse"

/**
* @since 1.0.0
*/
export const seeOther = (location: string): ServerResponse.ServerResponse =>
ServerResponse.empty({
status: 303,
headers: ServerHeaders.fromInput({ location })
})

0 comments on commit 4c919f1

Please sign in to comment.