@fluentcommerce/fc-connect-sdk 0.1.53 → 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 +30 -2
- package/README.md +39 -0
- package/dist/cjs/auth/index.d.ts +3 -0
- package/dist/cjs/auth/index.js +13 -0
- package/dist/cjs/auth/profile-loader.d.ts +18 -0
- package/dist/cjs/auth/profile-loader.js +208 -0
- package/dist/cjs/client-factory.d.ts +4 -0
- package/dist/cjs/client-factory.js +10 -0
- package/dist/cjs/clients/fluent-client.js +13 -6
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/utils/pagination-helpers.js +38 -2
- package/dist/cjs/versori/fluent-versori-client.js +11 -5
- package/dist/esm/auth/index.d.ts +3 -0
- package/dist/esm/auth/index.js +2 -0
- package/dist/esm/auth/profile-loader.d.ts +18 -0
- package/dist/esm/auth/profile-loader.js +169 -0
- package/dist/esm/client-factory.d.ts +4 -0
- package/dist/esm/client-factory.js +9 -0
- package/dist/esm/clients/fluent-client.js +13 -6
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +2 -1
- 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/dist/types/auth/index.d.ts +3 -0
- package/dist/types/auth/profile-loader.d.ts +18 -0
- package/dist/types/client-factory.d.ts +4 -0
- package/dist/types/index.d.ts +3 -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 -482
- 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
|
@@ -1,1533 +1,1533 @@
|
|
|
1
|
-
# Versori KV State Management
|
|
2
|
-
|
|
3
|
-
**FC Connect SDK Use Case Guide**
|
|
4
|
-
|
|
5
|
-
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
6
|
-
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
7
|
-
|
|
8
|
-
**Context**: Use Versori KV storage for duplicate prevention, checkpoints, and file tracking
|
|
9
|
-
|
|
10
|
-
**Complexity**: Low-Medium
|
|
11
|
-
|
|
12
|
-
**Runtime**: Versori Platform
|
|
13
|
-
|
|
14
|
-
**Estimated Lines**: ~400 lines
|
|
15
|
-
|
|
16
|
-
## What You'll Build
|
|
17
|
-
|
|
18
|
-
- VersoriKV adapter setup
|
|
19
|
-
- File tracking (prevent duplicates)
|
|
20
|
-
- Checkpoint/resume capabilities
|
|
21
|
-
- Batch processing state
|
|
22
|
-
- Error tracking and retry logic
|
|
23
|
-
|
|
24
|
-
## SDK Methods Used
|
|
25
|
-
|
|
26
|
-
- `VersoriKVAdapter(openKv())` - Wrap Versori KV for StateService
|
|
27
|
-
- `VersoriFileTracker(openKv())` - Simple file tracking
|
|
28
|
-
- `VersoriIndexedFileTracker(openKv())` - File tracking with listing support
|
|
29
|
-
- `StateService(logger)` - High-level state operations
|
|
30
|
-
- `stateService.isFileProcessed(kv, key)` - Check if processed
|
|
31
|
-
- `stateService.acquireLock(lockName, kv, timeoutMinutes)` - Distributed locking
|
|
32
|
-
- `stateService.getSyncState(kv, workflowId)` - Get sync state
|
|
33
|
-
- `stateService.updateSyncState(kv, files, workflowId)` - Update sync state
|
|
34
|
-
- `stateService.getDailyJob(kv, workflowId)` - Get daily job
|
|
35
|
-
- `stateService.setDailyJob(kv, workflowId, jobId, hours)` - Store daily job
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## Versori Workflows Structure
|
|
40
|
-
|
|
41
|
-
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
42
|
-
|
|
43
|
-
**Trigger Types:**
|
|
44
|
-
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
45
|
-
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
46
|
-
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
47
|
-
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
48
|
-
|
|
49
|
-
### Recommended Project Structure
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
kv-state-management/
|
|
53
|
-
├── index.ts # Entry point - exports all workflows
|
|
54
|
-
└── src/
|
|
55
|
-
├── workflows/
|
|
56
|
-
│ ├── webhook/
|
|
57
|
-
│ │ └── file-ingestion.ts # Webhook: File processing
|
|
58
|
-
│ │
|
|
59
|
-
│ └── scheduled/
|
|
60
|
-
│ └── daily-sync.ts # Scheduled: Daily sync
|
|
61
|
-
│
|
|
62
|
-
├── services/
|
|
63
|
-
│ └── state-management.service.ts # Shared state logic (reusable)
|
|
64
|
-
│
|
|
65
|
-
└── config/
|
|
66
|
-
└── state-config.json # Configuration
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
**Benefits:**
|
|
70
|
-
- ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
|
|
71
|
-
- ✅ Descriptive file names (easy to browse and understand)
|
|
72
|
-
- ✅ Scalable (add new workflows without cluttering)
|
|
73
|
-
- ✅ Reusable code in `services/` (DRY principle)
|
|
74
|
-
- ✅ Easy to modify individual workflows without affecting others
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## Complete Working Code
|
|
79
|
-
|
|
80
|
-
### Pattern 1: Simple File Duplicate Prevention
|
|
81
|
-
|
|
82
|
-
**Use Case**: Prevent reprocessing the same files in ingestion workflows.
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
import { webhook, fn } from '@versori/run';
|
|
86
|
-
// FC Connect SDK+
|
|
87
|
-
// Install: npm install @fluentcommerce/fc-connect-sdk@latest
|
|
88
|
-
// Docs: https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk
|
|
89
|
-
// GitHub: https://github.com/fluentcommerce/fc-connect-sdk
|
|
90
|
-
import { VersoriFileTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Simple file tracking pattern
|
|
94
|
-
* Perfect for basic duplicate prevention without complex state management
|
|
95
|
-
*/
|
|
96
|
-
export const simpleFileIngestion = webhook('ingest-files', {
|
|
97
|
-
response: { mode: 'sync' }
|
|
98
|
-
})
|
|
99
|
-
.then(fn('check-and-process-files', async (ctx) => {
|
|
100
|
-
const { openKv, log, data } = ctx;
|
|
101
|
-
|
|
102
|
-
// Initialize simple file tracker
|
|
103
|
-
const fileTracker = new VersoriFileTracker(openKv(':project:'), 'inventory-ingestion');
|
|
104
|
-
|
|
105
|
-
// Sample files to process
|
|
106
|
-
const files = data.files || [
|
|
107
|
-
{ name: 'inventory-2025-01-15.csv', url: 's3://bucket/file1.csv' },
|
|
108
|
-
{ name: 'inventory-2025-01-16.csv', url: 's3://bucket/file2.csv' }
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
const processedFiles = [];
|
|
112
|
-
const skippedFiles = [];
|
|
113
|
-
|
|
114
|
-
for (const file of files) {
|
|
115
|
-
// Check if file was already processed
|
|
116
|
-
const wasProcessed = await fileTracker.wasFileProcessed(file.name);
|
|
117
|
-
|
|
118
|
-
if (wasProcessed) {
|
|
119
|
-
log.info(`⏭️ [FileTracking] Skipping already processed file: ${file.name}`);
|
|
120
|
-
skippedFiles.push(file.name);
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
// Process the file (your ingestion logic here)
|
|
126
|
-
log.info(`🚀 [Processing] Processing file: ${file.name}`);
|
|
127
|
-
const recordCount = await processFile(file);
|
|
128
|
-
|
|
129
|
-
// Mark as processed with metadata
|
|
130
|
-
await fileTracker.markFileProcessed(file.name, {
|
|
131
|
-
recordCount,
|
|
132
|
-
processedBy: 'webhook-ingestion',
|
|
133
|
-
source: file.url
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
processedFiles.push({ name: file.name, recordCount });
|
|
137
|
-
log.info(`✅ [Success] Successfully processed: ${file.name} (${recordCount} records)`);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
// ? Enhanced: Error logging with recommendations
|
|
140
|
-
log.error('[KVStateManagement] Failed to process file', {
|
|
141
|
-
fileName: file.name,
|
|
142
|
-
error: error instanceof Error ? error.message : String(error),
|
|
143
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
144
|
-
recommendation: error.message?.includes('parse') || error.message?.includes('format')
|
|
145
|
-
? 'Check file format and structure - ensure file is valid'
|
|
146
|
-
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
147
|
-
? 'Check mapping configuration and verify file column structure'
|
|
148
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
149
|
-
? 'Check network connectivity and data source availability'
|
|
150
|
-
: 'Review error details and check file processing logic'
|
|
151
|
-
});
|
|
152
|
-
throw error; // Fail workflow so we can retry later
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Track last processed file
|
|
157
|
-
if (processedFiles.length > 0) {
|
|
158
|
-
const lastFile = processedFiles[processedFiles.length - 1];
|
|
159
|
-
await fileTracker.setLastProcessedFile(lastFile.name);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
success: true,
|
|
164
|
-
processed: processedFiles,
|
|
165
|
-
skipped: skippedFiles,
|
|
166
|
-
lastProcessedFile: await fileTracker.getLastProcessedFile()
|
|
167
|
-
};
|
|
168
|
-
}));
|
|
169
|
-
|
|
170
|
-
// Helper function (implement based on your data source)
|
|
171
|
-
async function processFile(file: { name: string; url: string }): Promise<number> {
|
|
172
|
-
// Your file processing logic
|
|
173
|
-
return 1000; // Return record count
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
**Key Points**:
|
|
178
|
-
|
|
179
|
-
- `VersoriFileTracker` is lightweight - perfect for simple use cases
|
|
180
|
-
- Automatic duplicate prevention with `wasFileProcessed()`
|
|
181
|
-
- Stores metadata like record count with each file
|
|
182
|
-
- Tracks last processed file for incremental processing
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
### Pattern 2: Distributed Locking with StateService
|
|
187
|
-
|
|
188
|
-
**Use Case**: Prevent concurrent workflow executions with distributed locks.
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
import { schedule, fn } from '@versori/run';
|
|
192
|
-
import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Distributed locking pattern
|
|
196
|
-
* Prevents multiple instances from running simultaneously
|
|
197
|
-
*/
|
|
198
|
-
export const scheduledIngestionWithLock = schedule('inventory-sync', '0 */6 * * *')
|
|
199
|
-
.then(fn('acquire-lock', async (ctx) => {
|
|
200
|
-
const { openKv, log } = ctx;
|
|
201
|
-
|
|
202
|
-
// Create StateService with KV adapter
|
|
203
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
204
|
-
const stateService = new StateService(log);
|
|
205
|
-
|
|
206
|
-
// Try to acquire lock (15 minute timeout)
|
|
207
|
-
const lockName = 'inventory-sync-lock';
|
|
208
|
-
const lockAcquired = await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
209
|
-
|
|
210
|
-
if (!lockAcquired) {
|
|
211
|
-
log.warn('⚠️ [Lock] Lock already held, skipping this run');
|
|
212
|
-
return {
|
|
213
|
-
shouldSkip: true,
|
|
214
|
-
reason: 'Lock already held by another instance'
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
log.info('🔐 [Lock] Lock acquired successfully');
|
|
219
|
-
return {
|
|
220
|
-
shouldSkip: false,
|
|
221
|
-
lockName,
|
|
222
|
-
kvAdapter,
|
|
223
|
-
stateService
|
|
224
|
-
};
|
|
225
|
-
}))
|
|
226
|
-
.then(fn('process-with-lock', async ({ data, log }) => {
|
|
227
|
-
if (data.shouldSkip) {
|
|
228
|
-
return data;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const { kvAdapter, stateService, lockName } = data;
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
// Your ingestion logic here
|
|
235
|
-
log.info('🚀 [Processing] Processing inventory sync...');
|
|
236
|
-
await performIngestion();
|
|
237
|
-
log.info('✅ [Success] Ingestion completed successfully');
|
|
238
|
-
|
|
239
|
-
return { success: true };
|
|
240
|
-
} finally {
|
|
241
|
-
// ALWAYS release lock in finally block
|
|
242
|
-
await stateService.releaseLock(lockName, kvAdapter);
|
|
243
|
-
log.info('🔓 [Lock] Lock released');
|
|
244
|
-
}
|
|
245
|
-
}))
|
|
246
|
-
.catch(fn('handle-error-and-release-lock', async ({ data, error, log }) => {
|
|
247
|
-
// Ensure lock is released even on error
|
|
248
|
-
if (data?.stateService && data?.lockName && data?.kvAdapter) {
|
|
249
|
-
await data.stateService.releaseLock(data.lockName, data.kvAdapter);
|
|
250
|
-
log.info('🔓 [Lock] Lock released after error');
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
log.error('❌ [Error] Ingestion failed', error as Error);
|
|
254
|
-
return { success: false, error: (error as Error).message };
|
|
255
|
-
}));
|
|
256
|
-
|
|
257
|
-
async function performIngestion(): Promise<void> {
|
|
258
|
-
// Your ingestion logic
|
|
259
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
260
|
-
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
**Key Points**:
|
|
264
|
-
|
|
265
|
-
- Distributed locking prevents concurrent executions
|
|
266
|
-
- Stale lock detection (automatically overrides expired locks)
|
|
267
|
-
- Lock timeout ensures recovery from crashes
|
|
268
|
-
- ALWAYS release locks in finally blocks
|
|
269
|
-
- Proper error handling to prevent lock leaks
|
|
270
|
-
|
|
271
|
-
---
|
|
272
|
-
|
|
273
|
-
### Pattern 3: Checkpoint & Resume
|
|
274
|
-
|
|
275
|
-
**Use Case**: Save progress during long-running workflows and resume from last checkpoint.
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
import { webhook, fn } from '@versori/run';
|
|
279
|
-
import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
280
|
-
|
|
281
|
-
interface CheckpointData {
|
|
282
|
-
currentBatch: number;
|
|
283
|
-
totalBatches: number;
|
|
284
|
-
processedRecords: number;
|
|
285
|
-
lastProcessedId: string;
|
|
286
|
-
startTime: string;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Checkpoint & resume pattern
|
|
291
|
-
* Useful for long-running batch processing that might fail partway through
|
|
292
|
-
*/
|
|
293
|
-
export const batchProcessingWithCheckpoints = webhook('process-batches', {
|
|
294
|
-
response: { mode: 'sync' }
|
|
295
|
-
})
|
|
296
|
-
.then(fn('check-checkpoint', async (ctx) => {
|
|
297
|
-
const { openKv, log, data } = ctx;
|
|
298
|
-
|
|
299
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
300
|
-
const stateService = new StateService(log);
|
|
301
|
-
const workflowId = 'batch-processing';
|
|
302
|
-
const checkpointKey = `checkpoint:${workflowId}`;
|
|
303
|
-
|
|
304
|
-
// Try to restore from checkpoint
|
|
305
|
-
const syncState = await stateService.getSyncState(kvAdapter, workflowId);
|
|
306
|
-
let checkpoint: CheckpointData | null = null;
|
|
307
|
-
|
|
308
|
-
if (syncState.isInitialized) {
|
|
309
|
-
// Check if there's a saved checkpoint (stored in custom state)
|
|
310
|
-
const stored = await kvAdapter.get([checkpointKey]);
|
|
311
|
-
if (stored?.value) {
|
|
312
|
-
checkpoint = stored.value as CheckpointData;
|
|
313
|
-
log.info('♻️ [Checkpoint] Resuming from checkpoint', checkpoint);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Get batches to process
|
|
318
|
-
const allBatches = data.batches || generateBatches(100); // 100 batches total
|
|
319
|
-
|
|
320
|
-
// Determine starting point
|
|
321
|
-
const startBatch = checkpoint ? checkpoint.currentBatch : 0;
|
|
322
|
-
const processedSoFar = checkpoint ? checkpoint.processedRecords : 0;
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
batches: allBatches,
|
|
326
|
-
startBatch,
|
|
327
|
-
processedSoFar,
|
|
328
|
-
kvAdapter,
|
|
329
|
-
stateService,
|
|
330
|
-
workflowId,
|
|
331
|
-
checkpointKey,
|
|
332
|
-
startTime: checkpoint?.startTime || new Date().toISOString()
|
|
333
|
-
};
|
|
334
|
-
}))
|
|
335
|
-
.then(fn('process-with-checkpoints', async ({ data, log }) => {
|
|
336
|
-
const {
|
|
337
|
-
batches,
|
|
338
|
-
startBatch,
|
|
339
|
-
processedSoFar,
|
|
340
|
-
kvAdapter,
|
|
341
|
-
stateService,
|
|
342
|
-
workflowId,
|
|
343
|
-
checkpointKey,
|
|
344
|
-
startTime
|
|
345
|
-
} = data;
|
|
346
|
-
|
|
347
|
-
let processedRecords = processedSoFar;
|
|
348
|
-
|
|
349
|
-
// Process batches starting from checkpoint
|
|
350
|
-
for (let i = startBatch; i < batches.length; i++) {
|
|
351
|
-
const batch = batches[i];
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
log.info(`🚀 [Processing] Processing batch ${i + 1}/${batches.length}`);
|
|
355
|
-
|
|
356
|
-
// Process the batch
|
|
357
|
-
const result = await processBatch(batch);
|
|
358
|
-
processedRecords += result.recordCount;
|
|
359
|
-
|
|
360
|
-
// Save checkpoint after each batch (or every N batches)
|
|
361
|
-
if ((i + 1) % 10 === 0 || i === batches.length - 1) {
|
|
362
|
-
const checkpoint: CheckpointData = {
|
|
363
|
-
currentBatch: i + 1,
|
|
364
|
-
totalBatches: batches.length,
|
|
365
|
-
processedRecords,
|
|
366
|
-
lastProcessedId: result.lastId,
|
|
367
|
-
startTime
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
await kvAdapter.set([checkpointKey], checkpoint);
|
|
371
|
-
log.info(`💾 [Checkpoint] Checkpoint saved at batch ${i + 1}`);
|
|
372
|
-
}
|
|
373
|
-
} catch (error) {
|
|
374
|
-
// ? Enhanced: Error logging with recommendations
|
|
375
|
-
log.error('[KVStateManagement] Batch processing failed', {
|
|
376
|
-
batchNumber: i + 1,
|
|
377
|
-
error: error instanceof Error ? error.message : String(error),
|
|
378
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
379
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
380
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
381
|
-
: error.message?.includes('mutation') || error.message?.includes('GraphQL')
|
|
382
|
-
? 'Check GraphQL mutation syntax and batch payload structure'
|
|
383
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
384
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
385
|
-
: 'Review error details - checkpoint saved for retry'
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
// Save checkpoint at failure point
|
|
389
|
-
const checkpoint: CheckpointData = {
|
|
390
|
-
currentBatch: i, // Retry this batch
|
|
391
|
-
totalBatches: batches.length,
|
|
392
|
-
processedRecords,
|
|
393
|
-
lastProcessedId: batch[0]?.id || '',
|
|
394
|
-
startTime
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
await kvAdapter.set([checkpointKey], checkpoint);
|
|
398
|
-
throw error; // Fail workflow so it can be retried
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// All batches processed - clear checkpoint
|
|
403
|
-
await kvAdapter.delete([checkpointKey]);
|
|
404
|
-
log.info('✅ [Checkpoint] All batches processed, checkpoint cleared');
|
|
405
|
-
|
|
406
|
-
// Update final sync state
|
|
407
|
-
await stateService.updateSyncState(kvAdapter, [{
|
|
408
|
-
fileName: 'batch-processing-complete',
|
|
409
|
-
lastModified: new Date().toISOString(),
|
|
410
|
-
recordCount: processedRecords
|
|
411
|
-
}], workflowId);
|
|
412
|
-
|
|
413
|
-
return {
|
|
414
|
-
success: true,
|
|
415
|
-
totalBatches: batches.length,
|
|
416
|
-
processedRecords,
|
|
417
|
-
duration: Date.now() - new Date(startTime).getTime()
|
|
418
|
-
};
|
|
419
|
-
}));
|
|
420
|
-
|
|
421
|
-
function generateBatches(count: number): any[] {
|
|
422
|
-
return Array.from({ length: count }, (_, i) => ({
|
|
423
|
-
id: `batch-${i + 1}`,
|
|
424
|
-
data: []
|
|
425
|
-
}));
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async function processBatch(batch: any): Promise<{ recordCount: number; lastId: string }> {
|
|
429
|
-
// Your batch processing logic
|
|
430
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
431
|
-
return { recordCount: 50, lastId: batch.id };
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
**Key Points**:
|
|
436
|
-
|
|
437
|
-
- Save checkpoints periodically (not after every operation)
|
|
438
|
-
- Store enough context to resume exactly where you left off
|
|
439
|
-
- Clear checkpoint after successful completion
|
|
440
|
-
- On failure, checkpoint allows resuming without reprocessing
|
|
441
|
-
|
|
442
|
-
---
|
|
443
|
-
|
|
444
|
-
### Pattern 4: Daily Job Management
|
|
445
|
-
|
|
446
|
-
**Use Case**: Reuse jobs across multiple batches throughout the day (DAILY strategy).
|
|
447
|
-
|
|
448
|
-
```typescript
|
|
449
|
-
import { webhook, fn } from '@versori/run';
|
|
450
|
-
import {
|
|
451
|
-
StateService,
|
|
452
|
-
VersoriKVAdapter,
|
|
453
|
-
createClient
|
|
454
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Daily job management pattern
|
|
458
|
-
* Reuses a single job for multiple batches within a day
|
|
459
|
-
*/
|
|
460
|
-
export const dailyJobIngestion = webhook('ingest-daily', {
|
|
461
|
-
response: { mode: 'sync' }
|
|
462
|
-
})
|
|
463
|
-
.then(fn('get-or-create-job', async (ctx) => {
|
|
464
|
-
// Destructure context inside function body
|
|
465
|
-
const { openKv, log, connections, activation } = ctx;
|
|
466
|
-
|
|
467
|
-
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
468
|
-
const stateService = new StateService(log);
|
|
469
|
-
const workflowId = 'daily-inventory-ingestion';
|
|
470
|
-
|
|
471
|
-
// Check for existing daily job
|
|
472
|
-
const existingJob = await stateService.getDailyJob(kvAdapter, workflowId);
|
|
473
|
-
|
|
474
|
-
let jobId: string;
|
|
475
|
-
let isNewJob = false;
|
|
476
|
-
|
|
477
|
-
if (existingJob && existingJob.jobId) {
|
|
478
|
-
// Reuse existing job
|
|
479
|
-
jobId = existingJob.jobId;
|
|
480
|
-
log.info(`♻️ [DailyJob] Reusing daily job: ${jobId}`, {
|
|
481
|
-
createdAt: existingJob.createdAt,
|
|
482
|
-
expiresAt: existingJob.expiresAt
|
|
483
|
-
});
|
|
484
|
-
} else {
|
|
485
|
-
// Create new job
|
|
486
|
-
const client = await createClient(ctx); // Auto-detects Versori context
|
|
487
|
-
|
|
488
|
-
const job = await client.createJob({
|
|
489
|
-
name: `daily-inventory-${new Date().toISOString().split('T')[0]}`,
|
|
490
|
-
retailerId: activation.getVariable('fluentRetailerId') as string
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
jobId = job.id;
|
|
494
|
-
isNewJob = true;
|
|
495
|
-
|
|
496
|
-
// Store job for 24 hours
|
|
497
|
-
await stateService.setDailyJob(kvAdapter, workflowId, jobId, 24);
|
|
498
|
-
log.info(`🆕 [DailyJob] Created new daily job: ${jobId}`);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return {
|
|
502
|
-
jobId,
|
|
503
|
-
isNewJob,
|
|
504
|
-
kvAdapter,
|
|
505
|
-
stateService,
|
|
506
|
-
workflowId,
|
|
507
|
-
client: await createClient(ctx) // Auto-detects Versori context
|
|
508
|
-
};
|
|
509
|
-
}))
|
|
510
|
-
.then(fn('send-batches-to-job', async ({ data, log }) => {
|
|
511
|
-
const { jobId, client, kvAdapter, stateService, workflowId } = data;
|
|
512
|
-
|
|
513
|
-
// Your batches to process
|
|
514
|
-
const batches = [
|
|
515
|
-
{ entities: [{ skuRef: 'SKU001', qty: 100 }] },
|
|
516
|
-
{ entities: [{ skuRef: 'SKU002', qty: 200 }] }
|
|
517
|
-
];
|
|
518
|
-
|
|
519
|
-
const batchResults = [];
|
|
520
|
-
|
|
521
|
-
for (const batch of batches) {
|
|
522
|
-
try {
|
|
523
|
-
const result = await client.sendBatch(jobId, {
|
|
524
|
-
action: 'UPSERT',
|
|
525
|
-
entityType: 'INVENTORY_CATALOGUE',
|
|
526
|
-
entities: batch.entities
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
batchResults.push({ success: true, batchId: result.id });
|
|
530
|
-
log.info(`✅ [Batch] Batch sent to job ${jobId}: ${result.id}`);
|
|
531
|
-
} catch (error) {
|
|
532
|
-
// ? Enhanced: Error logging with recommendations
|
|
533
|
-
log.error('[KVStateManagement] Batch submission failed', {
|
|
534
|
-
batchId: result?.id,
|
|
535
|
-
error: error instanceof Error ? error.message : String(error),
|
|
536
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
537
|
-
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
538
|
-
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
539
|
-
: error.message?.includes('batch') || error.message?.includes('job')
|
|
540
|
-
? 'Check batch API payload and job status'
|
|
541
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
542
|
-
? 'Check network connectivity and Fluent Commerce API availability'
|
|
543
|
-
: 'Review error details and check batch submission payload'
|
|
544
|
-
});
|
|
545
|
-
batchResults.push({ success: false, error: (error as Error).message });
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Update sync state with processed records
|
|
550
|
-
await stateService.updateSyncState(kvAdapter, [{
|
|
551
|
-
fileName: `daily-batch-${new Date().toISOString()}`,
|
|
552
|
-
lastModified: new Date().toISOString(),
|
|
553
|
-
recordCount: batches.reduce((sum, b) => sum + b.entities.length, 0)
|
|
554
|
-
}], workflowId);
|
|
555
|
-
|
|
556
|
-
return {
|
|
557
|
-
success: true,
|
|
558
|
-
jobId,
|
|
559
|
-
batchCount: batchResults.length,
|
|
560
|
-
successCount: batchResults.filter(r => r.success).length
|
|
561
|
-
};
|
|
562
|
-
}));
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
**Key Points**:
|
|
566
|
-
|
|
567
|
-
- Single job per day reduces API overhead
|
|
568
|
-
- Job expires automatically after 24 hours
|
|
569
|
-
- Tracks batch count per job
|
|
570
|
-
- Perfect for high-frequency ingestion workflows
|
|
571
|
-
|
|
572
|
-
---
|
|
573
|
-
|
|
574
|
-
### Pattern 5: Error State Management
|
|
575
|
-
|
|
576
|
-
**Use Case**: Track errors and implement retry logic with exponential backoff.
|
|
577
|
-
|
|
578
|
-
```typescript
|
|
579
|
-
import { webhook, fn } from '@versori/run';
|
|
580
|
-
import { VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
581
|
-
|
|
582
|
-
interface ErrorState {
|
|
583
|
-
fileName: string;
|
|
584
|
-
attemptCount: number;
|
|
585
|
-
lastError: string;
|
|
586
|
-
lastAttemptAt: string;
|
|
587
|
-
firstFailedAt: string;
|
|
588
|
-
nextRetryAt: string;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* Error state management pattern
|
|
593
|
-
* Tracks failures and implements smart retry logic
|
|
594
|
-
*/
|
|
595
|
-
export const retryableIngestion = webhook('ingest-with-retry', {
|
|
596
|
-
response: { mode: 'sync' }
|
|
597
|
-
})
|
|
598
|
-
.then(fn('process-with-retry-tracking', async (ctx) => {
|
|
599
|
-
const { openKv, log, data } = ctx;
|
|
600
|
-
|
|
601
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
602
|
-
const files = data.files || ['file1.csv', 'file2.csv', 'file3.csv'];
|
|
603
|
-
|
|
604
|
-
const results = {
|
|
605
|
-
succeeded: [] as string[],
|
|
606
|
-
failed: [] as string[],
|
|
607
|
-
skipped: [] as string[]
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
for (const fileName of files) {
|
|
611
|
-
const errorStateKey = ['error-state', fileName];
|
|
612
|
-
|
|
613
|
-
// Check if file has error state
|
|
614
|
-
const errorStateResult = await kvAdapter.get(errorStateKey);
|
|
615
|
-
const errorState = errorStateResult?.value as ErrorState | undefined;
|
|
616
|
-
|
|
617
|
-
// Check if we should retry
|
|
618
|
-
if (errorState) {
|
|
619
|
-
const nextRetry = new Date(errorState.nextRetryAt);
|
|
620
|
-
const now = new Date();
|
|
621
|
-
|
|
622
|
-
if (now < nextRetry) {
|
|
623
|
-
log.info(`⏭️ [Retry] Skipping ${fileName} until ${errorState.nextRetryAt}`, {
|
|
624
|
-
attemptCount: errorState.attemptCount,
|
|
625
|
-
lastError: errorState.lastError
|
|
626
|
-
});
|
|
627
|
-
results.skipped.push(fileName);
|
|
628
|
-
continue;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// Max retry attempts reached?
|
|
632
|
-
if (errorState.attemptCount >= 5) {
|
|
633
|
-
log.warn(`⚠️ [Retry] Max retries exceeded for ${fileName}, requires manual intervention`);
|
|
634
|
-
results.skipped.push(fileName);
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
log.info(`♻️ [Retry] Retrying ${fileName} (attempt ${errorState.attemptCount + 1})`, {
|
|
639
|
-
lastError: errorState.lastError,
|
|
640
|
-
firstFailedAt: errorState.firstFailedAt
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
try {
|
|
645
|
-
// Process the file
|
|
646
|
-
await processFileWithPossibleFailure(fileName);
|
|
647
|
-
|
|
648
|
-
// Success! Clear error state
|
|
649
|
-
if (errorState) {
|
|
650
|
-
await kvAdapter.delete(errorStateKey);
|
|
651
|
-
log.info(`✅ [Retry] File recovered after ${errorState.attemptCount} retries: ${fileName}`);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
results.succeeded.push(fileName);
|
|
655
|
-
} catch (error) {
|
|
656
|
-
// ? Enhanced: Error logging with recommendations
|
|
657
|
-
const errorMessage = (error as Error).message;
|
|
658
|
-
const attemptCount = errorState ? errorState.attemptCount + 1 : 1;
|
|
659
|
-
const now = new Date();
|
|
660
|
-
|
|
661
|
-
// Calculate next retry with exponential backoff
|
|
662
|
-
const backoffMinutes = Math.pow(2, attemptCount) * 5; // 5, 10, 20, 40, 80 minutes
|
|
663
|
-
const nextRetryAt = new Date(now.getTime() + backoffMinutes * 60 * 1000);
|
|
664
|
-
|
|
665
|
-
// Save error state
|
|
666
|
-
const newErrorState: ErrorState = {
|
|
667
|
-
fileName,
|
|
668
|
-
attemptCount,
|
|
669
|
-
lastError: errorMessage,
|
|
670
|
-
lastAttemptAt: now.toISOString(),
|
|
671
|
-
firstFailedAt: errorState?.firstFailedAt || now.toISOString(),
|
|
672
|
-
nextRetryAt: nextRetryAt.toISOString()
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
await kvAdapter.set(errorStateKey, newErrorState);
|
|
676
|
-
|
|
677
|
-
log.error('[KVStateManagement] File processing failed', {
|
|
678
|
-
fileName,
|
|
679
|
-
attemptCount,
|
|
680
|
-
error: error instanceof Error ? error.message : String(error),
|
|
681
|
-
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
682
|
-
nextRetryAt: nextRetryAt.toISOString(),
|
|
683
|
-
backoffMinutes,
|
|
684
|
-
recommendation: error.message?.includes('parse') || error.message?.includes('format')
|
|
685
|
-
? 'Check file format and structure - ensure file is valid'
|
|
686
|
-
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
687
|
-
? 'Check mapping configuration and verify file column structure'
|
|
688
|
-
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
689
|
-
? 'Check network connectivity and data source availability'
|
|
690
|
-
: `Will retry after ${backoffMinutes} minutes - review error details`
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
results.failed.push(fileName);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
return {
|
|
698
|
-
success: true,
|
|
699
|
-
results,
|
|
700
|
-
summary: {
|
|
701
|
-
succeeded: results.succeeded.length,
|
|
702
|
-
failed: results.failed.length,
|
|
703
|
-
skipped: results.skipped.length
|
|
704
|
-
}
|
|
705
|
-
};
|
|
706
|
-
}))
|
|
707
|
-
.then(fn('cleanup-old-errors', async (ctx) => {
|
|
708
|
-
const { openKv, log, data } = ctx;
|
|
709
|
-
|
|
710
|
-
// Cleanup error states older than 7 days
|
|
711
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
712
|
-
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
713
|
-
|
|
714
|
-
// Note: This requires tracking error state keys separately
|
|
715
|
-
// In production, use VersoriIndexedFileTracker pattern
|
|
716
|
-
|
|
717
|
-
log.info('Error state cleanup completed');
|
|
718
|
-
return data;
|
|
719
|
-
}));
|
|
720
|
-
|
|
721
|
-
async function processFileWithPossibleFailure(fileName: string): Promise<void> {
|
|
722
|
-
// Simulate random failures for demonstration
|
|
723
|
-
if (Math.random() < 0.3) {
|
|
724
|
-
throw new Error('Simulated processing failure');
|
|
725
|
-
}
|
|
726
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
727
|
-
}
|
|
728
|
-
```
|
|
729
|
-
|
|
730
|
-
**Key Points**:
|
|
731
|
-
|
|
732
|
-
- Exponential backoff prevents overwhelming failing systems
|
|
733
|
-
- Track first failure time for monitoring
|
|
734
|
-
- Max retry limit prevents infinite loops
|
|
735
|
-
- Clear error state on success
|
|
736
|
-
- Skip files until retry time
|
|
737
|
-
|
|
738
|
-
---
|
|
739
|
-
|
|
740
|
-
### Pattern 6: Advanced File Tracking with Indexing
|
|
741
|
-
|
|
742
|
-
**Use Case**: Track files with ability to list all processed files.
|
|
743
|
-
|
|
744
|
-
```typescript
|
|
745
|
-
import { webhook, fn } from '@versori/run';
|
|
746
|
-
import { VersoriIndexedFileTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* Advanced file tracking with indexing
|
|
750
|
-
* Allows listing all processed files (overcomes Versori KV list limitation)
|
|
751
|
-
*/
|
|
752
|
-
export const indexedFileTracking = webhook('manage-files', {
|
|
753
|
-
response: { mode: 'sync' }
|
|
754
|
-
})
|
|
755
|
-
.then(fn('track-and-manage-files', async (ctx) => {
|
|
756
|
-
const { openKv, log, data } = ctx;
|
|
757
|
-
|
|
758
|
-
// Initialize indexed file tracker
|
|
759
|
-
const fileTracker = new VersoriIndexedFileTracker(openKv(':project:'), 'inventory-ingestion');
|
|
760
|
-
const operation = data.operation || 'process'; // process, list, stats, cleanup
|
|
761
|
-
|
|
762
|
-
switch (operation) {
|
|
763
|
-
case 'process': {
|
|
764
|
-
const files = data.files || ['file1.csv', 'file2.csv'];
|
|
765
|
-
|
|
766
|
-
for (const file of files) {
|
|
767
|
-
// Process file
|
|
768
|
-
log.info(`🚀 [Processing] Processing: ${file}`);
|
|
769
|
-
const recordCount = Math.floor(Math.random() * 1000) + 100;
|
|
770
|
-
|
|
771
|
-
// Mark as processed (automatically updates index)
|
|
772
|
-
await fileTracker.markFileProcessed(file, {
|
|
773
|
-
recordCount,
|
|
774
|
-
source: 's3://bucket/' + file,
|
|
775
|
-
processingDuration: 1234
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
return { success: true, processed: files.length };
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
case 'list': {
|
|
783
|
-
// List all processed files
|
|
784
|
-
const processedFiles = await fileTracker.listProcessedFiles();
|
|
785
|
-
log.info(`📋 [Index] Found ${processedFiles.length} processed files`);
|
|
786
|
-
|
|
787
|
-
return {
|
|
788
|
-
success: true,
|
|
789
|
-
files: processedFiles.map(f => ({
|
|
790
|
-
name: f.fileName,
|
|
791
|
-
processedAt: f.metadata?.processedAt,
|
|
792
|
-
recordCount: f.metadata?.recordCount
|
|
793
|
-
}))
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
case 'stats': {
|
|
798
|
-
// Get processing statistics
|
|
799
|
-
const stats = await fileTracker.getStats();
|
|
800
|
-
log.info('📊 [Index] File processing statistics', stats);
|
|
801
|
-
|
|
802
|
-
return {
|
|
803
|
-
success: true,
|
|
804
|
-
stats
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
case 'cleanup': {
|
|
809
|
-
// Clear all processed files
|
|
810
|
-
const result = await fileTracker.clearAllProcessedFiles();
|
|
811
|
-
log.info(`🧹 [Index] Cleanup complete: ${result.success} deleted, ${result.failed} failed`);
|
|
812
|
-
|
|
813
|
-
return {
|
|
814
|
-
success: true,
|
|
815
|
-
deleted: result.success,
|
|
816
|
-
failed: result.failed
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
case 'remove': {
|
|
821
|
-
// Remove specific file
|
|
822
|
-
const fileName = data.fileName;
|
|
823
|
-
if (!fileName) {
|
|
824
|
-
throw new Error('fileName required for remove operation');
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
await fileTracker.removeProcessedFile(fileName);
|
|
828
|
-
log.info(`🗑️ [Index] Removed file: ${fileName}`);
|
|
829
|
-
|
|
830
|
-
return { success: true, removed: fileName };
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
default:
|
|
834
|
-
throw new Error(`Unknown operation: ${operation}`);
|
|
835
|
-
}
|
|
836
|
-
}));
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
**Key Points**:
|
|
840
|
-
|
|
841
|
-
- Maintains internal index to overcome Versori KV list limitation
|
|
842
|
-
- List all processed files
|
|
843
|
-
- Get aggregate statistics
|
|
844
|
-
- Cleanup operations
|
|
845
|
-
- Automatic index maintenance
|
|
846
|
-
|
|
847
|
-
---
|
|
848
|
-
|
|
849
|
-
### Pattern 7: MemoryInterpreter for In-Memory State
|
|
850
|
-
|
|
851
|
-
**Use Case**: Store interpreter state in memory for long-running connections (SFTP, database connections).
|
|
852
|
-
|
|
853
|
-
```typescript
|
|
854
|
-
import { webhook, fn } from '@versori/run';
|
|
855
|
-
import { VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
856
|
-
|
|
857
|
-
interface MemoryInterpreterState {
|
|
858
|
-
sftpConnections: Map<string, any>;
|
|
859
|
-
lastActivity: Map<string, number>;
|
|
860
|
-
connectionPool: any[];
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
/**
|
|
864
|
-
* MemoryInterpreter pattern
|
|
865
|
-
* Maintains in-memory state for connection pools and active sessions
|
|
866
|
-
*/
|
|
867
|
-
export const connectionManager = webhook('manage-connections', {
|
|
868
|
-
response: { mode: 'sync' }
|
|
869
|
-
})
|
|
870
|
-
.then(fn('initialize-memory-state', async (ctx) => {
|
|
871
|
-
const { openKv, log } = ctx;
|
|
872
|
-
|
|
873
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
874
|
-
const stateKey = ['memory-interpreter', 'connections'];
|
|
875
|
-
|
|
876
|
-
// Load existing state or initialize new
|
|
877
|
-
const existingState = await kvAdapter.get(stateKey);
|
|
878
|
-
|
|
879
|
-
const memoryState: MemoryInterpreterState = existingState?.value || {
|
|
880
|
-
sftpConnections: new Map(),
|
|
881
|
-
lastActivity: new Map(),
|
|
882
|
-
connectionPool: []
|
|
883
|
-
};
|
|
884
|
-
|
|
885
|
-
log.info('🧠 [MemoryInterpreter] State initialized', {
|
|
886
|
-
activeConnections: memoryState.sftpConnections.size,
|
|
887
|
-
poolSize: memoryState.connectionPool.length
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
return {
|
|
891
|
-
kvAdapter,
|
|
892
|
-
stateKey,
|
|
893
|
-
memoryState
|
|
894
|
-
};
|
|
895
|
-
}))
|
|
896
|
-
.then(fn('process-with-memory-state', async ({ data, log }) => {
|
|
897
|
-
const { kvAdapter, stateKey, memoryState } = data;
|
|
898
|
-
|
|
899
|
-
// Use memory state for processing
|
|
900
|
-
const connectionId = 'sftp-main';
|
|
901
|
-
const now = Date.now();
|
|
902
|
-
|
|
903
|
-
// Check if connection is stale (> 5 minutes inactive)
|
|
904
|
-
const lastActivity = memoryState.lastActivity.get(connectionId);
|
|
905
|
-
const isStale = lastActivity && (now - lastActivity) > 5 * 60 * 1000;
|
|
906
|
-
|
|
907
|
-
if (isStale) {
|
|
908
|
-
log.info('♻️ [MemoryInterpreter] Refreshing stale connection', {
|
|
909
|
-
connectionId,
|
|
910
|
-
lastActivity: new Date(lastActivity).toISOString()
|
|
911
|
-
});
|
|
912
|
-
|
|
913
|
-
// Refresh connection
|
|
914
|
-
memoryState.sftpConnections.delete(connectionId);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// Update activity timestamp
|
|
918
|
-
memoryState.lastActivity.set(connectionId, now);
|
|
919
|
-
|
|
920
|
-
// Persist updated state
|
|
921
|
-
await kvAdapter.set(stateKey, {
|
|
922
|
-
sftpConnections: Array.from(memoryState.sftpConnections.entries()),
|
|
923
|
-
lastActivity: Array.from(memoryState.lastActivity.entries()),
|
|
924
|
-
connectionPool: memoryState.connectionPool
|
|
925
|
-
});
|
|
926
|
-
|
|
927
|
-
log.info('💾 [MemoryInterpreter] State persisted', {
|
|
928
|
-
connections: memoryState.sftpConnections.size,
|
|
929
|
-
lastUpdate: new Date(now).toISOString()
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
return {
|
|
933
|
-
success: true,
|
|
934
|
-
activeConnections: memoryState.sftpConnections.size,
|
|
935
|
-
lastActivity: now
|
|
936
|
-
};
|
|
937
|
-
}));
|
|
938
|
-
```
|
|
939
|
-
|
|
940
|
-
**Key Points**:
|
|
941
|
-
|
|
942
|
-
- Use MemoryInterpreter for connection pools and active sessions
|
|
943
|
-
- Persist state to KV to survive workflow restarts
|
|
944
|
-
- Implement stale connection detection and refresh
|
|
945
|
-
- Track activity timestamps for connection reuse
|
|
946
|
-
- Serialize/deserialize Maps and complex objects properly
|
|
947
|
-
|
|
948
|
-
---
|
|
949
|
-
|
|
950
|
-
### Pattern 8: Connection Validation
|
|
951
|
-
|
|
952
|
-
**Use Case**: Validate data source connections before processing to catch configuration errors early.
|
|
953
|
-
|
|
954
|
-
```typescript
|
|
955
|
-
import { webhook, fn } from '@versori/run';
|
|
956
|
-
import {
|
|
957
|
-
SftpDataSource,
|
|
958
|
-
S3DataSource,
|
|
959
|
-
createClient
|
|
960
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
961
|
-
|
|
962
|
-
/**
|
|
963
|
-
* Connection validation pattern
|
|
964
|
-
* Validates all data sources before processing
|
|
965
|
-
*/
|
|
966
|
-
export const validatedIngestion = webhook('ingest-with-validation', {
|
|
967
|
-
response: { mode: 'sync' }
|
|
968
|
-
})
|
|
969
|
-
.then(fn('validate-connections', async (ctx) => {
|
|
970
|
-
const { log, activation } = ctx;
|
|
971
|
-
|
|
972
|
-
log.info('🔍 [Validation] Starting connection validation');
|
|
973
|
-
|
|
974
|
-
// Validate SFTP connection
|
|
975
|
-
const sftp = new SftpDataSource(
|
|
976
|
-
{
|
|
977
|
-
type: 'SFTP_XML',
|
|
978
|
-
connectionId: 'sftp-validation',
|
|
979
|
-
name: 'validated-sftp',
|
|
980
|
-
settings: {
|
|
981
|
-
host: activation.getVariable('sftpHost'),
|
|
982
|
-
port: parseInt(activation.getVariable('sftpPort') || '22', 10),
|
|
983
|
-
username: activation.getVariable('sftpUsername'),
|
|
984
|
-
password: activation.getVariable('sftpPassword'),
|
|
985
|
-
remotePath: '/incoming/',
|
|
986
|
-
encoding: 'utf8',
|
|
987
|
-
requireAbsolutePaths: true
|
|
988
|
-
}
|
|
989
|
-
},
|
|
990
|
-
log
|
|
991
|
-
);
|
|
992
|
-
|
|
993
|
-
try {
|
|
994
|
-
await sftp.validateConnection();
|
|
995
|
-
log.info('✅ [Validation] SFTP connection validated successfully');
|
|
996
|
-
} catch (error) {
|
|
997
|
-
log.error('❌ [Validation] SFTP connection failed', {
|
|
998
|
-
error: error instanceof Error ? error.message : String(error),
|
|
999
|
-
recommendation: error.message?.includes('authentication')
|
|
1000
|
-
? 'Check SFTP username and password in connection configuration'
|
|
1001
|
-
: error.message?.includes('timeout')
|
|
1002
|
-
? 'Check SFTP host and port - ensure server is reachable'
|
|
1003
|
-
: error.message?.includes('host')
|
|
1004
|
-
? 'Verify SFTP host address is correct'
|
|
1005
|
-
: 'Review SFTP connection settings and server logs'
|
|
1006
|
-
});
|
|
1007
|
-
throw error;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// Validate S3 connection
|
|
1011
|
-
const s3 = new S3DataSource(
|
|
1012
|
-
{
|
|
1013
|
-
type: 'S3_CSV',
|
|
1014
|
-
connectionId: 's3-validation',
|
|
1015
|
-
name: 'validated-s3',
|
|
1016
|
-
settings: {
|
|
1017
|
-
region: activation.getVariable('awsRegion') || 'us-east-1',
|
|
1018
|
-
bucket: activation.getVariable('s3Bucket'),
|
|
1019
|
-
prefix: 'incoming/',
|
|
1020
|
-
accessKeyId: activation.getVariable('awsAccessKeyId'),
|
|
1021
|
-
secretAccessKey: activation.getVariable('awsSecretAccessKey')
|
|
1022
|
-
}
|
|
1023
|
-
},
|
|
1024
|
-
log
|
|
1025
|
-
);
|
|
1026
|
-
|
|
1027
|
-
try {
|
|
1028
|
-
await s3.validateConnection();
|
|
1029
|
-
log.info('✅ [Validation] S3 connection validated successfully');
|
|
1030
|
-
} catch (error) {
|
|
1031
|
-
log.error('❌ [Validation] S3 connection failed', {
|
|
1032
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1033
|
-
recommendation: error.message?.includes('credentials')
|
|
1034
|
-
? 'Check AWS access key and secret in connection configuration'
|
|
1035
|
-
: error.message?.includes('bucket')
|
|
1036
|
-
? 'Verify S3 bucket exists and credentials have access'
|
|
1037
|
-
: error.message?.includes('region')
|
|
1038
|
-
? 'Check AWS region is correct for the bucket'
|
|
1039
|
-
: 'Review S3 connection settings and IAM permissions'
|
|
1040
|
-
});
|
|
1041
|
-
throw error;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// Validate Fluent Commerce API connection
|
|
1045
|
-
const client = await createClient(ctx);
|
|
1046
|
-
const retailerId = activation.getVariable('fluentRetailerId');
|
|
1047
|
-
|
|
1048
|
-
if (!retailerId) {
|
|
1049
|
-
log.error('❌ [Validation] Missing retailerId activation variable');
|
|
1050
|
-
throw new Error('fluentRetailerId is required for Fluent API operations');
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
client.setRetailerId(retailerId);
|
|
1054
|
-
|
|
1055
|
-
try {
|
|
1056
|
-
// Test connection with a simple query
|
|
1057
|
-
const testQuery = `query { retailers { edges { node { id } } } }`;
|
|
1058
|
-
await client.graphql({ query: testQuery });
|
|
1059
|
-
log.info('✅ [Validation] Fluent Commerce API connection validated');
|
|
1060
|
-
} catch (error) {
|
|
1061
|
-
log.error('❌ [Validation] Fluent Commerce API connection failed', {
|
|
1062
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1063
|
-
recommendation: error.message?.includes('401') || error.message?.includes('authentication')
|
|
1064
|
-
? 'Verify fluent_commerce connection OAuth2 credentials'
|
|
1065
|
-
: error.message?.includes('GraphQL')
|
|
1066
|
-
? 'Check GraphQL query syntax and permissions'
|
|
1067
|
-
: 'Review Fluent Commerce API connection settings'
|
|
1068
|
-
});
|
|
1069
|
-
throw error;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
log.info('✅ [Validation] All connections validated successfully');
|
|
1073
|
-
|
|
1074
|
-
return {
|
|
1075
|
-
sftp,
|
|
1076
|
-
s3,
|
|
1077
|
-
client,
|
|
1078
|
-
validated: true
|
|
1079
|
-
};
|
|
1080
|
-
}))
|
|
1081
|
-
.then(fn('process-with-validated-connections', async ({ data, log }) => {
|
|
1082
|
-
const { sftp, s3, client } = data;
|
|
1083
|
-
|
|
1084
|
-
log.info('🚀 [Processing] Starting ingestion with validated connections');
|
|
1085
|
-
|
|
1086
|
-
try {
|
|
1087
|
-
// Your ingestion logic here with validated connections
|
|
1088
|
-
const files = await sftp.listFiles({ remotePath: '/incoming/' });
|
|
1089
|
-
|
|
1090
|
-
log.info('📋 [Processing] Files discovered', { count: files.length });
|
|
1091
|
-
|
|
1092
|
-
// Process files...
|
|
1093
|
-
|
|
1094
|
-
return {
|
|
1095
|
-
success: true,
|
|
1096
|
-
filesProcessed: files.length
|
|
1097
|
-
};
|
|
1098
|
-
} finally {
|
|
1099
|
-
// Always dispose connections
|
|
1100
|
-
await sftp.dispose();
|
|
1101
|
-
await s3.dispose();
|
|
1102
|
-
log.info('🧹 [Cleanup] Connections disposed');
|
|
1103
|
-
}
|
|
1104
|
-
}));
|
|
1105
|
-
```
|
|
1106
|
-
|
|
1107
|
-
**Key Points**:
|
|
1108
|
-
|
|
1109
|
-
- Always validate connections before processing
|
|
1110
|
-
- Provide detailed error messages with recommendations
|
|
1111
|
-
- Use emoji logging for visual scanning
|
|
1112
|
-
- Fail fast on configuration errors
|
|
1113
|
-
- Dispose connections in finally blocks
|
|
1114
|
-
|
|
1115
|
-
---
|
|
1116
|
-
|
|
1117
|
-
## Production Best Practices
|
|
1118
|
-
|
|
1119
|
-
### 1. Emoji Logging for Operations Visibility
|
|
1120
|
-
|
|
1121
|
-
Use emoji prefixes in logs for quick visual scanning:
|
|
1122
|
-
|
|
1123
|
-
```typescript
|
|
1124
|
-
log.info('🔍 [Discovery] Scanning SFTP directory');
|
|
1125
|
-
log.info('✅ [Success] File processed successfully');
|
|
1126
|
-
log.error('❌ [Error] Connection failed', { error });
|
|
1127
|
-
log.warn('⚠️ [Warning] Partial batch failure');
|
|
1128
|
-
log.info('🧹 [Cleanup] Archiving processed files');
|
|
1129
|
-
log.info('🧠 [MemoryInterpreter] State synchronized');
|
|
1130
|
-
log.info('💾 [Persistence] State saved to KV');
|
|
1131
|
-
log.info('♻️ [Refresh] Stale connection renewed');
|
|
1132
|
-
log.info('🚀 [Start] Processing batch');
|
|
1133
|
-
log.info('📋 [Info] Configuration loaded');
|
|
1134
|
-
log.info('🔐 [Auth] Token refreshed');
|
|
1135
|
-
```
|
|
1136
|
-
|
|
1137
|
-
**Benefits:**
|
|
1138
|
-
- Quick visual scanning in production logs
|
|
1139
|
-
- Easy pattern recognition for operations
|
|
1140
|
-
- Faster debugging and troubleshooting
|
|
1141
|
-
- Better collaboration across teams
|
|
1142
|
-
|
|
1143
|
-
### 2. Connection Validation Pattern
|
|
1144
|
-
|
|
1145
|
-
Always validate connections before processing:
|
|
1146
|
-
|
|
1147
|
-
```typescript
|
|
1148
|
-
// ✅ CORRECT - Validate early
|
|
1149
|
-
const sftp = new SftpDataSource(config, log);
|
|
1150
|
-
await sftp.validateConnection();
|
|
1151
|
-
log.info('✅ Connection validated');
|
|
1152
|
-
|
|
1153
|
-
// Process files...
|
|
1154
|
-
|
|
1155
|
-
// ❌ WRONG - Discover errors during processing
|
|
1156
|
-
const sftp = new SftpDataSource(config, log);
|
|
1157
|
-
const files = await sftp.listFiles(); // Might fail here!
|
|
1158
|
-
```
|
|
1159
|
-
|
|
1160
|
-
### 3. Error Handling with Recommendations
|
|
1161
|
-
|
|
1162
|
-
Provide actionable recommendations in error logs:
|
|
1163
|
-
|
|
1164
|
-
```typescript
|
|
1165
|
-
catch (error) {
|
|
1166
|
-
log.error('❌ [Processing] File processing failed', {
|
|
1167
|
-
fileName: file.name,
|
|
1168
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1169
|
-
recommendation: error.message?.includes('authentication')
|
|
1170
|
-
? 'Check connection credentials in Connections section'
|
|
1171
|
-
: error.message?.includes('timeout')
|
|
1172
|
-
? 'Check network connectivity and increase timeout settings'
|
|
1173
|
-
: error.message?.includes('parse')
|
|
1174
|
-
? 'Verify file format matches expected structure'
|
|
1175
|
-
: 'Review error details and check file processing logic'
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
```
|
|
1179
|
-
|
|
1180
|
-
### 4. Resource Cleanup Pattern
|
|
1181
|
-
|
|
1182
|
-
Always dispose data sources in finally blocks:
|
|
1183
|
-
|
|
1184
|
-
```typescript
|
|
1185
|
-
const sftp = new SftpDataSource(config, log);
|
|
1186
|
-
try {
|
|
1187
|
-
await sftp.validateConnection();
|
|
1188
|
-
// Process files...
|
|
1189
|
-
} finally {
|
|
1190
|
-
await sftp.dispose();
|
|
1191
|
-
log.info('🧹 [Cleanup] SFTP connection disposed');
|
|
1192
|
-
}
|
|
1193
|
-
```
|
|
1194
|
-
|
|
1195
|
-
### 5. MemoryInterpreter State Management
|
|
1196
|
-
|
|
1197
|
-
Use MemoryInterpreter for connection pools and session state:
|
|
1198
|
-
|
|
1199
|
-
```typescript
|
|
1200
|
-
// Persist complex objects to KV
|
|
1201
|
-
await kvAdapter.set(['memory-interpreter', 'state'], {
|
|
1202
|
-
connections: Array.from(connectionMap.entries()),
|
|
1203
|
-
lastActivity: Array.from(activityMap.entries()),
|
|
1204
|
-
pool: connectionPool
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
// Restore from KV
|
|
1208
|
-
const state = await kvAdapter.get(['memory-interpreter', 'state']);
|
|
1209
|
-
const connectionMap = new Map(state.value.connections);
|
|
1210
|
-
```
|
|
1211
|
-
|
|
1212
|
-
### 6. JobTracker Integration
|
|
1213
|
-
|
|
1214
|
-
Use JobTracker for production monitoring:
|
|
1215
|
-
|
|
1216
|
-
```typescript
|
|
1217
|
-
const tracker = new JobTracker(openKv(':project:'), log);
|
|
1218
|
-
const jobId = `job-${Date.now()}`;
|
|
1219
|
-
|
|
1220
|
-
await tracker.createJob(jobId, {
|
|
1221
|
-
triggeredBy: 'schedule',
|
|
1222
|
-
stage: 'initialization',
|
|
1223
|
-
startTime: Date.now()
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
try {
|
|
1227
|
-
// Process...
|
|
1228
|
-
await tracker.markCompleted(jobId, { filesProcessed: 10 });
|
|
1229
|
-
} catch (error) {
|
|
1230
|
-
await tracker.markFailed(jobId, error.message);
|
|
1231
|
-
}
|
|
1232
|
-
```
|
|
1233
|
-
|
|
1234
|
-
---
|
|
1235
|
-
|
|
1236
|
-
## Testing State Management
|
|
1237
|
-
|
|
1238
|
-
### Local Testing Pattern
|
|
1239
|
-
|
|
1240
|
-
```typescript
|
|
1241
|
-
/**
|
|
1242
|
-
* Test state management locally
|
|
1243
|
-
*/
|
|
1244
|
-
export const testStateManagement = webhook('test-state', {
|
|
1245
|
-
response: { mode: 'sync' }
|
|
1246
|
-
})
|
|
1247
|
-
.then(fn('test-operations', async (ctx) => {
|
|
1248
|
-
const { openKv, log } = ctx;
|
|
1249
|
-
|
|
1250
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
1251
|
-
const stateService = new StateService(log);
|
|
1252
|
-
|
|
1253
|
-
// Test 1: Lock acquire and release
|
|
1254
|
-
log.info('🧪 [Test] Test 1: Lock management');
|
|
1255
|
-
const lockAcquired = await stateService.acquireLock('test-lock', kvAdapter, 5);
|
|
1256
|
-
log.info(`🔐 [Test] Lock acquired: ${lockAcquired}`);
|
|
1257
|
-
|
|
1258
|
-
// Try to acquire again (should fail)
|
|
1259
|
-
const lockAcquired2 = await stateService.acquireLock('test-lock', kvAdapter, 5);
|
|
1260
|
-
log.info(`⚠️ [Test] Second lock acquired: ${lockAcquired2} (should be false)`);
|
|
1261
|
-
|
|
1262
|
-
// Release lock
|
|
1263
|
-
await stateService.releaseLock('test-lock', kvAdapter);
|
|
1264
|
-
log.info('🔓 [Test] Lock released');
|
|
1265
|
-
|
|
1266
|
-
// Test 2: File tracking
|
|
1267
|
-
log.info('🧪 [Test] Test 2: File tracking');
|
|
1268
|
-
const fileTracker = new VersoriFileTracker(openKv(':project:'), 'test');
|
|
1269
|
-
await fileTracker.markFileProcessed('test-file.csv', { recordCount: 100 });
|
|
1270
|
-
const wasProcessed = await fileTracker.wasFileProcessed('test-file.csv');
|
|
1271
|
-
log.info(`✅ [Test] File processed: ${wasProcessed} (should be true)`);
|
|
1272
|
-
|
|
1273
|
-
const lastFile = await fileTracker.getLastProcessedFile();
|
|
1274
|
-
await fileTracker.setLastProcessedFile('test-file.csv');
|
|
1275
|
-
const newLastFile = await fileTracker.getLastProcessedFile();
|
|
1276
|
-
log.info(`📋 [Test] Last file: ${newLastFile}`);
|
|
1277
|
-
|
|
1278
|
-
// Test 3: Sync state
|
|
1279
|
-
log.info('🧪 [Test] Test 3: Sync state');
|
|
1280
|
-
await stateService.updateSyncState(kvAdapter, [{
|
|
1281
|
-
fileName: 'test.csv',
|
|
1282
|
-
lastModified: new Date().toISOString(),
|
|
1283
|
-
recordCount: 100
|
|
1284
|
-
}], 'test-workflow');
|
|
1285
|
-
|
|
1286
|
-
const syncState = await stateService.getSyncState(kvAdapter, 'test-workflow');
|
|
1287
|
-
log.info('💾 [Test] Sync state', syncState);
|
|
1288
|
-
|
|
1289
|
-
// Test 4: Daily job
|
|
1290
|
-
log.info('🧪 [Test] Test 4: Daily job');
|
|
1291
|
-
await stateService.setDailyJob(kvAdapter, 'test-workflow', 'job-123', 24);
|
|
1292
|
-
const dailyJob = await stateService.getDailyJob(kvAdapter, 'test-workflow');
|
|
1293
|
-
log.info('📋 [Test] Daily job', dailyJob);
|
|
1294
|
-
|
|
1295
|
-
return {
|
|
1296
|
-
success: true,
|
|
1297
|
-
message: '✅ All state management tests passed'
|
|
1298
|
-
};
|
|
1299
|
-
}));
|
|
1300
|
-
```
|
|
1301
|
-
|
|
1302
|
-
---
|
|
1303
|
-
|
|
1304
|
-
## Common Issues & Solutions
|
|
1305
|
-
|
|
1306
|
-
### Issue 1: Lock Not Released After Error
|
|
1307
|
-
|
|
1308
|
-
**Problem**: Workflow crashes and lock is never released, blocking future runs.
|
|
1309
|
-
|
|
1310
|
-
**Solution**: Always use try/finally or .catch() to ensure lock release:
|
|
1311
|
-
|
|
1312
|
-
```typescript
|
|
1313
|
-
export const safeWorkflow = schedule('safe-job', '0 * * * *')
|
|
1314
|
-
.then(fn('work', async (ctx) => {
|
|
1315
|
-
const { openKv, log } = ctx;
|
|
1316
|
-
|
|
1317
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
1318
|
-
const stateService = new StateService(log);
|
|
1319
|
-
const lockName = 'safe-lock';
|
|
1320
|
-
|
|
1321
|
-
try {
|
|
1322
|
-
await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
1323
|
-
// Your work here
|
|
1324
|
-
await doWork();
|
|
1325
|
-
} finally {
|
|
1326
|
-
// Always release, even on error
|
|
1327
|
-
await stateService.releaseLock(lockName, kvAdapter);
|
|
1328
|
-
}
|
|
1329
|
-
}));
|
|
1330
|
-
```
|
|
1331
|
-
|
|
1332
|
-
Or use the catch pattern:
|
|
1333
|
-
|
|
1334
|
-
```typescript
|
|
1335
|
-
export const workflowWithCatch = schedule('job', '0 * * * *')
|
|
1336
|
-
.then(fn('acquire', async (ctx) => {
|
|
1337
|
-
const { openKv, log } = ctx;
|
|
1338
|
-
|
|
1339
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
1340
|
-
const stateService = new StateService(log);
|
|
1341
|
-
await stateService.acquireLock('my-lock', kvAdapter, 15);
|
|
1342
|
-
return { kvAdapter, stateService };
|
|
1343
|
-
}))
|
|
1344
|
-
.then(fn('work', async ({ data }) => {
|
|
1345
|
-
// Work here
|
|
1346
|
-
return data;
|
|
1347
|
-
}))
|
|
1348
|
-
.catch(fn('cleanup', async ({ data }) => {
|
|
1349
|
-
// Release lock on any error
|
|
1350
|
-
if (data?.stateService) {
|
|
1351
|
-
await data.stateService.releaseLock('my-lock', data.kvAdapter);
|
|
1352
|
-
}
|
|
1353
|
-
throw new Error('Workflow failed');
|
|
1354
|
-
}));
|
|
1355
|
-
```
|
|
1356
|
-
|
|
1357
|
-
---
|
|
1358
|
-
|
|
1359
|
-
### Issue 2: Stale Lock Detection
|
|
1360
|
-
|
|
1361
|
-
**Problem**: Lock holder crashed and never released lock.
|
|
1362
|
-
|
|
1363
|
-
**Solution**: Use lock timeout - StateService automatically overrides stale locks:
|
|
1364
|
-
|
|
1365
|
-
```typescript
|
|
1366
|
-
// Lock with 15 minute timeout
|
|
1367
|
-
const acquired = await stateService.acquireLock('my-lock', kvAdapter, 15);
|
|
1368
|
-
|
|
1369
|
-
// If lock is older than 15 minutes, it will be overridden automatically
|
|
1370
|
-
// This prevents permanent deadlocks from crashed processes
|
|
1371
|
-
```
|
|
1372
|
-
|
|
1373
|
-
**How it works**:
|
|
1374
|
-
|
|
1375
|
-
- Each lock stores `expiresAt` timestamp
|
|
1376
|
-
- `acquireLock()` checks if existing lock is expired
|
|
1377
|
-
- Expired locks are automatically overridden
|
|
1378
|
-
- Recent locks return false (lock not acquired)
|
|
1379
|
-
|
|
1380
|
-
---
|
|
1381
|
-
|
|
1382
|
-
### Issue 3: Cannot List Processed Files
|
|
1383
|
-
|
|
1384
|
-
**Problem**: Versori KV doesn't support list operations, can't enumerate processed files.
|
|
1385
|
-
|
|
1386
|
-
**Solution**: Use `VersoriIndexedFileTracker` which maintains an index:
|
|
1387
|
-
|
|
1388
|
-
```typescript
|
|
1389
|
-
// Instead of VersoriFileTracker
|
|
1390
|
-
const fileTracker = new VersoriFileTracker(openKv(':project:'));
|
|
1391
|
-
|
|
1392
|
-
// Use VersoriIndexedFileTracker
|
|
1393
|
-
const indexedTracker = new VersoriIndexedFileTracker(openKv(':project:'));
|
|
1394
|
-
|
|
1395
|
-
// Now you can list files
|
|
1396
|
-
const allFiles = await indexedTracker.listProcessedFiles();
|
|
1397
|
-
const stats = await indexedTracker.getStats();
|
|
1398
|
-
```
|
|
1399
|
-
|
|
1400
|
-
**How it works**:
|
|
1401
|
-
|
|
1402
|
-
- Maintains separate index key with list of all file names
|
|
1403
|
-
- Index updated atomically when files are marked/removed
|
|
1404
|
-
- Enables listing without KV list support
|
|
1405
|
-
|
|
1406
|
-
---
|
|
1407
|
-
|
|
1408
|
-
### Issue 4: Checkpoint Corruption
|
|
1409
|
-
|
|
1410
|
-
**Problem**: Checkpoint data gets corrupted or becomes invalid.
|
|
1411
|
-
|
|
1412
|
-
**Solution**: Add validation and version to checkpoint data:
|
|
1413
|
-
|
|
1414
|
-
```typescript
|
|
1415
|
-
interface VersionedCheckpoint {
|
|
1416
|
-
version: number; // Schema version
|
|
1417
|
-
data: CheckpointData;
|
|
1418
|
-
checksum?: string; // Optional integrity check
|
|
1419
|
-
savedAt: string;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
async function saveCheckpoint(
|
|
1423
|
-
kvAdapter: VersoriKVAdapter,
|
|
1424
|
-
key: string[],
|
|
1425
|
-
data: CheckpointData
|
|
1426
|
-
): Promise<void> {
|
|
1427
|
-
const checkpoint: VersionedCheckpoint = {
|
|
1428
|
-
version: 1,
|
|
1429
|
-
data,
|
|
1430
|
-
savedAt: new Date().toISOString()
|
|
1431
|
-
};
|
|
1432
|
-
|
|
1433
|
-
await kvAdapter.set(key, checkpoint);
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
async function loadCheckpoint(
|
|
1437
|
-
kvAdapter: VersoriKVAdapter,
|
|
1438
|
-
key: string[]
|
|
1439
|
-
): Promise<CheckpointData | null> {
|
|
1440
|
-
try {
|
|
1441
|
-
const result = await kvAdapter.get(key);
|
|
1442
|
-
if (!result?.value) return null;
|
|
1443
|
-
|
|
1444
|
-
const checkpoint = result.value as VersionedCheckpoint;
|
|
1445
|
-
|
|
1446
|
-
// Validate version
|
|
1447
|
-
if (checkpoint.version !== 1) {
|
|
1448
|
-
console.warn('Checkpoint version mismatch, ignoring');
|
|
1449
|
-
return null;
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
// Check if checkpoint is too old (e.g., > 7 days)
|
|
1453
|
-
const savedAt = new Date(checkpoint.savedAt);
|
|
1454
|
-
const age = Date.now() - savedAt.getTime();
|
|
1455
|
-
if (age > 7 * 24 * 60 * 60 * 1000) {
|
|
1456
|
-
console.warn('Checkpoint too old, starting fresh');
|
|
1457
|
-
return null;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
return checkpoint.data;
|
|
1461
|
-
} catch (error) {
|
|
1462
|
-
console.error('Failed to load checkpoint', error);
|
|
1463
|
-
return null; // Start fresh on corruption
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
```
|
|
1467
|
-
|
|
1468
|
-
---
|
|
1469
|
-
|
|
1470
|
-
## Best Practices Summary
|
|
1471
|
-
|
|
1472
|
-
### Core State Management Practices
|
|
1473
|
-
|
|
1474
|
-
1. **Always Release Locks**: Use try/finally or .catch() to ensure locks are released
|
|
1475
|
-
2. **Use Lock Timeouts**: Set appropriate timeouts (15-30 minutes typical)
|
|
1476
|
-
3. **Checkpoint Periodically**: Not too often (overhead) or too rarely (lost progress)
|
|
1477
|
-
4. **Validate Restored State**: Check checkpoint age and integrity before using
|
|
1478
|
-
5. **Clear Completed Checkpoints**: Remove checkpoint data after successful completion
|
|
1479
|
-
6. **Use Indexed Tracker for Listing**: VersoriKV doesn't support list operations natively
|
|
1480
|
-
7. **Track Error State**: Implement exponential backoff for retries
|
|
1481
|
-
8. **Monitor Lock Ages**: Alert on locks held longer than expected
|
|
1482
|
-
9. **Namespace Your Keys**: Use prefixes to organize KV data (`workflow:resource:identifier`)
|
|
1483
|
-
10. **Document Key Schemas**: Keep track of what data is stored under which keys
|
|
1484
|
-
|
|
1485
|
-
### Production Patterns (NEW)
|
|
1486
|
-
|
|
1487
|
-
11. **Use Emoji Logging**: Add emoji prefixes for quick visual scanning in logs (see Pattern 8)
|
|
1488
|
-
12. **Validate Connections Early**: Always call `validateConnection()` before processing (see Pattern 8)
|
|
1489
|
-
13. **Provide Error Recommendations**: Include actionable recommendations in error logs
|
|
1490
|
-
14. **Dispose Resources**: Always dispose data sources in finally blocks
|
|
1491
|
-
15. **Use MemoryInterpreter**: For connection pools and session state (see Pattern 7)
|
|
1492
|
-
16. **Integrate JobTracker**: Track job lifecycle for production monitoring
|
|
1493
|
-
|
|
1494
|
-
---
|
|
1495
|
-
|
|
1496
|
-
## Related Guides
|
|
1497
|
-
|
|
1498
|
-
- **01-inventory-ingestion.md** - Complete ingestion workflow using state management
|
|
1499
|
-
- **02-inventory-extraction.md** - Extraction workflow with checkpoints
|
|
1500
|
-
- **04-batch-archival.md** - Archiving batch data to S3 (uses daily jobs)
|
|
1501
|
-
- **05-error-handling.md** - Advanced error handling patterns
|
|
1502
|
-
- **StateService full reference**: `../../02-CORE-GUIDES/ingestion/modules/07-state-management.md`
|
|
1503
|
-
|
|
1504
|
-
---
|
|
1505
|
-
|
|
1506
|
-
## Summary
|
|
1507
|
-
|
|
1508
|
-
This guide covered comprehensive state management patterns for Versori connectors:
|
|
1509
|
-
|
|
1510
|
-
1. **Simple File Tracking** - Prevent duplicate processing with VersoriFileTracker
|
|
1511
|
-
2. **Distributed Locking** - Prevent concurrent executions with StateService
|
|
1512
|
-
3. **Checkpoints** - Save progress and resume long-running workflows
|
|
1513
|
-
4. **Daily Jobs** - Reuse jobs across batches throughout the day
|
|
1514
|
-
5. **Error States** - Track failures and implement smart retry logic
|
|
1515
|
-
6. **Indexed Tracking** - List and manage processed files
|
|
1516
|
-
7. **MemoryInterpreter** (NEW) - In-memory state for connection pools
|
|
1517
|
-
8. **Connection Validation** (NEW) - Validate connections before processing
|
|
1518
|
-
|
|
1519
|
-
**Key Takeaways**:
|
|
1520
|
-
|
|
1521
|
-
- VersoriFileTracker for simple duplicate prevention
|
|
1522
|
-
- StateService for distributed locking and sync state
|
|
1523
|
-
- VersoriIndexedFileTracker when you need to list files
|
|
1524
|
-
- Always release locks in finally blocks
|
|
1525
|
-
- Checkpoint periodically to enable resume
|
|
1526
|
-
- Validate restored state before using
|
|
1527
|
-
- Use exponential backoff for retries
|
|
1528
|
-
- **Use emoji logging for production visibility** (NEW)
|
|
1529
|
-
- **Validate connections early with `validateConnection()`** (NEW)
|
|
1530
|
-
- **Use MemoryInterpreter for connection pool state** (NEW)
|
|
1531
|
-
- **Provide actionable error recommendations** (NEW)
|
|
1532
|
-
|
|
1533
|
-
State management is critical for robust production workflows. These patterns ensure your connectors are reliable, resumable, and prevent duplicate processing.
|
|
1
|
+
# Versori KV State Management
|
|
2
|
+
|
|
3
|
+
**FC Connect SDK Use Case Guide**
|
|
4
|
+
|
|
5
|
+
> **SDK**: [@fluentcommerce/fc-connect-sdk](https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk)
|
|
6
|
+
> **Version**: Use latest - `npm install @fluentcommerce/fc-connect-sdk@latest`
|
|
7
|
+
|
|
8
|
+
**Context**: Use Versori KV storage for duplicate prevention, checkpoints, and file tracking
|
|
9
|
+
|
|
10
|
+
**Complexity**: Low-Medium
|
|
11
|
+
|
|
12
|
+
**Runtime**: Versori Platform
|
|
13
|
+
|
|
14
|
+
**Estimated Lines**: ~400 lines
|
|
15
|
+
|
|
16
|
+
## What You'll Build
|
|
17
|
+
|
|
18
|
+
- VersoriKV adapter setup
|
|
19
|
+
- File tracking (prevent duplicates)
|
|
20
|
+
- Checkpoint/resume capabilities
|
|
21
|
+
- Batch processing state
|
|
22
|
+
- Error tracking and retry logic
|
|
23
|
+
|
|
24
|
+
## SDK Methods Used
|
|
25
|
+
|
|
26
|
+
- `VersoriKVAdapter(openKv())` - Wrap Versori KV for StateService
|
|
27
|
+
- `VersoriFileTracker(openKv())` - Simple file tracking
|
|
28
|
+
- `VersoriIndexedFileTracker(openKv())` - File tracking with listing support
|
|
29
|
+
- `StateService(logger)` - High-level state operations
|
|
30
|
+
- `stateService.isFileProcessed(kv, key)` - Check if processed
|
|
31
|
+
- `stateService.acquireLock(lockName, kv, timeoutMinutes)` - Distributed locking
|
|
32
|
+
- `stateService.getSyncState(kv, workflowId)` - Get sync state
|
|
33
|
+
- `stateService.updateSyncState(kv, files, workflowId)` - Update sync state
|
|
34
|
+
- `stateService.getDailyJob(kv, workflowId)` - Get daily job
|
|
35
|
+
- `stateService.setDailyJob(kv, workflowId, jobId, hours)` - Store daily job
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Versori Workflows Structure
|
|
40
|
+
|
|
41
|
+
**Key Concept**: Versori workflows are organized by **trigger type** at the first level, then by **specific workflow** with descriptive file names.
|
|
42
|
+
|
|
43
|
+
**Trigger Types:**
|
|
44
|
+
- **`webhook()`** → HTTP-based triggers (event-driven) - Creates HTTP endpoints
|
|
45
|
+
- **`schedule()`** → Time-based triggers (cron expressions) - NOT exposed as HTTP endpoints
|
|
46
|
+
- **`http()`** → External API calls (chained from webhook/schedule)
|
|
47
|
+
- **`fn()`** → Internal processing (chained from webhook/schedule)
|
|
48
|
+
|
|
49
|
+
### Recommended Project Structure
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
kv-state-management/
|
|
53
|
+
├── index.ts # Entry point - exports all workflows
|
|
54
|
+
└── src/
|
|
55
|
+
├── workflows/
|
|
56
|
+
│ ├── webhook/
|
|
57
|
+
│ │ └── file-ingestion.ts # Webhook: File processing
|
|
58
|
+
│ │
|
|
59
|
+
│ └── scheduled/
|
|
60
|
+
│ └── daily-sync.ts # Scheduled: Daily sync
|
|
61
|
+
│
|
|
62
|
+
├── services/
|
|
63
|
+
│ └── state-management.service.ts # Shared state logic (reusable)
|
|
64
|
+
│
|
|
65
|
+
└── config/
|
|
66
|
+
└── state-config.json # Configuration
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Benefits:**
|
|
70
|
+
- ✅ Clear trigger separation (`webhook/` vs `scheduled/`)
|
|
71
|
+
- ✅ Descriptive file names (easy to browse and understand)
|
|
72
|
+
- ✅ Scalable (add new workflows without cluttering)
|
|
73
|
+
- ✅ Reusable code in `services/` (DRY principle)
|
|
74
|
+
- ✅ Easy to modify individual workflows without affecting others
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Complete Working Code
|
|
79
|
+
|
|
80
|
+
### Pattern 1: Simple File Duplicate Prevention
|
|
81
|
+
|
|
82
|
+
**Use Case**: Prevent reprocessing the same files in ingestion workflows.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { webhook, fn } from '@versori/run';
|
|
86
|
+
// FC Connect SDK+
|
|
87
|
+
// Install: npm install @fluentcommerce/fc-connect-sdk@latest
|
|
88
|
+
// Docs: https://www.npmjs.com/package/@fluentcommerce/fc-connect-sdk
|
|
89
|
+
// GitHub: https://github.com/fluentcommerce/fc-connect-sdk
|
|
90
|
+
import { VersoriFileTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Simple file tracking pattern
|
|
94
|
+
* Perfect for basic duplicate prevention without complex state management
|
|
95
|
+
*/
|
|
96
|
+
export const simpleFileIngestion = webhook('ingest-files', {
|
|
97
|
+
response: { mode: 'sync' }
|
|
98
|
+
})
|
|
99
|
+
.then(fn('check-and-process-files', async (ctx) => {
|
|
100
|
+
const { openKv, log, data } = ctx;
|
|
101
|
+
|
|
102
|
+
// Initialize simple file tracker
|
|
103
|
+
const fileTracker = new VersoriFileTracker(openKv(':project:'), 'inventory-ingestion');
|
|
104
|
+
|
|
105
|
+
// Sample files to process
|
|
106
|
+
const files = data.files || [
|
|
107
|
+
{ name: 'inventory-2025-01-15.csv', url: 's3://bucket/file1.csv' },
|
|
108
|
+
{ name: 'inventory-2025-01-16.csv', url: 's3://bucket/file2.csv' }
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const processedFiles = [];
|
|
112
|
+
const skippedFiles = [];
|
|
113
|
+
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
// Check if file was already processed
|
|
116
|
+
const wasProcessed = await fileTracker.wasFileProcessed(file.name);
|
|
117
|
+
|
|
118
|
+
if (wasProcessed) {
|
|
119
|
+
log.info(`⏭️ [FileTracking] Skipping already processed file: ${file.name}`);
|
|
120
|
+
skippedFiles.push(file.name);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Process the file (your ingestion logic here)
|
|
126
|
+
log.info(`🚀 [Processing] Processing file: ${file.name}`);
|
|
127
|
+
const recordCount = await processFile(file);
|
|
128
|
+
|
|
129
|
+
// Mark as processed with metadata
|
|
130
|
+
await fileTracker.markFileProcessed(file.name, {
|
|
131
|
+
recordCount,
|
|
132
|
+
processedBy: 'webhook-ingestion',
|
|
133
|
+
source: file.url
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
processedFiles.push({ name: file.name, recordCount });
|
|
137
|
+
log.info(`✅ [Success] Successfully processed: ${file.name} (${recordCount} records)`);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// ? Enhanced: Error logging with recommendations
|
|
140
|
+
log.error('[KVStateManagement] Failed to process file', {
|
|
141
|
+
fileName: file.name,
|
|
142
|
+
error: error instanceof Error ? error.message : String(error),
|
|
143
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
144
|
+
recommendation: error.message?.includes('parse') || error.message?.includes('format')
|
|
145
|
+
? 'Check file format and structure - ensure file is valid'
|
|
146
|
+
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
147
|
+
? 'Check mapping configuration and verify file column structure'
|
|
148
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
149
|
+
? 'Check network connectivity and data source availability'
|
|
150
|
+
: 'Review error details and check file processing logic'
|
|
151
|
+
});
|
|
152
|
+
throw error; // Fail workflow so we can retry later
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Track last processed file
|
|
157
|
+
if (processedFiles.length > 0) {
|
|
158
|
+
const lastFile = processedFiles[processedFiles.length - 1];
|
|
159
|
+
await fileTracker.setLastProcessedFile(lastFile.name);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
success: true,
|
|
164
|
+
processed: processedFiles,
|
|
165
|
+
skipped: skippedFiles,
|
|
166
|
+
lastProcessedFile: await fileTracker.getLastProcessedFile()
|
|
167
|
+
};
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
// Helper function (implement based on your data source)
|
|
171
|
+
async function processFile(file: { name: string; url: string }): Promise<number> {
|
|
172
|
+
// Your file processing logic
|
|
173
|
+
return 1000; // Return record count
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Key Points**:
|
|
178
|
+
|
|
179
|
+
- `VersoriFileTracker` is lightweight - perfect for simple use cases
|
|
180
|
+
- Automatic duplicate prevention with `wasFileProcessed()`
|
|
181
|
+
- Stores metadata like record count with each file
|
|
182
|
+
- Tracks last processed file for incremental processing
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### Pattern 2: Distributed Locking with StateService
|
|
187
|
+
|
|
188
|
+
**Use Case**: Prevent concurrent workflow executions with distributed locks.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { schedule, fn } from '@versori/run';
|
|
192
|
+
import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Distributed locking pattern
|
|
196
|
+
* Prevents multiple instances from running simultaneously
|
|
197
|
+
*/
|
|
198
|
+
export const scheduledIngestionWithLock = schedule('inventory-sync', '0 */6 * * *')
|
|
199
|
+
.then(fn('acquire-lock', async (ctx) => {
|
|
200
|
+
const { openKv, log } = ctx;
|
|
201
|
+
|
|
202
|
+
// Create StateService with KV adapter
|
|
203
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
204
|
+
const stateService = new StateService(log);
|
|
205
|
+
|
|
206
|
+
// Try to acquire lock (15 minute timeout)
|
|
207
|
+
const lockName = 'inventory-sync-lock';
|
|
208
|
+
const lockAcquired = await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
209
|
+
|
|
210
|
+
if (!lockAcquired) {
|
|
211
|
+
log.warn('⚠️ [Lock] Lock already held, skipping this run');
|
|
212
|
+
return {
|
|
213
|
+
shouldSkip: true,
|
|
214
|
+
reason: 'Lock already held by another instance'
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
log.info('🔐 [Lock] Lock acquired successfully');
|
|
219
|
+
return {
|
|
220
|
+
shouldSkip: false,
|
|
221
|
+
lockName,
|
|
222
|
+
kvAdapter,
|
|
223
|
+
stateService
|
|
224
|
+
};
|
|
225
|
+
}))
|
|
226
|
+
.then(fn('process-with-lock', async ({ data, log }) => {
|
|
227
|
+
if (data.shouldSkip) {
|
|
228
|
+
return data;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const { kvAdapter, stateService, lockName } = data;
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
// Your ingestion logic here
|
|
235
|
+
log.info('🚀 [Processing] Processing inventory sync...');
|
|
236
|
+
await performIngestion();
|
|
237
|
+
log.info('✅ [Success] Ingestion completed successfully');
|
|
238
|
+
|
|
239
|
+
return { success: true };
|
|
240
|
+
} finally {
|
|
241
|
+
// ALWAYS release lock in finally block
|
|
242
|
+
await stateService.releaseLock(lockName, kvAdapter);
|
|
243
|
+
log.info('🔓 [Lock] Lock released');
|
|
244
|
+
}
|
|
245
|
+
}))
|
|
246
|
+
.catch(fn('handle-error-and-release-lock', async ({ data, error, log }) => {
|
|
247
|
+
// Ensure lock is released even on error
|
|
248
|
+
if (data?.stateService && data?.lockName && data?.kvAdapter) {
|
|
249
|
+
await data.stateService.releaseLock(data.lockName, data.kvAdapter);
|
|
250
|
+
log.info('🔓 [Lock] Lock released after error');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
log.error('❌ [Error] Ingestion failed', error as Error);
|
|
254
|
+
return { success: false, error: (error as Error).message };
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
async function performIngestion(): Promise<void> {
|
|
258
|
+
// Your ingestion logic
|
|
259
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Key Points**:
|
|
264
|
+
|
|
265
|
+
- Distributed locking prevents concurrent executions
|
|
266
|
+
- Stale lock detection (automatically overrides expired locks)
|
|
267
|
+
- Lock timeout ensures recovery from crashes
|
|
268
|
+
- ALWAYS release locks in finally blocks
|
|
269
|
+
- Proper error handling to prevent lock leaks
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### Pattern 3: Checkpoint & Resume
|
|
274
|
+
|
|
275
|
+
**Use Case**: Save progress during long-running workflows and resume from last checkpoint.
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import { webhook, fn } from '@versori/run';
|
|
279
|
+
import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
280
|
+
|
|
281
|
+
interface CheckpointData {
|
|
282
|
+
currentBatch: number;
|
|
283
|
+
totalBatches: number;
|
|
284
|
+
processedRecords: number;
|
|
285
|
+
lastProcessedId: string;
|
|
286
|
+
startTime: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Checkpoint & resume pattern
|
|
291
|
+
* Useful for long-running batch processing that might fail partway through
|
|
292
|
+
*/
|
|
293
|
+
export const batchProcessingWithCheckpoints = webhook('process-batches', {
|
|
294
|
+
response: { mode: 'sync' }
|
|
295
|
+
})
|
|
296
|
+
.then(fn('check-checkpoint', async (ctx) => {
|
|
297
|
+
const { openKv, log, data } = ctx;
|
|
298
|
+
|
|
299
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
300
|
+
const stateService = new StateService(log);
|
|
301
|
+
const workflowId = 'batch-processing';
|
|
302
|
+
const checkpointKey = `checkpoint:${workflowId}`;
|
|
303
|
+
|
|
304
|
+
// Try to restore from checkpoint
|
|
305
|
+
const syncState = await stateService.getSyncState(kvAdapter, workflowId);
|
|
306
|
+
let checkpoint: CheckpointData | null = null;
|
|
307
|
+
|
|
308
|
+
if (syncState.isInitialized) {
|
|
309
|
+
// Check if there's a saved checkpoint (stored in custom state)
|
|
310
|
+
const stored = await kvAdapter.get([checkpointKey]);
|
|
311
|
+
if (stored?.value) {
|
|
312
|
+
checkpoint = stored.value as CheckpointData;
|
|
313
|
+
log.info('♻️ [Checkpoint] Resuming from checkpoint', checkpoint);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Get batches to process
|
|
318
|
+
const allBatches = data.batches || generateBatches(100); // 100 batches total
|
|
319
|
+
|
|
320
|
+
// Determine starting point
|
|
321
|
+
const startBatch = checkpoint ? checkpoint.currentBatch : 0;
|
|
322
|
+
const processedSoFar = checkpoint ? checkpoint.processedRecords : 0;
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
batches: allBatches,
|
|
326
|
+
startBatch,
|
|
327
|
+
processedSoFar,
|
|
328
|
+
kvAdapter,
|
|
329
|
+
stateService,
|
|
330
|
+
workflowId,
|
|
331
|
+
checkpointKey,
|
|
332
|
+
startTime: checkpoint?.startTime || new Date().toISOString()
|
|
333
|
+
};
|
|
334
|
+
}))
|
|
335
|
+
.then(fn('process-with-checkpoints', async ({ data, log }) => {
|
|
336
|
+
const {
|
|
337
|
+
batches,
|
|
338
|
+
startBatch,
|
|
339
|
+
processedSoFar,
|
|
340
|
+
kvAdapter,
|
|
341
|
+
stateService,
|
|
342
|
+
workflowId,
|
|
343
|
+
checkpointKey,
|
|
344
|
+
startTime
|
|
345
|
+
} = data;
|
|
346
|
+
|
|
347
|
+
let processedRecords = processedSoFar;
|
|
348
|
+
|
|
349
|
+
// Process batches starting from checkpoint
|
|
350
|
+
for (let i = startBatch; i < batches.length; i++) {
|
|
351
|
+
const batch = batches[i];
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
log.info(`🚀 [Processing] Processing batch ${i + 1}/${batches.length}`);
|
|
355
|
+
|
|
356
|
+
// Process the batch
|
|
357
|
+
const result = await processBatch(batch);
|
|
358
|
+
processedRecords += result.recordCount;
|
|
359
|
+
|
|
360
|
+
// Save checkpoint after each batch (or every N batches)
|
|
361
|
+
if ((i + 1) % 10 === 0 || i === batches.length - 1) {
|
|
362
|
+
const checkpoint: CheckpointData = {
|
|
363
|
+
currentBatch: i + 1,
|
|
364
|
+
totalBatches: batches.length,
|
|
365
|
+
processedRecords,
|
|
366
|
+
lastProcessedId: result.lastId,
|
|
367
|
+
startTime
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
await kvAdapter.set([checkpointKey], checkpoint);
|
|
371
|
+
log.info(`💾 [Checkpoint] Checkpoint saved at batch ${i + 1}`);
|
|
372
|
+
}
|
|
373
|
+
} catch (error) {
|
|
374
|
+
// ? Enhanced: Error logging with recommendations
|
|
375
|
+
log.error('[KVStateManagement] Batch processing failed', {
|
|
376
|
+
batchNumber: i + 1,
|
|
377
|
+
error: error instanceof Error ? error.message : String(error),
|
|
378
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
379
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
380
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
381
|
+
: error.message?.includes('mutation') || error.message?.includes('GraphQL')
|
|
382
|
+
? 'Check GraphQL mutation syntax and batch payload structure'
|
|
383
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
384
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
385
|
+
: 'Review error details - checkpoint saved for retry'
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Save checkpoint at failure point
|
|
389
|
+
const checkpoint: CheckpointData = {
|
|
390
|
+
currentBatch: i, // Retry this batch
|
|
391
|
+
totalBatches: batches.length,
|
|
392
|
+
processedRecords,
|
|
393
|
+
lastProcessedId: batch[0]?.id || '',
|
|
394
|
+
startTime
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
await kvAdapter.set([checkpointKey], checkpoint);
|
|
398
|
+
throw error; // Fail workflow so it can be retried
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// All batches processed - clear checkpoint
|
|
403
|
+
await kvAdapter.delete([checkpointKey]);
|
|
404
|
+
log.info('✅ [Checkpoint] All batches processed, checkpoint cleared');
|
|
405
|
+
|
|
406
|
+
// Update final sync state
|
|
407
|
+
await stateService.updateSyncState(kvAdapter, [{
|
|
408
|
+
fileName: 'batch-processing-complete',
|
|
409
|
+
lastModified: new Date().toISOString(),
|
|
410
|
+
recordCount: processedRecords
|
|
411
|
+
}], workflowId);
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
totalBatches: batches.length,
|
|
416
|
+
processedRecords,
|
|
417
|
+
duration: Date.now() - new Date(startTime).getTime()
|
|
418
|
+
};
|
|
419
|
+
}));
|
|
420
|
+
|
|
421
|
+
function generateBatches(count: number): any[] {
|
|
422
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
423
|
+
id: `batch-${i + 1}`,
|
|
424
|
+
data: []
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function processBatch(batch: any): Promise<{ recordCount: number; lastId: string }> {
|
|
429
|
+
// Your batch processing logic
|
|
430
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
431
|
+
return { recordCount: 50, lastId: batch.id };
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Key Points**:
|
|
436
|
+
|
|
437
|
+
- Save checkpoints periodically (not after every operation)
|
|
438
|
+
- Store enough context to resume exactly where you left off
|
|
439
|
+
- Clear checkpoint after successful completion
|
|
440
|
+
- On failure, checkpoint allows resuming without reprocessing
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
### Pattern 4: Daily Job Management
|
|
445
|
+
|
|
446
|
+
**Use Case**: Reuse jobs across multiple batches throughout the day (DAILY strategy).
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
import { webhook, fn } from '@versori/run';
|
|
450
|
+
import {
|
|
451
|
+
StateService,
|
|
452
|
+
VersoriKVAdapter,
|
|
453
|
+
createClient
|
|
454
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Daily job management pattern
|
|
458
|
+
* Reuses a single job for multiple batches within a day
|
|
459
|
+
*/
|
|
460
|
+
export const dailyJobIngestion = webhook('ingest-daily', {
|
|
461
|
+
response: { mode: 'sync' }
|
|
462
|
+
})
|
|
463
|
+
.then(fn('get-or-create-job', async (ctx) => {
|
|
464
|
+
// Destructure context inside function body
|
|
465
|
+
const { openKv, log, connections, activation } = ctx;
|
|
466
|
+
|
|
467
|
+
const kvAdapter = new VersoriKVAdapter(openKv());
|
|
468
|
+
const stateService = new StateService(log);
|
|
469
|
+
const workflowId = 'daily-inventory-ingestion';
|
|
470
|
+
|
|
471
|
+
// Check for existing daily job
|
|
472
|
+
const existingJob = await stateService.getDailyJob(kvAdapter, workflowId);
|
|
473
|
+
|
|
474
|
+
let jobId: string;
|
|
475
|
+
let isNewJob = false;
|
|
476
|
+
|
|
477
|
+
if (existingJob && existingJob.jobId) {
|
|
478
|
+
// Reuse existing job
|
|
479
|
+
jobId = existingJob.jobId;
|
|
480
|
+
log.info(`♻️ [DailyJob] Reusing daily job: ${jobId}`, {
|
|
481
|
+
createdAt: existingJob.createdAt,
|
|
482
|
+
expiresAt: existingJob.expiresAt
|
|
483
|
+
});
|
|
484
|
+
} else {
|
|
485
|
+
// Create new job
|
|
486
|
+
const client = await createClient(ctx); // Auto-detects Versori context
|
|
487
|
+
|
|
488
|
+
const job = await client.createJob({
|
|
489
|
+
name: `daily-inventory-${new Date().toISOString().split('T')[0]}`,
|
|
490
|
+
retailerId: activation.getVariable('fluentRetailerId') as string
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
jobId = job.id;
|
|
494
|
+
isNewJob = true;
|
|
495
|
+
|
|
496
|
+
// Store job for 24 hours
|
|
497
|
+
await stateService.setDailyJob(kvAdapter, workflowId, jobId, 24);
|
|
498
|
+
log.info(`🆕 [DailyJob] Created new daily job: ${jobId}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
jobId,
|
|
503
|
+
isNewJob,
|
|
504
|
+
kvAdapter,
|
|
505
|
+
stateService,
|
|
506
|
+
workflowId,
|
|
507
|
+
client: await createClient(ctx) // Auto-detects Versori context
|
|
508
|
+
};
|
|
509
|
+
}))
|
|
510
|
+
.then(fn('send-batches-to-job', async ({ data, log }) => {
|
|
511
|
+
const { jobId, client, kvAdapter, stateService, workflowId } = data;
|
|
512
|
+
|
|
513
|
+
// Your batches to process
|
|
514
|
+
const batches = [
|
|
515
|
+
{ entities: [{ skuRef: 'SKU001', qty: 100 }] },
|
|
516
|
+
{ entities: [{ skuRef: 'SKU002', qty: 200 }] }
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
const batchResults = [];
|
|
520
|
+
|
|
521
|
+
for (const batch of batches) {
|
|
522
|
+
try {
|
|
523
|
+
const result = await client.sendBatch(jobId, {
|
|
524
|
+
action: 'UPSERT',
|
|
525
|
+
entityType: 'INVENTORY_CATALOGUE',
|
|
526
|
+
entities: batch.entities
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
batchResults.push({ success: true, batchId: result.id });
|
|
530
|
+
log.info(`✅ [Batch] Batch sent to job ${jobId}: ${result.id}`);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
// ? Enhanced: Error logging with recommendations
|
|
533
|
+
log.error('[KVStateManagement] Batch submission failed', {
|
|
534
|
+
batchId: result?.id,
|
|
535
|
+
error: error instanceof Error ? error.message : String(error),
|
|
536
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
537
|
+
recommendation: error.message?.includes('authentication') || error.message?.includes('401')
|
|
538
|
+
? 'Verify fluent_commerce connection and OAuth2 credentials in Connections section'
|
|
539
|
+
: error.message?.includes('batch') || error.message?.includes('job')
|
|
540
|
+
? 'Check batch API payload and job status'
|
|
541
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
542
|
+
? 'Check network connectivity and Fluent Commerce API availability'
|
|
543
|
+
: 'Review error details and check batch submission payload'
|
|
544
|
+
});
|
|
545
|
+
batchResults.push({ success: false, error: (error as Error).message });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Update sync state with processed records
|
|
550
|
+
await stateService.updateSyncState(kvAdapter, [{
|
|
551
|
+
fileName: `daily-batch-${new Date().toISOString()}`,
|
|
552
|
+
lastModified: new Date().toISOString(),
|
|
553
|
+
recordCount: batches.reduce((sum, b) => sum + b.entities.length, 0)
|
|
554
|
+
}], workflowId);
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
success: true,
|
|
558
|
+
jobId,
|
|
559
|
+
batchCount: batchResults.length,
|
|
560
|
+
successCount: batchResults.filter(r => r.success).length
|
|
561
|
+
};
|
|
562
|
+
}));
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Key Points**:
|
|
566
|
+
|
|
567
|
+
- Single job per day reduces API overhead
|
|
568
|
+
- Job expires automatically after 24 hours
|
|
569
|
+
- Tracks batch count per job
|
|
570
|
+
- Perfect for high-frequency ingestion workflows
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
### Pattern 5: Error State Management
|
|
575
|
+
|
|
576
|
+
**Use Case**: Track errors and implement retry logic with exponential backoff.
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
import { webhook, fn } from '@versori/run';
|
|
580
|
+
import { VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
581
|
+
|
|
582
|
+
interface ErrorState {
|
|
583
|
+
fileName: string;
|
|
584
|
+
attemptCount: number;
|
|
585
|
+
lastError: string;
|
|
586
|
+
lastAttemptAt: string;
|
|
587
|
+
firstFailedAt: string;
|
|
588
|
+
nextRetryAt: string;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Error state management pattern
|
|
593
|
+
* Tracks failures and implements smart retry logic
|
|
594
|
+
*/
|
|
595
|
+
export const retryableIngestion = webhook('ingest-with-retry', {
|
|
596
|
+
response: { mode: 'sync' }
|
|
597
|
+
})
|
|
598
|
+
.then(fn('process-with-retry-tracking', async (ctx) => {
|
|
599
|
+
const { openKv, log, data } = ctx;
|
|
600
|
+
|
|
601
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
602
|
+
const files = data.files || ['file1.csv', 'file2.csv', 'file3.csv'];
|
|
603
|
+
|
|
604
|
+
const results = {
|
|
605
|
+
succeeded: [] as string[],
|
|
606
|
+
failed: [] as string[],
|
|
607
|
+
skipped: [] as string[]
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
for (const fileName of files) {
|
|
611
|
+
const errorStateKey = ['error-state', fileName];
|
|
612
|
+
|
|
613
|
+
// Check if file has error state
|
|
614
|
+
const errorStateResult = await kvAdapter.get(errorStateKey);
|
|
615
|
+
const errorState = errorStateResult?.value as ErrorState | undefined;
|
|
616
|
+
|
|
617
|
+
// Check if we should retry
|
|
618
|
+
if (errorState) {
|
|
619
|
+
const nextRetry = new Date(errorState.nextRetryAt);
|
|
620
|
+
const now = new Date();
|
|
621
|
+
|
|
622
|
+
if (now < nextRetry) {
|
|
623
|
+
log.info(`⏭️ [Retry] Skipping ${fileName} until ${errorState.nextRetryAt}`, {
|
|
624
|
+
attemptCount: errorState.attemptCount,
|
|
625
|
+
lastError: errorState.lastError
|
|
626
|
+
});
|
|
627
|
+
results.skipped.push(fileName);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Max retry attempts reached?
|
|
632
|
+
if (errorState.attemptCount >= 5) {
|
|
633
|
+
log.warn(`⚠️ [Retry] Max retries exceeded for ${fileName}, requires manual intervention`);
|
|
634
|
+
results.skipped.push(fileName);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
log.info(`♻️ [Retry] Retrying ${fileName} (attempt ${errorState.attemptCount + 1})`, {
|
|
639
|
+
lastError: errorState.lastError,
|
|
640
|
+
firstFailedAt: errorState.firstFailedAt
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
// Process the file
|
|
646
|
+
await processFileWithPossibleFailure(fileName);
|
|
647
|
+
|
|
648
|
+
// Success! Clear error state
|
|
649
|
+
if (errorState) {
|
|
650
|
+
await kvAdapter.delete(errorStateKey);
|
|
651
|
+
log.info(`✅ [Retry] File recovered after ${errorState.attemptCount} retries: ${fileName}`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
results.succeeded.push(fileName);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
// ? Enhanced: Error logging with recommendations
|
|
657
|
+
const errorMessage = (error as Error).message;
|
|
658
|
+
const attemptCount = errorState ? errorState.attemptCount + 1 : 1;
|
|
659
|
+
const now = new Date();
|
|
660
|
+
|
|
661
|
+
// Calculate next retry with exponential backoff
|
|
662
|
+
const backoffMinutes = Math.pow(2, attemptCount) * 5; // 5, 10, 20, 40, 80 minutes
|
|
663
|
+
const nextRetryAt = new Date(now.getTime() + backoffMinutes * 60 * 1000);
|
|
664
|
+
|
|
665
|
+
// Save error state
|
|
666
|
+
const newErrorState: ErrorState = {
|
|
667
|
+
fileName,
|
|
668
|
+
attemptCount,
|
|
669
|
+
lastError: errorMessage,
|
|
670
|
+
lastAttemptAt: now.toISOString(),
|
|
671
|
+
firstFailedAt: errorState?.firstFailedAt || now.toISOString(),
|
|
672
|
+
nextRetryAt: nextRetryAt.toISOString()
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
await kvAdapter.set(errorStateKey, newErrorState);
|
|
676
|
+
|
|
677
|
+
log.error('[KVStateManagement] File processing failed', {
|
|
678
|
+
fileName,
|
|
679
|
+
attemptCount,
|
|
680
|
+
error: error instanceof Error ? error.message : String(error),
|
|
681
|
+
errorType: error instanceof Error ? error.constructor.name : 'Error',
|
|
682
|
+
nextRetryAt: nextRetryAt.toISOString(),
|
|
683
|
+
backoffMinutes,
|
|
684
|
+
recommendation: error.message?.includes('parse') || error.message?.includes('format')
|
|
685
|
+
? 'Check file format and structure - ensure file is valid'
|
|
686
|
+
: error.message?.includes('mapping') || error.message?.includes('field')
|
|
687
|
+
? 'Check mapping configuration and verify file column structure'
|
|
688
|
+
: error.message?.includes('connection') || error.message?.includes('timeout')
|
|
689
|
+
? 'Check network connectivity and data source availability'
|
|
690
|
+
: `Will retry after ${backoffMinutes} minutes - review error details`
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
results.failed.push(fileName);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
success: true,
|
|
699
|
+
results,
|
|
700
|
+
summary: {
|
|
701
|
+
succeeded: results.succeeded.length,
|
|
702
|
+
failed: results.failed.length,
|
|
703
|
+
skipped: results.skipped.length
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
}))
|
|
707
|
+
.then(fn('cleanup-old-errors', async (ctx) => {
|
|
708
|
+
const { openKv, log, data } = ctx;
|
|
709
|
+
|
|
710
|
+
// Cleanup error states older than 7 days
|
|
711
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
712
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
713
|
+
|
|
714
|
+
// Note: This requires tracking error state keys separately
|
|
715
|
+
// In production, use VersoriIndexedFileTracker pattern
|
|
716
|
+
|
|
717
|
+
log.info('Error state cleanup completed');
|
|
718
|
+
return data;
|
|
719
|
+
}));
|
|
720
|
+
|
|
721
|
+
async function processFileWithPossibleFailure(fileName: string): Promise<void> {
|
|
722
|
+
// Simulate random failures for demonstration
|
|
723
|
+
if (Math.random() < 0.3) {
|
|
724
|
+
throw new Error('Simulated processing failure');
|
|
725
|
+
}
|
|
726
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
**Key Points**:
|
|
731
|
+
|
|
732
|
+
- Exponential backoff prevents overwhelming failing systems
|
|
733
|
+
- Track first failure time for monitoring
|
|
734
|
+
- Max retry limit prevents infinite loops
|
|
735
|
+
- Clear error state on success
|
|
736
|
+
- Skip files until retry time
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
### Pattern 6: Advanced File Tracking with Indexing
|
|
741
|
+
|
|
742
|
+
**Use Case**: Track files with ability to list all processed files.
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
import { webhook, fn } from '@versori/run';
|
|
746
|
+
import { VersoriIndexedFileTracker } from '@fluentcommerce/fc-connect-sdk';
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Advanced file tracking with indexing
|
|
750
|
+
* Allows listing all processed files (overcomes Versori KV list limitation)
|
|
751
|
+
*/
|
|
752
|
+
export const indexedFileTracking = webhook('manage-files', {
|
|
753
|
+
response: { mode: 'sync' }
|
|
754
|
+
})
|
|
755
|
+
.then(fn('track-and-manage-files', async (ctx) => {
|
|
756
|
+
const { openKv, log, data } = ctx;
|
|
757
|
+
|
|
758
|
+
// Initialize indexed file tracker
|
|
759
|
+
const fileTracker = new VersoriIndexedFileTracker(openKv(':project:'), 'inventory-ingestion');
|
|
760
|
+
const operation = data.operation || 'process'; // process, list, stats, cleanup
|
|
761
|
+
|
|
762
|
+
switch (operation) {
|
|
763
|
+
case 'process': {
|
|
764
|
+
const files = data.files || ['file1.csv', 'file2.csv'];
|
|
765
|
+
|
|
766
|
+
for (const file of files) {
|
|
767
|
+
// Process file
|
|
768
|
+
log.info(`🚀 [Processing] Processing: ${file}`);
|
|
769
|
+
const recordCount = Math.floor(Math.random() * 1000) + 100;
|
|
770
|
+
|
|
771
|
+
// Mark as processed (automatically updates index)
|
|
772
|
+
await fileTracker.markFileProcessed(file, {
|
|
773
|
+
recordCount,
|
|
774
|
+
source: 's3://bucket/' + file,
|
|
775
|
+
processingDuration: 1234
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return { success: true, processed: files.length };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
case 'list': {
|
|
783
|
+
// List all processed files
|
|
784
|
+
const processedFiles = await fileTracker.listProcessedFiles();
|
|
785
|
+
log.info(`📋 [Index] Found ${processedFiles.length} processed files`);
|
|
786
|
+
|
|
787
|
+
return {
|
|
788
|
+
success: true,
|
|
789
|
+
files: processedFiles.map(f => ({
|
|
790
|
+
name: f.fileName,
|
|
791
|
+
processedAt: f.metadata?.processedAt,
|
|
792
|
+
recordCount: f.metadata?.recordCount
|
|
793
|
+
}))
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
case 'stats': {
|
|
798
|
+
// Get processing statistics
|
|
799
|
+
const stats = await fileTracker.getStats();
|
|
800
|
+
log.info('📊 [Index] File processing statistics', stats);
|
|
801
|
+
|
|
802
|
+
return {
|
|
803
|
+
success: true,
|
|
804
|
+
stats
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
case 'cleanup': {
|
|
809
|
+
// Clear all processed files
|
|
810
|
+
const result = await fileTracker.clearAllProcessedFiles();
|
|
811
|
+
log.info(`🧹 [Index] Cleanup complete: ${result.success} deleted, ${result.failed} failed`);
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
success: true,
|
|
815
|
+
deleted: result.success,
|
|
816
|
+
failed: result.failed
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
case 'remove': {
|
|
821
|
+
// Remove specific file
|
|
822
|
+
const fileName = data.fileName;
|
|
823
|
+
if (!fileName) {
|
|
824
|
+
throw new Error('fileName required for remove operation');
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
await fileTracker.removeProcessedFile(fileName);
|
|
828
|
+
log.info(`🗑️ [Index] Removed file: ${fileName}`);
|
|
829
|
+
|
|
830
|
+
return { success: true, removed: fileName };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
default:
|
|
834
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
835
|
+
}
|
|
836
|
+
}));
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
**Key Points**:
|
|
840
|
+
|
|
841
|
+
- Maintains internal index to overcome Versori KV list limitation
|
|
842
|
+
- List all processed files
|
|
843
|
+
- Get aggregate statistics
|
|
844
|
+
- Cleanup operations
|
|
845
|
+
- Automatic index maintenance
|
|
846
|
+
|
|
847
|
+
---
|
|
848
|
+
|
|
849
|
+
### Pattern 7: MemoryInterpreter for In-Memory State
|
|
850
|
+
|
|
851
|
+
**Use Case**: Store interpreter state in memory for long-running connections (SFTP, database connections).
|
|
852
|
+
|
|
853
|
+
```typescript
|
|
854
|
+
import { webhook, fn } from '@versori/run';
|
|
855
|
+
import { VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
856
|
+
|
|
857
|
+
interface MemoryInterpreterState {
|
|
858
|
+
sftpConnections: Map<string, any>;
|
|
859
|
+
lastActivity: Map<string, number>;
|
|
860
|
+
connectionPool: any[];
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* MemoryInterpreter pattern
|
|
865
|
+
* Maintains in-memory state for connection pools and active sessions
|
|
866
|
+
*/
|
|
867
|
+
export const connectionManager = webhook('manage-connections', {
|
|
868
|
+
response: { mode: 'sync' }
|
|
869
|
+
})
|
|
870
|
+
.then(fn('initialize-memory-state', async (ctx) => {
|
|
871
|
+
const { openKv, log } = ctx;
|
|
872
|
+
|
|
873
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
874
|
+
const stateKey = ['memory-interpreter', 'connections'];
|
|
875
|
+
|
|
876
|
+
// Load existing state or initialize new
|
|
877
|
+
const existingState = await kvAdapter.get(stateKey);
|
|
878
|
+
|
|
879
|
+
const memoryState: MemoryInterpreterState = existingState?.value || {
|
|
880
|
+
sftpConnections: new Map(),
|
|
881
|
+
lastActivity: new Map(),
|
|
882
|
+
connectionPool: []
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
log.info('🧠 [MemoryInterpreter] State initialized', {
|
|
886
|
+
activeConnections: memoryState.sftpConnections.size,
|
|
887
|
+
poolSize: memoryState.connectionPool.length
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
kvAdapter,
|
|
892
|
+
stateKey,
|
|
893
|
+
memoryState
|
|
894
|
+
};
|
|
895
|
+
}))
|
|
896
|
+
.then(fn('process-with-memory-state', async ({ data, log }) => {
|
|
897
|
+
const { kvAdapter, stateKey, memoryState } = data;
|
|
898
|
+
|
|
899
|
+
// Use memory state for processing
|
|
900
|
+
const connectionId = 'sftp-main';
|
|
901
|
+
const now = Date.now();
|
|
902
|
+
|
|
903
|
+
// Check if connection is stale (> 5 minutes inactive)
|
|
904
|
+
const lastActivity = memoryState.lastActivity.get(connectionId);
|
|
905
|
+
const isStale = lastActivity && (now - lastActivity) > 5 * 60 * 1000;
|
|
906
|
+
|
|
907
|
+
if (isStale) {
|
|
908
|
+
log.info('♻️ [MemoryInterpreter] Refreshing stale connection', {
|
|
909
|
+
connectionId,
|
|
910
|
+
lastActivity: new Date(lastActivity).toISOString()
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// Refresh connection
|
|
914
|
+
memoryState.sftpConnections.delete(connectionId);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Update activity timestamp
|
|
918
|
+
memoryState.lastActivity.set(connectionId, now);
|
|
919
|
+
|
|
920
|
+
// Persist updated state
|
|
921
|
+
await kvAdapter.set(stateKey, {
|
|
922
|
+
sftpConnections: Array.from(memoryState.sftpConnections.entries()),
|
|
923
|
+
lastActivity: Array.from(memoryState.lastActivity.entries()),
|
|
924
|
+
connectionPool: memoryState.connectionPool
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
log.info('💾 [MemoryInterpreter] State persisted', {
|
|
928
|
+
connections: memoryState.sftpConnections.size,
|
|
929
|
+
lastUpdate: new Date(now).toISOString()
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
return {
|
|
933
|
+
success: true,
|
|
934
|
+
activeConnections: memoryState.sftpConnections.size,
|
|
935
|
+
lastActivity: now
|
|
936
|
+
};
|
|
937
|
+
}));
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
**Key Points**:
|
|
941
|
+
|
|
942
|
+
- Use MemoryInterpreter for connection pools and active sessions
|
|
943
|
+
- Persist state to KV to survive workflow restarts
|
|
944
|
+
- Implement stale connection detection and refresh
|
|
945
|
+
- Track activity timestamps for connection reuse
|
|
946
|
+
- Serialize/deserialize Maps and complex objects properly
|
|
947
|
+
|
|
948
|
+
---
|
|
949
|
+
|
|
950
|
+
### Pattern 8: Connection Validation
|
|
951
|
+
|
|
952
|
+
**Use Case**: Validate data source connections before processing to catch configuration errors early.
|
|
953
|
+
|
|
954
|
+
```typescript
|
|
955
|
+
import { webhook, fn } from '@versori/run';
|
|
956
|
+
import {
|
|
957
|
+
SftpDataSource,
|
|
958
|
+
S3DataSource,
|
|
959
|
+
createClient
|
|
960
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Connection validation pattern
|
|
964
|
+
* Validates all data sources before processing
|
|
965
|
+
*/
|
|
966
|
+
export const validatedIngestion = webhook('ingest-with-validation', {
|
|
967
|
+
response: { mode: 'sync' }
|
|
968
|
+
})
|
|
969
|
+
.then(fn('validate-connections', async (ctx) => {
|
|
970
|
+
const { log, activation } = ctx;
|
|
971
|
+
|
|
972
|
+
log.info('🔍 [Validation] Starting connection validation');
|
|
973
|
+
|
|
974
|
+
// Validate SFTP connection
|
|
975
|
+
const sftp = new SftpDataSource(
|
|
976
|
+
{
|
|
977
|
+
type: 'SFTP_XML',
|
|
978
|
+
connectionId: 'sftp-validation',
|
|
979
|
+
name: 'validated-sftp',
|
|
980
|
+
settings: {
|
|
981
|
+
host: activation.getVariable('sftpHost'),
|
|
982
|
+
port: parseInt(activation.getVariable('sftpPort') || '22', 10),
|
|
983
|
+
username: activation.getVariable('sftpUsername'),
|
|
984
|
+
password: activation.getVariable('sftpPassword'),
|
|
985
|
+
remotePath: '/incoming/',
|
|
986
|
+
encoding: 'utf8',
|
|
987
|
+
requireAbsolutePaths: true
|
|
988
|
+
}
|
|
989
|
+
},
|
|
990
|
+
log
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
try {
|
|
994
|
+
await sftp.validateConnection();
|
|
995
|
+
log.info('✅ [Validation] SFTP connection validated successfully');
|
|
996
|
+
} catch (error) {
|
|
997
|
+
log.error('❌ [Validation] SFTP connection failed', {
|
|
998
|
+
error: error instanceof Error ? error.message : String(error),
|
|
999
|
+
recommendation: error.message?.includes('authentication')
|
|
1000
|
+
? 'Check SFTP username and password in connection configuration'
|
|
1001
|
+
: error.message?.includes('timeout')
|
|
1002
|
+
? 'Check SFTP host and port - ensure server is reachable'
|
|
1003
|
+
: error.message?.includes('host')
|
|
1004
|
+
? 'Verify SFTP host address is correct'
|
|
1005
|
+
: 'Review SFTP connection settings and server logs'
|
|
1006
|
+
});
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Validate S3 connection
|
|
1011
|
+
const s3 = new S3DataSource(
|
|
1012
|
+
{
|
|
1013
|
+
type: 'S3_CSV',
|
|
1014
|
+
connectionId: 's3-validation',
|
|
1015
|
+
name: 'validated-s3',
|
|
1016
|
+
settings: {
|
|
1017
|
+
region: activation.getVariable('awsRegion') || 'us-east-1',
|
|
1018
|
+
bucket: activation.getVariable('s3Bucket'),
|
|
1019
|
+
prefix: 'incoming/',
|
|
1020
|
+
accessKeyId: activation.getVariable('awsAccessKeyId'),
|
|
1021
|
+
secretAccessKey: activation.getVariable('awsSecretAccessKey')
|
|
1022
|
+
}
|
|
1023
|
+
},
|
|
1024
|
+
log
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
try {
|
|
1028
|
+
await s3.validateConnection();
|
|
1029
|
+
log.info('✅ [Validation] S3 connection validated successfully');
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
log.error('❌ [Validation] S3 connection failed', {
|
|
1032
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1033
|
+
recommendation: error.message?.includes('credentials')
|
|
1034
|
+
? 'Check AWS access key and secret in connection configuration'
|
|
1035
|
+
: error.message?.includes('bucket')
|
|
1036
|
+
? 'Verify S3 bucket exists and credentials have access'
|
|
1037
|
+
: error.message?.includes('region')
|
|
1038
|
+
? 'Check AWS region is correct for the bucket'
|
|
1039
|
+
: 'Review S3 connection settings and IAM permissions'
|
|
1040
|
+
});
|
|
1041
|
+
throw error;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Validate Fluent Commerce API connection
|
|
1045
|
+
const client = await createClient(ctx);
|
|
1046
|
+
const retailerId = activation.getVariable('fluentRetailerId');
|
|
1047
|
+
|
|
1048
|
+
if (!retailerId) {
|
|
1049
|
+
log.error('❌ [Validation] Missing retailerId activation variable');
|
|
1050
|
+
throw new Error('fluentRetailerId is required for Fluent API operations');
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
client.setRetailerId(retailerId);
|
|
1054
|
+
|
|
1055
|
+
try {
|
|
1056
|
+
// Test connection with a simple query
|
|
1057
|
+
const testQuery = `query { retailers { edges { node { id } } } }`;
|
|
1058
|
+
await client.graphql({ query: testQuery });
|
|
1059
|
+
log.info('✅ [Validation] Fluent Commerce API connection validated');
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
log.error('❌ [Validation] Fluent Commerce API connection failed', {
|
|
1062
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1063
|
+
recommendation: error.message?.includes('401') || error.message?.includes('authentication')
|
|
1064
|
+
? 'Verify fluent_commerce connection OAuth2 credentials'
|
|
1065
|
+
: error.message?.includes('GraphQL')
|
|
1066
|
+
? 'Check GraphQL query syntax and permissions'
|
|
1067
|
+
: 'Review Fluent Commerce API connection settings'
|
|
1068
|
+
});
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
log.info('✅ [Validation] All connections validated successfully');
|
|
1073
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
sftp,
|
|
1076
|
+
s3,
|
|
1077
|
+
client,
|
|
1078
|
+
validated: true
|
|
1079
|
+
};
|
|
1080
|
+
}))
|
|
1081
|
+
.then(fn('process-with-validated-connections', async ({ data, log }) => {
|
|
1082
|
+
const { sftp, s3, client } = data;
|
|
1083
|
+
|
|
1084
|
+
log.info('🚀 [Processing] Starting ingestion with validated connections');
|
|
1085
|
+
|
|
1086
|
+
try {
|
|
1087
|
+
// Your ingestion logic here with validated connections
|
|
1088
|
+
const files = await sftp.listFiles({ remotePath: '/incoming/' });
|
|
1089
|
+
|
|
1090
|
+
log.info('📋 [Processing] Files discovered', { count: files.length });
|
|
1091
|
+
|
|
1092
|
+
// Process files...
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
success: true,
|
|
1096
|
+
filesProcessed: files.length
|
|
1097
|
+
};
|
|
1098
|
+
} finally {
|
|
1099
|
+
// Always dispose connections
|
|
1100
|
+
await sftp.dispose();
|
|
1101
|
+
await s3.dispose();
|
|
1102
|
+
log.info('🧹 [Cleanup] Connections disposed');
|
|
1103
|
+
}
|
|
1104
|
+
}));
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**Key Points**:
|
|
1108
|
+
|
|
1109
|
+
- Always validate connections before processing
|
|
1110
|
+
- Provide detailed error messages with recommendations
|
|
1111
|
+
- Use emoji logging for visual scanning
|
|
1112
|
+
- Fail fast on configuration errors
|
|
1113
|
+
- Dispose connections in finally blocks
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
## Production Best Practices
|
|
1118
|
+
|
|
1119
|
+
### 1. Emoji Logging for Operations Visibility
|
|
1120
|
+
|
|
1121
|
+
Use emoji prefixes in logs for quick visual scanning:
|
|
1122
|
+
|
|
1123
|
+
```typescript
|
|
1124
|
+
log.info('🔍 [Discovery] Scanning SFTP directory');
|
|
1125
|
+
log.info('✅ [Success] File processed successfully');
|
|
1126
|
+
log.error('❌ [Error] Connection failed', { error });
|
|
1127
|
+
log.warn('⚠️ [Warning] Partial batch failure');
|
|
1128
|
+
log.info('🧹 [Cleanup] Archiving processed files');
|
|
1129
|
+
log.info('🧠 [MemoryInterpreter] State synchronized');
|
|
1130
|
+
log.info('💾 [Persistence] State saved to KV');
|
|
1131
|
+
log.info('♻️ [Refresh] Stale connection renewed');
|
|
1132
|
+
log.info('🚀 [Start] Processing batch');
|
|
1133
|
+
log.info('📋 [Info] Configuration loaded');
|
|
1134
|
+
log.info('🔐 [Auth] Token refreshed');
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
**Benefits:**
|
|
1138
|
+
- Quick visual scanning in production logs
|
|
1139
|
+
- Easy pattern recognition for operations
|
|
1140
|
+
- Faster debugging and troubleshooting
|
|
1141
|
+
- Better collaboration across teams
|
|
1142
|
+
|
|
1143
|
+
### 2. Connection Validation Pattern
|
|
1144
|
+
|
|
1145
|
+
Always validate connections before processing:
|
|
1146
|
+
|
|
1147
|
+
```typescript
|
|
1148
|
+
// ✅ CORRECT - Validate early
|
|
1149
|
+
const sftp = new SftpDataSource(config, log);
|
|
1150
|
+
await sftp.validateConnection();
|
|
1151
|
+
log.info('✅ Connection validated');
|
|
1152
|
+
|
|
1153
|
+
// Process files...
|
|
1154
|
+
|
|
1155
|
+
// ❌ WRONG - Discover errors during processing
|
|
1156
|
+
const sftp = new SftpDataSource(config, log);
|
|
1157
|
+
const files = await sftp.listFiles(); // Might fail here!
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
### 3. Error Handling with Recommendations
|
|
1161
|
+
|
|
1162
|
+
Provide actionable recommendations in error logs:
|
|
1163
|
+
|
|
1164
|
+
```typescript
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
log.error('❌ [Processing] File processing failed', {
|
|
1167
|
+
fileName: file.name,
|
|
1168
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1169
|
+
recommendation: error.message?.includes('authentication')
|
|
1170
|
+
? 'Check connection credentials in Connections section'
|
|
1171
|
+
: error.message?.includes('timeout')
|
|
1172
|
+
? 'Check network connectivity and increase timeout settings'
|
|
1173
|
+
: error.message?.includes('parse')
|
|
1174
|
+
? 'Verify file format matches expected structure'
|
|
1175
|
+
: 'Review error details and check file processing logic'
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
### 4. Resource Cleanup Pattern
|
|
1181
|
+
|
|
1182
|
+
Always dispose data sources in finally blocks:
|
|
1183
|
+
|
|
1184
|
+
```typescript
|
|
1185
|
+
const sftp = new SftpDataSource(config, log);
|
|
1186
|
+
try {
|
|
1187
|
+
await sftp.validateConnection();
|
|
1188
|
+
// Process files...
|
|
1189
|
+
} finally {
|
|
1190
|
+
await sftp.dispose();
|
|
1191
|
+
log.info('🧹 [Cleanup] SFTP connection disposed');
|
|
1192
|
+
}
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
### 5. MemoryInterpreter State Management
|
|
1196
|
+
|
|
1197
|
+
Use MemoryInterpreter for connection pools and session state:
|
|
1198
|
+
|
|
1199
|
+
```typescript
|
|
1200
|
+
// Persist complex objects to KV
|
|
1201
|
+
await kvAdapter.set(['memory-interpreter', 'state'], {
|
|
1202
|
+
connections: Array.from(connectionMap.entries()),
|
|
1203
|
+
lastActivity: Array.from(activityMap.entries()),
|
|
1204
|
+
pool: connectionPool
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// Restore from KV
|
|
1208
|
+
const state = await kvAdapter.get(['memory-interpreter', 'state']);
|
|
1209
|
+
const connectionMap = new Map(state.value.connections);
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
### 6. JobTracker Integration
|
|
1213
|
+
|
|
1214
|
+
Use JobTracker for production monitoring:
|
|
1215
|
+
|
|
1216
|
+
```typescript
|
|
1217
|
+
const tracker = new JobTracker(openKv(':project:'), log);
|
|
1218
|
+
const jobId = `job-${Date.now()}`;
|
|
1219
|
+
|
|
1220
|
+
await tracker.createJob(jobId, {
|
|
1221
|
+
triggeredBy: 'schedule',
|
|
1222
|
+
stage: 'initialization',
|
|
1223
|
+
startTime: Date.now()
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
try {
|
|
1227
|
+
// Process...
|
|
1228
|
+
await tracker.markCompleted(jobId, { filesProcessed: 10 });
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
await tracker.markFailed(jobId, error.message);
|
|
1231
|
+
}
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
---
|
|
1235
|
+
|
|
1236
|
+
## Testing State Management
|
|
1237
|
+
|
|
1238
|
+
### Local Testing Pattern
|
|
1239
|
+
|
|
1240
|
+
```typescript
|
|
1241
|
+
/**
|
|
1242
|
+
* Test state management locally
|
|
1243
|
+
*/
|
|
1244
|
+
export const testStateManagement = webhook('test-state', {
|
|
1245
|
+
response: { mode: 'sync' }
|
|
1246
|
+
})
|
|
1247
|
+
.then(fn('test-operations', async (ctx) => {
|
|
1248
|
+
const { openKv, log } = ctx;
|
|
1249
|
+
|
|
1250
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
1251
|
+
const stateService = new StateService(log);
|
|
1252
|
+
|
|
1253
|
+
// Test 1: Lock acquire and release
|
|
1254
|
+
log.info('🧪 [Test] Test 1: Lock management');
|
|
1255
|
+
const lockAcquired = await stateService.acquireLock('test-lock', kvAdapter, 5);
|
|
1256
|
+
log.info(`🔐 [Test] Lock acquired: ${lockAcquired}`);
|
|
1257
|
+
|
|
1258
|
+
// Try to acquire again (should fail)
|
|
1259
|
+
const lockAcquired2 = await stateService.acquireLock('test-lock', kvAdapter, 5);
|
|
1260
|
+
log.info(`⚠️ [Test] Second lock acquired: ${lockAcquired2} (should be false)`);
|
|
1261
|
+
|
|
1262
|
+
// Release lock
|
|
1263
|
+
await stateService.releaseLock('test-lock', kvAdapter);
|
|
1264
|
+
log.info('🔓 [Test] Lock released');
|
|
1265
|
+
|
|
1266
|
+
// Test 2: File tracking
|
|
1267
|
+
log.info('🧪 [Test] Test 2: File tracking');
|
|
1268
|
+
const fileTracker = new VersoriFileTracker(openKv(':project:'), 'test');
|
|
1269
|
+
await fileTracker.markFileProcessed('test-file.csv', { recordCount: 100 });
|
|
1270
|
+
const wasProcessed = await fileTracker.wasFileProcessed('test-file.csv');
|
|
1271
|
+
log.info(`✅ [Test] File processed: ${wasProcessed} (should be true)`);
|
|
1272
|
+
|
|
1273
|
+
const lastFile = await fileTracker.getLastProcessedFile();
|
|
1274
|
+
await fileTracker.setLastProcessedFile('test-file.csv');
|
|
1275
|
+
const newLastFile = await fileTracker.getLastProcessedFile();
|
|
1276
|
+
log.info(`📋 [Test] Last file: ${newLastFile}`);
|
|
1277
|
+
|
|
1278
|
+
// Test 3: Sync state
|
|
1279
|
+
log.info('🧪 [Test] Test 3: Sync state');
|
|
1280
|
+
await stateService.updateSyncState(kvAdapter, [{
|
|
1281
|
+
fileName: 'test.csv',
|
|
1282
|
+
lastModified: new Date().toISOString(),
|
|
1283
|
+
recordCount: 100
|
|
1284
|
+
}], 'test-workflow');
|
|
1285
|
+
|
|
1286
|
+
const syncState = await stateService.getSyncState(kvAdapter, 'test-workflow');
|
|
1287
|
+
log.info('💾 [Test] Sync state', syncState);
|
|
1288
|
+
|
|
1289
|
+
// Test 4: Daily job
|
|
1290
|
+
log.info('🧪 [Test] Test 4: Daily job');
|
|
1291
|
+
await stateService.setDailyJob(kvAdapter, 'test-workflow', 'job-123', 24);
|
|
1292
|
+
const dailyJob = await stateService.getDailyJob(kvAdapter, 'test-workflow');
|
|
1293
|
+
log.info('📋 [Test] Daily job', dailyJob);
|
|
1294
|
+
|
|
1295
|
+
return {
|
|
1296
|
+
success: true,
|
|
1297
|
+
message: '✅ All state management tests passed'
|
|
1298
|
+
};
|
|
1299
|
+
}));
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
---
|
|
1303
|
+
|
|
1304
|
+
## Common Issues & Solutions
|
|
1305
|
+
|
|
1306
|
+
### Issue 1: Lock Not Released After Error
|
|
1307
|
+
|
|
1308
|
+
**Problem**: Workflow crashes and lock is never released, blocking future runs.
|
|
1309
|
+
|
|
1310
|
+
**Solution**: Always use try/finally or .catch() to ensure lock release:
|
|
1311
|
+
|
|
1312
|
+
```typescript
|
|
1313
|
+
export const safeWorkflow = schedule('safe-job', '0 * * * *')
|
|
1314
|
+
.then(fn('work', async (ctx) => {
|
|
1315
|
+
const { openKv, log } = ctx;
|
|
1316
|
+
|
|
1317
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
1318
|
+
const stateService = new StateService(log);
|
|
1319
|
+
const lockName = 'safe-lock';
|
|
1320
|
+
|
|
1321
|
+
try {
|
|
1322
|
+
await stateService.acquireLock(lockName, kvAdapter, 15);
|
|
1323
|
+
// Your work here
|
|
1324
|
+
await doWork();
|
|
1325
|
+
} finally {
|
|
1326
|
+
// Always release, even on error
|
|
1327
|
+
await stateService.releaseLock(lockName, kvAdapter);
|
|
1328
|
+
}
|
|
1329
|
+
}));
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
Or use the catch pattern:
|
|
1333
|
+
|
|
1334
|
+
```typescript
|
|
1335
|
+
export const workflowWithCatch = schedule('job', '0 * * * *')
|
|
1336
|
+
.then(fn('acquire', async (ctx) => {
|
|
1337
|
+
const { openKv, log } = ctx;
|
|
1338
|
+
|
|
1339
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
1340
|
+
const stateService = new StateService(log);
|
|
1341
|
+
await stateService.acquireLock('my-lock', kvAdapter, 15);
|
|
1342
|
+
return { kvAdapter, stateService };
|
|
1343
|
+
}))
|
|
1344
|
+
.then(fn('work', async ({ data }) => {
|
|
1345
|
+
// Work here
|
|
1346
|
+
return data;
|
|
1347
|
+
}))
|
|
1348
|
+
.catch(fn('cleanup', async ({ data }) => {
|
|
1349
|
+
// Release lock on any error
|
|
1350
|
+
if (data?.stateService) {
|
|
1351
|
+
await data.stateService.releaseLock('my-lock', data.kvAdapter);
|
|
1352
|
+
}
|
|
1353
|
+
throw new Error('Workflow failed');
|
|
1354
|
+
}));
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
---
|
|
1358
|
+
|
|
1359
|
+
### Issue 2: Stale Lock Detection
|
|
1360
|
+
|
|
1361
|
+
**Problem**: Lock holder crashed and never released lock.
|
|
1362
|
+
|
|
1363
|
+
**Solution**: Use lock timeout - StateService automatically overrides stale locks:
|
|
1364
|
+
|
|
1365
|
+
```typescript
|
|
1366
|
+
// Lock with 15 minute timeout
|
|
1367
|
+
const acquired = await stateService.acquireLock('my-lock', kvAdapter, 15);
|
|
1368
|
+
|
|
1369
|
+
// If lock is older than 15 minutes, it will be overridden automatically
|
|
1370
|
+
// This prevents permanent deadlocks from crashed processes
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
**How it works**:
|
|
1374
|
+
|
|
1375
|
+
- Each lock stores `expiresAt` timestamp
|
|
1376
|
+
- `acquireLock()` checks if existing lock is expired
|
|
1377
|
+
- Expired locks are automatically overridden
|
|
1378
|
+
- Recent locks return false (lock not acquired)
|
|
1379
|
+
|
|
1380
|
+
---
|
|
1381
|
+
|
|
1382
|
+
### Issue 3: Cannot List Processed Files
|
|
1383
|
+
|
|
1384
|
+
**Problem**: Versori KV doesn't support list operations, can't enumerate processed files.
|
|
1385
|
+
|
|
1386
|
+
**Solution**: Use `VersoriIndexedFileTracker` which maintains an index:
|
|
1387
|
+
|
|
1388
|
+
```typescript
|
|
1389
|
+
// Instead of VersoriFileTracker
|
|
1390
|
+
const fileTracker = new VersoriFileTracker(openKv(':project:'));
|
|
1391
|
+
|
|
1392
|
+
// Use VersoriIndexedFileTracker
|
|
1393
|
+
const indexedTracker = new VersoriIndexedFileTracker(openKv(':project:'));
|
|
1394
|
+
|
|
1395
|
+
// Now you can list files
|
|
1396
|
+
const allFiles = await indexedTracker.listProcessedFiles();
|
|
1397
|
+
const stats = await indexedTracker.getStats();
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
**How it works**:
|
|
1401
|
+
|
|
1402
|
+
- Maintains separate index key with list of all file names
|
|
1403
|
+
- Index updated atomically when files are marked/removed
|
|
1404
|
+
- Enables listing without KV list support
|
|
1405
|
+
|
|
1406
|
+
---
|
|
1407
|
+
|
|
1408
|
+
### Issue 4: Checkpoint Corruption
|
|
1409
|
+
|
|
1410
|
+
**Problem**: Checkpoint data gets corrupted or becomes invalid.
|
|
1411
|
+
|
|
1412
|
+
**Solution**: Add validation and version to checkpoint data:
|
|
1413
|
+
|
|
1414
|
+
```typescript
|
|
1415
|
+
interface VersionedCheckpoint {
|
|
1416
|
+
version: number; // Schema version
|
|
1417
|
+
data: CheckpointData;
|
|
1418
|
+
checksum?: string; // Optional integrity check
|
|
1419
|
+
savedAt: string;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
async function saveCheckpoint(
|
|
1423
|
+
kvAdapter: VersoriKVAdapter,
|
|
1424
|
+
key: string[],
|
|
1425
|
+
data: CheckpointData
|
|
1426
|
+
): Promise<void> {
|
|
1427
|
+
const checkpoint: VersionedCheckpoint = {
|
|
1428
|
+
version: 1,
|
|
1429
|
+
data,
|
|
1430
|
+
savedAt: new Date().toISOString()
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
await kvAdapter.set(key, checkpoint);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
async function loadCheckpoint(
|
|
1437
|
+
kvAdapter: VersoriKVAdapter,
|
|
1438
|
+
key: string[]
|
|
1439
|
+
): Promise<CheckpointData | null> {
|
|
1440
|
+
try {
|
|
1441
|
+
const result = await kvAdapter.get(key);
|
|
1442
|
+
if (!result?.value) return null;
|
|
1443
|
+
|
|
1444
|
+
const checkpoint = result.value as VersionedCheckpoint;
|
|
1445
|
+
|
|
1446
|
+
// Validate version
|
|
1447
|
+
if (checkpoint.version !== 1) {
|
|
1448
|
+
console.warn('Checkpoint version mismatch, ignoring');
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Check if checkpoint is too old (e.g., > 7 days)
|
|
1453
|
+
const savedAt = new Date(checkpoint.savedAt);
|
|
1454
|
+
const age = Date.now() - savedAt.getTime();
|
|
1455
|
+
if (age > 7 * 24 * 60 * 60 * 1000) {
|
|
1456
|
+
console.warn('Checkpoint too old, starting fresh');
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
return checkpoint.data;
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
console.error('Failed to load checkpoint', error);
|
|
1463
|
+
return null; // Start fresh on corruption
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
---
|
|
1469
|
+
|
|
1470
|
+
## Best Practices Summary
|
|
1471
|
+
|
|
1472
|
+
### Core State Management Practices
|
|
1473
|
+
|
|
1474
|
+
1. **Always Release Locks**: Use try/finally or .catch() to ensure locks are released
|
|
1475
|
+
2. **Use Lock Timeouts**: Set appropriate timeouts (15-30 minutes typical)
|
|
1476
|
+
3. **Checkpoint Periodically**: Not too often (overhead) or too rarely (lost progress)
|
|
1477
|
+
4. **Validate Restored State**: Check checkpoint age and integrity before using
|
|
1478
|
+
5. **Clear Completed Checkpoints**: Remove checkpoint data after successful completion
|
|
1479
|
+
6. **Use Indexed Tracker for Listing**: VersoriKV doesn't support list operations natively
|
|
1480
|
+
7. **Track Error State**: Implement exponential backoff for retries
|
|
1481
|
+
8. **Monitor Lock Ages**: Alert on locks held longer than expected
|
|
1482
|
+
9. **Namespace Your Keys**: Use prefixes to organize KV data (`workflow:resource:identifier`)
|
|
1483
|
+
10. **Document Key Schemas**: Keep track of what data is stored under which keys
|
|
1484
|
+
|
|
1485
|
+
### Production Patterns (NEW)
|
|
1486
|
+
|
|
1487
|
+
11. **Use Emoji Logging**: Add emoji prefixes for quick visual scanning in logs (see Pattern 8)
|
|
1488
|
+
12. **Validate Connections Early**: Always call `validateConnection()` before processing (see Pattern 8)
|
|
1489
|
+
13. **Provide Error Recommendations**: Include actionable recommendations in error logs
|
|
1490
|
+
14. **Dispose Resources**: Always dispose data sources in finally blocks
|
|
1491
|
+
15. **Use MemoryInterpreter**: For connection pools and session state (see Pattern 7)
|
|
1492
|
+
16. **Integrate JobTracker**: Track job lifecycle for production monitoring
|
|
1493
|
+
|
|
1494
|
+
---
|
|
1495
|
+
|
|
1496
|
+
## Related Guides
|
|
1497
|
+
|
|
1498
|
+
- **01-inventory-ingestion.md** - Complete ingestion workflow using state management
|
|
1499
|
+
- **02-inventory-extraction.md** - Extraction workflow with checkpoints
|
|
1500
|
+
- **04-batch-archival.md** - Archiving batch data to S3 (uses daily jobs)
|
|
1501
|
+
- **05-error-handling.md** - Advanced error handling patterns
|
|
1502
|
+
- **StateService full reference**: `../../02-CORE-GUIDES/ingestion/modules/07-state-management.md`
|
|
1503
|
+
|
|
1504
|
+
---
|
|
1505
|
+
|
|
1506
|
+
## Summary
|
|
1507
|
+
|
|
1508
|
+
This guide covered comprehensive state management patterns for Versori connectors:
|
|
1509
|
+
|
|
1510
|
+
1. **Simple File Tracking** - Prevent duplicate processing with VersoriFileTracker
|
|
1511
|
+
2. **Distributed Locking** - Prevent concurrent executions with StateService
|
|
1512
|
+
3. **Checkpoints** - Save progress and resume long-running workflows
|
|
1513
|
+
4. **Daily Jobs** - Reuse jobs across batches throughout the day
|
|
1514
|
+
5. **Error States** - Track failures and implement smart retry logic
|
|
1515
|
+
6. **Indexed Tracking** - List and manage processed files
|
|
1516
|
+
7. **MemoryInterpreter** (NEW) - In-memory state for connection pools
|
|
1517
|
+
8. **Connection Validation** (NEW) - Validate connections before processing
|
|
1518
|
+
|
|
1519
|
+
**Key Takeaways**:
|
|
1520
|
+
|
|
1521
|
+
- VersoriFileTracker for simple duplicate prevention
|
|
1522
|
+
- StateService for distributed locking and sync state
|
|
1523
|
+
- VersoriIndexedFileTracker when you need to list files
|
|
1524
|
+
- Always release locks in finally blocks
|
|
1525
|
+
- Checkpoint periodically to enable resume
|
|
1526
|
+
- Validate restored state before using
|
|
1527
|
+
- Use exponential backoff for retries
|
|
1528
|
+
- **Use emoji logging for production visibility** (NEW)
|
|
1529
|
+
- **Validate connections early with `validateConnection()`** (NEW)
|
|
1530
|
+
- **Use MemoryInterpreter for connection pool state** (NEW)
|
|
1531
|
+
- **Provide actionable error recommendations** (NEW)
|
|
1532
|
+
|
|
1533
|
+
State management is critical for robust production workflows. These patterns ensure your connectors are reliable, resumable, and prevent duplicate processing.
|