Skip to content

Commit 7077d5e

Browse files
Merge pull request #115 from Palbahngmiyine/feat/v6-effect-migration
SOLAPI Node.js SDK 6.0.0-beta
2 parents 6a09980 + e23dc93 commit 7077d5e

72 files changed

Lines changed: 3793 additions & 3603 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 145 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
1-
# SOLAPI SDK for Node.js
1+
# AGENTS.md
22

3-
**Generated:** 2026-01-21
4-
**Commit:** 9df35df
5-
**Branch:** master
3+
SOLAPI SDK for Node.js. Effect 라이브러리 기반 함수형 프로그래밍 + 타입 안전 에러 처리.
64

7-
## OVERVIEW
8-
9-
Server-side SDK for SMS/LMS/MMS and Kakao messaging in Korea. Uses Effect library for type-safe functional programming with Data.TaggedError-based error handling.
10-
11-
## STRUCTURE
5+
## Structure
126

137
```
148
solapi-nodejs/
159
├── src/
1610
│ ├── index.ts # SolapiMessageService facade (entry point)
1711
│ ├── errors/ # Data.TaggedError types
1812
│ ├── lib/ # Core utilities (fetcher, auth, error handler)
19-
│ ├── models/ # Schemas, requests, responses (see models/AGENTS.md)
20-
│ ├── services/ # Domain services (see services/AGENTS.md)
13+
│ ├── models/ # Schemas, requests, responses
14+
│ ├── services/ # Domain services
2115
│ └── types/ # Shared type definitions
2216
├── test/ # Mirrors src/ structure
2317
├── examples/ # Usage examples (excluded from build)
2418
└── debug/ # Debug scripts
2519
```
2620

27-
## WHERE TO LOOK
21+
## Where to Look
2822

2923
| Task | Location | Notes |
3024
|------|----------|-------|
@@ -36,58 +30,160 @@ solapi-nodejs/
3630
| Fix API request issue | `src/lib/defaultFetcher.ts` | HTTP client with retry |
3731
| Understand error flow | `src/lib/effectErrorHandler.ts` | Effect → Promise conversion |
3832

39-
## CONVENTIONS
33+
## Conventions
34+
35+
### Effect Library (Mandatory)
36+
37+
**Async operations**: `Effect.tryPromise` 또는 `Effect.gen`
38+
```typescript
39+
Effect.tryPromise({
40+
try: () => fetch(url, options),
41+
catch: e => new NetworkError({ url, cause: e }),
42+
});
43+
```
44+
45+
**Complex flow**: `Effect.gen`
46+
```typescript
47+
Effect.gen(function* (_) {
48+
const auth = yield* _(buildAuth(params));
49+
const response = yield* _(fetchWithRetry(url, auth));
50+
return yield* _(parseResponse(response));
51+
});
52+
```
53+
54+
**Error to Promise**: 반드시 `runSafePromise` 경유
55+
```typescript
56+
return runSafePromise(effect);
57+
// BAD: try { await Effect.runPromise(...) } catch { }
58+
```
59+
60+
### Service Pattern
61+
62+
`DefaultService` 상속 → `this.request()` 사용:
63+
```typescript
64+
export default class MyService extends DefaultService {
65+
async myMethod(data: Request): Promise<Response> {
66+
return this.request<Request, Response>({
67+
httpMethod: 'POST',
68+
url: 'my/endpoint',
69+
body: data,
70+
});
71+
}
72+
}
73+
```
74+
75+
Effect.gen 활용 (복잡한 로직):
76+
```typescript
77+
async send(messages: Request): Promise<Response> {
78+
const effect = Effect.gen(function* (_) {
79+
const validated = yield* _(validateSchema(messages));
80+
return yield* _(Effect.promise(() => this.request(...)));
81+
});
82+
return runSafePromise(effect);
83+
}
84+
```
85+
86+
### Model Pattern
87+
88+
Three-layer architecture: `base/` (도메인) → `requests/` (입력 변환) → `responses/` (API 응답)
89+
90+
**Type + Schema**:
91+
```typescript
92+
export type MyType = Schema.Schema.Type<typeof mySchema>;
93+
export const mySchema = Schema.Struct({
94+
field: Schema.String,
95+
optional: Schema.optional(Schema.Number),
96+
});
97+
```
98+
99+
**Discriminated Union**:
100+
```typescript
101+
export const buttonSchema = Schema.Union(
102+
webButtonSchema, // { linkType: 'WL', ... }
103+
appButtonSchema, // { linkType: 'AL', ... }
104+
);
105+
```
40106

41-
**Effect Library (MANDATORY)**:
42-
- All errors: `Data.TaggedError` with environment-aware `toString()`
43-
- Async operations: `Effect.gen` + `Effect.tryPromise`, never wrap with try-catch
44-
- Validation: `Effect Schema` with `Schema.filter`, `Schema.transform`
45-
- Error execution: `runSafePromise()` / `runSafeSync()` from effectErrorHandler
107+
**Custom Validation**:
108+
```typescript
109+
Schema.String.pipe(
110+
Schema.filter(isValid, { message: () => 'Error message' }),
111+
);
112+
```
46113

47-
**TypeScript**:
48-
- **NEVER use `any`** — use `unknown` + type guards or Effect Schema
49-
- Strict mode enforced (`noUnusedLocals`, `noUnusedParameters`)
50-
- Path aliases: `@models`, `@lib`, `@services`, `@errors`, `@internal-types`
114+
### Lib Utilities
51115

