Files
MBN_STOCK_WEBVIEW/MainWindow.ManualFinancial.cs

2047 lines
71 KiB
C#

using System.Globalization;
using System.Text.Json;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW;
public sealed partial class MainWindow
{
private const string ManualFinancialCorrelationQuarantineMessage =
"A manual-financial database write result could not be confirmed. Restart the app and reconcile INPUT_* before another write.";
private readonly SemaphoreSlim _manualFinancialWriteGate = new(1, 1);
private readonly object _manualFinancialReadLanesGate = new();
private readonly Dictionary<string, ManualFinancialReadRequest> _manualFinancialReadLanes =
new(StringComparer.Ordinal);
private IManualFinancialScreenService? _manualFinancialScreenService;
private CancellationTokenSource? _manualFinancialWriteCancellation;
private string? _manualFinancialInitializationError;
private string? _manualFinancialWriteQuarantineMessage;
private int _manualFinancialRuntimeState;
private int _manualFinancialWriteInFlight;
private int _manualFinancialWriteQuarantined;
private int _manualFinancialBrowserGeneration;
/// <summary>
/// Root integration hook. Call after InitializeDatabaseRuntime. Every request
/// handler also calls it lazily, so an omitted eager call fails closed.
/// </summary>
private void InitializeManualFinancialRuntime()
{
if (Volatile.Read(ref _manualFinancialRuntimeState) != 0)
{
return;
}
var runtime = _databaseRuntime;
if (runtime is null ||
Interlocked.CompareExchange(ref _manualFinancialRuntimeState, 1, 0) != 0)
{
return;
}
try
{
_manualFinancialScreenService = new LegacyManualFinancialScreenService(
runtime.Executor,
new OracleManualFinancialMutationExecutor(
runtime.ConnectionFactory,
runtime.Options.Resilience));
}
catch
{
_manualFinancialScreenService = null;
_manualFinancialInitializationError =
"The manual-financial Oracle boundary could not be initialized.";
Volatile.Write(ref _manualFinancialRuntimeState, -1);
}
}
/// <summary>
/// Root WebMessage hook. True means the request belongs to this boundary,
/// including malformed payloads which receive an INVALID_REQUEST response.
/// </summary>
private bool TryHandleManualFinancialRequest(string? requestType, JsonElement payload)
{
switch (requestType)
{
case "request-manual-financial-list":
_ = HandleManualFinancialListRequestAsync(payload);
return true;
case "request-manual-financial-load":
_ = HandleManualFinancialLoadRequestAsync(payload);
return true;
case "create-manual-financial-record":
_ = HandleManualFinancialCreateRequestAsync(payload);
return true;
case "update-manual-financial-record":
_ = HandleManualFinancialUpdateRequestAsync(payload);
return true;
case "delete-manual-financial-record":
_ = HandleManualFinancialDeleteRequestAsync(payload);
return true;
case "delete-all-manual-financial-records":
_ = HandleManualFinancialDeleteAllRequestAsync(payload);
return true;
case "request-manual-financial-selection":
_ = HandleManualFinancialSelectionRequestAsync(payload);
return true;
default:
return false;
}
}
private async Task HandleManualFinancialListRequestAsync(JsonElement payload)
{
if (!TryParseManualFinancialListRequest(
payload,
out var requestId,
out var screen,
out var query,
out var maximumResults))
{
PostManualFinancialInvalidRequest(
SafeManualFinancialRequestId(payload),
"list",
SafeManualFinancialScreen(payload),
query: SafeManualFinancialQuery(payload));
return;
}
InitializeManualFinancialRuntime();
var service = _manualFinancialScreenService;
if (service is null)
{
PostManualFinancialUnavailable(requestId, "list", ToWireValue(screen), query: query);
return;
}
await ExecuteManualFinancialReadAsync(
requestId,
"list",
screen,
query,
string.Empty,
async cancellationToken =>
{
var result = await service.SearchAsync(
screen,
query,
maximumResults,
cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
return new ManualFinancialBridgeReply(
"manual-financial-list-results",
new
{
requestId,
screen = ToWireValue(screen),
query = result.Query,
result.RetrievedAt,
totalRowCount = result.Items.Count,
truncated = result.IsTruncated,
items = result.Items.Select(CreateManualFinancialSnapshotPayload)
});
});
}
private async Task HandleManualFinancialLoadRequestAsync(JsonElement payload)
{
if (!TryParseManualFinancialIdentityRequest(
payload,
["requestId", "screen", "stockName"],
out var requestId,
out var identity))
{
PostManualFinancialInvalidRequest(
SafeManualFinancialRequestId(payload),
"load",
SafeManualFinancialScreen(payload),
stockName: SafeManualFinancialStockName(payload));
return;
}
InitializeManualFinancialRuntime();
var service = _manualFinancialScreenService;
if (service is null)
{
PostManualFinancialUnavailable(
requestId,
"load",
ToWireValue(identity.Screen),
stockName: identity.StockName);
return;
}
await ExecuteManualFinancialReadAsync(
requestId,
"load",
identity.Screen,
string.Empty,
identity.StockName,
async cancellationToken =>
{
var snapshot = await service.GetAsync(identity, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
return new ManualFinancialBridgeReply(
"manual-financial-load-results",
new
{
requestId,
screen = ToWireValue(identity.Screen),
retrievedAt = DateTimeOffset.UtcNow,
snapshot = CreateManualFinancialSnapshotPayload(snapshot)
});
});
}
private async Task HandleManualFinancialSelectionRequestAsync(JsonElement payload)
{
if (!TryParseManualFinancialSelectionRequest(
payload,
out var requestId,
out var identity,
out var rowVersion,
out var stock))
{
PostManualFinancialInvalidRequest(
SafeManualFinancialRequestId(payload),
"selection",
SafeManualFinancialScreen(payload),
stockName: SafeManualFinancialStockName(payload));
return;
}
InitializeManualFinancialRuntime();
var service = _manualFinancialScreenService;
if (service is null || _stockSearchService is null)
{
PostManualFinancialUnavailable(
requestId,
"selection",
ToWireValue(identity.Screen),
stockName: identity.StockName);
return;
}
await ExecuteManualFinancialReadAsync(
requestId,
"selection",
identity.Screen,
string.Empty,
identity.StockName,
async cancellationToken =>
{
var snapshot = await service.GetAsync(identity, cancellationToken);
EnsureManualFinancialRowVersion(snapshot, rowVersion);
var verified = await ResolveManualFinancialStockAsync(
identity,
stock,
cancellationToken);
var result = ManualFinancialCutContracts.CreateSelection(snapshot, verified);
cancellationToken.ThrowIfCancellationRequested();
return new ManualFinancialBridgeReply(
"manual-financial-selection-results",
new
{
requestId,
screen = ToWireValue(identity.Screen),
stockName = identity.StockName,
rowVersion = snapshot.RowVersion,
result.BuilderKey,
code = result.CutCode,
aliases = new[] { result.CutCode },
selection = new
{
result.Selection.GroupCode,
result.Selection.Subject,
result.Selection.GraphicType,
result.Selection.Subtype,
result.Selection.DataCode
},
result.CurrentPage,
result.TotalPages
});
});
}
private async Task HandleManualFinancialCreateRequestAsync(JsonElement payload)
{
if (!TryParseManualFinancialCreateRequest(
payload,
out var requestId,
out var record,
out var stock))
{
PostManualFinancialInvalidRequest(
SafeManualFinancialRequestId(payload),
"create",
SafeManualFinancialScreen(payload),
stockName: SafeManualFinancialRecordStockName(payload));
return;
}
InitializeManualFinancialRuntime();
await ExecuteManualFinancialWriteAsync(
requestId,
"create",
record.Identity.Screen,
record.Identity.StockName,
async cancellationToken =>
{
_ = await ResolveManualFinancialStockAsync(
record.Identity,
stock,
cancellationToken);
},
(service, cancellationToken) => service.CreateAsync(record, cancellationToken));
}
private async Task HandleManualFinancialUpdateRequestAsync(JsonElement payload)
{
if (!TryParseManualFinancialUpdateRequest(
payload,
out var requestId,
out var replacement,
out var expectedRowVersion))
{
PostManualFinancialInvalidRequest(
SafeManualFinancialRequestId(payload),
"update",
SafeManualFinancialScreen(payload),
stockName: SafeManualFinancialStockName(payload));
return;
}
InitializeManualFinancialRuntime();
ManualFinancialSnapshot? expected = null;
await ExecuteManualFinancialWriteAsync(
requestId,
"update",
replacement.Identity.Screen,
replacement.Identity.StockName,
async cancellationToken =>
{
var service = RequireManualFinancialService();
expected = await service.GetAsync(replacement.Identity, cancellationToken);
EnsureManualFinancialRowVersion(expected, expectedRowVersion);
},
(service, cancellationToken) => service.UpdateAsync(
expected ?? throw new InvalidOperationException("Update preflight did not complete."),
replacement,
cancellationToken));
}
private async Task HandleManualFinancialDeleteRequestAsync(JsonElement payload)
{
if (!TryParseManualFinancialDeleteRequest(
payload,
out var requestId,
out var identity,
out var expectedRowVersion))
{
PostManualFinancialInvalidRequest(
SafeManualFinancialRequestId(payload),
"delete-one",
SafeManualFinancialScreen(payload),
stockName: SafeManualFinancialStockName(payload));
return;
}
InitializeManualFinancialRuntime();
ManualFinancialSnapshot? expected = null;
await ExecuteManualFinancialWriteAsync(
requestId,
"delete-one",
identity.Screen,
identity.StockName,
async cancellationToken =>
{
var service = RequireManualFinancialService();
expected = await service.GetAsync(identity, cancellationToken);
EnsureManualFinancialRowVersion(expected, expectedRowVersion);
},
(service, cancellationToken) => service.DeleteAsync(
expected ?? throw new InvalidOperationException("Delete preflight did not complete."),
cancellationToken));
}
private async Task HandleManualFinancialDeleteAllRequestAsync(JsonElement payload)
{
if (!TryParseManualFinancialDeleteAllRequest(
payload,
out var requestId,
out var screen))
{
PostManualFinancialInvalidRequest(
SafeManualFinancialRequestId(payload),
"delete-all",
SafeManualFinancialScreen(payload));
return;
}
InitializeManualFinancialRuntime();
await ExecuteManualFinancialWriteAsync(
requestId,
"delete-all",
screen,
string.Empty,
preflight: null,
(service, cancellationToken) => service.DeleteAllAsync(screen, cancellationToken));
}
private async Task ExecuteManualFinancialReadAsync(
string requestId,
string operation,
ManualFinancialScreenKind screen,
string query,
string stockName,
Func<CancellationToken, Task<ManualFinancialBridgeReply>> operationBody)
{
var requestCancellation = CancellationTokenSource.CreateLinkedTokenSource(
_lifetimeCancellation.Token);
var requestToken = requestCancellation.Token;
var browserGeneration = Volatile.Read(ref _manualFinancialBrowserGeneration);
var request = new ManualFinancialReadRequest(
operation,
requestId,
screen,
query,
stockName,
browserGeneration,
requestCancellation);
RegisterManualFinancialRead(request);
var databaseActivityEntered = false;
try
{
if (Volatile.Read(ref _playoutCommandInFlight) != 0)
{
requestCancellation.Cancel();
PostManualFinancialReadCancellationIfCurrent(
request,
"PLAYOUT_PRIORITY",
"The read was canceled because playout has database priority.");
return;
}
await _databaseActivityGate.WaitAsync(requestToken);
databaseActivityEntered = true;
requestToken.ThrowIfCancellationRequested();
if (Volatile.Read(ref _playoutCommandInFlight) != 0)
{
requestCancellation.Cancel();
PostManualFinancialReadCancellationIfCurrent(
request,
"PLAYOUT_PRIORITY",
"The read was canceled because playout has database priority.");
return;
}
if (browserGeneration != Volatile.Read(ref _manualFinancialBrowserGeneration))
{
requestCancellation.Cancel();
return;
}
var reply = await operationBody(requestToken);
requestToken.ThrowIfCancellationRequested();
if (browserGeneration != Volatile.Read(ref _manualFinancialBrowserGeneration) ||
!TryCompleteManualFinancialReadIfCurrent(request))
{
return;
}
PostMessage(reply.MessageType, reply.Payload);
}
catch (OperationCanceledException) when (requestToken.IsCancellationRequested)
{
PostManualFinancialReadCancellationIfCurrent(
request,
"PLAYOUT_PRIORITY",
"The read was canceled because playout has database priority.");
}
catch (DatabaseOperationException) when (requestToken.IsCancellationRequested)
{
PostManualFinancialReadCancellationIfCurrent(
request,
"PLAYOUT_PRIORITY",
"The read was canceled because playout has database priority.");
}
catch (Exception) when (requestToken.IsCancellationRequested)
{
PostManualFinancialReadCancellationIfCurrent(
request,
"PLAYOUT_PRIORITY",
"The read was canceled because playout has database priority.");
}
catch (ManualFinancialBridgeStaleException)
{
PostManualFinancialReadErrorIfCurrent(
request,
"STALE_ROW",
"The manual-financial record changed. Reload before selecting it.",
retryable: false);
}
catch (ManualFinancialNotFoundException)
{
PostManualFinancialReadErrorIfCurrent(
request,
"NOT_FOUND",
"The manual-financial record does not exist.",
retryable: false);
}
catch (ManualFinancialAmbiguousIdentityException)
{
PostManualFinancialReadErrorIfCurrent(
request,
"AMBIGUOUS_IDENTITY",
"The manual-financial STOCK_NAME is duplicated.",
retryable: false);
}
catch (ManualFinancialDataException)
{
PostManualFinancialReadErrorIfCurrent(
request,
"INVALID_DATA",
"Stored manual-financial data does not match the migrated contract.",
retryable: false);
_ = PostDatabaseStatusAsync(_lifetimeCancellation.Token);
}
catch (StockSearchDataException)
{
PostManualFinancialReadErrorIfCurrent(
request,
"INVALID_STOCK_DATA",
"The stock master result is ambiguous or invalid.",
retryable: false);
}
catch (ArgumentException)
{
PostManualFinancialReadErrorIfCurrent(
request,
"INVALID_REQUEST",
"The manual-financial request identity is invalid or ambiguous.",
retryable: false);
}
catch (DatabaseOperationException exception)
{
PostManualFinancialReadErrorIfCurrent(
request,
"DATABASE_ERROR",
exception.Message,
retryable: true);
_ = PostDatabaseStatusAsync(_lifetimeCancellation.Token);
}
catch
{
PostManualFinancialReadErrorIfCurrent(
request,
"DATABASE_ERROR",
"The manual-financial database read failed.",
retryable: true);
_ = PostDatabaseStatusAsync(_lifetimeCancellation.Token);
}
finally
{
if (databaseActivityEntered)
{
_databaseActivityGate.Release();
}
ReleaseManualFinancialRead(request);
requestCancellation.Dispose();
}
}
private async Task ExecuteManualFinancialWriteAsync(
string requestId,
string operation,
ManualFinancialScreenKind screen,
string stockName,
Func<CancellationToken, Task>? preflight,
Func<IManualFinancialScreenService, CancellationToken, Task<ManualFinancialMutationReceipt>>
operationBody)
{
var service = _manualFinancialScreenService;
if (service is null || !service.CanMutate)
{
PostManualFinancialUnavailable(
requestId,
operation,
ToWireValue(screen),
stockName: stockName);
return;
}
if (IsManualFinancialWriteQuarantined())
{
PostManualFinancialQuarantined(requestId, operation, screen, stockName);
return;
}
if (!await _manualFinancialWriteGate.WaitAsync(0))
{
PostManualFinancialMutationError(
requestId,
operation,
screen,
stockName,
"WRITE_BUSY",
"Another manual-financial write is already running.",
outcomeUnknown: false);
return;
}
var playoutGateEntered = false;
var databaseActivityEntered = false;
var mutationDispatched = false;
var requestCancellation = CancellationTokenSource.CreateLinkedTokenSource(
_lifetimeCancellation.Token);
var requestToken = requestCancellation.Token;
var browserGeneration = Volatile.Read(ref _manualFinancialBrowserGeneration);
Volatile.Write(ref _manualFinancialWriteCancellation, requestCancellation);
try
{
playoutGateEntered = await _playoutCommandGate.WaitAsync(0, requestToken);
if (!playoutGateEntered || !CanStartManualFinancialWrite())
{
PostManualFinancialPlayoutActive(requestId, operation, screen, stockName);
return;
}
databaseActivityEntered = await _databaseActivityGate.WaitAsync(0, requestToken);
if (!databaseActivityEntered)
{
PostManualFinancialMutationError(
requestId,
operation,
screen,
stockName,
"DATABASE_BUSY",
"The database is busy. Reload before making a new explicit write request.",
outcomeUnknown: false);
return;
}
requestToken.ThrowIfCancellationRequested();
if (!CanStartManualFinancialWrite())
{
PostManualFinancialPlayoutActive(requestId, operation, screen, stockName);
return;
}
if (browserGeneration != Volatile.Read(ref _manualFinancialBrowserGeneration))
{
requestCancellation.Cancel();
return;
}
if (preflight is not null)
{
await preflight(requestToken);
requestToken.ThrowIfCancellationRequested();
}
if (!CanStartManualFinancialWrite() ||
browserGeneration != Volatile.Read(ref _manualFinancialBrowserGeneration))
{
requestCancellation.Cancel();
return;
}
mutationDispatched = true;
Interlocked.Exchange(ref _manualFinancialWriteInFlight, 1);
var receipt = await operationBody(service, requestToken);
if (browserGeneration != Volatile.Read(ref _manualFinancialBrowserGeneration) ||
!ReferenceEquals(
Volatile.Read(ref _manualFinancialWriteCancellation),
requestCancellation))
{
LatchManualFinancialWriteQuarantine(
ManualFinancialCorrelationQuarantineMessage);
return;
}
PostMessage("manual-financial-mutation-result", new
{
requestId,
operationId = receipt.OperationId.ToString("N", CultureInfo.InvariantCulture),
operation,
screen = ToWireValue(receipt.Screen),
receipt.StockName,
receipt.CommittedAt,
receipt.AffectedRows
});
}
catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested)
{
if (mutationDispatched)
{
LatchManualFinancialWriteQuarantine(
ManualFinancialCorrelationQuarantineMessage);
}
}
catch (ManualFinancialBridgeStaleException)
{
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
"STALE_ROW",
"The record changed. Reload before submitting a new explicit write.",
outcomeUnknown: false,
requestCancellation);
}
catch (ManualFinancialNotFoundException)
{
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
"NOT_FOUND",
"The manual-financial record no longer exists.",
outcomeUnknown: false,
requestCancellation);
}
catch (ManualFinancialAmbiguousIdentityException)
{
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
"AMBIGUOUS_IDENTITY",
"The manual-financial STOCK_NAME is duplicated. No write was attempted.",
outcomeUnknown: false,
requestCancellation);
}
catch (ManualFinancialMutationRejectedException exception)
{
var (code, message) = exception.Reason switch
{
ManualFinancialMutationRejectionReason.DuplicateIdentity =>
("DUPLICATE_IDENTITY", "The stock already exists. Reload before editing it."),
ManualFinancialMutationRejectionReason.NotFoundOrChanged =>
("STALE_ROW", "The record changed or disappeared. Reload before editing it."),
_ =>
("AMBIGUOUS_IDENTITY", "The manual-financial identity is ambiguous.")
};
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
code,
message,
outcomeUnknown: false,
requestCancellation);
}
catch (ManualFinancialMutationException exception)
{
if (exception.OutcomeUnknown)
{
LatchManualFinancialWriteQuarantine(exception.Message);
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
"OUTCOME_UNKNOWN",
ManualFinancialCorrelationQuarantineMessage,
outcomeUnknown: true,
requestCancellation);
}
else
{
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
"WRITE_FAILED",
"The Oracle transaction rolled back and was not retried.",
outcomeUnknown: false,
requestCancellation);
}
}
catch (ArgumentException)
{
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
"INVALID_REQUEST",
"The manual-financial write identity or record is invalid.",
outcomeUnknown: false,
requestCancellation);
}
catch (StockSearchDataException)
{
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
"INVALID_STOCK_DATA",
"The stock master result is ambiguous or invalid.",
outcomeUnknown: false,
requestCancellation);
}
catch (OperationCanceledException)
{
if (mutationDispatched)
{
LatchManualFinancialWriteQuarantine(
ManualFinancialCorrelationQuarantineMessage);
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
"OUTCOME_UNKNOWN",
ManualFinancialCorrelationQuarantineMessage,
outcomeUnknown: true,
requestCancellation);
}
else
{
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
"CANCELLED",
"The write stopped before an Oracle transaction began.",
outcomeUnknown: false,
requestCancellation);
}
}
catch
{
if (mutationDispatched)
{
LatchManualFinancialWriteQuarantine(
ManualFinancialCorrelationQuarantineMessage);
}
PostManualFinancialWriteErrorIfCurrent(
requestId,
operation,
screen,
stockName,
mutationDispatched ? "OUTCOME_UNKNOWN" : "WRITE_FAILED",
mutationDispatched
? ManualFinancialCorrelationQuarantineMessage
: "The write failed before an Oracle transaction began.",
outcomeUnknown: mutationDispatched,
requestCancellation);
}
finally
{
Interlocked.Exchange(ref _manualFinancialWriteInFlight, 0);
if (databaseActivityEntered)
{
_databaseActivityGate.Release();
}
if (playoutGateEntered)
{
_playoutCommandGate.Release();
}
Interlocked.CompareExchange(
ref _manualFinancialWriteCancellation,
null,
requestCancellation);
requestCancellation.Dispose();
_manualFinancialWriteGate.Release();
}
}
private bool CanStartManualFinancialWrite()
{
if (Volatile.Read(ref _playoutCommandInFlight) != 0 ||
Volatile.Read(ref _playoutShutdownStarted) != 0 ||
IsBrowserCorrelationQuarantined() ||
IsManualFinancialWriteQuarantined())
{
return false;
}
return CanStartOperatorMutation(IsManualFinancialWriteQuarantined());
}
private async Task<VerifiedManualFinancialStock> ResolveManualFinancialStockAsync(
ManualFinancialIdentity identity,
ManualFinancialStockWire stock,
CancellationToken cancellationToken)
{
var stockService = _stockSearchService ?? throw new InvalidOperationException(
"The domestic stock-search service is unavailable.");
if (!string.Equals(identity.StockName, stock.Name, StringComparison.Ordinal))
{
throw new ArgumentException("The supplied stock name does not match the record.");
}
var search = await stockService.SearchAsync(
identity.StockName,
LegacyStockSearchService.MaximumResults,
cancellationToken);
var verified = ManualFinancialCutContracts.VerifyStock(
identity,
search.Items,
stock.Market,
stock.Code);
if (verified.Source != stock.Source ||
!string.Equals(verified.Name, stock.Name, StringComparison.Ordinal))
{
throw new ArgumentException("The supplied stock provider identity is invalid.");
}
return verified;
}
private IManualFinancialScreenService RequireManualFinancialService() =>
_manualFinancialScreenService ?? throw new InvalidOperationException(
"The manual-financial service is unavailable.");
private static void EnsureManualFinancialRowVersion(
ManualFinancialSnapshot snapshot,
string expectedRowVersion)
{
if (!string.Equals(snapshot.RowVersion, expectedRowVersion, StringComparison.Ordinal))
{
throw new ManualFinancialBridgeStaleException();
}
}
private void RegisterManualFinancialRead(ManualFinancialReadRequest request)
{
ManualFinancialReadRequest? superseded;
var shouldPostSuperseded = false;
lock (_manualFinancialReadLanesGate)
{
_manualFinancialReadLanes.TryGetValue(request.Lane, out superseded);
_manualFinancialReadLanes[request.Lane] = request;
shouldPostSuperseded = superseded?.TryComplete() == true;
}
if (superseded is null)
{
return;
}
CancelRequest(superseded.Cancellation);
if (shouldPostSuperseded)
{
PostManualFinancialReadError(
superseded.RequestId,
superseded.Operation,
superseded.Screen,
superseded.Query,
superseded.StockName,
"CANCELLED",
"The read was canceled because a newer request replaced it.",
retryable: true);
}
}
private bool TryCompleteManualFinancialReadIfCurrent(ManualFinancialReadRequest request)
{
lock (_manualFinancialReadLanesGate)
{
return request.BrowserGeneration ==
Volatile.Read(ref _manualFinancialBrowserGeneration) &&
_manualFinancialReadLanes.TryGetValue(request.Lane, out var current) &&
ReferenceEquals(current, request) &&
request.TryComplete();
}
}
private void ReleaseManualFinancialRead(ManualFinancialReadRequest request)
{
lock (_manualFinancialReadLanesGate)
{
if (_manualFinancialReadLanes.TryGetValue(request.Lane, out var current) &&
ReferenceEquals(current, request))
{
_manualFinancialReadLanes.Remove(request.Lane);
}
}
}
private ManualFinancialReadRequest[] SnapshotManualFinancialReads(bool clear)
{
lock (_manualFinancialReadLanesGate)
{
var requests = _manualFinancialReadLanes.Values.ToArray();
if (clear)
{
_manualFinancialReadLanes.Clear();
foreach (var request in requests)
{
_ = request.TryComplete();
}
}
return requests;
}
}
private void PostManualFinancialReadCancellationIfCurrent(
ManualFinancialReadRequest request,
string code,
string message) =>
PostManualFinancialReadErrorIfCurrent(
request,
code,
message,
retryable: true);
private void PostManualFinancialReadErrorIfCurrent(
ManualFinancialReadRequest request,
string code,
string message,
bool retryable)
{
if (_lifetimeCancellation.IsCancellationRequested ||
!TryCompleteManualFinancialReadIfCurrent(request))
{
return;
}
PostManualFinancialReadError(
request.RequestId,
request.Operation,
request.Screen,
request.Query,
request.StockName,
code,
message,
retryable);
}
private void PostManualFinancialWriteErrorIfCurrent(
string requestId,
string operation,
ManualFinancialScreenKind screen,
string stockName,
string code,
string message,
bool outcomeUnknown,
CancellationTokenSource requestCancellation)
{
if (_lifetimeCancellation.IsCancellationRequested ||
!ReferenceEquals(
Volatile.Read(ref _manualFinancialWriteCancellation),
requestCancellation))
{
return;
}
PostManualFinancialMutationError(
requestId,
operation,
screen,
stockName,
code,
message,
outcomeUnknown);
}
private void PostManualFinancialReadError(
string requestId,
string operation,
ManualFinancialScreenKind screen,
string query,
string stockName,
string code,
string message,
bool retryable)
{
if (operation == "list")
{
PostMessage("manual-financial-list-error", new
{
requestId,
screen = ToWireValue(screen),
query,
message
});
return;
}
PostMessage(
operation == "selection"
? "manual-financial-selection-error"
: "manual-financial-load-error",
new
{
requestId,
screen = ToWireValue(screen),
stockName,
code,
message,
retryable
});
}
private void PostManualFinancialInvalidRequest(
string requestId,
string operation,
string screen,
string query = "",
string stockName = "")
{
if (TryParseManualFinancialScreen(screen, out var parsedScreen))
{
if (operation is "list" or "load" or "selection")
{
PostManualFinancialReadError(
requestId,
operation,
parsedScreen,
query,
stockName,
"INVALID_REQUEST",
"The manual-financial request payload is invalid.",
retryable: false);
return;
}
PostManualFinancialMutationError(
requestId,
operation,
parsedScreen,
stockName,
"INVALID_REQUEST",
"The manual-financial write payload is invalid.",
outcomeUnknown: false);
return;
}
PostMessage("manual-financial-request-error", new
{
requestId,
operation,
code = "INVALID_REQUEST",
message = "The manual-financial request payload is invalid."
});
}
private void PostManualFinancialUnavailable(
string requestId,
string operation,
string screen,
string query = "",
string stockName = "")
{
if (TryParseManualFinancialScreen(screen, out var parsedScreen) &&
operation is "list" or "load" or "selection")
{
PostManualFinancialReadError(
requestId,
operation,
parsedScreen,
query,
stockName,
"DATABASE_UNAVAILABLE",
_manualFinancialInitializationError ??
_databaseInitializationError ??
"The manual-financial database boundary is unavailable.",
retryable: false);
return;
}
if (TryParseManualFinancialScreen(screen, out parsedScreen))
{
PostManualFinancialMutationError(
requestId,
operation,
parsedScreen,
stockName,
"DATABASE_UNAVAILABLE",
_manualFinancialInitializationError ??
_databaseInitializationError ??
"The manual-financial database boundary is unavailable.",
outcomeUnknown: false);
}
}
private void PostManualFinancialPlayoutActive(
string requestId,
string operation,
ManualFinancialScreenKind screen,
string stockName) =>
PostManualFinancialMutationError(
requestId,
operation,
screen,
stockName,
"PLAYOUT_ACTIVE",
"Manual-financial writes are allowed only while playout is fully IDLE.",
outcomeUnknown: false);
private void PostManualFinancialQuarantined(
string requestId,
string operation,
ManualFinancialScreenKind screen,
string stockName) =>
PostManualFinancialMutationError(
requestId,
operation,
screen,
stockName,
"OUTCOME_UNKNOWN",
GetOperatorMutationQuarantineMessage(
Volatile.Read(ref _manualFinancialWriteQuarantineMessage) ??
ManualFinancialCorrelationQuarantineMessage),
outcomeUnknown: true);
private void PostManualFinancialMutationError(
string requestId,
string operation,
ManualFinancialScreenKind screen,
string stockName,
string code,
string message,
bool outcomeUnknown)
{
PostMessage("manual-financial-mutation-error", new
{
requestId,
operation,
screen = ToWireValue(screen),
stockName,
code,
message,
retryable = false,
outcomeUnknown
});
}
private bool IsManualFinancialWriteQuarantined() =>
Volatile.Read(ref _manualFinancialWriteQuarantined) != 0 ||
IsOperatorMutationQuarantined();
private void LatchManualFinancialWriteQuarantine(string message)
{
LatchOperatorMutationQuarantine(message);
if (Interlocked.CompareExchange(ref _manualFinancialWriteQuarantined, 1, 0) == 0)
{
Volatile.Write(
ref _manualFinancialWriteQuarantineMessage,
string.IsNullOrWhiteSpace(message)
? ManualFinancialCorrelationQuarantineMessage
: message);
}
}
/// <summary>
/// Root browser-navigation/process-failure hook. Correlation loss during a
/// dispatched write permanently quarantines writes for this process.
/// </summary>
private void InvalidateManualFinancialBrowserRequests()
{
Interlocked.Increment(ref _manualFinancialBrowserGeneration);
if (Volatile.Read(ref _manualFinancialWriteInFlight) != 0)
{
LatchManualFinancialWriteQuarantine(
ManualFinancialCorrelationQuarantineMessage);
}
foreach (var request in SnapshotManualFinancialReads(clear: true))
{
CancelRequest(request.Cancellation);
}
CancelRequest(Interlocked.Exchange(ref _manualFinancialWriteCancellation, null));
}
/// <summary>Root playout-priority hook; writes own the playout gate.</summary>
private void CancelManualFinancialReadsForPlayout()
{
foreach (var request in SnapshotManualFinancialReads(clear: false))
{
CancelRequest(request.Cancellation);
}
}
/// <summary>Root window-close hook.</summary>
private void ShutdownManualFinancialRuntime()
{
if (Volatile.Read(ref _manualFinancialWriteInFlight) != 0)
{
LatchManualFinancialWriteQuarantine(
ManualFinancialCorrelationQuarantineMessage);
}
foreach (var request in SnapshotManualFinancialReads(clear: true))
{
CancelRequest(request.Cancellation);
}
CancelRequest(Interlocked.Exchange(ref _manualFinancialWriteCancellation, null));
_manualFinancialScreenService = null;
}
private static bool TryParseManualFinancialListRequest(
JsonElement payload,
out string requestId,
out ManualFinancialScreenKind screen,
out string query,
out int maximumResults)
{
requestId = string.Empty;
screen = default;
query = string.Empty;
maximumResults = LegacyManualFinancialScreenService.DefaultMaximumResults;
return payload.ValueKind == JsonValueKind.Object &&
HasOnlyProperties(payload, "requestId", "screen", "query", "maximumResults") &&
TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) &&
TryParseManualFinancialScreen(GetString(payload, "screen"), out screen) &&
TryGetManualFinancialQuery(payload, "query", out query) &&
TryGetOptionalManualFinancialLimit(payload, ref maximumResults);
}
private static bool TryParseManualFinancialIdentityRequest(
JsonElement payload,
string[] exactProperties,
out string requestId,
out ManualFinancialIdentity identity)
{
requestId = string.Empty;
identity = new ManualFinancialIdentity(default, string.Empty);
if (payload.ValueKind != JsonValueKind.Object ||
!HasOnlyProperties(payload, exactProperties) ||
!TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) ||
!TryParseManualFinancialScreen(GetString(payload, "screen"), out var screen) ||
!TryGetManualFinancialText(
payload,
"stockName",
200,
allowEmpty: false,
allowUnderscore: true,
out var stockName))
{
return false;
}
identity = new ManualFinancialIdentity(screen, stockName);
return true;
}
private static bool TryParseManualFinancialCreateRequest(
JsonElement payload,
out string requestId,
out ManualFinancialRecord record,
out ManualFinancialStockWire stock)
{
requestId = string.Empty;
record = null!;
stock = default;
return payload.ValueKind == JsonValueKind.Object &&
HasOnlyProperties(payload, "requestId", "screen", "stock", "record") &&
TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) &&
TryParseManualFinancialScreen(GetString(payload, "screen"), out var screen) &&
payload.TryGetProperty("record", out var recordElement) &&
TryParseManualFinancialRecord(recordElement, screen, out record) &&
payload.TryGetProperty("stock", out var stockElement) &&
TryParseManualFinancialStock(stockElement, record.Identity.StockName, out stock);
}
private static bool TryParseManualFinancialUpdateRequest(
JsonElement payload,
out string requestId,
out ManualFinancialRecord replacement,
out string expectedRowVersion)
{
requestId = string.Empty;
replacement = null!;
expectedRowVersion = string.Empty;
if (payload.ValueKind != JsonValueKind.Object ||
!HasOnlyProperties(
payload,
"requestId",
"screen",
"stockName",
"expectedRowVersion",
"record") ||
!TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) ||
!TryParseManualFinancialScreen(GetString(payload, "screen"), out var screen) ||
!TryGetManualFinancialText(
payload,
"stockName",
200,
allowEmpty: false,
allowUnderscore: true,
out var stockName) ||
!TryGetManualFinancialRowVersion(payload, "expectedRowVersion", out expectedRowVersion) ||
!payload.TryGetProperty("record", out var recordElement) ||
!TryParseManualFinancialRecord(recordElement, screen, out replacement))
{
return false;
}
return string.Equals(replacement.Identity.StockName, stockName, StringComparison.Ordinal);
}
private static bool TryParseManualFinancialDeleteRequest(
JsonElement payload,
out string requestId,
out ManualFinancialIdentity identity,
out string expectedRowVersion)
{
expectedRowVersion = string.Empty;
return TryParseManualFinancialIdentityRequest(
payload,
["requestId", "screen", "stockName", "expectedRowVersion"],
out requestId,
out identity) &&
TryGetManualFinancialRowVersion(
payload,
"expectedRowVersion",
out expectedRowVersion);
}
private static bool TryParseManualFinancialDeleteAllRequest(
JsonElement payload,
out string requestId,
out ManualFinancialScreenKind screen)
{
requestId = string.Empty;
screen = default;
if (payload.ValueKind != JsonValueKind.Object ||
!HasOnlyProperties(payload, "requestId", "screen", "confirmationToken") ||
!TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) ||
!TryParseManualFinancialScreen(GetString(payload, "screen"), out screen))
{
return false;
}
var expected = "DELETE_ALL_" + ManualFinancialCutContracts.Get(screen).TableName;
return string.Equals(
GetString(payload, "confirmationToken"),
expected,
StringComparison.Ordinal);
}
private static bool TryParseManualFinancialSelectionRequest(
JsonElement payload,
out string requestId,
out ManualFinancialIdentity identity,
out string rowVersion,
out ManualFinancialStockWire stock)
{
rowVersion = string.Empty;
stock = default;
if (!TryParseManualFinancialIdentityRequest(
payload,
["requestId", "screen", "stockName", "rowVersion", "stock"],
out requestId,
out identity) ||
!TryGetManualFinancialRowVersion(payload, "rowVersion", out rowVersion) ||
!payload.TryGetProperty("stock", out var stockElement) ||
!TryParseManualFinancialStock(stockElement, identity.StockName, out stock))
{
return false;
}
return true;
}
private static bool TryParseManualFinancialRecord(
JsonElement element,
ManualFinancialScreenKind screen,
out ManualFinancialRecord record)
{
record = null!;
if (element.ValueKind != JsonValueKind.Object ||
!TryGetManualFinancialText(
element,
"kind",
32,
allowEmpty: false,
allowUnderscore: false,
out var kind) ||
!string.Equals(kind, ToWireValue(screen), StringComparison.Ordinal) ||
!TryGetManualFinancialText(
element,
"stockName",
200,
allowEmpty: false,
allowUnderscore: true,
out var stockName))
{
return false;
}
var identity = new ManualFinancialIdentity(screen, stockName);
switch (screen)
{
case ManualFinancialScreenKind.RevenueComposition:
if (!HasOnlyProperties(element, "kind", "stockName", "baseDate", "slices") ||
!TryGetManualFinancialText(
element,
"baseDate",
200,
allowEmpty: true,
allowUnderscore: true,
out var baseDate) ||
!TryParseManualRevenueSlices(element, out var slices))
{
return false;
}
record = new ManualRevenueCompositionRecord(identity, baseDate, slices);
return true;
case ManualFinancialScreenKind.GrowthMetrics:
if (!HasOnlyProperties(
element,
"kind",
"stockName",
"salesGrowth",
"operatingProfitGrowth",
"netAssetGrowth",
"netIncomeGrowth",
"periods") ||
!TryParseManualGrowthSeries(element, "salesGrowth", out var salesGrowth) ||
!TryParseManualGrowthSeries(
element,
"operatingProfitGrowth",
out var operatingProfitGrowth) ||
!TryParseManualGrowthSeries(element, "netAssetGrowth", out var netAssetGrowth) ||
!TryParseManualGrowthSeries(element, "netIncomeGrowth", out var netIncomeGrowth) ||
!TryParseManualPeriods(element, out var periods))
{
return false;
}
record = new ManualGrowthMetricsRecord(
identity,
salesGrowth,
operatingProfitGrowth,
netAssetGrowth,
netIncomeGrowth,
periods);
return true;
case ManualFinancialScreenKind.Sales:
case ManualFinancialScreenKind.OperatingProfit:
if (!HasOnlyProperties(element, "kind", "stockName", "quarters") ||
!TryParseManualQuarters(element, out var quarters))
{
return false;
}
record = screen == ManualFinancialScreenKind.Sales
? new ManualSalesRecord(identity, quarters)
: new ManualOperatingProfitRecord(identity, quarters);
return true;
default:
return false;
}
}
private static bool TryParseManualRevenueSlices(
JsonElement record,
out IReadOnlyList<ManualRevenueSlice?> slices)
{
slices = Array.Empty<ManualRevenueSlice?>();
if (!record.TryGetProperty("slices", out var element) ||
element.ValueKind != JsonValueKind.Array || element.GetArrayLength() != 5)
{
return false;
}
var result = new List<ManualRevenueSlice?>(5);
foreach (var slice in element.EnumerateArray())
{
if (slice.ValueKind == JsonValueKind.Null)
{
result.Add(null);
continue;
}
if (slice.ValueKind != JsonValueKind.Object ||
!HasOnlyProperties(slice, "label", "percentageText", "percentage") ||
!TryGetManualFinancialText(
slice,
"label",
200,
allowEmpty: false,
allowUnderscore: false,
out var label) ||
!TryGetManualFinancialText(
slice,
"percentageText",
64,
allowEmpty: false,
allowUnderscore: false,
out var percentageText) ||
!slice.TryGetProperty("percentage", out var percentageElement) ||
percentageElement.ValueKind != JsonValueKind.Number ||
!percentageElement.TryGetDouble(out var percentage) ||
!double.IsFinite(percentage))
{
return false;
}
result.Add(new ManualRevenueSlice(label, percentageText, percentage));
}
slices = result;
return true;
}
private static bool TryParseManualGrowthSeries(
JsonElement record,
string propertyName,
out ManualGrowthSeriesValues values)
{
values = new ManualGrowthSeriesValues(null, null, null, null);
if (!record.TryGetProperty(propertyName, out var element) ||
element.ValueKind != JsonValueKind.Array || element.GetArrayLength() != 4)
{
return false;
}
var parsed = new double?[4];
var index = 0;
foreach (var value in element.EnumerateArray())
{
if (value.ValueKind == JsonValueKind.Null)
{
parsed[index++] = null;
continue;
}
if (value.ValueKind != JsonValueKind.Number ||
!value.TryGetDouble(out var number) || !double.IsFinite(number))
{
return false;
}
parsed[index++] = number;
}
values = new ManualGrowthSeriesValues(parsed[0], parsed[1], parsed[2], parsed[3]);
return true;
}
private static bool TryParseManualPeriods(
JsonElement record,
out ManualGrowthPeriodLabels periods)
{
periods = new ManualGrowthPeriodLabels(string.Empty, string.Empty, string.Empty, string.Empty);
if (!record.TryGetProperty("periods", out var element) ||
element.ValueKind != JsonValueKind.Array || element.GetArrayLength() != 4)
{
return false;
}
var parsed = new string[4];
var index = 0;
foreach (var value in element.EnumerateArray())
{
if (value.ValueKind != JsonValueKind.String ||
!IsSafeManualFinancialText(
value.GetString(),
200,
allowEmpty: true,
allowUnderscore: false,
out parsed[index++]))
{
return false;
}
}
periods = new ManualGrowthPeriodLabels(parsed[0], parsed[1], parsed[2], parsed[3]);
return true;
}
private static bool TryParseManualQuarters(
JsonElement record,
out IReadOnlyList<ManualQuarterValue> quarters)
{
quarters = Array.Empty<ManualQuarterValue>();
if (!record.TryGetProperty("quarters", out var element) ||
element.ValueKind != JsonValueKind.Array || element.GetArrayLength() != 6)
{
return false;
}
var result = new List<ManualQuarterValue>(6);
foreach (var item in element.EnumerateArray())
{
if (item.ValueKind != JsonValueKind.Object ||
!HasOnlyProperties(item, "quarter", "value") ||
!TryGetManualFinancialText(
item,
"quarter",
200,
allowEmpty: false,
allowUnderscore: false,
out var quarter) ||
!item.TryGetProperty("value", out var valueElement) ||
valueElement.ValueKind != JsonValueKind.Number ||
!valueElement.TryGetInt32(out var value))
{
return false;
}
result.Add(new ManualQuarterValue(quarter, value));
}
quarters = result;
return true;
}
private static bool TryParseManualFinancialStock(
JsonElement element,
string expectedName,
out ManualFinancialStockWire stock)
{
stock = default;
if (element.ValueKind != JsonValueKind.Object ||
!HasOnlyProperties(element, "market", "source", "name", "code") ||
!TryParseStockMarket(GetString(element, "market"), out var market) ||
!TryParseDataSource(GetString(element, "source"), out var source) ||
!TryGetManualFinancialText(
element,
"name",
200,
allowEmpty: false,
allowUnderscore: true,
out var name) ||
!TryGetManualFinancialStockCode(element, "code", out var code) ||
!string.Equals(name, expectedName, StringComparison.Ordinal) ||
(market is StockMarket.Kospi or StockMarket.Kosdaq
? source != DataSourceKind.Oracle
: source != DataSourceKind.MariaDb))
{
return false;
}
stock = new ManualFinancialStockWire(market, source, name, code);
return true;
}
private static bool TryGetOptionalManualFinancialLimit(
JsonElement payload,
ref int maximumResults)
{
if (!payload.TryGetProperty("maximumResults", out var element))
{
return true;
}
return element.ValueKind == JsonValueKind.Number &&
element.TryGetInt32(out maximumResults) &&
maximumResults is >= 1 and <= LegacyManualFinancialScreenService.MaximumResults;
}
private static bool TryGetManualFinancialQuery(
JsonElement payload,
string propertyName,
out string query)
{
query = string.Empty;
if (!payload.TryGetProperty(propertyName, out var element) ||
element.ValueKind != JsonValueKind.String)
{
return false;
}
var value = element.GetString();
return value is not null &&
value.Length <= LegacyManualFinancialScreenService.MaximumQueryLength &&
string.Equals(value, value.Trim(), StringComparison.Ordinal) &&
IsSafeManualFinancialText(
value,
LegacyManualFinancialScreenService.MaximumQueryLength,
allowEmpty: true,
allowUnderscore: true,
out query);
}
private static bool TryGetManualFinancialText(
JsonElement payload,
string propertyName,
int maximumLength,
bool allowEmpty,
bool allowUnderscore,
out string value)
{
value = string.Empty;
return payload.TryGetProperty(propertyName, out var element) &&
element.ValueKind == JsonValueKind.String &&
IsSafeManualFinancialText(
element.GetString(),
maximumLength,
allowEmpty,
allowUnderscore,
out value);
}
private static bool IsSafeManualFinancialText(
string? input,
int maximumLength,
bool allowEmpty,
bool allowUnderscore,
out string value)
{
value = input ?? string.Empty;
if (input is null || (!allowEmpty && input.Length == 0) ||
input.Length > maximumLength || input != input.Trim() ||
input.Contains('^') || (!allowUnderscore && input.Contains('_')))
{
return false;
}
foreach (var character in input)
{
var category = CharUnicodeInfo.GetUnicodeCategory(character);
if (char.IsControl(character) || char.IsSurrogate(character) ||
character == '\uFFFD' || category is
UnicodeCategory.Format or
UnicodeCategory.LineSeparator or
UnicodeCategory.ParagraphSeparator)
{
return false;
}
}
return true;
}
private static bool TryGetManualFinancialRowVersion(
JsonElement payload,
string propertyName,
out string value)
{
value = GetString(payload, propertyName) ?? string.Empty;
return value.Length == 64 && value.All(static character =>
char.IsAsciiDigit(character) || character is >= 'A' and <= 'F');
}
private static bool TryGetManualFinancialStockCode(
JsonElement payload,
string propertyName,
out string value)
{
value = GetString(payload, propertyName) ?? string.Empty;
return value.Length is >= 1 and <= 32 && value.All(char.IsAsciiLetterOrDigit);
}
private static bool TryParseManualFinancialScreen(
string? value,
out ManualFinancialScreenKind screen)
{
screen = value switch
{
"revenue-composition" => ManualFinancialScreenKind.RevenueComposition,
"growth-metrics" => ManualFinancialScreenKind.GrowthMetrics,
"sales" => ManualFinancialScreenKind.Sales,
"operating-profit" => ManualFinancialScreenKind.OperatingProfit,
_ => default
};
return value is "revenue-composition" or "growth-metrics" or
"sales" or "operating-profit";
}
private static string ToWireValue(ManualFinancialScreenKind screen) => screen switch
{
ManualFinancialScreenKind.RevenueComposition => "revenue-composition",
ManualFinancialScreenKind.GrowthMetrics => "growth-metrics",
ManualFinancialScreenKind.Sales => "sales",
ManualFinancialScreenKind.OperatingProfit => "operating-profit",
_ => throw new ArgumentOutOfRangeException(nameof(screen))
};
private static bool TryParseStockMarket(string? value, out StockMarket market)
{
market = value switch
{
"kospi" => StockMarket.Kospi,
"kosdaq" => StockMarket.Kosdaq,
"nxt-kospi" => StockMarket.NxtKospi,
"nxt-kosdaq" => StockMarket.NxtKosdaq,
_ => default
};
return value is "kospi" or "kosdaq" or "nxt-kospi" or "nxt-kosdaq";
}
private static bool TryParseDataSource(string? value, out DataSourceKind source)
{
source = value switch
{
"oracle" => DataSourceKind.Oracle,
"mariaDb" => DataSourceKind.MariaDb,
_ => default
};
return value is "oracle" or "mariaDb";
}
private static object CreateManualFinancialSnapshotPayload(ManualFinancialSnapshot snapshot) =>
new
{
stockName = snapshot.Record.Identity.StockName,
snapshot.RowVersion,
record = CreateManualFinancialRecordPayload(snapshot.Record)
};
private static object CreateManualFinancialRecordPayload(ManualFinancialRecord record) =>
record switch
{
ManualRevenueCompositionRecord revenue => new
{
kind = ToWireValue(revenue.Identity.Screen),
stockName = revenue.Identity.StockName,
revenue.BaseDate,
slices = revenue.Slices.Select(slice => slice is null
? null
: (object)new
{
slice.Label,
slice.PercentageText,
slice.Percentage
})
},
ManualGrowthMetricsRecord growth => new
{
kind = ToWireValue(growth.Identity.Screen),
stockName = growth.Identity.StockName,
salesGrowth = growth.SalesGrowth.ToArray(),
operatingProfitGrowth = growth.OperatingProfitGrowth.ToArray(),
netAssetGrowth = growth.NetAssetGrowth.ToArray(),
netIncomeGrowth = growth.NetIncomeGrowth.ToArray(),
periods = growth.Periods.ToArray()
},
ManualSalesRecord sales => new
{
kind = ToWireValue(sales.Identity.Screen),
stockName = sales.Identity.StockName,
quarters = sales.Quarters.Select(quarter => new
{
quarter.Quarter,
quarter.Value
})
},
ManualOperatingProfitRecord profit => new
{
kind = ToWireValue(profit.Identity.Screen),
stockName = profit.Identity.StockName,
quarters = profit.Quarters.Select(quarter => new
{
quarter.Quarter,
quarter.Value
})
},
_ => throw new ArgumentOutOfRangeException(nameof(record))
};
private static string SafeManualFinancialRequestId(JsonElement payload) =>
payload.ValueKind == JsonValueKind.Object &&
TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out var requestId)
? requestId
: string.Empty;
private static string SafeManualFinancialScreen(JsonElement payload)
{
var value = payload.ValueKind == JsonValueKind.Object
? GetString(payload, "screen")
: null;
return TryParseManualFinancialScreen(value, out var screen)
? ToWireValue(screen)
: string.Empty;
}
private static string SafeManualFinancialQuery(JsonElement payload) =>
payload.ValueKind == JsonValueKind.Object &&
TryGetManualFinancialQuery(payload, "query", out var query)
? query
: string.Empty;
private static string SafeManualFinancialStockName(JsonElement payload) =>
payload.ValueKind == JsonValueKind.Object &&
TryGetManualFinancialText(
payload,
"stockName",
200,
allowEmpty: false,
allowUnderscore: true,
out var stockName)
? stockName
: string.Empty;
private static string SafeManualFinancialRecordStockName(JsonElement payload)
{
if (payload.ValueKind != JsonValueKind.Object ||
!payload.TryGetProperty("record", out var record) ||
record.ValueKind != JsonValueKind.Object)
{
return string.Empty;
}
return SafeManualFinancialStockName(record);
}
private readonly record struct ManualFinancialBridgeReply(string MessageType, object Payload);
private sealed class ManualFinancialReadRequest
{
private int _terminalState;
public ManualFinancialReadRequest(
string lane,
string requestId,
ManualFinancialScreenKind screen,
string query,
string stockName,
int browserGeneration,
CancellationTokenSource cancellation)
{
Lane = lane;
RequestId = requestId;
Operation = lane;
Screen = screen;
Query = query;
StockName = stockName;
BrowserGeneration = browserGeneration;
Cancellation = cancellation;
}
public string Lane { get; }
public string RequestId { get; }
public string Operation { get; }
public ManualFinancialScreenKind Screen { get; }
public string Query { get; }
public string StockName { get; }
public int BrowserGeneration { get; }
public CancellationTokenSource Cancellation { get; }
public bool TryComplete() => Interlocked.CompareExchange(ref _terminalState, 1, 0) == 0;
}
private readonly record struct ManualFinancialStockWire(
StockMarket Market,
DataSourceKind Source,
string Name,
string Code);
private sealed class ManualFinancialBridgeStaleException : Exception;
}