diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs
index 2e3371f..fa0308b 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Accounts/Account.cs
@@ -475,6 +475,29 @@ public sealed class Account : INatsAccount
}
}
+ ///
+ /// Sets account-level message trace destination subject.
+ /// Mirrors Go (a *Account) setTraceDest(dest string).
+ ///
+ internal void SetTraceDest(string dest) => SetMessageTraceDestination(dest);
+
+ ///
+ /// Returns trace destination and sampling.
+ /// Mirrors Go (a *Account) getTraceDestAndSampling() (string, int).
+ ///
+ internal (string Destination, int Sampling) GetTraceDestAndSampling()
+ {
+ _mu.EnterReadLock();
+ try
+ {
+ return (_traceDest, _traceDestSampling);
+ }
+ finally
+ {
+ _mu.ExitReadLock();
+ }
+ }
+
// -------------------------------------------------------------------------
// Factory
// -------------------------------------------------------------------------
@@ -502,6 +525,12 @@ public sealed class Account : INatsAccount
///
public override string ToString() => Name;
+ ///
+ /// Returns the account name.
+ /// Mirrors Go (a *Account) String() string.
+ ///
+ public string String() => Name;
+
// -------------------------------------------------------------------------
// Shallow copy for config reload
// -------------------------------------------------------------------------
@@ -966,6 +995,12 @@ public sealed class Account : INatsAccount
internal int NumLocalConnectionsLocked() =>
(_clients?.Count ?? 0) - _sysclients - _nleafs;
+ ///
+ /// Returns local non-system, non-leaf client count. Lock must be held.
+ /// Mirrors Go (a *Account) numLocalConnections() int.
+ ///
+ internal int NumLocalConnectionsInternal() => NumLocalConnectionsLocked();
+
///
/// Returns all local connections including leaf nodes (minus system clients).
/// Mirrors Go (a *Account) numLocalAndLeafConnections() int.
@@ -1049,6 +1084,13 @@ public sealed class Account : INatsAccount
return _nleafs + _nrleafs >= MaxLeafNodes;
}
+ ///
+ /// Returns true if total leaf-node count reached the configured maximum.
+ /// Lock must be held by the caller.
+ /// Mirrors Go (a *Account) maxTotalLeafNodesReached() bool.
+ ///
+ internal bool MaxTotalLeafNodesReachedInternal() => MaxTotalLeafNodesReachedLocked();
+
///
/// Returns the total leaf-node count (local + remote).
/// Mirrors Go (a *Account) NumLeafNodes() int.
@@ -1115,6 +1157,93 @@ public sealed class Account : INatsAccount
}
}
+ ///
+ /// Returns true when there is at least one matching subscription for .
+ /// Mirrors Go (a *Account) SubscriptionInterest(subject string) bool.
+ ///
+ public bool SubscriptionInterest(string subject) => Interest(subject) > 0;
+
+ ///
+ /// Returns total number of plain and queue subscriptions matching .
+ /// Mirrors Go (a *Account) Interest(subject string) int.
+ ///
+ public int Interest(string subject)
+ {
+ _mu.EnterReadLock();
+ try
+ {
+ if (Sublist == null)
+ return 0;
+
+ var (np, nq) = Sublist.NumInterest(subject);
+ return np + nq;
+ }
+ finally
+ {
+ _mu.ExitReadLock();
+ }
+ }
+
+ ///
+ /// Increments the leaf-node count for a remote cluster.
+ /// Mirrors Go (a *Account) registerLeafNodeCluster(cluster string).
+ ///
+ internal void RegisterLeafNodeCluster(string cluster)
+ {
+ _mu.EnterWriteLock();
+ try
+ {
+ _leafClusters ??= new Dictionary(StringComparer.Ordinal);
+ _leafClusters.TryGetValue(cluster, out var current);
+ _leafClusters[cluster] = current + 1;
+ }
+ finally
+ {
+ _mu.ExitWriteLock();
+ }
+ }
+
+ ///
+ /// Returns true when this account already tracks one or more leaf nodes from .
+ /// Mirrors Go (a *Account) hasLeafNodeCluster(cluster string) bool.
+ ///
+ internal bool HasLeafNodeCluster(string cluster)
+ {
+ _mu.EnterReadLock();
+ try
+ {
+ return _leafClusters != null &&
+ _leafClusters.TryGetValue(cluster, out var count) &&
+ count > 0;
+ }
+ finally
+ {
+ _mu.ExitReadLock();
+ }
+ }
+
+ ///
+ /// Returns true when the account is leaf-cluster isolated to .
+ /// Mirrors Go (a *Account) isLeafNodeClusterIsolated(cluster string) bool.
+ ///
+ internal bool IsLeafNodeClusterIsolated(string cluster)
+ {
+ _mu.EnterReadLock();
+ try
+ {
+ if (string.IsNullOrEmpty(cluster))
+ return false;
+ if (_leafClusters == null || _leafClusters.Count > 1)
+ return false;
+
+ return _leafClusters.TryGetValue(cluster, out var count) && count == (ulong)_nleafs;
+ }
+ finally
+ {
+ _mu.ExitReadLock();
+ }
+ }
+
// -------------------------------------------------------------------------
// Subscription limit error throttle
// -------------------------------------------------------------------------
@@ -1460,6 +1589,167 @@ public sealed class Account : INatsAccount
}
}
+ // -------------------------------------------------------------------------
+ // Service export configuration
+ // -------------------------------------------------------------------------
+
+ ///
+ /// Configures an exported service with singleton response semantics.
+ /// Mirrors Go (a *Account) AddServiceExport(subject string, accounts []*Account) error.
+ ///
+ public Exception? AddServiceExport(string subject, IReadOnlyList? accounts = null) =>
+ AddServiceExportWithResponseAndAccountPos(subject, ServiceRespType.Singleton, accounts, 0);
+
+ ///
+ /// Configures an exported service with singleton response semantics and account-position auth.
+ /// Mirrors Go (a *Account) addServiceExportWithAccountPos(...).
+ ///
+ public Exception? AddServiceExportWithAccountPos(string subject, IReadOnlyList? accounts, uint accountPos) =>
+ AddServiceExportWithResponseAndAccountPos(subject, ServiceRespType.Singleton, accounts, accountPos);
+
+ ///
+ /// Configures an exported service with explicit response type.
+ /// Mirrors Go (a *Account) AddServiceExportWithResponse(...).
+ ///
+ public Exception? AddServiceExportWithResponse(string subject, ServiceRespType respType, IReadOnlyList? accounts = null) =>
+ AddServiceExportWithResponseAndAccountPos(subject, respType, accounts, 0);
+
+ ///
+ /// Configures an exported service with explicit response type and account-position auth.
+ /// Mirrors Go (a *Account) addServiceExportWithResponseAndAccountPos(...).
+ ///
+ public Exception? AddServiceExportWithResponseAndAccountPos(string subject, ServiceRespType respType, IReadOnlyList? accounts, uint accountPos)
+ {
+ if (!SubscriptionIndex.IsValidSubject(subject))
+ return ServerErrors.ErrBadSubject;
+
+ _mu.EnterWriteLock();
+ try
+ {
+ Exports.Services ??= new Dictionary(StringComparer.Ordinal);
+
+ if (!Exports.Services.TryGetValue(subject, out var serviceExport) || serviceExport == null)
+ serviceExport = new ServiceExportEntry();
+
+ if (respType != ServiceRespType.Singleton)
+ serviceExport.ResponseType = respType;
+
+ if (accounts != null || accountPos > 0)
+ {
+ var authErr = SetExportAuth(serviceExport, subject, accounts, accountPos);
+ if (authErr != null)
+ return authErr;
+ }
+
+ serviceExport.Account = this;
+ serviceExport.ResponseThreshold = ServerConstants.DefaultServiceExportResponseThreshold;
+ Exports.Services[subject] = serviceExport;
+ return null;
+ }
+ finally
+ {
+ _mu.ExitWriteLock();
+ }
+ }
+
+ ///
+ /// Enables latency tracking for with default sampling.
+ /// Mirrors Go (a *Account) TrackServiceExport(service, results string) error.
+ ///
+ public Exception? TrackServiceExport(string service, string results) =>
+ TrackServiceExportWithSampling(service, results, ServerConstants.DefaultServiceLatencySampling);
+
+ ///
+ /// Enables latency tracking for with explicit sampling.
+ /// Mirrors Go (a *Account) TrackServiceExportWithSampling(...).
+ ///
+ public Exception? TrackServiceExportWithSampling(string service, string results, int sampling)
+ {
+ if (sampling != 0 && (sampling < 1 || sampling > 100))
+ return ServerErrors.ErrBadSampling;
+ if (!SubscriptionIndex.IsValidPublishSubject(results))
+ return ServerErrors.ErrBadPublishSubject;
+ if (IsExportService(results))
+ return ServerErrors.ErrBadPublishSubject;
+
+ _mu.EnterWriteLock();
+ try
+ {
+ if (Exports.Services == null)
+ return ServerErrors.ErrMissingService;
+ if (!Exports.Services.TryGetValue(service, out var serviceExport))
+ return ServerErrors.ErrMissingService;
+
+ serviceExport ??= new ServiceExportEntry();
+ if (serviceExport.ResponseType != ServiceRespType.Singleton)
+ return ServerErrors.ErrBadServiceType;
+
+ serviceExport.Latency = new InternalServiceLatency
+ {
+ Sampling = sampling,
+ Subject = results,
+ };
+ Exports.Services[service] = serviceExport;
+
+ if (Imports.Services != null)
+ {
+ foreach (var imports in Imports.Services.Values)
+ {
+ foreach (var import in imports)
+ {
+ if (import?.Account?.Name != Name)
+ continue;
+ if (SubjectTransform.IsSubsetMatch(SubjectTransform.TokenizeSubject(import.To), service))
+ import.Latency = serviceExport.Latency;
+ }
+ }
+ }
+
+ return null;
+ }
+ finally
+ {
+ _mu.ExitWriteLock();
+ }
+ }
+
+ ///
+ /// Disables latency tracking for the exported service.
+ /// Mirrors Go (a *Account) UnTrackServiceExport(service string).
+ ///
+ public void UnTrackServiceExport(string service)
+ {
+ _mu.EnterWriteLock();
+ try
+ {
+ if (Exports.Services == null || !Exports.Services.TryGetValue(service, out var serviceExport) || serviceExport?.Latency == null)
+ return;
+
+ serviceExport.Latency = null;
+
+ if (Imports.Services == null)
+ return;
+
+ foreach (var imports in Imports.Services.Values)
+ {
+ foreach (var import in imports)
+ {
+ if (import?.Account?.Name != Name)
+ continue;
+ if (SubjectTransform.IsSubsetMatch(SubjectTransform.TokenizeSubject(import.To), service))
+ {
+ import.Latency = null;
+ import.M1 = null;
+ }
+ }
+ }
+ }
+ finally
+ {
+ _mu.ExitWriteLock();
+ }
+ }
+
// -------------------------------------------------------------------------
// Export checks
// -------------------------------------------------------------------------
@@ -2146,6 +2436,35 @@ public sealed class Account : INatsAccount
return false;
}
+ ///
+ /// Applies account-based authorization rules to an export descriptor.
+ /// Mirrors Go setExportAuth(&se.exportAuth, ...).
+ ///
+ private static Exception? SetExportAuth(ExportAuth auth, string subject, IReadOnlyList? accounts, uint accountPos)
+ {
+ if (!SubscriptionIndex.IsValidSubject(subject))
+ return ServerErrors.ErrBadSubject;
+
+ auth.AccountPosition = accountPos;
+
+ if (accounts == null || accounts.Count == 0)
+ {
+ auth.Approved = null;
+ return null;
+ }
+
+ var approved = new Dictionary(accounts.Count, StringComparer.Ordinal);
+ foreach (var account in accounts)
+ {
+ if (account == null)
+ continue;
+ approved[account.Name] = account;
+ }
+
+ auth.Approved = approved;
+ return null;
+ }
+
// -------------------------------------------------------------------------
// Export equality helpers
// -------------------------------------------------------------------------
diff --git a/porting.db b/porting.db
index 15af1ae..244b04e 100644
Binary files a/porting.db and b/porting.db differ