@reventlessdev/reventless-core 3.0.0-alpha.173 → 3.0.0-alpha.175

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 (81) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +5 -5
  3. package/src/ComponentType.res +8 -1
  4. package/src/ComponentType.res.mjs +6 -0
  5. package/src/RequestContext.res +45 -1
  6. package/src/RequestContext.res.mjs +12 -2
  7. package/src/ResourceAttribution.res +133 -0
  8. package/src/ResourceAttribution.res.mjs +96 -0
  9. package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +24 -5
  10. package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res.mjs +9 -3
  11. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res +6 -0
  12. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +3 -3
  13. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res +6 -1
  14. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +3 -1
  15. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +25 -7
  16. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +11 -8
  17. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res +2 -0
  18. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
  19. package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +4 -0
  20. package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res.mjs +2 -2
  21. package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +4 -0
  22. package/src/adapter/Runtime/PluginRuntime_Builder_Single.res.mjs +2 -2
  23. package/src/adapter/Runtime/Runtime.res +77 -13
  24. package/src/adapter/Runtime/Runtime.res.mjs +24 -8
  25. package/src/admin/Platform_Admin.res +1 -1
  26. package/src/admin/Platform_Admin.res.mjs +8 -2
  27. package/src/components/Aggregate/Aggregate_Builder.res +10 -2
  28. package/src/components/Aggregate/Aggregate_Builder.res.mjs +8 -2
  29. package/src/components/AutomationSlice/AutomationSlice_Builder.res +12 -2
  30. package/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs +8 -2
  31. package/src/components/CommandTopic/CommandTopic.res +5 -1
  32. package/src/components/CommandTopic/CommandTopic_Adapter.res +1 -0
  33. package/src/components/CommandTopic/CommandTopic_Builder.res +4 -4
  34. package/src/components/CommandTopic/CommandTopic_Builder.res.mjs +29 -30
  35. package/src/components/Counter/Counter_Builder.res +7 -1
  36. package/src/components/Counter/Counter_Builder.res.mjs +5 -2
  37. package/src/components/Dcb/Dcb_Builder.res +2 -2
  38. package/src/components/Dcb/Dcb_Builder.res.mjs +8 -2
  39. package/src/components/DcbEventLog/DcbEventLog_Builder.res +3 -0
  40. package/src/components/DcbEventLog/DcbEventLog_Builder.res.mjs +4 -1
  41. package/src/components/EventCollector/EventCollector.res +1 -0
  42. package/src/components/EventCollector/EventCollector_Adapter.res +1 -0
  43. package/src/components/EventCollector/EventCollector_Builder.res +4 -4
  44. package/src/components/EventCollector/EventCollector_Builder.res.mjs +2 -2
  45. package/src/components/EventLog/EventLog.res +5 -1
  46. package/src/components/EventLog/EventLog_Adapter.res +5 -1
  47. package/src/components/EventLog/EventLog_Builder.res +9 -4
  48. package/src/components/EventLog/EventLog_Builder.res.mjs +7 -8
  49. package/src/components/EventMapper/EventMapper_Builder.res +6 -1
  50. package/src/components/EventMapper/EventMapper_Builder.res.mjs +4 -1
  51. package/src/components/EventTopic/EventTopic.res +1 -0
  52. package/src/components/EventTopic/EventTopic_Adapter.res +1 -0
  53. package/src/components/EventTopic/EventTopic_Builder.res +4 -3
  54. package/src/components/EventTopic/EventTopic_Builder.res.mjs +2 -2
  55. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +11 -2
  56. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res.mjs +8 -2
  57. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +6 -1
  58. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res.mjs +4 -1
  59. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +12 -2
  60. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res.mjs +8 -2
  61. package/src/components/QueryDb/QueryDb.res +1 -0
  62. package/src/components/QueryDb/QueryDb_Adapter.res +1 -0
  63. package/src/components/QueryDb/QueryDb_Builder.res +4 -3
  64. package/src/components/QueryDb/QueryDb_Builder.res.mjs +2 -2
  65. package/src/components/ReadModel/ReadModel_Builder.res +12 -2
  66. package/src/components/ReadModel/ReadModel_Builder.res.mjs +8 -2
  67. package/src/components/SideEffectHandler/SideEffectHandler_Builder.res +6 -1
  68. package/src/components/SideEffectHandler/SideEffectHandler_Builder.res.mjs +4 -1
  69. package/src/components/StateViewSlice/StateViewSlice_Builder.res +12 -2
  70. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +8 -2
  71. package/src/plugin/component/Plugin_Builder.res +6 -0
  72. package/src/plugin/component/Plugin_Builder.res.mjs +3 -0
  73. package/src/plugin/component/Plugin_Helpers.res +1 -1
  74. package/src/plugin/component/Plugin_Helpers.res.mjs +4 -1
  75. package/tests/ResourceAttributionTest.res +70 -0
  76. package/tests/aggregate/AggregateCacheTest.res +1 -1
  77. package/tests/aggregate/AggregateConflictTest.res +1 -1
  78. package/tests/aggregate/AggregateFixtures.res +1 -1
  79. package/tests/aggregate/AggregateSnapshotTest.res +1 -1
  80. package/tests/eventlog/EventLogFixtures.res +1 -1
  81. package/tests/logger/LogFormatTest.res +43 -0
