@reventlessdev/reventless-local 3.0.0-alpha.249 → 3.0.0-alpha.250
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 +8 -0
- package/package.json +13 -13
- package/src/BakedManifest.res +5 -6
- package/src/EmitCapabilities.res +3 -1
- package/src/LocalPlatformRegistry.res +3 -1
- package/src/LocalPlatformStart.res +7 -6
- package/src/LocalSeedTarget.res +3 -1
- package/src/Platform.res +291 -215
- package/src/ShellConfig.res +10 -17
- package/src/UiHints.res +8 -14
- package/src/UiSlots.res +10 -17
- package/src/adapter/Api/LocalEvents_Server.res +5 -3
- package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
- package/src/adapter/Auth/LocalAuth.res +11 -15
- package/src/adapter/Auth/UserStore.res +5 -7
- package/src/adapter/Backend.res +1 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
- package/src/adapter/Counter/LocalCounterHandler.res +1 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
- package/src/adapter/DomainGraphQL_Server.res +35 -34
- package/src/adapter/DomainMCP_Server.res +45 -41
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
- package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
- package/src/adapter/LocalBus.res +12 -12
- package/src/adapter/LocalCapabilities.res.mjs +1 -1
- package/src/adapter/LocalGeocodeResolvers.res +1 -4
- package/src/adapter/LocalSideEffectHandler.res +1 -1
- package/src/adapter/LocalStateChangeDescriptor.res +2 -1
- package/src/adapter/MCP_ServerInstance.res +51 -39
- package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
- package/src/adapter/PlatformGraphQL_Server.res +9 -5
- package/src/adapter/ProjectionCheckpoint.res +8 -1
- package/src/adapter/ProjectionPending.res +1 -3
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
- package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
- package/src/adapter/SqliteDriver.res +16 -4
- package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
- package/src/components/Aggregate_Builder.res +6 -1
- package/src/components/Counter_Builder.res +1 -5
- package/src/components/DcbEventLog_Builder.res +5 -3
- package/src/components/ExtensionPoint_Builder.res +13 -15
- package/src/components/InboundTranslationSlice_Builder.res +3 -2
- package/src/components/OutboundTranslationSlice_Builder.res +3 -2
- package/src/components/ReadModel_Builder.res +18 -20
- package/src/components/StateViewSlice_Builder.res +3 -2
- package/src/components/Task_Builder.res +12 -13
- package/src/reset/LocalSeedReset.res +2 -5
- package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
- package/src/test/Mocks/MockEventLogStorage.res +8 -3
- package/src/test/Mocks/MockQueryDbStorage.res +3 -2
- package/src/test/TestRunner.res +1 -1
- package/tests/BakedManifestFilesTest.res +7 -10
- package/tests/LocalEventTapTest.res +26 -21
- package/tests/LocalPlatformStartTest.res +33 -28
- package/tests/PluginEventDecodeTest.res +3 -3
- package/tests/ShellConfigTest.res +30 -21
- package/tests/SplitApiFixtures.res +34 -10
- package/tests/SplitApiTest.res +92 -62
- package/tests/adapter/BackendParityTest.res +180 -130
- package/tests/adapter/CollectNEventsTest.res +6 -4
- package/tests/adapter/CommandAuthorizationTest.res +47 -41
- package/tests/adapter/CommandTopicChannelTest.res +163 -139
- package/tests/adapter/CounterHandlerTest.res +95 -77
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
- package/tests/adapter/DcbEventLogStorageTest.res +264 -207
- package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
- package/tests/adapter/EventCollectorChannelTest.res +153 -124
- package/tests/adapter/EventHistoryResolverTest.res +46 -18
- package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
- package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
- package/tests/adapter/EventTapTest.res +37 -20
- package/tests/adapter/EventTopicPublisherTest.res +74 -50
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
- package/tests/adapter/HeartbeatRunnerTest.res +99 -81
- package/tests/adapter/InboundTranslationMutationTest.res +4 -1
- package/tests/adapter/LocalAuthHttpTest.res +79 -85
- package/tests/adapter/LocalAuthLoginTest.res +42 -50
- package/tests/adapter/LocalAuthTest.res +8 -16
- package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
- package/tests/adapter/LocalBusBoundedTest.res +187 -117
- package/tests/adapter/LocalBusPubSubTest.res +118 -76
- package/tests/adapter/LocalDispatchContextTest.res +1 -2
- package/tests/adapter/LocalEvents_ServerTest.res +242 -187
- package/tests/adapter/PgProjectionCatchupTest.res +44 -10
- package/tests/adapter/ProjectionCheckpointTest.res +261 -206
- package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
- package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
- package/tests/adapter/QueryDbListResolverTest.res +225 -208
- package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
- package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
- package/tests/adapter/QueryDbStorageTest.res +163 -117
- package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
- package/tests/adapter/QueryEngineTest.res +179 -126
- package/tests/adapter/ScheduledPublisherTest.res +171 -135
- package/tests/adapter/ServedBucketHttpTest.res +65 -59
- package/tests/adapter/SortKey_FilterTest.res +19 -3
- package/tests/adapter/SqliteDriverTest.res +13 -10
- package/tests/adapter/TaskBucketTest.res +94 -80
- package/tests/components/aggregate/AggregateTest.res +31 -18
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
- package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
- package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
- package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
- package/tests/components/commandtopic/CommandTopicTest.res +24 -15
- package/tests/components/counter/CounterFixtures.res +6 -8
- package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
- package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
- package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
- package/tests/components/dcb/DcbFixtures.res +4 -1
- package/tests/components/dcb/DcbTest.res +9 -6
- package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
- package/tests/components/eventcollector/EventCollectorTest.res +6 -3
- package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
- package/tests/components/eventlog/EventLogConflictTest.res +8 -4
- package/tests/components/eventlog/EventLogFixtures.res +3 -3
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
- package/tests/components/eventlog/EventLogStreamTest.res +119 -56
- package/tests/components/eventlog/EventLogTest.res +20 -4
- package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
- package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
- package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
- package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
- package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
- package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
- package/tests/components/heartbeat/HeartbeatTest.res +56 -47
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
- package/tests/components/querydb/QueryDbFixtures.res +1 -5
- package/tests/components/querydb/QueryDbTest.res +2 -4
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
- package/tests/components/readmodel/ReadModelFixtures.res +8 -8
- package/tests/components/scheduler/SchedulerTest.res +76 -58
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
- package/tests/components/task/TaskFixtures.res +5 -5
- package/tests/components/task/TaskTest.res +95 -80
- package/tests/plugin/PluginsProjection_GWT.res +3 -1
- package/tests/plugin/UiFragments_GWT.res +6 -1
- package/tests/reset/LocalSeedResetTest.res +1 -0
|
@@ -28,8 +28,12 @@ open ReventlessCore.OutboundTranslationSlice_Callback
|
|
|
28
28
|
|
|
29
29
|
describe("OutboundTranslationSlice Callback", () => {
|
|
30
30
|
let _ = beforeEach(() => {
|
|
31
|
-
FireForgetCallback.todoItems
|
|
32
|
-
|
|
31
|
+
FireForgetCallback.todoItems
|
|
32
|
+
->Dict.keysToArray
|
|
33
|
+
->Array.forEach(k => FireForgetCallback.todoItems->Dict.delete(k))
|
|
34
|
+
CommandBackCallback.todoItems
|
|
35
|
+
->Dict.keysToArray
|
|
36
|
+
->Array.forEach(k => CommandBackCallback.todoItems->Dict.delete(k))
|
|
33
37
|
// Reset the translate function to its default
|
|
34
38
|
ProcessPaymentSpec.translateFn :=
|
|
35
39
|
(async (id, _item) => Ok(Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id})))))
|
|
@@ -38,304 +42,385 @@ describe("OutboundTranslationSlice Callback", () => {
|
|
|
38
42
|
})
|
|
39
43
|
|
|
40
44
|
describe("Phase 1 — collect", () => {
|
|
41
|
-
testPromise(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
testPromise(
|
|
46
|
+
"OrderShipped event creates a pending outbound item",
|
|
47
|
+
async () => {
|
|
48
|
+
FireForgetCallback.phase1([
|
|
49
|
+
("evt", OrderShipped({orderId: "ord-1", email: "test@example.com"})),
|
|
50
|
+
])
|
|
51
|
+
let items = FireForgetCallback.todoItems
|
|
52
|
+
expect(items->Dict.toArray->Array.length)->toBe(1)
|
|
53
|
+
let row = items->Dict.get("ord-1")
|
|
54
|
+
expect(row->Option.isSome)->toBe(true)
|
|
55
|
+
let r = row->Option.getOrThrow
|
|
56
|
+
expect(r.status)->toBe(Pending)
|
|
57
|
+
expect(r.retryCount)->toBe(0)
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
testPromise(
|
|
62
|
+
"duplicate event does not create a second item (idempotent)",
|
|
63
|
+
async () => {
|
|
64
|
+
FireForgetCallback.phase1([
|
|
65
|
+
("evt", OrderShipped({orderId: "ord-1", email: "test@example.com"})),
|
|
66
|
+
])
|
|
67
|
+
FireForgetCallback.phase1([
|
|
68
|
+
("evt", OrderShipped({orderId: "ord-1", email: "other@example.com"})),
|
|
69
|
+
])
|
|
70
|
+
let items = FireForgetCallback.todoItems
|
|
71
|
+
expect(items->Dict.toArray->Array.length)->toBe(1)
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
testPromise(
|
|
76
|
+
"multiple different events create multiple items",
|
|
77
|
+
async () => {
|
|
78
|
+
FireForgetCallback.phase1([
|
|
79
|
+
("evt", OrderShipped({orderId: "ord-1", email: "a@example.com"})),
|
|
80
|
+
("evt", OrderShipped({orderId: "ord-2", email: "b@example.com"})),
|
|
81
|
+
])
|
|
82
|
+
let items = FireForgetCallback.todoItems
|
|
83
|
+
expect(items->Dict.toArray->Array.length)->toBe(2)
|
|
84
|
+
},
|
|
85
|
+
)
|
|
68
86
|
})
|
|
69
87
|
|
|
70
88
|
describe("Phase 2 — translate (fire-and-forget)", () => {
|
|
71
|
-
testPromise(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
testPromise(
|
|
90
|
+
"Ok(None) marks item as Completed, no command published",
|
|
91
|
+
async () => {
|
|
92
|
+
FireForgetCallback.phase1([
|
|
93
|
+
("evt", OrderShipped({orderId: "ord-1", email: "test@example.com"})),
|
|
94
|
+
])
|
|
95
|
+
let publishedCommands = ref([])
|
|
96
|
+
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
|
|
97
|
+
publishedCommands := cmds
|
|
98
|
+
}
|
|
99
|
+
await FireForgetCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
100
|
+
expect(publishedCommands.contents->Array.length)->toBe(0)
|
|
101
|
+
let row = FireForgetCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
102
|
+
expect(row.status)->toBe(Completed)
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
testPromise(
|
|
107
|
+
"empty TODO list produces no commands",
|
|
108
|
+
async () => {
|
|
109
|
+
let publishedCommands = ref([])
|
|
110
|
+
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
|
|
111
|
+
publishedCommands := cmds
|
|
112
|
+
}
|
|
113
|
+
await FireForgetCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
114
|
+
expect(publishedCommands.contents->Array.length)->toBe(0)
|
|
115
|
+
},
|
|
116
|
+
)
|
|
91
117
|
})
|
|
92
118
|
|
|
93
119
|
describe("Phase 2 — translate (command-back)", () => {
|
|
94
|
-
testPromise(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
publishedCommands
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
testPromise(
|
|
121
|
+
"Ok(Some(...)) marks item as Completed and publishes command",
|
|
122
|
+
async () => {
|
|
123
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
124
|
+
let publishedCommands = ref([])
|
|
125
|
+
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
|
|
126
|
+
publishedCommands := Array.concat(publishedCommands.contents, cmds)
|
|
127
|
+
}
|
|
128
|
+
await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
129
|
+
expect(publishedCommands.contents->Array.length)->toBe(1)
|
|
130
|
+
let cmd = publishedCommands.contents->Array.getUnsafe(0)
|
|
131
|
+
expect(cmd.id)->toBe("ord-1")
|
|
132
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
133
|
+
expect(row.status)->toBe(Completed)
|
|
134
|
+
},
|
|
135
|
+
)
|
|
107
136
|
|
|
108
137
|
// The published command's `meta.service` must name the TARGET, not this
|
|
109
138
|
// slice. An aggregate derives its event's meta from the command's, and
|
|
110
139
|
// ReadModel/AutomationSlice callbacks dispatch mappings on `meta.service` —
|
|
111
140
|
// so a slice that stamps its own name here makes the target's events match
|
|
112
141
|
// no mapping and project as zero actions, with no error anywhere.
|
|
113
|
-
testPromise(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
publishedCommands
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
142
|
+
testPromise(
|
|
143
|
+
"published command is tagged with the target's name",
|
|
144
|
+
async () => {
|
|
145
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
146
|
+
let publishedCommands = ref([])
|
|
147
|
+
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
|
|
148
|
+
publishedCommands := Array.concat(publishedCommands.contents, cmds)
|
|
149
|
+
}
|
|
150
|
+
await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
151
|
+
let cmd = publishedCommands.contents->Array.getUnsafe(0)
|
|
152
|
+
expect(cmd.meta.service)->toBe("ConfirmPayment")
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
testPromise(
|
|
157
|
+
"Error marks item as Failed with incremented retryCount",
|
|
158
|
+
async () => {
|
|
159
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway timeout"))
|
|
160
|
+
|
|
161
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
162
|
+
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
|
|
163
|
+
await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
164
|
+
|
|
165
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
166
|
+
expect(row.status)->toBe(Failed)
|
|
167
|
+
expect(row.retryCount)->toBe(1)
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
testPromise(
|
|
172
|
+
"failed items with retryCount < maxRetries are retried",
|
|
173
|
+
async () => {
|
|
174
|
+
// First attempt fails
|
|
175
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("timeout"))
|
|
176
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
177
|
+
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
|
|
178
|
+
await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
179
|
+
let row1 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
180
|
+
expect(row1.retryCount)->toBe(1)
|
|
181
|
+
|
|
182
|
+
// Second attempt succeeds
|
|
183
|
+
ProcessPaymentSpec.translateFn :=
|
|
184
|
+
(async (id, _item) => Ok(Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id})))))
|
|
185
|
+
let publishedCommands = ref([])
|
|
186
|
+
let successPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
|
|
187
|
+
publishedCommands := cmds
|
|
188
|
+
}
|
|
189
|
+
await CommandBackCallback.phase2(successPublish, ~capabilities=Reventless.Capabilities.none)
|
|
190
|
+
expect(publishedCommands.contents->Array.length)->toBe(1)
|
|
191
|
+
let row2 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
192
|
+
expect(row2.status)->toBe(Completed)
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
testPromise(
|
|
197
|
+
"failed items beyond maxRetries are not retried",
|
|
198
|
+
async () => {
|
|
199
|
+
// ProcessPaymentSpec.maxRetries = 2, so after 2 failures it should stop
|
|
200
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("timeout"))
|
|
201
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
202
|
+
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
|
|
203
|
+
|
|
204
|
+
// Fail once — still retriable, and the status says so.
|
|
205
|
+
await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
206
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(
|
|
207
|
+
Failed,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
// The second failure spends the budget, and is marked at that moment rather
|
|
211
|
+
// than by falling out of the next pass's filter.
|
|
212
|
+
await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
213
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
214
|
+
expect(row.retryCount)->toBe(2)
|
|
215
|
+
expect(row.status)->toBe(Abandoned)
|
|
216
|
+
|
|
217
|
+
// Third attempt should not be tried
|
|
218
|
+
let publishedCommands = ref([])
|
|
219
|
+
let trackPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
|
|
220
|
+
publishedCommands := cmds
|
|
221
|
+
}
|
|
222
|
+
await CommandBackCallback.phase2(trackPublish, ~capabilities=Reventless.Capabilities.none)
|
|
223
|
+
expect(publishedCommands.contents->Array.length)->toBe(0)
|
|
224
|
+
let row2 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
225
|
+
// retryCount should still be 2 — not retried
|
|
226
|
+
expect(row2.retryCount)->toBe(2)
|
|
227
|
+
expect(row2.status)->toBe(Abandoned)
|
|
228
|
+
},
|
|
229
|
+
)
|
|
188
230
|
|
|
189
231
|
// What an older build wrote, and what a Spec that lowers maxRetries leaves
|
|
190
232
|
// behind: Failed at the ceiling, a status promising a retry no pass will make.
|
|
191
|
-
testPromise(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
233
|
+
testPromise(
|
|
234
|
+
"a row left stranded at the ceiling is normalised on the next pass",
|
|
235
|
+
async () => {
|
|
236
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("timeout"))
|
|
237
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
238
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
239
|
+
CommandBackCallback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 2})
|
|
240
|
+
|
|
241
|
+
await CommandBackCallback.phase2(
|
|
242
|
+
async _cmds => (),
|
|
243
|
+
~capabilities=Reventless.Capabilities.none,
|
|
244
|
+
)
|
|
245
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(
|
|
246
|
+
Abandoned,
|
|
247
|
+
)
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
testPromise(
|
|
252
|
+
"the last error survives the transition to Abandoned",
|
|
253
|
+
async () => {
|
|
254
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
|
|
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
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
260
|
+
expect(row.status)->toBe(Abandoned)
|
|
261
|
+
expect(row.lastError)->toEqual(Some("gateway down"))
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
testPromise(
|
|
266
|
+
"the row carries the ceiling its count is racing",
|
|
267
|
+
async () => {
|
|
268
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
269
|
+
expect(
|
|
270
|
+
(CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).maxRetries,
|
|
271
|
+
)->toEqual(Some(ProcessPaymentSpec.maxRetries))
|
|
272
|
+
},
|
|
273
|
+
)
|
|
221
274
|
|
|
222
275
|
// Abandonment is an outcome, and a slice that has one to report gets to say
|
|
223
276
|
// so — the row going quiet is otherwise the only trace.
|
|
224
|
-
testPromise(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
((
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
published
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
277
|
+
testPromise(
|
|
278
|
+
"a slice that answers onExhausted has its command published",
|
|
279
|
+
async () => {
|
|
280
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
|
|
281
|
+
ProcessPaymentSpec.onExhaustedFn :=
|
|
282
|
+
((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
|
|
283
|
+
|
|
284
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
285
|
+
let published = ref([])
|
|
286
|
+
let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
|
|
287
|
+
published := Array.concat(published.contents, cmds)
|
|
288
|
+
|
|
289
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
290
|
+
expect(published.contents->Array.length)->toBe(0) // still retriable
|
|
291
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
292
|
+
|
|
293
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(
|
|
294
|
+
Abandoned,
|
|
295
|
+
)
|
|
296
|
+
expect(published.contents->Array.length)->toBe(1)
|
|
297
|
+
expect((published.contents->Array.getUnsafe(0)).id)->toBe("ord-1")
|
|
298
|
+
// The target's name, not the slice's — the same rule the success path follows.
|
|
299
|
+
expect((published.contents->Array.getUnsafe(0)).meta.service)->toBe("ConfirmPayment")
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
testPromise(
|
|
304
|
+
"the hook is handed the error that ended it",
|
|
305
|
+
async () => {
|
|
306
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
|
|
307
|
+
let seen = ref(None)
|
|
308
|
+
ProcessPaymentSpec.onExhaustedFn :=
|
|
309
|
+
(
|
|
310
|
+
(_id, _item, lastError) => {
|
|
311
|
+
seen := lastError
|
|
312
|
+
None
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
317
|
+
let publish: ReventlessInfra.CommandTopic.publishJsons = async _cmds => ()
|
|
318
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
319
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
320
|
+
expect(seen.contents)->toEqual(Some("gateway down"))
|
|
321
|
+
},
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
testPromise(
|
|
325
|
+
"a silent slice publishes nothing, and the row is still Abandoned",
|
|
326
|
+
async () => {
|
|
327
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
|
|
328
|
+
// onExhaustedFn stays at its default None (reset in beforeEach).
|
|
329
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
330
|
+
let published = ref([])
|
|
331
|
+
let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds =>
|
|
332
|
+
published := Array.concat(published.contents, cmds)
|
|
333
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
334
|
+
await CommandBackCallback.phase2(publish, ~capabilities=Reventless.Capabilities.none)
|
|
335
|
+
expect(published.contents->Array.length)->toBe(0)
|
|
336
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(
|
|
337
|
+
Abandoned,
|
|
338
|
+
)
|
|
339
|
+
},
|
|
340
|
+
)
|
|
275
341
|
|
|
276
342
|
// The budget is what ran out, so there is nothing left to retry with — a row
|
|
277
343
|
// must not fall back to Failed and re-enter the sweep on a publish error.
|
|
278
|
-
testPromise(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
((
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
344
|
+
testPromise(
|
|
345
|
+
"a failed announcement leaves the row Abandoned",
|
|
346
|
+
async () => {
|
|
347
|
+
ProcessPaymentSpec.translateFn := (async (_id, _item) => Error("gateway down"))
|
|
348
|
+
ProcessPaymentSpec.onExhaustedFn :=
|
|
349
|
+
((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
|
|
350
|
+
|
|
351
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
352
|
+
let failOnAbandonment: ReventlessInfra.CommandTopic.publishJsons = async _cmds =>
|
|
353
|
+
JsError.throwWithMessage("topic unavailable")
|
|
354
|
+
await CommandBackCallback.phase2(
|
|
355
|
+
failOnAbandonment,
|
|
356
|
+
~capabilities=Reventless.Capabilities.none,
|
|
357
|
+
)
|
|
358
|
+
await CommandBackCallback.phase2(
|
|
359
|
+
failOnAbandonment,
|
|
360
|
+
~capabilities=Reventless.Capabilities.none,
|
|
361
|
+
)
|
|
362
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
363
|
+
expect(row.status)->toBe(Abandoned)
|
|
364
|
+
expect(row.retryCount)->toBe(2)
|
|
365
|
+
},
|
|
366
|
+
)
|
|
292
367
|
|
|
293
368
|
// A row an older build stranded reaches the hook too — it is being abandoned
|
|
294
369
|
// now, as far as anything downstream is concerned.
|
|
295
|
-
testPromise(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
370
|
+
testPromise(
|
|
371
|
+
"a stranded row announces when it is normalised",
|
|
372
|
+
async () => {
|
|
373
|
+
ProcessPaymentSpec.onExhaustedFn :=
|
|
374
|
+
((id, _item, _lastError) => Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
|
|
375
|
+
CommandBackCallback.phase1([("evt", PaymentReceived({orderId: "ord-1", amount: 50.0}))])
|
|
376
|
+
let row = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
377
|
+
CommandBackCallback.todoItems->Dict.set("ord-1", {...row, status: Failed, retryCount: 2})
|
|
378
|
+
|
|
379
|
+
let published = ref([])
|
|
380
|
+
await CommandBackCallback.phase2(
|
|
381
|
+
async cmds => published := cmds,
|
|
382
|
+
~capabilities=Reventless.Capabilities.none,
|
|
383
|
+
)
|
|
384
|
+
expect(published.contents->Array.length)->toBe(1)
|
|
385
|
+
expect((CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow).status)->toBe(
|
|
386
|
+
Abandoned,
|
|
387
|
+
)
|
|
388
|
+
},
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
testPromise(
|
|
392
|
+
"individual item failure does not affect other items",
|
|
393
|
+
async () => {
|
|
394
|
+
let callCount = ref(0)
|
|
395
|
+
ProcessPaymentSpec.translateFn :=
|
|
396
|
+
(
|
|
397
|
+
async (id, _item) => {
|
|
398
|
+
callCount := callCount.contents + 1
|
|
399
|
+
if callCount.contents == 1 {
|
|
400
|
+
Error("first item fails")
|
|
401
|
+
} else {
|
|
402
|
+
Ok(Some((id, ProcessPaymentSpec.ConfirmPayment({orderId: id}))))
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
CommandBackCallback.phase1([
|
|
408
|
+
("evt", PaymentReceived({orderId: "ord-1", amount: 50.0})),
|
|
409
|
+
("evt", PaymentReceived({orderId: "ord-2", amount: 75.0})),
|
|
410
|
+
])
|
|
411
|
+
let publishedCommands = ref([])
|
|
412
|
+
let mockPublish: ReventlessInfra.CommandTopic.publishJsons = async cmds => {
|
|
413
|
+
publishedCommands := Array.concat(publishedCommands.contents, cmds)
|
|
320
414
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
332
|
-
|
|
333
|
-
// ord-1 should be Failed, ord-2 should be Completed
|
|
334
|
-
let row1 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
335
|
-
expect(row1.status)->toBe(Failed)
|
|
336
|
-
let row2 = CommandBackCallback.todoItems->Dict.get("ord-2")->Option.getOrThrow
|
|
337
|
-
expect(row2.status)->toBe(Completed)
|
|
338
|
-
expect(publishedCommands.contents->Array.length)->toBe(1)
|
|
339
|
-
})
|
|
415
|
+
await CommandBackCallback.phase2(mockPublish, ~capabilities=Reventless.Capabilities.none)
|
|
416
|
+
|
|
417
|
+
// ord-1 should be Failed, ord-2 should be Completed
|
|
418
|
+
let row1 = CommandBackCallback.todoItems->Dict.get("ord-1")->Option.getOrThrow
|
|
419
|
+
expect(row1.status)->toBe(Failed)
|
|
420
|
+
let row2 = CommandBackCallback.todoItems->Dict.get("ord-2")->Option.getOrThrow
|
|
421
|
+
expect(row2.status)->toBe(Completed)
|
|
422
|
+
expect(publishedCommands.contents->Array.length)->toBe(1)
|
|
423
|
+
},
|
|
424
|
+
)
|
|
340
425
|
})
|
|
341
426
|
})
|