diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 34ac3b8..6463e2c 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -49,6 +49,7 @@ export default defineConfig({ { text: 'Why Trotsky', link: '/guide/why' }, { text: 'Features', link: '/guide/features' }, { text: 'Code of Conduct', link: '/guide/code-of-conduct' }, + { text: 'Architecture', link: '/guide/architecture' }, { text: 'FAQ', link: '/guide/faq' }, ] }, diff --git a/docs/guide/architecture.md b/docs/guide/architecture.md new file mode 100644 index 0000000..68a3c7f --- /dev/null +++ b/docs/guide/architecture.md @@ -0,0 +1,425 @@ +# Architecture + +This document explains the internal architecture of Trotsky, its design principles, and how the different components work together. + +## Overview + +Trotsky is built around a **builder pattern** that allows users to chain operations (called "steps") to interact with the AT Protocol / Bluesky API. The library provides a type-safe, fluent interface for building complex automation workflows. + +## Core Concepts + +### 1. Steps + +A **Step** is the fundamental building block in Trotsky. Each step represents a single operation, such as: +- Fetching an actor profile +- Liking a post +- Following an account +- Iterating through a list + +Steps are chainable and composable, allowing complex workflows to be built declaratively. + +```typescript +await Trotsky.init(agent) + .actor('alice.bsky.social') // Step 1: Get actor + .followers() // Step 2: Get followers + .each() // Step 3: Iterate + .follow() // Step 4: Follow each + .run() // Execute +``` + +### 2. Step Hierarchy + +Steps are organized in a class hierarchy: + +``` +Step (base class) +├── StepBuilder (chainable steps) +│ ├── Trotsky (entry point) +│ ├── StepActor +│ ├── StepPost +│ └── ... +├── StepBuilderList (steps that return lists) +│ ├── StepActors +│ ├── StepPosts +│ ├── StepActorFollowers +│ └── ... +└── StepBuilderStream (steps that stream data) + ├── StepStreamPosts + └── StepActorStreamPosts +``` + +**Key Properties:** +- **Parent**: Each step has a reference to its parent step +- **Context**: Data passed from parent to child (e.g., actor DID) +- **Output**: Result of executing the step +- **Agent**: AT Protocol agent for API calls + +### 3. Step Types + +#### Single-Item Steps +Steps that work with a single entity: +- `StepActor` - Single actor profile +- `StepPost` - Single post +- `StepList` - Single list + +#### List Steps +Steps that work with collections and support pagination: +- `StepActors` - Multiple actors +- `StepPosts` - Multiple posts +- `StepActorFollowers` - Actor's followers (paginated) + +#### Action Steps +Steps that perform an action without returning data: +- `StepActorFollow` - Follow an actor +- `StepPostLike` - Like a post +- `StepActorBlock` - Block an actor + +#### Utility Steps +Steps that modify execution flow: +- `StepWhen` - Conditional execution +- `StepTap` - Side effects without modifying flow +- `StepWait` - Delay execution +- `StepSave` - Save output to file + +## Component Organization + +### Directory Structure + +``` +lib/ +├── core/ # Core step implementations +│ ├── base/ # Base classes (Step, StepBuilder, etc.) +│ ├── mixins/ # Reusable mixins (ActorMixins, PostMixins) +│ └── utils/ # Utilities (logger, resolvable, etc.) +├── types/ # Shared type definitions +├── errors/ # Custom error classes +├── config/ # Configuration types +└── trotsky.ts # Main barrel export +``` + +### Key Files + +- **`Step.ts`**: Base class for all steps +- **`StepBuilder.ts`**: Base for chainable steps +- **`StepBuilderList.ts`**: Base for list/collection steps +- **`Trotsky.ts`**: Main entry point class +- **`trotsky.ts`**: Barrel export file + +## Design Patterns + +### 1. Builder Pattern + +The fluent interface allows chaining operations: + +```typescript +Trotsky.init(agent) + .actor('handle') + .posts() + .each() + .like() +``` + +Each method returns a new step instance that can be chained further. + +### 2. Mixins + +Common functionality is shared via mixins: + +```typescript +// ActorMixins.ts +export class ActorMixins { + followers() { return this.append(StepActorFollowers) } + posts() { return this.append(StepActorPosts) } + starterPacks() { return this.append(StepActorStarterPacks) } +} + +// StepActor extends both StepBuilder and ActorMixins +export class StepActor extends mix(StepBuilder, ActorMixins) {} +``` + +**Benefits:** +- Code reuse across similar steps +- Consistent API across step types +- Easy to add new functionality + +### 3. Context Propagation + +Data flows from parent to child through the context property: + +```typescript +Trotsky.init(agent) + .actor('alice') // Context: { did: 'did:plc:...', handle: 'alice', ... } + .followers() // Context inherited from parent + .each() // Context: individual follower + .follow() // Uses follower's DID from context +``` + +### 4. Lazy Execution + +Steps are not executed when chained - only when `.run()` is called: + +```typescript +const workflow = Trotsky.init(agent) + .actor('alice') + .posts() + .each() + .like() +// Nothing has executed yet + +await workflow.run() // NOW it executes +``` + +## Data Flow + +### 1. Execution Pipeline + +``` +User Code → Trotsky.init() → Chain Steps → .run() → Execute Pipeline + ↓ + Results/Side Effects +``` + +### 2. Step Execution + +Each step follows this lifecycle: + +1. **Construction**: Step is created via `.append()` +2. **Configuration**: Parameters are set (e.g., query params) +3. **Context Inheritance**: Receives context from parent +4. **Execution**: `.apply()` method is called +5. **Output Generation**: Result is stored in `.output` +6. **Child Execution**: Child steps receive this step's output as context + +### 3. Pagination + +List steps handle pagination automatically: + +```typescript +async applyPagination() { + let cursor: string | undefined + const items = [] + + while (true) { + const response = await this.agent.api({ cursor, limit: 50 }) + items.push(...response.items) + + cursor = response.cursor + if (!cursor || items.length >= limit) break + } + + this.output = items +} +``` + +## Error Handling + +Trotsky provides structured error classes: + +```typescript +try { + await Trotsky.init(agent).actor('invalid').run() +} catch (error) { + if (error instanceof ValidationError) { + console.log(error.code, error.details) + } else if (error instanceof AuthenticationError) { + console.log('Auth failed:', error.message) + } +} +``` + +**Error Classes:** +- `TrotskyError` - Base error class +- `ValidationError` - Input validation failures +- `AuthenticationError` - Auth/permission failures +- `RateLimitError` - Rate limit exceeded +- `PaginationError` - Pagination failures + +## Type Safety + +Trotsky leverages TypeScript's type system extensively: + +### 1. Generic Type Parameters + +```typescript +class Step { + parent: P // Parent step type + context: C // Context data type + output: O // Output data type +} +``` + +### 2. Type Inference + +Types are inferred through the chain: + +```typescript +const result = await Trotsky.init(agent) + .actor('alice') // StepActor + .posts() // StepActorPosts> + .runHere() + +// result.output is typed as AppBskyFeedDefs.PostView[] +``` + +### 3. Shared Types + +Common types are centralized in `lib/types/`: + +```typescript +import { ActorParam, PostUri, PaginationParams } from 'trotsky/types' +``` + +## Performance Considerations + +### 1. Pagination Limits + +Control pagination with `.take()`: + +```typescript +// Only fetch first 10 items +await Trotsky.init(agent) + .actor('alice') + .followers() + .take(10) + .run() +``` + +### 2. Rate Limiting + +Built-in rate limiting (configurable): + +```typescript +Trotsky.init(agent, { + rateLimit: { + enabled: true, + requestsPerMinute: 60 + } +}) +``` + +### 3. Batching + +Some operations support batching: + +```typescript +// Fetch multiple posts in one request +await Trotsky.init(agent).posts([uri1, uri2, uri3]).run() +``` + +## Extensibility + +### Adding New Steps + +1. **Create Step Class**: +```typescript +export class StepMyFeature extends StepBuilder { + async apply() { + const result = await this.agent.api.myFeature() + this.output = result + } +} +``` + +2. **Add to Trotsky Class**: +```typescript +myFeature(): StepMyFeature { + return this.append(StepMyFeature) +} +``` + +3. **Export**: +```typescript +// lib/trotsky.ts +export * from "./core/StepMyFeature" +``` + +### Using Mixins + +Add reusable functionality via mixins: + +```typescript +export class MyMixins { + customAction() { + return this.append(StepCustomAction) + } +} + +export class StepMyFeature extends mix(StepBuilder, MyMixins) {} +``` + +## Testing Strategy + +- **Unit Tests**: Test individual steps in isolation +- **Integration Tests**: Test step chains and workflows +- **Test Environment**: Uses `@atproto/dev-env` for realistic testing + +```typescript +describe('StepActor', () => { + test('should fetch actor profile', async () => { + const actor = await Trotsky.init(agent) + .actor('alice') + .runHere() + + expect(actor.output).toHaveProperty('handle') + }) +}) +``` + +## Future Architecture Plans + +### 1. Plugin System + +Support for custom plugins: + +```typescript +Trotsky.init(agent) + .use(new AnalyticsPlugin()) + .use(new CachePlugin()) +``` + +### 2. Middleware + +Request/response interceptors: + +```typescript +Trotsky.init(agent) + .beforeStep((step) => console.log(`Executing: ${step.name}`)) + .afterStep((step) => console.log(`Completed: ${step.name}`)) +``` + +### 3. Advanced Caching + +Built-in caching layer for frequently accessed data: + +```typescript +Trotsky.init(agent, { + cache: { + enabled: true, + ttl: 60000 + } +}) +``` + +## Best Practices + +1. **Use Type Inference**: Let TypeScript infer types instead of explicit annotations +2. **Chain Efficiently**: Minimize API calls by batching when possible +3. **Handle Errors**: Always wrap `.run()` in try/catch +4. **Rate Limit**: Use `.wait()` between actions to avoid rate limits +5. **Test Workflows**: Write integration tests for complex chains + +## Contributing + +When contributing to Trotsky's architecture: + +1. Follow existing patterns (Step hierarchy, mixins, etc.) +2. Add comprehensive JSDoc comments +3. Include unit and integration tests +4. Update this architecture document +5. Consider backward compatibility + +## References + +- [AT Protocol Documentation](https://atproto.com) +- [Bluesky API Reference](https://docs.bsky.app) +- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) diff --git a/lib/config/TrotskyConfig.ts b/lib/config/TrotskyConfig.ts new file mode 100644 index 0000000..f487edd --- /dev/null +++ b/lib/config/TrotskyConfig.ts @@ -0,0 +1,208 @@ +/** + * Configuration options for Trotsky instance. + * + * This module provides configuration interfaces and default values + * for customizing Trotsky's behavior. + * + * @module config + */ + +/** + * Logging configuration options. + * + * @public + */ +export interface LoggingConfig { + + /** Enable or disable logging */ + "enabled": boolean; + + /** Minimum log level to output */ + "level": "debug" | "info" | "warn" | "error"; + + /** Custom logger function (optional) */ + "logger"?: (level: string, message: string, meta?: Record) => void; +} + +/** + * Pagination configuration options. + * + * @public + */ +export interface PaginationConfig { + + /** Default page size for paginated requests */ + "defaultLimit": number; + + /** Maximum page size allowed */ + "maxLimit": number; + + /** Enable automatic pagination */ + "autoPaginate": boolean; +} + +/** + * Retry configuration options. + * + * @public + */ +export interface RetryConfig { + + /** Enable automatic retries on failure */ + "enabled": boolean; + + /** Maximum number of retry attempts */ + "maxAttempts": number; + + /** Backoff strategy for retries */ + "backoff": "linear" | "exponential"; + + /** Initial delay between retries (milliseconds) */ + "initialDelay": number; + + /** Maximum delay between retries (milliseconds) */ + "maxDelay": number; + + /** HTTP status codes that should trigger a retry */ + "retryableStatusCodes": number[]; +} + +/** + * Rate limiting configuration options. + * + * @public + */ +export interface RateLimitConfig { + + /** Enable built-in rate limiting */ + "enabled": boolean; + + /** Maximum requests per minute */ + "requestsPerMinute": number; + + /** Maximum concurrent requests */ + "concurrentRequests": number; + + /** Behavior when rate limit is hit */ + "onLimitReached": "throw" | "queue" | "drop"; +} + +/** + * Caching configuration options. + * + * @public + */ +export interface CacheConfig { + + /** Enable caching */ + "enabled": boolean; + + /** Default cache TTL in milliseconds */ + "defaultTTL": number; + + /** Maximum cache size (number of entries) */ + "maxSize": number; + + /** Cache key prefix */ + "keyPrefix": string; +} + +/** + * Complete Trotsky configuration. + * + * @public + */ +export interface TrotskyConfig { + + /** Logging configuration */ + "logging": LoggingConfig; + + /** Pagination configuration */ + "pagination": PaginationConfig; + + /** Retry configuration */ + "retry": RetryConfig; + + /** Rate limiting configuration */ + "rateLimit": RateLimitConfig; + + /** Caching configuration */ + "cache": CacheConfig; +} + +/** + * Partial configuration allowing users to override specific options. + * + * @public + */ +export type PartialTrotskyConfig = { + [K in keyof TrotskyConfig]?: Partial +} + +/** + * Default configuration values. + * + * These values are used when no custom configuration is provided. + * + * @public + */ +export const defaultConfig: TrotskyConfig = { + "logging": { + "enabled": false, + "level": "info" + }, + "pagination": { + "defaultLimit": 50, + "maxLimit": 100, + "autoPaginate": true + }, + "retry": { + "enabled": true, + "maxAttempts": 3, + "backoff": "exponential", + "initialDelay": 1000, + "maxDelay": 30000, + "retryableStatusCodes": [408, 429, 500, 502, 503, 504] + }, + "rateLimit": { + "enabled": false, + "requestsPerMinute": 60, + "concurrentRequests": 10, + "onLimitReached": "queue" + }, + "cache": { + "enabled": false, + "defaultTTL": 60000, // 1 minute + "maxSize": 1000, + "keyPrefix": "trotsky:" + } +} + +/** + * Merges partial configuration with default configuration. + * + * @param config - Partial configuration to merge + * @returns Complete configuration with defaults + * + * @example + * ```ts + * const config = mergeConfig({ + * logging: { enabled: true, level: "debug" } + * }) + * ``` + * + * @public + */ +export function mergeConfig (config?: PartialTrotskyConfig): TrotskyConfig { + if (!config) { + return { ...defaultConfig } + } + + return { + "logging": { ...defaultConfig.logging, ...config.logging }, + "pagination": { ...defaultConfig.pagination, ...config.pagination }, + "retry": { ...defaultConfig.retry, ...config.retry }, + "rateLimit": { ...defaultConfig.rateLimit, ...config.rateLimit }, + "cache": { ...defaultConfig.cache, ...config.cache } + } +} diff --git a/lib/config/index.ts b/lib/config/index.ts new file mode 100644 index 0000000..7d9f047 --- /dev/null +++ b/lib/config/index.ts @@ -0,0 +1,21 @@ +/** + * Central export point for Trotsky configuration. + * + * @module config + * @packageDocumentation + */ + +export type { + LoggingConfig, + PaginationConfig, + RetryConfig, + RateLimitConfig, + CacheConfig, + TrotskyConfig, + PartialTrotskyConfig +} from "./TrotskyConfig" + +export { + defaultConfig, + mergeConfig +} from "./TrotskyConfig" diff --git a/lib/errors/AuthenticationError.ts b/lib/errors/AuthenticationError.ts new file mode 100644 index 0000000..928559f --- /dev/null +++ b/lib/errors/AuthenticationError.ts @@ -0,0 +1,63 @@ +/** + * Error class for authentication and authorization failures. + * + * Thrown when authentication is required but missing, invalid, + * or when the user lacks permission for an operation. + * + * @example + * ```ts + * throw new AuthenticationError( + * "Authentication required for this operation", + * "AUTH_REQUIRED", + * "StepActorFollow" + * ) + * ``` + * + * @public + */ + +import { TrotskyError } from "./TrotskyError" + +export class AuthenticationError extends TrotskyError { + + /** + * Creates a new AuthenticationError. + * + * @param message - Human-readable error message + * @param code - Machine-readable error code + * @param step - Optional step name where error occurred + * @param cause - Optional underlying error + */ + constructor ( + message: string, + code: string = "AUTH_ERROR", + step?: string, + cause?: Error + ) { + super(message, code, step, cause) + this.name = "AuthenticationError" + } +} + +/** + * Common authentication error codes. + * + * @public + */ +export const AuthenticationErrorCode = { + + /** Authentication is required but not provided */ + "AUTH_REQUIRED": "AUTH_REQUIRED", + + /** Provided credentials are invalid */ + "INVALID_CREDENTIALS": "INVALID_CREDENTIALS", + + /** Session has expired */ + "SESSION_EXPIRED": "SESSION_EXPIRED", + + /** User lacks permission for this operation */ + "FORBIDDEN": "FORBIDDEN", + + /** Agent is not authenticated */ + "NOT_AUTHENTICATED": "NOT_AUTHENTICATED" +} as const diff --git a/lib/errors/PaginationError.ts b/lib/errors/PaginationError.ts new file mode 100644 index 0000000..bb83a45 --- /dev/null +++ b/lib/errors/PaginationError.ts @@ -0,0 +1,63 @@ +/** + * Error class for pagination-related failures. + * + * Thrown when pagination operations fail, such as invalid cursors, + * cursor expiration, or pagination API errors. + * + * @example + * ```ts + * throw new PaginationError( + * "Invalid pagination cursor", + * "INVALID_CURSOR", + * "StepActorFollowers" + * ) + * ``` + * + * @public + */ + +import { TrotskyError } from "./TrotskyError" + +export class PaginationError extends TrotskyError { + + /** + * Creates a new PaginationError. + * + * @param message - Human-readable error message + * @param code - Machine-readable error code + * @param step - Optional step name where error occurred + * @param cause - Optional underlying error + */ + constructor ( + message: string, + code: string = "PAGINATION_ERROR", + step?: string, + cause?: Error + ) { + super(message, code, step, cause) + this.name = "PaginationError" + } +} + +/** + * Common pagination error codes. + * + * @public + */ +export const PaginationErrorCode = { + + /** Cursor is invalid or malformed */ + "INVALID_CURSOR": "INVALID_CURSOR", + + /** Cursor has expired */ + "CURSOR_EXPIRED": "CURSOR_EXPIRED", + + /** Failed to fetch next page */ + "FETCH_FAILED": "FETCH_FAILED", + + /** Limit parameter is invalid */ + "INVALID_LIMIT": "INVALID_LIMIT", + + /** No more pages available */ + "NO_MORE_PAGES": "NO_MORE_PAGES" +} as const diff --git a/lib/errors/RateLimitError.ts b/lib/errors/RateLimitError.ts new file mode 100644 index 0000000..e42f597 --- /dev/null +++ b/lib/errors/RateLimitError.ts @@ -0,0 +1,81 @@ +/** + * Error class for rate limiting failures. + * + * Thrown when API rate limits are exceeded or when rate limiting + * is enforced by Trotsky's internal rate limiter. + * + * @example + * ```ts + * throw new RateLimitError( + * "Rate limit exceeded. Retry after 60 seconds", + * "RATE_LIMIT_EXCEEDED", + * "StepSearchPosts", + * 60 + * ) + * ``` + * + * @public + */ + +import { TrotskyError } from "./TrotskyError" + +export class RateLimitError extends TrotskyError { + + /** + * Number of seconds until rate limit resets (if known). + */ + public readonly retryAfter?: number + + /** + * Creates a new RateLimitError. + * + * @param message - Human-readable error message + * @param code - Machine-readable error code + * @param step - Optional step name where error occurred + * @param retryAfter - Optional seconds until retry is allowed + * @param cause - Optional underlying error + */ + constructor ( + message: string, + code: string = "RATE_LIMIT_ERROR", + step?: string, + retryAfter?: number, + cause?: Error + ) { + super(message, code, step, cause) + this.name = "RateLimitError" + this.retryAfter = retryAfter + } + + /** + * Returns a JSON representation including retry information. + * + * @returns Object containing error details with retry info + */ + override toJSON () { + return { + ...super.toJSON(), + "retryAfter": this.retryAfter + } + } +} + +/** + * Common rate limit error codes. + * + * @public + */ +export const RateLimitErrorCode = { + + /** API rate limit exceeded */ + "RATE_LIMIT_EXCEEDED": "RATE_LIMIT_EXCEEDED", + + /** Too many requests */ + "TOO_MANY_REQUESTS": "TOO_MANY_REQUESTS", + + /** Daily quota exceeded */ + "QUOTA_EXCEEDED": "QUOTA_EXCEEDED", + + /** Concurrent request limit exceeded */ + "CONCURRENT_LIMIT": "CONCURRENT_LIMIT" +} as const diff --git a/lib/errors/TrotskyError.ts b/lib/errors/TrotskyError.ts new file mode 100644 index 0000000..41486b7 --- /dev/null +++ b/lib/errors/TrotskyError.ts @@ -0,0 +1,107 @@ +/** + * Base error class for all Trotsky-related errors. + * + * This class extends the standard Error class and provides additional + * context such as error codes, step information, and causal errors. + * + * @example + * ```ts + * throw new TrotskyError( + * "Failed to fetch actor", + * "ACTOR_NOT_FOUND", + * "StepActor" + * ) + * ``` + * + * @public + */ +export class TrotskyError extends Error { + + /** + * Error code for programmatic handling. + */ + public readonly code: string + + /** + * Name of the step where the error occurred (if applicable). + */ + public readonly step?: string + + /** + * Original error that caused this error (if applicable). + */ + public override readonly cause?: Error + + /** + * Timestamp when the error was created. + */ + public readonly timestamp: Date + + /** + * Creates a new TrotskyError. + * + * @param message - Human-readable error message + * @param code - Machine-readable error code + * @param step - Optional step name where error occurred + * @param cause - Optional underlying error that caused this error + */ + constructor ( + message: string, + code: string, + step?: string, + cause?: Error + ) { + super(message) + this.name = "TrotskyError" + this.code = code + this.step = step + this.cause = cause + this.timestamp = new Date() + + // Maintains proper stack trace for where error was thrown (V8 only) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, TrotskyError) + } + } + + /** + * Returns a JSON representation of the error. + * + * @returns Object containing error details + */ + toJSON () { + return { + "name": this.name, + "message": this.message, + "code": this.code, + "step": this.step, + "timestamp": this.timestamp.toISOString(), + "stack": this.stack, + "cause": this.cause ? { + "name": this.cause.name, + "message": this.cause.message + } : undefined + } + } + + /** + * Returns a formatted string representation of the error. + * + * @returns Formatted error string + */ + override toString (): string { + const parts = [ + `${this.name} [${this.code}]: ${this.message}` + ] + + if (this.step) { + parts.push(`at step: ${this.step}`) + } + + if (this.cause) { + parts.push(`caused by: ${this.cause.message}`) + } + + return parts.join(" ") + } +} diff --git a/lib/errors/ValidationError.ts b/lib/errors/ValidationError.ts new file mode 100644 index 0000000..244cefb --- /dev/null +++ b/lib/errors/ValidationError.ts @@ -0,0 +1,87 @@ +/** + * Error class for validation failures. + * + * Thrown when input validation fails, such as invalid URIs, + * malformed parameters, or constraint violations. + * + * @example + * ```ts + * throw new ValidationError( + * "Invalid AT URI format", + * "INVALID_URI", + * "StepPost", + * { uri: "invalid://uri" } + * ) + * ``` + * + * @public + */ + +import { TrotskyError } from "./TrotskyError" + +export class ValidationError extends TrotskyError { + + /** + * Additional validation details (field names, values, etc.). + */ + public readonly details?: Record + + /** + * Creates a new ValidationError. + * + * @param message - Human-readable error message + * @param code - Machine-readable error code + * @param step - Optional step name where error occurred + * @param details - Optional validation details + * @param cause - Optional underlying error + */ + constructor ( + message: string, + code: string = "VALIDATION_ERROR", + step?: string, + details?: Record, + cause?: Error + ) { + super(message, code, step, cause) + this.name = "ValidationError" + this.details = details + } + + /** + * Returns a JSON representation including validation details. + * + * @returns Object containing error details with validation info + */ + override toJSON () { + return { + ...super.toJSON(), + "details": this.details + } + } +} + +/** + * Common validation error codes. + * + * @public + */ +export const ValidationErrorCode = { + + /** URI is invalid or malformed */ + "INVALID_URI": "INVALID_URI", + + /** Parameter is missing */ + "MISSING_PARAM": "MISSING_PARAM", + + /** Parameter value is invalid */ + "INVALID_PARAM": "INVALID_PARAM", + + /** Parameter type is incorrect */ + "INVALID_TYPE": "INVALID_TYPE", + + /** Parameter value is out of range */ + "OUT_OF_RANGE": "OUT_OF_RANGE", + + /** Required field is missing */ + "REQUIRED_FIELD": "REQUIRED_FIELD" +} as const diff --git a/lib/errors/index.ts b/lib/errors/index.ts new file mode 100644 index 0000000..e648ccb --- /dev/null +++ b/lib/errors/index.ts @@ -0,0 +1,132 @@ +/** + * Central export point for all Trotsky error classes. + * + * This module provides custom error classes for different failure scenarios, + * making it easier to handle and diagnose errors in Trotsky operations. + * + * @example + * ```ts + * import { PaginationError, ValidationError } from "trotsky/errors" + * + * try { + * await trotsky.actor("handle").followers().run() + * } catch (error) { + * if (error instanceof PaginationError) { + * console.error("Pagination failed:", error.code) + * } else if (error instanceof ValidationError) { + * console.error("Validation failed:", error.details) + * } + * } + * ``` + * + * @module errors + * @packageDocumentation + */ + +// Base error class +export { TrotskyError } from "./TrotskyError" + +// Specific error classes +export { + PaginationError, + PaginationErrorCode +} from "./PaginationError" + +export { + AuthenticationError, + AuthenticationErrorCode +} from "./AuthenticationError" + +export { + RateLimitError, + RateLimitErrorCode +} from "./RateLimitError" + +export { + ValidationError, + ValidationErrorCode +} from "./ValidationError" + +// Import for use in fromXRPCError function +import { TrotskyError as TrotskyErrorClass } from "./TrotskyError" +import { AuthenticationError as AuthenticationErrorClass } from "./AuthenticationError" +import { RateLimitError as RateLimitErrorClass } from "./RateLimitError" + +/** + * Type guard to check if an error is a Trotsky error. + * + * @param error - Error to check + * @returns True if error is a TrotskyError instance + * + * @example + * ```ts + * if (isTrotskyError(error)) { + * console.log(error.code, error.step) + * } + * ``` + * + * @public + */ +export function isTrotskyError (error: unknown): error is import("./TrotskyError").TrotskyError { + return error instanceof Error && "code" in error && "step" in error +} + +/** + * Helper to create error from AT Protocol XRPCError. + * + * @param error - XRPC error from AT Protocol client + * @param step - Step name where error occurred + * @returns Appropriate Trotsky error instance + * + * @example + * ```ts + * try { + * await agent.getProfile({ actor: did }) + * } catch (err) { + * throw fromXRPCError(err, "StepActor") + * } + * ``` + * + * @public + */ +export function fromXRPCError (error: unknown, step?: string): import("./TrotskyError").TrotskyError { + // Type guard to check if error has expected properties + const err = error as { + "status"?: number; + "message"?: string; + "error"?: string; + "headers"?: Record; + } + + // Check for common XRPC error statuses + if (err.status === 401 || err.status === 403) { + return new AuthenticationErrorClass( + err.message || "Authentication failed", + err.status === 401 ? "NOT_AUTHENTICATED" : "FORBIDDEN", + step, + error instanceof Error ? error : undefined + ) + } + + if (err.status === 429) { + const retryAfter = err.headers?.["retry-after"] + ? parseInt(err.headers["retry-after"], 10) + : undefined + + return new RateLimitErrorClass( + err.message || "Rate limit exceeded", + "RATE_LIMIT_EXCEEDED", + step, + retryAfter, + error instanceof Error ? error : undefined + ) + } + + // Default to generic TrotskyError + return new TrotskyErrorClass( + err.message || "Unknown error", + err.error || "UNKNOWN_ERROR", + step, + error instanceof Error ? error : undefined + ) +} diff --git a/lib/types/actor.ts b/lib/types/actor.ts new file mode 100644 index 0000000..2d9a2f3 --- /dev/null +++ b/lib/types/actor.ts @@ -0,0 +1,56 @@ +/** + * Type definitions for actor-related operations. + * @module types/actor + */ + +import type { AppBskyActorDefs, AtUri } from "@atproto/api" + +/** + * Parameter type for identifying a single actor. + * Can be either a DID string, handle string, or AtUri object. + * + * @example + * ```ts + * const actor1: ActorParam = "did:plc:example" + * const actor2: ActorParam = "alice.bsky.social" + * const actor3: ActorParam = new AtUri("at://did:plc:example/...") + * ``` + * + * @public + */ +export type ActorParam = string | AtUri + +/** + * Parameter type for identifying multiple actors. + * + * @public + */ +export type ActorsParam = ActorParam[] + +/** + * Output type for a single actor profile. + * + * @public + */ +export type ActorOutput = AppBskyActorDefs.ProfileViewDetailed + +/** + * Output type for multiple actor profiles. + * + * @public + */ +export type ActorsOutput = AppBskyActorDefs.ProfileView[] + +/** + * Output type for basic actor profile information. + * + * @public + */ +export type ActorProfileView = AppBskyActorDefs.ProfileView + +/** + * Output type for detailed actor profile information. + * + * @public + */ +export type ActorProfileViewDetailed = AppBskyActorDefs.ProfileViewDetailed diff --git a/lib/types/index.ts b/lib/types/index.ts new file mode 100644 index 0000000..a8680ce --- /dev/null +++ b/lib/types/index.ts @@ -0,0 +1,47 @@ +/** + * Central export point for all shared type definitions. + * + * This module provides type definitions used throughout Trotsky, + * offering a single source of truth for common types across the library. + * + * @module types + * @packageDocumentation + */ + +// Actor types +export type { + ActorParam, + ActorsParam, + ActorOutput, + ActorsOutput, + ActorProfileView, + ActorProfileViewDetailed +} from "./actor" + +// Post types +export type { + PostUri, + PostsUris, + PostOutput, + PostsOutput, + PostRecord, + CreatePostParams, + ReplyParams +} from "./post" + +// List types +export type { + ListUri, + ListsUris, + ListOutput, + ListsOutput, + ListItemView, + ListPurpose +} from "./list" + +// Pagination types +export type { + PaginationParams, + PaginatedResponse, + PaginatedQueryParams +} from "./pagination" diff --git a/lib/types/list.ts b/lib/types/list.ts new file mode 100644 index 0000000..171d788 --- /dev/null +++ b/lib/types/list.ts @@ -0,0 +1,57 @@ +/** + * Type definitions for list-related operations. + * @module types/list + */ + +import type { AppBskyGraphDefs, AtUri } from "@atproto/api" + +/** + * Parameter type for identifying a single list by its URI. + * + * @example + * ```ts + * const list: ListUri = "at://did:plc:example/app.bsky.graph.list/listid" + * ``` + * + * @public + */ +export type ListUri = string | AtUri + +/** + * Parameter type for identifying multiple lists by their URIs. + * + * @public + */ +export type ListsUris = ListUri[] + +/** + * Output type for a single list view. + * + * @public + */ +export type ListOutput = AppBskyGraphDefs.ListView + +/** + * Output type for multiple list views. + * + * @public + */ +export type ListsOutput = AppBskyGraphDefs.ListView[] + +/** + * Output type for list item views (members of a list). + * + * @public + */ +export type ListItemView = AppBskyGraphDefs.ListItemView + +/** + * Purpose/type of a list. + * + * @public + */ +export type ListPurpose = + | "app.bsky.graph.defs#modlist" + | "app.bsky.graph.defs#curatelist" + | "app.bsky.graph.defs#referencelist" + | (string & {}) diff --git a/lib/types/pagination.ts b/lib/types/pagination.ts new file mode 100644 index 0000000..d9cb953 --- /dev/null +++ b/lib/types/pagination.ts @@ -0,0 +1,41 @@ +/** + * Type definitions for pagination-related operations. + * @module types/pagination + */ + +/** + * Standard pagination parameters used across AT Protocol APIs. + * + * @public + */ +export interface PaginationParams { + + /** Maximum number of items to return per page */ + "limit"?: number; + + /** Cursor for pagination (opaque string from previous response) */ + "cursor"?: string; +} + +/** + * Standard paginated response structure. + * + * @typeParam T - The type of items in the paginated response + * @public + */ +export interface PaginatedResponse { + + /** Array of items for this page */ + "items": T[]; + + /** Cursor for fetching the next page (undefined if no more pages) */ + "cursor"?: string; +} + +/** + * Query parameters with pagination support. + * + * @typeParam T - Additional query parameters specific to the endpoint + * @public + */ +export type PaginatedQueryParams> = T & PaginationParams diff --git a/lib/types/post.ts b/lib/types/post.ts new file mode 100644 index 0000000..c6229cb --- /dev/null +++ b/lib/types/post.ts @@ -0,0 +1,99 @@ +/** + * Type definitions for post-related operations. + * @module types/post + */ + +import type { AppBskyFeedDefs, AppBskyFeedPost, AtUri } from "@atproto/api" + +/** + * Parameter type for identifying a single post by its URI. + * + * @example + * ```ts + * const post1: PostUri = "at://did:plc:example/app.bsky.feed.post/postid" + * const post2: PostUri = new AtUri("at://...") + * ``` + * + * @public + */ +export type PostUri = string | AtUri + +/** + * Parameter type for identifying multiple posts by their URIs. + * + * @public + */ +export type PostsUris = PostUri[] + +/** + * Output type for a single post view. + * + * @public + */ +export type PostOutput = AppBskyFeedDefs.PostView + +/** + * Output type for multiple post views. + * + * @public + */ +export type PostsOutput = AppBskyFeedDefs.PostView[] + +/** + * Type for post record data. + * + * @public + */ +export type PostRecord = AppBskyFeedPost.Record + +/** + * Parameters for creating a new post. + * + * @public + */ +export interface CreatePostParams { + + /** The text content of the post */ + "text": string; + + /** Optional facets for rich text (links, mentions, etc.) */ + "facets"?: AppBskyFeedPost.Record["facets"]; + + /** Optional reply reference */ + "reply"?: AppBskyFeedPost.Record["reply"]; + + /** Optional embed (images, external links, etc.) */ + "embed"?: AppBskyFeedPost.Record["embed"]; + + /** Optional language tags */ + "langs"?: string[]; + + /** Optional labels */ + "labels"?: AppBskyFeedPost.Record["labels"]; + + /** Optional tags */ + "tags"?: string[]; + + /** Creation timestamp (defaults to now) */ + "createdAt"?: string; +} + +/** + * Parameters for replying to a post. + * + * @public + */ +export interface ReplyParams { + + /** The text content of the reply */ + "text": string; + + /** Optional facets for rich text */ + "facets"?: AppBskyFeedPost.Record["facets"]; + + /** Optional embed */ + "embed"?: AppBskyFeedPost.Record["embed"]; + + /** Optional language tags */ + "langs"?: string[]; +}