@reventlessdev/reventless-core 3.0.0-alpha.134 → 3.0.0-alpha.136

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 (36) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +8 -8
  3. package/src/Projection.res +19 -10
  4. package/src/Projection.res.mjs +17 -9
  5. package/src/components/Aggregate/Aggregate_Callback.res +6 -3
  6. package/src/components/Aggregate/Aggregate_Callback.res.mjs +5 -4
  7. package/src/components/EventLog/EventLog.res +13 -1
  8. package/src/components/EventLog/EventLog_Operations.res +37 -34
  9. package/src/components/EventLog/EventLog_Operations.res.mjs +30 -13
  10. package/src/components/ReadModel/ReadModel.res +5 -0
  11. package/src/components/ReadModel/ReadModel_Builder.res +12 -0
  12. package/src/components/ReadModel/ReadModel_Builder.res.mjs +3 -0
  13. package/src/plugin/component/Plugin_Structure.res +9 -2
  14. package/src/plugin/component/Plugin_Structure.res.mjs +7 -4
  15. package/src/plugin/component/SchemaWalker.res +13 -1
  16. package/src/plugin/component/SchemaWalker.res.mjs +5 -4
  17. package/src/util/CommandPublisher.res +20 -10
  18. package/src/util/CommandPublisher.res.mjs +6 -10
  19. package/src/util/Util_Array.res +1 -1
  20. package/src/util/Util_Array.res.mjs +1 -1
  21. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +2 -1
  22. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +4 -2
  23. package/tests/aggregate/AggregateConflictTest.res +3 -3
  24. package/tests/aggregate/AggregateConflictTest.res.mjs +1 -1
  25. package/tests/aggregate/AggregateFixtures.res +3 -3
  26. package/tests/aggregate/AggregateFixtures.res.mjs +4 -1
  27. package/tests/eventlog/EventLogFixtures.res +2 -2
  28. package/tests/eventlog/EventLogFixtures.res.mjs +8 -2
  29. package/tests/eventlog/EventLogRetryTest.res +12 -11
  30. package/tests/eventlog/EventLogRetryTest.res.mjs +31 -7
  31. package/tests/plugin/ManifestVisibilityTest.res +14 -0
  32. package/tests/plugin/ManifestVisibilityTest.res.mjs +10 -0
  33. package/tests/projection/ProjectionOptimizeTest.res +38 -0
  34. package/tests/projection/ProjectionOptimizeTest.res.mjs +75 -0
  35. package/tests/util/CommandPublisherTest.res +56 -0
  36. package/tests/util/CommandPublisherTest.res.mjs +78 -0
