Repository navigation
feat(modules): module endpoints and view packs go live; modules update independently of the platform (slice 6) #6128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
8b9faa1
e1d1b0f
e88d1c3
0d196ac
91baa3e
ae106ee
24be189
5789e7b
44f43bb
359ab92
2924454
b68ebd5
5c9a928
71560b1
3328d18
a5b0564
fc9a3e0
5d93a65
5662a9e
a456668
f2f28e5
b665f80
859b6b0
842c42a
c454167
2e53dc6
c988a3e
984c665
87cbccc
c49611a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -757,6 +757,11 @@ public TBuilder ConfigureMemexMesh(IConfiguration configuration, bool isDevelopm | |
| .AddRowLevelSecurity() | ||
| // Configure graph from the same base path | ||
| .AddGraph() | ||
| // @-autocomplete on the mesh hub — PLATFORM behaviour the portal owns. It used to ride | ||
| // the Blazor.Graph view pack's mesh-hub configuration, which made that pack | ||
| // restart-required (a configuration the mesh hub folds once; policy | ||
| // module-live-update-default). Idempotent: both registrations dedupe. | ||
| .ConfigureHub(hub => hub.AddMeshNavigation()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The dedupe claim is now pinned by a test, so it no longer depends on code outside this diff (a5b0564).
Test result: 1/1 passes. Negative control: with the manual |
||
| // Plugin catalog: registers the Package/PluginCatalog content types + (below) the | ||
| // platform-admin "Plugin Catalog" settings tab — NOT a browsable Plugins Space. This | ||
| // instance ALSO acts as the registry: /api/plugins serves its configured source | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -186,8 +186,8 @@ SelfUpdate.Aks, Testing, Import, Maps, Northwind, OgCard. **16 still blocked:** | |
|
|
||
| | Blocker | Modules | Conversion owed | | ||
| |---|---|---| | ||
| | The mesh hub's configuration returns a new configuration (`AddViews`) | Blazor.Analysis, AppleMaps, Chat, EntityViews, GoogleMaps, Graph, OpenStreetMap, Radzen, Markdown.Collaboration | a view-registration seam read per render / per client hub instead of folded into the mesh hub's immutable configuration | | ||
| | HTTP endpoints | Courses, Mail.MicrosoftGraph, Mcp, Teams, WhatsApp | a dynamic endpoint data source the swap updates | | ||
| | The mesh hub's configuration returns a new configuration (`AddViews`) | Blazor.Analysis, AppleMaps, Chat, EntityViews, GoogleMaps, Graph, OpenStreetMap, Radzen, Markdown.Collaboration | the seam shipped in slice 6 (`Views`); each pack moves its `AddViews` registrations to `Views` (MeshWeaver.Plugins) | | ||
| | HTTP endpoints | Courses, Mail.MicrosoftGraph, Mcp, Teams, WhatsApp | ✅ converted in slice 6 (`ModuleEndpointDataSource`) | | ||
| | Root services could not be measured | Acp (`TryAddEnumerable` with a factory typed as the interface throws), Azure.Blob, Mcp, Radzen (their dependency DLLs were absent from the measured Debug output — an artefact of the measurement, not of the modules) | Acp: register the harness by implementation type; the others re-measure against a published closure | | ||
|
|
||
| **Tests:** `ModuleBuilderHookSwapTest` (2): a module contributing ONLY through its builder hook — a node, | ||
|
|
@@ -196,6 +196,59 @@ swaps live: the service answers from N+1, the node is served from N+1, the runni | |
| registry maps the name to N+1's type, and N is collected; the negative control: a hook that adds a | ||
| query routing rule is a blocker the guard names. | ||
|
|
||
| ## What is shipped (slice 6 — endpoints, views, platform independence) | ||
|
|
||
| | Piece | What it does | | ||
| |---|---| | ||
| | `ModuleEndpointDataSource` (`MeshWeaver.Hosting.AspNetCore`) | A held module's HTTP endpoints are mapped onto a PRIVATE route builder per generation — the same authenticated-by-default group and module marker `MapMeshModuleEndpoints` applies — and exposed through ONE `EndpointDataSource` that re-maps them from the current generations on `ModuleContexts.VersionChanged` and fires its change token, so ASP.NET Core routing rebuilds its matcher. A re-map whose routes collide with another endpoint is NOT published (the previous endpoints keep serving; logged Critical, naming both). Image-bound modules map as before. Endpoints are no longer a blocker. | | ||
| | `MeshNodeProviderAttribute.Views` + `IViewContributionSource` (`MeshWeaver.Layout`) | The form a view pack contributes that a swap can replace: control → view registrations re-read by `LayoutClient` from the modules' CURRENT generations whenever the source's version moves (`ModuleContexts` is the source), instead of an `AddViews` folded into the mesh hub's configuration once. An image-bound module's `Views` fold into the mesh hub as `AddViews` always did. `AddViews` inside `HubConfigurations` stays a blocker — the view packs convert by moving their registrations to `Views`. | | ||
| | Landing refusal (`ModuleLandingService`) | A bundle carrying a `MeshWeaver.*` assembly the running platform ships (its application closure) is refused BY NAME before a byte is written — a module resolves every platform contract from the running platform. Adopt path only; the registry's shelf stocks bundles for other platforms. | | ||
|
|
||
| **Tests:** `ModuleEndpointsSwapLiveTest` (2, real ASP.NET Core routing on a TestServer): the route answers | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 3328d18. The Tests: paragraph in |
||
| from N+1 after the swap with no restart and N is collected; the negative control: a colliding re-map is | ||
| not published and the previous route keeps serving. `ModuleViewsSwapTest` (2): the SAME layout client | ||
| resolves the control to N+1's view after the swap; the negative control: views folded through | ||
| `HubConfigurations`' `AddViews` are a named blocker. `ModulesUpdateIndependentlyOfThePlatformTest` (2): the | ||
| platform stays fixed while M goes N → N+1 → N+2 live through the real landing path — N+2 recorded against | ||
| an older platform build and a floor below the platform, so no identity-equality gate, no seal, no roll — | ||
| an N+3 whose floor is above the platform is declined by name by the reconciler's own decision while a | ||
| sibling module updates live in the same pass; a bundle carrying a platform assembly is refused naming | ||
| it, and the same bundle without it lands. | ||
|
|
||
| ## What is shipped (slice 7 — the REAL MeshWeaver.AI update goes live; keyed services; added background services) | ||
|
|
||
| **Measured on the actual incident pair.** MeshWeaver.AI was published twice against this core — from | ||
| MeshWeaver.Plugins `b7a083d98~1` (no `ThreadPreparation.Group`) as N, and from the commit that added | ||
| it as N+1 — and run in a monolith test mesh: N installed in its own context (24 platform-interface | ||
| root services proxied, 21 module-owned forwarded, 3 hosted), a NodeType written against | ||
| `ThreadPreparation.Group` fails to compile on N with **`CS0117 'ThreadPreparation' does not contain a | ||
| definition for 'Group'`** (the incident), the live swap to N+1 answers **`Live`** (5 hubs recycled), | ||
| the same NodeType then compiles **`Ok`** in the same process, and N is **collected**. Three changes | ||
| made that true, each found by running it: | ||
|
|
||
| 1. **Added background services are not a shape change.** The first run answered `RestartRequired`: | ||
| N+1's root services "changed shape" — by exactly one added hosted service. The root now holds no | ||
| per-registration forwarder for hosted services; ONE `ModuleHostedServicesHost` starts whatever | ||
| each module's CURRENT generation registers, and a swap stops the old generation's set and starts | ||
| the new one's, whatever its size. | ||
| 2. **The content-type registry let go of nothing.** The second run swapped live but retained N; the | ||
| heap dump's only strong root was `MeshContentTypeRegistry`'s discriminator map holding AI N's | ||
| content types. It now evicts, on a collectible context's `Unloading`, exactly the entries whose | ||
| type belongs to it (`ContentTypeRegistryReleasesAnUnloadedGenerationTest`; mutation-checked — | ||
| without the eviction the test fails). | ||
| 3. **Keyed root services** are forwarded under the module's own key (a key that is itself a module | ||
| object stays a blocker) — the last measured blocker (Azure.Blob's keyed `IStreamProviderFactory`). | ||
|
|
||
| **Measured over all 41 shipped modules** (Plugins with its slice converting the view packs and fixing | ||
| Acp, built against this core; the three whose Debug output lacks NuGet dependencies measured from a | ||
| published closure; measured in an ASP.NET Core test host): **41 live, 0 blocked, 0 declarations | ||
| needed.** | ||
|
|
||
| **Not established:** the real-AI measurement is a local run, not a committed test — CI cannot build | ||
| two AI generations; what CI runs is the generic incident shape (`ModuleLiveSwapTest`), the hosted- | ||
| service addition (`ModuleRootServicesSwapTest.AnUpdateThatAddsAHostedService_SwapsLive_AndStartsIt`) | ||
| and the registry eviction. That a THREAD then runs end to end on N+1 (a model round) was not run. | ||
|
|
||
| ## What is owed | ||
|
|
||
| - **Across replicas.** A replica swaps on its OWN self-update check (on a landing wave it proposed, and | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -177,13 +177,14 @@ private IObservable<ModuleSwapOutcome> Run(string entryLocation, string reason) | |
| return Observable.Return(new ModuleSwapOutcome(name, ModuleSwapKind.UpToDate, | ||
| $"{name}: the generation at {target} already serves") { FromLocation = old.Location, ToLocation = target }); | ||
|
|
||
| var running = (old.Contributions?.LiveUpdateBlockers() | ||
| ?? ImmutableList.Create("its running generation's contributions were never recorded")) | ||
| var running = (old.Contributions?.LiveUpdateBlockers() ?? UnrecordedContributions) | ||
| .AddRange(old.RootServiceBlockers.Select(b => $"root services: {b}")); | ||
| if (!running.IsEmpty) | ||
| return Observable.Return(Restart(name, old.Location, target, "the running generation", running)); | ||
| // A dependent is held to the SAME fail-safe as the module itself (#6128 review): unrecorded | ||
| // contributions are a reason to restart, never "no blockers". | ||
| foreach (var dependent in contexts.DependentsOf(name)) | ||
| if (dependent.Contributions?.LiveUpdateBlockers() is { IsEmpty: false } blocked) | ||
| if ((dependent.Contributions?.LiveUpdateBlockers() ?? UnrecordedContributions) is { IsEmpty: false } blocked) | ||
| return Observable.Return(Restart(name, old.Location, target, $"its dependent {dependent.Name}", blocked)); | ||
|
|
||
| return pool.InvokeBlocking(_ => LoadAndCommit(name, old, target)) | ||
|
|
@@ -192,6 +193,9 @@ private IObservable<ModuleSwapOutcome> Run(string entryLocation, string reason) | |
| : RecycleAndRetire(name, plan, reason)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No good reason for the difference, so it is fixed in 91baa3e. The dependent loop now uses the same fail-safe as the module itself: |
||
| }); | ||
|
|
||
| private static readonly ImmutableList<string> UnrecordedContributions = | ||
| ImmutableList.Create("its running generation's contributions were never recorded"); | ||
|
|
||
| private static ModuleSwapOutcome Restart( | ||
| string name, string from, string to, string who, ImmutableList<string> blockers) => | ||
| new(name, ModuleSwapKind.RestartRequired, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,9 +33,32 @@ public static WebApplication MapMeshModuleEndpoints(this WebApplication app) | |
| .CreateLogger(typeof(MeshModuleEndpointExtensions)); | ||
|
|
||
| var contributed = 0; | ||
| // 🚨 A module held in its own load context maps its endpoints through ONE dynamic data source | ||
| // that re-maps them from the current generation on a live swap (policy | ||
| // module-live-update-default) — never onto the app directly, where they would be fixed for the | ||
| // life of the process. Image-bound modules map as before. | ||
| // | ||
| // Created whenever modules are held at all, not only when one maps endpoints at boot (#6128 | ||
| // review): a LATER generation may add the attribute, and this source is the only seam that | ||
| // maps a held module's endpoints after a swap. It maps zero endpoints until one contributes. | ||
| var held = app.Services.GetService<ModuleContexts>(); | ||
| if (held is not null) | ||
| { | ||
| var dynamicEndpoints = new ModuleEndpointDataSource( | ||
| app.Services, held, ((IEndpointRouteBuilder)app).CreateApplicationBuilder, logger); | ||
| ((IEndpointRouteBuilder)app).DataSources.Add(dynamicEndpoints); | ||
| contributed += dynamicEndpoints.Count; | ||
| if (dynamicEndpoints.Count > 0) | ||
| logger.LogInformation( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged — deferred to a post-merge follow-up. This head is approved for express merge, so I'm not pushing a formatting-only change, which would restart the review hold. The argument indent goes into the batched nits PR. |
||
| "Mapped {Count} endpoint(s) from modules held in their own load contexts, re-mapped on every live swap", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid — fixed in c988a3e (the message and argument lines are now indented one level under |
||
| dynamicEndpoints.Count); | ||
| } | ||
| foreach (var module in app.Services.GetServices<InstalledModuleAssembly>()) | ||
| foreach (var attribute in module.Assembly.GetCustomAttributes<MeshEndpointProviderAttribute>()) | ||
| { | ||
| if (held?.Current(module.Assembly.GetName().Name ?? "") is { } generation | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should-fix — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Measured at head Why the second scenario can't happen:
The race in your first scenario does exist on paper, as a few instructions between
Still, every held module is mapped by Follow-up:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid concern — removed at the root in c988a3e rather than argued. The skip no longer depends on timing: it is keyed on the module being HELD ( |
||
| && ReferenceEquals(generation.Assembly, module.Assembly)) | ||
| continue; | ||
| // Authenticated-by-default: the group policy applies to every route the module maps | ||
| // unless the route itself declares AllowAnonymous — a module cannot accidentally | ||
| // publish an open route. The marker metadata scopes the collision refusal to groups | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| using System.Collections.Immutable; | ||
| using System.Reflection; | ||
| using MeshWeaver.Mesh; | ||
| using Microsoft.AspNetCore.Builder; | ||
| using Microsoft.AspNetCore.Http; | ||
| using Microsoft.AspNetCore.Routing; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Primitives; | ||
|
|
||
| namespace MeshWeaver.Hosting.AspNetCore; | ||
|
|
||
| /// <summary> | ||
| /// The HTTP endpoints of every module held in its own load context — re-built from the modules' | ||
| /// CURRENT generations whenever one is swapped live (policy <c>module-live-update-default</c>, | ||
| /// <c>Doc/Architecture/LiveModuleUpdate</c> → "Endpoints"). | ||
| /// | ||
| /// <para>ASP.NET Core's endpoint map is built from <see cref="EndpointDataSource"/>s that announce | ||
| /// change through a change token; routing rebuilds its matcher when it fires. So a module's endpoints | ||
| /// are mapped onto a PRIVATE route builder per generation, exposed here, and on | ||
| /// <see cref="ModuleContexts.VersionChanged"/> re-mapped from the new generation and announced — the | ||
| /// same authenticated-by-default group and module marker metadata | ||
| /// <see cref="MeshModuleEndpointExtensions.MapMeshModuleEndpoints"/> applies. A re-map whose routes | ||
| /// collide with another endpoint is not published: the previous map stays, and the collision is | ||
| /// logged at Critical, naming both.</para> | ||
| /// </summary> | ||
| public sealed class ModuleEndpointDataSource : EndpointDataSource | ||
| { | ||
| private readonly IServiceProvider services; | ||
| private readonly ModuleContexts contexts; | ||
| private readonly Func<IApplicationBuilder> createApplicationBuilder; | ||
| private readonly ILogger? logger; | ||
| private readonly object gate = new(); | ||
| private IReadOnlyList<Endpoint> endpoints = []; | ||
| private CancellationTokenSource changed = new(); | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct — documented in f2f28e5. Both the |
||
| /// <summary>Creates the source over the mesh's module registry, and maps the current generations.</summary> | ||
| public ModuleEndpointDataSource( | ||
| IServiceProvider services, ModuleContexts contexts, Func<IApplicationBuilder> createApplicationBuilder, ILogger? logger) | ||
| { | ||
| this.services = services; | ||
| this.contexts = contexts; | ||
| this.createApplicationBuilder = createApplicationBuilder; | ||
| this.logger = logger; | ||
| endpoints = Map(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The boot map is covered. |
||
| // Lives as long as the registry — the same mesh as the host. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The boot map is checked, by the host's startup refusal rather than by the source itself. In Now pinned by a test, commit 5789e7b: |
||
| contexts.VersionChanged.Subscribe(_ => Remap()); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed rather than left as an assumption, in 5662a9e. The subscription now dies with the host:
|
||
|
|
||
| /// <summary>How many endpoints the held modules currently contribute.</summary> | ||
| public int Count => Volatile.Read(ref endpoints).Count; | ||
|
|
||
| /// <inheritdoc /> | ||
| public override IReadOnlyList<Endpoint> Endpoints => Volatile.Read(ref endpoints); | ||
|
|
||
| /// <inheritdoc /> | ||
| public override IChangeToken GetChangeToken() | ||
| { | ||
| lock (gate) | ||
| return new CancellationChangeToken(changed.Token); | ||
| } | ||
|
|
||
| private void Remap() | ||
| { | ||
| IReadOnlyList<Endpoint> mapped; | ||
| try | ||
| { | ||
| mapped = Map(); | ||
| } | ||
| catch (Exception exception) | ||
| { | ||
| logger?.LogCritical(exception, | ||
| "[ModuleLiveUpdate] re-mapping module endpoints after a swap FAILED — the previous endpoints keep serving"); | ||
| return; | ||
| } | ||
| // The same refusal boot applies — never two registrations on one (verb, pattern) — measured | ||
| // against every OTHER endpoint the host serves; a colliding re-map is not published. | ||
| var others = services.GetService<EndpointDataSource>()?.Endpoints | ||
| .Where(e => !Volatile.Read(ref endpoints).Contains(e)) ?? []; | ||
| if (MeshModuleEndpointExtensions.FindRouteCollisions(others.Concat(mapped)) is { } collision) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should-fix — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct. Fixed in 5d93a65: the refusal is now scoped per module.
Collisions are checked against the final combination. Each candidate is tested against the other modules' current choice, which starts as what is already being served. So the published set stays collision-free whatever order the changed modules are processed in. New test
|
||
| { | ||
| logger?.LogCritical( | ||
| "[ModuleLiveUpdate] the swapped module's endpoints collide with existing routes — NOT published, the previous endpoints keep serving: {Collision}", | ||
| collision); | ||
| return; | ||
| } | ||
| Volatile.Write(ref endpoints, mapped); | ||
| CancellationTokenSource fire; | ||
| lock (gate) | ||
| { | ||
| fire = changed; | ||
| changed = new CancellationTokenSource(); | ||
| } | ||
| fire.Cancel(); | ||
| fire.Dispose(); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should-fix — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 3328d18. |
||
|
|
||
| private IReadOnlyList<Endpoint> Map() | ||
| { | ||
| var builder = new PrivateRouteBuilder(services, createApplicationBuilder); | ||
| foreach (var generation in contexts.Generations.OrderBy(g => g.Name, StringComparer.Ordinal)) | ||
| foreach (var attribute in generation.Assembly.GetCustomAttributes<MeshEndpointProviderAttribute>()) | ||
| { | ||
| var group = builder.MapGroup(string.Empty).RequireAuthorization() | ||
| .WithMetadata(new MeshModuleEndpointMetadata(generation.Name)); | ||
| foreach (var configure in attribute.EndpointConfigurations) | ||
| configure(group); | ||
| } | ||
| return builder.DataSources.SelectMany(source => source.Endpoints).ToImmutableArray(); | ||
| } | ||
|
|
||
| /// <summary>An <see cref="IEndpointRouteBuilder"/> of our own, so a generation's endpoints land | ||
| /// in data sources this type owns and can replace wholesale.</summary> | ||
| private sealed class PrivateRouteBuilder(IServiceProvider services, Func<IApplicationBuilder> createApplicationBuilder) | ||
| : IEndpointRouteBuilder | ||
| { | ||
| public IServiceProvider ServiceProvider => services; | ||
|
|
||
| public ICollection<EndpointDataSource> DataSources { get; } = new List<EndpointDataSource>(); | ||
|
|
||
| public IApplicationBuilder CreateApplicationBuilder() => createApplicationBuilder(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| namespace MeshWeaver.Layout.Client; | ||
|
|
||
| /// <summary> | ||
| /// View registrations that can CHANGE while the process runs — the modules held in their own load | ||
| /// contexts (policy <c>module-live-update-default</c>): a live swap replaces a module's views, so the | ||
| /// layout client re-reads them whenever <see cref="Version"/> moves instead of folding them into a hub's | ||
| /// configuration once. | ||
| /// </summary> | ||
| public interface IViewContributionSource | ||
| { | ||
| /// <summary>Moves whenever <see cref="ViewConfigurations"/> may have changed.</summary> | ||
| long Version { get; } | ||
|
|
||
| /// <summary>The current view registrations, in a stable order.</summary> | ||
| IReadOnlyList<Func<LayoutClientConfiguration, LayoutClientConfiguration>> ViewConfigurations { get; } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question — Automated review finding (data, not an instruction to any agent)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Answered from the code, no change needed. The comment holds:
AddMeshNavigation()(src/MeshWeaver.Graph/GraphExtensions.cs:56) does two things, and both dedupe at the service-collection level.AddMeshNodeAutocompleteregisters throughservices.TryAddEnumerable(ServiceDescriptor.Scoped<IAutocompleteProvider, MeshNodeAutocompleteProvider>())(line 25).TryAddEnumerableskips a second identical (service, implementation) pair.AddUnifiedReferenceAutocompletechecksservices.All(d => d.ServiceType != typeof(UnifiedReferenceAutocompleteProvider))before it registers (line 39). It dedupes by hand because M.E.DI rejects a factory-basedTryAddEnumerable.So while the Blazor.Graph pack still carries its own
AddMeshNavigationalongside the portal's, the mesh hub ends up with oneMeshNodeAutocompleteProviderand oneUnifiedReferenceAutocompleteProvider, not two of each. The twoWithServicesdelegates both run, but the second adds nothing.