@cocoar/signalarrr 4.0.0-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +375 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +333 -0
- package/dist/index.js.map +1 -0
- package/docs/authorization.md +182 -0
- package/docs/client-api.md +358 -0
- package/docs/getting-started.md +300 -0
- package/docs/migration-v4.md +228 -0
- package/docs/proxy-generation.md +157 -0
- package/docs/server-api.md +251 -0
- package/docs/streaming.md +215 -0
- package/package.json +43 -0
- package/scripts/copy-assets.cjs +34 -0
- package/scripts/postinstall.cjs +42 -0
- package/skills/signalarrr/SKILL.md +154 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Streaming
|
|
2
|
+
|
|
3
|
+
SignalARRR supports streaming in both directions using `IAsyncEnumerable<T>`,
|
|
4
|
+
`IObservable<T>`, and `ChannelReader<T>`.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Server-to-Client Streaming
|
|
9
|
+
|
|
10
|
+
### Define the contract
|
|
11
|
+
|
|
12
|
+
```csharp
|
|
13
|
+
[SignalARRRContract]
|
|
14
|
+
public interface IChatHub {
|
|
15
|
+
IAsyncEnumerable<string> StreamMessages(CancellationToken ct);
|
|
16
|
+
IObservable<int> ObserveCountdown(int from);
|
|
17
|
+
ChannelReader<LogEntry> StreamLogs(string filter);
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Implement on the server
|
|
22
|
+
|
|
23
|
+
```csharp
|
|
24
|
+
public class ChatMethods : ServerMethods<ChatHub>, IChatHub {
|
|
25
|
+
|
|
26
|
+
// IAsyncEnumerable — preferred for most streaming scenarios
|
|
27
|
+
public async IAsyncEnumerable<string> StreamMessages(
|
|
28
|
+
[EnumeratorCancellation] CancellationToken ct) {
|
|
29
|
+
while (!ct.IsCancellationRequested) {
|
|
30
|
+
yield return $"Message at {DateTime.Now:HH:mm:ss}";
|
|
31
|
+
await Task.Delay(1000, ct);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// IObservable — for Rx-based pipelines
|
|
36
|
+
public IObservable<int> ObserveCountdown(int from) {
|
|
37
|
+
return Observable.Interval(TimeSpan.FromSeconds(1))
|
|
38
|
+
.Take(from)
|
|
39
|
+
.Select(i => from - (int)i);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ChannelReader — for producer/consumer patterns
|
|
43
|
+
public ChannelReader<LogEntry> StreamLogs(string filter) {
|
|
44
|
+
var channel = Channel.CreateUnbounded<LogEntry>();
|
|
45
|
+
_ = WriteLogsAsync(channel.Writer, filter);
|
|
46
|
+
return channel.Reader;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async Task WriteLogsAsync(ChannelWriter<LogEntry> writer, string filter) {
|
|
50
|
+
try {
|
|
51
|
+
await foreach (var log in _logService.Watch(filter)) {
|
|
52
|
+
await writer.WriteAsync(log);
|
|
53
|
+
}
|
|
54
|
+
} finally {
|
|
55
|
+
writer.Complete();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Consume on the client
|
|
62
|
+
|
|
63
|
+
```csharp
|
|
64
|
+
var chat = connection.GetTypedMethods<IChatHub>();
|
|
65
|
+
|
|
66
|
+
// IAsyncEnumerable — natural async iteration
|
|
67
|
+
var cts = new CancellationTokenSource();
|
|
68
|
+
await foreach (var msg in chat.StreamMessages(cts.Token)) {
|
|
69
|
+
Console.WriteLine(msg);
|
|
70
|
+
if (shouldStop) cts.Cancel();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// IObservable — Rx subscription
|
|
74
|
+
chat.ObserveCountdown(10).Subscribe(
|
|
75
|
+
count => Console.WriteLine($"Countdown: {count}"),
|
|
76
|
+
() => Console.WriteLine("Done!"));
|
|
77
|
+
|
|
78
|
+
// ChannelReader — read from channel
|
|
79
|
+
var reader = chat.StreamLogs("error");
|
|
80
|
+
while (await reader.WaitToReadAsync()) {
|
|
81
|
+
while (reader.TryRead(out var log)) {
|
|
82
|
+
Console.WriteLine(log);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Client-to-Server Streaming (Server-Initiated)
|
|
90
|
+
|
|
91
|
+
The server can request a stream from the client. The client returns
|
|
92
|
+
`IAsyncEnumerable<T>` and SignalARRR handles the wire protocol automatically.
|
|
93
|
+
|
|
94
|
+
### Define the client contract
|
|
95
|
+
|
|
96
|
+
```csharp
|
|
97
|
+
[SignalARRRContract]
|
|
98
|
+
public interface IDataClient {
|
|
99
|
+
IAsyncEnumerable<int> StreamNumbers(int count);
|
|
100
|
+
Task<string> GetStatus();
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Implement on the client
|
|
105
|
+
|
|
106
|
+
```csharp
|
|
107
|
+
public class DataClientImpl : IDataClient {
|
|
108
|
+
public async IAsyncEnumerable<int> StreamNumbers(int count) {
|
|
109
|
+
for (int i = 0; i < count; i++) {
|
|
110
|
+
yield return i;
|
|
111
|
+
await Task.Delay(100);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public Task<string> GetStatus() => Task.FromResult("OK");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Register the implementation
|
|
119
|
+
connection.MessageHandler.RegisterInterface<IDataClient, DataClientImpl>();
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Request the stream from the server
|
|
123
|
+
|
|
124
|
+
```csharp
|
|
125
|
+
public class DataMethods : ServerMethods<MyHub> {
|
|
126
|
+
public async Task ProcessClientData() {
|
|
127
|
+
// Get typed proxy for the calling client
|
|
128
|
+
var client = ClientContext.GetTypedMethods<IDataClient>();
|
|
129
|
+
|
|
130
|
+
// Stream data from the client
|
|
131
|
+
await foreach (var number in client.StreamNumbers(100)) {
|
|
132
|
+
Logger.LogInformation("Received: {Number}", number);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### How it works internally
|
|
139
|
+
|
|
140
|
+
1. Server calls `StreamNumbers(100)` on the client proxy
|
|
141
|
+
2. Proxy sends a `ServerRequestMessage` with a `StreamId` to the client
|
|
142
|
+
3. Client invokes `DataClientImpl.StreamNumbers(100)`
|
|
143
|
+
4. Client enumerates the `IAsyncEnumerable<int>` and sends each item back
|
|
144
|
+
to the server via `StreamItemToServer(streamId, item)`
|
|
145
|
+
5. Client signals completion via `StreamCompleteToServer(streamId, null)`
|
|
146
|
+
6. Server reads items from a `Channel<object>` managed by `ServerStreamManager`
|
|
147
|
+
7. Server receives items as `IAsyncEnumerable<T>` through the proxy
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Cancellation
|
|
152
|
+
|
|
153
|
+
### Server-to-client stream cancellation
|
|
154
|
+
|
|
155
|
+
Pass a `CancellationToken` to the streaming method. When cancelled, the server
|
|
156
|
+
stops the stream:
|
|
157
|
+
|
|
158
|
+
```csharp
|
|
159
|
+
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
|
160
|
+
await foreach (var msg in chat.StreamMessages(cts.Token)) {
|
|
161
|
+
Console.WriteLine(msg);
|
|
162
|
+
}
|
|
163
|
+
// Stream stops after 30 seconds
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
On the server side, use `[EnumeratorCancellation]` on the `CancellationToken`
|
|
167
|
+
parameter:
|
|
168
|
+
|
|
169
|
+
```csharp
|
|
170
|
+
public async IAsyncEnumerable<string> StreamMessages(
|
|
171
|
+
[EnumeratorCancellation] CancellationToken ct) {
|
|
172
|
+
// ct is cancelled when the client disconnects or cancels
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Server-to-client CancellationToken propagation
|
|
177
|
+
|
|
178
|
+
When the server calls a client method, it can pass a `CancellationToken` that
|
|
179
|
+
propagates to the client:
|
|
180
|
+
|
|
181
|
+
```csharp
|
|
182
|
+
// Server side
|
|
183
|
+
var cts = new CancellationTokenSource();
|
|
184
|
+
var client = ClientContext.GetTypedMethods<IWorkerClient>();
|
|
185
|
+
_ = client.DoLongWork(cts.Token); // Token propagated to client
|
|
186
|
+
|
|
187
|
+
// Later...
|
|
188
|
+
cts.Cancel(); // Client's CancellationToken is cancelled remotely
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```csharp
|
|
192
|
+
// Client side
|
|
193
|
+
public class WorkerImpl : IWorkerClient {
|
|
194
|
+
public async Task DoLongWork(CancellationToken ct) {
|
|
195
|
+
while (!ct.IsCancellationRequested) {
|
|
196
|
+
await Task.Delay(1000, ct);
|
|
197
|
+
// Cancelled when server calls cts.Cancel()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Return type mapping
|
|
206
|
+
|
|
207
|
+
| Contract return type | Proxy dispatch | Wire protocol |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| `IAsyncEnumerable<T>` | `StreamAsync<T>()` | `StreamMessage` |
|
|
210
|
+
| `ChannelReader<T>` | `StreamAsync<T>()` → `ToChannelReader()` | `StreamMessage` |
|
|
211
|
+
| `IObservable<T>` | `StreamAsync<T>()` → `ToObservable()` | `StreamMessage` |
|
|
212
|
+
| `Task<T>` | `InvokeAsync<T>()` | `InvokeMessageResult` |
|
|
213
|
+
| `Task` | `SendAsync()` | `InvokeMessage` |
|
|
214
|
+
| `void` | `Send()` | `SendMessage` |
|
|
215
|
+
| `T` (sync) | `Invoke<T>()` | `InvokeMessageResult` |
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cocoar/signalarrr",
|
|
3
|
+
"version": "4.0.0-beta.15",
|
|
4
|
+
"description": "TypeScript client for SignalARRR — type-safe RPC over SignalR",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"require": "./dist/index.cjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup",
|
|
17
|
+
"dev": "tsup --watch",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"prepack": "node scripts/copy-assets.cjs",
|
|
21
|
+
"postinstall": "node scripts/postinstall.cjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/",
|
|
25
|
+
"skills/",
|
|
26
|
+
"docs/",
|
|
27
|
+
"scripts/"
|
|
28
|
+
],
|
|
29
|
+
"author": "Bernhard Windisch",
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@microsoft/signalr": "^10.0.0",
|
|
33
|
+
"@microsoft/signalr-protocol-msgpack": "^10.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"tsup": "^8.0.0",
|
|
37
|
+
"typescript": "^5.7.0",
|
|
38
|
+
"vitest": "^4.1.0"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Runs at `prepack` (before npm pack / npm publish).
|
|
4
|
+
* Copies the repo-root skills/ and docs/ into the package directory so they
|
|
5
|
+
* are included in the published tarball.
|
|
6
|
+
* Safe to run outside the repo — exits silently if sources are not found.
|
|
7
|
+
*/
|
|
8
|
+
const { existsSync, mkdirSync, cpSync, rmSync } = require('node:fs');
|
|
9
|
+
const { join } = require('node:path');
|
|
10
|
+
|
|
11
|
+
const pkgRoot = join(__dirname, '..');
|
|
12
|
+
const repoRoot = join(__dirname, '../../..');
|
|
13
|
+
|
|
14
|
+
const skillsSrc = join(repoRoot, 'skills', 'signalarrr');
|
|
15
|
+
const docsSrc = join(repoRoot, 'docs');
|
|
16
|
+
const skillsDst = join(pkgRoot, 'skills', 'signalarrr');
|
|
17
|
+
const docsDst = join(pkgRoot, 'docs');
|
|
18
|
+
|
|
19
|
+
if (!existsSync(skillsSrc) || !existsSync(docsSrc)) {
|
|
20
|
+
console.log('[signalarrr] prepack: source assets not found — skipping copy');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Clean and copy
|
|
25
|
+
if (existsSync(skillsDst)) rmSync(skillsDst, { recursive: true, force: true });
|
|
26
|
+
if (existsSync(docsDst)) rmSync(docsDst, { recursive: true, force: true });
|
|
27
|
+
|
|
28
|
+
mkdirSync(skillsDst, { recursive: true });
|
|
29
|
+
mkdirSync(docsDst, { recursive: true });
|
|
30
|
+
|
|
31
|
+
cpSync(skillsSrc, skillsDst, { recursive: true });
|
|
32
|
+
cpSync(docsSrc, docsDst, { recursive: true });
|
|
33
|
+
|
|
34
|
+
console.log('[signalarrr] prepack: assets copied');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Runs automatically after `npm install` in a consuming project.
|
|
4
|
+
* Mirrors the MSBuild buildTransitive targets behaviour:
|
|
5
|
+
* - Only copies if .claude / .github already exists (no folder pollution)
|
|
6
|
+
* - Copies SKILL.md → {agentDir}/skills/signalarrr/
|
|
7
|
+
* - Copies docs/ → {agentDir}/skills/signalarrr/references/
|
|
8
|
+
*/
|
|
9
|
+
const { existsSync, mkdirSync, cpSync } = require('node:fs');
|
|
10
|
+
const { join } = require('node:path');
|
|
11
|
+
|
|
12
|
+
const pkgRoot = join(__dirname, '..');
|
|
13
|
+
|
|
14
|
+
// INIT_CWD is set by npm to the directory where `npm install` was invoked.
|
|
15
|
+
const projectRoot = process.env.INIT_CWD ?? process.env.npm_config_local_prefix;
|
|
16
|
+
|
|
17
|
+
// Guard: not a consuming project install (e.g. `npm install` run inside this package itself)
|
|
18
|
+
if (!projectRoot || projectRoot === pkgRoot) process.exit(0);
|
|
19
|
+
|
|
20
|
+
const skillsSrc = join(pkgRoot, 'skills', 'signalarrr');
|
|
21
|
+
const docsSrc = join(pkgRoot, 'docs');
|
|
22
|
+
|
|
23
|
+
// Guard: assets were not bundled (dev build without prepack)
|
|
24
|
+
if (!existsSync(skillsSrc)) process.exit(0);
|
|
25
|
+
|
|
26
|
+
function copySkills(agentDir) {
|
|
27
|
+
if (!existsSync(agentDir)) return;
|
|
28
|
+
|
|
29
|
+
const skillsDst = join(agentDir, 'skills', 'signalarrr');
|
|
30
|
+
const refsDst = join(agentDir, 'skills', 'signalarrr', 'references');
|
|
31
|
+
|
|
32
|
+
mkdirSync(skillsDst, { recursive: true });
|
|
33
|
+
mkdirSync(refsDst, { recursive: true });
|
|
34
|
+
|
|
35
|
+
cpSync(skillsSrc, skillsDst, { recursive: true, force: true });
|
|
36
|
+
cpSync(docsSrc, refsDst, { recursive: true, force: true });
|
|
37
|
+
|
|
38
|
+
console.log(`[signalarrr] Skills copied to ${skillsDst}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
copySkills(join(projectRoot, '.claude'));
|
|
42
|
+
copySkills(join(projectRoot, '.github'));
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: signalarrr
|
|
3
|
+
description: >
|
|
4
|
+
Typed bidirectional RPC over SignalR using Cocoar.SignalARRR.
|
|
5
|
+
Use when working with HARRR hubs, ServerMethods, HARRRConnection,
|
|
6
|
+
[SignalARRRContract] interfaces, streaming (IAsyncEnumerable, IObservable,
|
|
7
|
+
ChannelReader), server-to-client calls, SignalARRR authorization,
|
|
8
|
+
or the @cocoar/signalarrr TypeScript/JavaScript npm client.
|
|
9
|
+
metadata:
|
|
10
|
+
author: Bernhard Windisch
|
|
11
|
+
version: "4.0"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Cocoar.SignalARRR
|
|
15
|
+
|
|
16
|
+
SignalARRR extends ASP.NET Core SignalR with typed bidirectional RPC.
|
|
17
|
+
Both server and client can call each other's methods through shared interfaces,
|
|
18
|
+
with compile-time proxy generation, streaming, cancellation propagation, and
|
|
19
|
+
ASP.NET Core authorization.
|
|
20
|
+
|
|
21
|
+
## When to use this skill
|
|
22
|
+
|
|
23
|
+
Use this skill when:
|
|
24
|
+
- Setting up a HARRR hub or ServerMethods class
|
|
25
|
+
- Creating or configuring a HARRRConnection (C# or TypeScript client)
|
|
26
|
+
- Defining `[SignalARRRContract]` interfaces for typed RPC
|
|
27
|
+
- Implementing streaming with IAsyncEnumerable, IObservable, or ChannelReader
|
|
28
|
+
- Server needs to call client methods and await responses
|
|
29
|
+
- Configuring authorization on hub methods
|
|
30
|
+
- Using the `@cocoar/signalarrr` npm package in a JavaScript/TypeScript project
|
|
31
|
+
- Troubleshooting SignalARRR connection or invocation issues
|
|
32
|
+
|
|
33
|
+
## Package structure
|
|
34
|
+
|
|
35
|
+
| Package | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `Cocoar.SignalARRR.Server` | Server-side: HARRR hub, ServerMethods, auth, ClientManager |
|
|
38
|
+
| `Cocoar.SignalARRR.Client` | Client-side .NET: HARRRConnection, typed proxies |
|
|
39
|
+
| `Cocoar.SignalARRR.Contracts` | Shared: `[SignalARRRContract]` attribute + source generator (reference from shared interface projects) |
|
|
40
|
+
| `Cocoar.SignalARRR.Common` | Wire protocol types (referenced automatically) |
|
|
41
|
+
| `Cocoar.SignalARRR.ProxyGenerator` | Proxy factory infrastructure (referenced automatically) |
|
|
42
|
+
| `Cocoar.SignalARRR.DynamicProxy` | Opt-in runtime proxy fallback via DispatchProxy |
|
|
43
|
+
| `Cocoar.SignalARRR.SourceGenerator` | Roslyn source generator (bundled in Contracts) |
|
|
44
|
+
| `@cocoar/signalarrr` (npm) | TypeScript/JavaScript client: `HARRRConnection`, `invoke`, `send`, `stream`, `onServerMethod` |
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
### 1. Define shared interfaces
|
|
49
|
+
|
|
50
|
+
In your shared project, reference `Cocoar.SignalARRR.Contracts`:
|
|
51
|
+
|
|
52
|
+
```csharp
|
|
53
|
+
[SignalARRRContract]
|
|
54
|
+
public interface IChatHub {
|
|
55
|
+
Task SendMessage(string user, string message);
|
|
56
|
+
Task<List<string>> GetHistory();
|
|
57
|
+
IAsyncEnumerable<string> StreamMessages(CancellationToken ct);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[SignalARRRContract]
|
|
61
|
+
public interface IChatClient {
|
|
62
|
+
void ReceiveMessage(string user, string message);
|
|
63
|
+
Task<string> GetClientName();
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Server setup
|
|
68
|
+
|
|
69
|
+
```csharp
|
|
70
|
+
// Program.cs
|
|
71
|
+
services.AddSignalR();
|
|
72
|
+
services.AddSignalARRR(options => options
|
|
73
|
+
.AddServerMethodsFrom(typeof(Program).Assembly));
|
|
74
|
+
|
|
75
|
+
app.UseRouting();
|
|
76
|
+
app.UseAuthentication(); // if using auth
|
|
77
|
+
app.UseAuthorization(); // if using auth
|
|
78
|
+
app.MapHARRRController<ChatHub>("/chathub");
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```csharp
|
|
82
|
+
// Hub
|
|
83
|
+
public class ChatHub : HARRR {
|
|
84
|
+
public ChatHub(IServiceProvider sp) : base(sp) { }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Server methods (auto-discovered)
|
|
88
|
+
public class ChatMethods : ServerMethods<ChatHub>, IChatHub {
|
|
89
|
+
public Task SendMessage(string user, string message) {
|
|
90
|
+
Clients.All.SendAsync("ReceiveMessage", user, message);
|
|
91
|
+
return Task.CompletedTask;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public Task<List<string>> GetHistory() => Task.FromResult(new List<string>());
|
|
95
|
+
|
|
96
|
+
public async IAsyncEnumerable<string> StreamMessages(
|
|
97
|
+
[EnumeratorCancellation] CancellationToken ct) {
|
|
98
|
+
while (!ct.IsCancellationRequested) {
|
|
99
|
+
yield return $"msg-{DateTime.Now:ss}";
|
|
100
|
+
await Task.Delay(1000, ct);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3. Client setup
|
|
107
|
+
|
|
108
|
+
```csharp
|
|
109
|
+
var connection = HARRRConnection.Create(builder => {
|
|
110
|
+
builder.WithUrl("https://localhost:5001/chathub");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await connection.StartAsync();
|
|
114
|
+
|
|
115
|
+
// Typed calls
|
|
116
|
+
var chat = connection.GetTypedMethods<IChatHub>();
|
|
117
|
+
await chat.SendMessage("Alice", "Hello!");
|
|
118
|
+
var history = await chat.GetHistory();
|
|
119
|
+
|
|
120
|
+
// Streaming
|
|
121
|
+
await foreach (var msg in chat.StreamMessages(cancellationToken)) {
|
|
122
|
+
Console.WriteLine(msg);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 4. Server-to-client calls
|
|
127
|
+
|
|
128
|
+
```csharp
|
|
129
|
+
// Inside a ServerMethods class — use ClientContext
|
|
130
|
+
var client = ClientContext.GetTypedMethods<IChatClient>();
|
|
131
|
+
var name = await client.GetClientName();
|
|
132
|
+
|
|
133
|
+
// Outside hub context — inject ClientManager
|
|
134
|
+
public class NotificationService {
|
|
135
|
+
private readonly ClientManager _clients;
|
|
136
|
+
public NotificationService(ClientManager clients) => _clients = clients;
|
|
137
|
+
|
|
138
|
+
public void NotifyClient(string connectionId) {
|
|
139
|
+
var client = _clients.GetTypedMethods<IChatClient>(connectionId);
|
|
140
|
+
client.ReceiveMessage("System", "Hello from server!");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Reference documentation
|
|
146
|
+
|
|
147
|
+
For detailed API documentation, see:
|
|
148
|
+
- [Getting Started](references/getting-started.md) — full setup walkthrough
|
|
149
|
+
- [Server API](references/server-api.md) — HARRR, ServerMethods, ClientManager, AddSignalARRR
|
|
150
|
+
- [Client API](references/client-api.md) — HARRRConnection, typed proxies, event handlers
|
|
151
|
+
- [Streaming](references/streaming.md) — IAsyncEnumerable, IObservable, ChannelReader patterns
|
|
152
|
+
- [Authorization](references/authorization.md) — [Authorize], hub-level inheritance, token flow
|
|
153
|
+
- [Proxy Generation](references/proxy-generation.md) — [SignalARRRContract], source generator, DynamicProxy
|
|
154
|
+
- [Migration from v2.x](references/migration-v4.md) — breaking changes and upgrade guide
|