@reventlessdev/reventless-gwt 1.0.0-alpha.237 → 1.0.0-alpha.239

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.
@@ -1,9 +1,15 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
3
7
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
4
9
  import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
5
10
  import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
6
11
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
12
+ import * as Capabilities_Fake$ReventlessGwt from "./Capabilities_Fake.res.mjs";
7
13
 
8
14
  function Make(Spec) {
9
15
  let test = (name, timeout, body) => JestBind$ReventlessGwt.testPromise(Spec.name, name, timeout, body);
@@ -181,7 +187,257 @@ function Make(Spec) {
181
187
  };
182
188
  }
183
189
 
190
+ function FromSlice(Spec) {
191
+ return Translation => {
192
+ let test = (name, timeout, body) => JestBind$ReventlessGwt.testPromise(Spec.name, name, timeout, body);
193
+ let testSync = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
194
+ let encItems = arr => arr.map(param => [
195
+ param[0],
196
+ Message$ReventlessCore.encode(param[1], Spec.outboundItemSchema)
197
+ ]);
198
+ let commandPairJson = (id, cmd) => {
199
+ let d = {};
200
+ d["id"] = id;
201
+ d["command"] = Message$ReventlessCore.encode(cmd, Spec.inboundCommandSchema);
202
+ return d;
203
+ };
204
+ let errorOf = exn => {
205
+ if (exn.RE_EXN_ID === S.Exn) {
206
+ return exn._1.message;
207
+ } else if (exn.RE_EXN_ID === "JsExn") {
208
+ return Stdlib_Option.getOr(Stdlib_JsExn.message(exn._1), "unknown error");
209
+ } else {
210
+ return "unknown error";
211
+ }
212
+ };
213
+ let givenEvent = e => e;
214
+ let whenCollect = (e, sourceIdOpt) => {
215
+ let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
216
+ return Translation.collect(e, sourceId);
217
+ };
218
+ let thenTodos = (actual, expected) => {
219
+ if (Primitive_object.equal(actual, expected)) {
220
+ return Outcome$ReventlessGwt.pass;
221
+ } else {
222
+ return Outcome$ReventlessGwt.fail({
223
+ TAG: "TodoMismatch",
224
+ expected: encItems(expected),
225
+ actual: encItems(actual)
226
+ });
227
+ }
228
+ };
229
+ let givenTodo = (id, item) => ({
230
+ id: id,
231
+ item: item,
232
+ fakes: Capabilities_Fake$ReventlessGwt.make(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined)
233
+ });
234
+ let givenCapabilities = (todo, fakes) => ({
235
+ id: todo.id,
236
+ item: todo.item,
237
+ fakes: fakes
238
+ });
239
+ let attemptWith = async (todo, run) => {
240
+ let result;
241
+ try {
242
+ result = await run(todo.id, todo.item);
243
+ } catch (raw_exn) {
244
+ let exn = Primitive_exceptions.internalToException(raw_exn);
245
+ result = {
246
+ TAG: "Error",
247
+ _0: errorOf(exn)
248
+ };
249
+ }
250
+ if (result.TAG !== "Ok") {
251
+ return result;
252
+ }
253
+ let match = result._0;
254
+ if (match === undefined) {
255
+ return result;
256
+ }
257
+ try {
258
+ Message$ReventlessCore.encode(match[1], Spec.inboundCommandSchema);
259
+ return result;
260
+ } catch (raw_exn$1) {
261
+ let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
262
+ return {
263
+ TAG: "Error",
264
+ _0: `failed to encode inbound command: ` + errorOf(exn$1)
265
+ };
266
+ }
267
+ };
268
+ let attempts = async (todo, limit, maxRetries, run) => {
269
+ let result = await attemptWith(todo, run);
270
+ let failures = Stdlib_Result.isError(result) ? 1 : 0;
271
+ while (Stdlib_Result.isError(result) && failures < limit) {
272
+ result = await attemptWith(todo, run);
273
+ if (Stdlib_Result.isError(result)) {
274
+ failures = failures + 1 | 0;
275
+ }
276
+ };
277
+ let match = result;
278
+ let tmp;
279
+ tmp = match.TAG === "Ok" ? "Completed" : (
280
+ failures >= maxRetries ? "Abandoned" : "Failed"
281
+ );
282
+ return {
283
+ id: todo.id,
284
+ item: todo.item,
285
+ result: result,
286
+ retries: failures,
287
+ status: tmp,
288
+ sent: todo.fakes.calls()
289
+ };
290
+ };
291
+ let whenTranslated = todo => attempts(todo, 1, Spec.maxRetries, (id, item) => Translation.translate(id, item, todo.fakes.capabilities));
292
+ let whenTranslateMocked = (todo, mock) => attempts(todo, 1, Spec.maxRetries, mock);
293
+ let whenTranslateRetrying = (todo, maxRetriesOpt, mock) => {
294
+ let maxRetries = maxRetriesOpt !== undefined ? maxRetriesOpt : Spec.maxRetries;
295
+ return attempts(todo, maxRetries, maxRetries, mock);
296
+ };
297
+ let whenExhausted = async (todo, lastError) => ({
298
+ id: todo.id,
299
+ item: todo.item,
300
+ result: {
301
+ TAG: "Ok",
302
+ _0: Translation.onExhausted(todo.id, todo.item, lastError)
303
+ },
304
+ retries: Spec.maxRetries,
305
+ status: "Abandoned",
306
+ sent: []
307
+ });
308
+ let thenCommand = async (pending, expectedId, expectedCmd) => {
309
+ let match = await pending;
310
+ let result = match.result;
311
+ if (result.TAG !== "Ok") {
312
+ return Outcome$ReventlessGwt.fail({
313
+ TAG: "TranslateError",
314
+ expected: "(command)",
315
+ actual: result._0
316
+ });
317
+ }
318
+ let actual = result._0;
319
+ if (actual !== undefined && actual[0] === expectedId && Primitive_object.equal(actual[1], expectedCmd)) {
320
+ return Outcome$ReventlessGwt.pass;
321
+ }
322
+ return Outcome$ReventlessGwt.fail({
323
+ TAG: "EventsMismatch",
324
+ expected: [commandPairJson(expectedId, expectedCmd)],
325
+ actual: Stdlib_Option.mapOr(actual, [], param => [commandPairJson(param[0], param[1])])
326
+ });
327
+ };
328
+ let thenNoCommand = async pending => {
329
+ let match = await pending;
330
+ let result = match.result;
331
+ if (result.TAG !== "Ok") {
332
+ return Outcome$ReventlessGwt.fail({
333
+ TAG: "TranslateError",
334
+ expected: "(no command)",
335
+ actual: result._0
336
+ });
337
+ }
338
+ let match$1 = result._0;
339
+ if (match$1 !== undefined) {
340
+ return Outcome$ReventlessGwt.fail({
341
+ TAG: "NoEventExpected",
342
+ actual: [commandPairJson(match$1[0], match$1[1])]
343
+ });
344
+ } else {
345
+ return Outcome$ReventlessGwt.pass;
346
+ }
347
+ };
348
+ let thenSent = async (pending, expected) => {
349
+ let match = await pending;
350
+ let sent = match.sent;
351
+ if (Primitive_object.equal(sent, expected)) {
352
+ return Outcome$ReventlessGwt.pass;
353
+ } else {
354
+ return Outcome$ReventlessGwt.fail({
355
+ TAG: "EventsMismatch",
356
+ expected: expected.map(Capabilities_Fake$ReventlessGwt.toJson),
357
+ actual: sent.map(Capabilities_Fake$ReventlessGwt.toJson)
358
+ });
359
+ }
360
+ };
361
+ let thenNothingSent = pending => thenSent(pending, []);
362
+ let thenRetryRecorded = async (pending, expectedRetries) => {
363
+ let match = await pending;
364
+ let retries = match.retries;
365
+ if (retries === expectedRetries) {
366
+ return Outcome$ReventlessGwt.pass;
367
+ } else {
368
+ return Outcome$ReventlessGwt.fail({
369
+ TAG: "StateMismatch",
370
+ key: "retries",
371
+ expected: expectedRetries,
372
+ actual: retries
373
+ });
374
+ }
375
+ };
376
+ let statusName = s => {
377
+ if (s === "Failed" || s === "Pending") {
378
+ return "#Failed";
379
+ } else if (s === "Abandoned") {
380
+ return "#Abandoned";
381
+ } else {
382
+ return "#Completed";
383
+ }
384
+ };
385
+ let thenTodoStatus = async (pending, expectedId, expectedStatus) => {
386
+ let match = await pending;
387
+ let status = match.status;
388
+ let id = match.id;
389
+ if (id === expectedId && statusName(status) === statusName(expectedStatus)) {
390
+ return Outcome$ReventlessGwt.pass;
391
+ }
392
+ let encOne = (rid, s) => Object.fromEntries([
393
+ [
394
+ "id",
395
+ rid
396
+ ],
397
+ [
398
+ "status",
399
+ statusName(s)
400
+ ]
401
+ ]);
402
+ return Outcome$ReventlessGwt.fail({
403
+ TAG: "StateMismatch",
404
+ key: id,
405
+ expected: encOne(expectedId, expectedStatus),
406
+ actual: encOne(id, status)
407
+ });
408
+ };
409
+ return {
410
+ describe: JestBind$ReventlessGwt.describe,
411
+ test: test,
412
+ testSync: testSync,
413
+ encItems: encItems,
414
+ commandPairJson: commandPairJson,
415
+ errorOf: errorOf,
416
+ givenEvent: givenEvent,
417
+ whenCollect: whenCollect,
418
+ thenTodos: thenTodos,
419
+ givenTodo: givenTodo,
420
+ fakes: Capabilities_Fake$ReventlessGwt.make,
421
+ givenCapabilities: givenCapabilities,
422
+ attemptWith: attemptWith,
423
+ attempts: attempts,
424
+ whenTranslated: whenTranslated,
425
+ whenTranslateMocked: whenTranslateMocked,
426
+ whenTranslateRetrying: whenTranslateRetrying,
427
+ whenExhausted: whenExhausted,
428
+ thenCommand: thenCommand,
429
+ thenNoCommand: thenNoCommand,
430
+ thenSent: thenSent,
431
+ thenNothingSent: thenNothingSent,
432
+ thenRetryRecorded: thenRetryRecorded,
433
+ statusName: statusName,
434
+ thenTodoStatus: thenTodoStatus
435
+ };
436
+ };
437
+ }
438
+
184
439
  export {
185
440
  Make,
441
+ FromSlice,
186
442
  }
