@reventlessdev/reventless-local 3.0.0-alpha.234 → 3.0.0-alpha.236
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 +23 -0
- package/package.json +9 -9
- package/tests/adapter/QueryDbListResolverTest.res +150 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +133 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +64 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +55 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res +2 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +10 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +1 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +5 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +1 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +5 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +135 -1
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +202 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +15 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +2 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.236 (2026-08-23)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.235 (2026-08-22)
|
|
15
|
+
|
|
16
|
+
* feat(core)!: a slice that has given up says so ([f5559ab](https://github.com/ReventlessDev/reventless-core/commit/f5559abede3a9cf2c9a48aadc69a666c46cc75bf))
|
|
17
|
+
|
|
18
|
+
### BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* `onExhausted` is required on the Translation and Automation
|
|
21
|
+
module types. ReScript has no optional module-type fields, so the alternative
|
|
22
|
+
was a PPX-injected default — and defaulting this one to silence is the bug.
|
|
23
|
+
A slice now states what abandonment means for its domain, `None` included.
|
|
24
|
+
GeocodeCustomerAddress answers with MarkAddressUnresolvable; the others say
|
|
25
|
+
nothing, on purpose.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
6
29
|
# 3.0.0-alpha.234 (2026-08-21)
|
|
7
30
|
|
|
8
31
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.236",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
38
38
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
|
|
39
39
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
40
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
41
40
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
|
|
41
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
42
42
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
43
|
-
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
46
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.96",
|
|
44
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.195",
|
|
45
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.150",
|
|
46
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.112",
|
|
47
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.248",
|
|
48
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.122",
|
|
49
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.17"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.0",
|
|
@@ -574,3 +574,153 @@ describe("QueryDb by-index resolver", () => {
|
|
|
574
574
|
}
|
|
575
575
|
})
|
|
576
576
|
})
|
|
577
|
+
|
|
578
|
+
// ─────────────────────────────────────────────────────────────
|
|
579
|
+
// Sub-id view — the `{name}Items` door
|
|
580
|
+
// ─────────────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
// The door only exists when a view declares a sub-id, and no fixture declared one
|
|
583
|
+
// alongside a union field, so it was the last door in the mechanism plan's table
|
|
584
|
+
// carrying a union nowhere.
|
|
585
|
+
|
|
586
|
+
@schema
|
|
587
|
+
type lineState = {
|
|
588
|
+
orderId: @s.matches(Reventless.DcbTag.string) string,
|
|
589
|
+
lineNo: string,
|
|
590
|
+
label: string,
|
|
591
|
+
geolocation?: geolocation,
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
let lineStateSchemaWithAnnotations =
|
|
595
|
+
lineStateSchema->S.Metadata.set(
|
|
596
|
+
~id=Reventless.StateAnnotations.stateAnnotationsId,
|
|
597
|
+
{
|
|
598
|
+
ids: ["orderId"],
|
|
599
|
+
compositeIds: [],
|
|
600
|
+
subIds: ["lineNo"],
|
|
601
|
+
compositeSubIds: [],
|
|
602
|
+
indexes: [],
|
|
603
|
+
hidden: [],
|
|
604
|
+
summary: [],
|
|
605
|
+
drillTargets: [],
|
|
606
|
+
drillTargetKeys: [],
|
|
607
|
+
collapsed: [],
|
|
608
|
+
scan: [],
|
|
609
|
+
scanSort: [],
|
|
610
|
+
semantic: [],
|
|
611
|
+
metric: [],
|
|
612
|
+
lifecycle: None,
|
|
613
|
+
groupBy: None,
|
|
614
|
+
visibility: None,
|
|
615
|
+
live: None,
|
|
616
|
+
retired: None,
|
|
617
|
+
},
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
let buildSubIdFixture = async (~name: string) => {
|
|
621
|
+
module Bus = LocalBus.Make()
|
|
622
|
+
module Storage = LocalQueryDbStorage.Make(Bus)
|
|
623
|
+
module Resolvers = QueryDbResolvers_GraphQL.Make(Bus)
|
|
624
|
+
|
|
625
|
+
module Spec = {
|
|
626
|
+
module Id = Reventless.Id.StringPure
|
|
627
|
+
let name = name
|
|
628
|
+
let moduleUrl: string = %raw(`import.meta.url`)
|
|
629
|
+
@schema
|
|
630
|
+
type state = lineState
|
|
631
|
+
let config = Reventless.ReadModel.config()
|
|
632
|
+
let subIdConfig = Some({
|
|
633
|
+
Reventless.ReadModel.subIdField: "lineNo",
|
|
634
|
+
getSubId: (state: lineState) => state.lineNo,
|
|
635
|
+
})
|
|
636
|
+
let authorization: Reventless.Authorization.permission = AllowAuthenticated
|
|
637
|
+
let visibility: Reventless.Visibility.t = Public
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
module QDbResolversAdapter = ReventlessCore.QueryDb_Adapter.NoResolvers(Storage)
|
|
641
|
+
module Maker = ReventlessCore.QueryDb_Builder.Make(Spec, Storage, QDbResolversAdapter)
|
|
642
|
+
|
|
643
|
+
ReventlessCore.Plugin_Helpers.queryFieldNamesRegistry->Dict.set(
|
|
644
|
+
name,
|
|
645
|
+
{
|
|
646
|
+
singleFieldName: name,
|
|
647
|
+
listFieldName: name ++ "s",
|
|
648
|
+
returnTypeName: name,
|
|
649
|
+
pluralTypeName: "[" ++ name ++ "]",
|
|
650
|
+
includeIdParam: true,
|
|
651
|
+
connectionSpec: true,
|
|
652
|
+
},
|
|
653
|
+
)
|
|
654
|
+
ReventlessCore.Plugin_Helpers.stateSchemaRegistry->Dict.set(
|
|
655
|
+
name,
|
|
656
|
+
lineStateSchemaWithAnnotations->S.castToUnknown,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
let queryDb = Maker.make(~api=(), ~apiRole=())
|
|
660
|
+
let ops = await queryDb->ReventlessCore.Component.operations->TestRunner.resolve
|
|
661
|
+
|
|
662
|
+
let _: ReventlessCore.QueryDb_Adapter.resolvers = Resolvers.make(
|
|
663
|
+
~name,
|
|
664
|
+
~api=(),
|
|
665
|
+
~apiRole=(),
|
|
666
|
+
~dataSourceName=""->Pulumi.Output.make,
|
|
667
|
+
~indexes=[],
|
|
668
|
+
~subIdField=Some("lineNo"),
|
|
669
|
+
~idResolverConfigs=[],
|
|
670
|
+
~idsResolverConfigs=[],
|
|
671
|
+
~authorization=Reventless.Authorization.AllowAuthenticated,
|
|
672
|
+
~opts=({}: Pulumi.CustomResourceOptions.t),
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
// Two lines under one order: the first carries the union, the second leaves the
|
|
676
|
+
// optional field absent.
|
|
677
|
+
let _ = await ops.save(
|
|
678
|
+
"o-1",
|
|
679
|
+
{orderId: "o-1", lineNo: "l-1", label: "First", geolocation: Located({lat: 48.2082, lng: 16.3738})},
|
|
680
|
+
Init,
|
|
681
|
+
None,
|
|
682
|
+
)
|
|
683
|
+
let _ = await ops.save("o-1", {orderId: "o-1", lineNo: "l-2", label: "Second"}, Init, None)
|
|
684
|
+
|
|
685
|
+
switch DomainGraphQL_Server.getQueryResolver(name ++ "Items") {
|
|
686
|
+
| Some(r) => r
|
|
687
|
+
| None => JsError.throwWithMessage("items resolver not registered: " ++ name ++ "Items")
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
describe("QueryDb sub-id resolver — the Items door", () => {
|
|
692
|
+
beforeEach(() => {
|
|
693
|
+
DomainGraphQL_Server.reset()
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
testPromise("answers the sub-items of one id, as edges", async () => {
|
|
697
|
+
let resolver = await buildSubIdFixture(~name="ItemA")
|
|
698
|
+
let response = await resolver(JSON.Encode.null, argsOf([("id", strJson("o-1"))]), emptyCtx)
|
|
699
|
+
let edges = getEdges(response)
|
|
700
|
+
expect(edges->Array.length)->toBe(2)
|
|
701
|
+
expect(edgeNodeField(edges->Array.getUnsafe(0), "lineNo"))->toBe("l-1")
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
// The gap this fixture exists to close, mirroring the by-index case above: a
|
|
705
|
+
// union reaching a caller through the Items door with the member type the
|
|
706
|
+
// write-time stamp put on it. Without it the member resolves to null and takes
|
|
707
|
+
// its non-nullable parent, so the row leaves the connection rather than erroring.
|
|
708
|
+
testPromise("carries a union field's member type through", async () => {
|
|
709
|
+
let resolver = await buildSubIdFixture(~name="ItemB")
|
|
710
|
+
let response = await resolver(JSON.Encode.null, argsOf([("id", strJson("o-1"))]), emptyCtx)
|
|
711
|
+
let first = getEdges(response)->Array.getUnsafe(0)
|
|
712
|
+
let geo = edgeNodeJson(first, "geolocation")->Option.flatMap(JSON.Decode.object)
|
|
713
|
+
expect(geo->Option.flatMap(o => o->Dict.get("__typename"))->Option.flatMap(JSON.Decode.string))
|
|
714
|
+
->toEqual(Some("GeolocationLocated"))
|
|
715
|
+
expect(geo->Option.flatMap(o => o->Dict.get("lat"))->Option.flatMap(JSON.Decode.float))
|
|
716
|
+
->toEqual(Some(48.2082))
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
testPromise("leaves an absent union absent", async () => {
|
|
720
|
+
let resolver = await buildSubIdFixture(~name="ItemC")
|
|
721
|
+
let response = await resolver(JSON.Encode.null, argsOf([("id", strJson("o-1"))]), emptyCtx)
|
|
722
|
+
let second = getEdges(response)->Array.getUnsafe(1)
|
|
723
|
+
expect(edgeNodeField(second, "label"))->toBe("Second")
|
|
724
|
+
expect(edgeNodeJson(second, "geolocation")->Option.isNone)->toBe(true)
|
|
725
|
+
})
|
|
726
|
+
})
|
|
@@ -648,6 +648,136 @@ globalThis.describe("QueryDb by-index resolver", () => {
|
|
|
648
648
|
});
|
|
649
649
|
});
|
|
650
650
|
|
|
651
|
+
let lineStateSchema = Sury.$schema(s => ({
|
|
652
|
+
orderId: s.m(DcbTag$Reventless.string),
|
|
653
|
+
lineNo: s.m(Sury.string),
|
|
654
|
+
label: s.m(Sury.string),
|
|
655
|
+
geolocation: s.m(Sury.$option(geolocationSchema$1))
|
|
656
|
+
}));
|
|
657
|
+
|
|
658
|
+
let lineStateSchemaWithAnnotations = Sury.$Metadata_set(lineStateSchema, StateAnnotations$Reventless.stateAnnotationsId, {
|
|
659
|
+
ids: ["orderId"],
|
|
660
|
+
compositeIds: [],
|
|
661
|
+
subIds: ["lineNo"],
|
|
662
|
+
compositeSubIds: [],
|
|
663
|
+
indexes: [],
|
|
664
|
+
hidden: [],
|
|
665
|
+
summary: [],
|
|
666
|
+
drillTargets: [],
|
|
667
|
+
drillTargetKeys: [],
|
|
668
|
+
collapsed: [],
|
|
669
|
+
scan: [],
|
|
670
|
+
scanSort: [],
|
|
671
|
+
semantic: [],
|
|
672
|
+
metric: [],
|
|
673
|
+
lifecycle: undefined,
|
|
674
|
+
groupBy: undefined,
|
|
675
|
+
visibility: undefined,
|
|
676
|
+
live: undefined,
|
|
677
|
+
retired: undefined
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
async function buildSubIdFixture(name) {
|
|
681
|
+
let Bus = LocalBus$ReventlessLocal.Make({});
|
|
682
|
+
let Storage = LocalQueryDbStorage$ReventlessLocal.Make(Bus);
|
|
683
|
+
let Resolvers = QueryDbResolvers_GraphQL$ReventlessLocal.Make(Bus);
|
|
684
|
+
let moduleUrl = import.meta.url;
|
|
685
|
+
let config = ReadModel$Reventless.config(undefined, undefined, undefined);
|
|
686
|
+
let subIdConfig = {
|
|
687
|
+
subIdField: "lineNo",
|
|
688
|
+
getSubId: state => state.lineNo
|
|
689
|
+
};
|
|
690
|
+
let QDbResolversAdapter = QueryDb_Adapter$ReventlessCore.NoResolvers({
|
|
691
|
+
make: Storage.make
|
|
692
|
+
});
|
|
693
|
+
let Maker = QueryDb_Builder$ReventlessCore.Make({
|
|
694
|
+
Id: {
|
|
695
|
+
schema: Id$Reventless.StringPure.schema,
|
|
696
|
+
make: prim => prim,
|
|
697
|
+
makeFromString: prim => prim,
|
|
698
|
+
toString: prim => prim,
|
|
699
|
+
cmp: Id$Reventless.StringPure.cmp
|
|
700
|
+
},
|
|
701
|
+
name: name,
|
|
702
|
+
moduleUrl: moduleUrl,
|
|
703
|
+
stateSchema: lineStateSchema,
|
|
704
|
+
config: config,
|
|
705
|
+
subIdConfig: subIdConfig,
|
|
706
|
+
authorization: "AllowAuthenticated",
|
|
707
|
+
visibility: "Public"
|
|
708
|
+
})({
|
|
709
|
+
make: Storage.make
|
|
710
|
+
})(QDbResolversAdapter);
|
|
711
|
+
Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[name] = {
|
|
712
|
+
singleFieldName: name,
|
|
713
|
+
listFieldName: name + "s",
|
|
714
|
+
returnTypeName: name,
|
|
715
|
+
pluralTypeName: "[" + name + "]",
|
|
716
|
+
includeIdParam: true,
|
|
717
|
+
connectionSpec: true
|
|
718
|
+
};
|
|
719
|
+
Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name] = lineStateSchemaWithAnnotations;
|
|
720
|
+
let queryDb = Maker.make(undefined, undefined, undefined, undefined, undefined);
|
|
721
|
+
let ops = await TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(queryDb));
|
|
722
|
+
Resolvers.make(name, undefined, undefined, Pulumi.output(""), [], "lineNo", [], [], "AllowAuthenticated", {});
|
|
723
|
+
await ops.save("o-1", {
|
|
724
|
+
orderId: "o-1",
|
|
725
|
+
lineNo: "l-1",
|
|
726
|
+
label: "First",
|
|
727
|
+
geolocation: {
|
|
728
|
+
TAG: "Located",
|
|
729
|
+
lat: 48.2082,
|
|
730
|
+
lng: 16.3738
|
|
731
|
+
}
|
|
732
|
+
}, "Init", undefined);
|
|
733
|
+
await ops.save("o-1", {
|
|
734
|
+
orderId: "o-1",
|
|
735
|
+
lineNo: "l-2",
|
|
736
|
+
label: "Second"
|
|
737
|
+
}, "Init", undefined);
|
|
738
|
+
let r = DomainGraphQL_Server$ReventlessLocal.getQueryResolver(name + "Items");
|
|
739
|
+
if (r !== undefined) {
|
|
740
|
+
return r;
|
|
741
|
+
} else {
|
|
742
|
+
return Stdlib_JsError.throwWithMessage("items resolver not registered: " + name + "Items");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
globalThis.describe("QueryDb sub-id resolver — the Items door", () => {
|
|
747
|
+
globalThis.beforeEach(() => DomainGraphQL_Server$ReventlessLocal.reset());
|
|
748
|
+
globalThis.test("answers the sub-items of one id, as edges", async () => {
|
|
749
|
+
let resolver = await buildSubIdFixture("ItemA");
|
|
750
|
+
let response = await resolver(null, Object.fromEntries([[
|
|
751
|
+
"id",
|
|
752
|
+
"o-1"
|
|
753
|
+
]]), emptyCtx);
|
|
754
|
+
let edges = getEdges(response);
|
|
755
|
+
globalThis.expect(edges.length).toBe(2);
|
|
756
|
+
globalThis.expect(edgeNodeField(edges[0], "lineNo")).toBe("l-1");
|
|
757
|
+
});
|
|
758
|
+
globalThis.test("carries a union field's member type through", async () => {
|
|
759
|
+
let resolver = await buildSubIdFixture("ItemB");
|
|
760
|
+
let response = await resolver(null, Object.fromEntries([[
|
|
761
|
+
"id",
|
|
762
|
+
"o-1"
|
|
763
|
+
]]), emptyCtx);
|
|
764
|
+
let first = getEdges(response)[0];
|
|
765
|
+
let geo = Stdlib_Option.flatMap(edgeNodeJson(first, "geolocation"), Stdlib_JSON.Decode.object);
|
|
766
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(geo, o => o["__typename"]), Stdlib_JSON.Decode.string)).toEqual("GeolocationLocated");
|
|
767
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(geo, o => o["lat"]), Stdlib_JSON.Decode.float)).toEqual(48.2082);
|
|
768
|
+
});
|
|
769
|
+
globalThis.test("leaves an absent union absent", async () => {
|
|
770
|
+
let resolver = await buildSubIdFixture("ItemC");
|
|
771
|
+
let response = await resolver(null, Object.fromEntries([[
|
|
772
|
+
"id",
|
|
773
|
+
"o-1"
|
|
774
|
+
]]), emptyCtx);
|
|
775
|
+
let second = getEdges(response)[1];
|
|
776
|
+
globalThis.expect(edgeNodeField(second, "label")).toBe("Second");
|
|
777
|
+
globalThis.expect(Stdlib_Option.isNone(edgeNodeJson(second, "geolocation"))).toBe(true);
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
651
781
|
export {
|
|
652
782
|
geolocationSchema$1 as geolocationSchema,
|
|
653
783
|
rowStateSchema,
|
|
@@ -667,5 +797,8 @@ export {
|
|
|
667
797
|
pageInfoBool,
|
|
668
798
|
pageInfoString,
|
|
669
799
|
buildFixture,
|
|
800
|
+
lineStateSchema,
|
|
801
|
+
lineStateSchemaWithAnnotations,
|
|
802
|
+
buildSubIdFixture,
|
|
670
803
|
}
|
|
671
804
|
/* Not a pure module */
|
|
@@ -179,6 +179,70 @@ describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
179
179
|
})
|
|
180
180
|
})
|
|
181
181
|
|
|
182
|
+
// A row used to leave the retry budget by falling out of phase2's filter, so
|
|
183
|
+
// `Failed` meant both "will be retried" and "never again" and only the count
|
|
184
|
+
// told them apart — against a ceiling that lives in the Spec, not in the row.
|
|
185
|
+
describe("retry exhaustion", () => {
|
|
186
|
+
let failingPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds =>
|
|
187
|
+
JsError.throwWithMessage("publish failed")
|
|
188
|
+
|
|
189
|
+
let collectOne = () =>
|
|
190
|
+
Callback.phase1(
|
|
191
|
+
[encodeShipOrderEvent(OrderPlaced({orderId: "ord-1", address: "123 Main St"}))],
|
|
192
|
+
testContext,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
let statusOf = id => (Callback.todoItems->Dict.get(id)->Option.getOrThrow).status
|
|
196
|
+
|
|
197
|
+
testPromise("the attempt that spends the budget writes Abandoned, not Failed", async () => {
|
|
198
|
+
collectOne()
|
|
199
|
+
// ShipOrderSpec.maxRetries is 3: attempts at retryCount 0, 1 and 2 leave the
|
|
200
|
+
// row retriable, and the third failure is the one that ends it.
|
|
201
|
+
await Callback.phase2(failingPublish)
|
|
202
|
+
expect(statusOf("ord-1"))->toBe(Failed)
|
|
203
|
+
await Callback.phase2(failingPublish)
|
|
204
|
+
expect(statusOf("ord-1"))->toBe(Failed)
|
|
205
|
+
await Callback.phase2(failingPublish)
|
|
206
|
+
expect(statusOf("ord-1"))->toBe(Abandoned)
|
|
207
|
+
expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).retryCount)->toBe(3)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
testPromise("an abandoned row is not picked up again", async () => {
|
|
211
|
+
collectOne()
|
|
212
|
+
await Callback.phase2(failingPublish)
|
|
213
|
+
await Callback.phase2(failingPublish)
|
|
214
|
+
await Callback.phase2(failingPublish)
|
|
215
|
+
expect(statusOf("ord-1"))->toBe(Abandoned)
|
|
216
|
+
|
|
217
|
+
// A pass that would have succeeded: the row must still not be attempted,
|
|
218
|
+
// which is what makes the status a decision rather than a label.
|
|
219
|
+
let published = ref([])
|
|
220
|
+
await Callback.phase2(async cmds => published := cmds)
|
|
221
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
222
|
+
expect(statusOf("ord-1"))->toBe(Abandoned)
|
|
223
|
+
expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).retryCount)->toBe(3)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
testPromise("a row left stranded at the ceiling is normalised on the next pass", async () => {
|
|
227
|
+
collectOne()
|
|
228
|
+
// What an older build wrote, and what a Spec that lowered maxRetries leaves
|
|
229
|
+
// behind: Failed at the ceiling, which no pass would ever pick up or mark.
|
|
230
|
+
let row = Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
231
|
+
Callback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 3})
|
|
232
|
+
|
|
233
|
+
let published = ref([])
|
|
234
|
+
await Callback.phase2(async cmds => published := cmds)
|
|
235
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
236
|
+
expect(statusOf("ord-1"))->toBe(Abandoned)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
testPromise("the row carries the ceiling its count is racing", async () => {
|
|
240
|
+
collectOne()
|
|
241
|
+
expect((Callback.todoItems->Dict.get("ord-1")->Option.getOrThrow).maxRetries)
|
|
242
|
+
->toEqual(Some(ShipOrderSpec.maxRetries))
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
182
246
|
describe("full lifecycle", () => {
|
|
183
247
|
testPromise("collect → process → resolve completes the TODO item", async () => {
|
|
184
248
|
Callback.phase1(
|
|
@@ -192,6 +192,61 @@ globalThis.describe("AutomationSlice Callback (mixed-source)", () => {
|
|
|
192
192
|
globalThis.expect(row2.status).toBe("Processing");
|
|
193
193
|
});
|
|
194
194
|
});
|
|
195
|
+
globalThis.describe("retry exhaustion", () => {
|
|
196
|
+
let failingPublish = async _cmds => Stdlib_JsError.throwWithMessage("publish failed");
|
|
197
|
+
let collectOne = () => Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
198
|
+
TAG: "OrderPlaced",
|
|
199
|
+
orderId: "ord-1",
|
|
200
|
+
address: "123 Main St"
|
|
201
|
+
})], AutomationSliceFixtures$ReventlessLocal.testContext);
|
|
202
|
+
let statusOf = id => Stdlib_Option.getOrThrow(Callback.todoItems[id], undefined).status;
|
|
203
|
+
globalThis.test("the attempt that spends the budget writes Abandoned, not Failed", async () => {
|
|
204
|
+
collectOne();
|
|
205
|
+
await Callback.phase2(failingPublish);
|
|
206
|
+
globalThis.expect(statusOf("ord-1")).toBe("Failed");
|
|
207
|
+
await Callback.phase2(failingPublish);
|
|
208
|
+
globalThis.expect(statusOf("ord-1")).toBe("Failed");
|
|
209
|
+
await Callback.phase2(failingPublish);
|
|
210
|
+
globalThis.expect(statusOf("ord-1")).toBe("Abandoned");
|
|
211
|
+
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).retryCount).toBe(3);
|
|
212
|
+
});
|
|
213
|
+
globalThis.test("an abandoned row is not picked up again", async () => {
|
|
214
|
+
collectOne();
|
|
215
|
+
await Callback.phase2(failingPublish);
|
|
216
|
+
await Callback.phase2(failingPublish);
|
|
217
|
+
await Callback.phase2(failingPublish);
|
|
218
|
+
globalThis.expect(statusOf("ord-1")).toBe("Abandoned");
|
|
219
|
+
let published = {
|
|
220
|
+
contents: []
|
|
221
|
+
};
|
|
222
|
+
await Callback.phase2(async cmds => {
|
|
223
|
+
published.contents = cmds;
|
|
224
|
+
});
|
|
225
|
+
globalThis.expect(published.contents.length).toBe(0);
|
|
226
|
+
globalThis.expect(statusOf("ord-1")).toBe("Abandoned");
|
|
227
|
+
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).retryCount).toBe(3);
|
|
228
|
+
});
|
|
229
|
+
globalThis.test("a row left stranded at the ceiling is normalised on the next pass", async () => {
|
|
230
|
+
collectOne();
|
|
231
|
+
let row = Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined);
|
|
232
|
+
let newrecord = {...row};
|
|
233
|
+
newrecord.retryCount = 3;
|
|
234
|
+
newrecord.status = "Failed";
|
|
235
|
+
Callback.todoItems["ord-1"] = newrecord;
|
|
236
|
+
let published = {
|
|
237
|
+
contents: []
|
|
238
|
+
};
|
|
239
|
+
await Callback.phase2(async cmds => {
|
|
240
|
+
published.contents = cmds;
|
|
241
|
+
});
|
|
242
|
+
globalThis.expect(published.contents.length).toBe(0);
|
|
243
|
+
globalThis.expect(statusOf("ord-1")).toBe("Abandoned");
|
|
244
|
+
});
|
|
245
|
+
globalThis.test("the row carries the ceiling its count is racing", async () => {
|
|
246
|
+
collectOne();
|
|
247
|
+
globalThis.expect(Stdlib_Option.getOrThrow(Callback.todoItems["ord-1"], undefined).maxRetries).toEqual(AutomationSliceFixtures$ReventlessLocal.ShipOrderSpec.maxRetries);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
195
250
|
globalThis.describe("full lifecycle", () => {
|
|
196
251
|
globalThis.test("collect → process → resolve completes the TODO item", async () => {
|
|
197
252
|
Callback.phase1([AutomationSliceFixtures$ReventlessLocal.encodeShipOrderEvent({
|
|
@@ -108,6 +108,7 @@ module ShipOrderAutomation: Reventless.AutomationSlice.Automation
|
|
|
108
108
|
with module Spec := ShipOrderSpec = {
|
|
109
109
|
let process = (id, _item: ShipOrderSpec.todoItem) =>
|
|
110
110
|
Some((id, ShipOrderSpec.CreateShipment({orderId: id})))
|
|
111
|
+
let onExhausted = (_id, _item: ShipOrderSpec.todoItem) => None
|
|
111
112
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
112
113
|
module M = Reventless.AutomationSlice.Mappings.Make(ShipOrderSpec)
|
|
113
114
|
module type Mapping = M.Mapping
|
|
@@ -117,6 +118,7 @@ module ShipOrderAutomation: Reventless.AutomationSlice.Automation
|
|
|
117
118
|
module SkipProcessAutomation: Reventless.AutomationSlice.Automation
|
|
118
119
|
with module Spec := SkipProcessSpec = {
|
|
119
120
|
let process = (_id, _item: SkipProcessSpec.todoItem) => None
|
|
121
|
+
let onExhausted = (_id, _item: SkipProcessSpec.todoItem) => None
|
|
120
122
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
121
123
|
module M = Reventless.AutomationSlice.Mappings.Make(SkipProcessSpec)
|
|
122
124
|
module type Mapping = M.Mapping
|
|
@@ -168,6 +168,10 @@ function process(id, _item) {
|
|
|
168
168
|
];
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
function onExhausted(_id, _item) {
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
171
175
|
let moduleUrl$2 = import.meta.url;
|
|
172
176
|
|
|
173
177
|
AutomationSlice$Reventless.Mappings.Make(ShipOrderSpec);
|
|
@@ -176,6 +180,7 @@ let mappings = [ShipOrderMapping];
|
|
|
176
180
|
|
|
177
181
|
let ShipOrderAutomation = {
|
|
178
182
|
process: process,
|
|
183
|
+
onExhausted: onExhausted,
|
|
179
184
|
moduleUrl: moduleUrl$2,
|
|
180
185
|
mappings: mappings
|
|
181
186
|
};
|
|
@@ -184,6 +189,10 @@ function process$1(_id, _item) {
|
|
|
184
189
|
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
function onExhausted$1(_id, _item) {
|
|
193
|
+
|
|
194
|
+
}
|
|
195
|
+
|
|
187
196
|
let moduleUrl$3 = import.meta.url;
|
|
188
197
|
|
|
189
198
|
AutomationSlice$Reventless.Mappings.Make(SkipProcessSpec);
|
|
@@ -192,6 +201,7 @@ let mappings$1 = [SkipProcessMapping];
|
|
|
192
201
|
|
|
193
202
|
let SkipProcessAutomation = {
|
|
194
203
|
process: process$1,
|
|
204
|
+
onExhausted: onExhausted$1,
|
|
195
205
|
moduleUrl: moduleUrl$3,
|
|
196
206
|
mappings: mappings$1
|
|
197
207
|
};
|
|
@@ -158,6 +158,7 @@ module FromDcb = AutomationSlice.Mapping.Make(
|
|
|
158
158
|
module AutoShipAutomation: AutomationSlice.Automation with module Spec := AutoShipSpec = {
|
|
159
159
|
let process = (id, _item: AutoShipSpec.todoItem) =>
|
|
160
160
|
Some((id, AutoShipSpec.Ship({orderId: id})))
|
|
161
|
+
let onExhausted = (_id, _item: AutoShipSpec.todoItem) => None
|
|
161
162
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
162
163
|
module M = AutomationSlice.Mappings.Make(AutoShipSpec)
|
|
163
164
|
module type Mapping = M.Mapping
|
|
@@ -264,6 +264,10 @@ function process(id, _item) {
|
|
|
264
264
|
];
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
function onExhausted(_id, _item) {
|
|
268
|
+
|
|
269
|
+
}
|
|
270
|
+
|
|
267
271
|
let moduleUrl$5 = import.meta.url;
|
|
268
272
|
|
|
269
273
|
AutomationSlice$Reventless.Mappings.Make(AutoShipSpec);
|
|
@@ -272,6 +276,7 @@ let mappings = [FromDcb];
|
|
|
272
276
|
|
|
273
277
|
let AutoShipAutomation = {
|
|
274
278
|
process: process,
|
|
279
|
+
onExhausted: onExhausted,
|
|
275
280
|
moduleUrl: moduleUrl$5,
|
|
276
281
|
mappings: mappings
|
|
277
282
|
};
|
|
@@ -117,6 +117,7 @@ module AutoFulfillAutomation: AutomationSlice.Automation
|
|
|
117
117
|
with module Spec := AutoFulfillSpec = {
|
|
118
118
|
let process = (id, item: AutoFulfillSpec.todoItem) =>
|
|
119
119
|
Some((id, AutoFulfillSpec.MarkFulfilled({orderId: item.orderId, productId: item.productId})))
|
|
120
|
+
let onExhausted = (_id, _item: AutoFulfillSpec.todoItem) => None
|
|
120
121
|
let moduleUrl: string = %raw(`import.meta.url`)
|
|
121
122
|
module M = AutomationSlice.Mappings.Make(AutoFulfillSpec)
|
|
122
123
|
module type Mapping = M.Mapping
|
|
@@ -149,6 +149,10 @@ function process(id, item) {
|
|
|
149
149
|
];
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
function onExhausted(_id, _item) {
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
152
156
|
let moduleUrl$1 = import.meta.url;
|
|
153
157
|
|
|
154
158
|
AutomationSlice$Reventless.Mappings.Make(AutoFulfillSpec);
|
|
@@ -160,6 +164,7 @@ let mappings = [
|
|
|
160
164
|
|
|
161
165
|
let AutoFulfillAutomation = {
|
|
162
166
|
process: process,
|
|
167
|
+
onExhausted: onExhausted,
|
|
163
168
|
moduleUrl: moduleUrl$1,
|
|
164
169
|
mappings: mappings
|
|
165
170
|
};
|
|
@@ -6,11 +6,13 @@ open OutboundTranslationSliceFixtures
|
|
|
6
6
|
module SendTrackingEmailTranslation = {
|
|
7
7
|
let collect = SendTrackingEmailSpec.collect
|
|
8
8
|
let translate = SendTrackingEmailSpec.translate
|
|
9
|
+
let onExhausted = SendTrackingEmailSpec.onExhausted
|
|
9
10
|
let moduleUrl = SendTrackingEmailSpec.moduleUrl
|
|
10
11
|
}
|
|
11
12
|
module ProcessPaymentTranslation = {
|
|
12
13
|
let collect = ProcessPaymentSpec.collect
|
|
13
14
|
let translate = ProcessPaymentSpec.translate
|
|
15
|
+
let onExhausted = ProcessPaymentSpec.onExhausted
|
|
14
16
|
let moduleUrl = ProcessPaymentSpec.moduleUrl
|
|
15
17
|
}
|
|
16
18
|
module FireForgetCallback = ReventlessCore.OutboundTranslationSlice_Callback.Make(
|
|
@@ -31,6 +33,8 @@ describe("OutboundTranslationSlice Callback", () => {
|
|
|
31
33
|
// Reset the translate function to its default
|
|
32
34
|
ProcessPaymentSpec.translateFn :=
|
|
33
35
|
(async (id, _item) => Ok(Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id})))))
|
|
36
|
+
// Default is a slice with nothing to say when its budget runs out.
|
|
37
|
+
ProcessPaymentSpec.onExhaustedFn := ((_id, _item, _lastError) => None)
|
|
34
38
|
})
|
|
35
39
|
|
|
36
40
|
describe("Phase 1 — collect", () => {
|
|
@@ -157,11 +161,17 @@ describe("OutboundTranslationSlice Callback", () => {
|
|
|
157
161
|
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
158
162
|
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
|
|
159
163
|
|
|
160
|
-
// Fail
|
|
164
|
+
// Fail once — still retriable, and the status says so.
|
|
161
165
|
await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
166
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)
|
|
167
|
+
->toBe(Failed)
|
|
168
|
+
|
|
169
|
+
// The second failure spends the budget, and is marked at that moment rather
|
|
170
|
+
// than by falling out of the next pass's filter.
|
|
162
171
|
await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
163
172
|
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
164
173
|
expect(row.retryCount)->toBe(2)
|
|
174
|
+
expect(row.status)->toBe(Abandoned)
|
|
165
175
|
|
|
166
176
|
// Third attempt should not be tried
|
|
167
177
|
let publishedCommands = ref([])
|
|
@@ -173,6 +183,130 @@ describe("OutboundTranslationSlice Callback", () => {
|
|
|
173
183
|
let row2 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
174
184
|
// retryCount should still be 2 — not retried
|
|
175
185
|
expect(row2.retryCount)->toBe(2)
|
|
186
|
+
expect(row2.status)->toBe(Abandoned)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// What an older build wrote, and what a Spec that lowers maxRetries leaves
|
|
190
|
+
// behind: Failed at the ceiling, a status promising a retry no pass will make.
|
|
191
|
+
testPromise("a row left stranded at the ceiling is normalised on the next pass", async () => {
|
|
192
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("timeout"))
|
|
193
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
194
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
195
|
+
CommandBackCallback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 2})
|
|
196
|
+
|
|
197
|
+
await CommandBackCallback.phase2(
|
|
198
|
+
async _cmds => (),
|
|
199
|
+
~capabilities=Reventless.Capabilities.none,
|
|
200
|
+
)
|
|
201
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)
|
|
202
|
+
->toBe(Abandoned)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
testPromise("the last error survives the transition to Abandoned", async () => {
|
|
206
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
|
|
207
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
208
|
+
let publish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
|
|
209
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
210
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
211
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
212
|
+
expect(row.status)->toBe(Abandoned)
|
|
213
|
+
expect(row.lastError)->toEqual(Some("gateway down"))
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
testPromise("the row carries the ceiling its count is racing", async () => {
|
|
217
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
218
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).maxRetries)
|
|
219
|
+
->toEqual(Some(ProcessPaymentSpec.maxRetries))
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// Abandonment is an outcome, and a slice that has one to report gets to say
|
|
223
|
+
// so — the row going quiet is otherwise the only trace.
|
|
224
|
+
testPromise("a slice that answers onExhausted has its command published", async () => {
|
|
225
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
|
|
226
|
+
ProcessPaymentSpec.onExhaustedFn :=
|
|
227
|
+
((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
|
|
228
|
+
|
|
229
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
230
|
+
let published = ref([])
|
|
231
|
+
let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
|
|
232
|
+
published := Array.concat(published.contents, cmds)
|
|
233
|
+
|
|
234
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
235
|
+
expect(published.contents->Array.length)->toBe(0) // still retriable
|
|
236
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
237
|
+
|
|
238
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)
|
|
239
|
+
->toBe(Abandoned)
|
|
240
|
+
expect(published.contents->Array.length)->toBe(1)
|
|
241
|
+
expect((published.contents->Array.getUnsafe(0)).id)->toBe("ord-1")
|
|
242
|
+
// The target's name, not the slice's — the same rule the success path follows.
|
|
243
|
+
expect((published.contents->Array.getUnsafe(0)).meta.service)->toBe("ConfirmPayment")
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
testPromise("the hook is handed the error that ended it", async () => {
|
|
247
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
|
|
248
|
+
let seen = ref(None)
|
|
249
|
+
ProcessPaymentSpec.onExhaustedFn :=
|
|
250
|
+
((_id, _item, lastError) => {
|
|
251
|
+
seen := lastError
|
|
252
|
+
None
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
256
|
+
let publish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
|
|
257
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
258
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
259
|
+
expect(seen.contents)->toEqual(Some("gateway down"))
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
testPromise("a silent slice publishes nothing, and the row is still Abandoned", async () => {
|
|
263
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
|
|
264
|
+
// onExhaustedFn stays at its default None (reset in beforeEach).
|
|
265
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
266
|
+
let published = ref([])
|
|
267
|
+
let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
|
|
268
|
+
published := Array.concat(published.contents, cmds)
|
|
269
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
270
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
271
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
272
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)
|
|
273
|
+
->toBe(Abandoned)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// The budget is what ran out, so there is nothing left to retry with — a row
|
|
277
|
+
// must not fall back to Failed and re-enter the sweep on a publish error.
|
|
278
|
+
testPromise("a failed announcement leaves the row Abandoned", async () => {
|
|
279
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
|
|
280
|
+
ProcessPaymentSpec.onExhaustedFn :=
|
|
281
|
+
((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
|
|
282
|
+
|
|
283
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
284
|
+
let failOnAbandonment: ReventlessInfra.CommandTopic.publishJsons = async _cmds =>
|
|
285
|
+
JsError.throwWithMessage("topic unavailable")
|
|
286
|
+
await CommandBackCallback.phase2(failOnAbandonment, ~capabilities=Reventless.Capabilities.none)
|
|
287
|
+
await CommandBackCallback.phase2(failOnAbandonment, ~capabilities=Reventless.Capabilities.none)
|
|
288
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
289
|
+
expect(row.status)->toBe(Abandoned)
|
|
290
|
+
expect(row.retryCount)->toBe(2)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// A row an older build stranded reaches the hook too — it is being abandoned
|
|
294
|
+
// now, as far as anything downstream is concerned.
|
|
295
|
+
testPromise("a stranded row announces when it is normalised", async () => {
|
|
296
|
+
ProcessPaymentSpec.onExhaustedFn :=
|
|
297
|
+
((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
|
|
298
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
299
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
300
|
+
CommandBackCallback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 2})
|
|
301
|
+
|
|
302
|
+
let published = ref([])
|
|
303
|
+
await CommandBackCallback.phase2(
|
|
304
|
+
async cmds => published := cmds,
|
|
305
|
+
~capabilities=Reventless.Capabilities.none,
|
|
306
|
+
)
|
|
307
|
+
expect(published.contents->Array.length)->toBe(1)
|
|
308
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)
|
|
309
|
+
->toBe(Abandoned)
|
|
176
310
|
})
|
|
177
311
|
|
|
178
312
|
testPromise("individual item failure does not affect other items", async () => {
|
package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
5
6
|
import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
|
|
6
7
|
import * as OutboundTranslationSliceFixtures$ReventlessLocal from "./OutboundTranslationSliceFixtures.res.mjs";
|
|
7
8
|
import * as OutboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
|
|
@@ -10,11 +11,14 @@ let collect = OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmail
|
|
|
10
11
|
|
|
11
12
|
let translate = OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmailSpec.translate;
|
|
12
13
|
|
|
14
|
+
let onExhausted = OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmailSpec.onExhausted;
|
|
15
|
+
|
|
13
16
|
let moduleUrl = OutboundTranslationSliceFixtures$ReventlessLocal.SendTrackingEmailSpec.moduleUrl;
|
|
14
17
|
|
|
15
18
|
let SendTrackingEmailTranslation = {
|
|
16
19
|
collect: collect,
|
|
17
20
|
translate: translate,
|
|
21
|
+
onExhausted: onExhausted,
|
|
18
22
|
moduleUrl: moduleUrl
|
|
19
23
|
};
|
|
20
24
|
|
|
@@ -22,11 +26,14 @@ let collect$1 = OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentS
|
|
|
22
26
|
|
|
23
27
|
let translate$1 = OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.translate;
|
|
24
28
|
|
|
29
|
+
let onExhausted$1 = OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.onExhausted;
|
|
30
|
+
|
|
25
31
|
let moduleUrl$1 = OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.moduleUrl;
|
|
26
32
|
|
|
27
33
|
let ProcessPaymentTranslation = {
|
|
28
34
|
collect: collect$1,
|
|
29
35
|
translate: translate$1,
|
|
36
|
+
onExhausted: onExhausted$1,
|
|
30
37
|
moduleUrl: moduleUrl$1
|
|
31
38
|
};
|
|
32
39
|
|
|
@@ -70,6 +77,7 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
|
|
|
70
77
|
}
|
|
71
78
|
]
|
|
72
79
|
});
|
|
80
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.onExhaustedFn.contents = (_id, _item, _lastError) => {};
|
|
73
81
|
});
|
|
74
82
|
globalThis.describe("Phase 1 — collect", () => {
|
|
75
83
|
globalThis.test("OrderShipped event creates a pending outbound item", async () => {
|
|
@@ -276,9 +284,11 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
|
|
|
276
284
|
]]);
|
|
277
285
|
let mockPublish = async _cmds => {};
|
|
278
286
|
await CommandBackCallback.phase2(mockPublish, Capabilities$Reventless.none);
|
|
287
|
+
globalThis.expect(Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined).status).toBe("Failed");
|
|
279
288
|
await CommandBackCallback.phase2(mockPublish, Capabilities$Reventless.none);
|
|
280
289
|
let row = Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined);
|
|
281
290
|
globalThis.expect(row.retryCount).toBe(2);
|
|
291
|
+
globalThis.expect(row.status).toBe("Abandoned");
|
|
282
292
|
let publishedCommands = {
|
|
283
293
|
contents: []
|
|
284
294
|
};
|
|
@@ -289,6 +299,198 @@ globalThis.describe("OutboundTranslationSlice Callback", () => {
|
|
|
289
299
|
globalThis.expect(publishedCommands.contents.length).toBe(0);
|
|
290
300
|
let row2 = Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined);
|
|
291
301
|
globalThis.expect(row2.retryCount).toBe(2);
|
|
302
|
+
globalThis.expect(row2.status).toBe("Abandoned");
|
|
303
|
+
});
|
|
304
|
+
globalThis.test("a row left stranded at the ceiling is normalised on the next pass", async () => {
|
|
305
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.translateFn.contents = async (_id, _item) => ({
|
|
306
|
+
TAG: "Error",
|
|
307
|
+
_0: "timeout"
|
|
308
|
+
});
|
|
309
|
+
CommandBackCallback.phase1([[
|
|
310
|
+
"evt",
|
|
311
|
+
{
|
|
312
|
+
TAG: "PaymentReceived",
|
|
313
|
+
orderId: "ord-1",
|
|
314
|
+
amount: 50.0
|
|
315
|
+
}
|
|
316
|
+
]]);
|
|
317
|
+
let row = Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined);
|
|
318
|
+
let newrecord = {...row};
|
|
319
|
+
newrecord.retryCount = 2;
|
|
320
|
+
newrecord.status = "Failed";
|
|
321
|
+
CommandBackCallback.todoItems["ord-1"] = newrecord;
|
|
322
|
+
await CommandBackCallback.phase2(async _cmds => {}, Capabilities$Reventless.none);
|
|
323
|
+
globalThis.expect(Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined).status).toBe("Abandoned");
|
|
324
|
+
});
|
|
325
|
+
globalThis.test("the last error survives the transition to Abandoned", async () => {
|
|
326
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.translateFn.contents = async (_id, _item) => ({
|
|
327
|
+
TAG: "Error",
|
|
328
|
+
_0: "gateway down"
|
|
329
|
+
});
|
|
330
|
+
CommandBackCallback.phase1([[
|
|
331
|
+
"evt",
|
|
332
|
+
{
|
|
333
|
+
TAG: "PaymentReceived",
|
|
334
|
+
orderId: "ord-1",
|
|
335
|
+
amount: 50.0
|
|
336
|
+
}
|
|
337
|
+
]]);
|
|
338
|
+
let publish = async _cmds => {};
|
|
339
|
+
await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
|
|
340
|
+
await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
|
|
341
|
+
let row = Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined);
|
|
342
|
+
globalThis.expect(row.status).toBe("Abandoned");
|
|
343
|
+
globalThis.expect(row.lastError).toEqual("gateway down");
|
|
344
|
+
});
|
|
345
|
+
globalThis.test("the row carries the ceiling its count is racing", async () => {
|
|
346
|
+
CommandBackCallback.phase1([[
|
|
347
|
+
"evt",
|
|
348
|
+
{
|
|
349
|
+
TAG: "PaymentReceived",
|
|
350
|
+
orderId: "ord-1",
|
|
351
|
+
amount: 50.0
|
|
352
|
+
}
|
|
353
|
+
]]);
|
|
354
|
+
globalThis.expect(Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined).maxRetries).toEqual(OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.maxRetries);
|
|
355
|
+
});
|
|
356
|
+
globalThis.test("a slice that answers onExhausted has its command published", async () => {
|
|
357
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.translateFn.contents = async (_id, _item) => ({
|
|
358
|
+
TAG: "Error",
|
|
359
|
+
_0: "gateway down"
|
|
360
|
+
});
|
|
361
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.onExhaustedFn.contents = (id, _item, _lastError) => [
|
|
362
|
+
id,
|
|
363
|
+
{
|
|
364
|
+
TAG: "ConfirmPayment",
|
|
365
|
+
orderId: id
|
|
366
|
+
}
|
|
367
|
+
];
|
|
368
|
+
CommandBackCallback.phase1([[
|
|
369
|
+
"evt",
|
|
370
|
+
{
|
|
371
|
+
TAG: "PaymentReceived",
|
|
372
|
+
orderId: "ord-1",
|
|
373
|
+
amount: 50.0
|
|
374
|
+
}
|
|
375
|
+
]]);
|
|
376
|
+
let published = {
|
|
377
|
+
contents: []
|
|
378
|
+
};
|
|
379
|
+
let publish = async cmds => {
|
|
380
|
+
published.contents = published.contents.concat(cmds);
|
|
381
|
+
};
|
|
382
|
+
await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
|
|
383
|
+
globalThis.expect(published.contents.length).toBe(0);
|
|
384
|
+
await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
|
|
385
|
+
globalThis.expect(Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined).status).toBe("Abandoned");
|
|
386
|
+
globalThis.expect(published.contents.length).toBe(1);
|
|
387
|
+
globalThis.expect(published.contents[0].id).toBe("ord-1");
|
|
388
|
+
globalThis.expect(published.contents[0].meta.service).toBe("ConfirmPayment");
|
|
389
|
+
});
|
|
390
|
+
globalThis.test("the hook is handed the error that ended it", async () => {
|
|
391
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.translateFn.contents = async (_id, _item) => ({
|
|
392
|
+
TAG: "Error",
|
|
393
|
+
_0: "gateway down"
|
|
394
|
+
});
|
|
395
|
+
let seen = {
|
|
396
|
+
contents: undefined
|
|
397
|
+
};
|
|
398
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.onExhaustedFn.contents = (_id, _item, lastError) => {
|
|
399
|
+
seen.contents = lastError;
|
|
400
|
+
};
|
|
401
|
+
CommandBackCallback.phase1([[
|
|
402
|
+
"evt",
|
|
403
|
+
{
|
|
404
|
+
TAG: "PaymentReceived",
|
|
405
|
+
orderId: "ord-1",
|
|
406
|
+
amount: 50.0
|
|
407
|
+
}
|
|
408
|
+
]]);
|
|
409
|
+
let publish = async _cmds => {};
|
|
410
|
+
await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
|
|
411
|
+
await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
|
|
412
|
+
globalThis.expect(seen.contents).toEqual("gateway down");
|
|
413
|
+
});
|
|
414
|
+
globalThis.test("a silent slice publishes nothing, and the row is still Abandoned", async () => {
|
|
415
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.translateFn.contents = async (_id, _item) => ({
|
|
416
|
+
TAG: "Error",
|
|
417
|
+
_0: "gateway down"
|
|
418
|
+
});
|
|
419
|
+
CommandBackCallback.phase1([[
|
|
420
|
+
"evt",
|
|
421
|
+
{
|
|
422
|
+
TAG: "PaymentReceived",
|
|
423
|
+
orderId: "ord-1",
|
|
424
|
+
amount: 50.0
|
|
425
|
+
}
|
|
426
|
+
]]);
|
|
427
|
+
let published = {
|
|
428
|
+
contents: []
|
|
429
|
+
};
|
|
430
|
+
let publish = async cmds => {
|
|
431
|
+
published.contents = published.contents.concat(cmds);
|
|
432
|
+
};
|
|
433
|
+
await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
|
|
434
|
+
await CommandBackCallback.phase2(publish, Capabilities$Reventless.none);
|
|
435
|
+
globalThis.expect(published.contents.length).toBe(0);
|
|
436
|
+
globalThis.expect(Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined).status).toBe("Abandoned");
|
|
437
|
+
});
|
|
438
|
+
globalThis.test("a failed announcement leaves the row Abandoned", async () => {
|
|
439
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.translateFn.contents = async (_id, _item) => ({
|
|
440
|
+
TAG: "Error",
|
|
441
|
+
_0: "gateway down"
|
|
442
|
+
});
|
|
443
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.onExhaustedFn.contents = (id, _item, _lastError) => [
|
|
444
|
+
id,
|
|
445
|
+
{
|
|
446
|
+
TAG: "ConfirmPayment",
|
|
447
|
+
orderId: id
|
|
448
|
+
}
|
|
449
|
+
];
|
|
450
|
+
CommandBackCallback.phase1([[
|
|
451
|
+
"evt",
|
|
452
|
+
{
|
|
453
|
+
TAG: "PaymentReceived",
|
|
454
|
+
orderId: "ord-1",
|
|
455
|
+
amount: 50.0
|
|
456
|
+
}
|
|
457
|
+
]]);
|
|
458
|
+
let failOnAbandonment = async _cmds => Stdlib_JsError.throwWithMessage("topic unavailable");
|
|
459
|
+
await CommandBackCallback.phase2(failOnAbandonment, Capabilities$Reventless.none);
|
|
460
|
+
await CommandBackCallback.phase2(failOnAbandonment, Capabilities$Reventless.none);
|
|
461
|
+
let row = Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined);
|
|
462
|
+
globalThis.expect(row.status).toBe("Abandoned");
|
|
463
|
+
globalThis.expect(row.retryCount).toBe(2);
|
|
464
|
+
});
|
|
465
|
+
globalThis.test("a stranded row announces when it is normalised", async () => {
|
|
466
|
+
OutboundTranslationSliceFixtures$ReventlessLocal.ProcessPaymentSpec.onExhaustedFn.contents = (id, _item, _lastError) => [
|
|
467
|
+
id,
|
|
468
|
+
{
|
|
469
|
+
TAG: "ConfirmPayment",
|
|
470
|
+
orderId: id
|
|
471
|
+
}
|
|
472
|
+
];
|
|
473
|
+
CommandBackCallback.phase1([[
|
|
474
|
+
"evt",
|
|
475
|
+
{
|
|
476
|
+
TAG: "PaymentReceived",
|
|
477
|
+
orderId: "ord-1",
|
|
478
|
+
amount: 50.0
|
|
479
|
+
}
|
|
480
|
+
]]);
|
|
481
|
+
let row = Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined);
|
|
482
|
+
let newrecord = {...row};
|
|
483
|
+
newrecord.retryCount = 2;
|
|
484
|
+
newrecord.status = "Failed";
|
|
485
|
+
CommandBackCallback.todoItems["ord-1"] = newrecord;
|
|
486
|
+
let published = {
|
|
487
|
+
contents: []
|
|
488
|
+
};
|
|
489
|
+
await CommandBackCallback.phase2(async cmds => {
|
|
490
|
+
published.contents = cmds;
|
|
491
|
+
}, Capabilities$Reventless.none);
|
|
492
|
+
globalThis.expect(published.contents.length).toBe(1);
|
|
493
|
+
globalThis.expect(Stdlib_Option.getOrThrow(CommandBackCallback.todoItems["ord-1"], undefined).status).toBe("Abandoned");
|
|
292
494
|
});
|
|
293
495
|
globalThis.test("individual item failure does not affect other items", async () => {
|
|
294
496
|
let callCount = {
|
|
@@ -36,6 +36,7 @@ module SendTrackingEmailSpec = {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
let translate = async (_id, _item, ~capabilities as _) => Ok(None)
|
|
39
|
+
let onExhausted = (_id, _item, ~lastError as _) => None
|
|
39
40
|
|
|
40
41
|
let maxRetries = 3
|
|
41
42
|
let heartbeatInterval = 60
|
|
@@ -77,6 +78,13 @@ module ProcessPaymentSpec = {
|
|
|
77
78
|
// one restate a type it does not use.
|
|
78
79
|
let translate = (id, item, ~capabilities as _) => translateFn.contents(id, item)
|
|
79
80
|
|
|
81
|
+
// Overridable like `translateFn`, so a test can assert both answers a slice may
|
|
82
|
+
// give when its budget runs out: say nothing, or tell the domain.
|
|
83
|
+
let onExhaustedFn: ref<(string, outboundItem, option<string>) => option<(string, inboundCommand)>> = ref(
|
|
84
|
+
(_id, _item, _lastError) => None
|
|
85
|
+
)
|
|
86
|
+
let onExhausted = (id, item, ~lastError) => onExhaustedFn.contents(id, item, lastError)
|
|
87
|
+
|
|
80
88
|
let maxRetries = 2
|
|
81
89
|
let heartbeatInterval = 30
|
|
82
90
|
let targetName = Some("ConfirmPayment")
|
|
@@ -56,6 +56,10 @@ async function translate(_id, _item, param) {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function onExhausted(_id, _item, param) {
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
59
63
|
let sourceNames = [];
|
|
60
64
|
|
|
61
65
|
let SendTrackingEmailSpec = {
|
|
@@ -66,6 +70,7 @@ let SendTrackingEmailSpec = {
|
|
|
66
70
|
inboundCommandSchema: inboundCommandSchema,
|
|
67
71
|
collect: collect,
|
|
68
72
|
translate: translate,
|
|
73
|
+
onExhausted: onExhausted,
|
|
69
74
|
maxRetries: 3,
|
|
70
75
|
heartbeatInterval: 60,
|
|
71
76
|
targetName: undefined,
|
|
@@ -119,6 +124,14 @@ function translate$1(id, item, param) {
|
|
|
119
124
|
return translateFn.contents(id, item);
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
let onExhaustedFn = {
|
|
128
|
+
contents: (_id, _item, _lastError) => {}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function onExhausted$1(id, item, lastError) {
|
|
132
|
+
return onExhaustedFn.contents(id, item, lastError);
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
let sourceNames$1 = [];
|
|
123
136
|
|
|
124
137
|
let ProcessPaymentSpec_targetName = "ConfirmPayment";
|
|
@@ -132,6 +145,8 @@ let ProcessPaymentSpec = {
|
|
|
132
145
|
collect: collect$1,
|
|
133
146
|
translateFn: translateFn,
|
|
134
147
|
translate: translate$1,
|
|
148
|
+
onExhaustedFn: onExhaustedFn,
|
|
149
|
+
onExhausted: onExhausted$1,
|
|
135
150
|
maxRetries: 2,
|
|
136
151
|
heartbeatInterval: 30,
|
|
137
152
|
targetName: ProcessPaymentSpec_targetName,
|
package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res
CHANGED
|
@@ -111,6 +111,8 @@ module SendConfirmTranslation: OutboundTranslationSlice.Translation
|
|
|
111
111
|
externalCalls->Array.push(item.orderId)
|
|
112
112
|
Ok(None)
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
let onExhausted = (_id, _item: SendConfirmSpec.outboundItem, ~lastError as _) => None
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
// ─────────────────────────────────────────────────────────────
|
package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs
CHANGED
|
@@ -141,9 +141,14 @@ async function translate(_id, item, param) {
|
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
function onExhausted(_id, _item, param) {
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
|
|
144
148
|
let SendConfirmTranslation = {
|
|
145
149
|
collect: collect,
|
|
146
150
|
translate: translate,
|
|
151
|
+
onExhausted: onExhausted,
|
|
147
152
|
moduleUrl: moduleUrl$3
|
|
148
153
|
};
|
|
149
154
|
|