@reventlessdev/reventless-local 3.0.0-alpha.233 → 3.0.0-alpha.235
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 +10 -10
- package/src/adapter/DomainGraphQL_Server.res +36 -0
- package/src/adapter/DomainGraphQL_Server.res.mjs +40 -1
- package/src/adapter/GraphQL_Server.res.mjs +6 -0
- package/src/adapter/PlatformGraphQL_Server.res +5 -0
- package/src/adapter/PlatformGraphQL_Server.res.mjs +4 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +171 -2
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +118 -1
- package/tests/adapter/QueryDbListResolverTest.res +150 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +133 -0
- package/tests/adapter/QueryDbResolvedFieldsTest.res +253 -0
- package/tests/adapter/QueryDbResolvedFieldsTest.res.mjs +291 -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
|
@@ -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
|
|