Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support CancellationToken in the last parameter of a receive method #236

Merged
merged 6 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/TypedSignalR.Client.TypeScript.Analyzer/InterfaceAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public class InterfaceAnalyzer : DiagnosticAnalyzer
};

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(AnnotationRule, HubAttributeAnnotationRule, ReceiverAttributeAnnotationRule, UnsupportedTypeRule, HubMethodReturnTypeRule, ReceiverMethodReturnTypeRule);
=> ImmutableArray.Create(AnnotationRule, HubAttributeAnnotationRule, ReceiverAttributeAnnotationRule, UnsupportedTypeRule, HubMethodReturnTypeRule, ReceiverMethodReturnTypeRule);

public override void Initialize(AnalysisContext context)
{
Expand Down Expand Up @@ -224,13 +224,23 @@ private static void AnalyzeReceiverInterface(
// Return type must be Task or Task<T>
ValidateReceiverReturnType(context, method, supportTypeSymbols, transpilationSourceAttributeSymbol, specialSymbols);

foreach (var parameter in method.Parameters)
// Validate method parameters type
for (int i = 0; i < method.Parameters.Length; i++)
{
ValidateType(context, parameter.Type, parameter.Locations[0], supportTypeSymbols, transpilationSourceAttributeSymbol);
if (IsLast(i, method.Parameters.Length)
// It is allowed to pass a CancellationToken as the last parameter of the receiver's method.
&& SymbolEqualityComparer.Default.Equals(method.Parameters[i].Type, specialSymbols.CancellationTokenSymbol))
{
continue;
}

ValidateType(context, method.Parameters[i].Type, method.Parameters[i].Locations[0], supportTypeSymbols, transpilationSourceAttributeSymbol);
}
}
}

private static bool IsLast(int index, int length) => index == length - 1;

