From 18a25d70ee743fc4ea0af32bf2735432a1ca5fc1 Mon Sep 17 00:00:00 2001 From: sunwrobert Date: Thu, 10 Sep 2026 13:11:12 +1000 Subject: [PATCH] chore: remove all skills --- .claude/skills/bun | 1 - .claude/skills/effect-ts | 1 - .claude/skills/img-to-react-structure | 1 - .claude/skills/interview-spec | 1 - .claude/skills/responsive-screenshots | 1 - .claude/skills/skill-author | 1 - AGENTS.md | 13 +- README.md | 31 +- scripts/init-project.sh | 21 +- skills/bun/SKILL.md | 110 -- skills/concision/SKILL.md | 6 - skills/effect-ts/SKILL.md | 338 ----- skills/effect-ts/references/alchemy-aws.md | 557 -------- skills/effect-ts/references/effect-atom.md | 633 --------- skills/effect-ts/references/effect-core.md | 494 ------- .../effect-ts/references/effect-solutions.md | 623 --------- skills/effect-ts/references/lucas-barake.md | 642 --------- skills/effect-ts/references/setup.md | 140 -- skills/effect-ts/references/teamwarp-api.md | 369 ----- skills/img-to-react-structure/SKILL.md | 72 - skills/interview-spec/SKILL.md | 11 - skills/project-starter/SKILL.md | 664 --------- skills/ralph/afk-ralph.sh | 25 - skills/ralph/ralph-once.sh | 8 - skills/responsive-screenshots/SKILL.md | 57 - skills/skill-author/SKILL.md | 1217 ----------------- 26 files changed, 7 insertions(+), 6030 deletions(-) delete mode 120000 .claude/skills/bun delete mode 120000 .claude/skills/effect-ts delete mode 120000 .claude/skills/img-to-react-structure delete mode 120000 .claude/skills/interview-spec delete mode 120000 .claude/skills/responsive-screenshots delete mode 120000 .claude/skills/skill-author delete mode 100644 skills/bun/SKILL.md delete mode 100644 skills/concision/SKILL.md delete mode 100644 skills/effect-ts/SKILL.md delete mode 100644 skills/effect-ts/references/alchemy-aws.md delete mode 100644 skills/effect-ts/references/effect-atom.md delete mode 100644 skills/effect-ts/references/effect-core.md delete mode 100644 skills/effect-ts/references/effect-solutions.md delete mode 100644 skills/effect-ts/references/lucas-barake.md delete mode 100644 skills/effect-ts/references/setup.md delete mode 100644 skills/effect-ts/references/teamwarp-api.md delete mode 100644 skills/img-to-react-structure/SKILL.md delete mode 100644 skills/interview-spec/SKILL.md delete mode 100644 skills/project-starter/SKILL.md delete mode 100755 skills/ralph/afk-ralph.sh delete mode 100755 skills/ralph/ralph-once.sh delete mode 100644 skills/responsive-screenshots/SKILL.md delete mode 100644 skills/skill-author/SKILL.md diff --git a/.claude/skills/bun b/.claude/skills/bun deleted file mode 120000 index b978302..0000000 --- a/.claude/skills/bun +++ /dev/null @@ -1 +0,0 @@ -../../skills/bun \ No newline at end of file diff --git a/.claude/skills/effect-ts b/.claude/skills/effect-ts deleted file mode 120000 index 6dbb260..0000000 --- a/.claude/skills/effect-ts +++ /dev/null @@ -1 +0,0 @@ -../../skills/effect-ts \ No newline at end of file diff --git a/.claude/skills/img-to-react-structure b/.claude/skills/img-to-react-structure deleted file mode 120000 index 69d61e9..0000000 --- a/.claude/skills/img-to-react-structure +++ /dev/null @@ -1 +0,0 @@ -../../skills/img-to-react-structure \ No newline at end of file diff --git a/.claude/skills/interview-spec b/.claude/skills/interview-spec deleted file mode 120000 index dde651a..0000000 --- a/.claude/skills/interview-spec +++ /dev/null @@ -1 +0,0 @@ -../../skills/interview-spec \ No newline at end of file diff --git a/.claude/skills/responsive-screenshots b/.claude/skills/responsive-screenshots deleted file mode 120000 index b9e0231..0000000 --- a/.claude/skills/responsive-screenshots +++ /dev/null @@ -1 +0,0 @@ -../../skills/responsive-screenshots \ No newline at end of file diff --git a/.claude/skills/skill-author b/.claude/skills/skill-author deleted file mode 120000 index b7d0705..0000000 --- a/.claude/skills/skill-author +++ /dev/null @@ -1 +0,0 @@ -../../skills/skill-author \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 0cb332e..1ff20ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Knowledge Base Agent Instructions -Canonical repository for agent skills, scripts, and shared instructions. This is the source of truth for coding standards. +Canonical repository for scripts and shared instructions. This is the source of truth for coding standards. ## Commands @@ -27,22 +27,11 @@ bun run test:unit # Run a single test file ``` knowledge-base/ -├── skills/ # Skill definitions (source of truth) -│ └── /SKILL.md # Skill instructions + optional refs/scripts -├── .claude/skills/ # Symlinks to skills/ (activates them) ├── scripts/ # Utility scripts (run from repo root) ├── AGENTS.md # Shared agent instructions └── CLAUDE.md -> AGENTS.md # Symlink for Claude Code ``` -### Skills Pattern - -Skills in `skills//SKILL.md` are templates. Symlink to activate: - -```bash -ln -s ../../skills/ .claude/skills/ -``` - ## Code Style ### Formatting (oxfmt) diff --git a/README.md b/README.md index 531863b..727a7ce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Knowledge Base -Canonical repository for agent skills, scripts, and shared instructions. Use this as the source of truth for all projects. This is a living document for how I like to build projects and write code. +Canonical repository for scripts and shared instructions. Use this as the source of truth for all projects. This is a living document for how I like to build projects and write code. ## Structure @@ -8,18 +8,13 @@ Canonical repository for agent skills, scripts, and shared instructions. Use thi knowledge-base/ ├── AGENTS.md # Shared agent instructions (code philosophy, etc.) ├── CLAUDE.md -> AGENTS.md # Symlink for Claude Code compatibility -├── skills/ # Skill definitions (source of truth) -│ ├── bun/ # Bun runtime preferences -│ ├── effect-ts/ # Effect TypeScript patterns + setup -│ └── skill-author/ # How to write effective skills -├── scripts/ # Portable utility scripts -└── .claude/skills/ # Symlinks to active skills +└── scripts/ # Utility scripts ``` ## Syncing With Other Repos -- Treat this repo as the canonical mirror for shared skills and instructions. -- When editing skills or scripts here, sync changes to consuming repos. +- Treat this repo as the canonical mirror for shared scripts and instructions. +- When editing instructions or scripts here, sync changes to consuming repos. - Scripts are designed to run from this repo's root (not portable to other projects). ## Pointer-Style AGENTS.md @@ -34,24 +29,6 @@ READ ~/Repositories/knowledge-base/AGENTS.md BEFORE ANYTHING (skip if missing). This keeps instructions centralized. Edit once here; all repos inherit updates. -## Skills - -Skills teach Claude specific capabilities. Each skill has a `SKILL.md` with frontmatter + instructions. - -| Skill | Description | -| -------------- | ---------------------------------- | -| `bun` | Prefer Bun over Node.js, npm, vite | -| `effect-ts` | Effect TypeScript setup + patterns | -| `skill-author` | Best practices for writing skills | - -### Activating Skills - -Skills in `skills/` are the source of truth. Symlink them to `.claude/skills/` to activate: - -```bash -ln -s ../../skills/ .claude/skills/ -``` - ## Scripts Utility scripts meant to be run from this repo's root: diff --git a/scripts/init-project.sh b/scripts/init-project.sh index 01c35f3..5bea6fc 100755 --- a/scripts/init-project.sh +++ b/scripts/init-project.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Initialize a new project with Claude Code settings and skills from knowledge-base. +# Initialize a new project with Claude Code settings from knowledge-base. # # Usage: # curl -fsSL https://raw.githubusercontent.com/sunwrobert/knowledge-base/main/scripts/init-project.sh | bash @@ -11,32 +11,15 @@ REPO="sunwrobert/knowledge-base" BRANCH="main" BASE_URL="https://raw.githubusercontent.com/${REPO}/${BRANCH}" -SKILLS=( - bun - conventional-commits - effect-ts - interview-spec - ultracite -) - TARGET_DIR="${1:-.}" echo "Initializing Claude Code settings in: ${TARGET_DIR}" -mkdir -p "${TARGET_DIR}/.claude/skills" +mkdir -p "${TARGET_DIR}/.claude" echo "Fetching settings.json..." curl -fsSL "${BASE_URL}/.claude/settings.json" -o "${TARGET_DIR}/.claude/settings.json" -for skill in "${SKILLS[@]}"; do - echo "Fetching skill: ${skill}..." - mkdir -p "${TARGET_DIR}/.claude/skills/${skill}" - curl -fsSL "${BASE_URL}/skills/${skill}/SKILL.md" -o "${TARGET_DIR}/.claude/skills/${skill}/SKILL.md" -done - echo "" echo "Done! Initialized:" echo " .claude/settings.json" -for skill in "${SKILLS[@]}"; do - echo " .claude/skills/${skill}/SKILL.md" -done diff --git a/skills/bun/SKILL.md b/skills/bun/SKILL.md deleted file mode 100644 index de60d02..0000000 --- a/skills/bun/SKILL.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: bun -description: Use Bun instead of Node.js, npm, pnpm, or vite. ---- - -Default to using Bun instead of Node.js. - -- Use `bun ` instead of `node ` or `ts-node ` -- Use `bun test` instead of `jest` or `vitest` -- Use `bun build ` instead of `webpack` or `esbuild` -- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` -- Use `bun run - - -``` - -With the following `frontend.tsx`: - -```tsx#frontend.tsx -import React from "react"; -import { createRoot } from "react-dom/client"; - -// import .css files directly and it works -import './index.css'; - -const root = createRoot(document.body); - -export default function Frontend() { - return

Hello, world!

