using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using ZB.MOM.WW.LocalDb; using ZB.MOM.WW.LocalDb.Replication; using ZB.MOM.WW.OtOpcUa.Runtime.DeploymentCache; namespace ZB.MOM.WW.OtOpcUa.Host.Configuration; /// /// Single registration point for the node-local LocalDb subsystem: the embedded SQLite store /// that caches the deployed-configuration artifact, plus the optional gRPC replication that /// mirrors it to the node's redundant pair peer. /// /// /// /// Exists as a named extension rather than inline AddZbLocalDb calls in /// Program.cs so the wiring is covered by a DI resolution test — Program.cs is /// top-level statements and cannot be exercised directly, which is exactly how a /// "registered but never resolvable" defect ships unnoticed. This family has shipped that /// defect three times (Secrets 0.2.0, Secrets 0.2.2, ScadaBridge #22). /// /// /// Driver-role nodes only. Admin-only nodes have no deployed configuration to cache, /// and registering here would impose the LocalDb:Path-required-or-no-boot constraint /// on them for nothing. /// /// /// Storage is unconditional; replication is default-OFF. A node with no /// PeerAddress and no SyncListenPort is simply a fast local SQLite file — /// AddZbLocalDbReplication registers the engine but it stays idle. Replication is /// opt-in per pair because there is no production distribution story for the sync /// ApiKey yet (the same open question the Secrets KEK has). /// /// public static class LocalDbRegistration { /// Configuration section holding LocalDbOptions. public const string LocalDbSectionPath = "LocalDb"; /// Configuration section holding ReplicationOptions. public const string ReplicationSectionPath = "LocalDb:Replication"; /// /// Configuration key carrying the dedicated h2c sync listener's port. Zero or absent means /// no listener is bound and the passive sync endpoint is not mapped. /// public const string SyncListenPortKey = "LocalDb:SyncListenPort"; /// /// The port the dedicated cleartext-HTTP/2 sync listener should bind, or 0 when the /// node should not listen at all. Defaults to 0 — absent configuration must mean /// "off". /// /// The application configuration. /// The configured sync port, or 0. public static int SyncListenPort(IConfiguration configuration) { ArgumentNullException.ThrowIfNull(configuration); return configuration.GetValue(SyncListenPortKey, defaultValue: 0); } /// /// Registers the local database (with the deployment-cache schema applied and registered for /// replication) and the replication engine. /// /// /// /// runs inside AddZbLocalDb's singleton factory, /// before the first caller receives the ILocalDb — so every consumer is guaranteed /// to see the tables created and their capture triggers installed. /// /// /// AddZbLocalDbReplication is called unconditionally rather than behind an /// Enabled flag because the library already models "off" as "no /// PeerAddress": the initiator background service idles and the passive endpoint /// is only reachable if Program.cs maps it, which it does only when /// is set. Adding a second gate on top would give two /// ways to express the same thing and a way for them to disagree. /// /// /// The service collection to add to. /// The application configuration. /// The same instance, for chaining. public static IServiceCollection AddOtOpcUaLocalDb( this IServiceCollection services, IConfiguration configuration) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); services.AddZbLocalDb(configuration, db => LocalDbSetup.OnReady(db, configuration)); services.AddZbLocalDbReplication(configuration); // The consumer-facing seam. WithOtOpcUaRuntimeActors resolves this optionally and threads it // into DriverHostActor; registering the store without this leaves the cache tables present, // replicating, and never written to. services.AddSingleton(); return services; } }