52-
**Testing**:
53-
- Unit: `vitest` with `Schema.decodeUnknownEither()` for validation tests
54-
- E2E: `@effect/vitest` with `it.effect()` and `Effect.gen`
55-
- Run: `pnpm test` / `pnpm test:watch`
116+
| File | Purpose |
117+
|------|---------|
118+
| `defaultFetcher.ts` | HTTP client — Effect.gen, retry 3x exponential backoff, Match |
119+
| `effectErrorHandler.ts` | `runSafePromise`, `runSafeSync`, `unwrapCause` |
120+
| `authenticator.ts` | HMAC-SHA256 auth header |
121+
| `stringifyQuery.ts` | URL query string builder (array handling) |
122+
| `fileToBase64.ts` | File/URL → Base64 |
123+
| `stringDateTrasnfer.ts` | Date parsing with `InvalidDateError` |
56124

57-
## ANTI-PATTERNS
125+
## Anti-Patterns
58126

59127
| Pattern | Why Bad | Do Instead |
60128
|---------|---------|------------|
61129
| `any` type | Loses type safety | `unknown` + type guards |
62130
| `as any`, `@ts-ignore` | Suppresses errors | Fix the type issue |
63-
| try-catch around Effect | Loses Effect benefits | Use `Effect.catchTag` |
64-
| Direct `throw new Error()` | Inconsistent error handling | Use `Data.TaggedError` |
131+
| try-catch around Effect | Loses Effect benefits | `Effect.catchTag` |
132+
| Direct `throw new Error()` | Inconsistent error handling | `Data.TaggedError` |
65133
| Empty catch blocks | Swallows errors | Handle or propagate |
134+
| Bypass `runSafePromise` | Loses error formatting | Always use `runSafePromise` |
135+
| Call `defaultFetcher` directly | Bypasses service layer | Use `this.request()` |
136+
| Skip schema validation | Runtime errors | Always validate input |
137+
| Interface when schema needed | No runtime validation | Use `Schema.Struct` |
138+
| Duplicate validation logic | Inconsistency | Compose schemas |
139+
| Hardcode API URL | Inflexible | Use `DefaultService.baseUrl` |
140+
| Mix Effect and Promise styles | Confusing | Pick one per method |
66141

67-
## COMMANDS
142+
## Architecture Notes
68143

69-
```bash
70-
pnpm dev # Watch mode (tsup)
71-
pnpm build # Lint + build
72-
pnpm lint # Biome check with auto-fix
73-
pnpm test # Run tests once
74-
pnpm test:watch # Watch mode
75-
pnpm docs # Generate TypeDoc
76-
```
77-
78-
## ARCHITECTURE NOTES
79-
80-
**Service Facade Pattern**: `SolapiMessageService` aggregates 7 domain services via `bindServices()` dynamic method binding. All services extend `DefaultService`.
144+
**Service Facade**: `SolapiMessageService`가 7개 도메인 서비스를 `bindServices()`로 동적 바인딩.
81145

82146
**Error Flow**:
83147
```
84-
API Response
85-
→ defaultFetcher (creates Effect errors)
86-
→ runSafePromise (converts to Promise)
87-
→ toCompatibleError (preserves properties on Error)
88-
→ Consumer
148+
API Response → defaultFetcher (Effect errors) → runSafePromise (Promise)
149+
→ 원본 Data.TaggedError 그대로 reject → Consumer
89150
```
90151

91-
**Production vs Development**: Error messages stripped of stack traces and detailed context in production (`process.env.NODE_ENV === 'production'`).
152+
**Production vs Development**: Production에서는 stack trace와 상세 컨텍스트가 제거됨.
153+
154+
**Retry Logic**: `defaultFetcher.ts` — 3회 재시도, exponential backoff (connection refused, reset, 503).
155+
156+
## Testing Guidelines (Detail)
157+
158+
### Failure Injection
159+
- 의존성 실패 시뮬레이션 (첫 호출, N번째 호출, 지속적 실패)
160+
- 타임아웃, 취소 케이스 포함
161+
- 부분 성공 후 실패 시나리오
162+
163+
### Concurrency
164+
- Race condition 없음 확인
165+
- Deadlock 없음 확인
166+
- 중복 실행 없음 확인
167+
168+
### Persistence
169+
- Atomic behavior (전부 또는 전무)
170+
- 중간 상태 오염 없음
171+
- 안전한 재시도 및 복구
172+
173+
### Fuzz (권장)
174+
- 입력 파싱/디코딩에 fuzz 테스트 적용
175+
- panic이나 무한 리소스 사용 없음 확인
176+
177+
### Style
178+
- 테이블 기반 테스트: `it.each()` 활용
179+
- 외부 의존성: fake/stub 사용
180+
- cleanup hooks (`afterEach`/`afterAll`)
181+
182+
## Sub-Agents
183+
184+
### tidy-first
185+
Kent Beck의 "Tidy First?" 원칙 적용 리팩토링 전문가.
186+
`.claude/agents/tidy-first.md` 참조.
92187

93-
**Retry Logic**: `defaultFetcher.ts` implements 3x retry with exponential backoff for retryable errors (connection refused, reset, 503).
188+
**자동 호출**: 기능 추가, 동작 구현, 코드 리뷰, 리팩토링 작업 시.
189+
**핵심 규칙**: 구조적 변경과 동작 변경을 항상 분리.

0 commit comments

Comments
 (0)