@reventlessdev/reventless-infra 3.0.0-alpha.151 → 3.0.0-alpha.153

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,44 @@
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.153 (2026-09-01)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **api:** [@no](https://github.com/no)Api was lost when a command variant was spliced into another type ([00403fb](https://github.com/ReventlessDev/reventless-core/commit/00403fb4b8873a68a69558829e598339a5012833))
11
+ * **capabilities:** a plugin's geocoding need was declared nowhere and failed silently ([67917dd](https://github.com/ReventlessDev/reventless-core/commit/67917dd504b43fa78b7c6a51644c9eae656b7f6b))
12
+ ### Features
13
+
14
+ * **capabilities:** a plugin can send a message without naming a provider ([b001a1e](https://github.com/ReventlessDev/reventless-core/commit/b001a1e9361a0f4d0affe228cb4c08c38a5a995e))
15
+ * **spec:** a command declares its lifecycle edge as a value ([40eee9f](https://github.com/ReventlessDev/reventless-core/commit/40eee9f7723dc05e418be680528f01967d074da4))
16
+ * **spec:** a graft leaves a trace the deployed plugin can read ([c08ff6c](https://github.com/ReventlessDev/reventless-core/commit/c08ff6c0f6177d58603e7ae1e5cec392d9bac16a))
17
+
18
+
19
+ # 3.0.0-alpha.152 (2026-08-27)
20
+
21
+ * feat(spec)!: reflect the command direction across a port, both halves ([f2fe258](https://github.com/ReventlessDev/reventless-core/commit/f2fe258d195b74f4a61488edee305665341020ea))
22
+ * feat(spec)!: drop the plugin protocol's reconnect event, which nothing published ([7549db6](https://github.com/ReventlessDev/reventless-core/commit/7549db6aac6ad3f94dbb8d72dac4b4e783756ad1))
23
+ * feat(spec)!: read the port's translation table off the mapping's arms ([956348a](https://github.com/ReventlessDev/reventless-core/commit/956348a9fcb256cfff9db51809bdc27d73360e6c))
24
+
25
+ ### BREAKING CHANGES
26
+
27
+ * `ExtensionPointMapping.Mapping` gains `acceptedCommands` and
28
+ `ExtensionMapping.Mapping` gains `issuedCommands`; both are derived, so a
29
+ mapping the ppx can read needs no source change, and one it cannot names itself
30
+ at compile time. Definitions persisted before the two new def fields must be
31
+ re-emitted before a consumer can read them as present — and until then they
32
+ decode as None, which means unknown, not "issues nothing".
33
+ * `PluginExtensionPointSpec.event` no longer declares
34
+ `PluginReconnected`. An extension matching on it was matching an event it could
35
+ never receive; drop the arm.
36
+ * `ExtensionPointMapping.Mapping` requires `publishedEvents` and
37
+ `ExtensionMapping.Mapping` requires `handledEvents`; the ppx injects both for
38
+ mappings it can read, so app code must build against the matching ppx. Plugin
39
+ definitions persisted before the new fields must be re-emitted before a consumer
40
+ reads them as present.
41
+
42
+
43
+
6
44
  # 3.0.0-alpha.151 (2026-08-23)
7
45
 
8
46
  **Note:** Version bump only for package @reventlessdev/reventless-infra
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-infra",
3
- "version": "3.0.0-alpha.151",
3
+ "version": "3.0.0-alpha.153",
4
4
  "description": "Infrastructure types for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -17,9 +17,9 @@
17
17
  "sury-ppx": "11.0.0-rc.2",
18
18
  "uuid": "^13.0.0",
19
19
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
20
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
- "@reventlessdev/reventless-spec": "3.0.0-alpha.123",
22
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0"
20
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.125",
21
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.0",
@@ -7,6 +7,18 @@ let noApiId: S.Metadata.Id.t<bool> = S.Metadata.Id.make(~namespace="api", ~name=
7
7
  let noApiVariantsId: S.Metadata.Id.t<Set.t<string>> =
8
8
  S.Metadata.Id.make(~namespace="api", ~name="noApiVariants")
9
9
 
10
+ /** The same exclusion, carried by the excluded *member* rather than by the union
11
+ it sits in — so it survives a variant spread.
12
+
13
+ A spread splices members: the generated code concatenates the source union's
14
+ `anyOf` into the target's. Metadata on the source *union* is not part of that,
15
+ so an exclusion recorded only there vanishes on the way into the host, and the
16
+ host publishes a command its author marked internal. A mark on the member
17
+ travels, because the member is what is spliced — the host's union holds the
18
+ same schema object. */
19
+ let noApiVariantId: S.Metadata.Id.t<bool> =
20
+ S.Metadata.Id.make(~namespace="api", ~name="noApiVariant")
21
+
10
22
  /** Internal sury metadata ID storing per-variant allowed-state lists for AutoUI command filtering.
11
23
  Maps variant name → set of status values under which the command is meaningful. */
12
24
  let allowedStatesId: S.Metadata.Id.t<dict<array<string>>> =
@@ -21,9 +33,60 @@ let targetStateId: S.Metadata.Id.t<dict<string>> =
21
33
  let markNoApi = (schema: S.t<'a>): S.t<'a> =>
22
34
  schema->S.Metadata.set(~id=noApiId, true)
23
35
 
24
- /** PPX helper: attaches variant-level exclusions to a command schema. Called by generated code. */
25
- let markNoApiVariants = (schema: S.t<'a>, variants: array<string>): S.t<'a> =>
26
- schema->S.Metadata.set(~id=noApiVariantsId, Set.fromArray(variants))
36
+ /** The constructor a union member stands for: the `TAG` literal of a
37
+ payload-bearing variant, or the bare literal a payload-less one compiles to. */
38
+ let variantNameOf = (member: S.t<unknown>): option<string> =>
39
+ switch member {
40
+ | Object({properties}) =>
41
+ properties
42
+ ->Dict.get("TAG")
43
+ ->Option.flatMap(tag =>
44
+ switch tag {
45
+ | String({const: ?Some(name)}) => Some(name)
46
+ | _ => None
47
+ }
48
+ )
49
+ | String({const: ?Some(name)}) => Some(name)
50
+ | _ => None
51
+ }
52
+
53
+ /**
54
+ PPX helper: attaches variant-level exclusions to a command schema. Called by
55
+ generated code.
56
+
57
+ Recorded twice, on purpose. The set on the union is what every reader has always
58
+ consulted; the per-member mark is what survives being spliced into another type.
59
+ Writing only the second would break any reader holding a union whose members it
60
+ did not build, and writing only the first is the defect this exists to close.
61
+
62
+ The union is rebuilt from the marked members because sury metadata is immutable —
63
+ setting it returns a new schema, so the members the original union holds would
64
+ otherwise still be the unmarked ones. Sound here because the PPX calls this
65
+ *first*, directly on the freshly built union, before any other metadata is
66
+ attached: there is nothing on the parent yet to lose. A schema that is not a
67
+ union, or that names no excluded variant, is left structurally untouched.
68
+ */
69
+ let markNoApiVariants = (schema: S.t<'a>, variants: array<string>): S.t<'a> => {
70
+ let excluded = Set.fromArray(variants)
71
+ let marked = switch (variants, schema->S.castToUnknown) {
72
+ | ([], _) => schema
73
+ | (_, AnyOf({anyOf})) =>
74
+ anyOf
75
+ ->Array.map(member =>
76
+ switch variantNameOf(member) {
77
+ | Some(name) if excluded->Set.has(name) => member->S.Metadata.set(~id=noApiVariantId, true)
78
+ | _ => member
79
+ }
80
+ )
81
+ ->S.union
82
+ // The rebuilt union describes the same values as the one handed in — same
83
+ // members, same order, only metadata added. `S.union` cannot express that in
84
+ // the type, so the equivalence is carried here rather than by the compiler.
85
+ ->(Obj.magic: S.t<unknown> => S.t<'a>)
86
+ | (_, _) => schema
87
+ }
88
+ marked->S.Metadata.set(~id=noApiVariantsId, excluded)
89
+ }
27
90
 
28
91
  /** PPX helper: attaches per-variant allowedStates to a command schema. Called by generated code
29
92
  emitted from the from-set of an @transition attribute. Each entry is (variantName, allowedStateNames). */
@@ -1,11 +1,14 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Sury from "sury";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
5
 
5
6
  let noApiId = Sury.$Metadata_Id_make("api", "noApi");
6
7
 
7
8
  let noApiVariantsId = Sury.$Metadata_Id_make("api", "noApiVariants");
8
9
 
10
+ let noApiVariantId = Sury.$Metadata_Id_make("api", "noApiVariant");
11
+
9
12
  let allowedStatesId = Sury.$Metadata_Id_make("api", "allowedStates");
10
13
 
11
14
  let targetStateId = Sury.$Metadata_Id_make("api", "targetState");
@@ -14,8 +17,41 @@ function markNoApi(schema) {
14
17
  return Sury.$Metadata_set(schema, noApiId, true);
15
18
  }
16
19
 
20
+ function variantNameOf(member) {
21
+ switch (member.type) {
22
+ case "string" :
23
+ let name = member.const;
24
+ if (name !== undefined) {
25
+ return name;
26
+ } else {
27
+ return;
28
+ }
29
+ case "object" :
30
+ return Stdlib_Option.flatMap(member.properties["TAG"], tag => {
31
+ if (tag.type !== "string") {
32
+ return;
33
+ }
34
+ let name = tag.const;
35
+ if (name !== undefined) {
36
+ return name;
37
+ }
38
+ });
39
+ default:
40
+ return;
41
+ }
42
+ }
43
+
17
44
  function markNoApiVariants(schema, variants) {
18
- return Sury.$Metadata_set(schema, noApiVariantsId, new Set(variants));
45
+ let excluded = new Set(variants);
46
+ let marked = variants.length !== 0 && schema.type === "anyOf" ? Sury.union(schema.anyOf.map(member => {
47
+ let name = variantNameOf(member);
48
+ if (name !== undefined && excluded.has(name)) {
49
+ return Sury.$Metadata_set(member, noApiVariantId, true);
50
+ } else {
51
+ return member;
52
+ }
53
+ })) : schema;
54
+ return Sury.$Metadata_set(marked, noApiVariantsId, excluded);
19
55
  }
20
56
 
21
57
  function markAllowedStates(schema, entries) {
@@ -29,9 +65,11 @@ function markTargetState(schema, entries) {
29
65
  export {
30
66
  noApiId,
31
67
  noApiVariantsId,
68
+ noApiVariantId,
32
69
  allowedStatesId,
33
70
  targetStateId,
34
71
  markNoApi,
72
+ variantNameOf,
35
73
  markNoApiVariants,
36
74
  markAllowedStates,
37
75
  markTargetState,
@@ -1,8 +1,5 @@
1
- /**
2
- A pre-serialized command routed to a foreign extension point.
3
- Used when an extension needs to dispatch to an extension point it does not
4
- own — the command JSON is forwarded opaquely without re-encoding.
5
- */
1
+ /** A command routed to an extension point this extension does not own —
2
+ forwarded opaquely, without re-encoding. */
6
3
  type forwardCommand = {
7
4
  extensionPointName: string,
8
5
  id: string,
@@ -11,18 +8,9 @@ type forwardCommand = {
11
8
 
12
9
  type id = string
13
10
 
14
- /**
15
- Actions returned by an extension's `mapIncomingEvent` function.
16
-
17
- When an extension point emits an event, the extension's mapping can:
18
- - publish a command to the aggregate it wraps (`PublishAggregateCommand`)
19
- - publish a command to the StateChangeSlice it wraps (`PublishStateChangeSliceCommand`)
20
- — no id argument; the framework derives the FIFO grouping id from the command's
21
- `@partitionTag` (or `@compositePartitionTag`) field.
22
- - publish a command back to the extension point (`PublishExtensionPointCommand`)
23
- - forward a command to another extension point opaquely (`ForwardCommand`)
24
- - handle an extension-point-defined directive via an async handler (`HandleDirective`)
25
- */
11
+ /** What `mapIncomingEvent` can do with a published event: command the wrapped
12
+ aggregate or slice, command the extension point, forward, or run a directive.
13
+ The slice forms take no id — the FIFO group comes from `@partitionTag`. */
26
14
  type incomingCommandAction<'aggregateCommand, 'extensionPointCommand, 'directive> =
27
15
  | PublishAggregateCommand(id, 'aggregateCommand)
28
16
  | PublishAggregateCommandAsync(promise<(id, 'aggregateCommand)>)
@@ -34,26 +22,14 @@ type incomingCommandAction<'aggregateCommand, 'extensionPointCommand, 'directive
34
22
  | ForwardCommand(forwardCommand)
35
23
  | HandleDirective(Reventless.Handler.handler<'directive>, 'directive)
36
24
 
37
- /**
38
- Actions returned by an extension's `mapOutgoingEvent` function.
39
-
40
- When the wrapped aggregate emits an event, the extension can:
41
- - publish a command to the extension point (`PublishExtensionPointCommand`)
42
- - forward a command to another extension point opaquely (`ForwardCommand`)
43
- - handle an extension-point-defined directive via an async handler (`HandleDirective`)
44
- */
25
+ /** What `mapOutgoingEvent` can do with a delegate event: command the extension
26
+ point, forward, or run a directive. */
45
27
  type outgoingCommandAction<'extensionPointCommand, 'directive> =
46
28
  | PublishExtensionPointCommand(id, 'extensionPointCommand)
47
29
  | ForwardCommand(forwardCommand)
48
30
  | HandleDirective(Reventless.Handler.handler<'directive>, 'directive)
49
31
 
50
- /**
51
- Maps an extension point event to actions on the wrapped aggregate or extension point.
52
-
53
- Called when the extension point emits an event that this extension handles.
54
- Receives the entity ID, the event, message metadata, the plugin definition,
55
- and the query engine.
56
- */
32
+ /** Maps a published event to actions on the wrapped delegate or the extension point. */
57
33
  type mapIncomingEvent<
58
34
  'extensionPointEvent,
59
35
  'aggregateCommand,
@@ -69,12 +45,8 @@ type mapIncomingEvent<
69
45
  incomingCommandAction<'aggregateCommand, 'extensionPointCommand, 'extensionPointDirective>,
70
46
  >
71
47
 
72
- /**
73
- Maps an aggregate event to actions on the extension point.
74
-
75
- Optionally defined — return `None` if the aggregate's outgoing events do not
76
- need to be reflected back through the extension point.
77
- */
48
+ /** Maps a delegate event back to actions on the extension point. `None` when
49
+ nothing flows back. */
78
50
  type mapOutgoingEvent<'aggregateEvent, 'extensionPointCommand, 'extensionPointDirective> = (
79
51
  string,
80
52
  'aggregateEvent,
@@ -82,11 +54,8 @@ type mapOutgoingEvent<'aggregateEvent, 'extensionPointCommand, 'extensionPointDi
82
54
  Reventless.Plugin.pluginDefinition,
83
55
  ) => array<outgoingCommandAction<'extensionPointCommand, 'extensionPointDirective>>
84
56
 
85
- /**
86
- The extension point protocol that this `Extension` connects to.
87
- Mirrors `ExtensionPoint.Spec` so the extension can be type-checked
88
- against the extension point's command / event / directive types.
89
- */
57
+ /** The extension point protocol this `Extension` connects to — mirrors
58
+ `ExtensionPoint.Spec` so the mapping type-checks against it. */
90
59
  module type Spec = {
91
60
  let name: string
92
61
  let moduleUrl: string
@@ -99,30 +68,35 @@ module type Spec = {
99
68
  type directive
100
69
  }
101
70
 
102
- /**
103
- Application-level implementation of an extension's bidirectional mapping.
71
+ /** One published event this extension handles and the commands it routes to —
72
+ the subscriber's half of `ExtensionPointMapping.publishedEvent`. Bare names,
73
+ resolved against the `Delegate`'s commands first, the EP's second. */
74
+ type handledEvent = {
75
+ name: string,
76
+ toCommandTypes: array<string>,
77
+ }
104
78
 
105
- - `ExtensionPoint` the extension point protocol this extension connects to
106
- - `Delegate` — the component this extension delegates to (aggregate or DCB slice)
107
- - `mapIncomingEvent` routes extension point events to delegate commands
108
- - `mapOutgoingEvent` optionally routes delegate events back to the EP
109
- */
79
+ /** One command this extension sends back to the port and the `Delegate` events
80
+ producing it — the subscriber's half of `ExtensionPointMapping.acceptedCommand`.
81
+ Read off `mapOutgoingEvent` only: an EP command published from an arm of
82
+ `mapIncomingEvent` is an event→command edge and stays in `handledEvent`. */
83
+ type issuedCommand = {
84
+ name: string,
85
+ fromEventTypes: array<string>,
86
+ }
87
+
88
+ /** An extension's bidirectional mapping between one extension point and one
89
+ delegate (aggregate or DCB slice). */
110
90
  module type Mapping = {
111
91
  module ExtensionPoint: Spec
112
92
  module Delegate: Reventless.Aggregate.Spec
113
93
 
114
- // npm-style specifier of the user extension file (the module exporting this
115
- // Mapping). Used by Plugin_Helpers + the bundled Plugin EventCollector entry
116
- // point to dynamic-import the user mapping for runtime reconstruction of
117
- // mapIncomingEvent / mapOutgoingEvent. PPX-injected on @@reventless.extension
118
- // files as the same specifier as the file-level moduleUrl.
94
+ // The extension file's own url dynamic-imported at runtime to reconstruct the
95
+ // two mapping functions. Injected by `@@reventless.extension`.
119
96
  let moduleUrl: string
120
97
 
121
- // npm-style specifier of the Delegate's source module. Used by the bundled
122
- // Plugin EventCollector entry point to dynamic-import the Delegate spec at
123
- // cold start (Mapping.ExtensionPoint and Mapping.Delegate are erased in the
124
- // compiled .res.mjs export). PPX-injected on @@reventless.extension files
125
- // as `Delegate.moduleUrl`.
98
+ // The Delegate's url the EventCollector imports its spec at cold start, since
99
+ // the compiled export erases `Mapping.Delegate`.
126
100
  let delegateModuleUrl: string
127
101
 
128
102
  let mapIncomingEvent: mapIncomingEvent<
@@ -135,12 +109,18 @@ module type Mapping = {
135
109
  let mapOutgoingEvent: option<
136
110
  mapOutgoingEvent<Delegate.event, ExtensionPoint.command, ExtensionPoint.directive>,
137
111
  >
112
+
113
+ /** Which published event routes to which commands — see `handledEvent`. Derived
114
+ by the PPX from `mapIncomingEvent`'s arms; hand-written only where it says
115
+ it cannot read them. */
116
+ let handledEvents: array<handledEvent>
117
+
118
+ /** Which commands travel back to the port — see `issuedCommand`. Derived from
119
+ `mapOutgoingEvent`'s arms, under the same rule. */
120
+ let issuedCommands: array<issuedCommand>
138
121
  }
139
122
 
140
- /**
141
- A dummy target used when an extension does not wrap a real aggregate or slice.
142
- Satisfies `Aggregate.Spec` with unit command / event / error types.
143
- */
123
+ /** A stand-in for an extension that wraps no aggregate or slice. */
144
124
  module NoDelegate = {
145
125
  let name = "NoDelegate"
146
126
 
@@ -167,6 +147,9 @@ module NoDelegate = {
167
147
  let moduleUrl: string = %raw(`import.meta.url`)
168
148
  let commandAuthorization = (_: command): Reventless.Authorization.permission =>
169
149
  AllowAuthenticated
150
+ type lifecycleState = unit
151
+ let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
152
+ let traits: array<Reventless.Trait.t> = []
170
153
  }
171
154
 
172
155
 
@@ -193,6 +176,14 @@ module type T = {
193
176
 
194
177
  let delegateName: string
195
178
 
179
+ // All carried through the functor: `Plugin_Structure` sees an extension only
180
+ // compiled, and `T` erases the Delegate's types, so the checks against the two
181
+ // tables need its command and event names rather than its schemas.
182
+ let handledEvents: array<handledEvent>
183
+ let issuedCommands: array<issuedCommand>
184
+ let delegateCommandNames: array<string>
185
+ let delegateEventNames: array<string>
186
+
196
187
  let mapIncomingEvent: (
197
188
  Reventless.Message.event'<string, ExtensionPoint.event>,
198
189
  pluginDefinition,
@@ -217,21 +208,18 @@ module Make = (MappingImpl: Mapping): (
217
208
  module Delegate = MappingImpl.Delegate
218
209
  let delegateName = Delegate.name
219
210
  let extensionPointName = Spec.name
211
+ let handledEvents = MappingImpl.handledEvents
212
+ let issuedCommands = MappingImpl.issuedCommands
213
+ let delegateCommandNames = Reventless.DcbTag.extractAllVariantNames(Delegate.commandSchema)
220
214
 
221
- // Variant TAGs the Delegate cares about — derived from the Delegate's event
222
- // schema at functor instantiation. Used to pre-filter incoming envelopes:
223
- // sibling variants on the source log (events the Delegate did not declare)
224
- // are silently skipped without any decode attempt.
225
- // extractAllVariantNames keeps payload-less variants (sury-compiled bare
226
- // `S.literal("Name")` strings) — the JSON envelope's `event` TAG still
227
- // carries the literal name, so the filter would otherwise drop them.
215
+ // Pre-filter for incoming envelopes: sibling variants the Delegate did not
216
+ // declare are skipped undecoded. Payload-less variants included the
217
+ // envelope's `event` TAG still carries their name.
228
218
  let acceptedTags = Reventless.DcbTag.extractAllVariantNames(Delegate.eventSchema)
219
+ let delegateEventNames = acceptedTags
229
220
 
230
- // Lazily-computed partition-tag derivation for `PublishStateChangeSliceCommand*`.
231
- // Derived from the Delegate's command schema; throws if no `@partitionTag` /
232
- // `@compositePartitionTag` annotation exists (i.e. the Delegate is an Aggregate,
233
- // not a StateChangeSlice — in which case the user should use
234
- // `PublishAggregateCommand` instead).
221
+ // Partition tag for `PublishStateChangeSliceCommand*`. Throws when the Delegate
222
+ // declares none an Aggregate, where `PublishAggregateCommand` is the right form.
235
223
  let derivedPartitionTagLazy = ref(None)
236
224
  let getDerivedPartitionTag = () =>
237
225
  switch derivedPartitionTagLazy.contents {
@@ -253,10 +241,8 @@ module Make = (MappingImpl: Mapping): (
253
241
  Reventless.DcbTag.getCompositePartitionKeyValue(tags, spec)
254
242
  }
255
243
 
256
- // Carry `comp` as a structured Effect log annotation EffectLogger.install
257
- // (in reventless-core, wired at Lambda startup) lifts it to the top-level
258
- // JSON `comp` field. Without the unified logger installed, Effect's default
259
- // logger still renders the annotation, just less prettily.
244
+ // `comp` as an Effect log annotation; EffectLogger.install lifts it to the
245
+ // top-level JSON field.
260
246
  let compLog = (comp, msg) =>
261
247
  Effect.logInfo(msg)->Effect.annotateLogs("comp", comp)->Effect.runSync
262
248
 
@@ -378,9 +364,7 @@ module Make = (MappingImpl: Mapping): (
378
364
 
379
365
  let doMapOutgoingEvent = (mapOutgoingEventImpl, targetEvent'Json, pluginDef) => {
380
366
  let tag = variantTagOfEnvelope(targetEvent'Json)
381
- // Pre-filter by TAG: sibling variants from the source log that the Delegate
382
- // did not declare are not this mapping's concern — skip silently with no
383
- // decode attempt.
367
+ // Not this mapping's concern skip without decoding.
384
368
  if !(acceptedTags->Array.includes(tag)) {
385
369
  []
386
370
  } else {
@@ -32,6 +32,12 @@ function commandAuthorization() {
32
32
  return "AllowAuthenticated";
33
33
  }
34
34
 
35
+ function commandTransition() {
36
+ return "Unrestricted";
37
+ }
38
+
39
+ let traits = [];
40
+
35
41
  let NoDelegate = {
36
42
  name: "NoDelegate",
37
43
  Id: Id,
@@ -39,7 +45,9 @@ let NoDelegate = {
39
45
  errorSchema: errorSchema,
40
46
  commandSchema: commandSchema,
41
47
  moduleUrl: moduleUrl,
42
- commandAuthorization: commandAuthorization
48
+ commandAuthorization: commandAuthorization,
49
+ commandTransition: commandTransition,
50
+ traits: traits
43
51
  };
44
52
 
45
53
  function Make(MappingImpl) {
@@ -47,6 +55,7 @@ function Make(MappingImpl) {
47
55
  let Delegate = MappingImpl.Delegate;
48
56
  let delegateName = Delegate.name;
49
57
  let extensionPointName = Spec.name;
58
+ let delegateCommandNames = DcbTag$Reventless.extractAllVariantNames(Delegate.commandSchema);
50
59
  let acceptedTags = DcbTag$Reventless.extractAllVariantNames(Delegate.eventSchema);
51
60
  let derivedPartitionTagLazy = {
52
61
  contents: undefined
@@ -248,6 +257,10 @@ function Make(MappingImpl) {
248
257
  }));
249
258
  return {
250
259
  delegateName: delegateName,
260
+ handledEvents: MappingImpl.handledEvents,
261
+ issuedCommands: MappingImpl.issuedCommands,
262
+ delegateCommandNames: delegateCommandNames,
263
+ delegateEventNames: acceptedTags,
251
264
  mapIncomingEvent: mapIncomingEvent,
252
265
  mapOutgoingEvent: mapOutgoingEvent
253
266
  };
@@ -1,10 +1,7 @@
1
1
  type extensionPointName = string
2
2
 
3
- /**
4
- An async handler for a typed extension-point directive. May call the scheduler
5
- or query engine as a side effect. Used as the handler argument in `HandleDirective`
6
- on both `commandAction` and `eventAction`.
7
- */
3
+ /** An async handler for a typed directive, used by `HandleDirective` on both
4
+ `commandAction` and `eventAction`. */
8
5
  type directiveHandler<'directive> = (
9
6
  Reventless.Schedule.create,
10
7
  Reventless.Schedule.delete,
@@ -12,37 +9,20 @@ type directiveHandler<'directive> = (
12
9
  'directive,
13
10
  ) => promise<unit>
14
11
 
15
- /**
16
- Actions returned by `mapIncomingCommand` what to do when the extension point
17
- receives a command from an extension.
18
-
19
- - `PublishCommand(id, cmd)` — publish a command to the wrapped aggregate
20
- - `HandleDirective(handler, directive)` — invoke an async handler for an
21
- extension-point-defined directive
22
- */
23
- /* these actions are needed for Mapping */
12
+ /** What `mapIncomingCommand` can do with a command an extension published:
13
+ publish one to the wrapped aggregate, or run a directive handler. */
24
14
  type commandAction<'command, 'directive> =
25
15
  | PublishCommand(string, 'command)
26
16
  | HandleDirective(directiveHandler<'directive>, 'directive)
27
17
 
28
- /**
29
- Actions returned by `mapOutgoingEvent` what to do when the wrapped aggregate
30
- emits an event that should be reflected through the extension point.
31
-
32
- - `PublishEvent(id, event)` — synchronously emit an extension point event
33
- - `PublishEventAsync(promise)` — resolve a promise and emit the resulting event
34
- - `HandleDirective(handler, directive)` — invoke an async handler for an
35
- extension-point-defined directive
36
- */
18
+ /** What `mapOutgoingEvent` can do with a Delegate event: publish an EP event,
19
+ publish one behind a promise, or run a directive handler. */
37
20
  type eventAction<'event, 'directive> =
38
21
  | PublishEvent(string, 'event)
39
22
  | PublishEventAsync(promise<(string, 'event)>)
40
23
  | HandleDirective(directiveHandler<'directive>, 'directive)
41
24
 
42
- /**
43
- The extension point protocol — defines the command, event, and directive types
44
- that extensions and aggregates exchange through this extension point.
45
- */
25
+ /** The extension point protocol: the command, event and directive types crossing it. */
46
26
  module type Spec = {
47
27
  let name: string
48
28
  let moduleUrl: string
@@ -55,26 +35,14 @@ module type Spec = {
55
35
  type directive
56
36
  }
57
37
 
58
- /**
59
- Maps an incoming extension point command to zero or more aggregate commands
60
- (or side-effect calls).
61
-
62
- Called when an extension publishes a command to this extension point.
63
- Receives the entity ID, the command, and the message metadata.
64
- */
38
+ /** Maps a command an extension published to zero or more aggregate commands. */
65
39
  type mapIncomingCommand<'extensionPointCommand, 'aggregateCommand, 'extensionPointDirective> = (
66
40
  string,
67
41
  'extensionPointCommand,
68
42
  Reventless.Message.meta,
69
43
  ) => array<commandAction<'aggregateCommand, 'extensionPointDirective>>
70
44
 
71
- /**
72
- Maps an aggregate outgoing event to zero or more extension point events
73
- (or side-effect calls).
74
-
75
- Called when the wrapped aggregate emits an event. Receives the entity ID,
76
- the event, message metadata, and the query engine.
77
- */
45
+ /** Maps an aggregate event to zero or more extension point events. */
78
46
  type mapOutgoingEvent<'aggregateEvent, 'extensionPointEvent, 'extensionPointDirective> = (
79
47
  string,
80
48
  'aggregateEvent,
@@ -82,22 +50,30 @@ type mapOutgoingEvent<'aggregateEvent, 'extensionPointEvent, 'extensionPointDire
82
50
  Reventless.QueryEngine.operations,
83
51
  ) => array<eventAction<'extensionPointEvent, 'extensionPointDirective>>
84
52
 
85
- /**
86
- Application-level implementation of the command / event mapping for one
87
- aggregate connected to an extension point.
53
+ /** One published event of this port and the `Delegate` events producing it. Bare
54
+ constructor names, qualified downstream by `Plugin_Structure`. Per published
55
+ event, so many-to-one the case a port exists for — reads naturally. */
56
+ type publishedEvent = {
57
+ name: string,
58
+ fromEventTypes: array<string>,
59
+ }
60
+
61
+ /** The port's inbound half: one command this port takes and the `Delegate`
62
+ commands it routes to. Keyed by the arriving command, mirroring
63
+ `ExtensionMapping.handledEvent`. */
64
+ type acceptedCommand = {
65
+ name: string,
66
+ toCommandTypes: array<string>,
67
+ }
88
68
 
89
- Pass this to `ExtensionPointMapping.Make(Spec, Mapping)` to produce a compiled
90
- `ExtensionPointMapping.T` module.
91
- */
69
+ /** One aggregate's mapping to an extension point. Pass to
70
+ `ExtensionPointMapping.Make` for the compiled `T`. */
92
71
  module type Mapping = {
93
72
  module ExtensionPoint: Spec
94
73
  module Delegate: Reventless.Aggregate.Spec
95
74
 
96
- // Module URL of the mapping file (e.g. catalog/src/ExtensionPoint/
97
- // Products_ExtensionPointMapping.res.mjs). Distinct from ExtensionPoint.moduleUrl
98
- // (the spec, in catalog-spec). Auto-injected by `@@reventless.spec` on
99
- // ExtensionPointMapping files. The EventCollector runtime needs THIS url
100
- // (not the spec's) to dynamic-import the mapping file's mapOutgoingEvent.
75
+ // The mapping file's own url, not the spec's the EventCollector runtime
76
+ // dynamic-imports mapOutgoingEvent from it. Injected by `@@reventless.spec`.
101
77
  let moduleUrl: string
102
78
 
103
79
  let mapIncomingCommand: mapIncomingCommand<
@@ -109,10 +85,17 @@ module type Mapping = {
109
85
  let mapOutgoingEvent: option<
110
86
  mapOutgoingEvent<Delegate.event, ExtensionPoint.event, ExtensionPoint.directive>,
111
87
  >
88
+
89
+ /** The port's translation table — see `publishedEvent`. Derived by the PPX from
90
+ `mapOutgoingEvent`'s own arms; write it by hand only where it says it cannot. */
91
+ let publishedEvents: array<publishedEvent>
92
+
93
+ /** The same for the command direction — see `acceptedCommand`. Derived from
94
+ `mapIncomingCommand`'s arms, under the same rule. */
95
+ let acceptedCommands: array<acceptedCommand>
112
96
  }
113
97
 
114
- // Internal pre-compiled action types used by the ExtensionPoint runtime.
115
- // Created by ExtensionPointMapping.Make (in reventless); consumed by ExtensionPoint_Callback
98
+ // Pre-compiled action types: made by Make, consumed by ExtensionPoint_Callback
116
99
  // and ExtensionPoint_Operations.
117
100
 
118
101
  /** Internal runtime action produced after pre-encoding a `commandAction`. Not for direct use. */
@@ -126,25 +109,15 @@ type abstractEventAction<'extensionPointEvent> =
126
109
  | AbstractPublishEventAsync(promise<(string, Reventless.Message.meta, JSON.t)>)
127
110
  | AbstractHandleDirective(unit => promise<unit>)
128
111
 
129
- /**
130
- A pre-compiled mapping module produced by `ExtensionPointMapping.Make(Spec, Mapping)`.
131
-
132
- The runtime uses `mapIncomingCommands` and `mapOutgoingEvent` to dispatch commands
133
- and events without knowing the concrete extension point or aggregate types.
134
- Application developers call `Make` themselves; the result satisfies this type.
135
- */
136
- // Pre-compiled mapping module type. Created by ExtensionPointMapping.Make(Spec, Mapping).
137
- // App developers call Make themselves; the result satisfies this type.
112
+ /** A compiled mapping, produced by `Make`. Lets the runtime dispatch without
113
+ knowing the concrete extension point or aggregate types. */
138
114
  module type T = {
139
115
  module ExtensionPoint: Spec
140
116
 
141
117
  /** Name of the target this mapping connects to the extension point. */
142
118
  let delegateName: string
143
119
 
144
- /**
145
- Converts a batch of typed extension point commands into pre-encoded abstract actions.
146
- Called by the extension point runtime for each incoming command batch.
147
- */
120
+ /** Pre-encodes a batch of typed EP commands for the runtime. */
148
121
  let mapIncomingCommands: (
149
122
  array<CommandTopic.topicItem<Reventless.Message.command'<Reventless.Id.String.t, ExtensionPoint.command>>>,
150
123
  Reventless.Schedule.create,
@@ -152,10 +125,7 @@ module type T = {
152
125
  Reventless.QueryEngine.operations,
153
126
  ) => array<abstractCommandAction>
154
127
 
155
- /**
156
- Converts a raw aggregate event JSON into pre-encoded abstract event actions.
157
- `None` if this mapping does not produce outgoing extension point events.
158
- */
128
+ /** Pre-encodes a raw aggregate event JSON. `None` if nothing is published out. */
159
129
  let mapOutgoingEvent: option<
160
130
  (
161
131
  JSON.t,
@@ -174,20 +144,13 @@ module Make = (MappingImpl: Mapping): (
174
144
  let delegateName = Delegate.name
175
145
  let extensionPointName = Spec.name
176
146
 
177
- // Variant TAGs the Delegate cares about — derived from the Delegate's event
178
- // schema at functor instantiation. Used to pre-filter incoming envelopes:
179
- // sibling variants on the source log (events the Delegate did not declare)
180
- // are silently skipped without any decode attempt.
181
- // Pre-filter incoming envelopes against the Delegate's full constructor
182
- // set — includes payload-less variants (e.g. UnknownPluginDetected
183
- // compiled to `S.literal("…")`) since the JSON envelope's `event` TAG
184
- // still carries the literal name even when the variant has no payload.
147
+ // Pre-filter for incoming envelopes: sibling variants the Delegate did not
148
+ // declare are skipped undecoded. Payload-less variants included the
149
+ // envelope's `event` TAG still carries their name.
185
150
  let acceptedTags = Reventless.DcbTag.extractAllVariantNames(Delegate.eventSchema)
186
151
 
187
- // Carry `comp` as a structured Effect log annotation EffectLogger.install
188
- // (in reventless-core, wired at Lambda startup) lifts it to the top-level
189
- // JSON `comp` field. Without the unified logger installed, Effect's default
190
- // logger still renders the annotation, just less prettily.
152
+ // `comp` as an Effect log annotation; EffectLogger.install lifts it to the
153
+ // top-level JSON field.
191
154
  let compLog = (comp, msg) =>
192
155
  Effect.logInfo(msg)->Effect.annotateLogs("comp", comp)->Effect.runSync
193
156
 
@@ -248,9 +211,7 @@ module Make = (MappingImpl: Mapping): (
248
211
  queryEngine,
249
212
  ) => {
250
213
  let tag = variantTagOfEnvelope(targetEventJson')
251
- // Pre-filter by TAG: sibling variants from the source log that the Delegate
252
- // did not declare are not this mapping's concern — skip silently with no
253
- // decode attempt.
214
+ // Not this mapping's concern skip without decoding.
254
215
  if !(acceptedTags->Array.includes(tag)) {
255
216
  []
256
217
  } else {
@@ -109,6 +109,7 @@ store, and reaching another plugin's store is the qualified form.
109
109
  type capability =
110
110
  | ObjectStore({plugin: string, store: string})
111
111
  | Geocoding
112
+ | Messaging
112
113
 
113
114
  // A provisioned geocoding place index, as returned by the framework's geocoding
114
115
  // capability helper. A record rather than a bare name so the handle can grow
@@ -181,6 +182,20 @@ type bakedManifest = {
181
182
 
182
183
  type geocoderIndex = {indexName: Pulumi.Input.t<string>}
183
184
 
185
+ /**
186
+ A provisioned messaging sender, as returned by the framework's messaging
187
+ capability helper.
188
+
189
+ `emailSender` is the verified address messages are sent *from* — a provider-side
190
+ identity, not something an application picks per message, which is why it is a
191
+ deploy-time handle rather than a field on `Messaging.message`. Absent when the
192
+ deployment provisions no email channel.
193
+
194
+ The channel set is derived from which senders are present rather than declared
195
+ beside them, so a handle carrying no sender at all cannot claim a channel.
196
+ */
197
+ type messagingSender = {emailSender?: Pulumi.Input.t<string>}
198
+
184
199
  // Options for the shell's map view mode. Written to config.json flat, beside
185
200
  // `viewModes`, because that is where the shell reads them; carried on the arm
186
201
  // here so a style set with the map off is unrepresentable.
@@ -426,6 +441,12 @@ module type T = {
426
441
  // on the platform API — and exports the index name for the unattended slice
427
442
  // path. Unset ⇒ no resolver, no export. In-memory platforms ignore this.
428
443
  geocoderPlaceIndex?: geocoderIndex,
444
+ // Optional messaging sender. When set, the deploy exports the verified
445
+ // sender address for the unattended slice path and grants those Lambdas the
446
+ // right to send from it. No client door — a browser has no business sending
447
+ // from the deployment's identity. Unset ⇒ no export, no grant. In-memory
448
+ // platforms ignore this.
449
+ messagingSender?: messagingSender,
429
450
  // Optional object store for direct-to-store uploads. When set, the deploy
430
451
  // provisions a presign service against it, threads that service's URL into
431
452
  // config.json as `uploadEndpoint`, and serves the store read-only from the
@@ -37,8 +37,10 @@ type event =
37
37
  // Emitted when a connecting plugin declared incompatible protocol versions.
38
38
  // The plugin is still connected; this event gives operators visibility.
39
39
  | IncompatiblePlugin(pluginDefinition)
40
+ // One fact for both arrivals, because the Plugin aggregate has one: per-version
41
+ // tracking made `VersionConnected` replace Connected/Reconnected, and a
42
+ // subscriber does the same work either way.
40
43
  | PluginConnected(pluginDefinition)
41
- | PluginReconnected(pluginDefinition)
42
44
  | PluginDisconnected(pluginDefinition)
43
45
  | PluginDeactivated(pluginDefinition)
44
46
  | PluginActivated(pluginDefinition)
@@ -45,10 +45,6 @@ let eventSchema = Sury.union([
45
45
  TAG: "PluginConnected",
46
46
  _0: s.m(Plugin$Reventless.pluginDefinitionSchema)
47
47
  })),
48
- Sury.$schema(s => ({
49
- TAG: "PluginReconnected",
50
- _0: s.m(Plugin$Reventless.pluginDefinitionSchema)
51
- })),
52
48
  Sury.$schema(s => ({
53
49
  TAG: "PluginDisconnected",
54
50
  _0: s.m(Plugin$Reventless.pluginDefinitionSchema)
@@ -153,6 +149,22 @@ let outboundTranslationSliceDefSchema = Plugin$Reventless.outboundTranslationSli
153
149
 
154
150
  let inboundTranslationSliceDefSchema = Plugin$Reventless.inboundTranslationSliceDefSchema;
155
151
 
152
+ let publishedEventDefSchema = Plugin$Reventless.publishedEventDefSchema;
153
+
154
+ let publishedEventDefArrayOptionSchema = Plugin$Reventless.publishedEventDefArrayOptionSchema;
155
+
156
+ let acceptedCommandDefSchema = Plugin$Reventless.acceptedCommandDefSchema;
157
+
158
+ let acceptedCommandDefArrayOptionSchema = Plugin$Reventless.acceptedCommandDefArrayOptionSchema;
159
+
160
+ let handledEventDefSchema = Plugin$Reventless.handledEventDefSchema;
161
+
162
+ let handledEventDefArrayOptionSchema = Plugin$Reventless.handledEventDefArrayOptionSchema;
163
+
164
+ let issuedCommandDefSchema = Plugin$Reventless.issuedCommandDefSchema;
165
+
166
+ let issuedCommandDefArrayOptionSchema = Plugin$Reventless.issuedCommandDefArrayOptionSchema;
167
+
156
168
  let extensionDefSchema = Plugin$Reventless.extensionDefSchema;
157
169
 
158
170
  let extensionPointDefSchema = Plugin$Reventless.extensionPointDefSchema;
@@ -163,6 +175,14 @@ let requiredStoreDeclarationSchema = Plugin$Reventless.requiredStoreDeclarationS
163
175
 
164
176
  let requiredStoreDeclarationArrayOptionSchema = Plugin$Reventless.requiredStoreDeclarationArrayOptionSchema;
165
177
 
178
+ let requiredCapabilityDeclarationSchema = Plugin$Reventless.requiredCapabilityDeclarationSchema;
179
+
180
+ let requiredCapabilityDeclarationArrayOptionSchema = Plugin$Reventless.requiredCapabilityDeclarationArrayOptionSchema;
181
+
182
+ let traitDeclarationSchema = Plugin$Reventless.traitDeclarationSchema;
183
+
184
+ let traitDeclarationArrayOptionSchema = Plugin$Reventless.traitDeclarationArrayOptionSchema;
185
+
166
186
  let pluginStructureSchema = Plugin$Reventless.pluginStructureSchema;
167
187
 
168
188
  let pluginStructureOffloadSchema = Plugin$Reventless.pluginStructureOffloadSchema;
@@ -200,11 +220,23 @@ export {
200
220
  automationSliceDefSchema,
201
221
  outboundTranslationSliceDefSchema,
202
222
  inboundTranslationSliceDefSchema,
223
+ publishedEventDefSchema,
224
+ publishedEventDefArrayOptionSchema,
225
+ acceptedCommandDefSchema,
226
+ acceptedCommandDefArrayOptionSchema,
227
+ handledEventDefSchema,
228
+ handledEventDefArrayOptionSchema,
229
+ issuedCommandDefSchema,
230
+ issuedCommandDefArrayOptionSchema,
203
231
  extensionDefSchema,
204
232
  extensionPointDefSchema,
205
233
  extensionPointDefArrayOptionSchema,
206
234
  requiredStoreDeclarationSchema,
207
235
  requiredStoreDeclarationArrayOptionSchema,
236
+ requiredCapabilityDeclarationSchema,
237
+ requiredCapabilityDeclarationArrayOptionSchema,
238
+ traitDeclarationSchema,
239
+ traitDeclarationArrayOptionSchema,
208
240
  pluginStructureSchema,
209
241
  pluginStructureOffloadSchema,
210
242
  pluginDefinitionSchema,