@reventlessdev/reventless-aws 3.0.0-alpha.185 → 3.0.0-alpha.186

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,13 @@
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.186 (2026-07-08)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **reventless-core:** composite DCB slices can read back their own events ([4604a91](https://github.com/ReventlessDev/reventless-core/commit/4604a9159fb8bf59b2191ad69fee6613c7f75cd9))
11
+
12
+
6
13
  # 3.0.0-alpha.185 (2026-07-08)
7
14
 
8
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.185",
3
+ "version": "3.0.0-alpha.186",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -9,16 +9,16 @@
9
9
  "sury": "11.0.0-alpha.4",
10
10
  "uuid": "^13.0.0",
11
11
  "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.20",
12
- "@reventlessdev/rescript-effect": "0.1.0-alpha.25",
13
12
  "@reventlessdev/rescript-jest": "1.0.0-alpha.6",
13
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.47",
14
14
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
15
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.25",
15
16
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.14",
16
- "@reventlessdev/reventless-core": "3.0.0-alpha.146",
17
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.47",
17
+ "@reventlessdev/reventless-core": "3.0.0-alpha.147",
18
18
  "@reventlessdev/reventless-infra": "3.0.0-alpha.91",
19
19
  "@reventlessdev/reventless-interop": "3.0.0-alpha.24",
20
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.10",
21
- "@reventlessdev/reventless-spec": "3.0.0-alpha.69"
20
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.69",
21
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.11"
22
22
  },
23
23
  "devDependencies": {
24
24
  "rescript": "^12.3.0",
@@ -124,7 +124,12 @@ let toItem = (
124
124
  item->Dict.set(attrName, tag.value->JSON.Encode.string)
125
125
  })
126
126
 
127
- // Add composite tag attribute if multiple tags
127
+ // Add composite tag attribute if multiple tags. The tag set here is exactly the
128
+ // event's *entity* tags (no framework provenance tag is smuggled in — see
129
+ // docs/plans/done/dcb-composite-query-clause-fence-contention.md), so the stored
130
+ // `tag_composite` key is byte-identical to the key a composite decision read
131
+ // builds from the command's entity tags — the write/read match a composite
132
+ // slice's OCC depends on.
128
133
  if event.tags->Array.length > 1 {
129
134
  let composite = compositeTagKey(event.tags)
130
135
  item->Dict.set("tag_composite", composite->JSON.Encode.string)
@@ -18,8 +18,7 @@ open PulumiAws
18
18
  // ── Handler code ─────────────────────────────────────────────────────────────
19
19
  //
20
20
  // Receives SNS-via-SQS records (rawMessageDelivery=true → body IS the event JSON).
21
- // Publishes {position, eventType, payload, originatorSlice} to AppSync Events channel.
22
- // The "originatorSlice" tag is injected by StateChangeSlice_Callback.encodeEvent.
21
+ // Publishes {position, eventType, payload} to AppSync Events channel.
23
22
 
24
23
  let makeHandlerCode = (~topicName: string): string => {
25
24
  // Mirrors StateTopic_AppSync.pathSegment: AppSync Events channel segments
@@ -72,8 +71,7 @@ export async function handler(event) {
72
71
  console.error("EventLogSubscription: failed to parse record body", record.body, e);
73
72
  continue;
74
73
  }
75
- const originatorSlice = body.tags?.find(t => t.key === "originatorSlice")?.value;
76
- const payload = { position: body.position, eventType: body.eventType, payload: body.data, originatorSlice: originatorSlice ?? null };
74
+ const payload = { position: body.position, eventType: body.eventType, payload: body.data };
77
75
  const reqBody = JSON.stringify({ id: record.messageId, channel: CHANNEL, events: [JSON.stringify(payload)] });
78
76
  const auth = await signedHeaders(url.hostname, "/event", reqBody);
79
77
  const res = await fetch(APPSYNC_ENDPOINT + "/event", {
@@ -61,8 +61,7 @@ export async function handler(event) {
61
61
  console.error("EventLogSubscription: failed to parse record body", record.body, e);
62
62
  continue;
63
63
  }
64
- const originatorSlice = body.tags?.find(t => t.key === "originatorSlice")?.value;
65
- const payload = { position: body.position, eventType: body.eventType, payload: body.data, originatorSlice: originatorSlice ?? null };
64
+ const payload = { position: body.position, eventType: body.eventType, payload: body.data };
66
65
  const reqBody = JSON.stringify({ id: record.messageId, channel: CHANNEL, events: [JSON.stringify(payload)] });
67
66
  const auth = await signedHeaders(url.hostname, "/event", reqBody);
68
67
  const res = await fetch(APPSYNC_ENDPOINT + "/event", {
@@ -526,6 +526,39 @@ describe("Runtime.buildConditionalTransactItems — composite partition fences o
526
526
  })
527
527
  })
528
528
 
529
+ describe("Runtime.toItem — tag_composite is keyed on the event's entity tags", () => {
530
+ // Guards the composite write/read key alignment a composite-partition slice's OCC
531
+ // depends on: the stored `tag_composite` must equal the key a composite decision
532
+ // read builds from the command's entity tags (`compositeTagKey`). Historically an
533
+ // `originatorSlice` provenance tag was smuggled into the tag set and polluted this
534
+ // key, so a composite slice could never read back its own events. See
535
+ // docs/analysis/dcb-event-provenance-and-metadata.md and
536
+ // docs/plans/done/dcb-composite-query-clause-fence-contention.md.
537
+ let tagOf = (key, value): Reventless.DcbTag.tag => {key, value}
538
+ let entityTags = [
539
+ tagOf("environment", "prod"),
540
+ tagOf("platformName", "plat"),
541
+ tagOf("pluginName", "plug"),
542
+ tagOf("componentName", "compA"),
543
+ tagOf("resourceName", "r1"),
544
+ ]
545
+ let storedEvent: ReventlessCore.DcbEventLog_Adapter.rawStoredEvent = {
546
+ eventType: "ResourceAdded",
547
+ data: JSON.Object(Dict.make()),
548
+ tags: entityTags,
549
+ meta: testMeta(),
550
+ }
551
+ let storedComposite =
552
+ Runtime.toItem("100", storedEvent, ~recordedAt="2026-07-08T00:00:00Z")
553
+ ->JSON.Decode.object
554
+ ->Option.flatMap(o => o->Dict.get("tag_composite"))
555
+ ->Option.flatMap(JSON.Decode.string)
556
+
557
+ testSync("stored tag_composite equals the key a composite read would query", () => {
558
+ expect(storedComposite)->toEqual(Some(Runtime.compositeTagKey(entityTags)))
559
+ })
560
+ })
561
+
529
562
  describe("Runtime.buildConditionalTransactItems — folded create guard (after=None)", () => {
530
563
  let event = (eventType, tags): ReventlessCore.DcbEventLog_Adapter.rawStoredEvent => {
531
564
  eventType,
@@ -653,6 +653,43 @@ globalThis.describe("Runtime.buildConditionalTransactItems — composite partiti
653
653
  });
654
654
  });
655
655
 
656
+ globalThis.describe("Runtime.toItem — tag_composite is keyed on the event's entity tags", () => {
657
+ let entityTags = [
658
+ {
659
+ key: "environment",
660
+ value: "prod"
661
+ },
662
+ {
663
+ key: "platformName",
664
+ value: "plat"
665
+ },
666
+ {
667
+ key: "pluginName",
668
+ value: "plug"
669
+ },
670
+ {
671
+ key: "componentName",
672
+ value: "compA"
673
+ },
674
+ {
675
+ key: "resourceName",
676
+ value: "r1"
677
+ }
678
+ ];
679
+ let storedEvent_data = {};
680
+ let storedEvent_meta = testMeta();
681
+ let storedEvent = {
682
+ eventType: "ResourceAdded",
683
+ data: storedEvent_data,
684
+ tags: entityTags,
685
+ meta: storedEvent_meta
686
+ };
687
+ let storedComposite = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.toItem("100", storedEvent, undefined, "2026-07-08T00:00:00Z")), o => o["tag_composite"]), Stdlib_JSON.Decode.string);
688
+ globalThis.test("stored tag_composite equals the key a composite read would query", () => {
689
+ globalThis.expect(storedComposite).toEqual(DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.compositeTagKey(entityTags));
690
+ });
691
+ });
692
+
656
693
  globalThis.describe("Runtime.buildConditionalTransactItems — folded create guard (after=None)", () => {
657
694
  let event = (eventType, tags) => ({
658
695
  eventType: eventType,
@@ -98,7 +98,7 @@ let runOneCompositeEvent: (string, JSON.t) => promise<JSON.t> = %raw(`
98
98
  }
99
99
  `)
100
100
 
101
- let buildAddResourceEvent = (~environment, ~resourceName): JSON.t => {
101
+ let buildCompositeCommandEvent = (~command, ~environment, ~resourceName): JSON.t => {
102
102
  // `command` is the command type name as a plain string — the shape AppSync's
103
103
  // direct-invoke resolver sends and `CommandGenerator.payload.command: string`
104
104
  // expects.
@@ -111,12 +111,15 @@ let buildAddResourceEvent = (~environment, ~resourceName): JSON.t => {
111
111
  ("ip", JSON.Encode.null),
112
112
  ])
113
113
  Dict.fromArray([
114
- ("command", "AddResource"->JSON.Encode.string),
114
+ ("command", command->JSON.Encode.string),
115
115
  ("arguments", arguments->JSON.Encode.object),
116
116
  ("meta", meta->JSON.Encode.object),
117
117
  ])->JSON.Encode.object
118
118
  }
119
119
 
120
+ let buildAddResourceEvent = (~environment, ~resourceName): JSON.t =>
121
+ buildCompositeCommandEvent(~command="AddResource", ~environment, ~resourceName)
122
+
120
123
  let buildAppSyncEvent = (widgetId): JSON.t => {
121
124
  // CommandGenerator payload: `{command, arguments, meta, identity?}` — the
122
125
  // exact shape AppSync's direct-invoke resolver sends (matches the resolver
@@ -203,4 +206,54 @@ describe("DcbCommandTopicEntryPoint integration", () => {
203
206
  await H.deleteTable(table)
204
207
  },
205
208
  )
209
+
210
+ // Composite read-back invariant
211
+ // (docs/plans/done/dcb-composite-query-clause-fence-contention.md).
212
+ //
213
+ // A composite-partition slice must be able to READ BACK its own events. The
214
+ // stored `tag_composite` key is computed from the event's tags; if the
215
+ // framework `originatorSlice` provenance tag (appended by
216
+ // StateChangeSlice_Callback.encodeEvent) leaks into that key, it diverges from
217
+ // the read key (built from the command's entity tags only) and the composite
218
+ // GSI read never matches — so a follow-up command reads empty state.
219
+ //
220
+ // `TouchResource` requires state `Added` (i.e. it must observe the prior
221
+ // `ResourceAdded`) to succeed. With the bug the read misses → state `Absent` →
222
+ // `NotFound` → CommandRejected. With the fix the read hits → `ResourceTouched`
223
+ // is produced → CommandAccepted and a second event persists.
224
+ testAsync(
225
+ "composite read-back: a follow-up command sees the slice's own prior event",
226
+ async () => {
227
+ let table = await H.createDcbTableWithTagKeys(
228
+ "EpCompositeRB_" ++ Date.now()->Float.toString,
229
+ ["environment", "resourceName"],
230
+ )
231
+ let env = "prod"
232
+ let res = "res-rb"
233
+
234
+ let addOutcome = await runOneCompositeEvent(
235
+ table.name,
236
+ buildCompositeCommandEvent(~command="AddResource", ~environment=env, ~resourceName=res),
237
+ )
238
+ expect(
239
+ addOutcome->JSON.stringifyAny->Option.getOr("")->String.includes("CommandAccepted"),
240
+ )->toBe(true)
241
+
242
+ let touchOutcome = await runOneCompositeEvent(
243
+ table.name,
244
+ buildCompositeCommandEvent(~command="TouchResource", ~environment=env, ~resourceName=res),
245
+ )
246
+ // The read must have observed the ResourceAdded — otherwise TouchResource
247
+ // rejects NotFound (the originatorSlice-in-tag_composite regression).
248
+ expect(
249
+ touchOutcome->JSON.stringifyAny->Option.getOr("")->String.includes("CommandAccepted"),
250
+ )->toBe(true)
251
+
252
+ // Both events persisted: ResourceAdded + ResourceTouched.
253
+ let events = await H.scanEventTypes(table)
254
+ expect(events->Array.toSorted(String.compare))->toEqual(["ResourceAdded", "ResourceTouched"])
255
+
256
+ await H.deleteTable(table)
257
+ },
258
+ )
206
259
  })
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
4
5
  import * as DcbIntegrationHarness$ReventlessAws from "./DcbIntegrationHarness.res.mjs";
5
6
  import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../../src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
6
7
 
@@ -70,7 +71,7 @@ let runOneCompositeEvent = (async (tableName, event) => {
70
71
  return commandOutcomeToJson(outcome);
71
72
  });
72
73
 
73
- function buildAddResourceEvent(environment, resourceName) {
74
+ function buildCompositeCommandEvent(command, environment, resourceName) {
74
75
  let $$arguments = Object.fromEntries([
75
76
  [
76
77
  "environment",
@@ -94,7 +95,7 @@ function buildAddResourceEvent(environment, resourceName) {
94
95
  return Object.fromEntries([
95
96
  [
96
97
  "command",
97
- "AddResource"
98
+ command
98
99
  ],
99
100
  [
100
101
  "arguments",
@@ -107,6 +108,10 @@ function buildAddResourceEvent(environment, resourceName) {
107
108
  ]);
108
109
  }
109
110
 
111
+ function buildAddResourceEvent(environment, resourceName) {
112
+ return buildCompositeCommandEvent("AddResource", environment, resourceName);
113
+ }
114
+
110
115
  function buildAppSyncEvent(widgetId) {
111
116
  let command = Object.fromEntries([[
112
117
  "AddWidget",
@@ -162,7 +167,7 @@ globalThis.describe("DcbCommandTopicEntryPoint integration", () => {
162
167
  "res-d",
163
168
  "res-e"
164
169
  ];
165
- let outcomes = await Promise.all(resourceNames.map(resourceName => runOneCompositeEvent(table.name, buildAddResourceEvent("prod", resourceName))));
170
+ let outcomes = await Promise.all(resourceNames.map(resourceName => runOneCompositeEvent(table.name, buildCompositeCommandEvent("AddResource", "prod", resourceName))));
166
171
  outcomes.forEach(outcomeJson => {
167
172
  let s = Stdlib_Option.getOr(JSON.stringify(outcomeJson), "<unserializable>");
168
173
  globalThis.expect(s.includes("CommandAccepted")).toBe(true);
@@ -175,6 +180,24 @@ globalThis.describe("DcbCommandTopicEntryPoint integration", () => {
175
180
  });
176
181
  return await DcbIntegrationHarness$ReventlessAws.deleteTable(table);
177
182
  });
183
+ globalThis.test("composite read-back: a follow-up command sees the slice's own prior event", async () => {
184
+ let table = await DcbIntegrationHarness$ReventlessAws.createDcbTableWithTagKeys("EpCompositeRB_" + Date.now().toString(), [
185
+ "environment",
186
+ "resourceName"
187
+ ]);
188
+ let env = "prod";
189
+ let res = "res-rb";
190
+ let addOutcome = await runOneCompositeEvent(table.name, buildCompositeCommandEvent("AddResource", env, res));
191
+ globalThis.expect(Stdlib_Option.getOr(JSON.stringify(addOutcome), "").includes("CommandAccepted")).toBe(true);
192
+ let touchOutcome = await runOneCompositeEvent(table.name, buildCompositeCommandEvent("TouchResource", env, res));
193
+ globalThis.expect(Stdlib_Option.getOr(JSON.stringify(touchOutcome), "").includes("CommandAccepted")).toBe(true);
194
+ let events = await DcbIntegrationHarness$ReventlessAws.scanEventTypes(table);
195
+ globalThis.expect(events.toSorted(Primitive_string.compare)).toEqual([
196
+ "ResourceAdded",
197
+ "ResourceTouched"
198
+ ]);
199
+ return await DcbIntegrationHarness$ReventlessAws.deleteTable(table);
200
+ });
178
201
  });
179
202
 
180
203
  let H;
@@ -183,6 +206,7 @@ export {
183
206
  H,
184
207
  runOneAppSyncEvent,
185
208
  runOneCompositeEvent,
209
+ buildCompositeCommandEvent,
186
210
  buildAddResourceEvent,
187
211
  buildAppSyncEvent,
188
212
  }
@@ -121,6 +121,21 @@ let scanFenceIds = async (table: Util_DynamoDb_Runtime.resolvedTable): array<str
121
121
  )
122
122
  }
123
123
 
124
+ // Returns the `event` attribute of every stored event row (fence sentinels carry
125
+ // no `event` attribute and are skipped). Lets a test assert which events actually
126
+ // persisted — the signal that a composite slice could read back its own state.
127
+ let scanEventTypes = async (table: Util_DynamoDb_Runtime.resolvedTable): array<string> => {
128
+ let items = await Util_DynamoDb_Runtime.scanStream({tableName: table.name})
129
+ ->Stream.runCollect
130
+ ->Effect.runPromise
131
+ items->Array.filterMap(item =>
132
+ item
133
+ ->JSON.Decode.object
134
+ ->Option.flatMap(obj => obj->Dict.get("event"))
135
+ ->Option.flatMap(JSON.Decode.string)
136
+ )
137
+ }
138
+
124
139
  let deleteTable = async (table: Util_DynamoDb_Runtime.resolvedTable) => {
125
140
  let input = Dict.fromArray([("TableName", s(table.name))])->JSON.Encode.object
126
141
  let _ = await send(deleteTableCommand(input))
@@ -168,6 +168,13 @@ async function scanFenceIds(table) {
168
168
  }));
169
169
  }
170
170
 
171
+ async function scanEventTypes(table) {
172
+ let items = await Effect.runPromise(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.scanStream({
173
+ TableName: table.name
174
+ })));
175
+ return Stdlib_Array.filterMap(items, item => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), obj => obj["event"]), Stdlib_JSON.Decode.string));
176
+ }
177
+
171
178
  async function deleteTable(table) {
172
179
  let input = Object.fromEntries([[
173
180
  "TableName",
@@ -213,6 +220,7 @@ export {
213
220
  createDcbTable,
214
221
  createDcbTableWithTagKeys,
215
222
  scanFenceIds,
223
+ scanEventTypes,
216
224
  deleteTable,
217
225
  counter,
218
226
  freshTable,
@@ -1,7 +1,7 @@
1
1
  // Composite-partition slice fixture for `DcbCommandTopicEntryPoint_IntegrationTest`.
2
2
  //
3
3
  // Reproduces the deploy-sync workload shape from
4
- // docs/plans/dcb-composite-fence-residual-burst-contention.md: a
4
+ // docs/plans/done/dcb-composite-fence-residual-burst-contention.md: a
5
5
  // `@compositePartitionTag` over a low-cardinality prefix (`environment`) plus a
6
6
  // high-cardinality tail (`resourceName`). Distinct resources sharing the same
7
7
  // `environment` must NOT contend — but only if the entry point threads
@@ -9,27 +9,49 @@
9
9
  // single-composite-fence collapse. Without that thread the slice writes one
10
10
  // fence per member and the shared `environment` member goes hot.
11
11
  //
12
+ // It ALSO guards the composite read-back invariant from
13
+ // docs/plans/done/dcb-composite-query-clause-fence-contention.md: `TouchResource`
14
+ // requires the slice to READ its own prior `ResourceAdded` (state must be
15
+ // `Added`), which only works if the stored `tag_composite` key matches the read
16
+ // key — i.e. the framework `originatorSlice` provenance tag is excluded from the
17
+ // composite key. With that bug present the read misses, state stays `Absent`, and
18
+ // `TouchResource` is rejected `NotFound`.
19
+ //
12
20
  // Explicit `@s.matches(Reventless.DcbTag.compositePartitionMember(...))` form
13
21
  // (not the `@compositePartitionTag` PPX shorthand) because reventless-ppx is not
14
22
  // wired into reventless-aws's rescript.json — same reason as EpTestSlice.
15
23
 
16
24
  @schema
17
- type consumedEvent = ResourceAdded({environment: string, resourceName: string})
25
+ type consumedEvent =
26
+ | ResourceAdded({environment: string, resourceName: string})
27
+ | ResourceTouched({environment: string, resourceName: string})
18
28
 
19
29
  @schema
20
- type command = AddResource({
21
- environment: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=0, ~sep="/")) string,
22
- resourceName: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=1, ~sep="/")) string,
23
- })
30
+ type command =
31
+ | AddResource({
32
+ environment: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=0, ~sep="/")) string,
33
+ resourceName: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=1, ~sep="/")) string,
34
+ })
35
+ | TouchResource({
36
+ environment: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=0, ~sep="/")) string,
37
+ resourceName: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=1, ~sep="/")) string,
38
+ })
24
39
 
