@reventlessdev/reventless-core 3.0.0-alpha.166 → 3.0.0-alpha.168

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +3 -3
  3. package/src/Projection.res +11 -6
  4. package/src/Projection.res.mjs +27 -25
  5. package/src/admin/AdminApi.res +9 -62
  6. package/src/admin/AdminApi.res.mjs +6 -28
  7. package/src/components/Api/GraphQL_Stitcher.res +4 -112
  8. package/src/components/Api/GraphQL_Stitcher.res.mjs +0 -90
  9. package/src/components/Dcb/Dcb_Builder.res +38 -21
  10. package/src/components/Dcb/Dcb_Builder.res.mjs +13 -4
  11. package/src/components/ReadModel/ReadModel_Callback.res +1 -1
  12. package/src/components/ReadModel/ReadModel_Callback.res.mjs +1 -1
  13. package/src/components/StateViewSlice/StateViewSlice_Builder.res +1 -1
  14. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +1 -1
  15. package/src/components/StateViewSlice/StateViewSlice_Callback.res +1 -1
  16. package/src/components/StateViewSlice/StateViewSlice_Callback.res.mjs +1 -1
  17. package/src/plugin/component/Plugin_Builder.res +5 -7
  18. package/src/plugin/component/Plugin_Builder.res.mjs +3 -6
  19. package/src/util/EffectLogger.res +5 -3
  20. package/src/util/EffectLogger.res.mjs +3 -10
  21. package/src/util/Logger.res +41 -11
  22. package/src/util/Logger.res.mjs +36 -10
  23. package/tests/api/GraphQL_StitcherTest.res +0 -198
  24. package/tests/api/GraphQL_StitcherTest.res.mjs +0 -168
  25. package/src/admin/ApiFragmentRegistry/ApiFragmentRegistryBehavior.res +0 -108
  26. package/src/admin/ApiFragmentRegistry/ApiFragmentRegistryBehavior.res.mjs +0 -190
  27. package/src/admin/ApiFragmentRegistry/ApiFragmentRegistrySpec.res +0 -71
  28. package/src/admin/ApiFragmentRegistry/ApiFragmentRegistrySpec.res.mjs +0 -92
  29. package/src/admin/ApiFragmentRegistry/ApiFragmentsProjection.res +0 -63
  30. package/src/admin/ApiFragmentRegistry/ApiFragmentsProjection.res.mjs +0 -108
  31. package/src/admin/ApiFragmentRegistry/ApiFragmentsReadModelSpec.res +0 -23
  32. package/src/admin/ApiFragmentRegistry/ApiFragmentsReadModelSpec.res.mjs +0 -43
  33. package/src/admin/Platform_ApiFragmentsApi.res +0 -32
  34. package/src/admin/Platform_ApiFragmentsApi.res.mjs +0 -52
  35. package/src/components/Api/Api_Builder.res +0 -40
  36. package/src/components/Api/Api_Builder.res.mjs +0 -33
  37. package/src/components/Api/Api_Operations.res +0 -13
  38. package/src/components/Api/Api_Operations.res.mjs +0 -12
  39. package/src/components/Api/GraphQL_PushPlanner.res +0 -65
  40. package/src/components/Api/GraphQL_PushPlanner.res.mjs +0 -46
  41. package/tests/admin/Platform_ApiFragmentsApiTest.res +0 -66
  42. package/tests/admin/Platform_ApiFragmentsApiTest.res.mjs +0 -68
  43. package/tests/api/GraphQL_PushPlannerTest.res +0 -62
  44. package/tests/api/GraphQL_PushPlannerTest.res.mjs +0 -80
@@ -3,7 +3,6 @@
3
3
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
- import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
7
6
  import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
8
7
 
9
8
  let log = Logger$ReventlessCore.fromEnv();
@@ -226,88 +225,6 @@ function stitchStandalone(fragment) {
226
225
  return assembleSdl([fragment], false);
227
226
  }
