@doclo/core 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -928,4 +928,164 @@ declare function getAllModels(): ResolvedModelMetadata[];
928
928
  */
929
929
  declare function clearModelRegistry(): void;
930
930
 
931
- export { type AcceptedMimeType, AccessMethod, type AllAutoVariables, type AutoVariablesForNode, type BaseProviderConfig, type CategorizeAutoVariables, type DocumentMimeType, type ExtractAutoVariables, type FeatureName, FlowInputValidationError, type InputRequirements, type ModelMetadata, type ModelQueryFilter, type NormalizedCapabilities, type NormalizedFeatures, type NormalizedProviderMetadata, type OCRProviderConfig, type OutputFormatSupport, type ParseAutoVariables, type PromptVariables, type ProviderConfig, type ProviderInputType, type ProviderInstance, type ProviderMetadataWithModels, type ProviderQueryFilter, type ProviderRegistry, type ProviderSecrets, ProviderVendor, type ResolvedModelMetadata, type VLMProviderConfig, bufferToBase64, bufferToDataUri, buildProviderFromConfig, buildProvidersFromConfigs, clearModelRegistry, clearProviderRegistry, defineMarkerProvider, defineSuryaProvider, defineVLMProvider, detectDocumentType, detectMimeTypeFromBase64, detectMimeTypeFromBase64Async, detectMimeTypeFromBytes, extractBase64, getAllModels, getAllProviders, getCheapestProviderFor, getModelsForNode, getProviderById, getProvidersBySource, getProvidersForLargeFiles, getProvidersForMimeType, isPDFDocument, queryModels, queryProviders, registerProviderMetadata, registerProviderWithModels, resolveDocument, resolveModelMetadata, validateFlowInputFormat, validateMimeType, validateMimeTypeAsync };
931
+ /**
932
+ * @doclo/core - Retry Utilities
933
+ *
934
+ * Shared retry infrastructure for LLM and OCR providers.
935
+ * Includes exponential backoff, circuit breaker pattern, and error classification.
936
+ */
937
+ /**
938
+ * Configuration for retry behavior
939
+ */
940
+ interface RetryConfig {
941
+ /** Maximum number of retry attempts (default: 2) */
942
+ maxRetries?: number;
943
+ /** Base delay in milliseconds between retries (default: 1000) */
944
+ retryDelay?: number;
945
+ /** Enable exponential backoff (default: true) */
946
+ useExponentialBackoff?: boolean;
947
+ /** Maximum delay cap in milliseconds (default: 30000) */
948
+ maxDelay?: number;
949
+ }
950
+ /**
951
+ * Configuration for circuit breaker behavior
952
+ */
953
+ interface CircuitBreakerConfig {
954
+ /** Number of consecutive failures before opening circuit (default: 3) */
955
+ threshold?: number;
956
+ /** Time in milliseconds before trying again after circuit opens (default: 30000) */
957
+ resetTimeout?: number;
958
+ }
959
+ /**
960
+ * Internal state for a circuit breaker
961
+ */
962
+ interface CircuitBreakerState {
963
+ consecutiveFailures: number;
964
+ lastFailureTime?: number;
965
+ isOpen: boolean;
966
+ }
967
+ /**
968
+ * Circuit breaker interface
969
+ */
970
+ interface CircuitBreaker {
971
+ /** Check if circuit is currently open (should skip requests) */
972
+ isOpen(): boolean;
973
+ /** Record a successful request (resets failure count) */
974
+ recordSuccess(): void;
975
+ /** Record a failed request (may open circuit) */
976
+ recordFailure(): void;
977
+ /** Get current state for inspection */
978
+ getState(): CircuitBreakerState;
979
+ }
980
+ /**
981
+ * Options for the withRetry wrapper
982
+ */
983
+ interface WithRetryOptions<T> extends RetryConfig {
984
+ /** Called before each retry attempt (for logging/observability) */
985
+ onRetry?: (attempt: number, error: Error, delay: number) => void | Promise<void>;
986
+ /** Override to parse Retry-After header from response errors */
987
+ getRetryAfter?: (error: Error) => number | undefined;
988
+ /** Circuit breaker to use (optional) */
989
+ circuitBreaker?: CircuitBreaker;
990
+ }
991
+ /** Default retry configuration */
992
+ declare const DEFAULT_RETRY_CONFIG: Required<RetryConfig>;
993
+ /** Default circuit breaker configuration */
994
+ declare const DEFAULT_CIRCUIT_BREAKER_CONFIG: Required<CircuitBreakerConfig>;
995
+ /**
996
+ * Determines if an error is retryable based on its message content.
997
+ *
998
+ * Retryable errors include:
999
+ * - HTTP 408, 429, 500, 502, 503, 504
1000
+ * - Timeout errors
1001
+ * - Rate limit errors
1002
+ * - Network errors (ECONNRESET, ETIMEDOUT, etc.)
1003
+ *
1004
+ * Non-retryable errors include:
1005
+ * - HTTP 400, 401, 403, 404 (client errors)
1006
+ * - Business logic failures
1007
+ *
1008
+ * @param error - The error to classify
1009
+ * @returns true if the error is retryable
1010
+ */
1011
+ declare function isRetryableError(error: Error): boolean;
1012
+ /**
1013
+ * Extracts HTTP status code from an error message if present.
1014
+ *
1015
+ * @param error - The error to extract status from
1016
+ * @returns The HTTP status code or undefined
1017
+ */
1018
+ declare function extractStatusCode(error: Error): number | undefined;
1019
+ /**
1020
+ * Parses Retry-After header value from error message or response.
1021
+ * Supports both seconds (integer) and HTTP-date formats.
1022
+ *
1023
+ * @param error - Error that may contain Retry-After information
1024
+ * @returns Delay in milliseconds, or undefined if not found
1025
+ */
1026
+ declare function parseRetryAfter(error: Error): number | undefined;
1027
+ /**
1028
+ * Calculates the delay before the next retry attempt.
1029
+ *
1030
+ * With exponential backoff enabled (default):
1031
+ * - Attempt 1: baseDelay * 2^0 = 1x baseDelay
1032
+ * - Attempt 2: baseDelay * 2^1 = 2x baseDelay
1033
+ * - Attempt 3: baseDelay * 2^2 = 4x baseDelay
1034
+ * Plus random jitter (0-1000ms) to prevent thundering herd.
1035
+ *
1036
+ * @param attempt - Current attempt number (1-indexed)
1037
+ * @param config - Retry configuration
1038
+ * @returns Delay in milliseconds
1039
+ */
1040
+ declare function calculateRetryDelay(attempt: number, config?: RetryConfig): number;
1041
+ /**
1042
+ * Creates or retrieves a circuit breaker for a given key.
1043
+ *
1044
+ * Circuit breakers prevent cascading failures by:
1045
+ * 1. Tracking consecutive failures per provider/endpoint
1046
+ * 2. "Opening" the circuit after threshold failures (skipping requests)
1047
+ * 3. Allowing a retry after resetTimeout (half-open state)
1048
+ * 4. Closing the circuit on success
1049
+ *
1050
+ * @param key - Unique identifier (e.g., "datalab:surya" or "openai:gpt-4")
1051
+ * @param config - Circuit breaker configuration
1052
+ * @returns CircuitBreaker instance
1053
+ */
1054
+ declare function createCircuitBreaker(key: string, config?: CircuitBreakerConfig): CircuitBreaker;
1055
+ /**
1056
+ * Clears all circuit breakers. Useful for testing.
1057
+ */
1058
+ declare function clearCircuitBreakers(): void;
1059
+ /**
1060
+ * Gets the circuit breaker for a key without creating one.
1061
+ *
1062
+ * @param key - Unique identifier
1063
+ * @returns CircuitBreaker or undefined if not found
1064
+ */
1065
+ declare function getCircuitBreaker(key: string): CircuitBreaker | undefined;
1066
+ /**
1067
+ * Wraps an async function with retry logic.
1068
+ *
1069
+ * @example
1070
+ * ```typescript
1071
+ * const result = await withRetry(
1072
+ * () => fetchWithTimeout(url, options),
1073
+ * {
1074
+ * maxRetries: 3,
1075
+ * retryDelay: 1000,
1076
+ * useExponentialBackoff: true,
1077
+ * onRetry: (attempt, error, delay) => {
1078
+ * console.log(`Retry ${attempt} after ${delay}ms: ${error.message}`);
1079
+ * }
1080
+ * }
1081
+ * );
1082
+ * ```
1083
+ *
1084
+ * @param fn - Async function to retry
1085
+ * @param options - Retry options
1086
+ * @returns Result of the function
1087
+ * @throws Last error if all retries fail
1088
+ */
1089
+ declare function withRetry<T>(fn: () => Promise<T>, options?: WithRetryOptions<T>): Promise<T>;
1090
+
1091
+ export { type AcceptedMimeType, AccessMethod, type AllAutoVariables, type AutoVariablesForNode, type BaseProviderConfig, type CategorizeAutoVariables, type CircuitBreaker, type CircuitBreakerConfig, type CircuitBreakerState, DEFAULT_CIRCUIT_BREAKER_CONFIG, DEFAULT_RETRY_CONFIG, type DocumentMimeType, type ExtractAutoVariables, type FeatureName, FlowInputValidationError, type InputRequirements, type ModelMetadata, type ModelQueryFilter, type NormalizedCapabilities, type NormalizedFeatures, type NormalizedProviderMetadata, type OCRProviderConfig, type OutputFormatSupport, type ParseAutoVariables, type PromptVariables, type ProviderConfig, type ProviderInputType, type ProviderInstance, type ProviderMetadataWithModels, type ProviderQueryFilter, type ProviderRegistry, type ProviderSecrets, ProviderVendor, type ResolvedModelMetadata, type RetryConfig, type VLMProviderConfig, type WithRetryOptions, bufferToBase64, bufferToDataUri, buildProviderFromConfig, buildProvidersFromConfigs, calculateRetryDelay, clearCircuitBreakers, clearModelRegistry, clearProviderRegistry, createCircuitBreaker, defineMarkerProvider, defineSuryaProvider, defineVLMProvider, detectDocumentType, detectMimeTypeFromBase64, detectMimeTypeFromBase64Async, detectMimeTypeFromBytes, extractBase64, extractStatusCode, getAllModels, getAllProviders, getCheapestProviderFor, getCircuitBreaker, getModelsForNode, getProviderById, getProvidersBySource, getProvidersForLargeFiles, getProvidersForMimeType, isPDFDocument, isRetryableError, parseRetryAfter, queryModels, queryProviders, registerProviderMetadata, registerProviderWithModels, resolveDocument, resolveModelMetadata, validateFlowInputFormat, validateMimeType, validateMimeTypeAsync, withRetry };
package/dist/index.js CHANGED
@@ -2288,7 +2288,187 @@ function getAllModels() {
2288
2288
  function clearModelRegistry() {
2289
2289
  modelRegistry.clear();
2290
2290
  }
2291
+
2292
+ // src/retry.ts
2293
+ var DEFAULT_RETRY_CONFIG = {
2294
+ maxRetries: 2,
2295
+ retryDelay: 1e3,
2296
+ useExponentialBackoff: true,
2297
+ maxDelay: 3e4
2298
+ };
2299
+ var DEFAULT_CIRCUIT_BREAKER_CONFIG = {
2300
+ threshold: 3,
2301
+ resetTimeout: 3e4
2302
+ };
2303
+ var RETRYABLE_STATUS_CODES = ["408", "429", "500", "502", "503", "504"];
2304
+ var RETRYABLE_ERROR_PATTERNS = [
2305
+ "timeout",
2306
+ "rate limit",
2307
+ "overloaded",
2308
+ "econnreset",
2309
+ "etimedout",
2310
+ "enotfound",
2311
+ "econnrefused",
2312
+ "socket hang up",
2313
+ "network error"
2314
+ ];
2315
+ function isRetryableError(error) {
2316
+ const message = error.message.toLowerCase();
2317
+ for (const code of RETRYABLE_STATUS_CODES) {
2318
+ if (message.includes(code)) {
2319
+ return true;
2320
+ }
2321
+ }
2322
+ for (const pattern of RETRYABLE_ERROR_PATTERNS) {
2323
+ if (message.includes(pattern)) {
2324
+ return true;
2325
+ }
2326
+ }
2327
+ return false;
2328
+ }
2329
+ function extractStatusCode(error) {
2330
+ const patterns = [
2331
+ /\b(\d{3})\b/,
2332
+ // Just the status code
2333
+ /status[:\s]+(\d{3})/i,
2334
+ /http[:\s]+(\d{3})/i,
2335
+ /failed[:\s]+(\d{3})/i
2336
+ ];
2337
+ for (const pattern of patterns) {
2338
+ const match = error.message.match(pattern);
2339
+ if (match && match[1]) {
2340
+ const code = parseInt(match[1], 10);
2341
+ if (code >= 100 && code < 600) {
2342
+ return code;
2343
+ }
2344
+ }
2345
+ }
2346
+ return void 0;
2347
+ }
2348
+ function parseRetryAfter(error) {
2349
+ const message = error.message;
2350
+ const match = message.match(/retry-after[:\s]+(\d+)/i);
2351
+ if (match && match[1]) {
2352
+ const seconds = parseInt(match[1], 10);
2353
+ if (!isNaN(seconds) && seconds > 0 && seconds < 3600) {
2354
+ return seconds * 1e3;
2355
+ }
2356
+ }
2357
+ return void 0;
2358
+ }
2359
+ function calculateRetryDelay(attempt, config = {}) {
2360
+ const {
2361
+ retryDelay = DEFAULT_RETRY_CONFIG.retryDelay,
2362
+ useExponentialBackoff = DEFAULT_RETRY_CONFIG.useExponentialBackoff,
2363
+ maxDelay = DEFAULT_RETRY_CONFIG.maxDelay
2364
+ } = config;
2365
+ if (!useExponentialBackoff) {
2366
+ return retryDelay;
2367
+ }
2368
+ const exponentialDelay = retryDelay * Math.pow(2, attempt - 1);
2369
+ const jitter = Math.random() * 1e3;
2370
+ return Math.min(exponentialDelay + jitter, maxDelay);
2371
+ }
2372
+ var circuitBreakerRegistry = /* @__PURE__ */ new Map();
2373
+ function createCircuitBreaker(key, config = {}) {
2374
+ const existing = circuitBreakerRegistry.get(key);
2375
+ if (existing) {
2376
+ return existing;
2377
+ }
2378
+ const {
2379
+ threshold = DEFAULT_CIRCUIT_BREAKER_CONFIG.threshold,
2380
+ resetTimeout = DEFAULT_CIRCUIT_BREAKER_CONFIG.resetTimeout
2381
+ } = config;
2382
+ let state = {
2383
+ consecutiveFailures: 0,
2384
+ isOpen: false
2385
+ };
2386
+ const circuitBreaker = {
2387
+ isOpen() {
2388
+ if (!state.isOpen) return false;
2389
+ if (state.lastFailureTime && Date.now() - state.lastFailureTime > resetTimeout) {
2390
+ state = {
2391
+ consecutiveFailures: 0,
2392
+ isOpen: false
2393
+ };
2394
+ return false;
2395
+ }
2396
+ return true;
2397
+ },
2398
+ recordSuccess() {
2399
+ state = {
2400
+ consecutiveFailures: 0,
2401
+ isOpen: false
2402
+ };
2403
+ },
2404
+ recordFailure() {
2405
+ state.consecutiveFailures++;
2406
+ state.lastFailureTime = Date.now();
2407
+ if (state.consecutiveFailures >= threshold) {
2408
+ state.isOpen = true;
2409
+ console.warn(`Circuit breaker opened for ${key} after ${state.consecutiveFailures} consecutive failures`);
2410
+ }
2411
+ },
2412
+ getState() {
2413
+ return { ...state };
2414
+ }
2415
+ };
2416
+ circuitBreakerRegistry.set(key, circuitBreaker);
2417
+ return circuitBreaker;
2418
+ }
2419
+ function clearCircuitBreakers() {
2420
+ circuitBreakerRegistry.clear();
2421
+ }
2422
+ function getCircuitBreaker(key) {
2423
+ return circuitBreakerRegistry.get(key);
2424
+ }
2425
+ async function withRetry(fn, options = {}) {
2426
+ const {
2427
+ maxRetries = DEFAULT_RETRY_CONFIG.maxRetries,
2428
+ retryDelay = DEFAULT_RETRY_CONFIG.retryDelay,
2429
+ useExponentialBackoff = DEFAULT_RETRY_CONFIG.useExponentialBackoff,
2430
+ maxDelay = DEFAULT_RETRY_CONFIG.maxDelay,
2431
+ onRetry,
2432
+ getRetryAfter,
2433
+ circuitBreaker
2434
+ } = options;
2435
+ if (circuitBreaker?.isOpen()) {
2436
+ throw new Error("Circuit breaker is open");
2437
+ }
2438
+ let lastError = null;
2439
+ const totalAttempts = maxRetries + 1;
2440
+ for (let attempt = 1; attempt <= totalAttempts; attempt++) {
2441
+ try {
2442
+ const result = await fn();
2443
+ circuitBreaker?.recordSuccess();
2444
+ return result;
2445
+ } catch (error) {
2446
+ lastError = error;
2447
+ const isLastAttempt = attempt === totalAttempts;
2448
+ const canRetry = !isLastAttempt && isRetryableError(lastError);
2449
+ if (!canRetry) {
2450
+ break;
2451
+ }
2452
+ let delay = calculateRetryDelay(attempt, { retryDelay, useExponentialBackoff, maxDelay });
2453
+ const retryAfter = getRetryAfter?.(lastError) ?? parseRetryAfter(lastError);
2454
+ if (retryAfter !== void 0 && retryAfter > 0) {
2455
+ delay = Math.min(retryAfter, maxDelay);
2456
+ }
2457
+ if (onRetry) {
2458
+ await onRetry(attempt, lastError, delay);
2459
+ }
2460
+ await sleep(delay);
2461
+ }
2462
+ }
2463
+ circuitBreaker?.recordFailure();
2464
+ throw lastError;
2465
+ }
2466
+ function sleep(ms) {
2467
+ return new Promise((resolve) => setTimeout(resolve, ms));
2468
+ }
2291
2469
  export {
2470
+ DEFAULT_CIRCUIT_BREAKER_CONFIG,
2471
+ DEFAULT_RETRY_CONFIG,
2292
2472
  FlowExecutionError,
2293
2473
  FlowInputValidationError,
2294
2474
  FlowValidationError,
@@ -2299,9 +2479,12 @@ export {
2299
2479
  bufferToDataUri,
2300
2480
  buildProviderFromConfig,
2301
2481
  buildProvidersFromConfigs,
2482
+ calculateRetryDelay,
2302
2483
  canStartForEachItemFlow,
2484
+ clearCircuitBreakers,
2303
2485
  clearModelRegistry,
2304
2486
  clearProviderRegistry,
2487
+ createCircuitBreaker,
2305
2488
  createIdentity,
2306
2489
  defineMarkerProvider,
2307
2490
  defineSuryaProvider,
@@ -2312,9 +2495,11 @@ export {
2312
2495
  detectMimeTypeFromBytes,
2313
2496
  extractBase64,
2314
2497
  extractErrorMessage,
2498
+ extractStatusCode,
2315
2499
  getAllModels,
2316
2500
  getAllProviders,
2317
2501
  getCheapestProviderFor,
2502
+ getCircuitBreaker,
2318
2503
  getCompatibleTargets,
2319
2504
  getDocumentPageCount,
2320
2505
  getModelsForNode,
@@ -2331,8 +2516,10 @@ export {
2331
2516
  getValidForEachStarters,
2332
2517
  isLocalEndpoint,
2333
2518
  isPDFDocument,
2519
+ isRetryableError,
2334
2520
  node,
2335
2521
  parseProviderString,
2522
+ parseRetryAfter,
2336
2523
  protectReservedVariables,
2337
2524
  queryModels,
2338
2525
  queryProviders,
@@ -2347,6 +2534,7 @@ export {
2347
2534
  validateJson,
2348
2535
  validateMimeType,
2349
2536
  validateMimeTypeAsync,
2350
- validateNodeConnection
2537
+ validateNodeConnection,
2538
+ withRetry
2351
2539
  };
2352
2540
  //# sourceMappingURL=index.js.map