; -} - -root.render(); -``` - -Then, run index.ts - -```sh -bun --hot ./index.ts -``` - -For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. diff --git a/skills/concision/SKILL.md b/skills/concision/SKILL.md deleted file mode 100644 index 5a88080..0000000 --- a/skills/concision/SKILL.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -name: concision -description: Invoke this when the user asks to be concise. ---- - -Be concise in prose. Sacrifice grammar for the sake of concision. diff --git a/skills/effect-ts/SKILL.md b/skills/effect-ts/SKILL.md deleted file mode 100644 index 0a76d92..0000000 --- a/skills/effect-ts/SKILL.md +++ /dev/null @@ -1,338 +0,0 @@ ---- -name: writing-effect-ts -description: Provides idiomatic patterns for Effect TypeScript. Use when writing Effect code (Effect.gen, Effect.fn, pipes), working with services and layers, data modeling with Schema, error handling, configuration, or testing with @effect/vitest. ---- - -# Effect-TS - -**Reference**: `effect-solutions show ` for detailed examples. Effect source at `~/.local/share/effect-solutions/effect` for deeper reference. - -## Basics - -### Effect.gen - sequencing - -```typescript -const program = Effect.gen(function* () { - const data = yield* fetchData - yield* Effect.logInfo(`Processing: ${data}`) - return yield* processData(data) -}) -``` - -### Effect.fn - traced functions - -```typescript -const processUser = Effect.fn("processUser")(function* (userId: string) { - const user = yield* getUser(userId) - return yield* processData(user) -}) -``` - -### Pipe - instrumentation - -```typescript -const program = fetchData.pipe( - Effect.timeout("5 seconds"), - Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.compose(Schedule.recurs(3)))), - Effect.tap((data) => Effect.logInfo(`Fetched: ${data}`)), - Effect.withSpan("fetchData") -) -``` - -## Services & Layers - -### Service definition - -```typescript -class Database extends Context.Tag("@app/Database")< - Database, - { - readonly query: (sql: string) => Effect.Effect - readonly execute: (sql: string) => Effect.Effect - } ->() {} -``` - -Rules: - -- Tag identifiers unique (`@path/ServiceName`) -- Methods have no deps (`R = never`) -- Use `readonly` - -### Layer implementation - -```typescript -class Users extends Context.Tag("@app/Users")< - Users, - { readonly findById: (id: UserId) => Effect.Effect } ->() { - static readonly layer = Layer.effect( - Users, - Effect.gen(function* () { - const http = yield* HttpClient.HttpClient - - const findById = Effect.fn("Users.findById")(function* (id: UserId) { - const response = yield* http.get(`/users/${id}`) - return yield* HttpClientResponse.schemaBodyJson(User)(response) - }) - - return Users.of({ findById }) - }) - ) -} -``` - -### Test layer - -```typescript -class Database extends Context.Tag("@app/Database")() { - static readonly testLayer = Layer.sync(Database, () => { - const store = new Map() - return Database.of({ - query: (sql) => Effect.succeed([...store.values()]), - execute: (sql) => Effect.sync(() => void store.clear()) - }) - }) -} -``` - -### Provide once at top - -```typescript -const appLayer = userServiceLayer.pipe( - Layer.provideMerge(databaseLayer), - Layer.provideMerge(configLayer) -) -const main = program.pipe(Effect.provide(appLayer)) -``` - -### Layer memoization - -Store parameterized layers in constants: - -```typescript -// Bad: creates two pools -Layer.provide(Postgres.layer({ poolSize: 10 })) -Layer.provide(Postgres.layer({ poolSize: 10 })) - -// Good: single pool -const postgresLayer = Postgres.layer({ poolSize: 10 }) -Layer.provide(postgresLayer) -``` - -## Data Modeling - -### Schema.Class - records - -```typescript -const UserId = Schema.String.pipe(Schema.brand("UserId")) -type UserId = typeof UserId.Type - -class User extends Schema.Class("User")({ - id: UserId, - name: Schema.String, - email: Schema.String, -}) {} -``` - -### Schema.TaggedClass - variants - -```typescript -class Success extends Schema.TaggedClass()("Success", { - value: Schema.Number, -}) {} - -class Failure extends Schema.TaggedClass()("Failure", { - error: Schema.String, -}) {} - -const Result = Schema.Union(Success, Failure) - -Match.valueTags(result, { - Success: ({ value }) => `Got: ${value}`, - Failure: ({ error }) => `Error: ${error}` -}) -``` - -### Branded types - -Brand all domain primitives: - -```typescript -const UserId = Schema.String.pipe(Schema.brand("UserId")) -const PostId = Schema.String.pipe(Schema.brand("PostId")) -const Email = Schema.String.pipe(Schema.brand("Email")) -const Port = Schema.Int.pipe(Schema.between(1, 65535), Schema.brand("Port")) -``` - -### JSON parsing - -```typescript -const MoveFromJson = Schema.parseJson(Move) -const move = yield* Schema.decodeUnknown(MoveFromJson)(jsonString) -const json = yield* Schema.encode(MoveFromJson)(move) -``` - -## Error Handling - -### Schema.TaggedError - -```typescript -class NotFoundError extends Schema.TaggedError()( - "NotFoundError", - { resource: Schema.String, id: Schema.String } -) {} -``` - -Yieldable - no `Effect.fail()` needed: - -```typescript -return status === 404 ? NotFoundError.make({ id }) : Effect.die(error) -``` - -### catchTag / catchTags - -```typescript -program.pipe(Effect.catchTag("HttpError", (e) => Effect.succeed("recovered"))) - -program.pipe(Effect.catchTags({ - HttpError: () => Effect.succeed("http"), - ValidationError: () => Effect.succeed("validation") -})) -``` - -### Errors vs defects - -- **Typed errors**: domain failures caller can handle (validation, not found, denied) -- **Defects**: unrecoverable bugs, invariant violations - -```typescript -const config = yield* loadConfig.pipe(Effect.orDie) -``` - -### Schema.Defect - wrap unknown - -```typescript -class ApiError extends Schema.TaggedError()( - "ApiError", - { endpoint: Schema.String, error: Schema.Defect } -) {} -``` - -## Config - -### Basic - -```typescript -const apiKey = yield* Config.redacted("API_KEY") -const port = yield* Config.integer("PORT") -``` - -### Config service pattern - -```typescript -class ApiConfig extends Context.Tag("@app/ApiConfig")< - ApiConfig, - { readonly apiKey: Redacted.Redacted; readonly baseUrl: string } ->() { - static readonly layer = Layer.effect( - ApiConfig, - Effect.gen(function* () { - const apiKey = yield* Config.redacted("API_KEY") - const baseUrl = yield* Config.string("API_BASE_URL").pipe( - Config.orElse(() => Config.succeed("https://api.example.com")) - ) - return ApiConfig.of({ apiKey, baseUrl }) - }) - ) -} -``` - -### Schema.Config - validation - -```typescript -const Port = Schema.Int.pipe(Schema.between(1, 65535), Schema.brand("Port")) -const port = yield* Schema.Config("PORT", Port) -``` - -### Primitives - -```typescript -Config.string("VAR") -Config.integer("PORT") -Config.boolean("DEBUG") -Config.redacted("SECRET") // hidden in logs -Config.url("URL") -Config.duration("TIMEOUT") -Config.array(Config.string(), "TAGS") -``` - -## Testing - -### Setup - -```bash -bun add -D vitest @effect/vitest -``` - -```typescript -// vitest.config.ts -export default defineConfig({ test: { include: ["tests/**/*.test.ts"] } }) -``` - -### Basic test - -```typescript -import { describe, expect, it } from "@effect/vitest" - -describe("Calculator", () => { - it.effect("adds numbers", () => - Effect.gen(function* () { - const result = yield* Effect.succeed(1 + 1) - expect(result).toBe(2) - }) - ) -}) -``` - -### Variants - -- `it.effect()` - Effect tests (most common) -- `it.scoped()` - scoped resources, auto cleanup -- `it.live()` - real clock (no TestClock) - -### TestClock - -```typescript -it.effect("time test", () => - Effect.gen(function* () { - const fiber = yield* Effect.delay(Effect.succeed("done"), "10 seconds").pipe(Effect.fork) - yield* TestClock.adjust("10 seconds") - expect(yield* Fiber.join(fiber)).toBe("done") - }) -) -``` - -### Provide layers - -```typescript -const testLayer = Events.layer.pipe( - Layer.provideMerge(Users.testLayer), - Layer.provideMerge(Tickets.testLayer) -) - -it.effect("test", () => - Effect.gen(function* () { - const events = yield* Events - // ... - }).pipe(Effect.provide(testLayer)) -) -``` - -### Modifiers - -```typescript -it.effect.skip("disabled", () => ...) -it.effect.only("focus", () => ...) -it.effect.fails("expected fail", () => ...) -``` diff --git a/skills/effect-ts/references/alchemy-aws.md b/skills/effect-ts/references/alchemy-aws.md deleted file mode 100644 index 210c78a..0000000 --- a/skills/effect-ts/references/alchemy-aws.md +++ /dev/null @@ -1,557 +0,0 @@ -# Alchemy AWS Patterns - -**Source**: `~/.local/repos/alchemy-run/alchemy-effect` and `~/.local/repos/alchemy-run/itty-aws` - -## Overview - -Infrastructure-as-code framework using Effect-TS for AWS resources. - -## AWS Service Client Pattern - -Generic client factory with retry and logging: - -```typescript -export const createAWSServiceClientLayer = , Client>( - tag: Tag, - clss: new (config: AWSClientConfig) => Client, -) => () => - Layer.effect( - tag, - Effect.gen(function* () { - const region = yield* Region - const credentials = yield* Credentials - - const client = new clss({ - region, - credentials: { - accessKeyId: Redacted.value(credentials.accessKeyId), - secretAccessKey: Redacted.value(credentials.secretAccessKey), - sessionToken: credentials.sessionToken - ? Redacted.value(credentials.sessionToken) - : undefined, - }, - }) - - // Proxy with automatic retry - return new Proxy(client as any, { - get: (target, prop) => (...args: any[]) => - target[prop](...args).pipe( - Effect.retry({ - while: (e: any) => - e._tag === "ThrottlingException" || - e._tag === "RequestLimitExceeded" || - e._tag === "TooManyRequestsException", - schedule: Schedule.exponential(10).pipe( - Schedule.intersect(Schedule.recurs(10)), - Schedule.jittered, - Schedule.modifyDelay(([out]) => - Duration.toMillis(out) > 3000 ? Duration.seconds(3) : out - ), - ), - }), - Logger.withMinimumLogLevel(LogLevel.Warning), - ), - }) - }), - ) - -// Usage -export const client = createAWSServiceClientLayer(LambdaClient, LambdaClientImpl) -export const client = createAWSServiceClientLayer(DynamoDBClient, DynamoDBClientImpl) -``` - -## Credentials Service - -```typescript -export class Credentials extends Context.Tag("AWS::Credentials")< - Credentials, - { - accessKeyId: Redacted.Redacted - secretAccessKey: Redacted.Redacted - sessionToken: Redacted.Redacted | undefined - expiration?: number - } ->() {} - -// From environment -export const fromEnv = Layer.effect( - Credentials, - Effect.gen(function* () { - const accessKeyId = yield* Config.redacted("AWS_ACCESS_KEY_ID") - const secretAccessKey = yield* Config.redacted("AWS_SECRET_ACCESS_KEY") - const sessionToken = yield* Config.redacted("AWS_SESSION_TOKEN").pipe( - Config.option - ) - return Credentials.of({ - accessKeyId, - secretAccessKey, - sessionToken: Option.getOrUndefined(sessionToken), - }) - }) -) - -// From SSO profile -export const fromSSO = (profile: string) => - Layer.effect( - Credentials, - Effect.gen(function* () { - const ssoToken = yield* loadSSOToken(profile) - const roleCredentials = yield* getRoleCredentials(ssoToken) - return Credentials.of({ - accessKeyId: Redacted.make(roleCredentials.accessKeyId), - secretAccessKey: Redacted.make(roleCredentials.secretAccessKey), - sessionToken: Redacted.make(roleCredentials.sessionToken), - expiration: roleCredentials.expiration, - }) - }) - ) -``` - -## Region Service - -```typescript -export class Region extends Context.Tag("AWS::Region")() {} - -export const fromEnv = Layer.effect( - Region, - Config.string("AWS_REGION").pipe( - Config.orElse(() => Config.string("AWS_DEFAULT_REGION")) - ) -) - -export const fromStageConfig = Layer.effect( - Region, - Effect.gen(function* () { - const stageConfig = yield* StageConfig - return stageConfig.region ?? (yield* Config.string("AWS_REGION")) - }) -) -``` - -## Provider Pattern - -Resource lifecycle management: - -```typescript -export interface Provider { - readonly stables?: string[] // Properties that don't trigger replace - - diff?: Effect.Effect< - { action: "replace" } | undefined, - never, - { id: string; news: Props; olds: Props } - > - - create: Effect.Effect< - Attrs, - ProviderError, - { id: string; news: Props; session: Session } - > - - update: Effect.Effect< - Attrs, - ProviderError, - { id: string; news: Props; olds: Props; output: Attrs; session: Session } - > - - delete: Effect.Effect< - void, - ProviderError, - { output: Attrs; session: Session } - > -} -``` - -## DynamoDB Table Provider - -```typescript -export const tableProvider = (): Layer.Layer< - Provider, - never, - App | DynamoDBClient -> => - Table.provider.effect( - Effect.gen(function* () { - const dynamodb = yield* DynamoDBClient - const app = yield* App - - return { - stables: ["tableName", "tableArn"], - - diff: Effect.fn(function* ({ news, olds }) { - // Check if keys changed (requires replace) - if ( - news.partitionKey !== olds.partitionKey || - news.sortKey !== olds.sortKey - ) { - return { action: "replace" } - } - }), - - create: Effect.fn(function* ({ id, news, session }) { - const tableName = news.tableName ?? (yield* createPhysicalName({ id, maxLength: 255 })) - - yield* dynamodb.createTable({ - TableName: tableName, - KeySchema: toKeySchema(news), - AttributeDefinitions: toAttributeDefinitions(news.attributes), - BillingMode: news.billingMode ?? "PAY_PER_REQUEST", - Tags: createTagsList(id, news.tags), - }) - - yield* session.note(`Table created: ${tableName}`) - - // Wait for table to be active - yield* waitForTableActive(dynamodb, tableName) - - return { - tableName, - tableArn: `arn:aws:dynamodb:${region}:${accountId}:table/${tableName}`, - partitionKey: news.partitionKey, - sortKey: news.sortKey, - } - }), - - update: Effect.fn(function* ({ id, news, olds, output, session }) { - // Update tags if changed - if (!deepEqual(news.tags, olds.tags)) { - yield* dynamodb.tagResource({ - ResourceArn: output.tableArn, - Tags: createTagsList(id, news.tags), - }) - } - return output - }), - - delete: Effect.fn(function* ({ output, session }) { - yield* dynamodb.deleteTable({ TableName: output.tableName }) - yield* session.note(`Table deleted: ${output.tableName}`) - }), - } - }), - ) -``` - -## SQS Queue with Retry - -```typescript -export const queueProvider = () => - Queue.provider.effect( - Effect.gen(function* () { - const sqs = yield* SQSClient - - return { - create: Effect.fn(function* ({ id, news, session }) { - const queueName = news.queueName ?? (yield* createPhysicalName({ id })) - - const response = yield* sqs - .createQueue({ - QueueName: queueName, - Attributes: createAttributes(news), - }) - .pipe( - Effect.retry({ - while: (e) => e.name === "QueueDeletedRecently", - schedule: Schedule.fixed(1000).pipe( - Schedule.tapOutput((i) => - session.note(`Queue was deleted recently, retrying... ${i + 1}s`) - ), - ), - }), - ) - - const queueUrl = response.QueueUrl! - yield* session.note(queueUrl) - - const queueArn = yield* sqs.getQueueAttributes({ - QueueUrl: queueUrl, - AttributeNames: ["QueueArn"], - }).pipe(Effect.map((r) => r.Attributes!.QueueArn!)) - - return { queueName, queueUrl, queueArn } - }), - - delete: Effect.fn(function* ({ output }) { - yield* sqs.deleteQueue({ QueueUrl: output.queueUrl }) - }), - } - }), - ) -``` - -## Resource Type Definition - -```typescript -export interface TableProps< - Items extends any = any, - PartitionKey extends keyof Items = keyof Items, - SortKey extends keyof Items | undefined = undefined, -> { - items: type - attributes: AttributesSchema - partitionKey: PartitionKey - sortKey?: SortKey - tableName?: string - billingMode?: "PAY_PER_REQUEST" | "PROVISIONED" - tags?: Record -} - -export interface TableAttrs { - tableName: Props["tableName"] extends string ? Props["tableName"] : string - tableArn: `arn:aws:dynamodb:${string}:${string}:table/${this["tableName"]}` - partitionKey: Props["partitionKey"] - sortKey: Props["sortKey"] -} - -export const Table = Resource<{ - ( - id: ID, - props: Props, - ): Table -}>("AWS.DynamoDB.Table") -``` - -## Lambda Function with Bindings - -```typescript -export interface FunctionProps extends RuntimeProps { - functionName?: string - main: string - handler?: string - memory?: number - runtime?: "nodejs20.x" | "nodejs22.x" - architecture?: "x86_64" | "arm64" - url?: boolean -} - -// Usage with capability bindings -export class Api extends Lambda.serve("Api", { - fetch: Effect.fn(function* (request) { - const item = yield* DynamoDB.getItem({ - table: SingleTable, - key: { id, name: "world" }, - }) - return { body: JSON.stringify(item) } - }), -})({ - main: import.meta.filename, - bindings: $( - DynamoDB.GetItem(SingleTable, { - leadingKeys: $.anyOf("USER#123"), - attributes: $.anyOf("id", "name", "age"), - }), - ), -}) {} - -export default Api.handler.pipe( - Effect.provide(Layer.mergeAll(SQS.clientFromEnv(), DynamoDB.clientFromEnv())), - Lambda.toHandler, -) -``` - -## Binding Pattern - -Capability-based IAM bindings: - -```typescript -export interface Binding< - Run extends IRuntime, - Cap extends Capability = Capability, -> { - runtime: Run - capability: Cap - tag: string - props: Props -} - -export const Binding = (runtime: any, cap: string, tag?: string) => { - const Tag = Context.Tag(`${runtime.type}(${cap}, ${tag ?? cap})`)() - - return Object.assign( - (resource: any, props?: any) => ({ - runtime, - capability: { - type: cap, - resource, - sid: `${cap}${resource.id}`.replace(/[^a-zA-Z0-9]/g, ""), - label: `${cap}(${resource.id})`, - }, - props, - isCustom: false, - tag: tag ?? cap, - Tag, - }), - { - provider: { - effect: (eff) => Layer.effect(Tag, eff), - succeed: (service) => Layer.succeed(Tag, service), - }, - }, - ) -} -``` - -## Error Types - -```typescript -export class AccessDeniedException extends S.TaggedError()( - "AccessDeniedException", - {}, -).pipe(withCategory(ERROR_CATEGORIES.AWS_ERROR)) {} - -export class UnknownAwsError extends S.TaggedError()( - "UnknownAwsError", - { - errorTag: S.String, - errorData: S.Any, - }, -).pipe(withCategory(ERROR_CATEGORIES.AWS_ERROR)) {} - -export class ProfileNotFound extends Data.TaggedError("Alchemy::AWS::ProfileNotFound")<{ - message: string - profile: string -}> {} - -export class ExpiredSSOToken extends Data.TaggedError("Alchemy::AWS::ExpiredSSOToken")<{ - message: string - profile: string -}> {} -``` - -## Layer Composition - -```typescript -export const resources = () => - Layer.mergeAll( - DynamoDB.tableProvider(), - Lambda.functionProvider(), - SQS.queueProvider(), - EC2.vpcProvider(), - ) - -export const bindings = () => - Layer.mergeAll( - DynamoDB.getItemFromLambdaFunction(), - SQS.queueEventSourceProvider(), - SQS.sendMessageFromLambdaFunction(), - ) - -export const clients = () => - Layer.mergeAll( - DynamoDB.client(), - Lambda.client(), - SQS.client(), - ) - -export const providers = () => - resources().pipe( - Layer.provideMerge(bindings()), - Layer.provideMerge(clients()), - Layer.provideMerge(Region.fromStageConfig()), - Layer.provideMerge(Credentials.fromStageConfig()), - ) -``` - -## Plan & Apply - -```typescript -export type CRUD = - | Create - | Update - | Delete - | Replace - | NoopUpdate - -export const applyPlan =