228
227
 
229
- function isIdentStart(line) {
230
- let c = line.codePointAt(0);
231
- if (c !== undefined) {
232
- if (c >= 65 && c <= 90 || c >= 97 && c <= 122) {
233
- return true;
234
- } else {
235
- return c === 95;
236
- }
237
- } else {
238
- return false;
239
- }
240
- }
241
-
242
- function rootTypeFieldNames(sdl, typeName) {
243
- let marker = `type ` + typeName;
244
- let typeStart = Stdlib_String.indexOfOpt(sdl, marker);
245
- if (typeStart === undefined) {
246
- return [];
247
- }
248
- let rest = sdl.slice(typeStart);
249
- let match = Stdlib_String.indexOfOpt(rest, "{");
250
- let match$1 = Stdlib_String.indexOfOpt(rest, "}");
251
- if (match !== undefined) {
252
- if (match$1 !== undefined) {
253
- if (match$1 > match) {
254
- return Stdlib_Array.filterMap(rest.slice(match + 1 | 0, match$1).split("\n"), line => {
255
- let trimmed = line.trim();
256
- if (!(trimmed.length > 0 && isIdentStart(trimmed))) {
257
- return;
258
- }
259
- let name = extractLeadingName(trimmed);
260
- let name$1 = name.endsWith(":") ? name.slice(0, name.length - 1 | 0) : name;
261
- if (name$1.length > 0) {
262
- return name$1;
263
- }
264
- });
265
- } else {
266
- return [];
267
- }
268
- } else {
269
- return [];
270
- }
271
- } else {
272
- return [];
273
- }
274
- }
275
-
276
- function countRootTypeFields(sdl, typeName) {
277
- return rootTypeFieldNames(sdl, typeName).length;
278
- }
279
-
280
- let rootTypeNames = [
281
- "Mutation",
282
- "Query",
283
- "Subscription"
284
- ];
285
-
286
- function allRootFieldNames(sdl) {
287
- return rootTypeNames.flatMap(typeName => rootTypeFieldNames(sdl, typeName));
288
- }
289
-
290
- function missingRootFields(expectedSdl, liveSdl) {
291
- let liveSet = new Set(allRootFieldNames(liveSdl));
292
- return allRootFieldNames(expectedSdl).filter(name => !liveSet.has(name));
293
- }
294
-
295
- function isCatastrophicSchemaShrink(currentSdl, newSdl, threshold) {
296
- let currentNames = allRootFieldNames(currentSdl);
297
- let newSet = new Set(allRootFieldNames(newSdl));
298
- let dropped = currentNames.filter(name => !newSet.has(name));
299
- if (dropped.length === 0) {
300
- return false;
301
- }
302
- let current = currentNames.length;
303
- let next = allRootFieldNames(newSdl).length;
304
- if (current > 0) {
305
- return next < current * threshold;
306
- } else {
307
- return false;
308
- }
309
- }
310
-
311
228
  export {
312
229
  log,
313
230
  encodeSubscriptionSource,
@@ -320,12 +237,5 @@ export {
320
237
  assembleSdl,
321
238
  stitch,
322
239
  stitchStandalone,
323
- isIdentStart,
324
- rootTypeFieldNames,
325
- countRootTypeFields,
326
- rootTypeNames,
327
- allRootFieldNames,
328
- missingRootFields,
329
- isCatastrophicSchemaShrink,
330
240
  }
331
241
  /* log Not a pure module */
@@ -35,8 +35,15 @@ type dcbResult = {
35
35
  dcbPublishJsons: option<Pulumi.Output.t<CommandTopic.publishJsons>>,
36
36
  // SQS URL of the DCB command topic — shared by all StateChangeSlices in this
37
37
  // plugin. Surfaced so the bundled Plugin EventCollector Lambda can dispatch
38
- // PublishStateChangeSliceCommand actions from user extensions to the right queue.
39
- dcbCommandTopicQueueUrl: option<Pulumi.Output.t<string>>,
38
+ // PublishStateChangeSliceCommand actions from user extensions to the right
39
+ // queue. Resolves to "" when the plugin has no DCB slices — deliberately NOT
40
+ // option<Pulumi.Output.t<string>> (that combination is a repo-wide
41
+ // anti-pattern; it minted the nested-option sentinel that broke the
42
+ // collector's SendMessage grant). The Output is gated on the topic's
43
+ // `Component.operations`, so it resolves only after the topic finished
44
+ // constructing — see the construction site and
45
+ // docs/analysis/ec-publish-to-aggregates-grant-broken.md.
46
+ dcbCommandTopicQueueUrl: Pulumi.Output.t<string>,
40
47
  mutationEntries: array<ReventlessInfra.Api.mutationSchemaEntry>,
41
48
  queryEntries: array<ReventlessInfra.Api.querySchemaEntry>,
42
49
  eventLogEntries: array<ReventlessInfra.Api.eventLogSchemaEntry>,
@@ -51,7 +58,7 @@ let emptyResult: dcbResult = {
51
58
  inboundTranslationSlicesOutputs: Dict.make(),
52
59
  dcbRuntimeSetup: None,
53
60
  dcbPublishJsons: None,
54
- dcbCommandTopicQueueUrl: None,
61
+ dcbCommandTopicQueueUrl: Pulumi.Output.make(""),
55
62
  mutationEntries: [],
56
63
  queryEntries: [],
57
64
  eventLogEntries: [],
@@ -956,24 +963,34 @@ module Make = (
956
963
  Sc.Spec.eventSchema->S.castToUnknown
957
964
  )
958
965
 
959
- // NB: don't use `Option.map(r => r.id)` here — Stdlib_Option.map has a
960
- // generic `'a => 'b` signature, so it wraps the lambda's return value
961
- // via Caml_option.some(...). The static type of r.id is
962
- // Pulumi.Output.t<string>, but a Pulumi Output is a JS object whose
963
- // .id getter can resolve to undefined during early CustomResource
964
- // construction; Caml_option.some(undefined) returns the nested-
965
- // option sentinel `{BS_PRIVATE_NESTED_SOME_NONE: 0}`, which then
966
- // flows through publishToAggregates into Lambda env vars and
967
- // Pulumi's type checker rejects it (expected string, got object).
968
- // The explicit switch keeps r.id's static type (non-option), so the
969
- // `Some(r.id)` construction is identity-compiled — no wrapping.
970
- let dcbCommandTopicQueueUrl = {
971
- let outputs: CommandTopic.outputs = dcbCommandTopic->Component.outputs->Obj.magic
972
- switch outputs.resources->Array.get(0) {
973
- | Some(r) => Some(r.id)
974
- | None => None
975
- }
976
- }
966
+ // Derive the DCB command topic's queue URL *after* the component's
967
+ // construct has stored its real resources. This reads `Component.outputs`
968
+ // as a synchronous side channel inside a callback gated on a DIFFERENT
969
+ // output (`Component.operations`, which resolves only post-construct) —
970
+ // reading `resources[0].id` at build time instead captured the SQS
971
+ // Queue's not-yet-resolved `.id`, which degenerated to the
972
+ // `{BS_PRIVATE_NESTED_SOME_NONE: 0}` nested-option sentinel and silently
973
+ // broke the Plugin EventCollector's sqs:SendMessage grant (cross-plugin
974
+ // extension → DCB-slice publishes → IAM AccessDenied).
975
+ //
976
+ // CONSUMER CONSTRAINT: because the value comes from that gated
977
+ // side-channel read (not from the `operations` payload itself), it must
978
+ // be resolved with a direct `.apply`, NOT batched through
979
+ // `Pulumi.Output.all`/`all2` — `all` invokes the callback at a different
980
+ // point in the graph where the side-channel read still yields the
981
+ // sentinel (verified on both preview and up). The EC grant builder
982
+ // therefore resolves it per-target. See
983
+ // docs/analysis/ec-publish-to-aggregates-grant-broken.md.
984
+ let dcbCommandTopicQueueUrl =
985
+ dcbCommandTopic
986
+ ->Component.operations
987
+ ->Pulumi.Output.flatMap(_ops => {
988
+ let outputs: CommandTopic.outputs = dcbCommandTopic->Component.outputs->Obj.magic
989
+ switch outputs.resources->Array.get(0) {
990
+ | Some(r) => r.id
991
+ | None => Pulumi.Output.make("")
992
+ }
993
+ })
977
994
  {
978
995
  dcbEventLogOutputs: Some(dcbEventLog->Component.outputs),
979
996
  stateChangeSlicesOutputs: Dict.fromArray(
@@ -4,6 +4,7 @@ import * as S from "sury/src/S.res.mjs";
4
4
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
6
  import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
7
+ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
7
8
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
9
  import * as Effect from "effect/Effect";
9
10
  import * as Pulumi from "@pulumi/pulumi";
@@ -55,6 +56,8 @@ let emptyResult_outboundTranslationSlicesOutputs = {};
55
56
 
56
57
  let emptyResult_inboundTranslationSlicesOutputs = {};
57
58
 
59
+ let emptyResult_dcbCommandTopicQueueUrl = Pulumi.output("");
60
+
58
61
  let emptyResult_mutationEntries = [];
59
62
 
60
63
  let emptyResult_queryEntries = [];
@@ -70,7 +73,7 @@ let emptyResult = {
70
73
  inboundTranslationSlicesOutputs: emptyResult_inboundTranslationSlicesOutputs,
71
74
  dcbRuntimeSetup: undefined,
72
75
  dcbPublishJsons: undefined,
73
- dcbCommandTopicQueueUrl: undefined,
76
+ dcbCommandTopicQueueUrl: emptyResult_dcbCommandTopicQueueUrl,
74
77
  mutationEntries: emptyResult_mutationEntries,
75
78
  queryEntries: emptyResult_queryEntries,
76
79
  eventLogEntries: emptyResult_eventLogEntries
@@ -544,9 +547,15 @@ function Make(DcbEventLogStorage) {
544
547
  };
545
548
  });
546
549
  let allProducedSchemas = stateChangeSlices.map(Sc => Sc.Spec.eventSchema);
547
- let outputs = Component$ReventlessCore.outputs(dcbCommandTopic);
548
- let r = outputs.resources[0];
549
- let dcbCommandTopicQueueUrl = r !== undefined ? r.id : undefined;
550
+ let dcbCommandTopicQueueUrl = Output$Pulumi.flatMap(Component$ReventlessCore.operations(dcbCommandTopic), _ops => {
551
+ let outputs = Component$ReventlessCore.outputs(dcbCommandTopic);
552
+ let r = outputs.resources[0];
553
+ if (r !== undefined) {
554
+ return r.id;
555
+ } else {
556
+ return Pulumi.output("");
557
+ }
558
+ });
550
559
  return {
551
560
  dcbEventLogOutputs: Component$ReventlessCore.outputs(dcbEventLog),
552
561
  stateChangeSlicesOutputs: Object.fromEntries(Object.entries(stateChangeSlicesOutputs).concat(Object.entries(asyncStateChangeSlicesOutputs))),
@@ -39,7 +39,7 @@ module Make = (
39
39
  _ => {
40
40
  let action = Projection.rewriteAction(action, ReadModelSpec.stateSchema)
41
41
  Effect.promise(
42
- () => Projection.handleAction(action, Spec.operations, ReadModelSpec.subIdConfig),
42
+ () => Projection.handleAction(~comp, action, Spec.operations, ReadModelSpec.subIdConfig),
43
43
  )->Effect.map(_ => ())
44
44
  },
45
45
  )
@@ -31,7 +31,7 @@ function Make(ReadModelSpec) {
31
31
  return actions;
32
32
  }).flat(), Effect.succeed(), (acc, action) => Effect.flatMap(acc, () => {
33
33
  let action$1 = Projection$ReventlessCore.rewriteAction(action, ReadModelSpec.stateSchema);
34
- return Effect.map(Effect.promise(() => Projection$ReventlessCore.handleAction(action$1, Spec.operations, ReadModelSpec.subIdConfig)), param => {});
34
+ return Effect.map(Effect.promise(() => Projection$ReventlessCore.handleAction(comp, action$1, Spec.operations, ReadModelSpec.subIdConfig)), param => {});
35
35
  }));
36
36
  });
37
37
  return {
@@ -130,7 +130,7 @@ module Make = (
130
130
  ->Array.reduce(Effect.succeed(), (acc, action) =>
131
131
  acc->Effect.flatMap(_ =>
132
132
  Effect.promise(() =>
133
- FrameworkProjection.handleAction(action, projectionOps, Spec.subIdConfig)
133
+ FrameworkProjection.handleAction(~comp, action, projectionOps, Spec.subIdConfig)
134
134
  )->Effect.map(_ => ())
135
135
  )
136
136
  )
@@ -94,7 +94,7 @@ function Make(RuntimeEnvironment) {
94
94
  let fieldsStr = f === "" ? "" : `({` + f + `})`;
95
95
  Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, dataDict, `handling event ` + idxStr + `/` + total + `: ` + LogFormat$ReventlessCore.bold(eventType) + fieldsStr + ` ` + actionsStr));
96
96
  return actions;
97
- }).flat(), Effect.succeed(), (acc, action) => Effect.flatMap(acc, () => Effect.map(Effect.promise(() => Projection$ReventlessCore.handleAction(action, projectionOps, Spec.subIdConfig)), param => {})));
97
+ }).flat(), Effect.succeed(), (acc, action) => Effect.flatMap(acc, () => Effect.map(Effect.promise(() => Projection$ReventlessCore.handleAction(comp, action, projectionOps, Spec.subIdConfig)), param => {})));
98
98
  });
99
99
  let handler = SpecificEventCollector.makeHandler(ec, jsonEventsHandler);
100
100
  let resources = Component$ReventlessCore.outputs(queryDb).resources;
@@ -59,6 +59,6 @@ module Make = (
59
59
  `skipped=${skipped->Int.toString} events (decode mismatch)`,
60
60
  )->Effect.runSync
61
61
  }
62
- await FrameworkProjection.handleActions(allActions, queryDbOps, Spec.subIdConfig)
62
+ await FrameworkProjection.handleActions(~comp, allActions, queryDbOps, Spec.subIdConfig)
63
63
  }
64
64
  }
@@ -38,7 +38,7 @@ function Make(Spec) {
38
38
  if (skipped > 0) {
39
39
  Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `skipped=` + skipped.toString() + ` events (decode mismatch)`));
40
40
  }
41
- return await Projection$ReventlessCore.handleActions(allActions, queryDbOps, Spec.subIdConfig);
41
+ return await Projection$ReventlessCore.handleActions(comp, allActions, queryDbOps, Spec.subIdConfig);
42
42
  };
43
43
  return {
44
44
  Spec: Spec,
@@ -776,13 +776,11 @@ module Make = (
776
776
  // All DCB StateChangeSlices share the same dcb command topic — map
777
777
  // every slice name to that single URL so PublishStateChangeSliceCommand
778
778
  // routes correctly regardless of which slice the extension targets.
779
- switch dcbResult.dcbCommandTopicQueueUrl {
780
- | Some(url) =>
781
- stateChangeSlices->Array.forEach((module(Sc: StateChangeSlice.T)) =>
782
- aggregateQueueUrls->Dict.set(Sc.Spec.name, url)
783
- )
784
- | None => ()
785
- }
779
+ // (No-op when the plugin has no DCB slices: the list is empty and the
780
+ // URL Output resolves to "".)
781
+ stateChangeSlices->Array.forEach((module(Sc: StateChangeSlice.T)) =>
782
+ aggregateQueueUrls->Dict.set(Sc.Spec.name, dcbResult.dcbCommandTopicQueueUrl)
783
+ )
786
784
 
787
785
  // Per-RM EventCollector SQS URL — populated only for plugins that
788
786
  // expose RMs the bundled handler may enqueue events into directly.
@@ -510,12 +510,9 @@ function Make(Spec) {
510
510
  return;
511
511
  }
512
512
  });
