Compare commits
No commits in common. "954e5ef86a41afe51948b55f6ba7e7612103ed1a" and "d2bdf7e4ebfcfccfddb4fdd872266c948733273e" have entirely different histories.
954e5ef86a
...
d2bdf7e4eb
|
|
@ -138,16 +138,7 @@
|
|||
"Bash(awk '/^; Assembly listing for method AyCode.Core.Tests.TestModels.TestOrder_All_False_GeneratedWriter:WriteProperties/{found=1} found && /^; Total bytes of code/{print; exit}' jitasm_tier1.txt)",
|
||||
"Bash(sed -n '50108,58000p' jitasm_tier1.txt)",
|
||||
"Bash(awk '/^; Assembly listing for method AyCode.Core.Tests.TestModels.TestOrder_All_False_GeneratedWriter:WriteProperties/{found=1; next} found && /^; Assembly listing for method/{exit} found && /mov[[:space:]]+byte[[:space:]]+ptr/{print}' jitasm_tier1.txt)",
|
||||
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" log --oneline -15 -- \"AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs\")",
|
||||
"Bash(dotnet workload *)",
|
||||
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" mv \"AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs\" \"AyCode.Services/SignalRs/AcSignalRDataSource.cs\")",
|
||||
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" mv \"AyCode.Services.Server/SignalRs/TrackingItemHelpers.cs\" \"AyCode.Services/SignalRs/TrackingItemHelpers.cs\")",
|
||||
"Bash(echo \"=== EXIT $? ===\")",
|
||||
"Bash(rm -rf \"H:/Applications/Mango/Source/FruitBankHybridApp/FruitBankHybrid.Web.Client/obj\" \"H:/Applications/Aycode/Source/AyCode.Blazor/AyCode.Blazor.Components/obj\" && dotnet publish \"H:/Applications/Mango/Source/FruitBankHybridApp/FruitBankHybrid.Web.Client/FruitBankHybrid.Web.Client.csproj\" -c Release 2>&1)",
|
||||
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" mv \"AyCode.Services.Server/docs/SIGNALR_DATASOURCE\" \"AyCode.Services/docs/SIGNALR_DATASOURCE\")",
|
||||
"Bash(awk 'NR==135' \"H:/Applications/Aycode/Source/AyCode.Core/.github/LLM_PROTOCOL_DECISIONS.md\")",
|
||||
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" status --short)",
|
||||
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" reset)"
|
||||
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" log --oneline -15 -- \"AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
# LLM Protocol Decisions — Archive: 2026-04
|
||||
|
||||
Archived entries from `LLM_PROTOCOL_DECISIONS.md` per the **last-15-active retention policy** (LLMP-DEC-67). All entries here have IDs in the LLMP-DEC namespace; year-month bucket = **2026-04** (entries dated 2026-04-XX).
|
||||
Archived entries from `LLM_PROTOCOL_DECISIONS.md` per the **last-15-active retention policy** (LLMP-DEC-65). All entries here have IDs in the LLMP-DEC namespace; year-month bucket = **2026-04** (entries dated 2026-04-XX).
|
||||
|
||||
> **Archive files are NOT auto-loaded** by `docs-discovery` or any session-start pre-load. Read on-demand when historical context is needed (e.g., a recent entry references LLMP-DEC-X — open this file to read the full text). IDs are preserved (never reassigned). Format identical to the active log.
|
||||
>
|
||||
|
|
|
|||
|
|
@ -75,11 +75,11 @@ The `<RAND>` suffix is **4 characters from `[A-Z0-9]`** (36⁴ ≈ 1.7 million c
|
|||
**Generation rules**:
|
||||
|
||||
1. Each new entry receives a fresh random suffix at creation time.
|
||||
2. Before finalizing: the agent globs existing entries (active topic file + all year-month bucket archive files for that topic) and verifies the suffix is not yet used.
|
||||
2. Before finalizing: the agent globs existing entries (active topic file + all year-bucketed archive files for that topic) and verifies the suffix is not yet used.
|
||||
3. If collision detected (extremely rare — birthday-paradox 50% probability at ~1300 entries per topic-type-prefix triple): regenerate the suffix.
|
||||
4. The suffix is **append-only** once assigned — never renumbered, never recycled, never reassigned to a different entry.
|
||||
|
||||
**At archive time** (`docs-archive` skill): performs collision-check before moving entries to year-month bucket archive files. If collision detected with an existing archive entry: skill aborts, signals the user, awaits manual resolution — no silent corruption.
|
||||
**At archive time** (`docs-archive` skill): performs collision-check before moving entries to year-bucketed archive files. If collision detected with an existing archive entry: skill aborts, signals the user, awaits manual resolution — no silent corruption.
|
||||
|
||||
## LLMP exception
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ Full doctrine: `../docs/ARCHITECTURE.md#framework-vs-consumer-boundary`
|
|||
|
||||
## SignalR
|
||||
7. **Tag-based transport (no conventional hub methods)** — SignalR communication should generally use the generic methods provided by `AcWebSignalRHubBase` (server) and `AcSignalRClientBase` (client). Request types are conventionally identified by `int` tags. Try to avoid adding custom, business-specific, or conventional string-based Hub methods (e.g., `GetUsers()`).
|
||||
8. **AcSignalRDataSource** — generic `IList<T>` with change tracking, CRUD via `SignalRCrudTags`, binary merge, rollback. See `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`. Transport docs: `AyCode.Services/docs/SIGNALR/README.md`.
|
||||
8. **AcSignalRDataSource** — generic `IList<T>` with change tracking, CRUD via `SignalRCrudTags`, binary merge, rollback. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`. Transport docs: `AyCode.Services/docs/SIGNALR/README.md`.
|
||||
9. ~~**JSON-in-Binary tech debt**~~ — **REMOVED** 2026-04-26 (resolved: `AyCode.Core/docs/XCUT/XCUT_ISSUES.md#accore-xcut-i-x8q1`). Number kept reserved to prevent renumbering of subsequent items.
|
||||
|
||||
## Critical Warnings
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -16,7 +16,7 @@ To make IDs like `ACCORE-LOG-I-K7M2`, `ACCORE-SIG-B-3R9P`, `ACCORE-XCUT-I-A4B7`
|
|||
| `AUTH` | AUTH | User authentication: bearer tokens, JWT, login flow, hub authorization | `AyCode.Core/docs/AUTH/` |
|
||||
| `SIG` | SIGNALR | SignalR transport: tags, client base, dispatch, session | `AyCode.Core/AyCode.Services/docs/SIGNALR/` (+ variant in `AyCode.Services.Server`) |
|
||||
| `SBP` | SIGNALR_BINARY_PROTOCOL | Binary wire protocol over SignalR: framing, chunking, argument read | `AyCode.Core/AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/` |
|
||||
| `SIGDS` | SIGNALR_DATASOURCE | Client-server DataSource on SignalR transport: change tracking, rollback, sync state, load lifecycle, IList<T> wrapper | `AyCode.Core/AyCode.Services/docs/SIGNALR_DATASOURCE/` |
|
||||
| `SIGDS` | SIGNALR_DATASOURCE | Client-server DataSource on SignalR transport: change tracking, rollback, sync state, load lifecycle, IList<T> wrapper | `AyCode.Core/AyCode.Services.Server/docs/SIGNALR_DATASOURCE/` |
|
||||
| `BIN` | BINARY | AcBinary serializer: features, format, writers, source generator | `AyCode.Core/AyCode.Core/docs/BINARY/` |
|
||||
| `TOON` | TOON | Toon serializer: LLM-optimized format with @meta/@types/@data sections | `AyCode.Core/AyCode.Core/docs/TOON/` |
|
||||
| `XCUT` | cross-cutting | Issues / TODOs spanning ≥2 ACCORE topics — one canonical home, referenced from each affected topic | `AyCode.Core/AyCode.Core/docs/XCUT/` |
|
||||
|
|
@ -58,7 +58,7 @@ The framework (this file) does NOT enumerate higher-layer topics — that would
|
|||
## ID format rules
|
||||
|
||||
1. **Format**: `<PREFIX>-<TOPIC>-<TYPE>-<RAND>` — all uppercase, hyphen-separated. The `<PREFIX>` component identifies the owning repo per `AyCode.Core/.github/REPO_PREFIXES.md` (and per each repo's own `@repo.prefix` field). **LLMP exception**: `LLMP-DEC-N` entries (workspace-meta Decision Log) skip the prefix and use sequential `N` instead of `<RAND>` — single-file serialization avoids parallel-branch collision.
|
||||
2. **Random suffix**: `<RAND>` is a 4-character alphanumeric suffix from `[A-Z0-9]` (~1.7M combinations per `<PREFIX>-<TOPIC>-<TYPE>` triple). Generated at entry creation; the agent globs existing entries (active topic file + all year-month bucket archive files) and verifies uniqueness; regenerate on rare collision.
|
||||
2. **Random suffix**: `<RAND>` is a 4-character alphanumeric suffix from `[A-Z0-9]` (~1.7M combinations per `<PREFIX>-<TOPIC>-<TYPE>` triple). Generated at entry creation; the agent globs existing entries (active topic file + all year-bucketed archive files) and verifies uniqueness; regenerate on rare collision.
|
||||
3. **Append-only**: once assigned, IDs never change. If an entry is reversed or superseded, add a NEW entry that references the prior one — do not renumber, do not re-randomize.
|
||||
4. **Hash anchor** (markdown cross-file refs): lowercase with hyphens preserved (`LOGGING_ISSUES.md#accore-log-i-k7m2` — GitHub auto-converts). Always use the full prefixed form; bare hash anchors without prefix are ambiguous across repos.
|
||||
5. **No sub-category in ID**: legacy sub-prefixes like `PROTO-`, `DISPATCH-`, `CONN-`, `DS-` are NOT allowed at ID level. Capture sub-category in the entry body header: `## ACCORE-SIG-I-K7M2 [PROTO]: ...`.
|
||||
|
|
@ -138,7 +138,7 @@ The body carries the **nuance**; the Status field only signals archive-eligibili
|
|||
|
||||
### Lifecycle: archive
|
||||
|
||||
`Closed` entries are eligible for rotation into year-month bucket archive files (`<file>_<YYYY>_<MM>.md`) via the `docs-archive` skill. Year-month derived from a date in the entry body. Archive operation is user-invoked — closed entries don't disappear automatically. See `AyCode.Core/.github/skills/docs-archive/SKILL.md`.
|
||||
`Closed` entries are eligible for rotation into year-bucketed archive files (`<file>_<year>.md`) via the `docs-archive` skill. Year derived from a date in the entry body. Archive operation is user-invoked — closed entries don't disappear automatically. See `AyCode.Core/.github/skills/docs-archive/SKILL.md`.
|
||||
|
||||
## Change history
|
||||
|
||||
|
|
|
|||
|
|
@ -118,24 +118,24 @@ If any `{DOMAIN}.md` is loaded (e.g., `LOGGING.md`), ALSO glob and load its comp
|
|||
|
||||
These are **paired docs** and must be loaded as a set. Skipping ISSUES/TODO risks reintroducing fixed bugs or conflicting with ongoing refactors.
|
||||
|
||||
## Archive files (`*_<YYYY>_<MM>.md`)
|
||||
## Archive files (`*_<year>.md`)
|
||||
|
||||
Closed entries from `_ISSUES.md` / `_TODO.md` / `LLM_PROTOCOL_DECISIONS.md` may be rotated into year-month bucket archive files by the `docs-archive` skill. Examples:
|
||||
- `LOGGING_ISSUES_2025_03.md`
|
||||
- `BINARY_TODO_2026_01.md`
|
||||
- `LLM_PROTOCOL_DECISIONS_2026_04.md`
|
||||
Closed entries from `_ISSUES.md` / `_TODO.md` / `LLM_PROTOCOL_DECISIONS.md` may be rotated into year-bucketed archive files by the `docs-archive` skill. Examples:
|
||||
- `LOGGING_ISSUES_2025.md`
|
||||
- `BINARY_TODO_2026.md`
|
||||
- `LLM_PROTOCOL_DECISIONS_2026.md`
|
||||
|
||||
### Default behaviour: NOT auto-loaded
|
||||
|
||||
The Step 2 glob patterns target **active** companions only — unsuffixed names. Year-suffixed variants are excluded by default. Practically:
|
||||
|
||||
- `**/docs/{TOPIC}/{TOPIC}_ISSUES.md` matches; `**/docs/{TOPIC}/{TOPIC}_ISSUES_2025_03.md` does NOT.
|
||||
- `**/docs/{TOPIC}/{TOPIC}_ISSUES.md` matches; `**/docs/{TOPIC}/{TOPIC}_ISSUES_2025.md` does NOT.
|
||||
- If a generic `{TOPIC}_*.md` pattern inadvertently matches year-suffixed files, filter them out before passing to Step 4 (Load).
|
||||
|
||||
### On-demand read (no user-confirm needed — read-only operation)
|
||||
|
||||
Read an archive file when ANY of these signals appears:
|
||||
- A loaded entry references an archived ID (e.g., `Superseded by ACCORE-LOG-I-K7M2` where the random-suffixed ID resolves only to a `_<YYYY>_<MM>.md` archive)
|
||||
- A loaded entry references an archived ID (e.g., `Superseded by ACCORE-LOG-I-K7M2` where the random-suffixed ID resolves only to a `_<year>.md` archive)
|
||||
- A code comment or other doc references an ID resolving only to an archive file
|
||||
- The user's request describes a behaviour pattern matching an archived `Fixed` entry's Description (regression suspicion)
|
||||
- The investigation feels like "this was solved before" — read the topic's archive(s) before re-deriving
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace AyCode.Core.Interfaces;
|
|||
public interface ICompanyInfoBase
|
||||
{
|
||||
string Name { get; }
|
||||
string? TaxId { get; }
|
||||
string TaxId { get; }
|
||||
string CountryCode { get; }
|
||||
string PostalCode { get; }
|
||||
string City { get; }
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ The framework explicitly does NOT provide type-dispatch infrastructure (no wire-
|
|||
This is a permanent architectural decision, not a "TODO not yet done." Do not file new TODOs proposing wire-format type-id, registry, or session/handshake infrastructure inside `AyCode.Core`.
|
||||
|
||||
**Workarounds (canonical patterns the consumer implements):**
|
||||
- **Tag-based dispatch above AcBinary** (recommended — matches CLAUDE.md Rule #7): prefix each message with an `int` (or enum) tag the consumer reads first to choose `Deserialize<TypeForTag>`. The consumer encapsulates the tag-read + type-dispatch in its own deserialization wrapper. Reference example: `AyCode.Services/SignalRs/AcSignalRDataSource` uses `SignalRCrudTags`.
|
||||
- **Tag-based dispatch above AcBinary** (recommended — matches CLAUDE.md Rule #7): prefix each message with an `int` (or enum) tag the consumer reads first to choose `Deserialize<TypeForTag>`. The consumer encapsulates the tag-read + type-dispatch in its own deserialization wrapper. Reference example: `AyCode.Services.Server/SignalRs/AcSignalRDataSource` uses `SignalRCrudTags`.
|
||||
- **Polymorphic envelope type**: define a single `T = Envelope` containing a discriminator field + raw payload bytes; the consumer deserializes the envelope, switches on the discriminator, and re-deserializes the payload as the concrete type from the inner `byte[]`. Adds a small layer of indirection but works on top of fix-T `Deserialize<Envelope>`.
|
||||
- **One input per type-stream**: separate streams per message-class. Practical when the type-set is small and the transport can afford multiple connections.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,169 +0,0 @@
|
|||
using System.IO.Pipelines;
|
||||
using AyCode.Services.SignalRs;
|
||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-pool starvation regression tests for the AsyncSegment paths
|
||||
/// (<c>ACCORE-SBP-I-T3W9</c>: blocking waits on .NET thread-pool threads deadlock the pool under
|
||||
/// concurrent load + real network latency — production-only, "stuck loading → 60 s timeout").
|
||||
///
|
||||
/// <para><b>Deser (receive) side — fixed.</b> The streaming deser blocks in
|
||||
/// <c>ManualResetEventSlim.Wait()</c> between network-delivered chunks. It now runs off the .NET
|
||||
/// thread pool on a never-queue + reuse dedicated executor (<see cref="AcBinaryDeserExecutor"/>) so
|
||||
/// the blocking wait never consumes a pool thread (which the producer's receive-continuations need
|
||||
/// → circular dependency). These tests assert that executor contract directly and should all pass.</para>
|
||||
///
|
||||
/// <para><b>Ser (send) side — TBD.</b> The per-chunk <c>SyncFlush</c> in <c>WriteMessageChunked</c>
|
||||
/// blocks a dispatch (pool) thread under client backpressure. The send-side test below asserts the
|
||||
/// DESIRED non-blocking behaviour and is <c>[Ignore]</c>d until that fix lands.</para>
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class AcBinaryProtocolThreadPoolTests
|
||||
{
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Deser (receive) side — AcBinaryDeserExecutor contract. Expected: all pass.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
[TestMethod]
|
||||
public void DeserExecutor_RunsOffThreadPool()
|
||||
{
|
||||
var onPool = (bool)AcBinaryDeserExecutor.Run(() => (object?)Thread.CurrentThread.IsThreadPoolThread).GetAwaiter().GetResult()!;
|
||||
|
||||
Assert.IsFalse(onPool,
|
||||
"Streaming deser must run OFF the .NET thread pool (dedicated thread) — the blocking " +
|
||||
"MRES.Wait() would otherwise pin a pool thread the producer's receive-continuation needs " +
|
||||
"→ circular-dependency starvation (ACCORE-SBP-I-T3W9).");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DeserExecutor_NeverQueues_AllConcurrentWorkStartsImmediately()
|
||||
{
|
||||
const int n = 32;
|
||||
using var allStarted = new CountdownEvent(n);
|
||||
using var release = new ManualResetEventSlim(false);
|
||||
|
||||
var tasks = new Task<object?>[n];
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
tasks[i] = AcBinaryDeserExecutor.Run(() =>
|
||||
{
|
||||
allStarted.Signal();
|
||||
release.Wait();
|
||||
|
||||
return (object?)null;
|
||||
});
|
||||
}
|
||||
|
||||
// Every item must START even though none has finished — proves the executor NEVER queues:
|
||||
// each submit immediately gets a thread (reuse idle or grow). A bounded-queue executor
|
||||
// would stall here (excess items wait behind busy workers) and the countdown never reaches 0.
|
||||
// 5 s is generous for 32 thread spawns (~ms) yet fails fast on a queue regression.
|
||||
Assert.IsTrue(allStarted.Wait(TimeSpan.FromSeconds(5)),
|
||||
$"Only {n - allStarted.CurrentCount}/{n} work items started — executor queued instead of " +
|
||||
"growing on demand (would reintroduce the [202] GetResult-on-queued-deser coupling).");
|
||||
|
||||
release.Set();
|
||||
Task.WaitAll(tasks);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DeserExecutor_ReusesThreads_AcrossSequentialWork()
|
||||
{
|
||||
var ids = new HashSet<int>();
|
||||
for (var i = 0; i < 50; i++)
|
||||
{
|
||||
var id = (int)AcBinaryDeserExecutor.Run(() => (object?)Environment.CurrentManagedThreadId).GetAwaiter().GetResult()!;
|
||||
ids.Add(id);
|
||||
}
|
||||
|
||||
// Sequential work: each completes before the next is submitted → the same idle worker is
|
||||
// reused (no per-message thread churn). The worker pushes itself to idle right after
|
||||
// SetResult (1 instruction) while the test thread runs ~10+ (incl. a TCS alloc) before the
|
||||
// next TryPop, so the worker almost always wins the race → 1-2 distinct threads in practice.
|
||||
// <= 4 leaves a small margin for the rare preempt-in-window case; more than that is suspect.
|
||||
Assert.IsTrue(ids.Count <= 4, $"Expected thread reuse across sequential work, got {ids.Count} distinct threads / 50 — churn instead of reuse.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DoNotParallelize] // stresses the shared process-wide thread pool
|
||||
public void DeserExecutor_CompletesUnderThreadPoolPressure()
|
||||
{
|
||||
// Occupy many .NET thread-pool threads with blocking work, then run a deser-style item.
|
||||
// Because the executor is off-pool it completes regardless of pool pressure — whereas the
|
||||
// old `Task.Run` deser would queue behind the busy pool and the [202] GetResult would block
|
||||
// (ACCORE-SBP-I-T3W9). This is the end-to-end resilience guard.
|
||||
using var release = new ManualResetEventSlim(false);
|
||||
var hogCount = Environment.ProcessorCount * 4;
|
||||
var hogs = new Task[hogCount];
|
||||
|
||||
for (var i = 0; i < hogCount; i++) hogs[i] = Task.Run(() => release.Wait());
|
||||
|
||||
try
|
||||
{
|
||||
var done = AcBinaryDeserExecutor.Run(() => (object?)42);
|
||||
Assert.IsTrue(done.Wait(TimeSpan.FromSeconds(5)), "Off-pool deser did not complete under thread-pool pressure — possible pool coupling.");
|
||||
Assert.AreEqual(42, (int)done.Result!);
|
||||
}
|
||||
finally
|
||||
{
|
||||
release.Set();
|
||||
Task.WaitAll(hogs);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Ser (send) side — known-open. Asserts DESIRED behaviour; [Ignore]d until the send-side fix.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
[TestMethod]
|
||||
[Ignore("Send-side fix TBD — ACCORE-SBP-I-T3W9. Asserts the DESIRED non-blocking behaviour; " +
|
||||
"today WriteMessageChunked blocks the calling (dispatch) thread under client backpressure " +
|
||||
"via the per-chunk SyncFlush. Un-ignore when the send-side fix lands.")]
|
||||
public void WriteMessageChunked_UnderBackpressure_DoesNotBlockCallingThread()
|
||||
{
|
||||
// A Pipe with a small pause threshold and NO reader → FlushAsync backpressures immediately
|
||||
// (simulates a stuck / slow mobile client). The chunked send's per-chunk SyncFlush then
|
||||
// blocks the calling thread. Desired post-fix behaviour: the send does not pin the caller.
|
||||
var pipe = new Pipe(new PipeOptions(pauseWriterThreshold: 4096, resumeWriterThreshold: 2048));
|
||||
|
||||
var opts = new AcBinaryHubProtocolOptions
|
||||
{
|
||||
ProtocolMode = BinaryProtocolMode.AsyncSegment,
|
||||
FlushTimeout = TimeSpan.FromSeconds(2), // bound so a stuck flush can't hang the test forever
|
||||
};
|
||||
var protocol = new AyCodeBinaryHubProtocol(opts);
|
||||
|
||||
// Large non-byte[] data arg → the chunked path engages and produces > pauseThreshold bytes.
|
||||
// The 4-arg shape mirrors the OnReceiveMessage(tag, requestId, SignalParams, data) convention
|
||||
// the other SignalR tests use — SignalParams here is convention/realism, not functionally
|
||||
// required (the large `bigData` arg alone triggers HasStreamableArgs → chunked send).
|
||||
var bigData = new string('x', 200_000);
|
||||
var msg = new InvocationMessage("OnReceiveMessage", new object?[]
|
||||
{
|
||||
1, (int?)1, new SignalParams(), bigData
|
||||
});
|
||||
|
||||
var writeThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
protocol.WriteMessage(msg, pipe.Writer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* TimeoutException on the bounded flush is acceptable for this probe */
|
||||
}
|
||||
})
|
||||
{ IsBackground = true };
|
||||
writeThread.Start();
|
||||
|
||||
// 2 s margin so a non-blocking send (returns in ms once it no longer waits on the flush) is
|
||||
// not falsely flagged on a slow CI; a blocking send stays pinned well past this.
|
||||
var finishedPromptly = writeThread.Join(TimeSpan.FromSeconds(2));
|
||||
Assert.IsTrue(finishedPromptly,
|
||||
"WriteMessageChunked blocked the calling thread under client backpressure (per-chunk " +
|
||||
"SyncFlush) — the send-side thread-pool starvation source (ACCORE-SBP-I-T3W9).");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using AyCode.Core.Enums;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using AyCode.Services.Server.SignalRs;
|
||||
using AyCode.Services.SignalRs;
|
||||
|
||||
namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ Server-side service implementations: JWT authentication, SendGrid email delivery
|
|||
| Document | Topic |
|
||||
|---|---|
|
||||
| `SIGNALR/README.md` | Server-side SignalR hub (dispatch, session, broadcast) |
|
||||
| `SIGNALR_DATASOURCE/README.md` | Real-time DataSource with CRUD & change tracking |
|
||||
|
||||
## Folder Structure
|
||||
|
||||
|
|
@ -18,7 +19,7 @@ Server-side service implementations: JWT authentication, SendGrid email delivery
|
|||
|---|---|
|
||||
| [`Emails/`](Emails/README.md) | SendGrid email service (registration, password reset) |
|
||||
| [`Logins/`](Logins/README.md) | Server-side login with JWT token generation |
|
||||
| [`SignalRs/`](SignalRs/README.md) | SignalR hub base, session service, client broadcast |
|
||||
| [`SignalRs/`](SignalRs/README.md) | SignalR hub base, session service, data source with change tracking, client broadcast |
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ using AyCode.Core.Loggers;
|
|||
using AyCode.Core.Serializers;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Services.SignalRs;
|
||||
using Castle.Core.Logging;
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace AyCode.Services.SignalRs
|
||||
namespace AyCode.Services.Server.SignalRs
|
||||
{
|
||||
//public class TrackingItemGuid<TDataItem>(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) : TrackingItem<TDataItem, Guid>(trackingState, currentValue, originalValue)
|
||||
// where TDataItem : class, IId<Guid> { }
|
||||
|
|
@ -477,9 +478,7 @@ namespace AyCode.Services.SignalRs
|
|||
|
||||
if (clearChangeTracking) TrackingItems.Clear();
|
||||
|
||||
// Tag nélküli (lokális/readonly) datasource: nincs honnan frissíteni — a szerver-refresh kimarad.
|
||||
// (Az explicit LoadDataSourceAsync hívás tag nélkül továbbra is dob — az valódi konfigurációs hiba.)
|
||||
if (refreshDataFromDbAsync && SignalRCrudTags.GetAllMessageTag != AcSignalRTags.None)
|
||||
if (refreshDataFromDbAsync)
|
||||
{
|
||||
LoadDataSourceAsync(false).Forget();
|
||||
return;
|
||||
|
|
@ -514,9 +513,7 @@ namespace AyCode.Services.SignalRs
|
|||
}
|
||||
else if (clearChangeTracking) TrackingItems.Clear();
|
||||
|
||||
// Tag nélküli (lokális/readonly) datasource: nincs honnan frissíteni — a szerver-refresh kimarad.
|
||||
// (Az explicit LoadDataSourceAsync hívás tag nélkül továbbra is dob — az valódi konfigurációs hiba.)
|
||||
if (refreshDataFromDbAsync && SignalRCrudTags.GetAllMessageTag != AcSignalRTags.None)
|
||||
if (refreshDataFromDbAsync)
|
||||
{
|
||||
LoadDataSourceAsync(false).Forget();
|
||||
return;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# SignalRs
|
||||
|
||||
Server-side SignalR hub infrastructure: hub base class, session management, and client broadcast service.
|
||||
Server-side SignalR hub infrastructure: hub base class, session management, data source with change tracking, and client broadcast service.
|
||||
|
||||
> **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR/README.md`.
|
||||
|
||||
|
|
@ -18,9 +18,17 @@ Server-side SignalR hub infrastructure: hub base class, session management, and
|
|||
|
||||
### Data Source
|
||||
|
||||
> **Relocated (LLMP-DEC-68):** `AcSignalRDataSource` — the client-side real-time collection — moved to `AyCode.Services/SignalRs/` and is **no longer part of this server-side assembly**. Full spec: `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`.
|
||||
> **Full specification:** `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`
|
||||
|
||||
- **`AcSignalRDataSource.cs`** — Generic real-time collection (`AcSignalRDataSource<TDataItem, TId, TIList>`) implementing `IList<T>` with full CRUD and change tracking.
|
||||
- **Change tracking:** `TrackingItem<T, TId>` wraps each modified item with `TrackingState` + `OriginalValue` for rollback. `ChangeTracking<T, TId>` manages the tracking list.
|
||||
- **Loading:** `LoadDataSource()` (sync), `LoadDataSourceAsync()` (async callback), `LoadItem(id)` (single). Binary path uses `BinaryToMerge()` for `AcObservableCollection` (batch UI update via `BeginUpdate/EndUpdate`).
|
||||
- **Saving:** `SaveChanges()` iterates tracked items, posts each via CRUD tag, rollbacks on failure. `SaveItem()` for individual saves.
|
||||
- **Sync state:** `IsSyncing` (Interlocked counter) + `OnSyncingStateChanged` event for UI loading indicators.
|
||||
- **Locking:** `object _syncRoot` (sync ops) + `SemaphoreSlim _asyncLock` (async ops). `GetEnumerator()` returns safe copy.
|
||||
- **Working reference list:** `SetWorkingReferenceList()` allows external list to become inner storage (zero-copy).
|
||||
- **Context:** `ContextIds` (object[]) + `FilterText` (string) sent with GetAll requests for server-side filtering.
|
||||
|
||||
### Utilities
|
||||
- **`ExtensionMethods.cs`** — `InvokeMethod()` — invokes methods and unwraps `Task`/`Task<T>`/`ValueTask` results.
|
||||
|
||||
> `TrackingItemHelpers.cs` (deep-clone helpers used by the data source) relocated to `AyCode.Services/SignalRs/` alongside `AcSignalRDataSource` — see LLMP-DEC-68.
|
||||
- **`TrackingItemHelpers.cs`** — Deep clone helpers: `JsonClone<T>()`, `ReflectionClone<T>()`.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Reflection;
|
||||
using AyCode.Core.Extensions;
|
||||
|
||||
namespace AyCode.Services.SignalRs;
|
||||
namespace AyCode.Services.Server.SignalRs;
|
||||
|
||||
public static class TrackingItemHelpers
|
||||
{
|
||||
|
|
@ -5,8 +5,7 @@ Topic docs for the `AyCode.Services.Server` project (Layer 0, server-side servic
|
|||
## Topics
|
||||
|
||||
- [`SIGNALR/`](SIGNALR/README.md) — Server-side SignalR hub (dispatch, session, broadcast)
|
||||
|
||||
> **Note:** `SIGNALR_DATASOURCE/` (the `AcSignalRDataSource` client-side collection) relocated to `AyCode.Services/docs/SIGNALR_DATASOURCE/` — see `LLMP-DEC-68`.
|
||||
- [`SIGNALR_DATASOURCE/`](SIGNALR_DATASOURCE/README.md) — Client-server DataSource on SignalR transport (change tracking, rollback, sync state, IList<T> wrapper)
|
||||
|
||||
## Navigation
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Server-side SignalR hub infrastructure: method dispatch, session management, broadcast, and diagnostics. Source: `SignalRs/` in this project.
|
||||
|
||||
> For client-side transport (tags, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`.
|
||||
> For the DataSource collection see `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md` (relocated to the client-side project).
|
||||
> For the DataSource collection see `../SIGNALR_DATASOURCE/README.md`.
|
||||
|
||||
## Server Processing
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# SignalR DataSource
|
||||
|
||||
Change-tracked real-time collection on top of the SignalR transport layer. Source: `SignalRs/AcSignalRDataSource.cs` in `AyCode.Services`.
|
||||
Change-tracked real-time collection on top of the SignalR transport layer. Source: `SignalRs/AcSignalRDataSource.cs` in `AyCode.Services.Server`.
|
||||
|
||||
> For the underlying transport (tag system, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`.
|
||||
> For server hub infrastructure see `AyCode.Services.Server/docs/SIGNALR/README.md`.
|
||||
> For server hub infrastructure see `../SIGNALR/README.md`.
|
||||
|
||||
## Overview
|
||||
|
||||
|
|
@ -253,7 +253,7 @@ Projects can also call the transport directly without DataSource — see `AyCode
|
|||
|
||||
| Component | Path |
|
||||
|---|---|
|
||||
| DataSource (abstract base) | `SignalRs/AcSignalRDataSource.cs` (in `AyCode.Services`) |
|
||||
| Tracking helpers (`JsonClone` / `ReflectionClone`) | `SignalRs/TrackingItemHelpers.cs` (in `AyCode.Services`) |
|
||||
| DataSource (abstract base) | `SignalRs/AcSignalRDataSource.cs` (in `AyCode.Services.Server`) |
|
||||
| Tracking helpers (`JsonClone` / `ReflectionClone`) | `SignalRs/TrackingItemHelpers.cs` (in `AyCode.Services.Server`) |
|
||||
| CRUD tags | `SignalRs/SignalRCrudTags.cs` (in `AyCode.Services`) |
|
||||
| Transport client | `SignalRs/AcSignalRClientBase.cs` (in `AyCode.Services`) |
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -24,12 +24,6 @@
|
|||
<ProjectReference Include="..\AyCode.Models\AyCode.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Lets the test project drive the internal AcBinaryDeserExecutor directly
|
||||
(deser-side thread-pool regression tests for ACCORE-SBP-I-T3W9). -->
|
||||
<InternalsVisibleTo Include="AyCode.Services.Server.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="docs\**\*.md" />
|
||||
<None Include="**\README.md" Exclude="$(DefaultItemExcludes);docs\**" />
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ public sealed class EkaerCompanyInfo : ICompanyInfoBase
|
|||
|
||||
public string? FullAddress => this.ComposeFullAddress();
|
||||
|
||||
/// <summary>A bejelentő saját telephelye (fizikai fel-/lerakodási hely): bejövőnél lerakodás, kimenőnél felrakodás.
|
||||
/// Magyar címnél a NAV a Name/VatNumber/Phone/Email-t is megköveteli.</summary>
|
||||
public LocationType? Site { get; set; }
|
||||
/// <summary>A saját telephely / lerakodási hely. Magyar címnél a NAV a Name/VatNumber/Phone/Email-t is megköveteli.</summary>
|
||||
public LocationType? UnloadLocation { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,9 @@ public sealed class EkaerSubmitService : IEkaerSubmitService
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(operations);
|
||||
|
||||
// 1) Validáció — csak BLOKKOLÓ (error) szintű hiba esetén NEM küldünk; a warning (pl. hiányzó rendszám,
|
||||
// ami a felrakodásig pótolható) nem blokkol. A teljes üzenetlistát visszaadjuk (warningostul).
|
||||
// 1) Validáció — bármilyen hiba esetén NEM küldünk; a teljes hibalistát visszaadjuk.
|
||||
var errors = _validator.Validate(operations);
|
||||
if (errors.HasErrors())
|
||||
if (errors.Count > 0)
|
||||
return EkaerSubmitResult.Invalid(errors);
|
||||
|
||||
// 2) Request összeállítás. A Header/User példányt itt kell létrehozni (a generált ctor csak a
|
||||
|
|
|
|||
|
|
@ -84,9 +84,7 @@ public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator
|
|||
}
|
||||
|
||||
// 3) Üzleti szabályok (az XSD nem jelöli [Required]-ként, de a NAV megköveteli).
|
||||
// A rendszám a NAV-nál legkésőbb a felrakodás megkezdéséig pótolható — a bejelentéskor NEM kötelező,
|
||||
// ezért hiánya WARNING (a bejelentés beküldhető), nem blokkoló error. Lásd EKAER_OPERATIONS.md (create→finalize).
|
||||
if (tradeCard.Vehicle is null) errors.Add(Warning($"{path}.vehicle", "a vonó jármű (rendszám) hiányzik — legkésőbb a felrakodás megkezdéséig pótolandó"));
|
||||
if (tradeCard.Vehicle is null) errors.Add(Error($"{path}.vehicle", "a vonó jármű (rendszám) kötelező — rendszám nélkül a NAV elutasítja a bejelentést"));
|
||||
if (tradeCard.Items.Count == 0) errors.Add(Error($"{path}.items", "legalább egy tétel szükséges"));
|
||||
|
||||
RequireText(tradeCard.SellerName, $"{path}.sellerName", "a feladó (eladó) neve kötelező", errors);
|
||||
|
|
@ -119,7 +117,7 @@ public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator
|
|||
Validator.TryValidateProperty(property.GetValue(instance), context, local);
|
||||
|
||||
foreach (var result in local)
|
||||
errors.Add(new EkaerValidationMessage(EkaerSeverity.Error, $"{path}.{property.Name}: {result.ErrorMessage}"));
|
||||
errors.Add(new ValidationResult($"{path}.{property.Name}: {result.ErrorMessage}", result.MemberNames));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +127,5 @@ public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator
|
|||
errors.Add(Error(path, message));
|
||||
}
|
||||
|
||||
private static EkaerValidationMessage Error(string path, string message) => new(EkaerSeverity.Error, $"{path}: {message}");
|
||||
|
||||
private static EkaerValidationMessage Warning(string path, string message) => new(EkaerSeverity.Warning, $"{path}: {message}");
|
||||
private static ValidationResult Error(string path, string message) => new($"{path}: {message}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace AyCode.Services.Nav.Ekaer;
|
||||
|
||||
/// <summary>Az EKÁER validációs üzenet súlyossága.</summary>
|
||||
public enum EkaerSeverity
|
||||
{
|
||||
/// <summary>Nem blokkoló — a bejelentés beküldhető, de valamit pótolni kell (pl. a rendszám a felrakodásig).</summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>Blokkoló — a NAV elutasítaná; beküldés előtt javítandó.</summary>
|
||||
Error,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Súlyossággal (warning/error) bővített validációs eredmény. A <see cref="ValidationResult"/>-ból származik,
|
||||
/// így a meglévő fogyasztók (akik csak az <c>ErrorMessage</c>-et használják) változatlanul működnek; aki a
|
||||
/// szintet is akarja, a <see cref="Severity"/>-t / a <c>GetSeverity()</c> kiterjesztést használja.
|
||||
/// </summary>
|
||||
public sealed class EkaerValidationMessage(EkaerSeverity severity, string message) : ValidationResult(message)
|
||||
{
|
||||
public EkaerSeverity Severity { get; } = severity;
|
||||
}
|
||||
|
||||
/// <summary>Súlyosság-segédek a validációs eredmény-listához.</summary>
|
||||
public static class EkaerValidationExtensions
|
||||
{
|
||||
/// <summary>A súlyosság; egy sima (nem <see cref="EkaerValidationMessage"/>) eredmény konzervatívan <see cref="EkaerSeverity.Error"/>.</summary>
|
||||
public static EkaerSeverity GetSeverity(this ValidationResult result)
|
||||
=> result is EkaerValidationMessage m ? m.Severity : EkaerSeverity.Error;
|
||||
|
||||
/// <summary>Van-e blokkoló (error) szintű üzenet — ennek alapján NEM küldhető a bejelentés.</summary>
|
||||
public static bool HasErrors(this IEnumerable<ValidationResult> results) => results.Any(r => r.GetSeverity() == EkaerSeverity.Error);
|
||||
|
||||
/// <summary>Van-e warning szintű üzenet (a beküldést nem blokkolja, de pótlandó).</summary>
|
||||
public static bool HasWarnings(this IEnumerable<ValidationResult> results) => results.Any(r => r.GetSeverity() == EkaerSeverity.Warning);
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
namespace AyCode.Services.Nav.Ekaer;
|
||||
|
||||
/// <summary>
|
||||
/// Általános EKÁER-konfiguráció kontraktus — bármely bejelentő projekt implementálja: a bejelentő cégadata
|
||||
/// (a telephely fel-/lerakodási hellyel), a deviza→HUF árfolyam és a bejelentési küszöbök. Read-only; a konkrét,
|
||||
/// config-kötött beállítást a DI szolgáltatja — ugyanúgy, mint az <see cref="INavCredentials"/>-t.
|
||||
/// A kettő szándékosan KÜLÖN kontraktus: az <c>INavCredentials</c> a NAV-hitelesítés (transport/titok), ez pedig
|
||||
/// a bejelentés üzleti tartalma — más a fogyasztójuk és más a változási okuk (ISP / Single Responsibility).
|
||||
/// </summary>
|
||||
public interface IEkaerSettings
|
||||
{
|
||||
/// <summary>A bejelentő saját cégadatai (eladó/címzett + a <see cref="EkaerCompanyInfo.Site"/> telephely).</summary>
|
||||
EkaerCompanyInfo Company { get; }
|
||||
|
||||
/// <summary>EUR→HUF árfolyam a tétel-érték HUF-ra számításához (HUF feladónál nem használt).</summary>
|
||||
double EurHufRate { get; }
|
||||
|
||||
/// <summary>Tömeg-küszöb kg-ban: e felett (vagy az érték-küszöb felett) kell EKÁER.</summary>
|
||||
double ThresholdWeightKg { get; }
|
||||
|
||||
/// <summary>Érték-küszöb HUF-ban (nettó): e felett (vagy a tömeg-küszöb felett) kell EKÁER.
|
||||
/// <c>int</c> elég: a legmagasabb EKÁER-küszöb 5 millió Ft (a tétel-érték maga viszont long — 11 jegyű).</summary>
|
||||
int ThresholdValueHuf { get; }
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@
|
|||
// xscgen --namespace http://schemas.nav.gov.hu/EKAER/1.0/ekaermanagement=AyCode.Services.Nav.Ekaer.Models --namespace http://schemas.nav.gov.hu/EKAER/1.0/common=AyCode.Services.Nav.Ekaer.Models.Common --nullable --separateFiles --output C:/Users/Fullepi/Downloads/ekaer/Generated2 C:/Users/Fullepi/Downloads/ekaer/ekaermanagement.xsd
|
||||
namespace AyCode.Services.Nav.Ekaer.Models
|
||||
{
|
||||
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("XmlSchemaClassGenerator", "3.0.1270.0")]
|
||||
[System.SerializableAttribute()]
|
||||
[System.Xml.Serialization.XmlTypeAttribute("LocationType", Namespace="http://schemas.nav.gov.hu/EKAER/1.0/ekaermanagement")]
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
using AyCode.Core.Interfaces;
|
||||
|
||||
namespace AyCode.Services.Nav.Ekaer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// KÉZZEL ÍRT partial-kiegészítés a generált <see cref="TradeCardItemType"/>-hoz — a generált fájlhoz NEM nyúlunk,
|
||||
/// az xscgen-újrageneráláskor ez a fájl megmarad.
|
||||
/// Az <see cref="IId{T}"/> implementáció grid-megjelenítéshez kell (IId<int>-megkötésű grid-bázisok
|
||||
/// readonly DataSource-aként). EXPLICIT implementáció, mert a NAV-séma saját <c>Id</c>-t definiál, ami
|
||||
/// <b>string</b> (<c>[XmlAttribute("id")]</c>, csak modify-kérésnél töltött) — a grid viszont int-et vár.
|
||||
/// SZÁMÍTOTT érték: az <c>ItemExternalId</c>-ból jön (a mapper oda a <c>ShippingItem.Id</c>-t írja),
|
||||
/// így betöltéskor nincs teendő. Az explicit tagot az XmlSerializer nem szerializálja — a NAV-wire érintetlen.
|
||||
/// </summary>
|
||||
public partial class TradeCardItemType : IId<int>
|
||||
{
|
||||
int IId<int>.Id
|
||||
{
|
||||
get => int.TryParse(ItemExternalId, out var id) ? id : 0;
|
||||
set => ItemExternalId = value.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -70,16 +70,6 @@ signingKey = Elek65Titkos
|
|||
SHA-512 = AF84DC456B82234E67550C80169E517FBDAB4403607293985DECB09F534D9F73FADAABEFEE932554FABBC49F6E8F74A5DD54EA359D6B7644D95CFF3530AFB889
|
||||
```
|
||||
|
||||
## Hozzáférés — kié a `user` + `signingKey`? (gyakori félreértés)
|
||||
|
||||
A fenti `user` / `passwordHash` / `signingKey` az **ADÓZÓé** (a bejelentésre kötelezett cégé), **NEM a szoftverfejlesztőé** (leellenőrizve: <https://ekaer.nav.gov.hu/faq/>, 2026-06):
|
||||
|
||||
- Az adózó **elsődleges felhasználóként, Ügyfélkapun** lép be az EKÁER-be, és **ő állítja be a titkos XML kommunikációs kulcsot** (`signingKey`) a saját fiókjában (a kulcs a webről jön, az XML-ben nem szerepel).
|
||||
- A szoftver egyszerűen az **adózó** `user`+`signingKey` párját használja (a fogyasztó config/secret-store-ából — itt: `INavCredentials`).
|
||||
- A fejlesztő legfeljebb **másodlagos felhasználóként** kap jogot (a primary user adja, NINCS külön NAV/Ügyfélkapu-regisztráció). **„Technikai felhasználó" itt NINCS.**
|
||||
|
||||
> **Másik interfész (2024–25): a „NAV M2M általános gépi interfész"** — ÚJ, általánosabb beküldő-csatorna, ahol **a fejlesztő a kliensprogramot regisztrálja** a NAV-nál (a végfelhasználók is regisztrálnak + NAV-tól kapott titkos kódokat írnak be). Ez **NEM** a fenti klasszikus EKÁER 2.2 XML-interfész. Ha a NAV oda terel, ott VAN fejlesztői regisztráció — production előtt érdemes tisztázni, melyik a hatályos.
|
||||
|
||||
## Műveletek (`tradeCardOperations` → `operation`)
|
||||
|
||||
| Operation | Jelentés |
|
||||
|
|
|
|||
|
|
@ -64,16 +64,9 @@ A `BaseResultType` (funcCode/reasonCode/msg) a `Nav/NavInterfaces.cs` `INavResul
|
|||
A fenti szabályokat a **`EkaerTradeCardValidator`** (`Nav/Ekaer/`) ellenőrzi a leképzett tradeCard-okon, **beküldés előtt**, hibalistával (nem áll meg az első hibánál):
|
||||
|
||||
- **Séma-szint:** a generált modellek `[Required]` / `[RegularExpression]` / hossz-attribútumai (rekurzívan a tradeCard + tételek + jármű + helyszínek fölött), a `System.ComponentModel.DataAnnotations.Validator`-ral. A `*Value`/`*Specified` segéd-property-ket kihagyja (a default `DateTime` különben tévesen bukna a timestamp-patternen).
|
||||
- **Üzleti szabályok** (az XSD-ben nem `[Required]`, de a NAV megköveteli): legalább egy tétel, feladó/címzett név+adószám+ország. A **vonó jármű (rendszám)** viszont **NEM blokkoló hiba, hanem `Warning`** (lásd a *Súlyosság* szakaszt) — a NAV csak a *felrakodás megkezdéséig* / `Finalize`-ig kéri (`TC_FINALIZE_VEHICLE_DATA_EMPTY`), `Create`-nél még hiányozhat (pl. a vevő viszi el az árut, beadáskor még nincs rendszám).
|
||||
- **Üzleti szabályok** (az XSD-ben nem `[Required]`, de a NAV megköveteli): vonó jármű kötelező, legalább egy tétel, feladó/címzett név+adószám+ország.
|
||||
- **`productVtsz` üres-string:** a modellen `[Required(AllowEmptyStrings=true)]` + pattern miatt az `""` elcsúszna a sémán (a `null` bukik, az `""` nem) — ezért a validátor külön lezárja.
|
||||
|
||||
### Súlyosság: `Warning` vs `Error` (2026-06)
|
||||
|
||||
A validátor üzenetei **súlyozottak**: `EkaerValidationMessage : ValidationResult` egy `EkaerSeverity { Warning, Error }` mezővel. A sima `ValidationResult` (séma-szint, `ValidateAnnotations`) **`Error`**-nak számít — a `GetSeverity()` kiterjesztés default-ja. Segédek: `HasErrors()` / `HasWarnings()`.
|
||||
|
||||
- **`Error`** = blokkoló (séma-bukás, hiányzó feladó/címzett/VTSZ, rossz formátum) → **nem küldhető**.
|
||||
- **`Warning`** = küldhető, de pótlandó adat — jelenleg a **hiányzó vonó jármű (rendszám)** (a felrakodásig / `Finalize`-ig pótolandó).
|
||||
|
||||
A **`EkaerSubmitService`** (`Nav/Ekaer/`) a kapu: `Validate` → **csak ha `HasErrors()`** áll meg (`EkaerSubmitResult.Invalid`); a `Warning`-ok **átmennek** (a beküldés mehet, a hiányt később pótolják), egyébként `EkaerManageService.ManageAsync`. A fogyasztó (FruitBank) ezt `Generated` / `GeneratedWithWarning` státuszra képezi.
|
||||
A **`EkaerSubmitService`** (`Nav/Ekaer/`) a kapu: `Validate` → ha a hibalista nem üres, **nem küld** (`EkaerSubmitResult.Invalid`); egyébként `EkaerManageService.ManageAsync`.
|
||||
|
||||
> ⚠️ **Nyitott (value):** a tétel `value` a NAV szerint **2021-től > 0 kötelező** (lásd a Tétel-validáció szakaszt), a validátor viszont **jelenleg nem követeli** — mert a FruitBank-mapper a deviza/FX tisztázásáig nem tölti. Amíg ez nyitott, a NAV **elutasíthatja** a beküldést. Lezárandó: a value kitöltése (deviza→HUF) + a validátorban `value > 0` üzleti szabály. (Plugin `EKAER_TODO` #3/#10.)
|
||||
|
|
|
|||
|
|
@ -21,19 +21,6 @@ A NAV adatszolgáltatás (jelenleg EKÁER) hivatalos forrásai + **LLM-barát ki
|
|||
|
||||
**Olvasási sorrend új belépőnek:** `EKAER_INTERFACE` → `EKAER_TRADECARD` → `EKAER_OPERATIONS` → `EKAER_VALIDATION`.
|
||||
|
||||
## Jogszabályi alap, kötelezettség, hozzáférés (kontextus, 2026-06)
|
||||
|
||||
A fenti PDF/XSD a **HOGYAN** (a 2.2 interfész — a tradeCard beküldése). Néhány **általános** dolog, ami tisztázódott (a forrásból ellenőrizve):
|
||||
|
||||
**Jogszabály (a MIKOR kötelező):** a hatályos rendelet a **13/2020. (XII. 23.) PM rendelet** (<https://net.jogtar.hu>). A leképezést/kötelezettséget érintő elvek:
|
||||
- az EKÁER-egység = **egy feladó → egy címzett, egy gépjárművel**; **§ 12:** EGY EKÁER-szám több *fel-/kirakodási címet* és több *terméktételt* is fedhet — **nem** szállítólevelenként kell darabolni;
|
||||
- a bejelentés-kötelezettség **küszöbe erre a relációra AGGREGÁLT** (egy fuvarozás keretében ugyanazon feladótól ugyanazon címzett részére ugyanazon gépjárművel — § 4 i);
|
||||
- a NAV **mindkét irányban büntet:** az **aluljelentés ÉS a túljelentés** is bírság — a kötelezettség-döntésnek pontosnak kell lennie.
|
||||
|
||||
> A konkrét küszöb-kategóriák/értékek a **jogszabálytól és a beállítástól függően változnak**, és a **fogyasztó** (pl. FruitBank) dönti el a kötelezettséget — itt szándékosan **nincs érték bedrótozva**. A fogyasztó-oldali kapu + a dátumozott küszöb-referencia: a FruitBank plugin `docs/EKAER/`.
|
||||
|
||||
**Hozzáférés (kié a `user` + `signingKey`):** az **ADÓZÓé** (a bejelentő cégé), **nem a fejlesztőé** — az adózó Ügyfélkapun, elsődleges felhasználóként állítja be a kulcsot, a szoftver az ő credentialját használja (`INavCredentials`). Részletek + a 2024–25-ös „NAV M2M" alternatíva: [`EKAER_INTERFACE.md`](EKAER_INTERFACE.md) → *Hozzáférés*.
|
||||
|
||||
## A réteg-kód
|
||||
|
||||
A base réteg (interfészek, auth, send-flow) doksija: [`../README.md`](../README.md).
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
using System.Collections.Concurrent;
|
||||
|
||||
namespace AyCode.Services.SignalRs;
|
||||
|
||||
/// <summary>
|
||||
/// Dedicated off-pool executor for the AsyncSegment streaming deserialization (see
|
||||
/// <c>SIGNALR_BINARY_PROTOCOL_ISSUES.md#accore-sbp-i-t3w9</c>).
|
||||
///
|
||||
/// <para>The streaming deser blocks in <c>AsyncPipeReaderInput.TryAdvanceSegment</c> →
|
||||
/// <c>ManualResetEventSlim.Wait()</c> between network-delivered chunks. Running that blocking
|
||||
/// wait on a .NET thread-pool thread (plain <c>Task.Run</c>) starves the pool: the signal that
|
||||
/// unblocks it (the producer <c>Feed()</c>+<c>Set()</c>) runs on the SignalR receive-continuation,
|
||||
/// which ALSO needs a pool thread → circular dependency → deadlock under concurrent load + real
|
||||
/// network latency.</para>
|
||||
///
|
||||
/// <para><b>Design — never-queue, grow-on-demand, reuse-forever:</b> every <see cref="Run"/>
|
||||
/// immediately hands the work to an idle worker (reuse) or spawns a new dedicated background
|
||||
/// thread (grow). It <b>never queues</b>. Consequences:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>The deser always starts immediately on its own off-pool thread → the <c>[202]</c>
|
||||
/// <c>GetResult()</c> always waits on a <i>running</i> deser (no queue-coupling).</item>
|
||||
/// <item>The blocking <c>Wait()</c> never consumes a .NET thread-pool thread → the producer's
|
||||
/// receive-continuations always get pool threads → no circular dependency, no starvation.</item>
|
||||
/// <item>Threads are reused (no per-message churn). Thread count is bounded in practice by peak
|
||||
/// concurrent chunked receives — one in-flight deser per connection (the <c>[202]</c>
|
||||
/// <c>GetResult</c> drains it before the next message on that connection).</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>This is <c>TaskCreationOptions.LongRunning</c>'s unconditional safety (never-queue,
|
||||
/// off-pool) plus thread reuse. Workers live forever — no idle-timeout, hence no idle-vs-assign
|
||||
/// race (the simplest provably-correct variant). An idle-timeout reclaim can be added later if the
|
||||
/// held-thread memory at peak concurrency becomes a concern; it must use a CAS idle→dead transition.</para>
|
||||
///
|
||||
/// <para><b>Rejected:</b> a bounded-queue executor reintroduces the <c>GetResult</c>-on-queued-deser
|
||||
/// coupling and drops requests on overload.</para>
|
||||
/// </summary>
|
||||
internal static class AcBinaryDeserExecutor
|
||||
{
|
||||
private static readonly ConcurrentStack<Worker> _idle = new();
|
||||
|
||||
// Monotonic id for distinct worker-thread names — eases dump / profiler / debug correlation.
|
||||
// Incremented only on grow (new thread), never per message.
|
||||
private static int _threadId;
|
||||
|
||||
/// <summary>
|
||||
/// Runs <paramref name="work"/> on a dedicated off-pool thread (reused idle worker, or a new
|
||||
/// one) — never queues. Returns a task completing with the work's result.
|
||||
/// </summary>
|
||||
public static Task<object?> Run(Func<object?> work)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
if (_idle.TryPop(out var worker))
|
||||
worker.Assign(work, tcs); // reuse an idle worker
|
||||
else
|
||||
new Worker().Start(work, tcs); // grow: spawn a new dedicated thread
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private sealed class Worker
|
||||
{
|
||||
// Single-permit park signal. Released exactly once per park (Assign), so the count
|
||||
// never exceeds 1 → no SemaphoreFullException. Release/Wait provide the memory barrier
|
||||
// that publishes the _work/_tcs writes to the worker thread.
|
||||
private readonly SemaphoreSlim _signal = new(0, 1);
|
||||
private Func<object?>? _work;
|
||||
private TaskCompletionSource<object?>? _tcs;
|
||||
|
||||
public void Start(Func<object?> work, TaskCompletionSource<object?> tcs)
|
||||
{
|
||||
_work = work;
|
||||
_tcs = tcs;
|
||||
new Thread(Loop) { IsBackground = true, Name = $"AcBinaryDeser-{Interlocked.Increment(ref _threadId)}" }.Start();
|
||||
}
|
||||
|
||||
// Called by Run only when this worker is parked in _signal.Wait() (it was popped from
|
||||
// _idle, which it entered only AFTER finishing the previous work). So _work/_tcs are
|
||||
// written here strictly while the worker is idle — no race with the worker's active read.
|
||||
public void Assign(Func<object?> work, TaskCompletionSource<object?> tcs)
|
||||
{
|
||||
_work = work;
|
||||
_tcs = tcs;
|
||||
_signal.Release();
|
||||
}
|
||||
|
||||
private void Loop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var work = _work ?? throw new InvalidOperationException("AcBinaryDeserExecutor worker woke without work — invariant violated");
|
||||
var tcs = _tcs ?? throw new InvalidOperationException("AcBinaryDeserExecutor worker woke without tcs — invariant violated");
|
||||
_work = null;
|
||||
_tcs = null;
|
||||
|
||||
try { tcs.SetResult(work()); }
|
||||
catch (Exception ex) { tcs.SetException(ex); }
|
||||
|
||||
_idle.Push(this); // become reusable
|
||||
_signal.Wait(); // park until the next Assign
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -929,20 +929,10 @@ public class AcBinaryHubProtocol : IHubProtocol
|
|||
// Lazy start: begin background deserialization after first chunk is written.
|
||||
// The deser task reads via AsyncPipeReaderInputAdapter (struct over class) which
|
||||
// calls TryAdvanceSegment on the input — blocks on ManualResetEventSlim.Wait when
|
||||
// out of data. Browser fallback: skip the background task — the MRES.Wait throws
|
||||
// out of data. Browser fallback: skip Task.Run — the MRES.Wait throws
|
||||
// PlatformNotSupportedException on WASM. Instead, buffer all chunks and run the
|
||||
// deserializer synchronously on CHUNK_END, where state.Input.Complete() has
|
||||
// already been called → TryAdvanceSegment never enters the Wait path.
|
||||
//
|
||||
// CRITICAL — off-pool via AcBinaryDeserExecutor, NOT the .NET thread pool. The deser
|
||||
// BLOCKS in ManualResetEventSlim.Wait() between network-delivered chunks. On the
|
||||
// thread pool (plain Task.Run) this pins a pool thread for the message's whole
|
||||
// network-transfer; the signal that unblocks it (the producer Feed()+Set()) runs on
|
||||
// the SignalR receive-continuation, which ALSO needs a pool thread → circular
|
||||
// dependency → pool starvation → "stuck loading → 60 s timeout" hang under concurrent
|
||||
// load. AcBinaryDeserExecutor runs the blocking wait on a dedicated, reused, never-
|
||||
// queued thread (see SIGNALR_BINARY_PROTOCOL_ISSUES.md#accore-sbp-i-t3w9) → pool stays
|
||||
// free for producers, no coupling, streaming overlap preserved, no churn.
|
||||
if (state.DeserTask == null && !IsBrowser)
|
||||
{
|
||||
_logger?.LogDebug("TryParseChunkData starting background deserialization targetType={TargetType}", state.StreamedArgType.Name);
|
||||
|
|
@ -951,7 +941,7 @@ public class AcBinaryHubProtocol : IHubProtocol
|
|||
var type = state.StreamedArgType;
|
||||
var opts = _options;
|
||||
|
||||
state.DeserTask = AcBinaryDeserExecutor.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
|
||||
state.DeserTask = Task.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
|
||||
}
|
||||
|
||||
input = input.Slice(totalNeeded);
|
||||
|
|
|
|||
|
|
@ -226,19 +226,7 @@ namespace AyCode.Services.SignalRs
|
|||
Logger.Error(errorText);
|
||||
throw new Exception(errorText);
|
||||
}
|
||||
|
||||
//yield return responseMessage.GetResponseData<TResponseData>();
|
||||
TResponseData? item;
|
||||
try
|
||||
{
|
||||
item = responseMessage.GetResponseData<TResponseData>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Stream deser; tag {messageTag}", ex); continue;
|
||||
}
|
||||
|
||||
yield return item;
|
||||
yield return responseMessage.GetResponseData<TResponseData>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -311,19 +299,7 @@ namespace AyCode.Services.SignalRs
|
|||
Logger.Error(errorText);
|
||||
throw new Exception(errorText);
|
||||
}
|
||||
|
||||
//yield return responseMessage.GetResponseData<TResponseData>();
|
||||
TResponseData? item;
|
||||
try
|
||||
{
|
||||
item = responseMessage.GetResponseData<TResponseData>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Stream deser; tag {messageTag}", ex); continue;
|
||||
}
|
||||
|
||||
yield return item;
|
||||
yield return responseMessage.GetResponseData<TResponseData>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,10 +183,8 @@ public sealed class SignalResponseDataMessage : ISignalResponseMessage
|
|||
/// </summary>
|
||||
public T? GetResponseData<T>()
|
||||
{
|
||||
var data = RawResponseData;
|
||||
if (data is T typed) return typed;
|
||||
|
||||
return data == null ? default : throw new InvalidCastException($"RawResponseData is NOT T! {typeof(T)} <> {data.GetType()}");
|
||||
if (RawResponseData is T typed) return typed;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and seri
|
|||
|
||||
### Client
|
||||
- **`AcSignalRClientBase.cs`** — Abstract SignalR client managing `HubConnection`, request/response tracking via pooled `SignalRRequestModel`. `SendCoreAsync` builds `SignalParams` (with `IsRawBytesData` when `T == byte[]`). CRUD helpers (Post, Get, GetAll, GetAllInto). Configurable timeouts.
|
||||
- **`IAcSignalRHubClient.cs`** — Client interface + `SignalResponseDataMessage` (sealed, `RawResponseData` is `object?` — typed object or byte[], `GetResponseData<T>()` safe `is T` cast — type-mismatch returns null, does **not** throw). Legacy message types (`IdMessage`, `SignalPostJsonDataMessage`) marked `[Obsolete]`.
|
||||
- **`IAcSignalRHubClient.cs`** — Client interface + `SignalResponseDataMessage` (sealed, `RawResponseData` is `object?` — typed object or byte[], `GetResponseData<T>()` direct cast). Legacy message types (`IdMessage`, `SignalPostJsonDataMessage`) marked `[Obsolete]`.
|
||||
- **`IAcSignalRHubBase.cs`** — Base hub interface: `OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, object data)`.
|
||||
- **`ISignalParams.cs`** — `ISignalParams` base interface + `SignalParams` (Status, DataSerializerType, Parameters `byte[]?`, SignalDataType `string?`, IsRawBytesData `bool`). Metadata travels as separate hub argument (AcBinary serialized). Parameters and data are independent — both nullable in any direction.
|
||||
|
||||
### Message Tagging
|
||||
- **`SignalMessageTagAttribute.cs`** — Three attributes: `TagAttribute` (base, int messageTag), `SignalRAttribute` (server method routing + client notification), `SignalRSendToClientAttribute` (client-side receive).
|
||||
- **`AcSignalRTags.cs`** — Static constants: `None`, `PingTag`, `EchoTag`.
|
||||
- **`SignalRCrudTags.cs`** — Sealed class bundling 5 independent CRUD tag integers. `GetMessageTagByTrackingState()` maps `TrackingState` -> tag. See `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`.
|
||||
- **`SignalRCrudTags.cs`** — Sealed class bundling 5 independent CRUD tag integers. `GetMessageTagByTrackingState()` maps `TrackingState` -> tag. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`.
|
||||
- **`SendToClientType.cs`** — Enum: None, Others, Caller, All.
|
||||
|
||||
### Serialization & Pooling
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ Topic docs for the `AyCode.Services` project (Layer 0, service abstractions).
|
|||
- [`LOGGING/`](LOGGING/README.md) — Remote logger (variant — sends log entries over the wire)
|
||||
- [`SIGNALR/`](SIGNALR/README.md) — SignalR transport (tag-based protocol, generic hub methods)
|
||||
- [`SIGNALR_BINARY_PROTOCOL/`](SIGNALR_BINARY_PROTOCOL/README.md) — Binary-over-SignalR wire format, chunked framing
|
||||
- [`SIGNALR_DATASOURCE/`](SIGNALR_DATASOURCE/README.md) — Client-server DataSource on SignalR transport (change tracking, rollback, sync state, IList<T> wrapper)
|
||||
- [`MVC/`](MVC/README.md) — ASP.NET Core MVC formatters for the AcBinary wire format (InputFormatter / OutputFormatter, `application/vnd.acbinary`)
|
||||
|
||||
## Navigation
|
||||
|
|
@ -19,4 +18,5 @@ Per the folder-navigation rule, start here when browsing `docs/`. Each topic fol
|
|||
- **Base logger** (framework): `../../AyCode.Core/AyCode.Core/docs/LOGGING/README.md`
|
||||
- **Server-side logger** (variant): `../../AyCode.Core.Server/docs/LOGGING/README.md`
|
||||
- **Server-side SignalR**: `../../AyCode.Services.Server/docs/SIGNALR/README.md`
|
||||
- **Server-side SignalR DataSource**: `../../AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`
|
||||
- **Binary serializer** (used by SIGNALR_BINARY_PROTOCOL): `../../AyCode.Core/AyCode.Core/docs/BINARY/README.md`
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Client-side SignalR transport: custom binary protocol, tag-based dispatch. Source: `SignalRs/`
|
||||
|
||||
> Server-side hub, session, broadcast: `AyCode.Services.Server/docs/SIGNALR/README.md`
|
||||
> DataSource collection: `../SIGNALR_DATASOURCE/README.md`
|
||||
> DataSource collection: `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`
|
||||
|
||||
## Design
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ Typed access via methods (PostDataJson pattern):
|
|||
| Request + data | `byte[]` | typed object | client responds to server with data |
|
||||
| Signal | null | null/empty | ping, status change, broadcast trigger |
|
||||
|
||||
`SignalResponseDataMessage` is an **internal DTO** for client-side callback routing and stream wire format — constructed in-memory from `signalParams` + `data`, never serialized as envelope on wire. `RawResponseData` is `object?` (typed object or byte[]). `GetResponseData<T>()` performs a safe `is T` cast — type-mismatch returns null, does **not** throw (so a caller-requested subtype that differs from the wire type silently yields null, not an error).
|
||||
`SignalResponseDataMessage` is an **internal DTO** for client-side callback routing and stream wire format — constructed in-memory from `signalParams` + `data`, never serialized as envelope on wire. `RawResponseData` is `object?` (typed object or byte[]). `GetResponseData<T>()` performs direct cast.
|
||||
|
||||
## Request/Response Flow
|
||||
|
||||
|
|
@ -129,8 +129,8 @@ OnReceiveMessage(tag, requestId, signalParams, object data)
|
|||
│ └─ { MessageTag, Status, DataSerializerType, RawResponseData = data }
|
||||
├─ Matching requestId in pending dict:
|
||||
│ ├─ Route: null→sync wait, Action→invoke, Func<Task>→await
|
||||
│ └─ GetResponseData<T>(): safe `is T` cast (mismatch → null, no throw)
|
||||
│ Protocol deserialized via SignalDataType = wire/server type (may ≠ caller's T)
|
||||
│ └─ GetResponseData<T>(): direct cast (T)RawResponseData
|
||||
│ Protocol already deserialized to correct type via SignalDataType
|
||||
└─ No match (broadcast):
|
||||
└─ abstract MessageReceived(tag, signalParams, object data).Forget()
|
||||
```
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,21 +5,6 @@
|
|||
|
||||
---
|
||||
|
||||
## ACCORE-SBP-T-Y2R8: Per-message protocol mode selection
|
||||
**Priority:** P2 · **Type:** Feature
|
||||
|
||||
**Problem:** Global `BinaryProtocolMode` config (program.cs) forces all messages through the same path — either Bytes (RAM spike on large payloads, no streaming overlap) or AsyncSegment (dedicated deser thread overhead on small frequent messages). Mixed workloads (small logs/telemetry + large DataSource/exports) cannot optimize per use-case.
|
||||
|
||||
**Solution:** Per-message mode override via invocation headers (`X-Binary-Protocol-Mode: Bytes|AsyncSegment`). Server `AcBinaryHubProtocol.SelectProtocolMode` checks header → explicit mode; fallback → `DefaultProtocolMode`. Client fluent API: `InvokeAsync(..., protocolMode: BinaryProtocolMode.Bytes)`.
|
||||
|
||||
**Use-cases:**
|
||||
- Small frequent (logs, telemetry, CRUD < 4KB): `Bytes` → minimal overhead, inline deser on pool thread
|
||||
- Large payloads (DataSource 10MB, file exports): `AsyncSegment` → streaming overlap + bounded RAM + dedicated deser thread
|
||||
|
||||
**Config:** `AcBinaryHubProtocolOptions.AllowPerMessageProtocolMode` gate (default: true).
|
||||
|
||||
**Non-solution:** Auto-detect size heuristic rejected — wire-format size unknown until serialize (dynamic collections, string interning, ID tracking).
|
||||
|
||||
## ACCORE-SBP-T-P8X9: ~~SegmentBufferReader isolated unit tests~~
|
||||
**Status:** Closed (2026-05-03) — obsoleted by `ACCORE-SBP-T-G7T2` · **Priority:** ~~P1~~ · **Type:** ~~Test coverage~~
|
||||
|
||||
|
|
|
|||
|
|
@ -20,19 +20,16 @@ flag (line ~88) that gates the background deserialize task at line ~936:
|
|||
```csharp
|
||||
if (state.DeserTask == null && !IsBrowser)
|
||||
{
|
||||
state.DeserTask = AcBinaryDeserExecutor.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
|
||||
state.DeserTask = Task.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
|
||||
}
|
||||
```
|
||||
|
||||
**Non-browser** (server / Windows-app): the deser-task runs on a dedicated
|
||||
off-pool thread via `AcBinaryDeserExecutor` (NOT `Task.Run` / the .NET thread
|
||||
pool — the blocking `ManualResetEventSlim.Wait()` between chunks would starve
|
||||
the pool, see `SIGNALR_BINARY_PROTOCOL_ISSUES.md#accore-sbp-i-t3w9`).
|
||||
**Non-browser** (server / Windows-app): the deser-task runs on a real thread.
|
||||
`AsyncPipeReaderInput.TryAdvanceSegment(...)` blocks on
|
||||
`ManualResetEventSlim.Wait()` when out of bytes — true pipeline parallelism
|
||||
between the producer (WebSocket-receive) and consumer (deserializer).
|
||||
|
||||
**Browser (WASM single-thread)**: the background deser is skipped. Chunks accumulate in
|
||||
**Browser (WASM single-thread)**: `Task.Run` is skipped. Chunks accumulate in
|
||||
`AsyncPipeReaderInput` from the WebSocket-receive callback. On `CHUNK_END`,
|
||||
`Input.Complete()` is called → `_completed = true`. The deserializer then
|
||||
runs **synchronously on the current thread** over the fully-buffered payload.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -76,7 +76,7 @@ For full architecture see `AyCode.Services/docs/SIGNALR/README.md`.
|
|||
| **SignalParams** | Metadata accompanying each message (separate hub arg). Carries Status, response type, packed parameters, raw-bytes flag. `[AcBinarySerializable]`. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. |
|
||||
| **Message Tag** | Integer identifier mapping to a method via `[SignalR(tag)]` or `[SignalRSendToClient(tag)]` attributes. |
|
||||
| **DynamicMethodRegistry** | Resolves message tags to `MethodInfo` at runtime. Static `ConcurrentDictionary` cache with lazy scan on miss. |
|
||||
| **SignalRCrudTags** | Sealed class bundling 5 independent tag integers (getAllTag, getItemTag, addTag, updateTag, removeTag) for entity CRUD. See `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`. |
|
||||
| **SignalRCrudTags** | Sealed class bundling 5 independent tag integers (getAllTag, getItemTag, addTag, updateTag, removeTag) for entity CRUD. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`. |
|
||||
| **AcBinaryHubProtocol** | Unsealed base `IHubProtocol` replacing SignalR's JSON+Base64 with `AcBinarySerializer`. Protocol name: `"acbinary"`. Full spec (write/read paths, chunked framing): `AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL/README.md`. |
|
||||
| **AyCodeBinaryHubProtocol** | Consumer-derived protocol (per-message header with type resolution, `IsRawBytesData`). Registered via `AddAcBinaryProtocol(...)` DI extension on both server and client. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. |
|
||||
| **AcBinaryHubProtocolOptions** | Mutable config for protocol registration (SerializerOptions, ProtocolMode, BufferSize, FlushTimeout, etc.). `Validate()` + `Clone()` for DI safety. Full spec: `AyCode.Services/docs/SIGNALR/README.md`. |
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ Top-level docs for `AyCode.Core` (Layer 0 — core framework).
|
|||
|
||||
- `AyCode.Core/docs/` — Logger, Binary serializer (paired topics: LOGGING/, BINARY/)
|
||||
- `AyCode.Core.Server/docs/` — Server-side logger variant (LOGGING/)
|
||||
- `AyCode.Services/docs/` — Remote logger variant, SignalR, SignalR binary protocol, SignalR DataSource (SIGNALR_DATASOURCE/)
|
||||
- `AyCode.Services.Server/docs/` — Server-side SignalR hub (SIGNALR/)
|
||||
- `AyCode.Services/docs/` — Remote logger variant, SignalR, SignalR binary protocol
|
||||
- `AyCode.Services.Server/docs/` — Server-side SignalR hub (SIGNALR/), SignalR DataSource (SIGNALR_DATASOURCE/)
|
||||
|
||||
## Navigation
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue