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;
}
}