private static void ValidateType(
SymbolAnalysisContext context,
ITypeSymbol typeSymbol,
Expand Down Expand Up @@ -399,7 +409,7 @@ private static void ValidateReceiverReturnType(
{
var typeArg = namedReturnTypeSymbol.TypeArguments[0];
ValidateType(context, typeArg, location, supportTypeSymbols, transpilationSourceAttribute);

return;
}

Expand Down
4 changes: 2 additions & 2 deletions src/TypedSignalR.Client.TypeScript/Templates/ApiTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public constructor(
this.Write(" const __");
this.Write(this.ToStringHelper.ToStringWithCulture(method.Name.Format(Options.NamingStyle)));
this.Write(" = ");
this.Write(this.ToStringHelper.ToStringWithCulture(method.WrapLambdaExpressionSyntax(Options)));
this.Write(this.ToStringHelper.ToStringWithCulture(method.TranslateReceiverMethodIntoLambdaExpressionSyntax(SpecialSymbols, Options)));
this.Write(";\r\n");
}
this.Write("\r\n");
Expand Down Expand Up @@ -184,7 +184,7 @@ public class ApiTemplateBase
/// <summary>
/// The string builder that generation-time code is using to assemble generated output
/// </summary>
protected System.Text.StringBuilder GenerationEnvironment
public System.Text.StringBuilder GenerationEnvironment
{
get
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class <#= receiverType.Name #>_Binder implements ReceiverRegister<<#= receiverTy
public readonly register = (connection: HubConnection, receiver: <#= receiverType.Name #>): Disposable => {

<# foreach(var method in receiverType.Methods) { #>
const __<#= method.Name.Format(Options.NamingStyle) #> = <#= method.WrapLambdaExpressionSyntax(Options) #>;
const __<#= method.Name.Format(Options.NamingStyle) #> = <#= method.TranslateReceiverMethodIntoLambdaExpressionSyntax(SpecialSymbols, Options) #>;
<# } #>

<# foreach(var method in receiverType.Methods) { #>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,34 @@ namespace TypedSignalR.Client.TypeScript.Templates;

internal static class MethodSymbolExtensions
{
public static string WrapLambdaExpressionSyntax(this IMethodSymbol methodSymbol, ITypedSignalRTranspilationOptions options)
public static string TranslateReceiverMethodIntoLambdaExpressionSyntax(this IMethodSymbol receiverMethodSymbol, SpecialSymbols specialSymbols, ITypedSignalRTranspilationOptions options)
{
if (methodSymbol.Parameters.Length == 0)
if (receiverMethodSymbol.Parameters.Length == 0)
{
return $"() => receiver.{methodSymbol.Name.Format(options.MethodStyle)}()";
return $"() => receiver.{receiverMethodSymbol.Name.Format(options.MethodStyle)}()";
}

var parameters = ParametersToTypeArray(methodSymbol, options);
return $"(...args: {parameters}) => receiver.{methodSymbol.Name.Format(options.MethodStyle)}(...args)";
if (receiverMethodSymbol.Parameters.Length == 1
// Ignore if the last parameter of a receiver's method is a CancellationToken.
&& SymbolEqualityComparer.Default.Equals(receiverMethodSymbol.Parameters[0].Type, specialSymbols.CancellationTokenSymbol))
{
return $"() => receiver.{receiverMethodSymbol.Name.Format(options.MethodStyle)}()";
}

var parameters = ParametersToTypeArray(receiverMethodSymbol, specialSymbols, options);

return $"(...args: {parameters}) => receiver.{receiverMethodSymbol.Name.Format(options.MethodStyle)}(...args)";

static string ParametersToTypeArray(IMethodSymbol methodSymbol, SpecialSymbols specialSymbols, ITypedSignalRTranspilationOptions options)
{
var methodParameters = SymbolEqualityComparer.Default.Equals(methodSymbol.Parameters.Last().Type, specialSymbols.CancellationTokenSymbol)
? methodSymbol.Parameters.SkipLast(1) // Ignore if the last parameter of a receiver's method is a CancellationToken.
: methodSymbol.Parameters;

var parameters = methodParameters.Select(x => TypeMapper.MapTo(x.Type, options));

return $"[{string.Join(", ", parameters)}]";
}
}

public static string CreateMethodString(this IMethodSymbol methodSymbol, SpecialSymbols specialSymbols, ITypedSignalRTranspilationOptions options)
Expand Down Expand Up @@ -78,12 +97,6 @@ private static string ReturnTypeToTypeScriptString(this IMethodSymbol methodSymb
return TypeMapper.MapTo(methodSymbol.ReturnType, options);
}

private static string ParametersToTypeArray(IMethodSymbol methodSymbol, ITypedSignalRTranspilationOptions options)
{
var parameters = methodSymbol.Parameters.Select(x => TypeMapper.MapTo(x.Type, options));
return $"[{string.Join(", ", parameters)}]";
}

private static string CreateUnaryMethodString(IMethodSymbol methodSymbol, SpecialSymbols specialSymbols, ITypedSignalRTranspilationOptions options)
{
var name = methodSymbol.Name.Format(options.MethodStyle);
Expand Down
4 changes: 2 additions & 2 deletions tests/TypeScriptTests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"test": "jest"
},
"dependencies": {
"@microsoft/signalr": "^8.0.0",
"@microsoft/signalr-protocol-msgpack": "^8.0.0"
"@microsoft/signalr": "^8.0.7",
"@microsoft/signalr-protocol-msgpack": "^8.0.7"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { HubConnectionBuilder } from '@microsoft/signalr'
import { getHubProxyFactory, getReceiverRegister } from '../generated/json/TypedSignalR.Client'
import { UserDefinedType } from '../generated/json/TypedSignalR.Client.TypeScript.Tests.Shared';
import { IReceiverWithCancellationToken } from '../generated/json/TypedSignalR.Client/TypedSignalR.Client.TypeScript.Tests.Shared';

const toUTCString = (date: string | Date): string => {
if (typeof date === 'string') {
const d = new Date(date);
return d.toUTCString();
}

return date.toUTCString();
}

const answerMessages: string[] = [
"b1f7cd73-13b8-49bd-9557-ffb38859d18b",
"3f5c3585-d01b-4f8f-8139-62a1241850e2",
"92021a22-5823-4501-8cbd-c20d4ca6e54c",
"5b134f73-2dc1-4271-8316-1a4250f42241",
"e73acd30-e034-4569-8f30-88ac34b99052",
"0d7531b5-0a36-4fe7-bbe5-8fee38c38c07",
"32915627-3df6-41dc-8d30-7c655c2f7e61",
"c875a6f9-9ddb-440b-a7e4-6e893f59ab9e",
];

const guids: string[] = [
"b2f626e5-b4d4-4713-891d-f6cb107e502e",
"22733524-2087-4701-a586-c6bf0ce36f74",
"b89324bf-daf2-422a-85f2-6843b9c09b6a",
"779769d1-0aee-4dba-82c7-9e1044836d75"
];

const dateTimes: string[] = [
"2017-04-17",
"2018-05-25",
"2019-03-31",
"2022-02-06",
];

const testMethod = async () => {
const connection = new HubConnectionBuilder()
.withUrl("http://localhost:5000/hubs/receiverTestWithCancellationTokenHub")
.build();

const receiveMessageList: [string, number][] = [];
const userDefinedList: UserDefinedType[] = [];
let notifyCallCount = 0;

const receiver: IReceiverWithCancellationToken = {
receiveMessage: (message: string, value: number): Promise<void> => {
receiveMessageList.push([message, value]);
return Promise.resolve();
},
notify: (): Promise<void> => {
notifyCallCount += 1;
return Promise.resolve();
},
receiveCustomMessage: (userDefined: UserDefinedType): Promise<void> => {
userDefinedList.push(userDefined)
return Promise.resolve();
}
}

const hubProxy = getHubProxyFactory("IReceiverTestHub")
.createHubProxy(connection);

const subscription = getReceiverRegister("IReceiverWithCancellationToken")
.register(connection, receiver);

try {
await connection.start();
await hubProxy.start();

expect(notifyCallCount).toEqual(17);

for (let i = 0; i < receiveMessageList.length; i++) {
expect(receiveMessageList[i][0]).toEqual(answerMessages[i]);
expect(receiveMessageList[i][1]).toEqual(i);
}

for (let i = 0; i < userDefinedList.length; i++) {
expect(userDefinedList[i].guid).toEqual(guids[i]);
expect(toUTCString(userDefinedList[i].dateTime)).toEqual(toUTCString(dateTimes[i]));
}
}
finally {
subscription.dispose();
await connection.stop()
}
}

test('receiver.test', testMethod);
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { HubConnectionBuilder } from '@microsoft/signalr'
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack';
import { getHubProxyFactory, getReceiverRegister } from '../generated/msgpack/TypedSignalR.Client'
import { UserDefinedType } from '../generated/msgpack/TypedSignalR.Client.TypeScript.Tests.Shared';
import { IReceiverWithCancellationToken } from '../generated/msgpack/TypedSignalR.Client/TypedSignalR.Client.TypeScript.Tests.Shared';

const toUTCString = (date: string | Date): string => {
if (typeof date === 'string') {
const d = new Date(date);
return d.toUTCString();
}

return date.toUTCString();
}

const answerMessages: string[] = [
"b1f7cd73-13b8-49bd-9557-ffb38859d18b",
"3f5c3585-d01b-4f8f-8139-62a1241850e2",
"92021a22-5823-4501-8cbd-c20d4ca6e54c",
"5b134f73-2dc1-4271-8316-1a4250f42241",
"e73acd30-e034-4569-8f30-88ac34b99052",
"0d7531b5-0a36-4fe7-bbe5-8fee38c38c07",
"32915627-3df6-41dc-8d30-7c655c2f7e61",
"c875a6f9-9ddb-440b-a7e4-6e893f59ab9e",
];

const guids: string[] = [
"b2f626e5-b4d4-4713-891d-f6cb107e502e",
"22733524-2087-4701-a586-c6bf0ce36f74",
"b89324bf-daf2-422a-85f2-6843b9c09b6a",
"779769d1-0aee-4dba-82c7-9e1044836d75"
];

const dateTimes: string[] = [
"2017-04-17",
"2018-05-25",
"2019-03-31",
"2022-02-06",
];

const testMethod = async () => {
const connection = new HubConnectionBuilder()
.withUrl("http://localhost:5000/hubs/receiverTestWithCancellationTokenHub")
.withHubProtocol(new MessagePackHubProtocol())
.build();

const receiveMessageList: [string, number][] = [];
const userDefinedList: UserDefinedType[] = [];
let notifyCallCount = 0;

const receiver: IReceiverWithCancellationToken = {
receiveMessage: (message: string, value: number): Promise<void> => {
receiveMessageList.push([message, value]);
return Promise.resolve();
},
notify: (): Promise<void> => {
notifyCallCount += 1;
return Promise.resolve();
},
receiveCustomMessage: (userDefined: UserDefinedType): Promise<void> => {
userDefinedList.push(userDefined)
return Promise.resolve();
}
}

const hubProxy = getHubProxyFactory("IReceiverTestHub")
.createHubProxy(connection);

const subscription = getReceiverRegister("IReceiverWithCancellationToken")
.register(connection, receiver);

try {
await connection.start();
await hubProxy.start();

expect(notifyCallCount).toEqual(17);

for (let i = 0; i < receiveMessageList.length; i++) {
expect(receiveMessageList[i][0]).toEqual(answerMessages[i]);
expect(receiveMessageList[i][1]).toEqual(i);
}

for (let i = 0; i < userDefinedList.length; i++) {
expect(userDefinedList[i].Guid).toEqual(guids[i]);
expect(toUTCString(userDefinedList[i].DateTime)).toEqual(toUTCString(dateTimes[i]));
}
}
finally {
subscription.dispose();
await connection.stop();
}
}

test('receiver.test', testMethod);
18 changes: 9 additions & 9 deletions tests/TypeScriptTests/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1205,18 +1205,18 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"

"@microsoft/signalr-protocol-msgpack@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@microsoft/signalr-protocol-msgpack/-/signalr-protocol-msgpack-8.0.0.tgz#cc1777c8614e2586d890f1a7ea47637f1cd42ffe"
integrity sha512-XtN5lUPVOtU96aqpB6z00o0TQayx5fmcf7CeQKDXF1flg8G96wtNCFXKb/p4sM/nvprjSmz0JiWQfc1TVXsa6Q==
"@microsoft/signalr-protocol-msgpack@^8.0.7":
version "8.0.7"
resolved "https://registry.yarnpkg.com/@microsoft/signalr-protocol-msgpack/-/signalr-protocol-msgpack-8.0.7.tgz#54302f89883831f84531962d6c1944e790c510fd"
integrity sha512-yrGt0E9l8X9HSF9P8gmnkVi+IDGNP6seU5/YAiFzIisH4VpvayVcf7Z1Lbu7nNwM/GzFgtFIa7jBivLQCCcyEQ==
dependencies:
"@microsoft/signalr" ">=8.0.0"
"@microsoft/signalr" ">=8.0.7"
"@msgpack/msgpack" "^2.7.0"

"@microsoft/signalr@>=8.0.0", "@microsoft/signalr@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@microsoft/signalr/-/signalr-8.0.0.tgz#cb1412e88e0527f40da9178fefc27a65c3ddeab0"
integrity sha512-K/wS/VmzRWePCGqGh8MU8OWbS1Zvu7DG7LSJS62fBB8rJUXwwj4axQtqrAAwKGUZHQF6CuteuQR9xMsVpM2JNA==
"@microsoft/signalr@>=8.0.7", "@microsoft/signalr@^8.0.7":
version "8.0.7"
resolved "https://registry.yarnpkg.com/@microsoft/signalr/-/signalr-8.0.7.tgz#94419ddbf9418753e493f4ae4c13990316ec2ea5"
integrity sha512-PHcdMv8v5hJlBkRHAuKG5trGViQEkPYee36LnJQx4xHOQ5LL4X0nEWIxOp5cCtZ7tu+30quz5V3k0b1YNuc6lw==
dependencies:
abort-controller "^3.0.0"
eventsource "^2.0.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Globalization;
using Microsoft.AspNetCore.SignalR;
using TypedSignalR.Client.TypeScript.Tests.Shared;

Expand Down
Loading