25
40
  @schema
26
- type error = AlreadyAdded
41
+ type error =
42
+ | AlreadyAdded
43
+ | NotFound
27
44
 
28
45
  @schema
29
- type event = ResourceAdded({
30
- environment: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=0, ~sep="/")) string,
31
- resourceName: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=1, ~sep="/")) string,
32
- })
46
+ type event =
47
+ | ResourceAdded({
48
+ environment: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=0, ~sep="/")) string,
49
+ resourceName: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=1, ~sep="/")) string,
50
+ })
51
+ | ResourceTouched({
52
+ environment: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=0, ~sep="/")) string,
53
+ resourceName: @s.matches(Reventless.DcbTag.compositePartitionMember(~position=1, ~sep="/")) string,
54
+ })
33
55
 
34
56
  let name = "EpCompositeSlice"
35
57
  let moduleUrl = "ep-test://EpCompositeSlice"
@@ -3,25 +3,49 @@
3
3
  import * as S from "sury/src/S.res.mjs";
4
4
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
5
5
 
6
- let consumedEventSchema = S.schema(s => ({
7
- TAG: "ResourceAdded",
8
- environment: s.m(S.string),
9
- resourceName: s.m(S.string)
10
- }));
11
-
12
- let commandSchema = S.schema(s => ({
13
- TAG: "AddResource",
14
- environment: s.m(DcbTag$Reventless.compositePartitionMember(0, "/")),
15
- resourceName: s.m(DcbTag$Reventless.compositePartitionMember(1, "/"))
16
- }));
17
-
18
- let errorSchema = S.literal("AlreadyAdded");
19
-
20
- let eventSchema = S.schema(s => ({
21
- TAG: "ResourceAdded",
22
- environment: s.m(DcbTag$Reventless.compositePartitionMember(0, "/")),
23
- resourceName: s.m(DcbTag$Reventless.compositePartitionMember(1, "/"))
24
- }));
6
+ let consumedEventSchema = S.union([
7
+ S.schema(s => ({
8
+ TAG: "ResourceAdded",
9
+ environment: s.m(S.string),
10
+ resourceName: s.m(S.string)
11
+ })),
12
+ S.schema(s => ({
13
+ TAG: "ResourceTouched",
14
+ environment: s.m(S.string),
15
+ resourceName: s.m(S.string)
16
+ }))
17
+ ]);
18
+
19
+ let commandSchema = S.union([
20
+ S.schema(s => ({
21
+ TAG: "AddResource",
22
+ environment: s.m(DcbTag$Reventless.compositePartitionMember(0, "/")),
23
+ resourceName: s.m(DcbTag$Reventless.compositePartitionMember(1, "/"))
24
+ })),
25
+ S.schema(s => ({
26
+ TAG: "TouchResource",
27
+ environment: s.m(DcbTag$Reventless.compositePartitionMember(0, "/")),
28
+ resourceName: s.m(DcbTag$Reventless.compositePartitionMember(1, "/"))
29
+ }))
30
+ ]);
31
+
32
+ let errorSchema = S.union([
33
+ S.literal("AlreadyAdded"),
34
+ S.literal("NotFound")
35
+ ]);
36
+
37
+ let eventSchema = S.union([
38
+ S.schema(s => ({
39
+ TAG: "ResourceAdded",
40
+ environment: s.m(DcbTag$Reventless.compositePartitionMember(0, "/")),
41
+ resourceName: s.m(DcbTag$Reventless.compositePartitionMember(1, "/"))
42
+ })),
43
+ S.schema(s => ({
44
+ TAG: "ResourceTouched",
45
+ environment: s.m(DcbTag$Reventless.compositePartitionMember(0, "/")),
46
+ resourceName: s.m(DcbTag$Reventless.compositePartitionMember(1, "/"))
47
+ }))
48
+ ]);
25
49
 