(plan: P) => - Effect.gen(function* () { - const cli = yield* CLI - const session = yield* cli.startApplySession(plan) - - // 1. Expand graph (create new, update existing, create replacements) - const resources = yield* expandAndPivot(plan, session) - - // 2. Delete orphans and replaced resources - yield* collectGarbage(plan, session) - - yield* session.done() - return resources - }) -``` - -## State Management - -```typescript -export type ResourceStatus = - | "creating" - | "created" - | "updating" - | "updated" - | "deleting" - | "deleted" - | "replacing" - | "replaced" - -interface BaseResourceState { - resourceType: string - logicalId: string - instanceId: string - status: ResourceStatus - downstream: string[] - props?: Props - attr?: Attr -} - -export interface CreatingResourceState extends BaseResourceState { - status: "creating" - props: Props -} - -export interface CreatedResourceState extends BaseResourceState { - status: "created" - props: Props - attr: Attr -} -``` - -## Testing Pattern - -```typescript -test( - "create, update, delete table", - Effect.gen(function* () { - const dynamodb = yield* DynamoDB.DynamoDBClient - - class Table extends DynamoDB.Table("Table", { - tableName: "test", - items: type<{ id: string }>, - attributes: { id: S.String }, - partitionKey: "id", - }) {} - - const stack = yield* apply(Table) - - const actualTable = yield* dynamodb.describeTable({ - TableName: stack.Table.tableName, - }) - expect(actualTable.Table?.TableArn).toEqual(stack.Table.tableArn) - - yield* destroy() - - yield* assertTableIsDeleted(stack.Table.tableName) - }).pipe(Effect.provide(AWS.providers())), -) - -const assertTableIsDeleted = Effect.fn(function* (tableName: string) { - const dynamodb = yield* DynamoDB.DynamoDBClient - yield* dynamodb - .describeTable({ TableName: tableName }) - .pipe( - Effect.flatMap(() => Effect.fail(new TableStillExists())), - Effect.retry({ - while: (e) => e._tag === "TableStillExists", - schedule: Schedule.exponential(100), - }), - Effect.catchTag("ResourceNotFoundException", () => Effect.void), - ) -}) -``` diff --git a/skills/effect-ts/references/effect-atom.md b/skills/effect-ts/references/effect-atom.md deleted file mode 100644 index 6a19b88..0000000 --- a/skills/effect-ts/references/effect-atom.md +++ /dev/null @@ -1,633 +0,0 @@ -# Effect-Atom Patterns - -**Source**: `~/.local/repos/tim-smart/effect-atom` - -## Overview - -Reactive state management for Effect with SSR/hydration support. - -``` -packages/atom/src/ -├── Atom.ts # Core atom definitions -├── Registry.ts # Registry interface & context -├── Result.ts # Initial/Success/Failure type -├── Hydration.ts # SSR dehydration/hydration -└── AtomRef.ts # Imperative reference API - -packages/atom-react/src/ -├── Hooks.ts # React hooks (useAtom, useAtomValue) -├── ReactHydration.ts # HydrationBoundary component -└── RegistryContext.ts # React context provider -``` - -## Atom Interface - -```typescript -export interface Atom extends Pipeable { - readonly [TypeId]: TypeId - readonly keepAlive: boolean - readonly lazy: boolean - readonly read: (get: Context) => A - readonly refresh?: (f: (atom: Atom) => void) => void - readonly label?: readonly [name: string, stack: string] - readonly idleTTL?: number -} -``` - -## Context Protocol - -```typescript -export interface Context { - (atom: Atom): A // Callable - get(this: Context, atom: Atom): A - result(this: Context, atom: Atom>, options?: { - readonly suspendOnWaiting?: boolean - }): Effect.Effect - subscribe(this: Context, atom: Atom, f: (_: A) => void, options?: { - readonly immediate?: boolean - }): void - stream(this: Context, atom: Atom): Stream.Stream - readonly registry: Registry.Registry -} -``` - -## Creating Atoms - -### Readable Atom - -```typescript -const countAtom = Atom.readable((get) => { - return get(baseAtom) * 2 -}) -``` - -### Writable Atom - -```typescript -const countAtom = Atom.writable( - (get) => get(baseAtom), - (ctx, value: number) => { - ctx.set(baseAtom, value) - } -) -``` - -### State Atom (shorthand for writable) - -```typescript -const countAtom = Atom.state(0) -``` - -### Effect Atom - -```typescript -const userAtom = Atom.effect((get) => - Effect.gen(function* () { - const id = get(userIdAtom) - return yield* fetchUser(id) - }) -) -``` - -### Family (parameterized atoms) - -```typescript -const userFamily = Atom.family((id: string) => - Atom.effect((get) => fetchUser(id)) -) - -// Usage -const user = get(userFamily("user-123")) -``` - -## Result Type - -Three-state result for async atoms: - -```typescript -export type Result = - | Initial - | Success - | Failure - -export interface Initial { - readonly _tag: "Initial" -} - -export interface Success { - readonly _tag: "Success" - readonly value: A - readonly timestamp: number -} - -export interface Failure { - readonly _tag: "Failure" - readonly cause: Cause.Cause - readonly previousSuccess: Option.Option> -} -``` - -### Waiting State - -```typescript -// Mark result as waiting (refetching) -const waitingResult = Result.waiting(result) -result.waiting // true - -// Check states -Result.isInitial(result) -Result.isSuccess(result) -Result.isFailure(result) -``` - -### Result Builder Pattern - -```typescript -Result.builder(result) - .onWaiting((r) => "Loading...") - .onInitial((r) => "Not started") - .onSuccess((value, r) => `Got: ${value}`) - .onFailure((cause, r) => `Error: ${cause}`) - .orElse(() => "Unknown state") -``` - -## AtomRuntime - Service Integration - -```typescript -const runtime = Atom.context({ memoMap: Layer.MemoMap.make() }) - -// Create runtime from layer -const appRuntime = runtime(AppLayer) - -// Create atoms that use services -const userAtom = appRuntime.atom((get) => - Effect.gen(function* () { - const userService = yield* UserService - return yield* userService.getCurrentUser() - }) -) - -// Create traced functions -const fetchUser = appRuntime.fn("fetchUser")((id: string) => - Effect.gen(function* () { - const userService = yield* UserService - return yield* userService.getById(id) - }) -) -``` - -## Function Atoms - -Atoms that accept arguments: - -```typescript -const searchAtom = Atom.fn((query: string, get: FnContext) => - Effect.gen(function* () { - const api = yield* SearchApi - return yield* api.search(query) - }) -) - -// Usage -registry.set(searchAtom, "search term") - -// Reset -registry.set(searchAtom, Atom.Reset) - -// Interrupt -registry.set(searchAtom, Atom.Interrupt) -``` - -## Registry - -```typescript -// Create registry -const registry = Registry.make({ - scheduleTask: (f) => scheduler.scheduleTask(f, 0), - defaultIdleTTL: 400, // Auto-dispose after idle -}) - -// Get atom value -const value = registry.get(myAtom) - -// Set writable atom -registry.set(myAtom, newValue) - -// Subscribe to changes -const unsubscribe = registry.subscribe(myAtom, (value) => { - console.log("New value:", value) -}) - -// Invalidate (trigger refresh) -registry.invalidate(myAtom) - -// Dispose registry -registry.dispose() -``` - -### Registry Layer - -```typescript -export const layerOptions = (options?: { - readonly scheduleTask?: (f: () => void) => void - readonly defaultIdleTTL?: number -}): Layer.Layer => - Layer.scoped( - AtomRegistry, - Effect.gen(function* () { - const scope = yield* Effect.scope - const registry = internal.make(options) - yield* Scope.addFinalizer(scope, Effect.sync(() => registry.dispose())) - return registry - }) - ) -``` - -## SSR Hydration - -### Server-side Dehydration - -```typescript -const dehydrated = Hydration.dehydrate(registry, { - encodeInitialAs: "promise" // "ignore" | "promise" | "value-only" -}) - -// Returns Array -// { -// key: string, -// value: encoded, -// dehydratedAt: timestamp, -// resultPromise?: Promise -// } -``` - -### Client-side Hydration - -```typescript -Hydration.hydrate(registry, dehydratedAtoms) -``` - -### Serializable Atoms - -Mark atoms for hydration: - -```typescript -const userAtom = Atom.effect((get) => fetchUser()).pipe( - Atom.serializable({ - key: "currentUser", - schema: UserSchema - }) -) -``` - -## React Integration - -### Registry Provider - -```typescript -import { RegistryProvider } from "@effect-atom/atom-react" - -function App() { - return ( - - - - ) -} -``` - -### useAtom Hook - -```typescript -const [value, setValue] = useAtom(countAtom) - -// With mode -const [value, setValue] = useAtom(countAtom, { mode: "promise" }) -``` - -### useAtomValue Hook - -```typescript -const value = useAtomValue(countAtom) -``` - -### useAtomSuspense Hook - -```typescript -// Suspends until result is Success -const result = useAtomSuspense(asyncAtom, { - suspendOnWaiting: true, - includeFailure: false -}) -``` - -### HydrationBoundary - -```typescript -import { HydrationBoundary } from "@effect-atom/atom-react" - -function App({ dehydratedState }) { - return ( - - - - - - ) -} -``` - -## useSyncExternalStore Integration - -Internal implementation for React 18+: - -```typescript -function useStore(registry: Registry, atom: Atom): A { - const store = makeStore(registry, atom) - return React.useSyncExternalStore( - store.subscribe, - store.snapshot, - store.getServerSnapshot - ) -} - -function makeStore(registry: Registry, atom: Atom): AtomStore { - return { - subscribe: (f) => registry.subscribe(atom, f), - snapshot: () => registry.get(atom), - getServerSnapshot: () => registry.get(atom) - } -} -``` - -## AtomRef - Mutable References - -Imperative reference with structural equality: - -```typescript -const ref = AtomRef.make({ count: 0, name: "test" }) - -// Get value -ref.value // { count: 0, name: "test" } - -// Set value (notifies if changed) -ref.set({ count: 1, name: "test" }) - -// Update -ref.update((v) => ({ ...v, count: v.count + 1 })) - -// Property lens -const countRef = ref.prop("count") -countRef.set(5) - -// Subscribe -ref.subscribe((value) => console.log(value)) -``` - -### Equality-based Notifications - -```typescript -// Only notifies if value actually changed -ref.set({ count: 0, name: "test" }) // No notification (equal) -ref.set({ count: 1, name: "test" }) // Notification (changed) -``` - -## Batch Updates - -```typescript -Atom.batch(() => { - registry.set(atomA, 1) - registry.set(atomB, 2) - registry.set(atomC, 3) -}) -// Single re-render after all updates -``` - -## Server Value Override - -Different values for SSR vs client: - -```typescript -const timeAtom = Atom.state(Date.now()).pipe( - Atom.withServerValue((get) => 0) // Always 0 on server -) -``` - -## Optimistic Updates - -```typescript -const atom = Atom.make(() => i) -const optimisticAtom = atom.pipe(Atom.optimistic) - -const fn = optimisticAtom.pipe( - Atom.optimisticFn({ - reducer: (_current, update: number) => update, - fn: Atom.fn(Effect.fnUntraced(function*() { - yield* latch.await - })) - }) -) - -// Optimistic phase: optimistic value shown but true value unchanged -r.set(fn, 1) -expect(r.get(optimisticAtom)).toEqual(1) // Optimistic -expect(r.get(atom)).toEqual(0) // True value - -// Commit phase: true value is now used -latch.unsafeOpen() -expect(r.get(optimisticAtom)).toEqual(2) // From server -``` - -### With Failure Rollback - -```typescript -const fn = optimisticAtom.pipe( - Atom.optimisticFn({ - reducer: (_, value) => value, - fn: Atom.fn()(Effect.fnUntraced(function*() { - yield* Effect.fail("error") - })) - }) -) - -r.set(fn, 1) -expect(r.get(optimisticAtom)).toEqual(1) // Optimistic - -// On failure, rollback to true value -latch.unsafeOpen() -expect(r.get(optimisticAtom)).toEqual(0) // Rolled back -``` - -## Stream Atoms - -```typescript -const count = Atom.make( - Stream.range(0, 2).pipe( - Stream.tap(() => Effect.sleep(50)) - ) -) - -let result = r.get(count) -assert(result.waiting) - -await vitest.advanceTimersByTimeAsync(50) -result = r.get(count) -assert.deepEqual(result.value, 0) // First value from stream -``` - -## Pull Pattern (Pagination) - -```typescript -const count = Atom.pull( - Stream.range(0, 1, 1).pipe( - Stream.tap(() => Effect.sleep(50)) - ) -) - -result = r.get(count) -assert.deepEqual(result.value, { done: false, items: [0] }) - -// Pull next batch -r.set(count, void 0) -result = r.get(count) -assert.deepEqual(result.value, { done: false, items: [0, 1] }) - -// Final pull -r.set(count, void 0) -assert.deepEqual(result.value, { done: true, items: [0, 1] }) -``` - -## Concurrent Async - -```typescript -const count = Atom.fn((_: number) => { - const latch = Effect.unsafeMakeLatch() - latches.push(latch) - return latch.await -}, { concurrent: true }) - -// Multiple concurrent fibers tracked -r.set(count, 1) -r.set(count, 1) -r.set(count, 1) -assert.strictEqual(latches.length, 3) // 3 concurrent fibers -``` - -## KeyValueStore Persistence - -```typescript -const persistedAtom = Atom.kvs({ - runtime: appRuntime, - key: "settings", - schema: SettingsSchema, - defaultValue: () => defaultSettings -}) - -// Reads from KeyValueStore on init, writes on set -``` - -## URL Search Parameter Persistence - -```typescript -const filterAtom = Atom.searchParam("filter", { - schema: Schema.Literal("all", "active", "completed") -}) - -// Syncs with URL ?filter=all -// Debounced updates to avoid URL spam -``` - -## Window Focus Refresh - -```typescript -const dataAtom = Atom.make(() => fetchData()).pipe( - Atom.refreshOnWindowFocus -) -// Automatically refreshes when window regains focus -``` - -## Idle TTL (Time To Live) - -```typescript -const state = Atom.make(0).pipe(Atom.setIdleTTL(10000)) // 10s TTL - -r.set(state, 10) -await vitest.advanceTimersByTimeAsync(10000) -expect(r.get(state)).toEqual(0) // Disposed, back to initial -``` - -## Transform Middleware - -```typescript -// Debounce middleware pattern -export const debounce = (self: Atom, duration: DurationInput): Atom => - Atom.transform(self, function(get) { - let timeout: number | undefined - let value = get.once(self) - - get.subscribe(self, function(val) { - value = val - if (timeout) clearTimeout(timeout) - timeout = setTimeout(() => get.setSelf(value), millis) - }) - - get.addFinalizer(() => { if (timeout) clearTimeout(timeout) }) - return value - }) -``` - -## Complete Example - -```typescript -// Schema for serialization -const TodoSchema = Schema.Struct({ - id: Schema.String, - title: Schema.String, - completed: Schema.Boolean -}) - -// Atoms -const todosAtom = Atom.effect((get) => - Effect.gen(function* () { - const api = yield* TodoApi - return yield* api.list() - }) -).pipe( - Atom.serializable({ - key: "todos", - schema: Schema.Array(TodoSchema) - }) -) - -const filteredTodosAtom = Atom.readable((get) => { - const todos = get(todosAtom) - const filter = get(filterAtom) - - return Result.map(todos, (items) => - items.filter(todo => - filter === "all" ? true : - filter === "completed" ? todo.completed : - !todo.completed - ) - ) -}) - -// React component -function TodoList() { - const result = useAtomValue(filteredTodosAtom) - - return Result.builder(result) - .onWaiting(() => ) - .onInitial(() =>

) - .onSuccess((todos) => ( -
    - {todos.map(todo => )} -
- )) - .onFailure((cause) => ) - .render() -} -``` diff --git a/skills/effect-ts/references/effect-core.md b/skills/effect-ts/references/effect-core.md deleted file mode 100644 index eefbd4a..0000000 --- a/skills/effect-ts/references/effect-core.md +++ /dev/null @@ -1,494 +0,0 @@ -# Effect-TS Core Patterns - -**Source**: `~/.local/repos/Effect-TS/effect` - -## Directory Structure - -``` -packages/effect/src/ # Public API (180+ modules) -packages/effect/src/internal/ # Private implementations -packages/cli/examples/ # Complete example apps (naval-fate, minigit) -``` - -## Effect.gen - Generator Composition - -Sequential effect composition using generators: - -```typescript -const program = Effect.gen(function* () { - const data = yield* fetchData - yield* Effect.logInfo(`Processing: ${data}`) - return yield* processData(data) -}) -``` - -**Adapter interface** supports chained transformations: - -```typescript -// Single effect -yield* effect - -// Chained (up to 12+ functions) -yield* a, f, g, h, final -``` - -## Effect.fn - Named Traced Functions - -Create named functions with automatic tracing: - -```typescript -const processUser = Effect.fn("processUser")(function* (userId: string) { - const user = yield* getUser(userId) - return yield* processData(user) -}) -``` - -## Pipe Composition - -Fundamental transformation primitive: - -```typescript -import { pipe } from "effect" - -pipe(value, f, g, h) // Basic transformation -pipe(Effect.succeed(5), Effect.map(n => n * 2)) // With Effects -pipe(context, Context.add(Tag, service)) // Context operations -``` - -## Context.Tag - Service Definition - -```typescript -// Generic tag -const MyService = Context.GenericTag("MyService") - -// Class-based tag (preferred) -class Database extends Context.Tag("@app/Database")< - Database, - { - readonly query: (sql: string) => Effect.Effect - readonly execute: (sql: string) => Effect.Effect - } ->() {} -``` - -## Context.Reference - Tags with Defaults - -```typescript -class Config extends Context.Reference()("Config", { - defaultValue: () => ({ timeout: 5000 }) -}) {} -``` - -## Layer - Service Implementations - -### Layer.succeed - Simple implementation - -```typescript -const DatabaseLive = Layer.succeed(Database, { - query: (sql) => Effect.succeed([]), - execute: (sql) => Effect.void -}) -``` - -### Layer.effect - Effectful implementation - -```typescript -const DatabaseLive = Layer.effect( - Database, - Effect.gen(function* () { - const config = yield* Config - const pool = yield* createPool(config) - return Database.of({ - query: (sql) => pool.query(sql), - execute: (sql) => pool.execute(sql) - }) - }) -) -``` - -### Layer.scoped - With resource cleanup - -```typescript -const DatabaseLive = Layer.scoped( - Database, - Effect.gen(function* () { - const pool = yield* Effect.acquireRelease( - createPool(), - (pool) => pool.close() - ) - return Database.of({ query: pool.query }) - }) -) -``` - -## Layer Composition - -```typescript -const appLayer = userServiceLayer.pipe( - Layer.provideMerge(databaseLayer), - Layer.provideMerge(configLayer) -) - -const main = program.pipe(Effect.provide(appLayer)) -``` - -## Data.TaggedError - -```typescript -export const TaggedError = (tag: Tag) => - class extends Error { - readonly _tag = tag - } - -// Usage -class ShipNotFoundError extends Data.TaggedError("ShipNotFoundError")<{ - readonly name: string - readonly x: number - readonly y: number -}> { - toString(): string { - return `Ship '${this.name}' not found at (${this.x}, ${this.y})` - } -} -``` - -## Error Handling - -### catchTag - Single error type - -```typescript -program.pipe( - Effect.catchTag("HttpError", (e) => Effect.succeed("recovered")) -) -``` - -### catchTags - Multiple error types - -```typescript -program.pipe( - Effect.catchTags({ - HttpError: () => Effect.succeed("http"), - ValidationError: () => Effect.succeed("validation") - }) -) -``` - -## serviceFunctions - Extract Service Methods - -```typescript -const { createShip, moveShip, shoot } = Effect.serviceFunctions(NavalFateStore) - -// Now call directly without yield* service first -yield* createShip("Enterprise") -``` - -## Data Structures - -### Data.Class - Structural Equality - -```typescript -class Person extends Data.Class<{ name: string }> {} - -const a = new Person({ name: "Mike" }) -const b = new Person({ name: "Mike" }) -Equal.equals(a, b) // true - structural equality -``` - -### Data.TaggedEnum - Discriminated Unions - -```typescript -type HttpError = Data.TaggedEnum<{ - NotFound: {} - InternalServerError: { reason: string } -}> - -const { $is, $match, NotFound, InternalServerError } = Data.taggedEnum() - -const error = NotFound() -const result = $match({ - NotFound: () => 0, - InternalServerError: ({ reason }) => 1 -})(error) -``` - -## Request - Batched Operations - -```typescript -class GetUser extends Request.Class {} - -const resolver = RequestResolver.makeBatched((requests: Array) => - Effect.gen(function* () { - const users = yield* fetchUsers(requests.map(r => r.id)) - return requests.map(r => users.find(u => u.id === r.id) ?? new UserNotFound()) - }) -) - -const getUser = (id: string) => Effect.request(new GetUser({ id }), resolver) -``` - -## Complete Example: Naval Fate CLI - -```typescript -// Domain model -class Ship extends Schema.Class("Ship")({ - name: Schema.String, - x: Schema.Number, - y: Schema.Number, - status: Schema.Literal("sailing", "destroyed") -}) { - static readonly create = (name: string) => - new Ship({ name, x: 0, y: 0, status: "sailing" }) - - move(x: number, y: number): Ship { - return new Ship({ ...this, x, y }) - } -} - -// Service interface -interface NavalFateStore { - createShip(name: string): Effect.Effect - moveShip(name: string, x: number, y: number): Effect.Effect -} - -const NavalFateStore = Context.GenericTag("NavalFateStore") - -// Layer implementation -const NavalFateStoreLive = Layer.effect( - NavalFateStore, - Effect.gen(function* () { - const store = yield* KeyValueStore.KeyValueStore - - const createShip = Effect.fn("createShip")(function* (name: string) { - const existing = yield* store.get(name) - if (Option.isSome(existing)) { - return yield* new ShipExistsError({ name }) - } - const ship = Ship.create(name) - yield* store.set(name, ship) - return ship - }) - - return NavalFateStore.of({ createShip, moveShip: ... }) - }) -) - -// CLI command -const newShipCommand = Command.make("new", { name: Args.text({ name: "name" }) }, ({ name }) => - Effect.gen(function* () { - yield* createShip(name) - yield* Console.log(`Created ship: '${name}'`) - }) -) - -// Run with layers -Effect.suspend(() => cli(process.argv)).pipe( - Effect.provide(MainLayer), - NodeRuntime.runMain -) -``` - -## Request Batching with Cache Control - -```typescript -// Access and manipulate request cache -const cache = yield* FiberRef.get(FiberRef.currentRequestCache) -yield* cache.invalidateAll - -// Batch requests made in parallel -const names = yield* Effect.zip(getAllUserNames, getAllUserNames, { - concurrent: true, - batching: true -}) - -// Control caching per-operation -yield* Effect.withRequestCaching(false) -yield* Effect.withRequestBatching(true) - -// Cache warming -yield* Effect.cacheRequestResult(GetAllIds({}), Exit.succeed(userIds)) -``` - -## Tagged Request Resolver - -```typescript -// Discriminated resolver for different request types -const UserResolverTagged = Resolver.fromEffectTagged()({ - GetAllIds: (reqs) => handleGetAllIds(reqs), - GetNameById: (reqs) => handleGetNameById(reqs) -}) - -// With batch size limit -const resolver = Resolver.makeBatched((requests) => ...) - .pipe(Resolver.batchN(15)) -``` - -## Stream GroupBy - -```typescript -const result = yield* pipe( - Stream.fromIterable(words), - Stream.groupByKey(identity, { bufferSize: 8192 }), - GroupBy.evaluate((key, stream) => - pipe( - Stream.runCollect(stream), - Effect.map((leftover) => [key, leftover.length] as const), - Stream.fromEffect - ) - ), - GroupBy.first(2), // Limit to first 2 groups - Stream.runCollect -) -``` - -## Stream Partition - -```typescript -const { evens, odds } = yield* pipe( - Stream.range(0, 5), - Stream.partition((n) => n % 2 === 0, { bufferSize: 1 }), - Effect.flatMap(([odds, evens]) => - Effect.all({ - evens: Stream.runCollect(evens), - odds: Stream.runCollect(odds) - }) - ), - Effect.scoped -) -``` - -## Fiber Inheritance - -```typescript -const fiberRef = yield* FiberRef.make(initial) -const child = yield* FiberRef.set(fiberRef, update).pipe(Effect.fork) - -// Inherit local state from child fiber -yield* pipe(child, Fiber.inheritAll) -const result = yield* FiberRef.get(fiberRef) // Has child's update -``` - -## STM - Software Transactional Memory - -```typescript -const transfer = ( - receiver: TRef.TRef, - sender: TRef.TRef, - amount: number -): Effect.Effect => - pipe( - TRef.get(sender), - STM.tap((balance) => STM.check(() => balance >= amount)), - STM.tap(() => TRef.update(receiver, (n) => n + amount)), - STM.tap(() => TRef.update(sender, (n) => n - amount)), - STM.zipRight(TRef.get(receiver)), - STM.commit - ) -``` - -## SubscriptionRef - Reactive State - -```typescript -const subscriptionRef = yield* SubscriptionRef.make(0) - -// Subscribe to changes as Stream -const subscriber = yield* pipe( - subscriptionRef.changes, - Stream.take(3), - Stream.runCollect, - Effect.fork -) - -yield* SubscriptionRef.update(subscriptionRef, (n) => n + 1) -``` - -## Schedule Composition - -```typescript -// Union (OR) - either schedule triggers -const mondayOrWednesday = monday.pipe(Schedule.union(wednesday)) - -// Intersect (AND) - both must trigger -const alsoWednesday = mondayOrWednesday.pipe(Schedule.intersect(wednesdayOrFriday)) - -// Reset after inactivity -const schedule = Schedule.recurs(5).pipe(Schedule.resetAfter("5 seconds")) -``` - -## Pool with Scope - -```typescript -const count = yield* Ref.make(0) -const get = Effect.acquireRelease( - Ref.updateAndGet(count, (n) => n + 1), - () => Ref.update(count, (n) => n - 1) -) - -const scope = yield* Scope.make() -yield* Scope.extend(Pool.make({ acquire: get, size: 10 }), scope) -yield* Scope.close(scope, Exit.succeed(void 0)) -``` - -## Concurrent Finalizers - -```typescript -yield* pipe( - Effect.addFinalizer(() => cleanupA), - Effect.zipRight(Effect.addFinalizer(() => cleanupB), { - concurrent: true, - concurrentFinalizers: true - }), - Effect.scoped -) -``` - -## Match - Exhaustive Pattern Matching - -```typescript -const match = M.type().pipe( - M.when(M.number, (n) => `number: ${n}`), - M.when(M.string, (s) => `string: ${s}`), - M.exhaustive -) - -// Tagged matching -const match2 = pipe( - M.type<{ _tag: "A"; a: number } | { _tag: "B"; b: number }>(), - M.when({ _tag: "A" }, (_) => _.a), - M.when({ _tag: "B" }, (_) => _.b), - M.exhaustive -) -``` - -## Experimental: Machine Actor Model - -```typescript -class SendEmail extends Request.TaggedClass("SendEmail") {} -class Shutdown extends Request.TaggedClass("Shutdown")() {} - -const mailer = Machine.makeWith>()((_, previous) => - Effect.gen(function*() { - const state = previous ?? List.empty() - return Machine.procedures.make(state).pipe( - Machine.procedures.add()("SendEmail", (ctx) => - Effect.as([void 0, List.append(ctx.state, ctx.request)]) - ), - Machine.procedures.add()("Shutdown", () => - Effect.interrupt - ) - ) - }) -).pipe(Machine.retry(Schedule.forever)) -``` - -## Experimental: Rate Limiter - -```typescript -const consume = limiter.consume({ - algorithm: "token-bucket", - onExceeded: "delay", // Block and wait - window: "10 seconds", - limit: 5, - key: "user-123" -}) -// Returns { delay, limit, remaining, resetAfter } -``` diff --git a/skills/effect-ts/references/effect-solutions.md b/skills/effect-ts/references/effect-solutions.md deleted file mode 100644 index dd3563e..0000000 --- a/skills/effect-ts/references/effect-solutions.md +++ /dev/null @@ -1,623 +0,0 @@ -# Effect Solutions Patterns - -**Source**: `~/.local/repos/kitlangton/effect-solutions` - -## Overview - -Curated Effect-TS patterns and best practices, organized by topic. - -## Effect.gen - Sequential Composition - -```typescript -const program = Effect.gen(function* () { - const data = yield* fetchData - yield* Effect.logInfo(`Processing: ${data}`) - return yield* processData(data) -}) -``` - -## Effect.fn - Named Traced Functions - -```typescript -const processUser = Effect.fn("processUser")(function* (userId: string) { - yield* Effect.logInfo(`Processing user ${userId}`) - const user = yield* getUser(userId) - return yield* processData(user) -}) - -// With second argument for cross-cutting concerns -const processUser = Effect.fn("processUser", { - timeout: "5 seconds", - retry: Schedule.exponential("100 millis"), -})(function* (userId: string) { - // ... -}) -``` - -## Pipe - Instrumentation - -```typescript -const program = fetchData.pipe( - Effect.timeout("5 seconds"), - Effect.retry( - Schedule.exponential("100 millis").pipe( - Schedule.compose(Schedule.recurs(3)) - ) - ), - Effect.tap((data) => Effect.logInfo(`Fetched: ${data}`)), - Effect.withSpan("fetchData") -) -``` - -## Service Definition - -```typescript -class Database extends Context.Tag("@app/Database")< - Database, - { - readonly query: (sql: string) => Effect.Effect - readonly execute: (sql: string) => Effect.Effect - } ->() {} -``` - -**Rules:** - -- Tag identifiers unique (`@path/ServiceName`) -- Methods have no deps (`R = never`) -- Use `readonly` - -## Layer Implementation - -```typescript -class Users extends Context.Tag("@app/Users")< - Users, - { readonly findById: (id: UserId) => Effect.Effect } ->() { - static readonly layer = Layer.effect( - Users, - Effect.gen(function* () { - const http = yield* HttpClient.HttpClient - - const findById = Effect.fn("Users.findById")(function* (id: UserId) { - const response = yield* http.get(`/users/${id}`) - return yield* HttpClientResponse.schemaBodyJson(User)(response) - }) - - return Users.of({ findById }) - }) - ) -} -``` - -## Test Layer - -```typescript -class Database extends Context.Tag("@app/Database")() { - static readonly testLayer = Layer.sync(Database, () => { - const store = new Map() - return Database.of({ - query: (sql) => Effect.succeed([...store.values()]), - execute: (sql) => Effect.sync(() => void store.clear()) - }) - }) -} -``` - -## Layer Composition - -```typescript -const appLayer = userServiceLayer.pipe( - Layer.provideMerge(databaseLayer), - Layer.provideMerge(configLayer) -) - -const main = program.pipe(Effect.provide(appLayer)) -``` - -## Layer Memoization - -Store parameterized layers in constants: - -```typescript -// Bad: creates two pools -Layer.provide(Postgres.layer({ poolSize: 10 })) -Layer.provide(Postgres.layer({ poolSize: 10 })) - -// Good: single pool -const postgresLayer = Postgres.layer({ poolSize: 10 }) -Layer.provide(postgresLayer) -``` - -## Schema.TaggedError - -```typescript -class ValidationError extends Schema.TaggedError()( - "ValidationError", - { - field: Schema.String, - message: Schema.String, - } -) {} - -class NotFoundError extends Schema.TaggedError()( - "NotFoundError", - { - resource: Schema.String, - id: Schema.String, - } -) {} - -// Yieldable - no Effect.fail() needed -return status === 404 - ? NotFoundError.make({ resource: "user", id }) - : Effect.die(error) -``` - -## Error Recovery - -```typescript -// Single tag -program.pipe( - Effect.catchTag("HttpError", (e) => Effect.succeed("recovered")) -) - -// Multiple tags -program.pipe( - Effect.catchTags({ - HttpError: () => Effect.succeed("http"), - ValidationError: () => Effect.succeed("validation") - }) -) - -// All errors -program.pipe( - Effect.catchAll((e) => Effect.succeed("recovered")) -) -``` - -## Typed Errors vs Defects - -- **Typed errors**: Domain failures caller can handle (validation, not found) -- **Defects**: Unrecoverable bugs, invariant violations - -```typescript -const config = yield* loadConfig.pipe(Effect.orDie) -``` - -## Schema.Defect - Wrap Unknown - -```typescript -class ApiError extends Schema.TaggedError()( - "ApiError", - { - endpoint: Schema.String, - error: Schema.Defect - } -) {} -``` - -## Branded Types - -Brand all domain primitives: - -```typescript -const UserId = Schema.String.pipe(Schema.brand("UserId")) -const PostId = Schema.String.pipe(Schema.brand("PostId")) -const Email = Schema.String.pipe(Schema.brand("Email")) -const Port = Schema.Int.pipe( - Schema.between(1, 65535), - Schema.brand("Port") -) -``` - -## Schema.Class - Records - -```typescript -const UserId = Schema.String.pipe(Schema.brand("UserId")) -type UserId = typeof UserId.Type - -class User extends Schema.Class("User")({ - id: UserId, - name: Schema.String, - email: Schema.String, -}) { - get displayName() { - return `${this.name} (${this.email})` - } -} -``` - -## Schema.TaggedClass - Variants - -```typescript -class Success extends Schema.TaggedClass()("Success", { - value: Schema.Number, -}) {} - -class Failure extends Schema.TaggedClass()("Failure", { - error: Schema.String, -}) {} - -const Result = Schema.Union(Success, Failure) - -Match.valueTags(result, { - Success: ({ value }) => `Got: ${value}`, - Failure: ({ error }) => `Error: ${error}` -}) -``` - -## JSON Parsing - -```typescript -const MoveFromJson = Schema.parseJson(Move) -const move = yield* Schema.decodeUnknown(MoveFromJson)(jsonString) -const json = yield* Schema.encode(MoveFromJson)(move) -``` - -## Config Primitives - -```typescript -Config.string("VAR") -Config.integer("PORT") -Config.boolean("DEBUG") -Config.redacted("SECRET") // hidden in logs -Config.url("URL") -Config.duration("TIMEOUT") -Config.array(Config.string(), "TAGS") -``` - -## Config Service Pattern - -```typescript -class ApiConfig extends Context.Tag("@app/ApiConfig")< - ApiConfig, - { readonly apiKey: Redacted.Redacted; readonly baseUrl: string } ->() { - static readonly layer = Layer.effect( - ApiConfig, - Effect.gen(function* () { - const apiKey = yield* Config.redacted("API_KEY") - const baseUrl = yield* Config.string("API_BASE_URL").pipe( - Config.orElse(() => Config.succeed("https://api.example.com")) - ) - return ApiConfig.of({ apiKey, baseUrl }) - }) - ) -} -``` - -## Schema.Config - Validation - -```typescript -const Port = Schema.Int.pipe(Schema.between(1, 65535), Schema.brand("Port")) -const port = yield* Schema.Config("PORT", Port) -``` - -## Testing Setup - -```bash -bun add -D vitest @effect/vitest -``` - -```typescript -// vitest.config.ts -export default defineConfig({ test: { include: ["tests/**/*.test.ts"] } }) -``` - -## Basic Test - -```typescript -import { describe, expect, it } from "@effect/vitest" - -describe("Calculator", () => { - it.effect("adds numbers", () => - Effect.gen(function* () { - const result = yield* Effect.succeed(1 + 1) - expect(result).toBe(2) - }) - ) -}) -``` - -## Test Variants - -- `it.effect()` - Effect tests (most common) -- `it.scoped()` - Scoped resources, auto cleanup -- `it.live()` - Real clock (no TestClock) - -## TestClock - -```typescript -it.effect("time test", () => - Effect.gen(function* () { - const fiber = yield* Effect.delay(Effect.succeed("done"), "10 seconds").pipe(Effect.fork) - yield* TestClock.adjust("10 seconds") - expect(yield* Fiber.join(fiber)).toBe("done") - }) -) -``` - -## Per-Test Layers - -```typescript -const testLayer = Events.layer.pipe( - Layer.provideMerge(Users.testLayer), - Layer.provideMerge(Tickets.testLayer) -) - -it.effect("test", () => - Effect.gen(function* () { - const events = yield* Events - // ... - }).pipe(Effect.provide(testLayer)) -) -``` - -## Test Modifiers - -```typescript -it.effect.skip("disabled", () => ...) -it.effect.only("focus", () => ...) -it.effect.fails("expected fail", () => ...) -``` - -## Service `use` Pattern - -For wrapping Promise-based libraries: - -```typescript -class FileSystem extends Effect.Service()("FileSystem", { - effect: Effect.gen(function* () { - const use =
( - fn: (fs: typeof nodeFs, signal: AbortSignal) => Promise - ): Effect.Effect => - Effect.tryPromise({ - try: (signal) => fn(nodeFs, signal), - catch: (cause) => new FileSystemError({ cause }), - }) - - return { use } as const - }), -}) {} - -// Usage -const content = yield* fileSystem.use((fs, signal) => - fs.readFile("config.json", { encoding: "utf-8", signal }) -) -``` - -## Real Service Example - -```typescript -export class BrowserService extends Context.Tag("@cli/BrowserService")< - BrowserService, - { - readonly open: (url: string) => Effect.Effect - } ->() { - // Production layer - static readonly layer = Layer.sync(BrowserService, () => { - const open = Effect.fn("BrowserService.open")((url: string) => - Effect.try({ - try: () => { - const platform = process.platform - const command = - platform === "darwin" ? ["open", url] : - platform === "win32" ? ["cmd", "/c", "start", "", url] : - ["xdg-open", url] - spawn(command, { stdout: "ignore", stderr: "ignore" }) - }, - catch: (error) => BrowserOpenError.make({ url, cause: error }), - }) - ) - return BrowserService.of({ open }) - }) - - // Test layer - static readonly testLayer = Layer.sync(BrowserService, () => { - const urls: string[] = [] - const open = Effect.fn("BrowserService.open.test")((url: string) => - Effect.sync(() => { urls.push(url) }) - ) - return BrowserService.of({ open }) - }) - - // No-op layer - static readonly noopLayer = Layer.sync(BrowserService, () => { - const open = Effect.fn("BrowserService.open.noop")( - (_url: string) => Effect.void - ) - return BrowserService.of({ open }) - }) -} -``` - -## CLI Entry Pattern - -```typescript -const MainLayer = UpdateNotifier.layer.pipe( - Layer.provide(UpdateNotifierConfig.layer), - Layer.merge(IssueService.layer.pipe(Layer.provide(BrowserService.layer))), - Layer.provideMerge(BunContext.layer), -) - -if (import.meta.main) { - pipe( - Effect.gen(function* () { - const notifier = yield* UpdateNotifier - yield* notifier.check(CLI_NAME, CLI_VERSION) - yield* runCli(process.argv) - }), - Effect.provide(MainLayer), - Effect.tapErrorCause((cause) => Console.error(pc.red(`Error: ${cause}`))), - Effect.catchAll(() => Effect.sync(() => process.exit(1))), - BunRuntime.runMain, - ) -} -``` - -## Service `use` Pattern - -For wrapping Promise-based libraries (Prisma, Drizzle, AWS SDK): - -```typescript -class FileSystem extends Effect.Service()("FileSystem", { - effect: Effect.gen(function* () { - const use = ( - fn: (fs: typeof nodeFs, signal: AbortSignal) => Promise - ): Effect.Effect => - Effect.tryPromise({ - try: (signal) => fn(nodeFs, signal), - catch: (cause) => new FileSystemError({ cause }), - }) - - return { use } as const - }), -}) {} - -// Usage -const content = yield* fileSystem.use((fs, signal) => - fs.readFile("config.json", { encoding: "utf-8", signal }) -) -``` - -## Real Service Example with Layer Variants - -```typescript -export class BrowserService extends Context.Tag("@cli/BrowserService")< - BrowserService, - { - readonly open: (url: string) => Effect.Effect - } ->() { - // Production layer - static readonly layer = Layer.sync(BrowserService, () => { - const open = Effect.fn("BrowserService.open")((url: string) => - Effect.try({ - try: () => { - const platform = process.platform - const command = - platform === "darwin" ? ["open", url] : - platform === "win32" ? ["cmd", "/c", "start", "", url] : - ["xdg-open", url] - spawn(command, { stdout: "ignore", stderr: "ignore" }) - }, - catch: (error) => BrowserOpenError.make({ url, cause: error }), - }) - ) - return BrowserService.of({ open }) - }) - - // Test layer (captures calls) - static readonly testLayer = Layer.sync(BrowserService, () => { - const urls: string[] = [] - const open = Effect.fn("BrowserService.open.test")((url: string) => - Effect.sync(() => { urls.push(url) }) - ) - return BrowserService.of({ open }) - }) - - // No-op layer (disabled feature) - static readonly noopLayer = Layer.sync(BrowserService, () => { - const open = Effect.fn("BrowserService.open.noop")( - (_url: string) => Effect.void - ) - return BrowserService.of({ open }) - }) -} -``` - -## Config Service with Internal Effects - -```typescript -export class UpdateNotifier extends Context.Tag("@cli/UpdateNotifier")< - UpdateNotifier, - { - readonly check: (pkgName: string, version: string) => Effect.Effect - } ->() { - static readonly layer = Layer.effect( - UpdateNotifier, - Effect.gen(function* () { - const config = yield* UpdateNotifierConfig - - // Named internal effects for tracing - const readCache = Effect.fn("UpdateNotifier.readCache")( - (file: string) => - Effect.tryPromise({ - try: () => readFile(file, "utf8"), - catch: () => null, - }) - ) - - const fetchLatest = Effect.fn("UpdateNotifier.fetchLatest")( - (pkgName: string) => - Effect.tryPromise({ - try: async () => { - const controller = new AbortController() - setTimeout(() => controller.abort(), config.timeout) - const res = await fetch( - `https://registry.npmjs.org/${pkgName}/latest`, - { signal: controller.signal } - ) - return res.ok ? Option.some((await res.json()).version) : Option.none() - }, - catch: () => Option.none(), - }) - ) - - const check = Effect.fn("UpdateNotifier.check")( - (pkgName: string, currentVersion: string) => - Effect.gen(function* () { - if (config.isCi) return - const cache = yield* readCache(cachePath(pkgName)) - // ... check logic - }) - ) - - return UpdateNotifier.of({ check }) - }), - ) - - static readonly testLayer = Layer.succeed( - UpdateNotifier, - UpdateNotifier.of({ check: () => Effect.void }), - ) -} -``` - -## CLI Entry Pattern - -```typescript -const MainLayer = UpdateNotifier.layer.pipe( - Layer.provide(UpdateNotifierConfig.layer), - Layer.merge(IssueService.layer.pipe(Layer.provide(BrowserService.layer))), - Layer.provideMerge(BunContext.layer), -) - -if (import.meta.main) { - pipe( - Effect.gen(function* () { - const notifier = yield* UpdateNotifier - yield* notifier.check(CLI_NAME, CLI_VERSION) - yield* runCli(process.argv) - }), - Effect.provide(MainLayer), - Effect.tapErrorCause((cause) => Console.error(pc.red(`Error: ${cause}`))), - Effect.catchAll(() => Effect.sync(() => process.exit(1))), - BunRuntime.runMain, - ) -} -``` - -## Best Practices Summary - -1. **Service-Driven Development**: Define contracts first, implement later -2. **Dependency Injection**: Use Context.Tag + Layer, provide once at top -3. **Layer Memoization**: Store parameterized layers in constants -4. **Typed Errors**: Use Schema.TaggedError for domain errors -5. **Brand Everything**: IDs, emails, URLs, counts - all primitives -6. **Per-Test Layers**: Fresh layer in each test, prevent state leakage -7. **Three Layer Variants**: .layer (prod), .testLayer (test), .noopLayer (disabled) -8. **Service `use` Pattern**: Wrap Promise-based libraries for Effect integration diff --git a/skills/effect-ts/references/lucas-barake.md b/skills/effect-ts/references/lucas-barake.md deleted file mode 100644 index 8c72d20..0000000 --- a/skills/effect-ts/references/lucas-barake.md +++ /dev/null @@ -1,642 +0,0 @@ -# Lucas Barake Full-Stack Patterns - -**Source**: `~/.local/repos/lucas-barake/effect-tanstack-start`, `~/.local/repos/lucas-barake/building-an-app-with-effect`, `~/.local/repos/lucas-barake/effect-monorepo` - -## Project Structures - -### effect-tanstack-start (Simple Full-stack) - -``` -src/ -├── api/ -│ ├── domain-rpc.ts # RPC contract definition -│ ├── domain-api.ts # HTTP API definition -│ ├── api-client.ts # Client-side API client -│ └── todo-schema.ts # Schemas and errors -├── routes/api/ -│ ├── $.ts # Catch-all API handler -│ └── -lib/ -│ ├── todos-service.ts # Service implementation -│ ├── todos-api-live.ts # HTTP handler -│ └── todos-rpc-live.ts # RPC handler -└── lib/ - └── atom-utils.ts # Effect-Atom integration -``` - -### effect-monorepo (Enterprise) - -``` -packages/ -├── domain/ -│ ├── EntityIds.ts # Branded IDs -│ ├── CustomHttpApiError.ts # All HTTP errors -│ ├── ManualCache.ts # Custom caching -│ └── styles-rpc.ts # RPC contracts -├── database/ -│ └── Database.ts # Connection pool, transactions -├── server/ -│ ├── server.ts # Entry point -│ └── public/ -│ ├── todos/ -│ │ ├── todos-repository.ts -│ │ └── todos-live.ts -│ ├── middlewares/ -│ │ └── auth-middleware-live.ts -│ └── sse/ -│ └── sse-manager.ts -└── client/ -``` - -## Service Definition (Effect.Service) - -```typescript -export class TodosService extends Effect.Service()( - "TodosService", - { - effect: Effect.gen(function* () { - const todosRef = yield* Ref.make>(new Map()) - - const list = Effect.gen(function* () { - const todos = yield* Ref.get(todosRef) - return Array.from(todos.values()) - }) - - const getById = (id: TodoId) => - Effect.gen(function* () { - const todos = yield* Ref.get(todosRef) - const todo = todos.get(id) - if (!todo) return yield* new TodoNotFound({ id }) - return todo - }) - - const create = (title: string) => - Effect.gen(function* () { - const id = TodoId.make(crypto.randomUUID()) - const todo: Todo = { id, title, completed: false } - yield* Ref.update(todosRef, (todos) => new Map(todos).set(id, todo)) - return todo - }) - - const update = (id: TodoId, data: Partial) => - Effect.gen(function* () { - const existing = yield* getById(id) - const updated = { ...existing, ...data } - yield* Ref.update(todosRef, (todos) => new Map(todos).set(id, updated)) - return updated - }) - - const remove = (id: TodoId) => - Effect.gen(function* () { - yield* getById(id) // Throws if not found - yield* Ref.update(todosRef, (todos) => { - const next = new Map(todos) - next.delete(id) - return next - }) - }) - - return { list, getById, create, update, remove } as const - }), - }, -) {} -``` - -## RPC Contract - -```typescript -export const DomainRpc = RpcGroup.make("domain").pipe( - RpcGroup.add( - Rpc.effect("listTodos", { - success: Schema.Array(TodoSchema), - }), - ), - RpcGroup.add( - Rpc.effect("createTodo", { - payload: Schema.Struct({ title: Schema.String }), - success: TodoSchema, - }), - ), - RpcGroup.add( - Rpc.effect("updateTodo", { - payload: Schema.Struct({ - id: TodoId, - title: Schema.optional(Schema.String), - completed: Schema.optional(Schema.Boolean), - }), - success: TodoSchema, - error: TodoNotFound, - }), - ), - RpcGroup.add( - Rpc.effect("deleteTodo", { - payload: Schema.Struct({ id: TodoId }), - success: Schema.Void, - error: TodoNotFound, - }), - ), -) -``` - -## HTTP API Definition - -```typescript -export const DomainApi = HttpApi.make("domain").pipe( - HttpApi.addGroup( - HttpApiGroup.make("todos").pipe( - HttpApiGroup.add( - HttpApiEndpoint.get("list", "/").pipe( - HttpApiEndpoint.addSuccess(Schema.Array(TodoSchema)), - ), - ), - HttpApiGroup.add( - HttpApiEndpoint.post("create", "/").pipe( - HttpApiEndpoint.setPayload(Schema.Struct({ title: Schema.String })), - HttpApiEndpoint.addSuccess(TodoSchema), - ), - ), - HttpApiGroup.add( - HttpApiEndpoint.patch("update", "/:id").pipe( - HttpApiEndpoint.setPath(Schema.Struct({ id: TodoId })), - HttpApiEndpoint.setPayload(Schema.Struct({ - title: Schema.optional(Schema.String), - completed: Schema.optional(Schema.Boolean), - })), - HttpApiEndpoint.addSuccess(TodoSchema), - HttpApiEndpoint.addError(TodoNotFound), - ), - ), - HttpApiGroup.prefix("/todos"), - ), - ), -) -``` - -## Dual Protocol Handler (RPC + HTTP) - -```typescript -// routes/api/$.ts -import { createAPIFileRoute } from "@tanstack/react-start/api" - -const RpcLive = RpcServer.layer(DomainRpc, TodosRpcLive) -const HttpLive = HttpLayerRouter.addHttpApi(DomainApi).pipe( - Layer.provide(TodosApiLive), -) - -const AllRoutes = Layer.mergeAll( - HttpLive, - RpcLive.pipe(Layer.provide(RpcMiddlewareLive)), -) - -const ServeLive = HttpLayerRouter.serve(AllRoutes) - -// ManagedRuntime for server functions -let runtime: ManagedRuntime.ManagedRuntime | null = null - -const getRuntime = () => { - if (runtime === null) { - runtime = ManagedRuntime.make( - ServeLive.pipe(Layer.provide(TodosService.Default)) - ) - } - return runtime -} - -// HMR cleanup -if (import.meta.hot) { - import.meta.hot.dispose(() => { - if (runtime !== null) { - Effect.runFork(runtime.dispose()) - runtime = null - } - }) -} - -export const APIRoute = createAPIFileRoute("/api/$")({ - GET: ({ request }) => getRuntime().runPromise(HttpLayerRouter.toWebHandler(request)), - POST: ({ request }) => getRuntime().runPromise(HttpLayerRouter.toWebHandler(request)), - PATCH: ({ request }) => getRuntime().runPromise(HttpLayerRouter.toWebHandler(request)), - DELETE: ({ request }) => getRuntime().runPromise(HttpLayerRouter.toWebHandler(request)), -}) -``` - -## Schema with HTTP Annotations - -```typescript -export const TodoId = Schema.String.pipe(Schema.brand("TodoId")) -export type TodoId = typeof TodoId.Type - -export const TodoSchema = Schema.Struct({ - id: TodoId, - title: Schema.String, - completed: Schema.Boolean, -}) -export type Todo = typeof TodoSchema.Type - -export class TodoNotFound extends Schema.TaggedError()( - "TodoNotFound", - { id: TodoId }, - HttpApiSchema.annotations({ status: 404 }), -) {} -``` - -## Comprehensive HTTP Errors - -```typescript -// CustomHttpApiError.ts - All HTTP error types - -// 4xx Client Errors -export class BadRequest extends S.TaggedError()( - "BadRequest", - { message: S.optional(S.String) }, - HttpApiSchema.annotations({ status: 400 }), -) {} - -export class Unauthorized extends S.TaggedError()( - "Unauthorized", - { message: S.optional(S.String) }, - HttpApiSchema.annotations({ status: 401 }), -) {} - -export class Forbidden extends S.TaggedError()( - "Forbidden", - { message: S.optional(S.String) }, - HttpApiSchema.annotations({ status: 403 }), -) {} - -export class NotFound extends S.TaggedError()( - "NotFound", - { message: S.optional(S.String) }, - HttpApiSchema.annotations({ status: 404 }), -) {} - -// 5xx Server Errors -export class InternalServerError extends S.TaggedError()( - "InternalServerError", - { message: S.optional(S.String) }, - HttpApiSchema.annotations({ status: 500 }), -) {} - -export class ServiceUnavailable extends S.TaggedError()( - "ServiceUnavailable", - { message: S.optional(S.String) }, - HttpApiSchema.annotations({ status: 503 }), -) {} - -// ~350 lines covering all HTTP status codes -``` - -## Repository Pattern - -```typescript -export class TodosRepository extends Effect.Service()( - "TodosRepository", - { - effect: Effect.gen(function* () { - const database = yield* Database - - const create = (title: string) => - database.use((db) => - db.insert(todos).values({ title }).returning() - ).pipe( - Effect.map((rows) => rows[0]), - Effect.withSpan("TodosRepository.create"), - ) - - const findById = (id: TodoId) => - database.use((db) => - db.select().from(todos).where(eq(todos.id, id)) - ).pipe( - Effect.flatMap((rows) => - rows.length === 0 - ? Effect.fail(new TodoNotFound({ id })) - : Effect.succeed(rows[0]) - ), - Effect.withSpan("TodosRepository.findById"), - ) - - const update = (id: TodoId, data: Partial) => - database.use((db) => - db.update(todos).set(data).where(eq(todos.id, id)).returning() - ).pipe( - Effect.flatMap((rows) => - rows.length === 0 - ? Effect.fail(new TodoNotFound({ id })) - : Effect.succeed(rows[0]) - ), - Effect.withSpan("TodosRepository.update"), - ) - - return { create, findById, update, delete: remove } as const - }), - }, -) {} -``` - -## Database Service - -```typescript -export class Database extends Context.Tag("Database")< - Database, - { - use: ( - fn: (db: DrizzleClient) => Promise - ) => Effect.Effect - } ->() { - static provide = (effect: Effect.Effect) => - Effect.provide(effect, DatabaseLive) -} - -const DatabaseLive = Layer.scoped( - Database, - Effect.gen(function* () { - const pool = yield* Effect.acquireRelease( - Effect.sync(() => new Pool({ connectionString: process.env.DATABASE_URL })), - (pool) => Effect.promise(() => pool.end()) - ) - - const db = drizzle(pool) - - return { - use: (fn) => - Effect.tryPromise({ - try: () => fn(db), - catch: (error) => { - if (error instanceof DatabaseError) { - // Map PostgreSQL error codes - if (error.code === "23505") { - return new UniqueViolation({ message: error.message }) - } - } - return new DatabaseError({ cause: error }) - }, - }), - } - }), -) -``` - -## Authentication Middleware - -```typescript -export const AuthMiddlewareLive = Layer.effect( - AuthMiddleware, - Effect.gen(function* () { - return { - bearer: (token) => - Effect.gen(function* () { - const decoded = yield* verifyToken(token).pipe( - Effect.mapError(() => new Unauthorized({ message: "Invalid token" })) - ) - - return CurrentUserSchema.make({ - userId: decoded.sub, - email: decoded.email, - permissions: decoded.permissions, - }) - }), - } - }), -) -``` - -## SSE Manager - -```typescript -export class SseManager extends Effect.Service()( - "SseManager", - { - effect: Effect.gen(function* () { - const connectionsRef = yield* Ref.make(MutableHashMap.empty()) - - const connect = (userId: string) => - Effect.gen(function* () { - const queue = yield* Queue.unbounded() - const connection: Connection = { userId, queue } - - yield* Ref.update(connectionsRef, (map) => { - MutableHashMap.set(map, userId, connection) - return map - }) - - return { - stream: Stream.fromQueue(queue).pipe( - Stream.map((event) => ServerSentEvent.encode(event)), - ), - close: Effect.gen(function* () { - yield* Ref.update(connectionsRef, (map) => { - MutableHashMap.remove(map, userId) - return map - }) - yield* Queue.shutdown(queue) - }), - } - }) - - const broadcast = (event: ServerSentEvent) => - Effect.gen(function* () { - const connections = yield* Ref.get(connectionsRef) - yield* Effect.forEach( - MutableHashMap.values(connections), - (conn) => Queue.offer(conn.queue, event), - { concurrency: "unbounded" } - ) - }) - - const sendTo = (userId: string, event: ServerSentEvent) => - Effect.gen(function* () { - const connections = yield* Ref.get(connectionsRef) - const connection = MutableHashMap.get(connections, userId) - if (Option.isSome(connection)) { - yield* Queue.offer(connection.value.queue, event) - } - }) - - return { connect, broadcast, sendTo } as const - }), - }, -) {} -``` - -## Manual Cache - -```typescript -export class ManualCache { - private cache = new Map() - private stats = { hits: 0, misses: 0 } - - constructor( - private readonly options: { - maxSize: number - ttlMs: number - } - ) {} - - get(key: K): Option.Option { - const entry = this.cache.get(key) - if (!entry) { - this.stats.misses++ - return Option.none() - } - if (Date.now() > entry.expiresAt) { - this.cache.delete(key) - this.stats.misses++ - return Option.none() - } - this.stats.hits++ - return Option.some(entry.value) - } - - set(key: K, value: V): void { - if (this.cache.size >= this.options.maxSize) { - // LRU eviction - const firstKey = this.cache.keys().next().value - if (firstKey) this.cache.delete(firstKey) - } - this.cache.set(key, { - value, - expiresAt: Date.now() + this.options.ttlMs, - }) - } - - invalidate(key: K): void { - this.cache.delete(key) - } -} -``` - -## API Client - -```typescript -export const ApiClient = Effect.gen(function* () { - const httpClient = yield* HttpClient.HttpClient - - // RPC client - const rpcClient = RpcClient.make(DomainRpc, { - fetch: (request) => - httpClient.execute( - HttpClientRequest.post("/api/rpc").pipe( - HttpClientRequest.setBody(request.body), - ) - ), - }) - - // HTTP client - const httpApiClient = yield* HttpApiClient.make(DomainApi, { - baseUrl: "/api", - transformClient: HttpClient.filterStatusOk, - }) - - return { - rpc: rpcClient, - http: httpApiClient, - } as const -}) -``` - -## Effect-Atom Integration - -```typescript -import { Atom } from "@effect-atom/atom" -import { Schema } from "effect" - -export const createSerializableAtom = ( - key: string, - schema: Schema.Schema, - initial: A -) => - Atom.state(initial).pipe( - Atom.serializable({ key, schema }) - ) - -// Usage -const todosAtom = createSerializableAtom( - "todos", - Schema.Array(TodoSchema), - [] -) -``` - -## Server Entry Point - -```typescript -const MainLayer = Layer.mergeAll( - TodosLive, - AuthMiddlewareLive, - SseManager.Default, -).pipe( - Layer.provide(TodosRepository.Default), - Layer.provide(DatabaseLive), -) - -const ServerLive = HttpLayerRouter.serve(AllRoutes).pipe( - Layer.provide(MainLayer), - Layer.provide(NodeHttpServer.layer({ port: 3000 })), -) - -Effect.gen(function* () { - yield* Effect.logInfo("Starting server...") - yield* Layer.launch(ServerLive) -}).pipe( - Effect.retry({ - while: (e) => e._tag === "DatabaseConnectionError", - schedule: Schedule.exponential("1 second").pipe( - Schedule.compose(Schedule.recurs(5)) - ), - }), - NodeRuntime.runMain, -) -``` - -## Schema Utilities - -```typescript -// Duration from seconds -export const DurationFromSeconds = Schema.transform( - Schema.Number, - Schema.DurationFromMillis, - { - decode: (seconds) => seconds * 1000, - encode: (millis) => millis / 1000, - } -) - -// Email validation -export const Email = Schema.String.pipe( - Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/), - Schema.brand("Email") -) - -// URL validation -export const URLString = Schema.String.pipe( - Schema.filter((s) => { - try { - new URL(s) - return true - } catch { - return false - } - }), - Schema.brand("URLString") -) - -// Fallible array (logs errors, returns valid items) -export const FallibleArray = (schema: Schema.Schema) => - Schema.Array(Schema.Unknown).pipe( - Schema.transform( - Schema.Array(schema), - { - decode: (items) => - items.flatMap((item) => { - const result = Schema.decodeUnknownEither(schema)(item) - if (Either.isLeft(result)) { - console.warn("Failed to decode item:", result.left) - return [] - } - return [result.right] - }), - encode: (items) => items, - } - ) - ) -``` diff --git a/skills/effect-ts/references/setup.md b/skills/effect-ts/references/setup.md deleted file mode 100644 index 2253eb8..0000000 --- a/skills/effect-ts/references/setup.md +++ /dev/null @@ -1,140 +0,0 @@ -You are an Effect TypeScript setup guide. Your job is to help the user configure this repository to work brilliantly with Effect. - -## **Tools** - -- **Todo list**: If available, use it to track progress. Create checklist at start, update as you complete steps. If no todo tool: show markdown checklist ONCE at start. -- **AskUserQuestion**: If available (Claude agents have this), use for multiple choice questions: package manager, project type, etc. - -**Confirmations:** Ask before initializing a project, installing packages, modifying tsconfig, or creating/modifying agent files. - -## **Before Starting** - -1. Introduce yourself as their Effect setup guide -2. Assess repository with a single command: - ```bash - ls -la package.json tsconfig.json bun.lock pnpm-lock.yaml package-lock.json .vscode AGENTS.md CLAUDE.md .claude .cursorrules 2>/dev/null; file AGENTS.md CLAUDE.md 2>/dev/null | grep -i link - ``` - This finds all relevant files and detects symlinks. From lock file, determine package manager (bun/pnpm/npm). If multiple lock files, ask which to use. If none, ask preference. -3. Check Effect Solutions CLI: run `effect-solutions list`. If missing, install (using package name `effect-solutions`). If output shows update available, update before continuing. -4. Create todo list (if you have the tool) - -**Checklist:** - -- [ ] Initialize project (if needed) -- [ ] Install Effect dependencies -- [ ] Effect Language Service setup -- [ ] TypeScript compiler configuration -- [ ] Package scripts -- [ ] Agent instruction files -- [ ] Clone Effect source -- [ ] Summary - ---- - -## Initialize Project (if needed) - -**Only if `package.json` doesn't exist:** - -- Read: `effect-solutions show project-setup` -- Follow initialization guidance -- Run: `[bun/pnpm/npm] init` - ---- - -## Install Effect Dependencies - -- Check if Effect is already in dependencies -- Determine packages based on project type: - - Always: `effect` - - CLI apps: `@effect/cli` - - HTTP servers/clients: `@effect/platform` -- Schema lives in `effect/Schema`; do not install `@effect/schema` (deprecated since Effect 3.10) -- Run: `[bun/pnpm/npm] add effect [...]` -- **Don't specify version** - use latest - ---- - -## Effect Language Service Setup - -This adds compile-time diagnostics for Effect: catches pipeline errors, validates service requirements. - -- Read: `effect-solutions show project-setup` -- Follow setup instructions: install package, configure tsconfig plugin, add prepare script, run patch - -**VS Code/Cursor Settings:** - -- If `.vscode` exists: set up settings automatically -- If not: ask if they use VS Code or Cursor, then create settings - ---- - -## TypeScript Compiler Configuration - -This configures compiler options (separate from the language service plugin above). - -- Read: `effect-solutions show tsconfig` -- Compare recommended settings with existing `tsconfig.json` -- Apply recommended settings - ---- - -## Package Scripts - -Check if `package.json` already has a typecheck script (e.g., `typecheck`, `check`, `type-check`). If not, add one for CLI type checking (CI, git hooks, etc.): - -- Simple projects: `"typecheck": "tsc --noEmit"` -- Monorepos with project references: `"typecheck": "tsc --build --noEmit"` - ---- - -## Agent Instruction Files - -These tell AI assistants about project tools. - -- Assess existing files: - - Both `CLAUDE.md` and `AGENTS.md` (not symlinked): update both - - One exists: update it, optionally create symlinked alternative - - Neither: create `CLAUDE.md` and symlink `AGENTS.md` to it - - One is symlink: update main file -- Insert between `` and ``: - -```markdown -## Effect Best Practices - -**Before implementing Effect features**, run `effect-solutions list` and read the relevant guide. - -Topics include: services and layers, data modeling, error handling, configuration, testing, HTTP clients, CLIs, observability, and project structure. - -**Effect Source Reference:** `~/.local/share/effect-solutions/effect` -Search here for real implementations when docs aren't enough. -``` - ---- - -## Clone Effect Source - -- Check if `~/.local/share/effect-solutions/effect` exists - -**If exists:** - -- Inform them it's already cloned -- Run `git pull` to get latest changes -- If pull fails: explain issue, ask about re-cloning - -**If doesn't exist:** - -- Ask if they want to clone (provides local implementation examples and API details) -- If yes: clone `https://github.com/Effect-TS/effect.git` to `~/.local/share/effect-solutions/effect` - ---- - -## Summary - -Provide summary: - -- Package manager -- Steps completed vs skipped (with reasons) -- Files created/modified -- Any errors encountered and how they were resolved - -Offer to help explore Effect Solutions topics or start working with Effect patterns. diff --git a/skills/effect-ts/references/teamwarp-api.md b/skills/effect-ts/references/teamwarp-api.md deleted file mode 100644 index 36ff379..0000000 --- a/skills/effect-ts/references/teamwarp-api.md +++ /dev/null @@ -1,369 +0,0 @@ -# TeamWarp API Patterns - -**Source**: `~/.local/repos/TeamWarp/effect-api-example` - -## Project Structure - -``` -apps/server/src/ -├── main.ts # Entry point with layer composition -├── api/groups/ # Endpoint implementations (Live) -└── db/ # Database layer - -packages/api/src/definition/ -├── WarpApi.ts # Main API composition -├── groups/ # Endpoint schemas (no implementation) -├── middleware/ # Middleware definitions -└── Pagination.ts # Shared utilities - -packages/shared/src/ # Branded types, validation schemas -``` - -## API Definition (Schema-first) - -### Main API - -```typescript -export class WarpApi extends HttpApi.make('WarpApi') - .add(Groups.HealthGroup) - .add(Groups.EmployeesGroup) - .prefix('/v1') -{} -``` - -### Simple Group - -```typescript -export const HealthGroup = HttpApiGroup.make('health').add( - HttpApiEndpoint.get('healthCheck', '/health').addSuccess( - S.Struct({ - status: S.Literal('ok'), - timestamp: S.DateTimeUtc, - version: S.String, - }), - ), -) -``` - -### Complex Group with Errors & Middleware - -```typescript -// Query parameters with schema composition -const ListEmployeesUrlParamsSchema = S.Struct({ - ...Pagination.ApiQueryParams(EmployeeTagSchema).fields, - types: S.optional(S.Array(EmployeeTypeSchema)), -}) - -// Tagged error with HTTP status -export class EmployeeNotFoundError extends S.TaggedError()( - 'EmployeeNotFoundError', - { - id: EmployeeTagSchema, - message: S.String, - }, - HttpApiSchema.annotations({ status: 404 }), -) {} - -// Group with endpoints, errors, middleware -export const EmployeesGroup = HttpApiGroup.make('Employees') - .add( - HttpApiEndpoint.get('list', '/') - .setUrlParams(ListEmployeesUrlParamsSchema) - .addSuccess(Pagination.ApiResponse(PublicEmployeeSchema)), - ) - .add( - HttpApiEndpoint.get('get', '/:id') - .setPath(S.Struct({ id: EmployeeTagSchema })) - .addSuccess(PublicEmployeeSchema) - .addError(EmployeeNotFoundError, { status: 404 }), - ) - .addError(HttpApiError.InternalServerError, { status: 500 }) - .middleware(ApiKeyAuthMiddleware) - .prefix('/employees') -``` - -## Branded Types - -```typescript -// Branded integer ID -export const EmployeeIdSchema = S.Number.pipe( - S.int(), - S.brand("EmployeeId") -) -export type EmployeeId = typeof EmployeeIdSchema.Type - -// Branded string with prefix validation -export const EmployeeTagSchema = S.String.pipe( - S.startsWith('emp_'), - S.annotations({ - title: 'EmployeeTag', - description: 'Unique identifier (e.g., emp_abc123)', - examples: ['emp_abc123'], - }), - S.brand('EmployeeTag'), -) -export type EmployeeTag = typeof EmployeeTagSchema.Type - -// Enum literals -export const EmployeeTypeSchema = S.Literal('full_time', 'hourly') - -// Email with regex validation -export const EmailSchema = S.String.pipe( - S.pattern(/^(?!\.)(?!.*\.\.)([a-z0-9_'+\-\.]*)[a-z0-9_'+\-]@([a-z0-9][a-z0-9\-]*\.)+[a-z]{2,}$/i), - S.annotations({ title: 'Email', examples: ['user@example.com'] }), - S.brand('Email'), -) -``` - -## Generic Pagination - -```typescript -// Response wrapper -export const ApiResponse = ( - dataSchema: S.Schema -) => - S.Struct({ - hasMore: S.Boolean, - data: S.Array(dataSchema), - }) - -// Query parameters with defaults -export const ApiQueryParams = ( - idSchema: S.Schema, - limit = { default: 50, max: 250, min: 1 } -) => - S.Struct({ - limit: S.optional( - S.NumberFromString.pipe( - S.int(), - S.greaterThanOrEqualTo(limit.min), - S.lessThanOrEqualTo(limit.max) - ) - ).pipe(S.withDecodingDefault(() => limit.default)), - afterId: S.optional(idSchema), - beforeId: S.optional(idSchema), - }) -``` - -## Handler Implementation - -```typescript -export const EmployeesGroupLive = HttpApiBuilder.group( - WarpApi, - 'Employees', - (handlers) => - handlers - .handle('list', ({ urlParams: { types, limit, afterId, beforeId } }) => - Effect.gen(function* () { - const drizzle = yield* PgDrizzle.PgDrizzle - - const filters: SQL[] = [] - if (types?.length) filters.push(inArray(employees.type, types)) - if (afterId) filters.push(gt(employees.tag, afterId)) - if (beforeId) filters.push(lt(employees.tag, beforeId)) - - const results = yield* drizzle - .select() - .from(employees) - .where(filters.length > 0 ? and(...filters) : undefined) - .orderBy(asc(employees.tag)) - .limit(limit + 1) - .pipe( - Effect.catchAll((e) => - Effect.gen(function* () { - yield* Effect.logError('Failed to list employees: ', e) - return yield* Effect.fail(new HttpApiError.InternalServerError()) - }), - ), - ) - - const hasMore = results.length > limit - const pageData = results.slice(0, limit) - - return { - hasMore, - data: pageData.map(dbEmployeeToPublicEmployee), - } - }), - ) - .handle('get', ({ path: { id } }) => - Effect.gen(function* () { - const drizzle = yield* PgDrizzle.PgDrizzle - const results = yield* drizzle - .select() - .from(employees) - .where(eq(employees.tag, id)) - - if (results.length === 0) { - return yield* Effect.fail( - new EmployeeNotFoundError({ id, message: `Employee not found: ${id}` }), - ) - } - - return dbEmployeeToPublicEmployee(results[0]) - }), - ), -) -``` - -## Database Layer - -```typescript -const PgLive = Layer.scopedContext( - Effect.gen(function* () { - const host = yield* Config.string('DB_HOST') - const port = yield* Config.number('DB_PORT') - const database = yield* Config.string('DB_NAME') - const password = yield* Config.redacted('DB_PASSWORD') - const username = yield* Config.string('DB_USERNAME') - - const client = yield* PgClient.make({ - password, - host, - port, - database, - username, - }) - - return Context.make(PgClient.PgClient, client) - .pipe(Context.add(Client.SqlClient, client)) - }), -).pipe(Layer.provide(Reactivity.layer)) - -const DrizzleLive = PgDrizzle.layerWithConfig({ - casing: 'snake_case', -}).pipe(Layer.provide(PgLive)) - -export const layer = Layer.mergeAll(PgLive, DrizzleLive) -``` - -## Middleware - -### Definition - -```typescript -export class ApiKeyUnauthorized extends S.TaggedError()( - 'ApiKeyUnauthorized', - { message: S.String }, - HttpApiSchema.annotations({ status: 401 }), -) {} - -export class ApiKeyAuthMiddleware extends HttpApiMiddleware.Tag()( - 'ApiKeyAuthMiddleware', - { - provides: HttpApiSecurity.apiKey({ - key: 'x-api-key', - in: 'header', - }), - failure: ApiKeyUnauthorized, - }, -) {} -``` - -### Implementation - -```typescript -export const ApiKeyAuthMiddlewareLive = Layer.effect( - ApiKeyAuthMiddleware, - Effect.gen(function* () { - return { - apiKey: (apiKey) => - Effect.gen(function* () { - const apiKeyValue = Redacted.value(apiKey) - - if (!apiKeyValue) { - yield* Effect.logWarning('No API key provided') - return yield* Effect.fail( - new ApiKeyUnauthorized({ message: 'API key is required' }), - ) - } - - return { apiKeyId: apiKeyValue } - }), - } - }), -) -``` - -## Server Entry Point - -```typescript -const ApiImplementationLive = HttpGroupsLive.pipe( - Layer.provide(SqlLive.layer) -) - -const HttpApiRoutes = HttpLayerRouter.addHttpApi(WarpApi).pipe( - Layer.provide(ApiImplementationLive), -) - -const DocsRoute = HttpApiScalar.layerHttpLayerRouter({ - api: WarpApi, - path: '/docs', -}) - -const OpenApiJsonRoute = HttpLayerRouter.add( - 'GET', - '/docs/openapi.json', - HttpServerResponse.json(OpenApi.fromApi(WarpApi)), -).pipe(Layer.provide(HttpLayerRouter.layer)) - -const AllRoutes = Layer.mergeAll( - HttpApiRoutes, - DocsRoute, - OpenApiJsonRoute, -).pipe(Layer.provide(HttpLayerRouter.cors())) - -const OTELNodeSdk = NodeSdk.layer(() => ({ - resource: { serviceName: 'warp-api' }, - spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()) -})) - -const ServerLive = HttpLayerRouter.serve(AllRoutes).pipe( - Layer.provide(BunHttpServer.layer({ port: 9277 })), - Layer.provide(OTELNodeSdk) -) - -BunRuntime.runMain(Layer.launch(ServerLive)) -``` - -## Type-safe Client - -```typescript -const client = yield* HttpApiClient.make(WarpApi, { - baseUrl, - transformClient: HttpClient.mapRequest((req) => - HttpClientRequest.setHeader(req, 'x-api-key', apiKey), - ), -}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient)) - -// Type-safe calls -const page1 = yield* client.Employees.list({ urlParams: { limit: 10 } }) -const employee = yield* client.Employees.get({ path: { id: 'emp_123' } }) -``` - -## Drizzle Schema Integration - -```typescript -// Use Schema literals for enum -export const employeeTypeEnum = pgEnum('employee_type', EmployeeTypeSchema.literals) - -// Type-safe columns with branded types -export const employees = pgTable('employees', { - id: integer().$type().primaryKey().generatedAlwaysAsIdentity(), - tag: varchar().$default(() => generateTag()).$type().notNull().unique(), - email: varchar().$type().notNull().unique(), - type: employeeTypeEnum().notNull(), -}) - -// Row mapping -type DbEmployee = typeof employees.$inferSelect - -function dbEmployeeToPublicEmployee(emp: DbEmployee): PublicEmployee { - return PublicEmployeeSchema.make({ - id: emp.tag, - email: emp.email, - type: emp.type, - createdAt: DateTime.unsafeFromDate(emp.createdAt), - }) -} -``` diff --git a/skills/img-to-react-structure/SKILL.md b/skills/img-to-react-structure/SKILL.md deleted file mode 100644 index 1bcd1bf..0000000 --- a/skills/img-to-react-structure/SKILL.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: img-to-react-structure -description: Converts a UI image/screenshot to React component structure. Translates image to ASCII layout representation, then suggests composable React components. Use when planning component architecture from designs or mockups. ---- - -# Image to React Structure - -Analyze UI images and suggest React component composition. - -## Workflow - -1. **Read the image** using the Read tool -2. **Create ASCII layout** - draw a simplified text representation showing the structural boxes/regions -3. **Map to React components** - suggest component hierarchy based on both image and ASCII structure - -## Step 1: View image - -``` -Read the image file to see the UI design. -``` - -## Step 2: ASCII layout - -Create a text-based structural representation. Use box-drawing characters: - -``` -┌─────────────────────────────────┐ -│ Header/Nav │ -├─────────┬───────────────────────┤ -│ │ │ -│ Sidebar │ Main Content │ -│ │ │ -│ ├───────────┬───────────┤ -│ │ Card │ Card │ -│ └───────────┴───────────┘ -└─────────┴───────────────────────┘ -``` - -Label each region with its purpose (nav, sidebar, card grid, footer, etc). - -## Step 3: React component mapping - -Based on the ASCII structure, suggest components: - -```tsx -// Example output format - -
- -
-
- - - - - - - -
-