bde042b4d4
Every parity-critical single-item MXAccess command now has a typed session helper instead of only a raw-Invoke escape hatch. Added per client: - Phase 1: AdviseSupervisory, WriteSecured, WriteSecured2, AuthenticateUser, ArchestrAUserToId - Phase 2: AddBufferedItem, SetBufferedUpdateInterval, Suspend, Activate - CLI-30: Unregister (Rust + .NET; Go/Python already had it) Each wraps the existing raw-command machinery (no new wire surface) and runs the same MXAccess-level reply validation (hresult < 0 + MxStatusProxy). MXAccess parity preserved: WriteSecured before AuthenticateUser+AdviseSupervisory surfaces the native failure unchanged (not pre-validated/reordered). Credentials (AuthenticateUser password, WriteSecured payloads) route through each client's secret-redaction seam and never reach logs/exceptions/ToString/Debug/Display; each suite asserts a distinctive credential is absent from surfaced errors. New CLI subcommands source credentials via flag/env, never echoed. - .NET: 21 helpers (validated + Raw), CLI subcommands, multi-secret CLI redactor. Build clean (0 warn), 102 passed. - Go: 9 helpers + *Raw variants, redactSecrets seam, promoted CLI advise-supervisory to typed. gofmt/vet/build/test clean. - Rust: 10 helpers incl. unregister; verified ensure_mxaccess_success runs on secured paths; error.rs credential scrub. fmt/check/test/clippy clean. - Python: 9 async helpers, redact_secret seam + _invoke_redacted, CLI commands. 145 passed. - Shared doc: ClientLibrariesDesign "Typed Command Parity" section. Java client typed parity is batched to windev (no local JRE); CLI-04 + CLI-30 stay open until it lands. Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
1449 lines
58 KiB
C#
1449 lines
58 KiB
C#
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Client;
|
|
|
|
/// <summary>
|
|
/// Represents one gateway-backed MXAccess session.
|
|
/// </summary>
|
|
public sealed class MxGatewaySession : IAsyncDisposable
|
|
{
|
|
private readonly MxGatewayClient _client;
|
|
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
|
private CloseSessionReply? _closeReply;
|
|
|
|
/// <summary>
|
|
/// Initializes a new session backed by the given MXAccess gateway client.
|
|
/// </summary>
|
|
/// <param name="client">The gateway client used for commands and events.</param>
|
|
/// <param name="openSessionReply">The server's session creation response.</param>
|
|
internal MxGatewaySession(
|
|
MxGatewayClient client,
|
|
OpenSessionReply openSessionReply)
|
|
{
|
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
|
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The session ID assigned by the gateway.
|
|
/// </summary>
|
|
public string SessionId => OpenSessionReply.SessionId;
|
|
|
|
/// <summary>
|
|
/// The server's session creation response containing metadata.
|
|
/// </summary>
|
|
public OpenSessionReply OpenSessionReply { get; }
|
|
|
|
/// <summary>
|
|
/// Closes the session on the gateway. Idempotent.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The server's close-session reply.</returns>
|
|
public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
if (_closeReply is not null)
|
|
{
|
|
return _closeReply;
|
|
}
|
|
|
|
await _closeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
if (_closeReply is not null)
|
|
{
|
|
return _closeReply;
|
|
}
|
|
|
|
_closeReply = await _client.CloseSessionRawAsync(
|
|
new CloseSessionRequest { SessionId = SessionId },
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
return _closeReply;
|
|
}
|
|
finally
|
|
{
|
|
_closeLock.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a client with the MXAccess session, returning a ServerHandle.
|
|
/// </summary>
|
|
/// <param name="clientName">Name to register.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The server handle assigned to the registered client.</returns>
|
|
public async Task<int> RegisterAsync(
|
|
string clientName,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await RegisterRawAsync(clientName, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a client with the MXAccess session without error checking.
|
|
/// </summary>
|
|
/// <param name="clientName">Name to register.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> RegisterRawAsync(
|
|
string clientName,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(clientName);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Register,
|
|
Register = new RegisterCommand { ClientName = clientName },
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an item to the MXAccess session, returning an ItemHandle.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemDefinition">The item tag address.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The item handle assigned to the new item.</returns>
|
|
public async Task<int> AddItemAsync(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await AddItemRawAsync(
|
|
serverHandle,
|
|
itemDefinition,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an item to the MXAccess session without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemDefinition">The item tag address.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> AddItemRawAsync(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AddItem,
|
|
AddItem = new AddItemCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemDefinition = itemDefinition,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an item with context to the MXAccess session, returning an ItemHandle.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemDefinition">The item tag address.</param>
|
|
/// <param name="itemContext">Additional context for the item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The item handle assigned to the new item.</returns>
|
|
public async Task<int> AddItem2Async(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
string itemContext,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await AddItem2RawAsync(
|
|
serverHandle,
|
|
itemDefinition,
|
|
itemContext,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an item with context to the MXAccess session without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemDefinition">The item tag address.</param>
|
|
/// <param name="itemContext">Additional context for the item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> AddItem2RawAsync(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
string itemContext,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AddItem2,
|
|
AddItem2 = new AddItem2Command
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemDefinition = itemDefinition,
|
|
ItemContext = itemContext ?? string.Empty,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to events for an item (advises in MXAccess terminology).
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task AdviseAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await AdviseRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to events for an item without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> AdviseRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Advise,
|
|
Advise = new AdviseCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribes from events for an item (unadvises in MXAccess terminology).
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task UnAdviseAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await UnAdviseRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribes from events for an item without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> UnAdviseRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.UnAdvise,
|
|
UnAdvise = new UnAdviseCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes an item from the MXAccess session.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task RemoveItemAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await RemoveItemRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes an item from the MXAccess session without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> RemoveItemRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.RemoveItem,
|
|
RemoveItem = new RemoveItemCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds multiple items to the MXAccess session in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="tagAddresses">The item tag addresses to add.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<string> tagAddresses,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(tagAddresses);
|
|
|
|
AddItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.TagAddresses.Add(tagAddresses);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AddItemBulk,
|
|
AddItemBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.AddItemBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advises multiple items in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandles">The ItemHandles to advise.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<int> itemHandles,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
|
|
AdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.ItemHandles.Add(itemHandles);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AdviseItemBulk,
|
|
AdviseItemBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes multiple items in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandles">The ItemHandles to remove.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<int> itemHandles,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
|
|
RemoveItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.ItemHandles.Add(itemHandles);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.RemoveItemBulk,
|
|
RemoveItemBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unadvises multiple items in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandles">The ItemHandles to unadvise.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<int> itemHandles,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
|
|
UnAdviseItemBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.ItemHandles.Add(itemHandles);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.UnAdviseItemBulk,
|
|
UnAdviseItemBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds and advises multiple items in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="tagAddresses">The item tag addresses to add and advise.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<string> tagAddresses,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(tagAddresses);
|
|
|
|
SubscribeBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.TagAddresses.Add(tagAddresses);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.SubscribeBulk,
|
|
SubscribeBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.SubscribeBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unadvises and removes multiple items in a single command.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandles">The ItemHandles to unsubscribe.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Per-item subscription results.</returns>
|
|
public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<int> itemHandles,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(itemHandles);
|
|
|
|
UnsubscribeBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.ItemHandles.Add(itemHandles);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.UnsubscribeBulk,
|
|
UnsubscribeBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk Write — sequential MXAccess Write per entry on the worker's STA.
|
|
/// Per-item failures appear as <see cref="BulkWriteResult"/> entries with
|
|
/// <c>WasSuccessful = false</c>; the call never throws on per-item errors.
|
|
/// Protocol-level failures still throw via EnsureProtocolSuccess.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="entries">Per-item write entries; each carries the item handle, value, and user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
public async Task<IReadOnlyList<BulkWriteResult>> WriteBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<WriteBulkEntry> entries,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entries);
|
|
|
|
WriteBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.Entries.Add(entries);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteBulk,
|
|
WriteBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.WriteBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk Write2 — sequential MXAccess Write2 (timestamped) per entry.
|
|
/// Per-item failures appear as <see cref="BulkWriteResult"/> entries with
|
|
/// <c>WasSuccessful = false</c>; the call never throws on per-item errors.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="entries">Per-item write entries; each carries the item handle, value, timestamp, and user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
public async Task<IReadOnlyList<BulkWriteResult>> Write2BulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<Write2BulkEntry> entries,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entries);
|
|
|
|
Write2BulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.Entries.Add(entries);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write2Bulk,
|
|
Write2Bulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.Write2Bulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk WriteSecured — sequential MXAccess WriteSecured per entry.
|
|
/// Credential-sensitive values must never reach logs; the client mirrors
|
|
/// the single-item WriteSecured redaction contract.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="entries">Per-item write entries; each carries the item handle, value, current user id, and verifier user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
public async Task<IReadOnlyList<BulkWriteResult>> WriteSecuredBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<WriteSecuredBulkEntry> entries,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entries);
|
|
|
|
WriteSecuredBulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.Entries.Add(entries);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecuredBulk,
|
|
WriteSecuredBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.WriteSecuredBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk WriteSecured2 — sequential MXAccess WriteSecured2 (timestamped) per entry.
|
|
/// Same redaction rules as <see cref="WriteSecuredBulkAsync"/>.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="entries">Per-item write entries; each carries the item handle, value, timestamp, current user id, and verifier user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>One <see cref="BulkWriteResult"/> per requested entry, in request order.</returns>
|
|
public async Task<IReadOnlyList<BulkWriteResult>> WriteSecured2BulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<WriteSecured2BulkEntry> entries,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(entries);
|
|
|
|
WriteSecured2BulkCommand command = new() { ServerHandle = serverHandle };
|
|
command.Entries.Add(entries);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecured2Bulk,
|
|
WriteSecured2Bulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.WriteSecured2Bulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk Read — snapshot the current value for each requested tag.
|
|
/// Returns the cached OnDataChange value when the tag is already advised
|
|
/// (<c>WasCached = true</c>), otherwise the worker takes the full AddItem +
|
|
/// Advise + wait + UnAdvise + RemoveItem snapshot lifecycle. Per-tag
|
|
/// failures (timeout, invalid tag) appear as <see cref="BulkReadResult"/>
|
|
/// entries with <c>WasSuccessful = false</c>; the call never throws on
|
|
/// per-tag errors.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="tagAddresses">Tag addresses to read (one per result).</param>
|
|
/// <param name="timeout">Per-call timeout for the snapshot lifecycle path; <see cref="TimeSpan.Zero"/> uses the gateway default.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>One <see cref="BulkReadResult"/> per requested tag, in request order.</returns>
|
|
public async Task<IReadOnlyList<BulkReadResult>> ReadBulkAsync(
|
|
int serverHandle,
|
|
IReadOnlyList<string> tagAddresses,
|
|
TimeSpan timeout,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(tagAddresses);
|
|
|
|
ReadBulkCommand command = new()
|
|
{
|
|
ServerHandle = serverHandle,
|
|
TimeoutMs = timeout <= TimeSpan.Zero ? 0u : (uint)Math.Min(timeout.TotalMilliseconds, uint.MaxValue),
|
|
};
|
|
command.TagAddresses.Add(tagAddresses);
|
|
|
|
MxCommandReply reply = await InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.ReadBulk,
|
|
ReadBulk = command,
|
|
},
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.ReadBulk?.Results.ToArray() ?? [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a value to an item on the MXAccess server.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The value to write.</param>
|
|
/// <param name="userId">User ID context for the write.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task WriteAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
int userId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await WriteRawAsync(serverHandle, itemHandle, value, userId, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes specific array indices to an item using default-fill semantics.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The gateway expands the sparse descriptor into a full <c>totalLength</c>-element array
|
|
/// before forwarding to the worker. Indices not listed in <paramref name="elements"/> are
|
|
/// written as the element type's default value — this is a RESET, not a preserve. The
|
|
/// current values at those positions are discarded. <paramref name="totalLength"/> is
|
|
/// required and must match the declared length of the array attribute.
|
|
/// </remarks>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="elementDataType">The MXAccess data type of each element.</param>
|
|
/// <param name="totalLength">The total declared length of the target array attribute.</param>
|
|
/// <param name="elements">Map of zero-based array index to scalar <see cref="MxValue"/>.</param>
|
|
/// <param name="userId">User ID context for the write.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public Task WriteArrayElementsAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxDataType elementDataType,
|
|
uint totalLength,
|
|
IReadOnlyDictionary<uint, MxValue> elements,
|
|
int userId = 0,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(elements);
|
|
MxValue value = BuildSparseArray(elementDataType, totalLength, elements);
|
|
return WriteAsync(serverHandle, itemHandle, value, userId, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds an <see cref="MxValue"/> whose <see cref="MxValue.SparseArrayValue"/> describes a
|
|
/// default-fill partial array write.
|
|
/// </summary>
|
|
/// <param name="elementDataType">The MXAccess data type of each element.</param>
|
|
/// <param name="totalLength">The total declared length of the target array attribute.</param>
|
|
/// <param name="elements">Map of zero-based array index to scalar <see cref="MxValue"/>.</param>
|
|
/// <returns>An <see cref="MxValue"/> with <see cref="MxValue.KindOneofCase.SparseArrayValue"/> set.</returns>
|
|
internal static MxValue BuildSparseArray(
|
|
MxDataType elementDataType,
|
|
uint totalLength,
|
|
IReadOnlyDictionary<uint, MxValue> elements)
|
|
{
|
|
MxSparseArray sparse = new()
|
|
{
|
|
ElementDataType = elementDataType,
|
|
TotalLength = totalLength,
|
|
};
|
|
foreach (KeyValuePair<uint, MxValue> kv in elements)
|
|
{
|
|
sparse.Elements.Add(new MxSparseElement { Index = kv.Key, Value = kv.Value });
|
|
}
|
|
|
|
return new MxValue { SparseArrayValue = sparse };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a value to an item on the MXAccess server without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The value to write.</param>
|
|
/// <param name="userId">User ID context for the write.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> WriteRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
int userId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write,
|
|
Write = new WriteCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
Value = value,
|
|
UserId = userId,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a value and timestamp to an item on the MXAccess server.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The value to write.</param>
|
|
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
|
/// <param name="userId">User ID context for the write.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task Write2Async(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
MxValue timestampValue,
|
|
int userId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await Write2RawAsync(
|
|
serverHandle,
|
|
itemHandle,
|
|
value,
|
|
timestampValue,
|
|
userId,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a value and timestamp to an item on the MXAccess server without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The value to write.</param>
|
|
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
|
/// <param name="userId">User ID context for the write.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> Write2RawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
MxValue timestampValue,
|
|
int userId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
ArgumentNullException.ThrowIfNull(timestampValue);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write2,
|
|
Write2 = new Write2Command
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
Value = value,
|
|
TimestampValue = timestampValue,
|
|
UserId = userId,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unregisters a previously registered client from the MXAccess session
|
|
/// (MXAccess <c>Unregister</c>), releasing its ServerHandle.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task UnregisterAsync(
|
|
int serverHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await UnregisterRawAsync(serverHandle, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unregisters a previously registered client without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> UnregisterRawAsync(
|
|
int serverHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Unregister,
|
|
Unregister = new UnregisterCommand { ServerHandle = serverHandle },
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to supervisory events for an item (MXAccess <c>AdviseSupervisory</c>).
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task AdviseSupervisoryAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await AdviseSupervisoryRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to supervisory events for an item without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> AdviseSupervisoryRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AdviseSupervisory,
|
|
AdviseSupervisory = new AdviseSupervisoryCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a buffered item to the MXAccess session (MXAccess <c>AddBufferedItem</c>),
|
|
/// returning an ItemHandle.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemDefinition">The item tag address.</param>
|
|
/// <param name="itemContext">Additional context for the item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The item handle assigned to the new buffered item.</returns>
|
|
public async Task<int> AddBufferedItemAsync(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
string itemContext,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await AddBufferedItemRawAsync(
|
|
serverHandle,
|
|
itemDefinition,
|
|
itemContext,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.AddBufferedItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a buffered item to the MXAccess session without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemDefinition">The item tag address.</param>
|
|
/// <param name="itemContext">Additional context for the item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> AddBufferedItemRawAsync(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
string itemContext,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AddBufferedItem,
|
|
AddBufferedItem = new AddBufferedItemCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemDefinition = itemDefinition,
|
|
ItemContext = itemContext ?? string.Empty,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the buffered-item update interval on the MXAccess session
|
|
/// (MXAccess <c>SetBufferedUpdateInterval</c>).
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="updateIntervalMilliseconds">The buffered update interval, in milliseconds.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task SetBufferedUpdateIntervalAsync(
|
|
int serverHandle,
|
|
int updateIntervalMilliseconds,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await SetBufferedUpdateIntervalRawAsync(
|
|
serverHandle,
|
|
updateIntervalMilliseconds,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the buffered-item update interval without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="updateIntervalMilliseconds">The buffered update interval, in milliseconds.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> SetBufferedUpdateIntervalRawAsync(
|
|
int serverHandle,
|
|
int updateIntervalMilliseconds,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.SetBufferedUpdateInterval,
|
|
SetBufferedUpdateInterval = new SetBufferedUpdateIntervalCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
UpdateIntervalMilliseconds = updateIntervalMilliseconds,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Suspends updates for an item (MXAccess <c>Suspend</c>).
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The item's MXSTATUS_PROXY as reported by the worker, or <c>null</c> if the reply omitted it.</returns>
|
|
public async Task<MxStatusProxy?> SuspendAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await SuspendRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.Suspend?.Status;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Suspends updates for an item without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> SuspendRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Suspend,
|
|
Suspend = new SuspendCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resumes updates for a suspended item (MXAccess <c>Activate</c>).
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The item's MXSTATUS_PROXY as reported by the worker, or <c>null</c> if the reply omitted it.</returns>
|
|
public async Task<MxStatusProxy?> ActivateAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await ActivateRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.Activate?.Status;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resumes updates for a suspended item without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> ActivateRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Activate,
|
|
Activate = new ActivateCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a secured value to an item on the MXAccess server (MXAccess <c>WriteSecured</c>).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// MXAccess parity: <c>WriteSecured</c> fails when it is issued before a value-bearing
|
|
/// NMX body or before a prior <c>AuthenticateUser</c> + <c>AdviseSupervisory</c>. That
|
|
/// native failure is surfaced unchanged — the client does not pre-validate or reorder it.
|
|
/// The <paramref name="value"/> is credential-sensitive and must never reach logs; the
|
|
/// client mirrors the single-item WriteSecured redaction contract.
|
|
/// </remarks>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The secured value to write.</param>
|
|
/// <param name="currentUserId">The current operator user id.</param>
|
|
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task WriteSecuredAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
int currentUserId,
|
|
int verifierUserId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await WriteSecuredRawAsync(
|
|
serverHandle,
|
|
itemHandle,
|
|
value,
|
|
currentUserId,
|
|
verifierUserId,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a secured value to an item without error checking. See
|
|
/// <see cref="WriteSecuredAsync"/> for the parity and redaction contract.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The secured value to write.</param>
|
|
/// <param name="currentUserId">The current operator user id.</param>
|
|
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> WriteSecuredRawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
int currentUserId,
|
|
int verifierUserId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecured,
|
|
WriteSecured = new WriteSecuredCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
CurrentUserId = currentUserId,
|
|
VerifierUserId = verifierUserId,
|
|
Value = value,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a secured value and timestamp to an item (MXAccess <c>WriteSecured2</c>).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Same parity and redaction contract as <see cref="WriteSecuredAsync"/>: the native
|
|
/// failure that occurs before a value-bearing NMX body or a prior authenticate is
|
|
/// surfaced unchanged, and the credential-sensitive <paramref name="value"/> must never
|
|
/// reach logs.
|
|
/// </remarks>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The secured value to write.</param>
|
|
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
|
/// <param name="currentUserId">The current operator user id.</param>
|
|
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
public async Task WriteSecured2Async(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
MxValue timestampValue,
|
|
int currentUserId,
|
|
int verifierUserId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await WriteSecured2RawAsync(
|
|
serverHandle,
|
|
itemHandle,
|
|
value,
|
|
timestampValue,
|
|
currentUserId,
|
|
verifierUserId,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a secured value and timestamp to an item without error checking. See
|
|
/// <see cref="WriteSecured2Async"/> for the parity and redaction contract.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
|
/// <param name="value">The secured value to write.</param>
|
|
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
|
/// <param name="currentUserId">The current operator user id.</param>
|
|
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> WriteSecured2RawAsync(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
MxValue value,
|
|
MxValue timestampValue,
|
|
int currentUserId,
|
|
int verifierUserId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(value);
|
|
ArgumentNullException.ThrowIfNull(timestampValue);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecured2,
|
|
WriteSecured2 = new WriteSecured2Command
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
CurrentUserId = currentUserId,
|
|
VerifierUserId = verifierUserId,
|
|
Value = value,
|
|
TimestampValue = timestampValue,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Authenticates an MXAccess verify-user (MXAccess <c>AuthenticateUser</c>), returning
|
|
/// the resolved user id used by <c>WriteSecured</c> / <c>WriteSecured2</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The <paramref name="verifyUserPassword"/> is a raw MXAccess credential. It is never
|
|
/// logged and never placed on the exception path: gateway/MXAccess failures surface only
|
|
/// reply-derived diagnostics (kind, HRESULT, MXSTATUS_PROXY), never the request payload.
|
|
/// </remarks>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="verifyUser">The user to verify.</param>
|
|
/// <param name="verifyUserPassword">The verify-user credential. Never logged.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The authenticated user id.</returns>
|
|
public async Task<int> AuthenticateUserAsync(
|
|
int serverHandle,
|
|
string verifyUser,
|
|
string verifyUserPassword,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await AuthenticateUserRawAsync(
|
|
serverHandle,
|
|
verifyUser,
|
|
verifyUserPassword,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.AuthenticateUser?.UserId ?? reply.ReturnValue.Int32Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Authenticates an MXAccess verify-user without error checking. See
|
|
/// <see cref="AuthenticateUserAsync"/> for the credential-handling contract.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="verifyUser">The user to verify.</param>
|
|
/// <param name="verifyUserPassword">The verify-user credential. Never logged.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> AuthenticateUserRawAsync(
|
|
int serverHandle,
|
|
string verifyUser,
|
|
string verifyUserPassword,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(verifyUser);
|
|
ArgumentNullException.ThrowIfNull(verifyUserPassword);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AuthenticateUser,
|
|
AuthenticateUser = new AuthenticateUserCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
VerifyUser = verifyUser,
|
|
VerifyUserPassword = verifyUserPassword,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves an ArchestrA user GUID to its MXAccess user id
|
|
/// (MXAccess <c>ArchestrAUserToId</c>).
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="userIdGuid">The ArchestrA user GUID to resolve.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The resolved MXAccess user id.</returns>
|
|
public async Task<int> ArchestraUserToIdAsync(
|
|
int serverHandle,
|
|
string userIdGuid,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
MxCommandReply reply = await ArchestraUserToIdRawAsync(serverHandle, userIdGuid, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
return reply.ArchestraUserToId?.UserId ?? reply.ReturnValue.Int32Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves an ArchestrA user GUID to its MXAccess user id without error checking.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
|
/// <param name="userIdGuid">The ArchestrA user GUID to resolve.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> ArchestraUserToIdRawAsync(
|
|
int serverHandle,
|
|
string userIdGuid,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(userIdGuid);
|
|
|
|
return InvokeCommandAsync(
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.ArchestraUserToId,
|
|
ArchestraUserToId = new ArchestrAUserToIdCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
UserIdGuid = userIdGuid,
|
|
},
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes an MXAccess command on this session.
|
|
/// </summary>
|
|
/// <param name="request">The command request.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The raw server reply.</returns>
|
|
public Task<MxCommandReply> InvokeAsync(
|
|
MxCommandRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
return _client.InvokeAsync(request, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Streams events from the worker for this session, optionally starting after a given sequence number.
|
|
/// </summary>
|
|
/// <param name="afterWorkerSequence">The sequence number to stream from. Defaults to 0.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>An async enumerable of events.</returns>
|
|
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
ulong afterWorkerSequence = 0,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return _client.StreamEventsAsync(
|
|
new StreamEventsRequest
|
|
{
|
|
SessionId = SessionId,
|
|
AfterWorkerSequence = afterWorkerSequence,
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Streams events as typed <see cref="MxEventStreamItem"/> values, surfacing
|
|
/// the gateway's reconnect-replay gap sentinel as an observable, typed signal.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When resuming with a stale <paramref name="afterWorkerSequence"/> (older than
|
|
/// the oldest event still retained in the session replay ring), the gateway emits
|
|
/// a single gap sentinel at the head of the stream. It arrives here as an item
|
|
/// with <see cref="MxEventStreamItem.IsReplayGap"/> true and
|
|
/// <see cref="MxEventStreamItem.ReplayGap"/> populated, meaning the consumer
|
|
/// missed events and MUST discard local state and re-snapshot. To resume without
|
|
/// incurring another gap, reconnect with
|
|
/// <c>afterWorkerSequence = item.ReplayGap.OldestAvailableSequence - 1</c>.
|
|
/// All other events pass through with <see cref="MxEventStreamItem.IsReplayGap"/>
|
|
/// false. Use <see cref="StreamEventsAsync"/> when raw generated
|
|
/// <see cref="MxEvent"/> messages are needed instead.
|
|
/// </remarks>
|
|
/// <param name="afterWorkerSequence">The sequence number to stream from. Defaults to 0.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>An async enumerable of typed event items.</returns>
|
|
public IAsyncEnumerable<MxEventStreamItem> StreamEventItemsAsync(
|
|
ulong afterWorkerSequence = 0,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return StreamEventsAsync(afterWorkerSequence, cancellationToken)
|
|
.AsStreamItemsAsync(cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the session and releases resources.
|
|
/// </summary>
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await CloseAsync().ConfigureAwait(false);
|
|
_closeLock.Dispose();
|
|
}
|
|
|
|
private Task<MxCommandReply> InvokeCommandAsync(
|
|
MxCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return _client.InvokeAsync(
|
|
new MxCommandRequest
|
|
{
|
|
SessionId = SessionId,
|
|
ClientCorrelationId = Guid.NewGuid().ToString("N"),
|
|
Command = command,
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
}
|