@reventlessdev/reventless-core 3.0.0-alpha.156 → 3.0.0-alpha.158

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 (74) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +6 -6
  3. package/src/admin/AdminApi.res +80 -10
  4. package/src/admin/AdminApi.res.mjs +45 -7
  5. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry.res +65 -0
  6. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry.res.mjs +87 -0
  7. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry_Behavior.res +58 -0
  8. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry_Behavior.res.mjs +135 -0
  9. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments.res +44 -0
  10. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments.res.mjs +74 -0
  11. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments_Projection.res +50 -0
  12. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments_Projection.res.mjs +80 -0
  13. package/src/admin/Platform_Admin.res +46 -1
  14. package/src/admin/Platform_Admin.res.mjs +20 -6
  15. package/src/admin/Platform_ApiFragmentsApi.res +32 -0
  16. package/src/admin/Platform_ApiFragmentsApi.res.mjs +52 -0
  17. package/src/admin/Platform_UIFragmentsApi.res +1 -1
  18. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +50 -0
  19. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +70 -0
  20. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry_Behavior.res +33 -0
  21. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry_Behavior.res.mjs +80 -0
  22. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res +33 -0
  23. package/src/admin/{UIFragmentRegistryReadModelSpec.res.mjs → UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs} +24 -2
  24. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res +34 -0
  25. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res.mjs +54 -0
  26. package/src/components/Api/Api_Naming.res +1 -1
  27. package/src/components/Api/GraphQL_FragmentGenerator.res +7 -11
  28. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +8 -22
  29. package/src/components/Api/GraphQL_PushPlanner.res +65 -0
  30. package/src/components/Api/GraphQL_PushPlanner.res.mjs +46 -0
  31. package/src/components/Api/GraphQL_Stitcher.res +72 -1
  32. package/src/components/Api/GraphQL_Stitcher.res.mjs +53 -2
  33. package/src/components/Dcb/Dcb_Builder.res +16 -6
  34. package/src/components/Dcb/Dcb_Builder.res.mjs +10 -7
  35. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +6 -1
  36. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +1 -1
  37. package/src/plugin/api/PluginBaseFragment.res +5 -6
  38. package/src/plugin/api/PluginBaseFragment.res.mjs +0 -1
  39. package/src/plugin/component/Plugin_Builder.res +4 -1
  40. package/src/plugin/component/Plugin_Builder.res.mjs +7 -6
  41. package/src/plugin/component/Plugin_Helpers.res +30 -0
  42. package/src/plugin/component/Plugin_Helpers.res.mjs +13 -3
  43. package/src/plugin/component/Plugin_SubscriptionSchema.res +19 -9
  44. package/src/plugin/component/Plugin_SubscriptionSchema.res.mjs +23 -14
  45. package/src/plugin/connect/PluginConnectExtension_Mapping.res +28 -6
  46. package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +34 -14
  47. package/src/plugin/connect/PluginExtensionPoint_Builder.res +7 -1
  48. package/src/plugin/connect/PluginExtensionPoint_Builder.res.mjs +5 -1
  49. package/src/plugin/connect/PluginExtensionPoint_Plugin.res +3 -1
  50. package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +4 -2
  51. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +52 -0
  52. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +74 -0
  53. package/src/plugin/lifecycle/PluginBehavior.res +9 -51
  54. package/src/plugin/lifecycle/PluginBehavior.res.mjs +39 -68
  55. package/src/plugin/lifecycle/PluginSpec.res +0 -18
  56. package/src/plugin/lifecycle/PluginSpec.res.mjs +0 -30
  57. package/src/plugin/lifecycle/PluginsProjection.res +2 -6
  58. package/src/plugin/lifecycle/PluginsProjection.res.mjs +0 -2
  59. package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
  60. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +0 -1
  61. package/tests/admin/Platform_ApiFragmentsApiTest.res +66 -0
  62. package/tests/admin/Platform_ApiFragmentsApiTest.res.mjs +68 -0
  63. package/tests/admin/Platform_UIFragmentsApiTest.res +1 -1
  64. package/tests/api/GraphQL_PushPlannerTest.res +62 -0
  65. package/tests/api/GraphQL_PushPlannerTest.res.mjs +80 -0
  66. package/tests/api/GraphQL_StitcherTest.res +63 -0
  67. package/tests/api/GraphQL_StitcherTest.res.mjs +75 -0
  68. package/tests/logger/LogFormatTest.res +1 -2
  69. package/tests/logger/LogFormatTest.res.mjs +1 -2
  70. package/tests/message/MessageTest.res +0 -3
  71. package/tests/message/MessageTest.res.mjs +0 -3
  72. package/src/admin/UIFragmentRegistryProjection.res +0 -43
  73. package/src/admin/UIFragmentRegistryProjection.res.mjs +0 -81
  74. package/src/admin/UIFragmentRegistryReadModelSpec.res +0 -13
