@reventlessdev/reventless-core 3.0.0-alpha.153 → 3.0.0-alpha.154

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,16 @@
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.154 (2026-07-11)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **plugin-lifecycle:** heal message decode of definitions persisted before a schema field existed ([6bb3e72](https://github.com/ReventlessDev/reventless-core/commit/6bb3e7259ad606a0f77fb670bcfc680256592003))
11
+ ### Features
12
+
13
+ * **monitoring:** deploy-time Monitoring hook seam for provisioned execution units ([30f1c23](https://github.com/ReventlessDev/reventless-core/commit/30f1c23ba118805dae83af9115341f4aff6db92b))
14
+
15
+
6
16
  # 3.0.0-alpha.153 (2026-07-10)
7
17
 
8
18
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.153",
3
+ "version": "3.0.0-alpha.154",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,16 +29,16 @@
29
29
  "sury": "11.0.0-alpha.4",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.25",
32
- "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
33
32
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
34
- "@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
35
33
  "@reventlessdev/rescript-jest": "1.0.0-alpha.6",
34
+ "@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
36
35
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
36
+ "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
37
37
  "@reventlessdev/rescript-ssh2": "1.1.0-alpha.11",
38
38
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.14",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.94",
40
39
  "@reventlessdev/reventless-interop": "3.0.0-alpha.24",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.72"
40
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.95",
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.73"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "^12.3.0",
package/src/Message.res CHANGED
@@ -64,10 +64,16 @@ let toCommandSchema' = (idSchema, commandSchema) =>
64
64
  })
65
65
  )
66
66
 
67
+ // Schema-migration-on-read: reuse the tolerant parse from `Reventless.Message`
68
+ // (`parseJsonTolerant` / `fillMissingDefaults`, included above) so a message persisted
69
+ // before a nested `@schema` field existed still decodes instead of bricking the
70
+ // aggregate's log replay. These two entry points thread the memoized envelope schema;
71
+ // `Reventless.Message.decode` (the other central decoder) is tolerant at the source.
72
+ // See docs/plans/platform-infrastructure-in-plugin-list.md (durable fix option 2).
67
73
  let decodeEvent' = (json, idSchema, eventSchema) =>
68
- json->S.parseJsonOrThrow(toEventSchema'(idSchema, eventSchema))
74
+ json->parseJsonTolerant(toEventSchema'(idSchema, eventSchema))
69
75
  let decodeCommand' = (json, idSchema, commandSchema) =>
70
- json->S.parseJsonOrThrow(toCommandSchema'(idSchema, commandSchema))
76
+ json->parseJsonTolerant(toCommandSchema'(idSchema, commandSchema))
71
77
 
72
78
  let encodeEvent' = (event', idSchema, eventSchema) =>
73
79
  event'->S.reverseConvertToJsonOrThrow(toEventSchema'(idSchema, eventSchema))
@@ -44,11 +44,11 @@ function toCommandSchema$p(idSchema, commandSchema) {
44
44
  }
45
45
 
46
46
  function decodeEvent$p(json, idSchema, eventSchema) {
47
- return S.parseJsonOrThrow(json, toEventSchema$p(idSchema, eventSchema));
47
+ return Message$Reventless.parseJsonTolerant(json, toEventSchema$p(idSchema, eventSchema));
48
48
  }
49
49
 
50
50
  function decodeCommand$p(json, idSchema, commandSchema) {
51
- return S.parseJsonOrThrow(json, toCommandSchema$p(idSchema, commandSchema));
51
+ return Message$Reventless.parseJsonTolerant(json, toCommandSchema$p(idSchema, commandSchema));
52
52
  }
53
53
 
54
54
  function encodeEvent$p(event$p, idSchema, eventSchema) {
@@ -384,6 +384,10 @@ let statusChangeSchema = Message$Reventless.statusChangeSchema;
384
384
 
385
385
  let commandJsonSchema = Message$Reventless.commandJsonSchema;
386
386
 
387
+ let fillMissingDefaults = Message$Reventless.fillMissingDefaults;
388
+
389
+ let parseJsonTolerant = Message$Reventless.parseJsonTolerant;
390
+
387
391
  let decode = Message$Reventless.decode;
388
392
 
389
393
  let encode = Message$Reventless.encode;
@@ -397,6 +401,8 @@ export {
397
401
  contextSchema,
398
402
  statusChangeSchema,
399
403
  commandJsonSchema,
404
+ fillMissingDefaults,
405
+ parseJsonTolerant,
400
406
  decode,
401
407
  encode,
402
408
  InvalidEvent,
@@ -0,0 +1,96 @@
1
+ /**
2
+ Deploy-time inventory hook: every runtime backend announces the execution units
3
+ it provisions so an extension can attach provider-native monitoring to each.
4
+
5
+ No-op by default. Zero behavioral change unless an extension registers a backend
6
+ via `use` before the platform/plugin build. The seam fires once per unit at
7
+ provisioning time (inside the deploy program) — never at invocation, and it runs
8
+ no code in production. Monitoring/alerting itself is deliberately NOT a framework
9
+ concern; this only exposes the choke point through which the framework says
10
+ "I just provisioned an execution unit" and leaves what to do about it to the
11
+ listener.
12
+
13
+ See `docs/plans/monitoring-hook-seam.md`.
14
+ */
15
+
16
+ /**
17
+ The ROLE of a provisioned execution unit, not its mechanism. What "failure"
18
+ means for a unit is provider-specific (a Lambda `Errors` metric vs. container
19
+ restarts vs. NATS redeliveries) and belongs in the extension's per-provider
20
+ `Backend`; core only says *that* a unit exists and *what role* it plays, which is
21
+ what keeps the contract stable across runtimes. `Other(string)` is the escape
22
+ hatch for support units and future roles (same pattern as the protocol's
23
+ `OtherKind`).
24
+ */
25
+ type unitKind =
26
+ | /** aggregates, state-change slices, extension points — failure = writes
27
+ rejected/dropped (the silent-freeze class) */
28
+ CommandHandler
29
+ | /** state-view slices, read models — failure = silently stale reads */
30
+ Projection
31
+ | /** automation slices, side-effect handlers, translations — failure = missed
32
+ side effects at boundaries */
33
+ Reactor
34
+ | /** cross-plugin/admin event ingestion — failure = event flow between plugins
35
+ stops */
36
+ EventCollector
37
+ | /** scheduled task runners */
38
+ Task
39
+ | /** heartbeat/keep-alive — failure = lifecycle detection AND any staleness
40
+ watchdog go blind (monitoring's own pulse) */
41
+ Scheduler
42
+ | /** receives messages that exhausted processing */
43
+ DeadLetterSink
44
+ | /** support units (counters, change-feed relays, migration runners, query
45
+ resolvers, …) — providers pass their name */
46
+ Other(string)
47
+
48
+ /**
49
+ A monitoring backend, registered by an extension (a deploy program) before the
50
+ platform builds. `onProvisioned` is called once per execution unit.
51
+
52
+ `~name` is the unit's STATIC logical name — the string the builder passed to its
53
+ provisioning call. A Pulumi-based backend derives logical resource names from it
54
+ (alarm/rule resource names must be stable and known at graph-construction time).
55
+ `~component` is the deploy-time `Adapter.resource`; its fields are `Output`s
56
+ (physical, suffix-bearing — right for a metric dimension like a CloudWatch
57
+ FunctionName or a PrometheusRule deployment selector, but unusable as a logical
58
+ resource name). Because `onProvisioned` fires during resource creation inside the
59
+ deploy program, a Pulumi-based backend can create monitoring resources in the
60
+ same stack, parented naturally.
61
+ */
62
+ module type Backend = {
63
+ let onProvisioned: (
64
+ ~kind: unitKind,
65
+ ~name: string,
66
+ ~component: ReventlessInfra.Adapter.resource,
67
+ ) => unit
68
+ }
69
+
70
+ /** Default backend: does nothing. Active unless an extension calls `use`. */
71
+ module Noop: Backend = {
72
+ let onProvisioned = (~kind as _, ~name as _, ~component as _) => ()
73
+ }
74
+
75
+ let backend: ref<module(Backend)> = ref(module(Noop: Backend))
76
+
77
+ /**
78
+ Register the monitoring backend. Must run before the platform/plugin build in the
79
+ deploy program (plain statement order — the registry is consulted lazily at each
80
+ provisioning site). Called once, by the extension, first.
81
+ */
82
+ let use = (b: module(Backend)) => backend := b
83
+
84
+ /**
85
+ Announce a provisioned execution unit to the registered backend. Called by
86
+ runtime backends at their provisioning sites (once per unit). No-op until an
87
+ extension registers a backend.
88
+ */
89
+ let notify = (
90
+ ~kind: unitKind,
91
+ ~name: string,
92
+ ~component: ReventlessInfra.Adapter.resource,
93
+ ) => {
94
+ module B = unpack(backend.contents)
95
+ B.onProvisioned(~kind, ~name, ~component)
96
+ }
@@ -0,0 +1,31 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ function onProvisioned(param, param$1, param$2) {
5
+
6
+ }
7
+
8
+ let Noop = {
9
+ onProvisioned: onProvisioned
10
+ };
11
+
12
+ let backend = {
13
+ contents: Noop
14
+ };
15
+
16
+ function use(b) {
17
+ backend.contents = b;
18
+ }
19
+
20
+ function notify(kind, name, component) {
21
+ let B = backend.contents;
22
+ B.onProvisioned(kind, name, component);
23
+ }
24
+
25
+ export {
26
+ Noop,
27
+ backend,
28
+ use,
29
+ notify,
30
+ }
31
+ /* No side effect */
@@ -0,0 +1,53 @@
1
+ open JestGlobals
2
+
3
+ // The registry only forwards `~component`; it never reads its fields. A
4
+ // structural stand-in keeps @pulumi/pulumi (whose Output.make would build a real
5
+ // resource) out of the Jest run — its ESM entry can't be imported under Jest.
6
+ let stubResource: ReventlessInfra.Adapter.resource = %raw(`{}`)
7
+
8
+ describe("Monitoring", () => {
9
+ testSync("Noop default: notify is silent and does not throw", () => {
10
+ // No backend registered — the default Noop backend swallows the call.
11
+ Monitoring.notify(~kind=CommandHandler, ~name="before", ~component=stubResource)
12
+ expect(true)->toBe(true)
13
+ })
14
+
15
+ testSync("use registers a backend that receives every notify, role and name", () => {
16
+ let recorded: array<(Monitoring.unitKind, string)> = []
17
+ module Recorder: Monitoring.Backend = {
18
+ let onProvisioned = (~kind, ~name, ~component as _) => recorded->Array.push((kind, name))
19
+ }
20
+ Monitoring.use(module(Recorder: Monitoring.Backend))
21
+
22
+ Monitoring.notify(~kind=CommandHandler, ~name="AllAggregatesCmdHandler", ~component=stubResource)
23
+ Monitoring.notify(~kind=Projection, ~name="AllStateViewSlices", ~component=stubResource)
24
+ Monitoring.notify(~kind=DeadLetterSink, ~name="DeadLetterQueue", ~component=stubResource)
25
+ Monitoring.notify(~kind=Other("Counter"), ~name="ProductCounter", ~component=stubResource)
26
+
27
+ expect(recorded)->toEqual([
28
+ (Monitoring.CommandHandler, "AllAggregatesCmdHandler"),
29
+ (Monitoring.Projection, "AllStateViewSlices"),
30
+ (Monitoring.DeadLetterSink, "DeadLetterQueue"),
31
+ (Monitoring.Other("Counter"), "ProductCounter"),
32
+ ])
33
+
34
+ // Restore the default so registry state doesn't leak to other cases.
35
+ Monitoring.use(module(Monitoring.Noop))
36
+ })
37
+
38
+ testSync("notify forwards the static name and component resource to the backend", () => {
39
+ let seen: ref<option<(string, ReventlessInfra.Adapter.resource)>> = ref(None)
40
+ module Capture: Monitoring.Backend = {
41
+ let onProvisioned = (~kind as _, ~name, ~component) => seen := Some((name, component))
42
+ }
43
+ Monitoring.use(module(Capture: Monitoring.Backend))
44
+
45
+ Monitoring.notify(~kind=Scheduler, ~name="HeartbeatUnit", ~component=stubResource)
46
+ switch seen.contents {
47
+ | Some((name, _component)) => expect(name)->toBe("HeartbeatUnit")
48
+ | None => expect("no notify received")->toBe("a notify")
49
+ }
50
+
51
+ Monitoring.use(module(Monitoring.Noop))
52
+ })
53
+ })
@@ -0,0 +1,82 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Monitoring$ReventlessCore from "../../src/adapter/Monitoring/Monitoring.res.mjs";
4
+
5
+ let stubResource = {};
6
+
7
+ globalThis.describe("Monitoring", () => {
8
+ globalThis.test("Noop default: notify is silent and does not throw", () => {
9
+ Monitoring$ReventlessCore.notify("CommandHandler", "before", stubResource);
10
+ globalThis.expect(true).toBe(true);
11
+ });
12
+ globalThis.test("use registers a backend that receives every notify, role and name", () => {
13
+ let recorded = [];
14
+ let onProvisioned = (kind, name, param) => {
15
+ recorded.push([
16
+ kind,
17
+ name
18
+ ]);
19
+ };
20
+ let Recorder = {
21
+ onProvisioned: onProvisioned
22
+ };
23
+ Monitoring$ReventlessCore.use(Recorder);
24
+ Monitoring$ReventlessCore.notify("CommandHandler", "AllAggregatesCmdHandler", stubResource);
25
+ Monitoring$ReventlessCore.notify("Projection", "AllStateViewSlices", stubResource);
26
+ Monitoring$ReventlessCore.notify("DeadLetterSink", "DeadLetterQueue", stubResource);
27
+ Monitoring$ReventlessCore.notify({
28
+ TAG: "Other",
29
+ _0: "Counter"
30
+ }, "ProductCounter", stubResource);
31
+ globalThis.expect(recorded).toEqual([
32
+ [
33
+ "CommandHandler",
34
+ "AllAggregatesCmdHandler"
35
+ ],
36
+ [
37
+ "Projection",
38
+ "AllStateViewSlices"
39
+ ],
40
+ [
41
+ "DeadLetterSink",
42
+ "DeadLetterQueue"
43
+ ],
44
+ [
45
+ {
46
+ TAG: "Other",
47
+ _0: "Counter"
48
+ },
49
+ "ProductCounter"
50
+ ]
51
+ ]);
52
+ Monitoring$ReventlessCore.use(Monitoring$ReventlessCore.Noop);
53
+ });
54
+ globalThis.test("notify forwards the static name and component resource to the backend", () => {
55
+ let seen = {
56
+ contents: undefined
57
+ };
58
+ let onProvisioned = (param, name, component) => {
59
+ seen.contents = [
60
+ name,
61
+ component
62
+ ];
63
+ };
64
+ let Capture = {
65
+ onProvisioned: onProvisioned
66
+ };
67
+ Monitoring$ReventlessCore.use(Capture);
68
+ Monitoring$ReventlessCore.notify("Scheduler", "HeartbeatUnit", stubResource);
69
+ let match = seen.contents;
70
+ if (match !== undefined) {
71
+ globalThis.expect(match[0]).toBe("HeartbeatUnit");
72
+ } else {
73
+ globalThis.expect("no notify received").toBe("a notify");
74
+ }
75
+ Monitoring$ReventlessCore.use(Monitoring$ReventlessCore.Noop);
76
+ });
77
+ });
78
+
79
+ export {
80
+ stubResource,
81
+ }
82
+ /* Not a pure module */
@@ -93,4 +93,150 @@ describe("Message should", () => {
93
93
 
94
94
  expect(eventId)->toBe("testId")
95
95
  })
96
+
97
+ // Schema-migration-on-read: a VersionConnected event persisted before `kind` (and
98
+ // before later pluginStructure fields like `events`/`chapter`) existed must still
99
+ // decode — otherwise the one stale event bricks the whole lifecycle aggregate. See
100
+ // docs/plans/platform-infrastructure-in-plugin-list.md (durable fix option 2).
101
+ testSync("tolerantly decode a VersionConnected persisted before kind/structure fields existed", () => {
102
+ open PluginSpec
103
+ let def: Reventless.Plugin.pluginDefinition = {
104
+ id: "id",
105
+ name: "Ordering",
106
+ version: "1.0.0",
107
+ extensionPoints: [],
108
+ extensions: [],
109
+ eventCollector: "arn",
110
+ extensionProtocols: [],
111
+ apiSchemaFragment: None,
112
+ apiTarget: None,
113
+ uiFragments: None,
114
+ structure: Some({
115
+ readModels: [],
116
+ stateViewSlices: [],
117
+ stateChangeSlices: [],
118
+ aggregates: [
119
+ {
120
+ name: "Order",
121
+ commands: [],
122
+ producedEventTypes: [],
123
+ consumedEventTypes: [],
124
+ linkedViews: [],
125
+ consistencyRead: None,
126
+ events: [],
127
+ chapter: None,
128
+ },
129
+ ],
130
+ automationSlices: [],
131
+ outboundTranslationSlices: [],
132
+ inboundTranslationSlices: [],
133
+ extensions: [],
134
+ extensionPoints: None,
135
+ }),
136
+ dcbEventLog: None,
137
+ kind: Domain,
138
+ }
139
+ let event' = {
140
+ id: "id",
141
+ meta: {
142
+ service: "svc",
143
+ time: "t",
144
+ ip: "ip",
145
+ user: "u",
146
+ msgId: "m",
147
+ correlationId: "c",
148
+ },
149
+ event: VersionConnected(def),
150
+ }
151
+ let json = event'->Message.encodeEvent'(S.string, PluginSpec.eventSchema)
152
+
153
+ // Simulate the persisted JSON as written before these fields existed: drop the
154
+ // mandatory enum `kind`, a top-level `T | null` field, and two nested writableDef
155
+ // fields (a mandatory array + a `T | null`).
156
+ let oldJson =
157
+ %raw(`function(j){
158
+ var c = JSON.parse(JSON.stringify(j));
159
+ var d = c.event._0;
160
+ delete d.kind;
161
+ delete d.apiSchemaFragment;
162
+ var agg = d.structure.aggregates[0];
163
+ delete agg.events;
164
+ delete agg.chapter;
165
+ return c;
166
+ }`)(json)
167
+
168
+ // Strict decode must reject the stale JSON — the tolerance is what saves it.
169
+ let strictThrew = switch oldJson->S.parseJsonOrThrow(
170
+ Message.toEventSchema'(S.string, PluginSpec.eventSchema),
171
+ ) {
172
+ | _ => false
173
+ | exception _ => true
174
+ }
175
+ expect(strictThrew)->toBe(true)
176
+
177
+ // Tolerant decode heals: kind -> Domain (first variant), missing array -> [],
178
+ // missing `T | null` fields -> None.
179
+ let decoded = oldJson->Message.decodeEvent'(S.string, PluginSpec.eventSchema)
180
+ switch decoded.event {
181
+ | VersionConnected(d) =>
182
+ expect(
183
+ switch d.kind {
184
+ | Domain => "Domain"
185
+ | _ => "other"
186
+ },
187
+ )->toBe("Domain")
188
+ expect(d.apiSchemaFragment->Option.isNone)->toBe(true)
189
+ let structure: Reventless.Plugin.pluginStructure = d.structure->Option.getOrThrow
190
+ let agg = structure.aggregates->Array.getUnsafe(0)
191
+ expect(agg.events->Array.length)->toBe(0)
192
+ expect(agg.chapter->Option.isNone)->toBe(true)
193
+ | _ => expect("wrong-variant")->toBe("VersionConnected")
194
+ }
195
+ })
196
+
197
+ // The exact aggregate-replay path: EventLog_Operations.decodeEvent reassembles the
198
+ // event variant and calls `Message.decode(Spec.eventSchema)`. This is what actually
199
+ // bricked in production, so assert `Message.decode` itself heals a stale def.
200
+ testSync("Message.decode heals a VersionConnected variant missing kind (the replay path)", () => {
201
+ open PluginSpec
202
+ let def: Reventless.Plugin.pluginDefinition = {
203
+ id: "id",
204
+ name: "Ordering",
205
+ version: "1.0.0",
206
+ extensionPoints: [],
207
+ extensions: [],
208
+ eventCollector: "arn",
209
+ extensionProtocols: [],
210
+ apiSchemaFragment: None,
211
+ apiTarget: None,
212
+ uiFragments: None,
213
+ structure: None,
214
+ dcbEventLog: None,
215
+ kind: Domain,
216
+ }
217
+ let variantJson = VersionConnected(def)->Message.encode(PluginSpec.eventSchema)
218
+ let staleJson =
219
+ %raw(`function(j){ var c = JSON.parse(JSON.stringify(j)); delete c._0.kind; return c; }`)(
220
+ variantJson,
221
+ )
222
+
223
+ // Strict decode of the stale variant throws (reproduces the production brick).
224
+ let strictThrew = switch staleJson->S.parseJsonOrThrow(PluginSpec.eventSchema) {
225
+ | _ => false
226
+ | exception _ => true
227
+ }
228
+ expect(strictThrew)->toBe(true)
229
+
230
+ // The replay decoder heals kind -> Domain.
231
+ switch staleJson->Message.decode(PluginSpec.eventSchema) {
232
+ | VersionConnected(d) =>
233
+ expect(
234
+ switch d.kind {
235
+ | Domain => "Domain"
236
+ | _ => "other"
237
+ },
238
+ )->toBe("Domain")
239
+ | _ => expect("wrong-variant")->toBe("VersionConnected")
240
+ }
241
+ })
96
242
  })
@@ -89,6 +89,133 @@ globalThis.describe("Message should", () => {
89
89
  let eventId = Stdlib_Option.getOrThrow(Message$ReventlessCore.idOfEvent$pJson(eventJson$p), undefined);
90
90
  globalThis.expect(eventId).toBe("testId");
91
91
  });
92
+ globalThis.test("tolerantly decode a VersionConnected persisted before kind/structure fields existed", () => {
93
+ let def = {
94
+ id: "id",
95
+ name: "Ordering",
96
+ version: "1.0.0",
97
+ extensionPoints: [],
98
+ extensions: [],
99
+ eventCollector: "arn",
100
+ extensionProtocols: [],
101
+ apiSchemaFragment: undefined,
102
+ apiTarget: undefined,
103
+ uiFragments: undefined,
104
+ structure: {
105
+ readModels: [],
106
+ stateViewSlices: [],
107
+ stateChangeSlices: [],
108
+ aggregates: [{
109
+ name: "Order",
110
+ commands: [],
111
+ producedEventTypes: [],
112
+ consumedEventTypes: [],
113
+ linkedViews: [],
114
+ consistencyRead: undefined,
115
+ events: [],
116
+ chapter: undefined
117
+ }],
118
+ automationSlices: [],
119
+ outboundTranslationSlices: [],
120
+ inboundTranslationSlices: [],
121
+ extensions: [],
122
+ extensionPoints: undefined
123
+ },
124
+ dcbEventLog: undefined,
125
+ kind: "Domain"
126
+ };
127
+ let event$p_meta = {
128
+ service: "svc",
129
+ time: "t",
130
+ ip: "ip",
131
+ user: "u",
132
+ msgId: "m",
133
+ correlationId: "c"
134
+ };
135
+ let event$p_event = {
136
+ TAG: "VersionConnected",
137
+ _0: def
138
+ };
139
+ let event$p = {
140
+ id: "id",
141
+ meta: event$p_meta,
142
+ event: event$p_event
143
+ };
144
+ let json = Message$ReventlessCore.encodeEvent$p(event$p, S.string, PluginSpec$ReventlessCore.eventSchema);
145
+ let oldJson = (function(j){
146
+ var c = JSON.parse(JSON.stringify(j));
147
+ var d = c.event._0;
148
+ delete d.kind;
149
+ delete d.apiSchemaFragment;
150
+ var agg = d.structure.aggregates[0];
151
+ delete agg.events;
152
+ delete agg.chapter;
153
+ return c;
154
+ })(json);
155
+ let strictThrew;
156
+ try {
157
+ S.parseJsonOrThrow(oldJson, Message$ReventlessCore.toEventSchema$p(S.string, PluginSpec$ReventlessCore.eventSchema));
158
+ strictThrew = false;
159
+ } catch (exn) {
160
+ strictThrew = true;
161
+ }
162
+ globalThis.expect(strictThrew).toBe(true);
163
+ let decoded = Message$ReventlessCore.decodeEvent$p(oldJson, S.string, PluginSpec$ReventlessCore.eventSchema);
164
+ let d = decoded.event;
165
+ if (d.TAG === "VersionConnected") {
166
+ let d$1 = d._0;
167
+ let match = d$1.kind;
168
+ let tmp;
169
+ tmp = match === "Domain" ? "Domain" : "other";
170
+ globalThis.expect(tmp).toBe("Domain");
171
+ globalThis.expect(Stdlib_Option.isNone(d$1.apiSchemaFragment)).toBe(true);
172
+ let structure = Stdlib_Option.getOrThrow(d$1.structure, undefined);
173
+ let agg = structure.aggregates[0];
174
+ globalThis.expect(agg.events.length).toBe(0);
175
+ globalThis.expect(Stdlib_Option.isNone(agg.chapter)).toBe(true);
176
+ return;
177
+ }
178
+ globalThis.expect("wrong-variant").toBe("VersionConnected");
179
+ });
180
+ globalThis.test("Message.decode heals a VersionConnected variant missing kind (the replay path)", () => {
181
+ let def = {
182
+ id: "id",
183
+ name: "Ordering",
184
+ version: "1.0.0",
185
+ extensionPoints: [],
186
+ extensions: [],
187
+ eventCollector: "arn",
188
+ extensionProtocols: [],
189
+ apiSchemaFragment: undefined,
190
+ apiTarget: undefined,
191
+ uiFragments: undefined,
192
+ structure: undefined,
193
+ dcbEventLog: undefined,
194
+ kind: "Domain"
195
+ };
196
+ let variantJson = Message$ReventlessCore.encode({
197
+ TAG: "VersionConnected",
198
+ _0: def
199
+ }, PluginSpec$ReventlessCore.eventSchema);
200
+ let staleJson = (function(j){ var c = JSON.parse(JSON.stringify(j)); delete c._0.kind; return c; })(variantJson);
201
+ let strictThrew;
202
+ try {
203
+ S.parseJsonOrThrow(staleJson, PluginSpec$ReventlessCore.eventSchema);
204
+ strictThrew = false;
205
+ } catch (exn) {
206
+ strictThrew = true;
207
+ }
208
+ globalThis.expect(strictThrew).toBe(true);
209
+ let d = Message$ReventlessCore.decode(staleJson, PluginSpec$ReventlessCore.eventSchema);
210
+ if (d.TAG === "VersionConnected") {
211
+ let match = d._0.kind;
212
+ let tmp;
213
+ tmp = match === "Domain" ? "Domain" : "other";
214
+ globalThis.expect(tmp).toBe("Domain");
215
+ return;
216
+ }
217
+ globalThis.expect("wrong-variant").toBe("VersionConnected");
218
+ });
92
219
  });
93
220
 
94
221
  /* Not a pure module */