Compare commits

..

No commits in common. "954e5ef86a41afe51948b55f6ba7e7612103ed1a" and "d2bdf7e4ebfcfccfddb4fdd872266c948733273e" have entirely different histories.

47 changed files with 101 additions and 604 deletions

View File

@ -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

View File

@ -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.
>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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.

View File

@ -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).");
}
}

View File

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

View File

@ -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

View File

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

View File

@ -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>()`.

View File

@ -1,7 +1,7 @@
using System.Reflection;
using AyCode.Core.Extensions;
namespace AyCode.Services.SignalRs;
namespace AyCode.Services.Server.SignalRs;
public static class TrackingItemHelpers
{

View File

@ -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

View File

@ -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

View File

@ -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`) |

View File

@ -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\**" />

View File

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

View File

@ -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

View File

@ -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}");
}

View File

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

View File

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

View File

@ -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")]

View File

@ -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&lt;int&gt;-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();
}
}

View File

@ -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 (202425): 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 |

View File

@ -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.)

View File

@ -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 202425-ö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).

View File

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

View File

@ -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);

View File

@ -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>();
}
}
}

View File

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

View File

@ -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

View File

@ -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`

View File

@ -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

View File

@ -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~~

View File

@ -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

View File

@ -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`. |

View File

@ -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