@@ -68,6 +68,15 @@ module Make = (
68
68
  let construct = (
69
69
  ~name: string,
70
70
  ~childName: string,
71
+ // API mutation-field prefix for a slice's GraphQL command fields. Defaults to `name`
72
+ // (`${plugin}_${command}`), so plugins are unchanged. The admin builder passes "Platform"
73
+ // so its exposed slice commands render as `Platform_RegisterApiFragment` — byte-equal to
74
+ // `Api_Naming.adminField("RegisterApiFragment")`, keeping the hand-declared admin SDL and
75
+ // the auto-bound DCB resolver field names aligned.
76
+ ~apiNamePrefix: string=name,
77
+ // True for the platform admin build — routes the DCB mutation resolvers to the Platform API
78
+ // (via the provider hook's `hooks.adminApi`) in split mode. Plugins leave it false.
79
+ ~onAdminApi: bool=false,
71
80
  ~environment: string="",
72
81
  ~platformName: string="",
73
82
  ~aggregateEventTopics: EventTopic.allOutputs=Dict.make(),
@@ -411,7 +420,7 @@ module Make = (
411
420
  registerResolver(
412
421
  ~kind=Dcb,
413
422
  ~fields=Api_Naming.sliceMutationFields(
414
- ~plugin=name,
423
+ ~plugin=apiNamePrefix,
415
424
  ~slice=S.Spec.name,
416
425
  ~commandSchema,
417
426
  )->Array.map(((f, _)) => f),
@@ -428,7 +437,7 @@ module Make = (
428
437
  registerResolver(
429
438
  ~kind=Dcb,
430
439
  ~fields=Api_Naming.sliceMutationFields(
431
- ~plugin=name,
440
+ ~plugin=apiNamePrefix,
432
441
  ~slice=S.Spec.name,
433
442
  ~commandSchema,
434
443
  )->Array.map(((f, _)) => f),
@@ -464,7 +473,7 @@ module Make = (
464
473
  ~componentKind=CommandGenerator_Callback.StateChangeSlice,
465
474
  ~stripIdFromParams=false,
466
475
  )
467
- Api_Naming.sliceMutationFields(~plugin=name, ~slice=S.Spec.name, ~commandSchema)
476
+ Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
468
477
  ->Array.forEach(((fieldName, _)) => bindHandler(~field=fieldName, ~generateCommand))
469
478
  }
470
479
  })
@@ -491,7 +500,7 @@ module Make = (
491
500
  ~componentKind=CommandGenerator_Callback.StateChangeSlice,
492
501
  ~stripIdFromParams=false,
493
502
  )
494
- Api_Naming.sliceMutationFields(~plugin=name, ~slice=S.Spec.name, ~commandSchema)
503
+ Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
495
504
  ->Array.forEach(((fieldName, _)) => bindHandler(~field=fieldName, ~generateCommand))
496
505
  }
497
506
  })
@@ -730,7 +739,7 @@ module Make = (
730
739
  if ApiNoApiHelpers.isNoApi(commandSchema) {
731
740
  []
732
741
  } else {
733
- Api_Naming.sliceMutationFields(~plugin=name, ~slice=S.Spec.name, ~commandSchema)
742
+ Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
734
743
  }
735
744
  })
736
745
  let dcbFieldNames = dcbMutationData->Array.map(((f, _)) => f)
@@ -759,6 +768,7 @@ module Make = (
759
768
  runtime: runtime->Obj.magic,
760
769
  fieldNames: dcbFieldNames,
761
770
  tags: dcbTags,
771
+ onAdminApi,
762
772
  opts,
763
773
  })
764
774
  | None => ()
@@ -825,7 +835,7 @@ module Make = (
825
835
  ) => {
826
836
  let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
827
837
  let fieldSpecs =
828
- Api_Naming.sliceMutationFields(~plugin=name, ~slice=S.Spec.name, ~commandSchema)
838
+ Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
829
839
  if ApiNoApiHelpers.isNoApi(commandSchema) || fieldSpecs->Array.length == 0 {
830
840
  None
831
841
  } else {
@@ -78,7 +78,9 @@ let emptyResult = {
78
78
 
79
79
  function Make(DcbEventLogStorage) {
80
80
  return DcbEventTopicPublisher => (DcbCommandTopicChannel => (DcbCommandTopicChannelAsync => (RuntimeBuilder => (HooksConfig => {
81
- let construct = (name, childName, environmentOpt, platformNameOpt, aggregateEventTopicsOpt, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponentsOpt, pluginStructure, opts) => {
81
+ let construct = (name, childName, apiNamePrefixOpt, onAdminApiOpt, environmentOpt, platformNameOpt, aggregateEventTopicsOpt, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponentsOpt, pluginStructure, opts) => {
82
+ let apiNamePrefix = apiNamePrefixOpt !== undefined ? apiNamePrefixOpt : name;
83
+ let onAdminApi = onAdminApiOpt !== undefined ? onAdminApiOpt : false;
82
84
  let environment = environmentOpt !== undefined ? environmentOpt : "";
83
85
  let platformName = platformNameOpt !== undefined ? platformNameOpt : "";
84
86
  let aggregateEventTopics = aggregateEventTopicsOpt !== undefined ? aggregateEventTopicsOpt : ({});
@@ -225,13 +227,13 @@ function Make(DcbEventLogStorage) {
225
227
  syncSlices.forEach(S => {
226
228
  let commandSchema = S.Spec.commandSchema;
227
229
  if (!ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
228
- return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, S.Spec.commandAuthorization);
230
+ return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, S.Spec.commandAuthorization);
229
231
  }
230
232
  });
231
233
  asyncSlices.forEach(S => {
232
234
  let commandSchema = S.Spec.commandSchema;
233
235
  if (!ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
234
- return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, S.Spec.commandAuthorization);
236
+ return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, S.Spec.commandAuthorization);
235
237
  }
236
238
  });
237
239
  }
@@ -244,7 +246,7 @@ function Make(DcbEventLogStorage) {
244
246
  return;
245
247
  }
246
248
  let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(ops.publishJsons, ops.publishJsonsAndWait, S.Spec.name, S.Spec.commandSchema, "StateChangeSlice", false);
247
- Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema).forEach(param => bindHandler(param[0], generateCommand));
249
+ Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).forEach(param => bindHandler(param[0], generateCommand));
248
250
  });
249
251
  });
250
252
  if (asyncDcbCommandTopicOpt !== undefined) {
@@ -255,7 +257,7 @@ function Make(DcbEventLogStorage) {
255
257
  return;
256
258
  }
257
259
  let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(asyncOps.publishJsons, asyncOps.publishJsonsAndWait, S.Spec.name, S.Spec.commandSchema, "StateChangeSlice", false);
258
- Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema).forEach(param => bindHandler(param[0], generateCommand));
260
+ Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).forEach(param => bindHandler(param[0], generateCommand));
259
261
  });
