diff --git a/docs/requirements/Component-TemplateEngine.md b/docs/requirements/Component-TemplateEngine.md index 7536cf76..ce087ac7 100644 --- a/docs/requirements/Component-TemplateEngine.md +++ b/docs/requirements/Component-TemplateEngine.md @@ -219,17 +219,22 @@ names, locks and edges: `InstanceService.SetAlarmOverrideAsync` (resolving one a name) and `TemplateDeletionService.CanDeleteTemplateAsync` (inheritor / derived / composer checks). `TemplateFolderService.DeleteFolderAsync`, which only counts templates per `FolderId`, reads the lighter `GetTemplateStubsAsync` identity projection -(review 2026-10-21 04/P6, 08/P1). Template member add/update paths still load the whole -analysis projection for collision detection; narrowing that to the edited template's -chain + composition closure needs a closure-scoped repository read that does not exist -yet. +(review 2026-10-21 04/P6, 08/P1). Template member add/update paths read only the +edited template's inheritance chain + composition closure through +`GetTemplateAnalysisClosureAsync` (the same projection, one query per closure depth — +Gitea #66). -The tracked, body-bearing `GetAllTemplatesAsync` remains the read for the two paths -that genuinely need it — the inheritance reconciler (which compares and copies -script bodies and writes through the loaded entities) and the flattener (which must -observe rows an in-flight bundle import has staged on the shared change tracker). -`ReconcileDescendantsAsync` additionally accepts an already-loaded tracked graph so -a caller holding one does not force a second load. +The tracked, body-bearing shape remains the read for the two paths that genuinely need +it — the inheritance reconciler (which compares and copies script bodies and writes +through the loaded entities) and the flattener (which must observe rows an in-flight +bundle import has staged on the shared change tracker). The automatic post-mutation +`ReconcileDescendantsAsync` reads only `GetTemplateReconcileClosureAsync` — the edited +template's derived subtree plus its own inheritance chain, tracked and fully included, +one load per subtree depth and chain level (Gitea #86). The reconcile is +inheritance-only, so every descendant's chain lies inside that set; a template with no +children costs a single empty probe query. It still accepts an already-loaded tracked +graph from a caller holding one. The operator `ResyncInheritedMembersAsync` and the +flattener keep reading the whole tracked graph via `GetAllTemplatesAsync`. ### Native Alarm Source Resolution diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs index 68146dd1..89c08489 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs @@ -69,6 +69,20 @@ public interface ITemplateEngineRepository /// A task that resolves to the closure templates without script bodies. Task> GetTemplateAnalysisClosureAsync(int templateId, CancellationToken cancellationToken = default); /// + /// Returns the TRACKED, fully-loaded templates (the shape: + /// attributes, alarms, scripts with bodies, compositions, native alarm sources) that the + /// inherited-member reconcile of 's descendants reads: its whole + /// derived subtree plus its own inheritance chain (see + /// ). + /// Backs TemplateService.ReconcileDescendantsAsync (Gitea #86), which previously + /// loaded every template on each base-member add/update/delete. Returns an empty list when the + /// template has no children — there is nothing to reconcile — after a single probe query. + /// + /// The template whose member set changed. + /// Cancellation token. + /// A task that resolves to the subtree followed by the template's chain. + Task> GetTemplateReconcileClosureAsync(int templateId, CancellationToken cancellationToken = default); + /// /// Returns an untracked identity/edge projection of every template (no children), ordered by name. /// /// Cancellation token. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateReconcileClosure.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateReconcileClosure.cs new file mode 100644 index 00000000..41f52b61 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Templates/TemplateReconcileClosure.cs @@ -0,0 +1,103 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Templates; + +/// +/// Computes the set of templates the inherited-member reconcile reads when a base member of one +/// template changes (Gitea #86): the template's whole derived subtree (children, +/// grandchildren, …) plus the template's own inheritance chain (itself, its parent, +/// grandparent, …). +/// +/// +/// This is the descendant-direction counterpart of (#66). +/// TemplateService.ReconcileDescendantsAsync reconciles every descendant D by +/// resolving D's inheritance chain (TemplateInheritanceResolver.Resolve), which is +/// inheritance only — compositions are never followed. D's chain is the path from +/// D up to the edited template (all inside the subtree) followed by the edited template's +/// own chain, and every member winner originates on that chain. So the subtree plus the root's +/// chain is everything the reconcile can read; the rest of the library is irrelevant. +/// +/// +/// +/// The walk is breadth-first by batch: one loadChildren call per subtree depth and one +/// loadByIds call per ancestor level, so a store-backed loader issues one query per +/// depth rather than one per template. When the template has no children — the common case — +/// the walk stops after the single children probe and returns an empty list, because there is +/// nothing to reconcile. Cycles terminate (each id is loaded at most once) and a dangling parent +/// edge simply ends the chain, exactly as the resolver's chain builder does. +/// +/// +public static class TemplateReconcileClosure +{ + /// + /// Collects the derived subtree and inheritance chain of . + /// + /// The template whose member set changed. + /// Loads every template whose parent is one of the given ids. + /// Loads the templates with the given ids (unknown ids are omitted). + /// Cancellation token. + /// + /// The subtree (breadth-first) followed by the root's chain, each template once; an empty + /// list when the template has no children. + /// + public static async Task> CollectAsync( + int templateId, + Func, CancellationToken, Task>> loadChildren, + Func, CancellationToken, Task>> loadByIds, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(loadChildren); + ArgumentNullException.ThrowIfNull(loadByIds); + + var loaded = new Dictionary(); + var result = new List