@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.
Files changed (27) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +10 -10
  3. package/src/adapter/DomainGraphQL_Server.res +36 -0
  4. package/src/adapter/DomainGraphQL_Server.res.mjs +40 -1
  5. package/src/adapter/GraphQL_Server.res.mjs +6 -0
  6. package/src/adapter/PlatformGraphQL_Server.res +5 -0
  7. package/src/adapter/PlatformGraphQL_Server.res.mjs +4 -0
  8. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +171 -2
  9. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +118 -1
  10. package/tests/adapter/QueryDbListResolverTest.res +150 -0
  11. package/tests/adapter/QueryDbListResolverTest.res.mjs +133 -0
  12. package/tests/adapter/QueryDbResolvedFieldsTest.res +253 -0
  13. package/tests/adapter/QueryDbResolvedFieldsTest.res.mjs +291 -0
  14. package/tests/components/automationslice/AutomationSliceCallbackTest.res +64 -0
  15. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +55 -0
  16. package/tests/components/automationslice/AutomationSliceFixtures.res +2 -0
  17. package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +10 -0
  18. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +1 -0
  19. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +5 -0
  20. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +1 -0
  21. package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +5 -0
  22. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +135 -1
  23. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +202 -0
  24. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -0
  25. package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +15 -0
  26. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +2 -0
  27. 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 twice
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 () => {
@@ -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,
@@ -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
  // ─────────────────────────────────────────────────────────────
@@ -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