260
262
  });
261
263
  }
@@ -397,7 +399,7 @@ function Make(DcbEventLogStorage) {
397
399
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
398
400
  return [];
399
401
  } else {
400
- return Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema);
402
+ return Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema);
401
403
  }
402
404
  });
403
405
  let dcbFieldNames = dcbMutationData.map(param => param[0]);
@@ -424,6 +426,7 @@ function Make(DcbEventLogStorage) {
424
426
  runtime: runtime,
425
427
  fieldNames: dcbFieldNames,
426
428
  tags: dcbTags,
429
+ onAdminApi: onAdminApi,
427
430
  opts: opts
428
431
  });
429
432
  }
@@ -451,7 +454,7 @@ function Make(DcbEventLogStorage) {
451
454
  };
452
455
  let mutationEntriesFromSlices = Stdlib_Array.filterMap(stateChangeSlices, S => {
453
456
  let commandSchema = S.Spec.commandSchema;
454
- let fieldSpecs = Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema);
457
+ let fieldSpecs = Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema);
455
458
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema) || fieldSpecs.length === 0) {
456
459
  return;
457
460
  }
@@ -34,7 +34,12 @@ module Make = (
34
34
  ~tagKeysByEventType: Dict.t<array<string>>,
35
35
  event: Spec.event,
36
36
  ): ReventlessInfra.DcbEventLog.rawEvent => {
37
- let json = event->JSON.stringifyAny->Option.getOrThrow->JSON.parseOrThrow
37
+ // Encode through the event schema (like the Aggregate path's Message.encode),
38
+ // NOT JSON.stringifyAny: the runtime representation drops `None` option
39
+ // fields entirely, while the consumer side (DcbDecode) parses with the sury
40
+ // schema — a js_nullable option field would reject the missing key and the
41
+ // event would be dropped as schema drift.
42
+ let json = event->S.reverseConvertToJsonOrThrow(Spec.eventSchema)
38
43
  let (eventType, data) = json->Message.splitMessage
39
44
  // Use `extractTagsExpanded` (not `extractTags`) so per-element tags on
40
45
  // `array<string>` fields are emitted — e.g. OrderPlaced's
@@ -182,7 +182,7 @@ function Make(Spec) {
182
182
  }
183
183
  let rawEvents = newEvents$1.map(e => {
184
184
  let parentMeta = command$p.meta;
185
- let json = JSON.parse(Stdlib_Option.getOrThrow(JSON.stringify(e), undefined));
185
+ let json = S.reverseConvertToJsonOrThrow(e, Spec.eventSchema);
186
186
  let match = Message$ReventlessCore.splitMessage(json);
187
187
  let eventType = match[0];
188
188
  let baseTags = DcbTag$Reventless.extractTagsExpanded(Spec.eventSchema, e);
@@ -18,13 +18,12 @@ let pluginExcludeFields: array<string> = [
18
18
 
19
19
  // Additional fields the SDL exposes but the Plugin list view should NOT
20
20
  // surface — they're option-of-nested-object types (e.g. apiSchemaFragment,
21
- // uiFragments, structure) which AutoUI currently renders as scalar columns
21
+ // structure) which AutoUI currently renders as scalar columns
22
22
  // and queries without a sub-selection, failing schema validation. Other
23
- // callers (host-shell's Platform_ComponentDefinitions / Platform_UIFragments)
24
- // keep querying them via dedicated fields and resolver paths.
23
+ // callers (host-shell's Platform_ComponentDefinitions) keep querying them via
24
+ // dedicated fields and resolver paths.
25
25
  let pluginUIOnlyExcludeFields: array<string> = pluginExcludeFields->Array.concat([
26
26
  "apiSchemaFragment",
27
- "uiFragments",
28
27
  "structure",
29
28
  ])
30
29
 
@@ -40,10 +39,10 @@ let pluginUIOnlyExcludeFields: array<string> = pluginExcludeFields->Array.concat
40
39
  let indexQueriesOfConfig = (config: Reventless.ReadModel.config) =>
41
40
  config.indexes->Array.length > 0 ? Some(config.indexes) : None
42
41
 
43
- // The UIFragmentRegistry read model is queried exclusively via the explicit
42
+ // The UiFragments StateViewSlice is queried exclusively via the explicit
44
43
  // flat `Platform_UIFragments: [Platform_UIFragmentEntry!]!` field declared in
45
44
  // `Platform_UIFragmentsApi.res` (and resolved by `Platform_UIFragments_Lambda.res`
46
- // on AWS / the seeded QueryDb in-memory). Reintroducing an auto-generated
45
+ // on AWS / the slice QueryDb in-memory). Reintroducing an auto-generated
47
46
  // connection field here collides with that name on SDL composition.
48
47
  let queryEntries: array<querySchemaEntry> = [
49
48
  {
@@ -20,7 +20,6 @@ let pluginExcludeFields = [
20
20
 
21
21
  let pluginUIOnlyExcludeFields = pluginExcludeFields.concat([
22
22
  "apiSchemaFragment",
23
- "uiFragments",
24
23
  "structure"
25
24
  ]);
26
25
 
@@ -295,6 +295,7 @@ module Make = (
295
295
  ...parts,
296
296
  types: Array.concat(parts.types, subResult.extraTypes),
297
297
  subscriptions: subResult.subscriptionFields,
298
+ subscriptionSources: subResult.subscriptionSources,
298
299
  })
299
300
  }
300
301
 
@@ -750,7 +751,6 @@ module Make = (
750
751
  extensionProtocols: [],
751
752
  apiSchemaFragment: Some(apiSchemaFragment),
752
753
  apiTarget: Some(capturedDeployTarget),
753
- uiFragments,
754
754
  structure: pluginStructure,
755
755
  dcbEventLog: dcbEventLogDef,
756
756
  // Business role from the deploy-time metadata registry (set via
@@ -807,6 +807,7 @@ module Make = (
807
807
  connectPluginExtensionIncomingEventHandler,
808
808
  ) = createConnectPluginExtension(
809
809
  ~pluginDefinition,
810
+ ~uiFragments,
810
811
  ~publishToPluginExtensionPoint,
811
812
  ~publishToAggregates,
812
813
  ~readModelNamesForSourceName,
@@ -821,6 +822,7 @@ module Make = (
821
822
  ~extensionsOutputs,
822
823
  ~pluginExtensionPointUnwrapped,
823
824
  ~pluginDefinition,
825
+ ~uiFragments,
824
826
  ~connectPluginExtensionIncomingEventHandler,
825
827
  ~extensionsHandlers,
826
828
  ~extensionPointsHandlers,
@@ -840,6 +842,7 @@ module Make = (
840
842
  ~extensionPointsOutputs,
841
843
  ~extensionsOutputs,
842
844
  ~pluginDefinition,
845
+ ~uiFragments,
843
846
  ~extensionsHandlers,
844
847
  ~extensionPointsHandlers,
845
848
  ~extensionRegistryInfos,
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Pulumi from "@pulumi/pulumi";
7
7
  import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
8
8
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
9
10
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
10
11
  import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
11
12
  import * as Plugin$ReventlessCore from "./Plugin.res.mjs";
@@ -146,7 +147,7 @@ function Make(Spec) {
146
147
  let DcbBuilder = Dcb_Builder$ReventlessCore.Make(DcbEventLogStorage)(DcbEventTopicPublisher)(DcbCommandTopicChannel)(DcbCommandTopicChannelAsync)(PluginRuntimeBuilder)({
147
148
  hooks: Spec.hooks
148
149
  });
149
- let dcbResult = DcbBuilder.construct(extra$1, childName, Spec.environment, Spec.platformName, aggregateEventTopics, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponents, pluginStructure, opts);
150
+ let dcbResult = DcbBuilder.construct(extra$1, childName, undefined, undefined, Spec.environment, Spec.platformName, aggregateEventTopics, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponents, pluginStructure, opts);
150
151
  let mutationEntriesFromAggregates = aggregates.flatMap(M => {
151
152
  let commandSchema = M.Spec.commandSchema;
152
153
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
@@ -227,7 +228,8 @@ function Make(Spec) {
227
228
  types: parts.types.concat(subResult.extraTypes),
228
229
  mutations: parts.mutations,
229
230
  queries: parts.queries,
230
- subscriptions: subResult.subscriptionFields
231
+ subscriptions: subResult.subscriptionFields,
232
+ subscriptionSources: subResult.subscriptionSources
231
233
  });
232
234
  Stdlib_Option.forEach(Spec.hooks.schemaTypeRegistrationHook, registerTypes => {
233
235
  let parts = GraphQL_Stitcher$ReventlessCore.decode(apiSchemaFragment);
@@ -496,7 +498,6 @@ function Make(Spec) {
496
498
  extensionProtocols: [],
497
499
  apiSchemaFragment: apiSchemaFragment,
498
500
  apiTarget: capturedDeployTarget,
499
- uiFragments: uiFragments,
500
501
  structure: pluginStructure,
501
502
  dcbEventLog: param[2],
502
503
  kind: Stdlib_Option.getOr(Stdlib_Option.flatMap(Plugin_Helpers$ReventlessCore.pluginMetadataRegistry.contents, m => m.kind), "Domain")
@@ -528,10 +529,10 @@ function Make(Spec) {
528
529
  readModelQueueUrls[param[0]] = urlOutput;
529
530
  });
530
531
  if (coreSetup !== undefined) {
531
- let match$4 = Plugin_Helpers$ReventlessCore.createConnectPluginExtension(pluginDefinition, publishToPluginExtensionPoint, publishToAggregates, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName, publishToReadModels, queryEngine, opts);
532
- EventCollectorHelper.connect(eventCollector, eventTopics, extensionPointsOutputs, extensionsOutputs, coreSetup[0], pluginDefinition, match$4[1], extensionsHandlers, extensionPointsHandlers, match$4[0], extensionRegistryInfos, match$1[2], aggregateQueueUrls, readModelQueueUrls, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName);
532
+ let match$4 = Plugin_Helpers$ReventlessCore.createConnectPluginExtension(pluginDefinition, uiFragments, publishToPluginExtensionPoint, publishToAggregates, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName, publishToReadModels, queryEngine, opts);
533
+ EventCollectorHelper.connect(eventCollector, eventTopics, extensionPointsOutputs, extensionsOutputs, coreSetup[0], pluginDefinition, Primitive_option.some(uiFragments), match$4[1], extensionsHandlers, extensionPointsHandlers, match$4[0], extensionRegistryInfos, match$1[2], aggregateQueueUrls, readModelQueueUrls, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName);
533
534
  } else {
534
- EventCollectorHelper.connect(eventCollector, eventTopics, extensionPointsOutputs, extensionsOutputs, undefined, pluginDefinition, undefined, extensionsHandlers, extensionPointsHandlers, undefined, extensionRegistryInfos, undefined, aggregateQueueUrls, readModelQueueUrls, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName);
535
+ EventCollectorHelper.connect(eventCollector, eventTopics, extensionPointsOutputs, extensionsOutputs, undefined, pluginDefinition, Primitive_option.some(uiFragments), undefined, extensionsHandlers, extensionPointsHandlers, undefined, extensionRegistryInfos, undefined, aggregateQueueUrls, readModelQueueUrls, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName);
535
536
  }
536
537
  let tasksOutputs = Plugin_Helpers$ReventlessCore.createTasks(tasks, aggregatesOutputs, scheduler, schedulerRoleUrn, publishToAggregates, queryEngine, Spec.resourceNaming, opts);
537
538
  let resolvers = Plugin_Helpers$ReventlessCore.createResolvers(allQueryDbs);
@@ -146,6 +146,12 @@ type eventCollectorContext = {
146
146
  // Pre-serialised JSON of the pluginDefinition (sury-encoded then JSON.stringify).
147
147
  // Output because it's assembled from resolved component outputs at deploy time.
148
148
  pluginDefinitionJson: Pulumi.Output.t<string>,
149
+ // Pre-serialised JSON of the plugin's UI-fragment manifest ("null" when the
150
+ // plugin ships none). Shipped as its own asset next to pluginDefinition.json —
151
+ // the manifest no longer rides pluginDefinition (the UiFragmentRegistry slice
152
+ // owns fragment state); the bundled Connect extension needs it at cold start
153
+ // to emit RegisterUiFragment in the handshake answer.
154
+ uiFragmentsJson: Pulumi.Output.t<string>,
149
155
  // Outgoing EPs whose events this Lambda's mapping handles. Admin Lambda: 1
150
156
  // entry (its own Plugin EP). Plugin Lambda: 0 (plugins don't process their own
151
157
  // EP outgoing events here).
@@ -368,6 +374,7 @@ let extractExtensionDefinitions = (extensionsOutputs: array<Extension.outputs>)
368
374
 
369
375
  let createConnectPluginExtension = (
370
376
  ~pluginDefinition,
377
+ ~uiFragments: option<Reventless.Plugin.uiFragmentManifest>,
371
378
  ~publishToPluginExtensionPoint,
372
379
  ~publishToAggregates,
373
380
  ~readModelNamesForSourceName,
@@ -379,6 +386,7 @@ let createConnectPluginExtension = (
379
386
  ->Pulumi.Output.apply(pluginDefinition => {
380
387
  module ConnectPluginExtension = PluginConnectExtension_Builder.Make({
381
388
  let pluginDefinition = pluginDefinition
389
+ let uiFragments = uiFragments
382
390
  })
383
391
  let connectPluginExtension = ConnectPluginExtension.make(
384
392
  ~publishToPluginExtensionPoint,
@@ -472,6 +480,10 @@ module MakeEventCollectorHelper = (
472
480
  ~extensionsOutputs: array<Extension.outputs>,
473
481
  ~pluginExtensionPointUnwrapped: option<ReventlessInterop.ExtensionPoint.resolvedOutputs>=?,
474
482
  ~pluginDefinition,
483
+ // The plugin's UI-fragment manifest — serialised into the per-plugin
484
+ // eventCollectorContext (uiFragmentsJson) so the AWS adapter ships it as a
485
+ // uiFragments.json asset for the bundled Connect extension's cold start.
486
+ ~uiFragments: option<Reventless.Plugin.uiFragmentManifest>=None,
475
487
  ~connectPluginExtensionIncomingEventHandler: option<Pulumi.Output.t<Pulumi.Output.t<Plugin_Callback.jsonEventsHandler>>>=?,
476
488
  ~extensionsHandlers,
477
489
  ~extensionPointsHandlers,
@@ -589,6 +601,11 @@ module MakeEventCollectorHelper = (
589
601
  ->S.reverseConvertToJsonOrThrow(Reventless.Plugin.pluginDefinitionSchema)
590
602
  ->JSON.stringify
591
603
  ->Pulumi.Output.make
604
+ let uiFragmentsJson =
605
+ uiFragments
606
+ ->S.reverseConvertToJsonOrThrow(Reventless.Plugin.uiFragmentManifestOptionSchema)
607
+ ->JSON.stringify
608
+ ->Pulumi.Output.make
592
609
  let pluginExtensionPointCmdTopicUrl = switch pluginExtensionPointUnwrapped {
593
610
  | Some(unwrapped) =>
594
611
  switch unwrapped.commandTopic.resources->Array.get(0) {
@@ -680,6 +697,7 @@ module MakeEventCollectorHelper = (
680
697
  ~componentName=ecName,
681
698
  ~context={
682
699
  pluginDefinitionJson,
700
+ uiFragmentsJson,
683
701
  extensionPoints: extensionPointEntries,
684
702
  connectExtension,
685
703
  extensions,
@@ -829,6 +847,11 @@ type dcbAppSyncResolverParams = {
829
847
  runtime: Runtime.environment<unknown>,
830
848
  fieldNames: array<string>,
831
849
  tags: array<string>,
850
+ // True when the slice belongs to the platform admin build (Platform_Admin.construct). In split
851
+ // mode admin resolvers live on the Platform API, so the provider hook binds them to `hooks.adminApi`
852
+ // instead of `hooks.api` (which is the deploy-target/Domain API and is read in the same deferred
853
+ // callback by plugin builds). See platformHooks.adminApi.
854
+ onAdminApi: bool,
832
855
  opts: Pulumi.ComponentResource.options,
833
856
  }
834
857
 
@@ -952,6 +975,12 @@ type platformHooks = {
952
975
  schedulerRoleUrn: ref<Pulumi.Output.t<string>>,
953
976
  api: ref<option<hookedValue<unknown>>>,
954
977
  apiRole: ref<option<hookedValue<unknown>>>,
978
+ // The API admin resolvers bind to (split mode: the Platform API; unified: same as `api`).
979
+ // Populated by makePlatform once the Platform API resource exists, and read — via the
980
+ // `onAdminApi` payload flag — by the DCB resolver hook so admin DCB mutations land on the
981
+ // Platform API even though `api` (read in the same deferred callback by plugin builds) points at
982
+ // the Domain/deploy-target API. `None` on platforms with no separate admin API (in-memory).
983
+ adminApi: ref<option<hookedValue<unknown>>>,
955
984
  // Current deploy target, set by deployPlugin before plugin.make() is called.
956
985
  // "Domain" (default) → resolvers/schema go to the DomainApi.
957
986
  // "Platform" → resolvers/schema go to the PlatformApi.
@@ -967,6 +996,7 @@ let noHooks: platformHooks = {
967
996
  schedulerRoleUrn: ref(Pulumi.Output.make("")),
968
997
  api: ref(None),
969
998
  apiRole: ref(None),
999
+ adminApi: ref(None),
970
1000
  deployTarget: ref("Domain"),
971
1001
  }
972
1002
 
@@ -11,6 +11,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
11
11
  import * as Pulumi from "@pulumi/pulumi";
12
12
  import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
13
13
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
14
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
14
15
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
15
16
  import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
16
17
  import * as Task$ReventlessCore from "../../components/Task/Task.res.mjs";
@@ -222,10 +223,11 @@ function extractExtensionDefinitions(extensionsOutputs) {
222
223
  }));
223
224
  }
224
225
 
225
- function createConnectPluginExtension(pluginDefinition, publishToPluginExtensionPoint, publishToAggregates, readModelNamesForSourceName, publishToReadModels, queryEngine, opts) {
226
+ function createConnectPluginExtension(pluginDefinition, uiFragments, publishToPluginExtensionPoint, publishToAggregates, readModelNamesForSourceName, publishToReadModels, queryEngine, opts) {
226
227
  return Output$Pulumi.unzip(pluginDefinition.apply(pluginDefinition => {
227
228
  let ConnectPluginExtension = PluginConnectExtension_Builder$ReventlessCore.Make({
228
- pluginDefinition: pluginDefinition
229
+ pluginDefinition: pluginDefinition,
230
+ uiFragments: uiFragments
229
231
  });
230
232
  let connectPluginExtension = ConnectPluginExtension.make(publishToPluginExtensionPoint, publishToAggregates, readModelNamesForSourceName, publishToReadModels, queryEngine, opts);
231
233
  let connectPluginExtensionOutputs = Component$ReventlessCore.outputs(connectPluginExtension);
@@ -264,7 +266,8 @@ function MakeEventCollectorHelper(RuntimeEnvironment) {
264
266
  eventCollectorUrn
265
267
  ];
266
268
  };
267
- let connect = (eventCollector, eventTopics, extensionPointsOutputs, extensionsOutputs, pluginExtensionPointUnwrapped, pluginDefinition, connectPluginExtensionIncomingEventHandler, extensionsHandlers, extensionPointsHandlers, connectPluginExtensionOutputs, extensionRegistryInfosOpt, extensionPointRegistryInfosOpt, aggregateQueueUrlsOpt, readModelQueueUrlsOpt, readModelNamesForSourceNameOpt) => {
269
+ let connect = (eventCollector, eventTopics, extensionPointsOutputs, extensionsOutputs, pluginExtensionPointUnwrapped, pluginDefinition, uiFragmentsOpt, connectPluginExtensionIncomingEventHandler, extensionsHandlers, extensionPointsHandlers, connectPluginExtensionOutputs, extensionRegistryInfosOpt, extensionPointRegistryInfosOpt, aggregateQueueUrlsOpt, readModelQueueUrlsOpt, readModelNamesForSourceNameOpt) => {
270
+ let uiFragments = uiFragmentsOpt !== undefined ? Primitive_option.valFromOption(uiFragmentsOpt) : undefined;
268
271
  let extensionRegistryInfos = extensionRegistryInfosOpt !== undefined ? extensionRegistryInfosOpt : [];
269
272
  let extensionPointRegistryInfos = extensionPointRegistryInfosOpt !== undefined ? extensionPointRegistryInfosOpt : [];
270
273
  let aggregateQueueUrls = aggregateQueueUrlsOpt !== undefined ? aggregateQueueUrlsOpt : ({});
@@ -314,6 +317,7 @@ function MakeEventCollectorHelper(RuntimeEnvironment) {
314
317
  let ecResource = Component$ReventlessCore.toPulumiResource(eventCollector);
315
318
  let ecName = ComponentType$ReventlessCore.nameOpt(ecResource.__name, EventCollector$ReventlessCore.componentType);
316
319
  let pluginDefinitionJson = Pulumi.output(JSON.stringify(S.reverseConvertToJsonOrThrow(pluginDefinition, Plugin$Reventless.pluginDefinitionSchema)));
320
+ let uiFragmentsJson = Pulumi.output(JSON.stringify(S.reverseConvertToJsonOrThrow(uiFragments, Plugin$Reventless.uiFragmentManifestOptionSchema)));
317
321
  let pluginExtensionPointCmdTopicUrl;
318
322
  if (pluginExtensionPointUnwrapped !== undefined) {
319
323
  let r = pluginExtensionPointUnwrapped.commandTopic.resources[0];
@@ -370,6 +374,7 @@ function MakeEventCollectorHelper(RuntimeEnvironment) {
370
374
  }) : [];
371
375
  registerEventCollectorContext(ecName, {
372
376
  pluginDefinitionJson: pluginDefinitionJson,
377
+ uiFragmentsJson: uiFragmentsJson,
373
378
  extensionPoints: extensionPointEntries,
374
379
  connectExtension: connectExtension,
375
380
  extensions: extensions,
@@ -468,6 +473,10 @@ let noHooks_apiRole = {
468
473
  contents: undefined
469
474
  };
470
475
 
476
+ let noHooks_adminApi = {
477
+ contents: undefined
478
+ };
479
+
471
480
  let noHooks_deployTarget = {
472
481
  contents: "Domain"
473
482
  };
@@ -478,6 +487,7 @@ let noHooks = {
478
487
  schedulerRoleUrn: noHooks_schedulerRoleUrn,
479
488
  api: noHooks_api,
480
489
  apiRole: noHooks_apiRole,
490
+ adminApi: noHooks_adminApi,
481
491
  deployTarget: noHooks_deployTarget
482
492
  };
483
493
 
@@ -6,8 +6,11 @@
6
6
  open ReventlessInfra.Api
7
7
 
8
8
  // ── Source C: mutation-triggered subscriptions ────────────────────────────────
9
- // One field per command variant. Fires when the mutation is ACCEPTED by AppSync.
10
- // Uses @aws_subscribe — no additional infrastructure is needed.
9
+ // One field per command variant. Fires when the mutation is accepted.
10
+ // The SDL emitted here is provider-neutral; the subscription→mutation link is
11
+ // carried as structured `subscriptionSource` metadata on the fragment. Each
12
+ // provider adds its own wiring (AppSync appends `@aws_subscribe` at push time;
13
+ // the local platform routes its PubSub bridge from the same metadata).
11
14
  //
12
15
  // Generated from the same mutation entries as the query/mutation SDL so that
13
16
  // field names and argument types are always in sync.
@@ -23,8 +26,13 @@ open ReventlessInfra.Api
23
26
  // return type, so the subscription field has to declare CommandResult as well.
24
27
  let sourceCFields = (
25
28
  ~mutationEntries: array<mutationSchemaEntry>,
26
- ): array<string> => {
29
+ ): (array<string>, array<GraphQL_Stitcher.subscriptionSource>) => {
27
30
  let fields: array<string> = []
31
+ let sources: array<GraphQL_Stitcher.subscriptionSource> = []
32
+ let pushField = (fieldName: string) => {
33
+ fields->Array.push(` on${fieldName}(id: ID): CommandResult`)
34
+ sources->Array.push({GraphQL_Stitcher.field: `on${fieldName}`, mutations: [fieldName]})
35
+ }
28
36
  mutationEntries->Array.forEach(entry => {
29
37
  let schema = entry.commandSchema
30
38
  switch schema {
@@ -32,20 +40,18 @@ let sourceCFields = (
32
40
  anyOf->Array.forEachWithIndex((_, i) => {
33
41
  let fieldName = entry.fieldNames->Array.get(i)->Option.getOr("")
34
42
  if fieldName->String.length > 0 {
35
- let sub = ` on${fieldName}(id: ID): CommandResult\n @aws_subscribe(mutations: ["${fieldName}"])`
36
- fields->Array.push(sub)
43
+ pushField(fieldName)
37
44
  }
38
45
  })
39
46
  | Object(_) =>
40
47
  let fieldName = entry.fieldNames->Array.get(0)->Option.getOr("")
41
48
  if fieldName->String.length > 0 {
42
- let sub = ` on${fieldName}(id: ID): CommandResult\n @aws_subscribe(mutations: ["${fieldName}"])`
43
- fields->Array.push(sub)
49
+ pushField(fieldName)
44
50
  }
45
51
  | _ => ()
46
52
  }
47
53
  })
48
- fields
54
+ (fields, sources)
49
55
  }
50
56
 
51
57
  // ── Source B: state-change subscriptions ─────────────────────────────────────
@@ -87,16 +93,20 @@ let sourceAFieldsAndTypes = (
87
93
  type result = {
88
94
  subscriptionFields: array<string>,
89
95
  extraTypes: array<string>,
96
+ subscriptionSources: array<GraphQL_Stitcher.subscriptionSource>,
90
97
  }
91
98
 
92
99
  let generate = (
93
100
  ~mutationEntries: array<mutationSchemaEntry>,
94
101
  ~eventLogEntries: array<eventLogSchemaEntry>,
95
102
  ): result => {
96
- let cFields = sourceCFields(~mutationEntries)
103
+ let (cFields, cSources) = sourceCFields(~mutationEntries)
97
104
  let (aFields, aTypes) = sourceAFieldsAndTypes(~eventLogEntries)
105
+ // Source A fields carry no source mapping — they are pushed via the
106
+ // provider's events channel, not triggered by a mutation.
98
107
  {
99
108
  subscriptionFields: Array.flat([cFields, aFields]),
100
109
  extraTypes: aTypes,
110
+ subscriptionSources: cSources,
101
111
  }
102
112
  }
@@ -4,32 +4,40 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
4
 
5
5
  function sourceCFields(mutationEntries) {
6
6
  let fields = [];
7
+ let sources = [];
8
+ let pushField = fieldName => {
9
+ fields.push(` on` + fieldName + `(id: ID): CommandResult`);
10
+ sources.push({
11
+ field: `on` + fieldName,
12
+ mutations: [fieldName]
13
+ });
14
+ };
7
15
  mutationEntries.forEach(entry => {
8
16
  let schema = entry.commandSchema;
9
17
  switch (schema.type) {
10
18
  case "object" :
11
19
  let fieldName = Stdlib_Option.getOr(entry.fieldNames[0], "");
12
- if (fieldName.length <= 0) {
20
+ if (fieldName.length > 0) {
21
+ return pushField(fieldName);
22
+ } else {
13
23
  return;
14
24
  }
15
- let sub = ` on` + fieldName + `(id: ID): CommandResult\n @aws_subscribe(mutations: ["` + fieldName + `"])`;
16
- fields.push(sub);
17
- return;
18
25
  case "union" :
19
26
  schema.anyOf.forEach((param, i) => {
20
27
  let fieldName = Stdlib_Option.getOr(entry.fieldNames[i], "");
21
- if (fieldName.length <= 0) {
22
- return;
28
+ if (fieldName.length > 0) {
29
+ return pushField(fieldName);
23
30
  }
24
- let sub = ` on` + fieldName + `(id: ID): CommandResult\n @aws_subscribe(mutations: ["` + fieldName + `"])`;
25
- fields.push(sub);
26
31
  });
27
32
  return;
28
33
  default:
29
34
  return;
30
35
  }
31
36
  });
32
- return fields;
37
+ return [
38
+ fields,
39
+ sources
40
+ ];
33
41
  }
34
42
 
35
43
  function sourceAFieldsAndTypes(eventLogEntries) {
@@ -52,14 +60,15 @@ function sourceAFieldsAndTypes(eventLogEntries) {
52
60
  }
53
61
 
54
62
  function generate(mutationEntries, eventLogEntries) {
55
- let cFields = sourceCFields(mutationEntries);
56
- let match = sourceAFieldsAndTypes(eventLogEntries);
63
+ let match = sourceCFields(mutationEntries);
64
+ let match$1 = sourceAFieldsAndTypes(eventLogEntries);
57
65
  return {
58
66
  subscriptionFields: [
59
- cFields,
60
- match[0]
67
+ match[0],
68
+ match$1[0]
61
69
  ].flat(),
62
- extraTypes: match[1]
70
+ extraTypes: match$1[1],
71
+ subscriptionSources: match[1]
63
72
  };
64
73
  }
65
74
 
@@ -20,6 +20,10 @@ module ExtensionMapping = ReventlessInfra.ExtensionMapping
20
20
 
21
21
  module type Spec = {
22
22
  let pluginDefinition: Reventless.Plugin.pluginDefinition
23
+ // The plugin's UI-fragment manifest (no longer carried on pluginDefinition —
24
+ // the UiFragmentRegistry slice owns fragment state; the definition keeps
25
+ // lifecycle only). None for pure backend plugins.
26
+ let uiFragments: option<Reventless.Plugin.uiFragmentManifest>
23
27
  }
24
28
 
25
29
  module Make = (Spec: Spec) => {
@@ -46,12 +50,30 @@ module Make = (Spec: Spec) => {
46
50
  let id = pluginDefinition.id
47
51
 
48
52
  switch event {
49
- | PluginExtensionPointSpec.UnknownPluginDetected if pluginId == id => [
50
- PublishExtensionPointCommand(
51
- id,
52
- PluginExtensionPointSpec.ConnectPlugin(pluginDefinition),
53
- ),
54
- ]
53
+ // Answer the handshake, and — when this plugin ships a UI-fragment manifest — register
54
+ // it with the admin UiFragmentRegistry slice in the same step (the admin EP's
55
+ // UI-fragment mapping routes RegisterUiFragment to the slice). Nested switch (not
56
+ // Array.concat) so the outer branch's expected type disambiguates the bare
57
+ // PublishExtensionPointCommand constructor.
58
+ | PluginExtensionPointSpec.UnknownPluginDetected if pluginId == id =>
59
+ switch Spec.uiFragments {
60
+ | Some(manifest) => [
61
+ PublishExtensionPointCommand(
62
+ id,
63
+ PluginExtensionPointSpec.ConnectPlugin(pluginDefinition),
64
+ ),
65
+ PublishExtensionPointCommand(
66
+ id,
67
+ PluginExtensionPointSpec.RegisterUiFragment(manifest),
68
+ ),
69
+ ]
70
+ | None => [
71
+ PublishExtensionPointCommand(
72
+ id,
73
+ PluginExtensionPointSpec.ConnectPlugin(pluginDefinition),
74
+ ),
75
+ ]
76
+ }
55
77
  // PluginConnected / PluginReconnected / PluginDeactivated for peer
56
78
  // plugins used to call DoConnectPlugin / DoDisconnectPlugin here so
57
79
  // each plugin's EC Lambda could subscribe / unsubscribe its own