diff --git a/.gitignore b/.gitignore index fb650be..440e162 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ node_modules !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +!.vscode/terminals.json # misc /.sass-cache diff --git a/.vscode/terminals.json b/.vscode/terminals.json new file mode 100644 index 0000000..1631c55 --- /dev/null +++ b/.vscode/terminals.json @@ -0,0 +1,15 @@ +{ + "autorun": true, + "autokill": true, + "terminals": [ + { + "name": "Docker", + "description": "Starts docker compose on startup", + "icon": "container", + "open": false, + "recycle": false, + "color": "terminal.ansiCyan", + "commands": ["docker-compose up -d", "exit"] + } + ] +} diff --git a/TODO.md b/TODO.md deleted file mode 100644 index e3a8d9d..0000000 --- a/TODO.md +++ /dev/null @@ -1,77 +0,0 @@ -# Effect Workshop - -## Best Practices - -- Don't run effects in effects -- Function composition enables tree shaking -- Use `Effect` everywhere where it makes sense (don't use `Either`, `Option` if it is not necessary) -- `Either`/`Option` makes sense when interoping with non-effect code -- `Effect` is lazy, `Option` and `Either` are eager -- `Effect.*` functions accept `Option`/`Either` as parameters (overload) -- Don't block the executor (no `while(true)`) -- Whenever something is not clear check the type signatures - -## Extras - -## Traits - -Like an `Iterator` - -- Equal - - Can be used to implement deep equality -- Hash -- Data - - `Data.case` -> Implements `Equal` and `Hash` for you - - `Data.class` -> Same but with classes - - `Data.TaggedClass` -> Same but adds a tag - - `Data.TaggedEnum` -> Unions of case classes - - `Data.TaggedError` -> can `yield*` errors (no wrap) -- Branded types - - Creates distinct types that are specializations of primitive types - - Created with a constructor function that validates the input (eg `NonNegativeNumber`) - - Use `Brand.Brand<""> - - Brand.nominal -> empty constructor function - - Brand.refined -> constructor with validation - -> use `Brand.error` to signal an error - -## Testing ??? - -## Config Management ??? - -## How to handle nesting - -- Use `gen` -- Use `Do` - -## Useful functions - -- `zip`: 2 effects -> 2 results in tuple -- `zipLeft`: 2 effects -> result of left -- `zipRight`: 2 effects -> result of right -- `flatMap` vs `zipRight` (depend or don't depend on result of previous result) -- `tap` vs `zipLeft` -- `andThen` -> combination of above -- Effect.all (with `mode`) - -### Creating a `Context` manually - -- `Context.empty` -- `pipe` + `Context.add` - -### Using Deferred To Create a CountDownLatch - -### Schema - -- Explain `` -- Simple types like `S.number` -- transform / S.transformOrFail -- Input: S.To -- Output: `S.From` -- is -> synchronous -- asserts -> throws -- validate -> Effectful validation -- encode / encodeSync / encodeEither / encodePromise / encodeOption -- decode ... -> does only transformation -- encodeUnknown ... -> does validation too -- decodeUnknown ... -> does validation too -- struct, union, array diff --git a/apps/eisenhower/INSTRUCTIONS.md b/apps/eisenhower/INSTRUCTIONS.md index 362000e..ff35fa7 100644 --- a/apps/eisenhower/INSTRUCTIONS.md +++ b/apps/eisenhower/INSTRUCTIONS.md @@ -68,9 +68,10 @@ As a user I want to get task notifications whenever there is a task that's past ### Create Excel Export -As a user I want to be able to create an Excel export of the matrices in the app. -Each matrix should have its own tab in the excel file, and tasks should be represented as rows. -The excel export is generated asynchronously and the file is written to the path specified by the user. +As a user I want to be able to create an Excel export of a matrix. + +Each task should have its own row in the file, format should be `.csv`. +The file is written to the path specified by the user. ## Other Tasks diff --git a/apps/eisenhower/NOTES.md b/apps/eisenhower/NOTES.md index 1f8a6da..2ad193c 100644 --- a/apps/eisenhower/NOTES.md +++ b/apps/eisenhower/NOTES.md @@ -294,14 +294,14 @@ export const make = sync(() => { ``` Oh, but wait...we need the database connection! Let's see what we can do. Since the repository functions don't have requirements we can create a context element that holds the database connection and -`yield*` it in the constructor. Create a new context element in the `service` folder named `Db.ts` and move the initialization logic there: +`yield*` it in the constructor. Create a new context element in the `db` folder named `Db.ts` and move the initialization logic there: ```ts import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; import { migrate } from "drizzle-orm/node-postgres/migrator"; import { Tag, gen, tryPromise } from "effect/Effect"; import pg from "pg"; -import * as schema from "../db/schema"; +import * as schema from "./schema"; import { Layer } from "effect"; const DB_URL = @@ -340,7 +340,8 @@ Then remove it from `index.ts`, and add `Db` to the context: import { Layer, ManagedRuntime } from "effect"; import express, { json } from "express"; import { ExcelRouter, MatrixRouter, TaskRouter } from "./router"; -import { Db, UUIDProvider } from "./service"; +import { UUIDProvider } from "./service"; +import { Db } from "./db/Db"; const PORT = 3333; @@ -365,7 +366,8 @@ Now we can change `sync` to `gen` in our repository implementation: ```ts import { gen } from "effect/Effect"; -import { Db, MatrixRepository } from "../../service"; +import { MatrixRepository } from "../../service"; +import { Db } from "../Db"; export const make = gen(function* () { const db = yield* Db; @@ -391,8 +393,9 @@ With these in place now we can create an actual implementation: import { eq } from "drizzle-orm"; import { fail, gen, tryPromise } from "effect/Effect"; import { Matrix, Task } from "../../domain"; -import { Db, MatrixRepository, UUIDProvider } from "../../service"; +import { MatrixRepository, UUIDProvider } from "../../service"; import { matrices } from "../schema"; +import { Db } from "../Db"; const DatabaseError = "DatabaseError" as const; const EntityNotFound = "EntityNotFound" as const; @@ -451,27 +454,34 @@ export const make = gen(function* () { return yield* fail(EntityNotFound); } }), - findAll: () => tryPromise({ - try: () => - db.query.matrices.findMany({ - columns: { - id: true, - name: true, - }, - }), - catch: () => DatabaseError, - }), + findAll: () => + tryPromise({ + try: () => + db.query.matrices.findMany({ + columns: { + id: true, + name: true, + }, + }), + catch: () => DatabaseError, + }), }); }); ``` -Ask them when did they start to feel the pain? +> Ask them when did they start to feel the pain? ## Introducing `Data` > Here we can refactor our domain objects to use `Data.Class`: +In Task.ts: + ```ts +import { Data } from "effect"; +import type { Importance } from "./Importance"; +import type { Urgency } from "./Urgency"; + export class Task extends Data.Class<{ id: string; name: string; @@ -490,6 +500,15 @@ export type UnsavedTask = Pick< }; export type TaskUpdate = Pick; +``` + +In Matrix.ts: + +```ts +import { Data } from "effect"; +import type { Importance } from "./Importance"; +import type { Task } from "./Task"; +import type { Urgency } from "./Urgency"; export class Matrix extends Data.Class<{ id: string; @@ -554,16 +573,17 @@ export const make = gen(function* () { return yield* fail(EntityNotFound); } }), - findAll: () => tryPromise({ - try: () => - db.query.matrices.findMany({ - columns: { - id: true, - name: true, - }, - }), - catch: () => DatabaseError, - }), + findAll: () => + tryPromise({ + try: () => + db.query.matrices.findMany({ + columns: { + id: true, + name: true, + }, + }), + catch: () => DatabaseError, + }), }); }); ``` @@ -638,16 +658,17 @@ export const make = gen(function* () { return yield* fail(EntityNotFound); } }), - findAll: () => tryPromise({ - try: () => - db.query.matrices.findMany({ - columns: { - id: true, - name: true, - }, - }), - catch: () => DatabaseError, - }), + findAll: () => + tryPromise({ + try: () => + db.query.matrices.findMany({ + columns: { + id: true, + name: true, + }, + }), + catch: () => DatabaseError, + }), }); }); ``` @@ -698,13 +719,15 @@ export const make = gen(function* () { return yield* fail(EntityNotFound); } }), - findAll: () => query(() => - db.query.matrices.findMany({ - with: { - tasks: true, - }, - }) - ), + findAll: () => + query(() => + db.query.matrices.findMany({ + columns: { + id: true, + name: true, + }, + }) + ), }); }); ``` @@ -718,10 +741,18 @@ export class DatabaseError extends TaggedError("DatabaseError")<{ message: string; }> {} +import { TaggedError } from "effect/Data"; + export class EntityNotFound extends TaggedError("EntityNotFound")<{ entity: string; filter: Record; -}> {} +}> { + override get message() { + return `Entity ${this.entity} not found with filter ${JSON.stringify( + this.filter + )}`; + } +} ``` > We can discuss Error vs TaggedError and how TaggedError helps with `catchTag` / `catchTags` @@ -781,8 +812,7 @@ There is one last neat trick that we can do. `TaggedError`s are yieldable: ```ts return ( - yield * - new EntityNotFound({ + yield* new EntityNotFound({ entity: "Matrix", filter: { id }, }) @@ -802,9 +832,10 @@ Solution is: import { eq } from "drizzle-orm"; import { gen } from "effect/Effect"; import { Task } from "../../domain"; -import { Db, TaskRepository, UUIDProvider } from "../../service"; +import { TaskRepository, UUIDProvider } from "../../service"; import { tasks } from "../schema"; import { query } from "./util"; +import { Db } from "../Db"; export const make = gen(function* () { const db = yield* Db; @@ -849,9 +880,8 @@ Now we can put these repos into layers and do a barrel export: ```ts // in DrizzleMatrixRepository.ts -import { Layer } from "effect"; +import { Layer, pipe } from "effect"; import { UUIDProvider } from "../../service"; -//! Move Db next to it! import { Db } from "./Db"; export const layer = Layer.effect(MatrixRepository, make); @@ -867,14 +897,12 @@ export const live = pipe( in DrizzleTaskRepository.ts ```ts -import { Layer } from "effect"; +import { Layer, pipe } from "effect"; import { UUIDProvider } from "../../service"; -//! Move Db next to it! import { Db } from "./Db"; -export const layer = Layer.effect(MatrixRepository, make); +export const layer = Layer.effect(TaskRepository, make); -// This will make requirements `never` export const live = pipe( layer, Layer.provide(Db.live), @@ -898,7 +926,6 @@ export const live = pipe( in index.ts ```ts -export * from "./Db"; export * as DrizzleMatrixRepository from "./DrizzleMatrixRepository"; export * as DrizzleTaskRepository from "./DrizzleTaskRepository"; ``` @@ -908,13 +935,10 @@ Now we can add all these to our context: ```ts import { Layer, ManagedRuntime } from "effect"; import express, { json } from "express"; +import { Db } from "./db/Db"; import { ExcelRouter, MatrixRouter, TaskRouter } from "./router"; import { UUIDProvider } from "./service"; -import { - Db, - DrizzleMatrixRepository, - DrizzleTaskRepository, -} from "./db/repository"; +import { DrizzleMatrixRepository, DrizzleTaskRepository } from "./db"; // ... @@ -936,8 +960,12 @@ First let's create a type that represents our runtime: ```ts import type { ManagedRuntime } from "effect/ManagedRuntime"; -import type { Db } from "./db/repository"; -import type { MatrixRepository, TaskRepository, UUIDProvider } from "./service"; +import type { + MatrixRepository, + TaskRepository, + UUIDProvider, +} from "../service"; +import type { Db } from "../db"; export type EisenhowerRuntime = ManagedRuntime< UUIDProvider | Db | MatrixRepository | TaskRepository, @@ -1044,13 +1072,13 @@ We also need to fix our runtime type: ```ts import type { ManagedRuntime } from "effect/ManagedRuntime"; -import type { Db } from "../db/repository"; import type { DatabaseError, MatrixRepository, TaskRepository, UUIDProvider, } from "../service"; +import type { Db } from "../db"; export type EisenhowerRuntime = ManagedRuntime< UUIDProvider | Db | MatrixRepository | TaskRepository, @@ -1131,5 +1159,476 @@ export const make = ({ runPromise }: EisenhowerRuntime) => { > Note that there are a few small changes, such as not having `completed` in the body of the patch request since we have a `complete` not an `update` function in the repository. +## Sending Notifications + +Now we're assuming that this is a single-user system so no authentication is necessary and we can just hard-code an email address. The point here is to have a service that executes scheduled jobs in a forked fiber. + +First let's take a look at the `NotificationService`: + +```ts +import { Tag, type Effect } from "effect/Effect"; +import type { TaskRepository } from "./TaskRepository"; +import type { EmailSender } from "./EmailSender"; + +export class NotificationService extends Tag("Service/NotificationService")< + NotificationService, + { + start: () => Effect; + } +>() {} +``` + +> Why isn't there a stop function? Structured concurrency explains this. + +> Also note the difference between the repository functions and this. We have requirements in +> the `start` function, and not in the `gen` that we used in the repositories. + +Now let's add a new function to `TaskRepository`: + +```ts +import { Tag, type Effect } from "effect/Effect"; +import type { Task, UnsavedTask } from "../domain"; +import type { DatabaseError, EntityNotFound } from "./error"; + +export class TaskRepository extends Tag("Service/TaskRepository")< + TaskRepository, + { + findExpired: () => Effect; + create: (task: UnsavedTask) => Effect; + complete: (id: string) => Effect; + delete: (id: string) => Effect; + } +>() {} +``` + +The implementation is straightforward (note the use of `pipe` and `map` instead of `gen`): + +```ts +import { and, eq, gte } from "drizzle-orm"; + +// ... + +findExpired: () => + pipe( + query(() => + db.query.tasks.findMany({ + where: and( + eq(tasks.completed, false), + gte(tasks.dueDate, new Date()) + ), + }) + ), + map((tasks) => tasks.map(Task.fromRaw)) + ), +``` + +this can be further simplified by using `Array`: + +```ts +import { and, eq, gte } from "drizzle-orm"; + +// ... +import { Layer, pipe, Array } from "effect"; + +// ... + +findExpired: () => + pipe( + query(() => + db.query.tasks.findMany({ + where: and( + eq(tasks.completed, false), + gte(tasks.dueDate, new Date()) + ), + }) + ), + map(Array.map(Task.fromRaw)) + ), +``` + +Now we can get the tasks that are expired and send an email. First let's write a function that we'll keep repeating: + +```ts +import { Array, Layer, Schedule, pipe } from "effect"; +import type { Effect } from "effect/Effect"; +import { + Tag, + all, + asVoid, + fork, + map, + repeat, + suspend, + sync, +} from "effect/Effect"; +import { EmailSender } from "./EmailSender"; +import { TaskRepository } from "./TaskRepository"; + +const EMAIL = "your@email.com"; + +const sendEmails = gen(function* () { + const tasks = yield* TaskRepository.findExpired(); + for (const task of tasks) { + yield* EmailSender.sendEmail({ + address: EMAIL, + content: `Task ${task.id} has expired`, + }); + } +}); +``` + +Now the service becomes: + +```ts +export class NotificationService extends Tag("Service/NotificationService")< + NotificationService, + { + start: () => Effect; + } +>() { + static make = sync(() => + NotificationService.of({ + start: () => + pipe(sendEmails, repeat(Schedule.fixed(500)), fork), + }) + ); + + static layer = Layer.effect(NotificationService, NotificationService.make); +} +``` + +EmailSender is very simple: + +```ts +import { Layer } from "effect"; +import { Tag, succeed, sync, type Effect } from "effect/Effect"; + +type Email = { + address: string; + content: string; +}; + +export class EmailSender extends Tag("Service/EmailSender")< + EmailSender, + { + sendEmail: (email: Email) => Effect; + } +>() { + static make = sync(() => + EmailSender.of({ + sendEmail: (email) => { + console.log(`Sending email to ${email.address}`); + return succeed(undefined); + }, + }) + ); + + static layer = Layer.effect(EmailSender, EmailSender.make); + + static live = EmailSender.layer; +} +``` + +Now we can add this to our main app: + +```ts +const NotificationServiceLive = pipe( + NotificationService.layer, + Layer.provide(EmailSender.live), + Layer.provide(DrizzleTaskRepository.live) +); + +const CONTEXT = Layer.mergeAll( + UUIDProvider.live, + Db.live, + DrizzleMatrixRepository.live, + DrizzleTaskRepository.live, + EmailSender.live, + NotificationServiceLive +); +// ... + +const start = async () => { + RUNTIME.runPromise(NotificationService.start()); + // ... +}; +``` + +Now start the app and ask them why this doesn't work! + +> They can add a `tap(() => logInfo("Sending emails..."))` to see what's happening + +So the first problem is that `runPromise` terminates the fiber, we need to use `runFork` instead: + +```ts +RUNTIME.runFork(NotificationService.start()); +``` + +But when we remove the `fork` we'll have a `number` as a return value (this is returned by `Schedule.fixed`) so we need to use `asVoid`: + +```ts +pipe(sendEmails, repeat(Schedule.fixed(500)), asVoid); +``` + +and in order to make this work we'll need to handle the error: + +```ts +const sendEmails = gen(function* () { + yield* logInfo("Checking for expired tasks..."); + const tasks = yield* either(TaskRepository.findExpired()); + if (isLeft(tasks)) { + yield* logError(`Couldn't query expired tasks: ${tasks.left.message}`); + } else { + for (const { id, name } of tasks.right) { + yield* EmailSender.sendEmail({ + address: EMAIL, + content: `Task ${name} (${id}) has expired`, + }); + } + } +}); +``` + +Now it should work! + +> They'll probably have questions about this, so it is a good idea to discuss it. + +Now there is a problem. We can't gracefully terminate our app because we don't handle the long-running fiber as a resource. We also have another problem which is an anti-pattern: + +```ts +export class NotificationService extends Tag("Service/NotificationService")< + NotificationService, + { + start: () => Effect; + } +>() {} +``` + +The requirements are listed on `start` and not in the `gen` function that creates this service. This leads to a dependency explosion, so we should refactor the `NotificationService`. And while we're at it we can also separate the concerns (scheduling and notifying) + +```ts +export class NotificationService extends Tag("Service/NotificationService")< + NotificationService, + { + notify: () => Effect; + } +>() { + static make = gen(function* () { + const emailSender = yield* EmailSender; + const taskRepository = yield* TaskRepository; + return NotificationService.of({ + notify: () => + gen(function* () { + yield* logInfo("Checking for expired tasks..."); + const tasks = yield* either(taskRepository.findExpired()); + if (isLeft(tasks)) { + yield* logError( + `Couldn't query expired tasks: ${tasks.left.message}` + ); + } else { + for (const { id, name } of tasks.right) { + yield* emailSender.sendEmail({ + address: EMAIL, + content: `Task ${name} (${id}) has expired`, + }); + } + } + }), + }); + }); + + static layer = Layer.effect(NotificationService, NotificationService.make); +} +``` + +then in our index we can add the scheduling as a separate layer that's handled as a resource: + +```ts +const NotificationServiceLive = pipe( + NotificationService.layer, + Layer.provide(EmailSender.live), + Layer.provide(DrizzleTaskRepository.live) +); + +export const ScheduleSendEmailsLive = pipe( + Layer.scopedDiscard( + forkScoped(repeat(NotificationService.notify(), Schedule.fixed(500))) + ), + Layer.provide(NotificationServiceLive) +); + +const CONTEXT = Layer.mergeAll( + UUIDProvider.live, + Db.live, + DrizzleMatrixRepository.live, + DrizzleTaskRepository.live, + EmailSender.live, + NotificationServiceLive, + ScheduleSendEmailsLive +); +``` + +There is one last problem. The runtime doesn't start until we "touch" it: + +```ts +await RUNTIME.runtime(); +``` + +Now what we're at it, stopping a runtime is also crucial if we don't want to leak socket connections and such when the app is terminated: + +```ts +process.once("SIGTERM", async () => { + await RUNTIME.dispose(); +}); +``` + +## Excel Export + +> Not really an excel, bu csv, but what the hell. + +Let's open `ExcelExporter`. It should be clear by now what needs to be done: we need to use the `MatrixRepository`, and a `File` handle as a resource. Implementation should be straightforward let them try to implement it on their own. A few pointers: + +- They need to fiddle around with the errors because `DatabaseError` and `EntityNotFound` needs to be mapped to `MatrixExportFailed` + +The solution: + +```ts +import { Layer, pipe } from "effect"; +import { + Tag, + acquireUseRelease, + gen, + mapError, + promise, + tryPromise, + type Effect, +} from "effect/Effect"; +import fs from "fs/promises"; +import { MatrixRepository } from "./MatrixRepository"; +import { MatrixExportFailed } from "./error"; + +export class ExcelExporter extends Tag("Service/ExcelExporter")< + ExcelExporter, + { + export: ( + matrixId: string, + path: string + ) => Effect; + } +>() { + static make = gen(function* () { + const matrixRepository = yield* MatrixRepository; + + return ExcelExporter.of({ + export: (matrixId, path) => + gen(function* () { + const matrix = yield* pipe( + matrixRepository.findById(matrixId), + mapError( + ({ message }) => + new MatrixExportFailed({ + message, + }) + ) + ); + + const data = matrix.tasks + .map( + ({ + id, + name, + completed, + dueDate, + importance, + urgency, + }) => { + return `${id},${name},${completed},${dueDate.toISOString()},${importance},${urgency}`; + } + ) + .join("\n"); + + const acquire = tryPromise({ + try: () => fs.open(path, "w"), + catch: (e) => + new MatrixExportFailed({ + message: + e instanceof Error + ? e.message + : "Unknown error", + }), + }); + + const use = (file: fs.FileHandle) => + promise(() => file.write(data)); + + const release = (file: fs.FileHandle) => + promise(() => file.close()); + + yield* acquireUseRelease(acquire, use, release); + }), + }); + }); + + static layer = Layer.effect(ExcelExporter, ExcelExporter.make); +} +``` + +adding this to our context should be a breeze: + +```ts +const ExcelExporterLive = pipe( + ExcelExporter.layer, + Layer.provide(DrizzleMatrixRepository.live) +); + +const CONTEXT = Layer.mergeAll( + UUIDProvider.live, + Db.live, + DrizzleMatrixRepository.live, + DrizzleTaskRepository.live, + EmailSender.live, + NotificationServiceLive, + ScheduleSendEmailsLive, + ExcelExporterLive +); +``` + +Then all we need to do is to use this in `ExcelRouter`: + +```ts +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { either } from "effect/Effect"; +import { isRight } from "effect/Either"; +import { Router } from "express"; +import { ExcelExporter } from "../service"; +import type { EisenhowerRuntime } from "../types"; + +export const make = (runtime: EisenhowerRuntime) => { + const router = Router(); + + router.get(`/`, async (req, res) => { + const { matrixId, path } = req.query; + const result = await runtime.runPromise( + either(ExcelExporter.export(matrixId!.toString(), path!.toString())) + ); + if (isRight(result)) { + res.status(200); + res.json({ + matrixId, + path, + }); + } else { + res.status(500); + res.json({ + error: result.left.message, + }); + } + }); + + return router; +}; +``` + +There should be an error here because we forgot to update the runtime type. Maybe someone noticed or figured it out! -## Sending Notifications \ No newline at end of file +> Note that doing proper deserialization / validation is not the goal of this exercise. diff --git a/apps/eisenhower/src/db/index.ts b/apps/eisenhower/src/db/index.ts deleted file mode 100644 index 953a2aa..0000000 --- a/apps/eisenhower/src/db/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./schema"; - diff --git a/apps/eisenhower/src/router/ExcelRouter.ts b/apps/eisenhower/src/router/ExcelRouter.ts index 4436c2e..e5a71ba 100644 --- a/apps/eisenhower/src/router/ExcelRouter.ts +++ b/apps/eisenhower/src/router/ExcelRouter.ts @@ -1,6 +1,7 @@ import { Router } from "express"; type ExportParams = { + matrixId: string; path: string; }; @@ -9,8 +10,8 @@ export const make = () => { router.post(`/`, async (req, res) => { // TODO: + const { matrixId, path }: ExportParams = req.body; console.log("Generating excel export"); - const { path }: ExportParams = req.body; res.status(501); res.json({ path, diff --git a/apps/eisenhower/src/service/EmailSender.ts b/apps/eisenhower/src/service/EmailSender.ts index 3562d98..299f2dc 100644 --- a/apps/eisenhower/src/service/EmailSender.ts +++ b/apps/eisenhower/src/service/EmailSender.ts @@ -8,6 +8,6 @@ type Email = { export class EmailSender extends Tag("Service/EmailSender")< EmailSender, { - sendEmail: (email: Email) => Effect; + sendEmail: (email: Email) => Effect; } >() {} diff --git a/apps/eisenhower/src/service/ExcelExporter.ts b/apps/eisenhower/src/service/ExcelExporter.ts index 8f3151a..e9ce18b 100644 --- a/apps/eisenhower/src/service/ExcelExporter.ts +++ b/apps/eisenhower/src/service/ExcelExporter.ts @@ -3,6 +3,6 @@ import { Tag, type Effect } from "effect/Effect"; export class ExcelExporter extends Tag("Service/ExcelExporter")< ExcelExporter, { - export: (matrixId: string) => Effect; + export: (matrixId: string, path: string) => Effect; } >() {} diff --git a/apps/eisenhower/src/service/TaskRepository.ts b/apps/eisenhower/src/service/TaskRepository.ts index 5f69e10..e68fa4d 100644 --- a/apps/eisenhower/src/service/TaskRepository.ts +++ b/apps/eisenhower/src/service/TaskRepository.ts @@ -1,8 +1,8 @@ import { Tag, type Effect } from "effect/Effect"; import type { Task, UnsavedTask } from "../domain"; -type EntityNotFound = "EntityNotFound"; type DatabaseError = "DatabaseError"; +type EntityNotFound = "EntityNotFound"; export class TaskRepository extends Tag("Service/TaskRepository")< TaskRepository, diff --git a/apps/eisenhower/src/service/index.ts b/apps/eisenhower/src/service/index.ts index e9cd947..9bfa6c7 100644 --- a/apps/eisenhower/src/service/index.ts +++ b/apps/eisenhower/src/service/index.ts @@ -3,4 +3,3 @@ export * from "./ExcelExporter"; export * from "./MatrixRepository"; export * from "./TaskRepository"; export * from "./UUIDProvider"; - diff --git a/apps/eisenhower/test/util.ts b/apps/eisenhower/test/util.ts new file mode 100644 index 0000000..94578d9 --- /dev/null +++ b/apps/eisenhower/test/util.ts @@ -0,0 +1,6 @@ +import { Context } from "effect"; +import { Tag, type TagClassShape } from "effect/Context"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ServiceOf | TagClassShape> = + Context.Tag.Service; \ No newline at end of file diff --git a/apps/slidev/slides.md b/apps/slidev/slides.md index 4013ab2..f799b5b 100644 --- a/apps/slidev/slides.md +++ b/apps/slidev/slides.md @@ -5162,25 +5162,26 @@ We mentioned this a while ago. # Best Practices - - - - - - - - +
    +
  • Don't run effects in effects
  • +
  • Function composition enables tree shaking
  • +
  • Use `Effect` everywhere where it makes sense (don't use Either / Option if it is not necessary)
  • +
  • Either / Option makes sense when interoping with non-effect code
  • +
  • Effect is lazy, Option and Either are eager
  • +
  • Effect.* functions accept Option / Either as parameters
  • +
  • Don't block the executor (no while(true))
  • +
  • Whenever something is not clear check the type signatures
  • +
  • If it doesn't have a parameter don't make it a function (Effects are blueprints)
  • +
  • Use ManagedRuntime if you are not fully integrated with Effect
  • +
  • ...
  • +
--- --- -# Other Topics - -> Other features of Effect - -**TODO** +# Other Features of Effect
  • Traits: Equal & Hash
  • @@ -5191,5 +5192,10 @@ We mentioned this a while ago.
  • Creating a Context manually
  • Using Deferred
  • Schema
  • - -
\ No newline at end of file +
  • ...
  • + + +--- +--- + +# Now Let's Write A Program In Effect! \ No newline at end of file diff --git a/apps/slidev/slides_old.md b/apps/slidev/slides_old.md deleted file mode 100644 index b9d655e..0000000 --- a/apps/slidev/slides_old.md +++ /dev/null @@ -1,637 +0,0 @@ ---- -# try also 'default' to start simple -theme: seriph -# random image from a curated Unsplash collection by Anthony -# like them? see https://unsplash.com/collections/94734566/slidev -background: https://cover.sli.dev -# some information about your slides, markdown enabled -title: Welcome to Slidev -info: | - ## Slidev Starter Template - Presentation slides for developers. - - Learn more at [Sli.dev](https://sli.dev) -# apply any unocss classes to the current slide -class: text-center -# https://sli.dev/custom/highlighters.html -highlighter: shiki -# https://sli.dev/guide/drawing -drawings: - persist: false -# slide transition: https://sli.dev/guide/animations#slide-transitions -transition: slide-left -# enable MDC Syntax: https://sli.dev/guide/syntax#mdc-syntax -mdc: true ---- - -# Welcome to Slidev - -Presentation slides for developers - -
    - - Press Space for next page - -
    - -
    - - - - -
    - - - ---- -transition: fade-out ---- - -# What is Slidev? - -Slidev is a slides maker and presenter designed for developers, consist of the following features - -- ๐Ÿ“ **Text-based** - focus on the content with Markdown, and then style them later -- ๐ŸŽจ **Themable** - theme can be shared and used with npm packages -- ๐Ÿง‘โ€๐Ÿ’ป **Developer Friendly** - code highlighting, live coding with autocompletion -- ๐Ÿคน **Interactive** - embedding Vue components to enhance your expressions -- ๐ŸŽฅ **Recording** - built-in recording and camera view -- ๐Ÿ“ค **Portable** - export into PDF, PNGs, or even a hostable SPA -- ๐Ÿ›  **Hackable** - anything possible on a webpage - -
    -
    - -Read more about [Why Slidev?](https://sli.dev/guide/why) - - - - - - - ---- -transition: slide-up -level: 2 ---- - -# Navigation - -Hover on the bottom-left corner to see the navigation's controls panel, [learn more](https://sli.dev/guide/navigation.html) - -## Keyboard Shortcuts - -| | | -| --- | --- | -| right / space| next animation or slide | -| left / shiftspace | previous animation or slide | -| up | previous slide | -| down | next slide | - - - -

    Here!

    - ---- -layout: two-cols -layoutClass: gap-16 ---- - -# Table of contents - -You can use the `Toc` component to generate a table of contents for your slides: - -```html - -``` - -The title will be inferred from your slide content, or you can override it with `title` and `level` in your frontmatter. - -::right:: - - - ---- -layout: image-right -image: https://cover.sli.dev ---- - -# Code - -Use code snippets and get the highlighting directly, and even types hover![^1] - -```ts {all|5|7|7-8|10|all} twoslash -// TwoSlash enables TypeScript hover information -// and errors in markdown code blocks -// More at https://shiki.style/packages/twoslash - -import { computed, ref } from 'vue' - -const count = ref(0) -const doubled = computed(() => count.value * 2) - -doubled.value = 2 -``` - - - - -<<< @/snippets/external.ts#snippet - - -[^1]: [Learn More](https://sli.dev/guide/syntax.html#line-highlighting) - - - - - - ---- -level: 2 ---- - -# Shiki Magic Move - -Powered by [shiki-magic-move](https://shiki-magic-move.netlify.app/), Slidev supports animations across multiple code snippets. - -Add multiple code blocks and wrap them with ````md magic-move (four backticks) to enable the magic move. For example: - -````md magic-move -```ts {*|2|*} -// step 1 -const author = reactive({ - name: 'John Doe', - books: [ - 'Vue 2 - Advanced Guide', - 'Vue 3 - Basic Guide', - 'Vue 4 - The Mystery' - ] -}) -``` - -```ts {*|1-2|3-4|3-4,8} -// step 2 -export default { - data() { - return { - author: { - name: 'John Doe', - books: [ - 'Vue 2 - Advanced Guide', - 'Vue 3 - Basic Guide', - 'Vue 4 - The Mystery' - ] - } - } - } -} -``` - -```ts -// step 3 -export default { - data: () => ({ - author: { - name: 'John Doe', - books: [ - 'Vue 2 - Advanced Guide', - 'Vue 3 - Basic Guide', - 'Vue 4 - The Mystery' - ] - } - }) -} -``` - -Non-code blocks are ignored. - -```vue - - -``` -```` - ---- - -# Components - -
    -
    - -You can use Vue components directly inside your slides. - -We have provided a few built-in components like `` and `` that you can use directly. And adding your custom components is also super easy. - -```html - -``` - - - - -Check out [the guides](https://sli.dev/builtin/components.html) for more. - -
    -
    - -```html - -``` - - - -
    -
    - - - ---- -class: px-20 ---- - -# Themes - -Slidev comes with powerful theming support. Themes can provide styles, layouts, components, or even configurations for tools. Switching between themes by just **one edit** in your frontmatter: - -
    - -```yaml ---- -theme: default ---- -``` - -```yaml ---- -theme: seriph ---- -``` - - - - - -
    - -Read more about [How to use a theme](https://sli.dev/themes/use.html) and -check out the [Awesome Themes Gallery](https://sli.dev/themes/gallery.html). - ---- - -# Clicks Animations - -You can add `v-click` to elements to add a click animation. - -
    - -This shows up when you click the slide: - -```html -
    This shows up when you click the slide.
    -``` - -
    - -
    - - - -The v-mark directive -also allows you to add -inline marks -, powered by [Rough Notation](https://roughnotation.com/): - -```html -inline markers -``` - - - -
    - -[Learn More](https://sli.dev/guide/animations#click-animations) - -
    - ---- - -# Motions - -Motion animations are powered by [@vueuse/motion](https://motion.vueuse.org/), triggered by `v-motion` directive. - -```html -
    - Slidev -
    -``` - -
    -
    - - - -
    - -
    - Slidev -
    -
    - - - - -
    - -[Learn More](https://sli.dev/guide/animations.html#motion) - -
    - ---- - -# LaTeX - -LaTeX is supported out-of-box powered by [KaTeX](https://katex.org/). - -
    - -Inline $\sqrt{3x-1}+(1+x)^2$ - -Block -$$ {1|3|all} -\begin{array}{c} - -\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & -= \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\ - -\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\ - -\nabla \cdot \vec{\mathbf{B}} & = 0 - -\end{array} -$$ - -
    - -[Learn more](https://sli.dev/guide/syntax#latex) - ---- - -# Diagrams - -You can create diagrams / graphs from textual descriptions, directly in your Markdown. - -
    - -```mermaid {scale: 0.5, alt: 'A simple sequence diagram'} -sequenceDiagram - Alice->John: Hello John, how are you? - Note over Alice,John: A typical interaction -``` - -```mermaid {theme: 'neutral', scale: 0.8} -graph TD -B[Text] --> C{Decision} -C -->|One| D[Result 1] -C -->|Two| E[Result 2] -``` - -```mermaid -mindmap - root((mindmap)) - Origins - Long history - ::icon(fa fa-book) - Popularisation - British popular psychology author Tony Buzan - Research - On effectiveness
    and features - On Automatic creation - Uses - Creative techniques - Strategic planning - Argument mapping - Tools - Pen and paper - Mermaid -``` - -```plantuml {scale: 0.7} -@startuml - -package "Some Group" { - HTTP - [First Component] - [Another Component] -} - -node "Other Groups" { - FTP - [Second Component] - [First Component] --> FTP -} - -cloud { - [Example 1] -} - -database "MySql" { - folder "This is my folder" { - [Folder 3] - } - frame "Foo" { - [Frame 4] - } -} - -[Another Component] --> [Example 1] -[Example 1] --> [Folder 3] -[Folder 3] --> [Frame 4] - -@enduml -``` - -
    - -[Learn More](https://sli.dev/guide/syntax.html#diagrams) - ---- -foo: bar -dragPos: - square: 691,33,167,_,-16 ---- - -# Draggable Elements - -Double-click on the draggable elements to edit their positions. - -
    - -###### Directive Usage - -```md - -``` - -
    - -###### Component Usage - -```md - - - Use the `v-drag` component to have a draggable container! - -``` - - -
    - Double-click me! -
    -
    - - - ---- -src: ./pages/multiple-entries.md -hide: false ---- - ---- - -# Monaco Editor - -Slidev provides built-in Monaco Editor support. - -Add `{monaco}` to the code block to turn it into an editor: - -```ts {monaco} -import { ref } from 'vue' -import { emptyArray } from './external' - -const arr = ref(emptyArray(10)) -``` - -Use `{monaco-run}` to create an editor that can execute the code directly in the slide: - -```ts {monaco-run} -import { version } from 'vue' -import { emptyArray, sayHello } from './external' - -sayHello() -console.log(`vue ${version}`) -console.log(emptyArray(10).reduce(fib => [...fib, fib.at(-1)! + fib.at(-2)!], [1, 1])) -``` - ---- -layout: center -class: text-center ---- - -# Learn More - -[Documentations](https://sli.dev) ยท [GitHub](https://github.com/slidevjs/slidev) ยท [Showcases](https://sli.dev/showcases.html) diff --git a/script/export-matrix b/script/export-matrix new file mode 100755 index 0000000..b1425fe --- /dev/null +++ b/script/export-matrix @@ -0,0 +1,8 @@ +#!/bin/bash + +P="${P:-3333}" + +curl --header "Content-Type: application/json" \ + --request POST \ + --data "{\"matrixId\":\"$1\", \"path\":\"$2\"}" \ + http://localhost:$P/excel \ No newline at end of file