Relocate AcSignalRDataSource to AyCode.Services
Moved AcSignalRDataSource and TrackingItemHelpers from AyCode.Services.Server to AyCode.Services (namespace updated). Updated all references, usings, and doc cross-refs to new location. Removed orphan SignalR.Core 1.2.0 package and .Server DLL refs from Blazor/Web.Client/Shared projects; WASM AOT build now works. Introduced AcBinaryDeserExecutor for off-pool streaming deserialization, fixing thread pool starvation (see updated protocol docs). Updated test usings and doc tables. No functional changes to DataSource logic. Decision log entry LLMP-DEC-68 documents the change.
This commit is contained in:
parent
aed36a71cb
commit
bb1dbd242e
|
|
@ -139,7 +139,15 @@
|
||||||
"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(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
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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/` |
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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> { }
|
||||||
|
|
@ -21,7 +21,7 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and seri
|
||||||
### 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
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -3,6 +3,31 @@
|
||||||
For planned/actionable work see `SIGNALR_BINARY_PROTOCOL_TODO.md`.
|
For planned/actionable work see `SIGNALR_BINARY_PROTOCOL_TODO.md`.
|
||||||
For higher-level SignalR abstractions see `../SIGNALR/SIGNALR_ISSUES.md`.
|
For higher-level SignalR abstractions see `../SIGNALR/SIGNALR_ISSUES.md`.
|
||||||
|
|
||||||
|
## ACCORE-SBP-I-T3W9: AsyncSegment blocking waits on thread-pool → starvation deadlock under concurrent load
|
||||||
|
|
||||||
|
**Severity:** Critical · **Status:** Partially Fixed (deser-side fixed 2026-06-08; send-side TBD) · **Area:** `AcBinaryHubProtocol` AsyncSegment send + receive paths
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
In `AsyncSegment` mode both directions do **synchronous blocking waits on .NET thread-pool threads**, which starve the pool under concurrent load + real network latency (production-only; never reproduces locally where there is no network and one user). Symptom: clients stuck loading → ~60 s timeout. Hits **all** client types (incl. WASM, which has no `.Wait` of its own — they are victims of server-side starvation).
|
||||||
|
|
||||||
|
- **Receive (deser):** `TryParseChunkData` runs the streaming deser via `Task.Run`, which blocks in `AsyncPipeReaderInput.TryAdvanceSegment` → `ManualResetEventSlim.Wait()` between network-delivered chunks. The unblocking `Feed()`+`Set()` runs on the SignalR receive-continuation — **also** a pool thread → circular dependency → pool starvation, broken only by the runtime's slow thread injection (~1-2/s).
|
||||||
|
- **Send (flush):** `WriteMessageChunked` → `SyncFlush(pipeWriter.FlushAsync())` (lines ~535, ~1135) blocks a dispatch (pool) thread **per chunk** under client backpressure (slow mobile clients) while streaming multi-MB responses. Forced by the synchronous `IHubProtocol.WriteMessage` API. **This is the dominant production trigger** (proven by the WASM-client hang + multi-MB server→client payloads).
|
||||||
|
|
||||||
|
`Bytes` mode avoids both (no background deser, no per-chunk sync flush) — hence stable as the current hotfix.
|
||||||
|
|
||||||
|
### Fix — deser (receive) side ✅ Fixed (2026-06-08)
|
||||||
|
|
||||||
|
Resolved — `TryParseChunkData` runs the blocking streaming deser off the .NET thread pool via `AcBinaryDeserExecutor` (`AyCode.Services/SignalRs/AcBinaryDeserExecutor.cs`). See that file's doc-comment for the design.
|
||||||
|
|
||||||
|
### Fix — send (flush) side
|
||||||
|
|
||||||
|
**TBD.** The per-chunk `SyncFlush` blocking is partly inherent to streaming-with-bounded-memory under the synchronous `WriteMessage` API. To be designed; entry updated when settled.
|
||||||
|
|
||||||
|
### Related TODO
|
||||||
|
|
||||||
|
(To be opened when the deser-side fix lands and the send-side direction is committed.)
|
||||||
|
|
||||||
## ACCORE-SBP-I-F6T2: AsyncSegment send-path unsupported on WebAssembly
|
## ACCORE-SBP-I-F6T2: AsyncSegment send-path unsupported on WebAssembly
|
||||||
|
|
||||||
**Severity:** Major (on WASM) · **Status:** Open · **Area:** `AsyncPipeWriterOutput` / WASM runtime
|
**Severity:** Major (on WASM) · **Status:** Open · **Area:** `AsyncPipeWriterOutput` / WASM runtime
|
||||||
|
|
|
||||||
|
|
@ -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