@@ -58,7 +58,12 @@ module Make = (
58
58
  let timeout = ReventlessInfra.RuntimeHints.resolveTimeout(runtime, ~default=30)
59
59
 
60
60
  module SpecificQueryDb = QueryDb_Builder.Make(Spec, QueryDbStorage, QueryDbResolvers)
61
- let queryDb = SpecificQueryDb.make(~api, ~apiRole, ~opts)
61
+ let queryDb = SpecificQueryDb.make(
62
+ ~api,
63
+ ~apiRole,
64
+ ~owner={kind: ComponentType.ReadModel, name: Spec.name},
65
+ ~opts,
66
+ )
62
67
 
63
68
  let toProjectionOperations: SpecificQueryDb.operations => projectionOperations = ({
64
69
  load,
@@ -114,7 +119,12 @@ module Make = (
114
119
  ->Component.operations
115
120
  ->Pulumi.Output.apply(operations => {
116
121
  let eventTopics = allEventTopics->EventTopic.filter(sourceNames)
117
- let eventCollector = SpecificEventCollector.make(~name, ~eventTopics, ~opts)
122
+ let eventCollector = SpecificEventCollector.make(
123
+ ~name,
124
+ ~eventTopics,
125
+ ~owner={kind: ComponentType.ReadModel, name: Spec.name},
126
+ ~opts,
127
+ )
118
128
 
119
129
  module Callback = ReadModel_Callback.Make(
120
130
  Spec,
@@ -27,7 +27,10 @@ function Make(Spec) {
27
27
  let memorySize = RuntimeHints$ReventlessInfra.resolveMemory(runtime, 1024);
28
28
  let timeout = RuntimeHints$ReventlessInfra.resolveTimeout(runtime, 30);
29
29
  let SpecificQueryDb = QueryDb_Builder$ReventlessCore.Make(Spec)(QueryDbStorage)(QueryDbResolvers);
30
- let queryDb = SpecificQueryDb.make(api, apiRole, undefined, opts);
30
+ let queryDb = SpecificQueryDb.make(api, apiRole, undefined, {
31
+ kind: "ReadModel",
32
+ name: Spec.name
33
+ }, opts);
31
34
  let toProjectionOperations = param => {
32
35
  let deleteBatch = param.deleteBatch;
33
36
  let $$delete = param.delete;
@@ -64,7 +67,10 @@ function Make(Spec) {
64
67
  let SpecificEventCollector = EventCollector_Builder$ReventlessCore.Make(RuntimeEnvironment)(EventCollectorChannel);
65
68
  let eventCollector = Component$ReventlessCore.operations(queryDb).apply(operations => {
66
69
  let eventTopics = EventTopic$ReventlessCore.filter(allEventTopics, sourceNames);
67
- let eventCollector = SpecificEventCollector.make(name, eventTopics, opts);
70
+ let eventCollector = SpecificEventCollector.make(name, eventTopics, {
71
+ kind: "ReadModel",
72
+ name: Spec.name
73
+ }, opts);
68
74
  let operations$1 = toProjectionOperations(operations);
69
75
  let Callback = ReadModel_Callback$ReventlessCore.Make(Spec)(Mappings)({
70
76
  ReadModelSpec: Spec,
@@ -27,7 +27,12 @@ module Make = (
27
27
  ->Belt.Set.String.fromArray
28
28
 
29
29
  let eventTopics = allEventTopics->EventTopic.filter(aggregateNames)
30
- let eventCollector = SpecificEventCollector.make(~name, ~eventTopics, ~opts)
30
+ let eventCollector = SpecificEventCollector.make(
31
+ ~name,
32
+ ~eventTopics,
33
+ ~owner={kind: ComponentType.SideEffectHandler, name},
34
+ ~opts,
35
+ )
31
36
 
32
37
  module Callback = SideEffectHandler_Callback.Make({
33
38
  let sideEffects = sideEffects
@@ -24,7 +24,10 @@ function Make(RuntimeEnvironment) {
24
24
  };
25
25
  let aggregateNames = Belt_SetString.fromArray(sideEffects.map(SideEffect => SideEffect.Source.name));
26
26
  let eventTopics = EventTopic$ReventlessCore.filter(allEventTopics, aggregateNames);
27
- let eventCollector = SpecificEventCollector.make(extra$1, eventTopics, opts);
27
+ let eventCollector = SpecificEventCollector.make(extra$1, eventTopics, {
28
+ kind: "SideEffectHandler",
29
+ name: extra$1
30
+ }, opts);
28
31
  let Callback = SideEffectHandler_Callback$ReventlessCore.Make({
29
32
  sideEffects: sideEffects,
30
33
  queryEngine: queryEngine
@@ -63,7 +63,12 @@ module Make = (
63
63
  let memorySize = ReventlessInfra.RuntimeHints.resolveMemory(runtime, ~default=1024)
64
64
  let timeout = ReventlessInfra.RuntimeHints.resolveTimeout(runtime, ~default=30)
65
65
 
66
- let queryDb = SpecificQueryDb.make(~api=Api.api(), ~apiRole=Api.apiRole(), ~opts)
66
+ let queryDb = SpecificQueryDb.make(
67
+ ~api=Api.api(),
68
+ ~apiRole=Api.apiRole(),
69
+ ~owner={kind: ComponentType.StateViewSlice, name: Spec.name},
70
+ ~opts,
71
+ )
67
72
 
68
73
  let dcbEventTopicOutputs: EventTopic.outputs = (dcbEventLog->Component.outputs).eventTopic
69
74
  let allEventTopics = Dict.fromArray([(Spec.name, dcbEventTopicOutputs)])
@@ -74,7 +79,12 @@ module Make = (
74
79
  ->Pulumi.Output.apply(queryDbOps => {
75
80
  let projectionOps = toProjectionOps(queryDbOps)
76
81
 
77
- let ec = SpecificEventCollector.make(~name=Spec.name, ~eventTopics=allEventTopics, ~opts)
82
+ let ec = SpecificEventCollector.make(
83
+ ~name=Spec.name,
84
+ ~eventTopics=allEventTopics,
85
+ ~owner={kind: ComponentType.StateViewSlice, name: Spec.name},
86
+ ~opts,
87
+ )
78
88
 
79
89
  let comp = `StateViewSlice(${Spec.name})`
80
90
 
@@ -61,7 +61,10 @@ function Make(RuntimeEnvironment) {
61
61
  };
62
62
  let memorySize = RuntimeHints$ReventlessInfra.resolveMemory(runtime, 1024);
63
63
  let timeout = RuntimeHints$ReventlessInfra.resolveTimeout(runtime, 30);
64
- let queryDb = SpecificQueryDb.make(Api.api(), Api.apiRole(), undefined, opts);
64
+ let queryDb = SpecificQueryDb.make(Api.api(), Api.apiRole(), undefined, {
65
+ kind: "StateViewSlice",
66
+ name: Spec.name
67
+ }, opts);
65
68
  let dcbEventTopicOutputs = Component$ReventlessCore.outputs(dcbEventLog).eventTopic;
66
69
  let allEventTopics = Object.fromEntries([[
67
70
  Spec.name,
@@ -69,7 +72,10 @@ function Make(RuntimeEnvironment) {
69
72
  ]]);
70
73
  let eventCollector = Component$ReventlessCore.operations(queryDb).apply(queryDbOps => {
71
74
  let projectionOps = toProjectionOps(queryDbOps);
72
- let ec = SpecificEventCollector.make(Spec.name, allEventTopics, opts);
75
+ let ec = SpecificEventCollector.make(Spec.name, allEventTopics, {
76
+ kind: "StateViewSlice",
77
+ name: Spec.name
78
+ }, opts);
73
79
  let comp = `StateViewSlice(` + Spec.name + `)`;
74
80
  let jsonEventsHandler = stream => Effect.flatMap(Stream.runCollect(stream), events => {
75
81
  let total = events.length.toString();
@@ -67,6 +67,11 @@ module Make = (
67
67
  let _prevPluginName = Logger.currentPluginName.contents
68
68
  Logger.currentPluginName := Some(name)
69
69
 
70
+ // Publish the owning plugin/platform for resource attribution, so adapters
71
+ // creating infrastructure below this point can tag it without every adapter
72
+ // signature having to carry the two names. Restored at the end of construct.
73
+ let _prevAttribution = ResourceAttribution.enter(~platform=Spec.platformName, ~plugin=name)
74
+
70
75
  // Register every component → plugin so runtime logs (fired after construct
71
76
  // returns) can resolve a comp like `StateChangeSlice(AddProduct)` to its
72
77
  // owning plugin via `Logger.componentPluginRegistry`. Also register the
@@ -975,6 +980,7 @@ module Make = (
975
980
  )
976
981
 
977
982
  Logger.currentPluginName := _prevPluginName
983
+ ResourceAttribution.restore(_prevAttribution)
978
984
  }
979
985
 
980
986
  let makeAutoUIManifest = (
@@ -31,6 +31,7 @@ import * as AdapterDeploytime$ReventlessCore from "../../adapter/AdapterDeployti
31
31
  import * as ExtensionMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionMapping.res.mjs";
32
32
  import * as ExtensionPoint$ReventlessInterop from "@reventlessdev/reventless-interop/src/components/ExtensionPoint.res.mjs";
33
33
  import * as Heartbeat_Builder$ReventlessCore from "../../components/Heartbeat/Heartbeat_Builder.res.mjs";
34
+ import * as ResourceAttribution$ReventlessCore from "../../ResourceAttribution.res.mjs";
34
35
  import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
35
36
  import * as Plugin_SubscriptionSchema$ReventlessCore from "./Plugin_SubscriptionSchema.res.mjs";
36
37
 
@@ -121,6 +122,7 @@ function Make(Spec) {
121
122
  return Component$ReventlessCore.make(ComponentType$ReventlessCore.toString(Plugin$ReventlessCore.componentType), name, (extra, extra$1) => {
122
123
  let _prevPluginName = Logger$ReventlessCore.currentPluginName.contents;
123
124
  Logger$ReventlessCore.currentPluginName.contents = extra$1;
125
+ let _prevAttribution = ResourceAttribution$ReventlessCore.enter(Spec.platformName, extra$1);
124
126
  Logger$ReventlessCore.registerComponentPlugin(extra$1, extra$1);
125
127
  aggregates.forEach(M => Logger$ReventlessCore.registerComponentPlugin(M.Spec.name, extra$1));
126
128
  readModels.forEach(R => Logger$ReventlessCore.registerComponentPlugin(R.Spec.name, extra$1));
@@ -626,6 +628,7 @@ function Make(Spec) {
626
628
  Component$ReventlessCore.setOutputs(extra, pluginOutputs);
627
629
  Plugin_Helpers$ReventlessCore.interopMetaOutput.contents = builderOutputs.apply(outputs => S.reverseConvertToJsonOrThrow(Plugin_Helpers$ReventlessCore.toInteropMeta(outputs), ExportMeta$ReventlessInterop.schema));
628
630
  Logger$ReventlessCore.currentPluginName.contents = _prevPluginName;
631
+ return ResourceAttribution$ReventlessCore.restore(_prevAttribution);
629
632
  }, opts);
630
633
  };
631
634
  return {
@@ -449,7 +449,7 @@ module MakeEventCollectorHelper = (
449
449
  EventCollectorChannel,
450
450
  )
451
451
  let make = (~name, ~eventTopics, ~opts) => {
452
- let eventCollector = PluginEventCollector.make(~name, ~eventTopics, ~opts)
452
+ let eventCollector = PluginEventCollector.make(~name, ~eventTopics, ~owner={kind: ComponentType.Plugin, name}, ~opts)
453
453
  let eventCollectorOutputs = eventCollector->Component.outputs
454
454
  // pluginDefinition.eventCollector must identify the EC's inbound queue
455
455
  // ARN — admin's `manageSubscriptions` uses it as the subscription target
@@ -255,7 +255,10 @@ function MakeEventCollectorHelper(RuntimeEnvironment) {
255
255
  return EventCollectorChannel => (PluginRuntimeBuilder => {
256
256
  let PluginEventCollector = EventCollector_Builder$ReventlessCore.Make(RuntimeEnvironment)(EventCollectorChannel);
257
257
  let make = (name, eventTopics, opts) => {
258
- let eventCollector = PluginEventCollector.make(name, eventTopics, opts);
258
+ let eventCollector = PluginEventCollector.make(name, eventTopics, {
259
+ kind: "Plugin",
260
+ name: name
261
+ }, opts);
259
262
  let eventCollectorOutputs = Component$ReventlessCore.outputs(eventCollector);
260
263
  let resources = eventCollectorOutputs.resources;
261
264
  let r = resources[resources.length - 1 | 0];
@@ -0,0 +1,70 @@
1
+ open JestGlobals
2
+
3
+ // The attribution vocabulary is the shared contract between the AWS tag renderer
4
+ // and the Kubernetes label renderer. These guard the wire spellings (which land
5
+ // in deployed resource metadata and in downstream inventories) and the ambient
6
+ // context's save/restore discipline, which is what keeps a plugin's resources
7
+ // from being attributed to whichever plugin was constructed before it.
8
+
9
+ module Attribution = ResourceAttribution
10
+
11
+ describe("ResourceAttribution.Scope — wire spellings", () => {
12
+ testSync("scopes render lower-case", () => {
13
+ expect(
14
+ [Attribution.Scope.Component, Plugin, Platform]->Array.map(Attribution.Scope.toString),
15
+ )->toEqual(["component", "plugin", "platform"])
16
+ })
17
+ })
18
+
19
+ describe("ResourceAttribution.Role — wire spellings", () => {
20
+ testSync("piece roles render as their constructor name", () => {
21
+ expect(
22
+ [Attribution.Role.EventLog, QueryDb, Runtime, DeadLetter]->Array.map(
23
+ Attribution.Role.toString,
24
+ ),
25
+ )->toEqual(["EventLog", "QueryDb", "Runtime", "DeadLetter"])
26
+ })
27
+
28
+ testSync("Other carries the caller's name verbatim", () => {
29
+ expect(Attribution.Role.Other("ChangeFeed")->Attribution.Role.toString)->toBe("ChangeFeed")
30
+ })
31
+
32
+ testSync("platform-level things are Platform, not Core", () => {
33
+ // `Core` predates the platform/plugin split and names nothing the framework
34
+ // builds; it also reads as the built-in core PLUGIN, which is a different
35
+ // thing. Platform substrate is attributed to `Platform`, matching
36
+ // `reventless:platform` and `Scope.Platform`.
37
+ expect(ComponentType.Platform->ComponentType.toString)->toBe("Platform")
38
+ expect(ComponentType.ofString("Platform"))->toEqual(Some(ComponentType.Platform))
39
+ })
40
+
41
+ testSync("a role is NOT a component kind — Runtime has no ComponentType twin", () => {
42
+ // The whole point of splitting role from kind: `Runtime` is a deployment
43
+ // piece, not something the domain model can be. If this ever resolves, the
44
+ // two vocabularies have started to merge again.
45
+ expect(ComponentType.ofString("Runtime"))->toEqual(None)
46
+ })
47
+ })
48
+
49
+ describe("ResourceAttribution — ambient plugin context", () => {
50
+ testSync("outside any plugin construct, nothing is attributed", () => {
51
+ expect(Attribution.current.contents.plugin)->toEqual(None)
52
+ expect(Attribution.current.contents.platform)->toEqual(None)
53
+ })
54
+
55
+ testSync("enter publishes the plugin and platform under construction", () => {
56
+ let previous = Attribution.enter(~platform="online-shop", ~plugin="Ordering")
57
+ expect(Attribution.current.contents.plugin)->toEqual(Some("Ordering"))
58
+ expect(Attribution.current.contents.platform)->toEqual(Some("online-shop"))
59
+ Attribution.restore(previous)
60
+ })
61
+
62
+ testSync("restore unwinds to the previous context, so plugins don't leak", () => {
63
+ let outer = Attribution.enter(~platform="online-shop", ~plugin="Catalog")
64
+ let inner = Attribution.enter(~platform="online-shop", ~plugin="Ordering")
65
+ Attribution.restore(inner)
66
+ expect(Attribution.current.contents.plugin)->toEqual(Some("Catalog"))
67
+ Attribution.restore(outer)
68
+ expect(Attribution.current.contents.plugin)->toEqual(None)
69
+ })
70
+ })
@@ -166,7 +166,7 @@ module TestOps = {
166
166
  }
167
167
  type component = Component.t<OuterEventLog.t, OuterEventLog.outputs, operations>
168
168
  // Never called — satisfies module type only
169
- let make = (~name as _: string, ~opts as _=?): component => Obj.magic(0)
169
+ let make = (~name as _: string, ~owner as _=?, ~opts as _=?): component => Obj.magic(0)
170
170
  }
171
171
  let eventLog: EventLog.operations = {
172
172
  append: mock.appendFn,
@@ -126,7 +126,7 @@ module TestOps = {
126
126
  writeSnapshot: (string, EventLog.snapshot) => promise<result<unit, string>>,
127
127
  }
128
128
  type component = Component.t<OuterEventLog.t, OuterEventLog.outputs, operations>
129
- let make = (~name as _: string, ~opts as _=?): component => Obj.magic(0)
129
+ let make = (~name as _: string, ~owner as _=?, ~opts as _=?): component => Obj.magic(0)
130
130
  }
131
131
  let eventLog: EventLog.operations = {
132
132
  append: mock.appendFn,
@@ -142,7 +142,7 @@ module TestOps = {
142
142
  }
143
143
  type component = Component.t<OuterEventLog.t, OuterEventLog.outputs, operations>
144
144
  // Never called — satisfies module type only
145
- let make = (~name as _: string, ~opts as _=?): component => Obj.magic(0)
145
+ let make = (~name as _: string, ~owner as _=?, ~opts as _=?): component => Obj.magic(0)
146
146
  }
147
147
  let eventLog: EventLog.operations = {
148
148
  append: mock.appendFn,
@@ -188,7 +188,7 @@ module MakeOps = (B: {let name: string}) => {
188
188
  writeSnapshot: (string, EventLog.snapshot) => promise<result<unit, string>>,
189
189
  }
190
190
  type component = Component.t<OuterEventLog.t, OuterEventLog.outputs, operations>
191
- let make = (~name as _: string, ~opts as _=?): component => Obj.magic(0)
191
+ let make = (~name as _: string, ~owner as _=?, ~opts as _=?): component => Obj.magic(0)
192
192
  }
193
193
  let eventLog: EventLog.operations = {
194
194
  append: mock.appendFn,
@@ -84,7 +84,7 @@ module MockEventTopic: EventTopic.T
84
84
  publishJsonStream: ReventlessInfra.EventTopic.publishJsonStream,
85
85
  }
86
86
  type component = Component.t<EventTopic.t, EventTopic.outputs, operations>
87
- let make = (~name as _, ~storageResources as _, ~opts as _=?): component => Obj.magic(0)
87
+ let make = (~name as _, ~storageResources as _, ~owner as _=?, ~opts as _=?): component => Obj.magic(0)
88
88
  }
89
89
 
90
90
  let mockEventTopicOps: MockEventTopic.operations = {
@@ -223,6 +223,49 @@ describe("JSON sink", () => {
223
223
  expect(lines->Array.length == 1 && line->fieldOf("plugin") == Some("Catalog"))->toBe(true)
224
224
  })
225
225
 
226
+ testSync("Logger.info resolves plugin from a kind-suffixed component name", () => {
227
+ // Event-collector comps carry the component *resource* name (`<Spec><Kind>`),
228
+ // while the registry holds the bare spec name — the prefix candidate bridges them.
229
+ Logger.registerComponentPlugin(~componentName="Products", ~pluginName="Catalog")
230
+ let lines = captureLogs(() =>
231
+ log.info(~comp="EventCollector(ProductsReadModel)", "projecting event")
232
+ )
233
+ let line = lines->Array.length == 1 ? lines->Array.getUnsafe(0) : ""
234
+ expect(lines->Array.length == 1 && line->fieldOf("plugin") == Some("Catalog"))->toBe(true)
235
+ })
236
+
237
+ testSync("Plugin resolution prefers the longest registered prefix", () => {
238
+ // `Order` (Ordering) must not win over `OrderLines` (Billing) for `OrderLinesReadModel`.
239
+ Logger.registerComponentPlugin(~componentName="Order", ~pluginName="Ordering")
240
+ Logger.registerComponentPlugin(~componentName="OrderLines", ~pluginName="Billing")
241
+ let lines = captureLogs(() =>
242
+ log.info(~comp="EventCollector(OrderLinesReadModel)", "projecting event")
243
+ )
244
+ let line = lines->Array.length == 1 ? lines->Array.getUnsafe(0) : ""
245
+ expect(lines->Array.length == 1 && line->fieldOf("plugin") == Some("Billing"))->toBe(true)
246
+ })
247
+
248
+ testSync("dispatch annotation carries comp onto a handler line that passes none", () => {
249
+ // What makes two collectors in one runtime process separable: the element's comp
250
+ // comes from the dispatch boundary, not from the application's own log call.
251
+ let lines = captureLogs(() =>
252
+ EffectLogger.logInfo("projected")
253
+ ->Runtime.annotateInvocation(
254
+ ~correlationId="cid-1",
255
+ ~causationId="parent-1",
256
+ ~comp="EventCollector(ProductsReadModel)",
257
+ )
258
+ ->Effect.runSync
259
+ )
260
+ let line = lines->Array.length >= 1 ? lines->Array.getUnsafe(0) : ""
261
+ let ok =
262
+ line->fieldOf("comp") == Some("EventCollector(ProductsReadModel)") &&
263
+ line->fieldOf("correlationId") == Some("cid-1") &&
264
+ line->fieldOf("causationId") == Some("parent-1") &&
265
+ line->fieldOf("message") == Some("projected")
266
+ expect(ok)->toBe(true)
267
+ })
268
+
226
269
  testSync("Logger.warn emits one clean JSON record", () => {
227
270
  let lines = captureLogs(() => log.warn(~comp="Platform", "heartbeat skipped"))
228
271
  expect(lines->Array.length == 1 && isCleanRecord(lines->Array.getUnsafe(0)))->toBe(true)