@reventlessdev/reventless-core 3.0.0-alpha.204 → 3.0.0-alpha.206
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 +6 -6
- package/src/components/Dcb/Dcb_Builder.res +3 -0
- package/src/components/Dcb/Dcb_Builder.res.mjs +1 -1
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +1 -0
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +64 -11
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res.mjs +82 -62
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res +7 -5
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs +2 -2
- package/tests/api/GeocodingTest.res +65 -0
- package/tests/api/GeocodingTest.res.mjs +66 -0
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.206 (2026-08-03)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **outbound:** let an outbound slice read an aggregate, and geocode addresses with it ([867e63e](https://github.com/ReventlessDev/reventless-core/commit/867e63e774ebc8b78b2b19c78645c8a12a8d06f6))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.205 (2026-08-02)
|
|
14
|
+
|
|
15
|
+
**Note:** Version bump only for package @reventlessdev/reventless-core
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.204 (2026-08-02)
|
|
7
22
|
|
|
8
23
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.206",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -33,17 +33,17 @@
|
|
|
33
33
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
34
34
|
"@reventlessdev/rescript-node": "2.0.0-alpha.1",
|
|
35
35
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
36
|
-
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.1",
|
|
37
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
38
|
-
"@reventlessdev/rescript-
|
|
39
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.121",
|
|
37
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.1",
|
|
40
38
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
41
|
-
"@reventlessdev/reventless-
|
|
39
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.123",
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.98",
|
|
41
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
45
45
|
"sury-ppx": "11.0.0-alpha.2",
|
|
46
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
46
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.62"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"rescript": "12.3.0"
|
|
@@ -621,6 +621,9 @@ module Make = (
|
|
|
621
621
|
->Array.map((module(OTS: OutboundTranslationSlice.T)) => {
|
|
622
622
|
let ots = OTS.make(
|
|
623
623
|
~dcbEventLog,
|
|
624
|
+
// Same dict the AutomationSlices get, so an outbound slice can
|
|
625
|
+
// name an Aggregate source by its Spec.name.
|
|
626
|
+
~allEventTopics,
|
|
624
627
|
~publishJsons,
|
|
625
628
|
~runtime=?componentRuntime->Dict.get(OTS.Spec.name),
|
|
626
629
|
~opts,
|
|
@@ -329,7 +329,7 @@ function Make(DcbEventLogStorage) {
|
|
|
329
329
|
];
|
|
330
330
|
}));
|
|
331
331
|
let outboundTranslationSlicesOutputs = Object.fromEntries(outboundTranslationSlices.map(OTS => {
|
|
332
|
-
let ots = OTS.make(dcbEventLog, publishJsons, componentRuntime[OTS.Spec.name], opts);
|
|
332
|
+
let ots = OTS.make(dcbEventLog, allEventTopics, publishJsons, componentRuntime[OTS.Spec.name], opts);
|
|
333
333
|
return [
|
|
334
334
|
OTS.Spec.name,
|
|
335
335
|
Component$ReventlessCore.outputs(ots)
|
|
@@ -29,6 +29,7 @@ module type T = {
|
|
|
29
29
|
|
|
30
30
|
let make: (
|
|
31
31
|
~dcbEventLog: DcbEventLog.component,
|
|
32
|
+
~allEventTopics: EventTopic.allOutputs=?,
|
|
32
33
|
~publishJsons: Pulumi.Output.t<CommandTopic.publishJsons>,
|
|
33
34
|
~runtime: ReventlessInfra.RuntimeHints.t=?,
|
|
34
35
|
~opts: Pulumi.ComponentResource.options=?,
|
|
@@ -65,7 +65,14 @@ module Make = (
|
|
|
65
65
|
})
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
let construct = (
|
|
68
|
+
let construct = (
|
|
69
|
+
~dcbEventLog: DcbEventLog.component,
|
|
70
|
+
~allEventTopics: EventTopic.allOutputs,
|
|
71
|
+
~publishJsons,
|
|
72
|
+
~runtime,
|
|
73
|
+
self,
|
|
74
|
+
_name,
|
|
75
|
+
) => {
|
|
69
76
|
let opts = {Pulumi.ComponentResource.parent: self->Component.toPulumiResource}
|
|
70
77
|
let memorySize = ReventlessInfra.RuntimeHints.resolveMemory(runtime, ~default=1024)
|
|
71
78
|
let timeout = ReventlessInfra.RuntimeHints.resolveTimeout(runtime, ~default=30)
|
|
@@ -86,7 +93,38 @@ module Make = (
|
|
|
86
93
|
// once per slice instead of collapsing to a single entry.
|
|
87
94
|
let dcbEventLogName =
|
|
88
95
|
(dcbEventLog->Component.toPulumiResource).name->Option.getOr("") ++ "DcbEventLog"
|
|
89
|
-
|
|
96
|
+
|
|
97
|
+
// Everything this slice could subscribe to: the plugin-wide dict the
|
|
98
|
+
// caller passed, plus its own DCB log — which is always reachable whether
|
|
99
|
+
// or not a dict was supplied, so the `[]` default works in tests and
|
|
100
|
+
// fixtures that construct a slice with nothing else around it.
|
|
101
|
+
let availableTopics = allEventTopics->Dict.copy
|
|
102
|
+
availableTopics->Dict.set(dcbEventLogName, dcbEventTopicOutputs)
|
|
103
|
+
|
|
104
|
+
let sourceNames = switch Spec.sourceNames {
|
|
105
|
+
| [] => [dcbEventLogName]
|
|
106
|
+
| declared => declared
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fail-fast, mirroring AutomationSlice_Builder: an unknown source name
|
|
110
|
+
// would otherwise be dropped by `EventTopic.filter`, leaving the collector
|
|
111
|
+
// subscribed to nothing. That surfaces much later as "the TODO list never
|
|
112
|
+
// populates", with nothing pointing at the typo that caused it.
|
|
113
|
+
sourceNames->Array.forEach(sourceName =>
|
|
114
|
+
if !(availableTopics->Dict.has(sourceName)) {
|
|
115
|
+
let availableNames =
|
|
116
|
+
availableTopics->Dict.keysToArray->Array.toSorted(String.compare)->Array.join(", ")
|
|
117
|
+
JsError.throwWithMessage(
|
|
118
|
+
`OutboundTranslationSlice "${Spec.name}" declares sourceName "${sourceName}", ` ++
|
|
119
|
+
`but no EventTopic with that key exists. ` ++
|
|
120
|
+
`Available source names: [${availableNames}]. ` ++
|
|
121
|
+
`A source is an Aggregate Spec.name or a DCB source name ` ++
|
|
122
|
+
`(typically "<pluginName>DcbEventLog"); \`[]\` means this plugin's own DCB log.`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
let eventTopics = availableTopics->EventTopic.filter(sourceNames->Belt.Set.String.fromArray)
|
|
90
128
|
|
|
91
129
|
// Build the EventCollector inside an Output.all2 so both `queryDbOps` and
|
|
92
130
|
// `publishJsonsFn` are captured in the same closure. The jsonEventsHandler
|
|
@@ -99,7 +137,7 @@ module Make = (
|
|
|
99
137
|
->Pulumi.Output.apply(((queryDbOps, publishJsonsFn)) => {
|
|
100
138
|
let ec = SpecificEventCollector.make(
|
|
101
139
|
~name=Spec.name,
|
|
102
|
-
~eventTopics
|
|
140
|
+
~eventTopics,
|
|
103
141
|
~owner={kind: ComponentType.OutboundTranslationSlice, name: Spec.name},
|
|
104
142
|
~opts,
|
|
105
143
|
)
|
|
@@ -114,14 +152,21 @@ module Make = (
|
|
|
114
152
|
// `event` payload — TAG + fields at the top level — not the
|
|
115
153
|
// wrapper. Splitting the wrapper yields eventType "Unknown",
|
|
116
154
|
// which DcbDecode drops without warning.
|
|
155
|
+
let envelope = json->JSON.Decode.object
|
|
117
156
|
let rawEvent =
|
|
118
|
-
json
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
157
|
+
envelope->Option.flatMap(d => d->Dict.get("event"))->Option.getOr(json)
|
|
158
|
+
// The entity the event was published for. An Aggregate's event
|
|
159
|
+
// payload does not repeat its own id, so without carrying this
|
|
160
|
+
// out of the envelope `collect` cannot name the subject of the
|
|
161
|
+
// outbound item it creates.
|
|
162
|
+
let sourceId =
|
|
163
|
+
envelope
|
|
164
|
+
->Option.flatMap(d => d->Dict.get("id"))
|
|
165
|
+
->Option.flatMap(JSON.Decode.string)
|
|
166
|
+
->Option.getOr("")
|
|
122
167
|
let (eventType, dataDict) = rawEvent->Message.splitMessage
|
|
123
168
|
switch decoder.decode(~eventType, ~data=dataDict) {
|
|
124
|
-
| Some(event) => [event]
|
|
169
|
+
| Some(event) => [(sourceId, event)]
|
|
125
170
|
| None => []
|
|
126
171
|
}
|
|
127
172
|
},
|
|
@@ -164,7 +209,7 @@ module Make = (
|
|
|
164
209
|
let resources = (queryDb->Component.outputs).resources
|
|
165
210
|
ec->EventCollectorRuntimeBuilder.forEventCollector(
|
|
166
211
|
~handler,
|
|
167
|
-
~eventTopics
|
|
212
|
+
~eventTopics,
|
|
168
213
|
~resources,
|
|
169
214
|
~memorySize,
|
|
170
215
|
~timeout,
|
|
@@ -184,8 +229,12 @@ module Make = (
|
|
|
184
229
|
}),
|
|
185
230
|
)
|
|
186
231
|
|
|
232
|
+
// Aggregated across every subscribed topic, as AutomationSlice does — with
|
|
233
|
+
// more than one source, the DCB log's resources alone are an undercount.
|
|
234
|
+
let aggregatedResources =
|
|
235
|
+
eventTopics->Dict.valuesToArray->Array.flatMap((t: EventTopic.outputs) => t.resources)
|
|
187
236
|
let outputs: OutboundTranslationSlice.outputs = {
|
|
188
|
-
resources:
|
|
237
|
+
resources: aggregatedResources,
|
|
189
238
|
queryDb: queryDb->Component.outputs,
|
|
190
239
|
}
|
|
191
240
|
self->Component.setOutputs(outputs)
|
|
@@ -193,6 +242,10 @@ module Make = (
|
|
|
193
242
|
|
|
194
243
|
let make = (
|
|
195
244
|
~dcbEventLog,
|
|
245
|
+
// Defaulted so every existing call site — fixtures included — keeps
|
|
246
|
+
// compiling; a slice with no declared sources needs nothing but its own
|
|
247
|
+
// DCB log, which `construct` adds regardless.
|
|
248
|
+
~allEventTopics=Dict.make(),
|
|
196
249
|
~publishJsons,
|
|
197
250
|
~runtime=?,
|
|
198
251
|
~opts=?,
|
|
@@ -200,7 +253,7 @@ module Make = (
|
|
|
200
253
|
Component.make(
|
|
201
254
|
~componentType=OutboundTranslationSlice.componentType->ComponentType.toString,
|
|
202
255
|
~name=Spec.name,
|
|
203
|
-
~construct=construct(~dcbEventLog, ~publishJsons, ~runtime, ...),
|
|
256
|
+
~construct=construct(~dcbEventLog, ~allEventTopics, ~publishJsons, ~runtime, ...),
|
|
204
257
|
~opts,
|
|
205
258
|
)
|
|
206
259
|
}
|
|
@@ -10,12 +10,16 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
10
10
|
import * as Effect from "effect/Effect";
|
|
11
11
|
import * as Stream$1 from "effect/Stream";
|
|
12
12
|
import * as Pulumi from "@pulumi/pulumi";
|
|
13
|
+
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
14
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
13
15
|
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
14
16
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
17
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
15
18
|
import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
|
|
16
19
|
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
17
20
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
18
21
|
import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
22
|
+
import * as EventTopic$ReventlessCore from "../EventTopic/EventTopic.res.mjs";
|
|
19
23
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
20
24
|
import * as ComponentType$ReventlessCore from "../../ComponentType.res.mjs";
|
|
21
25
|
import * as RuntimeHints$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/RuntimeHints.res.mjs";
|
|
@@ -50,74 +54,90 @@ function Make(RuntimeEnvironment) {
|
|
|
50
54
|
await queryDbOps.save(Id$Reventless.$$String.makeFromString(param[0]), param[1], "Overwrite", undefined);
|
|
51
55
|
});
|
|
52
56
|
};
|
|
53
|
-
let make = (dcbEventLog, publishJsons, runtime, opts) =>
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}, opts);
|
|
64
|
-
let dcbEventTopicOutputs = Component$ReventlessCore.outputs(dcbEventLog).eventTopic;
|
|
65
|
-
let dcbEventLogName = Stdlib_Option.getOr(Component$ReventlessCore.toPulumiResource(dcbEventLog).__name, "") + "DcbEventLog";
|
|
66
|
-
let allEventTopics = Object.fromEntries([[
|
|
67
|
-
dcbEventLogName,
|
|
68
|
-
dcbEventTopicOutputs
|
|
69
|
-
]]);
|
|
70
|
-
let eventCollector = Pulumi.all([
|
|
71
|
-
Component$ReventlessCore.operations(queryDb),
|
|
72
|
-
publishJsons
|
|
73
|
-
]).apply(param => {
|
|
74
|
-
let publishJsonsFn = param[1];
|
|
75
|
-
let queryDbOps = param[0];
|
|
76
|
-
let ec = SpecificEventCollector.make(Spec.name, allEventTopics, {
|
|
57
|
+
let make = (dcbEventLog, allEventTopicsOpt, publishJsons, runtime, opts) => {
|
|
58
|
+
let allEventTopics = allEventTopicsOpt !== undefined ? allEventTopicsOpt : ({});
|
|
59
|
+
return Component$ReventlessCore.make(ComponentType$ReventlessCore.toString(OutboundTranslationSlice$ReventlessCore.componentType), Spec.name, (extra, extra$1) => {
|
|
60
|
+
let opts_parent = Component$ReventlessCore.toPulumiResource(extra);
|
|
61
|
+
let opts = {
|
|
62
|
+
parent: opts_parent
|
|
63
|
+
};
|
|
64
|
+
let memorySize = RuntimeHints$ReventlessInfra.resolveMemory(runtime, 1024);
|
|
65
|
+
let timeout = RuntimeHints$ReventlessInfra.resolveTimeout(runtime, 30);
|
|
66
|
+
let queryDb = SpecificQueryDb.make(Api.api(), Api.apiRole(), undefined, {
|
|
77
67
|
kind: "OutboundTranslationSlice",
|
|
78
68
|
name: Spec.name
|
|
79
69
|
}, opts);
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
let dcbEventTopicOutputs = Component$ReventlessCore.outputs(dcbEventLog).eventTopic;
|
|
71
|
+
let dcbEventLogName = Stdlib_Option.getOr(Component$ReventlessCore.toPulumiResource(dcbEventLog).__name, "") + "DcbEventLog";
|
|
72
|
+
let availableTopics = Object.assign({}, allEventTopics);
|
|
73
|
+
availableTopics[dcbEventLogName] = dcbEventTopicOutputs;
|
|
74
|
+
let declared = Spec.sourceNames;
|
|
75
|
+
let sourceNames = declared.length !== 0 ? declared : [dcbEventLogName];
|
|
76
|
+
sourceNames.forEach(sourceName => {
|
|
77
|
+
if (sourceName in availableTopics) {
|
|
78
|
+
return;
|
|
88
79
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
80
|
+
let availableNames = Object.keys(availableTopics).toSorted(Primitive_string.compare).join(", ");
|
|
81
|
+
Stdlib_JsError.throwWithMessage(`OutboundTranslationSlice "` + Spec.name + `" declares sourceName "` + sourceName + `", but no EventTopic with that key exists. ` + (`Available source names: [` + availableNames + `]. `) + `A source is an Aggregate Spec.name or a DCB source name (typically "<pluginName>DcbEventLog"); \`[]\` means this plugin's own DCB log.`);
|
|
82
|
+
});
|
|
83
|
+
let eventTopics = EventTopic$ReventlessCore.filter(availableTopics, Belt_SetString.fromArray(sourceNames));
|
|
84
|
+
let eventCollector = Pulumi.all([
|
|
85
|
+
Component$ReventlessCore.operations(queryDb),
|
|
86
|
+
publishJsons
|
|
87
|
+
]).apply(param => {
|
|
88
|
+
let publishJsonsFn = param[1];
|
|
89
|
+
let queryDbOps = param[0];
|
|
90
|
+
let ec = SpecificEventCollector.make(Spec.name, eventTopics, {
|
|
91
|
+
kind: "OutboundTranslationSlice",
|
|
92
|
+
name: Spec.name
|
|
93
|
+
}, opts);
|
|
94
|
+
let jsonEventsHandler = stream => Effect.flatMap(Stream.runCollect(Stream$1.flatMap(Stream$1.mapEffect(stream, json => Effect.sync(() => {
|
|
95
|
+
let envelope = Stdlib_JSON.Decode.object(json);
|
|
96
|
+
let rawEvent = Stdlib_Option.getOr(Stdlib_Option.flatMap(envelope, d => d["event"]), json);
|
|
97
|
+
let sourceId = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(envelope, d => d["id"]), Stdlib_JSON.Decode.string), "");
|
|
98
|
+
let match = Message$ReventlessCore.splitMessage(rawEvent);
|
|
99
|
+
let event = decoder.decode(match[0], match[1]);
|
|
100
|
+
if (event !== undefined) {
|
|
101
|
+
return [[
|
|
102
|
+
sourceId,
|
|
103
|
+
Primitive_option.valFromOption(event)
|
|
104
|
+
]];
|
|
105
|
+
} else {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
})), events => Stream$1.fromIterable(events))), eventsArr => Effect.promise(async () => {
|
|
109
|
+
Callback.phase1(eventsArr);
|
|
110
|
+
await syncToQueryDb(queryDbOps);
|
|
111
|
+
Stdlib_Promise.$$catch(Callback.phase2(publishJsonsFn).then(() => syncToQueryDb(queryDbOps)), exn => {
|
|
112
|
+
let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
|
|
113
|
+
Effect.runSync(EffectLogger$ReventlessCore.logError(`OutboundTranslationSlice(` + Spec.name + `)`, undefined, `detached phase 2 error: ` + errMsg));
|
|
114
|
+
return Promise.resolve();
|
|
115
|
+
});
|
|
116
|
+
}));
|
|
117
|
+
let handler = SpecificEventCollector.makeHandler(ec, jsonEventsHandler);
|
|
118
|
+
let resources = Component$ReventlessCore.outputs(queryDb).resources;
|
|
119
|
+
EventCollectorRuntimeBuilder.forEventCollector(handler, eventTopics, resources, memorySize, timeout, ec);
|
|
120
|
+
return ec;
|
|
121
|
+
});
|
|
122
|
+
Component$ReventlessCore.setOperations(extra, Pulumi.all([
|
|
123
|
+
Output$Pulumi.flatMap(eventCollector, Component$ReventlessCore.operations),
|
|
124
|
+
publishJsons
|
|
125
|
+
]).apply(param => {
|
|
126
|
+
let publishJsonsFn = param[1];
|
|
127
|
+
return {
|
|
128
|
+
enqueueEvent: param[0].enqueueEvent,
|
|
129
|
+
translatePending: async () => await Callback.phase2(publishJsonsFn)
|
|
130
|
+
};
|
|
97
131
|
}));
|
|
98
|
-
let
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Component$ReventlessCore.setOperations(extra, Pulumi.all([
|
|
104
|
-
Output$Pulumi.flatMap(eventCollector, Component$ReventlessCore.operations),
|
|
105
|
-
publishJsons
|
|
106
|
-
]).apply(param => {
|
|
107
|
-
let publishJsonsFn = param[1];
|
|
108
|
-
return {
|
|
109
|
-
enqueueEvent: param[0].enqueueEvent,
|
|
110
|
-
translatePending: async () => await Callback.phase2(publishJsonsFn)
|
|
132
|
+
let aggregatedResources = Object.values(eventTopics).flatMap(t => t.resources);
|
|
133
|
+
let outputs_queryDb = Component$ReventlessCore.outputs(queryDb);
|
|
134
|
+
let outputs = {
|
|
135
|
+
resources: aggregatedResources,
|
|
136
|
+
queryDb: outputs_queryDb
|
|
111
137
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
let outputs = {
|
|
116
|
-
resources: outputs_resources,
|
|
117
|
-
queryDb: outputs_queryDb
|
|
118
|
-
};
|
|
119
|
-
return Component$ReventlessCore.setOutputs(extra, outputs);
|
|
120
|
-
}, opts);
|
|
138
|
+
return Component$ReventlessCore.setOutputs(extra, outputs);
|
|
139
|
+
}, opts);
|
|
140
|
+
};
|
|
121
141
|
return {
|
|
122
142
|
Spec: Spec,
|
|
123
143
|
Translation: Translation,
|
|
@@ -29,8 +29,10 @@ module type T = {
|
|
|
29
29
|
/** The in-memory TODO list -- maps item ID to row. */
|
|
30
30
|
let todoItems: Dict.t<todoRow>
|
|
31
31
|
|
|
32
|
-
/** Phase 1: update TODO list from a batch of
|
|
33
|
-
|
|
32
|
+
/** Phase 1: update TODO list from a batch of `(sourceId, event)` pairs
|
|
33
|
+
(collect only, no resolve). `sourceId` is the envelope id of the entity the
|
|
34
|
+
event was published for. */
|
|
35
|
+
let phase1: array<(string, Spec.consumedEvent)> => unit
|
|
34
36
|
|
|
35
37
|
/** Phase 2: translate all pending items, optionally publishing commands via publishJsons. */
|
|
36
38
|
let phase2: ReventlessInfra.CommandTopic.publishJsons => promise<unit>
|
|
@@ -54,10 +56,10 @@ module Make = (
|
|
|
54
56
|
let makeMeta = (): Reventless.Message.meta =>
|
|
55
57
|
Message.generateMeta(~service=`OutboundTranslationSlice:${Spec.name}`)
|
|
56
58
|
|
|
57
|
-
let phase1 = (events: array<Spec.consumedEvent>) => {
|
|
58
|
-
events->Array.forEach(event => {
|
|
59
|
+
let phase1 = (events: array<(string, Spec.consumedEvent)>) => {
|
|
60
|
+
events->Array.forEach(((sourceId, event)) => {
|
|
59
61
|
// Collect new outbound items
|
|
60
|
-
Translation.collect(event)->Array.forEach(((id, item)) => {
|
|
62
|
+
Translation.collect(event, ~sourceId)->Array.forEach(((id, item)) => {
|
|
61
63
|
switch todoItems->Dict.get(id) {
|
|
62
64
|
| Some(_) => () // Already exists -- skip (idempotent)
|
|
63
65
|
| None =>
|
|
@@ -34,8 +34,8 @@ function Make(Spec) {
|
|
|
34
34
|
let todoItems = {};
|
|
35
35
|
let makeMeta = () => Message$ReventlessCore.generateMeta(`OutboundTranslationSlice:` + Spec.name, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
36
36
|
let phase1 = events => {
|
|
37
|
-
events.forEach(
|
|
38
|
-
Translation.collect(
|
|
37
|
+
events.forEach(param => {
|
|
38
|
+
Translation.collect(param[1], param[0]).forEach(param => {
|
|
39
39
|
let id = param[0];
|
|
40
40
|
let match = todoItems[id];
|
|
41
41
|
if (match !== undefined) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// `confidentMatch` is the gate between "the geocoder said something" and "a
|
|
4
|
+
// coordinate is written into an event log". Every case it declines is a
|
|
5
|
+
// plausible-looking pin it refused to draw, so the declines are the assertions
|
|
6
|
+
// that matter here.
|
|
7
|
+
//
|
|
8
|
+
// It is provider-neutral policy (`Reventless.Geocoding`), shared by the SDK-backed
|
|
9
|
+
// transport in this package and by any plugin reaching a geocoder over HTTP.
|
|
10
|
+
|
|
11
|
+
module Geocoding = Reventless.Geocoding
|
|
12
|
+
|
|
13
|
+
let point = (~lat, ~lng) =>
|
|
14
|
+
switch Reventless.GeoPoint.make(~lat, ~lng) {
|
|
15
|
+
| Ok(p) => p
|
|
16
|
+
| Error(why) => fail(why)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let candidate = (~label, ~relevance=?): Geocoding.candidate => {
|
|
20
|
+
label,
|
|
21
|
+
point: point(~lat=48.2082, ~lng=16.3738),
|
|
22
|
+
relevance,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("Geocoder_AwsLocation_Geocoding.confidentMatch", () => {
|
|
26
|
+
testSync("a clear winner is the answer", () => {
|
|
27
|
+
let top = candidate(~label="Stephansplatz 1, Vienna", ~relevance=0.97)
|
|
28
|
+
let result = Geocoding.confidentMatch([top, candidate(~label="Elsewhere", ~relevance=0.4)])
|
|
29
|
+
expect(result->Option.map(c => c.Geocoding.label))->toEqual(Some("Stephansplatz 1, Vienna"))
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
testSync("a loose top match is declined", () => {
|
|
33
|
+
let result = Geocoding.confidentMatch([candidate(~label="Somewhere-ish", ~relevance=0.55)])
|
|
34
|
+
expect(result->Option.isNone)->toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
testSync("two near-equal candidates are ambiguous, not a winner", () => {
|
|
38
|
+
// The bare-town-name case: the service matched several places about equally
|
|
39
|
+
// well, and picking the first would pin a confident marker in the wrong one.
|
|
40
|
+
let result = Geocoding.confidentMatch([
|
|
41
|
+
candidate(~label="Springfield, IL", ~relevance=0.91),
|
|
42
|
+
candidate(~label="Springfield, MA", ~relevance=0.89),
|
|
43
|
+
])
|
|
44
|
+
expect(result->Option.isNone)->toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
testSync("an unscored candidate is not confident", () => {
|
|
48
|
+
// An index that reports no relevance cannot support an unattended decision;
|
|
49
|
+
// accepting it would make the threshold meaningless where it matters most.
|
|
50
|
+
let result = Geocoding.confidentMatch([candidate(~label="Unscored")])
|
|
51
|
+
expect(result->Option.isNone)->toBe(true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
testSync("no candidates, no match", () => {
|
|
55
|
+
expect(Geocoding.confidentMatch([])->Option.isNone)->toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
testSync("the thresholds are callable, not baked in", () => {
|
|
59
|
+
let loose = [candidate(~label="Loose", ~relevance=0.62)]
|
|
60
|
+
expect(Geocoding.confidentMatch(loose)->Option.isNone)->toBe(true)
|
|
61
|
+
expect(
|
|
62
|
+
Geocoding.confidentMatch(loose, ~minRelevance=0.5)->Option.map(c => c.Geocoding.label),
|
|
63
|
+
)->toEqual(Some("Loose"))
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.mjs";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as GeoPoint$Reventless from "@reventlessdev/reventless-spec/src/semantic/GeoPoint.res.mjs";
|
|
6
|
+
import * as Geocoding$Reventless from "@reventlessdev/reventless-spec/src/semantic/Geocoding.res.mjs";
|
|
7
|
+
|
|
8
|
+
function point(lat, lng) {
|
|
9
|
+
let p = GeoPoint$Reventless.make(lat, lng);
|
|
10
|
+
if (p.TAG === "Ok") {
|
|
11
|
+
return p._0;
|
|
12
|
+
} else {
|
|
13
|
+
return JestGlobals.fail(p._0);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function candidate(label, relevance) {
|
|
18
|
+
return {
|
|
19
|
+
label: label,
|
|
20
|
+
point: point(48.2082, 16.3738),
|
|
21
|
+
relevance: relevance
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
globalThis.describe("Geocoder_AwsLocation_Geocoding.confidentMatch", () => {
|
|
26
|
+
globalThis.test("a clear winner is the answer", () => {
|
|
27
|
+
let top = candidate("Stephansplatz 1, Vienna", 0.97);
|
|
28
|
+
let result = Geocoding$Reventless.confidentMatch([
|
|
29
|
+
top,
|
|
30
|
+
candidate("Elsewhere", 0.4)
|
|
31
|
+
], undefined, undefined);
|
|
32
|
+
globalThis.expect(Stdlib_Option.map(result, c => c.label)).toEqual("Stephansplatz 1, Vienna");
|
|
33
|
+
});
|
|
34
|
+
globalThis.test("a loose top match is declined", () => {
|
|
35
|
+
let result = Geocoding$Reventless.confidentMatch([candidate("Somewhere-ish", 0.55)], undefined, undefined);
|
|
36
|
+
globalThis.expect(Stdlib_Option.isNone(result)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
globalThis.test("two near-equal candidates are ambiguous, not a winner", () => {
|
|
39
|
+
let result = Geocoding$Reventless.confidentMatch([
|
|
40
|
+
candidate("Springfield, IL", 0.91),
|
|
41
|
+
candidate("Springfield, MA", 0.89)
|
|
42
|
+
], undefined, undefined);
|
|
43
|
+
globalThis.expect(Stdlib_Option.isNone(result)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
globalThis.test("an unscored candidate is not confident", () => {
|
|
46
|
+
let result = Geocoding$Reventless.confidentMatch([candidate("Unscored", undefined)], undefined, undefined);
|
|
47
|
+
globalThis.expect(Stdlib_Option.isNone(result)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
globalThis.test("no candidates, no match", () => {
|
|
50
|
+
globalThis.expect(Stdlib_Option.isNone(Geocoding$Reventless.confidentMatch([], undefined, undefined))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
globalThis.test("the thresholds are callable, not baked in", () => {
|
|
53
|
+
let loose = [candidate("Loose", 0.62)];
|
|
54
|
+
globalThis.expect(Stdlib_Option.isNone(Geocoding$Reventless.confidentMatch(loose, undefined, undefined))).toBe(true);
|
|
55
|
+
globalThis.expect(Stdlib_Option.map(Geocoding$Reventless.confidentMatch(loose, 0.5, undefined), c => c.label)).toEqual("Loose");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let Geocoding;
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
Geocoding,
|
|
63
|
+
point,
|
|
64
|
+
candidate,
|
|
65
|
+
}
|
|
66
|
+
/* Not a pure module */
|