Skip to content
bitzorcas
中EN

Concept

Realtime

Current Chat SignalR adapter, persistent truth, membership authorization, reconnect recovery, and Null-composition boundary.

Last updated

BitzOrcas has a SignalR implementation for Chat. ChatRealtimeHub is the connection surface and SignalRChatRealtimeAdapter routes message/read-marker payloads by persisted member user IDs. Realtime is neither a write entry point nor the source of truth.

authorized REST command → repository save → realtime adapter → SignalR clients
client reconnect ─────────────────────────→ REST history cursor

Critical path diagram

Follow the main path to identify responsibility handoffs, then use the prose to inspect failure branches and evidence.

Aggregate after repository Save

Handler calls adapter directly

SignalR hub

Membership user routing

Client refresh

Current boundary

  • Chat production composition can register SignalR; NullChatRealtimeAdapter keeps REST usable when absent.
  • Documents collaboration and whiteboards still permit Null Hubs and are not delivered realtime collaboration by default.
  • Notification Inbox remains durable truth; realtime hints depend on host composition.
  • There is no production SSE endpoint today; see SSE.

The Hub has no client-invokable methods and carries [Authorize]; it is a passive transport. Chat:Realtime:Enabled=true registers SignalR and maps the configured path, default /hubs/chat.

{
"Chat": {
"Realtime": {
"Enabled": true,
"HubPath": "/hubs/chat"
}
}
}

When disabled, no Hub is mapped and NullChatRealtimeAdapter preserves REST. That is optional transport degradation, not readiness for an Edition promising realtime.

Authorization

Connection authentication proves caller identity only. Subscription and broadcast routing use persisted Membership; a client cannot select an arbitrary group. Removing a member stops later broadcast, while REST authorization still controls history.

The adapter does not use SignalR groups. It extracts distinct IDs from channel.Memberships and calls Clients.Users(recipients). IUserIdProvider must therefore map connection identity to the exact Membership.UserId contract.

// Targets come from the persisted aggregate, never a client-provided group.
var recipients = channel.Memberships
.Select(member => member.UserId)
.Where(userId => !string.IsNullOrWhiteSpace(userId))
.Distinct(StringComparer.Ordinal)
.ToArray();
// SignalR user routing makes IUserIdProvider part of the protocol.
await hubContext.Clients.Users(recipients)
.SendAsync(ChatRealtimeMethods.MessageSent, payload, cancellationToken);

Tenant/channel fields in payload help client validation; they do not replace server-side recipient selection.

Event contract

Current methods include MessageSent and ReadMarkerUpdated. Message payload carries tenant, channel, message, sender, body, mentions, attachment file IDs, and sent time. Read-marker payload carries user, message, and read time.

Because body travels over realtime, logs and telemetry must not serialize full payloads. Treat method names and DTOs as versioned protocols.

Evolve toward post-commit delivery

The preferred path saves aggregate and Outbox in one transaction, then a post-commit consumer invokes realtime. Realtime failure becomes independently retryable and rollback cannot create a ghost hint.

command transaction: aggregate + repository + outbox → commit
post-commit consumer: outbox event → realtime adapter → SignalR
client: realtime hint → REST cursor/query confirms the fact

SignalR remains at-least-once even after that change. Consumers need stable IDs; clients deduplicate and recover from REST truth.

Reconnect and scale

SignalR delivery may be lost, duplicated, or reordered. Payloads carry stable message or event identity and ordering data, and reconnect fills gaps through a history cursor. A multi-instance deployment needs a verified SignalR scale-out/backplane composition; process-local connection state is not shared.

Current registration calls only AddSignalR(); no Redis/Azure SignalR scale-out is visible. A user connected to replica A is not automatically reached when replica B broadcasts. Configure and test a backplane/service for multi-replica production.

Slow clients and large payloads need message-size, keepalive, client-timeout, transport, and proxy WebSocket limits. Current composition does not prove dedicated values; deployment tests must establish capacity.

Client recovery

// Realtime is a hint: deduplicate by messageId.
connection.on("chat.message.sent", event => {
if (seen.has(event.messageId)) return;
seen.add(event.messageId);
inbox.apply(event);
});
// Reconnect fetches from the last durable cursor.
connection.onreconnected(() => history.catchUp(lastCommittedCursor));

GA checks

  • WebSocket upgrade, token expiry, member removal, and cross-tenant groups are tested.
  • Reconnection recovers from persistent history.
  • Multi-instance broadcast, backpressure, and maximum message size are load-tested.
  • Logs contain neither tokens nor complete chat bodies.
  • An Edition promising realtime cannot pass readiness with a Null adapter.

Also inject failure after Save, rollback after a visible send, duplicate/reordered messages, IUserIdProvider mismatch, and replica-A connection with replica-B broadcast. Until direct Handler broadcast is removed, do not promise strict post-commit event semantics.

See Chat Module.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%