@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
package/docs/02-CORE-GUIDES/ingestion/modules/02-core-guides-ingestion-07-state-management.md
CHANGED
|
@@ -1,1037 +1,1037 @@
|
|
|
1
|
-
# Module 7: State Management
|
|
2
|
-
|
|
3
|
-
[← Back to Ingestion Guide](../ingestion-readme.md)
|
|
4
|
-
|
|
5
|
-
**Module 7 of 9** | **Level**: Advanced | **Time**: 20 minutes
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Overview
|
|
10
|
-
|
|
11
|
-
This module covers state management for ingestion workflows, focusing on duplicate prevention, file tracking, and distributed locking. Learn how to build idempotent, production-ready ingestion pipelines using the SDK's `StateService`, `JobTracker`, and `VersoriFileTracker`.
|
|
12
|
-
|
|
13
|
-
## Learning Objectives
|
|
14
|
-
|
|
15
|
-
By the end of this module, you will:
|
|
16
|
-
|
|
17
|
-
- ✅ Understand why state management is critical for ingestion workflows
|
|
18
|
-
- ✅ Prevent duplicate file processing with file tracking
|
|
19
|
-
- ✅ Use distributed locking for concurrent safety
|
|
20
|
-
- ✅ Implement idempotent operations that can be safely retried
|
|
21
|
-
- ✅ Store and retrieve sync state for incremental processing
|
|
22
|
-
- ✅ Handle state storage in Versori and standalone environments
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Why State Management is Critical
|
|
27
|
-
|
|
28
|
-
### The Duplicate Processing Problem
|
|
29
|
-
|
|
30
|
-
**Without state management:**
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// ❌ WRONG - Reprocessing files creates duplicates
|
|
34
|
-
async function ingestFromS3Naive() {
|
|
35
|
-
const files = await s3.listFiles({ prefix: 'data/' });
|
|
36
|
-
|
|
37
|
-
for (const file of files) {
|
|
38
|
-
const data = await s3.downloadFile(file.path);
|
|
39
|
-
const records = await parser.parse(data);
|
|
40
|
-
|
|
41
|
-
// Problem: Every run processes ALL files again
|
|
42
|
-
await sendToBatchAPI(records);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Results:
|
|
47
|
-
// - Run 1: Processes 100 files ✅
|
|
48
|
-
// - Run 2: Processes same 100 files again ❌ DUPLICATES
|
|
49
|
-
// - Run 3: Processes same 100 files again ❌ MORE DUPLICATES
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
**With state management:**
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
// ✅ CORRECT - Only process new files
|
|
56
|
-
async function ingestFromS3WithState(state: StateService, kv: KVStore) {
|
|
57
|
-
const files = await s3.listFiles({ prefix: 'data/' });
|
|
58
|
-
|
|
59
|
-
for (const file of files) {
|
|
60
|
-
// Check if already processed
|
|
61
|
-
if (await state.isFileProcessed(kv, file.path)) {
|
|
62
|
-
console.log(`Skipping processed file: ${file.path}`);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const data = await s3.downloadFile(file.path);
|
|
67
|
-
const records = await parser.parse(data);
|
|
68
|
-
|
|
69
|
-
await sendToBatchAPI(records);
|
|
70
|
-
|
|
71
|
-
// Mark as processed
|
|
72
|
-
await state.updateSyncState(kv, [
|
|
73
|
-
{
|
|
74
|
-
fileName: file.path,
|
|
75
|
-
lastModified: file.lastModified,
|
|
76
|
-
recordCount: records.length,
|
|
77
|
-
},
|
|
78
|
-
]);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Results:
|
|
83
|
-
// - Run 1: Processes 100 new files ✅
|
|
84
|
-
// - Run 2: Skips 100 processed files, processes 10 new files ✅
|
|
85
|
-
// - Run 3: Skips 110 processed files, processes 5 new files ✅
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Business Impact
|
|
89
|
-
|
|
90
|
-
| Scenario | Without State | With State |
|
|
91
|
-
| ------------------------ | --------------------------- | ----------------------------------- |
|
|
92
|
-
| **Daily sync rerun** | Duplicates entire inventory | Processes only new files |
|
|
93
|
-
| **Retry failed batch** | Reprocesses all batches | Retries only failed batch |
|
|
94
|
-
| **Concurrent execution** | Race conditions, duplicates | Distributed locks prevent conflicts |
|
|
95
|
-
| **Incremental updates** | Must track externally | Automatic high-watermark tracking |
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## StateService Overview
|
|
100
|
-
|
|
101
|
-
The `StateService` provides comprehensive state management with three core capabilities:
|
|
102
|
-
|
|
103
|
-
### 1. File Processing Tracking
|
|
104
|
-
|
|
105
|
-
Track which files have been processed to prevent duplicates.
|
|
106
|
-
|
|
107
|
-
### 2. Distributed Locking
|
|
108
|
-
|
|
109
|
-
Ensure only one process handles a file at a time (concurrent safety).
|
|
110
|
-
|
|
111
|
-
### 3. Sync State Management
|
|
112
|
-
|
|
113
|
-
Store high-watermark timestamps for incremental processing.
|
|
114
|
-
|
|
115
|
-
### Architecture
|
|
116
|
-
|
|
117
|
-
```mermaid
|
|
118
|
-
graph TD
|
|
119
|
-
A[Ingestion Workflow] --> B{Check State}
|
|
120
|
-
B --> C[StateService]
|
|
121
|
-
C --> D[KV Store]
|
|
122
|
-
|
|
123
|
-
B -->|File Processed| E[Skip File]
|
|
124
|
-
B -->|New File| F[Acquire Lock]
|
|
125
|
-
|
|
126
|
-
F -->|Lock Acquired| G[Process File]
|
|
127
|
-
F -->|Lock Held| H[Wait/Skip]
|
|
128
|
-
|
|
129
|
-
G --> I[Send to Batch API]
|
|
130
|
-
I --> J[Mark as Processed]
|
|
131
|
-
J --> K[Release Lock]
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## File Tracking for Duplicate Prevention
|
|
137
|
-
|
|
138
|
-
### File Tracking with StateService (Recommended)
|
|
139
|
-
|
|
140
|
-
**Use `StateService`** for production workflows with sync state and metadata tracking:
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
144
|
-
// ✅ CORRECT: Access openKv from Versori context
|
|
145
|
-
// import { openKv } from '@versori/run'; // ❌ WRONG - Not a direct export
|
|
146
|
-
|
|
147
|
-
async function ingestWithFileTracking(
|
|
148
|
-
files: string[],
|
|
149
|
-
ctx: any
|
|
150
|
-
): Promise<void> {
|
|
151
|
-
const { log, openKv } = ctx;
|
|
152
|
-
const logger = toStructuredLogger(log, {
|
|
153
|
-
service: 'inventory-ingestion'
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const stateService = new StateService(logger);
|
|
157
|
-
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
158
|
-
let processedCount = 0;
|
|
159
|
-
let skippedCount = 0;
|
|
160
|
-
|
|
161
|
-
for (const fileName of files) {
|
|
162
|
-
// Check if file already processed
|
|
163
|
-
if (await state.isFileProcessed(kv, fileName)) {
|
|
164
|
-
log.info(`Skipping processed file: ${fileName}`);
|
|
165
|
-
skippedCount++;
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Process file
|
|
170
|
-
log.info(`Processing new file: ${fileName}`);
|
|
171
|
-
const records = await processFile(fileName);
|
|
172
|
-
await sendToBatchAPI(records);
|
|
173
|
-
|
|
174
|
-
// Mark as processed with metadata
|
|
175
|
-
await state.updateSyncState(kv, [
|
|
176
|
-
{
|
|
177
|
-
fileName,
|
|
178
|
-
lastModified: new Date().toISOString(),
|
|
179
|
-
recordCount: records.length,
|
|
180
|
-
},
|
|
181
|
-
]);
|
|
182
|
-
|
|
183
|
-
processedCount++;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
log.info(`Processed: ${processedCount}, Skipped: ${skippedCount}`);
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### Tracking with Metadata
|
|
191
|
-
|
|
192
|
-
Store rich metadata for auditing and troubleshooting:
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
interface FileProcessingMetadata {
|
|
196
|
-
fileName: string;
|
|
197
|
-
lastModified: string;
|
|
198
|
-
recordCount: number;
|
|
199
|
-
processedAt: string;
|
|
200
|
-
jobId?: string;
|
|
201
|
-
batchIds?: string[];
|
|
202
|
-
fileSize?: number;
|
|
203
|
-
checksumMD5?: string;
|
|
204
|
-
source?: string;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function trackFileWithMetadata(
|
|
208
|
-
stateService: StateService,
|
|
209
|
-
kv: KVStore,
|
|
210
|
-
fileName: string,
|
|
211
|
-
metadata: FileProcessingMetadata
|
|
212
|
-
): Promise<void> {
|
|
213
|
-
await stateService.updateSyncState(kv, [metadata]);
|
|
214
|
-
|
|
215
|
-
console.log('File tracked:', {
|
|
216
|
-
file: fileName,
|
|
217
|
-
records: metadata.recordCount,
|
|
218
|
-
jobId: metadata.jobId,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Usage
|
|
223
|
-
await trackFileWithMetadata(stateService, kv, 'inventory-2025-01-19.csv', {
|
|
224
|
-
fileName: 'inventory-2025-01-19.csv',
|
|
225
|
-
lastModified: '2025-01-19T08:00:00Z',
|
|
226
|
-
recordCount: 5000,
|
|
227
|
-
processedAt: new Date().toISOString(),
|
|
228
|
-
jobId: 'JOB-12345',
|
|
229
|
-
batchIds: ['BATCH-001', 'BATCH-002'],
|
|
230
|
-
fileSize: 1024000,
|
|
231
|
-
checksumMD5: 'a1b2c3d4e5f6',
|
|
232
|
-
source: 'S3',
|
|
233
|
-
});
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
|
-
## Distributed Locking for Concurrent Safety
|
|
239
|
-
|
|
240
|
-
### The Concurrency Problem
|
|
241
|
-
|
|
242
|
-
**Without locking:**
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
// ❌ WRONG - Race condition when multiple processes run
|
|
246
|
-
// Process A: Checks file not processed → Starts processing
|
|
247
|
-
// Process B: Checks file not processed → Starts processing
|
|
248
|
-
// Result: File processed twice simultaneously
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
**With locking:**
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
// ✅ CORRECT - Only one process acquires lock
|
|
255
|
-
// Process A: Acquires lock → Processes file → Releases lock
|
|
256
|
-
// Process B: Lock held → Waits or skips → Tries later
|
|
257
|
-
// Result: File processed exactly once
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### Acquire and Release Lock Pattern
|
|
261
|
-
|
|
262
|
-
```typescript
|
|
263
|
-
import { StateService, KVStore } from '@fluentcommerce/fc-connect-sdk';
|
|
264
|
-
|
|
265
|
-
async function processFileWithLock(
|
|
266
|
-
fileName: string,
|
|
267
|
-
state: StateService,
|
|
268
|
-
kv: KVStore
|
|
269
|
-
): Promise<void> {
|
|
270
|
-
const lockName = `file:${fileName}`;
|
|
271
|
-
const lockTimeoutMinutes = 15;
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
// Attempt to acquire lock
|
|
275
|
-
const lockAcquired = await state.acquireLock(lockName, kv, lockTimeoutMinutes);
|
|
276
|
-
|
|
277
|
-
if (!lockAcquired) {
|
|
278
|
-
console.log(`Lock held by another process, skipping: ${fileName}`);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
console.log(`Lock acquired for: ${fileName}`);
|
|
283
|
-
|
|
284
|
-
// Check if already processed (double-check after acquiring lock)
|
|
285
|
-
if (await state.isFileProcessed(kv, fileName)) {
|
|
286
|
-
console.log(`File already processed: ${fileName}`);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Process file
|
|
291
|
-
const records = await processFile(fileName);
|
|
292
|
-
await sendToBatchAPI(records);
|
|
293
|
-
|
|
294
|
-
// Mark as processed
|
|
295
|
-
await state.updateSyncState(kv, [
|
|
296
|
-
{
|
|
297
|
-
fileName,
|
|
298
|
-
lastModified: new Date().toISOString(),
|
|
299
|
-
recordCount: records.length,
|
|
300
|
-
},
|
|
301
|
-
]);
|
|
302
|
-
|
|
303
|
-
console.log(`Successfully processed: ${fileName}`);
|
|
304
|
-
} finally {
|
|
305
|
-
// ALWAYS release lock in finally block
|
|
306
|
-
await state.releaseLock(lockName, kv);
|
|
307
|
-
console.log(`Lock released for: ${fileName}`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
### Lock Timeout and Stale Lock Detection
|
|
313
|
-
|
|
314
|
-
The SDK automatically handles stale locks:
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
// Scenario: Process crashes while holding lock
|
|
318
|
-
// 1. Process A acquires lock with 15-minute timeout
|
|
319
|
-
// 2. Process A crashes at minute 10 (lock still held)
|
|
320
|
-
// 3. Minute 16: Lock expires automatically
|
|
321
|
-
// 4. Process B acquires stale lock (overrides expired lock)
|
|
322
|
-
|
|
323
|
-
async function processWithStaleLockHandling(
|
|
324
|
-
fileName: string,
|
|
325
|
-
state: StateService,
|
|
326
|
-
kv: KVStore
|
|
327
|
-
): Promise<void> {
|
|
328
|
-
const lockTimeoutMinutes = 15;
|
|
329
|
-
|
|
330
|
-
// SDK automatically detects and overrides stale locks
|
|
331
|
-
const lockAcquired = await state.acquireLock(`file:${fileName}`, kv, lockTimeoutMinutes);
|
|
332
|
-
|
|
333
|
-
if (!lockAcquired) {
|
|
334
|
-
// Lock is held by active process (not stale)
|
|
335
|
-
console.log('Lock actively held by another process');
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Lock acquired (either fresh or stale was overridden)
|
|
340
|
-
try {
|
|
341
|
-
await processFile(fileName);
|
|
342
|
-
} finally {
|
|
343
|
-
await state.releaseLock(`file:${fileName}`, kv);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
---
|
|
349
|
-
|
|
350
|
-
## Idempotent Processing
|
|
351
|
-
|
|
352
|
-
**Idempotency** means operations can be safely repeated without changing the result beyond the initial application.
|
|
353
|
-
|
|
354
|
-
### Idempotent Ingestion Pattern
|
|
355
|
-
|
|
356
|
-
```typescript
|
|
357
|
-
async function idempotentIngestion(
|
|
358
|
-
fileName: string,
|
|
359
|
-
state: StateService,
|
|
360
|
-
kv: KVStore
|
|
361
|
-
): Promise<{ success: boolean; message: string }> {
|
|
362
|
-
const lockName = `ingestion:${fileName}`;
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
// 1. Acquire lock (prevents concurrent processing)
|
|
366
|
-
const lockAcquired = await state.acquireLock(lockName, kv, 15);
|
|
367
|
-
if (!lockAcquired) {
|
|
368
|
-
return {
|
|
369
|
-
success: true,
|
|
370
|
-
message: 'File locked by another process, will retry later',
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// 2. Check if already processed (prevents duplicates)
|
|
375
|
-
if (await state.isFileProcessed(kv, fileName)) {
|
|
376
|
-
return {
|
|
377
|
-
success: true,
|
|
378
|
-
message: 'File already processed, skipping',
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// 3. Process file
|
|
383
|
-
const records = await processFile(fileName);
|
|
384
|
-
|
|
385
|
-
// 4. Send to Batch API (UPSERT is idempotent)
|
|
386
|
-
const job = await client.createJob({
|
|
387
|
-
name: `Import ${fileName}`,
|
|
388
|
-
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
await client.sendBatch(job.id, {
|
|
392
|
-
action: 'UPSERT', // ✅ UPSERT is idempotent
|
|
393
|
-
entityType: 'INVENTORY',
|
|
394
|
-
source: 'STATE_MANAGEMENT_EXAMPLE',
|
|
395
|
-
event: 'INVENTORY_UPDATE',
|
|
396
|
-
entities: records,
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
// 5. Mark as processed (atomic operation)
|
|
400
|
-
await state.updateSyncState(kv, [
|
|
401
|
-
{
|
|
402
|
-
fileName,
|
|
403
|
-
lastModified: new Date().toISOString(),
|
|
404
|
-
recordCount: records.length,
|
|
405
|
-
processedAt: new Date().toISOString(),
|
|
406
|
-
},
|
|
407
|
-
]);
|
|
408
|
-
|
|
409
|
-
return {
|
|
410
|
-
success: true,
|
|
411
|
-
message: `Processed ${records.length} records`,
|
|
412
|
-
};
|
|
413
|
-
} catch (error) {
|
|
414
|
-
// DO NOT mark as processed on error
|
|
415
|
-
console.error(`Processing failed for ${fileName}:`, error);
|
|
416
|
-
return {
|
|
417
|
-
success: false,
|
|
418
|
-
message: (error as Error).message,
|
|
419
|
-
};
|
|
420
|
-
} finally {
|
|
421
|
-
// Always release lock
|
|
422
|
-
await state.releaseLock(lockName, kv);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Safe to retry - will skip if already completed
|
|
427
|
-
await idempotentIngestion('inventory.csv', state, kv);
|
|
428
|
-
await idempotentIngestion('inventory.csv', state, kv); // ✅ Skips, no duplicates
|
|
429
|
-
await idempotentIngestion('inventory.csv', state, kv); // ✅ Skips, no duplicates
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
### Why This is Idempotent
|
|
433
|
-
|
|
434
|
-
| Step | Idempotency Guarantee |
|
|
435
|
-
| -------------------- | ---------------------------------------------- |
|
|
436
|
-
| **Lock acquisition** | If locked, skip (safe to retry) |
|
|
437
|
-
| **Processed check** | If processed, skip (safe to retry) |
|
|
438
|
-
| **Batch API UPSERT** | UPSERT is idempotent (same data = same result) |
|
|
439
|
-
| **State update** | Atomic operation (safe to retry) |
|
|
440
|
-
|
|
441
|
-
---
|
|
442
|
-
|
|
443
|
-
## Sync State for Incremental Processing
|
|
444
|
-
|
|
445
|
-
Track the last processed timestamp to enable incremental updates:
|
|
446
|
-
|
|
447
|
-
### Get and Update Sync State
|
|
448
|
-
|
|
449
|
-
```typescript
|
|
450
|
-
import { StateService, SyncState } from '@fluentcommerce/fc-connect-sdk';
|
|
451
|
-
|
|
452
|
-
async function incrementalSync(
|
|
453
|
-
state: StateService,
|
|
454
|
-
kv: KVStore,
|
|
455
|
-
workflowId: string
|
|
456
|
-
): Promise<void> {
|
|
457
|
-
// Get last sync state
|
|
458
|
-
const syncState: SyncState = await state.getSyncState(kv, workflowId);
|
|
459
|
-
|
|
460
|
-
if (syncState.isInitialized) {
|
|
461
|
-
console.log('Last sync:', syncState.lastProcessedTimestamp);
|
|
462
|
-
console.log('Last file:', syncState.lastProcessedFile);
|
|
463
|
-
} else {
|
|
464
|
-
console.log('First sync (no prior state)');
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Get files modified after last sync
|
|
468
|
-
const newFiles = await getFilesModifiedAfter(syncState.lastProcessedTimestamp);
|
|
469
|
-
|
|
470
|
-
if (newFiles.length === 0) {
|
|
471
|
-
console.log('No new files to process');
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const processedFiles = [];
|
|
476
|
-
|
|
477
|
-
for (const file of newFiles) {
|
|
478
|
-
const records = await processFile(file.name);
|
|
479
|
-
await sendToBatchAPI(records);
|
|
480
|
-
|
|
481
|
-
processedFiles.push({
|
|
482
|
-
fileName: file.name,
|
|
483
|
-
lastModified: file.lastModified,
|
|
484
|
-
recordCount: records.length,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Update sync state with all processed files
|
|
489
|
-
await state.updateSyncState(kv, processedFiles, workflowId);
|
|
490
|
-
|
|
491
|
-
console.log(`Synced ${processedFiles.length} new files`);
|
|
492
|
-
}
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### Daily Job Management with State
|
|
496
|
-
|
|
497
|
-
Reuse a single job for all batches processed in a day:
|
|
498
|
-
|
|
499
|
-
```typescript
|
|
500
|
-
import { StateService, DailyJob } from '@fluentcommerce/fc-connect-sdk';
|
|
501
|
-
|
|
502
|
-
async function processWithDailyJob(
|
|
503
|
-
files: string[],
|
|
504
|
-
state: StateService,
|
|
505
|
-
kv: KVStore,
|
|
506
|
-
client: FluentClient
|
|
507
|
-
): Promise<void> {
|
|
508
|
-
const workflowId = 'inventory-ingestion';
|
|
509
|
-
|
|
510
|
-
// Check for existing daily job
|
|
511
|
-
let dailyJob: DailyJob | null = await state.getDailyJob(kv, workflowId);
|
|
512
|
-
|
|
513
|
-
if (!dailyJob || new Date(dailyJob.expiresAt) < new Date()) {
|
|
514
|
-
// Create new daily job
|
|
515
|
-
const job = await client.createJob({
|
|
516
|
-
name: `Daily Inventory Sync - ${new Date().toISOString().split('T')[0]}`,
|
|
517
|
-
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
// Store daily job (expires in 24 hours)
|
|
521
|
-
await state.setDailyJob(kv, workflowId, job.id, 24);
|
|
522
|
-
|
|
523
|
-
dailyJob = {
|
|
524
|
-
jobId: job.id,
|
|
525
|
-
createdAt: new Date().toISOString(),
|
|
526
|
-
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
console.log(`Created daily job: ${job.id}`);
|
|
530
|
-
} else {
|
|
531
|
-
console.log(`Reusing daily job: ${dailyJob.jobId}`);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Process all files with same job
|
|
535
|
-
for (const fileName of files) {
|
|
536
|
-
if (await state.isFileProcessed(kv, fileName, workflowId)) {
|
|
537
|
-
continue;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const records = await processFile(fileName);
|
|
541
|
-
|
|
542
|
-
await client.sendBatch(dailyJob.jobId, {
|
|
543
|
-
action: 'UPSERT',
|
|
544
|
-
entityType: 'INVENTORY',
|
|
545
|
-
source: 'DAILY_JOB_REUSE',
|
|
546
|
-
event: 'INVENTORY_UPDATE',
|
|
547
|
-
entities: records,
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
await state.updateSyncState(
|
|
551
|
-
kv,
|
|
552
|
-
[
|
|
553
|
-
{
|
|
554
|
-
fileName,
|
|
555
|
-
lastModified: new Date().toISOString(),
|
|
556
|
-
recordCount: records.length,
|
|
557
|
-
},
|
|
558
|
-
],
|
|
559
|
-
workflowId
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
---
|
|
566
|
-
|
|
567
|
-
## State Storage Adapters
|
|
568
|
-
|
|
569
|
-
### Versori Platform (Built-in KV)
|
|
570
|
-
|
|
571
|
-
```typescript
|
|
572
|
-
import { StateService } from '@fluentcommerce/fc-connect-sdk';
|
|
573
|
-
import { workflow } from '@versori/run';
|
|
574
|
-
|
|
575
|
-
export const inventoryIngestion = workflow('inventory-ingestion', async ctx => {
|
|
576
|
-
const { log, openKv } = ctx;
|
|
577
|
-
|
|
578
|
-
// Open Versori KV store
|
|
579
|
-
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
580
|
-
|
|
581
|
-
// Create logger and state service
|
|
582
|
-
const logger = toStructuredLogger(log, {
|
|
583
|
-
service: 'inventory-ingestion'
|
|
584
|
-
});
|
|
585
|
-
const stateService = new StateService(logger);
|
|
586
|
-
|
|
587
|
-
// Use state methods with kvAdapter parameter
|
|
588
|
-
const isProcessed = await stateService.isFileProcessed(kvAdapter, 'inventory.csv');
|
|
589
|
-
|
|
590
|
-
if (!isProcessed) {
|
|
591
|
-
// Process file
|
|
592
|
-
await processFile('inventory.csv');
|
|
593
|
-
|
|
594
|
-
// Mark as processed
|
|
595
|
-
await stateService.updateSyncState(kvAdapter, [
|
|
596
|
-
{
|
|
597
|
-
fileName: 'inventory.csv',
|
|
598
|
-
lastModified: new Date().toISOString(),
|
|
599
|
-
recordCount: 1000,
|
|
600
|
-
},
|
|
601
|
-
]);
|
|
602
|
-
}
|
|
603
|
-
});
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
### Node.js Standalone (Custom KV)
|
|
607
|
-
|
|
608
|
-
```typescript
|
|
609
|
-
import { StateService, KVStore } from '@fluentcommerce/fc-connect-sdk';
|
|
610
|
-
|
|
611
|
-
// Implement KVStore interface with Redis, DynamoDB, etc.
|
|
612
|
-
class RedisKVStore implements KVStore {
|
|
613
|
-
constructor(private redis: RedisClient) {}
|
|
614
|
-
|
|
615
|
-
async get(keys: string[]): Promise<{ value: unknown } | null> {
|
|
616
|
-
const key = keys.join(':');
|
|
617
|
-
const value = await this.redis.get(key);
|
|
618
|
-
return value ? { value: JSON.parse(value) } : null;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
async set(keys: string[], value: unknown): Promise<void> {
|
|
622
|
-
const key = keys.join(':');
|
|
623
|
-
await this.redis.set(key, JSON.stringify(value));
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
async delete(keys: string[]): Promise<void> {
|
|
627
|
-
const key = keys.join(':');
|
|
628
|
-
await this.redis.del(key);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
atomic() {
|
|
632
|
-
// Implement atomic operations with Redis transactions
|
|
633
|
-
const operations: Array<{ type: string; key: string[]; value?: unknown }> = [];
|
|
634
|
-
|
|
635
|
-
return {
|
|
636
|
-
check: (entries: Array<{ key: string[]; versionstamp: string | null }>) => {
|
|
637
|
-
// Redis WATCH for optimistic locking
|
|
638
|
-
entries.forEach(e => this.redis.watch(e.key.join(':')));
|
|
639
|
-
},
|
|
640
|
-
set: (key: string[], value: unknown) => {
|
|
641
|
-
operations.push({ type: 'set', key, value });
|
|
642
|
-
},
|
|
643
|
-
commit: async (): Promise<boolean> => {
|
|
644
|
-
const multi = this.redis.multi();
|
|
645
|
-
operations.forEach(op => {
|
|
646
|
-
if (op.type === 'set') {
|
|
647
|
-
multi.set(op.key.join(':'), JSON.stringify(op.value));
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
const results = await multi.exec();
|
|
651
|
-
return results !== null;
|
|
652
|
-
},
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Usage
|
|
658
|
-
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
659
|
-
service: 'inventory-ingestion',
|
|
660
|
-
correlationId: generateCorrelationId()
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
const redisKV = new RedisKVStore(redisClient);
|
|
664
|
-
const stateService = new StateService(logger);
|
|
665
|
-
|
|
666
|
-
await stateService.isFileProcessed(redisKV, 'inventory.csv');
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
---
|
|
670
|
-
|
|
671
|
-
## Complete Production Example
|
|
672
|
-
|
|
673
|
-
Full ingestion workflow with state management, locking, and error handling:
|
|
674
|
-
|
|
675
|
-
```typescript
|
|
676
|
-
import {
|
|
677
|
-
createClient,
|
|
678
|
-
FluentClient,
|
|
679
|
-
StateService,
|
|
680
|
-
KVStore,
|
|
681
|
-
S3DataSource,
|
|
682
|
-
CSVParserService,
|
|
683
|
-
UniversalMapper,
|
|
684
|
-
BatchAction,
|
|
685
|
-
EntityType,
|
|
686
|
-
} from '@fluentcommerce/fc-connect-sdk';
|
|
687
|
-
|
|
688
|
-
interface IngestionConfig {
|
|
689
|
-
s3: {
|
|
690
|
-
bucket: string;
|
|
691
|
-
prefix: string;
|
|
692
|
-
};
|
|
693
|
-
fluent: {
|
|
694
|
-
baseUrl: string;
|
|
695
|
-
clientId: string;
|
|
696
|
-
clientSecret: string;
|
|
697
|
-
retailerId: string;
|
|
698
|
-
};
|
|
699
|
-
workflow: {
|
|
700
|
-
id: string;
|
|
701
|
-
lockTimeoutMinutes: number;
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
async function productionIngestionWithState(
|
|
706
|
-
config: IngestionConfig,
|
|
707
|
-
kv: KVStore
|
|
708
|
-
): Promise<{
|
|
709
|
-
success: boolean;
|
|
710
|
-
filesProcessed: number;
|
|
711
|
-
filesSkipped: number;
|
|
712
|
-
errors: string[];
|
|
713
|
-
}> {
|
|
714
|
-
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
715
|
-
service: 'inventory-ingestion',
|
|
716
|
-
correlationId: generateCorrelationId()
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
const stateService = new StateService(logger);
|
|
720
|
-
const s3 = new S3DataSource(
|
|
721
|
-
{
|
|
722
|
-
type: 'S3_CSV',
|
|
723
|
-
connectionId: 's3-state-example',
|
|
724
|
-
name: 'S3 State Example',
|
|
725
|
-
s3Config: {
|
|
726
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
727
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
728
|
-
region: process.env.AWS_REGION!,
|
|
729
|
-
bucket: 'inventory-bucket',
|
|
730
|
-
},
|
|
731
|
-
},
|
|
732
|
-
logger
|
|
733
|
-
);
|
|
734
|
-
|
|
735
|
-
const client = await createClient({
|
|
736
|
-
config: {
|
|
737
|
-
baseUrl: config.fluent.baseUrl,
|
|
738
|
-
clientId: config.fluent.clientId,
|
|
739
|
-
clientSecret: config.fluent.clientSecret,
|
|
740
|
-
retailerId: config.fluent.retailerId,
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
const parser = new CSVParserService();
|
|
745
|
-
const mapper = new UniversalMapper({
|
|
746
|
-
fields: {
|
|
747
|
-
ref: {
|
|
748
|
-
source: 'sku',
|
|
749
|
-
required: true,
|
|
750
|
-
comment: '✅ InventoryPositionInput.ref (String! required)',
|
|
751
|
-
},
|
|
752
|
-
productRef: {
|
|
753
|
-
source: 'sku',
|
|
754
|
-
required: true,
|
|
755
|
-
comment: '✅ InventoryPositionInput.productRef (String! required)',
|
|
756
|
-
},
|
|
757
|
-
locationRef: {
|
|
758
|
-
source: 'warehouse',
|
|
759
|
-
required: true,
|
|
760
|
-
comment: '✅ InventoryPositionInput.locationRef (String! required)',
|
|
761
|
-
},
|
|
762
|
-
qty: {
|
|
763
|
-
source: 'quantity',
|
|
764
|
-
resolver: 'sdk.parseInt',
|
|
765
|
-
required: true,
|
|
766
|
-
comment: '✅ InventoryPositionInput.qty (Int! required)',
|
|
767
|
-
},
|
|
768
|
-
status: {
|
|
769
|
-
source: 'status',
|
|
770
|
-
resolver: 'sdk.uppercase',
|
|
771
|
-
defaultValue: 'AVAILABLE',
|
|
772
|
-
comment: '✅ InventoryPositionInput.status (String optional)',
|
|
773
|
-
},
|
|
774
|
-
},
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
let filesProcessed = 0;
|
|
778
|
-
let filesSkipped = 0;
|
|
779
|
-
const errors: string[] = [];
|
|
780
|
-
|
|
781
|
-
try {
|
|
782
|
-
// List files from S3
|
|
783
|
-
const files = await s3.listFiles({ prefix: config.s3.prefix });
|
|
784
|
-
console.log(`Found ${files.length} files in S3`);
|
|
785
|
-
|
|
786
|
-
// Get or create daily job
|
|
787
|
-
let dailyJob = await state.getDailyJob(kv, config.workflow.id);
|
|
788
|
-
|
|
789
|
-
if (!dailyJob) {
|
|
790
|
-
const job = await client.createJob({
|
|
791
|
-
name: `Daily Sync - ${new Date().toISOString().split('T')[0]}`,
|
|
792
|
-
retailerId: config.fluent.retailerId,
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
await state.setDailyJob(kv, config.workflow.id, job.id, 24);
|
|
796
|
-
dailyJob = {
|
|
797
|
-
jobId: job.id,
|
|
798
|
-
createdAt: new Date().toISOString(),
|
|
799
|
-
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
console.log(`Using job: ${dailyJob.jobId}`);
|
|
804
|
-
|
|
805
|
-
for (const file of files) {
|
|
806
|
-
const lockName = `file:${file.path}`;
|
|
807
|
-
|
|
808
|
-
try {
|
|
809
|
-
// 1. Acquire lock
|
|
810
|
-
const lockAcquired = await state.acquireLock(
|
|
811
|
-
lockName,
|
|
812
|
-
kv,
|
|
813
|
-
config.workflow.lockTimeoutMinutes
|
|
814
|
-
);
|
|
815
|
-
|
|
816
|
-
if (!lockAcquired) {
|
|
817
|
-
console.log(`File locked by another process: ${file.path}`);
|
|
818
|
-
filesSkipped++;
|
|
819
|
-
continue;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// 2. Check if already processed
|
|
823
|
-
if (await state.isFileProcessed(kv, file.path, config.workflow.id)) {
|
|
824
|
-
console.log(`File already processed: ${file.path}`);
|
|
825
|
-
filesSkipped++;
|
|
826
|
-
await state.releaseLock(lockName, kv);
|
|
827
|
-
continue;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// 3. Download and parse file
|
|
831
|
-
const fileContent = await s3.downloadFile(file.path);
|
|
832
|
-
const records = await parser.parse(fileContent);
|
|
833
|
-
|
|
834
|
-
console.log(`Parsed ${records.length} records from ${file.path}`);
|
|
835
|
-
|
|
836
|
-
// 4. Transform data
|
|
837
|
-
const mappingResult = await mapper.map(records);
|
|
838
|
-
|
|
839
|
-
if (!mappingResult.success) {
|
|
840
|
-
throw new Error(`Mapping failed: ${JSON.stringify(mappingResult.errors)}`);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// 5. Send to Batch API
|
|
844
|
-
const batch = await client.sendBatch(dailyJob.jobId, {
|
|
845
|
-
action: 'UPSERT',
|
|
846
|
-
entityType: 'INVENTORY',
|
|
847
|
-
source: 'COMPLETE_WORKFLOW',
|
|
848
|
-
event: 'INVENTORY_UPDATE',
|
|
849
|
-
entities: mappingResult.data,
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
console.log(`Batch sent: ${batch.id}`);
|
|
853
|
-
|
|
854
|
-
// 6. Mark as processed
|
|
855
|
-
await state.updateSyncState(
|
|
856
|
-
kv,
|
|
857
|
-
[
|
|
858
|
-
{
|
|
859
|
-
fileName: file.path,
|
|
860
|
-
lastModified: file.lastModified || new Date().toISOString(),
|
|
861
|
-
recordCount: mappingResult.data.length,
|
|
862
|
-
processedAt: new Date().toISOString(),
|
|
863
|
-
},
|
|
864
|
-
],
|
|
865
|
-
config.workflow.id
|
|
866
|
-
);
|
|
867
|
-
|
|
868
|
-
filesProcessed++;
|
|
869
|
-
|
|
870
|
-
// 7. Release lock
|
|
871
|
-
await state.releaseLock(lockName, kv);
|
|
872
|
-
} catch (error) {
|
|
873
|
-
const errorMsg = `Failed to process ${file.path}: ${(error as Error).message}`;
|
|
874
|
-
console.error(errorMsg);
|
|
875
|
-
errors.push(errorMsg);
|
|
876
|
-
|
|
877
|
-
// Release lock on error
|
|
878
|
-
await state.releaseLock(lockName, kv);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
return {
|
|
883
|
-
success: errors.length === 0,
|
|
884
|
-
filesProcessed,
|
|
885
|
-
filesSkipped,
|
|
886
|
-
errors,
|
|
887
|
-
};
|
|
888
|
-
} catch (error) {
|
|
889
|
-
console.error('Ingestion failed:', error);
|
|
890
|
-
throw error;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Usage
|
|
895
|
-
const result = await productionIngestionWithState(
|
|
896
|
-
{
|
|
897
|
-
s3: {
|
|
898
|
-
bucket: 'inventory-bucket',
|
|
899
|
-
prefix: 'data/',
|
|
900
|
-
},
|
|
901
|
-
fluent: {
|
|
902
|
-
baseUrl: process.env.FLUENT_BASE_URL!,
|
|
903
|
-
clientId: process.env.FLUENT_CLIENT_ID!,
|
|
904
|
-
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
|
|
905
|
-
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
906
|
-
},
|
|
907
|
-
workflow: {
|
|
908
|
-
id: 'inventory-ingestion',
|
|
909
|
-
lockTimeoutMinutes: 15,
|
|
910
|
-
},
|
|
911
|
-
},
|
|
912
|
-
kv
|
|
913
|
-
);
|
|
914
|
-
|
|
915
|
-
console.log('Ingestion result:', result);
|
|
916
|
-
// Output:
|
|
917
|
-
// {
|
|
918
|
-
// success: true,
|
|
919
|
-
// filesProcessed: 10,
|
|
920
|
-
// filesSkipped: 5,
|
|
921
|
-
// errors: []
|
|
922
|
-
// }
|
|
923
|
-
```
|
|
924
|
-
|
|
925
|
-
---
|
|
926
|
-
|
|
927
|
-
## Troubleshooting
|
|
928
|
-
|
|
929
|
-
### Problem: Files Reprocessed Every Run
|
|
930
|
-
|
|
931
|
-
**Symptoms:**
|
|
932
|
-
|
|
933
|
-
- Same files processed repeatedly
|
|
934
|
-
- Duplicate inventory positions
|
|
935
|
-
|
|
936
|
-
**Solution:**
|
|
937
|
-
|
|
938
|
-
```typescript
|
|
939
|
-
// Check if state is being updated correctly
|
|
940
|
-
const syncState = await state.getSyncState(kv, workflowId);
|
|
941
|
-
console.log('Sync state:', syncState);
|
|
942
|
-
|
|
943
|
-
// Verify file is marked as processed
|
|
944
|
-
const isProcessed = await state.isFileProcessed(kv, fileName, workflowId);
|
|
945
|
-
console.log(`File ${fileName} processed:`, isProcessed);
|
|
946
|
-
|
|
947
|
-
// Ensure updateSyncState is called AFTER successful processing
|
|
948
|
-
await state.updateSyncState(kv, [{ fileName, lastModified, recordCount }], workflowId);
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
### Problem: Lock Never Released
|
|
952
|
-
|
|
953
|
-
**Symptoms:**
|
|
954
|
-
|
|
955
|
-
- Files stuck in "locked" state
|
|
956
|
-
- Processing stops
|
|
957
|
-
|
|
958
|
-
**Solution:**
|
|
959
|
-
|
|
960
|
-
```typescript
|
|
961
|
-
// ALWAYS use try-finally to release locks
|
|
962
|
-
try {
|
|
963
|
-
const lockAcquired = await state.acquireLock(lockName, kv, 15);
|
|
964
|
-
if (lockAcquired) {
|
|
965
|
-
await processFile();
|
|
966
|
-
}
|
|
967
|
-
} finally {
|
|
968
|
-
// ✅ CRITICAL: Release lock even if processing fails
|
|
969
|
-
await state.releaseLock(lockName, kv);
|
|
970
|
-
}
|
|
971
|
-
```
|
|
972
|
-
|
|
973
|
-
### Problem: Stale Locks Blocking Processing
|
|
974
|
-
|
|
975
|
-
**Symptoms:**
|
|
976
|
-
|
|
977
|
-
- Locks from crashed processes
|
|
978
|
-
- Processing stuck indefinitely
|
|
979
|
-
|
|
980
|
-
**Solution:**
|
|
981
|
-
|
|
982
|
-
```typescript
|
|
983
|
-
// SDK automatically overrides stale locks based on timeout
|
|
984
|
-
// If lock expired (past expiresAt), next acquireLock succeeds
|
|
985
|
-
|
|
986
|
-
// To manually clear stale locks (if needed):
|
|
987
|
-
await state.releaseLock(lockName, kv); // Force release
|
|
988
|
-
```
|
|
989
|
-
|
|
990
|
-
### Problem: State Lost Between Runs
|
|
991
|
-
|
|
992
|
-
**Symptoms:**
|
|
993
|
-
|
|
994
|
-
- First-time sync every run
|
|
995
|
-
- `isInitialized: false` always
|
|
996
|
-
|
|
997
|
-
**Solution:**
|
|
998
|
-
|
|
999
|
-
```typescript
|
|
1000
|
-
// Verify KV store persistence
|
|
1001
|
-
const testKey = ['test', 'persistence'];
|
|
1002
|
-
await kv.set(testKey, { value: 'test' });
|
|
1003
|
-
const retrieved = await kv.get(testKey);
|
|
1004
|
-
console.log('KV persistence test:', retrieved);
|
|
1005
|
-
|
|
1006
|
-
// Ensure workflowId is consistent
|
|
1007
|
-
const WORKFLOW_ID = 'inventory-ingestion'; // ✅ Use constant
|
|
1008
|
-
await state.updateSyncState(kv, files, WORKFLOW_ID);
|
|
1009
|
-
```
|
|
1010
|
-
|
|
1011
|
-
---
|
|
1012
|
-
|
|
1013
|
-
## Key Takeaways
|
|
1014
|
-
|
|
1015
|
-
- 🎯 **Always use state management** - Prevents duplicates and enables idempotency
|
|
1016
|
-
- 🎯 **Acquire locks before processing** - Prevents concurrent conflicts
|
|
1017
|
-
- 🎯 **Release locks in finally blocks** - Ensures cleanup even on errors
|
|
1018
|
-
- 🎯 **Track file metadata** - Rich metadata enables auditing and troubleshooting
|
|
1019
|
-
- 🎯 **Use workflow IDs** - Scope state to specific workflows
|
|
1020
|
-
- 🎯 **Handle stale locks** - SDK automatically overrides expired locks
|
|
1021
|
-
|
|
1022
|
-
---
|
|
1023
|
-
|
|
1024
|
-
## Next Steps
|
|
1025
|
-
|
|
1026
|
-
Continue to [Module 8: Performance Optimization](./02-core-guides-ingestion-08-performance-optimization.md) to learn how to optimize batch sizing, parallel processing, and job strategies for large-scale ingestion.
|
|
1027
|
-
|
|
1028
|
-
---
|
|
1029
|
-
|
|
1030
|
-
[← Previous: Batch API](./02-core-guides-ingestion-06-batch-api.md) | [Back to Guide](../ingestion-readme.md) | [Next: Performance Optimization →](./02-core-guides-ingestion-08-performance-optimization.md)
|
|
1031
|
-
|
|
1032
|
-
## Related Documentation
|
|
1033
|
-
|
|
1034
|
-
- [StateService API](../../api-reference/modules/api-reference-05-services.md#state-service) - Complete API documentation
|
|
1035
|
-
- [Versori KV Integration](../../../04-REFERENCE/platforms/versori/#key-value-storage) - Platform-specific KV usage
|
|
1036
|
-
- [Error Handling Guide](../../../03-PATTERN-GUIDES/error-handling/error-handling-readme.md) - Retry patterns and error recovery
|
|
1037
|
-
- [Best Practices](./02-core-guides-ingestion-09-best-practices.md) - Production patterns and monitoring
|
|
1
|
+
# Module 7: State Management
|
|
2
|
+
|
|
3
|
+
[← Back to Ingestion Guide](../ingestion-readme.md)
|
|
4
|
+
|
|
5
|
+
**Module 7 of 9** | **Level**: Advanced | **Time**: 20 minutes
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This module covers state management for ingestion workflows, focusing on duplicate prevention, file tracking, and distributed locking. Learn how to build idempotent, production-ready ingestion pipelines using the SDK's `StateService`, `JobTracker`, and `VersoriFileTracker`.
|
|
12
|
+
|
|
13
|
+
## Learning Objectives
|
|
14
|
+
|
|
15
|
+
By the end of this module, you will:
|
|
16
|
+
|
|
17
|
+
- ✅ Understand why state management is critical for ingestion workflows
|
|
18
|
+
- ✅ Prevent duplicate file processing with file tracking
|
|
19
|
+
- ✅ Use distributed locking for concurrent safety
|
|
20
|
+
- ✅ Implement idempotent operations that can be safely retried
|
|
21
|
+
- ✅ Store and retrieve sync state for incremental processing
|
|
22
|
+
- ✅ Handle state storage in Versori and standalone environments
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Why State Management is Critical
|
|
27
|
+
|
|
28
|
+
### The Duplicate Processing Problem
|
|
29
|
+
|
|
30
|
+
**Without state management:**
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// ❌ WRONG - Reprocessing files creates duplicates
|
|
34
|
+
async function ingestFromS3Naive() {
|
|
35
|
+
const files = await s3.listFiles({ prefix: 'data/' });
|
|
36
|
+
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const data = await s3.downloadFile(file.path);
|
|
39
|
+
const records = await parser.parse(data);
|
|
40
|
+
|
|
41
|
+
// Problem: Every run processes ALL files again
|
|
42
|
+
await sendToBatchAPI(records);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Results:
|
|
47
|
+
// - Run 1: Processes 100 files ✅
|
|
48
|
+
// - Run 2: Processes same 100 files again ❌ DUPLICATES
|
|
49
|
+
// - Run 3: Processes same 100 files again ❌ MORE DUPLICATES
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**With state management:**
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// ✅ CORRECT - Only process new files
|
|
56
|
+
async function ingestFromS3WithState(state: StateService, kv: KVStore) {
|
|
57
|
+
const files = await s3.listFiles({ prefix: 'data/' });
|
|
58
|
+
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
// Check if already processed
|
|
61
|
+
if (await state.isFileProcessed(kv, file.path)) {
|
|
62
|
+
console.log(`Skipping processed file: ${file.path}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await s3.downloadFile(file.path);
|
|
67
|
+
const records = await parser.parse(data);
|
|
68
|
+
|
|
69
|
+
await sendToBatchAPI(records);
|
|
70
|
+
|
|
71
|
+
// Mark as processed
|
|
72
|
+
await state.updateSyncState(kv, [
|
|
73
|
+
{
|
|
74
|
+
fileName: file.path,
|
|
75
|
+
lastModified: file.lastModified,
|
|
76
|
+
recordCount: records.length,
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Results:
|
|
83
|
+
// - Run 1: Processes 100 new files ✅
|
|
84
|
+
// - Run 2: Skips 100 processed files, processes 10 new files ✅
|
|
85
|
+
// - Run 3: Skips 110 processed files, processes 5 new files ✅
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Business Impact
|
|
89
|
+
|
|
90
|
+
| Scenario | Without State | With State |
|
|
91
|
+
| ------------------------ | --------------------------- | ----------------------------------- |
|
|
92
|
+
| **Daily sync rerun** | Duplicates entire inventory | Processes only new files |
|
|
93
|
+
| **Retry failed batch** | Reprocesses all batches | Retries only failed batch |
|
|
94
|
+
| **Concurrent execution** | Race conditions, duplicates | Distributed locks prevent conflicts |
|
|
95
|
+
| **Incremental updates** | Must track externally | Automatic high-watermark tracking |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## StateService Overview
|
|
100
|
+
|
|
101
|
+
The `StateService` provides comprehensive state management with three core capabilities:
|
|
102
|
+
|
|
103
|
+
### 1. File Processing Tracking
|
|
104
|
+
|
|
105
|
+
Track which files have been processed to prevent duplicates.
|
|
106
|
+
|
|
107
|
+
### 2. Distributed Locking
|
|
108
|
+
|
|
109
|
+
Ensure only one process handles a file at a time (concurrent safety).
|
|
110
|
+
|
|
111
|
+
### 3. Sync State Management
|
|
112
|
+
|
|
113
|
+
Store high-watermark timestamps for incremental processing.
|
|
114
|
+
|
|
115
|
+
### Architecture
|
|
116
|
+
|
|
117
|
+
```mermaid
|
|
118
|
+
graph TD
|
|
119
|
+
A[Ingestion Workflow] --> B{Check State}
|
|
120
|
+
B --> C[StateService]
|
|
121
|
+
C --> D[KV Store]
|
|
122
|
+
|
|
123
|
+
B -->|File Processed| E[Skip File]
|
|
124
|
+
B -->|New File| F[Acquire Lock]
|
|
125
|
+
|
|
126
|
+
F -->|Lock Acquired| G[Process File]
|
|
127
|
+
F -->|Lock Held| H[Wait/Skip]
|
|
128
|
+
|
|
129
|
+
G --> I[Send to Batch API]
|
|
130
|
+
I --> J[Mark as Processed]
|
|
131
|
+
J --> K[Release Lock]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## File Tracking for Duplicate Prevention
|
|
137
|
+
|
|
138
|
+
### File Tracking with StateService (Recommended)
|
|
139
|
+
|
|
140
|
+
**Use `StateService`** for production workflows with sync state and metadata tracking:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { StateService, VersoriKVAdapter } from '@fluentcommerce/fc-connect-sdk';
|
|
144
|
+
// ✅ CORRECT: Access openKv from Versori context
|
|
145
|
+
// import { openKv } from '@versori/run'; // ❌ WRONG - Not a direct export
|
|
146
|
+
|
|
147
|
+
async function ingestWithFileTracking(
|
|
148
|
+
files: string[],
|
|
149
|
+
ctx: any
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
const { log, openKv } = ctx;
|
|
152
|
+
const logger = toStructuredLogger(log, {
|
|
153
|
+
service: 'inventory-ingestion'
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const stateService = new StateService(logger);
|
|
157
|
+
const kv = new VersoriKVAdapter(openKv(':project:'));
|
|
158
|
+
let processedCount = 0;
|
|
159
|
+
let skippedCount = 0;
|
|
160
|
+
|
|
161
|
+
for (const fileName of files) {
|
|
162
|
+
// Check if file already processed
|
|
163
|
+
if (await state.isFileProcessed(kv, fileName)) {
|
|
164
|
+
log.info(`Skipping processed file: ${fileName}`);
|
|
165
|
+
skippedCount++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Process file
|
|
170
|
+
log.info(`Processing new file: ${fileName}`);
|
|
171
|
+
const records = await processFile(fileName);
|
|
172
|
+
await sendToBatchAPI(records);
|
|
173
|
+
|
|
174
|
+
// Mark as processed with metadata
|
|
175
|
+
await state.updateSyncState(kv, [
|
|
176
|
+
{
|
|
177
|
+
fileName,
|
|
178
|
+
lastModified: new Date().toISOString(),
|
|
179
|
+
recordCount: records.length,
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
processedCount++;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
log.info(`Processed: ${processedCount}, Skipped: ${skippedCount}`);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Tracking with Metadata
|
|
191
|
+
|
|
192
|
+
Store rich metadata for auditing and troubleshooting:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
interface FileProcessingMetadata {
|
|
196
|
+
fileName: string;
|
|
197
|
+
lastModified: string;
|
|
198
|
+
recordCount: number;
|
|
199
|
+
processedAt: string;
|
|
200
|
+
jobId?: string;
|
|
201
|
+
batchIds?: string[];
|
|
202
|
+
fileSize?: number;
|
|
203
|
+
checksumMD5?: string;
|
|
204
|
+
source?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function trackFileWithMetadata(
|
|
208
|
+
stateService: StateService,
|
|
209
|
+
kv: KVStore,
|
|
210
|
+
fileName: string,
|
|
211
|
+
metadata: FileProcessingMetadata
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
await stateService.updateSyncState(kv, [metadata]);
|
|
214
|
+
|
|
215
|
+
console.log('File tracked:', {
|
|
216
|
+
file: fileName,
|
|
217
|
+
records: metadata.recordCount,
|
|
218
|
+
jobId: metadata.jobId,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Usage
|
|
223
|
+
await trackFileWithMetadata(stateService, kv, 'inventory-2025-01-19.csv', {
|
|
224
|
+
fileName: 'inventory-2025-01-19.csv',
|
|
225
|
+
lastModified: '2025-01-19T08:00:00Z',
|
|
226
|
+
recordCount: 5000,
|
|
227
|
+
processedAt: new Date().toISOString(),
|
|
228
|
+
jobId: 'JOB-12345',
|
|
229
|
+
batchIds: ['BATCH-001', 'BATCH-002'],
|
|
230
|
+
fileSize: 1024000,
|
|
231
|
+
checksumMD5: 'a1b2c3d4e5f6',
|
|
232
|
+
source: 'S3',
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Distributed Locking for Concurrent Safety
|
|
239
|
+
|
|
240
|
+
### The Concurrency Problem
|
|
241
|
+
|
|
242
|
+
**Without locking:**
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// ❌ WRONG - Race condition when multiple processes run
|
|
246
|
+
// Process A: Checks file not processed → Starts processing
|
|
247
|
+
// Process B: Checks file not processed → Starts processing
|
|
248
|
+
// Result: File processed twice simultaneously
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**With locking:**
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// ✅ CORRECT - Only one process acquires lock
|
|
255
|
+
// Process A: Acquires lock → Processes file → Releases lock
|
|
256
|
+
// Process B: Lock held → Waits or skips → Tries later
|
|
257
|
+
// Result: File processed exactly once
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Acquire and Release Lock Pattern
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { StateService, KVStore } from '@fluentcommerce/fc-connect-sdk';
|
|
264
|
+
|
|
265
|
+
async function processFileWithLock(
|
|
266
|
+
fileName: string,
|
|
267
|
+
state: StateService,
|
|
268
|
+
kv: KVStore
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
const lockName = `file:${fileName}`;
|
|
271
|
+
const lockTimeoutMinutes = 15;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
// Attempt to acquire lock
|
|
275
|
+
const lockAcquired = await state.acquireLock(lockName, kv, lockTimeoutMinutes);
|
|
276
|
+
|
|
277
|
+
if (!lockAcquired) {
|
|
278
|
+
console.log(`Lock held by another process, skipping: ${fileName}`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log(`Lock acquired for: ${fileName}`);
|
|
283
|
+
|
|
284
|
+
// Check if already processed (double-check after acquiring lock)
|
|
285
|
+
if (await state.isFileProcessed(kv, fileName)) {
|
|
286
|
+
console.log(`File already processed: ${fileName}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Process file
|
|
291
|
+
const records = await processFile(fileName);
|
|
292
|
+
await sendToBatchAPI(records);
|
|
293
|
+
|
|
294
|
+
// Mark as processed
|
|
295
|
+
await state.updateSyncState(kv, [
|
|
296
|
+
{
|
|
297
|
+
fileName,
|
|
298
|
+
lastModified: new Date().toISOString(),
|
|
299
|
+
recordCount: records.length,
|
|
300
|
+
},
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
console.log(`Successfully processed: ${fileName}`);
|
|
304
|
+
} finally {
|
|
305
|
+
// ALWAYS release lock in finally block
|
|
306
|
+
await state.releaseLock(lockName, kv);
|
|
307
|
+
console.log(`Lock released for: ${fileName}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Lock Timeout and Stale Lock Detection
|
|
313
|
+
|
|
314
|
+
The SDK automatically handles stale locks:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// Scenario: Process crashes while holding lock
|
|
318
|
+
// 1. Process A acquires lock with 15-minute timeout
|
|
319
|
+
// 2. Process A crashes at minute 10 (lock still held)
|
|
320
|
+
// 3. Minute 16: Lock expires automatically
|
|
321
|
+
// 4. Process B acquires stale lock (overrides expired lock)
|
|
322
|
+
|
|
323
|
+
async function processWithStaleLockHandling(
|
|
324
|
+
fileName: string,
|
|
325
|
+
state: StateService,
|
|
326
|
+
kv: KVStore
|
|
327
|
+
): Promise<void> {
|
|
328
|
+
const lockTimeoutMinutes = 15;
|
|
329
|
+
|
|
330
|
+
// SDK automatically detects and overrides stale locks
|
|
331
|
+
const lockAcquired = await state.acquireLock(`file:${fileName}`, kv, lockTimeoutMinutes);
|
|
332
|
+
|
|
333
|
+
if (!lockAcquired) {
|
|
334
|
+
// Lock is held by active process (not stale)
|
|
335
|
+
console.log('Lock actively held by another process');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Lock acquired (either fresh or stale was overridden)
|
|
340
|
+
try {
|
|
341
|
+
await processFile(fileName);
|
|
342
|
+
} finally {
|
|
343
|
+
await state.releaseLock(`file:${fileName}`, kv);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Idempotent Processing
|
|
351
|
+
|
|
352
|
+
**Idempotency** means operations can be safely repeated without changing the result beyond the initial application.
|
|
353
|
+
|
|
354
|
+
### Idempotent Ingestion Pattern
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
async function idempotentIngestion(
|
|
358
|
+
fileName: string,
|
|
359
|
+
state: StateService,
|
|
360
|
+
kv: KVStore
|
|
361
|
+
): Promise<{ success: boolean; message: string }> {
|
|
362
|
+
const lockName = `ingestion:${fileName}`;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
// 1. Acquire lock (prevents concurrent processing)
|
|
366
|
+
const lockAcquired = await state.acquireLock(lockName, kv, 15);
|
|
367
|
+
if (!lockAcquired) {
|
|
368
|
+
return {
|
|
369
|
+
success: true,
|
|
370
|
+
message: 'File locked by another process, will retry later',
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 2. Check if already processed (prevents duplicates)
|
|
375
|
+
if (await state.isFileProcessed(kv, fileName)) {
|
|
376
|
+
return {
|
|
377
|
+
success: true,
|
|
378
|
+
message: 'File already processed, skipping',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 3. Process file
|
|
383
|
+
const records = await processFile(fileName);
|
|
384
|
+
|
|
385
|
+
// 4. Send to Batch API (UPSERT is idempotent)
|
|
386
|
+
const job = await client.createJob({
|
|
387
|
+
name: `Import ${fileName}`,
|
|
388
|
+
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await client.sendBatch(job.id, {
|
|
392
|
+
action: 'UPSERT', // ✅ UPSERT is idempotent
|
|
393
|
+
entityType: 'INVENTORY',
|
|
394
|
+
source: 'STATE_MANAGEMENT_EXAMPLE',
|
|
395
|
+
event: 'INVENTORY_UPDATE',
|
|
396
|
+
entities: records,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// 5. Mark as processed (atomic operation)
|
|
400
|
+
await state.updateSyncState(kv, [
|
|
401
|
+
{
|
|
402
|
+
fileName,
|
|
403
|
+
lastModified: new Date().toISOString(),
|
|
404
|
+
recordCount: records.length,
|
|
405
|
+
processedAt: new Date().toISOString(),
|
|
406
|
+
},
|
|
407
|
+
]);
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
success: true,
|
|
411
|
+
message: `Processed ${records.length} records`,
|
|
412
|
+
};
|
|
413
|
+
} catch (error) {
|
|
414
|
+
// DO NOT mark as processed on error
|
|
415
|
+
console.error(`Processing failed for ${fileName}:`, error);
|
|
416
|
+
return {
|
|
417
|
+
success: false,
|
|
418
|
+
message: (error as Error).message,
|
|
419
|
+
};
|
|
420
|
+
} finally {
|
|
421
|
+
// Always release lock
|
|
422
|
+
await state.releaseLock(lockName, kv);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Safe to retry - will skip if already completed
|
|
427
|
+
await idempotentIngestion('inventory.csv', state, kv);
|
|
428
|
+
await idempotentIngestion('inventory.csv', state, kv); // ✅ Skips, no duplicates
|
|
429
|
+
await idempotentIngestion('inventory.csv', state, kv); // ✅ Skips, no duplicates
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Why This is Idempotent
|
|
433
|
+
|
|
434
|
+
| Step | Idempotency Guarantee |
|
|
435
|
+
| -------------------- | ---------------------------------------------- |
|
|
436
|
+
| **Lock acquisition** | If locked, skip (safe to retry) |
|
|
437
|
+
| **Processed check** | If processed, skip (safe to retry) |
|
|
438
|
+
| **Batch API UPSERT** | UPSERT is idempotent (same data = same result) |
|
|
439
|
+
| **State update** | Atomic operation (safe to retry) |
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Sync State for Incremental Processing
|
|
444
|
+
|
|
445
|
+
Track the last processed timestamp to enable incremental updates:
|
|
446
|
+
|
|
447
|
+
### Get and Update Sync State
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
import { StateService, SyncState } from '@fluentcommerce/fc-connect-sdk';
|
|
451
|
+
|
|
452
|
+
async function incrementalSync(
|
|
453
|
+
state: StateService,
|
|
454
|
+
kv: KVStore,
|
|
455
|
+
workflowId: string
|
|
456
|
+
): Promise<void> {
|
|
457
|
+
// Get last sync state
|
|
458
|
+
const syncState: SyncState = await state.getSyncState(kv, workflowId);
|
|
459
|
+
|
|
460
|
+
if (syncState.isInitialized) {
|
|
461
|
+
console.log('Last sync:', syncState.lastProcessedTimestamp);
|
|
462
|
+
console.log('Last file:', syncState.lastProcessedFile);
|
|
463
|
+
} else {
|
|
464
|
+
console.log('First sync (no prior state)');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Get files modified after last sync
|
|
468
|
+
const newFiles = await getFilesModifiedAfter(syncState.lastProcessedTimestamp);
|
|
469
|
+
|
|
470
|
+
if (newFiles.length === 0) {
|
|
471
|
+
console.log('No new files to process');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const processedFiles = [];
|
|
476
|
+
|
|
477
|
+
for (const file of newFiles) {
|
|
478
|
+
const records = await processFile(file.name);
|
|
479
|
+
await sendToBatchAPI(records);
|
|
480
|
+
|
|
481
|
+
processedFiles.push({
|
|
482
|
+
fileName: file.name,
|
|
483
|
+
lastModified: file.lastModified,
|
|
484
|
+
recordCount: records.length,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Update sync state with all processed files
|
|
489
|
+
await state.updateSyncState(kv, processedFiles, workflowId);
|
|
490
|
+
|
|
491
|
+
console.log(`Synced ${processedFiles.length} new files`);
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Daily Job Management with State
|
|
496
|
+
|
|
497
|
+
Reuse a single job for all batches processed in a day:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import { StateService, DailyJob } from '@fluentcommerce/fc-connect-sdk';
|
|
501
|
+
|
|
502
|
+
async function processWithDailyJob(
|
|
503
|
+
files: string[],
|
|
504
|
+
state: StateService,
|
|
505
|
+
kv: KVStore,
|
|
506
|
+
client: FluentClient
|
|
507
|
+
): Promise<void> {
|
|
508
|
+
const workflowId = 'inventory-ingestion';
|
|
509
|
+
|
|
510
|
+
// Check for existing daily job
|
|
511
|
+
let dailyJob: DailyJob | null = await state.getDailyJob(kv, workflowId);
|
|
512
|
+
|
|
513
|
+
if (!dailyJob || new Date(dailyJob.expiresAt) < new Date()) {
|
|
514
|
+
// Create new daily job
|
|
515
|
+
const job = await client.createJob({
|
|
516
|
+
name: `Daily Inventory Sync - ${new Date().toISOString().split('T')[0]}`,
|
|
517
|
+
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Store daily job (expires in 24 hours)
|
|
521
|
+
await state.setDailyJob(kv, workflowId, job.id, 24);
|
|
522
|
+
|
|
523
|
+
dailyJob = {
|
|
524
|
+
jobId: job.id,
|
|
525
|
+
createdAt: new Date().toISOString(),
|
|
526
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
console.log(`Created daily job: ${job.id}`);
|
|
530
|
+
} else {
|
|
531
|
+
console.log(`Reusing daily job: ${dailyJob.jobId}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Process all files with same job
|
|
535
|
+
for (const fileName of files) {
|
|
536
|
+
if (await state.isFileProcessed(kv, fileName, workflowId)) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const records = await processFile(fileName);
|
|
541
|
+
|
|
542
|
+
await client.sendBatch(dailyJob.jobId, {
|
|
543
|
+
action: 'UPSERT',
|
|
544
|
+
entityType: 'INVENTORY',
|
|
545
|
+
source: 'DAILY_JOB_REUSE',
|
|
546
|
+
event: 'INVENTORY_UPDATE',
|
|
547
|
+
entities: records,
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
await state.updateSyncState(
|
|
551
|
+
kv,
|
|
552
|
+
[
|
|
553
|
+
{
|
|
554
|
+
fileName,
|
|
555
|
+
lastModified: new Date().toISOString(),
|
|
556
|
+
recordCount: records.length,
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
workflowId
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## State Storage Adapters
|
|
568
|
+
|
|
569
|
+
### Versori Platform (Built-in KV)
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
import { StateService } from '@fluentcommerce/fc-connect-sdk';
|
|
573
|
+
import { workflow } from '@versori/run';
|
|
574
|
+
|
|
575
|
+
export const inventoryIngestion = workflow('inventory-ingestion', async ctx => {
|
|
576
|
+
const { log, openKv } = ctx;
|
|
577
|
+
|
|
578
|
+
// Open Versori KV store
|
|
579
|
+
const kvAdapter = new VersoriKVAdapter(openKv(':project:'));
|
|
580
|
+
|
|
581
|
+
// Create logger and state service
|
|
582
|
+
const logger = toStructuredLogger(log, {
|
|
583
|
+
service: 'inventory-ingestion'
|
|
584
|
+
});
|
|
585
|
+
const stateService = new StateService(logger);
|
|
586
|
+
|
|
587
|
+
// Use state methods with kvAdapter parameter
|
|
588
|
+
const isProcessed = await stateService.isFileProcessed(kvAdapter, 'inventory.csv');
|
|
589
|
+
|
|
590
|
+
if (!isProcessed) {
|
|
591
|
+
// Process file
|
|
592
|
+
await processFile('inventory.csv');
|
|
593
|
+
|
|
594
|
+
// Mark as processed
|
|
595
|
+
await stateService.updateSyncState(kvAdapter, [
|
|
596
|
+
{
|
|
597
|
+
fileName: 'inventory.csv',
|
|
598
|
+
lastModified: new Date().toISOString(),
|
|
599
|
+
recordCount: 1000,
|
|
600
|
+
},
|
|
601
|
+
]);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Node.js Standalone (Custom KV)
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
import { StateService, KVStore } from '@fluentcommerce/fc-connect-sdk';
|
|
610
|
+
|
|
611
|
+
// Implement KVStore interface with Redis, DynamoDB, etc.
|
|
612
|
+
class RedisKVStore implements KVStore {
|
|
613
|
+
constructor(private redis: RedisClient) {}
|
|
614
|
+
|
|
615
|
+
async get(keys: string[]): Promise<{ value: unknown } | null> {
|
|
616
|
+
const key = keys.join(':');
|
|
617
|
+
const value = await this.redis.get(key);
|
|
618
|
+
return value ? { value: JSON.parse(value) } : null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async set(keys: string[], value: unknown): Promise<void> {
|
|
622
|
+
const key = keys.join(':');
|
|
623
|
+
await this.redis.set(key, JSON.stringify(value));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async delete(keys: string[]): Promise<void> {
|
|
627
|
+
const key = keys.join(':');
|
|
628
|
+
await this.redis.del(key);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
atomic() {
|
|
632
|
+
// Implement atomic operations with Redis transactions
|
|
633
|
+
const operations: Array<{ type: string; key: string[]; value?: unknown }> = [];
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
check: (entries: Array<{ key: string[]; versionstamp: string | null }>) => {
|
|
637
|
+
// Redis WATCH for optimistic locking
|
|
638
|
+
entries.forEach(e => this.redis.watch(e.key.join(':')));
|
|
639
|
+
},
|
|
640
|
+
set: (key: string[], value: unknown) => {
|
|
641
|
+
operations.push({ type: 'set', key, value });
|
|
642
|
+
},
|
|
643
|
+
commit: async (): Promise<boolean> => {
|
|
644
|
+
const multi = this.redis.multi();
|
|
645
|
+
operations.forEach(op => {
|
|
646
|
+
if (op.type === 'set') {
|
|
647
|
+
multi.set(op.key.join(':'), JSON.stringify(op.value));
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
const results = await multi.exec();
|
|
651
|
+
return results !== null;
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Usage
|
|
658
|
+
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
659
|
+
service: 'inventory-ingestion',
|
|
660
|
+
correlationId: generateCorrelationId()
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
const redisKV = new RedisKVStore(redisClient);
|
|
664
|
+
const stateService = new StateService(logger);
|
|
665
|
+
|
|
666
|
+
await stateService.isFileProcessed(redisKV, 'inventory.csv');
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## Complete Production Example
|
|
672
|
+
|
|
673
|
+
Full ingestion workflow with state management, locking, and error handling:
|
|
674
|
+
|
|
675
|
+
```typescript
|
|
676
|
+
import {
|
|
677
|
+
createClient,
|
|
678
|
+
FluentClient,
|
|
679
|
+
StateService,
|
|
680
|
+
KVStore,
|
|
681
|
+
S3DataSource,
|
|
682
|
+
CSVParserService,
|
|
683
|
+
UniversalMapper,
|
|
684
|
+
BatchAction,
|
|
685
|
+
EntityType,
|
|
686
|
+
} from '@fluentcommerce/fc-connect-sdk';
|
|
687
|
+
|
|
688
|
+
interface IngestionConfig {
|
|
689
|
+
s3: {
|
|
690
|
+
bucket: string;
|
|
691
|
+
prefix: string;
|
|
692
|
+
};
|
|
693
|
+
fluent: {
|
|
694
|
+
baseUrl: string;
|
|
695
|
+
clientId: string;
|
|
696
|
+
clientSecret: string;
|
|
697
|
+
retailerId: string;
|
|
698
|
+
};
|
|
699
|
+
workflow: {
|
|
700
|
+
id: string;
|
|
701
|
+
lockTimeoutMinutes: number;
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function productionIngestionWithState(
|
|
706
|
+
config: IngestionConfig,
|
|
707
|
+
kv: KVStore
|
|
708
|
+
): Promise<{
|
|
709
|
+
success: boolean;
|
|
710
|
+
filesProcessed: number;
|
|
711
|
+
filesSkipped: number;
|
|
712
|
+
errors: string[];
|
|
713
|
+
}> {
|
|
714
|
+
const logger = toStructuredLogger(createConsoleLogger(), {
|
|
715
|
+
service: 'inventory-ingestion',
|
|
716
|
+
correlationId: generateCorrelationId()
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const stateService = new StateService(logger);
|
|
720
|
+
const s3 = new S3DataSource(
|
|
721
|
+
{
|
|
722
|
+
type: 'S3_CSV',
|
|
723
|
+
connectionId: 's3-state-example',
|
|
724
|
+
name: 'S3 State Example',
|
|
725
|
+
s3Config: {
|
|
726
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
727
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
728
|
+
region: process.env.AWS_REGION!,
|
|
729
|
+
bucket: 'inventory-bucket',
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
logger
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
const client = await createClient({
|
|
736
|
+
config: {
|
|
737
|
+
baseUrl: config.fluent.baseUrl,
|
|
738
|
+
clientId: config.fluent.clientId,
|
|
739
|
+
clientSecret: config.fluent.clientSecret,
|
|
740
|
+
retailerId: config.fluent.retailerId,
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
const parser = new CSVParserService();
|
|
745
|
+
const mapper = new UniversalMapper({
|
|
746
|
+
fields: {
|
|
747
|
+
ref: {
|
|
748
|
+
source: 'sku',
|
|
749
|
+
required: true,
|
|
750
|
+
comment: '✅ InventoryPositionInput.ref (String! required)',
|
|
751
|
+
},
|
|
752
|
+
productRef: {
|
|
753
|
+
source: 'sku',
|
|
754
|
+
required: true,
|
|
755
|
+
comment: '✅ InventoryPositionInput.productRef (String! required)',
|
|
756
|
+
},
|
|
757
|
+
locationRef: {
|
|
758
|
+
source: 'warehouse',
|
|
759
|
+
required: true,
|
|
760
|
+
comment: '✅ InventoryPositionInput.locationRef (String! required)',
|
|
761
|
+
},
|
|
762
|
+
qty: {
|
|
763
|
+
source: 'quantity',
|
|
764
|
+
resolver: 'sdk.parseInt',
|
|
765
|
+
required: true,
|
|
766
|
+
comment: '✅ InventoryPositionInput.qty (Int! required)',
|
|
767
|
+
},
|
|
768
|
+
status: {
|
|
769
|
+
source: 'status',
|
|
770
|
+
resolver: 'sdk.uppercase',
|
|
771
|
+
defaultValue: 'AVAILABLE',
|
|
772
|
+
comment: '✅ InventoryPositionInput.status (String optional)',
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
let filesProcessed = 0;
|
|
778
|
+
let filesSkipped = 0;
|
|
779
|
+
const errors: string[] = [];
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
// List files from S3
|
|
783
|
+
const files = await s3.listFiles({ prefix: config.s3.prefix });
|
|
784
|
+
console.log(`Found ${files.length} files in S3`);
|
|
785
|
+
|
|
786
|
+
// Get or create daily job
|
|
787
|
+
let dailyJob = await state.getDailyJob(kv, config.workflow.id);
|
|
788
|
+
|
|
789
|
+
if (!dailyJob) {
|
|
790
|
+
const job = await client.createJob({
|
|
791
|
+
name: `Daily Sync - ${new Date().toISOString().split('T')[0]}`,
|
|
792
|
+
retailerId: config.fluent.retailerId,
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
await state.setDailyJob(kv, config.workflow.id, job.id, 24);
|
|
796
|
+
dailyJob = {
|
|
797
|
+
jobId: job.id,
|
|
798
|
+
createdAt: new Date().toISOString(),
|
|
799
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
console.log(`Using job: ${dailyJob.jobId}`);
|
|
804
|
+
|
|
805
|
+
for (const file of files) {
|
|
806
|
+
const lockName = `file:${file.path}`;
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
// 1. Acquire lock
|
|
810
|
+
const lockAcquired = await state.acquireLock(
|
|
811
|
+
lockName,
|
|
812
|
+
kv,
|
|
813
|
+
config.workflow.lockTimeoutMinutes
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
if (!lockAcquired) {
|
|
817
|
+
console.log(`File locked by another process: ${file.path}`);
|
|
818
|
+
filesSkipped++;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// 2. Check if already processed
|
|
823
|
+
if (await state.isFileProcessed(kv, file.path, config.workflow.id)) {
|
|
824
|
+
console.log(`File already processed: ${file.path}`);
|
|
825
|
+
filesSkipped++;
|
|
826
|
+
await state.releaseLock(lockName, kv);
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// 3. Download and parse file
|
|
831
|
+
const fileContent = await s3.downloadFile(file.path);
|
|
832
|
+
const records = await parser.parse(fileContent);
|
|
833
|
+
|
|
834
|
+
console.log(`Parsed ${records.length} records from ${file.path}`);
|
|
835
|
+
|
|
836
|
+
// 4. Transform data
|
|
837
|
+
const mappingResult = await mapper.map(records);
|
|
838
|
+
|
|
839
|
+
if (!mappingResult.success) {
|
|
840
|
+
throw new Error(`Mapping failed: ${JSON.stringify(mappingResult.errors)}`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// 5. Send to Batch API
|
|
844
|
+
const batch = await client.sendBatch(dailyJob.jobId, {
|
|
845
|
+
action: 'UPSERT',
|
|
846
|
+
entityType: 'INVENTORY',
|
|
847
|
+
source: 'COMPLETE_WORKFLOW',
|
|
848
|
+
event: 'INVENTORY_UPDATE',
|
|
849
|
+
entities: mappingResult.data,
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
console.log(`Batch sent: ${batch.id}`);
|
|
853
|
+
|
|
854
|
+
// 6. Mark as processed
|
|
855
|
+
await state.updateSyncState(
|
|
856
|
+
kv,
|
|
857
|
+
[
|
|
858
|
+
{
|
|
859
|
+
fileName: file.path,
|
|
860
|
+
lastModified: file.lastModified || new Date().toISOString(),
|
|
861
|
+
recordCount: mappingResult.data.length,
|
|
862
|
+
processedAt: new Date().toISOString(),
|
|
863
|
+
},
|
|
864
|
+
],
|
|
865
|
+
config.workflow.id
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
filesProcessed++;
|
|
869
|
+
|
|
870
|
+
// 7. Release lock
|
|
871
|
+
await state.releaseLock(lockName, kv);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
const errorMsg = `Failed to process ${file.path}: ${(error as Error).message}`;
|
|
874
|
+
console.error(errorMsg);
|
|
875
|
+
errors.push(errorMsg);
|
|
876
|
+
|
|
877
|
+
// Release lock on error
|
|
878
|
+
await state.releaseLock(lockName, kv);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return {
|
|
883
|
+
success: errors.length === 0,
|
|
884
|
+
filesProcessed,
|
|
885
|
+
filesSkipped,
|
|
886
|
+
errors,
|
|
887
|
+
};
|
|
888
|
+
} catch (error) {
|
|
889
|
+
console.error('Ingestion failed:', error);
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Usage
|
|
895
|
+
const result = await productionIngestionWithState(
|
|
896
|
+
{
|
|
897
|
+
s3: {
|
|
898
|
+
bucket: 'inventory-bucket',
|
|
899
|
+
prefix: 'data/',
|
|
900
|
+
},
|
|
901
|
+
fluent: {
|
|
902
|
+
baseUrl: process.env.FLUENT_BASE_URL!,
|
|
903
|
+
clientId: process.env.FLUENT_CLIENT_ID!,
|
|
904
|
+
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
|
|
905
|
+
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
906
|
+
},
|
|
907
|
+
workflow: {
|
|
908
|
+
id: 'inventory-ingestion',
|
|
909
|
+
lockTimeoutMinutes: 15,
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
kv
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
console.log('Ingestion result:', result);
|
|
916
|
+
// Output:
|
|
917
|
+
// {
|
|
918
|
+
// success: true,
|
|
919
|
+
// filesProcessed: 10,
|
|
920
|
+
// filesSkipped: 5,
|
|
921
|
+
// errors: []
|
|
922
|
+
// }
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
---
|
|
926
|
+
|
|
927
|
+
## Troubleshooting
|
|
928
|
+
|
|
929
|
+
### Problem: Files Reprocessed Every Run
|
|
930
|
+
|
|
931
|
+
**Symptoms:**
|
|
932
|
+
|
|
933
|
+
- Same files processed repeatedly
|
|
934
|
+
- Duplicate inventory positions
|
|
935
|
+
|
|
936
|
+
**Solution:**
|
|
937
|
+
|
|
938
|
+
```typescript
|
|
939
|
+
// Check if state is being updated correctly
|
|
940
|
+
const syncState = await state.getSyncState(kv, workflowId);
|
|
941
|
+
console.log('Sync state:', syncState);
|
|
942
|
+
|
|
943
|
+
// Verify file is marked as processed
|
|
944
|
+
const isProcessed = await state.isFileProcessed(kv, fileName, workflowId);
|
|
945
|
+
console.log(`File ${fileName} processed:`, isProcessed);
|
|
946
|
+
|
|
947
|
+
// Ensure updateSyncState is called AFTER successful processing
|
|
948
|
+
await state.updateSyncState(kv, [{ fileName, lastModified, recordCount }], workflowId);
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
### Problem: Lock Never Released
|
|
952
|
+
|
|
953
|
+
**Symptoms:**
|
|
954
|
+
|
|
955
|
+
- Files stuck in "locked" state
|
|
956
|
+
- Processing stops
|
|
957
|
+
|
|
958
|
+
**Solution:**
|
|
959
|
+
|
|
960
|
+
```typescript
|
|
961
|
+
// ALWAYS use try-finally to release locks
|
|
962
|
+
try {
|
|
963
|
+
const lockAcquired = await state.acquireLock(lockName, kv, 15);
|
|
964
|
+
if (lockAcquired) {
|
|
965
|
+
await processFile();
|
|
966
|
+
}
|
|
967
|
+
} finally {
|
|
968
|
+
// ✅ CRITICAL: Release lock even if processing fails
|
|
969
|
+
await state.releaseLock(lockName, kv);
|
|
970
|
+
}
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
### Problem: Stale Locks Blocking Processing
|
|
974
|
+
|
|
975
|
+
**Symptoms:**
|
|
976
|
+
|
|
977
|
+
- Locks from crashed processes
|
|
978
|
+
- Processing stuck indefinitely
|
|
979
|
+
|
|
980
|
+
**Solution:**
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
// SDK automatically overrides stale locks based on timeout
|
|
984
|
+
// If lock expired (past expiresAt), next acquireLock succeeds
|
|
985
|
+
|
|
986
|
+
// To manually clear stale locks (if needed):
|
|
987
|
+
await state.releaseLock(lockName, kv); // Force release
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
### Problem: State Lost Between Runs
|
|
991
|
+
|
|
992
|
+
**Symptoms:**
|
|
993
|
+
|
|
994
|
+
- First-time sync every run
|
|
995
|
+
- `isInitialized: false` always
|
|
996
|
+
|
|
997
|
+
**Solution:**
|
|
998
|
+
|
|
999
|
+
```typescript
|
|
1000
|
+
// Verify KV store persistence
|
|
1001
|
+
const testKey = ['test', 'persistence'];
|
|
1002
|
+
await kv.set(testKey, { value: 'test' });
|
|
1003
|
+
const retrieved = await kv.get(testKey);
|
|
1004
|
+
console.log('KV persistence test:', retrieved);
|
|
1005
|
+
|
|
1006
|
+
// Ensure workflowId is consistent
|
|
1007
|
+
const WORKFLOW_ID = 'inventory-ingestion'; // ✅ Use constant
|
|
1008
|
+
await state.updateSyncState(kv, files, WORKFLOW_ID);
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
---
|
|
1012
|
+
|
|
1013
|
+
## Key Takeaways
|
|
1014
|
+
|
|
1015
|
+
- 🎯 **Always use state management** - Prevents duplicates and enables idempotency
|
|
1016
|
+
- 🎯 **Acquire locks before processing** - Prevents concurrent conflicts
|
|
1017
|
+
- 🎯 **Release locks in finally blocks** - Ensures cleanup even on errors
|
|
1018
|
+
- 🎯 **Track file metadata** - Rich metadata enables auditing and troubleshooting
|
|
1019
|
+
- 🎯 **Use workflow IDs** - Scope state to specific workflows
|
|
1020
|
+
- 🎯 **Handle stale locks** - SDK automatically overrides expired locks
|
|
1021
|
+
|
|
1022
|
+
---
|
|
1023
|
+
|
|
1024
|
+
## Next Steps
|
|
1025
|
+
|
|
1026
|
+
Continue to [Module 8: Performance Optimization](./02-core-guides-ingestion-08-performance-optimization.md) to learn how to optimize batch sizing, parallel processing, and job strategies for large-scale ingestion.
|
|
1027
|
+
|
|
1028
|
+
---
|
|
1029
|
+
|
|
1030
|
+
[← Previous: Batch API](./02-core-guides-ingestion-06-batch-api.md) | [Back to Guide](../ingestion-readme.md) | [Next: Performance Optimization →](./02-core-guides-ingestion-08-performance-optimization.md)
|
|
1031
|
+
|
|
1032
|
+
## Related Documentation
|
|
1033
|
+
|
|
1034
|
+
- [StateService API](../../api-reference/modules/api-reference-05-services.md#state-service) - Complete API documentation
|
|
1035
|
+
- [Versori KV Integration](../../../04-REFERENCE/platforms/versori/#key-value-storage) - Platform-specific KV usage
|
|
1036
|
+
- [Error Handling Guide](../../../03-PATTERN-GUIDES/error-handling/error-handling-readme.md) - Retry patterns and error recovery
|
|
1037
|
+
- [Best Practices](./02-core-guides-ingestion-09-best-practices.md) - Production patterns and monitoring
|