@reventlessdev/reventless-aws 3.0.0-alpha.417 → 3.0.0-alpha.419
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 +15 -0
- package/package.json +9 -9
- package/src/adapter/Runtime/AggregateEntryPoint.mjs +12 -2
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +6 -2
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +6 -2
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +39 -8
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +31 -6
- package/tests/AggregateEntryPoint_WiringTest.res +124 -0
- package/tests/AggregateEntryPoint_WiringTest.res.mjs +152 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res +1 -1
- package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
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.419 (2026-11-01)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core:** a command's outcome, the flow through automations, and each view change are told ([dcaeed2](https://github.com/ReventlessDev/reventless-core/commit/dcaeed2fabe2047a21797b459f2931618474dec3))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.418 (2026-11-01)
|
|
14
|
+
|
|
15
|
+
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.417 (2026-10-30)
|
|
7
22
|
|
|
8
23
|
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.419",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -22,19 +22,19 @@
|
|
|
22
22
|
"sury": "11.0.0",
|
|
23
23
|
"uuid": "^13.0.0",
|
|
24
24
|
"yaml": "^2.8.3",
|
|
25
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.38",
|
|
25
26
|
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
26
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.37",
|
|
27
27
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
28
|
-
"@reventlessdev/rescript-
|
|
29
|
-
"@reventlessdev/rescript-
|
|
30
|
-
"@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
|
|
28
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.25",
|
|
29
|
+
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.38",
|
|
31
30
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
32
31
|
"@reventlessdev/rescript-web": "1.0.0-alpha.8",
|
|
33
|
-
"@reventlessdev/
|
|
32
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
|
|
33
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.326",
|
|
34
34
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.42",
|
|
35
|
-
"@reventlessdev/reventless-
|
|
36
|
-
"@reventlessdev/reventless-
|
|
37
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
35
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.190",
|
|
36
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.221",
|
|
37
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.192"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"rescript": "12.3.1",
|
|
@@ -137,12 +137,22 @@ async function checkPluginStatus(event) {
|
|
|
137
137
|
// Postgres EventLog runtime; absence keeps the DynamoDB path byte-identical.
|
|
138
138
|
// `eventLogTableName` doubles as the Postgres `event_log.log_name` discriminator
|
|
139
139
|
// (all aggregates share one Postgres table, unlike DynamoDB's table-per-aggregate).
|
|
140
|
-
|
|
140
|
+
// Exported for tests, which pass `makeStorage` to stand an in-memory storage in for
|
|
141
|
+
// DynamoDB and drive the functor wiring below exactly as a deployed handler does
|
|
142
|
+
// (tests/AggregateEntryPoint_WiringTest.res).
|
|
143
|
+
export function buildAggregateParts(
|
|
144
|
+
specModule,
|
|
145
|
+
decisionModule,
|
|
146
|
+
eventLogTableName,
|
|
147
|
+
queueUrl,
|
|
148
|
+
pgConnection,
|
|
149
|
+
makeStorage = makeStorageOps,
|
|
150
|
+
) {
|
|
141
151
|
const patchedSpec = patchSpecId(specModule);
|
|
142
152
|
// Typed core: backend-specific EventLog storage ops (Postgres when pgConnection
|
|
143
153
|
// is present, else DynamoDB). Returns the full 6-field EventLog_Adapter.operations
|
|
144
154
|
// — the shell previously dropped latestSnapshot/writeSnapshot.
|
|
145
|
-
const rawStorageOps =
|
|
155
|
+
const rawStorageOps = makeStorage(eventLogTableName, pgConnection);
|
|
146
156
|
const eventLogOps = eventLogOperationsMake(patchedSpec)({
|
|
147
157
|
Spec: patchedSpec,
|
|
148
158
|
EventTopic: { Spec: patchedSpec },
|
|
@@ -539,7 +539,7 @@ let makeOutboundJsonEventsHandler = (
|
|
|
539
539
|
->Option.getOr("")
|
|
540
540
|
let (eventType, dataDict) = rawEvent->ReventlessCore.Message.splitMessage
|
|
541
541
|
switch decoder.decode(~eventType, ~data=dataDict) {
|
|
542
|
-
| Some(event) => [(sourceId, event)]
|
|
542
|
+
| Some(event) => [(sourceId, event, ReventlessCore.Message.metaOfEnvelope(envelope))]
|
|
543
543
|
| None => []
|
|
544
544
|
}
|
|
545
545
|
})
|
|
@@ -549,7 +549,11 @@ let makeOutboundJsonEventsHandler = (
|
|
|
549
549
|
->Effect.flatMap(events =>
|
|
550
550
|
Effect.promise(async () => {
|
|
551
551
|
await loadTodoItems()
|
|
552
|
-
await callback.phase1(
|
|
552
|
+
await callback.phase1(
|
|
553
|
+
events->Array.map(((sourceId, event, _)) => (sourceId, event)),
|
|
554
|
+
~metas=events->Array.map(((_, _, meta)) => meta),
|
|
555
|
+
~capabilities=capabilities(),
|
|
556
|
+
)
|
|
553
557
|
await syncTodoItems()
|
|
554
558
|
await callback.phase2(publishJsons, ~capabilities=capabilities())
|
|
555
559
|
await syncTodoItems()
|
|
@@ -323,14 +323,18 @@ function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJso
|
|
|
323
323
|
if (event !== undefined) {
|
|
324
324
|
return [[
|
|
325
325
|
sourceId,
|
|
326
|
-
Primitive_option.valFromOption(event)
|
|
326
|
+
Primitive_option.valFromOption(event),
|
|
327
|
+
Message$ReventlessCore.metaOfEnvelope(envelope)
|
|
327
328
|
]];
|
|
328
329
|
} else {
|
|
329
330
|
return [];
|
|
330
331
|
}
|
|
331
332
|
})), events => Stream$1.fromIterable(events))), events => Effect.promise(async () => {
|
|
332
333
|
await loadTodoItems();
|
|
333
|
-
await callback.phase1(events
|
|
334
|
+
await callback.phase1(events.map(param => [
|
|
335
|
+
param[0],
|
|
336
|
+
param[1]
|
|
337
|
+
]), events.map(param => param[2]), capabilities());
|
|
334
338
|
await syncTodoItems();
|
|
335
339
|
await callback.phase2(publishJsons, capabilities());
|
|
336
340
|
return await syncTodoItems();
|
|
@@ -126,6 +126,20 @@ let decodeEnvelope = (json: JSON.t): envelope<'event> => {
|
|
|
126
126
|
|
|
127
127
|
let exnMessage = StreamRoutedEntryPoint_Ops.exnMessage
|
|
128
128
|
|
|
129
|
+
// The event an envelope carries, as the source of the rows its actions change.
|
|
130
|
+
// Read off the JSON, like `producerTime`: an envelope without a meta yields none.
|
|
131
|
+
let sourcesOf = (json: JSON.t): array<ReventlessCore.Projection.changeSource> => {
|
|
132
|
+
let fields = json->JSON.Decode.object->Option.getOr(Dict.make())
|
|
133
|
+
let meta = fields->Dict.get("meta")->Option.flatMap(JSON.Decode.object)
|
|
134
|
+
let string = (d, k) => d->Option.flatMap(d => d->Dict.get(k))->Option.flatMap(JSON.Decode.string)
|
|
135
|
+
switch string(meta, "msgId") {
|
|
136
|
+
| Some(msgId) =>
|
|
137
|
+
let (entityId, entityType) = ReventlessCore.ViewChange.entityOfEnvelope(fields)
|
|
138
|
+
[{msgId, correlationId: ?string(meta, "correlationId"), ?entityId, ?entityType}]
|
|
139
|
+
| None => []
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
// The typed core of buildJsonEventsHandler: decode each envelope, parse the
|
|
130
144
|
// event against the slice's consumed-event schema, project, and run every
|
|
131
145
|
// resulting action against the view table. A decode failure logs and yields no
|
|
@@ -140,7 +154,13 @@ let makeJsonEventsHandler = (
|
|
|
140
154
|
~queryDbOps: ReventlessCore.QueryDb_Adapter.operations,
|
|
141
155
|
~subIdConfig: option<Reventless.ReadModel.subIdConfig<JSON.t>>,
|
|
142
156
|
~stateSchema: option<S.t<unknown>>,
|
|
143
|
-
): ReventlessCore.EventCollector.jsonEventsHandler =>
|
|
157
|
+
): ReventlessCore.EventCollector.jsonEventsHandler => {
|
|
158
|
+
// Each row the events change is logged; the states are already JSON.
|
|
159
|
+
let onChange = ReventlessCore.ViewChange.reporter(
|
|
160
|
+
~comp=sliceName,
|
|
161
|
+
~view=sliceName,
|
|
162
|
+
~stateToJson=state => state,
|
|
163
|
+
)
|
|
144
164
|
stream =>
|
|
145
165
|
stream
|
|
146
166
|
->Stream.mapEffect(json =>
|
|
@@ -155,11 +175,14 @@ let makeJsonEventsHandler = (
|
|
|
155
175
|
// The deployed slice assembles JSON-level operations rather than going
|
|
156
176
|
// through the typed rewrite, so the trail is appended here — with this
|
|
157
177
|
// envelope's own time, which the apply below no longer has.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
178
|
+
let sources = sourcesOf(json)
|
|
179
|
+
let actions =
|
|
180
|
+
producerTime === ""
|
|
181
|
+
? actions
|
|
182
|
+
: actions->Array.map(
|
|
183
|
+
ReventlessCore.Projection.rewriteJsonAction(_, ~at=producerTime, ~stateSchema),
|
|
184
|
+
)
|
|
185
|
+
actions->Array.map(action => (action, sources))
|
|
163
186
|
} catch {
|
|
164
187
|
| exn =>
|
|
165
188
|
StreamRoutedEntryPoint_Ops.logError(
|
|
@@ -171,7 +194,7 @@ let makeJsonEventsHandler = (
|
|
|
171
194
|
)
|
|
172
195
|
)
|
|
173
196
|
->Stream.flatMap(actions => Stream.fromIterable(actions))
|
|
174
|
-
->Stream.runForEach(action =>
|
|
197
|
+
->Stream.runForEach(((action, sources)) =>
|
|
175
198
|
// Thread the slice's `subIdConfig` (generated from `@subId`) so
|
|
176
199
|
// sub-id-dependent actions like `UpdateMultiState` can resolve the sort
|
|
177
200
|
// key. Dropping it here made every such action hit the
|
|
@@ -181,9 +204,17 @@ let makeJsonEventsHandler = (
|
|
|
181
204
|
// actions. `~comp` is the slice's spec name — attribution for
|
|
182
205
|
// Projection.handleAction's debug-lazy action log.
|
|
183
206
|
Effect.promise(() =>
|
|
184
|
-
ReventlessCore.Projection.handleAction(
|
|
207
|
+
ReventlessCore.Projection.handleAction(
|
|
208
|
+
~comp=sliceName,
|
|
209
|
+
~sources,
|
|
210
|
+
~onChange,
|
|
211
|
+
action,
|
|
212
|
+
queryDbOps,
|
|
213
|
+
subIdConfig,
|
|
214
|
+
)
|
|
185
215
|
)->Effect.map(_ => ())
|
|
186
216
|
)
|
|
217
|
+
}
|
|
187
218
|
|
|
188
219
|
// ── Handler assembly (called by the shell per HANDLER_CONFIG entry) ─────────
|
|
189
220
|
// `sliceModules` types the shell's reads off the two dynamically imported
|
|
@@ -8,6 +8,7 @@ import * as Stream from "effect/Stream";
|
|
|
8
8
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
9
9
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
10
10
|
import * as Projection$ReventlessCore from "@reventlessdev/reventless-core/src/Projection.res.mjs";
|
|
11
|
+
import * as ViewChange$ReventlessCore from "@reventlessdev/reventless-core/src/ViewChange.res.mjs";
|
|
11
12
|
import * as ProjectionEntryPoint_Ops$ReventlessAws from "./ProjectionEntryPoint_Ops.res.mjs";
|
|
12
13
|
import * as StreamRoutedEntryPoint_Ops$ReventlessAws from "./StreamRoutedEntryPoint_Ops.res.mjs";
|
|
13
14
|
|
|
@@ -64,7 +65,25 @@ function decodeEnvelope(json) {
|
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
function sourcesOf(json) {
|
|
69
|
+
let fields = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(json), {});
|
|
70
|
+
let meta = Stdlib_Option.flatMap(fields["meta"], Stdlib_JSON.Decode.object);
|
|
71
|
+
let string = (d, k) => Stdlib_Option.flatMap(Stdlib_Option.flatMap(d, d => d[k]), Stdlib_JSON.Decode.string);
|
|
72
|
+
let msgId = string(meta, "msgId");
|
|
73
|
+
if (msgId === undefined) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
let match = ViewChange$ReventlessCore.entityOfEnvelope(fields);
|
|
77
|
+
return [{
|
|
78
|
+
msgId: msgId,
|
|
79
|
+
correlationId: string(meta, "correlationId"),
|
|
80
|
+
entityId: match[0],
|
|
81
|
+
entityType: match[1]
|
|
82
|
+
}];
|
|
83
|
+
}
|
|
84
|
+
|
|
67
85
|
function makeJsonEventsHandler(sliceName, eventSchema, project, queryDbOps, subIdConfig, stateSchema) {
|
|
86
|
+
let onChange = ViewChange$ReventlessCore.reporter(sliceName, sliceName, state => state);
|
|
68
87
|
return stream => Stream.runForEach(Stream.flatMap(Stream.mapEffect(stream, json => Effect.sync(() => {
|
|
69
88
|
try {
|
|
70
89
|
let match = decodeEnvelope(json);
|
|
@@ -74,11 +93,12 @@ function makeJsonEventsHandler(sliceName, eventSchema, project, queryDbOps, subI
|
|
|
74
93
|
meta: match.meta,
|
|
75
94
|
recordedAt: match.recordedAt
|
|
76
95
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
let sources = sourcesOf(json);
|
|
97
|
+
let actions$1 = producerTime === "" ? actions : actions.map(__x => Projection$ReventlessCore.rewriteJsonAction(__x, producerTime, stateSchema));
|
|
98
|
+
return actions$1.map(action => [
|
|
99
|
+
action,
|
|
100
|
+
sources
|
|
101
|
+
]);
|
|
82
102
|
} catch (raw_exn) {
|
|
83
103
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
84
104
|
StreamRoutedEntryPoint_Ops$ReventlessAws.logError("failed to decode event", {
|
|
@@ -87,7 +107,11 @@ function makeJsonEventsHandler(sliceName, eventSchema, project, queryDbOps, subI
|
|
|
87
107
|
});
|
|
88
108
|
return [];
|
|
89
109
|
}
|
|
90
|
-
})), actions => Stream.fromIterable(actions)),
|
|
110
|
+
})), actions => Stream.fromIterable(actions)), param => {
|
|
111
|
+
let sources = param[1];
|
|
112
|
+
let action = param[0];
|
|
113
|
+
return Effect.map(Effect.promise(() => Projection$ReventlessCore.handleAction(sliceName, sources, onChange, action, queryDbOps, subIdConfig)), param => {});
|
|
114
|
+
});
|
|
91
115
|
}
|
|
92
116
|
|
|
93
117
|
function makeRegisteredHandler(entry, modules) {
|
|
@@ -106,6 +130,7 @@ export {
|
|
|
106
130
|
parseHandlerConfig,
|
|
107
131
|
decodeEnvelope,
|
|
108
132
|
exnMessage,
|
|
133
|
+
sourcesOf,
|
|
109
134
|
makeJsonEventsHandler,
|
|
110
135
|
makeRegisteredHandler,
|
|
111
136
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// The deployed aggregate handler, wired exactly as `AggregateEntryPoint.mjs` wires it,
|
|
2
|
+
// over an in-memory storage in place of DynamoDB.
|
|
3
|
+
//
|
|
4
|
+
// The shell is hand-written JS that passes the compiled EventLog operations MODULE
|
|
5
|
+
// where the aggregate callback expects the `EventLog.operations` RECORD, so a field
|
|
6
|
+
// the record has and the module does not export under the same name type-checks
|
|
7
|
+
// everywhere and fails only in a Lambda. That is how a primed field
|
|
8
|
+
// (`replayStream'`, exported as `replayStream$p`) took down every command of the
|
|
9
|
+
// platform's Plugin aggregate on AWS while every in-process test passed. The
|
|
10
|
+
// command that broke was a derived one — it carries the stable-id header, which
|
|
11
|
+
// sends the handler down the cause-reading replay — so that is the command sent
|
|
12
|
+
// here, its redelivery, and a plain command, which between them reach every
|
|
13
|
+
// operation the aggregate callback reads.
|
|
14
|
+
|
|
15
|
+
open JestGlobals
|
|
16
|
+
|
|
17
|
+
module Spec = AggTestAggregate
|
|
18
|
+
|
|
19
|
+
// ── An in-memory storage, typed by the contract the shell's storage satisfies ──
|
|
20
|
+
|
|
21
|
+
let stored: dict<array<JSON.t>> = Dict.make()
|
|
22
|
+
|
|
23
|
+
let storage: ReventlessCore.EventLog_Adapter.operations = {
|
|
24
|
+
append: async (seq, id, events) => {
|
|
25
|
+
let current = stored->Dict.get(id)->Option.getOr([])
|
|
26
|
+
if current->Array.length != seq {
|
|
27
|
+
Error(ReventlessCore.EventLog.Conflict)
|
|
28
|
+
} else {
|
|
29
|
+
stored->Dict.set(id, current->Array.concat(events))
|
|
30
|
+
Ok()
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
replay: async id => stored->Dict.get(id)->Option.getOr([]),
|
|
34
|
+
replayStream: (id, ~fromSeq=0) =>
|
|
35
|
+
Stream.fromIterable(stored->Dict.get(id)->Option.getOr([])->Array.slice(~start=fromSeq)),
|
|
36
|
+
appendStream: (_, _, _) => Effect.succeed(),
|
|
37
|
+
latestSnapshot: async _ => Ok(None),
|
|
38
|
+
writeSnapshot: async (_, _) => Ok(),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── The shell's own wiring ────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
type parts = {handleJsonCommands: ReventlessCore.CommandTopic_Helpers.jsonCommandsHandler}
|
|
44
|
+
|
|
45
|
+
let buildParts: ReventlessCore.EventLog_Adapter.operations => promise<parts> = %raw(`
|
|
46
|
+
async (storage) => {
|
|
47
|
+
const { buildAggregateParts } = await import(
|
|
48
|
+
"@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs"
|
|
49
|
+
);
|
|
50
|
+
const spec = await import("./integration/AggTestAggregate.res.mjs");
|
|
51
|
+
const decision = await import("./integration/AggTestAggregateDecision.res.mjs");
|
|
52
|
+
return buildAggregateParts(
|
|
53
|
+
spec,
|
|
54
|
+
decision,
|
|
55
|
+
"agg-wiring-test",
|
|
56
|
+
"https://sqs.eu-west-1.amazonaws.com/000000000000/agg-wiring-test",
|
|
57
|
+
undefined,
|
|
58
|
+
() => storage,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
`)
|
|
62
|
+
|
|
63
|
+
/** A command a mapping derived: its id is stable across redeliveries, and the
|
|
64
|
+
header says so. */
|
|
65
|
+
let derived = (~id, ~msgId, name): JSON.t => {
|
|
66
|
+
let meta: Reventless.Message.meta = {
|
|
67
|
+
service: "AggWiringTest",
|
|
68
|
+
time: "2026-11-01T00:00:00.000Z",
|
|
69
|
+
msgId,
|
|
70
|
+
correlationId: msgId,
|
|
71
|
+
headers: Dict.fromArray([(ReventlessInfra.Message.stableIdHeader, "true")]),
|
|
72
|
+
}
|
|
73
|
+
ReventlessCore.Message.encodeCommand'(
|
|
74
|
+
{
|
|
75
|
+
Reventless.Message.id: Reventless.Id.String.makeFromString(id),
|
|
76
|
+
meta,
|
|
77
|
+
command: Spec.Add({name: name}),
|
|
78
|
+
},
|
|
79
|
+
Reventless.Id.String.schema,
|
|
80
|
+
Spec.commandSchema,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let send = async (parts: parts, json) =>
|
|
85
|
+
await parts.handleJsonCommands(
|
|
86
|
+
Stream.fromIterable([{ReventlessInfra.CommandTopic.reference: "r", command: json}]),
|
|
87
|
+
)->Effect.runPromise
|
|
88
|
+
|
|
89
|
+
describe("the deployed aggregate handler's wiring", () => {
|
|
90
|
+
beforeEach(() => stored->Dict.keysToArray->Array.forEach(k => stored->Dict.delete(k)))
|
|
91
|
+
|
|
92
|
+
testPromise("handles a derived command, which replays its stream with causes", async () => {
|
|
93
|
+
let parts = await buildParts(storage)
|
|
94
|
+
let results = await send(parts, derived(~id="a-1", ~msgId="m-1", "first"))
|
|
95
|
+
expect(results->Array.every(Result.isOk))->toBe(true)
|
|
96
|
+
expect(stored->Dict.get("a-1")->Option.map(Array.length))->toEqual(Some(1))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
testPromise("appends nothing for a redelivery of a derived command", async () => {
|
|
100
|
+
let parts = await buildParts(storage)
|
|
101
|
+
let _ = await send(parts, derived(~id="a-2", ~msgId="m-2", "first"))
|
|
102
|
+
let again = await send(parts, derived(~id="a-2", ~msgId="m-2", "first"))
|
|
103
|
+
expect(again->Array.every(Result.isOk))->toBe(true)
|
|
104
|
+
expect(stored->Dict.get("a-2")->Option.map(Array.length))->toEqual(Some(1))
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// An API command: no stable id, so the handler seeds from the snapshot read and
|
|
108
|
+
// replays events alone — the other operations the shell's module must carry.
|
|
109
|
+
testPromise("handles a command with a fresh id after a derived one", async () => {
|
|
110
|
+
let parts = await buildParts(storage)
|
|
111
|
+
let _ = await send(parts, derived(~id="a-3", ~msgId="m-3", "first"))
|
|
112
|
+
let fresh = derived(~id="a-3", ~msgId="m-4", "second")
|
|
113
|
+
switch fresh {
|
|
114
|
+
| Object(d) =>
|
|
115
|
+
d
|
|
116
|
+
->Dict.get("meta")
|
|
117
|
+
->Option.flatMap(JSON.Decode.object)
|
|
118
|
+
->Option.forEach(meta => meta->Dict.delete("headers"))
|
|
119
|
+
| _ => ()
|
|
120
|
+
}
|
|
121
|
+
let results = await send(parts, fresh)
|
|
122
|
+
expect(results->Array.every(Result.isOk))->toBe(true)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Stream from "effect/Stream";
|
|
10
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
11
|
+
import * as Message$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/Message.res.mjs";
|
|
12
|
+
import * as AggTestAggregate$ReventlessAws from "./integration/AggTestAggregate.res.mjs";
|
|
13
|
+
|
|
14
|
+
let stored = {};
|
|
15
|
+
|
|
16
|
+
async function storage_append(seq, id, events) {
|
|
17
|
+
let current = Stdlib_Option.getOr(stored[id], []);
|
|
18
|
+
if (current.length !== seq) {
|
|
19
|
+
return {
|
|
20
|
+
TAG: "Error",
|
|
21
|
+
_0: "Conflict"
|
|
22
|
+
};
|
|
23
|
+
} else {
|
|
24
|
+
stored[id] = current.concat(events);
|
|
25
|
+
return {
|
|
26
|
+
TAG: "Ok",
|
|
27
|
+
_0: undefined
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function storage_replay(id) {
|
|
33
|
+
return Stdlib_Option.getOr(stored[id], []);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function storage_replayStream(id, fromSeqOpt) {
|
|
37
|
+
let fromSeq = fromSeqOpt !== undefined ? fromSeqOpt : 0;
|
|
38
|
+
return Stream.fromIterable(Stdlib_Option.getOr(stored[id], []).slice(fromSeq));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function storage_appendStream(param, param$1, param$2) {
|
|
42
|
+
return Effect.succeed();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function storage_latestSnapshot(param) {
|
|
46
|
+
return {
|
|
47
|
+
TAG: "Ok",
|
|
48
|
+
_0: undefined
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function storage_writeSnapshot(param, param$1) {
|
|
53
|
+
return {
|
|
54
|
+
TAG: "Ok",
|
|
55
|
+
_0: undefined
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let storage = {
|
|
60
|
+
append: storage_append,
|
|
61
|
+
replay: storage_replay,
|
|
62
|
+
replayStream: storage_replayStream,
|
|
63
|
+
appendStream: storage_appendStream,
|
|
64
|
+
latestSnapshot: storage_latestSnapshot,
|
|
65
|
+
writeSnapshot: storage_writeSnapshot
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let buildParts = (async (storage) => {
|
|
69
|
+
const { buildAggregateParts } = await import(
|
|
70
|
+
"@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs"
|
|
71
|
+
);
|
|
72
|
+
const spec = await import("./integration/AggTestAggregate.res.mjs");
|
|
73
|
+
const decision = await import("./integration/AggTestAggregateDecision.res.mjs");
|
|
74
|
+
return buildAggregateParts(
|
|
75
|
+
spec,
|
|
76
|
+
decision,
|
|
77
|
+
"agg-wiring-test",
|
|
78
|
+
"https://sqs.eu-west-1.amazonaws.com/000000000000/agg-wiring-test",
|
|
79
|
+
undefined,
|
|
80
|
+
() => storage,
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
function derived(id, msgId, name) {
|
|
85
|
+
let meta_headers = Object.fromEntries([[
|
|
86
|
+
Message$ReventlessInfra.stableIdHeader,
|
|
87
|
+
"true"
|
|
88
|
+
]]);
|
|
89
|
+
let meta = {
|
|
90
|
+
service: "AggWiringTest",
|
|
91
|
+
time: "2026-11-01T00:00:00.000Z",
|
|
92
|
+
msgId: msgId,
|
|
93
|
+
correlationId: msgId,
|
|
94
|
+
headers: meta_headers
|
|
95
|
+
};
|
|
96
|
+
return Message$ReventlessCore.encodeCommand$p({
|
|
97
|
+
id: Id$Reventless.$$String.makeFromString(id),
|
|
98
|
+
meta: meta,
|
|
99
|
+
command: {
|
|
100
|
+
TAG: "Add",
|
|
101
|
+
name: name
|
|
102
|
+
}
|
|
103
|
+
}, Id$Reventless.$$String.schema, AggTestAggregate$ReventlessAws.commandSchema);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function send(parts, json) {
|
|
107
|
+
return await Effect.runPromise(parts.handleJsonCommands(Stream.fromIterable([{
|
|
108
|
+
command: json,
|
|
109
|
+
reference: "r"
|
|
110
|
+
}])));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
globalThis.describe("the deployed aggregate handler's wiring", () => {
|
|
114
|
+
globalThis.beforeEach(() => {
|
|
115
|
+
Object.keys(stored).forEach(k => Stdlib_Dict.$$delete(stored, k));
|
|
116
|
+
});
|
|
117
|
+
globalThis.test("handles a derived command, which replays its stream with causes", async () => {
|
|
118
|
+
let parts = await buildParts(storage);
|
|
119
|
+
let results = await send(parts, derived("a-1", "m-1", "first"));
|
|
120
|
+
globalThis.expect(results.every(Stdlib_Result.isOk)).toBe(true);
|
|
121
|
+
globalThis.expect(Stdlib_Option.map(stored["a-1"], prim => prim.length)).toEqual(1);
|
|
122
|
+
});
|
|
123
|
+
globalThis.test("appends nothing for a redelivery of a derived command", async () => {
|
|
124
|
+
let parts = await buildParts(storage);
|
|
125
|
+
await send(parts, derived("a-2", "m-2", "first"));
|
|
126
|
+
let again = await send(parts, derived("a-2", "m-2", "first"));
|
|
127
|
+
globalThis.expect(again.every(Stdlib_Result.isOk)).toBe(true);
|
|
128
|
+
globalThis.expect(Stdlib_Option.map(stored["a-2"], prim => prim.length)).toEqual(1);
|
|
129
|
+
});
|
|
130
|
+
globalThis.test("handles a command with a fresh id after a derived one", async () => {
|
|
131
|
+
let parts = await buildParts(storage);
|
|
132
|
+
await send(parts, derived("a-3", "m-3", "first"));
|
|
133
|
+
let fresh = derived("a-3", "m-4", "second");
|
|
134
|
+
if (typeof fresh === "object" && fresh !== null && !Array.isArray(fresh)) {
|
|
135
|
+
Stdlib_Option.forEach(Stdlib_Option.flatMap(fresh["meta"], Stdlib_JSON.Decode.object), meta => Stdlib_Dict.$$delete(meta, "headers"));
|
|
136
|
+
}
|
|
137
|
+
let results = await send(parts, fresh);
|
|
138
|
+
globalThis.expect(results.every(Stdlib_Result.isOk)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
let Spec;
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
Spec,
|
|
146
|
+
stored,
|
|
147
|
+
storage,
|
|
148
|
+
buildParts,
|
|
149
|
+
derived,
|
|
150
|
+
send,
|
|
151
|
+
}
|
|
152
|
+
/* buildParts Not a pure module */
|
|
@@ -388,7 +388,7 @@ describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler", () => {
|
|
|
388
388
|
let received = ref([])
|
|
389
389
|
let callback: AutomationSliceEntryPoint_Ops.outboundCallback<outboundEvent> = {
|
|
390
390
|
todoItems: Dict.make(),
|
|
391
|
-
phase1: async (events, ~capabilities as _) => {
|
|
391
|
+
phase1: async (events, ~metas as _=?, ~capabilities as _) => {
|
|
392
392
|
received := events
|
|
393
393
|
steps->Array.push("phase1")
|
|
394
394
|
},
|
|
@@ -581,7 +581,7 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler
|
|
|
581
581
|
contents: []
|
|
582
582
|
};
|
|
583
583
|
let callback_todoItems = {};
|
|
584
|
-
let callback_phase1 = async (events, param) => {
|
|
584
|
+
let callback_phase1 = async (events, param, param$1) => {
|
|
585
585
|
received.contents = events;
|
|
586
586
|
steps.push("phase1");
|
|
587
587
|
};
|