@reventlessdev/reventless-local 3.0.0-alpha.142 → 3.0.0-alpha.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.143 (2026-07-12)
7
+
8
+ ### Features
9
+
10
+ * **admin:** event-source the API fragment registry as admin DCB slices (Phase 2b) ([3fc40d2](https://github.com/ReventlessDev/reventless-core/commit/3fc40d247c54bf66ed84d625959f08299aa04a98))
11
+ * **admin:** event-source the UI fragment registry as admin DCB slices ([711581e](https://github.com/ReventlessDev/reventless-core/commit/711581e77c626e7d0fc35db8ec351f62a70bd8f2))
12
+ * **admin:** expose the API fragment registry as system-callable mutations (Phase 2d) ([8b1ec49](https://github.com/ReventlessDev/reventless-core/commit/8b1ec49813e67f303bc86d60470c9b95d2a8408e))
13
+ * **admin:** make the API fragment registry per-target (Domain | Platform) ([30491a9](https://github.com/ReventlessDev/reventless-core/commit/30491a9e14b4236c98cc756efb6de68ede1e77d7))
14
+ * **admin:** populate the API fragment registry on connect + status query (Phase 2c) ([106d96c](https://github.com/ReventlessDev/reventless-core/commit/106d96c904e135f8360d53e582f395c107db704a))
15
+ * **admin:** retire the Plugin-aggregate UI-fragment path in favour of the registry slices ([1dbc708](https://github.com/ReventlessDev/reventless-core/commit/1dbc708e7439b34ff970cc3d963d7835a8c6fd48))
16
+ * **admin:** serve Platform_UIFragments from the new UiFragmentRegistry slices ([7a47533](https://github.com/ReventlessDev/reventless-core/commit/7a47533acc95977fbeeb6375e94449056b655abc))
17
+ * **api:** neutralize core-emitted SDL — [@aws](https://github.com/aws)_subscribe moves to the AWS adapter (Phase 2a) ([9b27ec8](https://github.com/ReventlessDev/reventless-core/commit/9b27ec8f6ab2b7453b1c50b9891e89781ec3b31a))
18
+
19
+
6
20
  # 3.0.0-alpha.142 (2026-07-11)
7
21
 
8
22
  **Note:** Version bump only for package @reventlessdev/reventless-local
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.142",
3
+ "version": "3.0.0-alpha.143",
4
4
  "description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -32,16 +32,16 @@
32
32
  "sury": "11.0.0-alpha.4",
33
33
  "ws": "^8.18.0",
34
34
  "@reventlessdev/rescript-effect": "0.1.0-alpha.27",
35
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.18",
36
35
  "@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.20",
37
36
  "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
38
37
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
39
- "@reventlessdev/reventless-core": "3.0.0-alpha.156",
40
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.3",
41
- "@reventlessdev/reventless-infra": "3.0.0-alpha.97",
42
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.102",
43
- "@reventlessdev/reventless-spec": "3.0.0-alpha.75",
44
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.20"
38
+ "@reventlessdev/reventless-core": "3.0.0-alpha.157",
39
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.18",
40
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.103",
41
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.4",
42
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.98",
43
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.21",
44
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.76"
45
45
  },
46
46
  "devDependencies": {
47
47
  "rescript": "^12.3.0",
package/src/Platform.res CHANGED
@@ -44,6 +44,39 @@ let decodePluginEventEnvelope = (eventJson: JSON.t): option<ReventlessCore.Plugi
44
44
  | exception _ => None
45
45
  }
46
46
 
47
+ // Decode a UiFragmentRegistry slice event from the admin DcbEventLog's published
48
+ // bus envelope — same {id, meta, event} shape as the aggregate topic above
49
+ // (DcbEventLog_Operations publishes via Message.composeEventJson'). Top-level,
50
+ // not inside the functor, so the decode is unit-testable.
51
+ let decodeUiFragmentRegistryEventEnvelope = (
52
+ eventJson: JSON.t,
53
+ ): option<ReventlessCore.UiFragmentRegistry.event> =>
54
+ switch eventJson
55
+ ->JSON.Decode.object
56
+ ->Option.flatMap(d => d->Dict.get("event"))
57
+ ->Option.map(payload =>
58
+ payload->S.parseJsonOrThrow(ReventlessCore.UiFragmentRegistry.eventSchema)
59
+ ) {
60
+ | result => result
61
+ | exception _ => None
62
+ }
63
+
64
+ // Same envelope shape for the ApiFragmentRegistry slice's events on the shared
65
+ // admin DcbEventLog topic. Returns None for envelopes of other admin slices
66
+ // (schema mismatch), so a single subscriber can try both decoders.
67
+ let decodeApiFragmentRegistryEventEnvelope = (
68
+ eventJson: JSON.t,
69
+ ): option<ReventlessCore.ApiFragmentRegistry.event> =>
70
+ switch eventJson
71
+ ->JSON.Decode.object
72
+ ->Option.flatMap(d => d->Dict.get("event"))
73
+ ->Option.map(payload =>
74
+ payload->S.parseJsonOrThrow(ReventlessCore.ApiFragmentRegistry.eventSchema)
75
+ ) {
76
+ | result => result
77
+ | exception _ => None
78
+ }
79
+
47
80
  // Configurable platform — set silent=true to suppress diagnostic warnings in tests,
48
81
  // splitApi=true to serve core and plugin APIs on separate ports,
49
82
  // backend=Backend.Sqlite({...}) to opt into file-backed SQLite persistence.
@@ -190,6 +223,8 @@ module MakeWithConfig = (
190
223
  schedulerRoleUrn: ref(Pulumi.Output.make("")),
191
224
  api: ref(None),
192
225
  apiRole: ref(None),
226
+ // In-memory has a single schema (no split Domain/Platform API), so admin resolvers share `api`.
227
+ adminApi: ref(None),
193
228
  deployTarget: ref("Domain"),
194
229
  // Phase 1: register SDL + resolver stub synchronously.
195
230
  // Pass the resolved server so the correct target (domain or platform) receives the schema.
@@ -634,6 +669,26 @@ module MakeWithConfig = (
634
669
  StateViewSliceMaker.Make(Spec, Projection)
635
670
  }
636
671
 
672
+ // Admin fragment registry slices (docs/plans/event-sourced-fragment-registries.md): the
673
+ // platform UI- and API-fragment registries hosted as admin DCB slices sharing the admin
674
+ // DcbEventLog. Built once and passed into Admin.construct below.
675
+ module UiFragmentRegistrySlice = StateChangeSlice.Make(
676
+ ReventlessCore.UiFragmentRegistry,
677
+ ReventlessCore.UiFragmentRegistry_Behavior,
678
+ )
679
+ module UiFragmentsViewSlice = StateViewSlice.Make(
680
+ ReventlessCore.UiFragments,
681
+ ReventlessCore.UiFragments_Projection,
682
+ )
683
+ module ApiFragmentRegistrySlice = StateChangeSlice.Make(
684
+ ReventlessCore.ApiFragmentRegistry,
685
+ ReventlessCore.ApiFragmentRegistry_Behavior,
686
+ )
687
+ module ApiFragmentsViewSlice = StateViewSlice.Make(
688
+ ReventlessCore.ApiFragments,
689
+ ReventlessCore.ApiFragments_Projection,
690
+ )
691
+
637
692
  // In-memory has no DynamoDB streams — stream variant is identical to plain StateViewSlice.
638
693
  module StateViewSliceStream = {
639
694
  module Make = (
@@ -674,6 +729,7 @@ module MakeWithConfig = (
674
729
  mutations: [],
675
730
  queries: [],
676
731
  subscriptions: [],
732
+ subscriptionSources: [],
677
733
  })
678
734
 
679
735
  module Api = {
@@ -809,35 +865,6 @@ module MakeWithConfig = (
809
865
  // by dispatching a synthetic `Connect(pluginDefinition)` to LocalPluginAggregate
810
866
  // (see connectPlugin).
811
867
 
812
- // UIFragmentRegistry QueryDb store ops — initialized lazily on first seed call.
813
- let uiFragmentQueryDbOpsRef: ref<option<ReventlessCore.QueryDb_Adapter.operations>> = ref(None)
814
-
815
- // Backend-aware storage for the UIFragment registry — the same functor every
816
- // other read model uses. Replaces a hand-rolled memory-only store that ignored
817
- // BackendState, so the registry now persists under SQLite too.
818
- module UIFragmentStorage = LocalQueryDbStorage.Make(Bus)
819
-
820
- let ensureUIFragmentRegistryQueryDbStore = () => {
821
- switch uiFragmentQueryDbOpsRef.contents {
822
- | Some(ops) => ops
823
- | None =>
824
- let name = ReventlessCore.UIFragmentRegistryReadModelSpec.name
825
- // `make` registers the ops (and scan/stream) with the Bus for the active
826
- // backend; retrieve them synchronously via getQueryDb rather than resolving
827
- // the Pulumi Output.
828
- let _ = UIFragmentStorage.make(
829
- ~name,
830
- ~indexes=[],
831
- ~api=(),
832
- ~apiRole=(),
833
- ~opts=({}: Pulumi.CustomResourceOptions.t),
834
- )
835
- let ops = Bus.getQueryDb(name)->Option.getOrThrow
836
- uiFragmentQueryDbOpsRef := Some(ops)
837
- ops
838
- }
839
- }
840
-
841
868
  // In-memory store for plugin structures, keyed by plugin ID.
842
869
  let pluginStructuresStore: ref<dict<Reventless.Plugin.pluginStructure>> = ref(Dict.make())
843
870
 
@@ -876,12 +903,95 @@ module MakeWithConfig = (
876
903
  Bus.dispatchCommand(pluginCmdTopicKey, ReventlessCore.CommandTopic.encodeCommandJson(cmdJson))
877
904
  }
878
905
 
906
+ // Bus key for the shared admin DCB command topic, derived exactly as
907
+ // Dcb_Builder + CommandTopic_Builder name the channel:
908
+ // "Admin" -> +"Dcb" -> +CmdTopic ("AdminDcbCmdTopic").
909
+ let adminDcbCmdTopicKey = {
910
+ module CT = ReventlessCore.ComponentType
911
+ "AdminDcb"->CT.name(CT.CommandTopic)
912
+ }
913
+
914
+ // Dispatch a UiFragmentRegistry slice command in-process via the shared admin
915
+ // DCB command topic. The DCB filtering handler routes by the command's variant
916
+ // tag to the slice handler registered by Admin.construct's DcbBuilder. The real
917
+ // platform routes RegisterUiFragment through the admin PluginExtensionPoint's
918
+ // UI-fragment mapping; the local platform has no admin EP component (Connect is
919
+ // likewise dispatched straight to the Plugin aggregate), so this is the local
920
+ // analogue of that mapping's PublishCommand. Keyed by bare plugin NAME —
921
+ // dispatchCommand parks until the slice handler registers, so call-before-wire
922
+ // is safe.
923
+ let dispatchUiFragmentCommand = (
924
+ ~pluginName: string,
925
+ ~command: ReventlessCore.UiFragmentRegistry.command,
926
+ ) => {
927
+ let cmdJson: Reventless.Message.commandJson = {
928
+ id: pluginName,
929
+ meta: ReventlessCore.Message.generateMeta(~service=ReventlessCore.UiFragmentRegistry.name),
930
+ commandJson: command->S.reverseConvertToJsonOrThrow(
931
+ ReventlessCore.UiFragmentRegistry.commandSchema,
932
+ ),
933
+ }
934
+ Bus.dispatchCommand(adminDcbCmdTopicKey, ReventlessCore.CommandTopic.encodeCommandJson(cmdJson))
935
+ }
936
+
937
+ // Same in-process path for the ApiFragmentRegistry slice (the API-schema
938
+ // fragment registry). On the real platform the deploy is the caller (SigV4
939
+ // RegisterApiFragment mutation); locally we dispatch straight to the shared
940
+ // admin DCB command topic, the analogue used for the UI registry above.
941
+ let dispatchApiFragmentCommand = (
942
+ ~pluginName: string,
943
+ ~command: ReventlessCore.ApiFragmentRegistry.command,
944
+ ) => {
945
+ let cmdJson: Reventless.Message.commandJson = {
946
+ id: pluginName,
947
+ meta: ReventlessCore.Message.generateMeta(~service=ReventlessCore.ApiFragmentRegistry.name),
948
+ commandJson: command->S.reverseConvertToJsonOrThrow(
949
+ ReventlessCore.ApiFragmentRegistry.commandSchema,
950
+ ),
951
+ }
952
+ Bus.dispatchCommand(adminDcbCmdTopicKey, ReventlessCore.CommandTopic.encodeCommandJson(cmdJson))
953
+ }
954
+
955
+ // Platform_ApiFragments status-query resolver — scans the ApiFragments
956
+ // StateViewSlice QueryDb (populated by the ApiFragmentRegistry slice's
957
+ // projection) and encodes via the shared Platform_ApiFragmentsApi encoder, so
958
+ // the deploy-facing status shape matches AWS. Keyed by bare plugin name (one
959
+ // row per plugin), so no version collapse is needed. Registered at each admin
960
+ // server site below.
961
+ let apiFragmentsQueryResolver = async (_root, _args, _ctx): JSON.t => {
962
+ let items = switch Bus.getQueryDbScan(ReventlessCore.ApiFragments.name) {
963
+ | Some(scanAll) => scanAll()
964
+ | None => []
965
+ }
966
+ items
967
+ ->Array.filterMap(item =>
968
+ switch item->S.parseOrThrow(ReventlessCore.ApiFragments.stateSchema) {
969
+ | state => Some(ReventlessCore.Platform_ApiFragmentsApi.encodeApiFragmentEntry(state))
970
+ | exception _ => None
971
+ }
972
+ )
973
+ ->JSON.Encode.array
974
+ }
975
+
879
976
  // Subscription topics for the admin live-update channels.
880
977
  let pluginStatusSubTopic = "onPluginStatusChange"
881
978
  let uiFragmentSubTopic = "onUIFragmentChange"
882
979
 
883
- // Relocated Source-C emission: subscribe to the Plugin aggregate's event topic and
884
- // fan each lifecycle event out to the matching GraphQL subscription. Status now
980
+ // Bus key + service identity of the admin DcbEventLog's event topic. The
981
+ // Platform_Admin construct name is "Admin"; DcbEventLog_Builder hands that name
982
+ // to EventTopic_Builder, whose publisher registers under `<name>EventTopic`,
983
+ // and DcbEventLog_Operations stamps every published event's meta.service with
984
+ // `<name>DcbEventLog`.
985
+ let adminDcbEventTopicKey = {
986
+ module CT = ReventlessCore.ComponentType
987
+ "Admin"->CT.name(CT.EventTopic)
988
+ }
989
+ let adminDcbServiceName = "AdminDcbEventLog"
990
+
991
+ // Relocated Source-C emission: subscribe to the Plugin aggregate's event topic
992
+ // (lifecycle status) and the admin DcbEventLog event topic (UiFragmentRegistry
993
+ // slice events — the Plugin aggregate no longer emits UIFragment* events) and
994
+ // fan each event out to the matching GraphQL subscription. Status
885
995
  // flows from folding aggregate events (Connect / Activate / Deactivate / Retire),
886
996
  // not from direct QueryDb writes, so the subscription emission lives here rather
887
997
  // than inside the mutation resolvers. Idempotent: registered once per platform
@@ -923,37 +1033,68 @@ module MakeWithConfig = (
923
1033
  | Some(VersionDisconnected(def)) => publishStatus(~name=def.name, ~status="Disconnected")
924
1034
  | Some(VersionDeactivated(def)) => publishStatus(~name=def.name, ~status="Inactive")
925
1035
  | Some(VersionRetired(def)) => publishStatus(~name=def.name, ~status="Retired")
926
- | Some(UIFragmentRegistered(data)) =>
927
- publishUIFragment(
928
- ~name=ReventlessCore.Plugin.name(data.pluginId),
929
- ~changeKind="Registered",
930
- ~manifest=manifestJson(data.manifest),
931
- )
932
- | Some(UIFragmentUpdated(data)) =>
933
- publishUIFragment(
934
- ~name=ReventlessCore.Plugin.name(data.pluginId),
935
- ~changeKind="Updated",
936
- ~manifest=manifestJson(data.newManifest),
937
- )
938
- | Some(UIFragmentDeregistered(data)) =>
939
- publishUIFragment(
940
- ~name=ReventlessCore.Plugin.name(data.pluginId),
941
- ~changeKind="Deregistered",
942
- ~manifest=JSON.Encode.null,
943
- )
944
1036
  | Some(VersionDetected(_))
945
1037
  | Some(VersionSuperseded(_))
946
1038
  | Some(IncompatiblePluginDetected(_))
947
1039
  | None => ()
948
1040
  }
949
1041
  })
1042
+ // UI-fragment live updates — sourced from the UiFragmentRegistry slice's
1043
+ // events on the shared admin DcbEventLog topic. The slice's `pluginId` is
1044
+ // already the bare plugin name (the EP mapping keys the registry by name).
1045
+ // The service guard keeps this decoder scoped to DcbEventLog-published
1046
+ // envelopes as further admin slices join the shared log.
1047
+ Bus.subscribeToEvents(adminDcbEventTopicKey, async (service, _meta, eventJson) =>
1048
+ if service == adminDcbServiceName {
1049
+ switch decodeUiFragmentRegistryEventEnvelope(eventJson) {
1050
+ | Some(UiFragmentRegistered({pluginId, manifest})) =>
1051
+ publishUIFragment(
1052
+ ~name=pluginId,
1053
+ ~changeKind="Registered",
1054
+ ~manifest=manifestJson(manifest),
1055
+ )
1056
+ | Some(UiFragmentUpdated({pluginId, newManifest})) =>
1057
+ publishUIFragment(
1058
+ ~name=pluginId,
1059
+ ~changeKind="Updated",
1060
+ ~manifest=manifestJson(newManifest),
1061
+ )
1062
+ | Some(UiFragmentDeregistered({pluginId})) =>
1063
+ publishUIFragment(~name=pluginId, ~changeKind="Deregistered", ~manifest=JSON.Encode.null)
1064
+ | None => ()
1065
+ }
1066
+ // Local single writer for the API-schema fragment registry. On AWS the
1067
+ // subscriber stitches + pushes the schema then records the outcome; on
1068
+ // local the plugin's fields are already served by resolver registration,
1069
+ // so the "push" is a no-op and we simply record success — the honest
1070
+ // status for an in-process schema. Only Registered/Updated trigger a
1071
+ // record (never PushRecorded — no re-entrant loop) and the record's own
1072
+ // event decodes here as a no-op.
1073
+ switch decodeApiFragmentRegistryEventEnvelope(eventJson) {
1074
+ | Some(ApiFragmentRegistered({pluginId}))
1075
+ | Some(ApiFragmentUpdated({pluginId})) =>
1076
+ let _ = dispatchApiFragmentCommand(
1077
+ ~pluginName=pluginId,
1078
+ ~command=ReventlessCore.ApiFragmentRegistry.RecordApiFragmentPush({
1079
+ pluginId,
1080
+ ok: true,
1081
+ message: "",
1082
+ at: Date.make()->Date.toISOString,
1083
+ }),
1084
+ )
1085
+ | Some(ApiFragmentDeregistered(_))
1086
+ | Some(ApiFragmentPushRecorded(_))
1087
+ | None => ()
1088
+ }
1089
+ }
1090
+ )
950
1091
  }
951
1092
 
952
1093
  // Drive the admin Plugin read models the same way AWS does: build a
953
1094
  // `pluginDefinition` from the constructed plugin component outputs and dispatch a
954
1095
  // synthetic `Connect(def)` to LocalPluginAggregate, keyed by the plugin NAME (the
955
1096
  // Plugin aggregate is name-keyed). The Plugin aggregate emits `VersionConnected`
956
- // (+ `UIFragmentRegistered` / `VersionSuperseded`) on its event topic; the real
1097
+ // (+ `VersionSuperseded`) on its event topic; the real
957
1098
  // PluginsProjection subscription (registered by
958
1099
  // Admin.construct ~readModels) folds those events into the "Plugins" current view
959
1100
  // QueryDb store. This replaces the previous
@@ -1015,7 +1156,6 @@ module MakeWithConfig = (
1015
1156
  extensionProtocols: [],
1016
1157
  apiSchemaFragment,
1017
1158
  apiTarget: None,
1018
- uiFragments,
1019
1159
  structure: pluginStructure,
1020
1160
  dcbEventLog: None,
1021
1161
  kind: Domain,
@@ -1025,34 +1165,39 @@ module MakeWithConfig = (
1025
1165
  ~pluginName,
1026
1166
  ~command=ReventlessCore.PluginSpec.Connect(pluginDefinition),
1027
1167
  )
1028
- })
1029
- })
1030
- }
1031
-
1032
- // Seed the UIFragmentRegistry QueryDb from plugin outputs that carry a uiFragments manifest.
1033
- let seedUIFragmentRegistryQueryDb = (~pluginComponents: array<ReventlessCore.Plugin.component>) => {
1034
- let uiFragmentOps = ensureUIFragmentRegistryQueryDbStore()
1035
- pluginComponents->Array.forEach(plugin => {
1036
- let outputs: ReventlessInfra.Plugin.outputs = plugin->ReventlessCore.Component.outputs
1037
- let _ =
1038
- (outputs.id, outputs.uiFragments)
1039
- ->Pulumi.Output.all2
1040
- ->Pulumi.Output.apply(((id, uiFragments)) => {
1168
+ // Register the UI-fragment manifest with the UiFragmentRegistry slice
1169
+ // (via the shared admin DCB command topic), so the UiFragments
1170
+ // StateViewSlice populates in-process. `at` mirrors the EP mapping's
1171
+ // command-meta timestamp threading.
1041
1172
  switch uiFragments {
1042
1173
  | Some(manifest) =>
1043
- let state: ReventlessCore.UIFragmentRegistryReadModelSpec.state = {
1044
- pluginId: id,
1045
- remoteEntryUrl: manifest.remoteEntryUrl,
1046
- panels: manifest.panels,
1047
- pages: manifest.pages,
1048
- registeredAt: Date.make()->Date.toISOString,
1049
- updatedAt: Date.make()->Date.toISOString,
1050
- }
1051
- let entry =
1052
- state->S.reverseConvertToJsonOrThrow(
1053
- ReventlessCore.UIFragmentRegistryReadModelSpec.stateSchema,
1054
- )
1055
- let _ = uiFragmentOps.save(id, entry, Any, None)
1174
+ let _ = dispatchUiFragmentCommand(
1175
+ ~pluginName,
1176
+ ~command=ReventlessCore.UiFragmentRegistry.RegisterUiFragment({
1177
+ pluginId: pluginName,
1178
+ manifest,
1179
+ at: Date.make()->Date.toISOString,
1180
+ }),
1181
+ )
1182
+ | None => ()
1183
+ }
1184
+ // Register the API-schema fragment with the ApiFragmentRegistry slice.
1185
+ // On the real platform the deploy sends this as a SigV4 caller; locally
1186
+ // connect stands in. apiTarget is Domain (mirrors pluginDefinition.kind
1187
+ // = Domain / apiTarget = None above — the local platform has no
1188
+ // Platform-target plugins). The subscriber below records the push as ok,
1189
+ // since the fields are already served in-process by resolver registration.
1190
+ switch apiSchemaFragment {
1191
+ | Some(fragment) =>
1192
+ let _ = dispatchApiFragmentCommand(
1193
+ ~pluginName,
1194
+ ~command=ReventlessCore.ApiFragmentRegistry.RegisterApiFragment({
1195
+ pluginId: pluginName,
1196
+ fragment,
1197
+ apiTarget: Domain,
1198
+ at: Date.make()->Date.toISOString,
1199
+ }),
1200
+ )
1056
1201
  | None => ()
1057
1202
  }
1058
1203
  })
@@ -1326,8 +1471,8 @@ module MakeWithConfig = (
1326
1471
  ~resourceNaming=LocalPluginSpec.resourceNaming,
1327
1472
  ~api=(),
1328
1473
  ~apiRole=(),
1329
- ~stateChangeSlices=[],
1330
- ~stateViewSlices=[],
1474
+ ~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
1475
+ ~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
1331
1476
  ~automationSlices=[],
1332
1477
  ~outboundTranslationSlices=[],
1333
1478
  ~inboundTranslationSlices=[],
@@ -1409,7 +1554,6 @@ module MakeWithConfig = (
1409
1554
  // chains, so deferring their invocation changes no synchronous contract.
1410
1555
  let seedAdminStores = () => {
1411
1556
  connectPlugin(~pluginComponents=plugins)
1412
- seedUIFragmentRegistryQueryDb(~pluginComponents=plugins)
1413
1557
  seedPluginStructuresStore(~pluginComponents=plugins)
1414
1558
  }
1415
1559
  switch (projectionCatchup, pgProjectionCatchup) {
@@ -1455,7 +1599,7 @@ module MakeWithConfig = (
1455
1599
  )
1456
1600
 
1457
1601
  let pluginQueryDbName = ReventlessCore.PluginsReadModelSpec.name
1458
- let uiFragmentQueryDbName = ReventlessCore.UIFragmentRegistryReadModelSpec.name
1602
+ let uiFragmentQueryDbName = ReventlessCore.UiFragments.name
1459
1603
 
1460
1604
  // Wire the per-mutation plugin-status gate (Part 2.3 of the resolver plan).
1461
1605
  // Resolves at call-time so updates to Plugin RM status are reflected immediately.
@@ -1605,10 +1749,10 @@ module MakeWithConfig = (
1605
1749
  },
1606
1750
  )
1607
1751
 
1608
- // Platform_UIFragments resolver — reads the seeded UIFragmentRegistry QueryDb
1609
- // (same store the admin UIFragment / UIFragments queries scan) and encodes via
1610
- // the shared Platform_UIFragmentsApi encoder so AWS and in-memory return the
1611
- // same JSON shape.
1752
+ // Platform_UIFragments resolver — reads the UiFragments StateViewSlice
1753
+ // QueryDb (populated by the UiFragmentRegistry slice's projection) and
1754
+ // encodes via the shared Platform_UIFragmentsApi encoder so AWS and
1755
+ // in-memory return the same JSON shape.
1612
1756
  queryResolvers->Dict.set(
1613
1757
  "Platform_UIFragments",
1614
1758
  async (_root, _args, _ctx): JSON.t => {
@@ -1616,15 +1760,13 @@ module MakeWithConfig = (
1616
1760
  | Some(scanAll) => scanAll()
1617
1761
  | None => []
1618
1762
  }
1619
- // Collapse to one entry per plugin name (highest version). UIFragmentRegistry
1620
- // accumulates a row per deployed version and has no lifecycle status of its
1621
- // own, so without this a redeployed federation plugin surfaces duplicate
1622
- // fragments mirrors the dedup in Platform_UIFragments_Lambda.res.
1763
+ // Collapse to one entry per plugin name (highest version). The registry is
1764
+ // keyed by bare plugin name now (a no-op collapse), but rows persisted by
1765
+ // the pre-slice registry were keyed name@version keep the dedup so a
1766
+ // mixed store never surfaces duplicates (mirrors Platform_UIFragments_Lambda.res).
1623
1767
  let latestByName = Dict.make()
1624
1768
  items->Array.forEach(item =>
1625
- switch item->S.parseOrThrow(
1626
- ReventlessCore.UIFragmentRegistryReadModelSpec.stateSchema,
1627
- ) {
1769
+ switch item->S.parseOrThrow(ReventlessCore.UiFragments.stateSchema) {
1628
1770
  | state =>
1629
1771
  let name = ReventlessCore.Plugin.name(state.pluginId)
1630
1772
  let version = ReventlessCore.Plugin.version(state.pluginId)
@@ -1646,6 +1788,7 @@ module MakeWithConfig = (
1646
1788
  ->JSON.Encode.array
1647
1789
  },
1648
1790
  )
1791
+ queryResolvers->Dict.set("Platform_ApiFragments", apiFragmentsQueryResolver)
1649
1792
 
1650
1793
  // Composite-key / index admin query fields. Live loads from the seeded QueryDb stores.
1651
1794
  registerAdminItemsAndIndexResolvers(~queryResolvers, ~live=true)
@@ -1730,8 +1873,9 @@ module MakeWithConfig = (
1730
1873
  }
1731
1874
  )
1732
1875
  // UIFragment admin mutations — explicit admin-triggered fragment changes still
1733
- // publish onUIFragmentChange directly (aggregate-driven fragment changes are
1734
- // emitted by subscribeToPluginEvents). uiFragmentSubTopic is the shared topic.
1876
+ // publish onUIFragmentChange directly (slice-driven fragment changes are
1877
+ // emitted by subscribeToPluginEvents' admin-DCB subscription). uiFragmentSubTopic
1878
+ // is the shared topic.
1735
1879
  let makeUIEvent = (~pluginId, ~changeKind, ~manifest) =>
1736
1880
  JSON.Encode.object(
1737
1881
  Dict.fromArray([
@@ -1769,10 +1913,11 @@ module MakeWithConfig = (
1769
1913
  )
1770
1914
  adminRegisteredServers.contents->Array.push(platformGraphQL)
1771
1915
  // Register onUIFragmentChange + onPluginStatusChange subscriptions (Source C).
1916
+ // The SDL field constants are provider-neutral, shared with the AWS path.
1772
1917
  platformGraphQL.registerSubscriptions(
1773
1918
  ~sdlFields=[
1774
- " onUIFragmentChange: UIFragmentChangeEvent",
1775
- " onPluginStatusChange: PluginStatusChangeEvent",
1919
+ ReventlessCore.AdminApi.uiFragmentSubscriptionField,
1920
+ ReventlessCore.AdminApi.pluginStatusSubscriptionField,
1776
1921
  ],
1777
1922
  ~resolvers=Dict.fromArray([
1778
1923
  (
@@ -1892,8 +2037,8 @@ module MakeWithConfig = (
1892
2037
  ~resourceNaming=LocalPluginSpec.resourceNaming,
1893
2038
  ~api=(),
1894
2039
  ~apiRole=(),
1895
- ~stateChangeSlices=[],
1896
- ~stateViewSlices=[],
2040
+ ~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
2041
+ ~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
1897
2042
  ~automationSlices=[],
1898
2043
  ~outboundTranslationSlices=[],
1899
2044
  ~inboundTranslationSlices=[],
@@ -1925,6 +2070,7 @@ module MakeWithConfig = (
1925
2070
  "Platform_UIFragments",
1926
2071
  async (_root, _args, _ctx): JSON.t => JSON.Encode.array([]),
1927
2072
  )
2073
+ queryResolvers->Dict.set("Platform_ApiFragments", apiFragmentsQueryResolver)
1928
2074
  // Composite-key / index admin query fields — empty stubs in the platform-only
1929
2075
  // path (no plugins connected → no QueryDb seeded).
1930
2076
  registerAdminItemsAndIndexResolvers(~queryResolvers, ~live=false)
@@ -2058,8 +2204,8 @@ module MakeWithConfig = (
2058
2204
  ~resourceNaming=LocalPluginSpec.resourceNaming,
2059
2205
  ~api=(),
2060
2206
  ~apiRole=(),
2061
- ~stateChangeSlices=[],
2062
- ~stateViewSlices=[],
2207
+ ~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
2208
+ ~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
2063
2209
  ~automationSlices=[],
2064
2210
  ~outboundTranslationSlices=[],
2065
2211
  ~inboundTranslationSlices=[],
@@ -2099,23 +2245,19 @@ module MakeWithConfig = (
2099
2245
  queryResolvers->Dict.set(adminQueryEntry.listFieldName, async (_root, _args, _ctx): JSON.t =>
2100
2246
  connectionResponse([])
2101
2247
  )
2102
- // Platform_UIFragments — single-plugin path. Seeded by
2103
- // seedUIFragmentRegistryQueryDb below if the plugin component carries
2104
- // a uiFragments manifest, otherwise scans an empty store.
2248
+ // Platform_UIFragments — single-plugin path. Populated by the
2249
+ // UiFragments StateViewSlice when the plugin registers a manifest through
2250
+ // the admin EP (connectPlugin below), otherwise scans an empty store.
2105
2251
  queryResolvers->Dict.set(
2106
2252
  "Platform_UIFragments",
2107
2253
  async (_root, _args, _ctx): JSON.t => {
2108
- let items = switch Bus.getQueryDbScan(
2109
- ReventlessCore.UIFragmentRegistryReadModelSpec.name,
2110
- ) {
2254
+ let items = switch Bus.getQueryDbScan(ReventlessCore.UiFragments.name) {
2111
2255
  | Some(scanAll) => scanAll()
2112
2256
  | None => []
2113
2257
  }
2114
2258
  items
2115
2259
  ->Array.filterMap(item =>
2116
- switch item->S.parseOrThrow(
2117
- ReventlessCore.UIFragmentRegistryReadModelSpec.stateSchema,
2118
- ) {
2260
+ switch item->S.parseOrThrow(ReventlessCore.UiFragments.stateSchema) {
2119
2261
  | state =>
2120
2262
  Some(ReventlessCore.Platform_UIFragmentsApi.encodeUIFragmentEntry(state))
2121
2263
  | exception _ => None
@@ -2124,6 +2266,7 @@ module MakeWithConfig = (
2124
2266
  ->JSON.Encode.array
2125
2267
  },
2126
2268
  )
2269
+ queryResolvers->Dict.set("Platform_ApiFragments", apiFragmentsQueryResolver)
2127
2270
  // Platform_ComponentDefinitions resolver — SDL is already stitched into baseParts via
2128
2271
  // AdminApi.baseFragment so we register only the resolver here. Uses the shared
2129
2272
  // encoder so the dynamic-plugin admin server emits the same canonical shape as
@@ -2204,7 +2347,6 @@ module MakeWithConfig = (
2204
2347
  // Drive the admin Plugin read models via synthetic Connect dispatch through
2205
2348
  // LocalPluginAggregate (replaces the old direct-write seed).
2206
2349
  connectPlugin(~pluginComponents=[pluginComponent])
2207
- seedUIFragmentRegistryQueryDb(~pluginComponents=[pluginComponent])
2208
2350
  seedPluginStructuresStore(~pluginComponents=[pluginComponent])
2209
2351
 
2210
2352
  // Fire onPluginDeployed hooks so subscribers learn about this plugin.