@reventlessdev/reventless-spec 3.0.0-alpha.77 → 3.0.0-alpha.78

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,14 @@
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.78 (2026-07-16)
7
+
8
+ ### Features
9
+
10
+ * **aws:** consume per-component runtime hints; fold into shared command Lambdas ([7ba2485](https://github.com/ReventlessDev/reventless-core/commit/7ba248579f2472f2fc847b26033927199353f3e6))
11
+ * **core:** thread per-component runtime hints from plugin.json (spec/infra/core/local) ([a94f419](https://github.com/ReventlessDev/reventless-core/commit/a94f4199c4ac7eeeb24dd0a71d58fb73c1e514e6))
12
+
13
+
6
14
  # 3.0.0-alpha.77 (2026-07-15)
7
15
 
8
16
  ### Performance Improvements
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.77",
3
+ "version": "3.0.0-alpha.78",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -21,11 +21,11 @@
21
21
  "sury-ppx": "11.0.0-alpha.2"
22
22
  },
23
23
  "devDependencies": {
24
- "rescript": "^12.3.0",
25
- "@reventlessdev/rescript-jest": "1.0.0-alpha.7"
24
+ "rescript": "12.3.0",
25
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.8"
26
26
  },
27
27
  "peerDependencies": {
28
- "rescript": "^12.3.0"
28
+ "rescript": "12.3.0"
29
29
  },
30
30
  "publishConfig": {
31
31
  "registry": "https://registry.npmjs.org"
@@ -249,6 +249,86 @@ let renderExtensionMakeParam = (extensions: array<string>): option<string> =>
249
249
  Some(" ~extensions=[" ++ entries->Array.join(", ") ++ "],")
250
250
  }
251
251
 
252
+ // ── Per-component runtime hints (plugin.json `runtime` block) ─────────────────
253
+ // Emits `~componentRuntime=Dict.fromArray([("Customers", {memorySize: Some(2048),
254
+ // timeout: None}), …])` into `Platform.Plugin.make(...)`. Only emitted when the
255
+ // plugin declares a `runtime` block, so plugins without one keep a byte-identical
256
+ // generated Plugin.res.
257
+
258
+ let renderRuntimeHints = (h: Config.runtimeHints): string => {
259
+ let optInt = (o: option<int>) =>
260
+ switch o {
261
+ | Some(n) => "Some(" ++ n->Int.toString ++ ")"
262
+ | None => "None"
263
+ }
264
+ // Qualify the first field with the full module path so the record type resolves
265
+ // at the `Dict.fromArray([...])` call site (type-directed disambiguation does not
266
+ // propagate through the tuple/dict, so a bare `{memorySize, …}` has no type in scope).
267
+ "{ReventlessInfra.RuntimeHints.memorySize: " ++
268
+ optInt(h.memorySize) ++
269
+ ", timeout: " ++
270
+ optInt(h.timeout) ++ "}"
271
+ }
272
+
273
+ // Every component name reachable from the resolved plugin — the valid keys for
274
+ // the plugin.json `runtime` block. Mirrors the module names emitted above.
275
+ let collectComponentNames = (resolved: Pairing.resolved): array<string> =>
276
+ Array.flat([
277
+ resolved.aggregates->Array.map(({spec}) => spec),
278
+ resolved.readModels->Array.map(({readModel}) => readModel),
279
+ resolved.stateChangeSlices,
280
+ resolved.stateViewSlices,
281
+ resolved.stateViewSlicesStream,
282
+ resolved.automationSlices,
283
+ resolved.outboundTranslationSlices,
284
+ resolved.inboundTranslationSlices,
285
+ resolved.tasks,
286
+ resolved.extensions,
287
+ resolved.extensionPoints->Array.map(({group, mappings}) =>
288
+ switch group {
289
+ | Some(g) => g
290
+ | None => epModuleName(mappings->Array.getUnsafe(0))
291
+ }
292
+ ),
293
+ ])
294
+
295
+ // Typo protection: a `runtime` key that names no component is a warning, not a
296
+ // silent no-op. The override is still emitted (harmless — the deploy loop simply
297
+ // never looks it up).
298
+ let validateComponentRuntimeKeys = (
299
+ ~resolved: Pairing.resolved,
300
+ ~componentRuntime: dict<Config.runtimeHints>,
301
+ ): unit => {
302
+ let known = collectComponentNames(resolved)
303
+ componentRuntime
304
+ ->Dict.keysToArray
305
+ ->Array.forEach(name =>
306
+ if !(known->Array.includes(name)) {
307
+ Console.warn(
308
+ "Generator: plugin.json `runtime` override for `" ++
309
+ name ++
310
+ "` matches no component in this plugin. Known components: " ++
311
+ known->Array.join(", "),
312
+ )
313
+ }
314
+ )
315
+ }
316
+
317
+ let renderComponentRuntimeParam = (
318
+ componentRuntime: dict<Config.runtimeHints>,
319
+ ): option<string> => {
320
+ let entries = componentRuntime->Dict.toArray
321
+ if entries->Array.length === 0 {
322
+ None
323
+ } else {
324
+ let rendered =
325
+ entries
326
+ ->Array.map(((name, hints)) => "(\"" ++ name ++ "\", " ++ renderRuntimeHints(hints) ++ ")")
327
+ ->Array.join(", ")
328
+ Some(" ~componentRuntime=Dict.fromArray([" ++ rendered ++ "]),")
329
+ }
330
+ }
331
+
252
332
  let renderTaskMakeParam = (tasks: array<string>): option<string> =>
253
333
  if tasks->Array.length === 0 {
254
334
  None
@@ -705,8 +785,14 @@ let renderComposition = (
705
785
  ->Array.join(", ") ++ "],",
706
786
  )
707
787
  : None,
788
+ // Per-component runtime hints from plugin.json `runtime` (only emitted when
789
+ // present, so plugins without one keep a byte-identical generated Plugin.res).
790
+ renderComponentRuntimeParam(config.componentRuntime),
708
791
  ]
709
792
 
793
+ // Typo protection: warn on `runtime` keys that name no discovered component.
794
+ validateComponentRuntimeKeys(~resolved, ~componentRuntime=config.componentRuntime)
795
+
710
796
  makeParams->Array.filterMap(x => x)->Array.forEach(line => lines->Array.push(line))
711
797
  switch uiFragmentsParam {
712
798
  | Some(uiLines) => uiLines->Array.forEach(line => lines->Array.push(line))
@@ -178,6 +178,59 @@ function renderExtensionMakeParam(extensions) {
178
178
  return " ~extensions=[" + entries.join(", ") + "],";
179
179
  }
180
180
 
181
+ function renderRuntimeHints(h) {
182
+ let optInt = o => {
183
+ if (o !== undefined) {
184
+ return "Some(" + o.toString() + ")";
185
+ } else {
186
+ return "None";
187
+ }
188
+ };
189
+ return "{ReventlessInfra.RuntimeHints.memorySize: " + optInt(h.memorySize) + ", timeout: " + optInt(h.timeout) + "}";
190
+ }
191
+
192
+ function collectComponentNames(resolved) {
193
+ return [
194
+ resolved.aggregates.map(param => param.spec),
195
+ resolved.readModels.map(param => param.readModel),
196
+ resolved.stateChangeSlices,
197
+ resolved.stateViewSlices,
198
+ resolved.stateViewSlicesStream,
199
+ resolved.automationSlices,
200
+ resolved.outboundTranslationSlices,
201
+ resolved.inboundTranslationSlices,
202
+ resolved.tasks,
203
+ resolved.extensions,
204
+ resolved.extensionPoints.map(param => {
205
+ let group = param.group;
206
+ if (group !== undefined) {
207
+ return group;
208
+ } else {
209
+ return stripSuffix(param.mappings[0], "Mapping");
210
+ }
211
+ })
212
+ ].flat();
213
+ }
214
+
215
+ function validateComponentRuntimeKeys(resolved, componentRuntime) {
216
+ let known = collectComponentNames(resolved);
217
+ Object.keys(componentRuntime).forEach(name => {
218
+ if (!known.includes(name)) {
219
+ console.warn("Generator: plugin.json `runtime` override for `" + name + "` matches no component in this plugin. Known components: " + known.join(", "));
220
+ return;
221
+ }
222
+ });
223
+ }
224
+
225
+ function renderComponentRuntimeParam(componentRuntime) {
226
+ let entries = Object.entries(componentRuntime);
227
+ if (entries.length === 0) {
228
+ return;
229
+ }
230
+ let rendered = entries.map(param => "(\"" + param[0] + "\", " + renderRuntimeHints(param[1]) + ")").join(", ");
231
+ return " ~componentRuntime=Dict.fromArray([" + rendered + "]),";
232
+ }
233
+
181
234
  function renderTaskMakeParam(tasks) {
182
235
  if (tasks.length === 0) {
183
236
  return;
@@ -438,8 +491,10 @@ function renderComposition(config, resolved, componentChapters) {
438
491
  renderMakeParam("outboundTranslationSlices", resolved.outboundTranslationSlices, "Slice"),
439
492
  renderMakeParam("inboundTranslationSlices", resolved.inboundTranslationSlices, "Slice"),
440
493
  hasPluginStructure ? " ~pluginStructure=pluginStructure," : undefined,
441
- resolved.systemCallableComponents.length !== 0 ? " ~systemCallableComponents=[" + resolved.systemCallableComponents.map(n => "\"" + n + "\"").join(", ") + "]," : undefined
494
+ resolved.systemCallableComponents.length !== 0 ? " ~systemCallableComponents=[" + resolved.systemCallableComponents.map(n => "\"" + n + "\"").join(", ") + "]," : undefined,
495
+ renderComponentRuntimeParam(config.componentRuntime)
442
496
  ];
497
+ validateComponentRuntimeKeys(resolved, config.componentRuntime);
443
498
  Stdlib_Array.filterMap(makeParams, x => x).forEach(line => {
444
499
  lines.push(line);
445
500
  });
@@ -493,6 +548,10 @@ export {
493
548
  renderAggregateMakeParam,
494
549
  renderEpMakeParam,
495
550
  renderExtensionMakeParam,
551
+ renderRuntimeHints,
552
+ collectComponentNames,
553
+ validateComponentRuntimeKeys,
554
+ renderComponentRuntimeParam,
496
555
  renderTaskMakeParam,
497
556
  renderPluginStructureCall,
498
557
  validateUniqueSpecStems,
@@ -3,10 +3,22 @@
3
3
 
4
4
  type variant = Composition | Aws({compositionNamespace: string})
5
5
 
6
+ // Per-component runtime resource hints, parsed from the optional `runtime`
7
+ // block in plugin.json and keyed by component name. Both fields are optional;
8
+ // an omitted field falls through to the per-kind builder default. See
9
+ // docs/plans/configurable-component-runtime-resources.md.
10
+ type runtimeHints = {
11
+ memorySize: option<int>,
12
+ timeout: option<int>,
13
+ }
14
+
6
15
  type config = {
7
16
  name: string,
8
17
  heartbeatInterval: int,
9
18
  exclude: array<string>,
19
+ // Keyed by component name (e.g. "Customers", "PlaceOrder"). Empty when the
20
+ // plugin.json has no `runtime` block.
21
+ componentRuntime: dict<runtimeHints>,
10
22
  variant: variant,
11
23
  }
12
24
 
@@ -41,6 +53,31 @@ let getStrArrayField = (json: JSON.t, key: string): option<array<string>> =>
41
53
  ->Option.flatMap(JSON.Decode.array)
42
54
  ->Option.map(arr => arr->Array.filterMap(JSON.Decode.string))
43
55
 
56
+ // Parse the optional `runtime` object: each key is a component name, each value
57
+ // an object with optional `memorySize` / `timeout` ints. A missing block yields
58
+ // an empty dict; a non-object entry is skipped.
59
+ let getComponentRuntime = (json: JSON.t): dict<runtimeHints> => {
60
+ let result: dict<runtimeHints> = Dict.make()
61
+ json
62
+ ->JSON.Decode.object
63
+ ->Option.flatMap(d => d->Dict.get("runtime"))
64
+ ->Option.flatMap(JSON.Decode.object)
65
+ ->Option.forEach(runtimeObj =>
66
+ runtimeObj
67
+ ->Dict.toArray
68
+ ->Array.forEach(((componentName, entry)) =>
69
+ result->Dict.set(
70
+ componentName,
71
+ {
72
+ memorySize: getIntField(entry, "memorySize"),
73
+ timeout: getIntField(entry, "timeout"),
74
+ },
75
+ )
76
+ )
77
+ )
78
+ result
79
+ }
80
+
44
81
  let read = (~srcDir: string): config => {
45
82
  // Read package.json `name` from the parent of srcDir
46
83
  let packageJsonPath = Generator_Node.join([Generator_Node.dirname(srcDir), "package.json"])
@@ -63,6 +100,7 @@ let read = (~srcDir: string): config => {
63
100
  ->Option.flatMap(j => getIntField(j, "heartbeatInterval"))
64
101
  ->Option.getOr(5),
65
102
  exclude: pluginJson->Option.flatMap(j => getStrArrayField(j, "exclude"))->Option.getOr([]),
103
+ componentRuntime: pluginJson->Option.map(getComponentRuntime)->Option.getOr(Dict.make()),
66
104
  variant: Composition,
67
105
  }
68
106
  }
@@ -27,6 +27,20 @@ function getStrArrayField(json, key) {
27
27
  return Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d[key]), Stdlib_JSON.Decode.array), arr => Stdlib_Array.filterMap(arr, Stdlib_JSON.Decode.string));
28
28
  }
29
29
 
30
+ function getComponentRuntime(json) {
31
+ let result = {};
32
+ Stdlib_Option.forEach(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["runtime"]), Stdlib_JSON.Decode.object), runtimeObj => {
33
+ Object.entries(runtimeObj).forEach(param => {
34
+ let entry = param[1];
35
+ result[param[0]] = {
36
+ memorySize: getIntField(entry, "memorySize"),
37
+ timeout: getIntField(entry, "timeout")
38
+ };
39
+ });
40
+ });
41
+ return result;
42
+ }
43
+
30
44
  function read(srcDir) {
31
45
  let packageJsonPath = Nodepath.join(Nodepath.dirname(srcDir), "package.json");
32
46
  let packageJsonName = Stdlib_Option.flatMap(readJson(packageJsonPath), j => getStrField(j, "name"));
@@ -36,6 +50,7 @@ function read(srcDir) {
36
50
  name: PluginName$Reventless.resolve(Stdlib_Option.flatMap(pluginJson, j => getStrField(j, "name")), packageJsonName),
37
51
  heartbeatInterval: Stdlib_Option.getOr(Stdlib_Option.flatMap(pluginJson, j => getIntField(j, "heartbeatInterval")), 5),
38
52
  exclude: Stdlib_Option.getOr(Stdlib_Option.flatMap(pluginJson, j => getStrArrayField(j, "exclude")), []),
53
+ componentRuntime: Stdlib_Option.getOr(Stdlib_Option.map(pluginJson, getComponentRuntime), {}),
39
54
  variant: "Composition"
40
55
  };
41
56
  }
@@ -48,6 +63,7 @@ export {
48
63
  getStrField,
49
64
  getIntField,
50
65
  getStrArrayField,
66
+ getComponentRuntime,
51
67
  read,
52
68
  }
53
69
  /* node:fs Not a pure module */
@@ -46,10 +46,12 @@ if (srcDirArg === "") {
46
46
  let config_name = init.name;
47
47
  let config_heartbeatInterval = init.heartbeatInterval;
48
48
  let config_exclude = init.exclude;
49
+ let config_componentRuntime = init.componentRuntime;
49
50
  let config = {
50
51
  name: config_name,
51
52
  heartbeatInterval: config_heartbeatInterval,
52
53
  exclude: config_exclude,
54
+ componentRuntime: config_componentRuntime,
53
55
  variant: variant
54
56
  };
55
57
  let discovered = Discovery$Reventless.scan(srcDir, config_exclude);