-
-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add section on managing requirements to incremental adoption tutorial
- Loading branch information
Showing
8 changed files
with
276 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
content/tutorials/incremental-adoption/300-handling-requirements.mdx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
--- | ||
title: Handling Requirements | ||
excerpt: Learn how to incrementally adopt Effect into your application | ||
section: Incremental Adoption | ||
workspace: express | ||
--- | ||
|
||
### Handling Requirements | ||
|
||
Utilizing `Effect.runPromise` to interop with your existing application is fine when you are just getting started with adopting Effect. However, it will quickly become apparent that this approach does not scale, especially once you start using Effect to manage the requirements of your application and `Layer`s to compose the dependency graph between services. | ||
|
||
<Idea> | ||
For a detailed walkthrough of how to manage requirements within your Effect applications, take a look at the <a href="/docs/guides/context-management" target="_blank">Requirements Management</a> section of the documentation. | ||
</Idea> | ||
|
||
### Understanding the Problem | ||
|
||
To understand the problem, let's take a look at a simple example where we create a `Layer` which logs `"Hello, World"` when constructed. The layer is then provided to two Effect programs which are executed at two separate execution boundaries. | ||
|
||
```ts twoslash | ||
import { Console, Effect, Layer } from "effect" | ||
|
||
const HelloWorldLive = Layer.effectDiscard( | ||
Console.log("Hello, World!") | ||
) | ||
|
||
async function main() { | ||
// Execution Boundary #1 | ||
await Effect.succeed(1).pipe( | ||
Effect.provide(HelloWorldLive), | ||
Effect.runPromise | ||
) | ||
|
||
// Execution Boundary #2 | ||
await Effect.succeed(2).pipe( | ||
Effect.provide(HelloWorldLive), | ||
Effect.runPromise | ||
) | ||
} | ||
|
||
main() | ||
/** | ||
* Output: | ||
* Hello, World! | ||
* Hello, World! | ||
*/ | ||
``` | ||
|
||
As you can see from the output, the message `"Hello, World!"` is logged twice. This is because each call to `Effect.provide` will fully construct the dependency graph specified by the `Layer` and then provide it to the Effect program. This can create problems when your layers are meant to encapsulate logic that is only meant to be executed **once** (for example, creating a database connection pool) or when layer construction is **expensive** (for example, fetching a large number of remote assets and caching them in memory). | ||
|
||
To solve this problem, we need some sort of top-level, re-usable Effect `Runtime` which contains our fully constructed dependency graph, and then use that `Runtime` to execute our Effect programs instead of the default `Runtime` used by the `Effect.run*` methods. | ||
|
||
### Managed Runtimes | ||
|
||
The `ManagedRuntime` data type in Effect allows us to create a top-level, re-usable Effect `Runtime` which encapsulates a fully constructed dependency graph. In addition, `ManagedRuntime` gives us explicit control over when resources acquired by the runtime should be disposed. | ||
|
||
```ts twoslash | ||
import { Console, Effect, Layer, ManagedRuntime } from "effect" | ||
|
||
const HelloWorldLive = Layer.effectDiscard( | ||
Console.log("Hello, World!") | ||
) | ||
|
||
// Create a managed runtime from our layer | ||
const runtime = ManagedRuntime.make(HelloWorldLive) | ||
|
||
async function main() { | ||
// Execution Boundary #1 | ||
await Effect.succeed(1).pipe( | ||
runtime.runPromise | ||
) | ||
|
||
// Execution Boundary #2 | ||
await Effect.succeed(2).pipe( | ||
runtime.runPromise | ||
) | ||
|
||
// Dispose of resources when no longer needed | ||
await runtime.dispose() | ||
} | ||
|
||
main() | ||
/** | ||
* Output: | ||
* Hello, World! | ||
*/ | ||
``` | ||
|
||
Some things to note about the program above include: | ||
|
||
- `"Hello, World!"` is only logged to the console once | ||
- We no longer have to provide `HelloWorldLive` to each Effect program | ||
- Resources acquired by the `ManagedRuntime` must be manually disposed of | ||
|
||
### Exercise | ||
|
||
The team in charge of the `TodoRepository` has been hard at work and has managed to convert the `TodoRepository` into a completely Effect-based service complete with a `Layer` for service construction. | ||
|
||
Using what we have learned above, your tasks for this exercise include: | ||
|
||
- Create a `ManagedRuntime` which takes in the `TodoRepository` layer | ||
- Use the `ManagedRuntime` to run the Effect programs within the Express route handlers | ||
- For any Effect program which may result in a `TodoNotFoundError`: | ||
- Set the response status code to `404` | ||
- Return a JSON response that conforms to the following `{ "type": "TodoNotFound", "id": <TODO_ID> }` | ||
- **BONUS**: properly dispose of the `ManagedRuntime` when the server shuts down | ||
|
||
In the editor to the right, modify the code to restore functionality to our server. Please note that there is no one "correct" answer, as there are multiple ways to achieve the desired outcome. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import Express from "express" | ||
import { TodoRepository } from "./repo" | ||
|
||
const app = Express() | ||
|
||
app.use(Express.json() as Express.NextFunction) | ||
|
||
const repo = new TodoRepository() | ||
|
||
// Create Todo | ||
app.post("/todos", (req, res) => { | ||
repo.create(req.body.text).then((todo) => res.json(todo)) | ||
}) | ||
|
||
// Read Todo | ||
app.get("/todos/:id", (req, res) => { | ||
const id = Number.parseInt(req.params.id) | ||
repo.get(id).then((todo) => res.json(todo)) | ||
}) | ||
|
||
// Read Todos | ||
app.get("/todos", (_, res) => { | ||
repo.getAll().then((todos) => res.json(todos)) | ||
}) | ||
|
||
// Update Todo | ||
app.patch("/todos/:id", (req, res) => { | ||
const id = Number.parseInt(req.params.id) | ||
repo.update(id, req.body).then((todo) => res.json(todo)) | ||
}) | ||
|
||
// Delete Todo | ||
app.delete("/todos/:id", (req, res) => { | ||
const id = Number.parseInt(req.params.id) | ||
repo.delete(id).then((deleted) => res.json({ deleted })) | ||
}) | ||
|
||
app.listen(3000, () => { | ||
console.log("Server listing on port 3000...") | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,78 @@ | ||
import Express from "express" | ||
import { Effect } from "effect" | ||
import { Effect, ManagedRuntime } from "effect" | ||
import { TodoRepository } from "./repo" | ||
|
||
const app = Express() | ||
|
||
app.use(Express.json() as Express.NextFunction) | ||
|
||
const repo = new TodoRepository() | ||
|
||
const runtime = ManagedRuntime.make(TodoRepository.Live) | ||
|
||
// Create Todo | ||
app.post("/todos", (req, res) => { | ||
repo.create(req.body.text).then((todo) => res.json(todo)) | ||
TodoRepository.create(req.body.text).pipe( | ||
Effect.andThen((todo) => res.json(todo)), | ||
runtime.runPromise | ||
) | ||
}) | ||
|
||
// Read Todo | ||
app.get("/todos/:id", (req, res) => { | ||
const id = Number.parseInt(req.params.id) | ||
repo.get(id).then((todo) => res.json(todo)) | ||
TodoRepository.get(id).pipe( | ||
Effect.andThen((todo) => res.json(todo)), | ||
Effect.catchTag("TodoNotFoundError", () => | ||
Effect.sync(() => { | ||
res.status(404).json({ type: "TodoNotFound", id }) | ||
}) | ||
), | ||
runtime.runPromise | ||
) | ||
}) | ||
|
||
// Read Todos | ||
app.get("/todos", (_, res) => { | ||
repo.getAll().then((todos) => res.json(todos)) | ||
TodoRepository.getAll.pipe( | ||
Effect.andThen((todos) => res.json(todos)), | ||
runtime.runPromise | ||
) | ||
}) | ||
|
||
// Update Todo | ||
app.patch("/todos/:id", (req, res) => { | ||
const id = Number.parseInt(req.params.id) | ||
repo.update(id, req.body).then((todo) => res.json(todo)) | ||
TodoRepository.update(id, req.body).pipe( | ||
Effect.andThen((todo) => res.json(todo)), | ||
Effect.catchTag("TodoNotFoundError", () => | ||
Effect.sync(() => { | ||
res.status(404).json({ type: "TodoNotFound", id }) | ||
}) | ||
), | ||
runtime.runPromise | ||
) | ||
}) | ||
|
||
// Delete Todo | ||
app.delete("/todos/:id", (req, res) => { | ||
const id = Number.parseInt(req.params.id) | ||
repo.delete(id).then((deleted) => res.json(deleted)) | ||
TodoRepository.delete(id).pipe( | ||
Effect.andThen((deleted) => res.json({ deleted })), | ||
runtime.runPromise | ||
) | ||
}) | ||
|
||
app.listen(3000, () => { | ||
const server = app.listen(3000, () => { | ||
console.log("Server listing on port 3000...") | ||
}) | ||
|
||
// Graceful Shutdown | ||
process.on("SIGTERM", shutdown) | ||
process.on("SIGINT", shutdown) | ||
|
||
function shutdown() { | ||
server.close(() => { | ||
runtime.dispose().then(() => { | ||
process.exit(0) | ||
}) | ||
}) | ||
} |
Oops, something went wrong.