Skip to main content

effect-best-practices

Enforces Effect-TS patterns for services, errors, layers, atoms, and Effect.pipe composition. Use when writing Effect.Service, Schema.TaggedError, Layer, effect-atom, Effect.fn/`.pipe`, or `yield*` pipelines.

소스 정보

저장소
forcedotcom/salesforcedx-vscode
최근 소스 활동
2026년 9월 24일 23:28
감지된 SKILL.md 언어
영어
스타
1,035
포크
454

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
9 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
effect-best-practices
description
Enforces Effect-TS patterns for services, errors, layers, atoms, and Effect.pipe composition. Use when writing Effect.Service, Schema.TaggedError, Layer, effect-atom, Effect.fn/`.pipe`, or `yield*` pipelines.
review
always
version
1.6.1
For diff/plan review against these patterns, invoke the `effect-advocate` subagent (`.claude/agents/effect-advocate.md`). ## Effect LS diagnostics (agent usage) Cursor's `read_lints` does not surface Effect Language Server diagnostics. Use the CLI: ```bash npx effect-language-service diagnostics --file <path> # or whole project: npx effect-language-service diagnostics --project tsconfig.json ``` - The PostToolUse `verify-on-edit.sh` hook auto-runs `--file <edited>` on every `.ts` Edit/Write and surfaces output as `followup_message`. Address what it reports. - **Address warnings AND messages, not just errors.** `references/diagnostics-findings.md` maps each common finding to its fix; `config/effect-diagnostics.json` `enforcedRules` is the build gate. - Enforcing a rule takes two edits, not just an `enforcedRules` entry — see `references/diagnostics-findings.md`. - After a batch of edits, run `--project tsconfig.json` for the affected package to catch cross-file issues. - `effect-language-service quickfixes` shows proposed code changes. ## Quick Reference: Critical Rules | Category | DO | DON'T | | ----------------- | -------------------------------------------------------- | ---------------------------------------------------------------- | | Services | `Effect.Service` with `accessors: true` | `Context.Tag` for business logic | | Dependencies | `dependencies: [Dep.Default]` in service | Manual `Layer.provide` at usage sites | | Errors | `Schema.TaggedError` with `message` field | Plain classes or generic Error | | Error Specificity | Split tags when catch arms or field shapes differ; telemetry dimensions are fields on one tag | Extra tags that all print `message` / fire the same span | | Error Handling | `catchTag`/`catchTags`; catch only when needed | `catchAll`; swallowing; catching "just in case" | | IDs | Salesforce record/org: `SalesforceId`/`OrgId` (`core/schemas/salesforceId.ts`). `DefaultOrgInfoSchema.orgId`/`devHubOrgId`: `Schema.optional(OrgId)` like `cliId`. Else `Schema.UUID.pipe(Schema.brand("@App/EntityId"))` | Plain `string`; `getAuthInfoFields().orgId` ad hoc; `optionalWith` as Option on DefaultOrgInfo | | Functions | `Effect.fn` over `Effect.gen`; `.gen` only for shared pipes | Anonymous generators; nested `Effect.gen` to attach recovery; `.gen` for business logic | | Composition | `.pipe`; `const` only if read ≥2×. Details: `references/composition-style.md` | single-use `const x = yield*` then `f(x)` | | Params vs deps | Params = runtime data; dependencies = yield from context | Passing Ref/PubSub/service as params | | Naming | `FooCommand` for commands, domain names for helpers | `FooEffect` suffix (redundant; TS/Effect.fn already convey type) | | Logging | `Effect.log` with structured data | `console.log` | | Config | `Config.*` with validation | `process.env` directly (except build-time vars like `ESBUILD_*`) | | Time values | `Duration.seconds(30)`, `Duration.millis(5000)`; params as `Duration.DurationInput` | Numeric milliseconds as `number` params or `TIMEOUT_MS = 30_000` constants | | Options | `Option.match` with both cases | `Option.getOrThrow` | | Nullability | `Option<T>` in domain types | `null`/`undefined` | | Atoms | `Atom.make` outside components | Creating atoms inside render | | Atom State | `Atom.keepAlive` for global state | Forgetting keepAlive for persistent state | | Atom Updates | `useAtomSet` in React components | `Atom.update` imperatively from React | | Atom Cleanup | `get.addFinalizer()` for side effects | Missing cleanup for event listeners | | Resource Cleanup | Scoped service/layer + `Effect.addFinalizer` | Returning `dispose`; delegating Effect-owned resources to callers | | Atom Results | `Result.builder` with `onErrorTag` | Ignoring loading/error states | | Grouping | `Arr.groupBy` (effect/Array) | `Object.groupBy`, whose `Partial<Record>` forces a filter | ## Service Definition Pattern **Always use `Effect.Service`** for business logic services. This provides automatic accessors, built-in `Default` layer, and proper dependency declaration. ```typescript import { Effect } from 'effect'; export class UserService extends Effect.Service<UserService>()('UserService', { accessors: true, dependencies: [UserRepo.Default, CacheService.Default], effect: Effect.gen(function* () { const repo = yield* UserRepo; const cache = yield* CacheService; const findById = Effect.fn('UserService.findById')(function* (id: UserId) { const cached = yield* cache.get(id); if (Option.isSome(cached)) return cached.value; const user = yield* repo.findById(id); yield* cache.set(id, user); return user; }); const create = Effect.fn('UserService.create')(function* (data: CreateUserInput) { const user = yield* repo.create(data); yield* Effect.log('User created', { userId: user.id }); return user; }); return { findById, create }; }) }) {} // Usage - dependencies are already wired const program = UserService.findById(userId); // At app root const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default); ``` **When `Context.Tag` is acceptable:** - Infrastructure with runtime injection (Cloudflare KV, worker bindings) - Factory patterns where resources are provided externally - Interfaces with caller-provided implementations — no single canonical one to bundle as `.Default` (e.g. `SoqlBuilderService`, implemented once by the VS Code host and once by a test fake); see `references/service-patterns.md` ### Params vs Dependencies - **Params** = runtime data per call (IDs, user input, per-invocation config) - **Dependencies** = shared infrastructure (Ref, PubSub, SubscriptionRef, services) — provide via layer, **yield inside** the effect - Build Ref/PubSub/etc in the layer (e.g. `buildAllServicesLayer`); consumers yield them, don't receive as params ```typescript // WRONG - passing shared infra as params const createStatusBar = (pubsub: PubSub.PubSub<void>, stateRef: SubscriptionRef.SubscriptionRef<State>) => Effect.gen(...) // Caller must create and pass; wiring scattered at call sites // CORRECT - yield inside, build in layer const PubSubTag = Context.GenericTag<PubSub.PubSub<void>>("PubSub") const createStatusBar = Effect.gen(function* () { const pubsub = yield* PubSubTag const stateRef = yield* StateRefTag // ... }) // Layer: Layer.effect(PubSubTag, PubSub.sliding<void>(1)) ``` See `references/service-patterns.md` for detailed patterns. ## Error Definition Pattern **Always use `Schema.TaggedError`** for errors. This makes them serializable (required for RPC) and provides consistent structure. ```typescript import { Schema } from 'effect'; import { HttpApiSchema } from '@effect/platform'; export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()( 'UserNotFoundError', { userId: UserId, message: Schema.String }, HttpApiSchema.annotations({ status: 404 }) ) {} export class UserCreateError extends Schema.TaggedError<UserCreateError>()( 'UserCreateError', { message: Schema.String, cause: Schema.optional(Schema.String) }, HttpApiSchema.annotations({ status: 400 }) ) {} ``` **Error handling - use `catchTag`/`catchTags`:** ```typescript // CORRECT - preserves type information yield * repo.findById(id).pipe( Effect.catchTag('DatabaseError', err => Effect.fail(new UserNotFoundError({ userId: id, message: 'Lookup failed' })) ), Effect.catchTag('ConnectionError', err => Effect.fail(new ServiceUnavailableError({ message: 'Database unreachable' })) ) ); // CORRECT - multiple tags at once yield * effect.pipe( Effect.catchTags({ DatabaseError: err => Effect.fail(new UserNotFoundError({ userId: id, message: err.message })), ValidationError: err => Effect.fail(new InvalidEmailError({ email: input.email, message: err.message })) }) ); ``` ### When to Catch (and When Not To) **Most errors surface to the user** (message/toast at runtime). Only catch when: - **Genuinely ignore** – accept failure and continue (e.g. optional pre-create) - **Better message** – default vague; map to clearer domain error Expected skip (missing optional plugin, incompatible version): write nls on the success path (guard / Match branch). `fail`+`catchTag` is for unexpected recovery — a handler that isn't "print this string." Catch sparingly. No `catchAll` or "swallow to be safe." Use `catchTag`/`catchTags`; log or fail with improved error. ### Prefer Explicit Over Generic Errors Split tags when **catch arms or field shapes** differ — e.g. `message`+`cause` vs `message`+`cause`+`setting`. Same catch work (print `message`, one span) → one tag; telemetry dimensions are fields, not tags. nls variance lives in `message`. Frontend/RPC still splits when the UI branches (`UserNotFoundError` vs `ChannelNotFoundError`). See `references/error-patterns.md`. ### Accumulating Errors Across a Collection To **continue past failures** instead of short-circuiting on the first, don't hand-roll `Either` + `catchTag` + a re-loop. Use `Effect.partition` (both buckets), `Effect.validateAll` (all-or-nothing), or `Effect.validateFirst`. These recover the typed error channel per item but do NOT capture interruption — so a Cancel still aborts the whole loop. See `references/error-patterns.md` for the accumulation/interruption nuance, error remapping, and retry patterns. ## Schema & Branded Types Pattern **Brand all entity IDs** for type safety across service boundaries. This repo — Salesforce record/org ids are not UUIDs. Use `SalesforceId`/`OrgId` and `orgIdFrom`/`orgIdFromConnection` (`getFields()` / Connection; `Option`; `references/schema-patterns.md`). Other AuthFields: `authFieldsFrom`/`authFieldsFromConnection`. `DefaultOrgInfoSchema.orgId`/`devHubOrgId`: `Schema.optional(OrgId)` like `cliId` — not Option. ```typescript import { Schema } from 'effect'; // Entity IDs - always branded export const UserId = Schema.UUID.pipe(Schema.brand('@App/UserId')); export type UserId = Schema.Schema.Type<typeof UserId>; export const TenantId = Schema.UUID.pipe(Schema.brand('@App/TenantId')); export type TenantId = Schema.Schema.Type<typeof TenantId>; // Domain types - use Schema.Struct export const User = Schema.Struct({ id: UserId, email: Schema.String, name: Schema.String, tenantId: TenantId, createdAt: Schema.DateTimeUtc }); export type User = Schema.Schema.Type<typeof User>; // Input types for mutations export const CreateUserInput = Schema.Struct({ email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), name: Schema.String.pipe(Schema.minLength(1)), tenantId: TenantId }); export type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>; ``` **When NOT to brand:** - Simple strings that don't cross service boundaries (URLs, file paths) - Primitive config values See `references/schema-patterns.md` for transforms and advanced patterns. ## Function Pattern: Prefer Effect.fn over Effect.gen **Prefer `Effect.fn`** for effectful code. Provides automatic tracing with proper span names. Span name required; enforced by `local/require-effect-fn-span-name`. **Use `Effect.gen` only when** you need a shared effect with common `.pipe` attached so multiple consumers don't each pipe the same things — e.g. provided dependencies, common error handlers, retries. (Less common with Runtimes.) Service definition bodies are a valid use (shared wiring). ```typescript // CORRECT - Effect.fn with descriptive name const findById = Effect.fn('UserService.findById')(function* (id: UserId) { yield* Effect.annotateCurrentSpan('userId', id); return yield* repo.findById(id); }); // CORRECT - Effect.fn with multiple parameters const transfer = Effect.fn('AccountService.transfer')(function* (fromId: AccountId, toId: AccountId, amount: number) { yield* Effect.annotateCurrentSpan('fromId', fromId); yield* Effect.annotateCurrentSpan('toId', toId); yield* Effect.annotateCurrentSpan('amount', amount); // ... }); // WRONG - params on wrapper arrow, generator has none (closure capture) // Enforced by local/no-effect-fn-wrapper const findByIdBad = (id: UserId) => Effect.fn('UserService.findById')(function* () { yield* repo.findById(id); // id from closure }); // WRONG - Effect.fn invoked immediately (config-enforced effectFnIife). Effect.fn builds a reusable // function; for one-shot use write Effect.gen and keep the span with a piped withSpan. const opened = Effect.fn('FsService.open')(function* () { yield* fs.showTextDocument(uri); })(); // CORRECT const openedOk = Effect.gen(function* () { yield* fs.showTextDocument(uri); }).pipe(Effect.withSpan('FsService.open')); // Naming: Don't append Effect. For commands use FooCommand; for helpers/lifecycle use domain names.
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기