513
- let url = dcbResult.dcbCommandTopicQueueUrl;
514
- if (url !== undefined) {
515
- stateChangeSlices.forEach(Sc => {
516
- aggregateQueueUrls[Sc.Spec.name] = url;
517
- });
518
- }
513
+ stateChangeSlices.forEach(Sc => {
514
+ aggregateQueueUrls[Sc.Spec.name] = dcbResult.dcbCommandTopicQueueUrl;
515
+ });
519
516
  let readModelQueueUrls = {};
520
517
  Object.entries(readModelsOutputs).forEach(param => {
521
518
  let urlOutput = Output$Pulumi.flatMap(param[1].eventCollector, ecOutputs => {
@@ -68,8 +68,10 @@ external _withMinimumLogLevel: (Effect.t<'a, 'e, 'r>, effectLogLevel) => Effect.
68
68
 
69
69
  // Minimum level used when LOG_LEVEL is unset. Framework default is Info; a platform
70
70
  // may lower it (e.g. the in-memory dev platform sets Debug) via setDefaultMinLevel.
71
- let _defaultMin = ref(Logger.Info)
72
- let setDefaultMinLevel = (l: Logger.level) => _defaultMin := l
71
+ // The ref lives in Logger (single source of truth) so both the record logger
72
+ // (Logger.fromEnv) and this Effect logger share one platform default; re-exported
73
+ // here so existing `EffectLogger.setDefaultMinLevel` callers keep working.
74
+ let setDefaultMinLevel = Logger.setDefaultMinLevel
73
75
 
74
76
  // Resolve the active minimum: explicit LOG_LEVEL wins, else the platform default.
75
77
  let _effectMin = (): effectLogLevel =>
@@ -80,7 +82,7 @@ let _effectMin = (): effectLogLevel =>
80
82
  | Some("warn") => _llWarning
81
83
  | Some("error") => _llError
82
84
  | _ =>
83
- switch _defaultMin.contents {
85
+ switch Logger.defaultMinLevel.contents {
84
86
  | Logger.Debug => _llDebug
85
87
  | Logger.Info => _llInfo
86
88
  | Logger.Warn => _llWarning
@@ -26,14 +26,6 @@ function _messageToString(msg) {
26
26
  }
27
27
  }
28
28
 
29
- let _defaultMin = {
30
- contents: "Info"
31
- };
32
-
33
- function setDefaultMinLevel(l) {
34
- _defaultMin.contents = l;
35
- }
36
-
37
29
  function _effectMin() {
38
30
  let match = process.env.LOG_LEVEL;
39
31
  if (match !== undefined) {
@@ -50,7 +42,7 @@ function _effectMin() {
50
42
  return LogLevel.Warning;
51
43
  }
52
44
  }
53
- let match$1 = _defaultMin.contents;
45
+ let match$1 = Logger$ReventlessCore.defaultMinLevel.contents;
54
46
  switch (match$1) {
55
47
  case "Debug" :
56
48
  return LogLevel.Debug;
@@ -155,9 +147,10 @@ function logDebug(comp, detail, msg) {
155
147
  return Logger.withMinimumLogLevel(_annotate(Effect.logDebug(msg), comp, detail), _effectMin());
156
148
  }
157
149
 
150
+ let setDefaultMinLevel = Logger$ReventlessCore.setDefaultMinLevel;
151
+
158
152
  export {
159
153
  _messageToString,
160
- _defaultMin,
161
154
  setDefaultMinLevel,
162
155
  _effectMin,
163
156
  ordinalToLevel,
@@ -165,8 +165,18 @@ let emit = (
165
165
 
166
166
  // ─── Logger.t (record-based API) ─────────────────────────────────────────────
167
167
 
168
- let makeLogger = (~minLevel=Info): t => {
169
- let shouldLog = l => levelToInt(l) >= levelToInt(minLevel)
168
+ // Platform-tunable default minimum level, used when LOG_LEVEL is unset. Lives
169
+ // here (not in EffectLogger) so it is the single source of truth for BOTH logger
170
+ // paths — the record logger below and EffectLogger, which re-exports
171
+ // `setDefaultMinLevel`. A platform may lower it (e.g. reventless-local sets
172
+ // Debug); the framework default is Info.
173
+ let defaultMinLevel = ref(Info)
174
+ let setDefaultMinLevel = (l: level) => defaultMinLevel := l
175
+
176
+ // Build a logger from a per-call predicate. `shouldLog` is evaluated on every
177
+ // emit, so a logger created at module-init time still reflects a platform
178
+ // default set later (and a live LOG_LEVEL) instead of baking the level in once.
179
+ let makeLoggerWith = (shouldLog: level => bool): t => {
170
180
  let log = (level, ~comp=?, ~data=?, msg) =>
171
181
  if shouldLog(level) {
172
182
  let dataStr = data->Option.map(d => ` ${d->JSON.stringify}`)->Option.getOr("")
@@ -184,6 +194,11 @@ let makeLogger = (~minLevel=Info): t => {
184
194
  }
185
195
  }
186
196
 
197
+ // Fixed-level logger — the level is baked at creation. Used by tests that assert
198
+ // a specific minimum regardless of env/platform default.
199
+ let makeLogger = (~minLevel=Info): t =>
200
+ makeLoggerWith(l => levelToInt(l) >= levelToInt(minLevel))
201
+
187
202
  let silent: t = {
188
203
  debug: (~comp as _=?, ~data as _=?, _) => (),
189
204
  info: (~comp as _=?, ~data as _=?, _) => (),
@@ -192,15 +207,30 @@ let silent: t = {
192
207
  debugLazy: (~comp as _=?, _) => (),
193
208
  }
194
209
 
195
- // Reads LOG_LEVEL from process.env and returns a configured logger.
196
- // Call once at module top-level or startup — not inside hot loops.
210
+ // Active minimum level: explicit LOG_LEVEL wins, else the platform default.
211
+ // None ⇒ silent (suppress everything). Re-read per call — LOG_LEVEL via the
212
+ // process.env external, the platform default via `defaultMinLevel`.
213
+ let effectiveMin = (): option<level> =>
214
+ switch _logLevel {
215
+ | Some("silent") => None
216
+ | Some("debug") => Some(Debug)
217
+ | Some("info") => Some(Info)
218
+ | Some("warn") => Some(Warn)
219
+ | Some("error") => Some(Error)
220
+ | Some(_) => Some(Info) // unrecognized value ⇒ Info (unchanged behavior)
221
+ | None => Some(defaultMinLevel.contents)
222
+ }
223
+
224
+ // Reads LOG_LEVEL from process.env on every call and honors the platform default
225
+ // when unset. Safe to call at module top-level: the returned logger evaluates the
226
+ // level lazily, so a platform lowering the default afterwards (e.g.
227
+ // reventless-local → Debug) takes effect for loggers already constructed.
197
228
  // LOG_LEVEL=silent disables all output (used by the reventless-gwt CLI so
198
229
  // framework diagnostics don't interleave with NDJSON / TAP / JUnit streams).
199
230
  let fromEnv = (): t =>
200
- switch _logLevel {
201
- | Some("silent") => silent
202
- | Some("debug") => makeLogger(~minLevel=Debug)
203
- | Some("warn") => makeLogger(~minLevel=Warn)
204
- | Some("error") => makeLogger(~minLevel=Error)
205
- | _ => makeLogger(~minLevel=Info)
206
- }
231
+ makeLoggerWith(l =>
232
+ switch effectiveMin() {
233
+ | None => false
234
+ | Some(min) => levelToInt(l) >= levelToInt(min)
235
+ }
236
+ )
@@ -142,9 +142,15 @@ function emit(level, comp, plugin, detail, annotations, msg) {
142
142
  }
143
143
  }
144
144
 
145
- function makeLogger(minLevelOpt) {
146
- let minLevel = minLevelOpt !== undefined ? minLevelOpt : "Info";
147
- let shouldLog = l => levelToInt(l) >= levelToInt(minLevel);
145
+ let defaultMinLevel = {
146
+ contents: "Info"
147
+ };
148
+
149
+ function setDefaultMinLevel(l) {
150
+ defaultMinLevel.contents = l;
151
+ }
152
+
153
+ function makeLoggerWith(shouldLog) {
148
154
  let log = (level, comp, data, msg) => {
149
155
  if (!shouldLog(level)) {
150
156
  return;
@@ -165,6 +171,11 @@ function makeLogger(minLevelOpt) {
165
171
  };
166
172
  }
167
173
 
174
+ function makeLogger(minLevelOpt) {
175
+ let minLevel = minLevelOpt !== undefined ? minLevelOpt : "Info";
176
+ return makeLoggerWith(l => levelToInt(l) >= levelToInt(minLevel));
177
+ }
178
+
168
179
  function silent_debug(param, param$1, param$2) {
169
180
 
170
181
  }
@@ -193,25 +204,36 @@ let silent = {
193
204
  debugLazy: silent_debugLazy
194
205
  };
195
206
 
196
- function fromEnv() {
207
+ function effectiveMin() {
197
208
  let match = process.env.LOG_LEVEL;
198
209
  if (match === undefined) {
199
- return makeLogger("Info");
210
+ return defaultMinLevel.contents;
200
211
  }
201
212
  switch (match) {
202
213
  case "debug" :
203
- return makeLogger("Debug");
214
+ return "Debug";
204
215
  case "error" :
205
- return makeLogger("Error");
216
+ return "Error";
206
217
  case "silent" :
207
- return silent;
218
+ return;
208
219
  case "warn" :
209
- return makeLogger("Warn");
220
+ return "Warn";
210
221
  default:
211
- return makeLogger("Info");
222
+ return "Info";
212
223
  }
213
224
  }
214
225
 
226
+ function fromEnv() {
227
+ return makeLoggerWith(l => {
228
+ let min = effectiveMin();
229
+ if (min !== undefined) {
230
+ return levelToInt(l) >= levelToInt(min);
231
+ } else {
232
+ return false;
233
+ }
234
+ });
235
+ }
236
+
215
237
  let currentPluginName = LogPrefix$Reventless.currentPluginName;
216
238
 
217
239
  let registerComponentPlugin = LogPrefix$Reventless.registerComponentPlugin;
@@ -241,8 +263,12 @@ export {
241
263
  fmtPlainPrefix,
242
264
  resolvePlugin,
243
265
  emit,
266
+ defaultMinLevel,
267
+ setDefaultMinLevel,
268
+ makeLoggerWith,
244
269
  makeLogger,
245
270
  silent,
271
+ effectiveMin,
246
272
  fromEnv,
247
273
  }
248
274
  /* s Not a pure module */