26
50
  function commandAuthorization(param) {
27
51
  return "AllowAnonymous";
@@ -1,22 +1,35 @@
1
1
  // Behavior pair for `EpCompositeSlice`. Append-on-empty / reject-on-seen, per
2
2
  // composite entity: the decision read is scoped to one `{environment,
3
- // resourceName}` composite, so a fresh resource sees empty state and appends,
4
- // while a duplicate of the same composite key sees `true` and is rejected.
3
+ // resourceName}` composite, so a fresh resource sees `Absent` and appends, while
4
+ // a duplicate of the same composite key sees `Added` and is rejected.
5
+ //
6
+ // `TouchResource` is the read-back probe: it requires the slice to have already
7
+ // observed its own `ResourceAdded` (state `Added`) to succeed, so it exercises
8
+ // the composite read-matching-its-own-write invariant.
5
9
 
6
- type state = bool
10
+ type state =
11
+ | Absent
12
+ | Added
7
13
 
8
- let initialState = false
14
+ let initialState = Absent
9
15
 
10
- let evolve = (_state, _event: EpCompositeSlice.consumedEvent) => true
16
+ let evolve = (_state, event: EpCompositeSlice.consumedEvent) =>
17
+ switch event {
18
+ | ResourceAdded(_) => Added
19
+ | ResourceTouched(_) => Added
20
+ }
11
21
 
12
22
  let decide = (state, command: EpCompositeSlice.command): result<
13
23
  array<EpCompositeSlice.event>,
14
24
  EpCompositeSlice.error,
15
25
  > =>
16
26
  switch (state, command) {
17
- | (true, _) => Error(AlreadyAdded)
18
- | (false, AddResource({environment, resourceName})) =>
27
+ | (Added, AddResource(_)) => Error(AlreadyAdded)
28
+ | (Absent, AddResource({environment, resourceName})) =>
19
29
  Ok([ResourceAdded({environment, resourceName})])
30
+ | (Added, TouchResource({environment, resourceName})) =>
31
+ Ok([ResourceTouched({environment, resourceName})])
32
+ | (Absent, TouchResource(_)) => Error(NotFound)
20
33
  }
21
34
 
22
35
  let moduleUrl = "ep-test://EpCompositeSliceBehavior"
@@ -1,12 +1,28 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
 
4
- function evolve(_state, _event) {
5
- return true;
4
+ function evolve(_state, event) {
5
+ return "Added";
6
6
  }
7
7
 
8
8
  function decide(state, command) {
9
- if (state) {
9
+ if (state === "Absent") {
10
+ if (command.TAG === "AddResource") {
11
+ return {
12
+ TAG: "Ok",
13
+ _0: [{
14
+ TAG: "ResourceAdded",
15
+ environment: command.environment,
16
+ resourceName: command.resourceName
17
+ }]
18
+ };
19
+ } else {
20
+ return {
21
+ TAG: "Error",
22
+ _0: "NotFound"
23
+ };
24
+ }
25
+ } else if (command.TAG === "AddResource") {
10
26
  return {
11
27
  TAG: "Error",
12
28
  _0: "AlreadyAdded"
@@ -15,7 +31,7 @@ function decide(state, command) {
15
31
  return {
16
32
  TAG: "Ok",
17
33
  _0: [{
18
- TAG: "ResourceAdded",
34
+ TAG: "ResourceTouched",
19
35
  environment: command.environment,
20
36
  resourceName: command.resourceName
21
37
  }]
@@ -23,7 +39,7 @@ function decide(state, command) {
23
39
  }
24
40
  }
25
41
 
26
- let initialState = false;
42
+ let initialState = "Absent";
27
43
 
28
44
  let moduleUrl = "ep-test://EpCompositeSliceBehavior";
29
45