diff --git a/CHEATSHEET.md b/CHEATSHEET.md index 951d9c5..385779b 100644 --- a/CHEATSHEET.md +++ b/CHEATSHEET.md @@ -28,18 +28,18 @@ An `Effect` is a immutable value which describes some program that may require s ## Creating Effects -| **Function** | **Input** | **Output** | -| ----------------------- | ---------------------------------- | ----------------------------- | -| `succeed` | `A` | `Effect` | -| `fail` | `E` | `Effect` | -| `sync` | `() => A` | `Effect` | -| `try` | `() => A` | `Effect` | -| `try` (overload) | `() => A`, `unknown => E` | `Effect` | -| `promise` | `() => Promise` | `Effect` | -| `tryPromise` | `() => Promise` | `Effect` | -| `tryPromise` (overload) | `() => Promise`, `unknown => E` | `Effect` | -| `async` | `(Effect => void) => void` | `Effect` | -| `suspend` | `() => Effect` | `Effect` | +| **Function** | **Input** | **Output** | **Description** | +| ----------------------- | ---------------------------------- | ----------------------------- | -------------- | +| [`succeed`](https://effect.website/docs/getting-started/creating-effects/#succeed) | `A` | `Effect` | Creates an effect that succeeds with a value | +| [`fail`](https://effect.website/docs/getting-started/creating-effects/#fail) | `E` | `Effect` | Creates an effect that fails with an error | +| [`sync`](https://effect.website/docs/getting-started/creating-effects/#sync) | `() => A` | `Effect` | Creates an effect from a synchronous computation | +| [`try`](https://effect.website/docs/getting-started/creating-effects/#try) | `() => A` | `Effect` | Creates an effect from a synchronous computation that may throw | +| [`try`](https://effect.website/docs/getting-started/creating-effects/#try) (overload) | `() => A`, `unknown => E` | `Effect` | Creates an effect from a synchronous computation with custom error handling | +| [`promise`](https://effect.website/docs/getting-started/creating-effects/#promise) | `() => Promise` | `Effect` | Creates an effect from a promise | +| [`tryPromise`](https://effect.website/docs/getting-started/creating-effects/#trypromise) | `() => Promise` | `Effect` | Creates an effect from a promise that may reject | +| [`tryPromise`](https://effect.website/docs/getting-started/creating-effects/#trypromise) (overload) | `() => Promise`, `unknown => E` | `Effect` | Creates an effect from a promise with custom error handling | +| [`async`](https://effect.website/docs/getting-started/creating-effects/#async) | `(Effect => void) => void` | `Effect` | Creates an effect from an async callback | +| [`suspend`](https://effect.website/docs/getting-started/creating-effects/#suspend) | `() => Effect` | `Effect` | Creates an effect that is lazily evaluated | NOTE: `sync` and `promise` are for functions that will **NEVER** throw/reject. If they do, it will be considered a 'defect' (similar to a panic). @@ -48,23 +48,23 @@ NOTE: `sync` and `promise` are for functions that will **NEVER** throw/reject. I Effects must be 'run' to do anything. Effects should only be run **at the edges of your program**, when you must interact with the outside world or non-effect code. If you want to 'unwrap' the value from inside an effect without running it there are many combinators to do that. -| **Function** | **Input** | **Output** | -| ---------------- | -------------- | -------------------------- | -| `runSync` | `Effect` | `A` (throws `E`) | -| `runPromise` | `Effect` | `Promise` (rejects `E`) | -| `runSyncExit` | `Effect` | `Exit` | -| `runPromiseExit` | `Effect` | `Promise>` | +| **Function** | **Input** | **Output** | **Description** | +| ---------------- | -------------- | -------------------------- | -------------- | +| [`runSync`](https://effect.website/docs/getting-started/running-effects/#runsync) | `Effect` | `A` (throws `E`) | Runs the effect synchronously | +| [`runPromise`](https://effect.website/docs/getting-started/running-effects/#runpromise) | `Effect` | `Promise` (rejects `E`) | Runs the effect as a promise | +| [`runSyncExit`](https://effect.website/docs/getting-started/running-effects/#runsyncexit) | `Effect` | `Exit` | Runs the effect synchronously and returns an Exit | +| [`runPromiseExit`](https://effect.website/docs/getting-started/running-effects/#runpromiseexit) | `Effect` | `Promise>` | Runs the effect as a promise and returns an Exit | ## Composing Effects Combinators allow you to create new `Effect`s that operate on the reuslt of a previous `Effect` without 'running' it. -| **Function** | **Input** | **Output** | -| ------------ | ----------------------------------------- | --------------------------- | -| `map` | `Effect`, `A => B` | `Effect` | -| `flatMap` | `Effect`, `A => Effect` | `Effect` | -| `tap` | `Effect`, `A => Effect` | `Effect` | -| `all` | `[Effect, Effect, ...]` | `Effect<[A, B, ...], E, R>` | +| **Function** | **Input** | **Output** | **Description** | +| ------------ | ----------------------------------------- | --------------------------- | -------------- | +| [`map`](https://effect.website/docs/getting-started/building-pipelines/#map) | `Effect`, `A => B` | `Effect` | Transforms the success value | +| [`flatMap`](https://effect.website/docs/getting-started/building-pipelines/#flatmap) | `Effect`, `A => Effect` | `Effect` | Chains effects together | +| [`tap`](https://effect.website/docs/getting-started/building-pipelines/#tap) | `Effect`, `A => Effect` | `Effect` | Performs an effect but returns the original value | +| [`all`](https://effect.website/docs/getting-started/building-pipelines/#all) | `[Effect, Effect, ...]` | `Effect<[A, B, ...], E, R>` | Combines multiple effects | ## `pipe` and `Effect.gen` @@ -131,14 +131,14 @@ Effect.gen(function* () { ### Handling Expected Errors -| **Function** | **Input** | **Output** | **Description** | +| **Function** | **Input** | **Output** | **Description** | | ------------- | -------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------- | -| `catchAll` | `Effect`, `E1 => Effect` | `Effect` | Recovering from all errors | -| `catchTag` | `Effect`, `string`, `TaggedError => Effect` | `Effect \| E2, R>` | Recovering from specific tagged error | -| `catchAll` | `Effect`, `E1 => Option>` | `Effect` | Recovering from some errors | -| `either` | `Effect` | `Effect, never, R>` | Move error into success 'channel' | -| `match` | `Effect`, `A => B`, `E => C` | `Effect` | Handle both cases at once | -| `matchEffect` | `Effect`, `A => Effect`, `E1 => Effect` | `Effect` | Handle both cases with `Effect`s | +| [`catchAll`](https://effect.website/docs/error-management/fallback/#catchall) | `Effect`, `E1 => Effect` | `Effect` | Recovering from all errors | +| [`catchTag`](https://effect.website/docs/error-management/fallback/#catchtag) | `Effect`, `string`, `TaggedError => Effect` | `Effect \| E2, R>` | Recovering from specific tagged error | +| [`catchAll`](https://effect.website/docs/error-management/fallback/#catchall) | `Effect`, `E1 => Option>` | `Effect` | Recovering from some errors | +| [`either`](https://effect.website/docs/error-management/error-channel-operations/#either) | `Effect` | `Effect, never, R>` | Move error into success 'channel' | +| [`match`](https://effect.website/docs/error-management/matching/#match) | `Effect`, `A => B`, `E => C` | `Effect` | Handle both cases at once | +| [`matchEffect`](https://effect.website/docs/error-management/matching/#matcheffect) | `Effect`, `A => Effect`, `E1 => Effect` | `Effect` | Handle both cases with `Effect`s | ## Defining and Using Services @@ -200,19 +200,19 @@ Effect.runPromise(main); // Type Error! Missing 'Scope' service Effect.runPromise(Effect.scoped(main)); // logs: contents \n file closed! ``` -| **Function** | **Input** | **Output** | **Description** | +| **Function** | **Input** | **Output** | **Description** | | -------------- | ---------------------------------------- | -------------------------- | -------------------------------------------------- | -| `scoped` | `Effect` | `Effect` | Defines where the current `Scope` should be closed | -| `addFinalizer` | `Effect`, `(exit) => Effect<_>` | `Effect` | Add a 'finalizer' to the current `Effect` | +| [`scoped`](https://effect.website/docs/resource-management/scope/#scoped) | `Effect` | `Effect` | Defines where the current `Scope` should be closed | +| [`addFinalizer`](https://effect.website/docs/resource-management/scope/#addfinalizer) | `Effect`, `(exit) => Effect<_>` | `Effect` | Add a 'finalizer' to the current `Effect` | ## Repitition + Retry A `Schedule` defines a scheduled pattern for executing effects. There are many constructors + combinators, so check out the docs. -| **Function** | **Input** | **Output** | **Description** | +| **Function** | **Input** | **Output** | **Description** | | ------------ | ----------------------------- | ----------------- | ---------------------------------------------------------------------- | -| `repeat` | `Effect`, `Schedule` | `Effect` | Repeats the `Effect` on success by the pattern defined by the schedule | -| `retry` | `Effect`, `Schedule` | `Effect` | Retrys the `Effect` on failure by the pattern defined by the schedule | +| [`repeat`](https://effect.website/docs/scheduling/repetition/#repeat) | `Effect`, `Schedule` | `Effect` | Repeats the `Effect` on success by the pattern defined by the schedule | +| [`retry`](https://effect.website/docs/scheduling/repetition/#retry) | `Effect`, `Schedule` | `Effect` | Retrys the `Effect` on failure by the pattern defined by the schedule | ## "Traits" diff --git a/cheatsheet.test.ts b/cheatsheet.test.ts new file mode 100644 index 0000000..7e42115 --- /dev/null +++ b/cheatsheet.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from "vitest" +import { Effect, pipe } from "effect" +import * as fs from "node:fs/promises" +import * as path from "node:path" + +// Helper function to extract links from markdown content +const extractLinks = (content: string): Array<{ name: string; url: string }> => { + const linkRegex = /\[`([^`]+)`\]\((https:\/\/effect\.website\/docs\/[^)]+)\)/g + const links: Array<{ name: string; url: string }> = [] + let match + + while ((match = linkRegex.exec(content)) !== null) { + // Since we know our regex pattern has two capture groups that must exist + // when there's a match, we can safely assert these are strings + const name = match[1] as string + const url = match[2] as string + links.push({ name, url }) + } + + return links +} + +// Helper function to check if a URL is accessible +const checkUrl = (url: string): Effect.Effect => + Effect.tryPromise({ + try: () => + fetch(url, { method: "HEAD" }).then((res) => res.ok), + catch: (error) => new Error(`Failed to check URL ${url}: ${error}`) + }) + +describe("Effect Cheatsheet Links", () => { + it("should have all valid links", async () => { + // Read the cheatsheet file + const content = await fs.readFile( + path.join(process.cwd(), "CHEATSHEET.md"), + "utf-8" + ) + + // Extract all links + const links = extractLinks(content) + + // Check each link + const results = await Effect.runPromise( + Effect.all( + links.map(({ name, url }) => + pipe( + checkUrl(url), + Effect.map((isValid) => ({ name, url, isValid })) + ) + ), + { concurrency: 5 } + ) + ) + + // Filter out invalid links + const invalidLinks = results.filter((result) => !result.isValid) + + // Assert that all links are valid + expect(invalidLinks).toHaveLength(0) + + // If there are invalid links, provide detailed information + if (invalidLinks.length > 0) { + console.error("Invalid links found:") + invalidLinks.forEach(({ name, url }) => { + console.error(`- ${name}: ${url}`) + }) + } + }) +}) \ No newline at end of file diff --git a/package.json b/package.json index 318b974..6120e86 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "type": "module", "devDependencies": { "@types/bun": "latest", - "typescript": "^5.8.2" + "typescript": "^5.8.2", + "vitest": "^3.2.2" }, "peerDependencies": { "typescript": "^5.8.2"