@reventlessdev/reventless-local 3.0.0-alpha.320 → 3.0.0-alpha.321

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 CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.321 (2026-11-05)
7
+
8
+ ### Features
9
+
10
+ * each command's outcome is written as a line of its own ([84e5e3f](https://github.com/ReventlessDev/reventless-core/commit/84e5e3fa4d77e629c6f72d9079a8f82f6b26a5ac))
11
+ * **local:** the command outcomes can be read by the flows they started ([e43819d](https://github.com/ReventlessDev/reventless-core/commit/e43819da448ab09ea39a39636a0ef065e3f0c34b))
12
+
13
+
6
14
  # 3.0.0-alpha.320 (2026-11-02)
7
15
 
8
16
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.320",
3
+ "version": "3.0.0-alpha.321",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -36,17 +36,17 @@
36
36
  "ws": "^8.18.0",
37
37
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.31",
38
38
  "@reventlessdev/rescript-jest": "1.0.0-alpha.13",
39
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
40
39
  "@reventlessdev/rescript-effect": "1.0.0-alpha.1",
40
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
41
41
  "@reventlessdev/rescript-node": "2.0.0-alpha.26",
42
- "@reventlessdev/reventless-core": "3.0.0-alpha.328",
43
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.176",
44
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.277",
45
42
  "@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
46
- "@reventlessdev/reventless-infra": "3.0.0-alpha.222",
43
+ "@reventlessdev/reventless-core": "3.0.0-alpha.329",
44
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.278",
45
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.223",
46
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.177",
47
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.193",
47
48
  "@reventlessdev/reventless-seed": "1.0.0-alpha.41",
48
- "@reventlessdev/reventless-spec": "3.0.0-alpha.193",
49
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.192"
49
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.194"
50
50
  },
51
51
  "devDependencies": {
52
52
  "rescript": "12.3.1",
package/src/Platform.res CHANGED
@@ -120,6 +120,9 @@ module MakeWithConfig = (
120
120
  LocalViewChangeLog.install()
121
121
  // And what became of every command, for the same reason.
122
122
  LocalCommandOutcomeLog.install()
123
+ // The tap carries each outcome, so the line a deployed platform writes for it would only
124
+ // repeat it.
125
+ ReventlessCore.CommandTopic_Helpers.outcomeLines := false
123
126
 
124
127
  // What this platform provides for the roles its plugins need: every account it
125
128
  // can sign in as, built-in and loaded alike, or the file the store will load
@@ -78,6 +78,7 @@ import * as LocalViewChangeLog$ReventlessLocal from "./adapter/ViewChangeLog/Loc
78
78
  import * as PlatformMCP_Server$ReventlessLocal from "./adapter/PlatformMCP_Server.res.mjs";
79
79
  import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
80
80
  import * as Auth_GraphqlContext$ReventlessLocal from "./adapter/Auth/Auth_GraphqlContext.res.mjs";
81
+ import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
81
82
  import * as PgProjectionCatchup$ReventlessLocal from "./adapter/PgProjectionCatchup.res.mjs";
82
83
  import * as PluginsReadModelSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs";
83
84
  import * as StorePrefixCollision$ReventlessCore from "@reventlessdev/reventless-core/src/util/StorePrefixCollision.res.mjs";
@@ -160,6 +161,7 @@ function MakeWithConfig(Config) {
160
161
  LocalTapLines$ReventlessLocal.install();
161
162
  LocalViewChangeLog$ReventlessLocal.install();
162
163
  LocalCommandOutcomeLog$ReventlessLocal.install();
164
+ CommandTopic_Helpers$ReventlessCore.outcomeLines.contents = false;
163
165
  Role$Reventless.provideGroupsFrom(AccountStore$ReventlessLocal.providedGroups);
164
166
  OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
165
167
  TestRunner$ReventlessLocal.setup();
@@ -1972,6 +1974,7 @@ function Make($star) {
1972
1974
  LocalTapLines$ReventlessLocal.install();
1973
1975
  LocalViewChangeLog$ReventlessLocal.install();
1974
1976
  LocalCommandOutcomeLog$ReventlessLocal.install();
1977
+ CommandTopic_Helpers$ReventlessCore.outcomeLines.contents = false;
1975
1978
  Role$Reventless.provideGroupsFrom(AccountStore$ReventlessLocal.providedGroups);
1976
1979
  OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
1977
1980
  TestRunner$ReventlessLocal.setup();
@@ -10,15 +10,24 @@ type t = {
10
10
  item: JSON.t,
11
11
  }
12
12
 
13
- /** What a read asks for: every field narrows, none reads every outcome. */
14
- type filter = {correlationId?: string, msgId?: string}
13
+ /** What a read asks for: every field narrows, none reads every outcome. `startsFlow` keeps the
14
+ commands that started a flow: an API request, an inbound translation, a timer's work item. */
15
+ type filter = {correlationId?: string, msgId?: string, startsFlow?: bool}
16
+
17
+ /** A flow's first command is the one whose flow id is its own message id. */
18
+ let startsFlow = (e: t) => e.flow == Some(e.msgId)
15
19
 
16
20
  let matches = (f: filter, e: t) =>
17
21
  f.correlationId->Option.mapOr(true, c => e.flow == Some(c)) &&
18
- f.msgId->Option.mapOr(true, m => e.msgId == m)
22
+ f.msgId->Option.mapOr(true, m => e.msgId == m) &&
23
+ (f.startsFlow != Some(true) || startsFlow(e))
19
24
 
20
25
  let filterOfJson = (j: option<JSON.t>): filter => {
21
26
  let d = j->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make())
22
27
  let s = k => d->Dict.get(k)->Option.flatMap(JSON.Decode.string)
23
- {correlationId: ?s("correlationId"), msgId: ?s("msgId")}
28
+ {
29
+ correlationId: ?s("correlationId"),
30
+ msgId: ?s("msgId"),
31
+ startsFlow: ?(d->Dict.get("startsFlow")->Option.flatMap(JSON.Decode.bool)),
32
+ }
24
33
  }
@@ -4,9 +4,17 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
6
 
7
+ function startsFlow(e) {
8
+ return Primitive_object.equal(e.flow, e.msgId);
9
+ }
10
+
7
11
  function matches(f, e) {
8
- if (Stdlib_Option.mapOr(f.correlationId, true, c => Primitive_object.equal(e.flow, c))) {
9
- return Stdlib_Option.mapOr(f.msgId, true, m => e.msgId === m);
12
+ if (Stdlib_Option.mapOr(f.correlationId, true, c => Primitive_object.equal(e.flow, c)) && Stdlib_Option.mapOr(f.msgId, true, m => e.msgId === m)) {
13
+ if (Primitive_object.notequal(f.startsFlow, true)) {
14
+ return true;
15
+ } else {
16
+ return startsFlow(e);
17
+ }
10
18
  } else {
11
19
  return false;
12
20
  }
@@ -17,11 +25,13 @@ function filterOfJson(j) {
17
25
  let s = k => Stdlib_Option.flatMap(d[k], Stdlib_JSON.Decode.string);
18
26
  return {
19
27
  correlationId: s("correlationId"),
20
- msgId: s("msgId")
28
+ msgId: s("msgId"),
29
+ startsFlow: Stdlib_Option.flatMap(d["startsFlow"], Stdlib_JSON.Decode.bool)
21
30
  };
22
31
  }
23
32
 
24
33
  export {
34
+ startsFlow,
25
35
  matches,
26
36
  filterOfJson,
27
37
  }
@@ -52,6 +52,9 @@ let read = (db, ~filter: CommandOutcomeLogEntry.filter, ~before: option<int>, ~f
52
52
  }
53
53
  filter.correlationId->Option.forEach(c => add("flow = ?", str(c)))
54
54
  filter.msgId->Option.forEach(m => add("msg_id = ?", str(m)))
55
+ if filter.startsFlow == Some(true) {
56
+ where->Array.push("flow = msg_id")
57
+ }
55
58
  before->Option.forEach(b => add("seq < ?", JSON.Encode.int(b)))
56
59
  let sql = `SELECT seq, flow, msg_id, item FROM ${table}${where == []
57
60
  ? ""
@@ -56,6 +56,9 @@ function read(db, filter, before, first) {
56
56
  };
57
57
  Stdlib_Option.forEach(filter.correlationId, c => add("flow = ?", c));
58
58
  Stdlib_Option.forEach(filter.msgId, m => add("msg_id = ?", m));
59
+ if (Primitive_object.equal(filter.startsFlow, true)) {
60
+ where.push("flow = msg_id");
61
+ }
59
62
  Stdlib_Option.forEach(before, b => add("seq < ?", b));
60
63
  let sql = `SELECT seq, flow, msg_id, item FROM ` + table + (
61
64
  Primitive_object.equal(where, []) ? "" : " WHERE " + where.join(" AND ")
@@ -126,6 +126,7 @@ let viewChangeTypes = [
126
126
  `input Platform_CommandOutcomeFilter {
127
127
  correlationId: String
128
128
  msgId: String
129
+ startsFlow: Boolean
129
130
  }`,
130
131
  `type Platform_CommandOutcomeEdge {
131
132
  node: AWSJSON!
@@ -123,6 +123,7 @@ let viewChangeTypes = [
123
123
  `input Platform_CommandOutcomeFilter {
124
124
  correlationId: String
125
125
  msgId: String
126
+ startsFlow: Boolean
126
127
  }`,
127
128
  `type Platform_CommandOutcomeEdge {
128
129
  node: AWSJSON!
@@ -16,6 +16,10 @@ let registerCommandOutcome = CommandTopic$ReventlessCore.registerCommandOutcome;
16
16
 
17
17
  let clearCommandOutcome = CommandTopic$ReventlessCore.clearCommandOutcome;
18
18
 
19
+ let outcomeLines = CommandTopic$ReventlessCore.outcomeLines;
20
+
21
+ let logOutcome = CommandTopic$ReventlessCore.logOutcome;
22
+
19
23
  let fireCommandOutcome = CommandTopic$ReventlessCore.fireCommandOutcome;
20
24
 
21
25
  let acceptedResultChannel = CommandTopic$ReventlessCore.acceptedResultChannel;
@@ -56,6 +60,8 @@ export {
56
60
  nextOutcomeHookId,
57
61
  registerCommandOutcome,
58
62
  clearCommandOutcome,
63
+ outcomeLines,
64
+ logOutcome,
59
65
  fireCommandOutcome,
60
66
  acceptedResultChannel,
61
67
  rejectedResultChannel,
@@ -236,7 +236,7 @@ describe("the platform API's watching fields", () => {
236
236
  SqliteDriver.close(db)
237
237
  })
238
238
 
239
- // Three commands: two in one flow, one rejected.
239
+ // Three commands: two in one flow, which the first started, one rejected.
240
240
  let keepOutcomes = () => {
241
241
  let report = (
242
242
  ~flow,
@@ -252,11 +252,11 @@ describe("the platform API's watching fields", () => {
252
252
  entityId: Some("o-1"),
253
253
  eventCount: 1,
254
254
  })
255
- LocalCommandOutcomeLog.record(report(~flow="k-1", ~outcome=accepted, "c-1"))
255
+ LocalCommandOutcomeLog.record(report(~flow="c-1", ~outcome=accepted, "c-1"))
256
256
  LocalCommandOutcomeLog.record(report(~flow="k-2", ~outcome=accepted, "c-2"))
257
257
  LocalCommandOutcomeLog.record(
258
258
  report(
259
- ~flow="k-1",
259
+ ~flow="c-1",
260
260
  ~outcome=OutcomeRejected({
261
261
  errorCode: "AlreadyShipped",
262
262
  errorDetail: "",
@@ -285,13 +285,21 @@ describe("the platform API's watching fields", () => {
285
285
  )
286
286
  })
287
287
  }
288
- (await ask(`{"filter":{"correlationId":"k-1"}}`), await ask(`{"filter":{"msgId":"c-2"}}`))
288
+ (
289
+ await ask(`{"filter":{"correlationId":"c-1"}}`),
290
+ await ask(`{"filter":{"msgId":"c-2"}}`),
291
+ await ask(`{"filter":{"startsFlow":true}}`),
292
+ )
289
293
  }
290
294
 
291
- let outcomesExpected = ([("c-3", "rejected"), ("c-1", "accepted")], [("c-2", "accepted")])
295
+ let outcomesExpected = (
296
+ [("c-3", "rejected"), ("c-1", "accepted")],
297
+ [("c-2", "accepted")],
298
+ [("c-1", "accepted")],
299
+ )
292
300
 
293
301
  testPromise(
294
- "the command outcomes answer an admin by flow and by command, newest first",
302
+ "the command outcomes answer an admin by flow, by command and by flow start, newest first",
295
303
  async () => {
296
304
  BackendState.setMemory()
297
305
  LocalCommandOutcomeLog.resetForTests()
@@ -246,12 +246,12 @@ globalThis.describe("the platform API's watching fields", () => {
246
246
  entityId: "o-1",
247
247
  eventCount: 1
248
248
  };
249
- LocalCommandOutcomeLog$ReventlessLocal.record(report("k-1", accepted, "c-1"));
249
+ LocalCommandOutcomeLog$ReventlessLocal.record(report("c-1", accepted, "c-1"));
250
250
  LocalCommandOutcomeLog$ReventlessLocal.record(report("k-2", accepted, "c-2"));
251
251
  LocalCommandOutcomeLog$ReventlessLocal.record({
252
252
  component: "Order",
253
253
  reference: "c-3",
254
- correlationId: "k-1",
254
+ correlationId: "c-1",
255
255
  outcome: {
256
256
  TAG: "OutcomeRejected",
257
257
  errorCode: "AlreadyShipped",
@@ -275,8 +275,9 @@ globalThis.describe("the platform API's watching fields", () => {
275
275
  });
276
276
  };
277
277
  return [
278
- await ask(`{"filter":{"correlationId":"k-1"}}`),
279
- await ask(`{"filter":{"msgId":"c-2"}}`)
278
+ await ask(`{"filter":{"correlationId":"c-1"}}`),
279
+ await ask(`{"filter":{"msgId":"c-2"}}`),
280
+ await ask(`{"filter":{"startsFlow":true}}`)
280
281
  ];
281
282
  };
282
283
  let outcomesExpected_0 = [
@@ -293,11 +294,16 @@ globalThis.describe("the platform API's watching fields", () => {
293
294
  "c-2",
294
295
  "accepted"
295
296
  ]];
297
+ let outcomesExpected_2 = [[
298
+ "c-1",
299
+ "accepted"
300
+ ]];
296
301
  let outcomesExpected = [
297
302
  outcomesExpected_0,
298
- outcomesExpected_1
303
+ outcomesExpected_1,
304
+ outcomesExpected_2
299
305
  ];
300
- globalThis.test("the command outcomes answer an admin by flow and by command, newest first", async () => {
306
+ globalThis.test("the command outcomes answer an admin by flow, by command and by flow start, newest first", async () => {
301
307
  BackendState$ReventlessLocal.setMemory();
302
308
  LocalCommandOutcomeLog$ReventlessLocal.resetForTests();
303
309
  globalThis.expect(await outcomesRead()).toEqual(outcomesExpected);