/** * Feature Service * * Main service class for feature operations. * Provides CRUD operations and execution for Ductape Features. * * Based on Ductape Features Code-First API documentation. */ import { IProductFeature } from '../types/productsBuilder.types'; import { IFeatureServiceConfig, IFeatureConfig, IDefineFeatureOptions, IExecuteFeatureOptions, IFeatureExecutionResult, IFeatureDispatchInput, IFeatureDispatchResult, ISendSignalOptions, IQueryFeatureOptions, IFeatureReplayInput, IFeatureReplayResult, IFeatureRestartInput, IFeatureRestartResult, IFeatureResumeInput, IFeatureResumeResult, IFeatureReplayFromStepInput, IFeatureStatusInput, IFeatureStatus, IFeatureCancelInput, IFeatureCancelResult, IFeatureHistoryInput, IFeatureHistory, IStepDetailInput, IStepDetail, IRelatedExecutionsInput, IRelatedExecutions, ICompareExecutionsInput, IExecutionComparison } from './types'; /** * Error class for feature-related errors */ export declare class FeatureError extends Error { readonly code: string; readonly details?: Record; constructor(message: string, code: string, details?: Record); static configurationError(message: string): FeatureError; static validationError(message: string, details?: Record): FeatureError; static notFoundError(message: string): FeatureError; static executionError(message: string, details?: Record): FeatureError; } /** * Main Feature Service class * Provides unified interface for feature management and execution */ export declare class FeatureService { /** Service configuration */ private config; /** ProductBuilder instances cache (keyed by product tag) */ private productBuilders; /** Local feature configurations */ private localConfigs; /** Feature API service for backend communication */ private featureApiService; /** LogService instance for logging operations */ private logService; /** Current product ID for logging */ private productId; private _privateKey; /** Cache manager for 3-tier caching */ private cacheManager; /** Local cache for cache configurations to avoid repeated API calls */ private cacheConfigCache; /** * Create a new FeatureService instance * @param config - Optional configuration for authentication and workspace context */ constructor(config?: IFeatureServiceConfig & { private_key: string; access_key: string; }); /** * Update service configuration */ updateConfig(config: IFeatureServiceConfig & { access_key: string; }): void; /** * Get auth payload for API calls */ private getAuthPayload; /** * Ensure FeatureApiService is initialized */ private ensureApiService; /** * Get service configuration */ getConfig(): IFeatureServiceConfig | null; /** * Create a new ProductBuilder instance */ private createNewProductBuilder; /** * Get or create a ProductBuilder instance for the given product tag */ private getProductBuilder; /** Per-process guard so the one-time "cold cache" bulk warm (fetchFeatures()) only ever runs * once per product per process, even under concurrent define() calls for many features at * boot -- without this, N concurrent cache-miss defines would each independently trigger * their own redundant bulk fetch. */ private featureCacheWarmed; private featureFingerprintCacheKey; /** * A stable, deterministic fingerprint of exactly the fields that define a feature's real * content (the same field set IFeatureConfig already establishes as "what a feature * definition consists of" -- see features.types.ts). Deliberately excludes `tag` (redundant * with the cache key) and any server-added bookkeeping fields (`_id`, timestamps) a remote * IProductFeature carries that a locally-compiled schema never has -- comparing those would * make every fingerprint mismatch even when the real, meaningful content is unchanged. */ private computeFeatureFingerprint; private getCachedFeatureFingerprint; private setCachedFeatureFingerprint; /** * Fetches every real feature currently persisted for `productTag` in ONE bulk remote call * (`builder.fetchFeatures()`, already existing -- the same call Ductape's own Workbench uses) * and warms the Redis fingerprint cache for all of them at once. Real answer to "if Redis is * empty, fetch all existing features, then check if the defined feature is already there * before recreating it" -- this runs at most ONCE per product per process (concurrent * define() calls for different features share the same in-flight promise, never triggering * their own separate bulk fetch). */ private warmFeatureCache; /** * Initialize logging service */ private initializeLogService; /** * Validate cache tag exists in product and return cache configuration */ private validateCache; /** * Create a new ProcessorService instance for job scheduling */ private createNewProcessor; /** * Create a new feature from JSON schema * * @example * ```ts * await ductape.feature.create('my-product', { * tag: 'order-fulfillment', * name: 'Order Fulfillment', * steps: [ * { tag: 'validate', type: 'action', app: 'orders', event: 'validate', input: {} }, * { tag: 'process', type: 'database_action', database: 'orders-db', event: 'create', input: {} }, * ], * envs: [{ slug: 'prd' }], * }); * ``` */ create(productTag: string, featureData: Partial): Promise; /** True only when the remote catalogue read path can observe the feature. */ exists(featureTag: string, productTag: string): Promise; /** * Fetch all features for a product * * @example * ```ts * const features = await ductape.feature.fetchAll('my-product'); * features.forEach(f => console.log(f.name, f.tag)); * ``` */ fetchAll(productTag?: string): Promise; /** * Fetch a specific feature by tag * * @example * ```ts * const feature = await ductape.feature.fetch('order-fulfillment', 'my-product'); * console.log(feature?.name, feature?.steps.length); * ``` */ fetch(featureTag: string, productTag?: string): Promise; /** * Update a feature * * @example * ```ts * await ductape.feature.update('order-fulfillment', 'my-product', { * description: 'Updated description', * options: { timeout: 3600000 }, * }); * ``` */ update(featureTag: string, productTag: string, updates: Partial): Promise; /** * Delete a feature * * @example * ```ts * await ductape.feature.delete('order-fulfillment', 'my-product'); * ``` */ delete(featureTag: string, productTag: string): Promise; /** * Define a feature using the code-first API * Compiles the handler to JSON schema and creates the feature in one step * * @example * ```ts * const orderFeature = await ductape.feature.define({ * product: 'my-product', * tag: 'order-fulfillment', * name: 'Order Fulfillment', * handler: async (ctx) => { * const validation = await ctx.step('validate', async () => { * return ctx.api.run({ * app: 'inventory-service', * action: 'validate-order', * input: { body: ctx.input }, * }); * }); * * if (!validation.valid) { * return { success: false, error: validation.reason }; * } * * return { success: true }; * }, * }); * ``` */ define, TOutput = unknown>(options: IDefineFeatureOptions): Promise>; /** Compile several definitions without performing any remote writes. */ compileMany(definitions: IDefineFeatureOptions[]): Promise; /** * Publish a definition set with read-back verification. If a write fails, newly * created records are removed and pre-existing records are restored. */ publishMany(productTag: string, definitions: IDefineFeatureOptions[], options?: { dryRun?: boolean; }): Promise<{ product: string; dry_run: boolean; features: IProductFeature[]; }>; /** * Execute a feature * * @example * ```ts * const result = await ductape.feature.execute({ * product: 'my-product', * env: 'production', * tag: 'order-fulfillment', * input: { * order_id: 'ORD-12345', * items: [{ product_id: 'PROD-1', quantity: 2 }], * }, * }); * * console.log(result.status); // 'completed' | 'failed' | 'rolled_back' * console.log(result.output); * ``` */ execute(options: IExecuteFeatureOptions): Promise>; /** * Dispatch a feature to run as a scheduled job * * @example * ```ts * // Schedule a feature to run in 1 hour * const result = await ductape.feature.dispatch({ * product: 'my-product', * env: 'production', * feature: 'order-fulfillment', * input: { order_id: 'ORD-123' }, * schedule: { start_at: Date.now() + 3600000 }, * }); * * // Run on a cron schedule * const result = await ductape.feature.dispatch({ * product: 'my-product', * env: 'production', * feature: 'daily-report', * input: {}, * schedule: { cron: '0 0 * * *' }, // Daily at midnight * }); * ``` */ dispatch(data: IFeatureDispatchInput): Promise; /** * Send a signal to a running feature * * @example * ```ts * await ductape.feature.signal({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * signal: 'approve', * payload: { approver_id: 'user-456', comments: 'Approved' }, * }); * ``` */ signal(options: ISendSignalOptions): Promise; /** * Query a running feature * * @example * ```ts * const status = await ductape.feature.query({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * query: 'getStatus', * }); * * console.log(status.current_step, status.progress); * ``` */ query(options: IQueryFeatureOptions): Promise; /** * Replay a feature with the same input * * @example * ```ts * const result = await ductape.feature.replay({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * reason: 'Debugging payment failure', * }); * ``` */ replay(options: IFeatureReplayInput): Promise; /** * Restart a feature with new or modified input * * @example * ```ts * const result = await ductape.feature.restart({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * input: { email: 'corrected@email.com' }, * reason: 'Customer email was incorrect', * }); * ``` */ restart(options: IFeatureRestartInput): Promise; /** * Resume a paused or failed feature from where it stopped * * @example * ```ts * const result = await ductape.feature.resume({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * from_checkpoint: 'payment-complete', * }); * ``` */ resume(options: IFeatureResumeInput): Promise; /** * Replay a feature starting from a specific step * * @example * ```ts * const result = await ductape.feature.replayFromStep({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * from_step: 'process-payment', * }); * ``` */ replayFromStep(options: IFeatureReplayFromStepInput): Promise; /** * Get the status of a feature execution * * @example * ```ts * const status = await ductape.feature.status({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * }); * ``` */ status(options: IFeatureStatusInput): Promise; /** * Cancel a running feature * * @example * ```ts * const result = await ductape.feature.cancel({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * reason: 'User requested cancellation', * }); * ``` */ cancel(options: IFeatureCancelInput): Promise; /** * Get the execution history of a feature * * @example * ```ts * const history = await ductape.feature.history({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * }); * ``` */ history(options: IFeatureHistoryInput): Promise; /** * Get detailed information about a specific step * * @example * ```ts * const detail = await ductape.feature.stepDetail({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * step_tag: 'process-payment', * }); * ``` */ stepDetail(options: IStepDetailInput): Promise; /** * List all related executions (replays, restarts, resumes) * * @example * ```ts * const related = await ductape.feature.relatedExecutions({ * product: 'my-product', * env: 'production', * feature_id: 'wf-123', * }); * ``` */ relatedExecutions(options: IRelatedExecutionsInput): Promise; /** * Compare two feature executions * * @example * ```ts * const comparison = await ductape.feature.compare({ * product: 'my-product', * env: 'production', * feature_ids: ['wf-123', 'wf-456'], * }); * ``` */ compare(options: ICompareExecutionsInput): Promise; /** * Convert IProductFeature to IFeatureConfig */ private productFeatureToConfig; } /** * Result of feature.define() - contains the compiled schema and handler */ export interface IDefinedFeature, TOutput = unknown> { /** Feature tag */ tag: string; /** Feature name */ name: string; /** Original handler function */ handler: (ctx: any) => Promise; /** Compile to JSON schema */ compile: () => IProductFeature; /** The compiled JSON schema */ schema: IProductFeature; } export declare class FeatureCompilationError extends Error { readonly featureTag: string; readonly stepTag: string; constructor(featureTag: string, stepTag: string, message: string); } export declare const featureService: FeatureService; export default FeatureService;