@fluentcommerce/fc-connect-sdk 0.1.54 → 0.1.55
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/CHANGELOG.md +12 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/utils/pagination-helpers.js +38 -2
- package/dist/esm/versori/fluent-versori-client.js +11 -5
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/docs/00-START-HERE/EXPORT-VALIDATION.md +158 -158
- package/docs/00-START-HERE/cli-analyze-source-structure-guide.md +655 -655
- package/docs/00-START-HERE/cli-documentation-index.md +202 -202
- package/docs/00-START-HERE/cli-quick-reference.md +252 -252
- package/docs/00-START-HERE/decision-tree.md +552 -552
- package/docs/00-START-HERE/getting-started.md +1070 -1070
- package/docs/00-START-HERE/mapper-quick-decision-guide.md +235 -235
- package/docs/00-START-HERE/readme.md +237 -237
- package/docs/00-START-HERE/retailerid-configuration.md +404 -404
- package/docs/00-START-HERE/sdk-philosophy.md +794 -794
- package/docs/00-START-HERE/troubleshooting-quick-reference.md +1086 -1086
- package/docs/01-TEMPLATES/faq.md +686 -686
- package/docs/01-TEMPLATES/patterns/pattern-templates-guide.md +68 -68
- package/docs/01-TEMPLATES/patterns/patterns-csv-schema-validation-and-rejection-report.md +233 -233
- package/docs/01-TEMPLATES/patterns/patterns-custom-resolvers.md +407 -407
- package/docs/01-TEMPLATES/patterns/patterns-error-handling-retry.md +511 -511
- package/docs/01-TEMPLATES/patterns/patterns-field-mapping-universal.md +701 -701
- package/docs/01-TEMPLATES/patterns/patterns-large-file-splitting.md +1430 -1430
- package/docs/01-TEMPLATES/patterns/patterns-master-data-etl.md +2399 -2399
- package/docs/01-TEMPLATES/patterns/patterns-pagination-streaming.md +447 -447
- package/docs/01-TEMPLATES/patterns/patterns-state-duplicate-prevention.md +385 -385
- package/docs/01-TEMPLATES/readme.md +957 -957
- package/docs/01-TEMPLATES/standalone/standalone-asn-inbound-processing.md +1209 -1209
- package/docs/01-TEMPLATES/standalone/standalone-graphql-query-export.md +1140 -1140
- package/docs/01-TEMPLATES/standalone/standalone-graphql-to-parquet-partitioned-s3.md +432 -432
- package/docs/01-TEMPLATES/standalone/standalone-multi-channel-inventory-sync.md +1185 -1185
- package/docs/01-TEMPLATES/standalone/standalone-multi-source-aggregation.md +1462 -1462
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-batch-api.md +1390 -1390
- package/docs/01-TEMPLATES/standalone/standalone-s3-csv-inventory-to-batch.md +330 -330
- package/docs/01-TEMPLATES/standalone/standalone-scripts-guide.md +87 -87
- package/docs/01-TEMPLATES/standalone/standalone-sftp-xml-graphql.md +1444 -1444
- package/docs/01-TEMPLATES/standalone/standalone-webhook-payload-processing.md +688 -688
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-dropship-order-routing.md +193 -193
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-graphql-parquet-extraction.md +518 -518
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-inter-location-transfers.md +2162 -2162
- package/docs/01-TEMPLATES/versori/business-examples/business-examples-pre-order-allocation.md +2226 -2226
- package/docs/01-TEMPLATES/versori/business-examples/business-scenarios-guide.md +87 -87
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-connection-validation-pattern.md +656 -656
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-dual-workflow-connector.md +835 -835
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-guide.md +108 -108
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-kv-state-management.md +1533 -1533
- package/docs/01-TEMPLATES/versori/patterns/versori-patterns-xml-response-patterns.md +1160 -1160
- package/docs/01-TEMPLATES/versori/versori-platform-guide.md +201 -201
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-asn-purchase-order.md +1906 -1906
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-dropship-routing.md +1074 -1074
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-flash-sale-reserve.md +1395 -1395
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-generic-xml-order.md +888 -888
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-payment-gateway-integration.md +2478 -2478
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-rma-returns-comprehensive.md +2240 -2240
- package/docs/01-TEMPLATES/versori/webhooks/template-webhook-xml-order-ingestion.md +2029 -2029
- package/docs/01-TEMPLATES/versori/webhooks/webhook-templates-guide.md +140 -140
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/inventory-mapping.json +20 -20
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/products_2025-01-22.csv +11 -11
- package/docs/01-TEMPLATES/versori/workflows/_examples/sample-data/sample-data-guide.md +34 -34
- package/docs/01-TEMPLATES/versori/workflows/_examples/workflow-examples-guide.md +36 -36
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-modes-guide.md +1038 -1038
- package/docs/01-TEMPLATES/versori/workflows/extraction/extraction-workflows-guide.md +138 -138
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/graphql-extraction-guide.md +63 -63
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-csv.md +2062 -2062
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-fulfillments-to-sftp-xml.md +2294 -2294
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-s3-csv.md +2461 -2461
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-positions-to-sftp-xml.md +2529 -2529
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-csv.md +2464 -2464
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-inventory-quantities-to-s3-json.md +1959 -1959
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-s3-csv.md +1953 -1953
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-orders-to-sftp-xml.md +2541 -2541
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-s3-json.md +2384 -2384
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-products-to-sftp-xml.md +2445 -2445
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-csv.md +2355 -2355
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-s3-json.md +2042 -2042
- package/docs/01-TEMPLATES/versori/workflows/extraction/graphql-queries/template-extraction-virtual-positions-to-sftp-xml.md +2726 -2726
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/batch-api-guide.md +206 -206
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-cycle-count-reconciliation.md +2030 -2030
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-multi-channel-inventory-sync.md +1882 -1882
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-csv-inventory-batch.md +2827 -2827
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-json-inventory-batch.md +1952 -1952
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-s3-xml-inventory-batch.md +3289 -3289
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-csv-inventory-batch.md +3064 -3064
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-json-inventory-batch.md +3238 -3238
- package/docs/01-TEMPLATES/versori/workflows/ingestion/batch-api/template-ingestion-sftp-xml-inventory-batch.md +2977 -2977
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/event-api-guide.md +321 -321
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-json-order-cancel-event.md +959 -959
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-payload-xml-order-cancel-event.md +1170 -1170
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-csv-product-event.md +2312 -2312
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-json-product-event.md +2999 -2999
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-parquet-product-event.md +2836 -2836
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-s3-xml-product-event.md +2395 -2395
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-csv-product-event.md +2295 -2295
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-json-product-event.md +2602 -2602
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-parquet-product-event.md +2589 -2589
- package/docs/01-TEMPLATES/versori/workflows/ingestion/event-api/template-ingestion-sftp-xml-product-event.md +3578 -3578
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/graphql-mutations-guide.md +93 -93
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-json-order-update-graphql.md +1260 -1260
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-payload-xml-order-update-graphql.md +1472 -1472
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-control-graphql.md +2417 -2417
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-location-graphql.md +2811 -2811
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-csv-price-graphql.md +2619 -2619
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-json-location-graphql.md +2807 -2807
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-s3-xml-location-graphql.md +2373 -2373
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-control-graphql.md +2740 -2740
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-csv-location-graphql.md +2760 -2760
- package/docs/01-TEMPLATES/versori/workflows/ingestion/graphql-mutations/template-ingestion-sftp-json-location-graphql.md +1710 -1710
- package/docs/01-TEMPLATES/versori/workflows/ingestion/ingestion-workflows-guide.md +136 -136
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/rubix-webhooks-guide.md +520 -520
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-inline.md +1418 -1418
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-fulfilment-to-sftp-xml-universal-mapper.md +1785 -1785
- package/docs/01-TEMPLATES/versori/workflows/rubix-webhooks/template-webhook-rubix-order-attribute-update.md +824 -824
- package/docs/01-TEMPLATES/versori/workflows/workflows-overview-guide.md +646 -646
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-batch-archival.md +724 -724
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-job-tracker.md +627 -627
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-partial-batch-recovery.md +561 -561
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-quick-reference.md +367 -367
- package/docs/02-CORE-GUIDES/advanced-services/advanced-services-readme.md +407 -407
- package/docs/02-CORE-GUIDES/advanced-services/readme.md +49 -49
- package/docs/02-CORE-GUIDES/api-reference/api-reference-quick-reference.md +548 -548
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +702 -1171
- package/docs/02-CORE-GUIDES/api-reference/examples/client-initialization.ts +286 -286
- package/docs/02-CORE-GUIDES/api-reference/graphql-error-classification.md +337 -337
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +399 -520
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-03-authentication.md +199 -199
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-04-graphql-mapping.md +925 -925
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-05-services.md +1198 -1198
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-06-data-sources.md +1083 -1083
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-07-parsers.md +1097 -1097
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-pagination.md +513 -513
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +545 -597
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-error-handling.md +527 -527
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-09-webhook-validation.md +514 -514
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-extraction.md +557 -557
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-10-utilities.md +412 -412
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-cli-tools.md +423 -423
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-11-error-handling.md +716 -716
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-analyze-source-structure.md +518 -518
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -212
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-testing.md +300 -300
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-13-resolver-builder.md +322 -322
- package/docs/02-CORE-GUIDES/api-reference/readme.md +279 -279
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-quick-reference.md +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/auto-pagination-readme.md +277 -277
- package/docs/02-CORE-GUIDES/auto-pagination/examples/auto-pagination-readme.md +178 -178
- package/docs/02-CORE-GUIDES/auto-pagination/examples/common-patterns.ts +351 -351
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-products.ts +384 -384
- package/docs/02-CORE-GUIDES/auto-pagination/examples/paginate-virtual-positions.ts +308 -308
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-01-foundations.md +470 -470
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-02-quick-start.md +713 -713
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-03-configuration.md +754 -754
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-04-advanced-patterns.md +732 -732
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-05-sdk-integration.md +847 -847
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-06-troubleshooting.md +359 -359
- package/docs/02-CORE-GUIDES/auto-pagination/modules/auto-pagination-07-api-reference.md +462 -462
- package/docs/02-CORE-GUIDES/auto-pagination/readme.md +54 -54
- package/docs/02-CORE-GUIDES/data-sources/data-sources-file-operations-error-handling.md +1487 -1487
- package/docs/02-CORE-GUIDES/data-sources/data-sources-quick-reference.md +836 -836
- package/docs/02-CORE-GUIDES/data-sources/data-sources-readme.md +276 -276
- package/docs/02-CORE-GUIDES/data-sources/data-sources-sftp-credential-access-security.md +553 -553
- package/docs/02-CORE-GUIDES/data-sources/examples/common-patterns.ts +409 -409
- package/docs/02-CORE-GUIDES/data-sources/examples/data-sources-readme.md +178 -178
- package/docs/02-CORE-GUIDES/data-sources/examples/s3-operations.ts +308 -308
- package/docs/02-CORE-GUIDES/data-sources/examples/sftp-operations.ts +371 -371
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-01-foundations.md +735 -735
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-02-s3-operations.md +1302 -1302
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-03-sftp-operations.md +1379 -1379
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-04-file-patterns.md +941 -941
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-05-advanced-topics.md +813 -813
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-06-integration-patterns.md +486 -486
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-07-troubleshooting.md +387 -387
- package/docs/02-CORE-GUIDES/data-sources/modules/data-sources-08-api-reference.md +417 -417
- package/docs/02-CORE-GUIDES/data-sources/readme.md +77 -77
- package/docs/02-CORE-GUIDES/error-handling-guide.md +936 -936
- package/docs/02-CORE-GUIDES/extraction/examples/02-core-guides-extraction-readme.md +116 -116
- package/docs/02-CORE-GUIDES/extraction/examples/common-patterns.ts +428 -428
- package/docs/02-CORE-GUIDES/extraction/examples/extract-inventory-basic.ts +187 -187
- package/docs/02-CORE-GUIDES/extraction/extraction-quick-reference.md +596 -596
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-01-foundations.md +514 -514
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-02-basic-extraction.md +823 -823
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-03-parquet-processing.md +507 -507
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-04-data-enrichment.md +546 -546
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-05-transformation.md +494 -494
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-export-formats.md +458 -458
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-06-performance.md +138 -138
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-api-reference.md +148 -148
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-07-optimization.md +692 -692
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +1008 -1008
- package/docs/02-CORE-GUIDES/extraction/readme.md +151 -151
- package/docs/02-CORE-GUIDES/ingestion/examples/_simple-kv-store.ts +40 -40
- package/docs/02-CORE-GUIDES/ingestion/examples/error-recovery.ts +728 -728
- package/docs/02-CORE-GUIDES/ingestion/examples/event-driven.ts +501 -501
- package/docs/02-CORE-GUIDES/ingestion/examples/local-file-ingestion.ts +88 -88
- package/docs/02-CORE-GUIDES/ingestion/examples/parquet-ingestion.ts +117 -117
- package/docs/02-CORE-GUIDES/ingestion/examples/performance-optimized.ts +647 -647
- package/docs/02-CORE-GUIDES/ingestion/examples/s3-csv-ingestion.ts +169 -169
- package/docs/02-CORE-GUIDES/ingestion/examples/sftp-csv-ingestion.ts +134 -134
- package/docs/02-CORE-GUIDES/ingestion/ingestion-quick-reference.md +546 -546
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-01-introduction.md +626 -626
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-02-quick-start.md +658 -658
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-03-data-sources.md +1052 -1052
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-04-field-mapping.md +763 -763
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-05-advanced-parsers.md +676 -676
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-06-batch-api.md +1295 -1295
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-api-reference.md +138 -138
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md +1037 -1037
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-08-performance-optimization.md +1349 -1349
- package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-09-best-practices.md +1893 -1893
- package/docs/02-CORE-GUIDES/ingestion/readme.md +160 -160
- package/docs/02-CORE-GUIDES/logging-guide.md +585 -585
- package/docs/02-CORE-GUIDES/mapping/error-handling-patterns.md +401 -401
- package/docs/02-CORE-GUIDES/mapping/examples/02-core-guides-mapping-readme.md +128 -128
- package/docs/02-CORE-GUIDES/mapping/examples/common-patterns.ts +273 -273
- package/docs/02-CORE-GUIDES/mapping/examples/csv-location-ingestion.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/csv-mapping.ts +242 -242
- package/docs/02-CORE-GUIDES/mapping/examples/graphql-to-parquet-extraction.json +36 -36
- package/docs/02-CORE-GUIDES/mapping/examples/json-mapping.ts +213 -213
- package/docs/02-CORE-GUIDES/mapping/examples/json-product-to-mutation.json +48 -48
- package/docs/02-CORE-GUIDES/mapping/examples/xml-mapping.ts +291 -291
- package/docs/02-CORE-GUIDES/mapping/examples/xml-order-to-mutation.json +45 -45
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-quick-reference.md +463 -463
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/graphql-mutation-mapping-readme.md +227 -227
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-01-introduction.md +222 -222
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-02-quick-start.md +351 -351
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-03-schema-validation.md +569 -569
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-04-mapping-patterns.md +471 -471
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-05-configuration-reference.md +611 -611
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-advanced-xpath.md +148 -148
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-06-path-syntax.md +464 -464
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-api-reference.md +94 -94
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-07-array-handling.md +307 -307
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-08-custom-resolvers.md +544 -544
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-09-advanced-patterns.md +427 -427
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-10-hooks-and-variables.md +336 -336
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-11-error-handling.md +488 -488
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-12-arguments-vs-nodes.md +383 -383
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/modules/graphql-mutation-mapping-13-best-practices.md +477 -477
- package/docs/02-CORE-GUIDES/mapping/graphql-mutation-mapping/readme.md +62 -62
- package/docs/02-CORE-GUIDES/mapping/mapping-format-decision-tree.md +480 -480
- package/docs/02-CORE-GUIDES/mapping/mapping-graphql-alias-batching-guide.md +820 -820
- package/docs/02-CORE-GUIDES/mapping/mapping-javascript-objects.md +2369 -2369
- package/docs/02-CORE-GUIDES/mapping/mapping-mapper-comparison-guide.md +682 -682
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-07-api-reference.md +1327 -1327
- package/docs/02-CORE-GUIDES/mapping/modules/02-core-guides-mapping-08-error-handling.md +1142 -1142
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-04-use-cases.md +891 -891
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-helpers-resolvers.md +1126 -1126
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-06-sdk-resolvers.md +199 -199
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-07-api-reference.md +1319 -1319
- package/docs/02-CORE-GUIDES/mapping/readme.md +178 -178
- package/docs/02-CORE-GUIDES/mapping/resolver-registration.md +410 -410
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/common-patterns.ts +226 -226
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/custom-resolvers.ts +227 -227
- package/docs/02-CORE-GUIDES/mapping/resolvers/examples/sdk-resolvers-usage.ts +203 -203
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-readme.md +274 -274
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-api-reference.md +679 -679
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-cookbook.md +826 -826
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-guide.md +1330 -1330
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-helpers-reference.md +1437 -1437
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-parameters-reference.md +553 -553
- package/docs/02-CORE-GUIDES/mapping/resolvers/mapping-resolvers-resolver-troubleshooting.md +854 -854
- package/docs/02-CORE-GUIDES/mapping/resolvers/readme.md +75 -75
- package/docs/02-CORE-GUIDES/parsers/examples/02-core-guides-parsers-readme.md +161 -161
- package/docs/02-CORE-GUIDES/parsers/examples/csv-parser-examples.ts +110 -110
- package/docs/02-CORE-GUIDES/parsers/examples/json-parser-examples.ts +33 -33
- package/docs/02-CORE-GUIDES/parsers/examples/parquet-parser-examples.ts +47 -47
- package/docs/02-CORE-GUIDES/parsers/examples/xml-parser-examples.ts +38 -38
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-01-foundations.md +355 -355
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-02-csv-parser.md +772 -772
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-03-json-parser.md +789 -789
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-04-xml-parser.md +857 -857
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-05-parquet-parser.md +603 -603
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-integration-patterns.md +702 -702
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-06-streaming.md +121 -121
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-api-reference.md +89 -89
- package/docs/02-CORE-GUIDES/parsers/modules/02-core-guides-parsers-07-troubleshooting.md +727 -727
- package/docs/02-CORE-GUIDES/parsers/parsers-quick-reference.md +482 -482
- package/docs/02-CORE-GUIDES/parsers/parsers-readme.md +258 -258
- package/docs/02-CORE-GUIDES/parsers/readme.md +65 -65
- package/docs/02-CORE-GUIDES/readme.md +194 -194
- package/docs/02-CORE-GUIDES/webhook-validation/examples/basic-validation.ts +108 -108
- package/docs/02-CORE-GUIDES/webhook-validation/examples/common-patterns.ts +316 -316
- package/docs/02-CORE-GUIDES/webhook-validation/examples/webhook-validation-readme.md +61 -61
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-01-foundations.md +440 -440
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-02-quick-start.md +525 -525
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-03-versori-integration.md +741 -741
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-04-platform-integration.md +629 -629
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-05-configuration.md +535 -535
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-error-handling.md +611 -611
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-06-troubleshooting.md +124 -124
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-07-api-reference.md +511 -511
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-08-rubix-webhooks.md +590 -590
- package/docs/02-CORE-GUIDES/webhook-validation/modules/webhook-validation-09-rubix-event-vs-http-call.md +432 -432
- package/docs/02-CORE-GUIDES/webhook-validation/readme.md +239 -239
- package/docs/02-CORE-GUIDES/webhook-validation/webhook-validation-quick-reference.md +392 -392
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-quick-reference.md +498 -498
- package/docs/03-PATTERN-GUIDES/connector-scenarios/connector-scenarios-readme.md +313 -313
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/common-patterns.ts +612 -612
- package/docs/03-PATTERN-GUIDES/connector-scenarios/examples/connector-scenarios-readme.md +253 -253
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-01-foundations.md +452 -452
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-02-simple-scenarios.md +681 -681
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-03-intermediate-scenarios.md +637 -637
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-04-advanced-scenarios.md +650 -650
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-05-bidirectional-sync.md +233 -233
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-06-production-patterns.md +442 -442
- package/docs/03-PATTERN-GUIDES/connector-scenarios/modules/connector-scenarios-07-reference.md +445 -445
- package/docs/03-PATTERN-GUIDES/connector-scenarios/readme.md +31 -31
- package/docs/03-PATTERN-GUIDES/enterprise-integration-patterns.md +1528 -1528
- package/docs/03-PATTERN-GUIDES/error-handling/comprehensive-error-handling-guide.md +1437 -1437
- package/docs/03-PATTERN-GUIDES/error-handling/error-handling-quick-reference.md +390 -390
- package/docs/03-PATTERN-GUIDES/error-handling/examples/common-patterns.ts +438 -438
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-01-foundations.md +362 -362
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-02-error-types.md +850 -850
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-03-utf8-handling.md +456 -456
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-04-error-scenarios.md +658 -658
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-05-calling-patterns.md +671 -671
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-06-retry-strategies.md +1034 -1034
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-07-monitoring.md +653 -653
- package/docs/03-PATTERN-GUIDES/error-handling/modules/error-handling-08-api-reference.md +847 -847
- package/docs/03-PATTERN-GUIDES/error-handling/readme.md +36 -36
- package/docs/03-PATTERN-GUIDES/examples/__tests__/readme.md +40 -40
- package/docs/03-PATTERN-GUIDES/examples/__tests__/resolver-examples.test.js +282 -282
- package/docs/03-PATTERN-GUIDES/examples/test-data/03-pattern-guides-readme.md +110 -110
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-inventory.json +123 -123
- package/docs/03-PATTERN-GUIDES/examples/test-data/canonical-order.json +171 -171
- package/docs/03-PATTERN-GUIDES/examples/test-data/readme.md +28 -28
- package/docs/03-PATTERN-GUIDES/extraction/extraction-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/extraction/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/file-operations/examples/common-patterns.ts +407 -407
- package/docs/03-PATTERN-GUIDES/file-operations/examples/file-operations-readme.md +142 -142
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-quick-reference.md +462 -462
- package/docs/03-PATTERN-GUIDES/file-operations/file-operations-readme.md +379 -379
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-01-foundations.md +430 -430
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-02-quick-start.md +484 -484
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-03-s3-operations.md +507 -507
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-04-sftp-operations.md +963 -963
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-05-streaming-performance.md +503 -503
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-archive-patterns.md +386 -386
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-06-error-handling.md +117 -117
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-api-reference.md +78 -78
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-07-testing-troubleshooting.md +567 -567
- package/docs/03-PATTERN-GUIDES/file-operations/modules/file-operations-08-api-reference.md +1055 -1055
- package/docs/03-PATTERN-GUIDES/file-operations/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/ingestion/ingestion-readme.md +15 -15
- package/docs/03-PATTERN-GUIDES/ingestion/readme.md +25 -25
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/batch-processing.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/common-patterns.ts +360 -360
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/delta-sync.ts +130 -130
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/integration-patterns-readme.md +100 -100
- package/docs/03-PATTERN-GUIDES/integration-patterns/examples/real-time-webhook.ts +398 -398
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-quick-reference.md +962 -962
- package/docs/03-PATTERN-GUIDES/integration-patterns/integration-patterns-readme.md +134 -134
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-01-real-time-processing.md +991 -991
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-02-batch-processing.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-03-delta-sync.md +1108 -1108
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-04-webhook-patterns.md +1181 -1181
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-05-error-handling.md +1061 -1061
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-advanced-integration-services.md +1547 -1547
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-06-performance.md +109 -109
- package/docs/03-PATTERN-GUIDES/integration-patterns/modules/integration-patterns-07-api-reference.md +34 -34
- package/docs/03-PATTERN-GUIDES/integration-patterns/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/logging-minimal-mode.md +128 -128
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/common-patterns.ts +380 -380
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/multiple-connections-readme.md +139 -139
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/parallel-root-connections.ts +149 -149
- package/docs/03-PATTERN-GUIDES/multiple-connections/examples/real-world-scenarios.ts +405 -405
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-01-foundations.md +378 -378
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-02-quick-start.md +566 -566
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-03-targeting-connections.md +659 -659
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-04-parallel-queries.md +656 -656
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-05-best-practices.md +624 -624
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-api-reference.md +824 -824
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-06-versori.md +119 -119
- package/docs/03-PATTERN-GUIDES/multiple-connections/modules/multiple-connections-07-api-reference.md +87 -87
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-quick-reference.md +353 -353
- package/docs/03-PATTERN-GUIDES/multiple-connections/multiple-connections-readme.md +270 -270
- package/docs/03-PATTERN-GUIDES/multiple-connections/readme.md +30 -30
- package/docs/03-PATTERN-GUIDES/pagination/pagination-readme.md +14 -14
- package/docs/03-PATTERN-GUIDES/pagination/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/parquet/examples/common-patterns.ts +180 -180
- package/docs/03-PATTERN-GUIDES/parquet/examples/read-parquet.ts +48 -48
- package/docs/03-PATTERN-GUIDES/parquet/examples/write-parquet.ts +65 -65
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-01-introduction.md +393 -393
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-02-quick-start.md +572 -572
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-03-reading-parquet.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-04-writing-parquet.md +554 -554
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-05-graphql-extraction.md +405 -405
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-performance.md +104 -104
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-06-s3-integration.md +511 -511
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-api-reference.md +90 -90
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-07-performance-optimization.md +525 -525
- package/docs/03-PATTERN-GUIDES/parquet/modules/03-pattern-guides-parquet-08-best-practices.md +712 -712
- package/docs/03-PATTERN-GUIDES/parquet/parquet-quick-reference.md +683 -683
- package/docs/03-PATTERN-GUIDES/parquet/parquet-readme.md +248 -248
- package/docs/03-PATTERN-GUIDES/parquet/readme.md +32 -32
- package/docs/03-PATTERN-GUIDES/parsers/parsers-readme.md +12 -12
- package/docs/03-PATTERN-GUIDES/parsers/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/readme.md +159 -159
- package/docs/03-PATTERN-GUIDES/webhooks/readme.md +24 -24
- package/docs/03-PATTERN-GUIDES/webhooks/webhooks-readme.md +8 -8
- package/docs/04-REFERENCE/architecture/architecture-01-overview.md +427 -427
- package/docs/04-REFERENCE/architecture/architecture-02-client-architecture.md +424 -424
- package/docs/04-REFERENCE/architecture/architecture-03-data-flow.md +690 -690
- package/docs/04-REFERENCE/architecture/architecture-04-service-layer.md +834 -834
- package/docs/04-REFERENCE/architecture/architecture-05-integration-architecture.md +655 -655
- package/docs/04-REFERENCE/architecture/architecture-06-state-management.md +653 -653
- package/docs/04-REFERENCE/architecture/architecture-adding-new-data-sources.md +686 -686
- package/docs/04-REFERENCE/architecture/readme.md +279 -279
- package/docs/04-REFERENCE/platforms/deno/readme.md +117 -117
- package/docs/04-REFERENCE/platforms/nodejs/readme.md +146 -146
- package/docs/04-REFERENCE/platforms/readme.md +135 -135
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-01-introduction.md +398 -398
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-02-quick-start.md +560 -560
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-03-authentication.md +757 -757
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-04-workflows.md +2476 -2476
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-05-connections.md +1167 -1167
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-kv-storage.md +990 -990
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-06-state-management.md +121 -121
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-api-reference.md +68 -68
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-07-deployment.md +731 -731
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-08-best-practices.md +1111 -1111
- package/docs/04-REFERENCE/platforms/versori/modules/platforms-versori-09-signature-reference.md +766 -766
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-readme.md +299 -299
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-s3-sftp-configuration-guide.md +1425 -1425
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-api-key-security.md +816 -816
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-webhook-connection-security.md +681 -681
- package/docs/04-REFERENCE/platforms/versori/platforms-versori-workflow-task-types.md +708 -708
- package/docs/04-REFERENCE/platforms/versori/readme.md +108 -108
- package/docs/04-REFERENCE/readme.md +148 -148
- package/docs/04-REFERENCE/resolver-signature/examples/advanced-resolvers.ts +482 -482
- package/docs/04-REFERENCE/resolver-signature/examples/async-resolvers.ts +496 -496
- package/docs/04-REFERENCE/resolver-signature/examples/basic-resolvers.ts +343 -343
- package/docs/04-REFERENCE/resolver-signature/examples/resolver-signature-readme.md +188 -188
- package/docs/04-REFERENCE/resolver-signature/examples/testing-resolvers.ts +463 -463
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-01-foundations.md +286 -286
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-02-parameter-reference.md +643 -643
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-03-basic-examples.md +521 -521
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-04-advanced-patterns.md +739 -739
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-05-sdk-resolvers.md +531 -531
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-migration-guide.md +650 -650
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-06-testing.md +125 -125
- package/docs/04-REFERENCE/resolver-signature/modules/resolver-signature-07-api-reference.md +794 -794
- package/docs/04-REFERENCE/resolver-signature/readme.md +64 -64
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-quick-reference.md +270 -270
- package/docs/04-REFERENCE/resolver-signature/resolver-signature-readme.md +351 -351
- package/docs/04-REFERENCE/schema/fluent-commerce-schema.json +764 -764
- package/docs/04-REFERENCE/schema/readme.md +141 -141
- package/docs/04-REFERENCE/testing/examples/04-reference-testing-readme.md +158 -158
- package/docs/04-REFERENCE/testing/examples/fluent-testing.ts +62 -62
- package/docs/04-REFERENCE/testing/examples/health-check.ts +155 -155
- package/docs/04-REFERENCE/testing/examples/integration-test.ts +119 -119
- package/docs/04-REFERENCE/testing/examples/performance-test.ts +183 -183
- package/docs/04-REFERENCE/testing/examples/s3-testing.ts +127 -127
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-01-foundations.md +267 -267
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-02-s3-testing.md +599 -599
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-03-fluent-testing.md +589 -589
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-04-integration-testing.md +699 -699
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-05-debugging.md +478 -478
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-cicd-integration.md +463 -463
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-06-preflight-validation.md +131 -131
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-best-practices.md +499 -499
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-07-coverage-ci.md +165 -165
- package/docs/04-REFERENCE/testing/modules/04-reference-testing-08-api-reference.md +634 -634
- package/docs/04-REFERENCE/testing/readme.md +86 -86
- package/docs/04-REFERENCE/testing/testing-quick-reference.md +667 -667
- package/docs/04-REFERENCE/testing/testing-readme.md +286 -286
- package/docs/04-REFERENCE/troubleshooting/readme.md +144 -144
- package/docs/04-REFERENCE/troubleshooting/troubleshooting-deno-sftp-compatibility.md +392 -392
- package/docs/template-loading-matrix.md +242 -242
- package/package.json +5 -3
- package/docs/02-CORE-GUIDES/api-reference/cli-profile-integration.md +0 -377
|
@@ -1,1034 +1,1034 @@
|
|
|
1
|
-
# Module 6: Retry Strategies
|
|
2
|
-
|
|
3
|
-
**Level:** Advanced
|
|
4
|
-
**Estimated Time:** 25 minutes
|
|
5
|
-
|
|
6
|
-
## Overview
|
|
7
|
-
|
|
8
|
-
This module teaches you how to implement effective retry strategies, including how to distinguish retryable from non-retryable errors and how to use exponential backoff to avoid overwhelming systems.
|
|
9
|
-
|
|
10
|
-
## Learning Objectives
|
|
11
|
-
|
|
12
|
-
By the end of this module, you will:
|
|
13
|
-
- ✅ Know which errors are retryable and which are permanent
|
|
14
|
-
- ✅ Implement exponential backoff with jitter
|
|
15
|
-
- ✅ Use the SDK's `isRetryable()` method effectively
|
|
16
|
-
- ✅ Handle rate limiting and timeouts correctly
|
|
17
|
-
- ✅ Build production-ready retry logic
|
|
18
|
-
|
|
19
|
-
## Retryable vs Non-Retryable Errors
|
|
20
|
-
|
|
21
|
-
### Non-Retryable Errors (Permanent Failures)
|
|
22
|
-
|
|
23
|
-
These errors indicate problems with data or configuration that won't be fixed by retrying:
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
const NON_RETRYABLE_CODES = [
|
|
27
|
-
IngestionErrorCode.PARSE_ERROR, // Invalid XML/JSON/CSV
|
|
28
|
-
IngestionErrorCode.VALIDATION_ERROR, // Data validation failed
|
|
29
|
-
IngestionErrorCode.REQUIRED_FIELD_MISSING, // Missing required field
|
|
30
|
-
IngestionErrorCode.CONFIGURATION_ERROR, // Invalid config
|
|
31
|
-
IngestionErrorCode.FIELD_MAPPING_ERROR, // Mapping config wrong
|
|
32
|
-
IngestionErrorCode.INVALID_FILE_FORMAT, // Unsupported file type
|
|
33
|
-
];
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
**Why not retryable:**
|
|
37
|
-
- Source data is malformed
|
|
38
|
-
- Configuration is incorrect
|
|
39
|
-
- Business logic validation failed
|
|
40
|
-
- Schema mismatch
|
|
41
|
-
|
|
42
|
-
**What to do:**
|
|
43
|
-
1. Log the error with full context
|
|
44
|
-
2. Alert developers/operators
|
|
45
|
-
3. Fix the underlying issue (data or config)
|
|
46
|
-
4. Re-submit after fix
|
|
47
|
-
|
|
48
|
-
**Example:**
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
if (error instanceof FileParsingError) {
|
|
52
|
-
logger.error('PERMANENT ERROR - Fix source file:', {
|
|
53
|
-
fileName: error.fileName,
|
|
54
|
-
lineNumber: error.lineNumber,
|
|
55
|
-
message: error.message
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Don't retry - save to dead letter queue for manual review
|
|
59
|
-
await saveToDeadLetterQueue(fileName, content, error);
|
|
60
|
-
|
|
61
|
-
throw error; // Fail immediately
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Retryable Errors (Transient Failures)
|
|
66
|
-
|
|
67
|
-
These errors are likely temporary and may succeed if retried:
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
const RETRYABLE_CODES = [
|
|
71
|
-
IngestionErrorCode.NETWORK_ERROR, // Network connectivity issue
|
|
72
|
-
IngestionErrorCode.TIMEOUT_ERROR, // Request timed out
|
|
73
|
-
IngestionErrorCode.RATE_LIMIT_ERROR, // API rate limit hit
|
|
74
|
-
IngestionErrorCode.LOCK_ACQUISITION_FAILED, // Distributed lock conflict
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
// Also retryable:
|
|
78
|
-
// - FluentAPIError with statusCode >= 500 (Server Error)
|
|
79
|
-
// - FluentAPIError with statusCode === 429 (Rate Limit)
|
|
80
|
-
// - GraphQLExecutionError with code 'TIMEOUT' or 'NETWORK_ERROR'
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
**Why retryable:**
|
|
84
|
-
- Network issues are often temporary
|
|
85
|
-
- Timeouts may be due to temporary load
|
|
86
|
-
- Rate limits reset after time period
|
|
87
|
-
- Lock conflicts resolve when other process completes
|
|
88
|
-
|
|
89
|
-
**What to do:**
|
|
90
|
-
1. Check `error.isRetryable()` returns `true`
|
|
91
|
-
2. Implement exponential backoff
|
|
92
|
-
3. Respect rate limit headers (`retryAfter`)
|
|
93
|
-
4. Set maximum retry limit
|
|
94
|
-
5. Log retry attempts
|
|
95
|
-
|
|
96
|
-
**Example:**
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
if (error instanceof IngestionError && error.isRetryable()) {
|
|
100
|
-
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
101
|
-
logger.info(`Retryable error - will retry in ${delay}ms`, {
|
|
102
|
-
code: error.code,
|
|
103
|
-
attempt,
|
|
104
|
-
maxRetries
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
108
|
-
return retry();
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Using isRetryable()
|
|
113
|
-
|
|
114
|
-
The SDK's `IngestionError` class provides an `isRetryable()` method:
|
|
115
|
-
|
|
116
|
-
```typescript
|
|
117
|
-
abstract class IngestionError extends Error {
|
|
118
|
-
isRetryable(): boolean {
|
|
119
|
-
const retryableCodes = [
|
|
120
|
-
IngestionErrorCode.NETWORK_ERROR,
|
|
121
|
-
IngestionErrorCode.TIMEOUT_ERROR,
|
|
122
|
-
IngestionErrorCode.RATE_LIMIT_ERROR,
|
|
123
|
-
IngestionErrorCode.LOCK_ACQUISITION_FAILED,
|
|
124
|
-
];
|
|
125
|
-
|
|
126
|
-
return retryableCodes.includes(this.code);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Basic Usage
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
import { IngestionError } from '@fluentcommerce/fc-connect-sdk';
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
await processFile(fileName);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
if (error instanceof IngestionError) {
|
|
140
|
-
if (error.isRetryable()) {
|
|
141
|
-
// Retry logic
|
|
142
|
-
return scheduleRetry();
|
|
143
|
-
} else {
|
|
144
|
-
// Permanent failure
|
|
145
|
-
logger.error('Permanent error - manual intervention required');
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### Handling Non-SDK Errors
|
|
154
|
-
|
|
155
|
-
Not all errors extend `IngestionError`:
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
try {
|
|
159
|
-
await operation();
|
|
160
|
-
} catch (error) {
|
|
161
|
-
// Check SDK errors first
|
|
162
|
-
if (error instanceof IngestionError) {
|
|
163
|
-
if (!error.isRetryable()) {
|
|
164
|
-
throw error; // Don't retry
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// File parsing errors are never retryable
|
|
169
|
-
if (error instanceof FileParsingError) {
|
|
170
|
-
throw error; // Don't retry
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Path resolution errors are config issues
|
|
174
|
-
if (error instanceof PathResolutionError) {
|
|
175
|
-
throw error; // Don't retry
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Mapping errors are data/config issues
|
|
179
|
-
if (error instanceof MappingError) {
|
|
180
|
-
throw error; // Don't retry
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Unknown errors - retry with caution
|
|
184
|
-
if (attempt < maxRetries) {
|
|
185
|
-
logger.warn('Unknown error - attempting retry', { error });
|
|
186
|
-
await delay(1000 * attempt);
|
|
187
|
-
return retry();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
## Exponential Backoff
|
|
195
|
-
|
|
196
|
-
### Why Exponential Backoff?
|
|
197
|
-
|
|
198
|
-
**Linear backoff** (same delay each time):
|
|
199
|
-
```
|
|
200
|
-
Attempt 1: Wait 1s
|
|
201
|
-
Attempt 2: Wait 1s
|
|
202
|
-
Attempt 3: Wait 1s
|
|
203
|
-
```
|
|
204
|
-
❌ Doesn't give system time to recover from overload
|
|
205
|
-
|
|
206
|
-
**Exponential backoff** (increasing delay):
|
|
207
|
-
```
|
|
208
|
-
Attempt 1: Wait 1s
|
|
209
|
-
Attempt 2: Wait 2s
|
|
210
|
-
Attempt 3: Wait 4s
|
|
211
|
-
Attempt 4: Wait 8s
|
|
212
|
-
```
|
|
213
|
-
✅ Reduces load on overloaded systems
|
|
214
|
-
✅ Increases chance of success on each retry
|
|
215
|
-
|
|
216
|
-
### Basic Implementation
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
async function retryWithBackoff<T>(
|
|
220
|
-
operation: () => Promise<T>,
|
|
221
|
-
maxRetries: number = 3,
|
|
222
|
-
initialDelay: number = 1000
|
|
223
|
-
): Promise<T> {
|
|
224
|
-
let lastError: Error;
|
|
225
|
-
|
|
226
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
227
|
-
try {
|
|
228
|
-
return await operation();
|
|
229
|
-
} catch (error) {
|
|
230
|
-
lastError = error;
|
|
231
|
-
|
|
232
|
-
// Check if error is retryable
|
|
233
|
-
if (error instanceof IngestionError) {
|
|
234
|
-
if (!error.isRetryable()) {
|
|
235
|
-
throw error; // Don't retry permanent errors
|
|
236
|
-
}
|
|
237
|
-
} else if (error instanceof MappingError ||
|
|
238
|
-
error instanceof PathResolutionError ||
|
|
239
|
-
error instanceof FileParsingError) {
|
|
240
|
-
throw error; // These are permanent errors
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Last attempt - throw error
|
|
244
|
-
if (attempt === maxRetries - 1) {
|
|
245
|
-
throw lastError;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Calculate delay with exponential backoff
|
|
249
|
-
const delay = initialDelay * Math.pow(2, attempt);
|
|
250
|
-
|
|
251
|
-
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
|
|
252
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
throw lastError!;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Usage
|
|
260
|
-
const result = await retryWithBackoff(
|
|
261
|
-
() => processOrder(xmlContent),
|
|
262
|
-
3, // max retries
|
|
263
|
-
2000 // initial delay (2 seconds)
|
|
264
|
-
);
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### With Jitter
|
|
268
|
-
|
|
269
|
-
Adding random jitter prevents thundering herd problem:
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
async function retryWithBackoffAndJitter<T>(
|
|
273
|
-
operation: () => Promise<T>,
|
|
274
|
-
maxRetries: number = 3,
|
|
275
|
-
initialDelay: number = 1000,
|
|
276
|
-
maxDelay: number = 60000
|
|
277
|
-
): Promise<T> {
|
|
278
|
-
let lastError: Error;
|
|
279
|
-
|
|
280
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
281
|
-
try {
|
|
282
|
-
return await operation();
|
|
283
|
-
} catch (error) {
|
|
284
|
-
lastError = error;
|
|
285
|
-
|
|
286
|
-
// Check if retryable
|
|
287
|
-
if (error instanceof IngestionError && !error.isRetryable()) {
|
|
288
|
-
throw error;
|
|
289
|
-
}
|
|
290
|
-
if (error instanceof MappingError ||
|
|
291
|
-
error instanceof PathResolutionError ||
|
|
292
|
-
error instanceof FileParsingError) {
|
|
293
|
-
throw error;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (attempt === maxRetries - 1) {
|
|
297
|
-
throw lastError;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Exponential backoff with jitter
|
|
301
|
-
const exponentialDelay = initialDelay * Math.pow(2, attempt);
|
|
302
|
-
const jitter = Math.random() * 100; // 0-100ms random jitter
|
|
303
|
-
const delay = Math.min(exponentialDelay + jitter, maxDelay);
|
|
304
|
-
|
|
305
|
-
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
|
|
306
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
throw lastError!;
|
|
311
|
-
}
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
**Benefits of jitter:**
|
|
315
|
-
- ✅ Prevents multiple clients retrying at exact same time
|
|
316
|
-
- ✅ Spreads load more evenly
|
|
317
|
-
- ✅ Reduces likelihood of cascading failures
|
|
318
|
-
|
|
319
|
-
### Full-Jitter Strategy
|
|
320
|
-
|
|
321
|
-
AWS recommended approach - more aggressive randomization:
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
function calculateBackoffWithFullJitter(
|
|
325
|
-
attempt: number,
|
|
326
|
-
baseDelay: number = 1000,
|
|
327
|
-
maxDelay: number = 60000
|
|
328
|
-
): number {
|
|
329
|
-
const exponentialDelay = baseDelay * Math.pow(2, attempt);
|
|
330
|
-
const cappedDelay = Math.min(exponentialDelay, maxDelay);
|
|
331
|
-
|
|
332
|
-
// Full jitter: random value between 0 and cappedDelay
|
|
333
|
-
return Math.random() * cappedDelay;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Usage
|
|
337
|
-
const delay = calculateBackoffWithFullJitter(attempt);
|
|
338
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
## Rate Limiting
|
|
342
|
-
|
|
343
|
-
### Handling Rate Limit Errors
|
|
344
|
-
|
|
345
|
-
Respect the `retryAfter` value from rate limit errors:
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
import { IngestionError, IngestionErrorCode } from '@fluentcommerce/fc-connect-sdk';
|
|
349
|
-
|
|
350
|
-
async function handleRateLimit(
|
|
351
|
-
operation: () => Promise<any>,
|
|
352
|
-
maxRetries: number = 3
|
|
353
|
-
): Promise<any> {
|
|
354
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
355
|
-
try {
|
|
356
|
-
return await operation();
|
|
357
|
-
} catch (error) {
|
|
358
|
-
if (error instanceof IngestionError &&
|
|
359
|
-
error.code === IngestionErrorCode.RATE_LIMIT_ERROR) {
|
|
360
|
-
|
|
361
|
-
// Get retry-after value from error context
|
|
362
|
-
const retryAfter = error.context?.retryAfter || 60;
|
|
363
|
-
|
|
364
|
-
if (attempt < maxRetries - 1) {
|
|
365
|
-
logger.warn(`Rate limited - waiting ${retryAfter} seconds`, {
|
|
366
|
-
attempt,
|
|
367
|
-
maxRetries
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// Wait for rate limit to reset
|
|
371
|
-
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Non-rate-limit error or max retries exceeded
|
|
377
|
-
throw error;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
throw new Error('Max retries exceeded');
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### Rate Limit with Exponential Backoff Fallback
|
|
386
|
-
|
|
387
|
-
If no `retryAfter` provided, use exponential backoff:
|
|
388
|
-
|
|
389
|
-
```typescript
|
|
390
|
-
async function handleRateLimitSmart(
|
|
391
|
-
operation: () => Promise<any>,
|
|
392
|
-
maxRetries: number = 5
|
|
393
|
-
): Promise<any> {
|
|
394
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
395
|
-
try {
|
|
396
|
-
return await operation();
|
|
397
|
-
} catch (error) {
|
|
398
|
-
if (error instanceof IngestionError &&
|
|
399
|
-
error.code === IngestionErrorCode.RATE_LIMIT_ERROR) {
|
|
400
|
-
|
|
401
|
-
if (attempt >= maxRetries - 1) {
|
|
402
|
-
throw error; // Max retries exceeded
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Use retryAfter if provided, else exponential backoff
|
|
406
|
-
const retryAfter = error.context?.retryAfter;
|
|
407
|
-
let delay: number;
|
|
408
|
-
|
|
409
|
-
if (retryAfter) {
|
|
410
|
-
delay = retryAfter * 1000; // Convert to ms
|
|
411
|
-
logger.info(`Rate limited - retry after ${retryAfter}s`);
|
|
412
|
-
} else {
|
|
413
|
-
delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
414
|
-
logger.info(`Rate limited - retry after ${delay}ms (no retryAfter)`);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Not a rate limit error
|
|
422
|
-
throw error;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
throw new Error('Max retries exceeded');
|
|
427
|
-
}
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
## Timeout Handling
|
|
431
|
-
|
|
432
|
-
### Network Timeouts
|
|
433
|
-
|
|
434
|
-
Handle network timeouts with retry:
|
|
435
|
-
|
|
436
|
-
```typescript
|
|
437
|
-
async function executeWithTimeout<T>(
|
|
438
|
-
operation: () => Promise<T>,
|
|
439
|
-
timeoutMs: number = 30000,
|
|
440
|
-
maxRetries: number = 3
|
|
441
|
-
): Promise<T> {
|
|
442
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
443
|
-
try {
|
|
444
|
-
// Race between operation and timeout
|
|
445
|
-
const result = await Promise.race([
|
|
446
|
-
operation(),
|
|
447
|
-
new Promise<never>((_, reject) =>
|
|
448
|
-
setTimeout(() => reject(new Error('Operation timeout')), timeoutMs)
|
|
449
|
-
)
|
|
450
|
-
]);
|
|
451
|
-
|
|
452
|
-
return result;
|
|
453
|
-
|
|
454
|
-
} catch (error) {
|
|
455
|
-
if (error.message === 'Operation timeout') {
|
|
456
|
-
if (attempt < maxRetries - 1) {
|
|
457
|
-
const delay = Math.pow(2, attempt) * 1000;
|
|
458
|
-
logger.warn(`Timeout - retrying in ${delay}ms`, { attempt });
|
|
459
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Not a timeout or max retries exceeded
|
|
465
|
-
throw error;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
throw new Error('Max retries exceeded');
|
|
470
|
-
}
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
## SDK Built-In Retry Logic
|
|
474
|
-
|
|
475
|
-
### S3DataSource Retry
|
|
476
|
-
|
|
477
|
-
The SDK's S3DataSource uses the shared `RetryHelper` utility for consistent retry behavior:
|
|
478
|
-
|
|
479
|
-
```typescript
|
|
480
|
-
import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
|
|
481
|
-
|
|
482
|
-
const s3Config = {
|
|
483
|
-
type: 'S3_CSV' as const,
|
|
484
|
-
s3Config: {
|
|
485
|
-
bucket: 'my-bucket',
|
|
486
|
-
region: 'us-east-1',
|
|
487
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
488
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
489
|
-
maxAttempts: 3 // Default: 3 retries
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
const s3 = new S3DataSource(s3Config, logger);
|
|
494
|
-
|
|
495
|
-
// Automatic retry on:
|
|
496
|
-
// - HTTP 429 (rate limit)
|
|
497
|
-
// - HTTP 500, 502, 503, 504 (server errors)
|
|
498
|
-
// - Network errors (ECONNRESET, ETIMEDOUT, etc.)
|
|
499
|
-
const files = await s3.listFiles(); // Retries automatically
|
|
500
|
-
const content = await s3.downloadFile(key); // Retries automatically
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
**Retry Configuration:**
|
|
504
|
-
- **Max attempts**: Configurable via `maxAttempts` (default: 3)
|
|
505
|
-
- **Base delay**: 1000ms
|
|
506
|
-
- **Backoff factor**: 2 (exponential)
|
|
507
|
-
- **Max delay**: 8000ms (8 seconds)
|
|
508
|
-
- **Jitter**: Full jitter (AWS recommended)
|
|
509
|
-
- **Retryable status codes**: 429, 500, 502, 503, 504
|
|
510
|
-
- **Retryable network errors**: Connection resets, timeouts, unreachable hosts
|
|
511
|
-
|
|
512
|
-
**Implementation Note:** S3DataSource uses `RetryHelper.executeWithRetry()` from `src/utils/retry-helper.ts` for consistency across all SDK HTTP operations.
|
|
513
|
-
|
|
514
|
-
**SFTP Note:** SftpDataSource uses custom retry logic due to connection pooling requirements. See lines 536-645 for SFTP-specific retry patterns.
|
|
515
|
-
|
|
516
|
-
### SftpDataSource Retry
|
|
517
|
-
|
|
518
|
-
The SDK's SftpDataSource includes configurable retry logic with connection pooling:
|
|
519
|
-
|
|
520
|
-
```typescript
|
|
521
|
-
import { SftpDataSource, SftpDataSourceConfig } from '@fluentcommerce/fc-connect-sdk';
|
|
522
|
-
|
|
523
|
-
const sftpConfig: SftpDataSourceConfig = {
|
|
524
|
-
type: 'SFTP_CSV',
|
|
525
|
-
settings: {
|
|
526
|
-
host: 'sftp.example.com',
|
|
527
|
-
port: 22,
|
|
528
|
-
username: 'user',
|
|
529
|
-
password: 'pass',
|
|
530
|
-
remotePath: '/data',
|
|
531
|
-
filePattern: '*.csv',
|
|
532
|
-
|
|
533
|
-
// Enhanced retry configuration
|
|
534
|
-
retry: {
|
|
535
|
-
maxAttempts: 3, // Default: 3
|
|
536
|
-
baseDelayMs: 1000, // Default: 1000ms (1 second)
|
|
537
|
-
backoffFactor: 2, // Default: 2 (exponential)
|
|
538
|
-
maxDelayMs: 8000, // Default: 8000ms (8 seconds)
|
|
539
|
-
jitter: 'full', // Default: 'full' jitter
|
|
540
|
-
reconnectOnFailure: true, // Default: true - reconnect on error
|
|
541
|
-
isRetryable: (error) => {
|
|
542
|
-
// Custom retry logic (optional)
|
|
543
|
-
return error.message.includes('timeout');
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
const sftp = new SftpDataSource(sftpConfig, logger);
|
|
550
|
-
|
|
551
|
-
// Automatic retry with exponential backoff and connection pooling
|
|
552
|
-
const files = await sftp.listFiles(); // Retries automatically!
|
|
553
|
-
const content = await sftp.downloadFile('file.csv'); // Retries automatically!
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
**Retry behavior:**
|
|
557
|
-
- **Default strategy**: Exponential backoff with full jitter
|
|
558
|
-
- **Connection pooling**: Reuses connections, creates new ones when needed
|
|
559
|
-
- **Wait queue**: Queues requests when pool is full (prevents connection spam)
|
|
560
|
-
- **Reconnect on failure**: Automatically reconnects on transient errors
|
|
561
|
-
|
|
562
|
-
**Retryable errors:**
|
|
563
|
-
- Connection resets (`ECONNRESET`)
|
|
564
|
-
- Timeouts (`ETIMEDOUT`)
|
|
565
|
-
- Host unreachable (`EHOSTUNREACH`)
|
|
566
|
-
- Connection refused (`ECONNREFUSED`)
|
|
567
|
-
- Broken pipe (`EPIPE`)
|
|
568
|
-
- Message patterns: "connection lost", "timeout", "network error"
|
|
569
|
-
|
|
570
|
-
**Non-retryable errors:**
|
|
571
|
-
- Authentication failures
|
|
572
|
-
- Permission denied
|
|
573
|
-
- File not found
|
|
574
|
-
- Algorithm/cipher errors
|
|
575
|
-
- Key exchange failures
|
|
576
|
-
|
|
577
|
-
**Implementation details:**
|
|
578
|
-
```typescript
|
|
579
|
-
private async executeWithRetry<T>(
|
|
580
|
-
operation: () => Promise<T>,
|
|
581
|
-
operationName: string,
|
|
582
|
-
retryOverride?: Partial<SftpRetryConfig>
|
|
583
|
-
): Promise<T> {
|
|
584
|
-
const config = { ...this.retryConfig, ...retryOverride };
|
|
585
|
-
let lastError: Error;
|
|
586
|
-
|
|
587
|
-
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
|
588
|
-
try {
|
|
589
|
-
return await operation();
|
|
590
|
-
} catch (error) {
|
|
591
|
-
lastError = error as Error;
|
|
592
|
-
const isRetryable = config.isRetryable(lastError);
|
|
593
|
-
const isLastAttempt = attempt === config.maxAttempts;
|
|
594
|
-
|
|
595
|
-
if (!isRetryable || isLastAttempt) {
|
|
596
|
-
throw error;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Reconnect on failure if enabled
|
|
600
|
-
if (config.reconnectOnFailure) {
|
|
601
|
-
await this.closeConnection(connectionId);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Calculate delay with jitter
|
|
605
|
-
const delay = this.calculateDelay(attempt);
|
|
606
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
throw lastError!;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
private calculateDelay(attempt: number): number {
|
|
614
|
-
const exponentialDelay =
|
|
615
|
-
this.retryConfig.baseDelayMs * Math.pow(this.retryConfig.backoffFactor, attempt - 1);
|
|
616
|
-
const cappedDelay = Math.min(exponentialDelay, this.retryConfig.maxDelayMs);
|
|
617
|
-
|
|
618
|
-
if (this.retryConfig.jitter === 'none') {
|
|
619
|
-
return cappedDelay;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Full jitter: random between 0 and cappedDelay
|
|
623
|
-
return Math.floor(Math.random() * cappedDelay);
|
|
624
|
-
}
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
### Connection Pooling (SFTP only)
|
|
628
|
-
|
|
629
|
-
```typescript
|
|
630
|
-
// Connection pool configuration
|
|
631
|
-
const sftpConfig: SftpDataSourceConfig = {
|
|
632
|
-
type: 'SFTP_CSV',
|
|
633
|
-
settings: {
|
|
634
|
-
// ... other settings
|
|
635
|
-
maxConnections: 5 // Default: 5 concurrent connections
|
|
636
|
-
}
|
|
637
|
-
};
|
|
638
|
-
|
|
639
|
-
// Connection pooling features:
|
|
640
|
-
// - Reuses idle connections
|
|
641
|
-
// - Creates new connections up to maxConnections
|
|
642
|
-
// - Queues requests when pool is full
|
|
643
|
-
// - Automatically closes idle connections after timeout
|
|
644
|
-
// - Handles connection failures gracefully
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
### When to Use Built-In Retry vs Custom Retry
|
|
648
|
-
|
|
649
|
-
**Use SDK built-in retry when:**
|
|
650
|
-
- ✅ Working with S3DataSource or SftpDataSource
|
|
651
|
-
- ✅ Default retry behavior is acceptable
|
|
652
|
-
- ✅ You want zero configuration
|
|
653
|
-
- ✅ You need connection pooling (SFTP)
|
|
654
|
-
|
|
655
|
-
**Use custom retry when:**
|
|
656
|
-
- ✅ Working with FluentClient GraphQL operations
|
|
657
|
-
- ✅ Need custom retry logic beyond data source operations
|
|
658
|
-
- ✅ Need to coordinate retries across multiple services
|
|
659
|
-
- ✅ Want different retry strategies for different operations
|
|
660
|
-
|
|
661
|
-
**Example: Combining both**
|
|
662
|
-
|
|
663
|
-
```typescript
|
|
664
|
-
// S3 operations use built-in retry
|
|
665
|
-
const files = await s3.listFiles(); // Automatic retry
|
|
666
|
-
|
|
667
|
-
// Custom retry for high-level workflow
|
|
668
|
-
await retryOperation(
|
|
669
|
-
async () => {
|
|
670
|
-
// Download (automatic retry)
|
|
671
|
-
const content = await s3.downloadFile(files[0].path);
|
|
672
|
-
|
|
673
|
-
// Parse
|
|
674
|
-
const parsed = await parser.parse(content);
|
|
675
|
-
|
|
676
|
-
// Map
|
|
677
|
-
const payload = await mapper.map(parsed);
|
|
678
|
-
|
|
679
|
-
// Submit to Fluent (custom retry)
|
|
680
|
-
return await client.graphql(payload);
|
|
681
|
-
},
|
|
682
|
-
{
|
|
683
|
-
maxRetries: 5,
|
|
684
|
-
initialDelay: 2000
|
|
685
|
-
}
|
|
686
|
-
);
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
## Partial Batch Recovery
|
|
690
|
-
|
|
691
|
-
### PartialBatchRecovery Service
|
|
692
|
-
|
|
693
|
-
The SDK's `PartialBatchRecovery` service handles partial batch failures gracefully by retrying only failed records instead of the entire batch.
|
|
694
|
-
|
|
695
|
-
**When to use:**
|
|
696
|
-
- Large batch imports where some records may fail
|
|
697
|
-
- Network errors affecting subset of records
|
|
698
|
-
- Validation failures on specific records
|
|
699
|
-
- Want to minimize reprocessing overhead
|
|
700
|
-
|
|
701
|
-
```typescript
|
|
702
|
-
import {
|
|
703
|
-
PartialBatchRecovery,
|
|
704
|
-
createClient,
|
|
705
|
-
createConsoleLogger,
|
|
706
|
-
toStructuredLogger
|
|
707
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
708
|
-
|
|
709
|
-
// Standalone
|
|
710
|
-
const logger = toStructuredLogger(createConsoleLogger(), { service: 'BatchRecovery' });
|
|
711
|
-
|
|
712
|
-
// Versori Platform
|
|
713
|
-
const { log: logger } = ctx;
|
|
714
|
-
|
|
715
|
-
const recovery = new PartialBatchRecovery(logger);
|
|
716
|
-
const client = await createClient({ config });
|
|
717
|
-
|
|
718
|
-
// Process batch with automatic retry of failed records
|
|
719
|
-
const result = await recovery.processBatchWithRecovery(
|
|
720
|
-
inventoryRecords,
|
|
721
|
-
async (batch) => {
|
|
722
|
-
// Your batch processing logic
|
|
723
|
-
const job = await client.createJob({
|
|
724
|
-
name: 'Inventory Sync with Recovery',
|
|
725
|
-
retailerId: 'my-retailer'
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
const batchResult = await client.sendBatch(job.id, {
|
|
729
|
-
action: 'UPSERT',
|
|
730
|
-
entityType: 'INVENTORY',
|
|
731
|
-
entities: batch
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
return batchResult;
|
|
735
|
-
},
|
|
736
|
-
{
|
|
737
|
-
maxRetries: 3, // Retry each failed record up to 3 times
|
|
738
|
-
retryOnlyFailed: true, // Only retry failed records (default: true)
|
|
739
|
-
batchSize: 100, // Process in batches of 100
|
|
740
|
-
retryDelay: 1000, // Wait 1 second between retries
|
|
741
|
-
exponentialBackoff: true // Use exponential backoff (default: true)
|
|
742
|
-
}
|
|
743
|
-
);
|
|
744
|
-
|
|
745
|
-
// Results
|
|
746
|
-
console.log(`Total records: ${inventoryRecords.length}`);
|
|
747
|
-
console.log(`Successful: ${result.successCount}`);
|
|
748
|
-
console.log(`Failed: ${result.failedCount}`);
|
|
749
|
-
|
|
750
|
-
if (result.failedCount > 0) {
|
|
751
|
-
console.log('Failed records:');
|
|
752
|
-
result.failedRecords.forEach(({ record, error, attemptCount }) => {
|
|
753
|
-
console.error(`Record ${record.ref} failed after ${attemptCount} attempts:`, error.message);
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
### Recovery Result Structure
|
|
759
|
-
|
|
760
|
-
```typescript
|
|
761
|
-
interface RecoveryResult {
|
|
762
|
-
successCount: number; // Total successful records
|
|
763
|
-
failedCount: number; // Total failed after all retries
|
|
764
|
-
failedRecords: Array<{
|
|
765
|
-
record: any; // The record that failed
|
|
766
|
-
error: Error; // Last error that occurred
|
|
767
|
-
attemptCount: number; // Total retry attempts for this record
|
|
768
|
-
}>;
|
|
769
|
-
}
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
### Advanced Recovery Options
|
|
773
|
-
|
|
774
|
-
```typescript
|
|
775
|
-
const result = await recovery.processBatchWithRecovery(
|
|
776
|
-
records,
|
|
777
|
-
processFn,
|
|
778
|
-
{
|
|
779
|
-
maxRetries: 5,
|
|
780
|
-
retryOnlyFailed: true,
|
|
781
|
-
batchSize: 50,
|
|
782
|
-
retryDelay: 2000,
|
|
783
|
-
exponentialBackoff: true,
|
|
784
|
-
|
|
785
|
-
// Custom retry decision logic
|
|
786
|
-
shouldRetry: (error, attemptCount) => {
|
|
787
|
-
// Don't retry validation errors
|
|
788
|
-
if (error instanceof ValidationError) {
|
|
789
|
-
return false;
|
|
790
|
-
}
|
|
791
|
-
// Retry network/timeout errors
|
|
792
|
-
if (error instanceof IngestionError) {
|
|
793
|
-
return error.isRetryable() && attemptCount < 5;
|
|
794
|
-
}
|
|
795
|
-
return attemptCount < 3;
|
|
796
|
-
},
|
|
797
|
-
|
|
798
|
-
// Custom backoff calculation
|
|
799
|
-
calculateDelay: (attempt, baseDelay) => {
|
|
800
|
-
return Math.min(baseDelay * Math.pow(2, attempt), 30000);
|
|
801
|
-
},
|
|
802
|
-
|
|
803
|
-
// Progress callback
|
|
804
|
-
onProgress: (current, total) => {
|
|
805
|
-
logger.info(`Processing: ${current}/${total} records`);
|
|
806
|
-
},
|
|
807
|
-
|
|
808
|
-
// Retry callback
|
|
809
|
-
onRetry: (record, attempt, error) => {
|
|
810
|
-
logger.warn(`Retrying record ${record.ref}, attempt ${attempt}`, {
|
|
811
|
-
error: error.message
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
);
|
|
816
|
-
```
|
|
817
|
-
|
|
818
|
-
### Combining with Built-In Retry
|
|
819
|
-
|
|
820
|
-
PartialBatchRecovery works well with SDK's built-in retry logic:
|
|
821
|
-
|
|
822
|
-
```typescript
|
|
823
|
-
// S3 operations have automatic retry
|
|
824
|
-
const files = await s3.listFiles(); // Automatic retry on network errors
|
|
825
|
-
|
|
826
|
-
// Parse files
|
|
827
|
-
const records = [];
|
|
828
|
-
for (const file of files) {
|
|
829
|
-
const content = await s3.downloadFile(file.path); // Automatic retry
|
|
830
|
-
const parsed = await parser.parse(content);
|
|
831
|
-
records.push(...parsed);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// Use PartialBatchRecovery for batch submission
|
|
835
|
-
const result = await recovery.processBatchWithRecovery(
|
|
836
|
-
records,
|
|
837
|
-
async (batch) => {
|
|
838
|
-
const job = await client.createJob({ name: 'Sync', retailerId });
|
|
839
|
-
return await client.sendBatch(job.id, {
|
|
840
|
-
action: 'UPSERT',
|
|
841
|
-
entityType: 'INVENTORY',
|
|
842
|
-
entities: batch
|
|
843
|
-
});
|
|
844
|
-
},
|
|
845
|
-
{
|
|
846
|
-
maxRetries: 3,
|
|
847
|
-
retryOnlyFailed: true
|
|
848
|
-
}
|
|
849
|
-
);
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
### Benefits of PartialBatchRecovery
|
|
853
|
-
|
|
854
|
-
**Efficiency:**
|
|
855
|
-
- ✅ Only retries failed records (not entire batch)
|
|
856
|
-
- ✅ Reduces API calls and processing time
|
|
857
|
-
- ✅ Lower resource consumption
|
|
858
|
-
|
|
859
|
-
**Reliability:**
|
|
860
|
-
- ✅ Tracks each record individually
|
|
861
|
-
- ✅ Configurable retry logic per record
|
|
862
|
-
- ✅ Detailed failure reporting
|
|
863
|
-
|
|
864
|
-
**Observability:**
|
|
865
|
-
- ✅ Know exactly which records failed
|
|
866
|
-
- ✅ Understand failure patterns
|
|
867
|
-
- ✅ Better error reporting for monitoring
|
|
868
|
-
|
|
869
|
-
**Example: 1000 records, 10 fail**
|
|
870
|
-
- **Without PartialBatchRecovery:** Retry all 1000 records
|
|
871
|
-
- **With PartialBatchRecovery:** Retry only 10 failed records
|
|
872
|
-
|
|
873
|
-
## Production-Ready Retry Function
|
|
874
|
-
|
|
875
|
-
Complete retry implementation with all best practices:
|
|
876
|
-
|
|
877
|
-
```typescript
|
|
878
|
-
import {
|
|
879
|
-
IngestionError,
|
|
880
|
-
IngestionErrorCode,
|
|
881
|
-
FileParsingError,
|
|
882
|
-
PathResolutionError,
|
|
883
|
-
MappingError,
|
|
884
|
-
FluentAPIError,
|
|
885
|
-
GraphQLExecutionError
|
|
886
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
887
|
-
|
|
888
|
-
interface RetryOptions {
|
|
889
|
-
maxRetries?: number;
|
|
890
|
-
initialDelay?: number;
|
|
891
|
-
maxDelay?: number;
|
|
892
|
-
useJitter?: boolean;
|
|
893
|
-
onRetry?: (attempt: number, error: Error) => void;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
async function retryOperation<T>(
|
|
897
|
-
operation: () => Promise<T>,
|
|
898
|
-
options: RetryOptions = {}
|
|
899
|
-
): Promise<T> {
|
|
900
|
-
const {
|
|
901
|
-
maxRetries = 3,
|
|
902
|
-
initialDelay = 1000,
|
|
903
|
-
maxDelay = 60000,
|
|
904
|
-
useJitter = true,
|
|
905
|
-
onRetry
|
|
906
|
-
} = options;
|
|
907
|
-
|
|
908
|
-
let lastError: Error;
|
|
909
|
-
|
|
910
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
911
|
-
try {
|
|
912
|
-
return await operation();
|
|
913
|
-
|
|
914
|
-
} catch (error) {
|
|
915
|
-
lastError = error;
|
|
916
|
-
|
|
917
|
-
// 1. Don't retry permanent errors
|
|
918
|
-
if (error instanceof FileParsingError ||
|
|
919
|
-
error instanceof PathResolutionError ||
|
|
920
|
-
error instanceof MappingError) {
|
|
921
|
-
throw error;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// 2. Check FluentAPIError (HTTP 5xx / 429)
|
|
925
|
-
let isRetryable = false;
|
|
926
|
-
let retryDelay = 0;
|
|
927
|
-
|
|
928
|
-
if (error instanceof FluentAPIError) {
|
|
929
|
-
if (error.statusCode >= 500 || error.statusCode === 429) {
|
|
930
|
-
isRetryable = true;
|
|
931
|
-
} else {
|
|
932
|
-
throw error; // 4xx are permanent
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// 3. Check IngestionError
|
|
937
|
-
else if (error instanceof IngestionError) {
|
|
938
|
-
if (error.isRetryable()) {
|
|
939
|
-
isRetryable = true;
|
|
940
|
-
// Special handling for rate limit error context
|
|
941
|
-
if (error.code === IngestionErrorCode.RATE_LIMIT_ERROR) {
|
|
942
|
-
retryDelay = (error.context?.retryAfter || 60) * 1000;
|
|
943
|
-
}
|
|
944
|
-
} else {
|
|
945
|
-
throw error;
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// 4. Check GraphQLExecutionError
|
|
950
|
-
else if (error instanceof GraphQLExecutionError) {
|
|
951
|
-
const firstError = error.graphqlErrors[0];
|
|
952
|
-
if (firstError?.extensions?.code === 'TIMEOUT' ||
|
|
953
|
-
firstError?.extensions?.code === 'NETWORK_ERROR') {
|
|
954
|
-
isRetryable = true;
|
|
955
|
-
} else {
|
|
956
|
-
throw error; // Validation errors are permanent
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// Last attempt - throw
|
|
961
|
-
if (attempt >= maxRetries - 1) {
|
|
962
|
-
throw error;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// Calculate delay if not already set
|
|
966
|
-
if (!retryDelay) {
|
|
967
|
-
let delay = initialDelay * Math.pow(2, attempt);
|
|
968
|
-
if (useJitter) {
|
|
969
|
-
delay = Math.random() * delay;
|
|
970
|
-
}
|
|
971
|
-
retryDelay = Math.min(delay, maxDelay);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// Callback
|
|
975
|
-
if (onRetry) {
|
|
976
|
-
onRetry(attempt + 1, error);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
throw lastError!;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// Usage
|
|
987
|
-
const result = await retryOperation(
|
|
988
|
-
() => processOrder(xmlContent),
|
|
989
|
-
{
|
|
990
|
-
maxRetries: 5,
|
|
991
|
-
initialDelay: 2000,
|
|
992
|
-
maxDelay: 60000,
|
|
993
|
-
useJitter: true,
|
|
994
|
-
onRetry: (attempt, error) => {
|
|
995
|
-
logger.warn('Retrying operation', { attempt, error: error.message });
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
);
|
|
999
|
-
```
|
|
1000
|
-
|
|
1001
|
-
## Key Takeaways
|
|
1002
|
-
|
|
1003
|
-
- 🎯 **SDK built-in retry** - S3DataSource and SftpDataSource have automatic retry
|
|
1004
|
-
- 🎯 **Use isRetryable()** - Let SDK determine if error is transient
|
|
1005
|
-
- 🎯 **Exponential backoff** - Increases delay between retries (with jitter)
|
|
1006
|
-
- 🎯 **Connection pooling** - SftpDataSource reuses connections efficiently
|
|
1007
|
-
- 🎯 **Add jitter** - Prevents thundering herd problem (full jitter recommended)
|
|
1008
|
-
- 🎯 **Respect rate limits** - Use `retryAfter` when provided
|
|
1009
|
-
- 🎯 **Set max retries** - Prevent infinite retry loops (default: 3)
|
|
1010
|
-
- 🎯 **Don't retry permanent errors** - Parse, mapping, validation errors won't succeed
|
|
1011
|
-
|
|
1012
|
-
## Practice Exercise
|
|
1013
|
-
|
|
1014
|
-
Implement a retry function that:
|
|
1015
|
-
1. Retries only transient errors
|
|
1016
|
-
2. Uses exponential backoff with jitter
|
|
1017
|
-
3. Respects rate limit headers
|
|
1018
|
-
4. Has a maximum of 5 retries
|
|
1019
|
-
|
|
1020
|
-
<details>
|
|
1021
|
-
<summary>Click to see solution</summary>
|
|
1022
|
-
|
|
1023
|
-
See the "Production-Ready Retry Function" section above for a complete implementation.
|
|
1024
|
-
|
|
1025
|
-
</details>
|
|
1026
|
-
|
|
1027
|
-
## Next Steps
|
|
1028
|
-
|
|
1029
|
-
Continue to [Module 7: Monitoring →](./error-handling-07-monitoring.md) to learn how to effectively log and monitor errors.
|
|
1030
|
-
|
|
1031
|
-
---
|
|
1032
|
-
|
|
1033
|
-
**Previous:** [← Module 5: Calling Patterns](./error-handling-05-calling-patterns.md)
|
|
1034
|
-
**Next:** [Module 7: Monitoring →](./error-handling-07-monitoring.md)
|
|
1
|
+
# Module 6: Retry Strategies
|
|
2
|
+
|
|
3
|
+
**Level:** Advanced
|
|
4
|
+
**Estimated Time:** 25 minutes
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
This module teaches you how to implement effective retry strategies, including how to distinguish retryable from non-retryable errors and how to use exponential backoff to avoid overwhelming systems.
|
|
9
|
+
|
|
10
|
+
## Learning Objectives
|
|
11
|
+
|
|
12
|
+
By the end of this module, you will:
|
|
13
|
+
- ✅ Know which errors are retryable and which are permanent
|
|
14
|
+
- ✅ Implement exponential backoff with jitter
|
|
15
|
+
- ✅ Use the SDK's `isRetryable()` method effectively
|
|
16
|
+
- ✅ Handle rate limiting and timeouts correctly
|
|
17
|
+
- ✅ Build production-ready retry logic
|
|
18
|
+
|
|
19
|
+
## Retryable vs Non-Retryable Errors
|
|
20
|
+
|
|
21
|
+
### Non-Retryable Errors (Permanent Failures)
|
|
22
|
+
|
|
23
|
+
These errors indicate problems with data or configuration that won't be fixed by retrying:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const NON_RETRYABLE_CODES = [
|
|
27
|
+
IngestionErrorCode.PARSE_ERROR, // Invalid XML/JSON/CSV
|
|
28
|
+
IngestionErrorCode.VALIDATION_ERROR, // Data validation failed
|
|
29
|
+
IngestionErrorCode.REQUIRED_FIELD_MISSING, // Missing required field
|
|
30
|
+
IngestionErrorCode.CONFIGURATION_ERROR, // Invalid config
|
|
31
|
+
IngestionErrorCode.FIELD_MAPPING_ERROR, // Mapping config wrong
|
|
32
|
+
IngestionErrorCode.INVALID_FILE_FORMAT, // Unsupported file type
|
|
33
|
+
];
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Why not retryable:**
|
|
37
|
+
- Source data is malformed
|
|
38
|
+
- Configuration is incorrect
|
|
39
|
+
- Business logic validation failed
|
|
40
|
+
- Schema mismatch
|
|
41
|
+
|
|
42
|
+
**What to do:**
|
|
43
|
+
1. Log the error with full context
|
|
44
|
+
2. Alert developers/operators
|
|
45
|
+
3. Fix the underlying issue (data or config)
|
|
46
|
+
4. Re-submit after fix
|
|
47
|
+
|
|
48
|
+
**Example:**
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
if (error instanceof FileParsingError) {
|
|
52
|
+
logger.error('PERMANENT ERROR - Fix source file:', {
|
|
53
|
+
fileName: error.fileName,
|
|
54
|
+
lineNumber: error.lineNumber,
|
|
55
|
+
message: error.message
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Don't retry - save to dead letter queue for manual review
|
|
59
|
+
await saveToDeadLetterQueue(fileName, content, error);
|
|
60
|
+
|
|
61
|
+
throw error; // Fail immediately
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Retryable Errors (Transient Failures)
|
|
66
|
+
|
|
67
|
+
These errors are likely temporary and may succeed if retried:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const RETRYABLE_CODES = [
|
|
71
|
+
IngestionErrorCode.NETWORK_ERROR, // Network connectivity issue
|
|
72
|
+
IngestionErrorCode.TIMEOUT_ERROR, // Request timed out
|
|
73
|
+
IngestionErrorCode.RATE_LIMIT_ERROR, // API rate limit hit
|
|
74
|
+
IngestionErrorCode.LOCK_ACQUISITION_FAILED, // Distributed lock conflict
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Also retryable:
|
|
78
|
+
// - FluentAPIError with statusCode >= 500 (Server Error)
|
|
79
|
+
// - FluentAPIError with statusCode === 429 (Rate Limit)
|
|
80
|
+
// - GraphQLExecutionError with code 'TIMEOUT' or 'NETWORK_ERROR'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Why retryable:**
|
|
84
|
+
- Network issues are often temporary
|
|
85
|
+
- Timeouts may be due to temporary load
|
|
86
|
+
- Rate limits reset after time period
|
|
87
|
+
- Lock conflicts resolve when other process completes
|
|
88
|
+
|
|
89
|
+
**What to do:**
|
|
90
|
+
1. Check `error.isRetryable()` returns `true`
|
|
91
|
+
2. Implement exponential backoff
|
|
92
|
+
3. Respect rate limit headers (`retryAfter`)
|
|
93
|
+
4. Set maximum retry limit
|
|
94
|
+
5. Log retry attempts
|
|
95
|
+
|
|
96
|
+
**Example:**
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
if (error instanceof IngestionError && error.isRetryable()) {
|
|
100
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
101
|
+
logger.info(`Retryable error - will retry in ${delay}ms`, {
|
|
102
|
+
code: error.code,
|
|
103
|
+
attempt,
|
|
104
|
+
maxRetries
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
108
|
+
return retry();
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Using isRetryable()
|
|
113
|
+
|
|
114
|
+
The SDK's `IngestionError` class provides an `isRetryable()` method:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
abstract class IngestionError extends Error {
|
|
118
|
+
isRetryable(): boolean {
|
|
119
|
+
const retryableCodes = [
|
|
120
|
+
IngestionErrorCode.NETWORK_ERROR,
|
|
121
|
+
IngestionErrorCode.TIMEOUT_ERROR,
|
|
122
|
+
IngestionErrorCode.RATE_LIMIT_ERROR,
|
|
123
|
+
IngestionErrorCode.LOCK_ACQUISITION_FAILED,
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
return retryableCodes.includes(this.code);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Basic Usage
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { IngestionError } from '@fluentcommerce/fc-connect-sdk';
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await processFile(fileName);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error instanceof IngestionError) {
|
|
140
|
+
if (error.isRetryable()) {
|
|
141
|
+
// Retry logic
|
|
142
|
+
return scheduleRetry();
|
|
143
|
+
} else {
|
|
144
|
+
// Permanent failure
|
|
145
|
+
logger.error('Permanent error - manual intervention required');
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Handling Non-SDK Errors
|
|
154
|
+
|
|
155
|
+
Not all errors extend `IngestionError`:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
try {
|
|
159
|
+
await operation();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// Check SDK errors first
|
|
162
|
+
if (error instanceof IngestionError) {
|
|
163
|
+
if (!error.isRetryable()) {
|
|
164
|
+
throw error; // Don't retry
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// File parsing errors are never retryable
|
|
169
|
+
if (error instanceof FileParsingError) {
|
|
170
|
+
throw error; // Don't retry
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Path resolution errors are config issues
|
|
174
|
+
if (error instanceof PathResolutionError) {
|
|
175
|
+
throw error; // Don't retry
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Mapping errors are data/config issues
|
|
179
|
+
if (error instanceof MappingError) {
|
|
180
|
+
throw error; // Don't retry
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Unknown errors - retry with caution
|
|
184
|
+
if (attempt < maxRetries) {
|
|
185
|
+
logger.warn('Unknown error - attempting retry', { error });
|
|
186
|
+
await delay(1000 * attempt);
|
|
187
|
+
return retry();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Exponential Backoff
|
|
195
|
+
|
|
196
|
+
### Why Exponential Backoff?
|
|
197
|
+
|
|
198
|
+
**Linear backoff** (same delay each time):
|
|
199
|
+
```
|
|
200
|
+
Attempt 1: Wait 1s
|
|
201
|
+
Attempt 2: Wait 1s
|
|
202
|
+
Attempt 3: Wait 1s
|
|
203
|
+
```
|
|
204
|
+
❌ Doesn't give system time to recover from overload
|
|
205
|
+
|
|
206
|
+
**Exponential backoff** (increasing delay):
|
|
207
|
+
```
|
|
208
|
+
Attempt 1: Wait 1s
|
|
209
|
+
Attempt 2: Wait 2s
|
|
210
|
+
Attempt 3: Wait 4s
|
|
211
|
+
Attempt 4: Wait 8s
|
|
212
|
+
```
|
|
213
|
+
✅ Reduces load on overloaded systems
|
|
214
|
+
✅ Increases chance of success on each retry
|
|
215
|
+
|
|
216
|
+
### Basic Implementation
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
async function retryWithBackoff<T>(
|
|
220
|
+
operation: () => Promise<T>,
|
|
221
|
+
maxRetries: number = 3,
|
|
222
|
+
initialDelay: number = 1000
|
|
223
|
+
): Promise<T> {
|
|
224
|
+
let lastError: Error;
|
|
225
|
+
|
|
226
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
227
|
+
try {
|
|
228
|
+
return await operation();
|
|
229
|
+
} catch (error) {
|
|
230
|
+
lastError = error;
|
|
231
|
+
|
|
232
|
+
// Check if error is retryable
|
|
233
|
+
if (error instanceof IngestionError) {
|
|
234
|
+
if (!error.isRetryable()) {
|
|
235
|
+
throw error; // Don't retry permanent errors
|
|
236
|
+
}
|
|
237
|
+
} else if (error instanceof MappingError ||
|
|
238
|
+
error instanceof PathResolutionError ||
|
|
239
|
+
error instanceof FileParsingError) {
|
|
240
|
+
throw error; // These are permanent errors
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Last attempt - throw error
|
|
244
|
+
if (attempt === maxRetries - 1) {
|
|
245
|
+
throw lastError;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Calculate delay with exponential backoff
|
|
249
|
+
const delay = initialDelay * Math.pow(2, attempt);
|
|
250
|
+
|
|
251
|
+
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
|
|
252
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
throw lastError!;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Usage
|
|
260
|
+
const result = await retryWithBackoff(
|
|
261
|
+
() => processOrder(xmlContent),
|
|
262
|
+
3, // max retries
|
|
263
|
+
2000 // initial delay (2 seconds)
|
|
264
|
+
);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### With Jitter
|
|
268
|
+
|
|
269
|
+
Adding random jitter prevents thundering herd problem:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
async function retryWithBackoffAndJitter<T>(
|
|
273
|
+
operation: () => Promise<T>,
|
|
274
|
+
maxRetries: number = 3,
|
|
275
|
+
initialDelay: number = 1000,
|
|
276
|
+
maxDelay: number = 60000
|
|
277
|
+
): Promise<T> {
|
|
278
|
+
let lastError: Error;
|
|
279
|
+
|
|
280
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
281
|
+
try {
|
|
282
|
+
return await operation();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
lastError = error;
|
|
285
|
+
|
|
286
|
+
// Check if retryable
|
|
287
|
+
if (error instanceof IngestionError && !error.isRetryable()) {
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
if (error instanceof MappingError ||
|
|
291
|
+
error instanceof PathResolutionError ||
|
|
292
|
+
error instanceof FileParsingError) {
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (attempt === maxRetries - 1) {
|
|
297
|
+
throw lastError;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Exponential backoff with jitter
|
|
301
|
+
const exponentialDelay = initialDelay * Math.pow(2, attempt);
|
|
302
|
+
const jitter = Math.random() * 100; // 0-100ms random jitter
|
|
303
|
+
const delay = Math.min(exponentialDelay + jitter, maxDelay);
|
|
304
|
+
|
|
305
|
+
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
|
|
306
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
throw lastError!;
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Benefits of jitter:**
|
|
315
|
+
- ✅ Prevents multiple clients retrying at exact same time
|
|
316
|
+
- ✅ Spreads load more evenly
|
|
317
|
+
- ✅ Reduces likelihood of cascading failures
|
|
318
|
+
|
|
319
|
+
### Full-Jitter Strategy
|
|
320
|
+
|
|
321
|
+
AWS recommended approach - more aggressive randomization:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
function calculateBackoffWithFullJitter(
|
|
325
|
+
attempt: number,
|
|
326
|
+
baseDelay: number = 1000,
|
|
327
|
+
maxDelay: number = 60000
|
|
328
|
+
): number {
|
|
329
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt);
|
|
330
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelay);
|
|
331
|
+
|
|
332
|
+
// Full jitter: random value between 0 and cappedDelay
|
|
333
|
+
return Math.random() * cappedDelay;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Usage
|
|
337
|
+
const delay = calculateBackoffWithFullJitter(attempt);
|
|
338
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Rate Limiting
|
|
342
|
+
|
|
343
|
+
### Handling Rate Limit Errors
|
|
344
|
+
|
|
345
|
+
Respect the `retryAfter` value from rate limit errors:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { IngestionError, IngestionErrorCode } from '@fluentcommerce/fc-connect-sdk';
|
|
349
|
+
|
|
350
|
+
async function handleRateLimit(
|
|
351
|
+
operation: () => Promise<any>,
|
|
352
|
+
maxRetries: number = 3
|
|
353
|
+
): Promise<any> {
|
|
354
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
355
|
+
try {
|
|
356
|
+
return await operation();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (error instanceof IngestionError &&
|
|
359
|
+
error.code === IngestionErrorCode.RATE_LIMIT_ERROR) {
|
|
360
|
+
|
|
361
|
+
// Get retry-after value from error context
|
|
362
|
+
const retryAfter = error.context?.retryAfter || 60;
|
|
363
|
+
|
|
364
|
+
if (attempt < maxRetries - 1) {
|
|
365
|
+
logger.warn(`Rate limited - waiting ${retryAfter} seconds`, {
|
|
366
|
+
attempt,
|
|
367
|
+
maxRetries
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Wait for rate limit to reset
|
|
371
|
+
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Non-rate-limit error or max retries exceeded
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
throw new Error('Max retries exceeded');
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Rate Limit with Exponential Backoff Fallback
|
|
386
|
+
|
|
387
|
+
If no `retryAfter` provided, use exponential backoff:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
async function handleRateLimitSmart(
|
|
391
|
+
operation: () => Promise<any>,
|
|
392
|
+
maxRetries: number = 5
|
|
393
|
+
): Promise<any> {
|
|
394
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
395
|
+
try {
|
|
396
|
+
return await operation();
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (error instanceof IngestionError &&
|
|
399
|
+
error.code === IngestionErrorCode.RATE_LIMIT_ERROR) {
|
|
400
|
+
|
|
401
|
+
if (attempt >= maxRetries - 1) {
|
|
402
|
+
throw error; // Max retries exceeded
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Use retryAfter if provided, else exponential backoff
|
|
406
|
+
const retryAfter = error.context?.retryAfter;
|
|
407
|
+
let delay: number;
|
|
408
|
+
|
|
409
|
+
if (retryAfter) {
|
|
410
|
+
delay = retryAfter * 1000; // Convert to ms
|
|
411
|
+
logger.info(`Rate limited - retry after ${retryAfter}s`);
|
|
412
|
+
} else {
|
|
413
|
+
delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
414
|
+
logger.info(`Rate limited - retry after ${delay}ms (no retryAfter)`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Not a rate limit error
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
throw new Error('Max retries exceeded');
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Timeout Handling
|
|
431
|
+
|
|
432
|
+
### Network Timeouts
|
|
433
|
+
|
|
434
|
+
Handle network timeouts with retry:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
async function executeWithTimeout<T>(
|
|
438
|
+
operation: () => Promise<T>,
|
|
439
|
+
timeoutMs: number = 30000,
|
|
440
|
+
maxRetries: number = 3
|
|
441
|
+
): Promise<T> {
|
|
442
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
443
|
+
try {
|
|
444
|
+
// Race between operation and timeout
|
|
445
|
+
const result = await Promise.race([
|
|
446
|
+
operation(),
|
|
447
|
+
new Promise<never>((_, reject) =>
|
|
448
|
+
setTimeout(() => reject(new Error('Operation timeout')), timeoutMs)
|
|
449
|
+
)
|
|
450
|
+
]);
|
|
451
|
+
|
|
452
|
+
return result;
|
|
453
|
+
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (error.message === 'Operation timeout') {
|
|
456
|
+
if (attempt < maxRetries - 1) {
|
|
457
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
458
|
+
logger.warn(`Timeout - retrying in ${delay}ms`, { attempt });
|
|
459
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Not a timeout or max retries exceeded
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
throw new Error('Max retries exceeded');
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## SDK Built-In Retry Logic
|
|
474
|
+
|
|
475
|
+
### S3DataSource Retry
|
|
476
|
+
|
|
477
|
+
The SDK's S3DataSource uses the shared `RetryHelper` utility for consistent retry behavior:
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { S3DataSource } from '@fluentcommerce/fc-connect-sdk';
|
|
481
|
+
|
|
482
|
+
const s3Config = {
|
|
483
|
+
type: 'S3_CSV' as const,
|
|
484
|
+
s3Config: {
|
|
485
|
+
bucket: 'my-bucket',
|
|
486
|
+
region: 'us-east-1',
|
|
487
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
488
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
489
|
+
maxAttempts: 3 // Default: 3 retries
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const s3 = new S3DataSource(s3Config, logger);
|
|
494
|
+
|
|
495
|
+
// Automatic retry on:
|
|
496
|
+
// - HTTP 429 (rate limit)
|
|
497
|
+
// - HTTP 500, 502, 503, 504 (server errors)
|
|
498
|
+
// - Network errors (ECONNRESET, ETIMEDOUT, etc.)
|
|
499
|
+
const files = await s3.listFiles(); // Retries automatically
|
|
500
|
+
const content = await s3.downloadFile(key); // Retries automatically
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Retry Configuration:**
|
|
504
|
+
- **Max attempts**: Configurable via `maxAttempts` (default: 3)
|
|
505
|
+
- **Base delay**: 1000ms
|
|
506
|
+
- **Backoff factor**: 2 (exponential)
|
|
507
|
+
- **Max delay**: 8000ms (8 seconds)
|
|
508
|
+
- **Jitter**: Full jitter (AWS recommended)
|
|
509
|
+
- **Retryable status codes**: 429, 500, 502, 503, 504
|
|
510
|
+
- **Retryable network errors**: Connection resets, timeouts, unreachable hosts
|
|
511
|
+
|
|
512
|
+
**Implementation Note:** S3DataSource uses `RetryHelper.executeWithRetry()` from `src/utils/retry-helper.ts` for consistency across all SDK HTTP operations.
|
|
513
|
+
|
|
514
|
+
**SFTP Note:** SftpDataSource uses custom retry logic due to connection pooling requirements. See lines 536-645 for SFTP-specific retry patterns.
|
|
515
|
+
|
|
516
|
+
### SftpDataSource Retry
|
|
517
|
+
|
|
518
|
+
The SDK's SftpDataSource includes configurable retry logic with connection pooling:
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
import { SftpDataSource, SftpDataSourceConfig } from '@fluentcommerce/fc-connect-sdk';
|
|
522
|
+
|
|
523
|
+
const sftpConfig: SftpDataSourceConfig = {
|
|
524
|
+
type: 'SFTP_CSV',
|
|
525
|
+
settings: {
|
|
526
|
+
host: 'sftp.example.com',
|
|
527
|
+
port: 22,
|
|
528
|
+
username: 'user',
|
|
529
|
+
password: 'pass',
|
|
530
|
+
remotePath: '/data',
|
|
531
|
+
filePattern: '*.csv',
|
|
532
|
+
|
|
533
|
+
// Enhanced retry configuration
|
|
534
|
+
retry: {
|
|
535
|
+
maxAttempts: 3, // Default: 3
|
|
536
|
+
baseDelayMs: 1000, // Default: 1000ms (1 second)
|
|
537
|
+
backoffFactor: 2, // Default: 2 (exponential)
|
|
538
|
+
maxDelayMs: 8000, // Default: 8000ms (8 seconds)
|
|
539
|
+
jitter: 'full', // Default: 'full' jitter
|
|
540
|
+
reconnectOnFailure: true, // Default: true - reconnect on error
|
|
541
|
+
isRetryable: (error) => {
|
|
542
|
+
// Custom retry logic (optional)
|
|
543
|
+
return error.message.includes('timeout');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const sftp = new SftpDataSource(sftpConfig, logger);
|
|
550
|
+
|
|
551
|
+
// Automatic retry with exponential backoff and connection pooling
|
|
552
|
+
const files = await sftp.listFiles(); // Retries automatically!
|
|
553
|
+
const content = await sftp.downloadFile('file.csv'); // Retries automatically!
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**Retry behavior:**
|
|
557
|
+
- **Default strategy**: Exponential backoff with full jitter
|
|
558
|
+
- **Connection pooling**: Reuses connections, creates new ones when needed
|
|
559
|
+
- **Wait queue**: Queues requests when pool is full (prevents connection spam)
|
|
560
|
+
- **Reconnect on failure**: Automatically reconnects on transient errors
|
|
561
|
+
|
|
562
|
+
**Retryable errors:**
|
|
563
|
+
- Connection resets (`ECONNRESET`)
|
|
564
|
+
- Timeouts (`ETIMEDOUT`)
|
|
565
|
+
- Host unreachable (`EHOSTUNREACH`)
|
|
566
|
+
- Connection refused (`ECONNREFUSED`)
|
|
567
|
+
- Broken pipe (`EPIPE`)
|
|
568
|
+
- Message patterns: "connection lost", "timeout", "network error"
|
|
569
|
+
|
|
570
|
+
**Non-retryable errors:**
|
|
571
|
+
- Authentication failures
|
|
572
|
+
- Permission denied
|
|
573
|
+
- File not found
|
|
574
|
+
- Algorithm/cipher errors
|
|
575
|
+
- Key exchange failures
|
|
576
|
+
|
|
577
|
+
**Implementation details:**
|
|
578
|
+
```typescript
|
|
579
|
+
private async executeWithRetry<T>(
|
|
580
|
+
operation: () => Promise<T>,
|
|
581
|
+
operationName: string,
|
|
582
|
+
retryOverride?: Partial<SftpRetryConfig>
|
|
583
|
+
): Promise<T> {
|
|
584
|
+
const config = { ...this.retryConfig, ...retryOverride };
|
|
585
|
+
let lastError: Error;
|
|
586
|
+
|
|
587
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
|
588
|
+
try {
|
|
589
|
+
return await operation();
|
|
590
|
+
} catch (error) {
|
|
591
|
+
lastError = error as Error;
|
|
592
|
+
const isRetryable = config.isRetryable(lastError);
|
|
593
|
+
const isLastAttempt = attempt === config.maxAttempts;
|
|
594
|
+
|
|
595
|
+
if (!isRetryable || isLastAttempt) {
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Reconnect on failure if enabled
|
|
600
|
+
if (config.reconnectOnFailure) {
|
|
601
|
+
await this.closeConnection(connectionId);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Calculate delay with jitter
|
|
605
|
+
const delay = this.calculateDelay(attempt);
|
|
606
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
throw lastError!;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private calculateDelay(attempt: number): number {
|
|
614
|
+
const exponentialDelay =
|
|
615
|
+
this.retryConfig.baseDelayMs * Math.pow(this.retryConfig.backoffFactor, attempt - 1);
|
|
616
|
+
const cappedDelay = Math.min(exponentialDelay, this.retryConfig.maxDelayMs);
|
|
617
|
+
|
|
618
|
+
if (this.retryConfig.jitter === 'none') {
|
|
619
|
+
return cappedDelay;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Full jitter: random between 0 and cappedDelay
|
|
623
|
+
return Math.floor(Math.random() * cappedDelay);
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Connection Pooling (SFTP only)
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
// Connection pool configuration
|
|
631
|
+
const sftpConfig: SftpDataSourceConfig = {
|
|
632
|
+
type: 'SFTP_CSV',
|
|
633
|
+
settings: {
|
|
634
|
+
// ... other settings
|
|
635
|
+
maxConnections: 5 // Default: 5 concurrent connections
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// Connection pooling features:
|
|
640
|
+
// - Reuses idle connections
|
|
641
|
+
// - Creates new connections up to maxConnections
|
|
642
|
+
// - Queues requests when pool is full
|
|
643
|
+
// - Automatically closes idle connections after timeout
|
|
644
|
+
// - Handles connection failures gracefully
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### When to Use Built-In Retry vs Custom Retry
|
|
648
|
+
|
|
649
|
+
**Use SDK built-in retry when:**
|
|
650
|
+
- ✅ Working with S3DataSource or SftpDataSource
|
|
651
|
+
- ✅ Default retry behavior is acceptable
|
|
652
|
+
- ✅ You want zero configuration
|
|
653
|
+
- ✅ You need connection pooling (SFTP)
|
|
654
|
+
|
|
655
|
+
**Use custom retry when:**
|
|
656
|
+
- ✅ Working with FluentClient GraphQL operations
|
|
657
|
+
- ✅ Need custom retry logic beyond data source operations
|
|
658
|
+
- ✅ Need to coordinate retries across multiple services
|
|
659
|
+
- ✅ Want different retry strategies for different operations
|
|
660
|
+
|
|
661
|
+
**Example: Combining both**
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
// S3 operations use built-in retry
|
|
665
|
+
const files = await s3.listFiles(); // Automatic retry
|
|
666
|
+
|
|
667
|
+
// Custom retry for high-level workflow
|
|
668
|
+
await retryOperation(
|
|
669
|
+
async () => {
|
|
670
|
+
// Download (automatic retry)
|
|
671
|
+
const content = await s3.downloadFile(files[0].path);
|
|
672
|
+
|
|
673
|
+
// Parse
|
|
674
|
+
const parsed = await parser.parse(content);
|
|
675
|
+
|
|
676
|
+
// Map
|
|
677
|
+
const payload = await mapper.map(parsed);
|
|
678
|
+
|
|
679
|
+
// Submit to Fluent (custom retry)
|
|
680
|
+
return await client.graphql(payload);
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
maxRetries: 5,
|
|
684
|
+
initialDelay: 2000
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
## Partial Batch Recovery
|
|
690
|
+
|
|
691
|
+
### PartialBatchRecovery Service
|
|
692
|
+
|
|
693
|
+
The SDK's `PartialBatchRecovery` service handles partial batch failures gracefully by retrying only failed records instead of the entire batch.
|
|
694
|
+
|
|
695
|
+
**When to use:**
|
|
696
|
+
- Large batch imports where some records may fail
|
|
697
|
+
- Network errors affecting subset of records
|
|
698
|
+
- Validation failures on specific records
|
|
699
|
+
- Want to minimize reprocessing overhead
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
import {
|
|
703
|
+
PartialBatchRecovery,
|
|
704
|
+
createClient,
|
|
705
|
+
createConsoleLogger,
|
|
706
|
+
toStructuredLogger
|
|
707
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
708
|
+
|
|
709
|
+
// Standalone
|
|
710
|
+
const logger = toStructuredLogger(createConsoleLogger(), { service: 'BatchRecovery' });
|
|
711
|
+
|
|
712
|
+
// Versori Platform
|
|
713
|
+
const { log: logger } = ctx;
|
|
714
|
+
|
|
715
|
+
const recovery = new PartialBatchRecovery(logger);
|
|
716
|
+
const client = await createClient({ config });
|
|
717
|
+
|
|
718
|
+
// Process batch with automatic retry of failed records
|
|
719
|
+
const result = await recovery.processBatchWithRecovery(
|
|
720
|
+
inventoryRecords,
|
|
721
|
+
async (batch) => {
|
|
722
|
+
// Your batch processing logic
|
|
723
|
+
const job = await client.createJob({
|
|
724
|
+
name: 'Inventory Sync with Recovery',
|
|
725
|
+
retailerId: 'my-retailer'
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const batchResult = await client.sendBatch(job.id, {
|
|
729
|
+
action: 'UPSERT',
|
|
730
|
+
entityType: 'INVENTORY',
|
|
731
|
+
entities: batch
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
return batchResult;
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
maxRetries: 3, // Retry each failed record up to 3 times
|
|
738
|
+
retryOnlyFailed: true, // Only retry failed records (default: true)
|
|
739
|
+
batchSize: 100, // Process in batches of 100
|
|
740
|
+
retryDelay: 1000, // Wait 1 second between retries
|
|
741
|
+
exponentialBackoff: true // Use exponential backoff (default: true)
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
// Results
|
|
746
|
+
console.log(`Total records: ${inventoryRecords.length}`);
|
|
747
|
+
console.log(`Successful: ${result.successCount}`);
|
|
748
|
+
console.log(`Failed: ${result.failedCount}`);
|
|
749
|
+
|
|
750
|
+
if (result.failedCount > 0) {
|
|
751
|
+
console.log('Failed records:');
|
|
752
|
+
result.failedRecords.forEach(({ record, error, attemptCount }) => {
|
|
753
|
+
console.error(`Record ${record.ref} failed after ${attemptCount} attempts:`, error.message);
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### Recovery Result Structure
|
|
759
|
+
|
|
760
|
+
```typescript
|
|
761
|
+
interface RecoveryResult {
|
|
762
|
+
successCount: number; // Total successful records
|
|
763
|
+
failedCount: number; // Total failed after all retries
|
|
764
|
+
failedRecords: Array<{
|
|
765
|
+
record: any; // The record that failed
|
|
766
|
+
error: Error; // Last error that occurred
|
|
767
|
+
attemptCount: number; // Total retry attempts for this record
|
|
768
|
+
}>;
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Advanced Recovery Options
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
const result = await recovery.processBatchWithRecovery(
|
|
776
|
+
records,
|
|
777
|
+
processFn,
|
|
778
|
+
{
|
|
779
|
+
maxRetries: 5,
|
|
780
|
+
retryOnlyFailed: true,
|
|
781
|
+
batchSize: 50,
|
|
782
|
+
retryDelay: 2000,
|
|
783
|
+
exponentialBackoff: true,
|
|
784
|
+
|
|
785
|
+
// Custom retry decision logic
|
|
786
|
+
shouldRetry: (error, attemptCount) => {
|
|
787
|
+
// Don't retry validation errors
|
|
788
|
+
if (error instanceof ValidationError) {
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
// Retry network/timeout errors
|
|
792
|
+
if (error instanceof IngestionError) {
|
|
793
|
+
return error.isRetryable() && attemptCount < 5;
|
|
794
|
+
}
|
|
795
|
+
return attemptCount < 3;
|
|
796
|
+
},
|
|
797
|
+
|
|
798
|
+
// Custom backoff calculation
|
|
799
|
+
calculateDelay: (attempt, baseDelay) => {
|
|
800
|
+
return Math.min(baseDelay * Math.pow(2, attempt), 30000);
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
// Progress callback
|
|
804
|
+
onProgress: (current, total) => {
|
|
805
|
+
logger.info(`Processing: ${current}/${total} records`);
|
|
806
|
+
},
|
|
807
|
+
|
|
808
|
+
// Retry callback
|
|
809
|
+
onRetry: (record, attempt, error) => {
|
|
810
|
+
logger.warn(`Retrying record ${record.ref}, attempt ${attempt}`, {
|
|
811
|
+
error: error.message
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### Combining with Built-In Retry
|
|
819
|
+
|
|
820
|
+
PartialBatchRecovery works well with SDK's built-in retry logic:
|
|
821
|
+
|
|
822
|
+
```typescript
|
|
823
|
+
// S3 operations have automatic retry
|
|
824
|
+
const files = await s3.listFiles(); // Automatic retry on network errors
|
|
825
|
+
|
|
826
|
+
// Parse files
|
|
827
|
+
const records = [];
|
|
828
|
+
for (const file of files) {
|
|
829
|
+
const content = await s3.downloadFile(file.path); // Automatic retry
|
|
830
|
+
const parsed = await parser.parse(content);
|
|
831
|
+
records.push(...parsed);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Use PartialBatchRecovery for batch submission
|
|
835
|
+
const result = await recovery.processBatchWithRecovery(
|
|
836
|
+
records,
|
|
837
|
+
async (batch) => {
|
|
838
|
+
const job = await client.createJob({ name: 'Sync', retailerId });
|
|
839
|
+
return await client.sendBatch(job.id, {
|
|
840
|
+
action: 'UPSERT',
|
|
841
|
+
entityType: 'INVENTORY',
|
|
842
|
+
entities: batch
|
|
843
|
+
});
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
maxRetries: 3,
|
|
847
|
+
retryOnlyFailed: true
|
|
848
|
+
}
|
|
849
|
+
);
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### Benefits of PartialBatchRecovery
|
|
853
|
+
|
|
854
|
+
**Efficiency:**
|
|
855
|
+
- ✅ Only retries failed records (not entire batch)
|
|
856
|
+
- ✅ Reduces API calls and processing time
|
|
857
|
+
- ✅ Lower resource consumption
|
|
858
|
+
|
|
859
|
+
**Reliability:**
|
|
860
|
+
- ✅ Tracks each record individually
|
|
861
|
+
- ✅ Configurable retry logic per record
|
|
862
|
+
- ✅ Detailed failure reporting
|
|
863
|
+
|
|
864
|
+
**Observability:**
|
|
865
|
+
- ✅ Know exactly which records failed
|
|
866
|
+
- ✅ Understand failure patterns
|
|
867
|
+
- ✅ Better error reporting for monitoring
|
|
868
|
+
|
|
869
|
+
**Example: 1000 records, 10 fail**
|
|
870
|
+
- **Without PartialBatchRecovery:** Retry all 1000 records
|
|
871
|
+
- **With PartialBatchRecovery:** Retry only 10 failed records
|
|
872
|
+
|
|
873
|
+
## Production-Ready Retry Function
|
|
874
|
+
|
|
875
|
+
Complete retry implementation with all best practices:
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
import {
|
|
879
|
+
IngestionError,
|
|
880
|
+
IngestionErrorCode,
|
|
881
|
+
FileParsingError,
|
|
882
|
+
PathResolutionError,
|
|
883
|
+
MappingError,
|
|
884
|
+
FluentAPIError,
|
|
885
|
+
GraphQLExecutionError
|
|
886
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
887
|
+
|
|
888
|
+
interface RetryOptions {
|
|
889
|
+
maxRetries?: number;
|
|
890
|
+
initialDelay?: number;
|
|
891
|
+
maxDelay?: number;
|
|
892
|
+
useJitter?: boolean;
|
|
893
|
+
onRetry?: (attempt: number, error: Error) => void;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async function retryOperation<T>(
|
|
897
|
+
operation: () => Promise<T>,
|
|
898
|
+
options: RetryOptions = {}
|
|
899
|
+
): Promise<T> {
|
|
900
|
+
const {
|
|
901
|
+
maxRetries = 3,
|
|
902
|
+
initialDelay = 1000,
|
|
903
|
+
maxDelay = 60000,
|
|
904
|
+
useJitter = true,
|
|
905
|
+
onRetry
|
|
906
|
+
} = options;
|
|
907
|
+
|
|
908
|
+
let lastError: Error;
|
|
909
|
+
|
|
910
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
911
|
+
try {
|
|
912
|
+
return await operation();
|
|
913
|
+
|
|
914
|
+
} catch (error) {
|
|
915
|
+
lastError = error;
|
|
916
|
+
|
|
917
|
+
// 1. Don't retry permanent errors
|
|
918
|
+
if (error instanceof FileParsingError ||
|
|
919
|
+
error instanceof PathResolutionError ||
|
|
920
|
+
error instanceof MappingError) {
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// 2. Check FluentAPIError (HTTP 5xx / 429)
|
|
925
|
+
let isRetryable = false;
|
|
926
|
+
let retryDelay = 0;
|
|
927
|
+
|
|
928
|
+
if (error instanceof FluentAPIError) {
|
|
929
|
+
if (error.statusCode >= 500 || error.statusCode === 429) {
|
|
930
|
+
isRetryable = true;
|
|
931
|
+
} else {
|
|
932
|
+
throw error; // 4xx are permanent
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// 3. Check IngestionError
|
|
937
|
+
else if (error instanceof IngestionError) {
|
|
938
|
+
if (error.isRetryable()) {
|
|
939
|
+
isRetryable = true;
|
|
940
|
+
// Special handling for rate limit error context
|
|
941
|
+
if (error.code === IngestionErrorCode.RATE_LIMIT_ERROR) {
|
|
942
|
+
retryDelay = (error.context?.retryAfter || 60) * 1000;
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// 4. Check GraphQLExecutionError
|
|
950
|
+
else if (error instanceof GraphQLExecutionError) {
|
|
951
|
+
const firstError = error.graphqlErrors[0];
|
|
952
|
+
if (firstError?.extensions?.code === 'TIMEOUT' ||
|
|
953
|
+
firstError?.extensions?.code === 'NETWORK_ERROR') {
|
|
954
|
+
isRetryable = true;
|
|
955
|
+
} else {
|
|
956
|
+
throw error; // Validation errors are permanent
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Last attempt - throw
|
|
961
|
+
if (attempt >= maxRetries - 1) {
|
|
962
|
+
throw error;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Calculate delay if not already set
|
|
966
|
+
if (!retryDelay) {
|
|
967
|
+
let delay = initialDelay * Math.pow(2, attempt);
|
|
968
|
+
if (useJitter) {
|
|
969
|
+
delay = Math.random() * delay;
|
|
970
|
+
}
|
|
971
|
+
retryDelay = Math.min(delay, maxDelay);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Callback
|
|
975
|
+
if (onRetry) {
|
|
976
|
+
onRetry(attempt + 1, error);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
throw lastError!;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Usage
|
|
987
|
+
const result = await retryOperation(
|
|
988
|
+
() => processOrder(xmlContent),
|
|
989
|
+
{
|
|
990
|
+
maxRetries: 5,
|
|
991
|
+
initialDelay: 2000,
|
|
992
|
+
maxDelay: 60000,
|
|
993
|
+
useJitter: true,
|
|
994
|
+
onRetry: (attempt, error) => {
|
|
995
|
+
logger.warn('Retrying operation', { attempt, error: error.message });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
);
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
## Key Takeaways
|
|
1002
|
+
|
|
1003
|
+
- 🎯 **SDK built-in retry** - S3DataSource and SftpDataSource have automatic retry
|
|
1004
|
+
- 🎯 **Use isRetryable()** - Let SDK determine if error is transient
|
|
1005
|
+
- 🎯 **Exponential backoff** - Increases delay between retries (with jitter)
|
|
1006
|
+
- 🎯 **Connection pooling** - SftpDataSource reuses connections efficiently
|
|
1007
|
+
- 🎯 **Add jitter** - Prevents thundering herd problem (full jitter recommended)
|
|
1008
|
+
- 🎯 **Respect rate limits** - Use `retryAfter` when provided
|
|
1009
|
+
- 🎯 **Set max retries** - Prevent infinite retry loops (default: 3)
|
|
1010
|
+
- 🎯 **Don't retry permanent errors** - Parse, mapping, validation errors won't succeed
|
|
1011
|
+
|
|
1012
|
+
## Practice Exercise
|
|
1013
|
+
|
|
1014
|
+
Implement a retry function that:
|
|
1015
|
+
1. Retries only transient errors
|
|
1016
|
+
2. Uses exponential backoff with jitter
|
|
1017
|
+
3. Respects rate limit headers
|
|
1018
|
+
4. Has a maximum of 5 retries
|
|
1019
|
+
|
|
1020
|
+
<details>
|
|
1021
|
+
<summary>Click to see solution</summary>
|
|
1022
|
+
|
|
1023
|
+
See the "Production-Ready Retry Function" section above for a complete implementation.
|
|
1024
|
+
|
|
1025
|
+
</details>
|
|
1026
|
+
|
|
1027
|
+
## Next Steps
|
|
1028
|
+
|
|
1029
|
+
Continue to [Module 7: Monitoring →](./error-handling-07-monitoring.md) to learn how to effectively log and monitor errors.
|
|
1030
|
+
|
|
1031
|
+
---
|
|
1032
|
+
|
|
1033
|
+
**Previous:** [← Module 5: Calling Patterns](./error-handling-05-calling-patterns.md)
|
|
1034
|
+
**Next:** [Module 7: Monitoring →](./error-handling-07-monitoring.md)
|