@@ -54,7 +54,8 @@ function Make(Spec) {
54
54
  if (size <= 0) {
55
55
  return;
56
56
  }
57
- let commandsToSend = buffer.toSpliced(0, size);
57
+ let commandsToSend = buffer.slice(0, size);
58
+ buffer.splice(0, size);
58
59
  let promise = Config.publishCommands(Spec.name, toJsons(commandsToSend));
59
60
  running.contents = promise;
60
61
  let exit = 0;
@@ -88,7 +89,8 @@ function Make(Spec) {
88
89
  let bufferSizeStr = buffer.length.toString();
89
90
  let chunkCountStr = chunkCount.contents.toString();
90
91
  Effect.runSync(EffectLogger$ReventlessCore.logDebug("CommandPublisher", undefined, `send: bufferSize: ` + bufferSizeStr + `, chunk: ` + chunkCountStr + `, size: ` + sizeStr));
91
- let commandsToSend$1 = buffer.toSpliced(0, size$1);
92
+ let commandsToSend$1 = buffer.slice(0, size$1);
93
+ buffer.splice(0, size$1);
92
94
  let promise$1 = Config.publishCommands(Spec.name, toJsons(commandsToSend$1));
93
95
  running.contents = promise$1;
94
96
  let exit$1 = 0;
@@ -127,19 +129,13 @@ function Make(Spec) {
127
129
  };
128
130
  let clear = () => {
129
131
  Effect.runSync(EffectLogger$ReventlessCore.logDebug("CommandPublisher", undefined, "clear"));
130
- buffer.splice(0, 1);
132
+ buffer.splice(0, buffer.length);
131
133
  flush.contents = false;
132
134
  };
133
135
  let flush$1 = async () => {
134
136
  Effect.runSync(EffectLogger$ReventlessCore.logDebug("CommandPublisher", undefined, "flush"));
135
137
  flush.contents = true;
136
- let match = running.contents;
137
- if (match === undefined) {
138
- return await send();
139
- }
140
- while (Stdlib_Option.isNone(running.contents)) {
141
- await finishRunning();
142
- };
138
+ return await send();
143
139
  };
144
140
  return {
145
141
  buffer: buffer,
@@ -1,2 +1,2 @@
1
1
  let containsByPredicate: (array<'a>, 'a => bool) => bool = (arr, predicate) =>
2
- arr->Array.find(predicate)->Option.isNone
2
+ arr->Array.find(predicate)->Option.isSome
@@ -3,7 +3,7 @@
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
4
 
5
5
  function containsByPredicate(arr, predicate) {
6
- return Stdlib_Option.isNone(arr.find(predicate));
6
+ return Stdlib_Option.isSome(arr.find(predicate));
7
7
  }
8
8
 
9
9
  export {
@@ -59,10 +59,11 @@ let structure: pluginStructure = {
59
59
  consumedEventTypes: ["OrderPlaced"],
60
60
  inboundCommandTypes: ["Ship"],
61
61
  targetName: None,
62
+ externalSystem: None,
62
63
  },
63
64
  ],
64
65
  inboundTranslationSlices: [
65
- {name: "FromBilling", commandTypes: ["RecordPayment"], targetName: "Order"},
66
+ {name: "FromBilling", commandTypes: ["RecordPayment"], targetName: "Order", externalSystem: None},
66
67
  ],
67
68
  extensions: [
68
69
  {
@@ -85,13 +85,15 @@ let structure_outboundTranslationSlices = [{
85
85
  name: "ToShipper",
86
86
  consumedEventTypes: ["OrderPlaced"],
87
87
  inboundCommandTypes: ["Ship"],
88
- targetName: undefined
88
+ targetName: undefined,
89
+ externalSystem: undefined
89
90
  }];
90
91
 
91
92
  let structure_inboundTranslationSlices = [{
92
93
  name: "FromBilling",
93
94
  commandTypes: ["RecordPayment"],
94
- targetName: "Order"
95
+ targetName: "Order",
96
+ externalSystem: undefined
95
97
  }];
96
98
 
97
99
  let structure_extensions = [{
@@ -41,7 +41,7 @@ module TestBehavior = {
41
41
  }
42
42
 
43
43
  type mockEL = {
44
- appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, string>>,
44
+ appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, EventLog.appendError>>,
45
45
  replayFn: string => promise<array<AggSpec.event>>,
46
46
  replayStreamFn: string => Stream.t<AggSpec.event, string, unit>,
47
47
  getAll: unit => array<Message.event'<string, AggSpec.event>>,
@@ -61,7 +61,7 @@ let makeMockEL = (): mockEL => {
61
61
  appendCount := appendCount.contents + 1
62
62
  if failConflictRef.contents > 0 {
63
63
  failConflictRef := failConflictRef.contents - 1
64
- Error("conflict")
64
+ Error(EventLog.Conflict)
65
65
  } else {
66
66
  storedRef := storedRef.contents->Array.concat(newEvents)
67
67
  Ok(())
@@ -114,7 +114,7 @@ module TestOps = {
114
114
  type event = AggSpec.event
115
115
  }
116
116
  type operations = {
117
- append: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, string>>,
117
+ append: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, EventLog.appendError>>,
118
118
  replay: string => promise<array<AggSpec.event>>,
119
119
  replayStream: string => Stream.t<AggSpec.event, string, unit>,
120
120
  appendStream: (int, string, Stream.t<AggSpec.event, string, unit>) => Effect.t<unit, string, unit>,
@@ -87,7 +87,7 @@ function makeMockEL() {
87
87
  failConflictRef.contents = failConflictRef.contents - 1 | 0;
88
88
  return {
89
89
  TAG: "Error",
90
- _0: "conflict"
90
+ _0: "Conflict"
91
91
  };
92
92
  } else {
93
93
  storedRef.contents = storedRef.contents.concat(newEvents);
@@ -60,7 +60,7 @@ module TestBehavior = {
60
60
  // ─────────────────────────────────────────────────────────────
61
61
 
62
62
  type mockEL = {
63
- appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, string>>,
63
+ appendFn: (int, string, array<Message.event'<string, AggSpec.event>>) => promise<result<unit, EventLog.appendError>>,
64
64
  replayFn: string => promise<array<AggSpec.event>>,
65
65
  replayStreamFn: string => Stream.t<AggSpec.event, string, unit>,
66
66
  getAll: unit => array<Message.event'<string, AggSpec.event>>,
@@ -75,7 +75,7 @@ let makeMockEL = (): mockEL => {
75
75
  let appendFn = async (_seqNr, _id, newEvents) =>
76
76
  if failRef.contents {
77
77
  failRef := false
78
- Error("append failed")
78
+ Error(EventLog.StorageFailure("append failed"))
79
79
  } else {
80
80
  storedRef := storedRef.contents->Array.concat(newEvents)
81
81
  Ok(())
@@ -129,7 +129,7 @@ module TestOps = {
129
129
  int,
130
130
  string,
131
131
  array<Message.event'<string, AggSpec.event>>,
132
- ) => promise<result<unit, string>>,
132
+ ) => promise<result<unit, EventLog.appendError>>,
133
133
  replay: string => promise<array<AggSpec.event>>,
134
134
  replayStream: string => Stream.t<AggSpec.event, string, unit>,
135
135
  appendStream: (int, string, Stream.t<AggSpec.event, string, unit>) => Effect.t<unit, string, unit>,
@@ -129,7 +129,10 @@ function makeMockEL() {
129
129
  failRef.contents = false;
130
130
  return {
131
131
  TAG: "Error",
132
- _0: "append failed"
132
+ _0: {
133
+ TAG: "StorageFailure",
134
+ _0: "append failed"
135
+ }
133
136
  };
134
137
  } else {
135
138
  storedRef.contents = storedRef.contents.concat(newEvents);
@@ -28,10 +28,10 @@ let mockStorage: EventLog_Adapter.operations = {
28
28
  appendCallCount := appendCallCount.contents + 1
29
29
  if failNextAppend.contents {
30
30
  failNextAppend := false
31
- Error("mock storage failure")
31
+ Error(EventLog.StorageFailure("mock storage failure"))
32
32
  } else if failNextAppendsWithTransient.contents > 0 {
33
33
  failNextAppendsWithTransient := failNextAppendsWithTransient.contents - 1
34
- Error("ThrottlingException: Rate exceeded")
34
+ Error(EventLog.StorageFailure("ThrottlingException: Rate exceeded"))
35
35
  } else {
36
36
  let existing = storedEvents.contents->Dict.get(id)->Option.getOr([])
37
37
  storedEvents.contents->Dict.set(id, existing->Array.concat(jsons))
@@ -47,14 +47,20 @@ async function mockStorage_append(_seqNr, id, jsons) {
47
47
  failNextAppend.contents = false;
48
48
  return {
49
49
  TAG: "Error",
50
- _0: "mock storage failure"
50
+ _0: {
51
+ TAG: "StorageFailure",
52
+ _0: "mock storage failure"
53
+ }
51
54
  };
52
55
  }
53
56
  if (failNextAppendsWithTransient.contents > 0) {
54
57
  failNextAppendsWithTransient.contents = failNextAppendsWithTransient.contents - 1 | 0;
55
58
  return {
56
59
  TAG: "Error",
57
- _0: "ThrottlingException: Rate exceeded"
60
+ _0: {
61
+ TAG: "StorageFailure",
62
+ _0: "ThrottlingException: Rate exceeded"
63
+ }
58
64
  };
59
65
  }
60
66
  let existing = Stdlib_Option.getOr(storedEvents.contents[id], []);
@@ -16,36 +16,37 @@ let _ = beforeEach(() => reset())
16
16
 
17
17
  describe("EventLog_Operations — retry:", () => {
18
18
  describe("isTransient predicate:", () => {
19
+ let transient = msg => EventLog_Operations.isTransient(EventLog.StorageFailure(msg))
19
20
  testPromise("ThrottlingException is transient", async () => {
20
- expect(EventLog_Operations.isTransient("ThrottlingException: Rate exceeded"))->toBe(true)
21
+ expect(transient("ThrottlingException: Rate exceeded"))->toBe(true)
21
22
  })
22
23
 
23
24
  testPromise("ProvisionedThroughputExceededException is transient", async () => {
24
- expect(
25
- EventLog_Operations.isTransient("ProvisionedThroughputExceededException"),
26
- )->toBe(true)
25
+ expect(transient("ProvisionedThroughputExceededException"))->toBe(true)
27
26
  })
28
27
 
29
28
  testPromise("ServiceUnavailable is transient", async () => {
30
- expect(EventLog_Operations.isTransient("ServiceUnavailable"))->toBe(true)
29
+ expect(transient("ServiceUnavailable"))->toBe(true)
31
30
  })
32
31
 
33
32
  testPromise("RequestLimitExceeded is transient", async () => {
34
- expect(EventLog_Operations.isTransient("RequestLimitExceeded"))->toBe(true)
33
+ expect(transient("RequestLimitExceeded"))->toBe(true)
35
34
  })
36
35
 
37
36
  testPromise("InternalServerError is transient", async () => {
38
- expect(EventLog_Operations.isTransient("InternalServerError: something went wrong"))->toBe(
39
- true,
40
- )
37
+ expect(transient("InternalServerError: something went wrong"))->toBe(true)
41
38
  })
42
39
 
43
40
  testPromise("ValidationException is not transient", async () => {
44
- expect(EventLog_Operations.isTransient("ValidationException: invalid field"))->toBe(false)
41
+ expect(transient("ValidationException: invalid field"))->toBe(false)
45
42
  })
46
43
 
47
44
  testPromise("generic mock storage failure is not transient", async () => {
48
- expect(EventLog_Operations.isTransient("mock storage failure"))->toBe(false)
45
+ expect(transient("mock storage failure"))->toBe(false)
46
+ })
47
+
48
+ testPromise("a Conflict is never transient (retried a layer up, not here)", async () => {
49
+ expect(EventLog_Operations.isTransient(EventLog.Conflict))->toBe(false)
49
50
  })
50
51
  })
51
52
 
@@ -74,25 +74,49 @@ globalThis.beforeEach(() => EventLogFixtures$ReventlessCore.reset());
74
74
  globalThis.describe("EventLog_Operations — retry:", () => {
75
75
  globalThis.describe("isTransient predicate:", () => {
76
76
  globalThis.test("ThrottlingException is transient", async () => {
77
- globalThis.expect(EventLog_Operations$ReventlessCore.isTransient("ThrottlingException: Rate exceeded")).toBe(true);
77
+ globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
78
+ TAG: "StorageFailure",
79
+ _0: "ThrottlingException: Rate exceeded"
80
+ })).toBe(true);
78
81
  });
79
82
  globalThis.test("ProvisionedThroughputExceededException is transient", async () => {
80
- globalThis.expect(EventLog_Operations$ReventlessCore.isTransient("ProvisionedThroughputExceededException")).toBe(true);
83
+ globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
84
+ TAG: "StorageFailure",
85
+ _0: "ProvisionedThroughputExceededException"
86
+ })).toBe(true);
81
87
  });
82
88
  globalThis.test("ServiceUnavailable is transient", async () => {
83
- globalThis.expect(EventLog_Operations$ReventlessCore.isTransient("ServiceUnavailable")).toBe(true);
89
+ globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
90
+ TAG: "StorageFailure",
91
+ _0: "ServiceUnavailable"
92
+ })).toBe(true);
84
93
  });
85
94
  globalThis.test("RequestLimitExceeded is transient", async () => {
86
- globalThis.expect(EventLog_Operations$ReventlessCore.isTransient("RequestLimitExceeded")).toBe(true);
95
+ globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
96
+ TAG: "StorageFailure",
97
+ _0: "RequestLimitExceeded"
98
+ })).toBe(true);
87
99
  });
88
100
  globalThis.test("InternalServerError is transient", async () => {
89
- globalThis.expect(EventLog_Operations$ReventlessCore.isTransient("InternalServerError: something went wrong")).toBe(true);
101
+ globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
102
+ TAG: "StorageFailure",
103
+ _0: "InternalServerError: something went wrong"
104
+ })).toBe(true);
90
105
  });
91
106
  globalThis.test("ValidationException is not transient", async () => {
92
- globalThis.expect(EventLog_Operations$ReventlessCore.isTransient("ValidationException: invalid field")).toBe(false);
107
+ globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
108
+ TAG: "StorageFailure",
109
+ _0: "ValidationException: invalid field"
110
+ })).toBe(false);
93
111
  });
94
112
  globalThis.test("generic mock storage failure is not transient", async () => {
95
- globalThis.expect(EventLog_Operations$ReventlessCore.isTransient("mock storage failure")).toBe(false);
113
+ globalThis.expect(EventLog_Operations$ReventlessCore.isTransient({
114
+ TAG: "StorageFailure",
115
+ _0: "mock storage failure"
116
+ })).toBe(false);
117
+ });
118
+ globalThis.test("a Conflict is never transient (retried a layer up, not here)", async () => {
119
+ globalThis.expect(EventLog_Operations$ReventlessCore.isTransient("Conflict")).toBe(false);
96
120
  });
97
121
  });
98
122
  globalThis.describe("permanent storage failure:", () => {
@@ -45,6 +45,9 @@ module PublicReadModel: ReventlessInfra.ReadModel.T
45
45
  type role = unit
46
46
  type component
47
47
  let sourceNames: array<string> = []
48
+ // A read model that projects an event reaching it via a (DCB-log-sourced) mapping — the
49
+ // case Plugin_Structure must surface as a qualified `consumedEventTypes` entry.
50
+ let consumedEventNames: array<string> = ["OrderPlaced"]
48
51
  let make = (~api as _, ~apiRole as _, ~allEventTopics as _, ~opts as _=?): component =>
49
52
  Obj.magic(0)
50
53
  let outputs = (_: component): ReventlessInfra.ReadModel.outputs => Obj.magic(0)
@@ -60,6 +63,7 @@ module InternalReadModel: ReventlessInfra.ReadModel.T
60
63
  type role = unit
61
64
  type component
62
65
  let sourceNames: array<string> = []
66
+ let consumedEventNames: array<string> = []
63
67
  let make = (~api as _, ~apiRole as _, ~allEventTopics as _, ~opts as _=?): component =>
64
68
  Obj.magic(0)
65
69
  let outputs = (_: component): ReventlessInfra.ReadModel.outputs => Obj.magic(0)
@@ -128,6 +132,16 @@ describe("Plugin_Structure.make — visibility tagging", () => {
128
132
  expect(internal->Option.map(d => d.visibility))->toEqual(Some(Some("Internal")))
129
133
  })
130
134
 
135
+ testSync("a read model's mapping-consumed events surface as qualified consumedEventTypes", () => {
136
+ // Q6 #2: a classic read model fed via a (DCB-log-sourced) mapping now reflects the
137
+ // events it projects from, qualified to the plugin — so DomainGraph draws the
138
+ // Event→ReadModel projection edge (e.g. OrderPlaced → Customers).
139
+ let public = structure.readModels->Array.find(d => d.name == "PublicView")
140
+ expect(public->Option.map(d => d.consumedEventTypes))->toEqual(
141
+ Some(["VisibilityPlugin.OrderPlaced"]),
142
+ )
143
+ })
144
+
131
145
  testSync("aggregates / stateViewSlices stay empty when none are passed (sanity)", () => {
132
146
  expect((
133
147
  structure.aggregates->Array.length,
@@ -53,6 +53,8 @@ let InternalSpec = {
53
53
 
54
54
  let sourceNames = [];
55
55
 
56
+ let consumedEventNames = ["OrderPlaced"];
57
+
56
58
  function make(param, param$1, param$2, param$3) {
57
59
  return 0;
58
60
  }
@@ -89,6 +91,7 @@ let PublicReadModel_Spec = {
89
91
  let PublicReadModel = {
90
92
  Spec: PublicReadModel_Spec,
91
93
  sourceNames: sourceNames,
94
+ consumedEventNames: consumedEventNames,
92
95
  make: make,
93
96
  outputs: outputs,
94
97
  operations: operations,
@@ -97,6 +100,8 @@ let PublicReadModel = {
97
100
 
98
101
  let sourceNames$1 = [];
99
102
 
103
+ let consumedEventNames$1 = [];
104
+
100
105
  function make$1(param, param$1, param$2, param$3) {
101
106
  return 0;
102
107
  }
@@ -133,6 +138,7 @@ let InternalReadModel_Spec = {
133
138
  let InternalReadModel = {
134
139
  Spec: InternalReadModel_Spec,
135
140
  sourceNames: sourceNames$1,
141
+ consumedEventNames: consumedEventNames$1,
136
142
  make: make$1,
137
143
  outputs: outputs$1,
138
144
  operations: operations$1,
@@ -183,6 +189,10 @@ globalThis.describe("Plugin_Structure.make — visibility tagging", () => {
183
189
  let internal = structure.readModels.find(d => d.name === "InternalView");
184
190
  globalThis.expect(Stdlib_Option.map(internal, d => d.visibility)).toEqual("Internal");
185
191
  });
192
+ globalThis.test("a read model's mapping-consumed events surface as qualified consumedEventTypes", () => {
193
+ let $$public = structure.readModels.find(d => d.name === "PublicView");
194
+ globalThis.expect(Stdlib_Option.map($$public, d => d.consumedEventTypes)).toEqual(["VisibilityPlugin.OrderPlaced"]);
195
+ });
186
196
  globalThis.test("aggregates / stateViewSlices stay empty when none are passed (sanity)", () => {
187
197
  globalThis.expect([
188
198
  structure.aggregates.length,
@@ -0,0 +1,38 @@
1
+ // Regression test for Projection.optimizeActions (plan A7 / D2).
2
+ //
3
+ // optimizeActions merges consecutive per-id actions. A non-merging pair (Delete
4
+ // then Update) grows the accumulator to length 2; the next merge (Update+Set)
5
+ // must fold only the last action and keep everything before it exactly once.
6
+ // Before the `~end=count-1` fix, `previousActions` still contained the
7
+ // just-merged action, so the earlier action was duplicated — applied twice.
8
+
9
+ open JestGlobals
10
+ open Reventless.Projection
11
+
12
+ describe("Projection.optimizeActions", () => {
13
+ testSync("does not duplicate the pre-merge action (off-by-one regression)", () => {
14
+ let optimized = Projection.optimizeActions([Delete("a"), Update("a", x => x + 1), Set("a", 99)])
15
+ // Was 3 (with a duplicated Update("a")) before the fix.
16
+ expect(optimized->Array.length)->toBe(2)
17
+ let sets =
18
+ optimized->Array.filterMap(a =>
19
+ switch a {
20
+ | Set(id, s) => Some((id, s))
21
+ | _ => None
22
+ }
23
+ )
24
+ expect(sets)->toEqual([("a", 99)])
25
+ })
26
+
27
+ testSync("merges a same-id Create+Update chain into a single Create", () => {
28
+ let optimized = Projection.optimizeActions([Create("a", 0), Update("a", x => x + 1), Update("a", x => x + 10)])
29
+ let creates =
30
+ optimized->Array.filterMap(a =>
31
+ switch a {
32
+ | Create(id, s) => Some((id, s))
33
+ | _ => None
34
+ }
35
+ )
36
+ expect(creates)->toEqual([("a", 11)])
37
+ })
38
+ })
@@ -0,0 +1,75 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Projection$ReventlessCore from "../../src/Projection.res.mjs";
5
+
6
+ globalThis.describe("Projection.optimizeActions", () => {
7
+ globalThis.test("does not duplicate the pre-merge action (off-by-one regression)", () => {
8
+ let optimized = Projection$ReventlessCore.optimizeActions([
9
+ {
10
+ TAG: "Delete",
11
+ _0: "a"
12
+ },
13
+ {
14
+ TAG: "Update",
15
+ _0: "a",
16
+ _1: x => x + 1 | 0
17
+ },
18
+ {
19
+ TAG: "Set",
20
+ _0: "a",
21
+ _1: 99
22
+ }
23
+ ]);
24
+ globalThis.expect(optimized.length).toBe(2);
25
+ let sets = Stdlib_Array.filterMap(optimized, a => {
26
+ if (typeof a !== "object" || a.TAG !== "Set") {
27
+ return;
28
+ } else {
29
+ return [
30
+ a._0,
31
+ a._1
32
+ ];
33
+ }
34
+ });
35
+ globalThis.expect(sets).toEqual([[
36
+ "a",
37
+ 99
38
+ ]]);
39
+ });
40
+ globalThis.test("merges a same-id Create+Update chain into a single Create", () => {
41
+ let optimized = Projection$ReventlessCore.optimizeActions([
42
+ {
43
+ TAG: "Create",
44
+ _0: "a",
45
+ _1: 0
46
+ },
47
+ {
48
+ TAG: "Update",
49
+ _0: "a",
50
+ _1: x => x + 1 | 0
51
+ },
52
+ {
53
+ TAG: "Update",
54
+ _0: "a",
55
+ _1: x => x + 10 | 0
56
+ }
57
+ ]);
58
+ let creates = Stdlib_Array.filterMap(optimized, a => {
59
+ if (typeof a !== "object" || a.TAG !== "Create") {
60
+ return;
61
+ } else {
62
+ return [
63
+ a._0,
64
+ a._1
65
+ ];
66
+ }
67
+ });
68
+ globalThis.expect(creates).toEqual([[
69
+ "a",
70
+ 11
71
+ ]]);
72
+ });
73
+ });
74
+
75
+ /* Not a pure module */
@@ -0,0 +1,56 @@
1
+ // Regression tests for CommandPublisher (plan A7).
2
+ //
3
+ // Two bugs made the buffered publisher silently lose commands:
4
+ // - send() used the non-mutating `toSpliced`, so the buffer never drained and
5
+ // the wrong slice was published.
6
+ // - flush()'s in-flight arm looped on a dead condition and never sent.
7
+ // - clear() removed only the first buffered command instead of truncating.
8
+ //
9
+ // These tests drive the functor with a capturing publishCommands and assert the
10
+ // buffer flushes in order and clear() empties it.
11
+
12
+ open JestGlobals
13
+
14
+ S.enableJson()
15
+
16
+ let published: ref<array<Reventless.Message.commandJson>> = ref([])
17
+
18
+ module TestSpec = {
19
+ @schema let name = "TestCmd"
20
+ @schema type command = {value: int}
21
+ }
22
+
23
+ module TestConfig = {
24
+ let user = "tester"
25
+ let publishCommands: Task.publishCommands = (_name, cmds) => {
26
+ published := published.contents->Array.concat(cmds)
27
+ Promise.resolve()
28
+ }
29
+ let mode = CommandPublisher.SendAllInOneChunk
30
+ }
31
+
32
+ module Pub = CommandPublisher.Make(TestSpec, TestConfig)
33
+
34
+ describe("CommandPublisher", () => {
35
+ testPromise("flush sends all buffered commands in order and drains the buffer", async () => {
36
+ published := []
37
+ Pub.publish("id1", {value: 1})
38
+ Pub.publish("id2", {value: 2})
39
+ await Pub.flush()
40
+ let ids = published.contents->Array.map((c: Reventless.Message.commandJson) => c.id)
41
+ expect(ids)->toEqual(["id1", "id2"])
42
+ // Buffer drained: a second flush sends nothing more.
43
+ published := []
44
+ await Pub.flush()
45
+ expect(published.contents->Array.length)->toBe(0)
46
+ })
47
+
48
+ testPromise("clear truncates the whole buffer so nothing is later published", async () => {
49
+ published := []
50
+ Pub.publish("id3", {value: 3})
51
+ Pub.publish("id4", {value: 4})
52
+ Pub.clear()
53
+ await Pub.flush()
54
+ expect(published.contents->Array.length)->toBe(0)
55
+ })
56
+ })
@@ -0,0 +1,78 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as CommandPublisher$ReventlessCore from "../../src/util/CommandPublisher.res.mjs";
5
+
6
+ S.enableJson();
7
+
8
+ let published = {
9
+ contents: []
10
+ };
11
+
12
+ let commandSchema = S.schema(s => ({
13
+ value: s.m(S.int)
14
+ }));
15
+
16
+ function commandAuthorization(param) {
17
+ return "AllowAuthenticated";
18
+ }
19
+
20
+ let TestSpec = {
21
+ name: "TestCmd",
22
+ commandSchema: commandSchema,
23
+ commandAuthorization: commandAuthorization
24
+ };
25
+
26
+ function publishCommands(_name, cmds) {
27
+ published.contents = published.contents.concat(cmds);
28
+ return Promise.resolve();
29
+ }
30
+
31
+ let TestConfig = {
32
+ user: "tester",
33
+ publishCommands: publishCommands,
34
+ mode: "SendAllInOneChunk"
35
+ };
36
+
37
+ let Pub = CommandPublisher$ReventlessCore.Make(TestSpec)(TestConfig);
38
+
39
+ globalThis.describe("CommandPublisher", () => {
40
+ globalThis.test("flush sends all buffered commands in order and drains the buffer", async () => {
41
+ published.contents = [];
42
+ Pub.publish("id1", {
43
+ value: 1
44
+ });
45
+ Pub.publish("id2", {
46
+ value: 2
47
+ });
48
+ await Pub.flush();
49
+ let ids = published.contents.map(c => c.id);
50
+ globalThis.expect(ids).toEqual([
51
+ "id1",
52
+ "id2"
53
+ ]);
54
+ published.contents = [];
55
+ await Pub.flush();
56
+ globalThis.expect(published.contents.length).toBe(0);
57
+ });
58
+ globalThis.test("clear truncates the whole buffer so nothing is later published", async () => {
59
+ published.contents = [];
60
+ Pub.publish("id3", {
61
+ value: 3
62
+ });
63
+ Pub.publish("id4", {
64
+ value: 4
65
+ });
66
+ Pub.clear();
67
+ await Pub.flush();
68
+ globalThis.expect(published.contents.length).toBe(0);
69
+ });
70
+ });
71
+
72
+ export {
73
+ published,
74
+ TestSpec,
75
+ TestConfig,
76
+ Pub,
77
+ }
78
+ /* Not a pure module */