diff --git a/src/TypedSignalR.Client.TypeScript.Analyzer/InterfaceAnalyzer.cs b/src/TypedSignalR.Client.TypeScript.Analyzer/InterfaceAnalyzer.cs index 895b4f8..1b181aa 100644 --- a/src/TypedSignalR.Client.TypeScript.Analyzer/InterfaceAnalyzer.cs +++ b/src/TypedSignalR.Client.TypeScript.Analyzer/InterfaceAnalyzer.cs @@ -127,7 +127,7 @@ public class InterfaceAnalyzer : DiagnosticAnalyzer }; public override ImmutableArray SupportedDiagnostics - => ImmutableArray.Create(AnnotationRule, HubAttributeAnnotationRule, ReceiverAttributeAnnotationRule, UnsupportedTypeRule, HubMethodReturnTypeRule, ReceiverMethodReturnTypeRule); + => ImmutableArray.Create(AnnotationRule, HubAttributeAnnotationRule, ReceiverAttributeAnnotationRule, UnsupportedTypeRule, HubMethodReturnTypeRule, ReceiverMethodReturnTypeRule); public override void Initialize(AnalysisContext context) { @@ -224,13 +224,23 @@ private static void AnalyzeReceiverInterface( // Return type must be Task or Task 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, @@ -399,7 +409,7 @@ private static void ValidateReceiverReturnType( { var typeArg = namedReturnTypeSymbol.TypeArguments[0]; ValidateType(context, typeArg, location, supportTypeSymbols, transpilationSourceAttribute); - + return; } diff --git a/src/TypedSignalR.Client.TypeScript/Templates/ApiTemplate.cs b/src/TypedSignalR.Client.TypeScript/Templates/ApiTemplate.cs index 080b320..4db6087 100644 --- a/src/TypedSignalR.Client.TypeScript/Templates/ApiTemplate.cs +++ b/src/TypedSignalR.Client.TypeScript/Templates/ApiTemplate.cs @@ -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"); @@ -184,7 +184,7 @@ public class ApiTemplateBase /// /// The string builder that generation-time code is using to assemble generated output /// - protected System.Text.StringBuilder GenerationEnvironment + public System.Text.StringBuilder GenerationEnvironment { get { diff --git a/src/TypedSignalR.Client.TypeScript/Templates/ApiTemplate.tt b/src/TypedSignalR.Client.TypeScript/Templates/ApiTemplate.tt index 9a7b1d0..2991442 100644 --- a/src/TypedSignalR.Client.TypeScript/Templates/ApiTemplate.tt +++ b/src/TypedSignalR.Client.TypeScript/Templates/ApiTemplate.tt @@ -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) { #> diff --git a/src/TypedSignalR.Client.TypeScript/Templates/MethodSymbolExtensions.cs b/src/TypedSignalR.Client.TypeScript/Templates/MethodSymbolExtensions.cs index d35ce2c..d49e2b2 100644 --- a/src/TypedSignalR.Client.TypeScript/Templates/MethodSymbolExtensions.cs +++ b/src/TypedSignalR.Client.TypeScript/Templates/MethodSymbolExtensions.cs @@ -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) @@ -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); diff --git a/tests/TypeScriptTests/package.json b/tests/TypeScriptTests/package.json index 92025d9..41a8c4e 100644 --- a/tests/TypeScriptTests/package.json +++ b/tests/TypeScriptTests/package.json @@ -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" } } diff --git a/tests/TypeScriptTests/src/json/receiverWithCancellationToken.test.ts b/tests/TypeScriptTests/src/json/receiverWithCancellationToken.test.ts new file mode 100644 index 0000000..990f0f5 --- /dev/null +++ b/tests/TypeScriptTests/src/json/receiverWithCancellationToken.test.ts @@ -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 => { + receiveMessageList.push([message, value]); + return Promise.resolve(); + }, + notify: (): Promise => { + notifyCallCount += 1; + return Promise.resolve(); + }, + receiveCustomMessage: (userDefined: UserDefinedType): Promise => { + 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); diff --git a/tests/TypeScriptTests/src/msgpack/receiverWithCancellationToken.test.ts b/tests/TypeScriptTests/src/msgpack/receiverWithCancellationToken.test.ts new file mode 100644 index 0000000..714d98e --- /dev/null +++ b/tests/TypeScriptTests/src/msgpack/receiverWithCancellationToken.test.ts @@ -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 => { + receiveMessageList.push([message, value]); + return Promise.resolve(); + }, + notify: (): Promise => { + notifyCallCount += 1; + return Promise.resolve(); + }, + receiveCustomMessage: (userDefined: UserDefinedType): Promise => { + 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); diff --git a/tests/TypeScriptTests/yarn.lock b/tests/TypeScriptTests/yarn.lock index 50fa180..8e77c50 100644 --- a/tests/TypeScriptTests/yarn.lock +++ b/tests/TypeScriptTests/yarn.lock @@ -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" diff --git a/tests/TypedSignalR.Client.TypeScript.Tests.Server/Hubs/ReceiverTestHub.cs b/tests/TypedSignalR.Client.TypeScript.Tests.Server/Hubs/ReceiverTestHub.cs index 9b58e19..d690649 100644 --- a/tests/TypedSignalR.Client.TypeScript.Tests.Server/Hubs/ReceiverTestHub.cs +++ b/tests/TypedSignalR.Client.TypeScript.Tests.Server/Hubs/ReceiverTestHub.cs @@ -1,4 +1,3 @@ -using System.Globalization; using Microsoft.AspNetCore.SignalR; using TypedSignalR.Client.TypeScript.Tests.Shared; diff --git a/tests/TypedSignalR.Client.TypeScript.Tests.Server/Hubs/ReceiverTestWithCancellationTokenHub.cs b/tests/TypedSignalR.Client.TypeScript.Tests.Server/Hubs/ReceiverTestWithCancellationTokenHub.cs new file mode 100644 index 0000000..e824cf1 --- /dev/null +++ b/tests/TypedSignalR.Client.TypeScript.Tests.Server/Hubs/ReceiverTestWithCancellationTokenHub.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.SignalR; +using TypedSignalR.Client.TypeScript.Tests.Shared; + +namespace TypedSignalR.Client.TypeScript.Tests.Server.Hubs; + +public class ReceiverTestWithCancellationTokenHub : Hub, IReceiverTestHub +{ + private readonly string[] _message = new[] { + "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", + }; + + private readonly string[] _guids = new[] { + "b2f626e5-b4d4-4713-891d-f6cb107e502e", + "22733524-2087-4701-a586-c6bf0ce36f74", + "b89324bf-daf2-422a-85f2-6843b9c09b6a", + "779769d1-0aee-4dba-82c7-9e1044836d75" + }; + + private readonly string[] _dateTimes = new[] { + "2017-04-17", + "2018-05-25", + "2019-03-31", + "2022-02-06", + }; + + private readonly ILogger _logger; + + public ReceiverTestWithCancellationTokenHub(ILogger logger) + { + _logger = logger; + } + + public async Task Start() + { + _logger.Log(LogLevel.Information, "ReceiverTestHub.Start"); + + for (int i = 0; i < 17; i++) + { + await this.Clients.Caller.Notify(this.Context.ConnectionAborted); + } + + for (int i = 0; i < _message.Length; i++) + { + await this.Clients.Caller.ReceiveMessage(_message[i], i, this.Context.ConnectionAborted); + } + + for (int i = 0; i < _guids.Length; i++) + { + await this.Clients.Caller.ReceiveCustomMessage(new UserDefinedType() { Guid = Guid.Parse(_guids[i]), DateTime = DateTime.SpecifyKind(DateTime.Parse(_dateTimes[i]), DateTimeKind.Utc) }); + } + } +} diff --git a/tests/TypedSignalR.Client.TypeScript.Tests.Server/Program.cs b/tests/TypedSignalR.Client.TypeScript.Tests.Server/Program.cs index a9583b7..33016a2 100644 --- a/tests/TypedSignalR.Client.TypeScript.Tests.Server/Program.cs +++ b/tests/TypedSignalR.Client.TypeScript.Tests.Server/Program.cs @@ -47,6 +47,7 @@ app.MapHub("/hubs/UnaryHub"); app.MapHub("/hubs/SideEffectHub"); app.MapHub("/hubs/ReceiverTestHub"); +app.MapHub("/hubs/ReceiverTestWithCancellationTokenHub"); app.MapHub("/hubs/StreamingHub"); app.MapHub("/hubs/ClientResultsTestHub"); app.MapHub("/hubs/NestedTypeHub"); diff --git a/tests/TypedSignalR.Client.TypeScript.Tests.Shared/IReceiver.cs b/tests/TypedSignalR.Client.TypeScript.Tests.Shared/IReceiver.cs index 5873fe9..d4f5092 100644 --- a/tests/TypedSignalR.Client.TypeScript.Tests.Shared/IReceiver.cs +++ b/tests/TypedSignalR.Client.TypeScript.Tests.Shared/IReceiver.cs @@ -14,6 +14,14 @@ public interface IReceiver Task ReceiveCustomMessage(UserDefinedType userDefined); } +[Receiver] +public interface IReceiverWithCancellationToken +{ + Task ReceiveMessage(string message, int value, CancellationToken cancellationToken); + Task Notify(CancellationToken cancellationToken); + Task ReceiveCustomMessage(UserDefinedType userDefined); +} + [Hub] public interface IReceiverTestHub {