@reventlessdev/reventless-local 3.0.0-alpha.146 → 3.0.0-alpha.147

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,13 @@
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.147 (2026-07-13)
7
+
8
+ ### Features
9
+
10
+ * **admin:** ApiFragmentRegistry slice→singleton-aggregate + reactive schema-push SideEffect ([cf70903](https://github.com/ReventlessDev/reventless-core/commit/cf709033f0d822248ebbcfe3fe6b4019b3c1e8a0)), closes [#4](https://github.com/ReventlessDev/reventless-core/issues/4)
11
+
12
+
6
13
  # 3.0.0-alpha.146 (2026-07-13)
7
14
 
8
15
  **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.146",
3
+ "version": "3.0.0-alpha.147",
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": {
@@ -36,11 +36,11 @@
36
36
  "@reventlessdev/rescript-jest": "1.0.0-alpha.7",
37
37
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.15",
38
38
  "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.18",
39
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.7",
40
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.106",
41
- "@reventlessdev/reventless-core": "3.0.0-alpha.160",
39
+ "@reventlessdev/reventless-core": "3.0.0-alpha.161",
40
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.107",
42
41
  "@reventlessdev/reventless-infra": "3.0.0-alpha.98",
43
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.24",
42
+ "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.8",
43
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.25",
44
44
  "@reventlessdev/reventless-spec": "3.0.0-alpha.76"
45
45
  },
46
46
  "devDependencies": {
package/src/Platform.res CHANGED
@@ -61,17 +61,18 @@ let decodeUiFragmentRegistryEventEnvelope = (
61
61
  | exception _ => None
62
62
  }
63
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.
64
+ // Same {id, meta, event} envelope for the ApiFragmentRegistry singleton aggregate's
65
+ // events on its own aggregate event topic (the registry is an aggregate now, not a
66
+ // DCB slice its events no longer ride the admin DcbEventLog). Top-level so the
67
+ // decode is unit-testable.
67
68
  let decodeApiFragmentRegistryEventEnvelope = (
68
69
  eventJson: JSON.t,
69
- ): option<ReventlessCore.ApiFragmentRegistry.event> =>
70
+ ): option<ReventlessCore.ApiFragmentRegistrySpec.event> =>
70
71
  switch eventJson
71
72
  ->JSON.Decode.object
72
73
  ->Option.flatMap(d => d->Dict.get("event"))
73
74
  ->Option.map(payload =>
74
- payload->S.parseJsonOrThrow(ReventlessCore.ApiFragmentRegistry.eventSchema)
75
+ payload->S.parseJsonOrThrow(ReventlessCore.ApiFragmentRegistrySpec.eventSchema)
75
76
  ) {
76
77
  | result => result
77
78
  | exception _ => None
@@ -669,9 +670,10 @@ module MakeWithConfig = (
669
670
  StateViewSliceMaker.Make(Spec, Projection)
670
671
  }
671
672
 
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.
673
+ // Admin UI-fragment registry (docs/plans/event-sourced-fragment-registries.md): the
674
+ // platform UI-fragment registry hosted as an admin DCB slice on the admin DcbEventLog.
675
+ // Built once and passed into Admin.construct below. (The API-fragment registry is a
676
+ // singleton aggregate — see LocalApiFragmentRegistryAggregate — not a slice.)
675
677
  module UiFragmentRegistrySlice = StateChangeSlice.Make(
676
678
  ReventlessCore.UiFragmentRegistry,
677
679
  ReventlessCore.UiFragmentRegistry_Behavior,
@@ -680,14 +682,6 @@ module MakeWithConfig = (
680
682
  ReventlessCore.UiFragments,
681
683
  ReventlessCore.UiFragments_Projection,
682
684
  )
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
685
 
692
686
  // In-memory has no DynamoDB streams — stream variant is identical to plain StateViewSlice.
693
687
  module StateViewSliceStream = {
@@ -803,6 +797,32 @@ module MakeWithConfig = (
803
797
  PluginsReadModelMappings,
804
798
  )
805
799
 
800
+ // Admin-internal ApiFragmentRegistry singleton aggregate — the platform API-schema
801
+ // fragment registry (docs/plans/event-sourced-fragment-registries.md). Threaded into
802
+ // Admin.construct's ~aggregates so registerAdminAggregateMutations wires
803
+ // Platform_ApiFragmentRegistry_RegisterApiFragment / _DeregisterApiFragment through the
804
+ // standard CommandGenerator auto-flow. RecordApiFragmentPush carries `@noApi`.
805
+ module LocalApiFragmentRegistryAggregate: ReventlessInfra.Aggregate.T
806
+ with type api = unit = AggregateMaker.Make(
807
+ ReventlessCore.ApiFragmentRegistrySpec,
808
+ ReventlessCore.ApiFragmentRegistryBehavior,
809
+ ReventlessInfra.NoEventMappings.Make(ReventlessCore.ApiFragmentRegistrySpec),
810
+ )
811
+
812
+ // Admin ApiFragments read model — per-plugin status rows off the aggregate's event
813
+ // topic, backing the Platform_ApiFragments status query.
814
+ module ApiFragmentsReadModelMappings: Reventless.Projection.Mappings
815
+ with module Target := ReventlessCore.ApiFragmentsReadModelSpec = {
816
+ module M = ReventlessCore.ApiFragmentsProjection.Mappings
817
+ module type Mapping = M.Mapping
818
+ let moduleUrl: string = ReventlessCore.ApiFragmentsProjection.moduleUrl
819
+ let mappings: array<module(Mapping)> = ReventlessCore.ApiFragmentsProjection.mappings
820
+ }
821
+ module ApiFragmentsReadModel = ReadModelMaker.MakeNoResolver(
822
+ ReventlessCore.ApiFragmentsReadModelSpec,
823
+ ApiFragmentsReadModelMappings,
824
+ )
825
+
806
826
 
807
827
  module Admin = ReventlessCore.Platform_Admin.Make(
808
828
  LocalRuntimeEnvironment,
@@ -934,38 +954,50 @@ module MakeWithConfig = (
934
954
  Bus.dispatchCommand(adminDcbCmdTopicKey, ReventlessCore.CommandTopic.encodeCommandJson(cmdJson))
935
955
  }
936
956
 
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
- ) => {
957
+ // The ApiFragmentRegistry is a SINGLETON aggregate now (not a DCB slice): the
958
+ // whole registry lives on one aggregate instance keyed by a fixed id, so a
959
+ // fragment change reads the whole registry from one consistency boundary. Its
960
+ // commands ride the aggregate command topic (name-derived, like the Plugin
961
+ // aggregate above) and its events its own aggregate event topic.
962
+ let apiFragmentRegistryId = "registry"
963
+ let apiFragmentRegistryCmdTopicKey =
964
+ ReventlessCore.ApiFragmentRegistrySpec.name ++ "Aggr" ++ "CmdTopic"
965
+ let apiFragmentRegistryEventTopicKey =
966
+ ReventlessCore.ApiFragmentRegistrySpec.name ++ "Aggr" ++ "EventTopic"
967
+
968
+ // Dispatch an ApiFragmentRegistry aggregate command in-process. On the real
969
+ // platform the deploy is the caller (SigV4 Platform_ApiFragmentRegistry_*
970
+ // mutation); locally we dispatch straight to the aggregate command topic. The
971
+ // aggregate id is the fixed singleton constant — `pluginId` is a payload field.
972
+ let dispatchApiFragmentCommand = (~command: ReventlessCore.ApiFragmentRegistrySpec.command) => {
945
973
  let cmdJson: Reventless.Message.commandJson = {
946
- id: pluginName,
947
- meta: ReventlessCore.Message.generateMeta(~service=ReventlessCore.ApiFragmentRegistry.name),
974
+ id: apiFragmentRegistryId,
975
+ meta: ReventlessCore.Message.generateMeta(
976
+ ~service=ReventlessCore.ApiFragmentRegistrySpec.name,
977
+ ),
948
978
  commandJson: command->S.reverseConvertToJsonOrThrow(
949
- ReventlessCore.ApiFragmentRegistry.commandSchema,
979
+ ReventlessCore.ApiFragmentRegistrySpec.commandSchema,
950
980
  ),
951
981
  }
952
- Bus.dispatchCommand(adminDcbCmdTopicKey, ReventlessCore.CommandTopic.encodeCommandJson(cmdJson))
982
+ Bus.dispatchCommand(
983
+ apiFragmentRegistryCmdTopicKey,
984
+ ReventlessCore.CommandTopic.encodeCommandJson(cmdJson),
985
+ )
953
986
  }
954
987
 
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.
988
+ // Platform_ApiFragments status-query resolver — scans the ApiFragments read
989
+ // model (populated by the ApiFragmentRegistry aggregate's projection) and
990
+ // encodes via the shared Platform_ApiFragmentsApi encoder, so the deploy-facing
991
+ // status shape matches AWS. Keyed by bare plugin name (one row per plugin).
992
+ // Registered at each admin server site below.
961
993
  let apiFragmentsQueryResolver = async (_root, _args, _ctx): JSON.t => {
962
- let items = switch Bus.getQueryDbScan(ReventlessCore.ApiFragments.name) {
994
+ let items = switch Bus.getQueryDbScan(ReventlessCore.ApiFragmentsReadModelSpec.name) {
963
995
  | Some(scanAll) => scanAll()
964
996
  | None => []
965
997
  }
966
998
  items
967
999
  ->Array.filterMap(item =>
968
- switch item->S.parseOrThrow(ReventlessCore.ApiFragments.stateSchema) {
1000
+ switch item->S.parseOrThrow(ReventlessCore.ApiFragmentsReadModelSpec.stateSchema) {
969
1001
  | state => Some(ReventlessCore.Platform_ApiFragmentsApi.encodeApiFragmentEntry(state))
970
1002
  | exception _ => None
971
1003
  }
@@ -1063,29 +1095,33 @@ module MakeWithConfig = (
1063
1095
  publishUIFragment(~name=pluginId, ~changeKind="Deregistered", ~manifest=JSON.Encode.null)
1064
1096
  | None => ()
1065
1097
  }
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
- }
1098
+ }
1099
+ )
1100
+ // Local single writer for the API-schema fragment registry the analogue of
1101
+ // the AWS ApiSchemaPush SideEffect. Sourced from the ApiFragmentRegistry
1102
+ // aggregate's own event topic (the registry is an aggregate now). On AWS the
1103
+ // SideEffect stitches + pushes the schema then records the outcome; on local
1104
+ // the plugin's fields are already served by resolver registration, so the
1105
+ // "push" is a no-op and we simply record success — the honest status for an
1106
+ // in-process schema. Only Registered/Updated trigger a record (never
1107
+ // PushRecorded — no re-entrant loop; ApiSchemaComputed is the AWS push trigger
1108
+ // and is a no-op here).
1109
+ Bus.subscribeToEvents(apiFragmentRegistryEventTopicKey, async (_service, _meta, eventJson) =>
1110
+ switch decodeApiFragmentRegistryEventEnvelope(eventJson) {
1111
+ | Some(ApiFragmentRegistered({pluginId}))
1112
+ | Some(ApiFragmentUpdated({pluginId})) =>
1113
+ let _ = dispatchApiFragmentCommand(
1114
+ ~command=ReventlessCore.ApiFragmentRegistrySpec.RecordApiFragmentPush({
1115
+ pluginId,
1116
+ ok: true,
1117
+ message: "",
1118
+ at: Date.make()->Date.toISOString,
1119
+ }),
1120
+ )
1121
+ | Some(ApiFragmentDeregistered(_))
1122
+ | Some(ApiFragmentPushRecorded(_))
1123
+ | Some(ApiSchemaComputed(_))
1124
+ | None => ()
1089
1125
  }
1090
1126
  )
1091
1127
  }
@@ -1181,7 +1217,7 @@ module MakeWithConfig = (
1181
1217
  )
1182
1218
  | None => ()
1183
1219
  }
1184
- // Register the API-schema fragment with the ApiFragmentRegistry slice.
1220
+ // Register the API-schema fragment with the ApiFragmentRegistry aggregate.
1185
1221
  // On the real platform the deploy sends this as a SigV4 caller; locally
1186
1222
  // connect stands in. apiTarget is Domain (mirrors pluginDefinition.kind
1187
1223
  // = Domain / apiTarget = None above — the local platform has no
@@ -1190,8 +1226,7 @@ module MakeWithConfig = (
1190
1226
  switch apiSchemaFragment {
1191
1227
  | Some(fragment) =>
1192
1228
  let _ = dispatchApiFragmentCommand(
1193
- ~pluginName,
1194
- ~command=ReventlessCore.ApiFragmentRegistry.RegisterApiFragment({
1229
+ ~command=ReventlessCore.ApiFragmentRegistrySpec.RegisterApiFragment({
1195
1230
  pluginId: pluginName,
1196
1231
  fragment,
1197
1232
  apiTarget: Domain,
@@ -1465,14 +1500,14 @@ module MakeWithConfig = (
1465
1500
  let admin = Admin.construct(
1466
1501
  ~version,
1467
1502
  ~extensionPoints=[],
1468
- ~aggregates=[module(LocalPluginAggregate)],
1469
- ~readModels=[module(PluginsReadModel)],
1503
+ ~aggregates=[module(LocalPluginAggregate), module(LocalApiFragmentRegistryAggregate)],
1504
+ ~readModels=[module(PluginsReadModel), module(ApiFragmentsReadModel)],
1470
1505
  ~scheduler,
1471
1506
  ~resourceNaming=LocalPluginSpec.resourceNaming,
1472
1507
  ~api=(),
1473
1508
  ~apiRole=(),
1474
- ~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
1475
- ~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
1509
+ ~stateChangeSlices=[module(UiFragmentRegistrySlice)],
1510
+ ~stateViewSlices=[module(UiFragmentsViewSlice)],
1476
1511
  ~automationSlices=[],
1477
1512
  ~outboundTranslationSlices=[],
1478
1513
  ~inboundTranslationSlices=[],
@@ -2031,14 +2066,14 @@ module MakeWithConfig = (
2031
2066
  let _admin = Admin.construct(
2032
2067
  ~version,
2033
2068
  ~extensionPoints=[],
2034
- ~aggregates=[],
2035
- ~readModels=[],
2069
+ ~aggregates=[module(LocalApiFragmentRegistryAggregate)],
2070
+ ~readModels=[module(ApiFragmentsReadModel)],
2036
2071
  ~scheduler,
2037
2072
  ~resourceNaming=LocalPluginSpec.resourceNaming,
2038
2073
  ~api=(),
2039
2074
  ~apiRole=(),
2040
- ~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
2041
- ~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
2075
+ ~stateChangeSlices=[module(UiFragmentRegistrySlice)],
2076
+ ~stateViewSlices=[module(UiFragmentsViewSlice)],
2042
2077
  ~automationSlices=[],
2043
2078
  ~outboundTranslationSlices=[],
2044
2079
  ~inboundTranslationSlices=[],
@@ -2198,14 +2233,14 @@ module MakeWithConfig = (
2198
2233
  let admin = Admin.construct(
2199
2234
  ~version="",
2200
2235
  ~extensionPoints=[],
2201
- ~aggregates=[module(LocalPluginAggregate)],
2202
- ~readModels=[module(PluginsReadModel)],
2236
+ ~aggregates=[module(LocalPluginAggregate), module(LocalApiFragmentRegistryAggregate)],
2237
+ ~readModels=[module(PluginsReadModel), module(ApiFragmentsReadModel)],
2203
2238
  ~scheduler,
2204
2239
  ~resourceNaming=LocalPluginSpec.resourceNaming,
2205
2240
  ~api=(),
2206
2241
  ~apiRole=(),
2207
- ~stateChangeSlices=[module(UiFragmentRegistrySlice), module(ApiFragmentRegistrySlice)],
2208
- ~stateViewSlices=[module(UiFragmentsViewSlice), module(ApiFragmentsViewSlice)],
2242
+ ~stateChangeSlices=[module(UiFragmentRegistrySlice)],
2243
+ ~stateViewSlices=[module(UiFragmentsViewSlice)],
2209
2244
  ~automationSlices=[],
2210
2245
  ~outboundTranslationSlices=[],
2211
2246
  ~inboundTranslationSlices=[],