Compare commits
11 Commits
d2bdf7e4eb
...
954e5ef86a
| Author | SHA1 | Date |
|---|---|---|
|
|
954e5ef86a | |
|
|
1210393e67 | |
|
|
ad7d5813cb | |
|
|
52541628f9 | |
|
|
ecc8c97a80 | |
|
|
d2fadaa136 | |
|
|
ffc536446c | |
|
|
a8e90e5b59 | |
|
|
bb1dbd242e | |
|
|
aed36a71cb | |
|
|
6f60eaf310 |
|
|
@ -138,7 +138,16 @@
|
||||||
"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(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(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(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(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)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
||||||
# LLM Protocol Decisions — Archive: 2026-04
|
# LLM Protocol Decisions — Archive: 2026-04
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
> **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.
|
> **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**:
|
**Generation rules**:
|
||||||
|
|
||||||
1. Each new entry receives a fresh random suffix at creation time.
|
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-bucketed 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-month bucket 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.
|
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.
|
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-bucketed 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-month bucket archive files. If collision detected with an existing archive entry: skill aborts, signals the user, awaits manual resolution — no silent corruption.
|
||||||
|
|
||||||
## LLMP exception
|
## LLMP exception
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ Full doctrine: `../docs/ARCHITECTURE.md#framework-vs-consumer-boundary`
|
||||||
|
|
||||||
## SignalR
|
## 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()`).
|
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.Server/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/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.
|
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
|
## 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/` |
|
| `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`) |
|
| `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/` |
|
| `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.Server/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/docs/SIGNALR_DATASOURCE/` |
|
||||||
| `BIN` | BINARY | AcBinary serializer: features, format, writers, source generator | `AyCode.Core/AyCode.Core/docs/BINARY/` |
|
| `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/` |
|
| `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/` |
|
| `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
|
## 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.
|
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-bucketed 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-month bucket 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.
|
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.
|
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]: ...`.
|
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
|
### Lifecycle: archive
|
||||||
|
|
||||||
`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`.
|
`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`.
|
||||||
|
|
||||||
## Change history
|
## 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.
|
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 (`*_<year>.md`)
|
## Archive files (`*_<YYYY>_<MM>.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:
|
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.md`
|
- `LOGGING_ISSUES_2025_03.md`
|
||||||
- `BINARY_TODO_2026.md`
|
- `BINARY_TODO_2026_01.md`
|
||||||
- `LLM_PROTOCOL_DECISIONS_2026.md`
|
- `LLM_PROTOCOL_DECISIONS_2026_04.md`
|
||||||
|
|
||||||
### Default behaviour: NOT auto-loaded
|
### Default behaviour: NOT auto-loaded
|
||||||
|
|
||||||
The Step 2 glob patterns target **active** companions only — unsuffixed names. Year-suffixed variants are excluded by default. Practically:
|
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.md` does NOT.
|
- `**/docs/{TOPIC}/{TOPIC}_ISSUES.md` matches; `**/docs/{TOPIC}/{TOPIC}_ISSUES_2025_03.md` does NOT.
|
||||||
- If a generic `{TOPIC}_*.md` pattern inadvertently matches year-suffixed files, filter them out before passing to Step 4 (Load).
|
- 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)
|
### On-demand read (no user-confirm needed — read-only operation)
|
||||||
|
|
||||||
Read an archive file when ANY of these signals appears:
|
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 `_<year>.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 `_<YYYY>_<MM>.md` archive)
|
||||||
- A code comment or other doc references an ID resolving only to an archive file
|
- 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 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
|
- 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
|
public interface ICompanyInfoBase
|
||||||
{
|
{
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
string TaxId { get; }
|
string? TaxId { get; }
|
||||||
string CountryCode { get; }
|
string CountryCode { get; }
|
||||||
string PostalCode { get; }
|
string PostalCode { get; }
|
||||||
string City { 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`.
|
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):**
|
**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.Server/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/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>`.
|
- **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.
|
- **One input per type-stream**: separate streams per message-class. Practical when the type-set is small and the transport can afford multiple connections.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
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,6 +1,7 @@
|
||||||
using AyCode.Core.Enums;
|
using AyCode.Core.Enums;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using AyCode.Services.Server.SignalRs;
|
using AyCode.Services.Server.SignalRs;
|
||||||
|
using AyCode.Services.SignalRs;
|
||||||
|
|
||||||
namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;
|
namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ Server-side service implementations: JWT authentication, SendGrid email delivery
|
||||||
| Document | Topic |
|
| Document | Topic |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `SIGNALR/README.md` | Server-side SignalR hub (dispatch, session, broadcast) |
|
| `SIGNALR/README.md` | Server-side SignalR hub (dispatch, session, broadcast) |
|
||||||
| `SIGNALR_DATASOURCE/README.md` | Real-time DataSource with CRUD & change tracking |
|
|
||||||
|
|
||||||
## Folder Structure
|
## Folder Structure
|
||||||
|
|
||||||
|
|
@ -19,7 +18,7 @@ Server-side service implementations: JWT authentication, SendGrid email delivery
|
||||||
|---|---|
|
|---|---|
|
||||||
| [`Emails/`](Emails/README.md) | SendGrid email service (registration, password reset) |
|
| [`Emails/`](Emails/README.md) | SendGrid email service (registration, password reset) |
|
||||||
| [`Logins/`](Logins/README.md) | Server-side login with JWT token generation |
|
| [`Logins/`](Logins/README.md) | Server-side login with JWT token generation |
|
||||||
| [`SignalRs/`](SignalRs/README.md) | SignalR hub base, session service, data source with change tracking, client broadcast |
|
| [`SignalRs/`](SignalRs/README.md) | SignalR hub base, session service, client broadcast |
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# SignalRs
|
# SignalRs
|
||||||
|
|
||||||
Server-side SignalR hub infrastructure: hub base class, session management, data source with change tracking, and client broadcast service.
|
Server-side SignalR hub infrastructure: hub base class, session management, and client broadcast service.
|
||||||
|
|
||||||
> **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR/README.md`.
|
> **Architecture:** For full dispatch flow, tag system, and tech debt documentation see `AyCode.Services/docs/SIGNALR/README.md`.
|
||||||
|
|
||||||
|
|
@ -18,17 +18,9 @@ Server-side SignalR hub infrastructure: hub base class, session management, data
|
||||||
|
|
||||||
### Data Source
|
### Data Source
|
||||||
|
|
||||||
> **Full specification:** `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`
|
> **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`.
|
||||||
|
|
||||||
- **`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
|
### Utilities
|
||||||
- **`ExtensionMethods.cs`** — `InvokeMethod()` — invokes methods and unwraps `Task`/`Task<T>`/`ValueTask` results.
|
- **`ExtensionMethods.cs`** — `InvokeMethod()` — invokes methods and unwraps `Task`/`Task<T>`/`ValueTask` results.
|
||||||
- **`TrackingItemHelpers.cs`** — Deep clone helpers: `JsonClone<T>()`, `ReflectionClone<T>()`.
|
|
||||||
|
> `TrackingItemHelpers.cs` (deep-clone helpers used by the data source) relocated to `AyCode.Services/SignalRs/` alongside `AcSignalRDataSource` — see LLMP-DEC-68.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ Topic docs for the `AyCode.Services.Server` project (Layer 0, server-side servic
|
||||||
## Topics
|
## Topics
|
||||||
|
|
||||||
- [`SIGNALR/`](SIGNALR/README.md) — Server-side SignalR hub (dispatch, session, broadcast)
|
- [`SIGNALR/`](SIGNALR/README.md) — Server-side SignalR hub (dispatch, session, broadcast)
|
||||||
- [`SIGNALR_DATASOURCE/`](SIGNALR_DATASOURCE/README.md) — Client-server DataSource on SignalR transport (change tracking, rollback, sync state, IList<T> wrapper)
|
|
||||||
|
> **Note:** `SIGNALR_DATASOURCE/` (the `AcSignalRDataSource` client-side collection) relocated to `AyCode.Services/docs/SIGNALR_DATASOURCE/` — see `LLMP-DEC-68`.
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Server-side SignalR hub infrastructure: method dispatch, session management, broadcast, and diagnostics. Source: `SignalRs/` in this project.
|
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 client-side transport (tags, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`.
|
||||||
> For the DataSource collection see `../SIGNALR_DATASOURCE/README.md`.
|
> For the DataSource collection see `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md` (relocated to the client-side project).
|
||||||
|
|
||||||
## Server Processing
|
## Server Processing
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@
|
||||||
<ProjectReference Include="..\AyCode.Models\AyCode.Models.csproj" />
|
<ProjectReference Include="..\AyCode.Models\AyCode.Models.csproj" />
|
||||||
</ItemGroup>
|
</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>
|
<ItemGroup>
|
||||||
<None Include="docs\**\*.md" />
|
<None Include="docs\**\*.md" />
|
||||||
<None Include="**\README.md" Exclude="$(DefaultItemExcludes);docs\**" />
|
<None Include="**\README.md" Exclude="$(DefaultItemExcludes);docs\**" />
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ public sealed class EkaerCompanyInfo : ICompanyInfoBase
|
||||||
|
|
||||||
public string? FullAddress => this.ComposeFullAddress();
|
public string? FullAddress => this.ComposeFullAddress();
|
||||||
|
|
||||||
/// <summary>A saját telephely / lerakodási hely. Magyar címnél a NAV a Name/VatNumber/Phone/Email-t is megköveteli.</summary>
|
/// <summary>A bejelentő saját telephelye (fizikai fel-/lerakodási hely): bejövőnél lerakodás, kimenőnél felrakodás.
|
||||||
public LocationType? UnloadLocation { get; set; }
|
/// Magyar címnél a NAV a Name/VatNumber/Phone/Email-t is megköveteli.</summary>
|
||||||
|
public LocationType? Site { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,10 @@ public sealed class EkaerSubmitService : IEkaerSubmitService
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(operations);
|
ArgumentNullException.ThrowIfNull(operations);
|
||||||
|
|
||||||
// 1) Validáció — bármilyen hiba esetén NEM küldünk; a teljes hibalistát visszaadjuk.
|
// 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).
|
||||||
var errors = _validator.Validate(operations);
|
var errors = _validator.Validate(operations);
|
||||||
if (errors.Count > 0)
|
if (errors.HasErrors())
|
||||||
return EkaerSubmitResult.Invalid(errors);
|
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
|
// 2) Request összeállítás. A Header/User példányt itt kell létrehozni (a generált ctor csak a
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,9 @@ public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Üzleti szabályok (az XSD nem jelöli [Required]-ként, de a NAV megköveteli).
|
// 3) Üzleti szabályok (az XSD nem jelöli [Required]-ként, de a NAV megköveteli).
|
||||||
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"));
|
// 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.Items.Count == 0) errors.Add(Error($"{path}.items", "legalább egy tétel szükséges"));
|
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);
|
RequireText(tradeCard.SellerName, $"{path}.sellerName", "a feladó (eladó) neve kötelező", errors);
|
||||||
|
|
@ -117,7 +119,7 @@ public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator
|
||||||
Validator.TryValidateProperty(property.GetValue(instance), context, local);
|
Validator.TryValidateProperty(property.GetValue(instance), context, local);
|
||||||
|
|
||||||
foreach (var result in local)
|
foreach (var result in local)
|
||||||
errors.Add(new ValidationResult($"{path}.{property.Name}: {result.ErrorMessage}", result.MemberNames));
|
errors.Add(new EkaerValidationMessage(EkaerSeverity.Error, $"{path}.{property.Name}: {result.ErrorMessage}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,5 +129,7 @@ public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator
|
||||||
errors.Add(Error(path, message));
|
errors.Add(Error(path, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ValidationResult Error(string path, string message) => new($"{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}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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,8 +11,6 @@
|
||||||
// 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
|
// 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
|
namespace AyCode.Services.Nav.Ekaer.Models
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("XmlSchemaClassGenerator", "3.0.1270.0")]
|
[System.CodeDom.Compiler.GeneratedCodeAttribute("XmlSchemaClassGenerator", "3.0.1270.0")]
|
||||||
[System.SerializableAttribute()]
|
[System.SerializableAttribute()]
|
||||||
[System.Xml.Serialization.XmlTypeAttribute("LocationType", Namespace="http://schemas.nav.gov.hu/EKAER/1.0/ekaermanagement")]
|
[System.Xml.Serialization.XmlTypeAttribute("LocationType", Namespace="http://schemas.nav.gov.hu/EKAER/1.0/ekaermanagement")]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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,6 +70,16 @@ signingKey = Elek65Titkos
|
||||||
SHA-512 = AF84DC456B82234E67550C80169E517FBDAB4403607293985DECB09F534D9F73FADAABEFEE932554FABBC49F6E8F74A5DD54EA359D6B7644D95CFF3530AFB889
|
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`)
|
## Műveletek (`tradeCardOperations` → `operation`)
|
||||||
|
|
||||||
| Operation | Jelentés |
|
| Operation | Jelentés |
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,16 @@ 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):
|
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).
|
- **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): vonó jármű kötelező, legalább egy tétel, feladó/címzett név+adószám+ország.
|
- **Ü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).
|
||||||
- **`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.
|
- **`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.
|
||||||
|
|
||||||
A **`EkaerSubmitService`** (`Nav/Ekaer/`) a kapu: `Validate` → ha a hibalista nem üres, **nem küld** (`EkaerSubmitResult.Invalid`); egyébként `EkaerManageService.ManageAsync`.
|
### 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.
|
||||||
|
|
||||||
> ⚠️ **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.)
|
> ⚠️ **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,6 +21,19 @@ 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`.
|
**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 réteg-kód
|
||||||
|
|
||||||
A base réteg (interfészek, auth, send-flow) doksija: [`../README.md`](../README.md).
|
A base réteg (interfészek, auth, send-flow) doksija: [`../README.md`](../README.md).
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
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,10 +929,20 @@ public class AcBinaryHubProtocol : IHubProtocol
|
||||||
// Lazy start: begin background deserialization after first chunk is written.
|
// Lazy start: begin background deserialization after first chunk is written.
|
||||||
// The deser task reads via AsyncPipeReaderInputAdapter (struct over class) which
|
// The deser task reads via AsyncPipeReaderInputAdapter (struct over class) which
|
||||||
// calls TryAdvanceSegment on the input — blocks on ManualResetEventSlim.Wait when
|
// calls TryAdvanceSegment on the input — blocks on ManualResetEventSlim.Wait when
|
||||||
// out of data. Browser fallback: skip Task.Run — the MRES.Wait throws
|
// out of data. Browser fallback: skip the background task — the MRES.Wait throws
|
||||||
// PlatformNotSupportedException on WASM. Instead, buffer all chunks and run the
|
// PlatformNotSupportedException on WASM. Instead, buffer all chunks and run the
|
||||||
// deserializer synchronously on CHUNK_END, where state.Input.Complete() has
|
// deserializer synchronously on CHUNK_END, where state.Input.Complete() has
|
||||||
// already been called → TryAdvanceSegment never enters the Wait path.
|
// 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)
|
if (state.DeserTask == null && !IsBrowser)
|
||||||
{
|
{
|
||||||
_logger?.LogDebug("TryParseChunkData starting background deserialization targetType={TargetType}", state.StreamedArgType.Name);
|
_logger?.LogDebug("TryParseChunkData starting background deserialization targetType={TargetType}", state.StreamedArgType.Name);
|
||||||
|
|
@ -941,7 +951,7 @@ public class AcBinaryHubProtocol : IHubProtocol
|
||||||
var type = state.StreamedArgType;
|
var type = state.StreamedArgType;
|
||||||
var opts = _options;
|
var opts = _options;
|
||||||
|
|
||||||
state.DeserTask = Task.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
|
state.DeserTask = AcBinaryDeserExecutor.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
|
||||||
}
|
}
|
||||||
|
|
||||||
input = input.Slice(totalNeeded);
|
input = input.Slice(totalNeeded);
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,19 @@ namespace AyCode.Services.SignalRs
|
||||||
Logger.Error(errorText);
|
Logger.Error(errorText);
|
||||||
throw new Exception(errorText);
|
throw new Exception(errorText);
|
||||||
}
|
}
|
||||||
yield return responseMessage.GetResponseData<TResponseData>();
|
|
||||||
|
//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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -299,7 +311,19 @@ namespace AyCode.Services.SignalRs
|
||||||
Logger.Error(errorText);
|
Logger.Error(errorText);
|
||||||
throw new Exception(errorText);
|
throw new Exception(errorText);
|
||||||
}
|
}
|
||||||
yield return responseMessage.GetResponseData<TResponseData>();
|
|
||||||
|
//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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,12 @@ using AyCode.Core.Loggers;
|
||||||
using AyCode.Core.Serializers;
|
using AyCode.Core.Serializers;
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Services.SignalRs;
|
using AyCode.Services.SignalRs;
|
||||||
using Castle.Core.Logging;
|
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace AyCode.Services.Server.SignalRs
|
namespace AyCode.Services.SignalRs
|
||||||
{
|
{
|
||||||
//public class TrackingItemGuid<TDataItem>(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) : TrackingItem<TDataItem, Guid>(trackingState, currentValue, originalValue)
|
//public class TrackingItemGuid<TDataItem>(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) : TrackingItem<TDataItem, Guid>(trackingState, currentValue, originalValue)
|
||||||
// where TDataItem : class, IId<Guid> { }
|
// where TDataItem : class, IId<Guid> { }
|
||||||
|
|
@ -478,7 +477,9 @@ namespace AyCode.Services.Server.SignalRs
|
||||||
|
|
||||||
if (clearChangeTracking) TrackingItems.Clear();
|
if (clearChangeTracking) TrackingItems.Clear();
|
||||||
|
|
||||||
if (refreshDataFromDbAsync)
|
// 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)
|
||||||
{
|
{
|
||||||
LoadDataSourceAsync(false).Forget();
|
LoadDataSourceAsync(false).Forget();
|
||||||
return;
|
return;
|
||||||
|
|
@ -513,7 +514,9 @@ namespace AyCode.Services.Server.SignalRs
|
||||||
}
|
}
|
||||||
else if (clearChangeTracking) TrackingItems.Clear();
|
else if (clearChangeTracking) TrackingItems.Clear();
|
||||||
|
|
||||||
if (refreshDataFromDbAsync)
|
// 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)
|
||||||
{
|
{
|
||||||
LoadDataSourceAsync(false).Forget();
|
LoadDataSourceAsync(false).Forget();
|
||||||
return;
|
return;
|
||||||
|
|
@ -183,8 +183,10 @@ public sealed class SignalResponseDataMessage : ISignalResponseMessage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public T? GetResponseData<T>()
|
public T? GetResponseData<T>()
|
||||||
{
|
{
|
||||||
if (RawResponseData is T typed) return typed;
|
var data = RawResponseData;
|
||||||
return default;
|
if (data is T typed) return typed;
|
||||||
|
|
||||||
|
return data == null ? default : throw new InvalidCastException($"RawResponseData is NOT T! {typeof(T)} <> {data.GetType()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and seri
|
||||||
|
|
||||||
### Client
|
### 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.
|
- **`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>()` direct cast). Legacy message types (`IdMessage`, `SignalPostJsonDataMessage`) marked `[Obsolete]`.
|
- **`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]`.
|
||||||
- **`IAcSignalRHubBase.cs`** — Base hub interface: `OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, object data)`.
|
- **`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.
|
- **`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
|
### Message Tagging
|
||||||
- **`SignalMessageTagAttribute.cs`** — Three attributes: `TagAttribute` (base, int messageTag), `SignalRAttribute` (server method routing + client notification), `SignalRSendToClientAttribute` (client-side receive).
|
- **`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`.
|
- **`AcSignalRTags.cs`** — Static constants: `None`, `PingTag`, `EchoTag`.
|
||||||
- **`SignalRCrudTags.cs`** — Sealed class bundling 5 independent CRUD tag integers. `GetMessageTagByTrackingState()` maps `TrackingState` -> tag. See `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`.
|
- **`SignalRCrudTags.cs`** — Sealed class bundling 5 independent CRUD tag integers. `GetMessageTagByTrackingState()` maps `TrackingState` -> tag. See `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`.
|
||||||
- **`SendToClientType.cs`** — Enum: None, Others, Caller, All.
|
- **`SendToClientType.cs`** — Enum: None, Others, Caller, All.
|
||||||
|
|
||||||
### Serialization & Pooling
|
### Serialization & Pooling
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
|
||||||
namespace AyCode.Services.Server.SignalRs;
|
namespace AyCode.Services.SignalRs;
|
||||||
|
|
||||||
public static class TrackingItemHelpers
|
public static class TrackingItemHelpers
|
||||||
{
|
{
|
||||||
|
|
@ -7,6 +7,7 @@ Topic docs for the `AyCode.Services` project (Layer 0, service abstractions).
|
||||||
- [`LOGGING/`](LOGGING/README.md) — Remote logger (variant — sends log entries over the wire)
|
- [`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/`](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_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`)
|
- [`MVC/`](MVC/README.md) — ASP.NET Core MVC formatters for the AcBinary wire format (InputFormatter / OutputFormatter, `application/vnd.acbinary`)
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
@ -18,5 +19,4 @@ Per the folder-navigation rule, start here when browsing `docs/`. Each topic fol
|
||||||
- **Base logger** (framework): `../../AyCode.Core/AyCode.Core/docs/LOGGING/README.md`
|
- **Base logger** (framework): `../../AyCode.Core/AyCode.Core/docs/LOGGING/README.md`
|
||||||
- **Server-side logger** (variant): `../../AyCode.Core.Server/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**: `../../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`
|
- **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/`
|
Client-side SignalR transport: custom binary protocol, tag-based dispatch. Source: `SignalRs/`
|
||||||
|
|
||||||
> Server-side hub, session, broadcast: `AyCode.Services.Server/docs/SIGNALR/README.md`
|
> Server-side hub, session, broadcast: `AyCode.Services.Server/docs/SIGNALR/README.md`
|
||||||
> DataSource collection: `AyCode.Services.Server/docs/SIGNALR_DATASOURCE/README.md`
|
> DataSource collection: `../SIGNALR_DATASOURCE/README.md`
|
||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ Typed access via methods (PostDataJson pattern):
|
||||||
| Request + data | `byte[]` | typed object | client responds to server with data |
|
| Request + data | `byte[]` | typed object | client responds to server with data |
|
||||||
| Signal | null | null/empty | ping, status change, broadcast trigger |
|
| 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 direct cast.
|
`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).
|
||||||
|
|
||||||
## Request/Response Flow
|
## Request/Response Flow
|
||||||
|
|
||||||
|
|
@ -129,8 +129,8 @@ OnReceiveMessage(tag, requestId, signalParams, object data)
|
||||||
│ └─ { MessageTag, Status, DataSerializerType, RawResponseData = data }
|
│ └─ { MessageTag, Status, DataSerializerType, RawResponseData = data }
|
||||||
├─ Matching requestId in pending dict:
|
├─ Matching requestId in pending dict:
|
||||||
│ ├─ Route: null→sync wait, Action→invoke, Func<Task>→await
|
│ ├─ Route: null→sync wait, Action→invoke, Func<Task>→await
|
||||||
│ └─ GetResponseData<T>(): direct cast (T)RawResponseData
|
│ └─ GetResponseData<T>(): safe `is T` cast (mismatch → null, no throw)
|
||||||
│ Protocol already deserialized to correct type via SignalDataType
|
│ Protocol deserialized via SignalDataType = wire/server type (may ≠ caller's T)
|
||||||
└─ No match (broadcast):
|
└─ No match (broadcast):
|
||||||
└─ abstract MessageReceived(tag, signalParams, object data).Forget()
|
└─ 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,6 +5,21 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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~~
|
## ACCORE-SBP-T-P8X9: ~~SegmentBufferReader isolated unit tests~~
|
||||||
**Status:** Closed (2026-05-03) — obsoleted by `ACCORE-SBP-T-G7T2` · **Priority:** ~~P1~~ · **Type:** ~~Test coverage~~
|
**Status:** Closed (2026-05-03) — obsoleted by `ACCORE-SBP-T-G7T2` · **Priority:** ~~P1~~ · **Type:** ~~Test coverage~~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,19 @@ flag (line ~88) that gates the background deserialize task at line ~936:
|
||||||
```csharp
|
```csharp
|
||||||
if (state.DeserTask == null && !IsBrowser)
|
if (state.DeserTask == null && !IsBrowser)
|
||||||
{
|
{
|
||||||
state.DeserTask = Task.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
|
state.DeserTask = AcBinaryDeserExecutor.Run(() => AcBinaryDeserializer.Deserialize(input2, type, opts));
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Non-browser** (server / Windows-app): the deser-task runs on a real thread.
|
**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`).
|
||||||
`AsyncPipeReaderInput.TryAdvanceSegment(...)` blocks on
|
`AsyncPipeReaderInput.TryAdvanceSegment(...)` blocks on
|
||||||
`ManualResetEventSlim.Wait()` when out of bytes — true pipeline parallelism
|
`ManualResetEventSlim.Wait()` when out of bytes — true pipeline parallelism
|
||||||
between the producer (WebSocket-receive) and consumer (deserializer).
|
between the producer (WebSocket-receive) and consumer (deserializer).
|
||||||
|
|
||||||
**Browser (WASM single-thread)**: `Task.Run` is skipped. Chunks accumulate in
|
**Browser (WASM single-thread)**: the background deser is skipped. Chunks accumulate in
|
||||||
`AsyncPipeReaderInput` from the WebSocket-receive callback. On `CHUNK_END`,
|
`AsyncPipeReaderInput` from the WebSocket-receive callback. On `CHUNK_END`,
|
||||||
`Input.Complete()` is called → `_completed = true`. The deserializer then
|
`Input.Complete()` is called → `_completed = true`. The deserializer then
|
||||||
runs **synchronously on the current thread** over the fully-buffered payload.
|
runs **synchronously on the current thread** over the fully-buffered payload.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# SignalR DataSource
|
# SignalR DataSource
|
||||||
|
|
||||||
Change-tracked real-time collection on top of the SignalR transport layer. Source: `SignalRs/AcSignalRDataSource.cs` in `AyCode.Services.Server`.
|
Change-tracked real-time collection on top of the SignalR transport layer. Source: `SignalRs/AcSignalRDataSource.cs` in `AyCode.Services`.
|
||||||
|
|
||||||
> For the underlying transport (tag system, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`.
|
> For the underlying transport (tag system, wire protocol, client base) see `AyCode.Services/docs/SIGNALR/README.md`.
|
||||||
> For server hub infrastructure see `../SIGNALR/README.md`.
|
> For server hub infrastructure see `AyCode.Services.Server/docs/SIGNALR/README.md`.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|
@ -253,7 +253,7 @@ Projects can also call the transport directly without DataSource — see `AyCode
|
||||||
|
|
||||||
| Component | Path |
|
| Component | Path |
|
||||||
|---|---|
|
|---|---|
|
||||||
| DataSource (abstract base) | `SignalRs/AcSignalRDataSource.cs` (in `AyCode.Services.Server`) |
|
| DataSource (abstract base) | `SignalRs/AcSignalRDataSource.cs` (in `AyCode.Services`) |
|
||||||
| Tracking helpers (`JsonClone` / `ReflectionClone`) | `SignalRs/TrackingItemHelpers.cs` (in `AyCode.Services.Server`) |
|
| Tracking helpers (`JsonClone` / `ReflectionClone`) | `SignalRs/TrackingItemHelpers.cs` (in `AyCode.Services`) |
|
||||||
| CRUD tags | `SignalRs/SignalRCrudTags.cs` (in `AyCode.Services`) |
|
| CRUD tags | `SignalRs/SignalRCrudTags.cs` (in `AyCode.Services`) |
|
||||||
| Transport client | `SignalRs/AcSignalRClientBase.cs` (in `AyCode.Services`) |
|
| Transport client | `SignalRs/AcSignalRClientBase.cs` (in `AyCode.Services`) |
|
||||||
File diff suppressed because one or more lines are too long
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`. |
|
| **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. |
|
| **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. |
|
| **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.Server/docs/SIGNALR_DATASOURCE/README.md`. |
|
| **SignalRCrudTags** | Sealed class bundling 5 independent tag integers (getAllTag, getItemTag, addTag, updateTag, removeTag) for entity CRUD. See `AyCode.Services/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`. |
|
| **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`. |
|
| **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`. |
|
| **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/docs/` — Logger, Binary serializer (paired topics: LOGGING/, BINARY/)
|
||||||
- `AyCode.Core.Server/docs/` — Server-side logger variant (LOGGING/)
|
- `AyCode.Core.Server/docs/` — Server-side logger variant (LOGGING/)
|
||||||
- `AyCode.Services/docs/` — Remote logger variant, SignalR, SignalR binary protocol
|
- `AyCode.Services/docs/` — Remote logger variant, SignalR, SignalR binary protocol, SignalR DataSource (SIGNALR_DATASOURCE/)
|
||||||
- `AyCode.Services.Server/docs/` — Server-side SignalR hub (SIGNALR/), SignalR DataSource (SIGNALR_DATASOURCE/)
|
- `AyCode.Services.Server/docs/` — Server-side SignalR hub (SIGNALR/)
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue