@reventlessdev/reventless-spec 3.0.0-alpha.62 → 3.0.0-alpha.63
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 +7 -0
- package/package.json +1 -1
- package/src/components/DcbDecode.res +9 -1
- package/src/components/DcbDecode.res.mjs +1 -0
- package/src/components/DcbValidation.res +31 -3
- package/src/components/DcbValidation.res.mjs +44 -2
- package/src/generator/Pairing.res +10 -6
- package/src/generator/Pairing.res.mjs +6 -4
- package/src/generator/PluginGenerator.res +5 -0
- package/src/generator/PluginGenerator.res.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.63 (2026-07-02)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **spec,interop,layer-builder:** generator/protocol/build failure modes (plan A8,A9) ([66d7a54](https://github.com/ReventlessDev/reventless-core/commit/66d7a54e3a0afdbfe3ea2975f517d1d64d52c180))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.62 (2026-06-29)
|
|
7
14
|
|
|
8
15
|
### Features
|
package/package.json
CHANGED
|
@@ -120,7 +120,15 @@ let makeDecoder = (schema: S.t<'event>): makeDecoderResult<'event> => {
|
|
|
120
120
|
try {
|
|
121
121
|
Some(JSON.Object(jsonDict)->S.parseJsonOrThrow(schema))
|
|
122
122
|
} catch {
|
|
123
|
-
| _ =>
|
|
123
|
+
| _ =>
|
|
124
|
+
// A parse failure here means a stored event no longer matches the
|
|
125
|
+
// current schema (drift). Dropping it silently hid real data loss;
|
|
126
|
+
// surface it as a warning so it's diagnosable.
|
|
127
|
+
Console.warn(
|
|
128
|
+
"DcbDecode: dropped event `" ++
|
|
129
|
+
eventType ++ "` — payload does not match the current schema (drift?)",
|
|
130
|
+
)
|
|
131
|
+
None
|
|
124
132
|
}
|
|
125
133
|
}
|
|
126
134
|
|
|
@@ -100,6 +100,7 @@ function makeDecoder(schema) {
|
|
|
100
100
|
try {
|
|
101
101
|
return Primitive_option.some(S.parseJsonOrThrow(jsonDict, schema));
|
|
102
102
|
} catch (exn) {
|
|
103
|
+
console.warn("DcbDecode: dropped event `" + eventType + "` — payload does not match the current schema (drift?)");
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
105
106
|
};
|
|
@@ -56,7 +56,10 @@ let extractAllVariants = (schema: S.t<unknown>): array<variantInfo> =>
|
|
|
56
56
|
// Check if a variant has no payload fields (only TAG)
|
|
57
57
|
let isPayloadLess = (info: variantInfo): bool => info.fields->Dict.toArray->Array.length == 0
|
|
58
58
|
|
|
59
|
-
// Compare two sury schema types for structural compatibility
|
|
59
|
+
// Compare two sury schema types for structural compatibility. Objects and
|
|
60
|
+
// unions recurse: previously any (Object, Object) / (Union, Union) pair was
|
|
61
|
+
// declared compatible, so nested payload drift (a renamed/retyped nested field,
|
|
62
|
+
// a changed variant) passed validation undetected.
|
|
60
63
|
let rec schemasAreCompatible = (a: S.t<unknown>, b: S.t<unknown>): bool =>
|
|
61
64
|
switch (a, b) {
|
|
62
65
|
| (String(_), String(_)) => true
|
|
@@ -65,10 +68,35 @@ let rec schemasAreCompatible = (a: S.t<unknown>, b: S.t<unknown>): bool =>
|
|
|
65
68
|
| (BigInt(_), BigInt(_)) => true
|
|
66
69
|
| (Array({additionalItems: Schema(aItem)}), Array({additionalItems: Schema(bItem)})) =>
|
|
67
70
|
schemasAreCompatible(aItem, bItem)
|
|
68
|
-
| (
|
|
69
|
-
| (
|
|
71
|
+
| (Object({properties: aProps}), Object({properties: bProps})) => propsCompatible(aProps, bProps)
|
|
72
|
+
| (Union(_), Union(_)) =>
|
|
73
|
+
// Same set of variant tags, and each shared variant's payload compatible.
|
|
74
|
+
let av = extractAllVariants(a)
|
|
75
|
+
let bv = extractAllVariants(b)
|
|
76
|
+
let at = av->Array.map(v => v.tagName)->Array.toSorted(String.compare)
|
|
77
|
+
let bt = bv->Array.map(v => v.tagName)->Array.toSorted(String.compare)
|
|
78
|
+
at == bt &&
|
|
79
|
+
at->Array.every(tag =>
|
|
80
|
+
switch (av->Array.find(v => v.tagName == tag), bv->Array.find(v => v.tagName == tag)) {
|
|
81
|
+
| (Some(x), Some(y)) => propsCompatible(x.fields, y.fields)
|
|
82
|
+
| _ => false
|
|
83
|
+
}
|
|
84
|
+
)
|
|
70
85
|
| _ => false
|
|
71
86
|
}
|
|
87
|
+
// Two property maps are compatible when they carry the same field names and each
|
|
88
|
+
// field's schema is compatible.
|
|
89
|
+
and propsCompatible = (aProps: dict<S.t<unknown>>, bProps: dict<S.t<unknown>>): bool => {
|
|
90
|
+
let aKeys = aProps->Dict.keysToArray->Array.toSorted(String.compare)
|
|
91
|
+
let bKeys = bProps->Dict.keysToArray->Array.toSorted(String.compare)
|
|
92
|
+
aKeys == bKeys &&
|
|
93
|
+
aKeys->Array.every(k =>
|
|
94
|
+
switch (aProps->Dict.get(k), bProps->Dict.get(k)) {
|
|
95
|
+
| (Some(av), Some(bv)) => schemasAreCompatible(av, bv)
|
|
96
|
+
| _ => false
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
}
|
|
72
100
|
|
|
73
101
|
// Get a human-readable type name for a schema
|
|
74
102
|
let schemaTypeName = (schema: S.t<unknown>): string =>
|
|
@@ -95,15 +95,56 @@ function schemasAreCompatible(_a, _b) {
|
|
|
95
95
|
_a = aItem;
|
|
96
96
|
continue;
|
|
97
97
|
case "object" :
|
|
98
|
-
|
|
98
|
+
if (b.type === "object") {
|
|
99
|
+
return propsCompatible(a.properties, b.properties);
|
|
100
|
+
} else {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
99
103
|
case "union" :
|
|
100
|
-
|
|
104
|
+
if (b.type !== "union") {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
let av = extractAllVariants(a);
|
|
108
|
+
let bv = extractAllVariants(b);
|
|
109
|
+
let at = av.map(v => v.tagName).toSorted(Primitive_string.compare);
|
|
110
|
+
let bt = bv.map(v => v.tagName).toSorted(Primitive_string.compare);
|
|
111
|
+
if (Primitive_object.equal(at, bt)) {
|
|
112
|
+
return at.every(tag => {
|
|
113
|
+
let match = av.find(v => v.tagName === tag);
|
|
114
|
+
let match$1 = bv.find(v => v.tagName === tag);
|
|
115
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
116
|
+
return propsCompatible(match.fields, match$1.fields);
|
|
117
|
+
} else {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
101
124
|
default:
|
|
102
125
|
return false;
|
|
103
126
|
}
|
|
104
127
|
};
|
|
105
128
|
}
|
|
106
129
|
|
|
130
|
+
function propsCompatible(aProps, bProps) {
|
|
131
|
+
let aKeys = Object.keys(aProps).toSorted(Primitive_string.compare);
|
|
132
|
+
let bKeys = Object.keys(bProps).toSorted(Primitive_string.compare);
|
|
133
|
+
if (Primitive_object.equal(aKeys, bKeys)) {
|
|
134
|
+
return aKeys.every(k => {
|
|
135
|
+
let match = aProps[k];
|
|
136
|
+
let match$1 = bProps[k];
|
|
137
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
138
|
+
return schemasAreCompatible(match, match$1);
|
|
139
|
+
} else {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
107
148
|
function schemaTypeName(schema) {
|
|
108
149
|
switch (schema.type) {
|
|
109
150
|
case "never" :
|
|
@@ -381,6 +422,7 @@ export {
|
|
|
381
422
|
extractAllVariants,
|
|
382
423
|
isPayloadLess,
|
|
383
424
|
schemasAreCompatible,
|
|
425
|
+
propsCompatible,
|
|
384
426
|
schemaTypeName,
|
|
385
427
|
dedupeKeys,
|
|
386
428
|
validateCompositeReads,
|
|
@@ -316,12 +316,16 @@ let resolve = (discovered: array<Discovery.discoveredFile>, ~srcDir: string): re
|
|
|
316
316
|
switch epGroup {
|
|
317
317
|
| None => flatEpMappings->Array.push(stem)
|
|
318
318
|
| Some(g) =>
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
319
|
+
// `Option.getOr`'s default is evaluated eagerly, so the previous shape
|
|
320
|
+
// re-created and re-set a fresh empty array every iteration — overwriting
|
|
321
|
+
// the group's real entry and pushing onto a now-detached array. A group
|
|
322
|
+
// with ≥2 mappings ended up empty, emitting a Plugin.res that referenced a
|
|
323
|
+
// never-generated module (`Array.getUnsafe(0)` on `[]`). Use a switch so
|
|
324
|
+
// the create branch runs only when the group is genuinely absent.
|
|
325
|
+
switch Dict.get(epByGroup, g) {
|
|
326
|
+
| Some(arr) => arr->Array.push(stem)
|
|
327
|
+
| None => Dict.set(epByGroup, g, [stem])
|
|
328
|
+
}
|
|
325
329
|
}
|
|
326
330
|
})
|
|
327
331
|
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import * as Nodefs from "node:fs";
|
|
4
4
|
import * as Nodepath from "node:path";
|
|
5
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
-
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
6
|
import * as Generator_Node$Reventless from "./Generator_Node.res.mjs";
|
|
8
7
|
|
|
9
8
|
let implSuffixForStateChange = "_Behavior";
|
|
@@ -282,9 +281,12 @@ function resolve(discovered, srcDir) {
|
|
|
282
281
|
let epGroup = param.epGroup;
|
|
283
282
|
let stem = param.stem;
|
|
284
283
|
if (epGroup !== undefined) {
|
|
285
|
-
let
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
let arr = epByGroup[epGroup];
|
|
285
|
+
if (arr !== undefined) {
|
|
286
|
+
arr.push(stem);
|
|
287
|
+
} else {
|
|
288
|
+
epByGroup[epGroup] = [stem];
|
|
289
|
+
}
|
|
288
290
|
return;
|
|
289
291
|
}
|
|
290
292
|
flatEpMappings.push(stem);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Usage: generate-plugin <srcDir>
|
|
3
3
|
// generate-plugin --aws <Namespace> <srcDir>
|
|
4
4
|
|
|
5
|
+
@val external processExit: int => unit = "process.exit"
|
|
6
|
+
|
|
5
7
|
let () = {
|
|
6
8
|
let argv2 = Generator_Node.argv->Array.get(2)->Option.getOr("")
|
|
7
9
|
let argv3 = Generator_Node.argv->Array.get(3)->Option.getOr("")
|
|
@@ -23,6 +25,9 @@ let () = {
|
|
|
23
25
|
Console.error("Usage: generate-plugin <srcDir>")
|
|
24
26
|
Console.error(" generate-plugin --aws <Namespace> <srcDir>")
|
|
25
27
|
}
|
|
28
|
+
// Exit non-zero on a usage error so `prebuild` (and CI) actually fail
|
|
29
|
+
// instead of continuing green with no Plugin.res generated.
|
|
30
|
+
processExit(1)
|
|
26
31
|
} else {
|
|
27
32
|
// Resolve to absolute path (handles relative paths and trailing slashes)
|
|
28
33
|
let srcDir = Generator_Node.resolve([srcDirArg])
|
|
@@ -39,6 +39,7 @@ if (srcDirArg === "") {
|
|
|
39
39
|
console.error("Usage: generate-plugin <srcDir>");
|
|
40
40
|
console.error(" generate-plugin --aws <Namespace> <srcDir>");
|
|
41
41
|
}
|
|
42
|
+
process.exit(1);
|
|
42
43
|
} else {
|
|
43
44
|
let srcDir = Nodepath.resolve(srcDirArg);
|
|
44
45
|
let init = Config$Reventless.read(srcDir);
|