From a3e26822f9d06f2945565c5d853ffbb473e1fa7d Mon Sep 17 00:00:00 2001 From: Pierre Romera Date: Fri, 28 Nov 2025 23:34:43 +0100 Subject: [PATCH 1/3] refactor: move types and errors out of core --- docs/.vitepress/config.mts | 1 + lib/config/TrotskyConfig.ts | 183 ++++++++++++++++++++++++++++++ lib/config/index.ts | 21 ++++ lib/errors/AuthenticationError.ts | 57 ++++++++++ lib/errors/PaginationError.ts | 57 ++++++++++ lib/errors/RateLimitError.ts | 76 +++++++++++++ lib/errors/TrotskyError.ts | 106 +++++++++++++++++ lib/errors/ValidationError.ts | 80 +++++++++++++ lib/errors/index.ts | 123 ++++++++++++++++++++ lib/types/actor.ts | 56 +++++++++ lib/types/index.ts | 47 ++++++++ lib/types/list.ts | 57 ++++++++++ lib/types/pagination.ts | 37 ++++++ lib/types/post.ts | 87 ++++++++++++++ 14 files changed, 988 insertions(+) create mode 100644 lib/config/TrotskyConfig.ts create mode 100644 lib/config/index.ts create mode 100644 lib/errors/AuthenticationError.ts create mode 100644 lib/errors/PaginationError.ts create mode 100644 lib/errors/RateLimitError.ts create mode 100644 lib/errors/TrotskyError.ts create mode 100644 lib/errors/ValidationError.ts create mode 100644 lib/errors/index.ts create mode 100644 lib/types/actor.ts create mode 100644 lib/types/index.ts create mode 100644 lib/types/list.ts create mode 100644 lib/types/pagination.ts create mode 100644 lib/types/post.ts 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/lib/config/TrotskyConfig.ts b/lib/config/TrotskyConfig.ts new file mode 100644 index 0000000..9bc1899 --- /dev/null +++ b/lib/config/TrotskyConfig.ts @@ -0,0 +1,183 @@ +/** + * 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..c8d7ef1 --- /dev/null +++ b/lib/errors/AuthenticationError.ts @@ -0,0 +1,57 @@ +/** + * 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..c834307 --- /dev/null +++ b/lib/errors/PaginationError.ts @@ -0,0 +1,57 @@ +/** + * 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..2734485 --- /dev/null +++ b/lib/errors/RateLimitError.ts @@ -0,0 +1,76 @@ +/** + * 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..6fee027 --- /dev/null +++ b/lib/errors/TrotskyError.ts @@ -0,0 +1,106 @@ +/** + * 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..103dc61 --- /dev/null +++ b/lib/errors/ValidationError.ts @@ -0,0 +1,80 @@ +/** + * 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..b445ddb --- /dev/null +++ b/lib/errors/index.ts @@ -0,0 +1,123 @@ +/** + * 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" + +/** + * 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: any, step?: string): import("./TrotskyError").TrotskyError { + const { TrotskyError } = require("./TrotskyError") + const { AuthenticationError } = require("./AuthenticationError") + const { RateLimitError } = require("./RateLimitError") + + // Check for common XRPC error statuses + if (error.status === 401 || error.status === 403) { + return new AuthenticationError( + error.message || "Authentication failed", + error.status === 401 ? "NOT_AUTHENTICATED" : "FORBIDDEN", + step, + error + ) + } + + if (error.status === 429) { + const retryAfter = error.headers?.["retry-after"] + ? parseInt(error.headers["retry-after"], 10) + : undefined + + return new RateLimitError( + error.message || "Rate limit exceeded", + "RATE_LIMIT_EXCEEDED", + step, + retryAfter, + error + ) + } + + // Default to generic TrotskyError + return new TrotskyError( + error.message || "Unknown error", + error.error || "UNKNOWN_ERROR", + step, + error + ) +} 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..afbda84 --- /dev/null +++ b/lib/types/pagination.ts @@ -0,0 +1,37 @@ +/** + * 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..23a6108 --- /dev/null +++ b/lib/types/post.ts @@ -0,0 +1,87 @@ +/** + * 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[] +} From 2c1ff1b337c8828594137cd225cd5da4695eb46d Mon Sep 17 00:00:00 2001 From: Pierre Romera Date: Fri, 28 Nov 2025 23:34:53 +0100 Subject: [PATCH 2/3] doc: present architecture --- docs/guide/architecture.md | 425 +++++++++++++++++++++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 docs/guide/architecture.md 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) From f90a58f2ed70098a7c70b43bb3f326e6834177c4 Mon Sep 17 00:00:00 2001 From: Pierre Romera Date: Fri, 28 Nov 2025 22:43:09 +0000 Subject: [PATCH 3/3] refactor: lint --- lib/config/TrotskyConfig.ts | 133 ++++++++++++++++++------------ lib/errors/AuthenticationError.ts | 16 ++-- lib/errors/PaginationError.ts | 16 ++-- lib/errors/RateLimitError.ts | 13 ++- lib/errors/TrotskyError.ts | 1 + lib/errors/ValidationError.ts | 19 +++-- lib/errors/index.ts | 47 ++++++----- lib/types/pagination.ts | 12 ++- lib/types/post.ts | 36 +++++--- 9 files changed, 184 insertions(+), 109 deletions(-) diff --git a/lib/config/TrotskyConfig.ts b/lib/config/TrotskyConfig.ts index 9bc1899..f487edd 100644 --- a/lib/config/TrotskyConfig.ts +++ b/lib/config/TrotskyConfig.ts @@ -13,12 +13,15 @@ * @public */ export interface LoggingConfig { + /** Enable or disable logging */ - enabled: boolean + "enabled": boolean; + /** Minimum log level to output */ - level: "debug" | "info" | "warn" | "error" + "level": "debug" | "info" | "warn" | "error"; + /** Custom logger function (optional) */ - logger?: (level: string, message: string, meta?: Record) => void + "logger"?: (level: string, message: string, meta?: Record) => void; } /** @@ -27,12 +30,15 @@ export interface LoggingConfig { * @public */ export interface PaginationConfig { + /** Default page size for paginated requests */ - defaultLimit: number + "defaultLimit": number; + /** Maximum page size allowed */ - maxLimit: number + "maxLimit": number; + /** Enable automatic pagination */ - autoPaginate: boolean + "autoPaginate": boolean; } /** @@ -41,18 +47,24 @@ export interface PaginationConfig { * @public */ export interface RetryConfig { + /** Enable automatic retries on failure */ - enabled: boolean + "enabled": boolean; + /** Maximum number of retry attempts */ - maxAttempts: number + "maxAttempts": number; + /** Backoff strategy for retries */ - backoff: "linear" | "exponential" + "backoff": "linear" | "exponential"; + /** Initial delay between retries (milliseconds) */ - initialDelay: number + "initialDelay": number; + /** Maximum delay between retries (milliseconds) */ - maxDelay: number + "maxDelay": number; + /** HTTP status codes that should trigger a retry */ - retryableStatusCodes: number[] + "retryableStatusCodes": number[]; } /** @@ -61,14 +73,18 @@ export interface RetryConfig { * @public */ export interface RateLimitConfig { + /** Enable built-in rate limiting */ - enabled: boolean + "enabled": boolean; + /** Maximum requests per minute */ - requestsPerMinute: number + "requestsPerMinute": number; + /** Maximum concurrent requests */ - concurrentRequests: number + "concurrentRequests": number; + /** Behavior when rate limit is hit */ - onLimitReached: "throw" | "queue" | "drop" + "onLimitReached": "throw" | "queue" | "drop"; } /** @@ -77,14 +93,18 @@ export interface RateLimitConfig { * @public */ export interface CacheConfig { + /** Enable caching */ - enabled: boolean + "enabled": boolean; + /** Default cache TTL in milliseconds */ - defaultTTL: number + "defaultTTL": number; + /** Maximum cache size (number of entries) */ - maxSize: number + "maxSize": number; + /** Cache key prefix */ - keyPrefix: string + "keyPrefix": string; } /** @@ -93,16 +113,21 @@ export interface CacheConfig { * @public */ export interface TrotskyConfig { + /** Logging configuration */ - logging: LoggingConfig + "logging": LoggingConfig; + /** Pagination configuration */ - pagination: PaginationConfig + "pagination": PaginationConfig; + /** Retry configuration */ - retry: RetryConfig + "retry": RetryConfig; + /** Rate limiting configuration */ - rateLimit: RateLimitConfig + "rateLimit": RateLimitConfig; + /** Caching configuration */ - cache: CacheConfig + "cache": CacheConfig; } /** @@ -122,34 +147,34 @@ export type PartialTrotskyConfig = { * @public */ export const defaultConfig: TrotskyConfig = { - logging: { - enabled: false, - level: "info" + "logging": { + "enabled": false, + "level": "info" }, - pagination: { - defaultLimit: 50, - maxLimit: 100, - autoPaginate: true + "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] + "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" + "rateLimit": { + "enabled": false, + "requestsPerMinute": 60, + "concurrentRequests": 10, + "onLimitReached": "queue" }, - cache: { - enabled: false, - defaultTTL: 60000, // 1 minute - maxSize: 1000, - keyPrefix: "trotsky:" + "cache": { + "enabled": false, + "defaultTTL": 60000, // 1 minute + "maxSize": 1000, + "keyPrefix": "trotsky:" } } @@ -174,10 +199,10 @@ export function mergeConfig (config?: PartialTrotskyConfig): TrotskyConfig { } 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 } + "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/errors/AuthenticationError.ts b/lib/errors/AuthenticationError.ts index c8d7ef1..928559f 100644 --- a/lib/errors/AuthenticationError.ts +++ b/lib/errors/AuthenticationError.ts @@ -19,6 +19,7 @@ import { TrotskyError } from "./TrotskyError" export class AuthenticationError extends TrotskyError { + /** * Creates a new AuthenticationError. * @@ -44,14 +45,19 @@ export class AuthenticationError extends TrotskyError { * @public */ export const AuthenticationErrorCode = { + /** Authentication is required but not provided */ - AUTH_REQUIRED: "AUTH_REQUIRED", + "AUTH_REQUIRED": "AUTH_REQUIRED", + /** Provided credentials are invalid */ - INVALID_CREDENTIALS: "INVALID_CREDENTIALS", + "INVALID_CREDENTIALS": "INVALID_CREDENTIALS", + /** Session has expired */ - SESSION_EXPIRED: "SESSION_EXPIRED", + "SESSION_EXPIRED": "SESSION_EXPIRED", + /** User lacks permission for this operation */ - FORBIDDEN: "FORBIDDEN", + "FORBIDDEN": "FORBIDDEN", + /** Agent is not authenticated */ - NOT_AUTHENTICATED: "NOT_AUTHENTICATED" + "NOT_AUTHENTICATED": "NOT_AUTHENTICATED" } as const diff --git a/lib/errors/PaginationError.ts b/lib/errors/PaginationError.ts index c834307..bb83a45 100644 --- a/lib/errors/PaginationError.ts +++ b/lib/errors/PaginationError.ts @@ -19,6 +19,7 @@ import { TrotskyError } from "./TrotskyError" export class PaginationError extends TrotskyError { + /** * Creates a new PaginationError. * @@ -44,14 +45,19 @@ export class PaginationError extends TrotskyError { * @public */ export const PaginationErrorCode = { + /** Cursor is invalid or malformed */ - INVALID_CURSOR: "INVALID_CURSOR", + "INVALID_CURSOR": "INVALID_CURSOR", + /** Cursor has expired */ - CURSOR_EXPIRED: "CURSOR_EXPIRED", + "CURSOR_EXPIRED": "CURSOR_EXPIRED", + /** Failed to fetch next page */ - FETCH_FAILED: "FETCH_FAILED", + "FETCH_FAILED": "FETCH_FAILED", + /** Limit parameter is invalid */ - INVALID_LIMIT: "INVALID_LIMIT", + "INVALID_LIMIT": "INVALID_LIMIT", + /** No more pages available */ - NO_MORE_PAGES: "NO_MORE_PAGES" + "NO_MORE_PAGES": "NO_MORE_PAGES" } as const diff --git a/lib/errors/RateLimitError.ts b/lib/errors/RateLimitError.ts index 2734485..e42f597 100644 --- a/lib/errors/RateLimitError.ts +++ b/lib/errors/RateLimitError.ts @@ -20,6 +20,7 @@ import { TrotskyError } from "./TrotskyError" export class RateLimitError extends TrotskyError { + /** * Number of seconds until rate limit resets (if known). */ @@ -65,12 +66,16 @@ export class RateLimitError extends TrotskyError { * @public */ export const RateLimitErrorCode = { + /** API rate limit exceeded */ - RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED", + "RATE_LIMIT_EXCEEDED": "RATE_LIMIT_EXCEEDED", + /** Too many requests */ - TOO_MANY_REQUESTS: "TOO_MANY_REQUESTS", + "TOO_MANY_REQUESTS": "TOO_MANY_REQUESTS", + /** Daily quota exceeded */ - QUOTA_EXCEEDED: "QUOTA_EXCEEDED", + "QUOTA_EXCEEDED": "QUOTA_EXCEEDED", + /** Concurrent request limit exceeded */ - CONCURRENT_LIMIT: "CONCURRENT_LIMIT" + "CONCURRENT_LIMIT": "CONCURRENT_LIMIT" } as const diff --git a/lib/errors/TrotskyError.ts b/lib/errors/TrotskyError.ts index 6fee027..41486b7 100644 --- a/lib/errors/TrotskyError.ts +++ b/lib/errors/TrotskyError.ts @@ -16,6 +16,7 @@ * @public */ export class TrotskyError extends Error { + /** * Error code for programmatic handling. */ diff --git a/lib/errors/ValidationError.ts b/lib/errors/ValidationError.ts index 103dc61..244cefb 100644 --- a/lib/errors/ValidationError.ts +++ b/lib/errors/ValidationError.ts @@ -20,6 +20,7 @@ import { TrotskyError } from "./TrotskyError" export class ValidationError extends TrotskyError { + /** * Additional validation details (field names, values, etc.). */ @@ -65,16 +66,22 @@ export class ValidationError extends TrotskyError { * @public */ export const ValidationErrorCode = { + /** URI is invalid or malformed */ - INVALID_URI: "INVALID_URI", + "INVALID_URI": "INVALID_URI", + /** Parameter is missing */ - MISSING_PARAM: "MISSING_PARAM", + "MISSING_PARAM": "MISSING_PARAM", + /** Parameter value is invalid */ - INVALID_PARAM: "INVALID_PARAM", + "INVALID_PARAM": "INVALID_PARAM", + /** Parameter type is incorrect */ - INVALID_TYPE: "INVALID_TYPE", + "INVALID_TYPE": "INVALID_TYPE", + /** Parameter value is out of range */ - OUT_OF_RANGE: "OUT_OF_RANGE", + "OUT_OF_RANGE": "OUT_OF_RANGE", + /** Required field is missing */ - REQUIRED_FIELD: "REQUIRED_FIELD" + "REQUIRED_FIELD": "REQUIRED_FIELD" } as const diff --git a/lib/errors/index.ts b/lib/errors/index.ts index b445ddb..e648ccb 100644 --- a/lib/errors/index.ts +++ b/lib/errors/index.ts @@ -47,6 +47,11 @@ export { 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. * @@ -84,40 +89,44 @@ export function isTrotskyError (error: unknown): error is import("./TrotskyError * * @public */ -export function fromXRPCError (error: any, step?: string): import("./TrotskyError").TrotskyError { - const { TrotskyError } = require("./TrotskyError") - const { AuthenticationError } = require("./AuthenticationError") - const { RateLimitError } = require("./RateLimitError") +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 (error.status === 401 || error.status === 403) { - return new AuthenticationError( - error.message || "Authentication failed", - error.status === 401 ? "NOT_AUTHENTICATED" : "FORBIDDEN", + if (err.status === 401 || err.status === 403) { + return new AuthenticationErrorClass( + err.message || "Authentication failed", + err.status === 401 ? "NOT_AUTHENTICATED" : "FORBIDDEN", step, - error + error instanceof Error ? error : undefined ) } - if (error.status === 429) { - const retryAfter = error.headers?.["retry-after"] - ? parseInt(error.headers["retry-after"], 10) + if (err.status === 429) { + const retryAfter = err.headers?.["retry-after"] + ? parseInt(err.headers["retry-after"], 10) : undefined - return new RateLimitError( - error.message || "Rate limit exceeded", + return new RateLimitErrorClass( + err.message || "Rate limit exceeded", "RATE_LIMIT_EXCEEDED", step, retryAfter, - error + error instanceof Error ? error : undefined ) } // Default to generic TrotskyError - return new TrotskyError( - error.message || "Unknown error", - error.error || "UNKNOWN_ERROR", + return new TrotskyErrorClass( + err.message || "Unknown error", + err.error || "UNKNOWN_ERROR", step, - error + error instanceof Error ? error : undefined ) } diff --git a/lib/types/pagination.ts b/lib/types/pagination.ts index afbda84..d9cb953 100644 --- a/lib/types/pagination.ts +++ b/lib/types/pagination.ts @@ -9,10 +9,12 @@ * @public */ export interface PaginationParams { + /** Maximum number of items to return per page */ - limit?: number + "limit"?: number; + /** Cursor for pagination (opaque string from previous response) */ - cursor?: string + "cursor"?: string; } /** @@ -22,10 +24,12 @@ export interface PaginationParams { * @public */ export interface PaginatedResponse { + /** Array of items for this page */ - items: T[] + "items": T[]; + /** Cursor for fetching the next page (undefined if no more pages) */ - cursor?: string + "cursor"?: string; } /** diff --git a/lib/types/post.ts b/lib/types/post.ts index 23a6108..c6229cb 100644 --- a/lib/types/post.ts +++ b/lib/types/post.ts @@ -52,22 +52,30 @@ export type PostRecord = AppBskyFeedPost.Record * @public */ export interface CreatePostParams { + /** The text content of the post */ - text: string + "text": string; + /** Optional facets for rich text (links, mentions, etc.) */ - facets?: AppBskyFeedPost.Record["facets"] + "facets"?: AppBskyFeedPost.Record["facets"]; + /** Optional reply reference */ - reply?: AppBskyFeedPost.Record["reply"] + "reply"?: AppBskyFeedPost.Record["reply"]; + /** Optional embed (images, external links, etc.) */ - embed?: AppBskyFeedPost.Record["embed"] + "embed"?: AppBskyFeedPost.Record["embed"]; + /** Optional language tags */ - langs?: string[] + "langs"?: string[]; + /** Optional labels */ - labels?: AppBskyFeedPost.Record["labels"] + "labels"?: AppBskyFeedPost.Record["labels"]; + /** Optional tags */ - tags?: string[] + "tags"?: string[]; + /** Creation timestamp (defaults to now) */ - createdAt?: string + "createdAt"?: string; } /** @@ -76,12 +84,16 @@ export interface CreatePostParams { * @public */ export interface ReplyParams { + /** The text content of the reply */ - text: string + "text": string; + /** Optional facets for rich text */ - facets?: AppBskyFeedPost.Record["facets"] + "facets"?: AppBskyFeedPost.Record["facets"]; + /** Optional embed */ - embed?: AppBskyFeedPost.Record["embed"] + "embed"?: AppBskyFeedPost.Record["embed"]; + /** Optional language tags */ - langs?: string[] + "langs"?: string[]; }