187
- /* Message-ReventlessCore Not a pure module */
443
+ /* S Not a pure module */
@@ -0,0 +1,145 @@
1
+ // Automation_GWT.FromSlice over a two-source slice written as the framework
2
+ // takes it: a Spec, and a body with per-source mappings, `process` and
3
+ // `onExhausted`. The second source's events do not name the order, so its
4
+ // mapping keys the to-do by `~sourceId`.
5
+
6
+ module ShipPaid = {
7
+ let name = "ShipPaid"
8
+ let moduleUrl = ""
9
+
10
+ @schema
11
+ type todoItem = {orderId: string}
12
+
13
+ @schema
14
+ type command =
15
+ | Ship({orderId: string})
16
+ | GiveUp({orderId: string})
17
+
18
+ let maxRetries = 3
19
+ let heartbeatInterval = 60
20
+ let targetName = "Order"
21
+ }
22
+
23
+ module ShipPaid_Automation = {
24
+ module M = Reventless.AutomationSlice.Mappings.Make(ShipPaid)
25
+ module type Mapping = M.Mapping
26
+ let moduleUrl = ""
27
+
28
+ module Orders = {
29
+ module Id = Reventless.Id.String
30
+ let name = "Orders"
31
+ @schema
32
+ type event =
33
+ | OrderPlaced({orderId: string})
34
+ | OrderCancelled({orderId: string})
35
+ }
36
+
37
+ module Payments = {
38
+ module Id = Reventless.Id.String
39
+ let name = "Payments"
40
+ @schema
41
+ type event =
42
+ | Captured({amount: int})
43
+ | Refunded({amount: int})
44
+ }
45
+
46
+ module FromOrders = Reventless.AutomationSlice.Mapping.Make(
47
+ Orders,
48
+ ShipPaid,
49
+ {
50
+ let collect = (event, ~sourceId as _, _ctx) =>
51
+ switch event {
52
+ | Orders.OrderPlaced({orderId}) => [(orderId, ({orderId: orderId}: ShipPaid.todoItem))]
53
+ | OrderCancelled(_) => []
54
+ }
55
+ let resolve = event =>
56
+ switch event {
57
+ | Orders.OrderCancelled({orderId}) => Some(orderId)
58
+ | OrderPlaced(_) => None
59
+ }
60
+ },
61
+ )
62
+
63
+ module FromPayments = Reventless.AutomationSlice.Mapping.Make(
64
+ Payments,
65
+ ShipPaid,
66
+ {
67
+ let collect = (event, ~sourceId, _ctx) =>
68
+ switch event {
69
+ | Payments.Captured(_) => [(sourceId, ({orderId: sourceId}: ShipPaid.todoItem))]
70
+ | Refunded(_) => []
71
+ }
72
+ let resolve = event =>
73
+ switch event {
74
+ | Payments.Refunded(_) => None
75
+ | Captured(_) => None
76
+ }
77
+ },
78
+ )
79
+
80
+ let mappings: array<module(Mapping)> = [module(FromOrders), module(FromPayments)]
81
+
82
+ let process = (id, _item: ShipPaid.todoItem) => Some((id, ShipPaid.Ship({orderId: id})))
83
+ let onExhausted = (id, _item: ShipPaid.todoItem) => Some((id, ShipPaid.GiveUp({orderId: id})))
84
+ }
85
+
86
+ open ShipPaid
87
+ open ShipPaid_Automation
88
+ include Automation_GWT.FromSlice(ShipPaid, ShipPaid_Automation)
89
+ module O = Mapping(FromOrders)
90
+ module P = Mapping(FromPayments)
91
+
92
+ describe("Automation_GWT.FromSlice", () => {
93
+ test("collect through one mapping", () =>
94
+ O.givenEvent(OrderPlaced({orderId: "o1"}))
95
+ ->O.whenCollect
96
+ ->thenTodos([("o1", {orderId: "o1"})])
97
+ )
98
+
99
+ test("collect keys by the source id where the event does not name it", () =>
100
+ P.givenEvent(Captured({amount: 5}))
101
+ ->P.whenCollect(~sourceId="o2")
102
+ ->thenTodos([("o2", {orderId: "o2"})])
103
+ )
104
+
105
+ test("resolve through one mapping", () =>
106
+ O.givenEvent(OrderCancelled({orderId: "o1"}))
107
+ ->O.whenResolve
108
+ ->thenResolved(Some("o1"))
109
+ )
110
+
111
+ test("process", () =>
112
+ givenTodo("o1", {orderId: "o1"})->whenProcess->thenCommand("o1", Ship({orderId: "o1"}))
113
+ )
114
+
115
+ test("exhausted: onExhausted is what the domain hears", () =>
116
+ givenTodo("o1", {orderId: "o1"})->whenExhausted->thenCommand("o1", GiveUp({orderId: "o1"}))
117
+ )
118
+
119
+ test("sweep: both sources route by name, in event order", () =>
120
+ givenEvents([
121
+ P.event(Captured({amount: 5}), ~sourceId="o2"),
122
+ O.event(OrderPlaced({orderId: "o1"})),
123
+ ])
124
+ ->whenSweep
125
+ ->thenCommands([("o2", Ship({orderId: "o2"})), ("o1", Ship({orderId: "o1"}))])
126
+ )
127
+
128
+ test("sweep: a resolve completes only a row that exists", () =>
129
+ givenEvents([
130
+ O.event(OrderCancelled({orderId: "o1"})),
131
+ O.event(OrderPlaced({orderId: "o1"})),
132
+ O.event(OrderPlaced({orderId: "o3"})),
133
+ O.event(OrderCancelled({orderId: "o3"})),
134
+ ])
135
+ ->whenSweep
136
+ ->thenScenarioTodos([("o1", {orderId: "o1"})])
137
+ )
138
+
139
+ test("andThenEvents drains what later events resolve", () =>
140
+ givenEvents([O.event(OrderPlaced({orderId: "o1"}))])
141
+ ->whenSweep
142
+ ->andThenEvents([O.event(OrderCancelled({orderId: "o1"}))])
143
+ ->thenScenarioTodos([])
144
+ )
145
+ })